Bug Summary

File:librecad/src/ui/view/qg_graphicview.cpp
Warning:line 1595, column 14
Value stored to 'showSnapIndicatorLines' during its initialization is never read

Annotated Source Code

Press '?' to see keyboard shortcuts

clang -cc1 -cc1 -triple x86_64-pc-linux-gnu -O2 -analyze -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name qg_graphicview.cpp -analyzer-checker=core -analyzer-checker=apiModeling -analyzer-checker=unix -analyzer-checker=deadcode -analyzer-checker=cplusplus -analyzer-checker=security.insecureAPI.UncheckedReturn -analyzer-checker=security.insecureAPI.getpw -analyzer-checker=security.insecureAPI.gets -analyzer-checker=security.insecureAPI.mktemp -analyzer-checker=security.insecureAPI.mkstemp -analyzer-checker=security.insecureAPI.vfork -analyzer-checker=nullability.NullPassedToNonnull -analyzer-checker=nullability.NullReturnedFromNonnull -analyzer-output plist -w -setup-static-analyzer -mrelocation-model pic -pic-level 2 -fhalf-no-semantic-interposition -mframe-pointer=none -fmath-errno -ffp-contract=on -fno-rounding-math -mconstructor-aliases -funwind-tables=2 -target-cpu x86-64 -tune-cpu generic -debugger-tuning=gdb -fdebug-compilation-dir=/home/runner/work/LibreCAD/LibreCAD/librecad/src -fcoverage-compilation-dir=/home/runner/work/LibreCAD/LibreCAD/librecad/src -resource-dir /usr/lib/llvm-21/lib/clang/21 -D _REENTRANT -D DWGSUPPORT -D MUPARSER_STATIC -D LC_WITH_OPENTYPE=0 -D QC_APPDIR=librecad -D LC_VERSION=2.2.2_alpha1-853-ge2a8c2931 -D LC_PRERELEASE=true; -D QT_NO_DEBUG -D QT_SVG_LIB -D QT_PRINTSUPPORT_LIB -D QT_WIDGETS_LIB -D QT_GUI_LIB -D QT_NETWORK_LIB -D QT_CORE_LIB -I . -I /usr/include -I ../../libraries/lciconengine -I ../../libraries/libdxfrw/src -I ../../libraries/libdxfrw/src/intern -I ../../libraries/jwwlib/src -I ../../libraries/shapelib/src -I ../../libraries/qtbidi -I cmd -I lib/actions -I lib/actions/visual_snap -I lib/actions/options -I lib/creation -I lib/debug -I lib/engine -I lib/engine/document -I lib/engine/document/blocks -I lib/engine/document/container -I lib/engine/document/dimstyles -I lib/engine/document/entities -I lib/engine/document/entities/support -I lib/engine/document/fonts -I lib/engine/document/io -I lib/engine/document/layers -I lib/engine/document/linetypes -I lib/engine/document/patterns -I lib/engine/document/selection -I lib/engine/document/textstyles -I lib/engine/document/ucs -I lib/engine/document/variables -I lib/engine/document/views -I lib/engine/clipboard -I lib/engine/overlays -I lib/engine/overlays/angles_base -I lib/engine/overlays/highlight -I lib/engine/overlays/preview -I lib/engine/overlays/references -I lib/engine/overlays/crosshair -I lib/engine/overlays/info_cursor -I lib/engine/overlays/overlay_box -I lib/engine/overlays/ucs_mark -I lib/engine/undo -I lib/engine/utils -I lib/engine/settings -I lib/fileio -I lib/filters -I lib/generators -I lib/generators/makercamsvg -I lib/generators/layers -I lib/generators/image -I lib/gui -I lib/gui/grid -I lib/gui/render -I lib/gui/render/headless -I lib/gui/render/widget -I lib/information -I lib/math -I lib/modification -I lib/selection -I lib/selection/metaentity -I lib/selection/metaentity/entities -I lib/printing -I lib/properties -I actions -I actions/dock_widgets -I actions/dock_widgets/block -I actions/dock_widgets/entity_info -I actions/dock_widgets/layer -I actions/dock_widgets/library -I actions/dock_widgets/ucs_list -I actions/drawing -I actions/drawing/draw -I actions/drawing/draw/arc -I actions/drawing/draw/circle -I actions/drawing/draw/curve -I actions/drawing/draw/spline -I actions/drawing/draw/dimensions -I actions/drawing/draw/ellipse -I actions/drawing/draw/hatch -I actions/drawing/draw/image -I actions/drawing/draw/line -I actions/drawing/draw/point -I actions/drawing/draw/rect -I actions/drawing/draw/polygon -I actions/drawing/draw/misc -I actions/drawing/draw/line/shapes -I actions/drawing/draw/line/misc -I actions/drawing/draw/line/shapes/rect -I actions/drawing/draw/line/shapes/polygon -I actions/drawing/draw/polyline -I actions/drawing/draw/text -I actions/drawing/edit -I actions/drawing/info -I actions/drawing/pick -I actions/drawing/modify -I actions/drawing/pen -I actions/drawing/rel_zero -I actions/drawing/selection -I actions/drawing/snap -I actions/drawing/zoom -I actions/file -I actions/options -I actions/print_preview -I ui -I ui/action_options -I ui/action_options/circle -I ui/action_options/curve -I ui/action_options/spline -I ui/action_options/dimensions -I ui/action_options/edit -I ui/action_options/image -I ui/action_options/info -I ui/action_options/insert -I ui/action_options/line -I ui/action_options/rect -I ui/action_options/polygon -I ui/action_options/misc -I ui/action_options/modify -I ui/action_options/ellipse -I ui/action_options/other -I ui/action_options/polyline -I ui/action_options/point -I ui/action_options/print_preview -I ui/action_options/selection -I ui/action_options/snap -I ui/action_options/text -I ui/actions -I ui/components -I ui/components/relative_position_assistant -I ui/components/comboboxes -I ui/components/containers -I ui/components/creators -I ui/components/layouts -I ui/components/pen -I ui/components/status_bar -I ui/components/toolbars -I ui/components/utils -I ui/dialogs -I ui/dialogs/actions -I ui/dialogs/actions/modify -I ui/dialogs/actions/quick_selection -I ui/dialogs/modify -I ui/dialogs/entity -I ui/dialogs/creators -I ui/dialogs/file -I ui/dialogs/file/export -I ui/dialogs/file/export/layers -I ui/dialogs/file/export/image -I ui/dialogs/file/export/makercam -I ui/dialogs/main -I ui/dialogs/settings -I ui/dialogs/settings/dimstyles -I ui/dialogs/settings/dimstyles/dimstyle_manager -I ui/dialogs/settings/dimstyles/dimstyle_manager/support -I ui/dialogs/settings/options_device -I ui/dialogs/settings/options_drawing -I ui/dialogs/settings/options_general -I ui/dialogs/settings/options_widget -I ui/dialogs/settings/shortcuts -I ui/dock_widgets -I ui/dock_widgets/block_widget -I ui/dock_widgets/command_line -I ui/dock_widgets/entity_info -I ui/dock_widgets/layer_widget -I ui/dock_widgets/layers_tree -I ui/dock_widgets/library_widget -I ui/dock_widgets/pen_palette -I ui/dock_widgets/pen_wizard -I ui/dock_widgets/property_sheet -I ui/dock_widgets/property_sheet/lib -I ui/dock_widgets/property_sheet/lib/properties -I ui/dock_widgets/property_sheet/lib/view -I ui/dock_widgets/property_sheet/lib/view/edit -I ui/dock_widgets/property_sheet/lib/widgets -I ui/dock_widgets/property_sheet/lib/widgets/sheet -I ui/dock_widgets/property_sheet/metaentity -I ui/dock_widgets/property_sheet/metaentity/entities -I ui/dock_widgets/property_sheet/metaentity/entities/document -I ui/dock_widgets/property_sheet/properties -I ui/dock_widgets/property_sheet/properties/action -I ui/dock_widgets/property_sheet/properties/bool -I ui/dock_widgets/property_sheet/properties/color -I ui/dock_widgets/property_sheet/properties/double -I ui/dock_widgets/property_sheet/properties/enum -I ui/dock_widgets/property_sheet/properties/int -I ui/dock_widgets/property_sheet/properties/layer -I ui/dock_widgets/property_sheet/properties/linetype -I ui/dock_widgets/property_sheet/properties/linewidth -I ui/dock_widgets/property_sheet/properties/rect -I ui/dock_widgets/property_sheet/properties/rsvector -I ui/dock_widgets/property_sheet/properties/string -I ui/dock_widgets/views_list -I ui/dock_widgets/ucs_list -I ui/dock_widgets/workspaces -I ui/dock_widgets/cad -I ui/main -I ui/main/init -I ui/main/persistence -I ui/main/release_check -I ui/main/support -I ui/main/fontviewer -I ui/main/workspaces -I ui/view -I main -I main/console_dxf2pdf -I test -I plugins -I ../res -I ../res/arrows -I ../res/controls -I ../res/dxf -I ../res/gdt -I ../res/icons -I ../res/images -I ../../../Qt/6.9.0/gcc_64/include -I ../../../Qt/6.9.0/gcc_64/include/QtSvg -I ../../../Qt/6.9.0/gcc_64/include/QtPrintSupport -I ../../../Qt/6.9.0/gcc_64/include/QtWidgets -I ../../../Qt/6.9.0/gcc_64/include/QtGui -I ../../../Qt/6.9.0/gcc_64/include/QtNetwork -I ../../../Qt/6.9.0/gcc_64/include/QtCore -I ../../generated/librecad/moc -I ../../generated/librecad/ui -I ../../../Qt/6.9.0/gcc_64/mkspecs/linux-g++ -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/15/../../../../include/c++/15 -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/15/../../../../include/x86_64-linux-gnu/c++/15 -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/15/../../../../include/c++/15/backward -internal-isystem /usr/lib/llvm-21/lib/clang/21/include -internal-isystem /usr/local/include -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/15/../../../../x86_64-linux-gnu/include -internal-externc-isystem /usr/include/x86_64-linux-gnu -internal-externc-isystem /include -internal-externc-isystem /usr/include -std=gnu++1z -fdeprecated-macro -ferror-limit 19 -fgnuc-version=4.2.1 -fskip-odr-check-in-gmf -fcxx-exceptions -fexceptions -vectorize-loops -vectorize-slp -analyzer-output=html -faddrsig -D__GCC_HAVE_DWARF2_CFI_ASM=1 -o /home/runner/work/LibreCAD/LibreCAD/out/2026-11-06-031751-4837-1 -x c++ ui/view/qg_graphicview.cpp
1/****************************************************************************
2**
3** This file is part of the LibreCAD project, a 2D CAD program
4**
5** Copyright (C) 2010-2011 R. van Twisk (librecad@rvt.dds.nl)
6** Copyright (C) 2001-2003 RibbonSoft. All rights reserved.
7**
8**
9** This file may be distributed and/or modified under the terms of the
10** GNU General Public License version 2 as published by the Free Software
11** Foundation and appearing in the file gpl-2.0.txt included in the
12** packaging of this file.
13**
14** This program is distributed in the hope that it will be useful,
15** but WITHOUT ANY WARRANTY; without even the implied warranty of
16** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17** GNU General Public License for more details.
18**
19** You should have received a copy of the GNU General Public License
20** along with this program; if not, write to the Free Software
21** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
22**
23** This copyright notice MUST APPEAR in all copies of the script!
24**
25**********************************************************************/
26
27#include "qg_graphicview.h"
28
29#include <QGridLayout>
30#include <QMenu>
31#include <QNativeGestureEvent>
32#include <QPoint>
33#include <QPointingDevice>
34#include <QScopedValueRollback>
35#include <QSignalBlocker>
36#include <QTimer>
37#include <cmath>
38#include <cstdlib>
39#include <iostream>
40
41#include "lc_action_modify_move_adjust.h"
42#include "lc_action_select_single.h"
43#include "lc_actioncontext.h"
44#include "lc_eventhandler.h"
45#include "lc_graphicviewport.h"
46#include "lc_graphicviewrenderer.h"
47#include "lc_overlayentitiescontainer.h"
48#include "lc_quickinfowidget.h"
49#include "lc_rect.h"
50#include "lc_relative_point_input_widget.h"
51#include "lc_relative_position_editing_widget.h"
52#include "lc_ucs_mark.h"
53#include "lc_undosection.h"
54#include "lc_viewmath.h"
55#include "qc_applicationwindow.h"
56#include "qg_blockwidget.h"
57#include "qg_scrollbar.h"
58#include "rs.h"
59#include "rs_actiondefault.h"
60#include "rs_blocklist.h"
61#include "rs_debug.h"
62#include "rs_dialogfactoryinterface.h"
63#include "rs_entity.h"
64#include "rs_entitycontainer.h"
65#include "rs_graphic.h"
66#include "rs_insert.h"
67#include "rs_selection.h"
68#include "rs_settings.h"
69#include "rs_units.h"
70
71#ifdef EMU_C99
72#include "emu_c99.h"
73#endif
74
75namespace {
76 // Issue #1765: set default cursor size: 32x32
77 constexpr int g_cursorSize = 32; // fixme - sand - move to common public place
78 // Issue #1787: cursor hot spot at center by using hotX=hotY=-1
79 constexpr int HOTSPOT_XY = -1;
80
81 // zooming factor is wheel angle delta divided by this factor
82 constexpr double ZOOM_WHEEL_DIVISOR = 200.; // fixme - to settings
83
84 // Helper function to test validity of a rect
85 bool withinValidRange(const double x) {
86 return x >= RS_MINDOUBLE-1.0E+10 && x <= RS_MAXDOUBLE1.0E+10;
87 }
88
89 bool withinValidRange(const RS_Vector& vp) {
90 return vp.valid && withinValidRange(vp.x) && withinValidRange(vp.y);
91 }
92
93 // finite and ordered; a point-like drawing (min == max) is valid content
94 bool isRectValid(const RS_Vector& vpMin, const RS_Vector& vpMax) {
95 return withinValidRange(vpMin) && withinValidRange(vpMax) && vpMin.x <= vpMax.x && vpMin.y <= vpMax.y;
96 }
97}
98
99/**
100 * @brief snapEntity find the closest entity
101 * @param view
102 * @param event
103 * @param view - the graphic view
104 * @param event - the mouse event
105 * @return RS_Entity* - the closest entity within the range of g_cursorSize
106 * returns nullptr, if no entity is found in range
107 */
108RS_Entity* snapEntity(const QG_GraphicView& view, const QMouseEvent* event) {
109 if (event == nullptr) {
110 return nullptr;
111 }
112 const auto doc = view.getDocument();
113 if (doc == nullptr) {
114 // how it might be???
115 return nullptr;
116 }
117 const QPointF mapped = event->pos();
118 double distance = RS_MAXDOUBLE1.0E+10;
119 const auto viewPort = view.getViewPort();
120
121 const auto pos = viewPort->toWorldFromUi(mapped.x(), mapped.y());
122 const auto entity = doc->getNearestEntity(pos, &distance, RS2::ResolveNone);
123
124 return (viewPort->toGuiDX(distance) <= g_cursorSize) ? entity : nullptr;
125}
126
127// fixme - sand - remove, not needed?
128// Find an ancestor of the RS_Insert type.
129// Return nullptr, if none is found
130RS_Insert* getAncestorInsert(RS_Entity* entity) {
131 while (entity != nullptr) {
132 if (entity->rtti() == RS2::EntityInsert) {
133 RS_Insert* parent = getAncestorInsert(entity->getParent());
134 return parent != nullptr ? parent : static_cast<RS_Insert*>(entity);
135 }
136 entity = entity->getParent();
137 }
138 return nullptr;
139}
140
141// fixme - sand - remove, not needed?
142// whether the current insert is part of Text
143RS_Entity* getParentText(const RS_Insert* insert) {
144 if (insert == nullptr || insert->getBlock() != nullptr || insert->getParent() == nullptr) {
145 return nullptr;
146 }
147 switch (insert->getParent()->rtti()) {
148 case RS2::EntityText:
149 case RS2::EntityMText:
150 return insert->getParent();
151 default:
152 return nullptr;
153 }
154}
155
156// Show the entity property dialog on the closest entity in range
157void QG_GraphicView::showEntityPropertiesDialog(RS_Entity* entity) const {
158 if (entity == nullptr) {
159 return;
160 }
161
162 // snap to the top selected parent
163 while (entity != nullptr && entity->getParent() != nullptr && entity->getParent()->isSelected()) {
164 entity = entity->getParent();
165 }
166
167 launchEditProperty(entity);
168}
169
170void QG_GraphicView::launchEditProperty(RS_Entity* entity) const {
171 const auto* doc = getDocument();
172 if (entity == nullptr || doc == nullptr) {
173 return;
174 }
175 editAction(*entity);
176
177 // delete any temporary highlighting duplicates of the original
178 auto* defaultAction = dynamic_cast<RS_ActionDefault*>(getEventHandler()->getDefaultAction());
179 if (defaultAction != nullptr) {
180 defaultAction->clearHighLighting();
181 }
182}
183
184// Start the edit action:
185// Edit Block for an insert
186// Edit entity, otherwise
187void QG_GraphicView::editAction(RS_Entity& entity) const {
188 const auto doc = getDocument();
189 if (doc == nullptr) {
190 // fixme - DOC - review what for this check is used
191 return;
192 }
193 switch (entity.rtti()) {
194 case RS2::EntityInsert: {
195 // fixme - sand - rework block editing. Currently insert may not be changed after insertion! Window is needed!!
196 const auto& appWindow = QC_ApplicationWindow::getAppWindow(); // fixme - sand - remove static, it just one of parents?
197 RS_BlockList* blockList = appWindow->getBlockWidget()->getBlockList();
198 RS_Block* active = (blockList != nullptr) ? blockList->getActive() : nullptr;
199 const auto* insert = static_cast<RS_Insert*>(&entity);
200 RS_Block* current = insert->getBlockForInsert();
201 if (current == active) {
202 active = nullptr;
203 }
204 else if (blockList != nullptr) {
205 blockList->activate(current);
206 }
207 /*// fixme - sand - simplify
208 std::shared_ptr<RS_Block*> scoped{
209 &active,
210 [blockList](RS_Block** pointer) {
211 if (pointer != nullptr && *pointer != nullptr && blockList != nullptr) {
212 blockList->activate(*pointer);
213 }
214 }
215 };*/
216 switchToAction(RS2::ActionBlocksEdit);
217 if (active != nullptr && blockList != nullptr) {
218 blockList->activate(active);
219 }
220 break;
221 }
222 default: {
223 m_actionContext->saveContextMenuActionContext(&entity, RS_Vector(false), entity.isSelected());
224 switchToAction(RS2::ActionModifyEntity);
225 break;
226 }
227 }
228}
229
230// Support auto-panning when the cursor is close to the view border
231struct QG_GraphicView::AutoPanData {
232 void start(const double interval, QG_GraphicView& view) {
233 delayCounter = 0;
234 panTimer = std::make_unique<QTimer>(&view);
235 panTimer->start(interval);
236 connect(panTimer.get(), &QTimer::timeout, &view, &QG_GraphicView::autoPanStep);
237 }
238
239 std::unique_ptr<QTimer> panTimer;
240
241 QPoint panOffset;
242
243 unsigned delayCounter = 0U;
244 // skip the first events, to avoid unintensional panning
245 const unsigned delayCounterMax = 10U;
246 const double panOffsetMagnitude = 20.0;
247
248 const double panTimerIntervalMinimum = 20.0;
249 const double panTimerIntervalMaximum = 100.0;
250
251 // the sensitive border of the view
252 const RS_Vector probedAreaOffset = {50 /* pixels */, 50 /* pixels */};
253};
254
255struct QG_GraphicView::UCSHighlightData {
256 std::unique_ptr<QTimer> timer;
257
258 double timerInterval = 200.0;
259 int blinkNumber = 0;
260 int maxBlinkNumber = 15;
261 bool inVisiblePhase = false;
262 RS_Vector origin;
263 double angle = 0.0;
264 bool forWCS = false;
265
266 RS_Vector savedViewOffset = RS_Vector(0, 0, 0);
267 double savedViewFactor = 0.0;
268
269 void start(const double interval, QG_GraphicView& view) {
270 if (timer == nullptr) {
271 timer = std::make_unique<QTimer>(&view);
272 connect(timer.get(), &QTimer::timeout, &view, &QG_GraphicView::ucsHighlightStep);
273 }
274 timer->start(interval);
275 }
276
277 bool mayTick() {
278 blinkNumber++;
279 inVisiblePhase = !inVisiblePhase;
280 return blinkNumber <= maxBlinkNumber;
281 }
282
283 void stop() {
284 blinkNumber = 0;
285 inVisiblePhase = false;
286 timer->stop();
287 }
288};
289
290void createViewRenderer();
291
292void QG_GraphicView::initRelativePointInputWidget() {
293 m_relativePointWidgetHolder = new LC_RelativePointInputWidget(this, m_actionContext);
294 m_relativePointWidgetHolder->setVisible(false);
295}
296
297/**
298 * Constructor.
299 */
300// fixme - sand - files - init by action context???
301QG_GraphicView::QG_GraphicView(QWidget* parent, RS_Document* doc, LC_ActionContext* actionContext)
302 : RS_GraphicView(parent, {}), m_device("Mouse"),
303 m_cursorCad(new QCursor(QPixmap(":cursors/cur_cad_bmp.png"), HOTSPOT_XY, HOTSPOT_XY)),
304 m_cursorDel(new QCursor(QPixmap(":cursors/cur_del_bmp.png"), HOTSPOT_XY, HOTSPOT_XY)),
305 m_cursorSelect(new QCursor(QPixmap(":cursors/cur_select_bmp.png"), HOTSPOT_XY, HOTSPOT_XY)),
306 m_cursorMagnifier(new QCursor(QPixmap(":cursors/cur_glass_bmp.png"), HOTSPOT_XY, HOTSPOT_XY)),
307 m_cursorHand(new QCursor(QPixmap(":cursors/cur_hand_bmp.png"), HOTSPOT_XY, HOTSPOT_XY)), m_isSmoothScrolling(false),
308 m_ucsMarkOptions{std::make_unique<LC_UCSMarkOptions>()}, m_panData{std::make_unique<AutoPanData>()},
309 m_ucsHighlightData{std::make_unique<UCSHighlightData>()} {
310 RS_DEBUGRS_Debug::instance()->print("QG_GraphicView::QG_GraphicView()..");
311
312 if (doc != nullptr) {
313 setDocument(doc);
314 doc->setGraphicView(this);
315 actionContext->setDocumentAndView(doc, this);
316 setDefaultAction(new RS_ActionDefault(actionContext));
317 }
318
319 m_actionContext = actionContext;
320
321 getViewPort()->justSetOffsetAndFactor(0, 0, 4.0);
322 getViewPort()->setBorders(10, 10, 10, 10);
323
324 setMouseTracking(true);
325 setFocusPolicy(Qt::NoFocus);
326
327 // SourceForge issue 45 (Left-mouse drag shrinks window)
328 setAttribute(Qt::WA_NoMousePropagation);
329
330 // Issue #2264: prevents macOS from applying text-related features like the Caps Lock indicator to the non-text canvas
331#ifdef Q_OS_MAC
332 setAttribute(Qt::WA_InputMethodEnabled, false); setInputMethodHints(Qt::ImhNone);
333#endif
334}
335
336void QG_GraphicView::initView() {
337 createViewRenderer();
338 initRelativePointInputWidget();
339}
340
341void QG_GraphicView::createViewRenderer() {
342 if (getViewPort() != nullptr) {
343 getViewPort()->setSize(width(), height()); // fixme - sand - merge - CHECK THIS
344 setRenderer(std::make_unique<LC_GraphicViewRenderer>(getViewPort(), this));
345 }
346}
347
348void QG_GraphicView::layerToggled(RS_Layer*) {
349 redraw(RS2::RedrawDrawing);
350}
351
352/**
353 * Destructor
354 */
355QG_GraphicView::~QG_GraphicView() {
356 try {
357 // LC_ERR << "QG_GraphicView destructor";
358 cleanUp();
359 // LC_ERR << "QG_GraphicView destructor 1";
360 }
361 catch (...) {
362 LC_ERRRS_Debug::Log(RS_Debug::D_ERROR) << __func__ << "(): received exception";
363 }
364}
365
366/**
367 * @return width of widget.
368 */
369int QG_GraphicView::getWidth() const {
370 if (m_scrollbars) {
371 return width() - m_vScrollBar->sizeHint().width();
372 }
373 return width();
374}
375
376/**
377 * @return height of widget.
378 */
379int QG_GraphicView::getHeight() const {
380 if (m_scrollbars) {
381 return height() - m_hScrollBar->sizeHint().height();
382 }
383 return height();
384}
385
386/**
387 * Sets the mouse cursor to the given type.
388 */
389void QG_GraphicView::setMouseCursor(const RS2::CursorType cursorType) {
390 switch (cursorType) {
391 default: case RS2::ArrowCursor:
392 setCursor(Qt::ArrowCursor);
393 break;
394 case RS2::UpArrowCursor:
395 setCursor(Qt::UpArrowCursor);
396 break;
397 case RS2::CrossCursor:
398 setCursor(Qt::CrossCursor);
399 break;
400 case RS2::WaitCursor:
401 setCursor(Qt::WaitCursor);
402 break;
403 case RS2::IbeamCursor:
404 setCursor(Qt::IBeamCursor);
405 break;
406 case RS2::SizeVerCursor:
407 setCursor(Qt::SizeVerCursor);
408 break;
409 case RS2::SizeHorCursor:
410 setCursor(Qt::SizeHorCursor);
411 break;
412 case RS2::SizeBDiagCursor:
413 setCursor(Qt::SizeBDiagCursor);
414 break;
415 case RS2::SizeFDiagCursor:
416 setCursor(Qt::SizeFDiagCursor);
417 break;
418 case RS2::SizeAllCursor:
419 setCursor(Qt::SizeAllCursor);
420 break;
421 case RS2::BlankCursor:
422 setCursor(Qt::BlankCursor);
423 break;
424 case RS2::SplitVCursor:
425 setCursor(Qt::SplitVCursor);
426 break;
427 case RS2::SplitHCursor:
428 setCursor(Qt::SplitHCursor);
429 break;
430 case RS2::PointingHandCursor:
431 setCursor(Qt::PointingHandCursor);
432 break;
433 case RS2::ForbiddenCursor:
434 setCursor(Qt::ForbiddenCursor);
435 break;
436 case RS2::WhatsThisCursor:
437 setCursor(Qt::WhatsThisCursor);
438 break;
439 case RS2::OpenHandCursor:
440 setCursor(Qt::OpenHandCursor);
441 break;
442 case RS2::ClosedHandCursor:
443 setCursor(Qt::ClosedHandCursor);
444 break;
445 case RS2::CadCursor:
446 m_cursorHiding ? setCursor(Qt::BlankCursor) : setCursor(*m_cursorCad);
447 break;
448 case RS2::DelCursor:
449 setCursor(*m_cursorDel);
450 break;
451 case RS2::SelectCursor:
452 m_selectCursorHiding ? setCursor(Qt::BlankCursor) : setCursor(*m_cursorSelect);
453 break;
454 case RS2::MagnifierCursor:
455 setCursor(*m_cursorMagnifier);
456 break;
457 case RS2::MovingHandCursor:
458 setCursor(*m_cursorHand);
459 break;
460 }
461}
462
463/**
464 * Sets the text for the grid status widget in the left bottom corner.
465 */
466void QG_GraphicView::updateGridStatusWidget(QString text) {
467 emit gridStatusChanged(std::move(text));
468}
469
470void QG_GraphicView::dragEnterEvent(QDragEnterEvent* event) {
471 RS_GraphicView::dragEnterEvent(event);
472
473 /*
474 * fixme - sand - remove later, experiments with d&d
475 */
476 /* if (event->mimeData()->formats().contains("application/x-qabstractitemmodeldatalist")) {
477 // QStandardItemModel model;
478 // model.dropMimeData(event->mimeData(), Qt::CopyAction, 0,0, QModelIndex());
479 // auto item = model.item(0.0);
480 // LC_ERR << item->text();
481
482 QDrag::cancel();
483 QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
484 QC_ApplicationWindow::getAppWindow()->getLibraryWidget()->insert();
485 }*/
486}
487
488/**
489 * Redraws the widget.
490 */
491void QG_GraphicView::redraw(const RS2::RedrawMethod method, bool immediately) {
492 if ((method & RS2::RedrawDrawing) != 0) {
493 emit drawingRedrawRequested();
494 }
495 getRenderer()->invalidate(method);
496 update(); // Paint when ready to paint
497 if (immediately) {
498 repaint();
499 }
500}
501
502void QG_GraphicView::resizeEvent(QResizeEvent* e) {
503 RS_GraphicView::resizeEvent(e);
504 RS_DEBUGRS_Debug::instance()->print("QG_GraphicView::resizeEvent begin");
505 adjustOffsetControls();
506 adjustZoomControls();
507 // updateGrid();
508 // Small hack, delete the snapper during resizes
509 getViewPort()->clearOverlayDrawablesContainer(RS2::Snapper);
510 redraw();
511 RS_DEBUGRS_Debug::instance()->print("QG_GraphicView::resizeEvent end");
512}
513
514void QG_GraphicView::switchToAction(const RS2::ActionType actionType, void* data) const {
515 m_actionContext->setCurrentAction(actionType, data);
516}
517
518RS_Entity* QG_GraphicView::catchContextEntity(const QMouseEvent* event, RS_Vector& clickPos) const {
519 const auto doc = getDocument();
520 if (doc == nullptr || event == nullptr) {
521 return nullptr;
522 }
523
524 const QPointF mapped = event->pos();
525 double distance = RS_MAXDOUBLE1.0E+10;
526 const LC_GraphicViewport* viewPort = getViewPort();
527
528 clickPos = viewPort->toWorldFromUi(mapped.x(), mapped.y());
529 RS_Entity* entity = doc->getNearestEntity(clickPos, &distance, RS2::ResolveNone);
530
531 if (viewPort->toGuiDX(distance) <= g_cursorSize) {
532 return entity;
533 }
534 return nullptr;
535}
536
537bool QG_GraphicView::invokeContextMenuForMouseEvent(QMouseEvent* e) {
538 bool result = false;
539 RS_Vector clickPos;
540 RS_Entity* entity = catchContextEntity(e, clickPos);
541 const auto contextMenu = QC_ApplicationWindow::getAppWindow()->createGraphicViewContentMenu(e, this, entity, clickPos);
542 if (contextMenu != nullptr) {
543 if (!contextMenu->isEmpty()) {
544 auto actions = contextMenu->actions();
545 if (actions.size() == 1) {
546 const auto action = actions.front();
547 action->trigger();
548 result = true;
549 }
550 else {
551 contextMenu->exec(mapToGlobal(e->pos()));
552 result = true;
553 }
554 }
555 delete contextMenu;
556 }
557 return result;
558}
559
560void QG_GraphicView::mousePressEvent(QMouseEvent* event) {
561 // LC_ERR << "MOUSE PRESS";
562 // pan zoom with middle mouse button
563 if (event->button() == Qt::MiddleButton && event->modifiers() == Qt::NoModifier) {
564 switchToAction(RS2::ActionZoomPan);
565 getCurrentAction()->mousePressEvent(event);
566 }
567 else {
568 getEventHandler()->mousePressEvent(event);
569 }
570}
571
572void QG_GraphicView::mouseDoubleClickEvent(QMouseEvent* e) {
573 if (e->button() == Qt::MiddleButton && e->modifiers() == Qt::NoModifier) {
574 // Zoom the view directly so an in-progress drawing or editing action remains active.
575 getViewPort()->zoomAuto(false, true);
576 e->accept();
577 return;
578 }
579
580 if (getEventHandler()->hasAction()) {
581 }
582 else {
583 const auto defaultAction = getEventHandler()->getDefaultAction();
584 RS_Vector clickPos;
585 RS_Entity* entity = catchContextEntity(e, clickPos);
586 if (entity == nullptr) {
587 if (defaultAction == nullptr) {
588 invokeContextMenuForMouseEvent(e);
589 }
590 else if (defaultAction->getStatus() == RS_ActionInterface::InitialActionStatus) {
591 invokeContextMenuForMouseEvent(e);
592 }
593 }
594 else {
595 if (e->button() == Qt::LeftButton && e->modifiers() == Qt::NoModifier) {
596 if (defaultAction == nullptr) {
597 showEntityPropertiesDialog(entity);
598 }
599 else if (defaultAction->getStatus() == RS_ActionInterface::InitialActionStatus) {
600 showEntityPropertiesDialog(entity);
601 }
602 }
603 else {
604 invokeContextMenuForMouseEvent(e);
605 }
606 }
607 }
608 e->accept();
609}
610
611void QG_GraphicView::mouseReleaseEvent(QMouseEvent* event) {
612 RS_DEBUGRS_Debug::instance()->print("QG_GraphicView::mouseReleaseEvent");
613
614 event->accept();
615 if (getEventHandler()->hasAction()) {
616 switch (event->button()) {
617 case Qt::RightButton: {
618 if (getEventHandler()->hasAction()) {
619 back(event->modifiers());
620 }
621 break;
622 }
623 case Qt::XButton1:
624 processEnterKey();
625 emit xbutton1_released();
626 break;
627 default:
628 getEventHandler()->mouseReleaseEvent(event);
629 break;
630 }
631 }
632 else {
633 const auto defaultAction = getEventHandler()->getDefaultAction();
634 if (defaultAction != nullptr) {
635 const int defaultActionStatus = defaultAction->getStatus();
636 if (defaultActionStatus == RS_ActionInterface::InitialActionStatus) {
637 if (isMouseReleaseEventForDefaultAction(event)) {
638 defaultAction->mouseReleaseEvent(event);
639 }
640 else {
641 invokeContextMenuForMouseEvent(event);
642 }
643 }
644 else {
645 defaultAction->mouseReleaseEvent(event);
646 }
647 }
648 else {
649 invokeContextMenuForMouseEvent(event);
650 }
651 }
652 RS_DEBUGRS_Debug::instance()->print("QG_GraphicView::mouseReleaseEvent: OK");
653}
654
655bool QG_GraphicView::isMouseReleaseEventForDefaultAction(const QMouseEvent* event) {
656 // should correspond to LC_DlgMenuAssigner::validateShortcut()
657 if (event->button() == Qt::LeftButton) {
658 const auto modifiers = event->modifiers();
659 if (modifiers == Qt::NoModifier) {
660 // select
661 return true;
662 }
663 const bool control = modifiers & Qt::ControlModifier;
664 const bool alt = modifiers & Qt::AltModifier;
665 const bool shift = modifiers & Qt::ShiftModifier;
666 if (control && !alt && !shift) {
667 // pan
668 return true;
669 }
670 if (shift && !alt && !control) {
671 // select contour
672 return true;
673 }
674 }
675 return false;
676}
677
678void QG_GraphicView::mouseMoveEvent(QMouseEvent* event) {
679 if (isClosing()) {
680 event->accept();
681 return;
682 }
683 // LC_ERR << "OWN MOUSE MOVE";
684 if (isAutoPan(event)) {
685 startAutoPanTimer(event);
686 event->accept();
687 return;
688 }
689 m_panData->panTimer.reset();
690 // handle auto-panning
691 event->accept();
692 getEventHandler()->mouseMoveEvent(event);
693}
694
695bool QG_GraphicView::proceedEvent(QEvent* event) {
696 // skip events without a default action
697 // Hatch preview in qg_dlghatch doesn't have its default action
698 if (dynamic_cast<QInputEvent*>(event) == nullptr || getDefaultAction() != nullptr) {
699 return QWidget::event(event);
700 } // LC_ERR<< "Event Skipped";
701 return true;
702}
703
704bool QG_GraphicView::event(QEvent* event) {
705 if (event->type() == QEvent::NativeGesture) {
706 const auto* nge = static_cast<QNativeGestureEvent*>(event);
707
708 if (nge->gestureType() == Qt::ZoomNativeGesture) {
709 const double v = nge->value();
710 const RS2::ZoomDirection direction = std::signbit(v) ? RS2::Out : RS2::In;
711 const double factor = 1. + std::abs(v);
712
713 // It seems the NativeGestureEvent::pos() incorrectly reports global coordinates
714 const QPointF g = mapFromGlobal(nge->globalPosition().toPoint());
715 const RS_Vector mouse = getViewPort()->toWorldFromUi(g.x(), g.y());
716 doZoom(direction, mouse, factor);
717 }
718 return true;
719 }
720 if (event->type() == QEvent::LayoutDirectionChange) {
721 keepScrollbarsOnDrawingSide();
722 }
723 return proceedEvent(event);
724}
725
726/**
727 * The drawing is painted from x = 0 to getWidth(), so the vertical scrollbar stays on the
728 * right in every locale: QGridLayout mirrors columns in a right-to-left UI unless its origin
729 * corner is mirrored too.
730 */
731void QG_GraphicView::keepScrollbarsOnDrawingSide() const {
732 if (m_layout != nullptr) {
733 m_layout->setOriginCorner(isRightToLeft() ? Qt::TopRightCorner : Qt::TopLeftCorner);
734 }
735}
736
737void QG_GraphicView::doZoom(const RS2::ZoomDirection direction, const RS_Vector& center, const double zoomFactor) const {
738 if (direction == RS2::In) {
739 getViewPort()->zoomIn(zoomFactor, center);
740 }
741 else {
742 getViewPort()->zoomOut(zoomFactor, center);
743 }
744}
745
746/**
747 * support for the wacom graphic tablet.
748 */
749void QG_GraphicView::tabletEvent(QTabletEvent* e) {
750 if (testAttribute(Qt::WA_UnderMouse)) {
751#if QT_VERSION((6<<16)|(9<<8)|(0)) >= QT_VERSION_CHECK(6, 0, 0)((6<<16)|(0<<8)|(0))
752 switch (e->pointerType()) {
753 case QPointingDevice::PointerType::Eraser:
754 if (e->type() == QEvent::TabletRelease) {
755 if (getDocument() != nullptr) {
756 auto a = std::make_shared<LC_ActionSelectSingle>(m_actionContext);
757 setCurrentAction(a);
758 QMouseEvent ev(QEvent::MouseButtonRelease, e->position(), e->globalPosition(), Qt::LeftButton, Qt::LeftButton,
759 Qt::NoModifier); //RLZ
760 mouseReleaseEvent(&ev);
761 a->finish();
762
763 if (getDocument()->hasSelection()) {
764 switchToAction(RS2::ActionModifyDelete);
765 }
766 }
767 }
768 break;
769
770 case QPointingDevice::PointerType::Generic:
771 case QPointingDevice::PointerType::Pen:
772 case QPointingDevice::PointerType::Cursor:
773 if (e->type() == QEvent::TabletPress) {
774 QMouseEvent ev(QEvent::MouseButtonPress, e->position(), e->globalPosition(), Qt::LeftButton, Qt::LeftButton,
775 Qt::NoModifier); //RLZ
776 mousePressEvent(&ev);
777 }
778 else if (e->type() == QEvent::TabletRelease) {
779 QMouseEvent ev(QEvent::MouseButtonRelease, e->position(), e->globalPosition(), Qt::LeftButton, Qt::LeftButton,
780 Qt::NoModifier); //RLZ
781 mouseReleaseEvent(&ev);
782 }
783 else if (e->type() == QEvent::TabletMove) {
784 QMouseEvent ev(QEvent::MouseMove, e->position(), e->globalPosition(), Qt::NoButton, {}, Qt::NoModifier); //RLZ
785 mouseMoveEvent(&ev);
786 }
787 break;
788 default:
789 break;
790 }
791#else
792#if QT_VERSION((6<<16)|(9<<8)|(0)) >= QT_VERSION_CHECK(5, 15, 0)((5<<16)|(15<<8)|(0))
793 switch (e->deviceType()) {
794
795#else
796 switch (e->device()) {
797
798#endif
799 case QTabletEvent::Eraser:
800 if (e->type() == QEvent::TabletRelease) {
801 if (getDocument() != nullptr) {
802 RS_ActionSelectSingle* a = new RS_ActionSelectSingle(*getDocument(), *this);
803 setCurrentAction(a);
804 QMouseEvent ev(QEvent::MouseButtonRelease, e->position(), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); //RLZ
805 mouseReleaseEvent(&ev);
806 a->finish();
807
808 if (getDocument()->hasSelection()) {
809 setCurrentAction(new RS_ActionModifyDelete(*getDocument(), *this));
810 }
811 }
812 } break; case QTabletEvent::Stylus: case QTabletEvent::Puck:
813 if (e->type() == QEvent::TabletPress) {
814 QMouseEvent ev(QEvent::MouseButtonPress, e->position(), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); //RLZ
815 mousePressEvent(&ev);
816 }
817 else if (e->type() == QEvent::TabletRelease) {
818 QMouseEvent ev(QEvent::MouseButtonRelease, e->position(), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); //RLZ
819 mouseReleaseEvent(&ev);
820 }
821 else if (e->type() == QEvent::TabletMove) {
822 QMouseEvent ev(QEvent::MouseMove, e->position(), Qt::NoButton, {}, Qt::NoModifier); //RLZ
823 mouseMoveEvent(&ev);
824 } break; default:
825 break;
826 }
827#endif
828 }
829
830 // a 'mouse' click:
831 /*if (e->pressure()>10 && lastPressure<10) {
832 QMouseEvent e(QEvent::MouseButtonPress, e->pos(),
833 Qt::LeftButton, Qt::LeftButton);
834 mousePressEvent(&e);
835}
836 else if (e->pressure()<10 && lastPressure>10) {
837 QMouseEvent e(QEvent::MouseButtonRelease, e->pos(),
838 Qt::LeftButton, Qt::LeftButton);
839 mouseReleaseEvent(&e);
840} else if (lastPos!=e->pos()) {
841 QMouseEvent e(QEvent::MouseMove, e->pos(),
842 Qt::NoButton, 0);
843 mouseMoveEvent(&e);
844}
845
846 lastPressure = e->pressure();
847 lastPos = e->pos();
848 */
849}
850
851void QG_GraphicView::leaveEvent(QEvent* e) {
852 // stop auto-panning
853 m_panData->panTimer.reset();
854 getEventHandler()->mouseLeaveEvent();
855 QWidget::leaveEvent(e);
856}
857
858#if QT_VERSION((6<<16)|(9<<8)|(0)) >= QT_VERSION_CHECK(6, 0, 0)((6<<16)|(0<<8)|(0))
859void QG_GraphicView::enterEvent(QEnterEvent* e) {
860#else
861void QG_GraphicView::enterEvent(QEvent* e) {
862#endif
863 getEventHandler()->mouseEnterEvent();
864 QWidget::enterEvent(e);
865}
866
867void QG_GraphicView::focusOutEvent(QFocusEvent* e) {
868 QWidget::focusOutEvent(e);
869}
870
871void QG_GraphicView::focusInEvent(QFocusEvent* e) {
872 getEventHandler()->mouseEnterEvent();
873 QWidget::focusInEvent(e);
874}
875
876/**
877 * mouse wheel event. zooms in/out or scrolls when
878 * shift or ctrl is pressed.
879 */
880void QG_GraphicView::wheelEvent(QWheelEvent* e) {
881 // LC_ERR << "OWN WHEEL";
882 //RS_DEBUG->print("wheel: %d", e->delta());
883
884 //printf("state: %d\n", e->state());
885 //printf("ctrl: %d\n", Qt::ControlButton);
886
887 if (getDocument() == nullptr) {
888 return;
889 }
890
891#if QT_VERSION((6<<16)|(9<<8)|(0)) >= QT_VERSION_CHECK(5, 15, 0)((5<<16)|(15<<8)|(0))
892 // RS_Vector mouse = toGraph(e->position());
893 const QPointF& uiEventPosition = e->position();
894 const RS_Vector mouse = getViewPort()->toUCSFromGui(uiEventPosition.x(), uiEventPosition.y());
895#else
896 RS_Vector mouse = toGraph(e->position());
897#endif
898
899 if (m_device == "Trackpad") {
900 QPoint numPixels = e->pixelDelta();
901
902 // high-resolution scrolling triggers Pan instead of Zoom logic
903 m_isSmoothScrolling |= !numPixels.isNull();
904
905 if (m_isSmoothScrolling) {
906 if (e->phase() == Qt::ScrollEnd) {
907 m_isSmoothScrolling = false;
908 }
909 }
910 else // Trackpads that without high-resolution scrolling
911 // e.g. libinput-XWayland trackpads
912 {
913 numPixels = e->angleDelta() / 4;
914 }
915
916 if (!numPixels.isNull()) {
917 if (e->modifiers() == Qt::ControlModifier) {
918 // Hold ctrl to zoom. 1 % per pixel
919 const double v = (m_invertZoomDirection) ? (numPixels.y() / ZOOM_WHEEL_DIVISOR) : (-numPixels.y() / ZOOM_WHEEL_DIVISOR);
920 RS2::ZoomDirection direction;
921 if (v < 0) {
922 direction = RS2::Out;
923 }
924 else {
925 direction = RS2::In;
926 }
927
928 const double zoomFact = 1. + std::abs(v);
929 doZoom(direction, mouse, zoomFact);
930 }
931 else {
932 const int hDelta = (m_invertHorizontalScroll) ? -numPixels.x() : numPixels.x();
933 const int vDelta = (m_invertVerticalScroll) ? -numPixels.y() : numPixels.y();
934
935 // pan the viewport, not the scrollbars: a bar is bounded, panning is not (#2945)
936 getViewPort()->zoomPan(hDelta, vDelta);
937 }
938 redraw();
939 }
940 e->accept();
941 return;
942 }
943
944 if (e->angleDelta().isNull()) {
945 // A zero delta event occurs when smooth scrolling is ended. Ignore this
946 e->accept();
947 return;
948 }
949
950 bool scroll = false;
951 RS2::Direction direction = RS2::Up;
952
953 // scroll up / down:
954 const int angleDeltaY = e->angleDelta().y(); // delta for VERTICAL mouse wheel
955 int angleDeltaX = e->angleDelta().x(); // delta for HORIZONTAL mouse wheel
956
957 // for zoom, let's use just vertical scrolling, so below we'll rely on AngleDeltaY only.
958 // otherwise, horizontal scroll will not work :( Basically, that's a side-effect for porting to QT6
959 // so here let's use simpler logic
960 angleDeltaX = angleDeltaY;
961 if (e->modifiers() == Qt::ControlModifier) {
962 scroll = true;
963 direction = (angleDeltaY > 0) ? RS2::Up : RS2::Down;
964 }
965 else if (e->modifiers() == Qt::ShiftModifier) {
966 scroll = true;
967 direction = (angleDeltaX > 0) ? RS2::Left : RS2::Right;
968 }
969
970 // fixme - potentially, we can support mouses with two mouse wheels later if this will be reasonable.
971 // fixme - however, it looks as a kind of overkill - using on single vertical mouse wheel for scroll seems to be fine//
972 /*
973 if (e->modifiers() == Qt::ControlModifier) {
974 scroll = true;
975 if (angleDeltaY == 0){
976 //case Qt::Horizontal:
977 direction= (angleDeltaX > 0) ? RS2::Left : RS2::Right;
978 } else {
979 //case Qt::Vertical:
980 direction= (angleDeltaY > 0) ? RS2::Up : RS2::Down;
981 }
982 }
983 // scroll left / right:
984 else if (e->modifiers()==Qt::ShiftModifier) {
985 scroll = true;
986 if (angleDeltaY == 0){
987 //case Qt::Horizontal:
988 direction= (angleDeltaX > 0) ? RS2::Up : RS2::Down;
989 } else {
990 //case Qt::Vertical:
991 direction= (angleDeltaX > 0) ? RS2::Left : RS2::Right;
992 }
993 }*/
994
995 if (scroll && m_scrollbars) {
996 // pan the viewport with the direction the scrollbars used to give (issue #479);
997 // a bar is bounded, panning is not (#2945)
998 int delta = 0;
999
1000 switch (direction) {
1001 case RS2::Left:
1002 case RS2::Right:
1003 delta = (m_invertHorizontalScroll) ? -angleDeltaX : angleDeltaX;
1004 getViewPort()->zoomPan(-delta, 0);
1005 break;
1006 default:
1007 delta = (m_invertVerticalScroll) ? -angleDeltaY : angleDeltaY;
1008 getViewPort()->zoomPan(0, -delta);
1009 break;
1010 }
1011 }
1012 // zoom in / out:
1013 else if (e->modifiers() == 0) {
1014 // LC_ERR << " AngleDelta Y " << angleDeltaY;
1015
1016 const RS2::ZoomDirection zoomDirection = ((angleDeltaY > 0) != m_invertZoomDirection) ? RS2::In : RS2::Out;
1017
1018 const QPoint viewCenter{getWidth() / 2, getHeight() / 2};
1019 const QPoint delta = viewCenter - uiEventPosition.toPoint();
1020
1021 if (getPanOnZoom()) {
1022 QCursor::setPos(mapToGlobal(viewCenter));
1023 getViewPort()->zoomPan(delta.x(), delta.y());
1024 }
1025 if (!getPanOnZoom() || !getSkipFirstZoom() || (abs(delta.x()) < 32 && abs(delta.y()) < 32)) {
1026 const RS_Vector& zoomCenter = mouse;
1027 // LC_ERR << " Mouse " << mouse << " Direction: " << (zoomDirection == RS2::In ? "In" : "Out");
1028
1029 /*// todo - well, actually this is one-shot action... and it will lead to full action processing chain in action handler
1030 // todo - are we REALLY need it there? alternatively, zoom may be part of this class)
1031 auto zoomAction = std::make_unique<RS_ActionZoomIn>(m_actionContext, zoomDirection, RS2::Both, &zoomCenter,m_scrollZoomFactor);
1032 zoomAction->trigger();*/
1033 const double zoomFactor = std::pow(m_scrollZoomFactor, std::abs(angleDeltaY) / 120.0);
1034 doZoom(zoomDirection, zoomCenter, zoomFactor);
1035 }
1036 }
1037 redraw();
1038
1039 /* QMouseEvent event
1040 {
1041 QEvent::MouseMove,
1042 #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
1043 e->position(),
1044 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
1045 e->globalPosition(),
1046 #endif
1047 #else
1048 QPointF{static_cast<qreal>(e->x()), static_cast<qreal>(e->y())},
1049 #endif
1050 Qt::NoButton, Qt::NoButton, Qt::NoModifier
1051 };
1052 eventHandler->mouseMoveEvent(&event);
1053
1054 e->accept();*/
1055}
1056
1057// fixme - sand - move by keyboard support!!!
1058void QG_GraphicView::keyPressEvent(QKeyEvent* e) {
1059 if (getDocument() == nullptr) {
1060 return;
1061 }
1062 if (m_relativePointWidgetHolder->isVisible()) {
1063 return;
1064 }
1065 bool eventProcessed = false; // due to some weird reasons, even incoming event is already accepted (Win10)... so using own flag
1066 if (m_allowScrollAndMoveAdjustByKeys) {
1067 RS2::Direction direction = RS2::Up;
1068 bool scroll = e->modifiers() == Qt::NoModifier;
1069 const bool shift = e->modifiers() & Qt::ShiftModifier;
1070 const bool control = e->modifiers() & Qt::ControlModifier;
1071
1072 bool move = shift || control;
1073
1074 switch (e->key()) {
1075 case Qt::Key_Left:
1076 direction = RS2::Right;
1077 break;
1078 case Qt::Key_Right:
1079 direction = RS2::Left;
1080 break;
1081 case Qt::Key_Up:
1082 direction = RS2::Up;
1083 break;
1084 case Qt::Key_Down:
1085 direction = RS2::Down;
1086 break;
1087 default:
1088 scroll = false;
1089 move = false;
1090 break;
1091 }
1092
1093 if (scroll) {
1094 getViewPort()->zoomScroll(direction);
1095 eventProcessed = true;
1096 e->accept();
1097 }
1098 else if (move) {
1099 LC_ActionModifyMoveAdjust::MovementInfo::Step step = LC_ActionModifyMoveAdjust::MovementInfo::GRID;
1100 if (control) {
1101 if (shift) {
1102 step = LC_ActionModifyMoveAdjust::MovementInfo::META_GRID;
1103 }
1104 else {
1105 step = LC_ActionModifyMoveAdjust::MovementInfo::SUB_GRID;
1106 }
1107 }
1108 else if (shift) {
1109 step = LC_ActionModifyMoveAdjust::MovementInfo::GRID;
1110 }
1111
1112 LC_ActionModifyMoveAdjust::MovementInfo info(direction, step);
1113 switchToAction(RS2::ActionModifyMoveAdjust, &info);
1114 eventProcessed = true;
1115 e->accept();
1116 }
1117 }
1118 if (!eventProcessed) {
1119 getEventHandler()->keyPressEvent(e);
1120 }
1121}
1122
1123void QG_GraphicView::keyReleaseEvent(QKeyEvent* e) {
1124 getEventHandler()->keyReleaseEvent(e);
1125}
1126
1127/**
1128 * Drawing extents used as the scrollable content, in WCS.
1129 * @return false when the drawing has no extents (empty drawing)
1130 */
1131bool QG_GraphicView::scrollContentExtents(RS_Vector& wcsMin, RS_Vector& wcsMax) const {
1132 auto* viewport = getViewPort();
1133 if (viewport == nullptr || !viewport->getViewBorders(wcsMin, wcsMax)) {
1134 return false;
1135 }
1136 return isRectValid(wcsMin, wcsMax);
1137}
1138
1139namespace {
1140 void applyScrollState(QG_ScrollBar* bar, const LC_ScrollModel::State& state) {
1141 // the bars only display the viewport: changing them must not move it
1142 const QSignalBlocker blocker(bar);
1143 bar->setRange(0, state.maximum);
1144 bar->setPageStep(state.pageStep);
1145 bar->setSingleStep(state.singleStep);
1146 bar->setValue(state.value);
1147 // the drawing extents in the same ticks, so the band and the thumb share one mapping
1148 bar->setContentBand(state.hasContent, state.tickFor(state.contentMin), state.tickFor(state.contentMax));
1149 }
1150
1151 // The viewport fingerprint a scroll snapshot was built from (see LC_ScrollViewportKey).
1152 // The UCS is read by value (hasUCS()/getUcsOrigin()/getXAxisAngle()), never via
1153 // getCurrentUCS(), which heap-allocates a fresh LC_UCS on every call and would leak
1154 // here -- this function runs on essentially every viewport change.
1155 LC_ScrollViewportKey viewportKey(const LC_GraphicViewport& viewport) {
1156 const RS_Vector factor = viewport.getFactor();
1157 return {viewport.getOffsetX(), viewport.getOffsetY(), factor.x, factor.y,
1158 viewport.getWidth(), viewport.getHeight(),
1159 viewport.hasUCS(), viewport.getUcsOrigin(), viewport.getXAxisAngle()};
1160 }
1161}
1162
1163/**
1164 * Builds one axis's LC_ScrollModel::Axis/State from the viewport quantities that feed
1165 * it: the shared math behind both adjustOffsetControls() (which then pushes it into the
1166 * QScrollBar widgets) and rebaseScrollIfStale() (which only updates the internal
1167 * snapshot -- see its comment). \p ox / \p oy / \p width / \p height are the viewport's
1168 * CURRENT offsets and size, so the axis this builds always starts from the view that is
1169 * actually on screen right now.
1170 *
1171 * The int -> double conversions below keep the subtraction/negation itself in double
1172 * (rather than negating/subtracting as int first and widening the result), so this has
1173 * no int-overflow risk regardless of how extreme ox/oy/height are.
1174 */
1175LC_ScrollModel::State QG_GraphicView::computeAxisState(const bool isHorizontal, const bool hasContent,
1176 const RS_Vector& ucsMin, const RS_Vector& ucsMax,
1177 const RS_Vector& factor, const int ox, const int oy,
1178 const int width, const int height) const {
1179 if (isHorizontal) {
1180 return LC_ScrollModel::compute({hasContent, ucsMin.x * factor.x, ucsMax.x * factor.x,
1181 -static_cast<double>(ox), static_cast<double>(width)});
1182 }
1183 return LC_ScrollModel::compute({hasContent, -ucsMax.y * factor.y, -ucsMin.y * factor.y,
1184 static_cast<double>(oy) - static_cast<double>(height),
1185 static_cast<double>(height)});
1186}
1187
1188/**
1189* Called whenever the graphic view has changed.
1190* Projects the viewport onto the scrollbars: the scrollable region is
1191* (drawing extents united with the current view) +/- half a view, so the bars never
1192* clamp or move the view, and always have half a view of room beyond it. See LC_ScrollModel. Bar-driven changes are not synced while they run
1193* (the frozen snapshot); scheduleScrollResync() and sliderReleased sync them after.
1194*/
1195void QG_GraphicView::adjustOffsetControls() {
1196 if (!m_scrollbars || m_hScrollBar == nullptr || m_vScrollBar == nullptr || getDocument() == nullptr) {
1197 return;
1198 }
1199 // a bar slot is moving the view, or we are already syncing: keep the frozen snapshot
1200 if (m_scrollSyncing) {
1201 return;
1202 }
1203 // the range stays frozen while a thumb is held; sliderReleased re-syncs
1204 if (m_hScrollBar->isSliderDown() || m_vScrollBar->isSliderDown()) {
1205 return;
1206 }
1207 auto* viewport = getViewPort();
1208 const int width = viewport->getWidth();
1209 const int height = viewport->getHeight();
1210 if (width <= 0 || height <= 0) {
1211 return;
1212 }
1213 const QScopedValueRollback<bool> guard(m_scrollSyncing, true);
1214
1215 RS_Vector wcsMin;
1216 RS_Vector wcsMax;
1217 RS_Vector ucsMin;
1218 RS_Vector ucsMax;
1219 const bool hasContent = scrollContentExtents(wcsMin, wcsMax);
1220 if (hasContent) {
1221 viewport->ucsBoundsOfWcsBox(wcsMin, wcsMax, ucsMin, ucsMax);
1222 }
1223 const RS_Vector factor = viewport->getFactor();
1224 const int ox = viewport->getOffsetX();
1225 const int oy = viewport->getOffsetY();
1226
1227 m_hScroll = computeAxisState(true, hasContent, ucsMin, ucsMax, factor, ox, oy, width, height);
1228 m_vScroll = computeAxisState(false, hasContent, ucsMin, ucsMax, factor, ox, oy, width, height);
1229 m_scrollKey = viewportKey(*viewport);
1230 if (m_hScroll.valid) {
1231 applyScrollState(m_hScrollBar, m_hScroll);
1232 }
1233 if (m_vScroll.valid) {
1234 applyScrollState(m_vScrollBar, m_vScroll);
1235 }
1236 LC_LOGRS_Debug::Log() << __func__ << "(): H [0, " << m_hScroll.maximum << "] page " << m_hScroll.pageStep << " value "
1237 << m_hScroll.value << "; V [0, " << m_vScroll.maximum << "] page " << m_vScroll.pageStep << " value "
1238 << m_vScroll.value;
1239}
1240
1241/**
1242 * Some paths change the viewport without going through adjustOffsetControls() (a silent
1243 * VPORT restore, applyUCSAfterLoad(), a keyboard zoom while a thumb is held). When the
1244 * viewport no longer matches the fingerprint the snapshot was taken from, this rebuilds
1245 * m_hScroll/m_vScroll's SCALE (pixelsPerTick, maximum, pageStep) from the CURRENT content
1246 * extents, factor, offsets and size -- the same inputs and the same computeAxisState()
1247 * adjustOffsetControls() uses -- so a factor that changed silently is no longer stale
1248 * (the bug this fixes: only the origin used to be corrected, never the scale).
1249 *
1250 * The origin is then RE-ANCHORED, not taken from that fresh compute() as is: the bar
1251 * widget itself is not (and must not be, see below) touched here, so it still shows
1252 * whatever value it last displayed (m_hScroll.value, unchanged since); origin is solved
1253 * for so that viewStartFor(that unchanged value) reproduces the CURRENT actual view start
1254 * exactly, now through the fresh pixelsPerTick. This is what keeps a rebase with no value
1255 * change from moving the view, and what makes a value change from here measure correctly
1256 * in fresh, current pixels-per-tick rather than in whatever pixels-per-tick happened to
1257 * be in effect when the bar was last synced for real.
1258 *
1259 * This must NOT touch the QScrollBar widgets (applyScrollState()/setRange()/setValue()):
1260 * it runs from inside slotHScrolled()/slotVScrolled(), themselves called from a bar's own
1261 * valueChanged, so writing back to the bar here would recurse. The widgets simply stay
1262 * at their last-synced (possibly now stale) range/page-step until the next real resync,
1263 * either adjustOffsetControls() on sliderReleased or a later notified viewport change;
1264 * only the internal snapshot used to map THIS slot call's value needs to be correct.
1265 *
1266 * Returns the freshly computed viewport key (whether or not it differed from
1267 * m_scrollKey), so a caller that already needs it (a slot patching just the one field
1268 * that changed) does not have to call viewportKey() again from scratch.
1269 */
1270LC_ScrollViewportKey QG_GraphicView::rebaseScrollIfStale() {
1271 auto* viewport = getViewPort();
1272 const LC_ScrollViewportKey key = viewportKey(*viewport);
1273 if (key == m_scrollKey) {
1274 return key;
1275 }
1276 const int width = viewport->getWidth();
1277 const int height = viewport->getHeight();
1278 if (width <= 0 || height <= 0) {
1279 // No sane axis state can be built at a degenerate size: leave m_hScroll/m_vScroll
1280 // untouched, and hand back the OLD m_scrollKey (not the freshly computed one) so a
1281 // caller that stores our return value back into m_scrollKey does not mark this
1282 // change as already handled. The next call, once the size is sane again, must still
1283 // see the fingerprint mismatch and recompute for real.
1284 return m_scrollKey;
1285 }
1286 RS_Vector wcsMin;
1287 RS_Vector wcsMax;
1288 RS_Vector ucsMin;
1289 RS_Vector ucsMax;
1290 const bool hasContent = scrollContentExtents(wcsMin, wcsMax);
1291 if (hasContent) {
1292 viewport->ucsBoundsOfWcsBox(wcsMin, wcsMax, ucsMin, ucsMax);
1293 }
1294 const RS_Vector factor = viewport->getFactor();
1295 const int ox = viewport->getOffsetX();
1296 const int oy = viewport->getOffsetY();
1297
1298 if (m_hScroll.valid) {
1299 const double currentStart = -static_cast<double>(ox);
1300 const double anchorValue = m_hScroll.value;
1301 m_hScroll = computeAxisState(true, hasContent, ucsMin, ucsMax, factor, ox, oy, width, height);
1302 if (m_hScroll.valid) {
1303 m_hScroll.origin = currentStart - anchorValue * m_hScroll.pixelsPerTick;
1304 m_hScroll.value = static_cast<int>(anchorValue);
1305 }
1306 }
1307 if (m_vScroll.valid) {
1308 const double currentStart = static_cast<double>(oy) - static_cast<double>(height);
1309 const double anchorValue = m_vScroll.value;
1310 m_vScroll = computeAxisState(false, hasContent, ucsMin, ucsMax, factor, ox, oy, width, height);
1311 if (m_vScroll.valid) {
1312 m_vScroll.origin = currentStart - anchorValue * m_vScroll.pixelsPerTick;
1313 m_vScroll.value = static_cast<int>(anchorValue);
1314 }
1315 }
1316 m_scrollKey = key;
1317 return key;
1318}
1319
1320/**
1321 * Built on demand (QG_ScrollBar asks on QEvent::ToolTip), from the current view and the
1322 * drawing's UCS box computed now, from the same source as the scroll range
1323 * (scrollContentExtents() + ucsBoundsOfWcsBox(), as in adjustOffsetControls()), so a load
1324 * or an edit that has not resynced the bars yet is still described correctly. Values
1325 * are in the drawing's units and linear format. The view range follows
1326 * computeAxisState()'s mapping: a UCS coordinate c is the scroll-space pixel c * factor.x
1327 * (x) or -c * factor.y (y). Every statement is about this bar's axis only. Empty, so no
1328 * tooltip, while the band is switched off (Appearance/ScrollBarContentBand).
1329 */
1330QString QG_GraphicView::scrollBarToolTip(const bool horizontal) const {
1331 auto* viewport = getViewPort();
1332 if (!m_scrollBarContentBand || viewport == nullptr) {
1333 return {};
1334 }
1335 const RS_Vector factor = viewport->getFactor();
1336 const double scale = horizontal ? factor.x : factor.y;
1337 const double length = horizontal ? viewport->getWidth() : viewport->getHeight();
1338 if (!(scale > 0.0) || !(length > 0.0)) {
1339 return {};
1340 }
1341 const double offset = horizontal ? viewport->getOffsetX() : viewport->getOffsetY();
1342 const double viewMin = -offset / scale;
1343 const double viewMax = (length - offset) / scale;
1344
1345 const RS_Graphic* graphic = getGraphic();
1346 const RS2::Unit unit = graphic != nullptr ? graphic->getUnit() : RS2::None;
1347 const RS2::LinearFormat format = graphic != nullptr ? graphic->getLinearFormat() : RS2::Decimal;
1348 const int precision = graphic != nullptr ? graphic->getLinearPrecision() : 4;
1349 auto linear = [unit, format, precision](const double value) {
1350 return RS_Units::formatLinear(value, unit, format, precision);
1351 };
1352 const QString view = horizontal ? tr("View X %1..%2").arg(linear(viewMin), linear(viewMax))
1353 : tr("View Y %1..%2").arg(linear(viewMin), linear(viewMax));
1354
1355 RS_Vector wcsMin;
1356 RS_Vector wcsMax;
1357 if (!scrollContentExtents(wcsMin, wcsMax)) {
1358 return tr("The drawing is empty · %1", "scrollbar tooltip: %1 is the view's range").arg(view);
1359 }
1360 RS_Vector ucsMin;
1361 RS_Vector ucsMax;
1362 viewport->ucsBoundsOfWcsBox(wcsMin, wcsMax, ucsMin, ucsMax);
1363 const double drawingMin = horizontal ? ucsMin.x : ucsMin.y;
1364 const double drawingMax = horizontal ? ucsMax.x : ucsMax.y;
1365 const QString drawing = horizontal ? tr("Drawing X %1..%2").arg(linear(drawingMin), linear(drawingMax))
1366 : tr("Drawing Y %1..%2").arg(linear(drawingMin), linear(drawingMax));
1367
1368 // distance in view lengths: one decimal while it is small, whole numbers beyond
1369 auto views = [horizontal, viewLength = viewMax - viewMin](const double distance) {
1370 const double count = distance / viewLength;
1371 if (count < 0.1) {
1372 return horizontal ? tr("less than 0.1 view widths") : tr("less than 0.1 view heights");
1373 }
1374 const QString number = count < 10.0 ? QString::number(count, 'f', 1)
1375 : QString::number(std::round(count), 'f', 0);
1376 return horizontal ? tr("≈%1 view widths").arg(number) : tr("≈%1 view heights").arg(number);
1377 };
1378 QString where;
1379 switch (LC_ScrollModel::placement(drawingMin, drawingMax, viewMin, viewMax)) {
1380 case LC_ScrollModel::Placement::Covered:
1381 where = horizontal ? tr("view spans the drawing's X range") : tr("view spans the drawing's Y range");
1382 break;
1383 case LC_ScrollModel::Placement::Overlaps:
1384 where = horizontal ? tr("drawing's X range is in view") : tr("drawing's Y range is in view");
1385 break;
1386 case LC_ScrollModel::Placement::Before: {
1387 const double distance = viewMin - drawingMax;
1388 where = horizontal ? tr("drawing's X range is %1 to the left (%2)").arg(linear(distance), views(distance))
1389 : tr("drawing's Y range is %1 down (%2)").arg(linear(distance), views(distance));
1390 break;
1391 }
1392 case LC_ScrollModel::Placement::After: {
1393 const double distance = drawingMin - viewMax;
1394 where = horizontal ? tr("drawing's X range is %1 to the right (%2)").arg(linear(distance), views(distance))
1395 : tr("drawing's Y range is %1 up (%2)").arg(linear(distance), views(distance));
1396 break;
1397 }
1398 }
1399 return tr("%1 · %2 · %3", "scrollbar tooltip: the drawing's range, the view's range, where the drawing is")
1400 .arg(drawing, view, where);
1401}
1402
1403void QG_GraphicView::setScrollBarToolTips(const bool enabled) {
1404 for (QG_ScrollBar* bar : {m_hScrollBar, m_vScrollBar}) {
1405 if (bar == nullptr) {
1406 continue;
1407 }
1408 if (enabled) {
1409 const bool horizontal = bar->orientation() == Qt::Horizontal;
1410 bar->setToolTipProvider([this, horizontal] {
1411 return scrollBarToolTip(horizontal);
1412 });
1413 } else {
1414 bar->setToolTipProvider(nullptr);
1415 }
1416 }
1417}
1418
1419/**
1420 * override this to adjust controls and widgets that
1421 * control the zoom factor of the graphic.
1422 */
1423void QG_GraphicView::adjustZoomControls() {
1424}
1425
1426
1427/**
1428 * Shared body of slotHScrolled()/slotVScrolled(): rebase \p axisState if the viewport
1429 * changed silently, then map \p value through it and apply the result via \p
1430 * applyViewStart, which sets the viewport's offset for this axis and returns the
1431 * (int) offset it actually used. Only that one key field (ox for H, oy for V) is then
1432 * patched into a copy of the key rebaseScrollIfStale() already computed, rather than
1433 * calling viewportKey() again from scratch (see LC_ScrollViewportKey's comment).
1434 *
1435 * The H/V axes differ in more than one line each (Y is flipped and offset by the view
1436 * height, X is not), so those differences stay as the two explicit one-line lambdas at
1437 * each call site; this shares only what is byte-identical between them.
1438 */
1439void QG_GraphicView::driveScrollAxis(LC_ScrollModel::State& axisState, const int value,
1440 const std::function<int(double)>& applyViewStart,
1441 int LC_ScrollViewportKey::* const keyField) {
1442 if (!axisState.valid) {
1443 return;
1444 }
1445 LC_ScrollViewportKey key = rebaseScrollIfStale();
1446 const QScopedValueRollback<bool> driving(m_scrollSyncing, true);
1447 key.*keyField = applyViewStart(axisState.viewStartFor(value));
1448 axisState.value = value;
1449 m_scrollKey = key;
1450 scheduleScrollResync();
1451}
1452
1453/**
1454 * A discrete bar step (an arrow, a trough click, a wheel notch over a bar) is not synced
1455 * while it runs (the snapshot it maps through must not change under it), so the region
1456 * would stay where it was: at its end, the next step would have no room. One resync is
1457 * queued instead, coalescing the steps of one event-loop pass, so the region re-unions
1458 * around the new view and the bar always has half a view of room beyond it: repeated
1459 * steps scroll on without limit. A held thumb is left alone (the range stays frozen for
1460 * a 1:1 drag); sliderReleased resyncs it. The resync never moves the view.
1461 */
1462void QG_GraphicView::scheduleScrollResync() {
1463 if (m_scrollResyncPending || m_hScrollBar == nullptr || m_vScrollBar == nullptr) {
1464 return;
1465 }
1466 if (m_hScrollBar->isSliderDown() || m_vScrollBar->isSliderDown()) {
1467 return;
1468 }
1469 m_scrollResyncPending = true;
1470 QTimer::singleShot(0, this, [this] {
1471 m_scrollResyncPending = false;
1472 adjustOffsetControls();
1473 });
1474}
1475
1476/**
1477 * Slot for horizontal scroll events: maps the bar value through the snapshot
1478 * taken at the last sync. The viewport notification redraws the view.
1479 */
1480void QG_GraphicView::slotHScrolled(const int value) {
1481 driveScrollAxis(m_hScroll, value, [this](const double viewStart) {
1482 const int ox = -LC_ViewMath::saturatingRound(viewStart);
1483 getViewPort()->setOffsetX(ox);
1484 return ox;
1485 }, &LC_ScrollViewportKey::ox);
1486}
1487
1488/**
1489 * Slot for vertical scroll events.
1490 */
1491void QG_GraphicView::slotVScrolled(const int value) {
1492 driveScrollAxis(m_vScroll, value, [this](const double viewStart) {
1493 auto* viewport = getViewPort();
1494 const int oy = LC_ViewMath::saturatingRound(viewStart + viewport->getHeight());
1495 viewport->setOffsetY(oy);
1496 return oy;
1497 }, &LC_ScrollViewportKey::oy);
1498}
1499
1500/**
1501 * @brief setOffset
1502 * @param ox offset X
1503 * @param oy offset Y
1504 */
1505void QG_GraphicView::setOffset([[maybe_unused]] const int ox, [[maybe_unused]] const int oy) {
1506 getViewPort()->setOffsetX(ox);
1507 getViewPort()->setOffsetY(oy);
1508 // need to adjust offset control for scrollbars when setting graphicview offset
1509 adjustOffsetControls();
1510}
1511
1512void QG_GraphicView::layerActivated(RS_Layer* layer) {
1513 const bool applyLayerToSelectedEntities = LC_GET_ONE_BOOLRS_Settings::instance()->readBoolSingle("Modify", "ModifyEntitiesToActiveLayer");
1514
1515 if (applyLayerToSelectedEntities) {
1516 RS_Graphic* graphic = getGraphic();
1517 if (graphic != nullptr) {
1518 const auto doc = getDocument();
1519 const auto selection = doc->getSelection();
1520 if (!selection->isEmpty()) {
1521 QList<RS_Entity*> selected;
1522 selection->collectSelectedEntities(selected);
1523 if (!selected.isEmpty()) {
1524 doc->undoableModify(getViewPort(), [selected, layer](LC_DocumentModificationBatch& ctx)-> bool {
1525 for (const auto en : std::as_const(selected)) {
1526 if (en != nullptr && en->isAlive()) {
1527 RS_Entity* clone = en->clone();
1528 clone->setLayer(layer);
1529 clone->setPen(en->getPen(false));
1530 clone->clearSelectionFlag();
1531 clone->update();
1532 ctx += clone;
1533 ctx -= en;
1534 }
1535 }
1536 ctx.dontSetActiveLayerAndPen();
1537 return true;
1538 }, [this]([[maybe_unused]] LC_DocumentModificationBatch& ctx, RS_Document* d)-> void {
1539 RS_Selection::unselectAllInDocument(d, getViewPort());
1540 });
1541 }
1542 }
1543
1544 graphic->updateInserts();
1545 doc->calculateBorders();
1546 doc->clearSelectionFlag();
1547 redraw(RS2::RedrawDrawing);
1548 }
1549 }
1550}
1551
1552/**
1553 * Handles paint events by redrawing the graphic in this view.
1554 * usually that's very fast since we only paint the buffer we
1555 * have from the last call..
1556 */
1557void QG_GraphicView::paintEvent(QPaintEvent*) {
1558 getRenderer()->render();
1559}
1560
1561#define HIDE_SELECT_CURSORfalse false
1562
1563void QG_GraphicView::loadSettings() {
1564 RS_GraphicView::loadSettings();
1565
1566 {
1567 LC_GROUP_GUARDauto _guard_settings = RS_Settings::instance()->beginGroupGuard("Appearance");
1568 const int zoomFactor1000 = LC_GET_INTRS_Settings::instance()->readInt("ScrollZoomFactor", 1137);
1569 m_scrollZoomFactor = zoomFactor1000 / 1000.0;
1570
1571 m_ucsHighlightData->maxBlinkNumber = LC_GET_INTRS_Settings::instance()->readInt("UCSHighlightBlinkCount", 10) * 2;
1572 // one blink includes both for visible and invisible phase
1573 m_ucsHighlightData->timerInterval = LC_GET_INTRS_Settings::instance()->readInt("UCSHighlightBlinkDelay", 250);
1574 }
1575
1576 {
1577 LC_GROUP_GUARDauto _guard_settings = RS_Settings::instance()->beginGroupGuard("Defaults");
1578 m_invertZoomDirection = LC_GET_ONE_BOOLRS_Settings::instance()->readBoolSingle("Defaults", "InvertZoomDirection");
1579 m_invertHorizontalScroll = LC_GET_BOOLRS_Settings::instance()->readBool("WheelScrollInvertH");
1580 m_invertVerticalScroll = LC_GET_BOOLRS_Settings::instance()->readBool("WheelScrollInvertV");
1581 }
1582
1583 m_allowScrollAndMoveAdjustByKeys = LC_GET_ONE_BOOLRS_Settings::instance()->readBoolSingle("Keyboard", "AllowScrollMoveAdjustByKeys", true);
1584
1585 m_scrollBarContentBand = LC_GET_ONE_BOOLRS_Settings::instance()->readBoolSingle("Appearance", "ScrollBarContentBand",
1586 QG_ScrollBar::kContentBandDefault);
1587 if (m_hScrollBar != nullptr && m_vScrollBar != nullptr) {
1588 m_hScrollBar->setContentBandEnabled(m_scrollBarContentBand);
1589 m_vScrollBar->setContentBandEnabled(m_scrollBarContentBand);
1590 }
1591
1592 LC_GROUPRS_Settings::instance()->beginGroup("Appearance");
1593 {
1594 m_cursorHiding = LC_GET_BOOLRS_Settings::instance()->readBool("cursor_hiding", false);
1595 bool showSnapIndicatorLines = LC_GET_BOOLRS_Settings::instance()->readBool("indicator_lines_state", true);
Value stored to 'showSnapIndicatorLines' during its initialization is never read
1596 bool showSnapIndicatorShape = LC_GET_BOOLRS_Settings::instance()->readBool("indicator_shape_state", true);
1597 if (HIDE_SELECT_CURSORfalse) {
1598 // potentially, select cursor may be also hidden and so snapper will be used instead of cursor.
1599 // however, this will require review and modifications of significant amount of actions, so
1600 // probably I'll return to this later. In such case, the code within this "if" will be handy for such support
1601 m_selectCursorHiding = m_cursorHiding && (showSnapIndicatorLines || showSnapIndicatorShape);
1602 }
1603 m_selectCursorHiding = false;
1604 }
1605 LC_GROUP_ENDRS_Settings::instance()->endGroup();
1606 m_ucsMarkOptions->loadSettings();
1607
1608 LC_GROUPRS_Settings::instance()->beginGroup("Colors");
1609 {
1610 const RS_Color bgColor(LC_GET_STRRS_Settings::instance()->readStr("RelativePositionAssistantBackground", RS_Settings::RELATIVE_POSITION_BACKGROUND));
1611 const RS_Color txtColor(LC_GET_STRRS_Settings::instance()->readStr("RelativePositionAssistantText", RS_Settings::RELATIVE_POSITION_BACKGROUND));
1612 m_relativePointWidgetHolder->setWidgetColors(bgColor, txtColor);
1613 }
1614 LC_GROUP_ENDRS_Settings::instance()->endGroup();
1615
1616 LC_GROUPRS_Settings::instance()->beginGroup("RelativePositionAssistant");
1617 {
1618 const int fontSize = LC_GET_INTRS_Settings::instance()->readInt("FontSize", 10);
1619 const QString fontName = LC_GET_STRRS_Settings::instance()->readStr("FontName", "Helvetica");
1620 m_relativePointWidgetHolder->setFont(fontName, fontSize);
1621 }
1622 LC_GROUP_ENDRS_Settings::instance()->endGroup();
1623}
1624
1625void QG_GraphicView::setAntialiasing(const bool state) const {
1626 getRenderer()->setAntialiasing(state);
1627}
1628
1629bool QG_GraphicView::isAntialiasing() const {
1630 return getRenderer()->isAntialiasing();
1631}
1632
1633bool QG_GraphicView::isDraftMode() const {
1634 const auto* viewRenderer = dynamic_cast<LC_GraphicViewRenderer*>(getRenderer());
1635 return (viewRenderer != nullptr) ? viewRenderer->isDraftMode() : false;
1636}
1637
1638void QG_GraphicView::setDraftMode(const bool dm) {
1639 auto* viewRenderer = dynamic_cast<LC_GraphicViewRenderer*>(getRenderer());
1640 if (viewRenderer != nullptr) {
1641 viewRenderer->setDraftMode(dm);
1642 redraw();
1643 }
1644}
1645
1646void QG_GraphicView::setDraftLinesMode(const bool mode) const {
1647 auto* viewRenderer = dynamic_cast<LC_GraphicViewRenderer*>(getRenderer());
1648 if (viewRenderer != nullptr) {
1649 viewRenderer->setLineWidthScaling(!mode);
1650 }
1651}
1652
1653bool QG_GraphicView::isDraftLinesMode() const {
1654 const auto* viewRenderer = dynamic_cast<LC_GraphicViewRenderer*>(getRenderer());
1655 if (viewRenderer != nullptr) {
1656 return !viewRenderer->getLineWidthScaling();
1657 }
1658 return false;
1659}
1660
1661void QG_GraphicView::addScrollbars() {
1662 m_scrollbars = true;
1663
1664 m_hScrollBar = new QG_ScrollBar(Qt::Horizontal, this);
1665 m_vScrollBar = new QG_ScrollBar(Qt::Vertical, this);
1666 m_hScrollBar->setContentBandEnabled(m_scrollBarContentBand);
1667 m_vScrollBar->setContentBandEnabled(m_scrollBarContentBand);
1668 m_layout = new QGridLayout(this);
1669
1670 setOffset(50, 50);
1671
1672#if QT_VERSION((6<<16)|(9<<8)|(0)) >= QT_VERSION_CHECK(6, 0, 0)((6<<16)|(0<<8)|(0))
1673 m_layout->setContentsMargins(QMargins{});
1674#else
1675 layout->setMargin(0);
1676#endif
1677 m_layout->setSpacing(0);
1678 m_layout->setColumnStretch(0, 1);
1679 m_layout->setColumnStretch(1, 0);
1680 m_layout->setColumnStretch(2, 0);
1681 m_layout->setRowStretch(0, 1);
1682 m_layout->setRowStretch(1, 0);
1683 keepScrollbarsOnDrawingSide();
1684
1685 // adjustOffsetControls() freezes the range on sliderReleased through a direct
1686 // connection, relying on sliderReleased firing after the final SliderMove. With
1687 // tracking off, Qt emits sliderReleased before that move (qabstractslider.cpp),
1688 // and a resync there would discard the drag. QG_ScrollBar::init() enables tracking
1689 // unconditionally, so this invariant holds in release builds too, not only where
1690 // Q_ASSERT compiles in.
1691 m_hScrollBar->setCursor(Qt::ArrowCursor);
1692 m_layout->addWidget(m_hScrollBar, 1, 0);
1693 connect(m_hScrollBar, &QG_ScrollBar::valueChanged, this, &QG_GraphicView::slotHScrolled);
1694 connect(m_hScrollBar, &QG_ScrollBar::sliderReleased, this, &QG_GraphicView::adjustOffsetControls);
1695
1696 m_vScrollBar->setCursor(Qt::ArrowCursor);
1697 m_layout->addWidget(m_vScrollBar, 0, 1);
1698 connect(m_vScrollBar, &QG_ScrollBar::valueChanged, this, &QG_GraphicView::slotVScrolled);
1699 connect(m_vScrollBar, &QG_ScrollBar::sliderReleased, this, &QG_GraphicView::adjustOffsetControls);
1700}
1701
1702bool QG_GraphicView::hasScrollbars() const {
1703 return m_scrollbars;
1704}
1705
1706void QG_GraphicView::setCursorHiding(const bool state) {
1707 m_cursorHiding = state;
1708}
1709
1710void QG_GraphicView::setCurrentQAction(QAction* q_action) {
1711 getEventHandler()->setQAction(q_action);
1712
1713 if (m_recentActions.contains(q_action)) {
1714 m_recentActions.removeOne(q_action);
1715 }
1716 m_recentActions.prepend(q_action);
1717}
1718
1719void QG_GraphicView::startAutoPanTimer(const QMouseEvent* event) {
1720 if (event == nullptr) {
1721 return;
1722 }
1723 const RS_Vector cadArea_minCoord(0., 0.);
1724 const RS_Vector cadArea_maxCoord(getWidth(), getHeight());
1725 const LC_Rect cadArea_actual(cadArea_minCoord, cadArea_maxCoord);
1726 const LC_Rect cadArea_unprobed(cadArea_minCoord + m_panData->probedAreaOffset, cadArea_maxCoord - m_panData->probedAreaOffset);
1727
1728 RS_Vector mouseCoord{event->position()};
1729 mouseCoord.y = cadArea_actual.height() - mouseCoord.y;
1730
1731 const RS_Vector cadArea_centerPoint((cadArea_minCoord + cadArea_maxCoord) / 2.0);
1732 RS_Vector offset = mouseCoord - cadArea_centerPoint;
1733 offset = {std::abs(offset.x) - cadArea_unprobed.width() / 2., std::abs(offset.y) - cadArea_unprobed.height() / 2.};
1734 offset = {std::max(offset.x, 1.), std::max(offset.y, 1.)};
1735
1736 const double panOffset_angle{cadArea_centerPoint.angleTo(mouseCoord)};
1737
1738 /* It would be better if the below value was calculated in the code that deals with resizing the CAD area. */
1739 const double quarterAngle = cadArea_centerPoint.angleTo(cadArea_actual.upperRightCorner());
1740
1741 double percentageFactor;
1742
1743 if (((panOffset_angle > quarterAngle) && (panOffset_angle <= (M_PI3.14159265358979323846 - quarterAngle))) || ((panOffset_angle > (quarterAngle + M_PI3.14159265358979323846)) && (
1744 panOffset_angle <= (M_PI3.14159265358979323846 + M_PI3.14159265358979323846 - quarterAngle)))) {
1745 percentageFactor = (std::abs((mouseCoord - cadArea_centerPoint).y) - cadArea_unprobed.height() / 2.0) / (cadArea_actual.height() /
1746 2.0 - cadArea_unprobed.height() / 2.0);
1747 }
1748 else {
1749 percentageFactor = (std::abs((mouseCoord - cadArea_centerPoint).x) - cadArea_unprobed.width() / 2.0) / (cadArea_actual.width() / 2.0
1750 - cadArea_unprobed.width() / 2.0);
1751 }
1752
1753 const double panTimerInterval{
1754 m_panData->panTimerIntervalMinimum + ((m_panData->panTimerIntervalMaximum - m_panData->panTimerIntervalMinimum) * (1.0 -
1755 percentageFactor))
1756 };
1757
1758 offset = RS_Vector::polar(offset.magnitude(), M_PI3.14159265358979323846 - panOffset_angle);
1759 m_panData->panOffset = {static_cast<int>(offset.x), static_cast<int>(offset.y)};
1760
1761 if (m_panData->panTimer != nullptr) {
1762 m_panData->panTimer->setInterval(panTimerInterval);
1763 }
1764 else {
1765 m_panData->start(panTimerInterval, *this);
1766 }
1767
1768 if (RS_DEBUGRS_Debug::instance()->getLevel() >= RS_Debug::D_INFORMATIONAL) {
1769 std::cout << " CAD area centre point = " << cadArea_centerPoint << std::endl <<
1770 " Actual CAD area quarter angle (deg) = " << quarterAngle * 180.0 / M_PI3.14159265358979323846 << std::endl <<
1771 " Percentage factor = " << percentageFactor << std::endl << " Pan offset angle (radians) = " <<
1772 panOffset_angle << std::endl << " Pan offset angle (degrees) = " << panOffset_angle * 180.0 / M_PI3.14159265358979323846 << std::endl <<
1773 " Pan offset vector = " << m_panData->panOffset.x() << ", " << m_panData->panOffset.y() << std::endl
1774 //<< " Pan timer interval (ms) = " << m_panData->panTimer->interfac
1775 << std::endl << " Mouse (cursor) position (adjusted) = " << mouseCoord << std::endl <<
1776 " Mouse position w.r.t. centre point = " << mouseCoord - cadArea_centerPoint << std::endl << std::endl << std::endl;
1777 }
1778}
1779
1780bool QG_GraphicView::isAutoPan(const QMouseEvent* event) const {
1781 if (event == nullptr) {
1782 return false;
1783 }
1784
1785 const bool autopanEnabled = LC_GET_ONE_BOOLRS_Settings::instance()->readBoolSingle("Appearance", "Autopanning");
1786
1787 if (!autopanEnabled) {
1788 return false;
1789 }
1790
1791 const RS_Vector cadArea_minCoord(0., 0.);
1792 const RS_Vector cadArea_maxCoord(getWidth(), getHeight());
1793 const LC_Rect cadArea_actual(cadArea_minCoord, cadArea_maxCoord);
1794 const LC_Rect cadArea_unprobed(cadArea_minCoord + m_panData->probedAreaOffset, cadArea_maxCoord - m_panData->probedAreaOffset);
1795 if (cadArea_unprobed.width() < 0. || cadArea_unprobed.height() < 0.) {
1796 return false;
1797 }
1798
1799 const RS_Vector mouseCoord{event->position()};
1800
1801 if (RS_DEBUGRS_Debug::instance()->getLevel() >= RS_Debug::D_INFORMATIONAL) {
1802 std::cout << " Unprobed CAD area width and height = " << cadArea_unprobed.width() << "/" << cadArea_unprobed.height() << std::endl
1803 << " Actual CAD area width and height = " << cadArea_actual.width() << "/" << cadArea_actual.height() << std::endl <<
1804 " Mouse (cursor) position = " << mouseCoord << std::endl << std::endl;
1805 }
1806
1807 return cadArea_actual.inArea(mouseCoord) && !cadArea_unprobed.inArea(mouseCoord);
1808}
1809
1810void QG_GraphicView::deleteActionContext() const {
1811 delete m_actionContext;
1812}
1813
1814/*
1815 Auto-pans the CAD area.
1816 - by Melwyn Francis Carlo <carlo.melwyn@outlook.com>
1817*/
1818void QG_GraphicView::autoPanStep() const {
1819 // skip first steps to avoid unintensional panning
1820 m_panData->delayCounter = std::min(++m_panData->delayCounter, m_panData->delayCounterMax);
1821 if (m_panData->delayCounter < m_panData->delayCounterMax) {
1822 return;
1823 }
1824
1825 RS_DEBUGRS_Debug::instance()->print(RS_Debug::D_INFORMATIONAL, "%s(): Timer is ticking!", __func__);
1826 getViewPort()->zoomPan(m_panData->panOffset.x(), m_panData->panOffset.y());
1827}
1828
1829QString QG_GraphicView::obtainEntityDescription(RS_Entity* entity, const RS2::EntityDescriptionLevel shortDescription) {
1830 const LC_QuickInfoWidget* entityInfoWidget = QC_ApplicationWindow::getAppWindow()->getEntityInfoWidget();
1831 if (entityInfoWidget != nullptr) {
1832 QString result = entityInfoWidget->getEntityDescription(entity, shortDescription);
1833 return result;
1834 }
1835 return "";
1836}
1837
1838void QG_GraphicView::ucsHighlightStep() {
1839 const auto overlayContainer = getViewPort()->getOverlaysDrawablesContainer(RS2::OverlayGraphics::ActionPreviewEntity);
1840 overlayContainer->clear();
1841 if (m_ucsHighlightData->mayTick()) {
1842 if (m_ucsHighlightData->inVisiblePhase) {
1843 // note - potentially, here we may simply store data for custom ucs mark and create object in renderer....
1844 // that will eliminate storing ucs mark settings in this class
1845 const auto ucsMark = new LC_OverlayUCSMark(m_ucsHighlightData->origin, m_ucsHighlightData->angle, m_ucsHighlightData->forWCS,
1846 m_ucsMarkOptions.get());
1847 overlayContainer->add(ucsMark);
1848 }
1849 else {
1850 }
1851 }
1852 else {
1853 m_ucsHighlightData->stop();
1854 // restore current view position; setOffsetAndFactor (not justSetOffsetAndFactor)
1855 // notifies, so the scrollbars resync and the drawing layer, not just the overlay,
1856 // is re-rendered (RedrawDrawing, not only RedrawOverlay as below)
1857 getViewPort()->setOffsetAndFactor(m_ucsHighlightData->savedViewOffset.x, m_ucsHighlightData->savedViewOffset.y,
1858 m_ucsHighlightData->savedViewFactor);
1859 }
1860 redraw(RS2::RedrawOverlay);
1861 update();
1862}
1863
1864void QG_GraphicView::highlightUCSLocation(LC_UCS* ucs) {
1865 if (ucs == nullptr) {
1866 return;
1867 }
1868
1869 const auto viewport = getViewPort();
1870 // save current view position
1871 m_ucsHighlightData->savedViewOffset.x = viewport->getOffsetX();
1872 m_ucsHighlightData->savedViewOffset.y = viewport->getOffsetY();
1873 m_ucsHighlightData->savedViewFactor = viewport->getFactor().x;
1874
1875 const RS_Vector origin = ucs->getOrigin();
1876 const double angle = ucs->getXAxisDirection();
1877
1878 // try to ensure that origin of UCS is visible if it's outside of visible part of drawing
1879 const double AXIS_SIZE = viewport->toUcsDX(20); // fixme - ucs - or toUcsX?
1880 viewport->zoomAutoEnsurePointsIncluded(origin, origin.relative(AXIS_SIZE, angle), origin.relative(AXIS_SIZE, angle + M_PI_21.57079632679489661923));
1881
1882 double uiOriginPointX = 0., uiOriginPointY = 0.;
1883 viewport->toUI(origin, uiOriginPointX, uiOriginPointY);
1884
1885 const double ucsXAxisAngleInUCS = viewport->toUCSAngle(angle);
1886
1887 m_ucsHighlightData->origin = RS_Vector(uiOriginPointX, uiOriginPointY);
1888 m_ucsHighlightData->angle = -ucsXAxisAngleInUCS;
1889 m_ucsHighlightData->forWCS = !ucs->isUCS();
1890 const double timerInterval = m_ucsHighlightData->timerInterval;
1891 m_ucsHighlightData->start(timerInterval, *this);
1892}