1 /*
2 * Copyright 2012-2014 Thomas Schöps
3 * Copyright 2013-2017 Kai Pastor
4 *
5 * This file is part of OpenOrienteering.
6 *
7 * OpenOrienteering is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * OpenOrienteering is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with OpenOrienteering. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21
22 #include "tool_base.h"
23
24 #include <algorithm>
25 #include <cstdlib> // IWYU pragma: keep
26 #include <iterator>
27 #include <type_traits>
28
29 #include <QtGlobal>
30 #include <QCoreApplication> // IWYU pragma: keep
31 #include <QMouseEvent>
32 #include <QTimer>
33 #include <QEvent>
34 #include <QKeyEvent>
35 #include <QRectF>
36
37 #include "core/map.h"
38 #include "core/objects/object.h"
39 #include "core/renderables/renderable.h"
40 #include "gui/map/map_editor.h"
41 #include "gui/map/map_widget.h"
42 #include "gui/widgets/key_button_bar.h" // IWYU pragma: keep
43 #include "tools/tool_helpers.h"
44 #include "undo/object_undo.h"
45
46
47 #ifdef __clang_analyzer__
48 #define singleShot(A, B, C) singleShot(A, B, #C) // NOLINT
49 #endif
50
51
52 namespace OpenOrienteering {
53
54 // ### MapEditorToolBase::EditedItem ###
55
EditedItem(Object * active_object)56 MapEditorToolBase::EditedItem::EditedItem(Object* active_object)
57 : active_object { active_object }
58 , duplicate { active_object ? active_object->duplicate() : nullptr }
59 {
60 // nothing else
61
62 // These assertion must be within namespace MapEditorToolBase,
63 // because EditedItem is a private member of this namespace.
64 static_assert(std::is_nothrow_move_constructible<MapEditorToolBase::EditedItem>::value,
65 "MapEditorToolBase::EditedItem must be nothrow move constructible.");
66 static_assert(std::is_nothrow_move_assignable<MapEditorToolBase::EditedItem>::value,
67 "MapEditorToolBase::EditedItem must be nothrow move assignable.");
68 }
69
70
EditedItem(const EditedItem & prototype)71 MapEditorToolBase::EditedItem::EditedItem(const EditedItem& prototype)
72 : MapEditorToolBase::EditedItem::EditedItem { prototype.active_object }
73 {
74 // nothing else
75 }
76
77
EditedItem(EditedItem && prototype)78 MapEditorToolBase::EditedItem::EditedItem(EditedItem&& prototype) noexcept
79 : active_object { prototype.active_object }
80 , duplicate { std::move(prototype.duplicate) }
81 {
82 // nothing else
83 }
84
85
86
operator =(const EditedItem & prototype)87 MapEditorToolBase::EditedItem& MapEditorToolBase::EditedItem::operator=(const EditedItem& prototype)
88 {
89 if (&prototype == this)
90 return *this;
91
92 active_object = prototype.active_object;
93 duplicate.reset(prototype.duplicate ? prototype.duplicate->duplicate() : nullptr);
94 return *this;
95 }
96
97
operator =(EditedItem && prototype)98 MapEditorToolBase::EditedItem& MapEditorToolBase::EditedItem::operator=(EditedItem&& prototype) noexcept
99 {
100 if (&prototype == this)
101 return *this;
102
103 active_object = prototype.active_object;
104 duplicate = std::move(prototype.duplicate);
105 return *this;
106 }
107
108
109
isModified() const110 bool MapEditorToolBase::EditedItem::isModified() const
111 {
112 return !duplicate->equals(active_object, false);
113 }
114
115
116
117 // ### MapEditorToolBase ###
118
MapEditorToolBase(const QCursor & cursor,MapEditorTool::Type type,MapEditorController * editor,QAction * tool_action)119 MapEditorToolBase::MapEditorToolBase(const QCursor& cursor, MapEditorTool::Type type, MapEditorController* editor, QAction* tool_action)
120 : MapEditorTool(editor, type, tool_action),
121 effective_start_drag_distance(startDragDistance()),
122 angle_helper(new ConstrainAngleToolHelper()),
123 snap_helper(new SnappingToolHelper(this)),
124 cur_map_widget(editor->getMainWidget()),
125 cursor(scaledToScreen(cursor)),
126 renderables(new MapRenderables(map())),
127 old_renderables(new MapRenderables(map()))
128 {
129 angle_helper->setActive(false);
130 }
131
~MapEditorToolBase()132 MapEditorToolBase::~MapEditorToolBase()
133 {
134 old_renderables->clear(false);
135 if (key_button_bar)
136 editor->deletePopupWidget(key_button_bar);
137 }
138
139
init()140 void MapEditorToolBase::init()
141 {
142 connect(map(), &Map::objectSelectionChanged, this, &MapEditorToolBase::objectSelectionChanged);
143 connect(map(), &Map::selectedObjectEdited, this, &MapEditorToolBase::updateDirtyRect);
144 initImpl();
145 updateDirtyRect();
146 updateStatusText();
147
148 MapEditorTool::init();
149 }
150
initImpl()151 void MapEditorToolBase::initImpl()
152 {
153 // nothing
154 }
155
getCursor() const156 const QCursor& MapEditorToolBase::getCursor() const
157 {
158 return cursor;
159 }
160
161
162
mousePositionEvent(QMouseEvent * event,const MapCoordF & map_coord,MapWidget * widget)163 void MapEditorToolBase::mousePositionEvent(QMouseEvent* event, const MapCoordF& map_coord, MapWidget* widget)
164 {
165 active_modifiers = event->modifiers();
166 cur_pos = event->pos();
167 cur_pos_map = map_coord;
168 cur_map_widget = widget;
169 updateConstrainedPositions();
170
171 if (event->button() == Qt::LeftButton && event->type() == QEvent::MouseButtonPress)
172 {
173 click_pos = cur_pos;
174 click_pos_map = cur_pos_map;
175 constrained_click_pos = constrained_pos;
176 constrained_click_pos_map = constrained_pos_map;
177 }
178 else if (dragging_canceled)
179 {
180 click_pos = cur_pos;
181 click_pos_map = cur_pos_map;
182 constrained_click_pos = constrained_pos;
183 constrained_click_pos_map = constrained_pos_map;
184 dragging_canceled = false;
185 }
186 }
187
188
mousePressEvent(QMouseEvent * event,const MapCoordF & map_coord,MapWidget * widget)189 bool MapEditorToolBase::mousePressEvent(QMouseEvent* event, const MapCoordF& map_coord, MapWidget* widget)
190 {
191 mousePositionEvent(event, map_coord, widget);
192
193 if (event->button() == Qt::LeftButton)
194 {
195 clickPress();
196 return true;
197 }
198 else if (event->button() == Qt::RightButton)
199 {
200 // Do not show the ring menu when editing
201 return editingInProgress();
202 }
203 else
204 {
205 return false;
206 }
207 }
208
mouseMoveEvent(QMouseEvent * event,const MapCoordF & map_coord,MapWidget * widget)209 bool MapEditorToolBase::mouseMoveEvent(QMouseEvent* event, const MapCoordF& map_coord, MapWidget* widget)
210 {
211 mousePositionEvent(event, map_coord, widget);
212
213 if (event->buttons().testFlag(Qt::LeftButton))
214 {
215 if (dragging)
216 {
217 updateDragging();
218 }
219 else if ((cur_pos - click_pos).manhattanLength() >= effective_start_drag_distance)
220 {
221 startDragging();
222 }
223 return true;
224 }
225 else
226 {
227 mouseMove();
228 return false;
229 }
230 }
231
mouseReleaseEvent(QMouseEvent * event,const MapCoordF & map_coord,MapWidget * widget)232 bool MapEditorToolBase::mouseReleaseEvent(QMouseEvent* event, const MapCoordF& map_coord, MapWidget* widget)
233 {
234 mousePositionEvent(event, map_coord, widget);
235
236 if (event->button() == Qt::LeftButton)
237 {
238 if (dragging)
239 {
240 finishDragging();
241 }
242 else
243 {
244 clickRelease();
245 }
246 return true;
247 }
248 else if (event->button() == Qt::RightButton)
249 {
250 // Do not show the ring menu when editing
251 return editingInProgress();
252 }
253 else
254 {
255 return false;
256 }
257 }
258
keyPressEvent(QKeyEvent * event)259 bool MapEditorToolBase::keyPressEvent(QKeyEvent* event)
260 {
261 active_modifiers = event->modifiers();
262 #if defined(Q_OS_MACOS)
263 // FIXME: On Mac, QKeyEvent::modifiers() seems to return the keyboard
264 // modifier flags that existed immediately before the event occurred.
265 // This is in contradiction to the documentation for QKeyEvent::modifiers(),
266 // but the documented behaviour of (parent) QInputEvent::modifiers().
267 // Qt5 doc says QKeyEvent::modifiers() "cannot always be trusted." ...
268 switch (event->key())
269 {
270 case Qt::Key_Shift:
271 active_modifiers |= Qt::ShiftModifier;
272 break;
273 case Qt::Key_Control:
274 active_modifiers |= Qt::ControlModifier;
275 break;
276 case Qt::Key_Alt:
277 active_modifiers |= Qt::AltModifier;
278 break;
279 case Qt::Key_Meta:
280 active_modifiers |= Qt::MetaModifier;
281 break;
282 default:
283 ; // nothing
284 }
285 #endif
286 return keyPress(event);
287 }
288
keyReleaseEvent(QKeyEvent * event)289 bool MapEditorToolBase::keyReleaseEvent(QKeyEvent* event)
290 {
291 active_modifiers = event->modifiers();
292 #if defined(Q_OS_MACOS)
293 // FIXME: On Mac, QKeyEvent::modifiers() seems to return the keyboard
294 // modifier flags that existed immediately before the event occurred.
295 // This is in contradiction to the documentation for QKeyEvent::modifiers(),
296 // but the documented behaviour of (parent) QInputEvent::modifiers().
297 // Qt5 doc says QKeyEvent::modifiers() "cannot always be trusted." ...
298 switch (event->key())
299 {
300 case Qt::Key_Shift:
301 active_modifiers &= ~Qt::ShiftModifier;
302 break;
303 case Qt::Key_Control:
304 active_modifiers &= ~Qt::ControlModifier;
305 break;
306 case Qt::Key_Alt:
307 active_modifiers &= ~Qt::AltModifier;
308 break;
309 case Qt::Key_Meta:
310 active_modifiers &= ~Qt::MetaModifier;
311 break;
312 default:
313 ; // nothing
314 }
315 #endif
316 return keyRelease(event);
317 }
318
draw(QPainter * painter,MapWidget * widget)319 void MapEditorToolBase::draw(QPainter* painter, MapWidget* widget)
320 {
321 drawImpl(painter, widget);
322 if (angle_helper->isActive())
323 angle_helper->draw(painter, widget);
324 if (snap_helper->getFilter() != SnappingToolHelper::NoSnapping)
325 snap_helper->draw(painter, widget);
326 }
327
updateDirtyRect()328 void MapEditorToolBase::updateDirtyRect()
329 {
330 int pixel_border = 0;
331 QRectF rect;
332
333 map()->includeSelectionRect(rect);
334 if (angle_helper->isActive())
335 {
336 angle_helper->includeDirtyRect(rect);
337 pixel_border = qMax(pixel_border, angle_helper->getDisplayRadius());
338 }
339 if (snap_helper->getFilter() != SnappingToolHelper::NoSnapping)
340 {
341 snap_helper->includeDirtyRect(rect);
342 pixel_border = qMax(pixel_border, snap_helper->getDisplayRadius());
343 }
344
345 pixel_border = qMax(pixel_border, updateDirtyRectImpl(rect));
346 if (pixel_border >= 0)
347 map()->setDrawingBoundingBox(rect, pixel_border, true);
348 else
349 map()->clearDrawingBoundingBox();
350 }
351
objectSelectionChanged()352 void MapEditorToolBase::objectSelectionChanged()
353 {
354 objectSelectionChangedImpl();
355 }
356
drawImpl(QPainter * painter,MapWidget * widget)357 void MapEditorToolBase::drawImpl(QPainter* painter, MapWidget* widget)
358 {
359 drawSelectionOrPreviewObjects(painter, widget);
360 }
361
clickPress()362 void MapEditorToolBase::clickPress()
363 {
364 // nothing
365 }
366
clickRelease()367 void MapEditorToolBase::clickRelease()
368 {
369 // nothing
370 }
371
mouseMove()372 void MapEditorToolBase::mouseMove()
373 {
374 // nothing
375 }
376
gestureStarted()377 void MapEditorToolBase::gestureStarted()
378 {
379 if (dragging)
380 cancelDragging();
381 }
382
startDragging()383 void MapEditorToolBase::startDragging()
384 {
385 Q_ASSERT(!dragging);
386 dragging = true;
387 dragging_canceled = false;
388 dragStart();
389 dragMove();
390 }
391
updateDragging()392 void MapEditorToolBase::updateDragging()
393 {
394 Q_ASSERT(dragging);
395 dragMove();
396 }
397
finishDragging()398 void MapEditorToolBase::finishDragging()
399 {
400 Q_ASSERT(dragging);
401 dragMove();
402 dragging = false;
403 dragFinish();
404 }
405
cancelDragging()406 void MapEditorToolBase::cancelDragging()
407 {
408 Q_ASSERT(dragging);
409 dragging = false;
410 dragging_canceled = true;
411 dragCanceled();
412 }
413
dragStart()414 void MapEditorToolBase::dragStart()
415 {
416 // nothing
417 }
418
dragMove()419 void MapEditorToolBase::dragMove()
420 {
421 // nothing
422 }
423
dragFinish()424 void MapEditorToolBase::dragFinish()
425 {
426 // nothing
427 }
428
dragCanceled()429 void MapEditorToolBase::dragCanceled()
430 {
431 // nothing
432 }
433
keyPress(QKeyEvent * event)434 bool MapEditorToolBase::keyPress(QKeyEvent* event)
435 {
436 Q_UNUSED(event);
437 return false;
438 }
439
keyRelease(QKeyEvent * event)440 bool MapEditorToolBase::keyRelease(QKeyEvent* event)
441 {
442 Q_UNUSED(event);
443 return false;
444 }
445
updatePreviewObjectsSlot()446 void MapEditorToolBase::updatePreviewObjectsSlot()
447 {
448 preview_update_triggered = false;
449 if (editingInProgress())
450 updatePreviewObjects();
451 }
452
updateDirtyRectImpl(QRectF & rect)453 int MapEditorToolBase::updateDirtyRectImpl(QRectF& rect)
454 {
455 Q_UNUSED(rect);
456 return -1;
457 }
458
updatePreviewObjects()459 void MapEditorToolBase::updatePreviewObjects()
460 {
461 if (!editingInProgress())
462 {
463 qWarning("MapEditorToolBase::updatePreviewObjects() called but editing == false");
464 return;
465 }
466 for (auto object : editedObjects())
467 {
468 object->forceUpdate(); /// @todo get rid of force if possible;
469 // NOTE: only necessary because of setMap(nullptr) in startEditing(..)
470 renderables->insertRenderablesOfObject(object);
471 }
472 updateDirtyRect();
473 }
474
updatePreviewObjectsAsynchronously()475 void MapEditorToolBase::updatePreviewObjectsAsynchronously()
476 {
477 if (!editingInProgress())
478 {
479 qWarning("MapEditorToolBase::updatePreviewObjectsAsynchronously() called but editing == false");
480 return;
481 }
482
483 if (!preview_update_triggered)
484 {
485 QTimer::singleShot(10, this, &MapEditorToolBase::updatePreviewObjectsSlot);
486 preview_update_triggered = true;
487 }
488 }
489
drawSelectionOrPreviewObjects(QPainter * painter,MapWidget * widget,bool draw_opaque)490 void MapEditorToolBase::drawSelectionOrPreviewObjects(QPainter* painter, MapWidget* widget, bool draw_opaque)
491 {
492 map()->drawSelection(painter, true, widget, renderables->empty() ? nullptr : renderables.get(), draw_opaque);
493 }
494
495
startEditing()496 void MapEditorToolBase::startEditing()
497 {
498 Q_ASSERT(!editingInProgress());
499 setEditingInProgress(true);
500 }
501
502
startEditing(Object * object)503 void MapEditorToolBase::startEditing(Object* object)
504 {
505 Q_ASSERT(!editingInProgress());
506 setEditingInProgress(true);
507
508 Q_ASSERT(object);
509 if (Q_UNLIKELY(!object))
510 {
511 qWarning("MapEditorToolBase::startEditing(Object* object) called with object == nullptr");
512 return;
513 }
514
515 Q_ASSERT(edited_items.empty());
516 edited_items.reserve(1);
517 edited_items.emplace_back(object);
518
519 object->setMap(nullptr); // This is to keep the renderables out of the normal map.
520
521 // Cache old renderables until the object is inserted into the map again
522 old_renderables->insertRenderablesOfObject(object);
523 object->takeRenderables();
524 }
525
526
startEditing(const std::set<Object * > & objects)527 void MapEditorToolBase::startEditing(const std::set<Object*>& objects)
528 {
529 Q_ASSERT(!editingInProgress());
530 setEditingInProgress(true);
531
532 Q_ASSERT(edited_items.empty());
533 edited_items.reserve(objects.size());
534 for (auto object : objects)
535 {
536 edited_items.emplace_back(object);
537
538 object->setMap(nullptr); // This is to keep the renderables out of the normal map.
539
540 // Cache old renderables until the object is inserted into the map again
541 old_renderables->insertRenderablesOfObject(object);
542 object->takeRenderables();
543 }
544 }
545
abortEditing()546 void MapEditorToolBase::abortEditing()
547 {
548 Q_ASSERT(editingInProgress());
549
550 for (auto& edited_item : edited_items)
551 {
552 auto object = edited_item.active_object;
553 object->copyFrom(*edited_item.duplicate);
554 object->setMap(map());
555 object->update();
556 }
557 edited_items.clear();
558 renderables->clear();
559 old_renderables->clear(true);
560 MapEditorTool::setEditingInProgress(false);
561 }
562
563 // virtual
finishEditing()564 void MapEditorToolBase::finishEditing()
565 {
566 Q_ASSERT(editingInProgress());
567
568 if (!edited_items.empty())
569 {
570 auto undo_step = new ReplaceObjectsUndoStep(map());
571 for (auto& edited_item : edited_items)
572 {
573 auto object = edited_item.active_object;
574 object->setMap(map());
575 object->update();
576 undo_step->addObject(object, edited_item.duplicate.release());
577 }
578 edited_items.clear();
579 map()->push(undo_step);
580 }
581 renderables->clear();
582 old_renderables->clear(true);
583
584 MapEditorTool::finishEditing();
585 map()->setObjectsDirty();
586 map()->emitSelectionEdited();
587 }
588
589
editedObjectsModified() const590 bool MapEditorToolBase::editedObjectsModified() const
591 {
592 Q_ASSERT(editingInProgress());
593
594 return std::any_of(begin(edited_items), end(edited_items), [](const EditedItem& item) {
595 return item.isModified();
596 });
597 }
598
599
resetEditedObjects()600 void MapEditorToolBase::resetEditedObjects()
601 {
602 Q_ASSERT(editingInProgress());
603
604 for (auto& edited_item : edited_items)
605 {
606 auto object = edited_item.active_object;
607 object->copyFrom(*edited_item.duplicate);
608 object->setMap(nullptr); // This is to keep the renderables out of the normal map.
609 }
610 }
611
612
reapplyConstraintHelpers()613 void MapEditorToolBase::reapplyConstraintHelpers()
614 {
615 updateConstrainedPositions();
616 if (dragging)
617 dragMove();
618 else
619 mouseMove();
620 }
621
622
activateAngleHelperWhileEditing(bool enable)623 void MapEditorToolBase::activateAngleHelperWhileEditing(bool enable)
624 {
625 angle_helper->setActive(enable);
626 reapplyConstraintHelpers();
627 }
628
activateSnapHelperWhileEditing(bool enable)629 void MapEditorToolBase::activateSnapHelperWhileEditing(bool enable)
630 {
631 snap_helper->setFilter(enable ? SnappingToolHelper::AllTypes : SnappingToolHelper::NoSnapping);
632 reapplyConstraintHelpers();
633 }
634
635
updateConstrainedPositions()636 void MapEditorToolBase::updateConstrainedPositions()
637 {
638 if (snap_helper->getFilter() != SnappingToolHelper::NoSnapping)
639 {
640 SnappingToolHelperSnapInfo info;
641 constrained_pos_map = MapCoordF(snap_helper->snapToObject(cur_pos_map, cur_map_widget, &info, snap_exclude_object));
642 constrained_pos = cur_map_widget->mapToViewport(constrained_pos_map);
643 snapped_to_pos = info.type != SnappingToolHelper::NoSnapping;
644 }
645 else
646 {
647 constrained_pos_map = cur_pos_map;
648 constrained_pos = cur_pos;
649 snapped_to_pos = false;
650 }
651
652 if (angle_helper->isActive())
653 {
654 angle_helper->getConstrainedCursorPositions(constrained_pos_map, constrained_pos_map, constrained_pos, cur_map_widget);
655 }
656 }
657
658
659 #ifdef MAPPER_DEVELOPMENT_BUILD
660
generateNextSimulatedEvent()661 void MapEditorToolBase::generateNextSimulatedEvent()
662 {
663 auto next_pos = cur_pos + QPoint{qRound(10.0 - 20.0 * qrand() / RAND_MAX),
664 qRound(10.0 - 20.0 * qrand() / RAND_MAX)};
665 switch (simulation_state)
666 {
667 case 1:
668 {
669 qDebug("generateNextSimulatedEvent(): MouseButtonPress @ %d,%d", next_pos.x(), next_pos.y());
670 QMouseEvent press(QEvent::MouseButtonPress, next_pos, Qt::LeftButton, Qt::NoButton, active_modifiers);
671 qApp->sendEvent(mapWidget(), &press);
672 }
673 break;
674 case 2:
675 {
676 qDebug("generateNextSimulatedEvent(): MouseMove @ %d,%d", next_pos.x(), next_pos.y());
677 QMouseEvent move(QEvent::MouseMove, next_pos, Qt::NoButton, Qt::LeftButton, active_modifiers);
678 qApp->sendEvent(mapWidget(), &move);
679 }
680 break;
681 case 3:
682 {
683 qDebug("generateNextSimulatedEvent(): MouseButtonRelease @ %d,%d", next_pos.x(), next_pos.y());
684 QMouseEvent release(QEvent::MouseButtonRelease, next_pos, Qt::LeftButton, Qt::LeftButton, active_modifiers);
685 qApp->sendEvent(mapWidget(), &release);
686 }
687 Q_FALLTHROUGH();
688 default:
689 simulation_state = 0;
690 }
691 // Continue until Ctrl key is released and the current sequence was completed.
692 if (simulation_state != 0 || active_modifiers.testFlag(Qt::ControlModifier))
693 QTimer::singleShot(100, this, &MapEditorToolBase::generateNextSimulatedEvent);
694 ++simulation_state;
695 }
696
697 #endif // MAPPER_DEVELOPMENT_BUILD
698
699
700 } // namespace OpenOrienteering
701