Qt: Add html_escape to qt_ui_utils.
[metze/wireshark/wip.git] / ui / qt / tap_parameter_dialog.cpp
1 /* tap_parameter_dialog.cpp
2  *
3  * Wireshark - Network traffic analyzer
4  * By Gerald Combs <gerald@wireshark.org>
5  * Copyright 1998 Gerald Combs
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20  */
21
22 /*
23  * @file Tap parameter dialog class
24  *
25  * Base class for statistics dialogs. Subclasses must implement:
26  * - fillTree. Called when the dialog is first displayed and when a display
27  *   filter is applied. In most cases the subclass should clear the tree and
28  *   retap packets here.
29  * - filterExpression. If the subclass supports filtering context menu items
30  *   ("Apply As Filter", etc.) it should fill in ctx_menu_ and implement
31  *   filterExpression.
32  * - getTreeAsString or treeItemData. Used for "Copy" and "Save As...".
33  * -
34  */
35
36 #include "tap_parameter_dialog.h"
37 #include <ui_tap_parameter_dialog.h>
38
39 #include <errno.h>
40
41 #include "epan/stat_tap_ui.h"
42
43 #include "ui/last_open_dir.h"
44 #include <wsutil/utf8_entities.h>
45
46 #include "wsutil/file_util.h"
47
48 #include "progress_frame.h"
49 #include "qt_ui_utils.h"
50 #include "wireshark_application.h"
51
52 #include <QClipboard>
53 #include <QContextMenuEvent>
54 #include <QMessageBox>
55 #include <QFileDialog>
56
57 // The GTK+ counterpart uses tap_param_dlg, which we don't use. If we
58 // need tap parameters we should probably create a TapParameterDialog
59 // class based on WiresharkDialog and subclass it here.
60
61 // To do:
62 // - Add tap parameters? SCSI SRT uses PARAM_ENUM. Everything appears to use
63 //   PARAM_FILTER. Nothing uses _UINT, _STRING, or _UUID.
64 // - Update to match bug 9452 / r53657.
65 // - Create a TapParameterTreeWidgetItem class?
66 // - Better / more usable XML output.
67
68 const int expand_all_threshold_ = 100; // Arbitrary
69
70 static QHash<const QString, tpdCreator> cfg_str_to_creator_;
71 const QString TapParameterDialog::action_name_ = "TapParameterAction";
72
73 TapParameterDialog::TapParameterDialog(QWidget &parent, CaptureFile &cf, int help_topic) :
74     WiresharkDialog(parent, cf),
75     ui(new Ui::TapParameterDialog),
76     help_topic_(help_topic)
77 {
78     ui->setupUi(this);
79
80     // Only show a hint label if a subclass provides a hint.
81     ui->hintLabel->hide();
82
83     ctx_menu_.addAction(ui->actionCopyToClipboard);
84     ctx_menu_.addAction(ui->actionSaveAs);
85
86     QPushButton *button;
87     button = ui->buttonBox->addButton(tr("Copy"), QDialogButtonBox::ActionRole);
88     connect(button, SIGNAL(clicked()), this, SLOT(on_actionCopyToClipboard_triggered()));
89
90     button = ui->buttonBox->addButton(tr("Save as" UTF8_HORIZONTAL_ELLIPSIS), QDialogButtonBox::ActionRole);
91     connect(button, SIGNAL(clicked()), this, SLOT(on_actionSaveAs_triggered()));
92
93     connect(ui->displayFilterLineEdit, SIGNAL(textChanged(QString)),
94             this, SLOT(updateWidgets()));
95
96     ProgressFrame::addToButtonBox(ui->buttonBox, &parent);
97
98     if (help_topic_ < 1) {
99         ui->buttonBox->button(QDialogButtonBox::Help)->hide();
100     }
101
102     if (!ui->displayFilterLineEdit->text().isEmpty()) {
103         QString filter = ui->displayFilterLineEdit->text();
104         emit updateFilter(filter);
105     }
106     show_timer_ = new QTimer(this);
107     setRetapOnShow(true);
108 }
109
110 TapParameterDialog::~TapParameterDialog()
111 {
112     delete ui;
113     show_timer_->stop();
114     delete show_timer_;
115 }
116
117 void TapParameterDialog::registerDialog(const QString title, const char *cfg_abbr, register_stat_group_t group, stat_tap_init_cb tap_init_cb, tpdCreator creator)
118 {
119     stat_tap_ui ui_info;
120
121     ui_info.group = group;
122     ui_info.title = title.toUtf8().constData();
123     ui_info.cli_string = cfg_abbr;
124     ui_info.tap_init_cb = tap_init_cb;
125     ui_info.nparams = 0; // We'll need this for SCSI SRT
126     ui_info.params = NULL;
127     register_stat_tap_ui(&ui_info, NULL);
128
129     QString cfg_str = cfg_abbr;
130     cfg_str_to_creator_[cfg_str] = creator;
131
132     QAction *tpd_action = new QAction(title, NULL);
133     tpd_action->setObjectName(action_name_);
134     tpd_action->setData(cfg_str);
135     wsApp->addDynamicMenuGroupItem(group, tpd_action);
136 }
137
138 TapParameterDialog *TapParameterDialog::showTapParameterStatistics(QWidget &parent, CaptureFile &cf, const QString cfg_str, const QString arg, void *)
139 {
140     if (cfg_str_to_creator_.contains(cfg_str)) {
141         TapParameterDialog *tpd = cfg_str_to_creator_[cfg_str](parent, cfg_str, arg, cf);
142         return tpd;
143     }
144     return NULL;
145 }
146
147 QTreeWidget *TapParameterDialog::statsTreeWidget()
148 {
149     return ui->statsTreeWidget;
150 }
151
152 QLineEdit *TapParameterDialog::displayFilterLineEdit()
153 {
154     return ui->displayFilterLineEdit;
155 }
156
157 QPushButton *TapParameterDialog::applyFilterButton()
158 {
159     return ui->applyFilterButton;
160 }
161
162 QVBoxLayout *TapParameterDialog::verticalLayout()
163 {
164     return ui->verticalLayout;
165 }
166
167 QHBoxLayout *TapParameterDialog::filterLayout()
168 {
169     return ui->filterLayout;
170 }
171
172 QString TapParameterDialog::displayFilter()
173 {
174     return ui->displayFilterLineEdit->text();
175 }
176
177 // This assumes that we're called before signals are connected or show()
178 // is called.
179 void TapParameterDialog::setDisplayFilter(const QString &filter)
180 {
181     ui->displayFilterLineEdit->setText(filter);
182 }
183
184 void TapParameterDialog::setHint(const QString &hint)
185 {
186     ui->hintLabel->setText(hint);
187     ui->hintLabel->show();
188 }
189
190 void TapParameterDialog::setRetapOnShow(bool retap)
191 {
192     show_timer_->stop();
193     if (retap) {
194         show_timer_->singleShot(0, this, SLOT(on_applyFilterButton_clicked()));
195     }
196 }
197
198 void TapParameterDialog::filterActionTriggered()
199 {
200     FilterAction *fa = qobject_cast<FilterAction *>(QObject::sender());
201     QString filter_expr = filterExpression();
202
203     if (!fa || filter_expr.isEmpty()) {
204         return;
205     }
206
207     emit filterAction(filter_expr, fa->action(), fa->actionType());
208 }
209
210 QString TapParameterDialog::itemDataToPlain(QVariant var, int width)
211 {
212     QString plain_str;
213     int align_mul = 1;
214
215     switch (var.type()) {
216     case QVariant::String:
217         align_mul = -1;
218         // Fall through
219     case QVariant::Int:
220     case QVariant::UInt:
221         plain_str = var.toString();
222         break;
223     case QVariant::Double:
224         plain_str = QString::number(var.toDouble(), 'f', 6);
225         break;
226     default:
227         break;
228     }
229
230     if (plain_str.length() < width) {
231         plain_str = QString("%1").arg(plain_str, width * align_mul);
232     }
233     return plain_str;
234 }
235
236 QList<QVariant> TapParameterDialog::treeItemData(QTreeWidgetItem *) const
237 {
238     return QList<QVariant>();
239 }
240
241 const QString plain_sep_ = "  ";
242 QByteArray TapParameterDialog::getTreeAsString(st_format_type format)
243 {
244     QByteArray ba;
245     QTreeWidgetItemIterator it(ui->statsTreeWidget, QTreeWidgetItemIterator::NotHidden);
246
247     QList<int> col_widths;
248     QByteArray footer;
249
250     // Title + header
251     switch (format) {
252     case ST_FORMAT_PLAIN:
253     {
254         QTreeWidgetItemIterator width_it(it);
255         QString plain_header;
256         while (*width_it) {
257             QList<QVariant> tid = treeItemData((*width_it));
258             int col = 0;
259             foreach (QVariant var, tid) {
260                 if (col_widths.size() <= col) {
261                     col_widths.append(ui->statsTreeWidget->headerItem()->text(col).length());
262                 }
263                 if (var.type() == QVariant::String) {
264                     col_widths[col] = qMax(col_widths[col], itemDataToPlain(var).length());
265                 }
266                 col++;
267             }
268             ++width_it;
269         }
270         QStringList ph_parts;
271         for (int col = 0; col < ui->statsTreeWidget->columnCount() && col < col_widths.length(); col++) {
272             ph_parts << ui->statsTreeWidget->headerItem()->text(col);
273         }
274         plain_header = ph_parts.join(plain_sep_);
275
276         QByteArray top_separator;
277         top_separator.fill('=', plain_header.length());
278         top_separator.append('\n');
279         QString file_header = QString("%1 - %2:\n").arg(windowSubtitle(), cap_file_.fileName());
280         footer.fill('-', plain_header.length());
281         footer.append('\n');
282         plain_header.append('\n');
283
284         ba.append(top_separator);
285         ba.append(file_header);
286         ba.append(plain_header);
287         ba.append(footer);
288         break;
289     }
290     case ST_FORMAT_CSV:
291     {
292         QString csv_header;
293         QStringList ch_parts;
294         for (int col = 0; col < ui->statsTreeWidget->columnCount(); col++) {
295             ch_parts << QString("\"%1\"").arg(ui->statsTreeWidget->headerItem()->text(col));
296         }
297         csv_header = ch_parts.join(",");
298         csv_header.append('\n');
299         ba.append(csv_header.toUtf8().constData());
300         break;
301     }
302     case ST_FORMAT_XML:
303     {
304         // XXX What's a useful format? This mostly conforms to DocBook.
305         ba.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
306         QString title = html_escape(windowSubtitle());
307         QString xml_header = QString("<table>\n<title>%1</title>\n").arg(title);
308         ba.append(xml_header.toUtf8());
309         ba.append("<thead>\n<row>\n");
310         for (int col = 0; col < ui->statsTreeWidget->columnCount(); col++) {
311             title = html_escape(ui->statsTreeWidget->headerItem()->text(col));
312             title = QString("  <entry>%1</entry>\n").arg(title);
313             ba.append(title.toUtf8());
314         }
315         ba.append("</row>\n</thead>\n");
316         ba.append("<tbody>\n");
317         footer = "</tbody>\n</table>\n";
318         break;
319     }
320     case ST_FORMAT_YAML:
321     {
322         QString yaml_header;
323         ba.append("---\n");
324         yaml_header = QString("Description: \"%1\"\nFile: \"%2\"\nItems:\n").arg(windowSubtitle()).arg(cap_file_.fileName());
325         ba.append(yaml_header.toUtf8());
326         break;
327     }
328     default:
329         break;
330     }
331
332     // Data
333     while (*it) {
334         QList<QVariant> tid = treeItemData((*it));
335         if (tid.length() < 1) {
336             ++it;
337             continue;
338         }
339
340         if (tid.length() < ui->statsTreeWidget->columnCount()) {
341             // Assume we have a header
342         }
343
344         // Assume var length == columnCount
345         QString line;
346         QStringList parts;
347
348         switch (format) {
349         case ST_FORMAT_PLAIN:
350         {
351             int i = 0;
352             foreach (QVariant var, tid) {
353                 parts << itemDataToPlain(var, col_widths[i]);
354                 i++;
355             }
356             line = parts.join(plain_sep_);
357             line.append('\n');
358             break;
359         }
360         case ST_FORMAT_CSV:
361             foreach (QVariant var, tid) {
362                 if (var.type() == QVariant::String) {
363                     parts << QString("\"%1\"").arg(var.toString());
364                 } else {
365                     parts << var.toString();
366                 }
367             }
368             line = parts.join(",");
369             line.append('\n');
370             break;
371         case ST_FORMAT_XML:
372         {
373             line = "<row>\n";
374             foreach (QVariant var, tid) {
375                 QString entry = html_escape(var.toString());
376                 line.append(QString("  <entry>%1</entry>\n").arg(entry));
377             }
378             line.append("</row>\n");
379             break;
380         }
381         case ST_FORMAT_YAML:
382         {
383             int col = 0;
384             QString indent = "-";
385             foreach (QVariant var, tid) {
386                 QString entry;
387                 if (var.type() == QVariant::String) {
388                     entry = QString("\"%1\"").arg(var.toString());
389                 } else {
390                     entry = var.toString();
391                 }
392                 line.append(QString("  %1 %2: %3\n").arg(indent).arg(ui->statsTreeWidget->headerItem()->text(col), entry));
393                 indent = " ";
394                 col++;
395             }
396             break;
397         }
398         default:
399             break;
400         }
401
402         ba.append(line.toUtf8());
403         ++it;
404     }
405
406     // Footer
407     ba.append(footer); // plain only?
408     return ba;
409 }
410
411 void TapParameterDialog::drawTreeItems()
412 {
413     if (ui->statsTreeWidget->model()->rowCount() < expand_all_threshold_) {
414         ui->statsTreeWidget->expandAll();
415     }
416
417     for (int col = 0; col < ui->statsTreeWidget->columnCount(); col++) {
418         ui->statsTreeWidget->resizeColumnToContents(col);
419     }
420 }
421
422 void TapParameterDialog::contextMenuEvent(QContextMenuEvent *event)
423 {
424     bool enable = filterExpression().length() > 0 ? true : false;
425
426     foreach (QAction *fa, filter_actions_) {
427         fa->setEnabled(enable);
428     }
429
430     ctx_menu_.exec(event->globalPos());
431 }
432
433 void TapParameterDialog::addFilterActions()
434 {
435     QMenu *submenu;
436     QAction *insert_action = ctx_menu_.actions().first();
437
438     FilterAction::Action cur_action = FilterAction::ActionApply;
439     submenu = ctx_menu_.addMenu(FilterAction::actionName(cur_action));
440     foreach (FilterAction::ActionType at, FilterAction::actionTypes()) {
441         FilterAction *fa = new FilterAction(submenu, cur_action, at);
442         submenu->addAction(fa);
443         connect(fa, SIGNAL(triggered()), this, SLOT(filterActionTriggered()));
444         filter_actions_ << fa;
445     }
446     ctx_menu_.insertMenu(insert_action, submenu);
447
448     cur_action = FilterAction::ActionPrepare;
449     submenu = ctx_menu_.addMenu(FilterAction::actionName(cur_action));
450     foreach (FilterAction::ActionType at, FilterAction::actionTypes()) {
451         FilterAction *fa = new FilterAction(submenu, cur_action, at);
452         submenu->addAction(fa);
453         connect(fa, SIGNAL(triggered()), this, SLOT(filterActionTriggered()));
454         filter_actions_ << fa;
455     }
456     ctx_menu_.insertMenu(insert_action, submenu);
457
458     cur_action = FilterAction::ActionFind;
459     submenu = ctx_menu_.addMenu(FilterAction::actionName(cur_action));
460     foreach (FilterAction::ActionType at, FilterAction::actionTypes(cur_action)) {
461         FilterAction *fa = new FilterAction(submenu, cur_action, at);
462         submenu->addAction(fa);
463         connect(fa, SIGNAL(triggered()), this, SLOT(filterActionTriggered()));
464         filter_actions_ << fa;
465     }
466     ctx_menu_.insertMenu(insert_action, submenu);
467
468     cur_action = FilterAction::ActionColorize;
469     submenu = ctx_menu_.addMenu(FilterAction::actionName(cur_action));
470     foreach (FilterAction::ActionType at, FilterAction::actionTypes(cur_action)) {
471         FilterAction *fa = new FilterAction(submenu, cur_action, at);
472         submenu->addAction(fa);
473         connect(fa, SIGNAL(triggered()), this, SLOT(filterActionTriggered()));
474         filter_actions_ << fa;
475     }
476     ctx_menu_.insertMenu(insert_action, submenu);
477     ctx_menu_.insertSeparator(insert_action);
478 }
479
480 void TapParameterDialog::updateWidgets()
481 {
482     bool edit_enable = true;
483     bool apply_enable = true;
484
485     if (file_closed_) {
486         edit_enable = false;
487         apply_enable = false;
488     } else if (!ui->displayFilterLineEdit->checkFilter()) {
489         // XXX Tell the user why the filter is invalid.
490         apply_enable = false;
491     }
492     ui->displayFilterLineEdit->setEnabled(edit_enable);
493     ui->applyFilterButton->setEnabled(apply_enable);
494
495     WiresharkDialog::updateWidgets();
496 }
497
498 void TapParameterDialog::on_applyFilterButton_clicked()
499 {
500     beginRetapPackets();
501     if (!ui->displayFilterLineEdit->checkFilter())
502         return;
503
504     QString filter = ui->displayFilterLineEdit->text();
505     emit updateFilter(filter);
506     // If we wanted to be fancy we could add an isRetapping function to
507     // either WiresharkDialog or CaptureFile and use it in updateWidgets
508     // to enable and disable the apply button as needed.
509     // For now we use more simple but less useful logic.
510     bool df_enabled = ui->displayFilterLineEdit->isEnabled();
511     bool af_enabled = ui->applyFilterButton->isEnabled();
512     ui->displayFilterLineEdit->setEnabled(false);
513     ui->applyFilterButton->setEnabled(false);
514     fillTree();
515     ui->applyFilterButton->setEnabled(af_enabled);
516     ui->displayFilterLineEdit->setEnabled(df_enabled);
517     endRetapPackets();
518 }
519
520 void TapParameterDialog::on_actionCopyToClipboard_triggered()
521 {
522     wsApp->clipboard()->setText(getTreeAsString(ST_FORMAT_PLAIN));
523 }
524
525 void TapParameterDialog::on_actionSaveAs_triggered()
526 {
527     QString selectedFilter;
528     st_format_type format;
529     const char *file_ext;
530     FILE *f;
531     bool success = false;
532     int last_errno;
533
534     QFileDialog SaveAsDialog(this, wsApp->windowTitleString(tr("Save Statistics As" UTF8_HORIZONTAL_ELLIPSIS)),
535                                                             get_last_open_dir());
536     SaveAsDialog.setNameFilter(tr("Plain text file (*.txt);;"
537                                     "Comma separated values (*.csv);;"
538                                     "XML document (*.xml);;"
539                                     "YAML document (*.yaml)"));
540     SaveAsDialog.selectNameFilter(tr("Plain text file (*.txt)"));
541     SaveAsDialog.setAcceptMode(QFileDialog::AcceptSave);
542     if (!SaveAsDialog.exec()) {
543         return;
544     }
545     selectedFilter= SaveAsDialog.selectedNameFilter();
546     if (selectedFilter.contains("*.yaml", Qt::CaseInsensitive)) {
547         format = ST_FORMAT_YAML;
548         file_ext = ".yaml";
549     }
550     else if (selectedFilter.contains("*.xml", Qt::CaseInsensitive)) {
551         format = ST_FORMAT_XML;
552         file_ext = ".xml";
553     }
554     else if (selectedFilter.contains("*.csv", Qt::CaseInsensitive)) {
555         format = ST_FORMAT_CSV;
556         file_ext = ".csv";
557     }
558     else {
559         format = ST_FORMAT_PLAIN;
560         file_ext = ".txt";
561     }
562
563     // Get selected filename and add extension of necessary
564     QString file_name = SaveAsDialog.selectedFiles()[0];
565     if (!file_name.endsWith(file_ext, Qt::CaseInsensitive)) {
566         file_name.append(file_ext);
567     }
568
569     QByteArray tree_as_ba = getTreeAsString(format);
570
571     // actually save the file
572     f = ws_fopen (file_name.toUtf8().constData(), "w");
573     last_errno = errno;
574     if (f) {
575         if (fputs(tree_as_ba.data(), f) != EOF) {
576             success = true;
577         }
578         last_errno = errno;
579         fclose(f);
580     }
581     if (!success) {
582         QMessageBox::warning(this, tr("Error saving file %1").arg(file_name),
583                              g_strerror (last_errno));
584     }
585 }
586
587 void TapParameterDialog::on_buttonBox_helpRequested()
588 {
589     if (help_topic_ > 0) {
590         wsApp->helpTopicAction((topic_action_e) help_topic_);
591     }
592 }
593
594 /*
595  * Editor modelines
596  *
597  * Local Variables:
598  * c-basic-offset: 4
599  * tab-width: 8
600  * indent-tabs-mode: nil
601  * End:
602  *
603  * ex: set shiftwidth=4 tabstop=8 expandtab:
604  * :indentSize=4:tabSize=8:noTabs=true:
605  */