diff --git a/CMakeLists.txt b/CMakeLists.txt index cc82669f..575eb851 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -190,6 +190,10 @@ if(Qt5Core_FOUND) #set(QT_LIBRARIES Qt5::Gui Qt5::Widgets Qt5::WinExtras) set(QT_INCLUDE_DIRS ${Qt5Gui_INCLUDE_DIRS} ${Qt5Widgets_INCLUDE_DIRS}) set(QT_LIBRARIES Qt5::Gui Qt5::Widgets) + if(WIN32) + find_package(Qt5WinExtras REQUIRED) + list(APPEND QT_LIBRARIES Qt5::WinExtras) + endif() add_definitions(${Qt5Gui_DEFINITIONS} ${Qt5Widgets_DEFINITIONS}) else() find_package(Qt6Core QUIET) @@ -551,10 +555,13 @@ set(DSView_RESOURCES if(WIN32) # Use the DSView icon for the DSView.exe executable. + add_definitions(-DUNICODE -D_UNICODE) set(CMAKE_RC_COMPILE_OBJECT "${CMAKE_RC_COMPILER} -O coff -I${CMAKE_CURRENT_SOURCE_DIR} ") enable_language(RC) # app icon list(APPEND DSView_SOURCES applogo.rc) + list(APPEND DSView_SOURCES DSView/pv/winnativewidget.cpp DSView/pv/winshadow.cpp) + list(APPEND DSView_HEADERS DSView/pv/winshadow.h) endif() if(Qt5Core_FOUND) diff --git a/DSView/main.cpp b/DSView/main.cpp index 748a59d9..58d2d155 100644 --- a/DSView/main.cpp +++ b/DSView/main.cpp @@ -64,6 +64,7 @@ int main(int argc, char *argv[]) const char *open_file = NULL; int logLevel = -1; bool bStoreLog = false; + quintptr embedParent = 0; //----------------------rebuild command param #ifdef _WIN32 @@ -103,6 +104,8 @@ int main(int argc, char *argv[]) {"version", no_argument, 0, 'v'}, {"storelog", no_argument, 0, 's'}, {"help", no_argument, 0, 'h'}, + {"embed-parent", required_argument, 0, 1000}, + {"host-version", no_argument, 0, 1001}, {0, 0, 0, 0} }; @@ -113,6 +116,20 @@ int main(int argc, char *argv[]) switch (c) { + case 1000: { + bool ok = false; + embedParent = QString::fromUtf8(optarg).toULongLong(&ok, 0); +#ifdef _WIN32 + if (!ok || !embedParent || !IsWindow(reinterpret_cast(embedParent))) +#else + if (true) +#endif + return 2; + break; + } + case 1001: + printf("DSView host protocol 1\n"); + return 0; case 'l': // log level logLevel = atoi(optarg); break; @@ -174,6 +191,14 @@ bool bHighScale = true; QApplication::setApplicationName("DSView"); QApplication::setOrganizationName("DreamSourceLab"); QApplication::setOrganizationDomain("www.DreamSourceLab.com"); + if (embedParent) { + QApplication::setOrganizationName("SET"); + QApplication::setApplicationName("SETGUI-DSView"); + a.setProperty("embeddedHost", true); + // The native child uses Qt::Tool and is not a primary Qt window. + // Closing a modal dialog must not end the hosted application's loop. + a.setQuitOnLastWindowClosed(false); + } //----------------------init log dsv_log_init(); // Don't call before QApplication be inited @@ -199,6 +224,17 @@ bool bHighScale = true; AppControl *control = AppControl::Instance(); AppConfig &app = AppConfig::Instance(); app.LoadAll(); //load app config + if (embedParent) { + app.frameOptions.style = "dark"; + app.appOptions.fontSize = 10; + QString captureDir = QString::fromUtf8(qgetenv("DSVIEW_CAPTURE_DIR")); + if (QDir(captureDir).exists() && !captureDir.isEmpty()) { + app.userHistory.saveDir = captureDir; + app.userHistory.openDir = captureDir; + app.userHistory.exportDir = captureDir; + app.userHistory.screenShotPath = captureDir; + } + } LangResource::Instance()->Load(app.frameOptions.language); if (app.appOptions.ableSaveLog){ @@ -237,8 +273,23 @@ bool bHighScale = true; { pv::MainFrame w; control->Start(); - w.ShowFormInit(); - w.ShowHelpDocAsync(); //to show the dailog for open help document +#ifdef _WIN32 + if (embedParent) { + if (!w.ShowEmbedded(embedParent)) return 2; + printf("DSVIEW_READY %llu\n", static_cast(w.winId())); + printf("DSVIEW_CAPABILITIES OPEN_FILE\n"); + fflush(stdout); + // Optional render artifact for integration smoke tests. + const QString screenshot = QString::fromUtf8(qgetenv("DSVIEW_HOST_SCREENSHOT")); + if (!screenshot.isEmpty()) QTimer::singleShot(2500, &w, [&w, screenshot]() { + w.grab().save(screenshot); + }); + } else +#endif + { + w.ShowFormInit(); + w.ShowHelpDocAsync(); + } ret = a.exec(); //Run the application control->Stop(); diff --git a/DSView/pv/config/appconfig.cpp b/DSView/pv/config/appconfig.cpp index 1e318306..b4a7f5ea 100644 --- a/DSView/pv/config/appconfig.cpp +++ b/DSView/pv/config/appconfig.cpp @@ -438,8 +438,9 @@ bool AppConfig::IsDarkStyle() return false; } -QColor AppConfig::GetStyleColor() -{ +QColor AppConfig::GetStyleColor() +{ + if (qApp->property("embeddedHost").toBool()) return QColor("#10151d"); if (IsDarkStyle()){ return QColor(38, 38, 38); } @@ -551,4 +552,4 @@ QString GetProfileDir() #else return QStandardPaths::writableLocation(QStandardPaths::DataLocation); #endif -} \ No newline at end of file +} diff --git a/DSView/pv/data/decode/annotation.cpp b/DSView/pv/data/decode/annotation.cpp index f285247c..7da1849c 100644 --- a/DSView/pv/data/decode/annotation.cpp +++ b/DSView/pv/data/decode/annotation.cpp @@ -179,6 +179,16 @@ const std::vector& Annotation::annotations() const return resItem.cvt_lines; } +bool Annotation::contains_text(const QString &text) const +{ + // The waveform can display any of these labels, not just the longest one. + for (const auto &label : annotations()) { + if (label.contains(text, Qt::CaseInsensitive)) + return true; + } + return false; +} + bool Annotation::is_numberic() { AnnotationSourceItem *resItem = _status->m_resTable.GetItem(_resIndex); diff --git a/DSView/pv/data/decode/annotation.h b/DSView/pv/data/decode/annotation.h index a5b0e440..06067395 100644 --- a/DSView/pv/data/decode/annotation.h +++ b/DSView/pv/data/decode/annotation.h @@ -66,6 +66,8 @@ public: const std::vector& annotations() const; + bool contains_text(const QString &text) const; + private: uint64_t _start_sample; uint64_t _end_sample; diff --git a/DSView/pv/data/decodermodel.cpp b/DSView/pv/data/decodermodel.cpp index 61710e54..ee814a76 100644 --- a/DSView/pv/data/decodermodel.cpp +++ b/DSView/pv/data/decodermodel.cpp @@ -32,6 +32,58 @@ using namespace std; namespace pv { namespace data { +AnnotationMatchModel::AnnotationMatchModel(QAbstractItemModel *source, QObject *parent) + : QAbstractTableModel(parent), _source(source) +{ + connect(source, &QAbstractItemModel::modelAboutToBeReset, this, [this]() { + setMatches({}, QString()); + }); +} + +int AnnotationMatchModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : int(_matches.size()); +} + +int AnnotationMatchModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : 2; +} + +QModelIndex AnnotationMatchModel::sourceIndex(int row) const +{ + return _source && row >= 0 && row < int(_matches.size()) ? _matches[row] : QModelIndex(); +} + +void AnnotationMatchModel::setMatches(const std::vector &matches, const QString &term) +{ + beginResetModel(); + _matches = matches; + _term = term; + endResetModel(); +} + +QVariant AnnotationMatchModel::data(const QModelIndex &index, int role) const +{ + const QModelIndex source = sourceIndex(index.row()); + if (!index.isValid() || !source.isValid()) return QVariant(); + if (role == Qt::DisplayRole && index.column() == 0) + return _source->headerData(source.column(), Qt::Horizontal, role); + if (role == Qt::DisplayRole) { + const auto labels = source.data(Qt::ToolTipRole).toString().split('\n'); + for (const auto &label : labels) + if (label.contains(_term, Qt::CaseInsensitive)) return label; + } + return source.data(role); +} + +QVariant AnnotationMatchModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) return QVariant(); + if (orientation == Qt::Vertical) return section + 1; + return section == 0 ? tr("Annotation") : tr("Match"); +} + DecoderModel::DecoderModel(QObject *parent) : QAbstractTableModel(parent), _decoder_stack(NULL) @@ -68,17 +120,51 @@ QVariant DecoderModel::data(const QModelIndex &index, int role) const if (role == Qt::TextAlignmentRole) { return int(Qt::AlignLeft | Qt::AlignVCenter); } - else if (role == Qt::DisplayRole) { + else if (role == Qt::DisplayRole || role == Qt::ToolTipRole) { if (_decoder_stack) { pv::data::decode::Annotation ann; if (_decoder_stack->list_annotation(&ann, index.column(), index.row())) { - return ann.annotations().at(0); + const auto &labels = ann.annotations(); + if (role == Qt::DisplayRole) + return labels.empty() ? QVariant() : QVariant(labels.front()); + QStringList text; + for (const auto &label : labels) + if (!text.contains(label)) text.append(label); + return text.join("\n"); } } } return QVariant(); } +bool DecoderModel::annotation_matches(int column, int row, const QStringList &terms) const +{ + if (!_decoder_stack || terms.isEmpty() || terms.first().isEmpty()) + return false; + decode::Annotation ann; + if (!_decoder_stack->list_annotation(&ann, column, row) || + !ann.contains_text(terms.first())) + return false; + + if (terms.size() == 1) + return true; + + // Retain the numeric AA-BB-CC sequence search in each individual column. + // Text containing a hyphen is handled as one literal term by the caller. + if (!ann.is_numberic()) + return false; + uint64_t next_row = uint64_t(row) + 1; + for (int term = 1; term < terms.size(); ++term) { + do { + if (!_decoder_stack->list_annotation(&ann, column, next_row++)) + return false; + } while (!ann.is_numberic()); + if (!ann.contains_text(terms.at(term))) + return false; + } + return true; +} + QVariant DecoderModel::headerData(int section, Qt::Orientation orientation, int role) const diff --git a/DSView/pv/data/decodermodel.h b/DSView/pv/data/decodermodel.h index d35849f4..f2f15887 100644 --- a/DSView/pv/data/decodermodel.h +++ b/DSView/pv/data/decodermodel.h @@ -23,6 +23,8 @@ #define DSVIEW_PV_DATA_DECODERMODEL_H #include +#include +#include #include "decode/rowdata.h" @@ -31,6 +33,24 @@ namespace data { class DecoderStack; +// One matched annotation per row; columns in DecoderModel have independent +// timelines, so filtering its row numbers would also expose unrelated events. +class AnnotationMatchModel : public QAbstractTableModel +{ +public: + AnnotationMatchModel(QAbstractItemModel *source, QObject *parent); + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + void setMatches(const std::vector &matches, const QString &term); + QModelIndex sourceIndex(int row) const; +private: + QPointer _source; + std::vector _matches; + QString _term; +}; + namespace decode { class Annotation; class Decoder; @@ -47,6 +67,8 @@ public: QVariant data(const QModelIndex &index, int role) const; QVariant headerData(int section, Qt::Orientation orientation,int role) const; + bool annotation_matches(int column, int row, const QStringList &terms) const; + void setDecoderStack(DecoderStack *decoder_stack); inline DecoderStack* getDecoderStack(){ diff --git a/DSView/pv/data/decoderstack.cpp b/DSView/pv/data/decoderstack.cpp index cb6b0b9a..ca46182f 100644 --- a/DSView/pv/data/decoderstack.cpp +++ b/DSView/pv/data/decoderstack.cpp @@ -168,8 +168,7 @@ void DecoderStack::build_row() std::map::const_iterator iter = _rows_gshow.find(row); if (iter == _rows_gshow.end()) { _rows_gshow[row] = true; - if (row.title().contains("bit", Qt::CaseInsensitive) || - row.title().contains("warning", Qt::CaseInsensitive)) { + if (row.title().contains("bit", Qt::CaseInsensitive)) { _rows_lshow[row] = false; } else { _rows_lshow[row] = true; @@ -192,8 +191,7 @@ void DecoderStack::build_row() std::map::const_iterator iter = _rows_gshow.find(row); if (iter == _rows_gshow.end()) { _rows_gshow[row] = true; - if (row.title().contains("bit", Qt::CaseInsensitive) || - row.title().contains("warning", Qt::CaseInsensitive)) { + if (row.title().contains("bit", Qt::CaseInsensitive)) { _rows_lshow[row] = false; } else { _rows_lshow[row] = true; diff --git a/DSView/pv/dialogs/deviceoptions.cpp b/DSView/pv/dialogs/deviceoptions.cpp index 7d4ae1c4..efe19693 100644 --- a/DSView/pv/dialogs/deviceoptions.cpp +++ b/DSView/pv/dialogs/deviceoptions.cpp @@ -392,18 +392,15 @@ void DeviceOptions::logic_probes(QVBoxLayout &layout) QPushButton *enable_all_probes = new QPushButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_ENABLE_ALL), "Enable All")); QPushButton *disable_all_probes = new QPushButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_DISABLE_ALL), "Disable All")); - enable_all_probes->setMaximumHeight(33); - disable_all_probes->setMaximumHeight(33); + // Let Qt include the full label and themed padding in the preferred size. + enable_all_probes->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + disable_all_probes->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); enable_all_probes->setFont(font); - disable_all_probes->setFont(font); - - int bt_width = enable_all_probes->fontMetrics().horizontalAdvance(enable_all_probes->text()) + 20; - enable_all_probes->setMaximumWidth(bt_width); - disable_all_probes->setMaximumWidth(bt_width); - - this->update_font(); - - contentHeight += enable_all_probes->sizeHint().height(); + disable_all_probes->setFont(font); + + this->update_font(); + + contentHeight += qMax(enable_all_probes->sizeHint().height(), disable_all_probes->sizeHint().height()); contentHeight += channel_line_height * row2 + 50; connect(enable_all_probes, SIGNAL(clicked()), diff --git a/DSView/pv/dock/keywordlineedit.cpp b/DSView/pv/dock/keywordlineedit.cpp index 2c2e4722..9a33e11a 100644 --- a/DSView/pv/dock/keywordlineedit.cpp +++ b/DSView/pv/dock/keywordlineedit.cpp @@ -353,6 +353,10 @@ void PopupLineEdit::showPupopInput() _popup_input = input; connect(input, SIGNAL(sig_inputEnd(QString)), this, SLOT(onPopupInputEditEnd(QString))); + // The popup owns keyboard input on Windows. Forward Enter after it commits + // the text and closes, so actions connected to the outer editor still run. + connect(line, &QLineEdit::returnPressed, this, &QLineEdit::returnPressed, + Qt::QueuedConnection); if (_is_number_mode){ connect(line, SIGNAL(valueChanged(int)), this, SLOT(onPopupInputValueChanged(int))); diff --git a/DSView/pv/dock/protocoldock.cpp b/DSView/pv/dock/protocoldock.cpp index 648eb5b8..1a023707 100644 --- a/DSView/pv/dock/protocoldock.cpp +++ b/DSView/pv/dock/protocoldock.cpp @@ -37,14 +37,13 @@ #include #include #include -#include -#include -#include +#include #include #include #include #include #include +#include #include #include #include "../ui/msgbox.h" @@ -56,6 +55,11 @@ #include "../ui/langresource.h" #include "../appcontrol.h" #include "../ui/fn.h" +#include "../dialogs/decoderoptionsdlg.h" +#include "../dialogs/dsmessagebox.h" +#include +#include +#include using namespace std; @@ -69,9 +73,10 @@ ProtocolDock::ProtocolDock(QWidget *parent, view::View &view, SigSession *sessio _view(view) { _session = session; - _cur_search_index = -1; - _search_edited = false; _pro_add_button = NULL; + _search_timer = new QTimer(this); + _search_timer->setSingleShot(true); + connect(_search_timer, &QTimer::timeout, this, &ProtocolDock::search_step); //-----------------------------get protocol list GSList *l = const_cast(srd_decoder_list()); @@ -160,7 +165,6 @@ ProtocolDock::ProtocolDock(QWidget *parent, view::View &view, SigSession *sessio _ann_search_edit = new PopupLineEdit(bot_panel); _ann_search_button->setFixedWidth(_ann_search_button->height()); - _ann_search_button->setDisabled(true); QHBoxLayout *ann_search_layout = new QHBoxLayout(); ann_search_layout->setSpacing(2); @@ -170,6 +174,7 @@ ProtocolDock::ProtocolDock(QWidget *parent, view::View &view, SigSession *sessio ann_search_layout->addWidget(_nxt_button); _table_view = new QTableView(bot_panel); + _match_model = new data::AnnotationMatchModel(_session->get_decoder_model(), this); _table_view->setModel(_session->get_decoder_model()); _table_view->setAlternatingRowColors(true); _table_view->setShowGrid(false); @@ -191,12 +196,32 @@ ProtocolDock::ProtocolDock(QWidget *parent, view::View &view, SigSession *sessio bot_layout->addWidget(_table_view); bot_panel->setLayout(bot_layout); + // Keep decoder rows scrollable so their combined minimum height does not + // prevent the user from dragging the divider upwards. + auto top_scroll = new QScrollArea(this); + top_scroll->setFrameShape(QFrame::NoFrame); + top_scroll->setWidgetResizable(true); + top_scroll->setMinimumHeight(70); + top_scroll->setWidget(top_panel); + QSplitter *split_widget = new QSplitter(this); - split_widget->insertWidget(0, top_panel); + split_widget->insertWidget(0, top_scroll); split_widget->insertWidget(1, bot_panel); split_widget->setOrientation(Qt::Vertical); split_widget->setCollapsible(0, false); split_widget->setCollapsible(1, false); + split_widget->setHandleWidth(10); + split_widget->setStretchFactor(0, 0); + split_widget->setStretchFactor(1, 1); + split_widget->setSizes({180, 420}); + split_widget->handle(1)->setCursor(Qt::SizeVerCursor); + split_widget->handle(1)->setToolTip(tr("Drag up or down to resize Decoders and Decoding Results")); + if (qApp->property("embeddedHost").toBool()) { + split_widget->setStyleSheet( + "QSplitter#protocolWidget::handle:vertical { background: #243247; " + "border-top: 1px solid #344762; border-bottom: 1px solid #344762; }" + "QSplitter#protocolWidget::handle:vertical:hover { background: #1570d8; }"); + } this->setWidgetResizable(true); this->setWidget(split_widget); @@ -207,6 +232,7 @@ ProtocolDock::ProtocolDock(QWidget *parent, view::View &view, SigSession *sessio connect(_bot_set_button, SIGNAL(clicked()),this, SLOT(set_model())); connect(_pre_button, SIGNAL(clicked()),this, SLOT(search_pre())); connect(_nxt_button, SIGNAL(clicked()),this, SLOT(search_nxt())); + connect(_ann_search_button, SIGNAL(clicked()),this, SLOT(search_nxt())); connect(_pro_add_button, SIGNAL(clicked()),this, SLOT(on_add_protocol())); connect(_del_all_button, SIGNAL(clicked()),this, SLOT(on_del_all_protocol())); @@ -218,7 +244,12 @@ ProtocolDock::ProtocolDock(QWidget *parent, view::View &view, SigSession *sessio connect(_pro_search_button, SIGNAL(clicked()), this, SLOT(show_protocol_select())); - connect(_ann_search_edit, SIGNAL(editingFinished()), this, SLOT(search_changed())); + connect(_ann_search_edit, &QLineEdit::textChanged, this, &ProtocolDock::search_changed); + connect(_ann_search_edit, &QLineEdit::returnPressed, this, &ProtocolDock::search_nxt); + connect(_session->get_decoder_model(), &QAbstractItemModel::modelAboutToBeReset, + this, &ProtocolDock::invalidate_search); + connect(_session->get_decoder_model(), &QAbstractItemModel::modelReset, + this, &ProtocolDock::search_done); ADD_UI(this); @@ -241,6 +272,12 @@ ProtocolDock::~ProtocolDock() void ProtocolDock::retranslateUi() { _ann_search_edit->setPlaceholderText(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_SEARCH), "search")); + _ann_search_edit->setToolTip(tr("Search all enabled result columns, including short waveform labels.\n" + "Case insensitive. Enter or the magnifier: next match.\n" + "Numeric sequence: AA-BB-CC.")); + _ann_search_button->setToolTip(tr("Find next (Enter)")); + _pre_button->setToolTip(tr("Previous match")); + _nxt_button->setToolTip(tr("Next match")); _matchs_title_label->setText(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_MATCHING_ITEMS), "Matching Items:")); _bot_title_label->setText(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_PROTOCOL_LIST_VIEWER), "Protocol List Viewer")); _pro_keyword_edit->ResetText(); @@ -290,6 +327,89 @@ int ProtocolDock::get_protocol_index_by_id(QString id) return -1; } +bool ProtocolDock::add_host_decoder(const QString &id) +{ + if (_session->is_working() || _session->get_device()->get_work_mode() != LOGIC || + QApplication::activeModalWidget() || get_protocol_index_by_id(id) < 0) + return false; + _selected_protocol_id = id; + const size_t count = _protocol_lay_items.size(); + on_add_protocol(); + return _protocol_lay_items.size() > count; +} + +// Opt-in integration check used by SETGUI --check-dsview. Exercise the same +// modal dialogs and removal paths as the right-hand decoder panel. +void ProtocolDock::check_host_decoders() +{ + const QString report = QString::fromUtf8(qgetenv("DSVIEW_HOST_CHECK")); + if (report.isEmpty()) return; + auto log = [report](const QString &message) { + QFile file(report); + if (file.open(QIODevice::WriteOnly | QIODevice::Append)) + file.write((message + "\n").toUtf8()); + }; + auto timer = new QTimer(this); + int phase = 0; + connect(timer, &QTimer::timeout, this, [this, timer, phase, log, report]() mutable { + bool ok = true; + auto answer = [this](QMessageBox::StandardButton button) { + QTimer::singleShot(80, this, [button]() { + auto dialog = qobject_cast(QApplication::activeModalWidget()); + if (dialog && dialog->mBox()->button(button)) + dialog->mBox()->button(button)->click(); + }); + }; + if (phase < 4) { + const auto before = _protocol_lay_items.size(); + if (before == 0) ok = false; + else { + answer(phase % 2 ? QMessageBox::Yes : QMessageBox::No); + if (phase < 2) OnProtocolDelete(_protocol_lay_items.front()); + else on_del_all_protocol(); + const auto after = _protocol_lay_items.size(); + ok = phase % 2 == 0 ? after == before : after == (phase == 1 ? before - 1 : 0); + } + } else if (phase < 8) { + const QString id = QStringList({"gate_driver_timing", "set_uart", "set_can", "pm35_uart"})[phase - 4]; + if (get_protocol_index_by_id(id) < 0) ok = false; + else { + QTimer::singleShot(80, this, [report, id]() { + auto dialog = qobject_cast(QApplication::activeModalWidget()); + if (!dialog) return; + int channel = 1; + for (auto combo : dialog->findChildren()) { + if (combo->count() > 1 && combo->itemText(0) == "-" && combo->itemData(0).toInt() == -1) + combo->setCurrentIndex(qMin(channel++, combo->count() - 1)); + } + dialog->grab().save(report + "." + id + ".png"); + QMetaObject::invokeMethod(dialog, "on_accept", Qt::DirectConnection); + }); + ok = add_host_decoder(id); + if (ok) { + auto trace = _session->get_decode_signals().back(); + ok = QString(trace->decoder()->stack().back()->decoder()->id) == id; + } + } + } else if (phase == 8) { + answer(QMessageBox::Yes); + on_del_all_protocol(); + ok = _protocol_lay_items.empty() && _session->get_decode_signals().empty(); + } else { + log("PASS"); + timer->stop(); + timer->deleteLater(); + return; + } + log(QString("phase %1: %2").arg(phase++).arg(ok ? "ok" : "FAIL")); + if (!ok) { + timer->stop(); + timer->deleteLater(); + } + }); + timer->start(350); +} + void ProtocolDock::on_add_protocol() { if (_decoderInfoList.size() == 0){ @@ -471,7 +591,7 @@ void ProtocolDock::decoded_progress(int progress) index++; } - if (progress == 0 || progress % 10 == 1){ + if (progress == 0 || progress == 100 || progress % 10 == 1){ update_model(); } } @@ -481,8 +601,6 @@ void ProtocolDock::set_model() pv::dialogs::ProtocolList *protocollist_dlg = new pv::dialogs::ProtocolList(this, _session); protocollist_dlg->exec(); resize_table_view(_session->get_decoder_model()); - _model_proxy.setSourceModel(_session->get_decoder_model()); - search_done(); // clear mark_index of all DecoderStacks const auto &decode_sigs = _session->get_decode_signals(); @@ -513,15 +631,13 @@ void ProtocolDock::update_model() if (index >= decode_sigs.size()) decoder_model->setDecoderStack(decode_sigs.at(0)->decoder()); } - _model_proxy.setSourceModel(decoder_model); - search_done(); resize_table_view(decoder_model); } void ProtocolDock::resize_table_view(data::DecoderModel* decoder_model) { if (decoder_model->getDecoderStack()) { - for (int i = 0; i < decoder_model->columnCount(QModelIndex()) - 1; i++) { + for (int i = 0; i < _table_view->model()->columnCount(QModelIndex()) - 1; i++) { _table_view->resizeColumnToContents(i); if (_table_view->columnWidth(i) > 200) _table_view->setColumnWidth(i, 200); @@ -535,8 +651,11 @@ void ProtocolDock::resize_table_view(data::DecoderModel* decoder_model) } } -void ProtocolDock::item_clicked(const QModelIndex &index) +void ProtocolDock::item_clicked(const QModelIndex &table_index) { + const QModelIndex index = table_index.model() == _match_model + ? _match_model->sourceIndex(table_index.row()) : table_index; + if (!index.isValid()) return; pv::data::DecoderModel *decoder_model = _session->get_decoder_model(); auto decoder_stack = decoder_model->getDecoderStack(); @@ -555,41 +674,16 @@ void ProtocolDock::item_clicked(const QModelIndex &index) } } - _table_view->resizeRowToContents(index.row()); - if (index.column() != _model_proxy.filterKeyColumn()) { - _model_proxy.setFilterKeyColumn(index.column()); - _model_proxy.setSourceModel(decoder_model); - search_done(); - } - QModelIndex filterIndex = _model_proxy.mapFromSource(index); - if (filterIndex.isValid()) { - _cur_search_index = filterIndex.row(); - } else { - if (_model_proxy.rowCount() == 0) { - _cur_search_index = -1; - } else { - uint64_t up = 0; - uint64_t dn = _model_proxy.rowCount() - 1; - do { - uint64_t md = (up + dn)/2; - QModelIndex curIndex = _model_proxy.mapToSource(_model_proxy.index(md,_model_proxy.filterKeyColumn())); - if (index.row() == curIndex.row()) { - _cur_search_index = md; - break; - } else if (md == up) { - if (curIndex.row() < index.row() && up < dn) { - QModelIndex nxtIndex = _model_proxy.mapToSource(_model_proxy.index(md+1,_model_proxy.filterKeyColumn())); - if (nxtIndex.row() < index.row()) - md++; - } - _cur_search_index = md + ((curIndex.row() < index.row()) ? 0.5 : -0.5); - break; - } else if (curIndex.row() < index.row()) { - up = md; - } else if (curIndex.row() > index.row()) { - dn = md; - } - }while(1); + _table_view->resizeRowToContents(table_index.row()); + // A table click must not silently restrict subsequent searches to this column. + if (_search_index >= 0 && _search_index < int(_search_matches.size()) && + _search_matches[_search_index].index == index) + return; + _search_index = -1; + for (size_t i = 0; i < _search_matches.size(); ++i) { + if (_search_matches[i].index == index) { + _search_index = int(i); + break; } } } @@ -618,6 +712,21 @@ void ProtocolDock::export_table_view() void ProtocolDock::nav_table_view() { + if (_table_view->model() == _match_model) { + if (_search_running || _search_matches.empty()) return; + auto stack = _session->get_decoder_model()->getDecoderStack(); + if (!stack) return; + const uint64_t offset = _view.offset() * (stack->samplerate() * _view.scale()); + auto match = std::lower_bound(_search_matches.begin(), _search_matches.end(), offset, + [](const SearchMatch &item, uint64_t sample) { return item.sample < sample; }); + _search_index = match == _search_matches.end() ? int(_search_matches.size()) - 1 + : int(match - _search_matches.begin()); + const QModelIndex index = _match_model->index(_search_index, 1); + _table_view->setCurrentIndex(index); + _table_view->scrollTo(index); + item_clicked(index); + return; + } uint64_t row_index = 0; pv::data::DecoderModel *decoder_model = _session->get_decoder_model(); @@ -625,7 +734,8 @@ void ProtocolDock::nav_table_view() if (decoder_stack) { uint64_t offset = _view.offset() * (decoder_stack->samplerate() * _view.scale()); std::map rows = decoder_stack->get_rows_lshow(); - int column = _model_proxy.filterKeyColumn(); + const int selected_column = qMax(0, _table_view->currentIndex().column()); + int column = selected_column; for (std::map::const_iterator i = rows.begin(); i != rows.end(); i++) { if ((*i).second && column-- == 0) { @@ -633,7 +743,7 @@ void ProtocolDock::nav_table_view() break; } } - QModelIndex index = _model_proxy.mapToSource(_model_proxy.index(row_index, _model_proxy.filterKeyColumn())); + QModelIndex index = decoder_model->index(row_index, selected_column); if(index.isValid()){ @@ -660,196 +770,141 @@ void ProtocolDock::nav_table_view() void ProtocolDock::search_pre() { - search_update(); - // now the proxy only contains rows that match the name - // let's take the pre one and map it to the original model - if (_model_proxy.rowCount() == 0) { - _table_view->scrollToTop(); - _table_view->clearSelection(); - _matchs_label->setText(QString::number(0)); - _cur_search_index = -1; - return; - } - int i = 0; - uint64_t rowCount = _model_proxy.rowCount(); - QModelIndex matchingIndex; - pv::data::DecoderModel *decoder_model = _session->get_decoder_model(); - - auto decoder_stack = decoder_model->getDecoderStack(); - do { - _cur_search_index--; - if (_cur_search_index <= -1 || _cur_search_index >= _model_proxy.rowCount()) - _cur_search_index = _model_proxy.rowCount() - 1; - - matchingIndex = _model_proxy.mapToSource(_model_proxy.index(ceil(_cur_search_index),_model_proxy.filterKeyColumn())); - if (!decoder_stack || !matchingIndex.isValid()) - break; - i = 1; - uint64_t row = matchingIndex.row() + 1; - uint64_t col = matchingIndex.column(); - pv::data::decode::Annotation ann; - bool ann_valid = false; - - while(i < _str_list.size()) { - QString nxt = _str_list.at(i); - - do { - ann_valid = decoder_stack->list_annotation(&ann, col, row); - row++; - } - while(ann_valid && !ann.is_numberic()); - - if (ann_valid){ - QString source = ann.annotations().at(0); - if (source.contains(nxt)) - i++; - else - break; - } - else{ - break; - } - } - } - while(i < _str_list.size() && --rowCount); - - if(i >= _str_list.size() && matchingIndex.isValid()){ - _table_view->scrollTo(matchingIndex); - _table_view->setCurrentIndex(matchingIndex); - _table_view->clicked(matchingIndex); - } else { - _table_view->scrollToTop(); - _table_view->clearSelection(); - _matchs_label->setText(QString::number(0)); - _cur_search_index = -1; - } + navigate_search(-1); } void ProtocolDock::search_nxt() { - search_update(); - // now the proxy only contains rows that match the name - // let's take the pre one and map it to the original model - if (_model_proxy.rowCount() == 0) { - _table_view->scrollToTop(); - _table_view->clearSelection(); - _matchs_label->setText(QString::number(0)); - _cur_search_index = -1; + navigate_search(1); +} + +void ProtocolDock::navigate_search(int direction) +{ + if (_search_running) { + // Finish the current query first; repeated presses do not restart it. + _search_direction = direction; + _search_timer->start(0); return; } - - int i = 0; - uint64_t rowCount = _model_proxy.rowCount(); - QModelIndex matchingIndex; - pv::data::DecoderModel *decoder_model = _session->get_decoder_model(); - auto decoder_stack = decoder_model->getDecoderStack(); - - if (decoder_stack == NULL){ - dsv_err("decoder_stack is null"); + if (_search_matches.empty()) return; - } - - do { - _cur_search_index++; - if (_cur_search_index < 0 || _cur_search_index >= _model_proxy.rowCount()) - _cur_search_index = 0; - matchingIndex = _model_proxy.mapToSource(_model_proxy.index(floor(_cur_search_index),_model_proxy.filterKeyColumn())); - - if (!matchingIndex.isValid()) - break; - - i = 1; - uint64_t row = matchingIndex.row() + 1; - uint64_t col = matchingIndex.column(); - pv::data::decode::Annotation ann; - bool ann_valid = false; - - while(i < _str_list.size()) { - QString nxt = _str_list.at(i); + const int count = int(_search_matches.size()); + if (_search_index < 0) + _search_index = direction > 0 ? 0 : count - 1; + else + _search_index = (_search_index + direction + count) % count; + const QModelIndex index = _match_model->index(_search_index, 1); + _table_view->setCurrentIndex(index); + _table_view->scrollTo(index); + item_clicked(index); +} - do { - ann_valid = decoder_stack->list_annotation(&ann, col, row); - row++; - } - while(ann_valid && !ann.is_numberic()); - - if (ann_valid){ - QString source = ann.annotations().at(0); - if (source.contains(nxt)) - i++; - else - break; - } - else{ - break; - } - } - }while(i < _str_list.size() && --rowCount); - - if(i >= _str_list.size() && matchingIndex.isValid()){ - _table_view->scrollTo(matchingIndex); - _table_view->setCurrentIndex(matchingIndex); - _table_view->clicked(matchingIndex); - } else { - _table_view->scrollToTop(); - _table_view->clearSelection(); - _matchs_label->setText(QString::number(0)); - _cur_search_index = -1; - } +void ProtocolDock::invalidate_search() +{ + _search_timer->stop(); + _search_running = false; + _search_direction = 0; + _search_index = -1; + _search_matches.clear(); + _match_model->setMatches({}, QString()); + _search_column_sizes.clear(); + _search_row = 0; + _search_column = 0; + _matchs_label->setText(QString::number(0)); } void ProtocolDock::search_done() { - QString str = _ann_search_edit->text().trimmed(); - QRegularExpression rx("(-)"); - _str_list = str.split(rx); - _model_proxy.setFilterFixedString(_str_list.first()); - if (_str_list.size() > 1) - _matchs_label->setText("..."); - else - _matchs_label->setText(QString::number(_model_proxy.rowCount())); + const int pending_direction = _search_direction; + invalidate_search(); + const QString text = _ann_search_edit->text().trimmed(); + auto decoder_model = _session->get_decoder_model(); + QAbstractItemModel *table_model = text.isEmpty() + ? static_cast(decoder_model) : _match_model; + if (_table_view->model() != table_model) + _table_view->setModel(table_model); + auto decoder_stack = decoder_model->getDecoderStack(); + if (text.isEmpty() || !decoder_stack) + return; + + // Only numeric sequences use '-'. Labels like CAN-FD and negative numbers + // must remain literal text. Always match every waveform label variant. + static const QRegularExpression sequence( + "^[0-9A-Fa-f]+(?:\\s*-\\s*[0-9A-Fa-f]+)+$"); + _search_terms = sequence.match(text).hasMatch() + ? text.split(QRegularExpression("\\s*-\\s*")) : QStringList{text}; + for (int col = 0; col < decoder_model->columnCount(QModelIndex()); ++col) + _search_column_sizes.push_back(int(qMin( + decoder_stack->list_annotation_size(col), std::numeric_limits::max()))); + _search_running = true; + _search_direction = pending_direction; + _matchs_label->setText("..."); + _search_timer->start(0); } void ProtocolDock::search_changed() { - _search_edited = true; - _matchs_label->setText("..."); + // A new query cancels any pending navigation for the old text. + _search_direction = 0; + search_done(); + if (_search_running) + _search_timer->start(180); } -void ProtocolDock::search_update() +void ProtocolDock::search_step() { - if (!_search_edited) + if (!_search_running) return; - - pv::data::DecoderModel *decoder_model = _session->get_decoder_model(); - + auto decoder_model = _session->get_decoder_model(); auto decoder_stack = decoder_model->getDecoderStack(); - if (!decoder_stack) + if (!decoder_stack) { + invalidate_search(); return; + } - if (decoder_stack->list_annotation_size(_model_proxy.filterKeyColumn()) > ProgressRows) { - QFuture future; - future = QtConcurrent::run([&]{ - search_done(); - }); - Qt::WindowFlags flags = Qt::CustomizeWindowHint; - QProgressDialog dlg(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_SEARCHING), "Searching..."), - L_S(STR_PAGE_DLG, S_ID(IDS_DLG_CANCEL), "Cancel"),0,0,this,flags); - dlg.setWindowModality(Qt::WindowModal); - dlg.setWindowFlags(Qt::Dialog | Qt::FramelessWindowHint | Qt::WindowSystemMenuHint | - Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint); - dlg.setCancelButton(NULL); - - QFutureWatcher watcher; - connect(&watcher,SIGNAL(finished()),&dlg,SLOT(cancel())); - watcher.setFuture(future); - - dlg.exec(); - } else { - search_done(); + // Keep Qt models and widgets on the GUI thread, yielding between chunks. + // Model resets stop this timer and discard indices before a decoder dies. + QElapsedTimer elapsed; + elapsed.start(); + while (_search_column < int(_search_column_sizes.size())) { + if (_search_row >= _search_column_sizes[_search_column]) { + ++_search_column; + _search_row = 0; + continue; + } + const int row = _search_row++; + if (decoder_model->annotation_matches(_search_column, row, _search_terms)) { + pv::data::decode::Annotation ann; + if (decoder_stack->list_annotation(&ann, _search_column, row)) + _search_matches.push_back({decoder_model->index(row, _search_column), + ann.start_sample()}); + } + if (elapsed.elapsed() >= 8) { + _matchs_label->setText(QString("%1...").arg(qulonglong(_search_matches.size()))); + _search_timer->start(1); + return; + } } - _search_edited = false; + + // Result columns have independent row numbers. Navigate in time order. + std::sort(_search_matches.begin(), _search_matches.end(), + [](const SearchMatch &a, const SearchMatch &b) { + if (a.sample != b.sample) return a.sample < b.sample; + if (a.index.column() != b.index.column()) return a.index.column() < b.index.column(); + return a.index.row() < b.index.row(); + }); + _search_running = false; + std::vector matches; + matches.reserve(_search_matches.size()); + for (const auto &match : _search_matches) matches.push_back(match.index); + _match_model->setMatches(matches, _search_terms.first()); + resize_table_view(decoder_model); + _matchs_label->setText(QString::number(qulonglong(_search_matches.size()))); + const int direction = _search_direction; + _search_direction = 0; + if (direction) + navigate_search(direction); } //-------------------IProtocolItemLayerCallback @@ -1100,7 +1155,8 @@ void ProtocolDock::UpdateFont() QRect rc = fm.boundingRect(str); int lineHeight = rc.height() + 15; - _pro_keyword_edit->setFixedHeight(rc.height() + 5); + // Include the line edit's styled padding and border so glyphs are not clipped. + _pro_keyword_edit->setFixedHeight(qMax(rc.height() + 5, _pro_keyword_edit->sizeHint().height())); int pannelHeight = lineHeight * _protocol_lay_items.size() + _pro_keyword_edit->height(); if (pannelHeight < 100){ diff --git a/DSView/pv/dock/protocoldock.h b/DSView/pv/dock/protocoldock.h index 0da4c4cc..2bcceeb8 100644 --- a/DSView/pv/dock/protocoldock.h +++ b/DSView/pv/dock/protocoldock.h @@ -33,9 +33,8 @@ #include #include #include -#include +#include #include -#include #include #include "../data/decodermodel.h" #include "protocolitemlayer.h" @@ -75,14 +74,13 @@ public IUiWindow { Q_OBJECT -public: - static const uint64_t ProgressRows = 100000; - public: ProtocolDock(QWidget *parent, view::View &view, SigSession *session); ~ProtocolDock(); void del_all_protocol(); + bool add_host_decoder(const QString &id); + void check_host_decoders(); bool add_protocol_by_id(QString id, bool silent, std::list &sub_decoders); void reset_view(); @@ -119,6 +117,8 @@ private: void UpdateFont() override; void adjustPannelSize(); + void invalidate_search(); + void navigate_search(int direction); signals: void protocol_updated(); @@ -139,18 +139,29 @@ private slots: void search_nxt(); void search_done(); void search_changed(); - void search_update(); + void search_step(); void show_protocol_select(); private: SigSession *_session; view::View &_view; - QSortFilterProxyModel _model_proxy; - int _cur_search_index; - QStringList _str_list; + struct SearchMatch { + QModelIndex index; + uint64_t sample; + }; + std::vector _search_matches; + std::vector _search_column_sizes; + QTimer *_search_timer; + QStringList _search_terms; + int _search_row = 0; + int _search_column = 0; + int _search_index = -1; + int _search_direction = 0; + bool _search_running = false; QWidget *_top_panel; QTableView *_table_view; + data::AnnotationMatchModel *_match_model; QPushButton *_pre_button; QPushButton *_nxt_button; PopupLineEdit *_ann_search_edit; @@ -172,8 +183,6 @@ private: QString _selected_protocol_id; XToolButton *_pro_search_button; - mutable std::mutex _search_mutex; - bool _search_edited; }; } // namespace dock diff --git a/DSView/pv/mainframe.cpp b/DSView/pv/mainframe.cpp index 9f92d42b..f493c90e 100644 --- a/DSView/pv/mainframe.cpp +++ b/DSView/pv/mainframe.cpp @@ -44,7 +44,8 @@ #include #include #include -#include +#include +#include #include #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) @@ -97,7 +98,8 @@ MainFrame::MainFrame() #ifdef _WIN32 setWindowFlags(Qt::FramelessWindowHint); _is_win32_parent_window = true; - _taskBtn = NULL; + _taskBtn = NULL; + _taskPrg = NULL; isWin32 = true; #else setWindowFlags(Qt::Window | Qt::FramelessWindowHint | Qt::WindowSystemMenuHint); @@ -297,7 +299,7 @@ void MainFrame::resizeEvent(QResizeEvent *event) void MainFrame::closeEvent(QCloseEvent *event) { - writeSettings(); + if (!qApp->property("embeddedHost").toBool()) writeSettings(); if (_mainWindow->able_to_close()){ @@ -309,7 +311,8 @@ void MainFrame::closeEvent(QCloseEvent *event) } #endif - event->accept(); + event->accept(); + if (qApp->property("embeddedHost").toBool()) qApp->quit(); } else{ event->ignore(); @@ -645,7 +648,7 @@ void MainFrame::writeSettings() dsv_info("Save form, x:%d, y:%d, w:%d, h:%d", x, y, w, h); } -void MainFrame::ShowFormInit() +void MainFrame::ShowFormInit() { ReadSettings(); @@ -697,9 +700,80 @@ void MainFrame::ShowFormInit() AttachNativeWindow(); } #endif -} - -void MainFrame::AttachNativeWindow() +} + +#ifdef _WIN32 +bool MainFrame::ShowEmbedded(quintptr parentId) +{ + const HWND parent = reinterpret_cast(parentId); + DWORD parentPid = 0; + GetWindowThreadProcessId(parent, &parentPid); + if (!parentPid || !IsWindow(parent)) return false; + _titleBar->hide(); + _titleBar->EnableAbleDrag(false); + setMinimumSize(0, 0); + _mainWindow->setMinimumSize(0, 0); + setWindowFlags(Qt::FramelessWindowHint | Qt::Tool); + const HWND child = reinterpret_cast(winId()); + SetWindowLongPtr(child, GWL_STYLE, WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS); + SetWindowLongPtr(child, GWL_EXSTYLE, GetWindowLongPtr(child, GWL_EXSTYLE) & ~WS_EX_APPWINDOW); + SetLastError(0); + SetParent(child, parent); + if (GetLastError() != 0 || GetParent(child) != parent) return false; + _mainWindow->restore_dock(); + PopupDlgList::SetCurrentScreen(QGuiApplication::primaryScreen()); + QByteArray commandBuffer; + auto fit = [this, parent, child, parentPid, commandBuffer]() mutable { + DWORD currentPid = 0; + GetWindowThreadProcessId(parent, ¤tPid); + if (!IsWindow(parent) || currentPid != parentPid) { + qApp->quit(); + return; + } + DWORD available = 0; + const HANDLE input = GetStdHandle(STD_INPUT_HANDLE); + if (PeekNamedPipe(input, NULL, 0, NULL, &available, NULL) && available) { + char data[256]; + DWORD count = 0; + if (ReadFile(input, data, qMin(available, sizeof(data)), &count, NULL)) { + commandBuffer.append(data, count); + int newline; + while ((newline = commandBuffer.indexOf('\n')) >= 0) { + const QByteArray command = commandBuffer.left(newline).trimmed(); + commandBuffer.remove(0, newline + 1); + if (command == "CLOSE") close(); + else if (command.startsWith("ADD_DECODER ")) + _mainWindow->addHostDecoder(QString::fromUtf8(command.mid(12))); + else if (command.startsWith("OPEN_FILE ")) { + const QString path = QString::fromUtf8(QByteArray::fromBase64(command.mid(10))); + printf("DSVIEW_FILE_RESULT %s\n", _mainWindow->openHostFile(path)); + fflush(stdout); + } + } + if (commandBuffer.size() > 262144) commandBuffer.clear(); + } + } + RECT rect, childRect; + GetClientRect(parent, &rect); + GetWindowRect(child, &childRect); + const int width = rect.right - rect.left; + const int height = rect.bottom - rect.top; + if (width > 0 && height > 0 && + (childRect.right - childRect.left != width || childRect.bottom - childRect.top != height)) + ::MoveWindow(child, 0, 0, width, height, TRUE); + }; + QTimer *hostTimer = new QTimer(this); + connect(hostTimer, &QTimer::timeout, this, fit); + hostTimer->start(50); + fit(); + QFrame::show(); + if (!qgetenv("DSVIEW_HOST_CHECK").isEmpty()) + QTimer::singleShot(3500, _mainWindow, &MainWindow::checkHostDecoders); + return true; +} +#endif + +void MainFrame::AttachNativeWindow() { #ifdef _WIN32 @@ -1022,7 +1096,7 @@ void MainFrame::ReadSettings() void MainFrame::showEvent(QShowEvent *event) { // Taskbar Progress Effert for Win7 and Above - if (_taskBtn && _taskBtn->window() == NULL) { + if (!qApp->property("embeddedHost").toBool() && _taskBtn && _taskBtn->window() == NULL) { _taskBtn->setWindow(windowHandle()); _taskPrg = _taskBtn->progress(); } @@ -1032,8 +1106,9 @@ void MainFrame::showEvent(QShowEvent *event) void MainFrame::setTaskbarProgress(int progress) { -#ifdef _WIN32 - if (progress > 0) { +#ifdef _WIN32 + if (!_taskPrg) return; + if (progress > 0) { _taskPrg->setVisible(true); _taskPrg->setValue(progress); } else { @@ -1107,8 +1182,14 @@ QWidget* MainFrame::GetBodyView() } #ifdef _WIN32 -bool MainFrame::nativeEvent(const QByteArray &eventType, void *message, MESSAGE_RESULT_PTR result) -{ +bool MainFrame::nativeEvent(const QByteArray &eventType, void *message, MESSAGE_RESULT_PTR result) +{ + if (qApp->property("embeddedHost").toBool() && + static_cast(message)->message == WM_CLOSE) { + QTimer::singleShot(0, this, [this]() { close(); }); + *result = 0; + return true; + } if (_parentNativeWidget != NULL) { MSG *msg = static_cast(message); diff --git a/DSView/pv/mainframe.h b/DSView/pv/mainframe.h index 0c2ec3ee..76f91faa 100644 --- a/DSView/pv/mainframe.h +++ b/DSView/pv/mainframe.h @@ -99,7 +99,10 @@ public: public: MainFrame(); - void ShowFormInit(); + void ShowFormInit(); +#ifdef _WIN32 + bool ShowEmbedded(quintptr parentId); +#endif void ShowHelpDocAsync(); bool IsMaxsized(); diff --git a/DSView/pv/mainwindow.cpp b/DSView/pv/mainwindow.cpp index 74dc348b..c6800415 100644 --- a/DSView/pv/mainwindow.cpp +++ b/DSView/pv/mainwindow.cpp @@ -22,12 +22,16 @@ #include #include -#include +#include +#include #include #include #include #include -#include +#include +#include +#include +#include #include #include #include @@ -108,8 +112,60 @@ namespace pv { - namespace{ - QString tmp_file; + namespace{ + QString tmp_file; + + // Keep the instrument controls above the waveform, rather than above + // the right-hand docks. Narrow hosts scroll instead of wrapping Help. + class EmbeddedToolbars : public QScrollArea { + public: + EmbeddedToolbars(const QList &bars, QWidget *parent) + : QScrollArea(parent), _bars(bars) + { + setFrameShape(QFrame::NoFrame); + setWidgetResizable(true); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); + auto panel = new QWidget(this); + auto layout = new QHBoxLayout(panel); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + layout->setSizeConstraint(QLayout::SetMinAndMaxSize); + for (auto bar : _bars) { + layout->addWidget(bar); + bar->installEventFilter(this); + } + layout->addStretch(1); + setWidget(panel); + panel->installEventFilter(this); + updateSize(); + } + protected: + bool eventFilter(QObject *object, QEvent *event) override { + if (event->type() == QEvent::LayoutRequest) + updateSize(); + return QScrollArea::eventFilter(object, event); + } + void resizeEvent(QResizeEvent *event) override { + QScrollArea::resizeEvent(event); + updateSize(); + } + private: + void updateSize() { + int height = 0; + int width = 0; + for (auto bar : _bars) { + const QSize hint = bar->sizeHint(); + bar->setMinimumSize(hint); + width += hint.width(); + height = qMax(height, hint.height()); + } + if (width > viewport()->width()) + height += horizontalScrollBar()->sizeHint().height(); + setFixedHeight(height); + } + QList _bars; + }; } MainWindow::MainWindow(toolbars::TitleBar *title_bar, QWidget *parent) @@ -189,10 +245,15 @@ namespace pv setIconSize(QSize(40, 40)); - addToolBar(_sampling_bar); - addToolBar(_trig_bar); - addToolBar(_file_bar); - addToolBar(_logo_bar); + if (qApp->property("embeddedHost").toBool()) { + _vertical_layout->insertWidget(0, new EmbeddedToolbars( + {_sampling_bar, _trig_bar, _file_bar, _logo_bar}, _central_widget)); + } else { + addToolBar(_sampling_bar); + addToolBar(_trig_bar); + addToolBar(_file_bar); + addToolBar(_logo_bar); + } // Setup the dockWidget _protocol_dock = new QDockWidget(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_PROTOCOL_DOCK_TITLE), "Decode Protocol"), this); @@ -200,8 +261,43 @@ namespace pv _protocol_dock->setFeatures(QDockWidget::DockWidgetMovable); _protocol_dock->setAllowedAreas(Qt::RightDockWidgetArea); _protocol_dock->setVisible(false); - _protocol_widget = new dock::ProtocolDock(_protocol_dock, *_view, _session); - _protocol_dock->setWidget(_protocol_widget); + _protocol_widget = new dock::ProtocolDock(_protocol_dock, *_view, _session); + if (qApp->property("embeddedHost").toBool()) { + // The dock occupies the right column, but its visible panel can be + // shortened from the top. Move the title with the whole panel. + _protocol_dock->setTitleBarWidget(new QWidget(_protocol_dock)); + _protocol_dock->setFeatures(QDockWidget::NoDockWidgetFeatures); + auto outer = new QSplitter(Qt::Vertical, _protocol_dock); + outer->setObjectName("decoderOuterSplit"); + auto space = new QWidget(outer); + space->setMinimumHeight(0); + auto panel = new QWidget(outer); + auto panel_layout = new QVBoxLayout(panel); + panel_layout->setContentsMargins(0, 0, 0, 0); + panel_layout->setSpacing(0); + auto title = new QLabel(_protocol_dock->windowTitle(), panel); + title->setStyleSheet("background: #151d28; padding: 5px 10px;"); + connect(_protocol_dock, &QWidget::windowTitleChanged, title, &QLabel::setText); + panel_layout->addWidget(title); + panel_layout->addWidget(_protocol_widget, 1); + outer->addWidget(space); + outer->addWidget(panel); + outer->setCollapsible(0, true); + outer->setCollapsible(1, false); + outer->setHandleWidth(10); + outer->setStretchFactor(0, 0); + outer->setStretchFactor(1, 1); + outer->setSizes({0, 600}); + outer->handle(1)->setCursor(Qt::SizeVerCursor); + outer->handle(1)->setToolTip(tr("Drag the top edge to resize the entire Decoders panel")); + outer->setStyleSheet( + "QSplitter#decoderOuterSplit::handle:vertical { background: #243247; " + "border-top: 1px solid #344762; border-bottom: 1px solid #344762; }" + "QSplitter#decoderOuterSplit::handle:vertical:hover { background: #1570d8; }"); + _protocol_dock->setWidget(outer); + } else { + _protocol_dock->setWidget(_protocol_widget); + } _session->set_decoder_pannel(_protocol_widget); @@ -479,7 +575,38 @@ namespace pv return true; } - void MainWindow::on_protocol(bool visible) + const char *MainWindow::openHostFile(const QString &path) + { + if (_session->is_working() || _session->is_saving() || QApplication::activeModalWidget()) + return "busy"; + if (!QFileInfo(path).isFile() || QFileInfo(path).suffix().compare("dsl", Qt::CaseInsensitive)) + return "error"; + if (confirm_to_store_data()) { + on_save(); + return "save"; + } + if (_session->is_working() || _session->is_saving()) return "busy"; + if (_device_agent->is_hardware()) save_config(); + try { + return _session->set_file(path) ? "ok" : "error"; + } catch (...) { + return "error"; + } + } + + void MainWindow::addHostDecoder(const QString &id) + { + on_protocol(true); + _protocol_widget->add_host_decoder(id); + } + + void MainWindow::checkHostDecoders() + { + on_protocol(true); + _protocol_widget->check_host_decoders(); + } + + void MainWindow::on_protocol(bool visible) { _protocol_dock->setVisible(visible); @@ -1414,8 +1541,9 @@ namespace pv _session->update_lang_text(); } - void MainWindow::switchTheme(QString style) - { + void MainWindow::switchTheme(QString style) + { + if (qApp->property("embeddedHost").toBool()) style = "dark"; AppConfig &app = AppConfig::Instance(); if (app.frameOptions.style != style) @@ -1427,7 +1555,25 @@ namespace pv QString qssRes = ":/" + style + ".qss"; QFile qss(qssRes); qss.open(QFile::ReadOnly | QFile::Text); - qApp->setStyleSheet(qss.readAll()); + QString stylesheet = QString::fromUtf8(qss.readAll()); + if (qApp->property("embeddedHost").toBool()) { + QFile hostStyle(QString::fromUtf8(qgetenv("DSVIEW_HOST_STYLE"))); + if (hostStyle.open(QFile::ReadOnly | QFile::Text)) + stylesheet += QString::fromUtf8(hostStyle.readAll()); + QPalette palette; + palette.setColor(QPalette::Window, QColor("#10151d")); + palette.setColor(QPalette::Base, QColor("#111c29")); + palette.setColor(QPalette::AlternateBase, QColor("#151d28")); + palette.setColor(QPalette::WindowText, QColor("#dce6f2")); + palette.setColor(QPalette::Text, QColor("#dce6f2")); + palette.setColor(QPalette::Button, QColor("#243247")); + palette.setColor(QPalette::ButtonText, QColor("#dce6f2")); + palette.setColor(QPalette::Highlight, QColor("#1570d8")); + palette.setColor(QPalette::HighlightedText, Qt::white); + qApp->setPalette(palette); + qApp->setFont(QFont("Segoe UI", 10)); + } + qApp->setStyleSheet(stylesheet); qss.close(); UiManager::Instance()->Update(UI_UPDATE_ACTION_THEME); diff --git a/DSView/pv/mainwindow.h b/DSView/pv/mainwindow.h index a43e0427..8212fe14 100644 --- a/DSView/pv/mainwindow.h +++ b/DSView/pv/mainwindow.h @@ -93,7 +93,10 @@ public: public: explicit MainWindow(toolbars::TitleBar *title_bar, QWidget *parent = 0); - void openDoc(); + void openDoc(); + void addHostDecoder(const QString &id); + const char *openHostFile(const QString &path); + void checkHostDecoders(); public slots: void switchTheme(QString style); diff --git a/DSView/pv/toolbars/filebar.cpp b/DSView/pv/toolbars/filebar.cpp index f562431c..792c6971 100644 --- a/DSView/pv/toolbars/filebar.cpp +++ b/DSView/pv/toolbars/filebar.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include "filebar.h" #include "../ui/msgbox.h" @@ -146,7 +147,9 @@ void FileBar::on_actionOpen_triggered() this, L_S(STR_PAGE_DLG, S_ID(IDS_DLG_OPEN_FILE), "Open File"), app.userHistory.openDir, - "DSView Data (*.dsl)"); + qApp->property("embeddedHost").toBool() + ? "Digital captures (*.dsl *.csv);;DSView Data (*.dsl);;CSV (*.csv)" + : "DSView Data (*.dsl)"); if (!file_name.isEmpty()) { QString fname = path::GetDirectoryName(file_name); @@ -155,7 +158,12 @@ void FileBar::on_actionOpen_triggered() app.SaveHistory(); } - sig_load_file(file_name); + if (qApp->property("embeddedHost").toBool() && file_name.endsWith(".csv", Qt::CaseInsensitive)) { + printf("DSVIEW_IMPORT_CSV %s\n", file_name.toUtf8().toBase64().constData()); + fflush(stdout); + } else { + sig_load_file(file_name); + } } } diff --git a/DSView/pv/toolbars/logobar.cpp b/DSView/pv/toolbars/logobar.cpp index 4b3b492c..95c28b49 100644 --- a/DSView/pv/toolbars/logobar.cpp +++ b/DSView/pv/toolbars/logobar.cpp @@ -106,13 +106,17 @@ LogoBar::LogoBar(SigSession *session, QWidget *parent) : _logo_button.setToolButtonStyle(Qt::ToolButtonTextUnderIcon); _logo_button.setPopupMode(QToolButton::InstantPopup); - QWidget *spacer = new QWidget(this); - spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - addWidget(spacer); + if (!qApp->property("embeddedHost").toBool()) { + QWidget *spacer = new QWidget(this); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + addWidget(spacer); + } addWidget(&_logo_button); - QWidget *margin = new QWidget(this); - margin->setMinimumWidth(20); - addWidget(margin); + if (!qApp->property("embeddedHost").toBool()) { + QWidget *margin = new QWidget(this); + margin->setMinimumWidth(20); + addWidget(margin); + } connect(_action_en, SIGNAL(triggered()), this, SLOT(on_actionEn_triggered())); connect(_action_cn, SIGNAL(triggered()), this, SLOT(on_actionCn_triggered()));