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