From patchwork Tue Oct 16 15:53:01 2018 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Yordan Karadzhov X-Patchwork-Id: 10759567 Return-Path: Received: from mail-sn1nam01on0073.outbound.protection.outlook.com ([104.47.32.73]:27872 "EHLO NAM01-SN1-obe.outbound.protection.outlook.com" rhost-flags-OK-OK-OK-FAIL) by vger.kernel.org with ESMTP id S1726986AbeJPXoS (ORCPT ); Tue, 16 Oct 2018 19:44:18 -0400 From: Yordan Karadzhov To: "rostedt@goodmis.org" CC: "linux-trace-devel@vger.kernel.org" , Yordan Karadzhov Subject: [PATCH v2 04/23] kernel-shark-qt: Add Trace Viewer widget. Date: Tue, 16 Oct 2018 15:53:01 +0000 Message-ID: <20181016155232.5257-5-ykaradzhov@vmware.com> References: <20181016155232.5257-1-ykaradzhov@vmware.com> In-Reply-To: <20181016155232.5257-1-ykaradzhov@vmware.com> Content-Language: en-US MIME-Version: 1.0 Sender: linux-trace-devel-owner@vger.kernel.org List-ID: Content-Length: 22885 From: Yordan Karadzhov (VMware) This patch defines widget for browsing in the trace data shown in a text form. The provides a search panel and a Table view area. The panel and the table are integrated with the Dual Marker. Signed-off-by: Yordan Karadzhov (VMware) --- kernel-shark-qt/src/CMakeLists.txt | 6 +- kernel-shark-qt/src/KsTraceViewer.cpp | 657 ++++++++++++++++++++++++++ kernel-shark-qt/src/KsTraceViewer.hpp | 149 ++++++ 3 files changed, 810 insertions(+), 2 deletions(-) create mode 100644 kernel-shark-qt/src/KsTraceViewer.cpp create mode 100644 kernel-shark-qt/src/KsTraceViewer.hpp diff --git a/kernel-shark-qt/src/CMakeLists.txt b/kernel-shark-qt/src/CMakeLists.txt index 3fd518b..3f40930 100644 --- a/kernel-shark-qt/src/CMakeLists.txt +++ b/kernel-shark-qt/src/CMakeLists.txt @@ -34,14 +34,16 @@ if (Qt5Widgets_FOUND AND Qt5Network_FOUND) set (ks-guiLib_hdr KsUtils.hpp KsModels.hpp KsDualMarker.hpp - KsWidgetsLib.hpp) + KsWidgetsLib.hpp + KsTraceViewer.hpp) QT5_WRAP_CPP(ks-guiLib_hdr_moc ${ks-guiLib_hdr}) add_library(kshark-gui SHARED ${ks-guiLib_hdr_moc} KsUtils.cpp KsModels.cpp KsDualMarker.cpp - KsWidgetsLib.cpp) + KsWidgetsLib.cpp + KsTraceViewer.cpp) target_link_libraries(kshark-gui kshark-plot ${CMAKE_DL_LIBS} diff --git a/kernel-shark-qt/src/KsTraceViewer.cpp b/kernel-shark-qt/src/KsTraceViewer.cpp new file mode 100644 index 0000000..7f12365 --- /dev/null +++ b/kernel-shark-qt/src/KsTraceViewer.cpp @@ -0,0 +1,657 @@ +// SPDX-License-Identifier: LGPL-2.1 + +/* + * Copyright (C) 2017 VMware Inc, Yordan Karadzhov + */ + +/** + * @file KsTraceViewer.cpp + * @brief KernelShark Trace Viewer widget. + */ + +// C++11 +#include +#include + +// KernelShark +#include "KsTraceViewer.hpp" +#include "KsWidgetsLib.hpp" + +/** Create a default (empty) Trace viewer widget. */ +KsTraceViewer::KsTraceViewer(QWidget *parent) +: QWidget(parent), + _view(this), + _model(this), + _proxyModel(this), + _tableHeader(_model.header()), + _toolbar(this), + _labelSearch("Search: Column", this), + _labelGrFollows("Graph follows ", this), + _columnComboBox(this), + _selectComboBox(this), + _searchLineEdit(this), + _prevButton("Prev", this), + _nextButton("Next", this), + _searchStopButton(QIcon::fromTheme("process-stop"), "", this), + _pbAction(nullptr), + _graphFollowsCheckBox(this), + _searchProgBar(this), + _searchCountLabel("", this), + _searchDone(false), + _graphFollows(true), + _mState(nullptr), + _data(nullptr) +{ + int bWidth; + + this->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + + /* Make a search toolbar. */ + _toolbar.setOrientation(Qt::Horizontal); + _toolbar.setMaximumHeight(FONT_HEIGHT * 1.75); + + /* On the toolbar make two Combo boxes for the search settings. */ + _toolbar.addWidget(&_labelSearch); + _columnComboBox.addItems(_tableHeader); + + /* + * Using the old Signal-Slot syntax because + * QComboBox::currentIndexChanged has overloads. + */ + connect(&_columnComboBox, SIGNAL(currentIndexChanged(int)), + this, SLOT(_searchEdit(int))); + + _toolbar.addWidget(&_columnComboBox); + + _selectComboBox.addItem("contains"); + _selectComboBox.addItem("full match"); + _selectComboBox.addItem("does not have"); + + /* + * Using the old Signal-Slot syntax because + * QComboBox::currentIndexChanged has overloads. + */ + connect(&_selectComboBox, SIGNAL(currentIndexChanged(int)), + this, SLOT(_searchEdit(int))); + + _toolbar.addWidget(&_selectComboBox); + + /* On the toolbar, make a Line edit field for search. */ + _searchLineEdit.setMaximumWidth(FONT_WIDTH * 20); + + connect(&_searchLineEdit, &QLineEdit::returnPressed, + this, &KsTraceViewer::_search); + + connect(&_searchLineEdit, &QLineEdit::textEdited, + this, &KsTraceViewer::_searchEditText); + + _toolbar.addWidget(&_searchLineEdit); + _toolbar.addSeparator(); + + /* On the toolbar, add Prev & Next buttons. */ + bWidth = FONT_WIDTH * 6; + + _nextButton.setFixedWidth(bWidth); + _toolbar.addWidget(&_nextButton); + connect(&_nextButton, &QPushButton::pressed, + this, &KsTraceViewer::_next); + + _prevButton.setFixedWidth(bWidth); + _toolbar.addWidget(&_prevButton); + connect(&_prevButton, &QPushButton::pressed, + this, &KsTraceViewer::_prev); + + _toolbar.addSeparator(); + _searchProgBar.setMaximumWidth(FONT_WIDTH * 10); + _searchProgBar.setRange(0, 200); + _pbAction = _toolbar.addWidget(&_searchProgBar); + _pbAction->setVisible(false); + _toolbar.addWidget(&_searchCountLabel); + _searchStopAction = _toolbar.addWidget(&_searchStopButton); + _searchStopAction->setVisible(false); + connect(&_searchStopButton, &QPushButton::pressed, + this, &KsTraceViewer::_searchStop); + + /* + * On the toolbar, make a Check box for connecting the search pannel + * to the Graph widget. + */ + _toolbar.addSeparator(); + _toolbar.addWidget(&_graphFollowsCheckBox); + _toolbar.addWidget(&_labelGrFollows); + _graphFollowsCheckBox.setCheckState(Qt::Checked); + connect(&_graphFollowsCheckBox, &QCheckBox::stateChanged, + this, &KsTraceViewer::_graphFollowsChanged); + + /* Initialize the trace viewer. */ + _view.horizontalHeader()->setDefaultAlignment(Qt::AlignLeft); + _view.verticalHeader()->setVisible(false); + _view.setEditTriggers(QAbstractItemView::NoEditTriggers); + _view.setSelectionBehavior(QAbstractItemView::SelectRows); + _view.setSelectionMode(QAbstractItemView::SingleSelection); + _view.verticalHeader()->setDefaultSectionSize(FONT_HEIGHT * 1.25); + + _proxyModel.setSource(&_model); + _view.setModel(&_proxyModel); + connect(&_proxyModel, &QAbstractItemModel::modelReset, + this, &KsTraceViewer::_searchReset); + + _view.setContextMenuPolicy(Qt::CustomContextMenu); + connect(&_view, &QTableView::customContextMenuRequested, + this, &KsTraceViewer::_onCustomContextMenu); + + connect(&_view, &QTableView::clicked, + this, &KsTraceViewer::_clicked); + + /* Set the layout. */ + _layout.addWidget(&_toolbar); + _layout.addWidget(&_view); + this->setLayout(&_layout); +} + +/** + * @brief Load and show trace data. + * + * @param data: Input location for the KsDataStore object. + * KsDataStore::loadDataFile() must be called first. + */ +void KsTraceViewer::loadData(KsDataStore *data) +{ + _data = data; + _model.reset(); + _proxyModel.fill(data); + _model.fill(data); + this->_resizeToContents(); + + this->setMinimumHeight(SCREEN_HEIGHT / 5); +} + +/** Connect the QTableView widget and the State machine of the Dual marker. */ +void KsTraceViewer::setMarkerSM(KsDualMarkerSM *m) +{ + QString styleSheetA, styleSheetB; + + _mState = m; + _model.setColors(_mState->markerA()._color, + _mState->markerB()._color); + + /* + * Assign a property to State A of the Dual marker state machine. When + * the marker is in State A the background color of the selected row + * will be the same as the color of Marker A. + */ + styleSheetA = "selection-background-color : " + + _mState->markerA()._color.name() + ";"; + + _mState->stateAPtr()->assignProperty(&_view, "styleSheet", + styleSheetA); + + /* + * Assign a property to State B. When the marker is in State B the + * background color of the selected row will be the same as the color + * of Marker B. + */ + styleSheetB = "selection-background-color : " + + _mState->markerB()._color.name() + ";"; + + _mState->stateBPtr()->assignProperty(&_view, "styleSheet", + styleSheetB); +} + +/** Reset (empty) the table. */ +void KsTraceViewer::reset() +{ + this->setMinimumHeight(FONT_HEIGHT * 10); + _model.reset(); + _resizeToContents(); +} + +void KsTraceViewer::_searchReset() +{ + _searchProgBar.setValue(0); + _searchCountLabel.setText(""); + _proxyModel.searchProgress(); + _searchDone = false; +} + +/** Get the index of the first (top) visible row. */ +size_t KsTraceViewer::getTopRow() const +{ + return _view.indexAt(_view.rect().topLeft()).row(); +} + +/** Position given row at the top of the table. */ +void KsTraceViewer::setTopRow(size_t r) +{ + _view.scrollTo(_proxyModel.index(r, 0), + QAbstractItemView::PositionAtTop); +} + +/** Update the content of the table. */ +void KsTraceViewer::update(KsDataStore *data) +{ + /* The Proxy model has to be updated first! */ + _proxyModel.fill(data); + _model.update(data); + _data = data; + if (_mState->activeMarker()._isSet) + showRow(_mState->activeMarker()._pos, true); +} + +void KsTraceViewer::_onCustomContextMenu(const QPoint &point) +{ + QModelIndex i = _view.indexAt(point); + + if (i.isValid()) { + /* + * Use the index of the proxy model to retrieve the value + * of the row number in the source model. + */ + size_t row = _proxyModel.mapRowFromSource(i.row()); + KsQuickEntryMenu menu(_data, row, this); + + connect(&menu, &KsQuickEntryMenu::plotTask, + this, &KsTraceViewer::plotTask); + + menu.exec(mapToGlobal(point)); + } +} + +void KsTraceViewer::_searchEdit(int index) +{ + _searchReset(); // The search has been modified. +} + +void KsTraceViewer::_searchEditText(const QString &text) +{ + _searchReset(); // The search has been modified. +} + +void KsTraceViewer::_graphFollowsChanged(int state) +{ + _graphFollows = (bool) state; + + if (_graphFollows && _searchDone) + emit select(*_it); // Send a signal to the Graph widget. +} + +static bool notHaveCond(QString searchText, QString itemText) +{ + return !itemText.contains(searchText, Qt::CaseInsensitive); +} + +static bool containsCond(QString searchText, QString itemText) +{ + return itemText.contains(searchText, Qt::CaseInsensitive); +} + +static bool matchCond(QString searchText, QString itemText) +{ + return (itemText.compare(searchText, Qt::CaseInsensitive) == 0); +} + +void KsTraceViewer::_search() +{ + /* Disable the user input until the search is done. */ + _searchLineEdit.setReadOnly(true); + if (!_searchDone) { + int xColumn, xSelect; + QString xText; + + /* + * The search is not done. This means that the search settings + * have been modified since the last time we searched. + */ + _matchList.clear(); + xText = _searchLineEdit.text(); + xColumn = _columnComboBox.currentIndex(); + xSelect = _selectComboBox.currentIndex(); + + switch (xSelect) { + case Condition::Containes: + _searchItems(xColumn, xText, &containsCond); + break; + + case Condition::Match: + _searchItems(xColumn, xText, &matchCond); + break; + + case Condition::NotHave: + _searchItems(xColumn, xText, ¬HaveCond); + break; + + default: + break; + } + + if (!_matchList.empty()) { + this->showRow(*_it, true); + + if (_graphFollows) + emit select(*_it); // Send a signal to the Graph widget. + } + } else { + /* + * If the search is done, pressing "Enter" is equivalent + * to pressing "Next" button. + */ + this->_next(); + } + + /* Enable the user input. */ + _searchLineEdit.setReadOnly(false); +} + +void KsTraceViewer::_next() +{ + if (!_searchDone) { + _search(); + return; + } + + if (!_matchList.empty()) { // Items have been found. + ++_it; // Move the iterator. + if (_it == _matchList.end() ) { + // This is the last item of the list. Go back to the beginning. + _it = _matchList.begin(); + } + + // Select the row of the item. + showRow(*_it, true); + + if (_graphFollows) + emit select(*_it); // Send a signal to the Graph widget. + } +} + +void KsTraceViewer::_prev() +{ + if (!_searchDone) { + _search(); + return; + } + + if (!_matchList.empty()) { // Items have been found. + if (_it == _matchList.begin()) { + // This is the first item of the list. Go to the last item. + _it = _matchList.end() - 1; + } else { + --_it; // Move the iterator. + } + + // Select the row of the item. + showRow(*_it, true); + + if (_graphFollows) + emit select(*_it); // Send a signal to the Graph widget. + } +} + +void KsTraceViewer::_searchStop() +{ + _searchStopAction->setVisible(false); + _proxyModel.searchStop(); +} + +void KsTraceViewer::_clicked(const QModelIndex& i) +{ + if (_graphFollows) { + /* + * Use the index of the proxy model to retrieve the value + * of the row number in the base model. + */ + size_t row = _proxyModel.mapRowFromSource(i.row()); + emit select(row); // Send a signal to the Graph widget. + } +} + +/** Make a given row of the table visible. */ +void KsTraceViewer::showRow(size_t r, bool mark) +{ + /* + * Use the index in the source model to retrieve the value of the row number + * in the proxy model. + */ + QModelIndex index = _proxyModel.mapFromSource(_model.index(r, 0)); + + if (mark) { // The row will be selected (colored). + /* Get the first and the last visible rows of the table. */ + int visiTot = _view.indexAt(_view.rect().topLeft()).row(); + int visiBottom = _view.indexAt(_view.rect().bottomLeft()).row() - 2; + + /* Scroll only if the row to be shown in not vizible. */ + if (index.row() < visiTot || index.row() > visiBottom) + _view.scrollTo(index, QAbstractItemView::PositionAtCenter); + + _view.selectRow(index.row()); + } else { + /* + * Just make sure that the row is visible. It will show up at + * the top of the visible part of the table. + */ + _view.scrollTo(index, QAbstractItemView::PositionAtTop); + } +} + +/** Deselects the selected items (row) if any. */ +void KsTraceViewer::deselect() +{ + _view.clearSelection(); +} + +/** Switch the Dual marker. */ +void KsTraceViewer::markSwitch() +{ + /* The state of the Dual marker has changed. Get the new active marker. */ + DualMarkerState state = _mState->getState(); + + /* First deal with the passive marker. */ + if (_mState->getMarker(!state)._isSet) { + /* + * The passive marker is set. Use the model to color the row of + * the passive marker. + */ + _model.selectRow(!state, _mState->getMarker(!state)._pos); + } + else { + /* + * The passive marker is not set. + * Make sure that the model colors nothing. + */ + _model.selectRow(!state, -1); + } + + /* + * Now deal with the active marker. This has to be done after dealing + * with the model, because changing the model clears the selection. + */ + if (_mState->getMarker(state)._isSet) { + /* + * The active marker is set. Use QTableView to select its row. + * The index in the source model is used to retrieve the value + * of the row number in the proxy model. + */ + size_t row =_mState->getMarker(state)._pos; + + QModelIndex index = _proxyModel.mapFromSource(_model.index(row, 0)); + + /* + * The row of the active marker will be colored according to + * the assigned property of the current state of the Dual marker. + */ + _view.selectRow(index.row()); + } else { + _view.clearSelection(); + } +} + +/** + * Reimplemented event handler used to update the geometry of the widget on + * resize events. + */ +void KsTraceViewer::resizeEvent(QResizeEvent* event) +{ + int nColumns = _tableHeader.count(); + int tableSize(0), viewSize, freeSpace; + + for (int c = 0; c < nColumns; ++c) { + tableSize += _view.columnWidth(c); + } + + viewSize = _view.width() - + qApp->style()->pixelMetric(QStyle::PM_ScrollBarExtent); + + if ((freeSpace = viewSize - tableSize) > 0) { + _view.setColumnWidth(nColumns - 1, _view.columnWidth(nColumns - 1) + + freeSpace - + 2); /* Just a little bit less space. + * This will allow the scroll bar + * to disappear when the widget + * is extended to maximum. */ + } +} + +/** + * Reimplemented event handler used to move the active marker. + */ +void KsTraceViewer::keyReleaseEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Up || event->key() == Qt::Key_Down) { + QItemSelectionModel *sm = _view.selectionModel(); + if (sm->hasSelection()) { + /* Only one row at the time can be selected. */ + int row = sm->selectedRows()[0].row(); + emit select(row); // Send a signal to the Graph widget. + } + + return; + } + + QWidget::keyReleaseEvent(event); +} + +void KsTraceViewer::_resizeToContents() +{ + int rows, columnSize; + + _view.setVisible(false); + _view.resizeColumnsToContents(); + _view.setVisible(true); + + /* + * Because of some unknown reason the first column doesn't get + * resized properly by the code above. We will resize this + * column by hand. + */ + rows = _model.rowCount({}); + columnSize = STRING_WIDTH(QString("%1").arg(rows)) + FONT_WIDTH; + _view.setColumnWidth(0, columnSize); +} + +size_t KsTraceViewer::_searchItems(int column, + const QString &searchText, + condition_func cond) +{ + int count; + + _searchProgBar.show(); + _pbAction->setVisible(true); + + if (column == KsViewModel::TRACE_VIEW_COL_INFO || + column == KsViewModel::TRACE_VIEW_COL_LAT) { + _searchStopAction->setVisible(true); + _proxyModel.search(column, searchText, cond, &_matchList, + &_searchProgBar, &_searchCountLabel); + + _searchStopAction->setVisible(false); + } else { + _searchItemsMapReduce(column, searchText, cond); + } + + count = _matchList.count(); + + _pbAction->setVisible(false); + _searchCountLabel.setText(QString(" %1").arg(count)); + _searchDone = true; + + if (count == 0) // No items have been found. Do nothing. + return 0; + + QItemSelectionModel *sm = _view.selectionModel(); + if (sm->hasSelection()) { + /* Only one row at the time can be selected. */ + int row = sm->selectedRows()[0].row(); + + _view.clearSelection(); + _it = _matchList.begin(); + /* + * Move the iterator to the first element of the match list + * after the selected one. + */ + while (*_it <= row) { + ++_it; // Move the iterator. + if (_it == _matchList.end()) { + /* + * This is the last item of the list. Go back + * to the beginning. + */ + _it = _matchList.begin(); + break; + } + } + } else { + /* Move the iterator to the beginning of the match list. */ + _view.clearSelection(); + _it = _matchList.begin(); + } + + return count; +} + +void KsTraceViewer::_searchItemsMapReduce(int column, + const QString &searchText, + condition_func cond) +{ + int nThreads = std::thread::hardware_concurrency(); + std::vector> ranges(nThreads); + std::vector>> maps; + int i(0), nRows(_proxyModel.rowCount({})); + int delta(nRows / nThreads); + + auto lamSearchMap = [&] (const QPair &range, + bool notify) { + return _proxyModel.searchMap(column, searchText, cond, + range.first, range.second, + notify); + }; + + auto lamSearchReduce = [&] (QList &resultList, + const QList &mapList) { + resultList << mapList; + _searchProgBar.setValue(_searchProgBar.value() + 1); + }; + + for (auto &r: ranges) { + r.first = (i++) * delta; + r.second = r.first + delta - 1; + } + + /* + * If the range is not multiple of the number of threads, adjust + * the last range interval. + */ + ranges.back().second = nRows - 1; + maps.push_back(std::async(lamSearchMap, ranges[0], true)); + for (int r = 1; r < nThreads; ++r) + maps.push_back(std::async(lamSearchMap, ranges[r], false)); + + while (_proxyModel.searchProgress() < KS_PROGRESS_BAR_MAX- nThreads) { + std::unique_lock lk(_proxyModel._mutex); + _proxyModel._pbCond.wait(lk); + _searchProgBar.setValue(_proxyModel.searchProgress()); + QApplication::processEvents(); + } + + for (auto &m: maps) + lamSearchReduce(_matchList, m.get()); +} diff --git a/kernel-shark-qt/src/KsTraceViewer.hpp b/kernel-shark-qt/src/KsTraceViewer.hpp new file mode 100644 index 0000000..2a85e4f --- /dev/null +++ b/kernel-shark-qt/src/KsTraceViewer.hpp @@ -0,0 +1,149 @@ +/* SPDX-License-Identifier: LGPL-2.1 */ + +/* + * Copyright (C) 2017 VMware Inc, Yordan Karadzhov + */ + +/** + * @file KsTraceViewer.hpp + * @brief KernelShark Trace Viewer widget. + */ + +#ifndef _KS_TRACEVIEW_H +#define _KS_TRACEVIEW_H + +// Qt +#include + +// KernelShark +#include "KsUtils.hpp" +#include "KsModels.hpp" +#include "KsDualMarker.hpp" + +/** Matching condition function type. To be user for searchong. */ +typedef bool (*condition_func)(QString, QString); + +/** + * The KsTraceViewer class provides a widget for browsing in the trace data + * shown in a text form. + */ +class KsTraceViewer : public QWidget +{ + Q_OBJECT +public: + explicit KsTraceViewer(QWidget *parent = nullptr); + + void loadData(KsDataStore *data); + + void setMarkerSM(KsDualMarkerSM *m); + + void reset(); + + size_t getTopRow() const; + + void setTopRow(size_t r); + + void resizeEvent(QResizeEvent* event) override; + + void keyReleaseEvent(QKeyEvent *event); + + void markSwitch(); + + void showRow(size_t r, bool mark); + + void deselect(); + + void update(KsDataStore *data); + +signals: + /** Signal emitted when new row is selected. */ + void select(size_t); + + /** + * This signal is used to re-emitted the plotTask signal of the + * KsQuickEntryMenu. + */ + void plotTask(int pid); + +private: + QVBoxLayout _layout; + + QTableView _view; + + KsViewModel _model; + + KsFilterProxyModel _proxyModel; + + QStringList _tableHeader; + + QToolBar _toolbar; + + QLabel _labelSearch, _labelGrFollows; + + QComboBox _columnComboBox; + + QComboBox _selectComboBox; + + QLineEdit _searchLineEdit; + + QPushButton _prevButton, _nextButton, _searchStopButton; + + QAction *_pbAction, *_searchStopAction; + + QCheckBox _graphFollowsCheckBox; + + QProgressBar _searchProgBar; + + QLabel _searchCountLabel; + + bool _searchDone; + + bool _graphFollows; + + QList _matchList; + + QList::iterator _it; + + KsDualMarkerSM *_mState; + + KsDataStore *_data; + + enum Condition + { + Containes = 0, + Match = 1, + NotHave = 2 + }; + + void _searchReset(); + + void _resizeToContents(); + + size_t _searchItems(int column, const QString &searchText, + condition_func cond); + + void _searchItemsMapReduce(int column, const QString &searchText, + condition_func cond); + + void _searchEditText(const QString &); + + void _graphFollowsChanged(int); + + void _search(); + + void _next(); + + void _prev(); + + void _searchStop(); + + void _clicked(const QModelIndex& i); + + void _onCustomContextMenu(const QPoint &); + +private slots: + + void _searchEdit(int); +}; + +#endif // _KS_TRACEVIEW_H