From cf3e9de4d0c6f8a9994226b16f102a0bf231a917 2017-11-22 11:03:14 From: Thibaud Rabillard Date: 2017-11-22 11:03:14 Subject: [PATCH] Merge branch 'feature/InteractionModesAndCursors' into develop --- diff --git a/app/src/MainWindow.cpp b/app/src/MainWindow.cpp index 09f7870..aa9c8ee 100644 --- a/app/src/MainWindow.cpp +++ b/app/src/MainWindow.cpp @@ -168,9 +168,9 @@ MainWindow::MainWindow(QWidget *parent) openInspector(checked, true, openRightInspectorAction); }); - // //// // - // Menu // - // //// // + // //////////////// // + // Menu and Toolbar // + // //////////////// // this->menuBar()->addAction(tr("File")); auto toolsMenu = this->menuBar()->addMenu(tr("Tools")); toolsMenu->addAction(tr("Settings..."), [this]() { @@ -189,6 +189,105 @@ MainWindow::MainWindow(QWidget *parent) auto timeWidget = new TimeWidget{}; mainToolBar->addWidget(timeWidget); + auto actionPointerMode = new QAction{QIcon(":/icones/pointer.png"), "Pointer", this}; + actionPointerMode->setCheckable(true); + actionPointerMode->setChecked(sqpApp->plotsInteractionMode() + == SqpApplication::PlotsInteractionMode::None); + connect(actionPointerMode, &QAction::triggered, + []() { sqpApp->setPlotsInteractionMode(SqpApplication::PlotsInteractionMode::None); }); + + auto actionZoomMode = new QAction{QIcon(":/icones/zoom.png"), "Zoom", this}; + actionZoomMode->setCheckable(true); + actionZoomMode->setChecked(sqpApp->plotsInteractionMode() + == SqpApplication::PlotsInteractionMode::ZoomBox); + connect(actionZoomMode, &QAction::triggered, []() { + sqpApp->setPlotsInteractionMode(SqpApplication::PlotsInteractionMode::ZoomBox); + }); + + auto actionOrganisationMode = new QAction{QIcon(":/icones/drag.png"), "Organize", this}; + actionOrganisationMode->setCheckable(true); + actionOrganisationMode->setChecked(sqpApp->plotsInteractionMode() + == SqpApplication::PlotsInteractionMode::DragAndDrop); + connect(actionOrganisationMode, &QAction::triggered, []() { + sqpApp->setPlotsInteractionMode(SqpApplication::PlotsInteractionMode::DragAndDrop); + }); + + auto actionZonesMode = new QAction{QIcon(":/icones/rectangle.png"), "Zones", this}; + actionZonesMode->setCheckable(true); + actionZonesMode->setChecked(sqpApp->plotsInteractionMode() + == SqpApplication::PlotsInteractionMode::SelectionZones); + connect(actionZonesMode, &QAction::triggered, []() { + sqpApp->setPlotsInteractionMode(SqpApplication::PlotsInteractionMode::SelectionZones); + }); + + auto modeActionGroup = new QActionGroup{this}; + modeActionGroup->addAction(actionZoomMode); + modeActionGroup->addAction(actionZonesMode); + modeActionGroup->addAction(actionOrganisationMode); + modeActionGroup->addAction(actionPointerMode); + modeActionGroup->setExclusive(true); + + mainToolBar->addSeparator(); + mainToolBar->addAction(actionPointerMode); + mainToolBar->addAction(actionZoomMode); + mainToolBar->addAction(actionOrganisationMode); + mainToolBar->addAction(actionZonesMode); + mainToolBar->addSeparator(); + + auto btnCursor = new QToolButton{this}; + btnCursor->setIcon(QIcon(":/icones/cursor.png")); + btnCursor->setText("Cursor"); + btnCursor->setToolTip("Cursor"); + btnCursor->setPopupMode(QToolButton::InstantPopup); + auto cursorMenu = new QMenu("CursorMenu", this); + btnCursor->setMenu(cursorMenu); + + auto noCursorAction = cursorMenu->addAction("No Cursor"); + noCursorAction->setCheckable(true); + noCursorAction->setChecked(sqpApp->plotsCursorMode() + == SqpApplication::PlotsCursorMode::NoCursor); + connect(noCursorAction, &QAction::triggered, + []() { sqpApp->setPlotsCursorMode(SqpApplication::PlotsCursorMode::NoCursor); }); + + cursorMenu->addSeparator(); + auto verticalCursorAction = cursorMenu->addAction("Vertical Cursor"); + verticalCursorAction->setCheckable(true); + verticalCursorAction->setChecked(sqpApp->plotsCursorMode() + == SqpApplication::PlotsCursorMode::Vertical); + connect(verticalCursorAction, &QAction::triggered, + []() { sqpApp->setPlotsCursorMode(SqpApplication::PlotsCursorMode::Vertical); }); + + auto temporalCursorAction = cursorMenu->addAction("Temporal Cursor"); + temporalCursorAction->setCheckable(true); + temporalCursorAction->setChecked(sqpApp->plotsCursorMode() + == SqpApplication::PlotsCursorMode::Temporal); + connect(temporalCursorAction, &QAction::triggered, + []() { sqpApp->setPlotsCursorMode(SqpApplication::PlotsCursorMode::Temporal); }); + + auto horizontalCursorAction = cursorMenu->addAction("Horizontal Cursor"); + horizontalCursorAction->setCheckable(true); + horizontalCursorAction->setChecked(sqpApp->plotsCursorMode() + == SqpApplication::PlotsCursorMode::Horizontal); + connect(horizontalCursorAction, &QAction::triggered, + []() { sqpApp->setPlotsCursorMode(SqpApplication::PlotsCursorMode::Horizontal); }); + + auto crossCursorAction = cursorMenu->addAction("Cross Cursor"); + crossCursorAction->setCheckable(true); + crossCursorAction->setChecked(sqpApp->plotsCursorMode() + == SqpApplication::PlotsCursorMode::Cross); + connect(crossCursorAction, &QAction::triggered, + []() { sqpApp->setPlotsCursorMode(SqpApplication::PlotsCursorMode::Cross); }); + + mainToolBar->addWidget(btnCursor); + + auto cursorModeActionGroup = new QActionGroup{this}; + cursorModeActionGroup->setExclusive(true); + cursorModeActionGroup->addAction(noCursorAction); + cursorModeActionGroup->addAction(verticalCursorAction); + cursorModeActionGroup->addAction(temporalCursorAction); + cursorModeActionGroup->addAction(horizontalCursorAction); + cursorModeActionGroup->addAction(crossCursorAction); + // //////// // // Settings // // //////// // diff --git a/gui/include/Common/VisualizationDef.h b/gui/include/Common/VisualizationDef.h index 34f87f4..37194bd 100644 --- a/gui/include/Common/VisualizationDef.h +++ b/gui/include/Common/VisualizationDef.h @@ -4,4 +4,5 @@ /// Minimum height for graph added in zones (in pixels) extern const int GRAPH_MINIMUM_HEIGHT; + #endif // SCIQLOP_VISUALIZATIONDEF_H diff --git a/gui/include/SqpApplication.h b/gui/include/SqpApplication.h index c03124a..c9d37a2 100644 --- a/gui/include/SqpApplication.h +++ b/gui/include/SqpApplication.h @@ -49,6 +49,16 @@ public: /// doesn't live in a thread and access gui DragDropHelper &dragDropHelper() noexcept; + enum class PlotsInteractionMode { None, ZoomBox, DragAndDrop, SelectionZones }; + + enum class PlotsCursorMode { NoCursor, Vertical, Temporal, Horizontal, Cross }; + + PlotsInteractionMode plotsInteractionMode() const; + void setPlotsInteractionMode(PlotsInteractionMode mode); + + PlotsCursorMode plotsCursorMode() const; + void setPlotsCursorMode(PlotsCursorMode mode); + private: class SqpApplicationPrivate; spimpl::unique_impl_ptr impl; diff --git a/gui/include/Visualization/VisualizationCursorItem.h b/gui/include/Visualization/VisualizationCursorItem.h new file mode 100644 index 0000000..f8beb59 --- /dev/null +++ b/gui/include/Visualization/VisualizationCursorItem.h @@ -0,0 +1,26 @@ +#ifndef SCIQLOP_VISUALIZATIONCURSORITEM_H +#define SCIQLOP_VISUALIZATIONCURSORITEM_H + +#include +#include + +class QCustomPlot; + +class VisualizationCursorItem { +public: + VisualizationCursorItem(QCustomPlot *plot); + + void setVisible(bool value); + bool isVisible() const; + + void setPosition(double value); + void setAbsolutePosition(double value); + void setOrientation(Qt::Orientation orientation); + void setLabelText(const QString &text); + +private: + class VisualizationCursorItemPrivate; + spimpl::unique_impl_ptr impl; +}; + +#endif // SCIQLOP_VISUALIZATIONCURSORITEM_H diff --git a/gui/include/Visualization/VisualizationGraphWidget.h b/gui/include/Visualization/VisualizationGraphWidget.h index c97c9ca..19a4cc3 100644 --- a/gui/include/Visualization/VisualizationGraphWidget.h +++ b/gui/include/Visualization/VisualizationGraphWidget.h @@ -62,6 +62,18 @@ public: bool isDragAllowed() const override; void highlightForMerge(bool highlighted) override; + // Cursors + /// Adds or moves the vertical cursor at the specified value on the x-axis + void addVerticalCursor(double time); + /// Adds or moves the vertical cursor at the specified value on the x-axis + void addVerticalCursorAtViewportPosition(double position); + void removeVerticalCursor(); + /// Adds or moves the vertical cursor at the specified value on the y-axis + void addHorizontalCursor(double value); + /// Adds or moves the vertical cursor at the specified value on the y-axis + void addHorizontalCursorAtViewportPosition(double position); + void removeHorizontalCursor(); + signals: void synchronize(const SqpRange &range, const SqpRange &oldRange); void requestDataLoading(QVector > variable, const SqpRange &range, diff --git a/gui/include/Visualization/VisualizationZoneWidget.h b/gui/include/Visualization/VisualizationZoneWidget.h index f364645..211c0e5 100644 --- a/gui/include/Visualization/VisualizationZoneWidget.h +++ b/gui/include/Visualization/VisualizationZoneWidget.h @@ -70,6 +70,10 @@ public: QMimeData *mimeData() const override; bool isDragAllowed() const override; + void notifyMouseMoveInGraph(const QPointF &graphPosition, const QPointF &plotPosition, + VisualizationGraphWidget *graphWidget); + void notifyMouseLeaveGraph(VisualizationGraphWidget *graphWidget); + protected: void closeEvent(QCloseEvent *event) override; diff --git a/gui/meson.build b/gui/meson.build index 35c2317..7748236 100644 --- a/gui/meson.build +++ b/gui/meson.build @@ -76,7 +76,8 @@ gui_sources = [ 'src/Visualization/VisualizationDragWidget.cpp', 'src/Visualization/AxisRenderingUtils.cpp', 'src/Visualization/PlottablesRenderingUtils.cpp', - 'src/Visualization/MacScrollBarStyle.cpp' + 'src/Visualization/MacScrollBarStyle.cpp', + 'src/Visualization/VisualizationCursorItem.cpp' ] gui_inc = include_directories(['include']) diff --git a/gui/resources/icones/cursor.png b/gui/resources/icones/cursor.png new file mode 100644 index 0000000000000000000000000000000000000000..54ab9c64bd9f16c99453eb2c2ef0b9b5cb999a8c GIT binary patch literal 1047 zc%17D@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucLCF%=h?3y^w370~qEv>0#LT=By}Z;C1rt33 zJwr2>%=KRx7?|5VT^vIy;@;jp-F-Gu=J?0L>Dh@#5>EWB6W8=TaraQr#)-YHNs1Rw zXrwBtn%&S?b3=n$bLo`*4xSM^b*4fa{MYSIq9 zpL2gb$Yl%-_sl(f1Guktotd_}%rs`dMFf0S*6?r)~Ow`2Ec&(~zlAi}rjD z@(o*hYf5?8s->&0By4r7e7;m_wb$8@dyy-jt!R4_dVS8h)rDHqdsElF3c6Rh(rk6m zxAyPnyh?qhO07Mo^?pxCcj(Hyr>wQF+FhN%eQI~eZ!h2Aoy%5fzv}&T&b!oOs?~x$ zn}civmcB76KdJi8J~w>nInDQLLcZDNb1-`(Gt87YaNn?@Ny34bS>pgh7KqufcsrYc z^WBjAspWB6YyrV98P9}$zf%2l?({CkD+UcrJPX8_H98nABpjmV%OBwUdgkTASxbtp z8aK>8_3zS-_^F%zn}6T;F64UX%CORSpm$yYEsts~57YWx;OlI*^06QL)L4#_so|UQ zR!@IboF;!ocdO&nsAYTBTVIt6&1%_n-wf!_H}S4Z&9s0Hy&dB0rn!5k^Z8?|8MUqp zq$)=&d2{sSNxk!a;omlH3OYRHd+Bni#a>^-zeOLg)}LBy?{)M0L2K>Mll+>{5NB|> zd+Um3YWe?tQ)ccJKbWT-{0DQ-F&O_)nZct-!^cpB{Li}Y&@UbMVq?aUyGEp!;%|AM$y_;&ZVm+&y)@Owx;1CmuUL@ubb7|O10l#T0gzkE( z&2<5VW(|6wRt&p0YcQku1j8bWYQgU-tY77&nO|{C6`uqN-u#TrpS)N3`dl{O3XB4Y Nc)I$ztaD0e0sy_X$SeQ= literal 0 Hc$@OoTLZ& zdSOW1SrW;lTRYZar|Xj8q%{bN1<|MZ;Cxbg3Q3rB;uLT_l>)Zxa{buVjlnw6^qNE> zyYFTBAIvy9`y2k*7<1F5q_Rn~gxpXeeXeI~3#F`Xrgcx@&7m)K9n+cY17pu2k==o zyOkqui~uJI#~eA=iE_l3xi2d%Us4~4sX06+$~c3`mwUhgD zf+M+3_k!8jD66>fi4~0|F!PYx$3sKVE2VfpG?3ArnB*b^L8&fs`#=C}M(d>JGBz8F zEU}GuI}0apSmpH0O#RPeV`JSnvd9ljL@ORGrR`H2i1JpjwXu`t`g<>xi8_KDV2Uqe(mHk?q;O-N9ljIW?U`||1ibm&dA^X82W5? zsLv8#quQxoph2;hO~5DWf>&JE3-?#K8Xdt)1VPwz=@+?E0^*jgel?E2Fu1=)rx{)g zVgwnuw?=!(`nFKDc4kBfmp8QEot#t?M9WB2TFvAe>&!Sq6U7w-5jFIZ4Uz9^%bF)A z8wf&~>cl9tO5n$Lby?%MH0S^{b8oLVfk~0(RGyBWW#~zvFd2Xb+>Gkj63fZWLnY|c zH^WMjowqp0N0T^Ux8A#{z}L=G!~!5e_K{qrrrApcC3B-Nd2)}kQ{`YL8aDO$fJUo` zQmwuP1FMXe9<=SqzL1rb^(N1~bU%1PUSp~qxcWgu7#)7Jd`Rz?l#kqOp1aMOcPAT|w+M=jU*L|=t}1Kl;EOnCrXDshXk;{}hN;+hak^Lb<)djr zTj@GlX*#v){oQQRird%3eZp$3cscZ6wve5Hk%3RQMx&XxT$LT&sXsEaQp2;T$eYRk zKVL&Hd!PJE>9eH{y6Mal zlrcLd|CxCew@t#^CSd=doP*yoX$XyI2x4WgR%bVk}xU!uAUR2(P?9Z z<19{>Gr=`Kc%<@7aIL)V+F{g>o7+)xR#P3R-K9}MeGH& i=#x0YF&*ja%NgGOmFTvL=>h*Sq`g65tUCdTx&Huy@<=lP literal 0 Hc$@LopDOkQIA7z4}6!}KfMzH~HQmZixqeN4t zxQ<~dl4hl*3642l8<$F2S(>jjb4=6JYHSY8T6f*O*53R7&iC!_|M%YOtX&Z3?}I{` zBLM(FQ7PnLaOP>cjy52eEE4B_b67 z&`M2*bS72BfFI2``@%UPNlZ>gG&>eZj%Fotwk5`Lu*7ZKw>dbGS+&amfKX7$p5LF0 zn!CEC*u=-+OaRu}z)(k}3xo1WScCrs`NleA*ckbx*H@YhgBtSlNno7+e@#a<`6abw zUOd^#$f%T6W>v1QJXl#g^&vf7=5*V=U#;GimzUR-Q^ZtYq$PF;7QWk4+{G4XAPcp$ zIWq8z=)gJRE&cEAZ_)(5N24Ie-{{5BC}O0MYa&$(RPN=3K{pB(7`gt49c@H0r(7MSL^jvicM5 zg9b)Y|Ex?FteS9td1+tgCAKP7C3Yv#16pWDbBXRem$6!56UzRf>+*vx7wHUSX+L!; za2>j$&LSv_DiOUGY>ODLK1Bw1QttSOtw_!12MaLD)=^-hxt*esV+N@8qhW&gNE-pD z{>bIoer9LiC@d{21kFF}S2V904+nMn`+I|nGxlF=UW3Febe*B}YaJJ^0pBWCW4u5Y zj@bb9>?k4mV0QV04l5Iw6G_R+AK;<5*o6X;II8&cfsXl6(Z|UB-r^_{x9l(>78%ZE zM`b^-wf3{euG7K^jIH5ZMXkqppPrDsgvS;5VPL{jn-P_`4FA;9aumAmU za?sW8NYJ=9N;f`0zIwX4^q6Zc7Nn~s=)?edWlDMn0d#uC)eWaZ#(KV1G3{2Bt03R4YAE>X+!O85cGi^M_XC`j*>1dz zP=Rq^Eff%2Zuon+p3#5QZrb46Z*e^FJtQ<)u=$x?i1;Px?dGV8lWf>AQ?FWR-;(x; z1pcD|IWGI6PbY{J@tf6-!ab&fN@s&^*&C9bA^Ib})9XHZKL z7=ucD3LFLvGf0+Y_mptFU(H^PE=F1P@95=sM>gh$f-b%PF_PSC@6I+YJxcAjnBHBb zos1bo_nLG*pT#gFbvUP8s6bpx@iY!0aKvkXXW$B9}sfr8UEe*YX|G={=C!O-Kr8)Vaocx@;<9!g?=lmAAA#Z;jYlRLq10*6NX?WbbD-vb^gy z4VB-mdHk!7)C{g#cT1yZ@!hqKIbe&KCV@*(!*1OwpEqThUg97Uw=Ec<))Udbv=RXM zR_be>e0Ea`86<`$>d-R8H}SjF0H%4}Tm%^LwoBTN?N}BJ&f5X6c2o>!xmWuG$x-K{w2aFhTJJ?@DyV9PT8a`B@75*PMKAIvx%uRDyt3s%mkAwOcw!G1+J-=dY0r1bmEw)7p@^17|m zOjm_)ExVS$WWe(?EUIyZe64o;uH2gfCN415&jgFsxpmg(rHoOZLo}XBcpY^~RY!b| z_ZD{<3J!J{#Tkp^O>KH6bf$O6%?&&bc3v@O;X6qf>5~EsvX#=ajK0WZZ%{$D*uP5K zk+P!)yBZq3tb3!*8>|Cmx{g^C#ErrRV$&z?HWe literal 0 Hc$@A0YqDiGw$NbGC}W6FV;73DjBJ%eWgRhS^hy|OlXWC! zjK)$VhO8sK=zDD6`ySt4-(SDu`2F$B9QXZP*LhvndEfW*oY!;D*4mVl?HC&f1mZL| zJ8uvClYr|JD>Lvr8Uf}6{^)}3O^raW`i_4G28>q?tqegROd9)^7ZWf(5@_Zc3<7br z?Ot?Uh*BspDH3Ah65@dH4GHtQaRn6U$-q11J zb0NPw?f55=4qg>mS#d*F8Mzn(Gu3dlPU3rfd*=+V{7I}gK6AqGy<`W5uiO02pXXEW zs@`306`6eRJ;}uP*z1bUR&>_s1*P$-fP%%co$|TidDW;d%ev=0KGpXj=7Q!aw%&L|=x3$#9O}ckf0X9+G^Xu1bY;A3(E-#{9$uOnp7mv$ude^r$4ack{1NxN=QsRyl#CIU8jL>0} zs54{=rSH;4tg*wy=y6?%U+A5gy zD|h_khxMz%g(jNZ))crcRZ7}u9lyDnDTD?gmtC~Up^^dPn#L(bJqzS|OFB%_J-++# z#%3y`4>x@46hA!Ci)`TMQ6VBINSipdiRYD-C>WO0aIDWKLC{1*l;f>X+dVz@9MuZEAq-OZ z2s15vemYTB^BEt)r1V`f^8Nr|Xkex#fx6Fc(!y+n+DwPOP^8!4LG>ib%9!w0$EHK| zWJQ#cV>s`~u1ev}>=^weW%IbrFC_mTKx0(K{{yO1nv|x$lN-6*Aw-&)@eN&@B~ZVl z>A%t**H_z*p&5c-f|VwT-)1EvrC7L*zrXw;x$zA%XK0uZ7{n1>@Ief ztsIhy>9U{)GbN#DkroK$8O&~^M8!Pk>NrI6wode8LWNdb@w;v&G4N{kGr{Dd-39+V(@Ho1oa^T^-<=bVFwd~<6G2FTmNBJp^QzVDubb&L{G-OqCbn)yhQk?U{3G#ld!`6% z_&IOb{$@zyN4?f`yrN&u%6vG3mAf8X`awGIn^~iH%Cm#DAAN{kC{(@={s-(5>ZNcw zpU;o4rECYQPe_E8C6>FKjiYnh-&W84@b;Ww%tW=eXIlB;X0@`&zENsnD24+ax;=!3 zNA5(K8;%$>idP(Nc5Nw=CsKhSf7I%q%oC5VsR|-bE%KVxm6m0$lJ6V< zy2z7w{)O)%OZsS|@4*($))+Q85-Vt8fclhVPWzf^ligYnr@qG(3a#g)wbXvp<{>b( zu&@vjyaFH7*}sthW#Ja5Lb;v7N-aEqBAqg*^YbIMHhl&C)=^7_G|Jl_zR_E45;ZB& zJF8>!#muAE;BYwY*~-v1r?m>odAJb4iE1~YDc}Jjb5*PaId9(1x?=?HnY)edJ<-kWW6l1 zj9|kD+R0HuBLV(eEE13Y?2?Gyih`iE#d(a5TTe?B8j2||-?~Ody*}soQXL>J0aDet zVnG95PC4{G3CnRB0gb=)T8$XaXcH48$-Y*Wv_Z=g(~s{V-ZmMeNP}?IQW6nMI9CII zh=2N=?Hw&{PR>Q!pVJvK!EtnexkaK%<1%edW)GQI3O8rwv8FPuOb>uI@=f+E%oFva zma%gxb&89aw{MY`H3o{IASsjjmh{t=h=}d=CJzy8@1u0p^}S*zWNwnP(V*Dvjdwv# z+Qdk@`8O9|(UW~|%8|#$dUUzReIpYDOZtimm&#-rSbt>o>$TnRYkA8&?_{C@A)`~F z9-QDZb~Wp~D>647ESPzw*rR_P&9;iC2t>b)_^Sreg3WW*I-87X&Z^n)UM5rgWh+CS>Ohd25~nNmM@QXUfdKws{LB_?4GG7d|)O94-4i7zJ;E=KLT(u@LFZm(;KGpV6x9dIhSxxBNib&jZWNtWco#87wAmsZRfNYKTD&chl$ z-p*1ID2I^mn-6d7p5_jWy{X24DIRnOR{pWUZ#Sw-4ThZ=7IYutGx}h@upG*& zG`hQ`W-iq2#M{g!E^IFvDqJ9hC;{H4KF^Qf3`NQxd%>IuBP=*>k z9m0r;U6UduTuT(n700GQ^1DV@s`5y>;}rpHXO)qLxeg@x%TBQuN~$z^yZiD|@G;-+XavQ7d!Q$78gk3!l}FkT(y8uK zZNJ_ba=dvB??5(0GMmG2ea-H5P+o+Hixa_b0Z1@M$TQqaK37)sWNzAXD_l$#hl9CJ&(u3ZF7pE1}IlH~a0L(}Q2K`Wzo9jPJjr9y=90 zB@Y5pvOmHMoX(#PXFt7Ew;G$KC@i^7v8w|bA<1Y?;hS;I)K-xa-eAsp9t(-117wA< z1rnQ1u?9=r;DIXjF*xGPMG~=H$?K4J(oz|tnc)X zK!33Ma3>nnsfn0<_VW-z&qr-oT4>o*Rmzy!P6^Mno}kY#(n@dmIBF??i;CWAM_AYKUea3$2onyHD*K8IDXG6NEbcH~FIanGs0 z7= z;=o_MUYfk?Gy53Q!d!F$6^lJnxlEa@s8A6G1QwAAeo?6Uez_=-xyS}Zck$!r3nhP2 z`hgRY0yqu55cJPtL6QyDtZL%q{~lb@qgwwu4QRHD<^@a)Iy{kC9Q<+ZgJ>hsJtE6m zDlIoJm?eJ0QGk2d9Q;M?D~@!{EX2QuO;8!$2F%JjPV9i4-aFF2lV5Hz=RO9RZ zUB;~Jt%Ym@CtDLD2oaIAHfC>D6rKAm^bJEs{jT|)<&Eb0U#ZDClWdr%cTG?8w{@$Fcpsp?~Pzws8 zpGLfs6H(H!5?T$SQmJ24X69?zba;%j(Ut(b4nA~eqXN! zLXXGphS*Q2d-m@TIxC-pT-@7-{)v8J?mKb_NWwE3x`zj`-MjvmU7J6&&qS4jj=v zVVv6t4WBZn@KkC!?o+6F3r}kuJXTi14m-BjnyjEE<7YStY{o)|0O-oL4o%iQcGB>c ziKZOWoCHPKP`IIS#{p#Ri457B!kLFjZsrs}ZPDtyP`gJ8iJ}X15{gG^l_P;i8s}kY zR{7T7F>D8%3ko=qo=@tO0nd4Vwu6SoJ4-aNWYFMn$>`lF<6~n_dJWSOTeaan5O5c}%MgdQoyzE@nwE$6~Q7@-qv1@-VxH7~H@u z(zWAfLa9LSoRl24IEH_mQ@@$}%*4(&GV&dNs#?ha-5BZ?r!$st}aL1HXTh&wHh ko`?Ngc@Xk0HTE|H*~C-n#ZRu?Pgo#x6YKM@j39CU19kPdF#rGn literal 0 Hc$@dyti79mhX=c14AC5Jb_HWh5L@5W_+VR06L_ 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 delta 2972 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 BgKz)< literal 0 Hc$@icones/unplot.png icones/up.png icones/time.png + icones/zoom.png + icones/rectangle.png + icones/drag.png + icones/cursor.png + icones/pointer.png diff --git a/gui/src/SqpApplication.cpp b/gui/src/SqpApplication.cpp index 6ddf460..4cc6a83 100644 --- a/gui/src/SqpApplication.cpp +++ b/gui/src/SqpApplication.cpp @@ -21,7 +21,9 @@ public: m_TimeController{std::make_unique()}, m_NetworkController{std::make_unique()}, m_VisualizationController{std::make_unique()}, - m_DragDropHelper{std::make_unique()} + m_DragDropHelper{std::make_unique()}, + m_PlotInterractionMode(SqpApplication::PlotsInteractionMode::None), + m_PlotCursorMode(SqpApplication::PlotsCursorMode::NoCursor) { // /////////////////////////////// // // Connections between controllers // @@ -90,6 +92,9 @@ public: QThread m_VisualizationControllerThread; std::unique_ptr m_DragDropHelper; + + SqpApplication::PlotsInteractionMode m_PlotInterractionMode; + SqpApplication::PlotsCursorMode m_PlotCursorMode; }; @@ -161,3 +166,23 @@ DragDropHelper &SqpApplication::dragDropHelper() noexcept { return *impl->m_DragDropHelper; } + +SqpApplication::PlotsInteractionMode SqpApplication::plotsInteractionMode() const +{ + return impl->m_PlotInterractionMode; +} + +void SqpApplication::setPlotsInteractionMode(SqpApplication::PlotsInteractionMode mode) +{ + impl->m_PlotInterractionMode = mode; +} + +SqpApplication::PlotsCursorMode SqpApplication::plotsCursorMode() const +{ + return impl->m_PlotCursorMode; +} + +void SqpApplication::setPlotsCursorMode(SqpApplication::PlotsCursorMode mode) +{ + impl->m_PlotCursorMode = mode; +} diff --git a/gui/src/Visualization/VisualizationCursorItem.cpp b/gui/src/Visualization/VisualizationCursorItem.cpp new file mode 100644 index 0000000..ed74329 --- /dev/null +++ b/gui/src/Visualization/VisualizationCursorItem.cpp @@ -0,0 +1,163 @@ +#include +#include +#include + +/// Width of the cursor in pixel +const auto CURSOR_WIDTH = 3; + +/// Color of the cursor in the graph +const auto CURSOR_COLOR = QColor{68, 114, 196}; + +/// Line style of the cursor in the graph +auto CURSOR_PEN_STYLE = Qt::DotLine; + +struct VisualizationCursorItem::VisualizationCursorItemPrivate { + + QCustomPlot *m_Plot = nullptr; + + QCPItemStraightLine *m_LineItem = nullptr; + QCPItemText *m_LabelItem = nullptr; + + Qt::Orientation m_Orientation; + double m_Position = 0.0; + bool m_IsAbsolutePosition = false; + QString m_LabelText; + + explicit VisualizationCursorItemPrivate(QCustomPlot *plot) + : m_Plot(plot), m_Orientation(Qt::Vertical) + { + } + + void updateOrientation() + { + if (m_LineItem) { + switch (m_Orientation) { + case Qt::Vertical: + m_LineItem->point1->setTypeX(m_IsAbsolutePosition + ? QCPItemPosition::ptAbsolute + : QCPItemPosition::ptPlotCoords); + m_LineItem->point1->setTypeY(QCPItemPosition::ptAxisRectRatio); + m_LineItem->point2->setTypeX(m_IsAbsolutePosition + ? QCPItemPosition::ptAbsolute + : QCPItemPosition::ptPlotCoords); + m_LineItem->point2->setTypeY(QCPItemPosition::ptAxisRectRatio); + m_LabelItem->setPositionAlignment(Qt::AlignLeft | Qt::AlignTop); + break; + case Qt::Horizontal: + m_LineItem->point1->setTypeX(QCPItemPosition::ptAxisRectRatio); + m_LineItem->point1->setTypeY(m_IsAbsolutePosition + ? QCPItemPosition::ptAbsolute + : QCPItemPosition::ptPlotCoords); + m_LineItem->point2->setTypeX(QCPItemPosition::ptAxisRectRatio); + m_LineItem->point2->setTypeY(m_IsAbsolutePosition + ? QCPItemPosition::ptAbsolute + : QCPItemPosition::ptPlotCoords); + m_LabelItem->setPositionAlignment(Qt::AlignRight | Qt::AlignBottom); + } + } + } + + void updateCursorPosition() + { + if (m_LineItem) { + switch (m_Orientation) { + case Qt::Vertical: + m_LineItem->point1->setCoords(m_Position, 0); + m_LineItem->point2->setCoords(m_Position, 1); + m_LabelItem->position->setCoords(5, 5); + break; + case Qt::Horizontal: + m_LineItem->point1->setCoords(1, m_Position); + m_LineItem->point2->setCoords(0, m_Position); + m_LabelItem->position->setCoords(-5, -5); + } + } + } + + void updateLabelText() + { + if (m_LabelItem) { + m_LabelItem->setText(m_LabelText); + } + } +}; + +VisualizationCursorItem::VisualizationCursorItem(QCustomPlot *plot) + : impl{spimpl::make_unique_impl(plot)} +{ +} + +void VisualizationCursorItem::setVisible(bool value) +{ + if (value != isVisible()) { + + if (value) { + Q_ASSERT(!impl->m_LineItem && !impl->m_Plot); + + impl->m_LineItem = new QCPItemStraightLine{impl->m_Plot}; + auto pen = QPen{CURSOR_PEN_STYLE}; + pen.setColor(CURSOR_COLOR); + pen.setWidth(CURSOR_WIDTH); + impl->m_LineItem->setPen(pen); + impl->m_LineItem->setSelectable(false); + + impl->m_LabelItem = new QCPItemText{impl->m_Plot}; + impl->m_LabelItem->setColor(CURSOR_COLOR); + impl->m_LabelItem->setSelectable(false); + impl->m_LabelItem->position->setParentAnchor(impl->m_LineItem->point1); + impl->m_LabelItem->position->setTypeX(QCPItemPosition::ptAbsolute); + impl->m_LabelItem->position->setTypeY(QCPItemPosition::ptAbsolute); + + auto font = impl->m_LabelItem->font(); + font.setPointSize(10); + font.setBold(true); + impl->m_LabelItem->setFont(font); + + impl->updateOrientation(); + impl->updateLabelText(); + impl->updateCursorPosition(); + } + else { + Q_ASSERT(impl->m_LineItem && impl->m_Plot); + + // Note: the items are destroyed by QCustomPlot in removeItem + impl->m_Plot->removeItem(impl->m_LineItem); + impl->m_LineItem = nullptr; + impl->m_Plot->removeItem(impl->m_LabelItem); + impl->m_LabelItem = nullptr; + } + } +} + +bool VisualizationCursorItem::isVisible() const +{ + return impl->m_LineItem != nullptr; +} + +void VisualizationCursorItem::setPosition(double value) +{ + impl->m_Position = value; + impl->m_IsAbsolutePosition = false; + impl->updateLabelText(); + impl->updateCursorPosition(); +} + +void VisualizationCursorItem::setAbsolutePosition(double value) +{ + setPosition(value); + impl->m_IsAbsolutePosition = true; +} + +void VisualizationCursorItem::setOrientation(Qt::Orientation orientation) +{ + impl->m_Orientation = orientation; + impl->updateLabelText(); + impl->updateOrientation(); + impl->updateCursorPosition(); +} + +void VisualizationCursorItem::setLabelText(const QString &text) +{ + impl->m_LabelText = text; + impl->updateLabelText(); +} diff --git a/gui/src/Visualization/VisualizationDragDropContainer.cpp b/gui/src/Visualization/VisualizationDragDropContainer.cpp index df5b3f0..caa8ef6 100644 --- a/gui/src/Visualization/VisualizationDragDropContainer.cpp +++ b/gui/src/Visualization/VisualizationDragDropContainer.cpp @@ -21,7 +21,7 @@ struct VisualizationDragDropContainer::VisualizationDragDropContainerPrivate { QVBoxLayout *m_Layout; QHash m_AcceptedMimeTypes; QString m_PlaceHolderText; - DragDropHelper::PlaceHolderType m_PlaceHolderType = DragDropHelper::PlaceHolderType::Graph; + DragDropHelper::PlaceHolderType m_PlaceHolderType; VisualizationDragDropContainer::AcceptMimeDataFunction m_AcceptMimeDataFun = [](auto mimeData) { return true; }; @@ -29,6 +29,7 @@ struct VisualizationDragDropContainer::VisualizationDragDropContainerPrivate { int m_MinContainerHeight = 0; explicit VisualizationDragDropContainerPrivate(QWidget *widget) + : m_PlaceHolderType(DragDropHelper::PlaceHolderType::Graph) { m_Layout = new QVBoxLayout(widget); m_Layout->setContentsMargins(0, 0, 0, 0); diff --git a/gui/src/Visualization/VisualizationDragWidget.cpp b/gui/src/Visualization/VisualizationDragWidget.cpp index 57f7074..0214e5e 100644 --- a/gui/src/Visualization/VisualizationDragWidget.cpp +++ b/gui/src/Visualization/VisualizationDragWidget.cpp @@ -4,6 +4,8 @@ #include #include +#include + struct VisualizationDragWidget::VisualizationDragWidgetPrivate { QPoint m_DragStartPosition; @@ -38,16 +40,16 @@ void VisualizationDragWidget::mouseMoveEvent(QMouseEvent *event) return; } - if (!event->modifiers().testFlag(Qt::AltModifier)) { - return; - } + if (sqpApp->plotsInteractionMode() == SqpApplication::PlotsInteractionMode::DragAndDrop + || event->modifiers().testFlag(Qt::AltModifier)) { - if ((event->pos() - impl->m_DragStartPosition).manhattanLength() - < QApplication::startDragDistance()) { - return; - } + if ((event->pos() - impl->m_DragStartPosition).manhattanLength() + < QApplication::startDragDistance()) { + return; + } - emit dragDetected(this, impl->m_DragStartPosition); + emit dragDetected(this, impl->m_DragStartPosition); + } QWidget::mouseMoveEvent(event); } diff --git a/gui/src/Visualization/VisualizationGraphWidget.cpp b/gui/src/Visualization/VisualizationGraphWidget.cpp index e747ae2..532c09a 100644 --- a/gui/src/Visualization/VisualizationGraphWidget.cpp +++ b/gui/src/Visualization/VisualizationGraphWidget.cpp @@ -1,5 +1,6 @@ #include "Visualization/VisualizationGraphWidget.h" #include "Visualization/IVisualizationWidgetVisitor.h" +#include "Visualization/VisualizationCursorItem.h" #include "Visualization/VisualizationDefs.h" #include "Visualization/VisualizationGraphHelper.h" #include "Visualization/VisualizationGraphRenderingDelegate.h" @@ -23,10 +24,22 @@ Q_LOGGING_CATEGORY(LOG_VisualizationGraphWidget, "VisualizationGraphWidget") namespace { /// Key pressed to enable zoom on horizontal axis -const auto HORIZONTAL_ZOOM_MODIFIER = Qt::NoModifier; +const auto HORIZONTAL_ZOOM_MODIFIER = Qt::ControlModifier; /// Key pressed to enable zoom on vertical axis -const auto VERTICAL_ZOOM_MODIFIER = Qt::ControlModifier; +const auto VERTICAL_ZOOM_MODIFIER = Qt::ShiftModifier; + +/// Speed of a step of a wheel event for a pan, in percentage of the axis range +const auto PAN_SPEED = 5; + +/// Key pressed to enable a calibration pan +const auto VERTICAL_PAN_MODIFIER = Qt::AltModifier; + +/// Minimum size for the zoom box, in percentage of the axis range +const auto ZOOM_BOX_MIN_SIZE = 0.8; + +/// Format of the dates appearing in the label of a cursor +const auto CURSOR_LABELS_DATETIME_FORMAT = QStringLiteral("yyyy/MM/dd\nhh:mm:ss:zzz"); } // namespace @@ -47,6 +60,56 @@ struct VisualizationGraphWidget::VisualizationGraphWidgetPrivate { bool m_IsCalibration; /// Delegate used to attach rendering features to the plot std::unique_ptr m_RenderingDelegate; + + QCPItemRect *m_DrawingRect = nullptr; + std::unique_ptr m_HorizontalCursor = nullptr; + std::unique_ptr m_VerticalCursor = nullptr; + + void configureDrawingRect() + { + if (m_DrawingRect) { + QPen p; + p.setWidth(2); + m_DrawingRect->setPen(p); + } + } + + void startDrawingRect(const QPoint &pos, QCustomPlot &plot) + { + removeDrawingRect(plot); + + auto axisPos = posToAxisPos(pos, plot); + + m_DrawingRect = new QCPItemRect{&plot}; + configureDrawingRect(); + + m_DrawingRect->topLeft->setCoords(axisPos); + m_DrawingRect->bottomRight->setCoords(axisPos); + } + + void removeDrawingRect(QCustomPlot &plot) + { + if (m_DrawingRect) { + plot.removeItem(m_DrawingRect); // the item is deleted by QCustomPlot + m_DrawingRect = nullptr; + plot.replot(QCustomPlot::rpQueuedReplot); + } + } + + QPointF posToAxisPos(const QPoint &pos, QCustomPlot &plot) const + { + auto axisX = plot.axisRect()->axis(QCPAxis::atBottom); + auto axisY = plot.axisRect()->axis(QCPAxis::atLeft); + return QPointF{axisX->pixelToCoord(pos.x()), axisY->pixelToCoord(pos.y())}; + } + + bool pointIsInAxisRect(const QPointF &axisPoint, QCustomPlot &plot) const + { + auto axisX = plot.axisRect()->axis(QCPAxis::atBottom); + auto axisY = plot.axisRect()->axis(QCPAxis::atLeft); + + return axisX->range().contains(axisPoint.x()) && axisY->range().contains(axisPoint.y()); + } }; VisualizationGraphWidget::VisualizationGraphWidget(const QString &name, QWidget *parent) @@ -62,12 +125,17 @@ VisualizationGraphWidget::VisualizationGraphWidget(const QString &name, QWidget // Set qcpplot properties : // - Drag (on x-axis) and zoom are enabled // - Mouse wheel on qcpplot is intercepted to determine the zoom orientation - ui->widget->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectItems); - ui->widget->axisRect()->setRangeDrag(Qt::Horizontal); + ui->widget->setInteractions(QCP::iRangeZoom | QCP::iSelectItems); // The delegate must be initialized after the ui as it uses the plot impl->m_RenderingDelegate = std::make_unique(*this); + // Init the cursors + impl->m_HorizontalCursor = std::make_unique(&plot()); + impl->m_HorizontalCursor->setOrientation(Qt::Horizontal); + impl->m_VerticalCursor = std::make_unique(&plot()); + impl->m_VerticalCursor->setOrientation(Qt::Vertical); + connect(ui->widget, &QCustomPlot::mousePress, this, &VisualizationGraphWidget::onMousePress); connect(ui->widget, &QCustomPlot::mouseRelease, this, &VisualizationGraphWidget::onMouseRelease); @@ -264,6 +332,55 @@ void VisualizationGraphWidget::highlightForMerge(bool highlighted) plot().update(); } +void VisualizationGraphWidget::addVerticalCursor(double time) +{ + impl->m_VerticalCursor->setPosition(time); + impl->m_VerticalCursor->setVisible(true); + + auto text + = DateUtils::dateTime(time).toString(CURSOR_LABELS_DATETIME_FORMAT).replace(' ', '\n'); + impl->m_VerticalCursor->setLabelText(text); +} + +void VisualizationGraphWidget::addVerticalCursorAtViewportPosition(double position) +{ + impl->m_VerticalCursor->setAbsolutePosition(position); + impl->m_VerticalCursor->setVisible(true); + + auto axis = plot().axisRect()->axis(QCPAxis::atBottom); + auto text + = DateUtils::dateTime(axis->pixelToCoord(position)).toString(CURSOR_LABELS_DATETIME_FORMAT); + impl->m_VerticalCursor->setLabelText(text); +} + +void VisualizationGraphWidget::removeVerticalCursor() +{ + impl->m_VerticalCursor->setVisible(false); + plot().replot(QCustomPlot::rpQueuedReplot); +} + +void VisualizationGraphWidget::addHorizontalCursor(double value) +{ + impl->m_HorizontalCursor->setPosition(value); + impl->m_HorizontalCursor->setVisible(true); + impl->m_HorizontalCursor->setLabelText(QString::number(value)); +} + +void VisualizationGraphWidget::addHorizontalCursorAtViewportPosition(double position) +{ + impl->m_HorizontalCursor->setAbsolutePosition(position); + impl->m_HorizontalCursor->setVisible(true); + + auto axis = plot().axisRect()->axis(QCPAxis::atLeft); + impl->m_HorizontalCursor->setLabelText(QString::number(axis->pixelToCoord(position))); +} + +void VisualizationGraphWidget::removeHorizontalCursor() +{ + impl->m_HorizontalCursor->setVisible(false); + plot().replot(QCustomPlot::rpQueuedReplot); +} + void VisualizationGraphWidget::closeEvent(QCloseEvent *event) { Q_UNUSED(event); @@ -284,6 +401,13 @@ void VisualizationGraphWidget::leaveEvent(QEvent *event) { Q_UNUSED(event); impl->m_RenderingDelegate->showGraphOverlay(false); + + if (auto parentZone = parentZoneWidget()) { + parentZone->notifyMouseLeaveGraph(this); + } + else { + qCWarning(LOG_VisualizationGraphWidget()) << "leaveEvent: No parent zone widget"; + } } QCustomPlot &VisualizationGraphWidget::plot() noexcept @@ -336,6 +460,20 @@ void VisualizationGraphWidget::onRangeChanged(const QCPRange &t1, const QCPRange emit synchronize(graphRange, oldGraphRange); } } + + auto pos = mapFromGlobal(QCursor::pos()); + auto axisPos = impl->posToAxisPos(pos, plot()); + if (auto parentZone = parentZoneWidget()) { + if (impl->pointIsInAxisRect(axisPos, plot())) { + parentZone->notifyMouseMoveInGraph(pos, axisPos, this); + } + else { + parentZone->notifyMouseLeaveGraph(this); + } + } + else { + qCWarning(LOG_VisualizationGraphWidget()) << "onMouseMove: No parent zone widget"; + } } void VisualizationGraphWidget::onMouseMove(QMouseEvent *event) noexcept @@ -343,38 +481,91 @@ void VisualizationGraphWidget::onMouseMove(QMouseEvent *event) noexcept // Handles plot rendering when mouse is moving impl->m_RenderingDelegate->onMouseMove(event); + auto axisPos = impl->posToAxisPos(event->pos(), plot()); + + if (impl->m_DrawingRect) { + impl->m_DrawingRect->bottomRight->setCoords(axisPos); + } + + if (auto parentZone = parentZoneWidget()) { + if (impl->pointIsInAxisRect(axisPos, plot())) { + parentZone->notifyMouseMoveInGraph(event->pos(), axisPos, this); + } + else { + parentZone->notifyMouseLeaveGraph(this); + } + } + else { + qCWarning(LOG_VisualizationGraphWidget()) << "onMouseMove: No parent zone widget"; + } + VisualizationDragWidget::mouseMoveEvent(event); } void VisualizationGraphWidget::onMouseWheel(QWheelEvent *event) noexcept { - auto zoomOrientations = QFlags{}; + auto value = event->angleDelta().x() + event->angleDelta().y(); + if (value != 0) { + + auto direction = value > 0 ? 1.0 : -1.0; + auto isZoomX = event->modifiers().testFlag(HORIZONTAL_ZOOM_MODIFIER); + auto isZoomY = event->modifiers().testFlag(VERTICAL_ZOOM_MODIFIER); + impl->m_IsCalibration = event->modifiers().testFlag(VERTICAL_PAN_MODIFIER); + + auto zoomOrientations = QFlags{}; + zoomOrientations.setFlag(Qt::Horizontal, isZoomX); + zoomOrientations.setFlag(Qt::Vertical, isZoomY); + + ui->widget->axisRect()->setRangeZoom(zoomOrientations); - // Lambda that enables a zoom orientation if the key modifier related to this orientation - // has - // been pressed - auto enableOrientation - = [&zoomOrientations, event](const auto &orientation, const auto &modifier) { - auto orientationEnabled = event->modifiers().testFlag(modifier); - zoomOrientations.setFlag(orientation, orientationEnabled); - }; - enableOrientation(Qt::Vertical, VERTICAL_ZOOM_MODIFIER); - enableOrientation(Qt::Horizontal, HORIZONTAL_ZOOM_MODIFIER); + if (!isZoomX && !isZoomY) { + auto axis = plot().axisRect()->axis(QCPAxis::atBottom); + auto diff = direction * (axis->range().size() * (PAN_SPEED / 100.0)); - ui->widget->axisRect()->setRangeZoom(zoomOrientations); + axis->setRange(axis->range() + diff); + + if (plot().noAntialiasingOnDrag()) { + plot().setNotAntialiasedElements(QCP::aeAll); + } + + plot().replot(QCustomPlot::rpQueuedReplot); + } + } } void VisualizationGraphWidget::onMousePress(QMouseEvent *event) noexcept { - impl->m_IsCalibration = event->modifiers().testFlag(Qt::ControlModifier); - - plot().setInteraction(QCP::iRangeDrag, !event->modifiers().testFlag(Qt::AltModifier)); + if (sqpApp->plotsInteractionMode() == SqpApplication::PlotsInteractionMode::ZoomBox) { + impl->startDrawingRect(event->pos(), plot()); + } VisualizationDragWidget::mousePressEvent(event); } void VisualizationGraphWidget::onMouseRelease(QMouseEvent *event) noexcept { + if (impl->m_DrawingRect) { + + auto axisX = plot().axisRect()->axis(QCPAxis::atBottom); + auto axisY = plot().axisRect()->axis(QCPAxis::atLeft); + + auto newAxisXRange = QCPRange{impl->m_DrawingRect->topLeft->coords().x(), + impl->m_DrawingRect->bottomRight->coords().x()}; + + auto newAxisYRange = QCPRange{impl->m_DrawingRect->topLeft->coords().y(), + impl->m_DrawingRect->bottomRight->coords().y()}; + + impl->removeDrawingRect(plot()); + + if (newAxisXRange.size() > axisX->range().size() * (ZOOM_BOX_MIN_SIZE / 100.0) + && newAxisYRange.size() > axisY->range().size() * (ZOOM_BOX_MIN_SIZE / 100.0)) { + axisX->setRange(newAxisXRange); + axisY->setRange(newAxisYRange); + + plot().replot(QCustomPlot::rpQueuedReplot); + } + } + impl->m_IsCalibration = false; } diff --git a/gui/src/Visualization/VisualizationZoneWidget.cpp b/gui/src/Visualization/VisualizationZoneWidget.cpp index 3a52978..ed90b7c 100644 --- a/gui/src/Visualization/VisualizationZoneWidget.cpp +++ b/gui/src/Visualization/VisualizationZoneWidget.cpp @@ -53,7 +53,7 @@ void processGraphs(QLayout &layout, Fun fun) for (auto i = 0; i < layout.count(); ++i) { if (auto item = layout.itemAt(i)) { if (auto visualizationGraphWidget - = dynamic_cast(item->widget())) { + = qobject_cast(item->widget())) { fun(*visualizationGraphWidget); } } @@ -347,6 +347,59 @@ bool VisualizationZoneWidget::isDragAllowed() const return true; } +void VisualizationZoneWidget::notifyMouseMoveInGraph(const QPointF &graphPosition, + const QPointF &plotPosition, + VisualizationGraphWidget *graphWidget) +{ + processGraphs(*ui->dragDropContainer->layout(), [&graphPosition, &plotPosition, &graphWidget]( + VisualizationGraphWidget &processedGraph) { + + switch (sqpApp->plotsCursorMode()) { + case SqpApplication::PlotsCursorMode::Vertical: + processedGraph.removeHorizontalCursor(); + processedGraph.addVerticalCursorAtViewportPosition(graphPosition.x()); + break; + case SqpApplication::PlotsCursorMode::Temporal: + processedGraph.addVerticalCursor(plotPosition.x()); + processedGraph.removeHorizontalCursor(); + break; + case SqpApplication::PlotsCursorMode::Horizontal: + processedGraph.removeVerticalCursor(); + if (&processedGraph == graphWidget) { + processedGraph.addHorizontalCursorAtViewportPosition(graphPosition.y()); + } + else { + processedGraph.removeHorizontalCursor(); + } + break; + case SqpApplication::PlotsCursorMode::Cross: + if (&processedGraph == graphWidget) { + processedGraph.addVerticalCursorAtViewportPosition(graphPosition.x()); + processedGraph.addHorizontalCursorAtViewportPosition(graphPosition.y()); + } + else { + processedGraph.removeHorizontalCursor(); + processedGraph.removeVerticalCursor(); + } + break; + case SqpApplication::PlotsCursorMode::NoCursor: + processedGraph.removeHorizontalCursor(); + processedGraph.removeVerticalCursor(); + break; + } + + + }); +} + +void VisualizationZoneWidget::notifyMouseLeaveGraph(VisualizationGraphWidget *graphWidget) +{ + processGraphs(*ui->dragDropContainer->layout(), [](VisualizationGraphWidget &processedGraph) { + processedGraph.removeHorizontalCursor(); + processedGraph.removeVerticalCursor(); + }); +} + void VisualizationZoneWidget::closeEvent(QCloseEvent *event) { // Closes graphs in the zone