Commit 0eff8d451180c8c1328ff0d33de4e2f477147e16

Authored by Denis Dovzhenko
Committed by David Gräff
1 parent 52622ac9

Cursor measurements (#177)

* Cursor measurements
* encapsulate measurements grid into widget
* enable fg / bg color change after creation
openhantek/src/configdialog/DsoConfigScopePage.cpp
... ... @@ -27,8 +27,22 @@ DsoConfigScopePage::DsoConfigScopePage(DsoSettings *settings, QWidget *parent) :
27 27 graphGroup = new QGroupBox(tr("Graph"));
28 28 graphGroup->setLayout(graphLayout);
29 29  
  30 + cursorsLabel = new QLabel(tr("Position"));
  31 + cursorsComboBox = new QComboBox();
  32 + cursorsComboBox->addItem("Left", Qt::LeftToolBarArea);
  33 + cursorsComboBox->addItem("Right", Qt::RightToolBarArea);
  34 + cursorsComboBox->setCurrentIndex(settings->view.cursorGridPosition == Qt::LeftToolBarArea ? 0 : 1);
  35 +
  36 + cursorsLayout = new QGridLayout();
  37 + cursorsLayout->addWidget(cursorsLabel, 0, 0);
  38 + cursorsLayout->addWidget(cursorsComboBox, 0, 1);
  39 +
  40 + cursorsGroup = new QGroupBox(tr("Cursors"));
  41 + cursorsGroup->setLayout(cursorsLayout);
  42 +
30 43 mainLayout = new QVBoxLayout();
31 44 mainLayout->addWidget(graphGroup);
  45 + mainLayout->addWidget(cursorsGroup);
32 46 mainLayout->addStretch(1);
33 47  
34 48 setLayout(mainLayout);
... ... @@ -38,4 +52,5 @@ DsoConfigScopePage::DsoConfigScopePage(DsoSettings *settings, QWidget *parent) :
38 52 void DsoConfigScopePage::saveSettings() {
39 53 settings->view.interpolation = (Dso::InterpolationMode)interpolationComboBox->currentIndex();
40 54 settings->view.digitalPhosphorDepth = digitalPhosphorDepthSpinBox->value();
  55 + settings->view.cursorGridPosition = (Qt::ToolBarArea)cursorsComboBox->currentData().toUInt();
41 56 }
... ...
openhantek/src/configdialog/DsoConfigScopePage.h
... ... @@ -37,4 +37,9 @@ class DsoConfigScopePage : public QWidget {
37 37 QSpinBox *digitalPhosphorDepthSpinBox;
38 38 QLabel *interpolationLabel;
39 39 QComboBox *interpolationComboBox;
  40 +
  41 + QGroupBox *cursorsGroup;
  42 + QGridLayout *cursorsLayout;
  43 + QLabel *cursorsLabel;
  44 + QComboBox *cursorsComboBox;
40 45 };
... ...
openhantek/src/dsowidget.cpp
... ... @@ -22,6 +22,7 @@
22 22 #include "viewconstants.h"
23 23 #include "viewsettings.h"
24 24 #include "widgets/levelslider.h"
  25 +#include "widgets/datagrid.h"
25 26  
26 27 static int zoomScopeRow = 0;
27 28  
... ... @@ -38,12 +39,14 @@ DsoWidget::DsoWidget(DsoSettingsScope *scope, DsoSettingsView *view, const Dso::
38 39 setupSliders(mainSliders);
39 40 setupSliders(zoomSliders);
40 41  
41   - connect(mainScope, &GlScope::markerMoved, [this](int marker, double position) {
42   - double value = std::round(position / MARKER_STEP) * MARKER_STEP;
43   -
44   - this->scope->horizontal.marker[marker] = value;
45   - this->mainSliders.markerSlider->setValue(marker, value);
46   - this->mainScope->markerUpdated();
  42 + connect(mainScope, &GlScope::markerMoved, [this](unsigned cursorIndex, unsigned marker) {
  43 + mainSliders.markerSlider->setValue(marker, this->scope->getMarker(marker));
  44 + mainScope->updateCursor(cursorIndex);
  45 + zoomScope->updateCursor(cursorIndex);
  46 + });
  47 + connect(zoomScope, &GlScope::markerMoved, [this](unsigned cursorIndex, unsigned marker) {
  48 + mainScope->updateCursor(cursorIndex);
  49 + zoomScope->updateCursor(cursorIndex);
47 50 });
48 51  
49 52 // The table for the settings
... ... @@ -142,40 +145,88 @@ DsoWidget::DsoWidget(DsoSettingsScope *scope, DsoSettingsView *view, const Dso::
142 145 updateSpectrumDetails((unsigned)channel);
143 146 }
144 147  
  148 + // Cursors
  149 + cursorDataGrid = new DataGrid(this);
  150 + cursorDataGrid->setBackgroundColor(view->screen.background);
  151 + cursorDataGrid->addItem(tr("Markers"), view->screen.text);
  152 + for (ChannelID channel = 0; channel < scope->voltage.size(); ++channel) {
  153 + cursorDataGrid->addItem(scope->voltage[channel].name, view->screen.voltage[channel]);
  154 + }
  155 + for (ChannelID channel = 0; channel < scope->spectrum.size(); ++channel) {
  156 + cursorDataGrid->addItem(scope->spectrum[channel].name, view->screen.spectrum[channel]);
  157 + }
  158 + cursorDataGrid->selectItem(0);
  159 +
  160 + connect(cursorDataGrid, &DataGrid::itemSelected, [this] (unsigned index) {
  161 + mainScope->cursorSelected(index);
  162 + zoomScope->cursorSelected(index);
  163 + });
  164 + connect(cursorDataGrid, &DataGrid::itemUpdated, [this, scope] (unsigned index) {
  165 + unsigned channelCount = scope->countChannels();
  166 + if (0 < index && index < channelCount + 1) {
  167 + ChannelID channel = index - 1;
  168 + if (scope->voltage[channel].used) {
  169 + unsigned shape = (unsigned)scope->voltage[channel].cursor.shape;
  170 + if (shape == DsoSettingsScopeCursor::NONE) {
  171 + scope->voltage[channel].cursor.shape = DsoSettingsScopeCursor::RECTANGULAR;
  172 + } else {
  173 + scope->voltage[channel].cursor.shape = DsoSettingsScopeCursor::NONE;
  174 + }
  175 + }
  176 + } else if (channelCount < index && index < 2 * channelCount + 1) {
  177 + ChannelID channel = index - channelCount - 1;
  178 + if (scope->spectrum[channel].used) {
  179 + unsigned shape = (unsigned)scope->spectrum[channel].cursor.shape;
  180 + if (shape == DsoSettingsScopeCursor::NONE) {
  181 + scope->spectrum[channel].cursor.shape = DsoSettingsScopeCursor::RECTANGULAR;
  182 + } else {
  183 + scope->spectrum[channel].cursor.shape = DsoSettingsScopeCursor::NONE;
  184 + }
  185 + }
  186 + }
  187 + updateMarkerDetails();
  188 + mainScope->updateCursor(index);
  189 + zoomScope->updateCursor(index);
  190 + });
  191 +
  192 + scope->horizontal.cursor.shape = DsoSettingsScopeCursor::VERTICAL;
  193 +
145 194 // The layout for the widgets
146 195 mainLayout = new QGridLayout();
147   - mainLayout->setColumnStretch(2, 1); // Scopes increase their size
  196 + mainLayout->setColumnStretch(3, 1); // Scopes increase their size
148 197 // Bars around the scope, needed because the slider-drawing-area is outside
149 198 // the scope at min/max
150   - mainLayout->setColumnMinimumWidth(1, mainSliders.triggerPositionSlider->preMargin());
151   - mainLayout->setColumnMinimumWidth(3, mainSliders.triggerPositionSlider->postMargin());
  199 + mainLayout->setColumnMinimumWidth(2, mainSliders.triggerPositionSlider->preMargin());
  200 + mainLayout->setColumnMinimumWidth(4, mainSliders.triggerPositionSlider->postMargin());
152 201 mainLayout->setSpacing(0);
153 202 int row = 0;
154   - mainLayout->addLayout(settingsLayout, row++, 0, 1, 5);
  203 + mainLayout->addLayout(settingsLayout, row++, 1, 1, 5);
155 204 // 5x5 box for mainScope & mainSliders
156 205 mainLayout->setRowMinimumHeight(row + 1, mainSliders.offsetSlider->preMargin());
157 206 mainLayout->setRowMinimumHeight(row + 3, mainSliders.offsetSlider->postMargin());
158 207 mainLayout->setRowStretch(row + 2, 1);
159   - mainLayout->addWidget(mainScope, row + 2, 2);
160   - mainLayout->addWidget(mainSliders.offsetSlider, row + 1, 0, 3, 2, Qt::AlignRight);
161   - mainLayout->addWidget(mainSliders.triggerPositionSlider, row, 1, 2, 3, Qt::AlignBottom);
162   - mainLayout->addWidget(mainSliders.triggerLevelSlider, row + 1, 3, 3, 2, Qt::AlignLeft);
163   - mainLayout->addWidget(mainSliders.markerSlider, row + 3, 1, 2, 3, Qt::AlignTop);
  208 + mainLayout->addWidget(mainScope, row + 2, 3);
  209 + mainLayout->addWidget(mainSliders.offsetSlider, row + 1, 1, 3, 2, Qt::AlignRight);
  210 + mainLayout->addWidget(mainSliders.triggerPositionSlider, row, 2, 2, 3, Qt::AlignBottom);
  211 + mainLayout->addWidget(mainSliders.triggerLevelSlider, row + 1, 4, 3, 2, Qt::AlignLeft);
  212 + mainLayout->addWidget(mainSliders.markerSlider, row + 3, 2, 2, 3, Qt::AlignTop);
164 213 row += 5;
165 214 // Separators and markerLayout
166   - mainLayout->setRowMinimumHeight(row++, 4);
167   - mainLayout->addLayout(markerLayout, row++, 0, 1, 5);
  215 + mainLayout->setRowMinimumHeight(row++, 5);
  216 + mainLayout->addLayout(markerLayout, row++, 1, 1, 5);
168 217 mainLayout->setRowMinimumHeight(row++, 4);
169 218 // 5x5 box for zoomScope & zoomSliders
170 219 zoomScopeRow = row + 2;
171   - mainLayout->addWidget(zoomScope, zoomScopeRow, 2);
172   - mainLayout->addWidget(zoomSliders.offsetSlider, row + 1, 0, 3, 2, Qt::AlignRight);
173   - mainLayout->addWidget(zoomSliders.triggerPositionSlider, row, 1, 2, 3, Qt::AlignBottom);
174   - mainLayout->addWidget(zoomSliders.triggerLevelSlider, row + 1, 3, 3, 2, Qt::AlignLeft);
  220 + mainLayout->addWidget(zoomScope, zoomScopeRow, 3);
  221 + mainLayout->addWidget(zoomSliders.offsetSlider, row + 1, 1, 3, 2, Qt::AlignRight);
  222 + mainLayout->addWidget(zoomSliders.triggerPositionSlider, row, 2, 2, 3, Qt::AlignBottom);
  223 + mainLayout->addWidget(zoomSliders.triggerLevelSlider, row + 1, 4, 3, 2, Qt::AlignLeft);
175 224 row += 5;
176 225 // Separator and embedded measurementLayout
177 226 mainLayout->setRowMinimumHeight(row++, 8);
178   - mainLayout->addLayout(measurementLayout, row++, 0, 1, 5);
  227 + mainLayout->addLayout(measurementLayout, row++, 1, 1, 5);
  228 +
  229 + updateCursorGrid(view->cursorsVisible);
179 230  
180 231 // The widget itself
181 232 setPalette(palette);
... ... @@ -187,20 +238,54 @@ DsoWidget::DsoWidget(DsoSettingsScope *scope, DsoSettingsView *view, const Dso::
187 238 connect(mainSliders.offsetSlider, &LevelSlider::valueChanged, this, &DsoWidget::updateOffset);
188 239 connect(zoomSliders.offsetSlider, &LevelSlider::valueChanged, this, &DsoWidget::updateOffset);
189 240  
190   - connect(mainSliders.triggerPositionSlider, &LevelSlider::valueChanged, this, &DsoWidget::updateTriggerPosition);
191   - zoomSliders.triggerPositionSlider->setEnabled(false);
  241 + connect(mainSliders.triggerPositionSlider, &LevelSlider::valueChanged, [this](int index, double value) {
  242 + updateTriggerPosition(index, value, true);
  243 + });
  244 + connect(zoomSliders.triggerPositionSlider, &LevelSlider::valueChanged, [this](int index, double value) {
  245 + updateTriggerPosition(index, value, false);
  246 + });
192 247  
193 248 connect(mainSliders.triggerLevelSlider, &LevelSlider::valueChanged, this, &DsoWidget::updateTriggerLevel);
194 249 connect(zoomSliders.triggerLevelSlider, &LevelSlider::valueChanged, this, &DsoWidget::updateTriggerLevel);
195 250  
196 251 connect(mainSliders.markerSlider, &LevelSlider::valueChanged, [this](int index, double value) {
197 252 updateMarker(index, value);
198   - mainScope->update();
199   - zoomScope->update();
  253 + mainScope->updateCursor();
  254 + zoomScope->updateCursor();
200 255 });
201 256 zoomSliders.markerSlider->setEnabled(false);
202 257 }
203 258  
  259 +void DsoWidget::updateCursorGrid(bool enabled) {
  260 + if (!enabled) {
  261 + cursorDataGrid->selectItem(0);
  262 + cursorDataGrid->setParent(nullptr);
  263 + mainScope->cursorSelected(0);
  264 + zoomScope->cursorSelected(0);
  265 + return;
  266 + }
  267 +
  268 + switch (view->cursorGridPosition) {
  269 + case Qt::LeftToolBarArea:
  270 + if (mainLayout->itemAtPosition(0, 0) == nullptr) {
  271 + cursorDataGrid->setParent(nullptr);
  272 + mainLayout->addWidget(cursorDataGrid, 0, 0, mainLayout->rowCount(), 1);
  273 + }
  274 + break;
  275 + case Qt::RightToolBarArea:
  276 + if (mainLayout->itemAtPosition(0, 6) == nullptr) {
  277 + cursorDataGrid->setParent(nullptr);
  278 + mainLayout->addWidget(cursorDataGrid, 0, 6, mainLayout->rowCount(), 1);
  279 + }
  280 + break;
  281 + default:
  282 + if (cursorDataGrid->parent() != nullptr) {
  283 + cursorDataGrid->setParent(nullptr);
  284 + }
  285 + break;
  286 + }
  287 +}
  288 +
204 289 void DsoWidget::setupSliders(DsoWidget::Sliders &sliders) {
205 290 // The offset sliders for all possible channels
206 291 sliders.offsetSlider = new LevelSlider(Qt::RightArrow);
... ... @@ -248,7 +333,7 @@ void DsoWidget::setupSliders(DsoWidget::Sliders &amp;sliders) {
248 333 sliders.markerSlider->addSlider(QString::number(marker + 1), marker);
249 334 sliders.markerSlider->setLimits(marker, -DIVS_TIME / 2, DIVS_TIME / 2);
250 335 sliders.markerSlider->setStep(marker, MARKER_STEP);
251   - sliders.markerSlider->setValue(marker, scope->horizontal.marker[marker]);
  336 + sliders.markerSlider->setValue(marker, scope->horizontal.cursor.pos[marker].x());
252 337 sliders.markerSlider->setIndexVisible(marker, true);
253 338 }
254 339 }
... ... @@ -285,7 +370,7 @@ void DsoWidget::setMeasurementVisible(ChannelID channel) {
285 370 }
286 371  
287 372 static QString markerToString(DsoSettingsScope *scope, unsigned index) {
288   - double value = (DIVS_TIME * (0.5 - scope->trigger.position) + scope->horizontal.marker[index]) * scope->horizontal.timebase;
  373 + double value = (DIVS_TIME * (0.5 - scope->trigger.position) + scope->getMarker(index)) * scope->horizontal.timebase;
289 374 int precision = 3 - (int)floor(log10(fabs(value)));
290 375  
291 376 if (scope->horizontal.timebase < 1e-9)
... ... @@ -302,8 +387,9 @@ static QString markerToString(DsoSettingsScope *scope, unsigned index) {
302 387  
303 388 /// \brief Update the label about the marker measurements
304 389 void DsoWidget::updateMarkerDetails() {
305   - double divs = fabs(scope->horizontal.marker[1] - scope->horizontal.marker[0]);
  390 + double divs = fabs(scope->horizontal.cursor.pos[1].x() - scope->horizontal.cursor.pos[0].x());
306 391 double time = divs * scope->horizontal.timebase;
  392 + double freq = divs * scope->horizontal.frequencybase;
307 393  
308 394 QString prefix(tr("Markers"));
309 395 if (view->zoom) {
... ... @@ -315,6 +401,36 @@ void DsoWidget::updateMarkerDetails() {
315 401 markerInfoLabel->setText(prefix.append(": %1 %2").arg(markerToString(scope, 0)).arg(markerToString(scope, 1)));
316 402 markerTimeLabel->setText(valueToString(time, UNIT_SECONDS, 4));
317 403 markerFrequencyLabel->setText(valueToString(1.0 / time, UNIT_HERTZ, 4));
  404 +
  405 + int index = 0;
  406 + cursorDataGrid->updateInfo(index++, true, QString(), valueToString(time, UNIT_SECONDS, 4), valueToString(freq, UNIT_HERTZ, 4));
  407 +
  408 + for (ChannelID channel = 0; channel < scope->voltage.size(); ++channel) {
  409 + if (scope->voltage[channel].used) {
  410 + QPointF p0 = scope->voltage[channel].cursor.pos[0];
  411 + QPointF p1 = scope->voltage[channel].cursor.pos[1];
  412 + cursorDataGrid->updateInfo(index, true,
  413 + scope->voltage[channel].cursor.shape != DsoSettingsScopeCursor::NONE ? tr("ON") : tr("OFF"),
  414 + valueToString(fabs(p1.x() - p0.x()) * scope->horizontal.timebase, UNIT_SECONDS, 4),
  415 + valueToString(fabs(p1.y() - p0.y()) * scope->gain(channel), UNIT_VOLTS, 4));
  416 + } else {
  417 + cursorDataGrid->updateInfo(index, false);
  418 + }
  419 + ++index;
  420 + }
  421 + for (ChannelID channel = 0; channel < scope->spectrum.size(); ++channel) {
  422 + if (scope->spectrum[channel].used) {
  423 + QPointF p0 = scope->spectrum[channel].cursor.pos[0];
  424 + QPointF p1 = scope->spectrum[channel].cursor.pos[1];
  425 + cursorDataGrid->updateInfo(index, true,
  426 + scope->spectrum[channel].cursor.shape != DsoSettingsScopeCursor::NONE ? tr("ON") : tr("OFF"),
  427 + valueToString(fabs(p1.x() - p0.x()) * scope->horizontal.frequencybase, UNIT_HERTZ, 4),
  428 + valueToString(fabs(p1.y() - p0.y()) * scope->spectrum[channel].magnitude, UNIT_DECIBEL, 4));
  429 + } else {
  430 + cursorDataGrid->updateInfo(index, false);
  431 + }
  432 + ++index;
  433 + }
318 434 }
319 435  
320 436 /// \brief Update the label about the trigger settings
... ... @@ -385,10 +501,13 @@ void DsoWidget::updateSpectrumMagnitude(ChannelID channel) { updateSpectrumDetai
385 501 void DsoWidget::updateSpectrumUsed(ChannelID channel, bool used) {
386 502 if (channel >= (unsigned int)scope->voltage.size()) return;
387 503  
  504 +// if (!used && cursorDataGrid->spectrumCursors[channel].selector->isChecked()) cursorDataGrid->selectItem(0);
  505 +
388 506 mainSliders.offsetSlider->setIndexVisible(scope->voltage.size() + channel, used);
389 507 zoomSliders.offsetSlider->setIndexVisible(scope->voltage.size() + channel, used);
390 508  
391 509 updateSpectrumDetails(channel);
  510 + updateMarkerDetails();
392 511 }
393 512  
394 513 /// \brief Handles modeChanged signal from the trigger dock.
... ... @@ -452,6 +571,8 @@ void DsoWidget::updateVoltageGain(ChannelID channel) {
452 571 void DsoWidget::updateVoltageUsed(ChannelID channel, bool used) {
453 572 if (channel >= (unsigned int)scope->voltage.size()) return;
454 573  
  574 +// if (!used && cursorDataGrid->voltageCursors[channel].selector->isChecked()) cursorDataGrid->selectItem(0);
  575 +
455 576 mainSliders.offsetSlider->setIndexVisible(channel, used);
456 577 zoomSliders.offsetSlider->setIndexVisible(channel, used);
457 578  
... ... @@ -460,6 +581,7 @@ void DsoWidget::updateVoltageUsed(ChannelID channel, bool used) {
460 581  
461 582 setMeasurementVisible(channel);
462 583 updateVoltageDetails(channel);
  584 + updateMarkerDetails();
463 585 }
464 586  
465 587 /// \brief Change the record length.
... ... @@ -549,11 +671,11 @@ void DsoWidget::updateOffset(ChannelID channel, double value) {
549 671  
550 672 if (channel < scope->voltage.size() * 2) {
551 673 if (mainSliders.offsetSlider->value(channel) != value) {
552   - QSignalBlocker blocker(mainSliders.offsetSlider);
  674 + const QSignalBlocker blocker(mainSliders.offsetSlider);
553 675 mainSliders.offsetSlider->setValue(channel, value);
554 676 }
555 677 if (zoomSliders.offsetSlider->value(channel) != value) {
556   - QSignalBlocker blocker(zoomSliders.offsetSlider);
  678 + const QSignalBlocker blocker(zoomSliders.offsetSlider);
557 679 zoomSliders.offsetSlider->setValue(channel, value);
558 680 }
559 681 }
... ... @@ -561,18 +683,38 @@ void DsoWidget::updateOffset(ChannelID channel, double value) {
561 683 emit offsetChanged(channel, value);
562 684 }
563 685  
  686 +/// \brief Translate horizontal position (0..1) from main view to zoom view.
  687 +double DsoWidget::mainToZoom(double position) const {
  688 + double m1 = scope->getMarker(0);
  689 + double m2 = scope->getMarker(1);
  690 + if (m1 > m2) std::swap(m1, m2);
  691 + return ((position - 0.5) * DIVS_TIME - m1) / (m2 - m1);
  692 +}
  693 +
  694 +/// \brief Translate horizontal position (0..1) from zoom view to main view.
  695 +double DsoWidget::zoomToMain(double position) const {
  696 + double m1 = scope->getMarker(0);
  697 + double m2 = scope->getMarker(1);
  698 + if (m1 > m2) std::swap(m1, m2);
  699 + return 0.5 + (m1 + position * (m2 - m1)) / DIVS_TIME;
  700 +}
  701 +
564 702 /// \brief Handles signals affecting trigger position in the zoom view.
565 703 void DsoWidget::adaptTriggerPositionSlider() {
566   - double m1 = scope->horizontal.marker[0];
567   - double m2 = scope->horizontal.marker[1];
  704 + double value = mainToZoom(scope->trigger.position);
568 705  
569   - if (m1 > m2) std::swap(m1, m2);
570   - double value = (scope->trigger.position - 0.5) * DIVS_TIME;
571   - if (m1 != m2 && m1 <= value && value <= m2) {
572   - zoomSliders.triggerPositionSlider->setIndexVisible(0, true);
573   - zoomSliders.triggerPositionSlider->setValue(0, (value - m1) / (m2 - m1));
  706 + LevelSlider &slider = *zoomSliders.triggerPositionSlider;
  707 + const QSignalBlocker blocker(slider);
  708 + if (slider.minimum(0) <= value && value <= slider.maximum(0)) {
  709 + slider.setEnabled(true);
  710 + slider.setValue(0, value);
574 711 } else {
575   - zoomSliders.triggerPositionSlider->setIndexVisible(0, false);
  712 + slider.setEnabled(false);
  713 + if (value < slider.minimum(0)) {
  714 + slider.setValue(0, slider.minimum(0));
  715 + } else {
  716 + slider.setValue(0, slider.maximum(0));
  717 + }
576 718 }
577 719 }
578 720  
... ... @@ -580,16 +722,22 @@ void DsoWidget::adaptTriggerPositionSlider() {
580 722 /// \param index The index of the slider.
581 723 /// \param value The new triggerPosition in seconds relative to the first
582 724 /// sample.
583   -void DsoWidget::updateTriggerPosition(int index, double value) {
  725 +void DsoWidget::updateTriggerPosition(int index, double value, bool mainView) {
584 726 if (index != 0) return;
585 727  
586   - scope->trigger.position = value;
587   - adaptTriggerPositionSlider();
  728 + if (mainView) {
  729 + scope->trigger.position = value;
  730 + adaptTriggerPositionSlider();
  731 + } else {
  732 + scope->trigger.position = zoomToMain(value);
  733 + const QSignalBlocker blocker(mainSliders.triggerPositionSlider);
  734 + mainSliders.triggerPositionSlider->setValue(index, scope->trigger.position);
  735 + }
588 736  
589 737 updateTriggerDetails();
590 738 updateMarkerDetails();
591 739  
592   - emit triggerPositionChanged(value * scope->horizontal.timebase * DIVS_TIME);
  740 + emit triggerPositionChanged(scope->trigger.position * scope->horizontal.timebase * DIVS_TIME);
593 741 }
594 742  
595 743 /// \brief Handles valueChanged signal from the trigger level slider.
... ... @@ -599,11 +747,11 @@ void DsoWidget::updateTriggerLevel(ChannelID channel, double value) {
599 747 scope->voltage[channel].trigger = value;
600 748  
601 749 if (mainSliders.triggerLevelSlider->value(channel) != value) {
602   - QSignalBlocker blocker(mainSliders.triggerLevelSlider);
  750 + const QSignalBlocker blocker(mainSliders.triggerLevelSlider);
603 751 mainSliders.triggerLevelSlider->setValue(channel, value);
604 752 }
605 753 if (zoomSliders.triggerLevelSlider->value(channel) != value) {
606   - QSignalBlocker blocker(zoomSliders.triggerLevelSlider);
  754 + const QSignalBlocker blocker(zoomSliders.triggerLevelSlider);
607 755 zoomSliders.triggerLevelSlider->setValue(channel, value);
608 756 }
609 757  
... ... @@ -616,10 +764,7 @@ void DsoWidget::updateTriggerLevel(ChannelID channel, double value) {
616 764 /// \param marker The index of the slider.
617 765 /// \param value The new marker position.
618 766 void DsoWidget::updateMarker(int marker, double value) {
619   - scope->horizontal.marker[marker] = value;
620   -
  767 + scope->setMarker(marker, value);
621 768 adaptTriggerPositionSlider();
622 769 updateMarkerDetails();
623   -
624   - emit markerChanged(marker, value);
625 770 }
... ...
openhantek/src/dsowidget.h
... ... @@ -15,12 +15,15 @@
15 15 class SpectrumGenerator;
16 16 struct DsoSettingsScope;
17 17 struct DsoSettingsView;
  18 +class DataGrid;
18 19  
19 20 /// \brief The widget for the oszilloscope-screen
20 21 /// This widget contains the scopes and all level sliders.
21 22 class DsoWidget : public QWidget {
22 23 Q_OBJECT
23 24  
  25 + public:
  26 +
24 27 struct Sliders {
25 28 LevelSlider *offsetSlider; ///< The sliders for the graph offsets
26 29 LevelSlider *triggerPositionSlider; ///< The slider for the pretrigger
... ... @@ -28,7 +31,6 @@ class DsoWidget : public QWidget {
28 31 LevelSlider *markerSlider; ///< The sliders for the markers
29 32 };
30 33  
31   - public:
32 34 /// \brief Initializes the components of the oszilloscope-screen.
33 35 /// \param settings The settings object containing the oscilloscope settings.
34 36 /// \param dataAnalyzer The data analyzer that should be used as data source.
... ... @@ -50,6 +52,9 @@ class DsoWidget : public QWidget {
50 52 void updateTriggerDetails();
51 53 void updateVoltageDetails(ChannelID channel);
52 54  
  55 + double mainToZoom(double position) const;
  56 + double zoomToMain(double position) const;
  57 +
53 58 Sliders mainSliders;
54 59 Sliders zoomSliders;
55 60  
... ... @@ -79,6 +84,8 @@ class DsoWidget : public QWidget {
79 84 std::vector<QLabel *> measurementAmplitudeLabel; ///< Amplitude of the signal (V)
80 85 std::vector<QLabel *> measurementFrequencyLabel; ///< Frequency of the signal (Hz)
81 86  
  87 + DataGrid *cursorDataGrid;
  88 +
82 89 DsoSettingsScope* scope;
83 90 DsoSettingsView* view;
84 91 const Dso::ControlSpecification* spec;
... ... @@ -113,11 +120,12 @@ class DsoWidget : public QWidget {
113 120  
114 121 // Scope control
115 122 void updateZoom(bool enabled);
  123 + void updateCursorGrid(bool enabled);
116 124  
117 125 private slots:
118 126 // Sliders
119 127 void updateOffset(ChannelID channel, double value);
120   - void updateTriggerPosition(int index, double value);
  128 + void updateTriggerPosition(int index, double value, bool mainView = true);
121 129 void updateTriggerLevel(ChannelID channel, double value);
122 130 void updateMarker(int marker, double value);
123 131  
... ... @@ -126,5 +134,4 @@ class DsoWidget : public QWidget {
126 134 void offsetChanged(ChannelID channel, double value); ///< A graph offset has been changed
127 135 void triggerPositionChanged(double value); ///< The pretrigger has been changed
128 136 void triggerLevelChanged(ChannelID channel, double value); ///< A trigger level has been changed
129   - void markerChanged(unsigned int marker, double value); ///< A marker position has been changed
130 137 };
... ...
openhantek/src/exporting/legacyexportdrawer.cpp
... ... @@ -115,10 +115,12 @@ bool LegacyExportDrawer::exportSamples(const PPresult *result, QPaintDevice* pai
115 115 painter.setPen(colorValues->text);
116 116  
117 117 // Calculate variables needed for zoomed scope
118   - double divs = fabs(settings->scope.horizontal.marker[1] - settings->scope.horizontal.marker[0]);
  118 + double m1 = settings->scope.getMarker(0);
  119 + double m2 = settings->scope.getMarker(1);
  120 + double divs = fabs(m2 - m1);
119 121 double time = divs * settings->scope.horizontal.timebase;
120 122 double zoomFactor = DIVS_TIME / divs;
121   - double zoomOffset = (settings->scope.horizontal.marker[0] + settings->scope.horizontal.marker[1]) / 2;
  123 + double zoomOffset = (m1 + m2) / 2;
122 124  
123 125 if (settings->view.zoom) {
124 126 scopeHeight = (double)(paintDevice->height() - (channelCount + 5) * lineHeight) / 2;
... ...
openhantek/src/glscope.cpp
... ... @@ -54,50 +54,106 @@ void GlScope::fixOpenGLversion(QSurfaceFormat::RenderableType t) {
54 54  
55 55 GlScope::GlScope(DsoSettingsScope *scope, DsoSettingsView *view, QWidget *parent)
56 56 : QOpenGLWidget(parent), scope(scope), view(view) {
57   - vaMarker.resize(MARKER_COUNT);
  57 + cursorInfo.clear();
  58 + cursorInfo.push_back(&scope->horizontal.cursor);
  59 + selectedCursor = 0;
  60 + for (ChannelID channel = 0; channel < scope->voltage.size(); ++channel) {
  61 + cursorInfo.push_back(&scope->voltage[channel].cursor);
  62 + }
  63 + for (ChannelID channel = 0; channel < scope->spectrum.size(); ++channel) {
  64 + cursorInfo.push_back(&scope->spectrum[channel].cursor);
  65 + }
  66 + vaMarker.resize(cursorInfo.size());
58 67 }
59 68  
60 69 GlScope::~GlScope() {/* virtual destructor necessary */}
61 70  
  71 +QPointF GlScope::eventToPosition(QMouseEvent *event) {
  72 + QPointF position((double)(event->x() - width() / 2) * DIVS_TIME / (double)width(),
  73 + (double)(height() / 2 - event->y()) * DIVS_VOLTAGE / (double)height());
  74 + if (zoomed) {
  75 + double m1 = scope->getMarker(0);
  76 + double m2 = scope->getMarker(1);
  77 + if (m1 > m2) std::swap(m1, m2);
  78 + position.setX(m1 + (0.5 + (position.x() / DIVS_TIME)) * (m2 - m1));
  79 + }
  80 + return position;
  81 +}
  82 +
62 83 void GlScope::mousePressEvent(QMouseEvent *event) {
63   - if (!zoomed && event->button() == Qt::LeftButton) {
64   - double position = (double)(event->x() - width() / 2) * DIVS_TIME / (double)width();
65   - double distance = DIVS_TIME;
  84 + if (!(zoomed && selectedCursor == 0) && event->button() == Qt::LeftButton) {
  85 + QPointF position = eventToPosition(event);
66 86 selectedMarker = NO_MARKER;
  87 + DsoSettingsScopeCursor *cursor = cursorInfo[selectedCursor];
67 88 // Capture nearest marker located within snap area (+/- 1% of full scale).
68   - for (unsigned marker = 0; marker < MARKER_COUNT; ++marker) {
69   - if (!scope->horizontal.marker_visible[marker]) continue;
70   - if (fabs(scope->horizontal.marker[marker] - position) < std::min(distance, DIVS_TIME / 100.0)) {
71   - distance = fabs(scope->horizontal.marker[marker] - position);
72   - selectedMarker = marker;
  89 + double dX0 = fabs(cursor->pos[0].x() - position.x());
  90 + double dX1 = fabs(cursor->pos[1].x() - position.x());
  91 + double dY0 = fabs(cursor->pos[0].y() - position.y());
  92 + double dY1 = fabs(cursor->pos[1].y() - position.y());
  93 +
  94 + switch (cursor->shape) {
  95 + case DsoSettingsScopeCursor::RECTANGULAR:
  96 + if (std::min(dX0, dX1) < 1.0 / DIVS_SUB && std::min(dY0, dY1) < 1.0 / DIVS_SUB) {
  97 + // Do we need to swap Y-coords?
  98 + if ((dX0 < dX1 && dY0 > dY1) || (dX0 > dX1 && dY0 < dY1)) {
  99 + std::swap(cursor->pos[0].ry(), cursor->pos[1].ry());
  100 + }
  101 + selectedMarker = (dX0 < dX1) ? 0 : 1;
73 102 }
  103 + break;
  104 + case DsoSettingsScopeCursor::VERTICAL:
  105 + if (dX0 < dX1) {
  106 + if (dX0 < 1.0 / DIVS_SUB) selectedMarker = 0;
  107 + } else {
  108 + if (dX1 < 1.0 / DIVS_SUB) selectedMarker = 1;
  109 + }
  110 + break;
  111 + case DsoSettingsScopeCursor::HORIZONTAL:
  112 + if (dY0 < dY1) {
  113 + if (dY0 < 1.0 / DIVS_SUB) selectedMarker = 0;
  114 + } else {
  115 + if (dY1 < 1.0 / DIVS_SUB) selectedMarker = 1;
  116 + }
  117 + break;
  118 + case DsoSettingsScopeCursor::NONE:
  119 + break;
  120 + default:
  121 + break;
  122 + }
  123 + if (selectedMarker != NO_MARKER) {
  124 + cursorInfo[selectedCursor]->pos[selectedMarker] = position;
  125 + if (selectedCursor == 0) emit markerMoved(selectedCursor, selectedMarker);
74 126 }
75   - if (selectedMarker != NO_MARKER) { emit markerMoved(selectedMarker, position); }
76 127 }
77 128 event->accept();
78 129 }
79 130  
80 131 void GlScope::mouseMoveEvent(QMouseEvent *event) {
81   - if (!zoomed && (event->buttons() & Qt::LeftButton) != 0) {
82   - double position = (double)(event->x() - width() / 2) * DIVS_TIME / (double)width();
  132 + if (!(zoomed && selectedCursor == 0) && (event->buttons() & Qt::LeftButton) != 0) {
  133 + QPointF position = eventToPosition(event);
83 134 if (selectedMarker == NO_MARKER) {
84 135 // User started draging outside the snap area of any marker:
85 136 // move all markers to current position and select last marker in the array.
86 137 for (unsigned marker = 0; marker < MARKER_COUNT; ++marker) {
87   - emit markerMoved(marker, position);
  138 + cursorInfo[selectedCursor]->pos[marker] = position;
  139 + emit markerMoved(selectedCursor, marker);
88 140 selectedMarker = marker;
89 141 }
90 142 } else if (selectedMarker < MARKER_COUNT) {
91   - emit markerMoved(selectedMarker, position);
  143 + cursorInfo[selectedCursor]->pos[selectedMarker] = position;
  144 + emit markerMoved(selectedCursor, selectedMarker);
92 145 }
93 146 }
94 147 event->accept();
95 148 }
96 149  
97 150 void GlScope::mouseReleaseEvent(QMouseEvent *event) {
98   - if (!zoomed && event->button() == Qt::LeftButton) {
99   - double position = (double)(event->x() - width() / 2) * DIVS_TIME / (double)width();
100   - if (selectedMarker < MARKER_COUNT) { emit markerMoved(selectedMarker, position); }
  151 + if (!(zoomed && selectedCursor == 0) && event->button() == Qt::LeftButton) {
  152 + QPointF position = eventToPosition(event);
  153 + if (selectedMarker < MARKER_COUNT) {
  154 + cursorInfo[selectedCursor]->pos[selectedMarker] = position;
  155 + emit markerMoved(selectedCursor, selectedMarker);
  156 + }
101 157 selectedMarker = NO_MARKER;
102 158 }
103 159 event->accept();
... ... @@ -209,12 +265,11 @@ void GlScope::initializeGL() {
209 265 m_marker.create();
210 266 m_marker.bind();
211 267 m_marker.setUsagePattern(QOpenGLBuffer::StaticDraw);
212   - m_marker.allocate(int(vaMarker.size() * sizeof(Line)));
  268 + m_marker.allocate(int(vaMarker.size() * sizeof(Vertices)));
213 269 program->enableAttributeArray(vertexLocation);
214 270 program->setAttributeBuffer(vertexLocation, GL_FLOAT, 0, 3, 0);
215 271 }
216   -
217   - markerUpdated();
  272 + updateCursor();
218 273  
219 274 m_program = std::move(program);
220 275 shaderCompileSuccess = true;
... ... @@ -239,18 +294,65 @@ void GlScope::showData(std::shared_ptr&lt;PPresult&gt; data) {
239 294 update();
240 295 }
241 296  
242   -void GlScope::markerUpdated() {
243   -
244   - for (unsigned marker = 0; marker < vaMarker.size(); ++marker) {
245   - if (!scope->horizontal.marker_visible[marker]) continue;
246   - vaMarker[marker] = {QVector3D((GLfloat)scope->horizontal.marker[marker], -DIVS_VOLTAGE, 0.0f),
247   - QVector3D((GLfloat)scope->horizontal.marker[marker], DIVS_VOLTAGE, 0.0f)};
  297 +void GlScope::generateVertices(unsigned marker, const DsoSettingsScopeCursor &cursor) {
  298 + const float Z_ORDER = 1.0f;
  299 + switch (cursor.shape) {
  300 + case DsoSettingsScopeCursor::NONE:
  301 + vaMarker[marker] = {
  302 + QVector3D(-DIVS_TIME, -DIVS_VOLTAGE, Z_ORDER),
  303 + QVector3D(-DIVS_TIME, DIVS_VOLTAGE, Z_ORDER),
  304 + QVector3D( DIVS_TIME, DIVS_VOLTAGE, Z_ORDER),
  305 + QVector3D( DIVS_TIME, -DIVS_VOLTAGE, Z_ORDER)
  306 + };
  307 + break;
  308 + case DsoSettingsScopeCursor::VERTICAL:
  309 + vaMarker[marker] = {
  310 + QVector3D(cursor.pos[0].x(), -DIVS_VOLTAGE, Z_ORDER),
  311 + QVector3D(cursor.pos[0].x(), DIVS_VOLTAGE, Z_ORDER),
  312 + QVector3D(cursor.pos[1].x(), DIVS_VOLTAGE, Z_ORDER),
  313 + QVector3D(cursor.pos[1].x(), -DIVS_VOLTAGE, Z_ORDER)
  314 + };
  315 + break;
  316 + case DsoSettingsScopeCursor::HORIZONTAL:
  317 + vaMarker[marker] = {
  318 + QVector3D(-DIVS_TIME, cursor.pos[0].y(), Z_ORDER),
  319 + QVector3D( DIVS_TIME, cursor.pos[0].y(), Z_ORDER),
  320 + QVector3D( DIVS_TIME, cursor.pos[1].y(), Z_ORDER),
  321 + QVector3D(-DIVS_TIME, cursor.pos[1].y(), Z_ORDER)
  322 + };
  323 + break;
  324 + case DsoSettingsScopeCursor::RECTANGULAR:
  325 + if ((cursor.pos[1].x() - cursor.pos[0].x()) * (cursor.pos[1].y() - cursor.pos[0].y()) > 0.0) {
  326 + vaMarker[marker] = {
  327 + QVector3D(cursor.pos[0].x(), cursor.pos[0].y(), Z_ORDER),
  328 + QVector3D(cursor.pos[1].x(), cursor.pos[0].y(), Z_ORDER),
  329 + QVector3D(cursor.pos[1].x(), cursor.pos[1].y(), Z_ORDER),
  330 + QVector3D(cursor.pos[0].x(), cursor.pos[1].y(), Z_ORDER)
  331 + };
  332 + } else {
  333 + vaMarker[marker] = {
  334 + QVector3D(cursor.pos[0].x(), cursor.pos[0].y(), Z_ORDER),
  335 + QVector3D(cursor.pos[0].x(), cursor.pos[1].y(), Z_ORDER),
  336 + QVector3D(cursor.pos[1].x(), cursor.pos[1].y(), Z_ORDER),
  337 + QVector3D(cursor.pos[1].x(), cursor.pos[0].y(), Z_ORDER)
  338 + };
  339 + }
  340 + break;
  341 + default:
  342 + break;
248 343 }
  344 +}
249 345  
  346 +void GlScope::updateCursor(unsigned index) {
  347 + if (index > 0) {
  348 + generateVertices(index, *cursorInfo[index]);
  349 + } else for (index = 0; index < cursorInfo.size(); ++index) {
  350 + generateVertices(index, *cursorInfo[index]);
  351 + }
250 352 // Write coordinates to GPU
251 353 makeCurrent();
252 354 m_marker.bind();
253   - m_marker.write(0, vaMarker.data(), vaMarker.size() * sizeof(Line));
  355 + m_marker.write(0, vaMarker.data(), vaMarker.size() * sizeof(Vertices));
254 356 }
255 357  
256 358 void GlScope::paintGL() {
... ... @@ -268,12 +370,13 @@ void GlScope::paintGL() {
268 370 // Apply zoom settings via matrix transformation
269 371 if (zoomed) {
270 372 QMatrix4x4 m;
271   - m.scale(QVector3D(DIVS_TIME / (GLfloat)fabs(scope->horizontal.marker[1] - scope->horizontal.marker[0]), 1.0f,
272   - 1.0f));
273   - m.translate((GLfloat) - (scope->horizontal.marker[0] + scope->horizontal.marker[1]) / 2, 0.0f, 0.0f);
  373 + m.scale(QVector3D(DIVS_TIME / (GLfloat)fabs(scope->getMarker(1) - scope->getMarker(0)), 1.0f, 1.0f));
  374 + m.translate((GLfloat) - (scope->getMarker(0) + scope->getMarker(1)) / 2, 0.0f, 0.0f);
274 375 m_program->setUniformValue(matrixLocation, pmvMatrix * m);
275 376 }
276 377  
  378 + drawMarkers();
  379 +
277 380 unsigned historyIndex = 0;
278 381 for (Graph &graph : m_GraphHistory) {
279 382 for (ChannelID channel = 0; channel < scope->voltage.size(); ++channel) {
... ... @@ -287,8 +390,6 @@ void GlScope::paintGL() {
287 390  
288 391 if (zoomed) { m_program->setUniformValue(matrixLocation, pmvMatrix); }
289 392  
290   - if (!this->zoomed) drawMarkers();
291   -
292 393 drawGrid();
293 394 m_program->release();
294 395 }
... ... @@ -432,21 +533,38 @@ void GlScope::drawGrid() {
432 533 m_vaoGrid[2].release();
433 534 }
434 535  
  536 +void GlScope::drawVertices(QOpenGLFunctions *gl, unsigned marker, QColor color) {
  537 + m_program->setUniformValue(colorLocation, (marker == selectedCursor) ? color : color.darker());
  538 + gl->glDrawArrays(GL_LINE_LOOP, GLint(marker * VERTICES_ARRAY_SIZE), VERTICES_ARRAY_SIZE);
  539 + if (cursorInfo[marker]->shape == DsoSettingsScopeCursor::RECTANGULAR) {
  540 + color.setAlphaF(0.25);
  541 + m_program->setUniformValue(colorLocation, color.darker());
  542 + gl->glDrawArrays(GL_TRIANGLE_FAN, GLint(marker * VERTICES_ARRAY_SIZE), VERTICES_ARRAY_SIZE);
  543 + }
  544 +}
  545 +
435 546 void GlScope::drawMarkers() {
436 547 auto *gl = context()->functions();
437   - QColor trColor = view->screen.markers;
438   - m_program->setUniformValue(colorLocation, trColor);
439 548  
440 549 m_vaoMarker.bind();
441 550  
442   - // Draw all
443   - gl->glLineWidth(1);
444   - gl->glDrawArrays(GL_LINES, 0, (GLsizei)vaMarker.size() * 2);
  551 + unsigned marker = 0;
  552 + drawVertices(gl, marker, view->screen.markers);
  553 + ++marker;
445 554  
446   - // Draw selected
447   - if (selectedMarker != NO_MARKER) {
448   - gl->glLineWidth(3);
449   - gl->glDrawArrays(GL_LINES, selectedMarker * 2, (GLsizei)2);
  555 + if (view->cursorsVisible) {
  556 + gl->glDepthMask(GL_FALSE);
  557 + for (ChannelID channel = 0; channel < scope->voltage.size(); ++channel, ++marker) {
  558 + if (scope->voltage[channel].used) {
  559 + drawVertices(gl, marker, view->screen.voltage[channel]);
  560 + }
  561 + }
  562 + for (ChannelID channel = 0; channel < scope->spectrum.size(); ++channel, ++marker) {
  563 + if (scope->spectrum[channel].used) {
  564 + drawVertices(gl, marker, view->screen.spectrum[channel]);
  565 + }
  566 + }
  567 + gl->glDepthMask(GL_TRUE);
450 568 }
451 569  
452 570 m_vaoMarker.release();
... ...
openhantek/src/glscope.h
... ... @@ -18,6 +18,7 @@
18 18  
19 19 struct DsoSettingsView;
20 20 struct DsoSettingsScope;
  21 +struct DsoSettingsScopeCursor;
21 22 class PPresult;
22 23  
23 24 /// \brief OpenGL accelerated widget that displays the oscilloscope screen.
... ... @@ -40,7 +41,8 @@ class GlScope : public QOpenGLWidget {
40 41 * @param data
41 42 */
42 43 void showData(std::shared_ptr<PPresult> data);
43   - void markerUpdated();
  44 + void updateCursor(unsigned index = 0);
  45 + void cursorSelected(unsigned index) { selectedCursor = index; updateCursor(index); }
44 46  
45 47 protected:
46 48 /// \brief Initializes the scope widget.
... ... @@ -70,11 +72,14 @@ class GlScope : public QOpenGLWidget {
70 72 void drawGrid();
71 73 /// Draw vertical lines at marker positions
72 74 void drawMarkers();
  75 + void generateVertices(unsigned marker, const DsoSettingsScopeCursor &cursor);
  76 + void drawVertices(QOpenGLFunctions *gl, unsigned marker, QColor color);
73 77  
74 78 void drawVoltageChannelGraph(ChannelID channel, Graph &graph, int historyIndex);
75 79 void drawSpectrumChannelGraph(ChannelID channel, Graph &graph, int historyIndex);
  80 + QPointF eventToPosition(QMouseEvent *event);
76 81 signals:
77   - void markerMoved(unsigned marker, double position);
  82 + void markerMoved(unsigned cursorIndex, unsigned marker);
78 83  
79 84 private:
80 85 // User settings
... ... @@ -85,16 +90,20 @@ class GlScope : public QOpenGLWidget {
85 90 // Marker
86 91 const unsigned NO_MARKER = UINT_MAX;
87 92 #pragma pack(push, 1)
88   - struct Line {
89   - QVector3D x;
90   - QVector3D y;
  93 + struct Vertices {
  94 + QVector3D a, b, c, d;
91 95 };
92 96 #pragma pack(pop)
93   - std::vector<Line> vaMarker;
  97 + const unsigned VERTICES_ARRAY_SIZE = sizeof(Vertices) / sizeof(QVector3D);
  98 + std::vector<Vertices> vaMarker;
94 99 unsigned selectedMarker = NO_MARKER;
95 100 QOpenGLBuffer m_marker;
96 101 QOpenGLVertexArrayObject m_vaoMarker;
97 102  
  103 + // Cursors
  104 + std::vector<DsoSettingsScopeCursor *> cursorInfo;
  105 + unsigned selectedCursor = 0;
  106 +
98 107 // Grid
99 108 QOpenGLBuffer m_grid;
100 109 QOpenGLVertexArrayObject m_vaoGrid[3];
... ...
openhantek/src/mainwindow.cpp
... ... @@ -39,6 +39,7 @@ MainWindow::MainWindow(HantekDsoControl *dsoControl, DsoSettings *settings, Expo
39 39 ui->actionManualCommand->setIcon(iconFont->icon(fa::edit));
40 40 ui->actionDigital_phosphor->setIcon(QIcon(":/images/digitalphosphor.svg"));
41 41 ui->actionZoom->setIcon(iconFont->icon(fa::crop));
  42 + ui->actionCursors->setIcon(iconFont->icon(fa::crosshairs));
42 43  
43 44 // Window title
44 45 setWindowIcon(QIcon(":openhantek.png"));
... ... @@ -160,6 +161,7 @@ MainWindow::MainWindow(HantekDsoControl *dsoControl, DsoSettings *settings, Expo
160 161 auto usedChanged = [this, dsoControl, spec](ChannelID channel, bool used) {
161 162 if (channel >= (unsigned int)mSettings->scope.voltage.size()) return;
162 163  
  164 +// if (!used) dsoWidget->
163 165 bool mathUsed = mSettings->scope.anyUsed(spec->channels);
164 166  
165 167 // Normal channel, check if voltage/spectrum or math channel is used
... ... @@ -267,6 +269,18 @@ MainWindow::MainWindow(HantekDsoControl *dsoControl, DsoSettings *settings, Expo
267 269 });
268 270 ui->actionZoom->setChecked(mSettings->view.zoom);
269 271  
  272 + connect(ui->actionCursors, &QAction::toggled, [this](bool enabled) {
  273 + mSettings->view.cursorsVisible = enabled;
  274 +
  275 + if (mSettings->view.cursorsVisible)
  276 + this->ui->actionCursors->setStatusTip(tr("Hide measurements"));
  277 + else
  278 + this->ui->actionCursors->setStatusTip(tr("Show measurements"));
  279 +
  280 + this->dsoWidget->updateCursorGrid(enabled);
  281 + });
  282 + ui->actionCursors->setChecked(mSettings->view.cursorsVisible);
  283 +
270 284 connect(ui->actionAbout, &QAction::triggered, [this]() {
271 285 QMessageBox::about(
272 286 this, tr("About OpenHantek %1").arg(VERSION),
... ...
openhantek/src/mainwindow.ui
... ... @@ -20,7 +20,7 @@
20 20 <x>0</x>
21 21 <y>0</y>
22 22 <width>800</width>
23   - <height>25</height>
  23 + <height>28</height>
24 24 </rect>
25 25 </property>
26 26 <widget class="QMenu" name="menuFile">
... ... @@ -39,6 +39,8 @@
39 39 </property>
40 40 <addaction name="actionDigital_phosphor"/>
41 41 <addaction name="actionZoom"/>
  42 + <addaction name="actionCursors"/>
  43 + <addaction name="separator"/>
42 44 <addaction name="actionManualCommand"/>
43 45 </widget>
44 46 <widget class="QMenu" name="menuOscilloscope">
... ... @@ -84,6 +86,7 @@
84 86 <addaction name="separator"/>
85 87 <addaction name="actionDigital_phosphor"/>
86 88 <addaction name="actionZoom"/>
  89 + <addaction name="actionCursors"/>
87 90 <addaction name="separator"/>
88 91 </widget>
89 92 <action name="actionOpen">
... ... @@ -167,6 +170,14 @@
167 170 <string>Manual command</string>
168 171 </property>
169 172 </action>
  173 + <action name="actionCursors">
  174 + <property name="checkable">
  175 + <bool>true</bool>
  176 + </property>
  177 + <property name="text">
  178 + <string>Cursors</string>
  179 + </property>
  180 + </action>
170 181 </widget>
171 182 <resources/>
172 183 <connections/>
... ...
openhantek/src/scopesettings.h
... ... @@ -3,6 +3,7 @@
3 3 #pragma once
4 4  
5 5 #include <QString>
  6 +#include <QPointF>
6 7  
7 8 #include "hantekdso/controlspecification.h"
8 9 #include "hantekdso/enums.h"
... ... @@ -12,12 +13,22 @@
12 13 #define MARKER_COUNT 2 ///< Number of markers
13 14 #define MARKER_STEP (DIVS_TIME / 100.0)
14 15  
  16 +/// \brief Holds the cursor parameters
  17 +struct DsoSettingsScopeCursor {
  18 + enum CursorShape {
  19 + NONE,
  20 + HORIZONTAL,
  21 + VERTICAL,
  22 + RECTANGULAR
  23 + } shape = NONE;
  24 + QPointF pos[MARKER_COUNT] = {{-1.0, -1.0}, {1.0, 1.0}}; ///< Position in div
  25 +};
  26 +
15 27 /// \brief Holds the settings for the horizontal axis.
16 28 struct DsoSettingsScopeHorizontal {
17 29 Dso::GraphFormat format = Dso::GraphFormat::TY; ///< Graph drawing mode of the scope
18 30 double frequencybase = 1e3; ///< Frequencybase in Hz/div
19   - double marker[MARKER_COUNT] = {-1.0, 1.0}; ///< Marker positions in div
20   - bool marker_visible[MARKER_COUNT] = {true, true};
  31 + DsoSettingsScopeCursor cursor;
21 32  
22 33 unsigned int recordLength = 0; ///< Sample count
23 34  
... ... @@ -39,25 +50,27 @@ struct DsoSettingsScopeTrigger {
39 50 unsigned swTriggerSampleSet = 11; ///< Software trigger, sample set
40 51 };
41 52  
  53 +/// \brief Base for DsoSettingsScopeSpectrum and DsoSettingsScopeVoltage
  54 +struct DsoSettingsScopeChannel {
  55 + QString name; ///< Name of this channel
  56 + bool used = false; ///< true if the channel is turned on
  57 + DsoSettingsScopeCursor cursor;
  58 +};
  59 +
42 60 /// \brief Holds the settings for the spectrum analysis.
43   -struct DsoSettingsScopeSpectrum {
44   - ChannelID channel;
45   - double magnitude = 20.0; ///< The vertical resolution in dB/div
46   - QString name; ///< Name of this channel
  61 +struct DsoSettingsScopeSpectrum : public DsoSettingsScopeChannel {
47 62 double offset = 0.0; ///< Vertical offset in divs
48   - bool used = false; ///< true if the spectrum is turned on
  63 + double magnitude = 20.0; ///< The vertical resolution in dB/div
49 64 };
50 65  
51 66 /// \brief Holds the settings for the normal voltage graphs.
52 67 /// TODO Use ControlSettingsVoltage
53   -struct DsoSettingsScopeVoltage {
  68 +struct DsoSettingsScopeVoltage : public DsoSettingsScopeChannel {
54 69 double offset = 0.0; ///< Vertical offset in divs
55 70 double trigger = 0.0; ///< Trigger level in V
56 71 unsigned gainStepIndex = 6; ///< The vertical resolution in V/div (default = 1.0)
57 72 unsigned couplingOrMathIndex = 0; ///< Different index: coupling for real- and mode for math-channels
58   - QString name; ///< Name of this channel
59 73 bool inverted = false; ///< true if the channel is inverted (mirrored on cross-axis)
60   - bool used = false; ///< true if this channel is enabled
61 74 };
62 75  
63 76 /// \brief Holds the settings for the oscilloscope.
... ... @@ -77,4 +90,11 @@ struct DsoSettingsScope {
77 90 }
78 91 // Channels, including math channels
79 92 unsigned countChannels() const { return (unsigned)voltage.size(); }
  93 +
  94 + double getMarker(unsigned int marker) const {
  95 + return marker < MARKER_COUNT ? horizontal.cursor.pos[marker].x() : 0.0;
  96 + }
  97 + void setMarker(unsigned int marker, double value) {
  98 + if (marker < MARKER_COUNT) horizontal.cursor.pos[marker].setX(value);
  99 + }
80 100 };
... ...
openhantek/src/settings.cpp
... ... @@ -75,10 +75,10 @@ void DsoSettings::load() {
75 75 if (store->contains("format")) scope.horizontal.format = (Dso::GraphFormat)store->value("format").toInt();
76 76 if (store->contains("frequencybase"))
77 77 scope.horizontal.frequencybase = store->value("frequencybase").toDouble();
78   - for (int marker = 0; marker < 2; ++marker) {
  78 + for (int marker = 0; marker < MARKER_COUNT; ++marker) {
79 79 QString name;
80 80 name = QString("marker%1").arg(marker);
81   - if (store->contains(name)) scope.horizontal.marker[marker] = store->value(name).toDouble();
  81 + if (store->contains(name)) scope.setMarker(marker, store->value(name).toDouble());
82 82 }
83 83 if (store->contains("timebase")) scope.horizontal.timebase = store->value("timebase").toDouble();
84 84 if (store->contains("recordLength")) scope.horizontal.recordLength = store->value("recordLength").toUInt();
... ... @@ -100,6 +100,17 @@ void DsoSettings::load() {
100 100 scope.spectrum[channel].magnitude = store->value("magnitude").toDouble();
101 101 if (store->contains("offset")) scope.spectrum[channel].offset = store->value("offset").toDouble();
102 102 if (store->contains("used")) scope.spectrum[channel].used = store->value("used").toBool();
  103 + store->beginGroup("cursor");
  104 + if (store->contains("shape")) scope.spectrum[channel].cursor.shape =
  105 + DsoSettingsScopeCursor::CursorShape(store->value("shape").toUInt());
  106 + for (int marker = 0; marker < MARKER_COUNT; ++marker) {
  107 + QString name;
  108 + name = QString("x%1").arg(marker);
  109 + if (store->contains(name)) scope.spectrum[channel].cursor.pos[marker].setX(store->value(name).toDouble());
  110 + name = QString("y%1").arg(marker);
  111 + if (store->contains(name)) scope.spectrum[channel].cursor.pos[marker].setY(store->value(name).toDouble());
  112 + }
  113 + store->endGroup();
103 114 store->endGroup();
104 115 }
105 116 // Vertical axis
... ... @@ -112,6 +123,17 @@ void DsoSettings::load() {
112 123 if (store->contains("offset")) scope.voltage[channel].offset = store->value("offset").toDouble();
113 124 if (store->contains("trigger")) scope.voltage[channel].trigger = store->value("trigger").toDouble();
114 125 if (store->contains("used")) scope.voltage[channel].used = store->value("used").toBool();
  126 + store->beginGroup("cursor");
  127 + if (store->contains("shape")) scope.voltage[channel].cursor.shape =
  128 + DsoSettingsScopeCursor::CursorShape(store->value("shape").toUInt());
  129 + for (int marker = 0; marker < MARKER_COUNT; ++marker) {
  130 + QString name;
  131 + name = QString("x%1").arg(marker);
  132 + if (store->contains(name)) scope.voltage[channel].cursor.pos[marker].setX(store->value(name).toDouble());
  133 + name = QString("y%1").arg(marker);
  134 + if (store->contains(name)) scope.voltage[channel].cursor.pos[marker].setY(store->value(name).toDouble());
  135 + }
  136 + store->endGroup();
115 137 store->endGroup();
116 138 }
117 139  
... ... @@ -159,7 +181,10 @@ void DsoSettings::load() {
159 181 if (store->contains("interpolation"))
160 182 view.interpolation = (Dso::InterpolationMode)store->value("interpolation").toInt();
161 183 if (store->contains("screenColorImages")) view.screenColorImages = store->value("screenColorImages").toBool();
162   - if (store->contains("zoom")) view.zoom = (Dso::InterpolationMode)store->value("zoom").toBool();
  184 + if (store->contains("zoom")) view.zoom = store->value("zoom").toBool();
  185 + if (store->contains("cursorGridPosition"))
  186 + view.cursorGridPosition = (Qt::ToolBarArea)store->value("cursorGridPosition").toUInt();
  187 + if (store->contains("cursorsVisible")) view.cursorsVisible = store->value("cursorsVisible").toBool();
163 188 store->endGroup();
164 189  
165 190 store->beginGroup("window");
... ... @@ -184,8 +209,8 @@ void DsoSettings::save() {
184 209 store->beginGroup("horizontal");
185 210 store->setValue("format", scope.horizontal.format);
186 211 store->setValue("frequencybase", scope.horizontal.frequencybase);
187   - for (int marker = 0; marker < 2; ++marker)
188   - store->setValue(QString("marker%1").arg(marker), scope.horizontal.marker[marker]);
  212 + for (int marker = 0; marker < MARKER_COUNT; ++marker)
  213 + store->setValue(QString("marker%1").arg(marker), scope.getMarker(marker));
189 214 store->setValue("timebase", scope.horizontal.timebase);
190 215 store->setValue("recordLength", scope.horizontal.recordLength);
191 216 store->setValue("samplerate", scope.horizontal.samplerate);
... ... @@ -205,6 +230,16 @@ void DsoSettings::save() {
205 230 store->setValue("magnitude", scope.spectrum[channel].magnitude);
206 231 store->setValue("offset", scope.spectrum[channel].offset);
207 232 store->setValue("used", scope.spectrum[channel].used);
  233 + store->beginGroup("cursor");
  234 + store->setValue("shape", scope.spectrum[channel].cursor.shape);
  235 + for (int marker = 0; marker < MARKER_COUNT; ++marker) {
  236 + QString name;
  237 + name = QString("x%1").arg(marker);
  238 + store->setValue(name, scope.spectrum[channel].cursor.pos[marker].x());
  239 + name = QString("y%1").arg(marker);
  240 + store->setValue(name, scope.spectrum[channel].cursor.pos[marker].y());
  241 + }
  242 + store->endGroup();
208 243 store->endGroup();
209 244 }
210 245 // Vertical axis
... ... @@ -216,6 +251,16 @@ void DsoSettings::save() {
216 251 store->setValue("offset", scope.voltage[channel].offset);
217 252 store->setValue("trigger", scope.voltage[channel].trigger);
218 253 store->setValue("used", scope.voltage[channel].used);
  254 + store->beginGroup("cursor");
  255 + store->setValue("shape", scope.voltage[channel].cursor.shape);
  256 + for (int marker = 0; marker < MARKER_COUNT; ++marker) {
  257 + QString name;
  258 + name = QString("x%1").arg(marker);
  259 + store->setValue(name, scope.voltage[channel].cursor.pos[marker].x());
  260 + name = QString("y%1").arg(marker);
  261 + store->setValue(name, scope.voltage[channel].cursor.pos[marker].y());
  262 + }
  263 + store->endGroup();
219 264 store->endGroup();
220 265 }
221 266  
... ... @@ -259,6 +304,8 @@ void DsoSettings::save() {
259 304 store->setValue("interpolation", view.interpolation);
260 305 store->setValue("screenColorImages", view.screenColorImages);
261 306 store->setValue("zoom", view.zoom);
  307 + store->setValue("cursorGridPosition", view.cursorGridPosition);
  308 + store->setValue("cursorsVisible", view.cursorsVisible);
262 309 store->endGroup();
263 310  
264 311 store->beginGroup("window");
... ...
openhantek/src/viewsettings.h
... ... @@ -42,6 +42,8 @@ struct DsoSettingsView {
42 42 Dso::InterpolationMode interpolation = Dso::INTERPOLATION_LINEAR; ///< Interpolation mode for the graph
43 43 bool screenColorImages = false; ///< true exports images with screen colors
44 44 bool zoom = false; ///< true if the magnified scope is enabled
  45 + Qt::ToolBarArea cursorGridPosition = Qt::RightToolBarArea;
  46 + bool cursorsVisible = false;
45 47  
46 48 unsigned digitalPhosphorDraws() const {
47 49 return digitalPhosphor ? digitalPhosphorDepth : 1;
... ...
openhantek/src/widgets/datagrid.cpp 0 → 100644
  1 +// SPDX-License-Identifier: GPL-2.0+
  2 +
  3 +#include "datagrid.h"
  4 +
  5 +#include <QGridLayout>
  6 +#include <QLabel>
  7 +#include <QPushButton>
  8 +#include <QButtonGroup>
  9 +
  10 +DataGrid::DataGrid(QWidget *parent) : QGroupBox(parent)
  11 +{
  12 + cursorsLayout = new QGridLayout();
  13 + cursorsLayout->setSpacing(5);
  14 + cursorsSelectorGroup = new QButtonGroup();
  15 + cursorsSelectorGroup->setExclusive(true);
  16 +
  17 + connect(cursorsSelectorGroup,
  18 + static_cast<void(QButtonGroup::*)(int)>(&QButtonGroup::buttonPressed), [this] (int index) {
  19 + emit itemSelected(index);
  20 + });
  21 +
  22 + setLayout(cursorsLayout);
  23 + setFixedWidth(180);
  24 +}
  25 +
  26 +DataGrid::CursorInfo::CursorInfo() {
  27 + selector = new QPushButton();
  28 + selector->setCheckable(true);
  29 + shape = new QPushButton();
  30 + deltaXLabel = new QLabel();
  31 + deltaXLabel->setAlignment(Qt::AlignRight);
  32 + deltaYLabel = new QLabel();
  33 + deltaYLabel->setAlignment(Qt::AlignRight);
  34 +}
  35 +
  36 +void DataGrid::CursorInfo::configure(const QString &text, const QColor &bgColor, const QColor &fgColor) {
  37 + palette.setColor(QPalette::Background, bgColor);
  38 + palette.setColor(QPalette::WindowText, fgColor);
  39 +
  40 + selector->setText(text);
  41 + selector->setStyleSheet(QString(R"(
  42 + QPushButton {
  43 + color: %2;
  44 + background-color: %1;
  45 + border: 1px solid %2;
  46 + }
  47 + QPushButton:checked {
  48 + color: %1;
  49 + background-color: %2;
  50 + }
  51 + QPushButton:disabled {
  52 + color: %3;
  53 + border: 1px dotted %2;
  54 + }
  55 + )").arg(bgColor.name(QColor::HexArgb))
  56 + .arg(fgColor.name(QColor::HexArgb))
  57 + .arg(fgColor.darker().name(QColor::HexArgb)));
  58 +
  59 + shape->setStyleSheet(QString(R"(
  60 + QPushButton {
  61 + color: %2;
  62 + background-color: %1;
  63 + border: none
  64 + }
  65 + )").arg(bgColor.name(QColor::HexArgb))
  66 + .arg(fgColor.name(QColor::HexArgb)));
  67 +
  68 + deltaXLabel->setPalette(palette);
  69 + deltaYLabel->setPalette(palette);
  70 +}
  71 +
  72 +void DataGrid::setBackgroundColor(const QColor &bgColor) {
  73 + backgroundColor = bgColor;
  74 + for (auto it : items) {
  75 + it.configure(it.selector->text(), bgColor, it.palette.color(QPalette::WindowText));
  76 + }
  77 +}
  78 +
  79 +void DataGrid::configureItem(unsigned index, const QColor &fgColor) {
  80 + if (index < items.size()) {
  81 + items[index].configure(items[index].selector->text(), backgroundColor, fgColor);
  82 + }
  83 +}
  84 +
  85 +unsigned DataGrid::addItem(const QString &text, const QColor &fgColor) {
  86 + unsigned index = items.size();
  87 + items.resize(index + 1);
  88 +
  89 + CursorInfo& info = items.at(index);
  90 + info.configure(text, backgroundColor, fgColor);
  91 + cursorsSelectorGroup->addButton(info.selector, index);
  92 +
  93 + connect(info.shape, &QPushButton::clicked, [this, index] () {
  94 + emit itemUpdated(index);
  95 + });
  96 +
  97 + cursorsLayout->addWidget(info.selector, 3 * index, 0);
  98 + cursorsLayout->addWidget(info.shape, 3 * index, 1);
  99 + cursorsLayout->addWidget(info.deltaXLabel, 3 * index + 1, 0);
  100 + cursorsLayout->addWidget(info.deltaYLabel, 3 * index + 1, 1);
  101 + cursorsLayout->setRowMinimumHeight(3 * index + 2, 10);
  102 + cursorsLayout->setRowStretch(3 * index, 0);
  103 + cursorsLayout->setRowStretch(3 * index + 3, 1);
  104 +
  105 + return index;
  106 +}
  107 +
  108 +void DataGrid::updateInfo(unsigned index, bool visible, const QString &strShape, const QString &strX, const QString &strY) {
  109 + if (index >= items.size()) return;
  110 + CursorInfo &info = items.at(index);
  111 + info.selector->setEnabled(visible);
  112 + if (visible) {
  113 + info.shape->setText(strShape);
  114 + info.deltaXLabel->setText(strX);
  115 + info.deltaYLabel->setText(strY);
  116 + } else {
  117 + info.shape->setText(QString());
  118 + info.deltaXLabel->setText(QString());
  119 + info.deltaYLabel->setText(QString());
  120 + }
  121 +}
  122 +
  123 +void DataGrid::selectItem(unsigned index) {
  124 + if (index >= items.size()) return;
  125 + items[index].selector->setChecked(true);
  126 +}
... ...
openhantek/src/widgets/datagrid.h 0 → 100644
  1 +// SPDX-License-Identifier: GPL-2.0+
  2 +
  3 +#pragma once
  4 +
  5 +#include <QGroupBox>
  6 +#include <QPalette>
  7 +
  8 +class QPushButton;
  9 +class QButtonGroup;
  10 +class QLabel;
  11 +class QGridLayout;
  12 +
  13 +class DataGrid : public QGroupBox
  14 +{
  15 + Q_OBJECT
  16 +public:
  17 + explicit DataGrid(QWidget *parent = nullptr);
  18 +
  19 + struct CursorInfo {
  20 + QPalette palette; ///< The widget's palette
  21 + QPushButton *selector; ///< The name of the channel
  22 + QPushButton *shape; ///< The cursor shape
  23 + QLabel *deltaXLabel; ///< The horizontal distance between cursors
  24 + QLabel *deltaYLabel; ///< The vertical distance between cursors
  25 +
  26 + CursorInfo();
  27 + void configure(const QString &text, const QColor &bgColor, const QColor &fgColor);
  28 + };
  29 +
  30 + unsigned addItem(const QString &text, const QColor &fgColor);
  31 + void setBackgroundColor(const QColor &bgColor);
  32 + void configureItem(unsigned index, const QColor &fgColor);
  33 + void updateInfo(unsigned index, bool visible, const QString &strShape = QString(),
  34 + const QString &strX = QString(), const QString &strY = QString());
  35 +
  36 +signals:
  37 + void itemSelected(unsigned index);
  38 + void itemUpdated(unsigned index);
  39 +
  40 +public slots:
  41 + void selectItem(unsigned index);
  42 +
  43 +private:
  44 + QColor backgroundColor;
  45 + QButtonGroup *cursorsSelectorGroup;
  46 + QGridLayout *cursorsLayout;
  47 + std::vector<CursorInfo> items;
  48 +};
... ...
openhantek/src/widgets/sispinbox.cpp
... ... @@ -160,18 +160,19 @@ void SiSpinBox::setMode(const int mode) { this-&gt;mode = mode; }
160 160  
161 161 /// \brief Generic initializations.
162 162 void SiSpinBox::init() {
163   - this->setMinimum(1e-12);
164   - this->setMaximum(1e12);
165   - this->setValue(1.0);
166   - this->setDecimals(DBL_MAX_10_EXP + DBL_DIG); // Disable automatic rounding
167   - this->unit = unit;
168   - this->steps << 1.0 << 2.0 << 5.0 << 10.0;
169   -
170   - this->steppedTo = false;
171   - this->stepId = 0;
172   - this->mode = 0;
173   -
174   - connect(this, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this, &SiSpinBox::resetSteppedTo);
  163 + setMinimum(1e-12);
  164 + setMaximum(1e12);
  165 + setValue(1.0);
  166 + setDecimals(DBL_MAX_10_EXP + DBL_DIG); // Disable automatic rounding
  167 + setFocusPolicy(Qt::NoFocus);
  168 + steps << 1.0 << 2.0 << 5.0 << 10.0;
  169 +
  170 + steppedTo = false;
  171 + stepId = 0;
  172 + mode = 0;
  173 +
  174 + connect(this, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged),
  175 + this, &SiSpinBox::resetSteppedTo);
175 176 }
176 177  
177 178 /// \brief Resets the ::steppedTo flag after the value has been changed.
... ...