summaryrefslogtreecommitdiffstats
path: root/ui/qt/widgets/resolved_addresses_view.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'ui/qt/widgets/resolved_addresses_view.cpp')
-rw-r--r--ui/qt/widgets/resolved_addresses_view.cpp261
1 files changed, 261 insertions, 0 deletions
diff --git a/ui/qt/widgets/resolved_addresses_view.cpp b/ui/qt/widgets/resolved_addresses_view.cpp
new file mode 100644
index 00000000..82b2a299
--- /dev/null
+++ b/ui/qt/widgets/resolved_addresses_view.cpp
@@ -0,0 +1,261 @@
+/** @file
+ *
+ * Wireshark - Network traffic analyzer
+ * By Gerald Combs <gerald@wireshark.org>
+ * Copyright 1998 Gerald Combs
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include <config.h>
+#define WS_LOG_DOMAIN LOG_DOMAIN_QTUI
+
+#include <ui/qt/widgets/resolved_addresses_view.h>
+#include <ui/qt/models/resolved_addresses_models.h>
+#include <ui/qt/widgets/wireshark_file_dialog.h>
+
+#include <QHeaderView>
+#include <QMessageBox>
+#include <QClipboard>
+#include <QTextStream>
+#include <QJsonArray>
+#include <QJsonObject>
+#include <QJsonDocument>
+#include <QContextMenuEvent>
+
+#include "main_application.h"
+
+#include <wsutil/wslog.h>
+
+ResolvedAddressesView::ResolvedAddressesView(QWidget *parent) : QTableView(parent)
+{
+ setEditTriggers(QAbstractItemView::NoEditTriggers);
+ setSortingEnabled(true);
+ setSelectionBehavior(QAbstractItemView::SelectRows);
+ horizontalHeader()->setStretchLastSection(true);
+ verticalHeader()->setVisible(false);
+
+ // creating this action is mostly to override the default Ctrl-C handling
+ // (which could also be done by overriding KeyPressEvent) and to make the
+ // keyboard shortcut show up in the context menu.
+#if QT_VERSION < QT_VERSION_CHECK(6, 3, 0)
+ clip_action_ = new QAction(tr("as Plain Text"), this);
+ clip_action_->setShortcut(QKeySequence(QKeySequence::Copy));
+ connect(clip_action_, &QAction::triggered, this, &ResolvedAddressesView::clipboardAction);
+ addAction(clip_action_);
+#else
+ clip_action_ = addAction(tr("as Plain Text"), QKeySequence(QKeySequence::Copy), this, &ResolvedAddressesView::clipboardAction);
+#endif
+ clip_action_->setProperty("copy_as", ResolvedAddressesView::EXPORT_TEXT);
+ clip_action_->setProperty("selected", true);
+}
+
+QMenu* ResolvedAddressesView::createCopyMenu(bool selected, QWidget *parent)
+{
+ QMenu *copy_menu;
+ if (selected) {
+ copy_menu = new QMenu(tr("Copy selected rows"), parent);
+ } else {
+ copy_menu = new QMenu(tr("Copy table"), parent);
+ }
+ copy_menu->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy")));
+ QAction *ca;
+ if (selected) {
+ copy_menu->addAction(clip_action_);
+ } else {
+ ca = copy_menu->addAction(tr("as Plain Text"), this, &ResolvedAddressesView::clipboardAction);
+ ca->setProperty("copy_as", ResolvedAddressesView::EXPORT_TEXT);
+ ca->setProperty("selected", selected);
+ }
+ ca = copy_menu->addAction(tr("as CSV"), this, &ResolvedAddressesView::clipboardAction);
+ ca->setProperty("copy_as", ResolvedAddressesView::EXPORT_CSV);
+ ca->setProperty("selected", selected);
+ ca = copy_menu->addAction(tr("as JSON"), this, &ResolvedAddressesView::clipboardAction);
+ ca->setProperty("copy_as", ResolvedAddressesView::EXPORT_JSON);
+ ca->setProperty("selected", selected);
+
+ return copy_menu;
+}
+
+void ResolvedAddressesView::contextMenuEvent(QContextMenuEvent *e)
+{
+ if (!e)
+ return;
+
+ QMenu *ctxMenu = new QMenu(this);
+ ctxMenu->setAttribute(Qt::WA_DeleteOnClose);
+ ctxMenu->addMenu(createCopyMenu(true, ctxMenu));
+ QAction *act = ctxMenu->addAction(tr("Save selected rows as…"));
+ act->setIcon(QIcon::fromTheme(QStringLiteral("document-save-as")));
+ act->setProperty("selected", true);
+ connect(act, &QAction::triggered, this, &ResolvedAddressesView::saveAs);
+ ctxMenu->addSeparator();
+ ctxMenu->addMenu(createCopyMenu(false, ctxMenu));
+ act = ctxMenu->addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), tr("Save table as…"), this, &ResolvedAddressesView::saveAs);
+ act->setProperty("selected", false);
+
+ ctxMenu->popup(e->globalPos());
+}
+
+AStringListListModel* ResolvedAddressesView::dataModel() const
+{
+ QSortFilterProxyModel *proxy = qobject_cast<QSortFilterProxyModel *>(model());
+
+ if (proxy) {
+ QAbstractItemModel *source = proxy->sourceModel();
+ while (qobject_cast<QSortFilterProxyModel *>(source) != nullptr) {
+ proxy = qobject_cast<QSortFilterProxyModel *>(source);
+ source = proxy->sourceModel();
+ }
+ return qobject_cast<AStringListListModel *>(source);
+ }
+ return nullptr;
+}
+
+void ResolvedAddressesView::clipboardAction()
+{
+ QAction *ca = qobject_cast<QAction *>(sender());
+ if (ca && ca->property("copy_as").isValid()) {
+ copyToClipboard(static_cast<eResolvedAddressesExport>(ca->property("copy_as").toInt()),
+ ca->property("selected").toBool());
+ }
+}
+
+void ResolvedAddressesView::copyToClipboard(eResolvedAddressesExport format, bool selected)
+{
+ QString clipText;
+ QTextStream stream(&clipText, QIODevice::Text);
+ toTextStream(stream, format, selected);
+ mainApp->clipboard()->setText(stream.readAll());
+}
+
+void ResolvedAddressesView::saveAs()
+{
+ bool selected = false;
+ QAction *ca = qobject_cast<QAction *>(sender());
+ if (ca && ca->property("selected").isValid()) {
+ selected = true;
+ }
+ QString caption(mainApp->windowTitleString(tr("Save Resolved Addresses As…")));
+ QString txtFilter = tr("Plain text (*.txt)");
+ QString csvFilter = tr("CSV Document (*.csv)");
+ QString jsonFilter = tr("JSON Document (*.json)");
+ QString selectedFilter;
+ QString fileName = WiresharkFileDialog::getSaveFileName(this, caption,
+ mainApp->openDialogInitialDir().canonicalPath(),
+ QString("%1;;%2;;%3").arg(txtFilter).arg(csvFilter).arg(jsonFilter),
+ &selectedFilter);
+ if (fileName.isEmpty()) {
+ return;
+ }
+
+ eResolvedAddressesExport format(EXPORT_TEXT);
+ if (selectedFilter.compare(csvFilter) == 0) {
+ format = EXPORT_CSV;
+ } else if (selectedFilter.compare(jsonFilter) == 0) {
+ format = EXPORT_JSON;
+ }
+
+ // macOS and Windows use the native file dialog, which enforces file
+ // extensions. UN*X dialogs generally don't. That's ok here, at
+ // least for the text format, because hosts and ethers and services
+ // files don't have an extension.
+ QFile saveFile(fileName);
+ if (saveFile.open(QFile::WriteOnly | QFile::Text)) {
+ QTextStream stream(&saveFile);
+ toTextStream(stream, format, selected);
+ saveFile.close();
+ } else {
+ QMessageBox::warning(this, tr("Warning").arg(saveFile.fileName()),
+ tr("Unable to save %1: %2").arg(saveFile.fileName().arg(saveFile.errorString())));
+ }
+}
+
+
+void ResolvedAddressesView::toTextStream(QTextStream& stream,
+ eResolvedAddressesExport format, bool selected) const
+{
+ if (model() == nullptr) {
+ return;
+ }
+
+ // XXX: TrafficTree and TapParameterDialog have similar
+ // "export a QAbstractItemModel to a QTextStream in TEXT, CSV or JSON"
+ // functions that could be made into common code.
+ QStringList rowText;
+ if (format == EXPORT_TEXT) {
+ if (qobject_cast<PortsModel*>(dataModel()) != nullptr) {
+ // Format of services(5)
+ if (!selected) {
+ stream << "# service-name\tport/protocol\n";
+ }
+ for (int row = 0; row < model()->rowCount(); row++) {
+ if (selected && !selectionModel()->isRowSelected(row, QModelIndex())) continue;
+ rowText.clear();
+ rowText << model()->data(model()->index(row, PORTS_COL_NAME)).toString();
+ rowText << QString("%1/%2")
+ .arg(model()->data(model()->index(row, PORTS_COL_PORT)).toString())
+ .arg(model()->data(model()->index(row, PORTS_COL_PROTOCOL)).toString());
+ stream << rowText.join("\t") << "\n";
+ }
+ } else {
+ // Format as hosts(5) and ethers(5)
+ if (!selected) {
+ for (int col = 0; col < model()->columnCount(); col++) {
+ rowText << model()->headerData(col, Qt::Horizontal).toString();
+ }
+ stream << "# " << rowText.join("\t") << "\n";
+ }
+ for (int row = 0; row < model()->rowCount(); row++) {
+ if (selected && !selectionModel()->isRowSelected(row, QModelIndex())) continue;
+ rowText.clear();
+ for (int col = 0; col < model()->columnCount(); col++) {
+ rowText << model()->data(model()->index(row, col)).toString();
+ }
+ stream << rowText.join("\t") << "\n";
+ }
+ }
+ } else if (format == EXPORT_CSV) {
+ for (int col = 0; col < model()->columnCount(); col++) {
+ rowText << model()->headerData(col, Qt::Horizontal).toString();
+ }
+ if (!selected) {
+ stream << rowText.join(",") << "\n";
+ }
+ for (int row = 0; row < model()->rowCount(); row++) {
+ if (selected && !selectionModel()->isRowSelected(row, QModelIndex())) continue;
+ rowText.clear();
+ for (int col = 0; col < model()->columnCount(); col++) {
+ QVariant v = model()->data(model()->index(row, col));
+ if (!v.isValid()) {
+ rowText << QStringLiteral("\"\"");
+ } else if (v.userType() == QMetaType::QString) {
+ rowText << QString("\"%1\"").arg(v.toString().replace('\"', "\"\""));
+ } else {
+ rowText << v.toString();
+ }
+ }
+ stream << rowText.join(",") << "\n";
+ }
+ } else if (format == EXPORT_JSON) {
+ QMap<int, QString> headers;
+ for (int col = 0; col < model()->columnCount(); col++)
+ headers.insert(col, model()->headerData(col, Qt::Horizontal, Qt::DisplayRole).toString());
+
+ QJsonArray records;
+
+ for (int row = 0; row < model()->rowCount(); row++) {
+ if (selected && !selectionModel()->isRowSelected(row, QModelIndex())) continue;
+ QJsonObject rowData;
+ foreach(int col, headers.keys()) {
+ QModelIndex idx = model()->index(row, col);
+ rowData.insert(headers[col], model()->data(idx).toString());
+ }
+ records.push_back(rowData);
+ }
+
+ QJsonDocument json;
+ json.setArray(records);
+ stream << json.toJson();
+ }
+}