/* * Copyright 2012-2014 Thomas Schöps * Copyright 2013-2017 Kai Pastor * * This file is part of OpenOrienteering. * * OpenOrienteering is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * OpenOrienteering is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OpenOrienteering. If not, see . */ #include "tool_base.h" #include #include // IWYU pragma: keep #include #include #include #include // IWYU pragma: keep #include #include #include #include #include #include "core/map.h" #include "core/objects/object.h" #include "core/renderables/renderable.h" #include "gui/map/map_editor.h" #include "gui/map/map_widget.h" #include "gui/widgets/key_button_bar.h" // IWYU pragma: keep #include "tools/tool_helpers.h" #include "undo/object_undo.h" #ifdef __clang_analyzer__ #define singleShot(A, B, C) singleShot(A, B, #C) // NOLINT #endif namespace OpenOrienteering { // ### MapEditorToolBase::EditedItem ### MapEditorToolBase::EditedItem::EditedItem(Object* active_object) : active_object { active_object } , duplicate { active_object ? active_object->duplicate() : nullptr } { // nothing else // These assertion must be within namespace MapEditorToolBase, // because EditedItem is a private member of this namespace. static_assert(std::is_nothrow_move_constructible::value, "MapEditorToolBase::EditedItem must be nothrow move constructible."); static_assert(std::is_nothrow_move_assignable::value, "MapEditorToolBase::EditedItem must be nothrow move assignable."); } MapEditorToolBase::EditedItem::EditedItem(const EditedItem& prototype) : MapEditorToolBase::EditedItem::EditedItem { prototype.active_object } { // nothing else } MapEditorToolBase::EditedItem::EditedItem(EditedItem&& prototype) noexcept : active_object { prototype.active_object } , duplicate { std::move(prototype.duplicate) } { // nothing else } MapEditorToolBase::EditedItem& MapEditorToolBase::EditedItem::operator=(const EditedItem& prototype) { if (&prototype == this) return *this; active_object = prototype.active_object; duplicate.reset(prototype.duplicate ? prototype.duplicate->duplicate() : nullptr); return *this; } MapEditorToolBase::EditedItem& MapEditorToolBase::EditedItem::operator=(EditedItem&& prototype) noexcept { if (&prototype == this) return *this; active_object = prototype.active_object; duplicate = std::move(prototype.duplicate); return *this; } bool MapEditorToolBase::EditedItem::isModified() const { return !duplicate->equals(active_object, false); } // ### MapEditorToolBase ### MapEditorToolBase::MapEditorToolBase(const QCursor& cursor, MapEditorTool::Type type, MapEditorController* editor, QAction* tool_action) : MapEditorTool(editor, type, tool_action), effective_start_drag_distance(startDragDistance()), angle_helper(new ConstrainAngleToolHelper()), snap_helper(new SnappingToolHelper(this)), cur_map_widget(editor->getMainWidget()), cursor(scaledToScreen(cursor)), renderables(new MapRenderables(map())), old_renderables(new MapRenderables(map())) { angle_helper->setActive(false); } MapEditorToolBase::~MapEditorToolBase() { old_renderables->clear(false); if (key_button_bar) editor->deletePopupWidget(key_button_bar); } void MapEditorToolBase::init() { connect(map(), &Map::objectSelectionChanged, this, &MapEditorToolBase::objectSelectionChanged); connect(map(), &Map::selectedObjectEdited, this, &MapEditorToolBase::updateDirtyRect); initImpl(); updateDirtyRect(); updateStatusText(); MapEditorTool::init(); } void MapEditorToolBase::initImpl() { // nothing } const QCursor& MapEditorToolBase::getCursor() const { return cursor; } void MapEditorToolBase::mousePositionEvent(QMouseEvent* event, const MapCoordF& map_coord, MapWidget* widget) { active_modifiers = event->modifiers(); cur_pos = event->pos(); cur_pos_map = map_coord; cur_map_widget = widget; updateConstrainedPositions(); if (event->button() == Qt::LeftButton && event->type() == QEvent::MouseButtonPress) { click_pos = cur_pos; click_pos_map = cur_pos_map; constrained_click_pos = constrained_pos; constrained_click_pos_map = constrained_pos_map; } else if (dragging_canceled) { click_pos = cur_pos; click_pos_map = cur_pos_map; constrained_click_pos = constrained_pos; constrained_click_pos_map = constrained_pos_map; dragging_canceled = false; } } bool MapEditorToolBase::mousePressEvent(QMouseEvent* event, const MapCoordF& map_coord, MapWidget* widget) { mousePositionEvent(event, map_coord, widget); if (event->button() == Qt::LeftButton) { clickPress(); return true; } else if (event->button() == Qt::RightButton) { // Do not show the ring menu when editing return editingInProgress(); } else { return false; } } bool MapEditorToolBase::mouseMoveEvent(QMouseEvent* event, const MapCoordF& map_coord, MapWidget* widget) { mousePositionEvent(event, map_coord, widget); if (event->buttons().testFlag(Qt::LeftButton)) { if (dragging) { updateDragging(); } else if ((cur_pos - click_pos).manhattanLength() >= effective_start_drag_distance) { startDragging(); } return true; } else { mouseMove(); return false; } } bool MapEditorToolBase::mouseReleaseEvent(QMouseEvent* event, const MapCoordF& map_coord, MapWidget* widget) { mousePositionEvent(event, map_coord, widget); if (event->button() == Qt::LeftButton) { if (dragging) { finishDragging(); } else { clickRelease(); } return true; } else if (event->button() == Qt::RightButton) { // Do not show the ring menu when editing return editingInProgress(); } else { return false; } } bool MapEditorToolBase::keyPressEvent(QKeyEvent* event) { active_modifiers = event->modifiers(); #if defined(Q_OS_MACOS) // FIXME: On Mac, QKeyEvent::modifiers() seems to return the keyboard // modifier flags that existed immediately before the event occurred. // This is in contradiction to the documentation for QKeyEvent::modifiers(), // but the documented behaviour of (parent) QInputEvent::modifiers(). // Qt5 doc says QKeyEvent::modifiers() "cannot always be trusted." ... switch (event->key()) { case Qt::Key_Shift: active_modifiers |= Qt::ShiftModifier; break; case Qt::Key_Control: active_modifiers |= Qt::ControlModifier; break; case Qt::Key_Alt: active_modifiers |= Qt::AltModifier; break; case Qt::Key_Meta: active_modifiers |= Qt::MetaModifier; break; default: ; // nothing } #endif return keyPress(event); } bool MapEditorToolBase::keyReleaseEvent(QKeyEvent* event) { active_modifiers = event->modifiers(); #if defined(Q_OS_MACOS) // FIXME: On Mac, QKeyEvent::modifiers() seems to return the keyboard // modifier flags that existed immediately before the event occurred. // This is in contradiction to the documentation for QKeyEvent::modifiers(), // but the documented behaviour of (parent) QInputEvent::modifiers(). // Qt5 doc says QKeyEvent::modifiers() "cannot always be trusted." ... switch (event->key()) { case Qt::Key_Shift: active_modifiers &= ~Qt::ShiftModifier; break; case Qt::Key_Control: active_modifiers &= ~Qt::ControlModifier; break; case Qt::Key_Alt: active_modifiers &= ~Qt::AltModifier; break; case Qt::Key_Meta: active_modifiers &= ~Qt::MetaModifier; break; default: ; // nothing } #endif return keyRelease(event); } void MapEditorToolBase::draw(QPainter* painter, MapWidget* widget) { drawImpl(painter, widget); if (angle_helper->isActive()) angle_helper->draw(painter, widget); if (snap_helper->getFilter() != SnappingToolHelper::NoSnapping) snap_helper->draw(painter, widget); } void MapEditorToolBase::updateDirtyRect() { int pixel_border = 0; QRectF rect; map()->includeSelectionRect(rect); if (angle_helper->isActive()) { angle_helper->includeDirtyRect(rect); pixel_border = qMax(pixel_border, angle_helper->getDisplayRadius()); } if (snap_helper->getFilter() != SnappingToolHelper::NoSnapping) { snap_helper->includeDirtyRect(rect); pixel_border = qMax(pixel_border, snap_helper->getDisplayRadius()); } pixel_border = qMax(pixel_border, updateDirtyRectImpl(rect)); if (pixel_border >= 0) map()->setDrawingBoundingBox(rect, pixel_border, true); else map()->clearDrawingBoundingBox(); } void MapEditorToolBase::objectSelectionChanged() { objectSelectionChangedImpl(); } void MapEditorToolBase::drawImpl(QPainter* painter, MapWidget* widget) { drawSelectionOrPreviewObjects(painter, widget); } void MapEditorToolBase::clickPress() { // nothing } void MapEditorToolBase::clickRelease() { // nothing } void MapEditorToolBase::mouseMove() { // nothing } void MapEditorToolBase::gestureStarted() { if (dragging) cancelDragging(); } void MapEditorToolBase::startDragging() { Q_ASSERT(!dragging); dragging = true; dragging_canceled = false; dragStart(); dragMove(); } void MapEditorToolBase::updateDragging() { Q_ASSERT(dragging); dragMove(); } void MapEditorToolBase::finishDragging() { Q_ASSERT(dragging); dragMove(); dragging = false; dragFinish(); } void MapEditorToolBase::cancelDragging() { Q_ASSERT(dragging); dragging = false; dragging_canceled = true; dragCanceled(); } void MapEditorToolBase::dragStart() { // nothing } void MapEditorToolBase::dragMove() { // nothing } void MapEditorToolBase::dragFinish() { // nothing } void MapEditorToolBase::dragCanceled() { // nothing } bool MapEditorToolBase::keyPress(QKeyEvent* event) { Q_UNUSED(event); return false; } bool MapEditorToolBase::keyRelease(QKeyEvent* event) { Q_UNUSED(event); return false; } void MapEditorToolBase::updatePreviewObjectsSlot() { preview_update_triggered = false; if (editingInProgress()) updatePreviewObjects(); } int MapEditorToolBase::updateDirtyRectImpl(QRectF& rect) { Q_UNUSED(rect); return -1; } void MapEditorToolBase::updatePreviewObjects() { if (!editingInProgress()) { qWarning("MapEditorToolBase::updatePreviewObjects() called but editing == false"); return; } for (auto object : editedObjects()) { object->forceUpdate(); /// @todo get rid of force if possible; // NOTE: only necessary because of setMap(nullptr) in startEditing(..) renderables->insertRenderablesOfObject(object); } updateDirtyRect(); } void MapEditorToolBase::updatePreviewObjectsAsynchronously() { if (!editingInProgress()) { qWarning("MapEditorToolBase::updatePreviewObjectsAsynchronously() called but editing == false"); return; } if (!preview_update_triggered) { QTimer::singleShot(10, this, &MapEditorToolBase::updatePreviewObjectsSlot); preview_update_triggered = true; } } void MapEditorToolBase::drawSelectionOrPreviewObjects(QPainter* painter, MapWidget* widget, bool draw_opaque) { map()->drawSelection(painter, true, widget, renderables->empty() ? nullptr : renderables.get(), draw_opaque); } void MapEditorToolBase::startEditing() { Q_ASSERT(!editingInProgress()); setEditingInProgress(true); } void MapEditorToolBase::startEditing(Object* object) { Q_ASSERT(!editingInProgress()); setEditingInProgress(true); Q_ASSERT(object); if (Q_UNLIKELY(!object)) { qWarning("MapEditorToolBase::startEditing(Object* object) called with object == nullptr"); return; } Q_ASSERT(edited_items.empty()); edited_items.reserve(1); edited_items.emplace_back(object); object->setMap(nullptr); // This is to keep the renderables out of the normal map. // Cache old renderables until the object is inserted into the map again old_renderables->insertRenderablesOfObject(object); object->takeRenderables(); } void MapEditorToolBase::startEditing(const std::set& objects) { Q_ASSERT(!editingInProgress()); setEditingInProgress(true); Q_ASSERT(edited_items.empty()); edited_items.reserve(objects.size()); for (auto object : objects) { edited_items.emplace_back(object); object->setMap(nullptr); // This is to keep the renderables out of the normal map. // Cache old renderables until the object is inserted into the map again old_renderables->insertRenderablesOfObject(object); object->takeRenderables(); } } void MapEditorToolBase::abortEditing() { Q_ASSERT(editingInProgress()); for (auto& edited_item : edited_items) { auto object = edited_item.active_object; object->copyFrom(*edited_item.duplicate); object->setMap(map()); object->update(); } edited_items.clear(); renderables->clear(); old_renderables->clear(true); MapEditorTool::setEditingInProgress(false); } // virtual void MapEditorToolBase::finishEditing() { Q_ASSERT(editingInProgress()); if (!edited_items.empty()) { auto undo_step = new ReplaceObjectsUndoStep(map()); for (auto& edited_item : edited_items) { auto object = edited_item.active_object; object->setMap(map()); object->update(); undo_step->addObject(object, edited_item.duplicate.release()); } edited_items.clear(); map()->push(undo_step); } renderables->clear(); old_renderables->clear(true); MapEditorTool::finishEditing(); map()->setObjectsDirty(); map()->emitSelectionEdited(); } bool MapEditorToolBase::editedObjectsModified() const { Q_ASSERT(editingInProgress()); return std::any_of(begin(edited_items), end(edited_items), [](const EditedItem& item) { return item.isModified(); }); } void MapEditorToolBase::resetEditedObjects() { Q_ASSERT(editingInProgress()); for (auto& edited_item : edited_items) { auto object = edited_item.active_object; object->copyFrom(*edited_item.duplicate); object->setMap(nullptr); // This is to keep the renderables out of the normal map. } } void MapEditorToolBase::reapplyConstraintHelpers() { updateConstrainedPositions(); if (dragging) dragMove(); else mouseMove(); } void MapEditorToolBase::activateAngleHelperWhileEditing(bool enable) { angle_helper->setActive(enable); reapplyConstraintHelpers(); } void MapEditorToolBase::activateSnapHelperWhileEditing(bool enable) { snap_helper->setFilter(enable ? SnappingToolHelper::AllTypes : SnappingToolHelper::NoSnapping); reapplyConstraintHelpers(); } void MapEditorToolBase::updateConstrainedPositions() { if (snap_helper->getFilter() != SnappingToolHelper::NoSnapping) { SnappingToolHelperSnapInfo info; constrained_pos_map = MapCoordF(snap_helper->snapToObject(cur_pos_map, cur_map_widget, &info, snap_exclude_object)); constrained_pos = cur_map_widget->mapToViewport(constrained_pos_map); snapped_to_pos = info.type != SnappingToolHelper::NoSnapping; } else { constrained_pos_map = cur_pos_map; constrained_pos = cur_pos; snapped_to_pos = false; } if (angle_helper->isActive()) { angle_helper->getConstrainedCursorPositions(constrained_pos_map, constrained_pos_map, constrained_pos, cur_map_widget); } } #ifdef MAPPER_DEVELOPMENT_BUILD void MapEditorToolBase::generateNextSimulatedEvent() { auto next_pos = cur_pos + QPoint{qRound(10.0 - 20.0 * qrand() / RAND_MAX), qRound(10.0 - 20.0 * qrand() / RAND_MAX)}; switch (simulation_state) { case 1: { qDebug("generateNextSimulatedEvent(): MouseButtonPress @ %d,%d", next_pos.x(), next_pos.y()); QMouseEvent press(QEvent::MouseButtonPress, next_pos, Qt::LeftButton, Qt::NoButton, active_modifiers); qApp->sendEvent(mapWidget(), &press); } break; case 2: { qDebug("generateNextSimulatedEvent(): MouseMove @ %d,%d", next_pos.x(), next_pos.y()); QMouseEvent move(QEvent::MouseMove, next_pos, Qt::NoButton, Qt::LeftButton, active_modifiers); qApp->sendEvent(mapWidget(), &move); } break; case 3: { qDebug("generateNextSimulatedEvent(): MouseButtonRelease @ %d,%d", next_pos.x(), next_pos.y()); QMouseEvent release(QEvent::MouseButtonRelease, next_pos, Qt::LeftButton, Qt::LeftButton, active_modifiers); qApp->sendEvent(mapWidget(), &release); } Q_FALLTHROUGH(); default: simulation_state = 0; } // Continue until Ctrl key is released and the current sequence was completed. if (simulation_state != 0 || active_modifiers.testFlag(Qt::ControlModifier)) QTimer::singleShot(100, this, &MapEditorToolBase::generateNextSimulatedEvent); ++simulation_state; } #endif // MAPPER_DEVELOPMENT_BUILD } // namespace OpenOrienteering