1 /*
2  *    Copyright 2012, 2013 Thomas Schöps
3  *    Copyright 2014-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 "symbol_render_widget.h"
23 
24 #include <QApplication>
25 #include <QBuffer>
26 #include <QClipboard>
27 #include <QDrag>
28 #include <QInputDialog>
29 #include <QMenu>
30 #include <QMessageBox>
31 #include <QMimeData>
32 #include <QPainter>
33 #include <QPaintEvent>
34 #include <QResizeEvent>
35 #include <QScopedValueRollback>
36 
37 #include "settings.h"
38 #include "core/map.h"
39 #include "core/objects/object.h"
40 #include "core/symbols/area_symbol.h"
41 #include "core/symbols/combined_symbol.h"
42 #include "core/symbols/line_symbol.h"
43 #include "core/symbols/point_symbol.h"
44 #include "core/symbols/symbol.h"
45 #include "core/symbols/symbol_icon_decorator.h"
46 #include "core/symbols/text_symbol.h"
47 #include "gui/symbols/symbol_setting_dialog.h"
48 #include "gui/widgets/symbol_tooltip.h"
49 #include "util/backports.h"  // IWYU pragma: keep
50 #include "util/overriding_shortcut.h"
51 
52 
53 namespace OpenOrienteering {
54 
55 namespace MimeType {
56 
57 /// The index of a symbol during drag-and-drop
OpenOrienteeringSymbolIndex()58 const QString OpenOrienteeringSymbolIndex()
59 {
60 	return QStringLiteral("openorienteering/symbol_index");
61 }
62 
63 /// Symbol definitions
OpenOrienteeringSymbols()64 const QString OpenOrienteeringSymbols()
65 {
66 	return QStringLiteral("openorienteering/symbols");
67 }
68 
69 
70 }  // namespace MimeType
71 
72 
73 
74 //### SymbolRenderWidget ###
75 
SymbolRenderWidget(Map * map,bool mobile_mode,QWidget * parent)76 SymbolRenderWidget::SymbolRenderWidget(Map* map, bool mobile_mode, QWidget* parent)
77 : QWidget(parent)
78 , map(map)
79 , mobile_mode(mobile_mode)
80 , selection_locked(false)
81 , dragging(false)
82 , current_symbol_index(-1)
83 , hover_symbol_index(-1)
84 , last_drop_pos(-1)
85 , last_drop_row(-1)
86 , icon_size(Settings::getInstance().getSymbolWidgetIconSizePx())
87 , icons_per_row(6)
88 , num_rows(5)
89 , preferred_size(icons_per_row * icon_size, num_rows * icon_size)
90 , hidden_symbol_decoration(new HiddenSymbolDecorator(icon_size))
91 , protected_symbol_decoration(new ProtectedSymbolDecorator(icon_size))
92 {
93 	setBackgroundRole(QPalette::Base);
94 	setMouseTracking(true);
95 	setFocusPolicy(Qt::ClickFocus);
96 	setAcceptDrops(true);
97 
98 	QShortcut* description_shortcut = new OverridingShortcut(
99 	  QKeySequence(tr("F1", "Shortcut for displaying the symbol's description")),
100 	  this
101 	);
102 	tooltip = new SymbolToolTip(this, description_shortcut);
103 	// TODO: Use a placeholder in the literal and pass the actual shortcut's string representation.
104 	setStatusTip(tr("For symbols with description, press F1 while the tooltip is visible to show it"));
105 
106 	context_menu = new QMenu(this);
107 
108 	QMenu* new_menu = new QMenu(tr("New symbol"), context_menu);
109 	new_menu->setIcon(QIcon(QStringLiteral(":/images/plus.png")));
110 	/*QAction* new_point_action =*/ new_menu->addAction(tr("Point"), this, SLOT(newPointSymbol()));
111 	/*QAction* new_line_action =*/ new_menu->addAction(tr("Line"), this, SLOT(newLineSymbol()));
112 	/*QAction* new_area_action =*/ new_menu->addAction(tr("Area"), this, SLOT(newAreaSymbol()));
113 	/*QAction* new_text_action =*/ new_menu->addAction(tr("Text"), this, SLOT(newTextSymbol()));
114 	/*QAction* new_combined_action =*/ new_menu->addAction(tr("Combined"), this, SLOT(newCombinedSymbol()));
115 	context_menu->addMenu(new_menu);
116 
117 	edit_action = context_menu->addAction(tr("Edit"), this, SLOT(editSymbol()));
118 	duplicate_action = context_menu->addAction(QIcon(QStringLiteral(":/images/tool-duplicate.png")), tr("Duplicate"), this, SLOT(duplicateSymbol()));
119 	delete_action = context_menu->addAction(QIcon(QStringLiteral(":/images/minus.png")), tr("Delete"), this, SLOT(deleteSymbols()));
120 	scale_action = context_menu->addAction(QIcon(QStringLiteral(":/images/tool-scale.png")), tr("Scale..."), this, SLOT(scaleSymbol()));
121 	context_menu->addSeparator();
122 	copy_action = context_menu->addAction(QIcon(QStringLiteral(":/images/copy.png")), tr("Copy"), this, SLOT(copySymbols()));
123 	paste_action = context_menu->addAction(QIcon(QStringLiteral(":/images/paste.png")), tr("Paste"), this, SLOT(pasteSymbols()));
124 	context_menu->addSeparator();
125 	switch_symbol_action = context_menu->addAction(QIcon(QStringLiteral(":/images/tool-switch-symbol.png")), tr("Switch symbol of selected objects"), this, SIGNAL(switchSymbolClicked()));
126 	fill_border_action = context_menu->addAction(QIcon(QStringLiteral(":/images/tool-fill-border.png")), tr("Fill / Create border for selected objects"), this, SIGNAL(fillBorderClicked()));
127 	// text will be filled in by updateContextMenuState()
128 	select_objects_action = context_menu->addAction(QIcon(QStringLiteral(":/images/tool-edit.png")), {}, this, SLOT(selectObjectsExclusively()));
129 	select_objects_additionally_action = context_menu->addAction(QIcon(QStringLiteral(":/images/tool-edit.png")), {}, this, SLOT(selectObjectsAdditionally()));
130 	deselect_objects_action = context_menu->addAction(QIcon(QStringLiteral(":/images/tool-edit.png")), {}, this, SLOT(deselectObjects()));
131 	context_menu->addSeparator();
132 	hide_action = context_menu->addAction({}, this, SLOT(setSelectedSymbolVisibility(bool)));
133 	hide_action->setCheckable(true);
134 	protect_action = context_menu->addAction({}, this, SLOT(setSelectedSymbolProtection(bool)));
135 	protect_action->setCheckable(true);
136 	context_menu->addSeparator();
137 
138 	show_custom_icons = context_menu->addAction(tr("Show custom icons"), this, SLOT(setCustomIconsVisible(bool)));
139 	show_custom_icons->setCheckable(true);
140 
141 	QMenu* select_menu = new QMenu(tr("Select symbols"), context_menu);
142 	select_menu->addAction(tr("Select all"), this, SLOT(selectAll()));
143 	select_menu->addAction(tr("Select unused"), this, SLOT(selectUnused()));
144 	select_menu->addSeparator();
145 	select_menu->addAction(tr("Invert selection"), this, SLOT(invertSelection()));
146 	context_menu->addMenu(select_menu);
147 
148 	QMenu* sort_menu = new QMenu(tr("Sort symbols"), context_menu);
149 	sort_menu->addAction(tr("Sort by number"), this, SLOT(sortByNumber()));
150 	sort_menu->addAction(tr("Sort by primary color"), this, SLOT(sortByColor()));
151 	sort_menu->addAction(tr("Sort by primary color priority"), this, SLOT(sortByColorPriority()));
152 	sort_manual_action = sort_menu->addAction(tr("Enable drag and drop"));
153 	sort_manual_action->setCheckable(true);
154 	context_menu->addMenu(sort_menu);
155 
156 	connect(map, &Map::colorDeleted, this, QOverload<>::of(&QWidget::update));
157 	connect(map, &Map::symbolAdded, this, &SymbolRenderWidget::updateAll);
158 	connect(map, &Map::symbolDeleted, this, &SymbolRenderWidget::symbolDeleted);
159 	connect(map, &Map::symbolChanged, this, &SymbolRenderWidget::symbolChanged);
160 	connect(map, &Map::symbolIconChanged, this, &SymbolRenderWidget::updateSingleIcon);
161 	connect(map, &Map::symbolIconZoomChanged, this, &SymbolRenderWidget::updateAll);
162 	connect(&Settings::getInstance(), &Settings::settingsChanged, this, &SymbolRenderWidget::settingsChanged);
163 }
164 
~SymbolRenderWidget()165 SymbolRenderWidget::~SymbolRenderWidget()
166 {
167 	; // nothing
168 }
169 
symbolDeleted(int pos,const Symbol * old_symbol)170 void SymbolRenderWidget::symbolDeleted(int pos, const Symbol *old_symbol)
171 {
172 	Q_UNUSED(old_symbol);
173 
174 	std::set<int>::iterator it = selected_symbols.find(pos);
175 	if (it != selected_symbols.end())
176 	{
177 		// Remove pos
178 		std::set<int>::iterator new_it = it;
179 		++new_it;
180 		selected_symbols.erase(it);
181 		// Renumber successors
182 		for (it = new_it; it != selected_symbols.end(); it = ++new_it)
183 		{
184 				new_it = selected_symbols.insert(*it - 1).first;
185 				selected_symbols.erase(it);
186 		}
187 		emitGuarded_selectedSymbolsChanged();
188 	}
189 
190 	adjustLayout();
191 	update();
192 }
193 
symbolChanged(int pos,const Symbol * new_symbol,const Symbol * old_symbol)194 void SymbolRenderWidget::symbolChanged(int pos, const Symbol* new_symbol, const Symbol* old_symbol)
195 {
196 	Q_UNUSED(new_symbol);
197 	Q_UNUSED(old_symbol);
198 
199 	updateSingleIcon(pos);
200 
201 	if (isSymbolSelected(pos))
202 		emitGuarded_selectedSymbolsChanged();
203 }
204 
updateAll()205 void SymbolRenderWidget::updateAll()
206 {
207 	adjustLayout();
208 	update();
209 }
210 
updateSingleIcon(int i)211 void SymbolRenderWidget::updateSingleIcon(int i)
212 {
213 	if (i >= 0)
214 	{
215 		QPoint pos = iconPosition(i);
216 		update(pos.x(), pos.y(), icon_size, icon_size);
217 	}
218 }
219 
settingsChanged()220 void SymbolRenderWidget::settingsChanged()
221 {
222 	const auto new_size = Settings::getInstance().getSymbolWidgetIconSizePx();
223 	if (icon_size != new_size)
224 	{
225 		for (int i = 0; i < map->getNumSymbols(); ++i)
226 		{
227 			auto symbol = map->getSymbol(i);
228 			if (symbol->getIcon(map).width() != new_size)
229 				symbol->resetIcon();
230 		}
231 		updateAll();
232 	}
233 }
234 
235 
236 
sizeHint() const237 QSize SymbolRenderWidget::sizeHint() const
238 {
239 	return preferred_size;
240 }
241 
adjustLayout()242 void SymbolRenderWidget::adjustLayout()
243 {
244 	auto old_icon_size = icon_size;
245 	icon_size = Settings::getInstance().getSymbolWidgetIconSizePx() + 1;
246 	// Allow symbol widget to be that much wider than the viewport
247 	int overflow = icon_size / 3;
248 	icons_per_row = qMax(1, (width() + overflow) / icon_size);
249 	num_rows = qMax(1, (map->getNumSymbols() + icons_per_row -1) / icons_per_row);
250 	setFixedHeight(num_rows * icon_size);
251 
252 	if (old_icon_size != icon_size)
253 	{
254 		hidden_symbol_decoration.reset(new HiddenSymbolDecorator(icon_size));
255 		protected_symbol_decoration.reset(new ProtectedSymbolDecorator(icon_size));
256 	}
257 }
258 
emitGuarded_selectedSymbolsChanged()259 void SymbolRenderWidget::emitGuarded_selectedSymbolsChanged()
260 {
261 	QScopedValueRollback<bool> save(selection_locked);
262 	selection_locked = true;
263 	emit selectedSymbolsChanged();
264 }
265 
singleSelectedSymbol() const266 const Symbol* SymbolRenderWidget::singleSelectedSymbol() const
267 {
268 	if (selected_symbols.size() != 1)
269 		return nullptr;
270 	return static_cast<const Map*>(map)->getSymbol(*(selected_symbols.begin()));
271 }
272 
singleSelectedSymbol()273 Symbol* SymbolRenderWidget::singleSelectedSymbol()
274 {
275 	if (selected_symbols.size() != 1)
276 		return nullptr;
277 	return map->getSymbol(*(selected_symbols.begin()));
278 }
279 
isSymbolSelected(const Symbol * symbol) const280 bool SymbolRenderWidget::isSymbolSelected(const Symbol* symbol) const
281 {
282 	return isSymbolSelected(map->findSymbolIndex(symbol));
283 }
284 
isSymbolSelected(int i) const285 bool SymbolRenderWidget::isSymbolSelected(int i) const
286 {
287 	return selected_symbols.find(i) != selected_symbols.end();
288 }
289 
selectSingleSymbol(const Symbol * symbol)290 void SymbolRenderWidget::selectSingleSymbol(const Symbol *symbol)
291 {
292 	int index = map->findSymbolIndex(symbol);
293     if (index >= 0)
294 		selectSingleSymbol(index);
295 }
296 
selectSingleSymbol(int i)297 void SymbolRenderWidget::selectSingleSymbol(int i)
298 {
299 	if (selection_locked)
300 		return;
301 
302 	if (selected_symbols.size() == 1 && *selected_symbols.begin() == i)
303 		return;	// Symbol already selected as only selection
304 
305 	updateSelectedIcons();
306 	selected_symbols.clear();
307 	if (i >= 0)
308 	{
309 		selected_symbols.insert(i);
310 		updateSingleIcon(i);
311 	}
312 	current_symbol_index = i;
313 
314 	emitGuarded_selectedSymbolsChanged();
315 }
316 
hover(const QPoint & pos)317 void SymbolRenderWidget::hover(const QPoint& pos)
318 {
319 	int i = symbolIndexAt(pos);
320 
321 	if (i != hover_symbol_index)
322 	{
323 		updateSingleIcon(hover_symbol_index);
324 		hover_symbol_index = i;
325 		updateSingleIcon(hover_symbol_index);
326 
327 		if (hover_symbol_index >= 0)
328 		{
329 			Symbol* symbol = map->getSymbol(hover_symbol_index);
330 			if (tooltip->getSymbol() != symbol)
331 			{
332 				const QRect icon_rect(mapToGlobal(iconPosition(hover_symbol_index)), QSize(icon_size, icon_size));
333 				tooltip->scheduleShow(symbol, map, icon_rect, mobile_mode);
334 			}
335 		}
336 		else
337 		{
338 			tooltip->reset();
339 		}
340 	}
341 }
342 
iconPosition(int i) const343 QPoint SymbolRenderWidget::iconPosition(int i) const
344 {
345 	return QPoint((i % icons_per_row) * icon_size, (i / icons_per_row) * icon_size);
346 }
347 
symbolIndexAt(const QPoint & pos) const348 int SymbolRenderWidget::symbolIndexAt(const QPoint& pos) const
349 {
350 	int i = -1;
351 
352 	int icon_in_row = pos.x() / icon_size;
353 	if (icon_in_row < icons_per_row)
354 	{
355 		i = icon_in_row + icons_per_row * (pos.y() / icon_size);
356 		if (i >= map->getNumSymbols())
357 			i = -1;
358 	}
359 
360 	return i;
361 }
362 
updateSelectedIcons()363 void SymbolRenderWidget::updateSelectedIcons()
364 {
365 	for (auto symbol_index : selected_symbols)
366 		updateSingleIcon(symbol_index);
367 }
368 
dropPosition(const QPoint & pos,int & row,int & pos_in_row)369 bool SymbolRenderWidget::dropPosition(const QPoint& pos, int& row, int& pos_in_row)
370 {
371 	row = pos.y() / icon_size;
372 	if (row >= num_rows)
373 	{
374 		row = -1;
375 		pos_in_row = -1;
376 		return false;
377 	}
378 
379 	pos_in_row = (pos.x() + icon_size / 2) / icon_size;
380 	if (pos_in_row > icons_per_row)
381 	{
382 		pos_in_row = icons_per_row;
383 	}
384 
385 	int global_pos = row * icons_per_row + pos_in_row;
386 	if (global_pos > map->getNumSymbols())
387 	{
388 		row = -1;
389 		pos_in_row = -1;
390 		return false;
391 	}
392 
393 	return true;
394 }
395 
dropIndicatorRect(int row,int pos_in_row)396 QRect SymbolRenderWidget::dropIndicatorRect(int row, int pos_in_row)
397 {
398 	const int rect_width = 3;
399 	return QRect(pos_in_row * icon_size - rect_width / 2 - 1, row * icon_size - 1, rect_width, icon_size + 2);
400 }
401 
drawIcon(QPainter & painter,int i) const402 void SymbolRenderWidget::drawIcon(QPainter &painter, int i) const
403 {
404 	painter.save();
405 
406 	Symbol* symbol = map->getSymbol(i);
407 	painter.drawImage(0, 0, symbol->getIcon(map));
408 
409 	if (isSymbolSelected(i) || i == current_symbol_index)
410 	{
411 		painter.setOpacity(0.5);
412 		QPen pen(Qt::white);
413 		pen.setWidth(2);
414 		pen.setJoinStyle(Qt::MiterJoin);
415 		painter.setPen(pen);
416 		painter.drawRect(QRectF(1.5f, 1.5f, icon_size - 5, icon_size - 5));
417 
418 		painter.setOpacity(1.0);
419 	}
420 
421 	if (symbol->isProtected())
422 		protected_symbol_decoration->draw(painter);
423 
424 	if (symbol->isHidden())
425 		hidden_symbol_decoration->draw(painter);
426 
427 	if (isSymbolSelected(i))
428 	{
429 		QPen pen(qRgb(12, 0, 255));
430 		pen.setWidth(2);
431 		pen.setJoinStyle(Qt::MiterJoin);
432 		painter.setPen(pen);
433 		painter.drawRect(QRectF(0.5f, 0.5f, icon_size - 3, icon_size - 3));
434 
435 		if (i == hover_symbol_index)
436 		{
437 			QPen pen(qRgb(255, 150, 0));
438 			pen.setWidth(2);
439 			pen.setDashPattern(QVector<qreal>() << 1 << 2);
440 			pen.setJoinStyle(Qt::MiterJoin);
441 			painter.setPen(pen);
442 			painter.drawRect(QRectF(0.5f, 0.5f, icon_size - 3, icon_size - 3));
443 		}
444 		else if (i == current_symbol_index)
445 		{
446 			QPen pen(Qt::white);
447 			pen.setDashPattern(QVector<qreal>() << 2 << 2);
448 			painter.setPen(pen);
449 			painter.drawRect(1, 1, icon_size - 4, icon_size - 4);
450 		}
451 	}
452 	else if (i == hover_symbol_index)
453 	{
454 		QPen pen(qRgb(255, 150, 0));
455 		pen.setWidth(2);
456 		pen.setJoinStyle(Qt::MiterJoin);
457 		painter.setPen(pen);
458 		painter.drawRect(QRectF(0.5f, 0.5f, icon_size - 3, icon_size - 3));
459 	}
460 
461 	painter.setPen(Qt::gray);
462 	painter.drawLine(QPoint(0, icon_size - 1), QPoint(icon_size - 1, icon_size - 1));
463 	painter.drawLine(QPoint(icon_size - 1, 0), QPoint(icon_size - 1, icon_size - 2));
464 
465 	painter.restore();
466 }
467 
paintEvent(QPaintEvent * event)468 void SymbolRenderWidget::paintEvent(QPaintEvent* event)
469 {
470 	QRect event_rect = event->rect().adjusted(-icon_size, -icon_size, 0, 0);
471 
472 	QPainter painter(this);
473 	painter.setPen(Qt::gray);
474 
475 	int x = 0;
476 	int y = 0;
477 	for (int i = 0; i < map->getNumSymbols(); ++i)
478 	{
479 		if (event_rect.contains(x,y))
480 		{
481 			painter.save();
482 			painter.translate(x, y);
483 			drawIcon(painter, i);
484 			painter.restore();
485 		}
486 
487 		x += icon_size;
488 		if (x >= icons_per_row * icon_size)
489 		{
490 			x = 0;
491 			y +=  icon_size;
492 		}
493 	}
494 
495 	// Drop indicator?
496 	if (last_drop_pos >= 0 && last_drop_row >= 0)
497 	{
498 		QRect drop_rect = dropIndicatorRect(last_drop_row, last_drop_pos);
499 		painter.setPen(qRgb(255, 150, 0));
500 		painter.setBrush(Qt::NoBrush);
501 		painter.drawRect(drop_rect.left(), drop_rect.top(), drop_rect.width() - 1, drop_rect.height() - 1);
502 	}
503 
504 	painter.end();
505 }
506 
resizeEvent(QResizeEvent * event)507 void SymbolRenderWidget::resizeEvent(QResizeEvent* event)
508 {
509 	if (width() != event->oldSize().width())
510 	{
511 		adjustLayout();
512 	}
513 }
514 
mouseMoveEvent(QMouseEvent * event)515 void SymbolRenderWidget::mouseMoveEvent(QMouseEvent* event)
516 {
517 	if (mobile_mode)
518 	{
519 		if (event->buttons() & Qt::LeftButton)
520 		{
521 			hover(event->pos());
522 
523 			if ((event->pos() - last_click_pos).manhattanLength() < Settings::getInstance().getStartDragDistancePx())
524 				return;
525 			dragging = sort_manual_action->isChecked();
526 		}
527 	}
528 	else
529 	{
530 		if (event->buttons() & Qt::LeftButton
531 		    && current_symbol_index >= 0
532 		    && sort_manual_action->isChecked())
533 		{
534 			if ((event->pos() - last_click_pos).manhattanLength() < Settings::getInstance().getStartDragDistancePx())
535 				return;
536 
537 			tooltip->hide();
538 
539 			QDrag* drag = new QDrag(this);
540 			QMimeData* mime_data = new QMimeData();
541 
542 			QByteArray data;
543 			data.append((const char*)&current_symbol_index, sizeof(int));
544 			mime_data->setData(MimeType::OpenOrienteeringSymbolIndex(), data);
545 			drag->setMimeData(mime_data);
546 
547 			drag->exec(Qt::MoveAction);
548 		}
549 		else if (event->button() == Qt::NoButton)
550 		{
551 			hover(event->pos());
552 		}
553 	}
554 	event->accept();
555 }
556 
mousePressEvent(QMouseEvent * event)557 void SymbolRenderWidget::mousePressEvent(QMouseEvent* event)
558 {
559 	dragging = false;
560 
561 	if (mobile_mode)
562 	{
563 		tooltip->hide();
564 
565 		last_click_pos = event->pos();
566 		hover(event->pos());
567 		return;
568 	}
569 
570 	updateSingleIcon(current_symbol_index);
571 	int old_symbol_index = current_symbol_index;
572 	current_symbol_index = symbolIndexAt(event->pos());
573 	updateSingleIcon(current_symbol_index);
574 
575 	if (event->button() == Qt::LeftButton || event->button() == Qt::RightButton)
576 	{
577 		if (event->modifiers() & Qt::ControlModifier)
578 		{
579 			if (current_symbol_index >= 0)
580 			{
581 				if (!isSymbolSelected(current_symbol_index))
582 					selected_symbols.insert(current_symbol_index);
583 				else
584 					selected_symbols.erase(current_symbol_index);
585 				emitGuarded_selectedSymbolsChanged();
586 			}
587 		}
588 		else if (event->modifiers() & Qt::ShiftModifier)
589 		{
590 			if (current_symbol_index >= 0)
591 			{
592 				bool insert = !isSymbolSelected(current_symbol_index);
593 				int i = (old_symbol_index >= 0) ? old_symbol_index : current_symbol_index;
594 				while (true)
595 				{
596 					if (insert)
597 						selected_symbols.insert(i);
598 					else
599 						selected_symbols.erase(i);
600 					updateSingleIcon(i);
601 
602 					if (current_symbol_index > i)
603 						++i;
604 					else if (current_symbol_index < i)
605 						--i;
606 					else
607 						break;
608 				}
609 				emitGuarded_selectedSymbolsChanged();
610 			}
611 		}
612 		else
613 		{
614 			if (event->button() == Qt::LeftButton ||
615 				(event->button() == Qt::RightButton &&
616 				current_symbol_index >= 0 && !isSymbolSelected(current_symbol_index)))
617 				selectSingleSymbol(current_symbol_index);
618 		}
619 	}
620 
621 	else if (event->button() == Qt::LeftButton && current_symbol_index >= 0 && !(event->modifiers() & Qt::ShiftModifier))
622 	{
623 		last_click_pos = event->pos();
624 		last_drop_pos = -1;
625 		last_drop_row = -1;
626 	}
627 }
628 
mouseReleaseEvent(QMouseEvent * event)629 void SymbolRenderWidget::mouseReleaseEvent(QMouseEvent* event)
630 {
631 	if (mobile_mode)
632 	{
633 		if (dragging)
634 		{
635 			dragging = false;
636 			return;
637 		}
638 
639 		hover(event->pos());
640 		if (tooltip->isVisible())
641 			return;
642 
643 		updateSingleIcon(current_symbol_index);
644 		current_symbol_index = symbolIndexAt(event->pos());
645 		updateSingleIcon(current_symbol_index);
646 
647 		if (current_symbol_index >= 0 && !isSymbolSelected(current_symbol_index))
648 			selectSingleSymbol(current_symbol_index);
649 		else
650 			emitGuarded_selectedSymbolsChanged(); // HACK, will close the symbol selection screen
651 	}
652 }
653 
mouseDoubleClickEvent(QMouseEvent * event)654 void SymbolRenderWidget::mouseDoubleClickEvent(QMouseEvent* event)
655 {
656 	if (mobile_mode)
657 		return;
658 
659     int i = symbolIndexAt(event->pos());
660 	if (i < 0)
661 		return;
662 
663 	updateSingleIcon(current_symbol_index);
664 	current_symbol_index = i;
665 	updateSingleIcon(current_symbol_index);
666 	editSymbol();
667 }
668 
leaveEvent(QEvent * event)669 void SymbolRenderWidget::leaveEvent(QEvent* event)
670 {
671 	Q_UNUSED(event);
672 	updateSingleIcon(hover_symbol_index);
673 	hover_symbol_index = -1;
674 
675 	tooltip->reset();
676 }
677 
dragEnterEvent(QDragEnterEvent * event)678 void SymbolRenderWidget::dragEnterEvent(QDragEnterEvent* event)
679 {
680 	if (event->mimeData()->hasFormat(MimeType::OpenOrienteeringSymbolIndex()))
681 		event->acceptProposedAction();
682 }
683 
dragMoveEvent(QDragMoveEvent * event)684 void SymbolRenderWidget::dragMoveEvent(QDragMoveEvent* event)
685 {
686 	if (event->mimeData()->hasFormat(MimeType::OpenOrienteeringSymbolIndex()))
687 	{
688 		int row, pos_in_row;
689 		if (!dropPosition(event->pos(), row, pos_in_row))
690 		{
691 			if (last_drop_pos >= 0 && last_drop_row >= 0)
692 			{
693 				update(dropIndicatorRect(last_drop_row, last_drop_pos));
694 				last_drop_pos = -1;
695 				last_drop_row = -1;
696 			}
697 			return;
698 		}
699 
700 		if (row != last_drop_row || pos_in_row != last_drop_pos)
701 		{
702 			if (last_drop_row >= 0 && last_drop_pos >= 0)
703 				update(dropIndicatorRect(last_drop_row, last_drop_pos));
704 
705 			last_drop_row = row;
706 			last_drop_pos = pos_in_row;
707 
708 			if (last_drop_row >= 0 && last_drop_pos >= 0)
709 				update(dropIndicatorRect(last_drop_row, last_drop_pos));
710 		}
711 
712 		event->acceptProposedAction();
713 	}
714 }
715 
dropEvent(QDropEvent * event)716 void SymbolRenderWidget::dropEvent(QDropEvent* event)
717 {
718 	last_drop_row = -1;
719 	last_drop_pos = -1;
720 
721 	if (event->source() != this || current_symbol_index < 0)
722 		return;
723 
724 	if (event->proposedAction() == Qt::MoveAction)
725 	{
726 		int row, pos_in_row;
727 		if (!dropPosition(event->pos(), row, pos_in_row))
728 			return;
729 
730 		int pos = row * icons_per_row + pos_in_row;
731 		if (pos == current_symbol_index)
732 			return;
733 
734 		event->acceptProposedAction();
735 
736         // save selection
737         std::set<Symbol *> sel;
738         for (auto symbol_index : selected_symbols) {
739             sel.insert(map->getSymbol(symbol_index));
740         }
741 
742 		map->moveSymbol(current_symbol_index, pos);
743 		update();
744 
745 
746 		if (pos > current_symbol_index)
747 			--pos;
748 		current_symbol_index = pos;
749 		selectSingleSymbol(pos);
750 	}
751 }
752 
selectObjectsExclusively()753 void SymbolRenderWidget::selectObjectsExclusively()
754 {
755 	emit selectObjectsClicked(true);
756 }
757 
selectObjectsAdditionally()758 void SymbolRenderWidget::selectObjectsAdditionally()
759 {
760 	emit selectObjectsClicked(false);
761 }
762 
deselectObjects()763 void SymbolRenderWidget::deselectObjects()
764 {
765 	emit deselectObjectsClicked();
766 }
767 
newPointSymbol()768 void SymbolRenderWidget::newPointSymbol()
769 {
770 	newSymbol(new PointSymbol());
771 }
772 
newLineSymbol()773 void SymbolRenderWidget::newLineSymbol()
774 {
775 	newSymbol(new LineSymbol());
776 }
777 
newAreaSymbol()778 void SymbolRenderWidget::newAreaSymbol()
779 {
780 	newSymbol(new AreaSymbol());
781 }
782 
newTextSymbol()783 void SymbolRenderWidget::newTextSymbol()
784 {
785 	newSymbol(new TextSymbol());
786 }
787 
newCombinedSymbol()788 void SymbolRenderWidget::newCombinedSymbol()
789 {
790 	newSymbol(new CombinedSymbol());
791 }
792 
editSymbol()793 void SymbolRenderWidget::editSymbol()
794 {
795 	Q_ASSERT(current_symbol_index >= 0);
796 
797 	Symbol* symbol = map->getSymbol(current_symbol_index);
798 	SymbolSettingDialog dialog(symbol, map, this);
799 	dialog.setWindowModality(Qt::WindowModal);
800 	if (dialog.exec() == QDialog::Accepted)
801 	{
802 		map->setSymbol(dialog.getNewSymbol().release(), current_symbol_index);
803 	}
804 }
805 
scaleSymbol()806 void SymbolRenderWidget::scaleSymbol()
807 {
808 	Q_ASSERT(!selected_symbols.empty());
809 
810 	bool ok;
811 	double percent = QInputDialog::getDouble(this, tr("Scale symbols"), tr("Scale to percentage:"), 100, 0, 999999, 6, &ok);
812 	if (!ok || percent == 100)
813 		return;
814 
815 	for (auto symbol_index : selected_symbols)
816 	{
817 		auto symbol = map->getSymbol(symbol_index);
818 		symbol->scale(percent / 100.0);
819 		updateSingleIcon(current_symbol_index);
820 		map->changeSymbolForAllObjects(symbol, symbol);	// update the objects
821 	}
822 
823 	map->setSymbolsDirty();
824 }
825 
deleteSymbols()826 void SymbolRenderWidget::deleteSymbols()
827 {
828 	// Collect selected symbols
829 	std::vector<const Symbol*> saved_selection;
830 	saved_selection.reserve(selected_symbols.size());
831 
832 	bool need_confirmation = true;
833 	for (auto symbol_index : selected_symbols)
834 	{
835 		auto* symbol = map->getSymbol(symbol_index);
836 		if (need_confirmation && map->existsObjectWithSymbol(symbol))
837 		{
838 			auto answer = QMessageBox::warning(
839 			                  this,
840 			                  tr("Confirmation"),
841 			                  tr("The map contains objects with the symbol \"%1\". "
842 			                     "Deleting it will delete those objects and clear the undo history! "
843 			                     "Do you really want to do that?")
844 			                  .arg(symbol->getName()),
845 			                  QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No | QMessageBox::Cancel);
846 			switch (answer)
847 			{
848 			case QMessageBox::Cancel:
849 				return;
850 			case QMessageBox::YesToAll:
851 				need_confirmation = false;
852 				break;
853 			case QMessageBox::Yes:
854 				break;
855 			case QMessageBox::No:
856 				continue;
857 			default:
858 				Q_UNREACHABLE();
859 			}
860 		}
861 		saved_selection.push_back(symbol);
862 	}
863 
864 	// Delete collected symbols
865 	for (auto* symbol : saved_selection)
866 		map->deleteSymbol(map->findSymbolIndex(symbol));
867 
868 	if (selected_symbols.empty()) {
869 		if (map->getFirstSelectedObject())
870 			selectSingleSymbol(map->getFirstSelectedObject()->getSymbol());
871 		else
872 			selectSingleSymbol(-1);
873 	}
874 }
875 
duplicateSymbol()876 void SymbolRenderWidget::duplicateSymbol()
877 {
878 	Q_ASSERT(current_symbol_index >= 0);
879 
880 	map->addSymbol(duplicate(*map->getSymbol(current_symbol_index)).release(), current_symbol_index + 1);
881 	selectSingleSymbol(current_symbol_index + 1);
882 }
883 
copySymbols()884 void SymbolRenderWidget::copySymbols()
885 {
886 	// Create map containing all colors and the selected symbols.
887 	// Copying all colors improves preservation of relative order during paste.
888 	Map copy_map;
889 	copy_map.setScaleDenominator(map->getScaleDenominator());
890 	copy_map.importMap(*map, Map::ColorImport);
891 
892 	std::vector<bool> selection(map->getNumSymbols(), false);
893 	for (auto symbol_index : selected_symbols)
894 		selection[symbol_index] = true;
895 
896 	copy_map.importMap(*map, Map::MinimalSymbolImport, &selection);
897 
898 	// Save map to memory
899 	QBuffer buffer;
900 	if (!copy_map.exportToIODevice(buffer))
901 	{
902 		QMessageBox::warning(nullptr, tr("Error"), tr("An internal error occurred, sorry!"));
903 		return;
904 	}
905 
906 	// Put buffer into clipboard
907 	QMimeData* mime_data = new QMimeData();
908 	mime_data->setData(MimeType::OpenOrienteeringSymbols(), buffer.data());
909 	QApplication::clipboard()->setMimeData(mime_data);
910 }
911 
pasteSymbols()912 void SymbolRenderWidget::pasteSymbols()
913 {
914 	if (!QApplication::clipboard()->mimeData()->hasFormat(MimeType::OpenOrienteeringSymbols()))
915 	{
916 		QMessageBox::warning(nullptr, tr("Error"), tr("There are no symbols in clipboard which could be pasted!"));
917 		return;
918 	}
919 
920 	// Get buffer from clipboard
921 	QByteArray byte_array = QApplication::clipboard()->mimeData()->data(MimeType::OpenOrienteeringSymbols());
922 	QBuffer buffer(&byte_array);
923 
924 	// Create map from buffer
925 	Map paste_map;
926 	if (!paste_map.importFromIODevice(buffer))
927 	{
928 		QMessageBox::warning(nullptr, tr("Error"), tr("An internal error occurred, sorry!"));
929 		return;
930 	}
931 
932 	if (paste_map.getScaleDenominator() != map->getScaleDenominator())
933 	{
934 		/// \todo Adjust message to this particular action, and remove Map context.
935 		///       See also MapEditor::importMap.
936 		int answer = QMessageBox::question(
937 		                 window(),
938 		                 Map::tr("Question"),
939 		                 Map::tr("The scale of the imported data is 1:%1 "
940 		                         "which is different from this map's scale of 1:%2.\n\n"
941 		                         "Rescale the imported data?")
942 		                 .arg(QLocale().toString(paste_map.getScaleDenominator()),
943 		                      QLocale().toString(map->getScaleDenominator())),
944 		                 QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::Yes);
945 		if (answer == QMessageBox::Cancel)
946 			return;
947 
948 		paste_map.changeScale(map->getScaleDenominator(), 1.0, {0, 0}, answer == QMessageBox::Yes, false, false, false);
949 	}
950 
951 	// Ensure that a change in selection is detected
952 	selectSingleSymbol(-1);
953 
954 	// Import pasted map
955 	map->importMap(paste_map, Map::MinimalSymbolImport, nullptr, current_symbol_index, false);
956 
957 	selectSingleSymbol(current_symbol_index);
958 }
959 
setSelectedSymbolVisibility(bool checked)960 void SymbolRenderWidget::setSelectedSymbolVisibility(bool checked)
961 {
962 	bool selection_changed = false;
963 	for (auto symbol_index : selected_symbols)
964 	{
965 		auto symbol = map->getSymbol(symbol_index);
966 		if (symbol->isHidden() != checked)
967 		{
968 			symbol->setHidden(checked);
969 			updateSingleIcon(symbol_index);
970 			if (checked)
971 				selection_changed |= map->removeSymbolFromSelection(symbol, false);
972 		}
973 	}
974 	if (selection_changed)
975 		map->emitSelectionChanged();
976 	map->updateAllMapWidgets();
977 	map->setSymbolsDirty();
978 	emitGuarded_selectedSymbolsChanged();
979 }
980 
setSelectedSymbolProtection(bool checked)981 void SymbolRenderWidget::setSelectedSymbolProtection(bool checked)
982 {
983 	bool selection_changed = false;
984 	for (auto symbol_index : selected_symbols)
985 	{
986 		auto symbol = map->getSymbol(symbol_index);
987 		if (symbol->isProtected() != checked)
988 		{
989 			symbol->setProtected(checked);
990 			updateSingleIcon(symbol_index);
991 			if (checked)
992 				selection_changed |= map->removeSymbolFromSelection(symbol, false);
993 		}
994 	}
995 	if (selection_changed)
996 		map->emitSelectionChanged();
997 	map->setSymbolsDirty();
998 	emitGuarded_selectedSymbolsChanged();
999 }
1000 
selectAll()1001 void SymbolRenderWidget::selectAll()
1002 {
1003 	for (int i = 0; i < map->getNumSymbols(); ++i)
1004 		selected_symbols.insert(i);
1005 	emitGuarded_selectedSymbolsChanged();
1006 	update();
1007 }
1008 
selectUnused()1009 void SymbolRenderWidget::selectUnused()
1010 {
1011 	std::vector<bool> symbols_in_use;
1012 	map->determineSymbolsInUse(symbols_in_use);
1013 
1014 	updateSelectedIcons();
1015 	selected_symbols.clear();
1016 	for (size_t i = 0, end = symbols_in_use.size(); i < end; ++i)
1017 	{
1018 		if (!symbols_in_use[i])
1019 		{
1020 			selected_symbols.insert(i);
1021 			updateSingleIcon(i);
1022 		}
1023 	}
1024 	emitGuarded_selectedSymbolsChanged();
1025 }
1026 
invertSelection()1027 void SymbolRenderWidget::invertSelection()
1028 {
1029 	std::set<int> new_set;
1030 	for (int i = 0; i < map->getNumSymbols(); ++i)
1031 	{
1032 		if (!isSymbolSelected(i))
1033 			new_set.insert(i);
1034 	}
1035 	swap(selected_symbols, new_set);
1036 	emitGuarded_selectedSymbolsChanged();
1037 	update();
1038 }
1039 
sortByNumber()1040 void SymbolRenderWidget::sortByNumber()
1041 {
1042     sort(Symbol::lessByNumber);
1043 }
1044 
sortByColor()1045 void SymbolRenderWidget::sortByColor()
1046 {
1047 	sort(Symbol::lessByColor(map));
1048 }
1049 
sortByColorPriority()1050 void SymbolRenderWidget::sortByColorPriority()
1051 {
1052 	sort(Symbol::lessByColorPriority);
1053 }
1054 
setCustomIconsVisible(bool checked)1055 void SymbolRenderWidget::setCustomIconsVisible(bool checked)
1056 {
1057 	for (int i = 0; i < map->getNumSymbols(); ++i)
1058 	{
1059 		auto symbol = map->getSymbol(i);
1060 		if (!symbol->getCustomIcon().isNull())
1061 			symbol->resetIcon();
1062 	}
1063 	Settings::getInstance().setSetting(Settings::SymbolWidget_ShowCustomIcons, checked);
1064 }
1065 
showContextMenu(const QPoint & global_pos)1066 void SymbolRenderWidget::showContextMenu(const QPoint& global_pos)
1067 {
1068 	updateContextMenuState();
1069 	context_menu->popup(global_pos);
1070 }
1071 
contextMenuEvent(QContextMenuEvent * event)1072 void SymbolRenderWidget::contextMenuEvent(QContextMenuEvent* event)
1073 {
1074 	showContextMenu(event->globalPos());
1075 	event->accept();
1076 }
1077 
updateContextMenuState()1078 void SymbolRenderWidget::updateContextMenuState()
1079 {
1080 	bool have_selection = selectedSymbolsCount() > 0;
1081 	bool single_selection = selectedSymbolsCount() == 1 && current_symbol_index >= 0;
1082 	const Symbol* single_symbol = singleSelectedSymbol();
1083 	bool all_symbols_hidden = have_selection;
1084 	bool all_symbols_protected = have_selection;
1085 	for (auto symbol_index : selected_symbols)
1086 	{
1087 		if (!map->getSymbol(symbol_index)->isHidden())
1088 			all_symbols_hidden = false;
1089 		if (!map->getSymbol(symbol_index)->isProtected())
1090 			all_symbols_protected = false;
1091 		if (!all_symbols_hidden && !all_symbols_protected)
1092 			break;
1093 	}
1094 
1095 	bool single_symbol_compatible;
1096 	bool single_symbol_different;
1097 	map->getSelectionToSymbolCompatibility(single_symbol, single_symbol_compatible, single_symbol_different);
1098 
1099 	edit_action->setEnabled(single_selection);
1100 	scale_action->setEnabled(have_selection);
1101 	copy_action->setEnabled(have_selection);
1102 	paste_action->setEnabled(QApplication::clipboard()->mimeData()->hasFormat(MimeType::OpenOrienteeringSymbols()));
1103 	switch_symbol_action->setEnabled(single_symbol_compatible && single_symbol_different);
1104 	fill_border_action->setEnabled(single_symbol_compatible && single_symbol_different);
1105 	hide_action->setEnabled(have_selection);
1106 	hide_action->setChecked(all_symbols_hidden);
1107 	protect_action->setEnabled(have_selection);
1108 	protect_action->setChecked(all_symbols_protected);
1109 	duplicate_action->setEnabled(single_selection);
1110 	delete_action->setEnabled(have_selection);
1111 
1112 	if (single_selection)
1113 	{
1114 		select_objects_action->setText(tr("Select all objects with this symbol"));
1115 		select_objects_additionally_action->setText(tr("Add all objects with this symbol to selection"));
1116 		deselect_objects_action->setText(tr("Remove all objects with this symbol from selection"));
1117 		hide_action->setText(tr("Hide objects with this symbol"));
1118 		protect_action->setText(tr("Protect objects with this symbol"));
1119 	}
1120 	else
1121 	{
1122 		select_objects_action->setText(tr("Select all objects with selected symbols"));
1123 		select_objects_additionally_action->setText(tr("Add all objects with selected symbols to selection"));
1124 		deselect_objects_action->setText(tr("Remove all objects with selected symbols from selection"));
1125 		hide_action->setText(tr("Hide objects with selected symbols"));
1126 		protect_action->setText(tr("Protect objects with selected symbols"));
1127 	}
1128 	select_objects_action->setEnabled(have_selection);
1129 	select_objects_additionally_action->setEnabled(have_selection);
1130 	deselect_objects_action->setEnabled(have_selection);
1131 
1132 	show_custom_icons->setChecked(Settings::getInstance().getSetting(Settings::SymbolWidget_ShowCustomIcons).toBool());
1133 }
1134 
newSymbol(Symbol * prototype)1135 bool SymbolRenderWidget::newSymbol(Symbol* prototype)
1136 {
1137 	SymbolSettingDialog dialog(prototype, map, this);
1138 	dialog.setWindowModality(Qt::WindowModal);
1139 	delete prototype;
1140 	if (dialog.exec() == QDialog::Rejected)
1141 		return false;
1142 
1143 	int pos = (current_symbol_index >= 0) ? current_symbol_index : map->getNumSymbols();
1144 	map->addSymbol(dialog.getNewSymbol().release(), pos);
1145 	// Ensure that a change in selection is detected
1146 	selectSingleSymbol(-1);
1147 	selectSingleSymbol(pos);
1148 	return true;
1149 }
1150 
1151 template<typename T>
sort(T compare)1152 void SymbolRenderWidget::sort(T compare)
1153 {
1154 	// save selection
1155 	std::set<const Symbol*> saved_selection;
1156 	for (auto symbol_index : selected_symbols)
1157 	{
1158 		saved_selection.insert(map->getSymbol(symbol_index));
1159 	}
1160 
1161 	map->sortSymbols(compare);
1162 
1163 	// restore selection
1164 	selected_symbols.clear();
1165 	for (int i = 0; i < map->getNumSymbols(); ++i)
1166 	{
1167 		if (saved_selection.find(map->getSymbol(i)) != saved_selection.end())
1168 			selected_symbols.insert(i);
1169 	}
1170 
1171 	update();
1172 }
1173 
1174 
1175 }  // namespace OpenOrienteering
1176