Files
templates/tools/dsview/setgui-host.patch

1687 lines
63 KiB
Diff

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} <SOURCE> <OBJECT>")
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<HWND>(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<unsigned long long>(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<QString>& 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<QString>& 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<QModelIndex> &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 <QAbstractTableModel>
+#include <QStringList>
+#include <QPointer>
#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<QModelIndex> &matches, const QString &term);
+ QModelIndex sourceIndex(int row) const;
+private:
+ QPointer<QAbstractItemModel> _source;
+ std::vector<QModelIndex> _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 decode::Row, bool>::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 decode::Row, bool>::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 <QHeaderView>
#include <QScrollBar>
#include <QRegularExpression>
-#include <QFuture>
-#include <QProgressDialog>
-#include <QtConcurrent/QtConcurrent>
+#include <QElapsedTimer>
#include <QSizePolicy>
#include <assert.h>
#include <map>
#include <string>
#include <algorithm>
+#include <limits>
#include <QTableWidgetItem>
#include <QHeaderView>
#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 <QApplication>
+#include <QFile>
+#include <QTimer>
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<GSList*>(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<dialogs::DSMessageBox*>(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<dialogs::DecoderOptionsDlg*>(QApplication::activeModalWidget());
+ if (!dialog) return;
+ int channel = 1;
+ for (auto combo : dialog->findChildren<QComboBox*>()) {
+ 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<const pv::data::decode::Row, bool> 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 pv::data::decode::Row, bool>::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<QAbstractItemModel*>(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<uint64_t>(
+ decoder_stack->list_annotation_size(col), std::numeric_limits<int>::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<void> 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<void> 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<QModelIndex> 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 <QScrollArea>
#include <QSplitter>
#include <QTableView>
-#include <QSortFilterProxyModel>
+#include <QTimer>
#include <vector>
-#include <mutex>
#include <list>
#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<pv::data::decode::Decoder*> &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<SearchMatch> _search_matches;
+ std::vector<int> _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 <QFile>
#include <QGuiApplication>
#include <QFont>
-#include <algorithm>
+#include <algorithm>
+#include <cstdio>
#include <QWindow>
#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<HWND>(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<HWND>(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, &currentPid);
+ 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<DWORD>(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<MSG*>(message)->message == WM_CLOSE) {
+ QTimer::singleShot(0, this, [this]() { close(); });
+ *result = 0;
+ return true;
+ }
if (_parentNativeWidget != NULL)
{
MSG *msg = static_cast<MSG*>(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 <QAction>
#include <QButtonGroup>
-#include <QFileDialog>
+#include <QFileDialog>
+#include <QFileInfo>
#include <QMessageBox>
#include <QMenu>
#include <QMenuBar>
#include <QStatusBar>
-#include <QVBoxLayout>
+#include <QVBoxLayout>
+#include <QScrollArea>
+#include <QScrollBar>
+#include <QSplitter>
#include <QWidget>
#include <QDesktopServices>
#include <QKeyEvent>
@@ -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<QToolBar*> &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<QToolBar*> _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 <QFileDialog>
#include <deque>
#include <QApplication>
+#include <cstdio>
#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()));