--- 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 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("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("scope_y_range"); if (range) range->setValue(50); passed = passed && range && range->value() == 50; // Ручная шкала ±50 А не меняет параметры расчётной модели двигателя. + auto *fit_y = scope->findChild("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("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 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 before; // Список позволяет обнаружить потерю или лишнее добавление любой дорожки. --- a/DSView/pv/view/decodetrace.cpp +++ b/DSView/pv/view/decodetrace.cpp @@ -34,6 +34,7 @@ #include #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 #include #include +#include // Безопасные идентификаторы фаз и дополнительных графиков не содержат указателей на результаты декодирования. #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 _cur_row_headings; std::vector _cur_row_heading_offsets; // Относительные координаты заголовков учитывают разную высоту текстовых и графических строк. + struct ScopeRegion { double top, bottom; QString decoder; QStringList rows; QString title; }; // Одна область связывает фактические координаты кадра с безопасными идентификаторами выбранных кривых. + std::vector _scope_regions; // Список пересоздаётся при рисовании, учитывая совмещение фаз, скрытие строк и ручную высоту графиков. }; --- a/DSView/pv/view/header.cpp +++ b/DSView/pv/view/header.cpp @@ -25,6 +25,7 @@ #include #include #include +#include // Положение контекстного меню определяется текущим нажатием, без зависимости от последнего наведения мыши. #include #include #include @@ -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(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)