1 /*
2 * Copyright (C) 2014-2018 Christopho, Solarus - http://www.solarus-games.org
3 *
4 * Solarus Quest Editor is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * Solarus Quest Editor is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17 #include "widgets/enum_menus.h"
18 #include "widgets/gui_tools.h"
19 #include "widgets/pan_tool.h"
20 #include "widgets/tileset_scene.h"
21 #include "widgets/tileset_view.h"
22 #include "widgets/zoom_tool.h"
23 #include "ground_traits.h"
24 #include "pattern_separation_traits.h"
25 #include "point.h"
26 #include "rectangle.h"
27 #include "tileset_model.h"
28 #include "view_settings.h"
29 #include <QAction>
30 #include <QApplication>
31 #include <QDrag>
32 #include <QGraphicsItem>
33 #include <QMenu>
34 #include <QMimeData>
35 #include <QMouseEvent>
36 #include <QScrollBar>
37 #include <QPainterPath>
38
39 namespace SolarusEditor {
40
41 /**
42 * @brief Creates a tileset view.
43 * @param parent The parent widget or nullptr.
44 */
TilesetView(QWidget * parent)45 TilesetView::TilesetView(QWidget* parent) :
46 QGraphicsView(parent),
47 scene(nullptr),
48 create_border_set_action(nullptr),
49 change_pattern_id_action(nullptr),
50 delete_patterns_action(nullptr),
51 last_integer_pattern_id(0),
52 state(State::NORMAL),
53 view_settings(nullptr),
54 zoom(1.0),
55 read_only(false),
56 multi_selection_enabled(true) {
57
58 setAcceptDrops(true);
59 setAlignment(Qt::AlignTop | Qt::AlignLeft);
60
61 create_border_set_action = new QAction(
62 QIcon(":/images/border_kind_5.png"),
63 tr("Create contour..."),
64 this
65 );
66 create_border_set_action->setShortcut(tr("Ctrl+B"));
67 create_border_set_action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
68 connect(create_border_set_action, &QAction::triggered,
69 this, [this]() {
70 QStringList pattern_ids = get_model()->get_selected_ids();
71 pattern_ids.sort();
72 emit create_border_set_requested(pattern_ids);
73 });
74 addAction(create_border_set_action);
75
76 change_pattern_id_action = new QAction(
77 QIcon(":/images/icon_edit.png"), tr("Change id..."), this);
78 change_pattern_id_action->setShortcut(tr("F2"));
79 change_pattern_id_action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
80 connect(change_pattern_id_action, &QAction::triggered,
81 this, &TilesetView::change_selected_pattern_id_requested);
82 addAction(change_pattern_id_action);
83
84 delete_patterns_action = new QAction(
85 QIcon(":/images/icon_delete.png"), tr("Delete..."), this);
86 delete_patterns_action->setShortcut(QKeySequence::Delete);
87 delete_patterns_action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
88 connect(delete_patterns_action, &QAction::triggered,
89 this, &TilesetView::delete_selected_patterns_requested);
90 addAction(delete_patterns_action);
91
92 set_repeat_mode_actions = EnumMenus<PatternRepeatMode>::create_actions(
93 *this,
94 EnumMenuCheckableOption::CHECKABLE_EXCLUSIVE,
95 [this](PatternRepeatMode repeat_mode) {
96 emit change_selected_patterns_repeat_mode_requested(repeat_mode);
97 });
98 // TODO add shortcut support to EnumMenus
99 set_repeat_mode_actions[static_cast<int>(PatternRepeatMode::ALL)]->setShortcut(tr("A"));
100 set_repeat_mode_actions[static_cast<int>(PatternRepeatMode::HORIZONTAL)]->setShortcut(tr("H"));
101 set_repeat_mode_actions[static_cast<int>(PatternRepeatMode::VERTICAL)]->setShortcut(tr("V"));
102 set_repeat_mode_actions[static_cast<int>(PatternRepeatMode::NONE)]->setShortcut(tr("N"));
103 for (QAction* action : set_repeat_mode_actions) {
104 action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
105 }
106
107 ViewSettings* view_settings = new ViewSettings(this);
108 set_view_settings(*view_settings);
109 }
110
111 /**
112 * @brief Returns the tileset represented in this view.
113 * @return The tileset, or nullptr if there is currently no tileset.
114 */
get_model()115 TilesetModel* TilesetView::get_model() {
116
117 return this->model;
118 }
119
120 /**
121 * @brief Sets the tileset to represent in this view.
122 * @param model The tileset model, or nullptr to remove any model.
123 */
set_model(TilesetModel * model)124 void TilesetView::set_model(TilesetModel* model) {
125
126 int horizontal_scrollbar_value = 0;
127 int vertical_scrollbar_value = 0;
128 double zoom = 2.0; // Initial zoom: x2.
129
130 if (this->model != nullptr) {
131 disconnect(this->model, nullptr,
132 this, nullptr);
133 this->model = nullptr;
134 this->scene = nullptr;
135 horizontal_scrollbar_value = horizontalScrollBar()->value();
136 vertical_scrollbar_value = verticalScrollBar()->value();
137 if (view_settings != nullptr) {
138 zoom = view_settings->get_zoom();
139 }
140 }
141
142 this->model = model;
143
144 if (model != nullptr) {
145 // Create the scene from the model.
146 scene = new TilesetScene(*model, this);
147 setScene(scene);
148
149 if (model->get_patterns_image().isNull()) {
150 return;
151 }
152
153 // Restore the previous zoom and scrollbar positions.
154 if (view_settings != nullptr) {
155 view_settings->set_zoom(zoom);
156 }
157
158 horizontalScrollBar()->setValue(0); // To force an actual change (refresh bug).
159 horizontalScrollBar()->setValue(10);
160 horizontalScrollBar()->setValue(horizontal_scrollbar_value);
161
162 verticalScrollBar()->setValue(0);
163 verticalScrollBar()->setValue(10);
164 verticalScrollBar()->setValue(vertical_scrollbar_value);
165
166 // Install panning and zooming helpers.
167 new PanTool(this);
168 new ZoomTool(this);
169
170 connect(model, &TilesetModel::modelReset,
171 this, &TilesetView::notify_tileset_changed);
172 connect(model, &TilesetModel::tileset_image_file_reloaded,
173 this, static_cast<void (TilesetView::*)()>(&TilesetView::update));
174 }
175 }
176
177 /**
178 * @brief Called when the tileset model has changed.
179 */
notify_tileset_changed()180 void TilesetView::notify_tileset_changed() {
181
182 current_area_items.clear();
183 initially_selected_items.clear();
184 start_state_normal();
185 }
186
187 /**
188 * @brief Returns the tileset scene represented in this view.
189 * @return The scene or nullptr if no tileset was set.
190 */
get_scene()191 TilesetScene* TilesetView::get_scene() {
192 return scene;
193 }
194
195 /**
196 * @brief Sets the view settings for this view.
197 *
198 * When they change, the view is updated accordingly.
199 *
200 * @param view_settings The settings to watch.
201 */
set_view_settings(ViewSettings & view_settings)202 void TilesetView::set_view_settings(ViewSettings& view_settings) {
203
204 this->view_settings = &view_settings;
205
206 connect(&view_settings, SIGNAL(zoom_changed(double)),
207 this, SLOT(update_zoom()));
208 update_zoom();
209
210 connect(this->view_settings, SIGNAL(grid_visibility_changed(bool)),
211 this, SLOT(update_grid_visibility()));
212 connect(this->view_settings, SIGNAL(grid_size_changed(QSize)),
213 this, SLOT(update_grid_visibility()));
214 connect(this->view_settings, SIGNAL(grid_style_changed(GridStyle)),
215 this, SLOT(update_grid_visibility()));
216 connect(this->view_settings, SIGNAL(grid_color_changed(QColor)),
217 this, SLOT(update_grid_visibility()));
218 update_grid_visibility();
219
220 horizontalScrollBar()->setValue(0);
221 verticalScrollBar()->setValue(0);
222 }
223
224 /**
225 * @brief Returns whether the view is in read-only mode.
226 * @return @c true if the mode is read-only, @c false if changes can be
227 * made to the tileset.
228 */
is_read_only() const229 bool TilesetView::is_read_only() const {
230 return read_only;
231 }
232
233 /**
234 * @brief Sets whether the view is in read-only mode.
235 * @param read_only @c true to block changes from this view, @c false to allow them.
236 */
set_read_only(bool read_only)237 void TilesetView::set_read_only(bool read_only) {
238 this->read_only = read_only;
239 }
240
241 /**
242 * @brief Returns whether multiple selection is allowed.
243 * @return @c true if the user can select multiple patterns.
244 */
is_multi_selection_enabled() const245 bool TilesetView::is_multi_selection_enabled() const {
246 return multi_selection_enabled;
247 }
248
249 /**
250 * @brief Sets whether multiple selection is allowed.
251 * @return param multi_selection_enabled @c true to allow multiple selection.
252 */
set_multi_selection_enabled(bool multi_selection_enabled)253 void TilesetView::set_multi_selection_enabled(bool multi_selection_enabled) {
254 this->multi_selection_enabled = multi_selection_enabled;
255 }
256
257 /**
258 * @brief Sets the zoom level of the view from the settings.
259 *
260 * Zooming will be anchored at the mouse position.
261 * The zoom value will be clamped between 0.25 and 4.0.
262 */
update_zoom()263 void TilesetView::update_zoom() {
264
265 if (view_settings == nullptr) {
266 return;
267 }
268
269 double zoom = view_settings->get_zoom();
270 zoom = qMin(4.0, qMax(0.25, zoom));
271
272 if (zoom == this->zoom) {
273 return;
274 }
275
276 setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
277 double scale_factor = zoom / this->zoom;
278 scale(scale_factor, scale_factor);
279 this->zoom = zoom;
280 }
281
282 /**
283 * @brief Scales the view by a factor of 2.
284 *
285 * Zooming will be anchored at the mouse position.
286 * The maximum zoom value is 4.0: this function does nothing if you try to
287 * zoom more.
288 */
zoom_in()289 void TilesetView::zoom_in() {
290
291 if (view_settings == nullptr) {
292 return;
293 }
294
295 view_settings->set_zoom(view_settings->get_zoom() * 2.0);
296 }
297
298 /**
299 * @brief Scales the view by a factor of 0.5.
300 *
301 * Zooming will be anchored at the mouse position.
302 * The maximum zoom value is 0.25: this function does nothing if you try to
303 * zoom less.
304 */
zoom_out()305 void TilesetView::zoom_out() {
306
307 if (view_settings == nullptr) {
308 return;
309 }
310
311 view_settings->set_zoom(view_settings->get_zoom() / 2.0);
312 }
313
314 /**
315 * @brief Shows or hides the grid according to the view settings.
316 */
update_grid_visibility()317 void TilesetView::update_grid_visibility() {
318
319 if (view_settings == nullptr) {
320 return;
321 }
322
323 if (view_settings->is_grid_visible()) {
324 // Necessary to correctly show the grid when scrolling,
325 // because it is part of the foreground, not of graphics items.
326 setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
327 }
328 else {
329 // Faster.
330 setViewportUpdateMode(QGraphicsView::MinimalViewportUpdate);
331 }
332
333 if (scene != nullptr) {
334 // The foreground has changed.
335 scene->invalidate();
336 }
337 }
338
339 /**
340 * @brief Selects all patterns.
341 */
select_all()342 void TilesetView::select_all() {
343
344 if (scene == nullptr) {
345 return;
346 }
347
348 if (!multi_selection_enabled &&
349 model->get_num_patterns() > 1) {
350 return;
351 }
352
353 scene->select_all();
354 }
355
356 /**
357 * @brief Unselects all patterns.
358 */
unselect_all()359 void TilesetView::unselect_all() {
360
361 if (scene == nullptr) {
362 return;
363 }
364
365 scene->unselect_all();
366 }
367
368 /**
369 * @brief Draws the tileset view.
370 * @param event The paint event.
371 */
paintEvent(QPaintEvent * event)372 void TilesetView::paintEvent(QPaintEvent* event) {
373
374 QGraphicsView::paintEvent(event);
375
376 if (view_settings == nullptr || !view_settings->is_grid_visible()) {
377 return;
378 }
379
380 QSize grid = view_settings->get_grid_size();
381 QRect rect = event->rect();
382 rect.setTopLeft(mapFromScene(0, 0));
383
384 // Draw the grid.
385 QPainter painter(viewport());
386 GuiTools::draw_grid(
387 painter, rect, grid * zoom, view_settings->get_grid_color(),
388 view_settings->get_grid_style());
389 }
390
391 /**
392 * @brief Receives a mouse press event.
393 * @param event The event to handle.
394 */
mousePressEvent(QMouseEvent * event)395 void TilesetView::mousePressEvent(QMouseEvent* event) {
396
397 if (model == nullptr) {
398 return;
399 }
400
401 if (state == State::NORMAL) {
402
403 QList<QGraphicsItem*> items_under_mouse = items(
404 QRect(event->pos(), QSize(1, 1)),
405 Qt::IntersectsItemBoundingRect // Pick transparent items too.
406 );
407 QGraphicsItem* item = items_under_mouse.empty() ? nullptr : items_under_mouse.first();
408
409 const bool control_or_shift = (event->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier));
410
411 bool keep_selected = false;
412 if (control_or_shift && is_multi_selection_enabled()) {
413 // If ctrl or shift is pressed, keep the existing selection.
414 keep_selected = true;
415 }
416 else if (item != nullptr && item->isSelected()) {
417 // When clicking an already selected item, keep the existing selection too.
418 keep_selected = true;
419 }
420
421 if (!keep_selected) {
422 scene->clearSelection();
423 }
424
425 if (event->button() == Qt::LeftButton) {
426 if (item != nullptr &&
427 item->isSelected() &&
428 !model->is_selection_empty() &&
429 !control_or_shift &&
430 !is_read_only()) {
431 // Clicking on an already selected item: allow to move it.
432 start_state_moving_patterns(event->pos());
433 }
434 else {
435 if (is_multi_selection_enabled()) {
436 // Don't select the item yet, initialize a selection rectangle.
437 initially_selected_items = scene->selectedItems();
438 start_state_drawing_rectangle(event->pos());
439 }
440 else {
441 // No multiple selection is allowed: don't draw a selection rectangle,
442 // directly consider this as a click.
443 if (item != nullptr) {
444 // An item was clicked.
445 if (control_or_shift) {
446 // Toggle the selected state of the item.
447
448 item->setSelected(!item->isSelected());
449 emit selection_changed_by_user();
450 } else {
451 // Select the clicked item.
452 item->setSelected(true);
453 emit selection_changed_by_user();
454 }
455 }
456 }
457 }
458 }
459 else {
460 if (item != nullptr && !item->isSelected()) {
461 // Select the right-clicked item.
462 item->setSelected(true);
463 emit selection_changed_by_user();
464 }
465 }
466 }
467 }
468
469 /**
470 * @brief Receives a mouse release event.
471 * @param event The event to handle.
472 */
mouseReleaseEvent(QMouseEvent * event)473 void TilesetView::mouseReleaseEvent(QMouseEvent* event) {
474
475 if (model == nullptr) {
476 return;
477 }
478
479 bool do_selection = false;
480 if (state == State::DRAWING_RECTANGLE) {
481 // If the rectangle is empty, consider it was a click and not a drag.
482 // In this case we simply select the clicked item.
483 do_selection = current_area_items.first()->rect().isEmpty();
484 end_state_drawing_rectangle();
485 }
486 else if (state == State::MOVING_PATTERNS) {
487 end_state_moving_patterns();
488 }
489
490 if (do_selection) {
491 if (event->button() == Qt::LeftButton || event->button() == Qt::RightButton) {
492
493 // Left or right button: possibly change the selection.
494 QList<QGraphicsItem*> items_under_mouse = items(
495 QRect(event->pos(), QSize(1, 1)),
496 Qt::IntersectsItemBoundingRect // Pick transparent items too.
497 );
498 QGraphicsItem* item = items_under_mouse.empty() ? nullptr : items_under_mouse.first();
499
500 const bool control_or_shift = (event->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier));
501
502 bool keep_selected = false;
503 if (control_or_shift) {
504 // If ctrl or shift is pressed, keep the existing selection.
505 keep_selected = true;
506 }
507 else if (item != nullptr && item->isSelected()) {
508 // When clicking an already selected item, keep the existing selection too.
509 keep_selected = true;
510 }
511
512 if (!keep_selected) {
513 bool selection_was_empty = get_model()->is_selection_empty();
514 scene->clearSelection();
515
516 if (item == nullptr && selection_was_empty) {
517 // The user clicked outside any item, to unselect everything.
518 emit selection_changed_by_user();
519 }
520 }
521
522 if (item != nullptr) {
523 // Clicked an item.
524
525 if (event->button() == Qt::LeftButton) {
526
527 if (control_or_shift) {
528 // Left-clicking an item while pressing control or shift: toggle it.
529 item->setSelected(!item->isSelected());
530 emit selection_changed_by_user();
531 }
532 else {
533 if (!item->isSelected()) {
534 // Select the item.
535 item->setSelected(true);
536 emit selection_changed_by_user();
537 }
538 }
539 }
540 }
541 }
542 }
543
544 QGraphicsView::mouseReleaseEvent(event);
545 }
546
547 /**
548 * @brief Receives a mouse double click event.
549 * @param event The event to handle.
550 */
mouseDoubleClickEvent(QMouseEvent * event)551 void TilesetView::mouseDoubleClickEvent(QMouseEvent* event) {
552
553 // Nothing special but we don't want the behavior from the parent class.
554 Q_UNUSED(event);
555 }
556
557 /**
558 * @brief Receives a mouse move event.
559 * @param event The event to handle.
560 */
mouseMoveEvent(QMouseEvent * event)561 void TilesetView::mouseMoveEvent(QMouseEvent* event) {
562
563 if (model == nullptr) {
564 return;
565 }
566
567 if (state == State::DRAWING_RECTANGLE) {
568
569 // Compute the selected area.
570 QPoint dragging_previous_point = dragging_current_point;
571 dragging_current_point = mapToScene(event->pos()).toPoint() / 8 * 8;
572
573 if (dragging_current_point != dragging_previous_point) {
574
575 update_current_areas(dragging_start_point, dragging_current_point);
576 emit selection_changed_by_user();
577 }
578 }
579
580 // The parent class tracks mouse movements for internal needs
581 // such as anchoring the viewport to the mouse when zooming.
582 QGraphicsView::mouseMoveEvent(event);
583 }
584
585 /**
586 * @brief Receives a context menu event.
587 * @param event The event to handle.
588 */
contextMenuEvent(QContextMenuEvent * event)589 void TilesetView::contextMenuEvent(QContextMenuEvent* event) {
590
591 if (scene == nullptr) {
592 return;
593 }
594
595 QPoint where;
596 if (event->pos() != QPoint(0, 0)) {
597 where = event->pos();
598 }
599 else {
600 QList<QGraphicsItem*> selected_items = scene->selectedItems();
601 where = mapFromScene(selected_items.first()->pos() + QPoint(8, 8));
602 }
603
604 show_context_menu(where);
605 }
606
607 /**
608 * @brief Shows a context menu with actions relative to the selected patterns.
609 *
610 * Does nothing if the view is in read-only mode.
611 *
612 * @param where Where to show the menu, in view coordinates.
613 */
show_context_menu(const QPoint & where)614 void TilesetView::show_context_menu(const QPoint& where) {
615
616 if (model == nullptr) {
617 return;
618 }
619
620 if (is_read_only()) {
621 return;
622 }
623
624 QList<int> selected_indexes = model->get_selected_indexes();
625 if (selected_indexes.empty()) {
626 return;
627 }
628
629 QMenu* menu = new QMenu(this);
630
631 // Ground.
632 build_context_menu_ground(*menu, selected_indexes);
633
634 // Default layer.
635 QMenu* layer_menu = new QMenu(tr("Default layer"), this);
636 build_context_menu_layer(*layer_menu, selected_indexes);
637 menu->addSeparator();
638 menu->addMenu(layer_menu);
639
640 // Repeat mode.
641 QMenu* repeat_mode_menu = new QMenu(tr("Repeatable"), this);
642 build_context_menu_repeat_mode(*repeat_mode_menu, selected_indexes);
643 menu->addMenu(repeat_mode_menu);
644
645 // Animation.
646 QMenu* scrolling_menu = new QMenu(tr("Scrolling"), this);
647 build_context_menu_scrolling(*scrolling_menu, selected_indexes);
648 menu->addMenu(scrolling_menu);
649
650 // Border set.
651 if (selected_indexes.size() <= 12) {
652 menu->addAction(create_border_set_action);
653 }
654
655 // Change pattern id.
656 menu->addSeparator();
657 change_pattern_id_action->setEnabled(model->get_selected_index() != -1);
658 menu->addAction(change_pattern_id_action);
659
660 // Delete patterns.
661 menu->addSeparator();
662 menu->addAction(delete_patterns_action);
663
664 // Create the menu at 1,1 to avoid the cursor being already in the first item.
665 menu->popup(viewport()->mapToGlobal(where) + QPoint(1, 1));
666 }
667
668 /**
669 * @brief Builds the ground part of a context menu for patterns.
670 * @param menu The menu to fill.
671 * @param indexes Patterns to build a context menu for.
672 */
build_context_menu_ground(QMenu & menu,const QList<int> & indexes)673 void TilesetView::build_context_menu_ground(
674 QMenu& menu, const QList<int>& indexes) {
675
676 if (indexes.empty()) {
677 return;
678 }
679
680 // See if the ground is common.
681 Ground ground;
682 bool common = model->is_common_pattern_ground(indexes, ground);
683
684 // Add ground actions to the menu.
685 QList<QAction*> ground_actions = EnumMenus<Ground>::create_actions(
686 menu,
687 EnumMenuCheckableOption::CHECKABLE_EXCLUSIVE,
688 [this](Ground ground) {
689 emit change_selected_patterns_ground_requested(ground);
690 });
691
692 if (common) {
693 int ground_index = static_cast<int>(ground);
694 QAction* checked_action = ground_actions[ground_index];
695 checked_action->setChecked(true);
696 // Add a checkmark (there is none when there is already an icon).
697 checked_action->setText("\u2714 " + checked_action->text());
698 }
699 }
700
701 /**
702 * @brief Builds the default layer part of a context menu for patterns.
703 * @param menu The menu to fill.
704 * @param indexes Patterns to build a context menu for.
705 */
build_context_menu_layer(QMenu & menu,const QList<int> & indexes)706 void TilesetView::build_context_menu_layer(
707 QMenu& menu, const QList<int>& indexes) {
708
709 if (indexes.empty()) {
710 return;
711 }
712
713 // See if the default layer is common.
714 int common_layer = 0;
715 bool common = model->is_common_pattern_default_layer(indexes, common_layer);
716
717 // Add 3 layer actions to the menu.
718 // (If more layers are necessary, the user can still use the spinbox
719 // in the patterns properties view.)
720 for (int i = 0; i < 3; ++i) {
721 QAction* action = new QAction(tr("Layer %1").arg(i), &menu);
722 action->setCheckable(true);
723 menu.addAction(action);
724 connect(action, &QAction::triggered, [this, i]() {
725 emit change_selected_patterns_default_layer_requested(i);
726 });
727
728 if (common && i == common_layer) {
729 action->setChecked(true);
730 }
731 }
732
733 }
734
735 /**
736 * @brief Builds the repeat mode part of a context menu for patterns.
737 * @param menu The menu to fill.
738 * @param indexes Patterns to build a context menu for.
739 */
build_context_menu_repeat_mode(QMenu & menu,const QList<int> & indexes)740 void TilesetView::build_context_menu_repeat_mode(
741 QMenu& menu, const QList<int>& indexes) {
742
743 if (indexes.empty()) {
744 return;
745 }
746
747 // See if the repeat mode is common.
748 PatternRepeatMode repeat_mode = PatternRepeatMode::ALL;
749 bool common = model->is_common_pattern_repeat_mode(indexes, repeat_mode);
750
751 menu.addActions(set_repeat_mode_actions);
752
753 if (common) {
754 int repeat_mode_index = static_cast<int>(repeat_mode);
755 QAction* checked_action = set_repeat_mode_actions[repeat_mode_index];
756 checked_action->setChecked(true);
757 }
758 }
759
760 /**
761 * @brief Builds the scrolling property part of a context menu for patterns.
762 * @param menu The menu to fill.
763 * @param indexes Patterns to build a context menu for.
764 */
build_context_menu_scrolling(QMenu & menu,const QList<int> & indexes)765 void TilesetView::build_context_menu_scrolling(
766 QMenu& menu, const QList<int>& indexes) {
767
768 if (indexes.empty()) {
769 return;
770 }
771
772 // See if the scrolling is common.
773 PatternScrolling scrolling;
774 bool common = model->is_common_pattern_scrolling(indexes, scrolling);
775
776 // Add actions to the menu.
777 QList<QAction*> scrolling_actions = EnumMenus<PatternScrolling>::create_actions(
778 menu,
779 EnumMenuCheckableOption::CHECKABLE_EXCLUSIVE,
780 [this](PatternScrolling animation) {
781 emit change_selected_patterns_scrolling_requested(animation);
782 });
783
784 if (common) {
785 int scrolling_index = static_cast<int>(scrolling);
786 QAction* checked_action = scrolling_actions[scrolling_index];
787 checked_action->setChecked(true);
788 }
789 }
790
791 /**
792 * @brief Sets the normal state.
793 */
start_state_normal()794 void TilesetView::start_state_normal() {
795
796 this->state = State::NORMAL;
797 }
798
799 /**
800 * @brief Moves to the state of drawing a rectangle for a selection or a
801 * new pattern.
802 * @param initial_point Where the user starts drawing the rectangle,
803 * in view coordinates.
804 */
start_state_drawing_rectangle(const QPoint & initial_point)805 void TilesetView::start_state_drawing_rectangle(const QPoint& initial_point) {
806
807 this->state = State::DRAWING_RECTANGLE;
808 this->dragging_start_point = mapToScene(initial_point).toPoint() / 8 * 8;
809 this->dragging_current_point = this->dragging_start_point;
810
811 QGraphicsRectItem *item = new QGraphicsRectItem();
812 item->setPen(QPen(Qt::yellow));
813 scene->addItem(item);
814 current_area_items.push_front(item);
815 }
816
817 /**
818 * @brief Finishes drawing a rectangle.
819 */
end_state_drawing_rectangle()820 void TilesetView::end_state_drawing_rectangle() {
821
822 QRect rectangle = current_area_items.first()->rect().toRect();
823 if (!rectangle.isEmpty() &&
824 sceneRect().contains(rectangle) &&
825 get_items_intersecting_current_areas(true).isEmpty() &&
826 model->is_selection_empty() &&
827 !is_read_only()) {
828
829 // Context menu to create a pattern.
830 QMenu menu;
831 EnumMenus<Ground>::create_actions(
832 menu, EnumMenuCheckableOption::NON_CHECKABLE, [this, rectangle](Ground ground) {
833 QString pattern_id;
834 do {
835 ++last_integer_pattern_id;
836 pattern_id = QString::number(last_integer_pattern_id);
837 } while (model->id_to_index(pattern_id) != -1);
838
839 emit create_pattern_requested(pattern_id, rectangle, ground);
840 });
841
842 // Put most actions in a submenu to make the context menu smaller.
843 QMenu sub_menu(tr("New pattern (more options)"));
844 const QList<QAction*> actions = menu.actions();
845 for (QAction* action : actions) {
846 Ground ground = static_cast<Ground>(action->data().toInt());
847 if (ground == Ground::TRAVERSABLE ||
848 ground == Ground::WALL) {
849 action->setText(tr("New pattern (%1)").arg(GroundTraits::get_friendly_name(ground)));
850 }
851 else {
852 menu.removeAction(action);
853 sub_menu.addAction(action);
854 }
855 }
856 menu.addMenu(&sub_menu);
857
858 menu.addSeparator();
859 menu.addAction(tr("Cancel"));
860 menu.exec(cursor().pos() + QPoint(1, 1));
861 }
862
863 clear_current_areas();
864 initially_selected_items.clear();
865 start_state_normal();
866 }
867
868 /**
869 * @brief Moves to the state of moving the selected pattern.
870 * @param initial_point Where the user starts dragging the pattern,
871 * in view coordinates.
872 */
start_state_moving_patterns(const QPoint & initial_point)873 void TilesetView::start_state_moving_patterns(const QPoint& initial_point) {
874
875 if (model->is_selection_empty()) {
876 return;
877 }
878
879 state = State::MOVING_PATTERNS;
880 dragging_start_point = Point::floor_8(mapToScene(initial_point));
881 dragging_current_point = dragging_start_point;
882
883 const QList<int>& selected_indexes = model->get_selected_indexes();
884 for (int index : selected_indexes) {
885 const QRect& box = model->get_pattern_frames_bounding_box(index);
886 QGraphicsRectItem *item = new QGraphicsRectItem(box);
887 item->setPen(QPen(Qt::yellow));
888 scene->addItem(item);
889 current_area_items.append(item);
890 }
891
892 const QRect& pattern_frame = model->get_pattern_frame(selected_indexes.first());
893 const QPoint& hot_spot = initial_point - mapFromScene(pattern_frame.topLeft());
894 QPixmap drag_pixmap = model->get_pattern_image(selected_indexes.first());
895
896 if (view_settings != nullptr) {
897 double zoom = view_settings->get_zoom();
898 drag_pixmap = drag_pixmap.scaled(pattern_frame.size() * zoom);
899 }
900
901 // TODO make a pixmap of all selected patterns.
902 QDrag* drag = new QDrag(this);
903 drag->setPixmap(drag_pixmap);
904 drag->setHotSpot(hot_spot);
905
906 QStringList pattern_ids;
907 for (int index : selected_indexes) {
908 pattern_ids << model->index_to_id(index);
909 }
910 std::sort(pattern_ids.begin(), pattern_ids.end());
911 QString text_data = pattern_ids.join("\n");
912
913 QMimeData* data = new QMimeData();
914 data->setText(text_data);
915
916 drag->setMimeData(data);
917 drag->exec(Qt::MoveAction | Qt::CopyAction); // Blocking call during the drag operation.
918
919 clear_current_areas();
920 start_state_normal();
921 }
922
923 /**
924 * @brief Finishes moving a pattern.
925 */
end_state_moving_patterns()926 void TilesetView::end_state_moving_patterns() {
927
928 QPoint delta = dragging_current_point - dragging_start_point;
929 QRect box = get_selection_bounding_box();
930 box.translate(delta);
931 if (!box.isEmpty() &&
932 sceneRect().contains(box) &&
933 get_items_intersecting_current_areas(true).isEmpty() &&
934 !model->is_selection_empty() &&
935 !is_read_only() &&
936 dragging_current_point != dragging_start_point) {
937
938 // Context menu to move the patterns.
939 QMenu menu;
940 QAction* move_pattern_action = new QAction(tr("Move here"), this);
941 connect(move_pattern_action, &QAction::triggered, [this, delta] {
942 emit change_selected_patterns_position_requested(delta);
943 });
944 menu.addAction(move_pattern_action);
945 QAction* duplicate_pattern_action = new QAction(
946 QIcon(":/images/icon_copy.png"), tr("Duplicate here"), this);
947 duplicate_pattern_action->setEnabled(
948 get_items_intersecting_current_areas(false).isEmpty());
949 connect(duplicate_pattern_action, &QAction::triggered, [this, delta] {
950 emit duplicate_selected_patterns_requested(delta);
951 });
952 menu.addAction(duplicate_pattern_action);
953 menu.addSeparator();
954 menu.addAction(tr("Cancel"));
955 menu.exec(cursor().pos() + QPoint(1, 1));
956 }
957
958 clear_current_areas();
959 start_state_normal();
960 }
961
dragEnterEvent(QDragEnterEvent * event)962 void TilesetView::dragEnterEvent(QDragEnterEvent* event) {
963
964 if (state != State::MOVING_PATTERNS) {
965 return;
966 }
967
968 if (event->mimeData()->hasFormat("text/plain")) {
969 event->acceptProposedAction();
970 }
971 }
972
dragMoveEvent(QDragMoveEvent * event)973 void TilesetView::dragMoveEvent(QDragMoveEvent* event) {
974
975 if (state != State::MOVING_PATTERNS) {
976 return;
977 }
978
979 dragging_current_point = Point::floor_8(mapToScene(event->pos()));
980 QPoint delta = dragging_current_point - dragging_start_point;
981
982 clear_current_areas();
983
984 bool valid_move = true;
985 const QList<int>& selected_indexes = model->get_selected_indexes();
986 const QList<QGraphicsItem*> selected_items = scene->selectedItems();
987 for (int index : selected_indexes) {
988
989 QRect area = model->get_pattern_frames_bounding_box(index);
990 area.translate(delta);
991 QGraphicsRectItem* item = new QGraphicsRectItem(area);
992
993 // Check overlapping existing patterns.
994 QSet<QGraphicsItem*> overlapping_items = scene->items(
995 area.adjusted(1, 1, -1, -1), Qt::IntersectsItemBoundingRect).toSet();
996
997 // Filter out the patterns that are being moved,
998 // that is, allow the destination to overlap the source.
999 for (QGraphicsItem* selected_item : selected_items) {
1000 overlapping_items.remove(selected_item);
1001 }
1002
1003 if (!area.isEmpty() &&
1004 sceneRect().contains(area) &&
1005 overlapping_items.isEmpty() &&
1006 !is_read_only()) {
1007 item->setPen(QPen(Qt::yellow));
1008 } else {
1009 item->setPen(QPen(Qt::red));
1010 item->setZValue(1);
1011 valid_move = false;
1012 }
1013
1014 // Let the drag cursor show if the move is legal.
1015 event->setAccepted(valid_move);
1016
1017 scene->addItem(item);
1018 current_area_items.append(item);
1019 }
1020 }
1021
dragLeaveEvent(QDragLeaveEvent * event)1022 void TilesetView::dragLeaveEvent(QDragLeaveEvent* event) {
1023
1024 Q_UNUSED(event);
1025 }
1026
dropEvent(QDropEvent * event)1027 void TilesetView::dropEvent(QDropEvent* event) {
1028
1029 if (state != State::MOVING_PATTERNS) {
1030 return;
1031 }
1032
1033 if (event->dropAction() == Qt::CopyAction) {
1034 event->acceptProposedAction();
1035 }
1036 else if (event->dropAction() == Qt::MoveAction) {
1037 event->acceptProposedAction();
1038 }
1039 end_state_moving_patterns();
1040 }
1041
1042 /**
1043 * @brief Updates the position of the rectangle(s) the user is drawing or moving.
1044 *
1045 * In state DRAWING_RECTANGLE, if the specified area is the same as before,
1046 * nothing is done.
1047 *
1048 * @param start_point The starting point of drawing or moving.
1049 * @param current_point The current point of drawing or moving.
1050 */
update_current_areas(const QPoint & start_point,const QPoint & current_point)1051 void TilesetView::update_current_areas(
1052 const QPoint& start_point, const QPoint& current_point) {
1053
1054 if (state == State::DRAWING_RECTANGLE) {
1055
1056 QRect area = Rectangle::from_two_points(start_point, current_point);
1057 if (current_area_items.first()->rect().toRect() == area) {
1058 // No change.
1059 return;
1060 }
1061 current_area_items.first()->setRect(area);
1062
1063 // Select items strictly in the rectangle.
1064 scene->clearSelection();
1065 QPainterPath path;
1066 path.addRect(QRect(area.topLeft() - QPoint(1, 1),
1067 area.size() + QSize(2, 2)));
1068 scene->setSelectionArea(path, Qt::ContainsItemBoundingRect);
1069
1070 // Re-select items that were already selected if Ctrl or Shift was pressed.
1071 for (QGraphicsItem* item : initially_selected_items) {
1072 item->setSelected(true);
1073 }
1074 }
1075 }
1076
1077 /**
1078 * @brief Clears rectangle(s) the user is drawing or moving.
1079 */
clear_current_areas()1080 void TilesetView::clear_current_areas() {
1081
1082 for (QGraphicsRectItem* item : current_area_items) {
1083 scene->removeItem(item);
1084 delete item;
1085 }
1086 current_area_items.clear();
1087 }
1088
1089 /**
1090 * @brief Returns all items that intersect the rectangles drawn by the user
1091 * except selected items.
1092 * @param ignore_selected @c true if the selection should be ignored.
1093 * @return The items that intersect the drawn rectangle.
1094 */
get_items_intersecting_current_areas(bool ignore_selected) const1095 QList<QGraphicsItem*> TilesetView::get_items_intersecting_current_areas(
1096 bool ignore_selected) const {
1097
1098 QList<QGraphicsItem*> items;
1099
1100 for (QGraphicsRectItem* item : current_area_items) {
1101 QRect area = item->rect().toRect().adjusted(1, 1, -1, -1);
1102 items.append(scene->items(area, Qt::IntersectsItemBoundingRect));
1103 items.removeAll(item); // Ignore the drawn rectangle itself.
1104 }
1105
1106 // Ignore selected items.
1107 if (ignore_selected) {
1108 const QList<QGraphicsItem*> selected_items = scene->selectedItems();
1109 for (QGraphicsItem* item : selected_items) {
1110 items.removeAll(item);
1111 }
1112 }
1113
1114 return items;
1115 }
1116
1117 /**
1118 * @brief Returns the bounding box corresponding to all selected tile patterns.
1119 * @return The bounding box of all the selected tile patterns.
1120 */
get_selection_bounding_box() const1121 QRect TilesetView::get_selection_bounding_box() const {
1122
1123 QRect bounding_box;
1124 const QList<int> selected_indexes = model->get_selected_indexes();
1125 for (int index : selected_indexes) {
1126 bounding_box = bounding_box.united(
1127 model->get_pattern_frames_bounding_box(index));
1128 }
1129 return bounding_box;
1130 }
1131
1132 }
1133