/** @file * * Wireshark - Network traffic analyzer * By Gerald Combs * Copyright 1998 Gerald Combs * * SPDX-License-Identifier: GPL-2.0-or-later */ #include #include #include #include #include #include #include #include #include "ui/recent.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include MenuEditAction::MenuEditAction(QString text, QString hintText, QObject * parent) : QWidgetAction(parent), _hintText(hintText), _text(text), _lineEdit(nullptr) {} QWidget * MenuEditAction::createWidget(QWidget *parent) { _lineEdit = new QLineEdit(parent); _lineEdit->setAlignment(Qt::AlignRight); _lineEdit->setText(_text); _lineEdit->setPlaceholderText(_hintText); connect(_lineEdit, &QLineEdit::returnPressed, this, &MenuEditAction::triggerEntry); return _lineEdit; } void MenuEditAction::triggerEntry() { if (_lineEdit) _text = _lineEdit->text(); emit trigger(); } QString MenuEditAction::text() const { return _text; } TrafficTreeHeaderView::TrafficTreeHeaderView(GList ** recentColumnList, QWidget * parent): QHeaderView(Qt::Horizontal, parent) { _recentColumnList = recentColumnList; setContextMenuPolicy(Qt::CustomContextMenu); _actions = new QActionGroup(this); QAction * filterAction = _actions->addAction(tr("Less than")); filterAction->setCheckable(true); filterAction->setChecked(true); filterAction->setProperty("filter_action", (int)TrafficDataFilterProxy::TRAFFIC_DATA_LESS); filterAction = _actions->addAction(tr("Greater than")); filterAction->setCheckable(true); filterAction->setProperty("filter_action", (int)TrafficDataFilterProxy::TRAFFIC_DATA_GREATER); filterAction = _actions->addAction(tr("Equal")); filterAction->setCheckable(true); filterAction->setProperty("filter_action", (int)TrafficDataFilterProxy::TRAFFIC_DATA_EQUAL); connect(this, &QHeaderView::customContextMenuRequested, this, &TrafficTreeHeaderView::headerContextMenu); } TrafficTreeHeaderView::~TrafficTreeHeaderView() {} void TrafficTreeHeaderView::headerContextMenu(const QPoint &pos) { TrafficTree * tree = qobject_cast(parent()); if (!tree) return; TrafficDataFilterProxy * proxy = qobject_cast(tree->model()); if (sender() != this || ! proxy) return; QMenu * ctxMenu = new QMenu(this); ctxMenu->setAttribute(Qt::WA_DeleteOnClose); QAction * headerAction = ctxMenu->addAction(tr("Columns to display")); headerAction->setEnabled(false); for (int col = 0; col < tree->dataModel()->columnCount(); col++) { QString name = tree->dataModel()->headerData(col).toString(); QAction * action = new QAction(name); action->setCheckable(true); action->setChecked(proxy->columnVisible(col)); action->setProperty("col_nr", col); ctxMenu->addAction(action); connect(action, &QAction::triggered, this, &TrafficTreeHeaderView::columnTriggered); } ctxMenu->addSeparator(); int column = logicalIndexAt(pos); bool is_address = false; QModelIndex sourceIdx = proxy->mapToSource(proxy->index(0, column)); if (qobject_cast(proxy->sourceModel()) && sourceIdx.column() == EndpointDataModel::ENDP_COLUMN_ADDR) { is_address = true; } else if (qobject_cast(proxy->sourceModel()) && (sourceIdx.column() == ConversationDataModel::CONV_COLUMN_SRC_ADDR || sourceIdx.column() == ConversationDataModel::CONV_COLUMN_DST_ADDR)) { is_address = true; } if (! is_address) { QString columnText = model()->headerData(column, Qt::Horizontal).toString(); QAction * filterAction = ctxMenu->addAction(tr("Filter %1 by").arg(columnText)); filterAction->setEnabled(false); ctxMenu->addActions(_actions->actions()); MenuEditAction * editAction = new MenuEditAction(_filterText, tr("Enter filter value")); editAction->setProperty("column", column); ctxMenu->addAction(editAction); connect(editAction, &MenuEditAction::triggered, this, &TrafficTreeHeaderView::filterColumn); } connect(ctxMenu, &QMenu::triggered, this, &TrafficTreeHeaderView::menuActionTriggered); ctxMenu->popup(mapToGlobal(pos)); } void TrafficTreeHeaderView::applyRecent() { TrafficTree * tree = qobject_cast(parent()); if (!tree) return; QList columns; for (GList * endTab = *_recentColumnList; endTab; endTab = endTab->next) { QString colStr = QString((const char *)endTab->data); bool ok = false; int col = colStr.toInt(&ok); if (ok) columns << col; } if (columns.count() > 0) { TrafficDataFilterProxy * proxy = qobject_cast(tree->model()); for (int col = 0; col < tree->dataModel()->columnCount(); col++) { proxy->setColumnVisibility(col, columns.contains(col)); } } } void TrafficTreeHeaderView::columnTriggered(bool checked) { TrafficTree * tree = qobject_cast(parent()); if (!tree) return; TrafficDataFilterProxy * proxy = qobject_cast(tree->model()); QAction * entry = qobject_cast(sender()); if (! proxy || ! entry || ! entry->property("col_nr").isValid()) return; int col = entry->property("col_nr").toInt(); proxy->setColumnVisibility(col, checked); prefs_clear_string_list(*_recentColumnList); *_recentColumnList = NULL; QList visible; for (int col = 0; col < tree->dataModel()->columnCount(); col++) { if (proxy->columnVisible(col)) { visible << col; gchar *nr = qstring_strdup(QString::number(col)); *_recentColumnList = g_list_append(*_recentColumnList, nr); } } emit columnsHaveChanged(visible); } void TrafficTreeHeaderView::menuActionTriggered(QAction * act) { if (_actions && _actions->actions().contains(act)) { QMenu * menu = qobject_cast(sender()); if (menu) { MenuEditAction * menuAction = nullptr; foreach(QAction * _act, menu->actions()) { if (qobject_cast(_act)) { menuAction = qobject_cast(_act); break; } } int column = menuAction ? menuAction->property("column").toInt() : -1; if (column >= 0) { _filterText = menuAction->text().trimmed(); if (_filterText.length() == 0) column = -1; int filterOn = act->property("filter_action").toInt(); emit filterOnColumn(column, filterOn, _filterText); } } } } void TrafficTreeHeaderView::filterColumn(bool) { MenuEditAction * menuAction = qobject_cast(sender()); if (!menuAction) return; int filterOn = TrafficDataFilterProxy::TRAFFIC_DATA_LESS; foreach(QAction * act, _actions->actions()) { if (act->isChecked() && act->property("filter_action").isValid()) { filterOn = act->property("filter_action").toInt(); break; } } int column = menuAction->property("column").toInt(); _filterText = menuAction->text().trimmed(); if (_filterText.length() == 0) column = -1; emit filterOnColumn(column, filterOn, _filterText); } TrafficDataFilterProxy::TrafficDataFilterProxy(QObject *parent) : QSortFilterProxyModel(parent), _filterColumn(-1), _filterOn(-1), _filterText(QString()) { setSortRole(ATapDataModel::UNFORMATTED_DISPLAYDATA); } void TrafficDataFilterProxy::filterForColumn(int column, int filterOn, QString filterText) { if (filterOn < 0 || filterOn > TrafficDataFilterProxy::TRAFFIC_DATA_EQUAL) column = -1; _filterColumn = column; _filterOn = filterOn; _filterText = filterText; invalidateFilter(); } int TrafficDataFilterProxy::mapToSourceColumn(int proxyColumn) const { ATapDataModel * model = qobject_cast(sourceModel()); int column = proxyColumn; if (model) { if (qobject_cast(model)) { if (model->portsAreHidden() && column > EndpointDataModel::ENDP_COLUMN_ADDR) column++; if (! model->showTotalColumn()) { if (column > EndpointDataModel::ENDP_COLUMN_BYTES) column+=2; } } else if (qobject_cast(model)) { if (model->portsAreHidden()) { if (column > ConversationDataModel::CONV_COLUMN_SRC_ADDR) column++; if (column > ConversationDataModel::CONV_COLUMN_DST_ADDR) column++; } ConversationDataModel * convModel = qobject_cast(model); if (!convModel->showConversationId() && column > ConversationDataModel::CONV_COLUMN_BYTES) { column++; } if (! model->showTotalColumn()) { if (column > ConversationDataModel::CONV_COLUMN_CONV_ID) column+=2; } } } return column; } bool TrafficDataFilterProxy::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { ATapDataModel * dataModel = qobject_cast(sourceModel()); if (dataModel) { bool isFiltered = dataModel->data(dataModel->index(source_row, 0), ATapDataModel::ROW_IS_FILTERED).toBool(); if (isFiltered && dataModel->filter().length() > 0) return false; int sourceColumn = mapToSourceColumn(_filterColumn); QModelIndex srcIdx = dataModel->index(source_row, sourceColumn); if (srcIdx.isValid()) { QVariant data = srcIdx.data(ATapDataModel::UNFORMATTED_DISPLAYDATA); bool filtered = false; /* QVariant comparisons coerce to the first parameter type, so * putting data first and converting the string to it is important. */ #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) /* QVariant::compare coerces strings to numeric types, but does * not try to automatically convert them to datetime related types. */ QVariant rhs = QVariant(_filterText); if (data.userType() == QMetaType::QDateTime) { /* When we display start time in absolute format, we only * display the time portion, so users will expect to enter * time-only filters. Convert to QTime instead of QDateTime. * (Sorting in the table will use the date as well, but it's * unreasonable to expect users to type in a date that they * can't see when filtering.) */ QTime filterTime = QTime::fromString(_filterText, Qt::ISODateWithMs); if (filterTime.isValid()) { rhs.setValue(filterTime); } else { rhs = QVariant(); } data.setValue(data.toTime()); } QPartialOrdering result = QVariant::compare(data, rhs); if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_LESS) filtered = result < 0; else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_GREATER) filtered = result > 0; else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_EQUAL) filtered = result == 0; #else /* The comparisons are deprecated in 5.15. This is most of the * implementation of QAbstractItemModelPrivate::isVariantLessThan * from the Qt source. */ if (_filterText.isEmpty()) filtered = true; else if (data.isNull()) filtered = false; else { switch (data.userType()) { case QMetaType::Int: case QMetaType::UInt: case QMetaType::LongLong: if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_LESS) filtered = data.toLongLong() < _filterText.toLongLong(); else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_GREATER) filtered = data.toLongLong() > _filterText.toLongLong(); else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_EQUAL) filtered = data.toLongLong() == _filterText.toLongLong(); break; case QMetaType::Float: case QMetaType::Double: if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_LESS) filtered = data.toDouble() < _filterText.toDouble(); else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_GREATER) filtered = data.toDouble() > _filterText.toDouble(); else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_EQUAL) filtered = data.toDouble() == _filterText.toDouble(); break; case QMetaType::QDateTime: case QMetaType::QTime: /* When we display start time in absolute format, we only * display the time portion, so users will expect to enter * time-only filters. Convert to QTime instead of QDateTime. */ if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_LESS) filtered = data.toTime() < QTime::fromString(_filterText, Qt::ISODateWithMs); else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_GREATER) filtered = data.toTime() > QTime::fromString(_filterText, Qt::ISODateWithMs); else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_EQUAL) filtered = data.toTime() == QTime::fromString(_filterText, Qt::ISODateWithMs); break; case QMetaType::QString: default: /* XXX: We don't do UTF-8 aware coallating in Packet List * (because it's slow), but possibly could here. */ if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_LESS) filtered = data.toString() < _filterText; else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_GREATER) filtered = data.toString() > _filterText; else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_EQUAL) filtered = data.toString() == _filterText; break; } } #endif if (!filtered) return false; } } return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); } bool TrafficDataFilterProxy::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const { if (! source_left.isValid() || ! qobject_cast(source_left.model())) return false; if (! source_right.isValid() || ! qobject_cast(source_right.model())) return false; ATapDataModel * model = qobject_cast(sourceModel()); if (! model || source_left.model() != model || source_right.model() != model) return false; QVariant datA = source_left.data(ATapDataModel::UNFORMATTED_DISPLAYDATA); QVariant datB = source_right.data(ATapDataModel::UNFORMATTED_DISPLAYDATA); bool is_address = false; if (qobject_cast(model) && source_left.column() == EndpointDataModel::ENDP_COLUMN_ADDR && source_left.column() == source_right.column()) { is_address = true; } else if (qobject_cast(model) && (source_left.column() == ConversationDataModel::CONV_COLUMN_SRC_ADDR || source_left.column() == ConversationDataModel::CONV_COLUMN_DST_ADDR) && source_left.column() == source_right.column()) { is_address = true; } if (is_address) { bool result = false; bool identical = false; int addressTypeA = model->data(source_left, ATapDataModel::DATA_ADDRESS_TYPE).toInt(); int addressTypeB = model->data(source_right, ATapDataModel::DATA_ADDRESS_TYPE).toInt(); if (addressTypeA != 0 && addressTypeB != 0 && addressTypeA != addressTypeB) { result = addressTypeA < addressTypeB; } else if (addressTypeA != 0 && addressTypeA == addressTypeB) { if (addressTypeA == AT_IPv4) { quint32 valA = model->data(source_left, ATapDataModel::DATA_IPV4_INTEGER).value(); quint32 valB = model->data(source_right, ATapDataModel::DATA_IPV4_INTEGER).value(); result = valA < valB; identical = valA == valB; } else if (addressTypeA == AT_NUMERIC) { quint32 valA = datA.toInt(); quint32 valB = datB.toInt(); result = valA < valB; identical = valA == valB; } else { result = QString::compare(datA.toString(), datB.toString(), Qt::CaseInsensitive) < 0; identical = QString::compare(datA.toString(), datB.toString(), Qt::CaseInsensitive) == 0; } int portColumn = EndpointDataModel::ENDP_COLUMN_PORT; if (identical && qobject_cast(model)) { QModelIndex tstA, tstB; if (source_left.column() == ConversationDataModel::CONV_COLUMN_SRC_ADDR) { portColumn = ConversationDataModel::CONV_COLUMN_SRC_PORT; int col = ConversationDataModel::CONV_COLUMN_DST_ADDR; tstA = model->index(source_left.row(), col); tstB = model->index(source_right.row(), col); } else if (source_left.column() == ConversationDataModel::CONV_COLUMN_DST_ADDR) { portColumn = ConversationDataModel::CONV_COLUMN_DST_PORT; int col = ConversationDataModel::CONV_COLUMN_SRC_ADDR; tstA = model->index(source_left.row(), col); tstB = model->index(source_right.row(), col); } if (addressTypeA == AT_IPv4) { quint32 valX = model->data(tstA, ATapDataModel::DATA_IPV4_INTEGER).value(); quint32 valY = model->data(tstB, ATapDataModel::DATA_IPV4_INTEGER).value(); result = valX < valY; identical = valX == valY; } else { result = QString::compare(model->data(tstA).toString().toLower(), model->data(tstB).toString(), Qt::CaseInsensitive) < 0; identical = QString::compare(model->data(tstA).toString().toLower(), model->data(tstB).toString(), Qt::CaseInsensitive) == 0; } } if (! result && identical && ! model->portsAreHidden()) { int portA = model->data(model->index(source_left.row(), portColumn)).toInt(); int portB = model->data(model->index(source_right.row(), portColumn)).toInt(); return portA < portB; } } return result; } return QSortFilterProxyModel::lessThan(source_left, source_right); } bool TrafficDataFilterProxy::filterAcceptsColumn(int source_column, const QModelIndex &) const { if (hideColumns_.contains(source_column)) return false; ATapDataModel * model = qobject_cast(sourceModel()); if (model) { if (model->portsAreHidden()) { if (qobject_cast(model) && source_column == EndpointDataModel::ENDP_COLUMN_PORT) return false; if (qobject_cast(model) && (source_column == ConversationDataModel::CONV_COLUMN_SRC_PORT || source_column == ConversationDataModel::CONV_COLUMN_DST_PORT)) return false; } if (! model->showTotalColumn()) { if (qobject_cast(model) && (source_column == EndpointDataModel::ENDP_COLUMN_PACKETS_TOTAL || source_column == EndpointDataModel::ENDP_COLUMN_BYTES_TOTAL)) return false; if (qobject_cast(model) && (source_column == ConversationDataModel::CONV_COLUMN_PACKETS_TOTAL || source_column == ConversationDataModel::CONV_COLUMN_BYTES_TOTAL)) return false; } if (qobject_cast(model)) { ConversationDataModel * convModel = qobject_cast(model); if (source_column == ConversationDataModel::CONV_COLUMN_CONV_ID && ! convModel->showConversationId()) return false; } } return true; } void TrafficDataFilterProxy::setColumnVisibility(int column, bool visible) { hideColumns_.removeAll(column); if (!visible) hideColumns_.append(column); invalidateFilter(); } bool TrafficDataFilterProxy::columnVisible(int column) const { return ! hideColumns_.contains(column); } TrafficTree::TrafficTree(QString baseName, GList ** recentColumnList, QWidget *parent) : QTreeView(parent) { _tapEnabled = true; _saveRaw = true; _baseName = baseName; _exportRole = ATapDataModel::UNFORMATTED_DISPLAYDATA; _header = nullptr; setAlternatingRowColors(true); setRootIsDecorated(false); setSortingEnabled(true); setContextMenuPolicy(Qt::CustomContextMenu); _header = new TrafficTreeHeaderView(recentColumnList); setHeader(_header); connect(_header, &TrafficTreeHeaderView::columnsHaveChanged, this, &TrafficTree::columnsHaveChanged); connect(this, &QTreeView::customContextMenuRequested, this, &TrafficTree::customContextMenu); } void TrafficTree::setModel(QAbstractItemModel * model) { if (model) { TrafficDataFilterProxy * proxy = qobject_cast(model); if (proxy) { connect(_header, &TrafficTreeHeaderView::filterOnColumn, proxy, &TrafficDataFilterProxy::filterForColumn); } } QTreeView::setModel(model); } void TrafficTree::tapListenerEnabled(bool enable) { _tapEnabled = enable; } ATapDataModel * TrafficTree::dataModel() { QSortFilterProxyModel * proxy = qobject_cast(model()); if (proxy) return qobject_cast(proxy->sourceModel()); return nullptr; } void TrafficTree::customContextMenu(const QPoint &pos) { if (sender() != this) return; QMenu * ctxMenu = new QMenu(this); ctxMenu->setAttribute(Qt::WA_DeleteOnClose); bool isConv = false; QModelIndex idx = indexAt(pos); TrafficDataFilterProxy * proxy = qobject_cast(model()); if (proxy) idx = proxy->mapToSource(idx); ConversationDataModel * model = qobject_cast(dataModel()); if (model) isConv = true; ctxMenu->addMenu(createActionSubMenu(FilterAction::ActionApply, idx, isConv)); ctxMenu->addMenu(createActionSubMenu(FilterAction::ActionPrepare, idx, isConv)); ctxMenu->addMenu(createActionSubMenu(FilterAction::ActionFind, idx, isConv)); ctxMenu->addMenu(createActionSubMenu(FilterAction::ActionColorize, idx, isConv)); ctxMenu->addSeparator(); ctxMenu->addMenu(createCopyMenu()); ctxMenu->addSeparator(); QAction * act = ctxMenu->addAction(tr("Resize all columns to content")); connect(act, &QAction::triggered, this, &TrafficTree::resizeAction); ctxMenu->popup(mapToGlobal(pos)); } static QMap fad_to_cd_; static void initDirection() { if (fad_to_cd_.count() == 0) { fad_to_cd_[FilterAction::ActionDirectionAToFromB] = CONV_DIR_A_TO_FROM_B; fad_to_cd_[FilterAction::ActionDirectionAToB] = CONV_DIR_A_TO_B; fad_to_cd_[FilterAction::ActionDirectionAFromB] = CONV_DIR_A_FROM_B; fad_to_cd_[FilterAction::ActionDirectionAToFromAny] = CONV_DIR_A_TO_FROM_ANY; fad_to_cd_[FilterAction::ActionDirectionAToAny] = CONV_DIR_A_TO_ANY; fad_to_cd_[FilterAction::ActionDirectionAFromAny] = CONV_DIR_A_FROM_ANY; fad_to_cd_[FilterAction::ActionDirectionAnyToFromB] = CONV_DIR_ANY_TO_FROM_B; fad_to_cd_[FilterAction::ActionDirectionAnyToB] = CONV_DIR_ANY_TO_B; fad_to_cd_[FilterAction::ActionDirectionAnyFromB] = CONV_DIR_ANY_FROM_B; } } QMenu * TrafficTree::createActionSubMenu(FilterAction::Action cur_action, QModelIndex idx, bool isConversation) { initDirection(); conv_item_t * conv_item = nullptr; bool hasConvId = false; if (isConversation) { ConversationDataModel * model = qobject_cast(dataModel()); if (model) { conv_item = model->itemForRow(idx.row()); hasConvId = model->showConversationId(idx.row()); } } QMenu * subMenu = new QMenu(FilterAction::actionName(cur_action)); subMenu->setEnabled(_tapEnabled); foreach (FilterAction::ActionType at, FilterAction::actionTypes()) { if (isConversation && conv_item) { QMenu *subsubmenu = subMenu->addMenu(FilterAction::actionTypeName(at)); if (hasConvId && (cur_action == FilterAction::ActionApply || cur_action == FilterAction::ActionPrepare)) { QString filter = QString("%1.stream eq %2").arg(conv_item->ctype == CONVERSATION_TCP ? "tcp" : "udp").arg(conv_item->conv_id); FilterAction * act = new FilterAction(subsubmenu, cur_action, at, tr("Filter on stream id")); act->setProperty("filter", filter); subsubmenu->addAction(act); connect(act, &QAction::triggered, this, &TrafficTree::useFilterAction); } foreach (FilterAction::ActionDirection ad, FilterAction::actionDirections()) { FilterAction *fa = new FilterAction(subsubmenu, cur_action, at, ad); QString filter = get_conversation_filter(conv_item, (conv_direction_e) fad_to_cd_[fa->actionDirection()]); fa->setProperty("filter", filter); subsubmenu->addAction(fa); connect(fa, &QAction::triggered, this, &TrafficTree::useFilterAction); } } else { FilterAction *fa = new FilterAction(subMenu, cur_action, at); fa->setProperty("filter", idx.data(ATapDataModel::DISPLAY_FILTER)); subMenu->addAction(fa); connect(fa, &QAction::triggered, this, &TrafficTree::useFilterAction); } } return subMenu; } QMenu * TrafficTree::createCopyMenu(QWidget *parent) { QMenu *copy_menu = new QMenu(tr("Copy %1 table").arg(_baseName), parent); QAction *ca; ca = copy_menu->addAction(tr("as CSV")); ca->setToolTip(tr("Copy all values of this page to the clipboard in CSV (Comma Separated Values) format.")); ca->setProperty("copy_as", TrafficTree::CLIPBOARD_CSV); connect(ca, &QAction::triggered, this, &TrafficTree::clipboardAction); ca = copy_menu->addAction(tr("as YAML")); ca->setToolTip(tr("Copy all values of this page to the clipboard in the YAML data serialization format.")); ca->setProperty("copy_as", TrafficTree::CLIPBOARD_YAML); connect(ca, &QAction::triggered, this, &TrafficTree::clipboardAction); ca = copy_menu->addAction(tr("as JSON")); ca->setToolTip(tr("Copy all values of this page to the clipboard in the JSON data serialization format.")); ca->setProperty("copy_as", TrafficTree::CLIPBOARD_JSON); connect(ca, &QAction::triggered, this, &TrafficTree::clipboardAction); copy_menu->addSeparator(); ca = copy_menu->addAction(tr("Save data as raw")); ca->setToolTip(tr("Disable data formatting for export/clipboard and save as raw data")); ca->setCheckable(true); ca->setChecked(_exportRole == ATapDataModel::UNFORMATTED_DISPLAYDATA); connect(ca, &QAction::triggered, this, &TrafficTree::toggleSaveRawAction); return copy_menu; } void TrafficTree::useFilterAction() { FilterAction *fa = qobject_cast(sender()); if (!fa || !_tapEnabled) return; QString filter = fa->property("filter").toString(); if (filter.length() > 0) { MainWindow * mainWin = (MainWindow *)(mainApp->mainWindow()); mainWin->setDisplayFilter(filter, fa->action(), fa->actionType()); } } void TrafficTree::clipboardAction() { QAction * ca = qobject_cast(sender()); if (ca && ca->property("copy_as").isValid()) copyToClipboard((eTrafficTreeClipboard)ca->property("copy_as").toInt()); } void TrafficTree::resizeAction() { for (int col = 0; col < model()->columnCount(); col++) resizeColumnToContents(col); } void TrafficTree::toggleSaveRawAction() { if (_exportRole == ATapDataModel::UNFORMATTED_DISPLAYDATA) _exportRole = Qt::DisplayRole; else _exportRole = ATapDataModel::UNFORMATTED_DISPLAYDATA; } void TrafficTree::copyToClipboard(eTrafficTreeClipboard type) { if (!model()) return; QString clipText; QTextStream stream(&clipText, QIODevice::Text); if (type == CLIPBOARD_CSV) { QMap headers; QStringList rdsl; for (int cnt = 0; cnt < model()->columnCount(); cnt++) { rdsl << model()->headerData(cnt, Qt::Horizontal, Qt::DisplayRole).toString(); } stream << rdsl.join(",") << "\n"; for (int row = 0; row < model()->rowCount(); row++) { rdsl.clear(); for (int col = 0; col < model()->columnCount(); col++) { QModelIndex idx = model()->index(row, col); QVariant v = model()->data(idx, _exportRole); if (!v.isValid()) { rdsl << "\"\""; } else if (v.userType() == QMetaType::QString) { rdsl << QString("\"%1\"").arg(v.toString()); } else { rdsl << v.toString(); } } stream << rdsl.join(",") << '\n'; } } else if (type == CLIPBOARD_YAML) { stream << "---" << '\n'; QMap headers; for (int cnt = 0; cnt < model()->columnCount(); cnt++) headers.insert(cnt, model()->headerData(cnt, Qt::Horizontal, Qt::DisplayRole).toString()); for (int row = 0; row < model()->rowCount(); row++) { stream << "-" << '\n'; for (int col = 0; col < model()->columnCount(); col++) { QModelIndex idx = model()->index(row, col); QVariant v = model()->data(idx, _exportRole); stream << " - " << headers[col] << ": " << v.toString() << '\n'; } } } else if (type == CLIPBOARD_JSON) { QMap headers; for (int cnt = 0; cnt < model()->columnCount(); cnt++) headers.insert(cnt, model()->headerData(cnt, Qt::Horizontal, Qt::DisplayRole).toString()); QJsonArray records; for (int row = 0; row < model()->rowCount(); row++) { QJsonObject rowData; foreach(int col, headers.keys()) { QModelIndex idx = model()->index(row, col); rowData.insert(headers[col], model()->data(idx, _exportRole).toString()); } records.push_back(rowData); } QJsonDocument json; json.setArray(records); stream << json.toJson(); } mainApp->clipboard()->setText(stream.readAll()); } void TrafficTree::disableTap() { ATapDataModel * model = dataModel(); if (!model) return; model->disableTap(); } void TrafficTree::applyRecentColumns() { if (_header) _header->applyRecent(); } void TrafficTree::columnsChanged(QList columns) { TrafficDataFilterProxy * proxy = qobject_cast(model()); if (!proxy) return; for (int col = 0; col < dataModel()->columnCount(); col++) { proxy->setColumnVisibility(col, columns.contains(col)); } resizeAction(); }