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