1 /*
2 * Copyright 2012, 2013 Thomas Schöps
3 * Copyright 2012-2019 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 "map_editor.h"
23 #include "map_editor_p.h"
24
25 #include <algorithm>
26 #include <cstddef>
27 #include <functional>
28 #include <iterator>
29 #include <limits>
30 #include <set>
31 #include <vector>
32 // IWYU pragma: no_include <type_traits>
33
34 #include <Qt>
35 #include <QtGlobal>
36 #include <QtMath>
37 #include <QAbstractButton>
38 #include <QAction>
39 #include <QActionGroup>
40 #include <QApplication>
41 #include <QBuffer>
42 #include <QByteArray>
43 #include <QComboBox>
44 #include <QDate>
45 #include <QDialog>
46 #include <QDir>
47 #include <QDockWidget>
48 #include <QEvent>
49 #include <QFileInfo>
50 #include <QFlags>
51 #include <QFont>
52 #include <QFontMetrics>
53 #include <QFrame>
54 #include <QHBoxLayout>
55 #include <QIcon>
56 #include <QImage> // IWYU pragma: keep
57 #include <QInputDialog>
58 #include <QIODevice>
59 #include <QKeyEvent> // IWYU pragma: keep
60 #include <QKeySequence>
61 #include <QLabel>
62 #include <QLatin1Char>
63 #include <QLatin1String>
64 #include <QLineEdit>
65 #include <QList>
66 #include <QLocale>
67 #include <QMenu>
68 #include <QMenuBar>
69 #include <QMessageBox>
70 // IWYU pragma: no_include <QMetaObject>
71 #include <QMimeData>
72 #include <QPainter>
73 #include <QPixmap>
74 #include <QPoint>
75 #include <QPushButton>
76 #include <QRect>
77 #include <QRectF>
78 #include <QSettings>
79 #include <QSignalBlocker>
80 #include <QSignalMapper>
81 #include <QSize>
82 #include <QSizeGrip> // IWYU pragma: keep
83 #include <QSizePolicy>
84 #include <QSplitter>
85 #include <QStatusBar>
86 #include <QStringList>
87 #include <QTextEdit>
88 #include <QToolBar>
89 #include <QToolButton>
90 #include <QVariant>
91 #include <QVBoxLayout>
92 #include <QWidget>
93
94 #ifdef Q_OS_ANDROID
95 #include <QtAndroidExtras/QAndroidJniObject>
96 #endif
97
98 #include "settings.h"
99 #include "core/georeferencing.h"
100 #include "core/map.h"
101 #include "core/map_coord.h"
102 #include "core/map_part.h"
103 #include "core/map_view.h"
104 #include "core/objects/boolean_tool.h"
105 #include "core/objects/object.h"
106 #include "core/objects/object_operations.h"
107 #include "core/symbols/area_symbol.h"
108 #include "core/symbols/symbol.h"
109 #include "core/symbols/symbol_icon_decorator.h"
110 #include "fileformats/file_format.h"
111 #include "fileformats/file_format_registry.h"
112 #include "fileformats/file_import_export.h"
113 #include "gui/configure_grid_dialog.h"
114 #include "gui/file_dialog.h"
115 #include "gui/georeferencing_dialog.h"
116 #include "gui/main_window.h"
117 #include "gui/print_widget.h"
118 #include "gui/text_browser_dialog.h"
119 #include "gui/util_gui.h"
120 #include "gui/map/map_dialog_rotate.h"
121 #include "gui/map/map_dialog_scale.h"
122 #include "gui/map/map_editor_activity.h"
123 #include "gui/map/map_find_feature.h"
124 #include "gui/map/map_widget.h"
125 #include "gui/symbols/symbol_replacement.h"
126 #include "gui/widgets/action_grid_bar.h"
127 #include "gui/widgets/color_list_widget.h"
128 #include "gui/widgets/compass_display.h"
129 #include "gui/widgets/key_button_bar.h" // IWYU pragma: keep
130 #include "gui/widgets/measure_widget.h"
131 #include "gui/widgets/symbol_widget.h"
132 #include "gui/widgets/tags_widget.h"
133 #include "gui/widgets/template_list_widget.h"
134 #include "sensors/compass.h"
135 #include "sensors/gps_display.h"
136 #include "sensors/gps_temporary_markers.h"
137 #include "sensors/gps_track_recorder.h"
138 #include "templates/template.h"
139 #include "templates/template_dialog_reopen.h"
140 #include "templates/template_position_dock_widget.h"
141 #include "templates/template_tool_paint.h"
142 #include "templates/template_track.h"
143 #include "tools/cut_tool.h"
144 #include "tools/cut_hole_tool.h"
145 #include "tools/cutout_tool.h"
146 #include "tools/distribute_points_tool.h"
147 #include "tools/draw_circle_tool.h"
148 #include "tools/draw_freehand_tool.h"
149 #include "tools/draw_path_tool.h"
150 #include "tools/draw_point_tool.h"
151 #include "tools/draw_point_gps_tool.h"
152 #include "tools/draw_rectangle_tool.h"
153 #include "tools/draw_text_tool.h"
154 #include "tools/edit_point_tool.h"
155 #include "tools/edit_line_tool.h"
156 #include "tools/fill_tool.h"
157 #include "tools/pan_tool.h"
158 #include "tools/rotate_pattern_tool.h"
159 #include "tools/rotate_tool.h"
160 #include "tools/scale_tool.h"
161 #include "tools/tool.h"
162 #include "undo/map_part_undo.h"
163 #include "undo/object_undo.h"
164 #include "undo/undo.h"
165 #include "undo/undo_manager.h"
166 #include "util/backports.h" // IWYU pragma: keep
167
168 #ifdef MAPPER_USE_GDAL
169 #include "gdal/ogr_template.h"
170 #endif
171
172
173 #ifdef __clang_analyzer__
174 #define singleShot(A, B, C) singleShot(A, B, #C) // NOLINT
175 #endif
176
177
178 namespace OpenOrienteering {
179
180 class PointSymbol;
181
182
183 namespace {
184
185 /**
186 * Creates a partial, resizable widget overlay over the main window.
187 *
188 * This widget is meant to be used as a dock widget replacement in the
189 * mobile app. In landscape mode, the child widget is placed on the right
190 * side, spanning the full height. In portrait mode, the child is placed
191 * on the top, spanning the full width.
192 */
createDockWidgetSubstitute(MainWindow * window,QWidget * child)193 QSplitter* createDockWidgetSubstitute(MainWindow* window, QWidget* child)
194 {
195 auto* splitter = new QSplitter(window);
196 splitter->setChildrenCollapsible(false);
197
198 auto* placeholder = new QWidget();
199
200 splitter->setAttribute(Qt::WA_NoSystemBackground, true);
201 placeholder->setAttribute(Qt::WA_NoSystemBackground, true);
202 child->setAutoFillBackground(true);
203
204 auto geometry = window->geometry();
205 splitter->setGeometry(geometry);
206 if (geometry.height() > geometry.width())
207 {
208 splitter->setOrientation(Qt::Vertical);
209 splitter->addWidget(child);
210 splitter->addWidget(placeholder);
211 }
212 else
213 {
214 splitter->setOrientation(Qt::Horizontal);
215 splitter->addWidget(placeholder);
216 splitter->addWidget(child);
217 }
218
219 return splitter;
220 }
221
222 template<class T>
containsPathObject(const T & container)223 bool containsPathObject(const T& container)
224 {
225 return std::any_of(begin(container), end(container), [](const auto* object) {
226 return object->getType() == Object::Path;
227 });
228 }
229
230
231 } // namespace
232
233
234
235 namespace MimeType {
236
237 /// The MIME type of Mapper data
OpenOrienteeringObjects()238 QString OpenOrienteeringObjects()
239 {
240 return QStringLiteral("openorienteering/objects");
241 }
242
243
244 } // namespace MimeType
245
246
247
248 // ### MapEditorController ###
249
MapEditorController(OperatingMode mode,Map * map,MapView * map_view)250 MapEditorController::MapEditorController(OperatingMode mode, Map* map, MapView* map_view)
251 : MainWindowController()
252 , mobile_mode(Settings::getInstance().touchModeEnabled())
253 , active_symbol(nullptr)
254 , template_list_widget(nullptr)
255 , mappart_remove_act(nullptr)
256 , mappart_merge_act(nullptr)
257 , mappart_merge_menu(nullptr)
258 , mappart_move_menu(nullptr)
259 , mappart_selector_box(nullptr)
260 , mappart_merge_mapper(new QSignalMapper(this))
261 , mappart_move_mapper(new QSignalMapper(this))
262 {
263 this->mode = mode;
264 this->map = nullptr;
265 main_view = nullptr;
266 symbol_widget = nullptr;
267 window = nullptr;
268 editing_in_progress = false;
269
270 cut_hole_menu = nullptr;
271
272 if (map)
273 setMapAndView(map, map_view ? map_view : new MapView(this, map));
274
275 editor_activity = nullptr;
276 current_tool = nullptr;
277 override_tool = nullptr;
278 last_painted_on_template = nullptr;
279
280 paste_act = nullptr;
281 reopen_template_act = nullptr;
282 overprinting_simulation_act = nullptr;
283
284 toolbar_view = nullptr;
285 toolbar_mapparts = nullptr;
286 toolbar_drawing = nullptr;
287 toolbar_editing = nullptr;
288 toolbar_advanced_editing = nullptr;
289 print_dock_widget = nullptr;
290 measure_dock_widget = nullptr;
291 symbol_dock_widget = nullptr;
292
293 statusbar_zoom_frame = nullptr;
294 statusbar_cursorpos_label = nullptr;
295
296 gps_display = nullptr;
297 gps_track_recorder = nullptr;
298 compass_display = nullptr;
299 gps_marker_display = nullptr;
300
301 actionsById[""] = new QAction(this); // dummy action
302
303 connect(mappart_merge_mapper, QOverload<int>::of(&QSignalMapper::mapped), this, &MapEditorController::mergeCurrentMapPartTo);
304 connect(mappart_move_mapper, QOverload<int>::of(&QSignalMapper::mapped), this, &MapEditorController::reassignObjectsToMapPart);
305 }
306
~MapEditorController()307 MapEditorController::~MapEditorController()
308 {
309 paste_act = nullptr;
310 delete current_tool;
311 delete override_tool;
312 delete editor_activity;
313 delete toolbar_view;
314 delete toolbar_drawing;
315 delete toolbar_editing;
316 delete toolbar_advanced_editing;
317 delete toolbar_mapparts;
318 delete print_dock_widget;
319 delete measure_dock_widget;
320 if (color_dock_widget)
321 delete color_dock_widget;
322 delete symbol_dock_widget;
323 if (template_dock_widget)
324 delete template_dock_widget;
325 if (tags_dock_widget)
326 delete tags_dock_widget;
327 delete cut_hole_menu;
328 delete mappart_merge_act;
329 delete mappart_merge_menu;
330 delete mappart_move_menu;
331 if (mappart_selector_box)
332 delete mappart_selector_box;
333 for (TemplatePositionDockWidget* widget : qAsConst(template_position_widgets))
334 delete widget;
335 delete gps_display;
336 delete gps_track_recorder;
337 delete compass_display;
338 delete map;
339 }
340
menuBarVisible()341 bool MapEditorController::menuBarVisible()
342 {
343 if (mode == SymbolEditor)
344 return false;
345
346 return MainWindowController::menuBarVisible();
347 }
348
isInMobileMode() const349 bool MapEditorController::isInMobileMode() const
350 {
351 return mobile_mode;
352 }
353
setTool(MapEditorTool * new_tool)354 void MapEditorController::setTool(MapEditorTool* new_tool)
355 {
356 if (current_tool)
357 {
358 if (current_tool->editingInProgress())
359 {
360 current_tool->finishEditing();
361 }
362 current_tool->deleteLater();
363 }
364
365 if (!override_tool)
366 {
367 map->clearDrawingBoundingBox();
368 window->setStatusBarText(QString{});
369 }
370
371 current_tool = new_tool;
372 if (current_tool && !override_tool)
373 {
374 current_tool->init();
375 }
376
377 if (!override_tool)
378 map_widget->setTool(current_tool);
379 }
380
setEditTool()381 void MapEditorController::setEditTool()
382 {
383 if (!current_tool || current_tool->toolType() != MapEditorTool::EditPoint)
384 setTool(new EditPointTool(this, edit_tool_act));
385 }
386
setOverrideTool(MapEditorTool * new_override_tool)387 void MapEditorController::setOverrideTool(MapEditorTool* new_override_tool)
388 {
389 if (override_tool == new_override_tool)
390 return;
391
392 if (override_tool)
393 {
394 if (override_tool->editingInProgress())
395 {
396 override_tool->finishEditing();
397 }
398 delete override_tool;
399 }
400
401 map->clearDrawingBoundingBox();
402 window->setStatusBarText(QString{});
403
404 override_tool = new_override_tool;
405 if (override_tool)
406 {
407 override_tool->init();
408 }
409 else if (current_tool)
410 {
411 current_tool->init();
412 }
413
414 map_widget->setTool(override_tool ? override_tool : current_tool);
415 }
416
getDefaultDrawToolForSymbol(const Symbol * symbol)417 MapEditorTool* MapEditorController::getDefaultDrawToolForSymbol(const Symbol* symbol)
418 {
419 if (!symbol)
420 return new EditPointTool(this, edit_tool_act);
421 else if (symbol->getType() == Symbol::Point)
422 return new DrawPointTool(this, draw_point_act);
423 else if (symbol->getType() == Symbol::Line || symbol->getType() == Symbol::Area || symbol->getType() == Symbol::Combined)
424 return new DrawPathTool(this, draw_path_act, false, true);
425 else if (symbol->getType() == Symbol::Text)
426 return new DrawTextTool(this, draw_text_act);
427 else
428 Q_ASSERT(false);
429 return nullptr;
430 }
431
432
setEditingInProgress(bool value)433 void MapEditorController::setEditingInProgress(bool value)
434 {
435 if (value != editing_in_progress)
436 {
437 editing_in_progress = value;
438
439 // Widgets
440 map_widget->setGesturesEnabled(!editing_in_progress);
441 Q_ASSERT(symbol_widget);
442 symbol_widget->setEnabled(!editing_in_progress);
443 if (isInMobileMode())
444 mobile_symbol_selector_action->setEnabled(!editing_in_progress);
445 if (color_dock_widget)
446 color_dock_widget->widget()->setEnabled(!editing_in_progress);
447 if (mappart_selector_box)
448 mappart_selector_box->setEnabled(!editing_in_progress);
449
450 // Edit menu
451 undo_act->setEnabled(!editing_in_progress && map->undoManager().canUndo());
452 redo_act->setEnabled(!editing_in_progress && map->undoManager().canRedo());
453 updatePasteAvailability();
454 select_all_act->setEnabled(!editing_in_progress);
455 select_nothing_act->setEnabled(!editing_in_progress);
456 invert_selection_act->setEnabled(!editing_in_progress);
457 select_by_current_symbol_act->setEnabled(!editing_in_progress);
458 find_feature->setEnabled(!editing_in_progress);
459
460 // Map menu
461 georeferencing_act->setEnabled(!editing_in_progress);
462 scale_map_act->setEnabled(!editing_in_progress);
463 rotate_map_act->setEnabled(!editing_in_progress);
464 map_notes_act->setEnabled(!editing_in_progress);
465
466 // Map menu, continued
467 const int num_parts = map->getNumParts();
468 mappart_add_act->setEnabled(!editing_in_progress);
469 mappart_rename_act->setEnabled(!editing_in_progress && num_parts > 0);
470 mappart_remove_act->setEnabled(!editing_in_progress && num_parts > 1);
471 mappart_move_menu->setEnabled(!editing_in_progress && num_parts > 1);
472 mappart_merge_act->setEnabled(!editing_in_progress && num_parts > 1);
473 mappart_merge_menu->setEnabled(!editing_in_progress && num_parts > 1);
474
475 // Symbol menu
476 symbol_set_id_act->setEnabled(!editing_in_progress);
477 scale_all_symbols_act->setEnabled(!editing_in_progress);
478 load_symbols_from_act->setEnabled(!editing_in_progress);
479 load_crt_act->setEnabled(!editing_in_progress);
480
481 updateObjectDependentActions();
482 updateSymbolDependentActions();
483 updateSymbolAndObjectDependentActions();
484 }
485 }
486
isEditingInProgress() const487 bool MapEditorController::isEditingInProgress() const
488 {
489 return editing_in_progress;
490 }
491
492
setEditorActivity(MapEditorActivity * new_activity)493 void MapEditorController::setEditorActivity(MapEditorActivity* new_activity)
494 {
495 delete editor_activity;
496 map->clearActivityBoundingBox();
497
498 editor_activity = new_activity;
499 if (editor_activity)
500 editor_activity->init();
501
502 map_widget->setActivity(editor_activity);
503 }
504
addTemplatePositionDockWidget(Template * temp)505 void MapEditorController::addTemplatePositionDockWidget(Template* temp)
506 {
507 Q_ASSERT(!existsTemplatePositionDockWidget(temp));
508 auto* dock_widget = new TemplatePositionDockWidget(temp, this, window);
509 addFloatingDockWidget(dock_widget);
510 template_position_widgets.insert(temp, dock_widget);
511 }
512
removeTemplatePositionDockWidget(Template * temp)513 void MapEditorController::removeTemplatePositionDockWidget(Template* temp)
514 {
515 emit templatePositionDockWidgetClosed(temp);
516
517 if (auto* w = getTemplatePositionDockWidget(temp))
518 w->deleteLater();
519 int num_deleted = template_position_widgets.remove(temp);
520 Q_ASSERT(num_deleted == 1);
521 Q_UNUSED(num_deleted);
522 }
523
showPopupWidget(QWidget * child_widget,const QString & title)524 void MapEditorController::showPopupWidget(QWidget* child_widget, const QString& title)
525 {
526 if (mobile_mode)
527 {
528 // FIXME: This is used for KeyButtonBar only
529 // and not related to mobile_mode!
530 QSize size = child_widget->sizeHint();
531 QRect map_widget_rect = map_widget->rect();
532
533 child_widget->setParent(map_widget);
534 child_widget->setGeometry(
535 qMax(0, qRound(map_widget_rect.center().x() - 0.5f * size.width())),
536 qMax(0, map_widget_rect.bottom() - size.height()),
537 qMin(size.width(), map_widget_rect.width()),
538 qMin(size.height(), map_widget_rect.height())
539 );
540 child_widget->show();
541 }
542 else
543 {
544 auto* dock_widget = new QDockWidget(title, window);
545 dock_widget->setFeatures(dock_widget->features() & ~QDockWidget::DockWidgetClosable);
546 dock_widget->setWidget(child_widget);
547
548 // Show dock in floating state
549 dock_widget->setFloating(true);
550 dock_widget->show();
551 dock_widget->setGeometry(window->geometry().left() + 40, window->geometry().top() + 100, dock_widget->width(), dock_widget->height());
552 }
553 }
554
deletePopupWidget(QWidget * child_widget)555 void MapEditorController::deletePopupWidget(QWidget* child_widget)
556 {
557 if (mobile_mode)
558 {
559 delete child_widget;
560 }
561 else
562 {
563 // Delete the dock widget
564 delete child_widget->parentWidget();
565 }
566 }
567
568
saveTo(const QString & path,const FileFormat & format)569 bool MapEditorController::saveTo(const QString& path, const FileFormat& format)
570 {
571 if (editing_in_progress)
572 {
573 QMessageBox::warning(window,
574 tr("Editing in progress"),
575 tr("The map is currently being edited. "
576 "Please finish the edit operation before saving.") );
577 return false;
578 }
579
580 if (!exportTo(path, format))
581 return false;
582
583 map->setHasUnsavedChanges(false);
584 map->undoManager().setClean();
585 window->showStatusBarMessage(tr("Map saved"), 1000);
586 return true;
587 }
588
589
exportTo(const QString & path,const FileFormat & format)590 bool MapEditorController::exportTo(const QString& path, const FileFormat& format)
591 {
592 if (!map || editing_in_progress)
593 return false;
594
595 auto exporter = format.makeExporter(path, map, main_view);
596 if (!exporter)
597 {
598 auto message =
599 tr("Cannot export the map as\n"
600 "\"%1\"\n"
601 "because saving as %2 (.%3) is not supported.").
602 arg(path,
603 format.description(),
604 format.fileExtensions().join(QLatin1String(", ")) );
605 QMessageBox::warning(nullptr, tr("Error"), message);
606 return false;
607 }
608
609 if (!exporter->doExport())
610 {
611 auto message = tr("Cannot save file\n%1:\n%2")
612 .arg(path, exporter->warnings().back());
613 QMessageBox::warning(nullptr, tr("Error"), message);
614 return false;
615 }
616
617 if (!exporter->warnings().empty())
618 {
619 MainWindow::showMessageBox(nullptr,
620 tr("Warning"),
621 tr("The map export generated warnings."),
622 exporter->warnings() );
623 }
624
625 return true;
626 }
627
628
loadFrom(const QString & path,const FileFormat & format,QWidget * dialog_parent)629 bool MapEditorController::loadFrom(const QString& path, const FileFormat& format, QWidget* dialog_parent)
630 {
631 if (!dialog_parent)
632 dialog_parent = window;
633
634 if (!map)
635 {
636 map = new Map();
637 main_view = new MapView(this, map);
638 }
639 #ifdef __clang_analyzer__
640 // clang-analyzer-core.CallAndMessage seems to be unable to realize
641 // that map != null from the previous block.
642 if (!map) { return false; }
643 #endif
644
645 auto importer = format.makeImporter(path, map, main_view);
646 if (!importer)
647 {
648 QMessageBox::warning(dialog_parent, tr("Error"),
649 MainWindow::tr("Cannot open file:\n%1\n\n%2")
650 .arg(path, MainWindow::tr("Invalid file type.")));
651 return false;
652 }
653
654 if (!importer->doImport())
655 {
656 delete map;
657 map = nullptr;
658 main_view = nullptr;
659
660 Q_ASSERT(!importer->warnings().empty());
661 QMessageBox::warning(dialog_parent, tr("Error"), importer->warnings().back());
662 return false;
663 }
664
665 setMapAndView(map, main_view);
666 map->setHasUnsavedChanges(false);
667 if (!importer->warnings().empty())
668 MainWindow::showMessageBox(dialog_parent, tr("Warning"), tr("The map import generated warnings."), importer->warnings());
669 return true;
670 }
671
672
attach(MainWindow * window)673 void MapEditorController::attach(MainWindow* window)
674 {
675 print_dock_widget = nullptr;
676 measure_dock_widget = nullptr;
677 color_dock_widget = nullptr;
678 symbol_dock_widget = nullptr;
679 std::function<void(const QString&)> zoom_display_function;
680
681 this->window = window;
682 if (mode == MapEditor)
683 {
684 window->setHasUnsavedChanges(map->hasUnsavedChanges());
685 }
686 connect(map, &Map::hasUnsavedChanged, window, &MainWindow::setHasUnsavedChanges);
687
688 #ifdef Q_OS_ANDROID
689 QAndroidJniObject::callStaticMethod<void>("org/openorienteering/mapper/MapperActivity",
690 "lockOrientation",
691 "()V");
692 #endif
693 if (mobile_mode)
694 {
695 window->setWindowState(window->windowState() | Qt::WindowFullScreen);
696 zoom_display_function = [window](const QString& text) {
697 window->showStatusBarMessage(text, 1000);
698 };
699 }
700 else
701 {
702 // Add zoom / cursor position field to status bar
703 auto* statusbar_zoom_icon = new QLabel();
704 auto fontmetrics = statusbar_zoom_icon->fontMetrics();
705 auto pixmap = QPixmap(QLatin1String(":/images/magnifying-glass.png"));
706 auto scale = qreal(fontmetrics.height()) / pixmap.height();
707 if (scale < 0.9 || scale > 1.1)
708 pixmap = pixmap.scaledToHeight(qRound(scale * pixmap.height()), Qt::SmoothTransformation);
709 statusbar_zoom_icon->setPixmap(pixmap);
710
711 auto* statusbar_zoom_label = new QLabel();
712 statusbar_zoom_label->setMinimumWidth(fontmetrics.width(QLatin1String(" 0.333x")));
713 statusbar_zoom_label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
714 statusbar_zoom_label->setFrameShape(QFrame::NoFrame);
715
716 zoom_display_function = [statusbar_zoom_label](const QString& text) {
717 statusbar_zoom_label->setText(text);
718 };
719
720 statusbar_zoom_frame = new QFrame();
721 #ifdef Q_OS_WIN
722 statusbar_zoom_frame->setFrameShape(QFrame::NoFrame);
723 #else
724 statusbar_zoom_frame->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
725 #endif
726 auto* statusbar_zoom_frame_layout = new QHBoxLayout();
727 statusbar_zoom_frame_layout->setMargin(0);
728 statusbar_zoom_frame_layout->setSpacing(0);
729 statusbar_zoom_frame_layout->addSpacing(1);
730 statusbar_zoom_frame_layout->addWidget(statusbar_zoom_icon);
731 statusbar_zoom_frame_layout->addWidget(statusbar_zoom_label);
732 statusbar_zoom_frame->setLayout(statusbar_zoom_frame_layout);
733
734 statusbar_cursorpos_label = new QLabel();
735 #ifdef Q_OS_WIN
736 statusbar_cursorpos_label->setFrameShape(QFrame::NoFrame);
737 #else
738 statusbar_cursorpos_label->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
739 #endif
740 statusbar_cursorpos_label->setMinimumWidth(fontmetrics.width(QLatin1String("-3,333.33 -333.33 (mm)")));
741 statusbar_cursorpos_label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
742
743 window->statusBar()->addPermanentWidget(statusbar_zoom_frame);
744 window->statusBar()->addPermanentWidget(statusbar_cursorpos_label);
745 }
746
747 // Create map widget
748 map_widget = new MapWidget(mode == MapEditor, mode == SymbolEditor);
749 map_widget->setMapView(main_view);
750 map_widget->setZoomDisplay(zoom_display_function);
751
752 // Create menu and toolbar together, so actions can be inserted into one or both
753 if (mode == MapEditor)
754 {
755 gps_display = new GPSDisplay(map_widget, map->getGeoreferencing());
756 gps_marker_display = new GPSTemporaryMarkers(map_widget, gps_display);
757
758 createActions();
759 if (mobile_mode)
760 {
761 createMobileGUI();
762 compass_display = new CompassDisplay(map_widget->parentWidget());
763 compass_display->setVisible(false);
764 }
765 else
766 {
767 map_widget->setCursorposLabel(statusbar_cursorpos_label);
768 window->setCentralWidget(map_widget);
769
770 createMenuAndToolbars();
771 restoreWindowState();
772 }
773 }
774 else if (mode == SymbolEditor)
775 {
776 map_widget->setCursorposLabel(statusbar_cursorpos_label);
777 window->setCentralWidget(map_widget);
778 }
779
780 // Update enabled/disabled state for the tools ...
781 updateWidgets();
782 templateAvailabilityChanged();
783 // ... and make sure it is kept up to date for copy/paste
784 connect(QApplication::clipboard(), &QClipboard::changed, this, &MapEditorController::clipboardChanged);
785 clipboardChanged(QClipboard::Clipboard);
786
787 if (mode == MapEditor)
788 {
789 // Create/show the dock widgets
790 if (mobile_mode)
791 {
792 createSymbolWidget(window);
793 }
794 else
795 {
796 symbol_window_act->trigger();
797 createColorWindow();
798 createTemplateWindow();
799 createTagEditor();
800
801 if (map->getNumColors() == 0)
802 QTimer::singleShot(0, color_dock_widget, &QWidget::show);
803 }
804
805 // Auto-select the edit tool
806 edit_tool_act->setChecked(true);
807 setEditTool();
808
809 // Set the coordinates display mode
810 coordsDisplayChanged();
811 }
812 else
813 {
814 // Set the coordinates display mode
815 map_widget->setCoordsDisplay(MapWidget::MAP_COORDS);
816 }
817 }
818
newAction(const char * id,const QString & tr_text,QObject * receiver,const char * slot,const char * icon,const QString & tr_tip,const char * whats_this_link)819 QAction* MapEditorController::newAction(const char* id, const QString &tr_text, QObject* receiver, const char* slot, const char* icon, const QString& tr_tip, const char* whats_this_link)
820 {
821 Q_ASSERT(window); // Qt documentation recommends that actions are created as children of the window they are used in.
822 QAction* action = new QAction(icon ? QIcon(QLatin1String(":/images/") + QLatin1String(icon)) : QIcon(), tr_text, window);
823 if (!tr_tip.isEmpty()) action->setStatusTip(tr_tip);
824 if (whats_this_link) action->setWhatsThis(Util::makeWhatThis(whats_this_link));
825 if (receiver) QObject::connect(action, SIGNAL(triggered()), receiver, slot);
826 actionsById[id] = action;
827 action->setMenuRole(QAction::NoRole);
828 return action;
829 }
830
newCheckAction(const char * id,const QString & tr_text,QObject * receiver,const char * slot,const char * icon,const QString & tr_tip,const char * whats_this_link)831 QAction* MapEditorController::newCheckAction(const char* id, const QString &tr_text, QObject* receiver, const char* slot, const char* icon, const QString& tr_tip, const char* whats_this_link)
832 {
833 auto* action = newAction(id, tr_text, nullptr, nullptr, icon, tr_tip, whats_this_link);
834 action->setCheckable(true);
835 if (receiver) QObject::connect(action, SIGNAL(triggered(bool)), receiver, slot);
836 return action;
837 }
838
newToolAction(const char * id,const QString & tr_text,QObject * receiver,const char * slot,const char * icon,const QString & tr_tip,const char * whats_this_link)839 QAction* MapEditorController::newToolAction(const char* id, const QString &tr_text, QObject* receiver, const char* slot, const char* icon, const QString& tr_tip, const char* whats_this_link)
840 {
841 Q_ASSERT(window); // Qt documentation recommends that actions are created as children of the window they are used in.
842 QAction* action = new MapEditorToolAction(icon ? QIcon(QLatin1String(":/images/") + QLatin1String(icon)) : QIcon(), tr_text, window);
843 if (!tr_tip.isEmpty()) action->setStatusTip(tr_tip);
844 if (whats_this_link) action->setWhatsThis(Util::makeWhatThis(whats_this_link));
845 if (receiver) QObject::connect(action, SIGNAL(activated()), receiver, slot);
846 actionsById[id] = action;
847 return action;
848 }
849
findAction(const char * id)850 QAction* MapEditorController::findAction(const char* id)
851 {
852 if (!actionsById.contains(id)) return actionsById[""];
853 else return actionsById[id];
854 }
855
getAction(const char * id)856 QAction* MapEditorController::getAction(const char* id)
857 {
858 return actionsById.contains(id) ? actionsById[id] : nullptr;
859 }
860
assignKeyboardShortcuts()861 void MapEditorController::assignKeyboardShortcuts()
862 {
863 // Standard keyboard shortcuts
864 findAction("print")->setShortcut(QKeySequence::Print);
865 findAction("undo")->setShortcut(QKeySequence::Undo);
866 findAction("redo")->setShortcut(QKeySequence::Redo);
867 findAction("cut")->setShortcut(QKeySequence::Cut);
868 findAction("copy")->setShortcut(QKeySequence::Copy);
869 findAction("paste")->setShortcut(QKeySequence::Paste);
870
871 // Custom keyboard shortcuts
872
873 // QKeySequence::Deselect is empty for Windows, so be explicit all select-*
874 findAction("select-all")->setShortcut(QKeySequence(tr("Ctrl+A")));
875 findAction("select-nothing")->setShortcut(QKeySequence(tr("Ctrl+Shift+A")));
876 findAction("invert-selection")->setShortcut(QKeySequence(tr("Ctrl+I")));
877
878 findAction("showgrid")->setShortcut(QKeySequence(tr("G")));
879 findAction("zoomin")->setShortcuts(QList<QKeySequence>() << QKeySequence(Qt::Key_F7) << QKeySequence(Qt::Key_Plus) << QKeySequence(Qt::KeypadModifier + Qt::Key_Plus));
880 findAction("zoomout")->setShortcuts(QList<QKeySequence>() << QKeySequence(Qt::Key_F8) << QKeySequence(Qt::Key_Minus) << QKeySequence(Qt::KeypadModifier + Qt::Key_Minus));
881 findAction("hatchareasview")->setShortcut(QKeySequence(Qt::Key_F2));
882 findAction("baselineview")->setShortcut(QKeySequence(Qt::Key_F3));
883 findAction("hidealltemplates")->setShortcut(QKeySequence(Qt::Key_F10));
884 findAction("overprintsimulation")->setShortcut(QKeySequence(Qt::Key_F4));
885 findAction("fullscreen")->setShortcut(QKeySequence(Qt::Key_F11));
886 tags_window_act->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_6));
887 color_window_act->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_7));
888 symbol_window_act->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_8));
889 template_window_act->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_9));
890
891 findAction("editobjects")->setShortcut(QKeySequence(tr("E")));
892 findAction("editlines")->setShortcut(QKeySequence(tr("L")));
893 findAction("drawpoint")->setShortcut(QKeySequence(tr("S")));
894 findAction("drawpath")->setShortcut(QKeySequence(tr("P")));
895 findAction("drawcircle")->setShortcut(QKeySequence(tr("O")));
896 findAction("drawrectangle")->setShortcut(QKeySequence(tr("Ctrl+R")));
897 findAction("drawfill")->setShortcut(QKeySequence(tr("F")));
898 findAction("drawtext")->setShortcut(QKeySequence(tr("T")));
899
900 findAction("duplicate")->setShortcut(QKeySequence(tr("D")));
901 findAction("switchsymbol")->setShortcut(QKeySequence(tr("Ctrl+G")));
902 findAction("fillborder")->setShortcut(QKeySequence(tr("Ctrl+F")));
903 findAction("switchdashes")->setShortcut(QKeySequence(tr("Ctrl+D")));
904 findAction("connectpaths")->setShortcut(QKeySequence(tr("C")));
905 findAction("rotateobjects")->setShortcut(QKeySequence(tr("R")));
906 findAction("scaleobjects")->setShortcut(QKeySequence(tr("Z")));
907 findAction("cutobject")->setShortcut(QKeySequence(tr("K")));
908 findAction("cuthole")->setShortcut(QKeySequence(tr("H")));
909 findAction("measure")->setShortcut(QKeySequence(tr("M")));
910 findAction("booleanunion")->setShortcut(QKeySequence(tr("U")));
911 findAction("converttocurves")->setShortcut(QKeySequence(tr("N")));
912 findAction("simplify")->setShortcut(QKeySequence(tr("Ctrl+M")));
913 }
914
createActions()915 void MapEditorController::createActions()
916 {
917 // Define all the actions, saving them into variables as necessary. Can also get them by ID.
918 #ifdef QT_PRINTSUPPORT_LIB
919 auto* print_act_mapper = new QSignalMapper(this);
920 connect(print_act_mapper, QOverload<int>::of(&QSignalMapper::mapped), this, QOverload<int>::of(&MapEditorController::printClicked));
921 print_act = newAction("print", tr("Print..."), print_act_mapper, SLOT(map()), "print.png", QString{}, "file_menu.html");
922 print_act_mapper->setMapping(print_act, PrintWidget::PRINT_TASK);
923 export_image_act = newAction("export-image", tr("&Image"), print_act_mapper, SLOT(map()), nullptr, QString{}, "file_menu.html");
924 print_act_mapper->setMapping(export_image_act, PrintWidget::EXPORT_IMAGE_TASK);
925 export_pdf_act = newAction("export-pdf", tr("&PDF"), print_act_mapper, SLOT(map()), nullptr, QString{}, "file_menu.html");
926 print_act_mapper->setMapping(export_pdf_act, PrintWidget::EXPORT_PDF_TASK);
927 if (auto* vector_format = FileFormats.findFormat("OGR-export"))
928 export_vector_act = newAction("export-vector", vector_format->description(), this, SLOT(exportVector()), nullptr, {}, "edit_menu.html");
929 else
930 export_vector_act = nullptr;
931
932 #else
933 print_act = nullptr;
934 export_image_act = nullptr;
935 export_pdf_act = nullptr;
936 #endif
937
938 undo_act = newAction("undo", tr("Undo"), this, SLOT(undo()), "undo.png", tr("Undo the last step"), "edit_menu.html");
939 redo_act = newAction("redo", tr("Redo"), this, SLOT(redo()), "redo.png", tr("Redo the last step"), "edit_menu.html");
940 cut_act = newAction("cut", tr("Cu&t"), this, SLOT(cut()), "cut.png", QString{}, "edit_menu.html");
941 cut_act->setMenuRole(QAction::TextHeuristicRole);
942 copy_act = newAction("copy", tr("C&opy"), this, SLOT(copy()), "copy.png", QString{}, "edit_menu.html");
943 copy_act->setMenuRole(QAction::TextHeuristicRole);
944 paste_act = newAction("paste", tr("&Paste"), this, SLOT(paste()), "paste", QString{}, "edit_menu.html");
945 paste_act->setMenuRole(QAction::TextHeuristicRole);
946 delete_act = newAction("delete", tr("Delete"), this, SLOT(deleteClicked()), "delete.png", QString{}, "toolbars.html#delete");
947 select_all_act = newAction("select-all", tr("Select all"), this, SLOT(selectAll()), nullptr, QString{}, "edit_menu.html");
948 select_nothing_act = newAction("select-nothing", tr("Select nothing"), this, SLOT(selectNothing()), nullptr, QString{}, "edit_menu.html");
949 invert_selection_act = newAction("invert-selection", tr("Invert selection"), this, SLOT(invertSelection()), nullptr, QString{}, "edit_menu.html");
950 select_by_current_symbol_act = newAction("select-by-symbol", QApplication::translate("OpenOrienteering::SymbolRenderWidget", "Select all objects with selected symbols"), this, SLOT(selectByCurrentSymbols()), nullptr, QString{}, "edit_menu.html");
951 find_feature.reset(new MapFindFeature(*this));
952
953 clear_undo_redo_history_act = newAction("clearundoredohistory", tr("Clear undo / redo history"), this, SLOT(clearUndoRedoHistory()), nullptr, tr("Clear the undo / redo history to reduce map file size."), "edit_menu.html");
954
955 show_grid_act = newCheckAction("showgrid", tr("Show grid"), this, SLOT(showGrid()), "grid.png", QString{}, "grid.html");
956 configure_grid_act = newAction("configuregrid", tr("Configure grid..."), this, SLOT(configureGrid()), "grid.png", QString{}, "grid.html");
957 pan_act = newToolAction("panmap", tr("Pan"), this, SLOT(pan()), "move.png", QString{}, "view_menu.html");
958 move_to_gps_pos_act = newAction("movegps", tr("Move to my location"), this, SLOT(moveToGpsPos()), "move-to-gps.png", QString{}, "view_menu.html");
959 move_to_gps_pos_act->setEnabled(false);
960 zoom_in_act = newAction("zoomin", tr("Zoom in"), this, SLOT(zoomIn()), "view-zoom-in.png", QString{}, "view_menu.html");
961 zoom_out_act = newAction("zoomout", tr("Zoom out"), this, SLOT(zoomOut()), "view-zoom-out.png", QString{}, "view_menu.html");
962 show_all_act = newAction("showall", tr("Show whole map"), this, SLOT(showWholeMap()), "view-show-all.png", QString{}, "view_menu.html");
963 fullscreen_act = newAction("fullscreen", tr("Toggle fullscreen mode"), window, SLOT(toggleFullscreenMode()), nullptr, QString{}, "view_menu.html");
964 custom_zoom_act = newAction("setzoom", tr("Set custom zoom factor..."), this, SLOT(setCustomZoomFactorClicked()), nullptr, QString{}, "view_menu.html");
965
966 hatch_areas_view_act = newCheckAction("hatchareasview", tr("Hatch areas"), this, SLOT(hatchAreas(bool)), "view-hatch-areas.png", QString{}, "view_menu.html");
967 baseline_view_act = newCheckAction("baselineview", tr("Baseline view"), this, SLOT(baselineView(bool)), "view-baseline.png", QString{}, "view_menu.html");
968 hide_all_templates_act = newCheckAction("hidealltemplates", tr("Hide all templates"), this, SLOT(hideAllTemplates(bool)), nullptr, QString{}, "view_menu.html");
969 overprinting_simulation_act = newCheckAction("overprintsimulation", tr("Overprinting simulation"), this, SLOT(overprintingSimulation(bool)), nullptr, QString{}, "view_menu.html");
970
971 symbol_window_act = newCheckAction("symbolwindow", tr("Symbol window"), this, SLOT(showSymbolWindow(bool)), "symbols.png", tr("Show/Hide the symbol window"), "symbol_dock_widget.html");
972 color_window_act = newCheckAction("colorwindow", tr("Color window"), this, SLOT(showColorWindow(bool)), "colors.png", tr("Show/Hide the color window"), "color_dock_widget.html");
973 symbol_set_id_act = newAction("symbol-set-id", tr("Symbol set ID..."), this, SLOT(symbolSetIdClicked()), nullptr, tr("Edit the symbol set ID"), nullptr);
974 load_symbols_from_act = newAction("loadsymbols", tr("Replace symbol set..."), this, SLOT(loadSymbolsFromClicked()), nullptr, tr("Replace the symbols with those from another map file"), "symbol_replace_dialog.html");
975 load_crt_act = newAction("loadcrt", tr("Load CRT file..."), this, SLOT(loadCrtClicked()), nullptr, tr("Assign new symbols by cross-reference table"), "symbol_replace_dialog.html");
976 /*QAction* load_colors_from_act = newAction("loadcolors", tr("Load colors from..."), this, SLOT(loadColorsFromClicked()), nullptr, tr("Replace the colors with those from another map file"));*/
977
978 scale_all_symbols_act = newAction("scaleall", tr("Scale all symbols..."), this, SLOT(scaleAllSymbolsClicked()), nullptr, tr("Scale the whole symbol set"), "map_menu.html");
979 georeferencing_act = newAction("georef", tr("Georeferencing..."), this, SLOT(editGeoreferencing()), nullptr, QString{}, "georeferencing.html");
980 scale_map_act = newAction("scalemap", tr("Change map scale..."), this, SLOT(scaleMapClicked()), "tool-scale.png", tr("Change the map scale and adjust map objects and symbol sizes"), "map_menu.html");
981 rotate_map_act = newAction("rotatemap", tr("Rotate map..."), this, SLOT(rotateMapClicked()), "tool-rotate.png", tr("Rotate the whole map"), "map_menu.html");
982 map_notes_act = newAction("mapnotes", tr("Map notes..."), this, SLOT(mapNotesClicked()), nullptr, QString{}, "map_menu.html");
983
984 template_window_act = newCheckAction("templatewindow", tr("Template setup window"), this, SLOT(showTemplateWindow(bool)), "templates", tr("Show/Hide the template window"), "templates_menu.html");
985 //QAction* template_config_window_act = newCheckAction("templateconfigwindow", tr("Template configurations window"), this, SLOT(showTemplateConfigurationsWindow(bool)), "window-new", tr("Show/Hide the template configurations window"));
986 //QAction* template_visibilities_window_act = newCheckAction("templatevisibilitieswindow", tr("Template visibilities window"), this, SLOT(showTemplateVisbilitiesWindow(bool)), "window-new", tr("Show/Hide the template visibilities window"));
987 open_template_act = newAction("opentemplate", tr("Open template..."), this, SLOT(openTemplateClicked()), nullptr, QString{}, "templates_menu.html");
988 reopen_template_act = newAction("reopentemplate", tr("Reopen template..."), this, SLOT(reopenTemplateClicked()), nullptr, QString{}, "templates_menu.html");
989
990 tags_window_act = newCheckAction("tagswindow", tr("Tag editor"), this, SLOT(showTagsWindow(bool)), "window-new", tr("Show/Hide the tag editor window"), "tag_editor.html");
991
992 edit_tool_act = newToolAction("editobjects", tr("Edit objects"), this, SLOT(editToolClicked()), "tool-edit.png", QString{}, "toolbars.html#tool_edit_point");
993 edit_line_tool_act = newToolAction("editlines", tr("Edit lines"), this, SLOT(editLineToolClicked()), "tool-edit-line.png", QString{}, "toolbars.html#tool_edit_line");
994 draw_point_act = newToolAction("drawpoint", tr("Set point objects"), this, SLOT(drawPointClicked()), "draw-point.png", QString{}, "toolbars.html#tool_draw_point");
995 draw_path_act = newToolAction("drawpath", tr("Draw paths"), this, SLOT(drawPathClicked()), "draw-path.png", QString{}, "toolbars.html#tool_draw_path");
996 draw_circle_act = newToolAction("drawcircle", tr("Draw circles and ellipses"), this, SLOT(drawCircleClicked()), "draw-circle.png", QString{}, "toolbars.html#tool_draw_circle");
997 draw_rectangle_act = newToolAction("drawrectangle", tr("Draw rectangles"), this, SLOT(drawRectangleClicked()), "draw-rectangle.png", QString{}, "toolbars.html#tool_draw_rectangle");
998 draw_freehand_act = newToolAction("drawfreehand", tr("Draw free-handedly"), this, SLOT(drawFreehandClicked()), "draw-freehand.png", QString{}, "toolbars.html#tool_draw_freehand"); // TODO: documentation
999 draw_fill_act = newToolAction("drawfill", tr("Fill bounded areas"), this, SLOT(drawFillClicked()), "tool-fill.png", QString{}, "toolbars.html#tool_draw_fill"); // TODO: documentation
1000 draw_text_act = newToolAction("drawtext", tr("Write text"), this, SLOT(drawTextClicked()), "draw-text.png", QString{}, "toolbars.html#tool_draw_text");
1001 duplicate_act = newAction("duplicate", tr("Duplicate"), this, SLOT(duplicateClicked()), "tool-duplicate.png", QString{}, "toolbars.html#duplicate");
1002 switch_symbol_act = newAction("switchsymbol", tr("Switch symbol"), this, SLOT(switchSymbolClicked()), "tool-switch-symbol.png", QString{}, "toolbars.html#switch_symbol");
1003 fill_border_act = newAction("fillborder", tr("Fill / Create border"), this, SLOT(fillBorderClicked()), "tool-fill-border.png", QString{}, "toolbars.html#fill_create_border");
1004 switch_dashes_act = newAction("switchdashes", tr("Switch dash direction"), this, SLOT(switchDashesClicked()), "tool-switch-dashes", QString{}, "toolbars.html#switch_dashes");
1005 connect_paths_act = newAction("connectpaths", tr("Connect paths"), this, SLOT(connectPathsClicked()), "tool-connect-paths.png", QString{}, "toolbars.html#connect");
1006
1007 cut_tool_act = newToolAction("cutobject", tr("Cut object"), this, SLOT(cutClicked()), "tool-cut.png", QString{}, "toolbars.html#cut_tool");
1008 cut_hole_act = newToolAction("cuthole", tr("Cut free form hole"), this, SLOT(cutHoleClicked()), "tool-cut-hole.png", QString{}, "toolbars.html#cut_hole"); // cut hole using a path
1009 cut_hole_circle_act = newToolAction("cutholecircle", tr("Cut round hole"), this, SLOT(cutHoleCircleClicked()), "tool-cut-hole.png", QString{}, "toolbars.html#cut_hole");
1010 cut_hole_rectangle_act = newToolAction("cutholerectangle", tr("Cut rectangular hole"), this, SLOT(cutHoleRectangleClicked()), "tool-cut-hole.png", QString{}, "toolbars.html#cut_hole");
1011 cut_hole_menu = new QMenu(tr("Cut hole"));
1012 cut_hole_menu->menuAction()->setMenuRole(QAction::NoRole);
1013 cut_hole_menu->setIcon(QIcon(QString::fromLatin1(":/images/tool-cut-hole.png")));
1014 cut_hole_menu->addAction(cut_hole_act);
1015 cut_hole_menu->addAction(cut_hole_circle_act);
1016 cut_hole_menu->addAction(cut_hole_rectangle_act);
1017
1018 rotate_act = newToolAction("rotateobjects", tr("Rotate objects"), this, SLOT(rotateClicked()), "tool-rotate.png", QString{}, "toolbars.html#rotate");
1019 rotate_pattern_act = newToolAction("rotatepatterns", tr("Rotate pattern"), this, SLOT(rotatePatternClicked()), "tool-rotate-pattern.png", QString{}, "toolbars.html#tool_rotate_pattern");
1020 scale_act = newToolAction("scaleobjects", tr("Scale objects"), this, SLOT(scaleClicked()), "tool-scale.png", QString{}, "toolbars.html#scale");
1021 measure_act = newCheckAction("measure", tr("Measure lengths and areas"), this, SLOT(measureClicked(bool)), "tool-measure.png", QString{}, "toolbars.html#measure");
1022 boolean_union_act = newAction("booleanunion", tr("Unify areas"), this, SLOT(booleanUnionClicked()), "tool-boolean-union.png", QString{}, "toolbars.html#unify_areas");
1023 boolean_intersection_act = newAction("booleanintersection", tr("Intersect areas"), this, SLOT(booleanIntersectionClicked()), "tool-boolean-intersection.png", QString{}, "toolbars.html#intersect_areas");
1024 boolean_difference_act = newAction("booleandifference", tr("Cut away from area"), this, SLOT(booleanDifferenceClicked()), "tool-boolean-difference.png", QString{}, "toolbars.html#area_difference");
1025 boolean_xor_act = newAction("booleanxor", tr("Area XOr"), this, SLOT(booleanXOrClicked()), "tool-boolean-xor.png", QString{}, "toolbars.html#area_xor");
1026 boolean_merge_holes_act = newAction("booleanmergeholes", tr("Merge area holes"), this, SLOT(booleanMergeHolesClicked()), "tool-boolean-merge-holes.png", QString{}, "toolbars.html#area_merge_holes"); // TODO:documentation
1027 convert_to_curves_act = newAction("converttocurves", tr("Convert to curves"), this, SLOT(convertToCurvesClicked()), "tool-convert-to-curves.png", QString{}, "toolbars.html#convert_to_curves");
1028 simplify_path_act = newAction("simplify", tr("Simplify path"), this, SLOT(simplifyPathClicked()), "tool-simplify-path.png", QString{}, "toolbars.html#simplify_path");
1029 cutout_physical_act = newToolAction("cutoutphysical", tr("Cutout"), this, SLOT(cutoutPhysicalClicked()), "tool-cutout-physical.png", QString{}, "toolbars.html#cutout_physical");
1030 cutaway_physical_act = newToolAction("cutawayphysical", tr("Cut away"), this, SLOT(cutawayPhysicalClicked()), "tool-cutout-physical-inner.png", QString{}, "toolbars.html#cutaway_physical");
1031 distribute_points_act = newAction("distributepoints", tr("Distribute points along path"), this, SLOT(distributePointsClicked()), "tool-distribute-points.png", QString{}, "toolbars.html#distribute_points"); // TODO: write documentation
1032
1033 paint_on_template_act = new QAction(QIcon(QString::fromLatin1(":/images/pencil.png")), tr("Paint on template"), this);
1034 paint_on_template_act->setMenuRole(QAction::NoRole);
1035 paint_on_template_act->setCheckable(true);
1036 paint_on_template_act->setWhatsThis(Util::makeWhatThis("toolbars.html#draw_on_template"));
1037 connect(paint_on_template_act, &QAction::triggered, this, &MapEditorController::paintOnTemplateClicked);
1038
1039 paint_on_template_settings_act = new QAction(QIcon(QString::fromLatin1(":/images/paint-on-template-settings.png")), tr("Paint on template settings"), this);
1040 paint_on_template_settings_act->setMenuRole(QAction::NoRole);
1041 paint_on_template_settings_act->setWhatsThis(Util::makeWhatThis("toolbars.html#draw_on_template"));
1042 connect(paint_on_template_settings_act, &QAction::triggered, this, &MapEditorController::paintOnTemplateSelectClicked);
1043
1044 touch_cursor_action = newCheckAction("touchcursor", tr("Enable touch cursor"), map_widget, SLOT(enableTouchCursor(bool)), "tool-touch-cursor.png", QString{}, "toolbars.html#touch_cursor"); // TODO: write documentation
1045 gps_display_action = newCheckAction("gpsdisplay", tr("Enable GPS display"), this, SLOT(enableGPSDisplay(bool)), "tool-gps-display.png", QString{}, "toolbars.html#gps_display"); // TODO: write documentation
1046 gps_display_action->setEnabled(map->getGeoreferencing().isValid() && ! map->getGeoreferencing().isLocal());
1047 gps_distance_rings_action = newCheckAction("gpsdistancerings", tr("Enable GPS distance rings"), this, SLOT(enableGPSDistanceRings(bool)), "gps-distance-rings.png", QString{}, "toolbars.html#gps_distance_rings"); // TODO: write documentation
1048 gps_distance_rings_action->setEnabled(false);
1049 draw_point_gps_act = newToolAction("drawpointgps", tr("Set point object at GPS position"), this, SLOT(drawPointGPSClicked()), "draw-point-gps.png", QString{}, "toolbars.html#tool_draw_point_gps"); // TODO: write documentation
1050 draw_point_gps_act->setEnabled(false);
1051 gps_temporary_point_act = newAction("gpstemporarypoint", tr("Set temporary marker at GPS position"), this, SLOT(gpsTemporaryPointClicked()), "gps-temporary-point.png", QString{}, "toolbars.html#gps_temporary_point"); // TODO: write documentation
1052 gps_temporary_point_act->setEnabled(false);
1053 gps_temporary_path_act = newCheckAction("gpstemporarypath", tr("Create temporary path at GPS position"), this, SLOT(gpsTemporaryPathClicked(bool)), "gps-temporary-path.png", QString{}, "toolbars.html#gps_temporary_path"); // TODO: write documentation
1054 gps_temporary_path_act->setEnabled(false);
1055 gps_temporary_clear_act = newAction("gpstemporaryclear", tr("Clear temporary GPS markers"), this, SLOT(gpsTemporaryClearClicked()), "gps-temporary-clear.png", QString{}, "toolbars.html#gps_temporary_clear"); // TODO: write documentation
1056 gps_temporary_clear_act->setEnabled(false);
1057
1058 compass_action = newCheckAction("compassdisplay", tr("Enable compass display"), this, SLOT(enableCompassDisplay(bool)), "compass.png", QString{}, "toolbars.html#compass_display"); // TODO: write documentation
1059 align_map_with_north_act = newCheckAction("alignmapwithnorth", tr("Align map with north"), this, SLOT(alignMapWithNorth(bool)), "rotate-map.png", QString{}, "toolbars.html#align_map_with_north"); // TODO: write documentation
1060
1061 mappart_add_act = newAction("addmappart", tr("Add new part..."), this, SLOT(addMapPart()));
1062 mappart_rename_act = newAction("renamemappart", tr("Rename current part..."), this, SLOT(renameMapPart()));
1063 mappart_remove_act = newAction("removemappart", tr("Remove current part"), this, SLOT(removeMapPart()));
1064 mappart_merge_act = newAction("mergemapparts", tr("Merge all parts"), this, SLOT(mergeAllMapParts()));
1065
1066 import_act = newAction("import", tr("Import..."), this, SLOT(importClicked()), nullptr, QString{}, "file_menu.html");
1067
1068 map_coordinates_act = new QAction(tr("Map coordinates"), this);
1069 map_coordinates_act->setCheckable(true);
1070 projected_coordinates_act = new QAction(tr("Projected coordinates"), this);
1071 projected_coordinates_act->setCheckable(true);
1072 geographic_coordinates_act = new QAction(tr("Latitude/Longitude (Dec)"), this);
1073 geographic_coordinates_act->setCheckable(true);
1074 geographic_coordinates_dms_act = new QAction(tr("Latitude/Longitude (DMS)"), this);
1075 geographic_coordinates_dms_act->setCheckable(true);
1076 auto* coordinates_group = new QActionGroup(this);
1077 coordinates_group->addAction(map_coordinates_act);
1078 coordinates_group->addAction(projected_coordinates_act);
1079 coordinates_group->addAction(geographic_coordinates_act);
1080 coordinates_group->addAction(geographic_coordinates_dms_act);
1081 QObject::connect(coordinates_group, &QActionGroup::triggered, this, &MapEditorController::coordsDisplayChanged);
1082 map_coordinates_act->setChecked(true);
1083 QObject::connect(&map->getGeoreferencing(), &Georeferencing::projectionChanged, this, &MapEditorController::projectionChanged);
1084 projectionChanged();
1085
1086 copy_coords_act = newAction("copy-coords", tr("Copy position"), this, SLOT(copyDisplayedCoords()), "copy-coords.png", tr("Copy position to clipboard."));
1087 }
1088
createMenuAndToolbars()1089 void MapEditorController::createMenuAndToolbars()
1090 {
1091 statusbar_cursorpos_label->setContextMenuPolicy(Qt::ActionsContextMenu);
1092 statusbar_cursorpos_label->addAction(map_coordinates_act);
1093 statusbar_cursorpos_label->addAction(projected_coordinates_act);
1094 statusbar_cursorpos_label->addAction(geographic_coordinates_act);
1095 statusbar_cursorpos_label->addAction(geographic_coordinates_dms_act);
1096
1097 // Refactored so we can do custom key bindings in the future
1098 assignKeyboardShortcuts();
1099
1100 // Extend file menu
1101 QMenu* file_menu = window->getFileMenu();
1102 QAction* insertion_act = window->getFileMenuExtensionAct();
1103 #ifdef QT_PRINTSUPPORT_LIB
1104 file_menu->insertAction(insertion_act, print_act);
1105 file_menu->insertSeparator(insertion_act);
1106 insertion_act = print_act;
1107 #endif
1108 file_menu->insertAction(insertion_act, import_act);
1109 #ifdef QT_PRINTSUPPORT_LIB
1110 QMenu* export_menu = new QMenu(tr("&Export as..."), file_menu);
1111 export_menu->menuAction()->setMenuRole(QAction::NoRole);
1112 export_menu->addAction(export_image_act);
1113 export_menu->addAction(export_pdf_act);
1114 if (export_vector_act)
1115 export_menu->addAction(export_vector_act);
1116 file_menu->insertMenu(insertion_act, export_menu);
1117 #endif
1118 file_menu->insertSeparator(insertion_act);
1119
1120 // Edit menu
1121 QMenu* edit_menu = window->menuBar()->addMenu(tr("&Edit"));
1122 edit_menu->setWhatsThis(Util::makeWhatThis("edit_menu.html"));
1123 edit_menu->addAction(undo_act);
1124 edit_menu->addAction(redo_act);
1125 edit_menu->addSeparator();
1126 edit_menu->addAction(cut_act);
1127 edit_menu->addAction(copy_act);
1128 edit_menu->addAction(paste_act);
1129 edit_menu->addAction(delete_act);
1130 edit_menu->addSeparator();
1131 edit_menu->addAction(select_all_act);
1132 edit_menu->addAction(select_nothing_act);
1133 edit_menu->addAction(invert_selection_act);
1134 edit_menu->addAction(select_by_current_symbol_act);
1135 edit_menu->addSeparator();
1136 edit_menu->addAction(find_feature->showDialogAction());
1137 edit_menu->addAction(find_feature->findNextAction());
1138 edit_menu->addSeparator();
1139 edit_menu->addAction(clear_undo_redo_history_act);
1140
1141 // View menu
1142 QMenu* view_menu = window->menuBar()->addMenu(tr("&View"));
1143 view_menu->setWhatsThis(Util::makeWhatThis("view_menu.html"));
1144 view_menu->addAction(pan_act);
1145 view_menu->addAction(zoom_in_act);
1146 view_menu->addAction(zoom_out_act);
1147 view_menu->addAction(show_all_act);
1148 view_menu->addAction(custom_zoom_act);
1149 view_menu->addSeparator();
1150 view_menu->addAction(show_grid_act);
1151 view_menu->addAction(hatch_areas_view_act);
1152 view_menu->addAction(baseline_view_act);
1153 view_menu->addAction(overprinting_simulation_act);
1154 view_menu->addAction(hide_all_templates_act);
1155 view_menu->addSeparator();
1156 QMenu* coordinates_menu = new QMenu(tr("Display coordinates as..."), view_menu);
1157 coordinates_menu->menuAction()->setMenuRole(QAction::NoRole);
1158 coordinates_menu->addAction(map_coordinates_act);
1159 coordinates_menu->addAction(projected_coordinates_act);
1160 coordinates_menu->addAction(geographic_coordinates_act);
1161 coordinates_menu->addAction(geographic_coordinates_dms_act);
1162 view_menu->addMenu(coordinates_menu);
1163 view_menu->addSeparator();
1164 view_menu->addAction(fullscreen_act);
1165 view_menu->addSeparator();
1166 toolbars_menu = view_menu->addMenu(tr("Toolbars"));
1167 view_menu->addAction(tags_window_act);
1168 view_menu->addAction(color_window_act);
1169 view_menu->addAction(symbol_window_act);
1170 view_menu->addAction(template_window_act);
1171
1172 // Tools menu
1173 QMenu *tools_menu = window->menuBar()->addMenu(tr("&Tools"));
1174 tools_menu->setWhatsThis(Util::makeWhatThis("tools_menu.html"));
1175 tools_menu->addAction(edit_tool_act);
1176 tools_menu->addAction(edit_line_tool_act);
1177 tools_menu->addAction(draw_point_act);
1178 tools_menu->addAction(draw_path_act);
1179 tools_menu->addAction(draw_circle_act);
1180 tools_menu->addAction(draw_rectangle_act);
1181 tools_menu->addAction(draw_freehand_act);
1182 tools_menu->addAction(draw_fill_act);
1183 tools_menu->addAction(draw_text_act);
1184 tools_menu->addAction(duplicate_act);
1185 tools_menu->addAction(switch_symbol_act);
1186 tools_menu->addAction(fill_border_act);
1187 tools_menu->addAction(switch_dashes_act);
1188 tools_menu->addAction(connect_paths_act);
1189 tools_menu->addAction(boolean_union_act);
1190 tools_menu->addAction(boolean_intersection_act);
1191 tools_menu->addAction(boolean_difference_act);
1192 tools_menu->addAction(boolean_xor_act);
1193 tools_menu->addAction(boolean_merge_holes_act);
1194 tools_menu->addAction(cut_tool_act);
1195 tools_menu->addMenu(cut_hole_menu);
1196 tools_menu->addAction(rotate_act);
1197 tools_menu->addAction(rotate_pattern_act);
1198 tools_menu->addAction(scale_act);
1199 tools_menu->addAction(measure_act);
1200 tools_menu->addAction(convert_to_curves_act);
1201 tools_menu->addAction(simplify_path_act);
1202 tools_menu->addAction(cutout_physical_act);
1203 tools_menu->addAction(cutaway_physical_act);
1204 tools_menu->addAction(distribute_points_act);
1205 tools_menu->addAction(touch_cursor_action);
1206
1207 // Map menu
1208 QMenu* map_menu = window->menuBar()->addMenu(tr("M&ap"));
1209 map_menu->setWhatsThis(Util::makeWhatThis("map_menu.html"));
1210 map_menu->addAction(georeferencing_act);
1211 map_menu->addAction(configure_grid_act);
1212 map_menu->addSeparator();
1213 map_menu->addAction(scale_map_act);
1214 map_menu->addAction(rotate_map_act);
1215 map_menu->addAction(map_notes_act);
1216 map_menu->addSeparator();
1217 updateMapPartsUI();
1218 map_menu->addAction(mappart_add_act);
1219 map_menu->addAction(mappart_rename_act);
1220 map_menu->addAction(mappart_remove_act);
1221 map_menu->addMenu(mappart_move_menu);
1222 map_menu->addMenu(mappart_merge_menu);
1223 map_menu->addAction(mappart_merge_act);
1224
1225 // Symbols menu
1226 QMenu* symbols_menu = window->menuBar()->addMenu(tr("Sy&mbols"));
1227 symbols_menu->setWhatsThis(Util::makeWhatThis("symbols_menu.html"));
1228 symbols_menu->addAction(symbol_window_act);
1229 symbols_menu->addAction(color_window_act);
1230 symbols_menu->addSeparator();
1231 symbols_menu->addAction(symbol_set_id_act);
1232 symbols_menu->addAction(scale_all_symbols_act);
1233 symbols_menu->addAction(load_symbols_from_act);
1234 symbols_menu->addAction(load_crt_act);
1235 /*symbols_menu->addAction(load_colors_from_act);*/
1236
1237 // Templates menu
1238 QMenu* template_menu = window->menuBar()->addMenu(tr("&Templates"));
1239 template_menu->setWhatsThis(Util::makeWhatThis("templates_menu.html"));
1240 template_menu->addAction(template_window_act);
1241 /*template_menu->addAction(template_config_window_act);
1242 template_menu->addAction(template_visibilities_window_act);*/
1243 template_menu->addSeparator();
1244 template_menu->addAction(open_template_act);
1245 template_menu->addAction(reopen_template_act);
1246
1247 // Extend and activate general toolbar
1248 QToolBar* main_toolbar = window->getGeneralToolBar();
1249 #ifdef QT_PRINTSUPPORT_LIB
1250 main_toolbar->addAction(print_act);
1251 main_toolbar->addSeparator();
1252 #endif
1253 main_toolbar->addAction(cut_act);
1254 main_toolbar->addAction(copy_act);
1255 main_toolbar->addAction(paste_act);
1256 main_toolbar->addSeparator();
1257 main_toolbar->addAction(undo_act);
1258 main_toolbar->addAction(redo_act);
1259 window->addToolBar(main_toolbar);
1260
1261 // View toolbar
1262 toolbar_view = window->addToolBar(tr("View"));
1263 toolbar_view->setObjectName(QString::fromLatin1("View toolbar"));
1264 auto* grid_button = new QToolButton();
1265 grid_button->setCheckable(true);
1266 grid_button->setDefaultAction(show_grid_act);
1267 grid_button->setPopupMode(QToolButton::MenuButtonPopup);
1268 auto* grid_menu = new QMenu(grid_button);
1269 grid_menu->addAction(tr("Configure grid..."));
1270 grid_button->setMenu(grid_menu);
1271 connect(grid_menu, &QMenu::triggered, this, &MapEditorController::configureGrid);
1272 toolbar_view->addWidget(grid_button);
1273 toolbar_view->addSeparator();
1274 toolbar_view->addAction(pan_act);
1275 toolbar_view->addAction(zoom_in_act);
1276 toolbar_view->addAction(zoom_out_act);
1277 toolbar_view->addAction(show_all_act);
1278
1279 // MapParts toolbar
1280 toolbar_mapparts = window->addToolBar(tr("Map parts"));
1281 toolbar_mapparts->setObjectName(QString::fromLatin1("Map parts toolbar"));
1282 if (!mappart_selector_box)
1283 {
1284 mappart_selector_box = new QComboBox(toolbar_mapparts);
1285 mappart_selector_box->setToolTip(tr("Map parts"));
1286 }
1287 mappart_selector_box->setSizeAdjustPolicy(QComboBox::AdjustToContents);
1288 connect(mappart_selector_box, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &MapEditorController::changeMapPart);
1289 toolbar_mapparts->addWidget(mappart_selector_box);
1290
1291 window->addToolBarBreak();
1292
1293 // Drawing toolbar
1294 toolbar_drawing = window->addToolBar(tr("Drawing"));
1295 toolbar_drawing->setObjectName(QString::fromLatin1("Drawing toolbar"));
1296 toolbar_drawing->addAction(edit_tool_act);
1297 toolbar_drawing->addAction(edit_line_tool_act);
1298 toolbar_drawing->addAction(draw_point_act);
1299 toolbar_drawing->addAction(draw_path_act);
1300 toolbar_drawing->addAction(draw_circle_act);
1301 toolbar_drawing->addAction(draw_rectangle_act);
1302 toolbar_drawing->addAction(draw_freehand_act);
1303 toolbar_drawing->addAction(draw_fill_act);
1304 toolbar_drawing->addAction(draw_text_act);
1305 toolbar_drawing->addSeparator();
1306
1307 auto* paint_on_template_button = new QToolButton();
1308 paint_on_template_button->setCheckable(true);
1309 paint_on_template_button->setDefaultAction(paint_on_template_act);
1310 paint_on_template_button->setPopupMode(QToolButton::MenuButtonPopup);
1311 auto* paint_on_template_menu = new QMenu(paint_on_template_button);
1312 paint_on_template_menu->addAction(tr("Select template..."));
1313 paint_on_template_button->setMenu(paint_on_template_menu);
1314 connect(paint_on_template_menu, &QMenu::triggered, this, &MapEditorController::paintOnTemplateSelectClicked);
1315 toolbar_drawing->addWidget(paint_on_template_button);
1316
1317 // Editing toolbar
1318 toolbar_editing = window->addToolBar(tr("Editing"));
1319 toolbar_editing->setObjectName(QString::fromLatin1("Editing toolbar"));
1320 toolbar_editing->addAction(delete_act);
1321 toolbar_editing->addAction(duplicate_act);
1322 toolbar_editing->addAction(switch_symbol_act);
1323 toolbar_editing->addAction(fill_border_act);
1324 toolbar_editing->addAction(switch_dashes_act);
1325 toolbar_editing->addAction(connect_paths_act);
1326 toolbar_editing->addAction(boolean_union_act);
1327 toolbar_editing->addAction(cut_tool_act);
1328
1329 auto* cut_hole_button = new QToolButton();
1330 cut_hole_button->setCheckable(true);
1331 cut_hole_button->setToolButtonStyle(Qt::ToolButtonIconOnly);
1332 cut_hole_button->setDefaultAction(cut_hole_act);
1333 cut_hole_button->setPopupMode(QToolButton::MenuButtonPopup);
1334 cut_hole_button->setMenu(cut_hole_menu);
1335 toolbar_editing->addWidget(cut_hole_button);
1336
1337 toolbar_editing->addAction(rotate_act);
1338 toolbar_editing->addAction(rotate_pattern_act);
1339 toolbar_editing->addAction(scale_act);
1340 toolbar_editing->addAction(measure_act);
1341
1342 // Advanced editing toolbar
1343 toolbar_advanced_editing = window->addToolBar(tr("Advanced editing"));
1344 toolbar_advanced_editing->setObjectName(QString::fromLatin1("Advanced editing toolbar"));
1345 toolbar_advanced_editing->addAction(cutout_physical_act);
1346 toolbar_advanced_editing->addAction(cutaway_physical_act);
1347 toolbar_advanced_editing->addAction(convert_to_curves_act);
1348 toolbar_advanced_editing->addAction(simplify_path_act);
1349 toolbar_advanced_editing->addAction(distribute_points_act);
1350 toolbar_advanced_editing->addAction(boolean_intersection_act);
1351 toolbar_advanced_editing->addAction(boolean_difference_act);
1352 toolbar_advanced_editing->addAction(boolean_xor_act);
1353 toolbar_advanced_editing->addAction(boolean_merge_holes_act);
1354
1355 toolbars_menu->addAction(main_toolbar->toggleViewAction());
1356 toolbars_menu->addAction(toolbar_view->toggleViewAction());
1357 toolbars_menu->addAction(toolbar_mapparts->toggleViewAction());
1358 toolbars_menu->addAction(toolbar_drawing->toggleViewAction());
1359 toolbars_menu->addAction(toolbar_editing->toggleViewAction());
1360 toolbars_menu->addAction(toolbar_advanced_editing->toggleViewAction());
1361
1362 QWidget* context_menu = map_widget->getContextMenu();
1363 context_menu->addAction(edit_tool_act);
1364 context_menu->addAction(draw_point_act);
1365 context_menu->addAction(draw_path_act);
1366 context_menu->addAction(draw_rectangle_act);
1367 context_menu->addAction(cut_tool_act);
1368 context_menu->addAction(cut_hole_act);
1369 context_menu->addAction(switch_dashes_act);
1370 context_menu->addAction(connect_paths_act);
1371 context_menu->addAction(copy_coords_act);
1372 }
1373
createMobileGUI()1374 void MapEditorController::createMobileGUI()
1375 {
1376 // Create mobile-specific actions
1377 mobile_symbol_selector_action = new QAction(tr("Select symbol"), this);
1378 connect(mobile_symbol_selector_action, &QAction::triggered, this, &MapEditorController::mobileSymbolSelectorClicked);
1379
1380 mobile_symbol_button_menu = new QMenu(window);
1381 mobile_symbol_button_menu->addAction(QString{}); // reserved for symbol name
1382 auto* description_action = mobile_symbol_button_menu->addAction(QApplication::translate("OpenOrienteering::SymbolPropertiesWidget", "Description"));
1383 connect(description_action, &QAction::triggered, this, [this]() {
1384 auto* symbol = symbol_widget->getSingleSelectedSymbol();
1385 auto document = QString{ symbol->getNumberAsString() + QLatin1Char(' ')
1386 + QLatin1String("<b>") + symbol->getName() + QLatin1String("</b>\n\n")
1387 + symbol->getDescription() };
1388 // Cf. SymbolToolTip::scheduleShow
1389 document.replace(QLatin1Char('\n'), QStringLiteral("<br>"));
1390 document.remove(QLatin1Char('\r'));
1391 TextBrowserDialog description_dialog(document, window);
1392 description_dialog.exec();
1393 });
1394 mobile_symbol_button_menu->addSeparator();
1395 auto* hide_symbol_action = mobile_symbol_button_menu->addAction(QApplication::translate("OpenOrienteering::SymbolRenderWidget", "Hide objects with this symbol"));
1396 hide_symbol_action->setCheckable(true);
1397 connect(hide_symbol_action, &QAction::triggered, this, [this](bool value) {
1398 auto* symbol = symbol_widget->getSingleSelectedSymbol();
1399 symbol->setHidden(value);
1400 if (!value && map->removeSymbolFromSelection(symbol, false))
1401 map->emitSelectionChanged();
1402 map->updateAllMapWidgets();
1403 map->setSymbolsDirty();
1404 selectedSymbolsChanged();
1405 });
1406 auto* protected_symbol_action = mobile_symbol_button_menu->addAction(QApplication::translate("OpenOrienteering::SymbolRenderWidget", "Protect objects with this symbol"));
1407 protected_symbol_action->setCheckable(true);
1408 connect(protected_symbol_action, &QAction::triggered, this, [this](bool value) {
1409 auto* symbol = symbol_widget->getSingleSelectedSymbol();
1410 symbol->setProtected(value);
1411 if (!value && map->removeSymbolFromSelection(symbol, false))
1412 map->emitSelectionChanged();
1413 map->setSymbolsDirty();
1414 selectedSymbolsChanged();
1415 });
1416
1417 QAction* hide_top_bar_action = new QAction(QIcon(QString::fromLatin1(":/images/arrow-thin-upleft.png")), tr("Hide top bar"), this);
1418 connect(hide_top_bar_action, &QAction::triggered, this, &MapEditorController::hideTopActionBar);
1419
1420 QAction* show_top_bar_action = new QAction(QIcon(QString::fromLatin1(":/images/arrow-thin-downright.png")), tr("Show top bar"), this);
1421 connect(show_top_bar_action, &QAction::triggered, this, &MapEditorController::showTopActionBar);
1422
1423 Q_ASSERT(mappart_selector_box);
1424 QAction* mappart_action = new QAction(QIcon(QString::fromLatin1(":/images/map-parts.png")), tr("Map parts"), this);
1425 connect(mappart_action, &QAction::triggered, this, [this, mappart_action]() {
1426 auto* mappart_button = top_action_bar->getButtonForAction(mappart_action);
1427 if (top_action_bar->buttonDisplay(mappart_button) == ActionGridBar::DisplayOverflow)
1428 mappart_button = top_action_bar->getButtonForAction(top_action_bar->getOverflowAction());
1429 mappart_selector_box->setGeometry(mappart_button->geometry());
1430 mappart_selector_box->showPopup();
1431 });
1432 connect(mappart_selector_box, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &MapEditorController::changeMapPart);
1433
1434 // Create button for showing the top bar again after hiding it
1435 const auto button_size_px = qRound(Util::mmToPixelPhysical(Settings::getInstance().getSetting(Settings::ActionGridBar_ButtonSizeMM).toReal()));
1436 const auto icon_size_px = button_size_px - 12;
1437 const auto icon_size = QSize{icon_size_px, icon_size_px};
1438
1439 QIcon icon = show_top_bar_action->icon();
1440 QPixmap pixmap = icon.pixmap(icon_size, QIcon::Normal, QIcon::Off);
1441 if (pixmap.width() < icon_size_px)
1442 {
1443 pixmap = pixmap.scaled(icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
1444 icon.addPixmap(pixmap);
1445 show_top_bar_action->setIcon(icon);
1446 }
1447
1448 show_top_bar_button = new QToolButton(window);
1449 show_top_bar_button->setDefaultAction(show_top_bar_action);
1450 show_top_bar_button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
1451 show_top_bar_button->setAutoRaise(true);
1452 show_top_bar_button->setIconSize(icon_size);
1453 show_top_bar_button->setGeometry(0, 0, button_size_px, button_size_px);
1454
1455
1456 // Create bottom action bar
1457 bottom_action_bar = new ActionGridBar(ActionGridBar::Horizontal, 2);
1458
1459 // Left side
1460 int col = 0;
1461 bottom_action_bar->addAction(zoom_in_act, 0, col);
1462 bottom_action_bar->addAction(pan_act, 1, col++);
1463
1464 bottom_action_bar->addAction(zoom_out_act, 0, col);
1465 auto* zoom_out_button = bottom_action_bar->getButtonForAction(zoom_out_act);
1466 auto* mobile_zoom_out_menu = new QMenu(zoom_out_button);
1467 auto* zoom_1x_action = mobile_zoom_out_menu->addAction(tr("1x zoom"));
1468 connect(zoom_1x_action, &QAction::triggered, this, [this]() {
1469 main_view->setZoom(1);
1470 });
1471 auto* zoom_2x_action = mobile_zoom_out_menu->addAction(tr("2x zoom"));
1472 connect(zoom_2x_action, &QAction::triggered, this, [this]() {
1473 main_view->setZoom(2);
1474 });
1475 zoom_out_button->setMenu(mobile_zoom_out_menu);
1476
1477 bottom_action_bar->addAction(move_to_gps_pos_act, 1, col++);
1478
1479 bottom_action_bar->addAction(hatch_areas_view_act, 0, col);
1480 bottom_action_bar->addAction(baseline_view_act, 1, col++);
1481
1482 bottom_action_bar->addAction(gps_temporary_path_act, 0, col);
1483 auto* temp_path_button = bottom_action_bar->getButtonForAction(gps_temporary_path_act);
1484 auto* mobile_gps_temp_path_menu = new QMenu(temp_path_button);
1485 mobile_gps_temp_path_menu->addAction(gps_temporary_clear_act);
1486 temp_path_button->setMenu(mobile_gps_temp_path_menu);
1487
1488 bottom_action_bar->addAction(gps_temporary_point_act, 1, col++);
1489
1490 bottom_action_bar->addAction(paint_on_template_act, 0, col);
1491 auto* paint_on_template_button = bottom_action_bar->getButtonForAction(paint_on_template_act);
1492 auto* mobile_paint_on_template_menu = new QMenu(paint_on_template_button);
1493 mobile_paint_on_template_menu->addAction(paint_on_template_settings_act);
1494 paint_on_template_button->setMenu(mobile_paint_on_template_menu);
1495
1496 // Right side
1497 bottom_action_bar->addActionAtEnd(mobile_symbol_selector_action, 0, 1, 2, 2);
1498 auto* button = bottom_action_bar->getButtonForAction(mobile_symbol_selector_action);
1499 button->setPopupMode(QToolButton::DelayedPopup);
1500
1501 col = 2;
1502 bottom_action_bar->addActionAtEnd(draw_point_act, 0, col);
1503 bottom_action_bar->addActionAtEnd(draw_point_gps_act, 1, col++);
1504
1505 bottom_action_bar->addActionAtEnd(draw_path_act, 0, col);
1506 bottom_action_bar->addActionAtEnd(draw_freehand_act, 1, col++);
1507
1508 bottom_action_bar->addActionAtEnd(draw_rectangle_act, 0, col);
1509 bottom_action_bar->addActionAtEnd(draw_circle_act, 1, col++);
1510
1511 //bottom_action_bar->addActionAtEnd(draw_fill_act, 0, col);
1512 bottom_action_bar->addActionAtEnd(draw_text_act, 1, col++);
1513
1514
1515 // Create top action bar
1516 top_action_bar = new ActionGridBar(ActionGridBar::Horizontal, 2);
1517
1518 // Left side
1519 col = 0;
1520 top_action_bar->addAction(hide_top_bar_action, 0, col);
1521 top_action_bar->addAction(window->getSaveAct(), 1, col++);
1522
1523 top_action_bar->addAction(compass_action, 0, col);
1524 top_action_bar->addAction(gps_display_action, 1, col++);
1525
1526 top_action_bar->addAction(gps_distance_rings_action, 0, col);
1527 top_action_bar->addAction(align_map_with_north_act, 1, col++);
1528
1529 top_action_bar->addAction(show_grid_act, 0, col);
1530 top_action_bar->addAction(show_all_act, 1, col++);
1531
1532 // Right side
1533 col = 0;
1534 top_action_bar->addActionAtEnd(window->getCloseAct(), 0, col);
1535 top_action_bar->addActionAtEnd(top_action_bar->getOverflowAction(), 1, col++);
1536
1537 top_action_bar->addActionAtEnd(redo_act, 0, col);
1538 top_action_bar->addActionAtEnd(undo_act, 1, col++);
1539
1540 top_action_bar->addActionAtEnd(touch_cursor_action, 0, col);
1541 top_action_bar->addActionAtEnd(template_window_act, 1, col++);
1542
1543 top_action_bar->addActionAtEnd(edit_tool_act, 0, col);
1544 top_action_bar->addActionAtEnd(edit_line_tool_act, 1, col++);
1545
1546 top_action_bar->addActionAtEnd(delete_act, 0, col);
1547 top_action_bar->addActionAtEnd(duplicate_act, 1, col++);
1548
1549 top_action_bar->addActionAtEnd(switch_symbol_act, 0, col);
1550 top_action_bar->addActionAtEnd(fill_border_act, 1, col++);
1551
1552 top_action_bar->addActionAtEnd(switch_dashes_act, 0, col);
1553 top_action_bar->addActionAtEnd(boolean_union_act, 1, col++);
1554
1555 top_action_bar->addActionAtEnd(cut_tool_act, 0, col);
1556 top_action_bar->addActionAtEnd(connect_paths_act, 1, col++);
1557
1558 top_action_bar->addActionAtEnd(rotate_act, 0, col);
1559 top_action_bar->addActionAtEnd(cut_hole_act, 1, col++);
1560
1561 top_action_bar->addActionAtEnd(scale_act, 0, col);
1562 top_action_bar->addActionAtEnd(rotate_pattern_act, 1, col++);
1563
1564 top_action_bar->addActionAtEnd(convert_to_curves_act, 0, col);
1565 top_action_bar->addActionAtEnd(simplify_path_act, 1, col++);
1566
1567 top_action_bar->addActionAtEnd(distribute_points_act, 0, col);
1568 top_action_bar->addActionAtEnd(boolean_difference_act, 1, col++);
1569
1570 top_action_bar->addActionAtEnd(measure_act, 0, col);
1571 top_action_bar->addActionAtEnd(boolean_merge_holes_act, 1, col++);
1572
1573 top_action_bar->addActionAtEnd(mappart_action, 1, col++);
1574
1575 bottom_action_bar->setToUseOverflowActionFrom(top_action_bar);
1576
1577 top_action_bar->setParent(map_widget);
1578
1579 auto* container_widget = new QWidget();
1580 auto* layout = new QVBoxLayout();
1581 layout->setMargin(0);
1582 layout->setSpacing(0);
1583 layout->addWidget(map_widget, 1);
1584 layout->addWidget(bottom_action_bar);
1585 container_widget->setLayout(layout);
1586 window->setCentralWidget(container_widget);
1587 }
1588
detach()1589 void MapEditorController::detach()
1590 {
1591 // Terminate all editing
1592 setTool(nullptr);
1593 setOverrideTool(nullptr);
1594
1595 saveWindowState();
1596
1597 // Avoid a crash triggered by pressing Ctrl-W during loading.
1598 if (nullptr != symbol_dock_widget)
1599 window->removeDockWidget(symbol_dock_widget);
1600 else if (nullptr != symbol_widget)
1601 delete symbol_widget;
1602
1603 delete gps_display;
1604 gps_display = nullptr;
1605 delete gps_track_recorder;
1606 gps_track_recorder = nullptr;
1607 delete compass_display;
1608 compass_display = nullptr;
1609 delete gps_marker_display;
1610 gps_marker_display = nullptr;
1611
1612 find_feature.reset(nullptr);
1613
1614 window->setCentralWidget(nullptr);
1615 delete map_widget;
1616
1617 delete statusbar_zoom_frame;
1618 delete statusbar_cursorpos_label;
1619
1620 #ifdef Q_OS_ANDROID
1621 QAndroidJniObject::callStaticMethod<void>("org/openorienteering/mapper/MapperActivity",
1622 "unlockOrientation",
1623 "()V");
1624 #endif
1625
1626 if (mobile_mode)
1627 window->setWindowState(window->windowState() & ~Qt::WindowFullScreen);
1628 }
1629
1630
setWindowStateChanged()1631 void MapEditorController::setWindowStateChanged()
1632 {
1633 if (!window_state_changed && !mobile_mode && mode != SymbolEditor)
1634 {
1635 window_state_changed = true;
1636 QTimer::singleShot(10, this, &MapEditorController::saveWindowState);
1637 }
1638 }
1639
saveWindowState()1640 void MapEditorController::saveWindowState()
1641 {
1642 if (window_state_changed)
1643 {
1644 QSettings settings;
1645 settings.beginGroup(QString::fromUtf8(metaObject()->className()));
1646 settings.setValue(QString::fromLatin1("state"), window->saveState());
1647 window_state_changed = false;
1648 }
1649 }
1650
restoreWindowState()1651 void MapEditorController::restoreWindowState()
1652 {
1653 if (!mobile_mode && mode != SymbolEditor)
1654 {
1655 QSettings settings;
1656 settings.beginGroup(QString::fromUtf8(metaObject()->className()));
1657 auto const key = QString::fromLatin1("state");
1658 auto const state = settings.value(key).toByteArray();
1659 settings.remove(key); // Avoid repeated crash from invalid data, GH-1366.
1660 settings.sync();
1661
1662 window->restoreState(state);
1663 if (toolbar_mapparts && mappart_selector_box)
1664 toolbar_mapparts->setVisible(mappart_selector_box->count() > 1);
1665
1666 settings.setValue(key, state); // Save valid state again.
1667 window_state_changed = false;
1668 }
1669 }
1670
keyPressEventFilter(QKeyEvent * event)1671 bool MapEditorController::keyPressEventFilter(QKeyEvent* event)
1672 {
1673 #if defined(Q_OS_ANDROID)
1674 if (event->key() == Qt::Key_Back)
1675 {
1676 if (symbol_widget && symbol_widget->isVisible())
1677 {
1678 mobileSymbolSelectorFinished();
1679 return true;
1680 }
1681
1682 if (isEditingInProgress() && !map_widget->findChild<KeyButtonBar*>())
1683 {
1684 QKeyEvent escape_pressed{ QEvent::KeyPress, Qt::Key_Escape, event->modifiers() };
1685 QCoreApplication::sendEvent(window, &escape_pressed);
1686 QKeyEvent escape_released{ QEvent::KeyRelease, Qt::Key_Escape, event->modifiers() };
1687 QCoreApplication::sendEvent(window, &escape_released);
1688 return true;
1689 }
1690 }
1691 #endif
1692
1693 return map_widget->keyPressEventFilter(event);
1694 }
1695
keyReleaseEventFilter(QKeyEvent * event)1696 bool MapEditorController::keyReleaseEventFilter(QKeyEvent* event)
1697 {
1698 return map_widget->keyReleaseEventFilter(event);
1699 }
1700
1701
1702
1703 // slot
exportVector()1704 void MapEditorController::exportVector()
1705 {
1706 QSettings settings;
1707 QString import_directory = settings.value(QString::fromLatin1("importFileDirectory"), QDir::homePath()).toString();
1708
1709 auto* format = FileFormats.findFormat("OGR-export");
1710 if (!format)
1711 return; /// \todo Error message?
1712
1713 QString filename = FileDialog::getSaveFileName(
1714 window,
1715 tr("Export"),
1716 import_directory,
1717 QString::fromLatin1("%1 (%2);;%3 (*.*)")
1718 .arg(format->description(),
1719 QLatin1String("*.") + format->fileExtensions().join(QString::fromLatin1(" *.")),
1720 tr("All files")) );
1721 if (filename.isEmpty() || filename.isNull())
1722 return;
1723
1724 settings.setValue(QString::fromLatin1("importFileDirectory"), QFileInfo(filename).canonicalPath());
1725
1726 exportTo(filename, *format);
1727 }
1728
1729
printClicked(int task)1730 void MapEditorController::printClicked(int task)
1731 {
1732 #ifdef QT_PRINTSUPPORT_LIB
1733 if (!print_dock_widget)
1734 {
1735 print_dock_widget = new EditorDockWidget(QString{}, nullptr, this, window);
1736 print_dock_widget->setAllowedAreas(Qt::NoDockWidgetArea);
1737 print_dock_widget->toggleViewAction()->setVisible(false);
1738 print_widget = new PrintWidget(map, window, main_view, this, print_dock_widget);
1739 connect(print_dock_widget, &QDockWidget::visibilityChanged, this, [this]() {
1740 print_widget->setActive(print_dock_widget->isVisible());
1741 } );
1742 connect(print_widget, &PrintWidget::closeClicked, print_dock_widget, &QWidget::close);
1743 connect(print_widget, &PrintWidget::finished, print_dock_widget, &QWidget::close);
1744 connect(print_widget, &PrintWidget::taskChanged, print_dock_widget, &QWidget::setWindowTitle);
1745 print_dock_widget->setWidget(print_widget);
1746 print_dock_widget->setObjectName(QString::fromLatin1("Print dock widget"));
1747 addFloatingDockWidget(print_dock_widget);
1748 }
1749
1750 print_widget->setActive(true); // Make sure to save the state before setting the task.
1751 print_widget->setTask((PrintWidget::TaskFlags)task);
1752 print_dock_widget->show();
1753 print_dock_widget->raise();
1754 #else
1755 Q_UNUSED(task)
1756 QMessageBox::warning(window, tr("Error"), tr("Print / Export is not available in this program version!"));
1757 #endif
1758 }
1759
undo()1760 void MapEditorController::undo()
1761 {
1762 doUndo(false);
1763 }
1764
redo()1765 void MapEditorController::redo()
1766 {
1767 doUndo(true);
1768 }
1769
doUndo(bool redo)1770 void MapEditorController::doUndo(bool redo)
1771 {
1772 if ((!redo && !map->undoManager().canUndo()) ||
1773 (redo && !map->undoManager().canRedo()))
1774 {
1775 // This should not happen as the action should be deactivated in this case!
1776 QMessageBox::critical(window, tr("Error"), tr("No undo steps available."));
1777 return;
1778 }
1779
1780 if (redo)
1781 map->undoManager().redo(window);
1782 else
1783 map->undoManager().undo(window);
1784 }
1785
cut()1786 void MapEditorController::cut()
1787 {
1788 copy();
1789 //: Past tense. Displayed when an Edit > Cut operation is completed.
1790 window->showStatusBarMessage(tr("Cut %n object(s)", nullptr, map->getNumSelectedObjects()), 2000);
1791 map->deleteSelectedObjects();
1792 }
1793
copy()1794 void MapEditorController::copy()
1795 {
1796 if (map->getNumSelectedObjects() == 0)
1797 return;
1798
1799 // Create map containing required objects and their symbol and color dependencies
1800 Map copy_map;
1801 copy_map.setScaleDenominator(map->getScaleDenominator());
1802
1803 std::vector<bool> symbol_filter;
1804 symbol_filter.assign(map->getNumSymbols(), false);
1805 for (const auto* object : map->selectedObjects())
1806 {
1807 int symbol_index = map->findSymbolIndex(object->getSymbol());
1808 if (symbol_index >= 0)
1809 symbol_filter[symbol_index] = true;
1810 }
1811
1812 // Copy all colors. This improves preservation of relative order during paste.
1813 copy_map.importMap(*map, Map::ColorImport);
1814
1815 // Export symbols and colors into copy_map
1816 auto symbol_map = copy_map.importMap(*map, Map::MinimalSymbolImport, &symbol_filter, -1, true);
1817
1818 // Duplicate all selected objects into copy map
1819 for (const auto* object : map->selectedObjects())
1820 {
1821 auto* new_object = object->duplicate();
1822 if (symbol_map.contains(new_object->getSymbol()))
1823 new_object->setSymbol(symbol_map.value(new_object->getSymbol()), true);
1824
1825 copy_map.addObject(new_object);
1826 }
1827
1828 // Save map to memory
1829 QBuffer buffer;
1830 if (!copy_map.exportToIODevice(buffer))
1831 {
1832 QMessageBox::warning(nullptr, tr("Error"), tr("An internal error occurred, sorry!"));
1833 return;
1834 }
1835
1836 // Put buffer into clipboard
1837 auto* mime_data = new QMimeData();
1838 mime_data->setData(MimeType::OpenOrienteeringObjects(), buffer.data());
1839 QApplication::clipboard()->setMimeData(mime_data);
1840
1841 // Show message
1842 window->showStatusBarMessage(tr("Copied %n object(s)", nullptr, map->getNumSelectedObjects()), 2000);
1843 }
1844
1845
paste()1846 void MapEditorController::paste()
1847 {
1848 if (editing_in_progress)
1849 return;
1850 if (!QApplication::clipboard()->mimeData()->hasFormat(MimeType::OpenOrienteeringObjects()))
1851 {
1852 QMessageBox::warning(nullptr, tr("Error"), tr("There are no objects in clipboard which could be pasted!"));
1853 return;
1854 }
1855
1856 // Get buffer from clipboard
1857 QByteArray byte_array = QApplication::clipboard()->mimeData()->data(MimeType::OpenOrienteeringObjects());
1858 QBuffer buffer(&byte_array);
1859 buffer.open(QIODevice::ReadOnly);
1860
1861 // Create map from buffer
1862 Map paste_map;
1863 if (!paste_map.importFromIODevice(buffer))
1864 {
1865 QMessageBox::warning(nullptr, tr("Error"), tr("An internal error occurred, sorry!"));
1866 return;
1867 }
1868
1869 // Move objects in paste_map so their bounding box center is at this map's viewport center.
1870 // This makes the pasted objects appear at the center of the viewport.
1871 QRectF paste_extent = paste_map.calculateExtent(true, false, nullptr);
1872 auto offset = main_view->center() - paste_extent.center();
1873
1874 MapPart* part = paste_map.getCurrentPart();
1875 for (int i = 0; i < part->getNumObjects(); ++i)
1876 part->getObject(i)->move(offset);
1877
1878 // Import pasted map. Do not blindly import all colors.
1879 importMap(paste_map, Map::MinimalObjectImport, window);
1880
1881 // Show message
1882 window->showStatusBarMessage(tr("Pasted %n object(s)", nullptr, paste_map.getNumObjects()), 2000);
1883 }
1884
1885
clearUndoRedoHistory()1886 void MapEditorController::clearUndoRedoHistory()
1887 {
1888 map->undoManager().clear();
1889 map->setOtherDirty();
1890 }
1891
spotColorPresenceChanged(bool has_spot_colors)1892 void MapEditorController::spotColorPresenceChanged(bool has_spot_colors)
1893 {
1894 if (overprinting_simulation_act)
1895 {
1896 if (has_spot_colors)
1897 {
1898 overprinting_simulation_act->setEnabled(true);
1899 }
1900 else
1901 {
1902 overprinting_simulation_act->setChecked(false);
1903 overprinting_simulation_act->setEnabled(false);
1904 }
1905 }
1906 }
1907
showGrid()1908 void MapEditorController::showGrid()
1909 {
1910 main_view->setGridVisible(show_grid_act->isChecked());
1911 }
1912
configureGrid()1913 void MapEditorController::configureGrid()
1914 {
1915 ConfigureGridDialog dialog(window, *map, show_grid_act->isChecked());
1916 dialog.setWindowModality(Qt::WindowModal);
1917 if (dialog.exec() == QDialog::Accepted)
1918 {
1919 map->setGrid(dialog.resultGrid());
1920 if (dialog.gridVisible() != show_grid_act->isChecked())
1921 show_grid_act->trigger();
1922 }
1923 }
1924
pan()1925 void MapEditorController::pan()
1926 {
1927 setTool(new PanTool(this, pan_act));
1928 }
1929
moveToGpsPos()1930 void MapEditorController::moveToGpsPos()
1931 {
1932 if (!gps_display->hasValidPosition())
1933 return;
1934 auto cur_gps_pos = gps_display->getLatestGPSCoord();
1935 main_view->setCenter({ cur_gps_pos.x(), cur_gps_pos.y() });
1936 gps_display->startBlinking(3);
1937 }
1938
zoomIn()1939 void MapEditorController::zoomIn()
1940 {
1941 main_view->zoomSteps(1);
1942 }
zoomOut()1943 void MapEditorController::zoomOut()
1944 {
1945 main_view->zoomSteps(-1);
1946 }
setCustomZoomFactorClicked()1947 void MapEditorController::setCustomZoomFactorClicked()
1948 {
1949 bool ok;
1950 double factor = QInputDialog::getDouble(window, tr("Set custom zoom factor"), tr("Zoom factor:"), main_view->getZoom(), MapView::zoom_out_limit, MapView::zoom_in_limit, 3, &ok);
1951 if (!ok || factor == main_view->getZoom())
1952 return;
1953
1954 main_view->setZoom(factor);
1955 }
1956
hatchAreas(bool checked)1957 void MapEditorController::hatchAreas(bool checked)
1958 {
1959 map->setAreaHatchingEnabled(checked);
1960 // Update all areas
1961 map->applyOnMatchingObjects(&Object::forceUpdate, ObjectOp::ContainsSymbolType{Symbol::Area});
1962 }
1963
baselineView(bool checked)1964 void MapEditorController::baselineView(bool checked)
1965 {
1966 map->setBaselineViewEnabled(checked);
1967 map->updateAllObjects();
1968 }
1969
hideAllTemplates(bool checked)1970 void MapEditorController::hideAllTemplates(bool checked)
1971 {
1972 hide_all_templates_act->setChecked(checked);
1973 main_view->setAllTemplatesHidden(checked);
1974 }
1975
overprintingSimulation(bool checked)1976 void MapEditorController::overprintingSimulation(bool checked)
1977 {
1978 if (checked && !map->hasSpotColors())
1979 {
1980 checked = false;
1981 }
1982 main_view->setOverprintingSimulationEnabled(checked);
1983 }
1984
coordsDisplayChanged()1985 void MapEditorController::coordsDisplayChanged()
1986 {
1987 if (geographic_coordinates_dms_act->isChecked())
1988 map_widget->setCoordsDisplay(MapWidget::GEOGRAPHIC_COORDS_DMS);
1989 else if (geographic_coordinates_act->isChecked())
1990 map_widget->setCoordsDisplay(MapWidget::GEOGRAPHIC_COORDS);
1991 else if (projected_coordinates_act->isChecked())
1992 map_widget->setCoordsDisplay(MapWidget::PROJECTED_COORDS);
1993 else
1994 map_widget->setCoordsDisplay(MapWidget::MAP_COORDS);
1995 }
1996
copyDisplayedCoords()1997 void MapEditorController::copyDisplayedCoords()
1998 {
1999 QApplication::clipboard()->setText(statusbar_cursorpos_label->text());
2000 }
2001
projectionChanged()2002 void MapEditorController::projectionChanged()
2003 {
2004 const Georeferencing& geo(map->getGeoreferencing());
2005
2006 projected_coordinates_act->setText(geo.getProjectedCoordinatesName());
2007
2008 bool enable_geographic = !geo.isLocal();
2009 geographic_coordinates_act->setEnabled(enable_geographic);
2010 geographic_coordinates_dms_act->setEnabled(enable_geographic);
2011 if (!enable_geographic &&
2012 (map_widget->getCoordsDisplay() == MapWidget::GEOGRAPHIC_COORDS ||
2013 map_widget->getCoordsDisplay() == MapWidget::GEOGRAPHIC_COORDS_DMS))
2014 {
2015 map_widget->setCoordsDisplay(MapWidget::MAP_COORDS);
2016 map_coordinates_act->setChecked(true);
2017 }
2018 }
2019
showSymbolWindow(bool show)2020 void MapEditorController::showSymbolWindow(bool show)
2021 {
2022 if (!symbol_dock_widget)
2023 {
2024 symbol_dock_widget = new EditorDockWidget(tr("Symbols"), symbol_window_act, this, window);
2025 createSymbolWidget();
2026 symbol_dock_widget->setObjectName(QString::fromLatin1("Symbol dock widget"));
2027 if (!window->restoreDockWidget(symbol_dock_widget))
2028 window->addDockWidget(Qt::RightDockWidgetArea, symbol_dock_widget, Qt::Vertical);
2029 }
2030
2031 symbol_dock_widget->setVisible(show);
2032 }
2033
createColorWindow()2034 void MapEditorController::createColorWindow()
2035 {
2036 Q_ASSERT(!color_dock_widget);
2037
2038 color_dock_widget = new EditorDockWidget(tr("Colors"), color_window_act, this, window);
2039 color_dock_widget->setWidget(new ColorListWidget(map, window, color_dock_widget));
2040 color_dock_widget->widget()->setEnabled(!editing_in_progress);
2041 color_dock_widget->setObjectName(QString::fromLatin1("Color dock widget"));
2042 if (!window->restoreDockWidget(color_dock_widget))
2043 window->addDockWidget(Qt::LeftDockWidgetArea, color_dock_widget, Qt::Vertical);
2044 color_dock_widget->setVisible(false);
2045 }
2046
showColorWindow(bool show)2047 void MapEditorController::showColorWindow(bool show)
2048 {
2049 if (!color_dock_widget)
2050 createColorWindow();
2051
2052 color_dock_widget->setVisible(show);
2053 }
2054
2055
symbolSetIdClicked()2056 void MapEditorController::symbolSetIdClicked()
2057 {
2058 bool ok;
2059 auto id = QInputDialog::getText(window, tr("Symbol set ID"),
2060 tr("Edit the symbol set ID:"), QLineEdit::Normal,
2061 map->symbolSetId(), &ok);
2062 if (ok)
2063 map->setSymbolSetId(id);
2064 }
2065
2066
loadSymbolsFromClicked()2067 void MapEditorController::loadSymbolsFromClicked()
2068 {
2069 SymbolReplacement(*map).withSymbolSetFileDialog(window);
2070 }
2071
2072
loadCrtClicked()2073 void MapEditorController::loadCrtClicked()
2074 {
2075 SymbolReplacement(*map, *map).withCrtFileDialog(window);
2076 }
2077
2078
loadColorsFromClicked()2079 void MapEditorController::loadColorsFromClicked()
2080 {
2081 // TODO
2082 }
2083
scaleAllSymbolsClicked()2084 void MapEditorController::scaleAllSymbolsClicked()
2085 {
2086 bool ok;
2087 double percent = QInputDialog::getDouble(window, tr("Scale all symbols"), tr("Scale to percentage:"), 100, 0, 999999, 6, &ok);
2088 if (!ok || percent == 100)
2089 return;
2090
2091 map->scaleAllSymbols(percent / 100.0);
2092 }
2093
scaleMapClicked()2094 void MapEditorController::scaleMapClicked()
2095 {
2096 ScaleMapDialog dialog(window, map);
2097 dialog.setWindowModality(Qt::WindowModal);
2098 dialog.exec();
2099 }
2100
rotateMapClicked()2101 void MapEditorController::rotateMapClicked()
2102 {
2103 RotateMapDialog dialog(window, map);
2104 dialog.setWindowModality(Qt::WindowModal);
2105 dialog.exec();
2106 }
2107
mapNotesClicked()2108 void MapEditorController::mapNotesClicked()
2109 {
2110 QDialog dialog(window, Qt::WindowSystemMenuHint | Qt::WindowTitleHint);
2111 dialog.setWindowTitle(tr("Map notes"));
2112 dialog.setWindowModality(Qt::WindowModal);
2113
2114 auto* text_edit = new QTextEdit();
2115 text_edit->setPlainText(map->getMapNotes());
2116 QPushButton* cancel_button = new QPushButton(tr("Cancel"));
2117 QPushButton* ok_button = new QPushButton(QIcon(QString::fromLatin1(":/images/arrow-right.png")), tr("OK"));
2118 ok_button->setDefault(true);
2119
2120 auto* buttons_layout = new QHBoxLayout();
2121 buttons_layout->addWidget(cancel_button);
2122 buttons_layout->addStretch(1);
2123 buttons_layout->addWidget(ok_button);
2124
2125 auto* layout = new QVBoxLayout();
2126 layout->addWidget(text_edit);
2127 layout->addLayout(buttons_layout);
2128 dialog.setLayout(layout);
2129
2130 connect(cancel_button, &QAbstractButton::clicked, &dialog, &QDialog::reject);
2131 connect(ok_button, &QAbstractButton::clicked, &dialog, &QDialog::accept);
2132
2133 if (dialog.exec() == QDialog::Accepted)
2134 {
2135 if (text_edit->toPlainText() != map->getMapNotes())
2136 {
2137 map->setMapNotes(text_edit->toPlainText());
2138 map->setHasUnsavedChanges(true);
2139 }
2140 }
2141 }
2142
createTemplateWindow()2143 void MapEditorController::createTemplateWindow()
2144 {
2145 Q_ASSERT(!template_dock_widget);
2146
2147 template_list_widget = new TemplateListWidget(map, main_view, this);
2148 connect(hide_all_templates_act, &QAction::toggled, template_list_widget, &TemplateListWidget::setAllTemplatesHidden);
2149
2150 if (isInMobileMode())
2151 {
2152 template_dock_widget = createDockWidgetSubstitute(window, template_list_widget);
2153 connect(template_list_widget, &TemplateListWidget::closeClicked, this, [this]() { showTemplateWindow(false); });
2154 }
2155 else
2156 {
2157 auto* dock_widget = new EditorDockWidget(tr("Templates"), template_window_act, this, window);
2158 dock_widget->setWidget(template_list_widget);
2159 dock_widget->setObjectName(QString::fromLatin1("Templates dock widget"));
2160 if (!window->restoreDockWidget(dock_widget))
2161 window->addDockWidget(Qt::RightDockWidgetArea, dock_widget, Qt::Vertical);
2162 dock_widget->setVisible(false);
2163
2164 template_dock_widget = dock_widget;
2165 }
2166 }
2167
showTemplateWindow(bool show)2168 void MapEditorController::showTemplateWindow(bool show)
2169 {
2170 if (!template_dock_widget)
2171 createTemplateWindow();
2172
2173 template_window_act->setChecked(show);
2174 template_dock_widget->setVisible(show);
2175 }
2176
openTemplateClicked()2177 void MapEditorController::openTemplateClicked()
2178 {
2179 auto new_template = TemplateListWidget::showOpenTemplateDialog(window, this);
2180 if (new_template)
2181 {
2182 hideAllTemplates(false);
2183 showTemplateWindow(true);
2184
2185 // FIXME: this should be done through the core map, not through the UI
2186 template_list_widget->addTemplateAt(new_template.release(), -1);
2187 }
2188 }
2189
reopenTemplateClicked()2190 void MapEditorController::reopenTemplateClicked()
2191 {
2192 hideAllTemplates(false);
2193
2194 QString map_directory = window->currentPath();
2195 if (!map_directory.isEmpty())
2196 map_directory = QFileInfo(map_directory).canonicalPath();
2197 auto* dialog = new ReopenTemplateDialog(window, map, map_directory);
2198 dialog->setWindowModality(Qt::WindowModal);
2199 dialog->exec();
2200 delete dialog;
2201 }
2202
templateAvailabilityChanged()2203 void MapEditorController::templateAvailabilityChanged()
2204 {
2205 // Nothing
2206 }
2207
closedTemplateAvailabilityChanged()2208 void MapEditorController::closedTemplateAvailabilityChanged()
2209 {
2210 if (reopen_template_act)
2211 reopen_template_act->setEnabled(map->getNumClosedTemplates() > 0);
2212 }
2213
createTagEditor()2214 void MapEditorController::createTagEditor()
2215 {
2216 Q_ASSERT(!tags_dock_widget);
2217
2218 auto* tags_widget = new TagsWidget(map, main_view, this);
2219 tags_dock_widget = new EditorDockWidget(tr("Tag Editor"), tags_window_act, this, window);
2220 tags_dock_widget->setWidget(tags_widget);
2221 tags_dock_widget->setObjectName(QString::fromLatin1("Tag editor dock widget"));
2222 if (!window->restoreDockWidget(tags_dock_widget))
2223 window->addDockWidget(Qt::RightDockWidgetArea, tags_dock_widget, Qt::Vertical);
2224 tags_dock_widget->setVisible(false);
2225 }
2226
showTagsWindow(bool show)2227 void MapEditorController::showTagsWindow(bool show)
2228 {
2229 if (!tags_dock_widget)
2230 createTagEditor();
2231
2232 tags_window_act->setChecked(show);
2233 tags_dock_widget->setVisible(show);
2234 }
2235
2236
editGeoreferencing()2237 void MapEditorController::editGeoreferencing()
2238 {
2239 if (georeferencing_dialog.isNull())
2240 {
2241 auto* dialog = new GeoreferencingDialog(this);
2242 georeferencing_dialog.reset(dialog);
2243 connect(dialog, &QDialog::finished, this, &MapEditorController::georeferencingDialogFinished);
2244 }
2245 georeferencing_dialog->exec();
2246 }
2247
georeferencingDialogFinished()2248 void MapEditorController::georeferencingDialogFinished()
2249 {
2250 georeferencing_dialog.take()->deleteLater();
2251 map->updateAllMapWidgets();
2252
2253 bool gps_display_possible = map->getGeoreferencing().isValid() && ! map->getGeoreferencing().isLocal();
2254 if (!gps_display_possible)
2255 {
2256 gps_display_action->setChecked(false);
2257 if (gps_display)
2258 enableGPSDisplay(false);
2259 }
2260 gps_display_action->setEnabled(gps_display_possible);
2261 }
2262
selectedSymbolsChanged()2263 void MapEditorController::selectedSymbolsChanged()
2264 {
2265 Symbol* symbol = symbol_widget->getSingleSelectedSymbol();
2266
2267 if (mobile_mode)
2268 {
2269 auto* symbol_button = bottom_action_bar->getButtonForAction(mobile_symbol_selector_action);
2270
2271 // (Re-)create the mobile_symbol_selector_action icon
2272 QSize icon_size = bottom_action_bar->getIconSize(2, 2);
2273 QPixmap pixmap(icon_size);
2274 pixmap.fill(Qt::white);
2275 if (symbol_widget->selectedSymbolsCount() != 1)
2276 {
2277 QFont font(window->font());
2278 font.setPixelSize(icon_size.height() / 5);
2279 QPainter painter(&pixmap);
2280 painter.setFont(font);
2281 QString text = (symbol_widget->selectedSymbolsCount() == 0) ?
2282 //: Keep it short. Should not be much longer per line than the longest word in the original.
2283 tr("No\nsymbol\nselected") :
2284 //: Keep it short. Should not be much longer per line than the longest word in the original.
2285 tr("Multiple\nsymbols\nselected");
2286 painter.drawText(pixmap.rect(), Qt::AlignCenter, text);
2287
2288 symbol_button->setMenu(nullptr);
2289 }
2290 else //if (symbol_widget->getNumSelectedSymbols() == 1)
2291 {
2292 auto image = symbol->getCustomIcon();
2293 if (image.isNull())
2294 image = symbol->createIcon(*map, qMin(icon_size.width(), icon_size.height()));
2295 else
2296 image = image.scaled(icon_size.width(), icon_size.height(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
2297 if (symbol->isHidden() || symbol->isProtected())
2298 {
2299 QPainter p(&image);
2300 if (symbol->isHidden())
2301 HiddenSymbolDecorator(icon_size.width()).draw(p);
2302 if (symbol->isProtected())
2303 ProtectedSymbolDecorator(icon_size.width()).draw(p);
2304 }
2305 pixmap = QPixmap::fromImage(image);
2306
2307 symbol_button->setMenu(mobile_symbol_button_menu);
2308 const auto actions = mobile_symbol_button_menu->actions();
2309 int i = 0;
2310 actions[i]->setText(symbol->getNumberAsString() + QLatin1Char(' ') + symbol->getPlainTextName());
2311 actions[++i]->setVisible(!symbol->getDescription().isEmpty());
2312 ++i; // separator
2313 actions[++i]->setChecked(symbol->isHidden());
2314 actions[++i]->setChecked(symbol->isProtected());
2315 }
2316 mobile_symbol_selector_action->setIcon(QIcon(pixmap));
2317 }
2318
2319 // FIXME: Postpone switch of active symbol while editing is progress
2320 if (active_symbol != symbol)
2321 {
2322 active_symbol = symbol;
2323
2324 if (symbol && !symbol->isHidden() && !symbol->isProtected() && current_tool)
2325 {
2326 // Auto-switch to a draw tool when selecting a symbol under certain conditions
2327 if (current_tool->toolType() == MapEditorTool::Pan || current_tool->toolType() == MapEditorTool::Scribble
2328 || ((current_tool->toolType() == MapEditorTool::EditLine || current_tool->toolType() == MapEditorTool::EditPoint) && map->getNumSelectedObjects() == 0))
2329 {
2330 current_tool->switchToDefaultDrawTool(active_symbol);
2331 }
2332 }
2333
2334 if (symbol)
2335 window->showStatusBarMessage(symbol->getNumberAsString() + QLatin1Char(' ') + symbol->getPlainTextName(), 1000);
2336 }
2337
2338 // Even when the symbol (pointer) hasn't changed,
2339 // the symbol's visibility may constitute a change.
2340 emit activeSymbolChanged(active_symbol);
2341
2342 updateSymbolDependentActions();
2343 updateSymbolAndObjectDependentActions();
2344 }
2345
objectSelectionChanged()2346 void MapEditorController::objectSelectionChanged()
2347 {
2348 if (mode != MapEditor)
2349 return;
2350
2351 updateObjectDependentActions();
2352
2353 // Automatic symbol selection of selected objects
2354 if (symbol_widget && !editing_in_progress)
2355 {
2356 bool uniform_symbol_selected = true;
2357 const Symbol* uniform_symbol = nullptr;
2358 for (const auto* object : map->selectedObjects())
2359 {
2360 const auto* symbol = object->getSymbol();
2361 if (!uniform_symbol)
2362 {
2363 uniform_symbol = symbol;
2364 }
2365 else if (uniform_symbol != symbol)
2366 {
2367 uniform_symbol_selected = false;
2368 break;
2369 }
2370 }
2371 if (uniform_symbol_selected && Settings::getInstance().getSettingCached(Settings::MapEditor_ChangeSymbolWhenSelecting).toBool())
2372 symbol_widget->selectSingleSymbol(uniform_symbol);
2373 }
2374
2375 updateSymbolAndObjectDependentActions();
2376 }
2377
updateSymbolDependentActions()2378 void MapEditorController::updateSymbolDependentActions()
2379 {
2380 const Symbol* symbol = activeSymbol();
2381 const Symbol::Type type = (symbol && !editing_in_progress) ? symbol->getType() : Symbol::NoSymbol;
2382
2383 updateDrawPointGPSAvailability();
2384 draw_point_act->setEnabled(type == Symbol::Point && !symbol->isHidden());
2385 draw_point_act->setStatusTip(tr("Place point objects on the map.") + (draw_point_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select a point symbol to be able to use this tool."))));
2386 draw_path_act->setEnabled((type == Symbol::Line || type == Symbol::Area || type == Symbol::Combined) && !symbol->isHidden());
2387 draw_path_act->setStatusTip(tr("Draw polygonal and curved lines.") + (draw_path_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select a line, area or combined symbol to be able to use this tool."))));
2388 draw_circle_act->setEnabled(draw_path_act->isEnabled());
2389 draw_circle_act->setStatusTip(tr("Draw circles and ellipses.") + (draw_circle_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select a line, area or combined symbol to be able to use this tool."))));
2390 draw_rectangle_act->setEnabled(draw_path_act->isEnabled());
2391 draw_rectangle_act->setStatusTip(tr("Draw rectangles.") + (draw_rectangle_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select a line, area or combined symbol to be able to use this tool."))));
2392 draw_freehand_act->setEnabled(draw_path_act->isEnabled());
2393 draw_freehand_act->setStatusTip(tr("Draw paths free-handedly.") + (draw_freehand_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select a line, area or combined symbol to be able to use this tool."))));
2394 draw_fill_act->setEnabled(draw_path_act->isEnabled());
2395 draw_fill_act->setStatusTip(tr("Fill bounded areas.") + (draw_fill_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select a line, area or combined symbol to be able to use this tool."))));
2396 draw_text_act->setEnabled(type == Symbol::Text && !symbol->isHidden());
2397 draw_text_act->setStatusTip(tr("Write text on the map.") + (draw_text_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select a text symbol to be able to use this tool."))));
2398 }
2399
updateObjectDependentActions()2400 void MapEditorController::updateObjectDependentActions()
2401 {
2402 bool have_multiple_parts = map->getNumParts() > 1;
2403 bool have_selection = map->getNumSelectedObjects() > 0 && !editing_in_progress;
2404 bool single_object_selected = map->getNumSelectedObjects() == 1 && !editing_in_progress;
2405 bool have_line = false;
2406 bool have_area = false;
2407 bool have_area_with_holes = false;
2408 bool have_rotatable_pattern = false;
2409 bool have_rotatable_object = false;
2410 int num_selected_paths = 0;
2411 bool first_selected_is_path = have_selection && map->getFirstSelectedObject()->getType() == Object::Path;
2412 bool uniform_symbol_selected = true;
2413 const Symbol* uniform_symbol = nullptr;
2414 const Symbol* first_selected_symbol= have_selection ? map->getFirstSelectedObject()->getSymbol() : nullptr;
2415 std::vector< bool > symbols_in_selection(map->getNumSymbols(), false);
2416
2417 if (!editing_in_progress)
2418 {
2419 for (const auto* object : map->selectedObjects())
2420 {
2421 const auto* symbol = object->getSymbol();
2422 int symbol_index = map->findSymbolIndex(symbol);
2423 if (symbol_index >= 0 && symbol_index < (int)symbols_in_selection.size())
2424 symbols_in_selection[symbol_index] = true;
2425
2426 if (uniform_symbol_selected)
2427 {
2428 if (!uniform_symbol)
2429 {
2430 uniform_symbol = symbol;
2431 }
2432 else if (uniform_symbol != symbol)
2433 {
2434 uniform_symbol = nullptr;
2435 uniform_symbol_selected = false;
2436 }
2437 }
2438
2439 have_rotatable_object |= symbol->isRotatable();
2440
2441 if (Symbol::areTypesCompatible(symbol->getType(), Symbol::Area))
2442 {
2443 ++num_selected_paths;
2444
2445 if (symbol->getType() == Symbol::Area)
2446 {
2447 have_rotatable_pattern |= symbol->asArea()->hasRotatableFillPattern();
2448 }
2449
2450 int const contained_types = symbol->getContainedTypes();
2451
2452 if (contained_types & Symbol::Line)
2453 {
2454 have_line = true;
2455 }
2456
2457 if (contained_types & Symbol::Area)
2458 {
2459 have_area = true;
2460 have_area_with_holes |= object->asPath()->parts().size() > 1;
2461 }
2462 }
2463 }
2464
2465 if (have_area && !have_rotatable_pattern)
2466 {
2467 map->determineSymbolUseClosure(symbols_in_selection);
2468 for (std::size_t i = 0, end = symbols_in_selection.size(); i < end; ++i)
2469 {
2470 if (symbols_in_selection[i])
2471 {
2472 Symbol* symbol = map->getSymbol(i);
2473 if (symbol->getType() == Symbol::Area)
2474 {
2475 have_rotatable_pattern = symbol->asArea()->hasRotatableFillPattern();
2476 break;
2477 }
2478 }
2479 }
2480 }
2481 }
2482
2483 // have_selection
2484 cut_act->setEnabled(have_selection);
2485 copy_act->setEnabled(have_selection);
2486 delete_act->setEnabled(have_selection);
2487 delete_act->setStatusTip(tr("Deletes the selected objects.") + (delete_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select at least one object to activate this tool."))));
2488 duplicate_act->setEnabled(have_selection);
2489 duplicate_act->setStatusTip(tr("Duplicate the selected objects.") + (duplicate_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select at least one object to activate this tool."))));
2490 rotate_act->setEnabled(have_selection);
2491 rotate_act->setStatusTip(tr("Rotate the selected objects.") + (rotate_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select at least one object to activate this tool."))));
2492 scale_act->setEnabled(have_selection);
2493 scale_act->setStatusTip(tr("Scale the selected objects.") + (scale_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select at least one object to activate this tool."))));
2494 mappart_move_menu->setEnabled(have_selection && have_multiple_parts);
2495
2496 // have_rotatable_pattern || have_rotatable_point
2497 rotate_pattern_act->setEnabled(have_rotatable_pattern || have_rotatable_object);
2498 rotate_pattern_act->setStatusTip(tr("Set the direction of area fill patterns or point objects.") + (rotate_pattern_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select an area object with rotatable fill pattern or a rotatable point object to activate this tool."))));
2499
2500 // have_line
2501 switch_dashes_act->setEnabled(have_line);
2502 switch_dashes_act->setStatusTip(tr("Switch the direction of symbols on line objects.") + (switch_dashes_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select at least one line object to activate this tool."))));
2503 connect_paths_act->setEnabled(have_line);
2504 connect_paths_act->setStatusTip(tr("Connect endpoints of paths which are close together.") + (connect_paths_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select at least one line object to activate this tool."))));
2505
2506 // have_are || have_line
2507 cut_tool_act->setEnabled(have_area || have_line);
2508 cut_tool_act->setStatusTip(tr("Cut the selected objects into smaller parts.") + (cut_tool_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select at least one line or area object to activate this tool."))));
2509 convert_to_curves_act->setEnabled(have_area || have_line);
2510 convert_to_curves_act->setStatusTip(tr("Turn paths made of straight segments into smooth bezier splines.") + (convert_to_curves_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select a path object to activate this tool."))));
2511 simplify_path_act->setEnabled(have_area || have_line);
2512 simplify_path_act->setStatusTip(tr("Reduce the number of points in path objects while trying to retain their shape.") + (simplify_path_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select a path object to activate this tool."))));
2513
2514 // cut_hole_enabled
2515 const bool cut_hole_enabled = single_object_selected && have_area;
2516 cut_hole_act->setEnabled(cut_hole_enabled);
2517 cut_hole_act->setStatusTip(tr("Cut a hole into the selected area object.") + (cut_hole_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select a single area object to activate this tool."))));
2518 cut_hole_circle_act->setEnabled(cut_hole_enabled);
2519 cut_hole_circle_act->setStatusTip(cut_hole_act->statusTip());
2520 cut_hole_rectangle_act->setEnabled(cut_hole_enabled);
2521 cut_hole_rectangle_act->setStatusTip(cut_hole_act->statusTip());
2522 cut_hole_menu->setEnabled(cut_hole_enabled);
2523
2524 // boolean_prerequisite [&& x]
2525 bool const boolean_prerequisite = first_selected_is_path && num_selected_paths >= 2;
2526 QString const extra_status_tip = QLatin1Char(' ') +
2527 ( boolean_prerequisite
2528 ? tr("Resulting symbol: %1 %2.").arg(first_selected_symbol->getNumberAsString(), first_selected_symbol->getPlainTextName())
2529 : tr("Select at least two area or path objects activate this tool.") );
2530 boolean_union_act->setEnabled(boolean_prerequisite);
2531 boolean_union_act->setStatusTip(tr("Unify overlapping objects.") + extra_status_tip);
2532 boolean_intersection_act->setEnabled(boolean_prerequisite);
2533 boolean_intersection_act->setStatusTip(tr("Remove all parts which are not overlaps with the first selected object.") + extra_status_tip);
2534 boolean_difference_act->setEnabled(boolean_prerequisite);
2535 boolean_difference_act->setStatusTip(tr("Remove overlapped parts of the first selected object.") + (boolean_prerequisite ? QString{} : extra_status_tip));
2536 boolean_xor_act->setEnabled(boolean_prerequisite);
2537 boolean_xor_act->setStatusTip(tr("Remove all parts which overlap the first selected object.") + extra_status_tip);
2538
2539 // special
2540 boolean_merge_holes_act->setEnabled(single_object_selected && have_area_with_holes);
2541 boolean_merge_holes_act->setStatusTip(tr("Merge area holes together, or merge holes with the object boundary to cut out this part.") + (boolean_merge_holes_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select one area object with holes to activate this tool."))));
2542
2543 // cutout_enabled
2544 bool const cutout_enabled = single_object_selected && (have_area || have_line) && !have_area_with_holes && (*(map->selectedObjectsBegin()))->asPath()->parts().front().isClosed();
2545 cutout_physical_act->setEnabled(cutout_enabled);
2546 cutout_physical_act->setStatusTip(tr("Create a cutout of some objects or the whole map.") + (cutout_physical_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select a closed path object as cutout shape to activate this tool."))));
2547 cutaway_physical_act->setEnabled(cutout_enabled);
2548 cutaway_physical_act->setStatusTip(tr("Cut away some objects or everything in a limited area.") + (cutaway_physical_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select a closed path object as cutout shape to activate this tool."))));
2549 }
2550
updateSymbolAndObjectDependentActions()2551 void MapEditorController::updateSymbolAndObjectDependentActions()
2552 {
2553 const Symbol* single_symbol = activeSymbol();
2554 bool single_symbol_compatible = false;
2555 bool single_symbol_different = false;
2556 if (!editing_in_progress)
2557 {
2558 map->getSelectionToSymbolCompatibility(single_symbol, single_symbol_compatible, single_symbol_different);
2559 }
2560
2561 switch_symbol_act->setEnabled(single_symbol_compatible && single_symbol_different);
2562 switch_symbol_act->setStatusTip(tr("Switches the symbol of the selected objects to the selected symbol.") + (switch_symbol_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select at least one object and a fitting, different symbol to activate this tool."))));
2563 fill_border_act->setEnabled(single_symbol_compatible && single_symbol_different);
2564 fill_border_act->setStatusTip(tr("Fill the selected lines or create a border for the selected areas.") + (fill_border_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select at least one object and a fitting, different symbol to activate this tool."))));
2565 distribute_points_act->setEnabled(single_symbol && single_symbol->getType() == Symbol::Point
2566 && containsPathObject(map->selectedObjects()));
2567 distribute_points_act->setStatusTip(tr("Places evenly spaced point objects along an existing path object") + (distribute_points_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select at least one path object and a single point symbol to activate this tool."))));
2568 }
2569
undoStepAvailabilityChanged()2570 void MapEditorController::undoStepAvailabilityChanged()
2571 {
2572 if (mode != MapEditor)
2573 return;
2574
2575 undo_act->setEnabled(map->undoManager().canUndo());
2576 redo_act->setEnabled(map->undoManager().canRedo());
2577 clear_undo_redo_history_act->setEnabled(undo_act->isEnabled() || redo_act->isEnabled());
2578 }
2579
clipboardChanged(QClipboard::Mode mode)2580 void MapEditorController::clipboardChanged(QClipboard::Mode mode)
2581 {
2582 if (mode == QClipboard::Clipboard)
2583 updatePasteAvailability();
2584 }
2585
updatePasteAvailability()2586 void MapEditorController::updatePasteAvailability()
2587 {
2588 if (paste_act)
2589 {
2590 paste_act->setEnabled(
2591 QApplication::clipboard()->mimeData()
2592 && QApplication::clipboard()->mimeData()->hasFormat(MimeType::OpenOrienteeringObjects())
2593 && !editing_in_progress);
2594 }
2595 }
2596
showWholeMap()2597 void MapEditorController::showWholeMap()
2598 {
2599 QRectF map_extent = map->calculateExtent(true, !main_view->areAllTemplatesHidden(), main_view);
2600 map_widget->adjustViewToRect(map_extent, MapWidget::ContinuousZoom);
2601 }
2602
editToolClicked()2603 void MapEditorController::editToolClicked()
2604 {
2605 setEditTool();
2606 }
2607
editLineToolClicked()2608 void MapEditorController::editLineToolClicked()
2609 {
2610 if (!current_tool || current_tool->toolType() != MapEditorTool::EditLine)
2611 setTool(new EditLineTool(this, edit_line_tool_act));
2612 }
2613
drawPointClicked()2614 void MapEditorController::drawPointClicked()
2615 {
2616 setTool(new DrawPointTool(this, draw_point_act));
2617 }
2618
drawPathClicked()2619 void MapEditorController::drawPathClicked()
2620 {
2621 setTool(new DrawPathTool(this, draw_path_act, false, true));
2622 }
2623
drawCircleClicked()2624 void MapEditorController::drawCircleClicked()
2625 {
2626 setTool(new DrawCircleTool(this, draw_circle_act, false));
2627 }
2628
drawRectangleClicked()2629 void MapEditorController::drawRectangleClicked()
2630 {
2631 setTool(new DrawRectangleTool(this, draw_rectangle_act, false));
2632 }
2633
drawFreehandClicked()2634 void MapEditorController::drawFreehandClicked()
2635 {
2636 setTool(new DrawFreehandTool(this, draw_freehand_act, false));
2637 }
2638
drawFillClicked()2639 void MapEditorController::drawFillClicked()
2640 {
2641 setTool(new FillTool(this, draw_fill_act));
2642 }
2643
drawTextClicked()2644 void MapEditorController::drawTextClicked()
2645 {
2646 setTool(new DrawTextTool(this, draw_text_act));
2647 }
2648
deleteClicked()2649 void MapEditorController::deleteClicked()
2650 {
2651 if (editing_in_progress)
2652 return;
2653 map->deleteSelectedObjects();
2654 }
2655
duplicateClicked()2656 void MapEditorController::duplicateClicked()
2657 {
2658 Q_ASSERT(!map->selectedObjects().empty());
2659
2660 std::vector<Object*> new_objects;
2661 new_objects.reserve(map->getNumSelectedObjects());
2662
2663 for (const auto* object : map->selectedObjects())
2664 {
2665 Object* duplicate = object->duplicate();
2666 map->addObject(duplicate);
2667 new_objects.push_back(duplicate);
2668 }
2669
2670 auto* undo_step = new DeleteObjectsUndoStep(map);
2671 MapPart* part = map->getCurrentPart();
2672
2673 map->clearObjectSelection(false);
2674 for (auto* object : new_objects)
2675 {
2676 undo_step->addObject(part->findObjectIndex(object));
2677 map->addObjectToSelection(object, object == new_objects.back());
2678 }
2679
2680 map->setObjectsDirty();
2681 map->push(undo_step);
2682 setEditTool();
2683 window->showStatusBarMessage(tr("Duplicated %n object(s)", nullptr, int(new_objects.size())), 2000);
2684 }
2685
switchSymbolClicked()2686 void MapEditorController::switchSymbolClicked()
2687 {
2688 SwitchSymbolUndoStep* switch_step = nullptr;
2689 ReplaceObjectsUndoStep* replace_step = nullptr;
2690 AddObjectsUndoStep* add_step = nullptr;
2691 DeleteObjectsUndoStep* delete_step = nullptr;
2692 std::vector<Object*> old_objects;
2693 std::vector<Object*> new_objects;
2694 MapPart* part = map->getCurrentPart();
2695 Symbol* symbol = activeSymbol();
2696
2697 bool close_paths = false, split_up = false;
2698 auto contained_types = symbol->getContainedTypes();
2699 if (contained_types & Symbol::Area && !(contained_types & Symbol::Line))
2700 close_paths = true;
2701 else if (contained_types & Symbol::Line && !(contained_types & Symbol::Area))
2702 split_up = true;
2703
2704 if (close_paths)
2705 {
2706 replace_step = new ReplaceObjectsUndoStep(map);
2707 }
2708 else if (split_up)
2709 {
2710 add_step = new AddObjectsUndoStep(map);
2711 delete_step = new DeleteObjectsUndoStep(map);
2712 }
2713 else
2714 {
2715 switch_step = new SwitchSymbolUndoStep(map);
2716 }
2717
2718 for (auto* object : map->selectedObjects())
2719 {
2720 if (close_paths)
2721 {
2722 replace_step->addObject(part->findObjectIndex(object), object->duplicate());
2723 }
2724 else if (split_up)
2725 {
2726 add_step->addObject(part->findObjectIndex(object), object);
2727 old_objects.push_back(object);
2728 }
2729 else
2730 {
2731 switch_step->addObject(part->findObjectIndex(object), object->getSymbol());
2732 }
2733
2734 if (!split_up)
2735 {
2736 object->setSymbol(symbol, true);
2737 }
2738
2739 if (object->getType() == Object::Path)
2740 {
2741 PathObject* path_object = object->asPath();
2742 if (close_paths)
2743 {
2744 path_object->closeAllParts();
2745 }
2746 else if (split_up)
2747 {
2748 for (const auto& part : path_object->parts())
2749 {
2750 auto* new_object = new PathObject { part };
2751 new_object->setSymbol(symbol, true);
2752 new_objects.push_back(new_object);
2753 }
2754
2755 Q_ASSERT(!new_objects.empty());
2756 if (!new_objects.empty())
2757 {
2758 new_objects.front()->setTags(path_object->tags());
2759 }
2760 }
2761 }
2762
2763 object->update();
2764 }
2765
2766 if (split_up)
2767 {
2768 map->clearObjectSelection(false);
2769 for (auto* object : old_objects)
2770 {
2771 map->releaseObject(object);
2772 }
2773 for (auto* object : new_objects)
2774 {
2775 map->addObject(object);
2776 map->addObjectToSelection(object, false);
2777 }
2778 map->emitSelectionChanged();
2779 // Do not merge this loop into the upper one;
2780 // theoretically undo step indices could be wrong this way.
2781 for (auto* object : new_objects)
2782 {
2783 delete_step->addObject(part->findObjectIndex(object));
2784 }
2785 }
2786
2787 map->setObjectsDirty();
2788 if (close_paths)
2789 {
2790 map->push(replace_step);
2791 }
2792 else if (split_up)
2793 {
2794 auto* combined_step = new CombinedUndoStep(map);
2795 combined_step->push(add_step);
2796 combined_step->push(delete_step);
2797 map->push(combined_step);
2798 }
2799 else
2800 {
2801 map->push(switch_step);
2802 }
2803 map->emitSelectionEdited();
2804 // Also emit selectionChanged, as symbols of selected objects changed
2805 map->emitSelectionChanged();
2806 }
2807
fillBorderClicked()2808 void MapEditorController::fillBorderClicked()
2809 {
2810 Symbol* symbol = activeSymbol();
2811 std::vector<Object*> new_objects;
2812 new_objects.reserve(map->getNumSelectedObjects());
2813
2814 bool close_paths = false, split_up = false;
2815 auto contained_types = symbol->getContainedTypes();
2816 if (contained_types & Symbol::Area && !(contained_types & Symbol::Line))
2817 close_paths = true;
2818 else if (contained_types & Symbol::Line && !(contained_types & Symbol::Area))
2819 split_up = true;
2820
2821 auto* undo_step = new DeleteObjectsUndoStep(map);
2822 MapPart* part = map->getCurrentPart();
2823
2824 for (const auto* object : map->selectedObjects())
2825 {
2826 if (split_up && object->getType() == Object::Path)
2827 {
2828 const auto* path_object = object->asPath();
2829 for (const auto& part : path_object->parts())
2830 {
2831 auto* new_object = new PathObject { part };
2832 new_object->setSymbol(symbol, true);
2833 map->addObject(new_object);
2834 new_objects.push_back(new_object);
2835 }
2836 }
2837 else
2838 {
2839 Object* duplicate = object->duplicate();
2840 duplicate->setSymbol(symbol, true);
2841 if (close_paths && duplicate->getType() == Object::Path)
2842 {
2843 PathObject* path_object = duplicate->asPath();
2844 path_object->closeAllParts();
2845 }
2846 map->addObject(duplicate);
2847 new_objects.push_back(duplicate);
2848 }
2849 }
2850
2851 map->setObjectsDirty();
2852 map->clearObjectSelection(false);
2853 for (int i = 0; i < (int)new_objects.size(); ++i)
2854 {
2855 map->addObjectToSelection(new_objects[i], i == (int)new_objects.size() - 1);
2856 undo_step->addObject(part->findObjectIndex(new_objects[i]));
2857 }
2858 map->push(undo_step);
2859 }
selectObjectsClicked(bool select_exclusively)2860 void MapEditorController::selectObjectsClicked(bool select_exclusively)
2861 {
2862 bool selection_changed = false;
2863 if (select_exclusively)
2864 {
2865 if (map->getNumSelectedObjects() > 0)
2866 selection_changed = true;
2867 map->clearObjectSelection(false);
2868 }
2869
2870 bool object_selected = false;
2871 MapPart* part = map->getCurrentPart();
2872 for (int i = 0, size = part->getNumObjects(); i < size; ++i)
2873 {
2874 Object* object = part->getObject(i);
2875 if (symbol_widget->isSymbolSelected(object->getSymbol()) && !(!select_exclusively && map->isObjectSelected(object)))
2876 {
2877 map->addObjectToSelection(object, false);
2878 object_selected = true;
2879 }
2880 }
2881
2882 selection_changed |= object_selected;
2883 if (selection_changed)
2884 map->emitSelectionChanged();
2885
2886 if (object_selected)
2887 {
2888 if (current_tool && current_tool->isDrawTool())
2889 setEditTool();
2890 }
2891 else
2892 {
2893 QMessageBox::warning(window, tr("Object selection"), tr("No objects were selected because there are no objects with the selected symbols."));
2894 }
2895 }
2896
deselectObjectsClicked()2897 void MapEditorController::deselectObjectsClicked()
2898 {
2899 bool selection_changed = false;
2900
2901 MapPart* part = map->getCurrentPart();
2902 for (int i = 0, size = part->getNumObjects(); i < size; ++i)
2903 {
2904 Object* object = part->getObject(i);
2905 if (symbol_widget->isSymbolSelected(object->getSymbol()) && map->isObjectSelected(object))
2906 {
2907 map->removeObjectFromSelection(object, false);
2908 selection_changed = true;
2909 }
2910 }
2911
2912 if (selection_changed)
2913 {
2914 map->emitSelectionChanged();
2915
2916 if (current_tool && current_tool->isDrawTool())
2917 setEditTool();
2918 }
2919 }
2920
selectAll()2921 void MapEditorController::selectAll()
2922 {
2923 auto num_selected_objects = map->getNumSelectedObjects();
2924 map->clearObjectSelection(false);
2925 map->getCurrentPart()->applyOnAllObjects([this](Object* object) {
2926 map->addObjectToSelection(object, false);
2927 });
2928
2929 if (map->getNumSelectedObjects() != num_selected_objects)
2930 {
2931 map->emitSelectionChanged();
2932 if (current_tool && current_tool->isDrawTool())
2933 setEditTool();
2934 }
2935 }
2936
selectNothing()2937 void MapEditorController::selectNothing()
2938 {
2939 if (map->getNumSelectedObjects() > 0)
2940 map->clearObjectSelection(true);
2941 }
2942
invertSelection()2943 void MapEditorController::invertSelection()
2944 {
2945 auto selection = Map::ObjectSelection{ map->selectedObjects() };
2946 map->clearObjectSelection(false);
2947 map->getCurrentPart()->applyOnAllObjects([this, &selection](Object* object) {
2948 if (selection.find(object) == end(selection))
2949 map->addObjectToSelection(object, false);
2950 });
2951
2952 if (map->getCurrentPart()->getNumObjects() > 0)
2953 {
2954 map->emitSelectionChanged();
2955 if (current_tool && current_tool->isDrawTool())
2956 setEditTool();
2957 }
2958 }
2959
selectByCurrentSymbols()2960 void MapEditorController::selectByCurrentSymbols()
2961 {
2962 selectObjectsClicked(true);
2963 }
2964
switchDashesClicked()2965 void MapEditorController::switchDashesClicked()
2966 {
2967 auto* undo_step = new SwitchDashesUndoStep(map);
2968 MapPart* part = map->getCurrentPart();
2969
2970 for (auto* object : map->selectedObjects())
2971 {
2972 if (object->getSymbol()->getContainedTypes() & Symbol::Line)
2973 {
2974 PathObject* path = reinterpret_cast<PathObject*>(object);
2975 path->reverse();
2976 object->update();
2977
2978 undo_step->addObject(part->findObjectIndex(object));
2979 }
2980 }
2981
2982 map->setObjectsDirty();
2983 map->push(undo_step);
2984 map->emitSelectionEdited();
2985 }
2986
2987 /// \todo Review use of container API
connectPaths_FindClosestEnd(const std::vector<Object * > & objects,const PathObject * a,int a_index,PathPartVector::size_type path_part_a,bool path_part_a_begin,PathObject ** out_b,int * out_b_index,int * out_path_part_b,bool * out_path_part_b_begin)2988 float connectPaths_FindClosestEnd(const std::vector<Object*>& objects, const PathObject* a, int a_index, PathPartVector::size_type path_part_a, bool path_part_a_begin, PathObject** out_b, int* out_b_index, int* out_path_part_b, bool* out_path_part_b_begin)
2989 {
2990 float best_dist_sq = std::numeric_limits<float>::max();
2991 for (int i = a_index; i < (int)objects.size(); ++i)
2992 {
2993 const auto* b = static_cast<const PathObject*>(objects[i]);
2994 if (b->getSymbol() != a->getSymbol())
2995 continue;
2996
2997 auto num_parts = b->parts().size();
2998 for (PathPartVector::size_type path_part_b = (a == b) ? path_part_a : 0; path_part_b < num_parts; ++path_part_b)
2999 {
3000 const PathPart& part = b->parts()[path_part_b];
3001 if (!part.isClosed())
3002 {
3003 for (int begin = 0; begin < 2; ++begin)
3004 {
3005 bool path_part_b_begin = (begin == 0);
3006 if (a == b && path_part_a == path_part_b && path_part_a_begin == path_part_b_begin)
3007 continue;
3008
3009 const MapCoord coord_a = a->getCoordinate(path_part_a_begin ? a->parts()[path_part_a].first_index : (a->parts()[path_part_a].last_index));
3010 const MapCoord coord_b = b->getCoordinate(path_part_b_begin ? b->parts()[path_part_b].first_index : (b->parts()[path_part_b].last_index));
3011 float distance_sq = coord_a.distanceSquaredTo(coord_b);
3012 if (distance_sq < best_dist_sq)
3013 {
3014 best_dist_sq = distance_sq;
3015 *out_b = const_cast<PathObject*>(b); // = /* non-const */ object[i]
3016 *out_b_index = i;
3017 *out_path_part_b = path_part_b;
3018 *out_path_part_b_begin = path_part_b_begin;
3019 if (distance_sq == 0)
3020 return 0;
3021 }
3022 }
3023 }
3024 }
3025 }
3026 return best_dist_sq;
3027 }
3028
connectPathsClicked()3029 void MapEditorController::connectPathsClicked()
3030 {
3031 std::vector<Object*> objects;
3032 std::vector<Object*> undo_objects;
3033 std::vector<Object*> deleted_objects;
3034
3035 // Collect all objects in question
3036 objects.reserve(map->getNumSelectedObjects());
3037 undo_objects.reserve(map->getNumSelectedObjects());
3038 for (auto* object : map->selectedObjects())
3039 {
3040 if (object->getSymbol()->getContainedTypes() & Symbol::Line && object->getType() == Object::Path)
3041 {
3042 object->update();
3043 objects.push_back(object);
3044 undo_objects.push_back(nullptr);
3045 }
3046 }
3047
3048 AddObjectsUndoStep* add_step = nullptr;
3049 MapPart* part = map->getCurrentPart();
3050
3051 /// \todo Fix connectPathsClicked()
3052 #ifndef __clang_analyzer__
3053 while (true)
3054 {
3055 // Find the closest pair of open ends of objects with the same symbol,
3056 // which is closer than a threshold
3057 PathObject* best_object_a = nullptr;
3058 PathObject* best_object_b = nullptr;
3059 int best_object_a_index = 0;
3060 int best_object_b_index = 0;
3061 auto best_part_a = PathPartVector::size_type { 0 };
3062 auto best_part_b = PathPartVector::size_type { 0 };
3063 bool best_part_a_begin = false;
3064 bool best_part_b_begin = false;
3065 float best_dist_sq = std::numeric_limits<float>::max();
3066
3067 for (int i = 0; i < (int)objects.size(); ++i)
3068 {
3069 PathObject* a = reinterpret_cast<PathObject*>(objects[i]);
3070
3071 // Choose connection threshold as maximum of 0.35mm, 1.5 * largest line extent, and 6 pixels
3072 // TODO: instead of 6 pixels, use a physical size as soon as screen dpi is in the settings
3073 auto close_distance_sq = qMax(0.35, 1.5 * a->getSymbol()->calculateLargestLineExtent());
3074 close_distance_sq = qMax(close_distance_sq, 0.001 * main_view->pixelToLength(6));
3075 close_distance_sq = qPow(close_distance_sq, 2);
3076
3077 auto num_parts = a->parts().size();
3078 for (PathPartVector::size_type path_part_a = 0; path_part_a < num_parts; ++path_part_a)
3079 {
3080 PathPart& part = a->parts()[path_part_a];
3081 if (!part.isClosed())
3082 {
3083 PathObject* b;
3084 int b_index;
3085 int path_part_b;
3086 bool path_part_b_begin;
3087 float distance_sq;
3088
3089 for (int begin = 0; begin < 2; ++begin)
3090 {
3091 bool path_part_a_begin = (begin == 0);
3092 distance_sq = connectPaths_FindClosestEnd(objects, a, i, path_part_a, path_part_a_begin, &b, &b_index, &path_part_b, &path_part_b_begin);
3093 if (distance_sq <= close_distance_sq && distance_sq < best_dist_sq)
3094 {
3095 best_dist_sq = distance_sq;
3096 best_object_a = a;
3097 best_object_b = b;
3098 best_object_a_index = i;
3099 best_object_b_index = b_index;
3100 best_part_a = path_part_a;
3101 best_part_b = path_part_b;
3102 best_part_a_begin = path_part_a_begin;
3103 best_part_b_begin = path_part_b_begin;
3104 }
3105 }
3106 }
3107 }
3108 }
3109
3110 // Abort if no possible connections found
3111 if (best_dist_sq == std::numeric_limits<float>::max())
3112 break;
3113
3114 // Create undo objects for a and b
3115 if (!undo_objects[best_object_a_index])
3116 undo_objects[best_object_a_index] = best_object_a->duplicate();
3117 if (!undo_objects[best_object_b_index])
3118 undo_objects[best_object_b_index] = best_object_b->duplicate();
3119
3120 // Connect the best parts
3121 if (best_part_a_begin && best_part_b_begin)
3122 {
3123 best_object_b->parts()[best_part_b].reverse();
3124 best_object_a->connectPathParts(best_part_a, best_object_b, best_part_b, true);
3125 }
3126 else if (best_part_a_begin && !best_part_b_begin)
3127 {
3128 if (best_object_a == best_object_b && best_part_a == best_part_b)
3129 best_object_a->parts()[best_part_a].connectEnds();
3130 else
3131 best_object_a->connectPathParts(best_part_a, best_object_b, best_part_b, true);
3132 }
3133 else if (!best_part_a_begin && best_part_b_begin)
3134 {
3135 if (best_object_a == best_object_b && best_part_a == best_part_b)
3136 best_object_a->parts()[best_part_a].connectEnds();
3137 else
3138 best_object_a->connectPathParts(best_part_a, best_object_b, best_part_b, false);
3139 }
3140 else //if (!best_part_a_begin && !best_part_b_begin)
3141 {
3142 best_object_b->parts()[best_part_b].reverse();
3143 best_object_a->connectPathParts(best_part_a, best_object_b, best_part_b, false);
3144 }
3145
3146 if (best_object_a != best_object_b)
3147 {
3148 // Copy all remaining parts of object b over to a
3149 best_object_a->getCoordinateRef(best_object_a->getCoordinateCount() - 1).setHolePoint(true);
3150 for (auto i = PathPartVector::size_type { 0 }; i < best_object_b->parts().size(); ++i)
3151 {
3152 if (i != best_part_b)
3153 best_object_a->appendPathPart(best_object_b->parts()[i]);
3154 }
3155
3156 // Create an add step for b
3157 int b_index_in_part = part->findObjectIndex(best_object_b);
3158 deleted_objects.push_back(best_object_b);
3159 if (!add_step)
3160 add_step = new AddObjectsUndoStep(map);
3161 add_step->addObject(b_index_in_part, undo_objects[best_object_b_index]);
3162 undo_objects[best_object_b_index] = nullptr;
3163
3164 // Delete b from the active list
3165 objects.erase(objects.begin() + best_object_b_index);
3166 undo_objects.erase(undo_objects.begin() + best_object_b_index);
3167 }
3168 else
3169 {
3170 // Delete the part which has been merged with another part
3171 if (best_part_a != best_part_b)
3172 best_object_a->deletePart(best_part_b);
3173 }
3174 }
3175 #endif
3176
3177 // Create undo step?
3178 ReplaceObjectsUndoStep* replace_step = nullptr;
3179 for (int i = 0; i < (int)undo_objects.size(); ++i)
3180 {
3181 if (undo_objects[i])
3182 {
3183 // The object was changed, update the new version
3184 objects[i]->forceUpdate(); /// @todo get rid of force if possible
3185
3186 // Add the old version to the undo step
3187 if (!replace_step)
3188 replace_step = new ReplaceObjectsUndoStep(map);
3189 replace_step->addObject(part->findObjectIndex(objects[i]), undo_objects[i]);
3190 }
3191 }
3192
3193 if (add_step)
3194 {
3195 for (auto* object : deleted_objects)
3196 {
3197 map->removeObjectFromSelection(object, false);
3198 map->getCurrentPart()->deleteObject(object);
3199 }
3200 }
3201
3202 if (add_step || replace_step)
3203 {
3204 auto* undo_step = new CombinedUndoStep(map);
3205 if (replace_step)
3206 undo_step->push(replace_step);
3207 if (add_step)
3208 undo_step->push(add_step);
3209 map->push(undo_step);
3210 map->setObjectsDirty();
3211 }
3212
3213 map->emitSelectionChanged();
3214 map->emitSelectionEdited();
3215 }
cutClicked()3216 void MapEditorController::cutClicked()
3217 {
3218 setTool(new CutTool(this, cut_tool_act));
3219 }
cutHoleClicked()3220 void MapEditorController::cutHoleClicked()
3221 {
3222 setTool(new CutHoleTool(this, cut_hole_act, CutHoleTool::Path));
3223 }
3224
cutHoleCircleClicked()3225 void MapEditorController::cutHoleCircleClicked()
3226 {
3227 setTool(new CutHoleTool(this, cut_hole_circle_act, CutHoleTool::Circle));
3228 }
3229
cutHoleRectangleClicked()3230 void MapEditorController::cutHoleRectangleClicked()
3231 {
3232 setTool(new CutHoleTool(this, cut_hole_rectangle_act, CutHoleTool::Rect));
3233 }
3234
rotateClicked()3235 void MapEditorController::rotateClicked()
3236 {
3237 setTool(new RotateTool(this, rotate_act));
3238 }
3239
rotatePatternClicked()3240 void MapEditorController::rotatePatternClicked()
3241 {
3242 setTool(new RotatePatternTool(this, rotate_pattern_act));
3243 }
3244
scaleClicked()3245 void MapEditorController::scaleClicked()
3246 {
3247 setTool(new ScaleTool(this, scale_act));
3248 }
3249
measureClicked(bool checked)3250 void MapEditorController::measureClicked(bool checked)
3251 {
3252 if (!measure_dock_widget)
3253 {
3254 measure_dock_widget = new EditorDockWidget(tr("Measure"), measure_act, this, window);
3255 measure_dock_widget->toggleViewAction()->setVisible(false);
3256 auto* measure_widget = new MeasureWidget(map);
3257 measure_dock_widget->setWidget(measure_widget);
3258 measure_dock_widget->setObjectName(QString::fromLatin1("Measure dock widget"));
3259 addFloatingDockWidget(measure_dock_widget);
3260 }
3261
3262 measure_dock_widget->setVisible(checked);
3263 }
3264
booleanUnionClicked()3265 void MapEditorController::booleanUnionClicked()
3266 {
3267 if (!BooleanTool(BooleanTool::Union, map).executePerSymbol())
3268 QMessageBox::warning(window, tr("Error"), tr("Unification failed."));
3269 }
3270
booleanIntersectionClicked()3271 void MapEditorController::booleanIntersectionClicked()
3272 {
3273 if (!BooleanTool(BooleanTool::Intersection, map).execute())
3274 QMessageBox::warning(window, tr("Error"), tr("Intersection failed."));
3275 }
3276
booleanDifferenceClicked()3277 void MapEditorController::booleanDifferenceClicked()
3278 {
3279 if (!BooleanTool(BooleanTool::Difference, map).execute())
3280 QMessageBox::warning(window, tr("Error"), tr("Difference failed."));
3281 }
3282
booleanXOrClicked()3283 void MapEditorController::booleanXOrClicked()
3284 {
3285 if (!BooleanTool(BooleanTool::XOr, map).execute())
3286 QMessageBox::warning(window, tr("Error"), tr("XOr failed."));
3287 }
3288
booleanMergeHolesClicked()3289 void MapEditorController::booleanMergeHolesClicked()
3290 {
3291 if (map->getNumSelectedObjects() != 1)
3292 return;
3293
3294 if (!BooleanTool(BooleanTool::MergeHoles, map).execute())
3295 QMessageBox::warning(window, tr("Error"), tr("Merging holes failed."));
3296 }
3297
convertToCurvesClicked()3298 void MapEditorController::convertToCurvesClicked()
3299 {
3300 auto* undo_step = new ReplaceObjectsUndoStep(map);
3301 MapPart* part = map->getCurrentPart();
3302
3303 for (auto* object : map->selectedObjects())
3304 {
3305 if (object->getType() != Object::Path)
3306 continue;
3307
3308 PathObject* path = object->asPath();
3309 PathObject* undo_duplicate = nullptr;
3310 if (path->convertToCurves(&undo_duplicate))
3311 {
3312 undo_step->addObject(part->findObjectIndex(path), undo_duplicate);
3313 // TODO: make threshold configurable?
3314 const auto threshold = 0.08;
3315 path->simplify(nullptr, threshold);
3316 }
3317 path->update();
3318 }
3319
3320 if (undo_step->isEmpty())
3321 delete undo_step;
3322 else
3323 {
3324 map->setObjectsDirty();
3325 map->push(undo_step);
3326 map->emitSelectionEdited();
3327 }
3328 }
3329
simplifyPathClicked()3330 void MapEditorController::simplifyPathClicked()
3331 {
3332 // TODO: make threshold configurable!
3333 const auto threshold = 0.1;
3334
3335 auto* undo_step = new ReplaceObjectsUndoStep(map);
3336 MapPart* part = map->getCurrentPart();
3337
3338 for (auto* object : map->selectedObjects())
3339 {
3340 if (object->getType() != Object::Path)
3341 continue;
3342
3343 PathObject* path = object->asPath();
3344 PathObject* undo_duplicate = nullptr;
3345 if (path->simplify(&undo_duplicate, threshold))
3346 undo_step->addObject(part->findObjectIndex(path), undo_duplicate);
3347 path->update();
3348 }
3349
3350 if (undo_step->isEmpty())
3351 delete undo_step;
3352 else
3353 {
3354 map->setObjectsDirty();
3355 map->push(undo_step);
3356 map->emitSelectionEdited();
3357 }
3358 }
3359
cutoutPhysicalClicked()3360 void MapEditorController::cutoutPhysicalClicked()
3361 {
3362 setTool(new CutoutTool(this, cutout_physical_act, false));
3363 }
3364
cutawayPhysicalClicked()3365 void MapEditorController::cutawayPhysicalClicked()
3366 {
3367 setTool(new CutoutTool(this, cutaway_physical_act, true));
3368 }
3369
distributePointsClicked()3370 void MapEditorController::distributePointsClicked()
3371 {
3372 Q_ASSERT(activeSymbol()->getType() == Symbol::Point);
3373 PointSymbol* point = activeSymbol()->asPoint();
3374
3375 DistributePointsTool::Settings settings;
3376 if (!DistributePointsTool::showSettingsDialog(window, point, settings))
3377 return;
3378
3379 // Create points along paths
3380 std::vector<PointObject*> created_objects;
3381 for (const auto* object : map->selectedObjects())
3382 {
3383 if (object->getType() == Object::Path)
3384 DistributePointsTool::execute(object->asPath(), point, settings, created_objects);
3385 }
3386 if (created_objects.empty())
3387 return;
3388
3389 // Add points to map
3390 for (auto* o : created_objects)
3391 map->addObject(o);
3392
3393 // Create undo step and select new objects
3394 map->clearObjectSelection(false);
3395 MapPart* part = map->getCurrentPart();
3396 auto* delete_step = new DeleteObjectsUndoStep(map);
3397 for (std::size_t i = 0; i < created_objects.size(); ++i)
3398 {
3399 Object* object = created_objects[i];
3400 delete_step->addObject(part->findObjectIndex(object));
3401 map->addObjectToSelection(object, i == created_objects.size() - 1);
3402 }
3403 map->push(delete_step);
3404 map->setObjectsDirty();
3405 }
3406
addFloatingDockWidget(QDockWidget * dock_widget)3407 void MapEditorController::addFloatingDockWidget(QDockWidget* dock_widget)
3408 {
3409 if (!window->restoreDockWidget(dock_widget))
3410 {
3411 dock_widget->setFloating(true);
3412 // We must set the size from the sizeHint() ourselves,
3413 // otherwise QDockWidget may use the minimum size.
3414 QRect geometry(window->pos(), dock_widget->sizeHint());
3415 geometry.translate(window->width() - geometry.width(), window->centralWidget()->y());
3416 const int max_height = window->centralWidget()->height();
3417 if (geometry.height() > max_height)
3418 geometry.setHeight(max_height);
3419 dock_widget->setGeometry(geometry);
3420 connect(dock_widget, &QDockWidget::dockLocationChanged, this, &MapEditorController::setWindowStateChanged);
3421 }
3422 }
3423
paintOnTemplateClicked(bool checked)3424 void MapEditorController::paintOnTemplateClicked(bool checked)
3425 {
3426 if (!checked)
3427 finishPaintOnTemplate();
3428 else if (last_painted_on_template)
3429 paintOnTemplate(last_painted_on_template);
3430 else
3431 paintOnTemplateSelectClicked();
3432 }
3433
paintOnTemplateSelectClicked()3434 void MapEditorController::paintOnTemplateSelectClicked()
3435 {
3436 PaintOnTemplateSelectDialog paintDialog(map, main_view, last_painted_on_template, window);
3437 paintDialog.setWindowModality(Qt::WindowModal);
3438 if (paintDialog.exec() == QDialog::Accepted)
3439 {
3440 last_painted_on_template = paintDialog.getSelectedTemplate();
3441 paintOnTemplate(last_painted_on_template);
3442 }
3443 }
3444
enableGPSDisplay(bool enable)3445 void MapEditorController::enableGPSDisplay(bool enable)
3446 {
3447 if (enable)
3448 {
3449 gps_display->startUpdates();
3450
3451 // Create gps_track_recorder if we can determine a template track filename
3452 constexpr int gps_track_draw_update_interval = 10 * 1000; // in milliseconds
3453 if (! window->currentPath().isEmpty())
3454 {
3455 // Find or create a template for the track with a specific name
3456 QString gpx_file_path =
3457 QFileInfo(window->currentPath()).absoluteDir().canonicalPath()
3458 + QLatin1Char('/')
3459 + QFileInfo(window->currentPath()).completeBaseName()
3460 + QLatin1String(" - GPS-")
3461 + QDate::currentDate().toString(Qt::ISODate)
3462 + QLatin1String(".gpx");
3463
3464 bool new_template = true; // Indicates that we really add a new template.
3465 TemplateTrack* track = nullptr;
3466 int template_index = 0;
3467 for ( ; template_index < map->getNumTemplates(); ++template_index)
3468 {
3469 auto* temp = map->getTemplate(template_index);
3470 if (temp->getTemplatePath() == gpx_file_path)
3471 {
3472 // There is a template for this track.
3473 new_template = false;
3474 track = qobject_cast<TemplateTrack*>(temp);
3475 break;
3476 }
3477 }
3478
3479 // Derive new visibility from previous/default one.
3480 auto visibility = main_view->getTemplateVisibility(track);
3481 visibility.opacity = std::max(0.5, visibility.opacity);
3482 visibility.visible = true;
3483
3484 if (!track)
3485 {
3486 if (!new_template)
3487 {
3488 // Need to replace the template at template_index
3489 map->setTemplateAreaDirty(template_index);
3490 map->deleteTemplate(template_index);
3491 }
3492 track = new TemplateTrack(gpx_file_path, map);
3493 // This will set the state to 'Loaded', so we need to reset it
3494 // to `Unloaded` to allow for loading the file when it becomes
3495 // visible for the first time.
3496 track->configureForGPSTrack();
3497 if (QFileInfo::exists(gpx_file_path))
3498 {
3499 track->unloadTemplateFile();
3500 track->loadTemplateFile(false);
3501 }
3502 map->addTemplate(track, template_index);
3503 // When the map is saved, the new track must be saved even if it is empty.
3504 track->setHasUnsavedChanges(true);
3505 map->setTemplatesDirty();
3506 }
3507
3508 main_view->setTemplateVisibility(track, visibility);
3509 map->setTemplateAreaDirty(template_index);
3510
3511 gps_track_recorder = new GPSTrackRecorder(gps_display, track, gps_track_draw_update_interval, map_widget);
3512 }
3513 }
3514 else
3515 {
3516 gps_display->stopUpdates();
3517
3518 delete gps_track_recorder;
3519 gps_track_recorder = nullptr;
3520 }
3521 gps_display->setVisible(enable);
3522
3523 if (! enable)
3524 {
3525 gps_marker_display->stopPath();
3526 gps_temporary_path_act->setChecked(false);
3527 }
3528
3529 gps_distance_rings_action->setEnabled(enable);
3530 gps_temporary_point_act->setEnabled(enable);
3531 gps_temporary_path_act->setEnabled(enable);
3532 updateDrawPointGPSAvailability();
3533 }
3534
enableGPSDistanceRings(bool enable)3535 void MapEditorController::enableGPSDistanceRings(bool enable)
3536 {
3537 gps_display->enableDistanceRings(enable);
3538 }
3539
updateDrawPointGPSAvailability()3540 void MapEditorController::updateDrawPointGPSAvailability()
3541 {
3542 const Symbol* symbol = activeSymbol();
3543 draw_point_gps_act->setEnabled(symbol && gps_display->isVisible() && symbol->getType() == Symbol::Point && !symbol->isHidden());
3544 move_to_gps_pos_act->setEnabled(gps_display->isVisible());
3545 }
3546
drawPointGPSClicked()3547 void MapEditorController::drawPointGPSClicked()
3548 {
3549 setTool(new DrawPointGPSTool(gps_display, this, draw_point_gps_act));
3550 }
3551
gpsTemporaryPointClicked()3552 void MapEditorController::gpsTemporaryPointClicked()
3553 {
3554 if (gps_marker_display->addPoint())
3555 gps_temporary_clear_act->setEnabled(true);
3556 }
3557
gpsTemporaryPathClicked(bool enable)3558 void MapEditorController::gpsTemporaryPathClicked(bool enable)
3559 {
3560 if (enable)
3561 {
3562 gps_marker_display->startPath();
3563 gps_temporary_clear_act->setEnabled(true);
3564 }
3565 else
3566 gps_marker_display->stopPath();
3567 }
3568
gpsTemporaryClearClicked()3569 void MapEditorController::gpsTemporaryClearClicked()
3570 {
3571 if (QMessageBox::question(window, tr("Clear temporary markers"), tr("Are you sure you want to delete all temporary GPS markers? This cannot be undone."), QMessageBox::Yes | QMessageBox::No) == QMessageBox::No)
3572 return;
3573
3574 gps_marker_display->stopPath();
3575 gps_temporary_path_act->setChecked(false);
3576 gps_marker_display->clear();
3577 gps_temporary_clear_act->setEnabled(false);
3578 }
3579
enableCompassDisplay(bool enable)3580 void MapEditorController::enableCompassDisplay(bool enable)
3581 {
3582 if (!compass_display || !show_top_bar_button || !top_action_bar)
3583 {
3584 Q_ASSERT(!"Prerequisites must be initialized");
3585 }
3586 else if (enable)
3587 {
3588 auto size = compass_display->sizeHint();
3589 if (top_action_bar->isVisible())
3590 compass_display->setGeometry(0, top_action_bar->size().height(), size.width(), size.height());
3591 else
3592 compass_display->setGeometry(show_top_bar_button->size().width(), 0, size.width(), size.height());
3593 connect(&Compass::getInstance(), &Compass::azimuthChanged, compass_display, &CompassDisplay::setAzimuth);
3594 compass_display->show();
3595 }
3596 else
3597 {
3598 disconnect(&Compass::getInstance(), &Compass::azimuthChanged, compass_display, &CompassDisplay::setAzimuth);
3599 compass_display->hide();
3600 }
3601 }
3602
alignMapWithNorth(bool enable)3603 void MapEditorController::alignMapWithNorth(bool enable)
3604 {
3605 const int update_interval = 1000; // milliseconds
3606
3607 if (enable)
3608 {
3609 Compass::getInstance().startUsage();
3610 gps_display->enableHeadingIndicator(true);
3611
3612 connect(&align_map_with_north_timer, &QTimer::timeout, this, &MapEditorController::alignMapWithNorthUpdate);
3613 align_map_with_north_timer.start(update_interval);
3614 alignMapWithNorthUpdate();
3615 }
3616 else
3617 {
3618 align_map_with_north_timer.disconnect();
3619 align_map_with_north_timer.stop();
3620
3621 gps_display->enableHeadingIndicator(false);
3622 Compass::getInstance().stopUsage();
3623
3624 main_view->setRotation(0);
3625 }
3626 }
3627
alignMapWithNorthUpdate()3628 void MapEditorController::alignMapWithNorthUpdate()
3629 {
3630 // Time in milliseconds for which the rotation should not be updated after
3631 // the user interacted with the map widget
3632 const int interaction_time_threshold = 1500;
3633 if (map_widget->getTimeSinceLastInteraction() < interaction_time_threshold)
3634 return;
3635
3636 // Set map rotation
3637 main_view->setRotation(M_PI / -180.0 * Compass::getInstance().getCurrentAzimuth());
3638 }
3639
hideTopActionBar()3640 void MapEditorController::hideTopActionBar()
3641 {
3642 top_action_bar->hide();
3643 show_top_bar_button->show();
3644 show_top_bar_button->raise();
3645
3646 compass_display->move(show_top_bar_button->size().width(), 0);
3647 }
3648
showTopActionBar()3649 void MapEditorController::showTopActionBar()
3650 {
3651 show_top_bar_button->hide();
3652 top_action_bar->show();
3653 top_action_bar->raise();
3654
3655 compass_display->move(0, top_action_bar->size().height());
3656 }
3657
mobileSymbolSelectorClicked()3658 void MapEditorController::mobileSymbolSelectorClicked()
3659 {
3660 // NOTE: this does not handle window resizes, however it is assumed that in
3661 // mobile mode there is no window, instead the application is running fullscreen.
3662 symbol_widget->setGeometry(window->rect());
3663 symbol_widget->raise();
3664 symbol_widget->show();
3665 connect(symbol_widget, &SymbolWidget::selectedSymbolsChanged, this, &MapEditorController::mobileSymbolSelectorFinished);
3666 }
3667
mobileSymbolSelectorFinished()3668 void MapEditorController::mobileSymbolSelectorFinished()
3669 {
3670 disconnect(symbol_widget, &SymbolWidget::selectedSymbolsChanged, this, &MapEditorController::mobileSymbolSelectorFinished);
3671 symbol_widget->hide();
3672 }
3673
updateMapPartsUI()3674 void MapEditorController::updateMapPartsUI()
3675 {
3676 if (!mappart_selector_box)
3677 {
3678 mappart_selector_box = new QComboBox();
3679 mappart_selector_box->setToolTip(tr("Map parts"));
3680 }
3681
3682 if (!mappart_merge_menu)
3683 {
3684 mappart_merge_menu = new QMenu();
3685 mappart_merge_menu->menuAction()->setMenuRole(QAction::NoRole);
3686 mappart_merge_menu->setTitle(tr("Merge this part with"));
3687 }
3688
3689 if (!mappart_move_menu)
3690 {
3691 mappart_move_menu = new QMenu();
3692 mappart_move_menu->menuAction()->setMenuRole(QAction::NoRole);
3693 mappart_move_menu->setTitle(tr("Move selected objects to"));
3694 }
3695
3696 const QSignalBlocker selector_box_blocker(mappart_selector_box);
3697 mappart_selector_box->clear();
3698 mappart_merge_menu->clear();
3699 mappart_move_menu->clear();
3700
3701 const int count = map ? map->getNumParts() : 0;
3702 const bool have_multiple_parts = (count > 1);
3703 mappart_merge_menu->setEnabled(have_multiple_parts);
3704 mappart_move_menu->setEnabled(have_multiple_parts && map->getNumSelectedObjects() > 0);
3705 if (mappart_remove_act)
3706 {
3707 mappart_remove_act->setEnabled(have_multiple_parts);
3708 mappart_merge_act->setEnabled(have_multiple_parts);
3709 }
3710 if (toolbar_mapparts && !toolbar_mapparts->isVisible())
3711 {
3712 toolbar_mapparts->setVisible(have_multiple_parts);
3713 }
3714
3715 if (count > 0)
3716 {
3717 const int current = map->getCurrentPartIndex();
3718 for (int i = 0; i < count; ++i)
3719 {
3720 QString part_name = map->getPart(i)->getName();
3721 mappart_selector_box->addItem(part_name);
3722
3723 if (i != current)
3724 {
3725 auto* action = new QAction(part_name, this);
3726 mappart_merge_mapper->setMapping(action, i);
3727 connect(action, QOverload<bool>::of(&QAction::triggered), mappart_merge_mapper, QOverload<>::of(&QSignalMapper::map));
3728 mappart_merge_menu->addAction(action);
3729
3730 action = new QAction(part_name, this);
3731 mappart_move_mapper->setMapping(action, i);
3732 connect(action, QOverload<bool>::of(&QAction::triggered), mappart_move_mapper, QOverload<>::of(&QSignalMapper::map));
3733 mappart_move_menu->addAction(action);
3734 }
3735 }
3736
3737 mappart_selector_box->setCurrentIndex(current);
3738 }
3739 }
3740
addMapPart()3741 void MapEditorController::addMapPart()
3742 {
3743 bool accepted = false;
3744 QString name = QInputDialog::getText(
3745 window,
3746 tr("Add new part..."),
3747 tr("Enter the name of the map part:"),
3748 QLineEdit::Normal,
3749 QString(),
3750 &accepted );
3751 if (accepted && !name.isEmpty())
3752 {
3753 auto* part = new MapPart(name, map);
3754 map->addPart(part, map->getCurrentPartIndex() + 1);
3755 map->setCurrentPart(part);
3756 map->push(new MapPartUndoStep(map, MapPartUndoStep::RemoveMapPart, part));
3757 }
3758 }
3759
removeMapPart()3760 void MapEditorController::removeMapPart()
3761 {
3762 auto* part = map->getCurrentPart();
3763
3764 QMessageBox::StandardButton button =
3765 QMessageBox::question(
3766 window,
3767 tr("Remove current part"),
3768 tr("Do you want to remove map part \"%1\" and all its objects?").arg(part->getName()),
3769 QMessageBox::Yes | QMessageBox::No );
3770
3771 if (button == QMessageBox::Yes)
3772 {
3773 auto index = map->getCurrentPartIndex();
3774 UndoStep* undo_step = new MapPartUndoStep(map, MapPartUndoStep::AddMapPart, index);
3775
3776 auto i = part->getNumObjects();
3777 if (i > 0)
3778 {
3779 auto* add_step = new AddObjectsUndoStep(map);
3780 do
3781 {
3782 --i;
3783 auto* object = part->getObject(i);
3784 add_step->addObject(i, object);
3785 part->releaseObject(object);
3786 }
3787 while (i > 0);
3788
3789 auto* combined_step = new CombinedUndoStep(map);
3790 combined_step->push(add_step);
3791 combined_step->push(undo_step);
3792 undo_step = combined_step;
3793 }
3794
3795 map->push(undo_step);
3796 map->removePart(index);
3797 }
3798 }
3799
renameMapPart()3800 void MapEditorController::renameMapPart()
3801 {
3802 bool accepted = false;
3803 QString name =
3804 QInputDialog::getText(
3805 window,
3806 tr("Rename current part..."),
3807 tr("Enter the name of the map part:"),
3808 QLineEdit::Normal,
3809 map->getCurrentPart()->getName(),
3810 &accepted );
3811 if (accepted && !name.isEmpty())
3812 {
3813 map->push(new MapPartUndoStep(map, MapPartUndoStep::ModifyMapPart, map->getCurrentPartIndex()));
3814 map->getCurrentPart()->setName(name);
3815 }
3816 }
3817
changeMapPart(int index)3818 void MapEditorController::changeMapPart(int index)
3819 {
3820 if (index >= 0)
3821 {
3822 map->setCurrentPartIndex(std::size_t(index));
3823 window->showStatusBarMessage(tr("Switched to map part '%1'.").arg(map->getCurrentPart()->getName()), 1000);
3824 }
3825 }
3826
reassignObjectsToMapPart(int target)3827 void MapEditorController::reassignObjectsToMapPart(int target)
3828 {
3829 auto current = map->getCurrentPartIndex();
3830 auto* current_part = map->getPart(current);
3831 std::vector<int> objects(map->selectedObjects().size());
3832 std::transform(map->selectedObjectsBegin(), map->selectedObjectsEnd(), begin(objects), [current_part](auto* object) {
3833 return current_part->findObjectIndex(object);
3834 });
3835 std::sort(objects.rbegin(), objects.rend());
3836 map->reassignObjectsToMapPart(begin(objects), end(objects), current, target);
3837
3838 auto* undo = new SwitchPartUndoStep(map, target, current);
3839 for (auto i : objects)
3840 undo->addObject(i);
3841 map->push(undo);
3842 }
3843
mergeCurrentMapPartTo(int target)3844 void MapEditorController::mergeCurrentMapPartTo(int target)
3845 {
3846 MapPart* const source_part = map->getCurrentPart();
3847 MapPart* const target_part = map->getPart(target);
3848 const QMessageBox::StandardButton button =
3849 QMessageBox::question(
3850 window,
3851 tr("Merge map parts"),
3852 tr("Do you want to move all objects from map part \"%1\" to \"%2\", "
3853 "and to remove \"%1\"?")
3854 .arg(source_part->getName(), target_part->getName()),
3855 QMessageBox::Yes | QMessageBox::No );
3856
3857 if (button == QMessageBox::Yes)
3858 {
3859 // Beware that the source part is removed, and
3860 // the target part's index might change during merge.
3861 auto source = map->getCurrentPartIndex();
3862 UndoStep* add_part_step = new MapPartUndoStep(map, MapPartUndoStep::AddMapPart, source);
3863
3864 auto first = map->mergeParts(source, target);
3865
3866 auto* switch_part_undo = new SwitchPartUndoStep(map, target, source);
3867 for (auto i = target_part->getNumObjects(); i > first; --i)
3868 switch_part_undo->addObject(0);
3869
3870 auto* undo = new CombinedUndoStep(map);
3871 undo->push(switch_part_undo);
3872 undo->push(add_part_step);
3873 map->push(undo);
3874 }
3875 }
3876
mergeAllMapParts()3877 void MapEditorController::mergeAllMapParts()
3878 {
3879 QString const name = map->getCurrentPart()->getName();
3880 const QMessageBox::StandardButton button =
3881 QMessageBox::question(
3882 window,
3883 tr("Merge map parts"),
3884 tr("Do you want to move all objects to map part \"%1\", and to remove all other map parts?").arg(name),
3885 QMessageBox::Yes | QMessageBox::No );
3886
3887 if (button == QMessageBox::Yes)
3888 {
3889 auto* undo = new CombinedUndoStep(map);
3890
3891 // For simplicity, we merge to the first part,
3892 // but keep the properties (i.e. name) of the current part.
3893 map->setCurrentPartIndex(0);
3894 MapPart* target_part = map->getPart(0);
3895
3896 for (auto i = map->getNumParts() - 1; i > 0; --i)
3897 {
3898 UndoStep* add_part_step = new MapPartUndoStep(map, MapPartUndoStep::AddMapPart, i);
3899 auto first = map->mergeParts(i, 0);
3900 auto* switch_part_undo = new SwitchPartUndoStep(map, 0, i);
3901 for (auto j = target_part->getNumObjects(); j > first; --j)
3902 switch_part_undo->addObject(0);
3903 undo->push(switch_part_undo);
3904 undo->push(add_part_step);
3905 }
3906
3907 undo->push(new MapPartUndoStep(map, MapPartUndoStep::ModifyMapPart, 0));
3908 target_part->setName(name);
3909
3910 map->push(undo);
3911 }
3912 }
3913
3914
paintOnTemplate(Template * temp)3915 void MapEditorController::paintOnTemplate(Template* temp)
3916 {
3917 auto* tool = qobject_cast<PaintOnTemplateTool*>(getTool());
3918 if (!tool)
3919 {
3920 tool = new PaintOnTemplateTool(this, paint_on_template_act);
3921 setTool(tool);
3922 }
3923
3924 hideAllTemplates(false);
3925 auto vis = main_view->getTemplateVisibility(temp);
3926 vis.visible = true;
3927 main_view->setTemplateVisibility(temp, vis);
3928 temp->setTemplateAreaDirty();
3929
3930 tool->setTemplate(temp);
3931 }
3932
finishPaintOnTemplate()3933 void MapEditorController::finishPaintOnTemplate()
3934 {
3935 if (auto* tool = qobject_cast<PaintOnTemplateTool*>(current_tool))
3936 {
3937 tool->deactivate();
3938 }
3939 }
3940
templateAdded(int,const Template *)3941 void MapEditorController::templateAdded(int /*pos*/, const Template* /*temp*/)
3942 {
3943 if (map->getNumTemplates() == 1)
3944 templateAvailabilityChanged();
3945 }
3946
templateDeleted(int,const Template *)3947 void MapEditorController::templateDeleted(int /*pos*/, const Template* /*temp*/)
3948 {
3949 if (map->getNumTemplates() == 0)
3950 templateAvailabilityChanged();
3951 }
3952
setMapAndView(Map * map,MapView * map_view)3953 void MapEditorController::setMapAndView(Map* map, MapView* map_view)
3954 {
3955 Q_ASSERT(map);
3956 Q_ASSERT(map_view);
3957
3958 if (this->map)
3959 {
3960 this->map->disconnect(this);
3961 if (symbol_widget)
3962 {
3963 this->map->disconnect(symbol_widget);
3964 }
3965 }
3966
3967 this->map = map;
3968 this->main_view = map_view;
3969 updateMapPartsUI();
3970
3971 connect(&map->undoManager(), &UndoManager::canRedoChanged, this, &MapEditorController::undoStepAvailabilityChanged);
3972 connect(&map->undoManager(), &UndoManager::canUndoChanged, this, &MapEditorController::undoStepAvailabilityChanged);
3973 connect(map, &Map::objectSelectionChanged, this, &MapEditorController::objectSelectionChanged);
3974 connect(map, &Map::templateAdded, this, &MapEditorController::templateAdded);
3975 connect(map, &Map::templateDeleted, this, &MapEditorController::templateDeleted);
3976 connect(map, &Map::closedTemplateAvailabilityChanged, this, &MapEditorController::closedTemplateAvailabilityChanged);
3977 connect(map, &Map::spotColorPresenceChanged, this, &MapEditorController::spotColorPresenceChanged);
3978 connect(map, &Map::currentMapPartChanged, this, &MapEditorController::updateMapPartsUI);
3979 connect(map, &Map::mapPartAdded, this, &MapEditorController::updateMapPartsUI);
3980 connect(map, &Map::mapPartChanged, this, &MapEditorController::updateMapPartsUI);
3981 connect(map, &Map::mapPartDeleted, this, &MapEditorController::updateMapPartsUI);
3982
3983 if (symbol_widget)
3984 {
3985 delete symbol_widget;
3986 symbol_widget = nullptr;
3987 createSymbolWidget();
3988 }
3989
3990 if (window)
3991 {
3992 updateWidgets();
3993 }
3994 }
3995
updateWidgets()3996 void MapEditorController::updateWidgets()
3997 {
3998 undoStepAvailabilityChanged();
3999 objectSelectionChanged();
4000 if (mode == MapEditor)
4001 {
4002 if (main_view)
4003 {
4004 show_grid_act->setChecked(main_view->isGridVisible());
4005 hide_all_templates_act->setChecked(main_view->areAllTemplatesHidden());
4006 overprinting_simulation_act->setChecked(main_view->isOverprintingSimulationEnabled());
4007 }
4008 if (map)
4009 {
4010 hatch_areas_view_act->setChecked(map->isAreaHatchingEnabled());
4011 baseline_view_act->setChecked(map->isBaselineViewEnabled());
4012 closedTemplateAvailabilityChanged();
4013 spotColorPresenceChanged(map->hasSpotColors());
4014 updateMapPartsUI();
4015 }
4016 }
4017 }
4018
createSymbolWidget(QWidget * parent)4019 void MapEditorController::createSymbolWidget(QWidget* parent)
4020 {
4021 if (!symbol_widget)
4022 {
4023 symbol_widget = new SymbolWidget(map, mobile_mode, parent);
4024 connect(symbol_widget, &SymbolWidget::switchSymbolClicked, this, &MapEditorController::switchSymbolClicked);
4025 connect(symbol_widget, &SymbolWidget::fillBorderClicked, this, &MapEditorController::fillBorderClicked);
4026 connect(symbol_widget, &SymbolWidget::selectObjectsClicked, this, &MapEditorController::selectObjectsClicked);
4027 connect(symbol_widget, &SymbolWidget::deselectObjectsClicked, this, &MapEditorController::deselectObjectsClicked);
4028 connect(symbol_widget, &SymbolWidget::selectedSymbolsChanged, this, &MapEditorController::selectedSymbolsChanged);
4029 if (symbol_dock_widget)
4030 {
4031 Q_ASSERT(!parent);
4032 symbol_dock_widget->setWidget(symbol_widget);
4033 }
4034
4035 selectedSymbolsChanged();
4036 }
4037 }
4038
importClicked()4039 void MapEditorController::importClicked()
4040 {
4041 QSettings settings;
4042 QString import_directory = settings.value(QString::fromLatin1("importFileDirectory"), QDir::homePath()).toString();
4043
4044 QStringList map_extensions;
4045 for (auto* format : FileFormats.formats())
4046 {
4047 if (format->supportsReading())
4048 map_extensions.append(format->fileExtensions());
4049 }
4050 map_extensions.push_back(QLatin1String("gpx"));
4051 map_extensions.sort(Qt::CaseInsensitive);
4052 map_extensions.removeDuplicates();
4053
4054 QString filename = FileDialog::getOpenFileName(
4055 window,
4056 tr("Import..."),
4057 import_directory,
4058 QString::fromLatin1("%1 (%2);;%3 (*.*)").arg(
4059 tr("Importable files"), QLatin1String("*.") + map_extensions.join(QString::fromLatin1(" *.")), tr("All files")) );
4060 if (filename.isEmpty())
4061 return;
4062
4063 settings.setValue(QString::fromLatin1("importFileDirectory"), QFileInfo(filename).canonicalPath());
4064
4065 /**
4066 * Finding the most appropriate import function in the following way:
4067 * - If the extensions is ".gpx", the (default) import is via TemplateTrack.
4068 * - If the format is understood by OgrFileFormat, it is handled by
4069 * OgrTemplate import. Note that the OgrTemplate import may also handle
4070 * GPX, but it is left to the OgrFileFormat (user configuration) whether
4071 * it wants to report its support for the GPX format.
4072 * - Every other recognized map file is imported regularly.
4073 */
4074 char const* format_id = "";
4075 if (filename.endsWith(QLatin1String(".gpx"), Qt::CaseInsensitive))
4076 format_id = "GPX";
4077
4078 auto* map_format = FileFormats.findFormatForFilename(filename, &FileFormat::supportsFileImport);
4079 if (map_format)
4080 format_id = map_format->id();
4081
4082 if (qstrcmp(format_id, "OGR") == 0)
4083 {
4084 importOgrFile(filename);
4085 return;
4086 }
4087
4088 if (qstrcmp(format_id, "GPX") == 0)
4089 {
4090 // Legacy GPX file import
4091 importGpxFile(filename);
4092 return; // Error reporting in Track::import()
4093 }
4094
4095 if (format_id != nullptr)
4096 {
4097 importMapFile(filename, false);
4098 return;
4099 }
4100
4101 QMessageBox::critical(window, tr("Error"), tr("Cannot import the selected file because its file format is not supported."));
4102 }
4103
importGpxFile(const QString & filename)4104 bool MapEditorController::importGpxFile(const QString& filename)
4105 {
4106 Map imported_map;
4107 imported_map.setGeoreferencing(map->getGeoreferencing());
4108
4109 TemplateTrack temp(filename, &imported_map);
4110 if (!temp.configureAndLoad(window, main_view))
4111 return false;
4112
4113 return importMapWithReplacement(imported_map, Map::MinimalObjectImport | Map::GeorefImport, filename);
4114 }
4115
importMapFile(const QString & filename,bool show_errors)4116 bool MapEditorController::importMapFile(const QString& filename, bool show_errors)
4117 {
4118 Map imported_map;
4119 imported_map.setScaleDenominator(map->getScaleDenominator()); // for non-scaled geodata
4120
4121 auto importer = FileFormats.makeImporter(filename, imported_map, nullptr);
4122 if (!importer)
4123 {
4124 if (show_errors)
4125 {} /// \todo error message
4126 return false;
4127 }
4128
4129 if (!importer->doImport())
4130 {
4131 if (show_errors)
4132 QMessageBox::warning(window, tr("Error"), importer->warnings().back());
4133 return false;
4134 }
4135
4136 if (show_errors && !importer->warnings().empty())
4137 MainWindow::showMessageBox(window, tr("Warning"), tr("The map import generated warnings."), importer->warnings());
4138
4139 return importMapWithReplacement(imported_map, Map::MinimalObjectImport | Map::GeorefImport, filename);
4140 }
4141
importOgrFile(const QString & filename)4142 bool MapEditorController::importOgrFile(const QString& filename)
4143 {
4144 #if MAPPER_USE_GDAL
4145 OgrTemplate ogr_template {filename, map};
4146 if (!ogr_template.configureAndLoad(window, main_view))
4147 return false;
4148
4149 auto template_map = ogr_template.takeTemplateMap();
4150 if (Q_UNLIKELY(!template_map))
4151 return false;
4152
4153 TemplateTransform transform;
4154 if (!ogr_template.isTemplateGeoreferenced())
4155 {
4156 ogr_template.getTransform(transform);
4157 template_map->applyOnAllObjects(transform.makeObjectTransform());
4158 template_map->setGeoreferencing(map->getGeoreferencing());
4159 }
4160 auto template_scale = (transform.template_scale_x + transform.template_scale_y) / 2;
4161 template_scale *= qreal(template_map->getScaleDenominator()) / map->getScaleDenominator();
4162 if (!qFuzzyCompare(template_scale, 1))
4163 {
4164 template_map->scaleAllSymbols(template_scale);
4165 }
4166
4167 return importMapWithReplacement(*template_map, Map::MinimalObjectImport | Map::GeorefImport, filename);
4168 #else
4169 return false;
4170 #endif
4171 }
4172
4173
importMapWithReplacement(Map & imported_map,Map::ImportMode mode,const QString & crt_file_hint)4174 bool MapEditorController::importMapWithReplacement(
4175 Map& imported_map,
4176 Map::ImportMode mode,
4177 const QString& crt_file_hint)
4178 {
4179 if (!SymbolReplacement(imported_map, *map).withAutoCrtFile(window, crt_file_hint))
4180 {
4181 auto choice = QMessageBox::question(window,
4182 ::OpenOrienteering::Map::tr("Import..."),
4183 ::OpenOrienteering::Map::tr("Symbol replacement was canceled.\n"
4184 "Import the data anyway?"),
4185 QMessageBox::Yes | QMessageBox::No,
4186 QMessageBox::No);
4187 if (choice == QMessageBox::No)
4188 return false;
4189 }
4190
4191 importMap(imported_map, mode, window);
4192 return true;
4193 }
4194
4195
importMap(Map & other,Map::ImportMode mode,QWidget * dialog_parent,std::vector<bool> * filter,int symbol_insert_pos,bool merge_duplicate_symbols)4196 QHash<const Symbol*, Symbol*> MapEditorController::importMap(
4197 Map& other,
4198 Map::ImportMode mode,
4199 QWidget* dialog_parent,
4200 std::vector<bool>* filter,
4201 int symbol_insert_pos,
4202 bool merge_duplicate_symbols)
4203 {
4204 // Check if there is something to import
4205 if (other.getNumColors() == 0
4206 && other.getNumSymbols() == 0
4207 && other.getNumObjects() == 0)
4208 {
4209 QMessageBox::critical(dialog_parent, tr("Error"), tr("Nothing to import."));
4210 return {};
4211 }
4212
4213 // Check scale
4214 if ((mode & 0x0f) != Map::ColorImport
4215 && other.getNumSymbols() > 0
4216 && other.getScaleDenominator() != map->getScaleDenominator())
4217 {
4218 /// \todo Move message to this context.
4219 int answer = QMessageBox::question(
4220 dialog_parent,
4221 tr("Question"),
4222 tr("The scale of the imported data is 1:%1 "
4223 "which is different from this map's scale of 1:%2.\n\n"
4224 "Rescale the imported data?")
4225 .arg(QLocale().toString(other.getScaleDenominator()),
4226 QLocale().toString(map->getScaleDenominator())),
4227 QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::Yes);
4228 if (answer == QMessageBox::Yes)
4229 {
4230 other.changeScale(map->getScaleDenominator(), 1.0, MapCoord(0, 0), true, true, true, true);
4231 }
4232 }
4233
4234 return map->importMap(other, mode, filter, symbol_insert_pos, merge_duplicate_symbols);
4235 }
4236
4237
4238
4239 // slot
setViewOptionsEnabled(bool enabled)4240 void MapEditorController::setViewOptionsEnabled(bool enabled)
4241 {
4242 pan_act->setEnabled(enabled);
4243 show_grid_act->setEnabled(enabled);
4244 // hatch_areas_view_act->setEnabled(enabled);
4245 // baseline_view_act->setEnabled(enabled);
4246 overprinting_simulation_act->setEnabled(enabled);
4247 hide_all_templates_act->setEnabled(enabled);
4248 }
4249
4250
4251 // ### EditorDockWidget ###
4252
EditorDockWidget(const QString & title,QAction * action,MapEditorController * editor,QWidget * parent)4253 EditorDockWidget::EditorDockWidget(const QString& title, QAction* action, MapEditorController* editor, QWidget* parent)
4254 : QDockWidget(title, parent)
4255 , action(action)
4256 , editor(editor)
4257 {
4258 if (editor)
4259 {
4260 connect(this, &EditorDockWidget::dockLocationChanged, editor, &MapEditorController::setWindowStateChanged);
4261 }
4262
4263 if (action)
4264 {
4265 connect(toggleViewAction(), &QAction::toggled, action, &QAction::setChecked);
4266 }
4267
4268 #ifdef Q_OS_ANDROID
4269 size_grip = new QSizeGrip(this);
4270 size_grip->resize(size_grip->sizeHint());
4271 size_grip->setVisible(isTopLevel());
4272 connect(this, &QDockWidget::topLevelChanged, size_grip, &QWidget::setVisible);
4273 #endif
4274 }
4275
event(QEvent * event)4276 bool EditorDockWidget::event(QEvent* event)
4277 {
4278 switch (event->type())
4279 {
4280 case QEvent::ShortcutOverride:
4281 if (editor->getWindow()->shortcutsBlocked())
4282 event->accept();
4283 break;
4284
4285 case QEvent::Show:
4286 QTimer::singleShot(0, this, &QWidget::raise);
4287 break;
4288
4289 default:
4290 ; // nothing
4291 }
4292
4293 return QDockWidget::event(event);
4294 }
4295
resizeEvent(QResizeEvent * event)4296 void EditorDockWidget::resizeEvent(QResizeEvent* event)
4297 {
4298 #ifdef Q_OS_ANDROID
4299 int fw = style()->pixelMetric(QStyle::PM_DockWidgetFrameWidth, 0, this);
4300 size_grip->move(rect().bottomRight() - size_grip->rect().bottomRight() - QPoint{fw, fw});
4301 size_grip->raise();
4302
4303 if (isTopLevel())
4304 {
4305 auto const frame = frameGeometry();
4306 auto const old_frame = QRect{pos(), event->oldSize() + frame.size() - size()};
4307 if (old_frame.united(frame) != frame)
4308 editor->getMainWidget()->window()->update();
4309 }
4310 #endif
4311
4312 QDockWidget::resizeEvent(event);
4313 }
4314
4315
4316
4317 // ### MapEditorToolAction ###
4318
MapEditorToolAction(const QIcon & icon,const QString & text,QObject * parent)4319 MapEditorToolAction::MapEditorToolAction(const QIcon& icon, const QString& text, QObject* parent)
4320 : QAction(icon, text, parent)
4321 {
4322 setCheckable(true);
4323 setMenuRole(QAction::NoRole);
4324 connect(this, &QAction::triggered, this, &MapEditorToolAction::triggeredImpl);
4325 }
4326
triggeredImpl(bool checked)4327 void MapEditorToolAction::triggeredImpl(bool checked)
4328 {
4329 if (checked)
4330 emit activated();
4331 else
4332 setChecked(true);
4333 }
4334
4335
4336 } // namespace OpenOrienteering
4337