189 lines
25 KiB
Diff
189 lines
25 KiB
Diff
--- a/DSView/pv/mainwindow.cpp
|
||
+++ b/DSView/pv/mainwindow.cpp
|
||
@@ -84,6 +84,8 @@
|
||
#include "dock/protocoldock.h"
|
||
|
||
#include "view/view.h"
|
||
+#include "view/decoder_scope.h" // Нативная проверка открывает и проверяет отдельный осциллограф в реальном процессе DSView.
|
||
+#include "view/decodetrace.h" // Тест использует настоящий нижний анализатор и его контекстное действие без подмены данных.
|
||
#include "view/trace.h"
|
||
#include "view/signal.h"
|
||
#include "view/dsosignal.h"
|
||
@@ -652,6 +654,51 @@
|
||
|
||
void MainWindow::checkHostScope() // Проверяет добавление аналоговых дорожек отдельно от сценариев диалогов декодеров.
|
||
{ // Отдельная точка запуска не требует Ethernet-патча и доступна в базовой сборке DSView.
|
||
+ if (qEnvironmentVariableIsSet("DSVIEW_HOST_CHECK_DECODER_SCOPE")) { // Дополнительный сценарий выполняется только при явном запуске проверки и не влияет на пользовательскую сессию.
|
||
+ QTimer::singleShot(1500, _view, [this]() { // Пауза даёт трём анализаторам короткой тестовой записи завершить полный расчёт перед проверкой окна.
|
||
+ bool passed = !_session->get_decode_signals().empty(); // Без исходного анализатора положительный результат невозможен.
|
||
+ auto *trace = passed ? _session->get_decode_signals().back() : nullptr; // Тестовая запись заканчивается именно графиком токов Ia/Ib/Ic.
|
||
+ const double scale = _view->scale(); const int64_t offset = _view->offset(); // Независимое увеличение не должно менять область основной записи.
|
||
+ const size_t digital_count = _session->get_signals().size(); const size_t decoder_count = _session->get_decode_signals().size(); // Данные и соседние анализаторы сохраняются при открытии и закрытии дополнительного окна.
|
||
+ auto *stack = trace ? trace->decoder() : nullptr; const uint64_t results = stack ? stack->get_result_count() : 0; // Повторный анализ не требуется, поэтому число готовых результатов должно оставаться неизменным.
|
||
+ QString diagnostic = QString("target_y=%1 source_results=%2 running=%3").arg(trace ? trace->get_y() : -1).arg(results).arg(stack ? stack->IsRunning() : false); // Диагностика отличает ошибку выбора области от незавершённого исходного анализа.
|
||
+ auto mark = [&diagnostic, &passed](const char *stage) { if (!passed) diagnostic += QString(" failed_after=") + stage; }; // Первая неуспешная стадия остаётся видимой без повторного запуска отладочного сценария.
|
||
+ _view->get_time_view()->repaint(); _view->get_time_view()->grab().save(QString::fromUtf8(qgetenv("DSVIEW_HOST_CHECK")) + ".source.png"); // Перерисовывается настоящий временной Viewport, расположенный внутри общего QScrollArea::viewport.
|
||
+ auto find_scope = []() -> QWidget* { for (auto *widget : QApplication::topLevelWidgets()) if (widget->objectName() == "decoder_scope_window") return widget; return nullptr; }; // Проверка ищет настоящее отдельное окно, а не его миниатюру или область основного вида.
|
||
+ if (trace) { QMouseEvent click(QEvent::MouseButtonDblClick, QPointF(_view->get_view_width() / 2, trace->get_y() - trace->text_row_height() / 2), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(_view->get_time_view(), &click); } // Событие получает именно временной график, а не его контейнер QScrollArea; точка лежит выше строки предупреждений.
|
||
+ QPointer<QWidget> scope = find_scope(); // Слабая ссылка проверяет фактическое удаление при закрытии окна.
|
||
+ passed = passed && scope && scope->isWindow() && scope->property("scope_decoder").toString() == "pwm_motor" && scope->property("scope_rows").toStringList() == QStringList({"wave_a", "wave_b", "wave_c"}); // В отдельном окне должны оказаться ровно три фазных тока без соседних графиков.
|
||
+ mark("open_selected"); // Проверяется выбор нижнего графика до остальных взаимодействий.
|
||
+ if (scope) { // Последующие действия выполняются только при успешном создании настоящего окна.
|
||
+ scope->resize(1100, 640); scope->grab(); passed = passed && scope->property("scope_segments").toULongLong() > 0; mark("render"); // QWidget::grab принудительно рисует новое окно даже до первого события его экспозиции в Windows.
|
||
+ const double span = scope->property("scope_span").toDouble(); const QPointF point(600, 300); // Внутренняя точка гарантированно находится в области графика после изменения размера.
|
||
+ QWheelEvent wheel(point, scope->mapToGlobal(point.toPoint()), QPoint(), QPoint(0, 120), Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false); QApplication::sendEvent(scope, &wheel); // Один реальный шаг колеса увеличивает временной масштаб только отдельного осциллографа.
|
||
+ passed = passed && scope->property("scope_span").toDouble() < span && _view->scale() == scale && _view->offset() == offset; // Проверяется независимость времени от основного DSView.
|
||
+ mark("zoom"); // Уменьшение видимого интервала подтверждается собственными координатами отдельного окна.
|
||
+ const double begin = scope->property("scope_start").toDouble(); QMouseEvent press(QEvent::MouseButtonPress, point, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(scope, &press); // Начальная точка жеста принадлежит текущему увеличенному интервалу.
|
||
+ QMouseEvent move(QEvent::MouseMove, point - QPointF(100, 0), Qt::NoButton, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(scope, &move); // Перемещение влево должно показать более поздний участок той же записи.
|
||
+ QMouseEvent release(QEvent::MouseButtonRelease, point - QPointF(100, 0), Qt::LeftButton, Qt::NoButton, Qt::NoModifier); QApplication::sendEvent(scope, &release); passed = passed && scope->property("scope_start").toDouble() > begin; // Отпускание завершает жест после реального изменения временного положения.
|
||
+ mark("pan"); // Прокрутка должна менять начало собственного видимого интервала.
|
||
+ auto *fit = scope->findChild<QPushButton*>("scope_fit_time"); if (fit) fit->click(); passed = passed && fit && std::abs(scope->property("scope_span").toDouble() - double(stack->sample_count())) < 0.5; mark("fit_time"); // Полная запись сравнивается с числом исходных отсчётов, а не с округлённым начальным масштабом основного окна.
|
||
+ auto *range = scope->findChild<QDoubleSpinBox*>("scope_y_range"); if (range) range->setValue(50); passed = passed && range && range->value() == 50; // Ручная шкала ±50 А не меняет параметры расчётной модели двигателя.
|
||
+ auto *fit_y = scope->findChild<QPushButton*>("scope_fit_amplitude"); if (fit_y) fit_y->click(); passed = passed && fit_y && range && range->value() > 50; mark("fit_amplitude"); // Автомасштаб должен вместить пусковые токи около 80 А после ручного выбора ±50 А.
|
||
+ scope->repaint(); const QString screenshot = QString::fromUtf8(qgetenv("DSVIEW_HOST_CHECK")) + ".png"; passed = scope->grab().save(screenshot) && passed; // Снимок содержит только отдельный выбранный график и служит визуальным артефактом проверки.
|
||
+ passed = passed && stack->get_result_count() == results && _session->get_signals().size() == digital_count && _session->get_decode_signals().size() == decoder_count; // Все взаимодействия используют кэш и сохраняют исходную цифровую запись.
|
||
+ mark("cached_results"); // Открытие и перемещение графика не должны запускать повторное декодирование.
|
||
+ scope->close(); QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); passed = passed && scope.isNull(); // Закрытие дополнительного осциллографа должно действительно освобождать его объект.
|
||
+ mark("close"); // Исчезновение окна проверяется слабой ссылкой на настоящий QWidget.
|
||
+ trace->show_scope_menu(_view->mapToGlobal(QPoint(100, 100))); auto *menu = _view->findChild<QMenu*>("decoder_scope_menu"); // Повторное открытие проверяет альтернативный пользовательский путь через контекстное меню имени.
|
||
+ passed = passed && menu && menu->actions().size() == 1; if (menu && !menu->actions().empty()) { menu->actions().front()->trigger(); menu->close(); } // Нижний совмещённый график должен предлагаться одним действием A/B/C.
|
||
+ QPointer<QWidget> reopened = find_scope(); passed = passed && reopened; // Закрытие предыдущего окна не мешает повторному открытию того же анализатора.
|
||
+ mark("menu_reopen"); // Проверяется наличие одного контекстного действия и его связь с теми же готовыми данными.
|
||
+ _session->remove_decoder(int(decoder_count - 1)); QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); // Удаление тестового анализатора воспроизводит смену источника и проверяет слабую связь окна с данными.
|
||
+ passed = passed && reopened.isNull() && _session->get_signals().size() == digital_count && _session->get_decode_signals().size() == decoder_count - 1; // Окно закрывается вместе с источником, а цифровые каналы и два соседних анализатора сохраняются.
|
||
+ mark("source_removal"); // Удаление исходного стека не должно оставлять окно с недействительными указателями.
|
||
+ } // В случае отсутствия окна журнал получит явный FAIL без обращения по нулевому указателю.
|
||
+ QFile report(QString::fromUtf8(qgetenv("DSVIEW_HOST_CHECK"))); if (report.open(QIODevice::WriteOnly | QIODevice::Truncate)) report.write(passed ? "PASS selected decoder scope / render / independent zoom and pan / range / cached results / reopen / source removal\n" : (QString("FAIL detached decoder scope ") + diagnostic + "\n").toUtf8()); // Отчёт проверяет полный жизненный цикл и при ошибке сохраняет координаты выбора и состояние исходного анализа.
|
||
+ }); // Проверка не блокирует главный цикл событий DSView во время ожидания результата.
|
||
+ return; // Демонстрационные CH1…CH4 в этом отдельном сценарии не добавляются.
|
||
+ } // В обычной работе ветка проверки отключена.
|
||
if (qEnvironmentVariableIsSet("DSVIEW_HOST_CHECK_SCOPE")) { // Явный проверочный режим использует реальные действия меню, не подключая USB или Ethernet.
|
||
const auto original_signals = _session->get_signals(); // Копия указателей проверяет сохранность существующей цифровой записи.
|
||
std::vector<pv::view::Trace*> before; // Список позволяет обнаружить потерю или лишнее добавление любой дорожки.
|
||
--- a/DSView/pv/view/decodetrace.cpp
|
||
+++ b/DSView/pv/view/decodetrace.cpp
|
||
@@ -34,6 +34,7 @@
|
||
#include <QApplication>
|
||
#include "decodetrace.h"
|
||
#include "pwm_waveform.h" // Индекс экстремумов сохраняет форму PWM при любом масштабе времени.
|
||
+#include "decoder_scope.h" // Самостоятельное окно использует тот же индекс выбранной графической области анализатора.
|
||
#include "../sigsession.h"
|
||
#include "../data/decoderstack.h"
|
||
#include "../data/decode/decoder.h"
|
||
@@ -222,6 +223,7 @@
|
||
|
||
_cur_row_headings.clear();
|
||
_cur_row_heading_offsets.clear(); // Координаты и подписи пересоздаются вместе после изменения видимых строк декодера.
|
||
+ _scope_regions.clear(); // Старые области выбора не должны оставаться после смены раскладки, очистки или нового декодирования.
|
||
|
||
// Show sample rate as 1Hz when it is unknown
|
||
if (samplerate == 0.0)
|
||
@@ -278,6 +280,8 @@
|
||
const bool allocate = !row_combined || !wave_shown; // Каждому дополнительному расчётному графику всегда выделяется собственная область.
|
||
if (allocate) combined_y = y; // Раздельный режим начинает новую область для каждого канала.
|
||
const int wave_top = combined_y; // Следующие совмещённые фазы используют ту же вертикальную систему координат.
|
||
+ if (allocate) _scope_regions.push_back({wave_top - annotation_height * 0.5, wave_top + annotation_height * 2.5, QString::fromUtf8(dec->decoder()->id), QStringList{row_id}, get_name() + QStringLiteral(" · ") + (row_combined ? QStringLiteral("A / B / C") : row.title())}); // Координаты совпадают с областью отсечения нарисованного графика, а совмещённые фазы открываются вместе.
|
||
+ else _scope_regions.back().rows.append(row_id); // Вторая и третья фазы той же области добавляются в одно отдельное окно без предупреждений и других анализаторов.
|
||
p.save(); // Локальные настройки пера и отсечения не затрагивают остальные строки анализатора.
|
||
const int wave_y = wave_top + annotation_height; // Нулевая линия располагается в середине собственной либо общей трёхстрочной области PWM.
|
||
p.setClipRect(QRectF(left, wave_top - annotation_height * 0.5, right - left, annotation_height * 3)); // Совмещённые фазы получают одинаковую область отсечения.
|
||
@@ -786,6 +790,26 @@
|
||
}
|
||
|
||
//to show decoder's property setting dialog
|
||
+bool DecodeTrace::open_scope_at(int y) // Обработчик вызывается только для двойного щелчка в области данных DSView.
|
||
+{ // Текстовые аннотации и цифровые сигналы сохраняют прежнее поведение мыши.
|
||
+ for (const auto ®ion : _scope_regions) if (y >= region.top && y < region.bottom) { // Попадание учитывает актуальные высоты областей и объединение фаз текущего кадра.
|
||
+ auto *window = new DecoderScopeWindow(_decoder_stack, _view, region.decoder, region.rows, region.title); window->show(); // Новое окно разделяет готовые данные только выбранного анализатора и имеет собственную временную шкалу.
|
||
+ return true; // Исходный обработчик двойного щелчка больше не добавляет курсор поверх открытого графика.
|
||
+ } // Все области принадлежат только данному экземпляру декодера.
|
||
+ return false; // При отсутствии графика исходное взаимодействие DSView продолжается.
|
||
+} // Удаление источника автоматически закрывает созданное окно через слабую ссылку DecoderScopeWindow.
|
||
+
|
||
+void DecodeTrace::show_scope_menu(const QPoint &position) // Альтернативный способ открытия доступен через название анализатора слева.
|
||
+{ // Меню содержит только реально отображаемые графические области текущего анализатора.
|
||
+ if (_scope_regions.empty()) return; // Обычные текстовые декодеры не получают пустого контекстного меню.
|
||
+ auto *menu = new QMenu(_view); menu->setObjectName("decoder_scope_menu"); menu->setAttribute(Qt::WA_DeleteOnClose); // Меню принадлежит текущему представлению и освобождается при закрытии.
|
||
+ for (const auto ®ion : _scope_regions) { // Раздельные фазы, общий A/B/C, θ и несущая перечисляются как отдельные варианты.
|
||
+ auto *action = menu->addAction(QString::fromUtf8("Открыть в отдельном окне: ") + region.title); // Название явно указывает, какой именно график будет вынесен.
|
||
+ connect(action, &QAction::triggered, this, [this, region]() { auto *window = new DecoderScopeWindow(_decoder_stack, _view, region.decoder, region.rows, region.title); window->show(); }); // Контекст DecodeTrace отменяет обработчик, если исходный анализатор удалён.
|
||
+ } // Выбор одной области не открывает соседние анализаторы или цифровые дорожки.
|
||
+ menu->popup(position); // Неблокирующее меню сохраняет обработку потока декодирования и команд SETGUI.
|
||
+} // Позиция передаётся в глобальных координатах мыши.
|
||
+
|
||
bool DecodeTrace::create_popup(bool isnew)
|
||
{
|
||
(void)isnew;
|
||
--- a/DSView/pv/view/decodetrace.h
|
||
+++ b/DSView/pv/view/decodetrace.h
|
||
@@ -30,6 +30,7 @@
|
||
#include <QFormLayout>
|
||
#include <QWidget>
|
||
#include <QString>
|
||
+#include <QStringList> // Безопасные идентификаторы фаз и дополнительных графиков не содержат указателей на результаты декодирования.
|
||
|
||
#include "trace.h"
|
||
#include "../prop/binding/decoderoptions.h"
|
||
@@ -143,6 +144,8 @@
|
||
void* get_key_handel();
|
||
|
||
bool create_popup(bool isnew);
|
||
+ bool open_scope_at(int y); // Двойной щелчок открывает только графическую область под указанной координатой представления.
|
||
+ void show_scope_menu(const QPoint &position); // Контекстное меню имени анализатора перечисляет его отдельные графики.
|
||
|
||
protected:
|
||
void paint_type_options(QPainter &p, int right, const QPoint pt, QColor fore);
|
||
@@ -196,6 +199,8 @@
|
||
|
||
std::vector<QString> _cur_row_headings;
|
||
std::vector<int> _cur_row_heading_offsets; // Относительные координаты заголовков учитывают разную высоту текстовых и графических строк.
|
||
+ struct ScopeRegion { double top, bottom; QString decoder; QStringList rows; QString title; }; // Одна область связывает фактические координаты кадра с безопасными идентификаторами выбранных кривых.
|
||
+ std::vector<ScopeRegion> _scope_regions; // Список пересоздаётся при рисовании, учитывая совмещение фаз, скрытие строк и ручную высоту графиков.
|
||
|
||
};
|
||
|
||
--- a/DSView/pv/view/header.cpp
|
||
+++ b/DSView/pv/view/header.cpp
|
||
@@ -25,6 +25,7 @@
|
||
#include <QColorDialog>
|
||
#include <QInputDialog>
|
||
#include <QMenu>
|
||
+#include <QContextMenuEvent> // Положение контекстного меню определяется текущим нажатием, без зависимости от последнего наведения мыши.
|
||
#include <QMouseEvent>
|
||
#include <QPainter>
|
||
#include <QRect>
|
||
@@ -544,14 +545,11 @@
|
||
|
||
void Header::contextMenuEvent(QContextMenuEvent *event)
|
||
{
|
||
- (void)event;
|
||
-
|
||
- int action;
|
||
-
|
||
- const auto t = get_mTrace(action, _mouse_point);
|
||
-
|
||
- if (!t || !t->selected() || action != Trace::LABEL)
|
||
- return;
|
||
+ int action = 0; // Поиск использует штатные области имени, метки и цвета дорожки.
|
||
+ auto *trace = get_mTrace(action, event->pos()); // Правый щелчок выбирает конкретный анализатор под курсором, включая ещё не выделенный.
|
||
+ if (trace && trace->get_type() == SR_CHANNEL_DECODER) { // Контекстное открытие графиков не изменяет меню цифровых и аппаратных аналоговых каналов.
|
||
+ static_cast<DecodeTrace*>(trace)->show_scope_menu(event->globalPos()); event->accept(); // Пользователь выбирает одну осциллограмму этого анализатора в неблокирующем меню.
|
||
+ } // Для текстового декодера без графических строк отдельное меню не появляется.
|
||
}
|
||
|
||
void Header::on_action_set_name_triggered()
|
||
--- a/DSView/pv/view/viewport.cpp
|
||
+++ b/DSView/pv/view/viewport.cpp
|
||
@@ -1260,6 +1260,10 @@
|
||
if (!_view.get_view_rect().contains(event->pos()))
|
||
return;
|
||
|
||
+ if (event->button() == Qt::LeftButton) for (auto *trace : _view.session().get_decode_signals()) { // Двойной щелчок по графику анализатора обрабатывается раньше добавления обычных измерительных курсоров.
|
||
+ if (trace->open_scope_at(event->pos().y())) { event->accept(); return; } // Проверяются фактические области кадра; новое окно содержит только выбранную осциллограмму.
|
||
+ } // Щелчки по цифровым сигналам, текстам и остальным видам сохраняют прежнюю обработку DSView.
|
||
+
|
||
int mode = _view.session().get_device()->get_work_mode();
|
||
|
||
if (mode == LOGIC)
|