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/gui_tools.h"
18 #include "widgets/pan_tool.h"
19 #include "widgets/sprite_scene.h"
20 #include "widgets/sprite_view.h"
21 #include "widgets/zoom_tool.h"
22 #include "point.h"
23 #include "view_settings.h"
24 #include <QAction>
25 #include <QApplication>
26 #include <QMenu>
27 #include <QMouseEvent>
28 #include <QScrollBar>
29 #include <QtMath>
30 
31 namespace SolarusEditor {
32 
33 /**
34  * @brief Creates a sprite view.
35  * @param parent The parent widget or nullptr.
36  */
SpriteView(QWidget * parent)37 SpriteView::SpriteView(QWidget* parent) :
38   QGraphicsView(parent),
39   scene(nullptr),
40   delete_direction_action(nullptr),
41   state(State::NORMAL),
42   create_multiframe_direction(false),
43   view_settings(nullptr),
44   zoom(1.0) {
45 
46   setAlignment(Qt::AlignTop | Qt::AlignLeft);
47   current_area_item.setZValue(2);
48 
49   delete_direction_action = new QAction(
50         QIcon(":/images/icon_delete.png"), tr("Delete..."), this);
51   delete_direction_action->setShortcut(QKeySequence::Delete);
52   delete_direction_action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
53   connect(delete_direction_action, SIGNAL(triggered()),
54           this, SIGNAL(delete_selected_direction_requested()));
55   addAction(delete_direction_action);
56   duplicate_direction_action = new QAction(
57         QIcon(":/images/icon_copy.png"), tr("Duplicate..."), this);
58   // TODO: set a shortcut to duplicate a direction
59   connect(duplicate_direction_action, SIGNAL(triggered()),
60           this, SLOT(duplicate_selected_direction_requested()));
61   addAction(duplicate_direction_action);
62 
63   change_num_frames_columns_action = new QAction(
64         tr("Change the number of frames/columns"), this);
65   change_num_frames_columns_action->setShortcut(tr("R"));
66   change_num_frames_columns_action->setShortcutContext(
67     Qt::WidgetWithChildrenShortcut);
68   connect(change_num_frames_columns_action, SIGNAL(triggered()),
69           this, SLOT(change_num_frames_columns_requested()));
70   addAction(change_num_frames_columns_action);
71 
72   change_num_frames_action = new QAction(
73         tr("Change the number of frames"), this);
74   // TODO: set a shortcut to changing the number of frames
75   connect(change_num_frames_action, SIGNAL(triggered()),
76           this, SLOT(change_num_frames_requested()));
77   addAction(change_num_frames_action);
78 
79   change_num_columns_action = new QAction(
80         tr("Change the number of columns"), this);
81   // TODO: set a shortcut to changing the number of columns
82   connect(change_num_columns_action, SIGNAL(triggered()),
83           this, SLOT(change_num_columns_requested()));
84   addAction(change_num_columns_action);
85 
86   ViewSettings* view_settings = new ViewSettings(this);
87   set_view_settings(*view_settings);
88 }
89 
90 /**
91  * @brief Returns the sprite scene represented in this view.
92  * @return The scene or nullptr if no sprite was set.
93  */
get_scene()94 SpriteScene* SpriteView::get_scene() {
95   return scene;
96 }
97 
98 /**
99  * @brief Sets the sprite to represent in this view.
100  * @param model The sprite model, or nullptr to remove any model.
101  * This class does not take ownership on the model.
102  * The model can be deleted safely.
103  */
set_model(SpriteModel * model)104 void SpriteView::set_model(SpriteModel* model) {
105 
106   if (this->model != nullptr) {
107     this->model = nullptr;
108     this->scene = nullptr;
109   }
110 
111   this->model = model;
112 
113   if (model != nullptr) {
114     // Create the scene from the model.
115     scene = new SpriteScene(*model, this);
116     setScene(scene);
117 
118     // Enable useful features if there is an image.
119     setDragMode(QGraphicsView::RubberBandDrag);
120 
121     if (view_settings != nullptr) {
122       view_settings->set_zoom(2.0);  // Initial zoom: x2.
123     }
124     horizontalScrollBar()->setValue(0);
125     verticalScrollBar()->setValue(0);
126 
127     // Install panning and zooming helpers.
128     new PanTool(this);
129     new ZoomTool(this);
130   }
131 }
132 
133 /**
134  * @brief Sets the view settings for this view.
135  *
136  * When they change, the view is updated accordingly.
137  *
138  * @param view_settings The settings to watch.
139  */
set_view_settings(ViewSettings & view_settings)140 void SpriteView::set_view_settings(ViewSettings& view_settings) {
141 
142   this->view_settings = &view_settings;
143 
144   connect(&view_settings, SIGNAL(zoom_changed(double)),
145           this, SLOT(update_zoom()));
146   update_zoom();
147 
148   connect(this->view_settings, SIGNAL(grid_visibility_changed(bool)),
149           this, SLOT(update_grid_visibility()));
150   connect(this->view_settings, SIGNAL(grid_size_changed(QSize)),
151           this, SLOT(update_grid_visibility()));
152   connect(this->view_settings, SIGNAL(grid_style_changed(GridStyle)),
153           this, SLOT(update_grid_visibility()));
154   connect(this->view_settings, SIGNAL(grid_color_changed(QColor)),
155           this, SLOT(update_grid_visibility()));
156   update_grid_visibility();
157 
158   horizontalScrollBar()->setValue(0);
159   verticalScrollBar()->setValue(0);
160 }
161 
162 /**
163  * @brief Sets the zoom level of the view from the settings.
164  *
165  * Zooming will be anchored at the mouse position.
166  * The zoom value will be clamped between 0.25 and 4.0.
167  */
update_zoom()168 void SpriteView::update_zoom() {
169 
170   if (view_settings == nullptr) {
171     return;
172   }
173 
174   double zoom = view_settings->get_zoom();
175   zoom = qMin(4.0, qMax(0.25, zoom));
176 
177   if (zoom == this->zoom) {
178     return;
179   }
180 
181   setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
182   double scale_factor = zoom / this->zoom;
183   scale(scale_factor, scale_factor);
184   this->zoom = zoom;
185 }
186 
187 /**
188  * @brief Scales the view by a factor of 2.
189  *
190  * Zooming will be anchored at the mouse position.
191  * The maximum zoom value is 4.0: this function does nothing if you try to
192  * zoom more.
193  */
zoom_in()194 void SpriteView::zoom_in() {
195 
196   if (view_settings == nullptr) {
197     return;
198   }
199 
200   view_settings->set_zoom(view_settings->get_zoom() * 2.0);
201 }
202 
203 /**
204  * @brief Scales the view by a factor of 0.5.
205  *
206  * Zooming will be anchored at the mouse position.
207  * The maximum zoom value is 0.25: this function does nothing if you try to
208  * zoom less.
209  */
zoom_out()210 void SpriteView::zoom_out() {
211 
212   if (view_settings == nullptr) {
213     return;
214   }
215 
216   view_settings->set_zoom(view_settings->get_zoom() / 2.0);
217 }
218 
219 /**
220  * @brief Shows or hides the grid according to the view settings.
221  */
update_grid_visibility()222 void SpriteView::update_grid_visibility() {
223 
224   if (view_settings == nullptr) {
225     return;
226   }
227 
228   if (view_settings->is_grid_visible()) {
229     // Necessary to correctly show the grid when scrolling,
230     // because it is part of the foreground, not of graphics items.
231     setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
232   }
233   else {
234     // Faster.
235     setViewportUpdateMode(QGraphicsView::MinimalViewportUpdate);
236   }
237 
238   if (scene != nullptr) {
239     // The foreground has changed.
240     scene->invalidate();
241   }
242 }
243 
244 /**
245  * @brief Slot called when the user asks for ducplicate the selected direction.
246  */
duplicate_selected_direction_requested()247 void SpriteView::duplicate_selected_direction_requested() {
248 
249   SpriteModel::Index index = model->get_selected_index();
250   if (!index.is_direction_index()) {
251     return;
252   }
253   QPoint posistion = model->get_direction_position(index);
254   emit duplicate_selected_direction_requested(posistion);
255 }
256 
257 /**
258  * @brief Maps a mouse event position to a scene position.
259  * @param point The mouse position.
260  * @param snap_to_grid Set to @c true to snap the position to the grid.
261  * @return The scene position.
262  */
map_to_scene(const QPoint & point,bool snap_to_grid)263 QPoint SpriteView::map_to_scene(const QPoint& point, bool snap_to_grid) {
264 
265   QPoint mapped_point = mapToScene(point).toPoint();
266 
267   if (snap_to_grid) {
268     // TODO: use the grid size (settings).
269     return mapped_point / 8 * 8;
270   }
271 
272   return mapped_point;
273 }
274 
275 /**
276  * @brief Change the number of frames and columns of the selected direction.
277  * @param mode The changing mode.
278  */
change_num_frames_columns(const ChangingNumFramesColumnsMode & mode)279 void SpriteView::change_num_frames_columns(
280   const ChangingNumFramesColumnsMode& mode) {
281 
282   if (state != State::NORMAL) {
283     return;
284   }
285 
286   SpriteModel::Index index = model->get_selected_index();
287   if (!index.is_direction_index()) {
288     return;
289   }
290 
291   start_state_changing_num_frames_columns(mode);
292 }
293 
294 /**
295  * @brief Slot called when the user asks for change number of frames/columns.
296  */
change_num_frames_columns_requested()297 void SpriteView::change_num_frames_columns_requested() {
298 
299   change_num_frames_columns(ChangingNumFramesColumnsMode::CHANGE_BOTH);
300 }
301 
302 /**
303  * @brief Slot called when the user asks for change number of frames.
304  */
change_num_frames_requested()305 void SpriteView::change_num_frames_requested() {
306 
307   change_num_frames_columns(ChangingNumFramesColumnsMode::CHANGE_NUM_FRAMES);
308 }
309 
310 /**
311  * @brief Slot called when the user asks for change number of columns.
312  */
change_num_columns_requested()313 void SpriteView::change_num_columns_requested() {
314 
315   change_num_frames_columns(ChangingNumFramesColumnsMode::CHANGE_NUM_COLUMNS);
316 }
317 
318 /**
319  * @brief Draws the sprite view.
320  * @param event The paint event.
321  */
paintEvent(QPaintEvent * event)322 void SpriteView::paintEvent(QPaintEvent* event) {
323 
324   QGraphicsView::paintEvent(event);
325 
326   if (view_settings == nullptr || !view_settings->is_grid_visible()) {
327     return;
328   }
329 
330   QSize grid = view_settings->get_grid_size();
331   QRect rect = event->rect();
332   rect.setTopLeft(mapFromScene(0, 0));
333 
334   // Draw the grid.
335   QPainter painter(viewport());
336   GuiTools::draw_grid(
337     painter, rect, grid * zoom, view_settings->get_grid_color(),
338     view_settings->get_grid_style());
339 }
340 
341 /**
342  * @brief Receives a focus out event.
343  * @param event The event to handle.
344  */
focusOutEvent(QFocusEvent * event)345 void SpriteView::focusOutEvent(QFocusEvent* event) {
346 
347   if (state == State::CHANGING_NUM_FRAMES_COLUMNS) {
348     cancel_state_changing_num_frames_columns();
349   }
350   QGraphicsView::focusOutEvent(event);
351 }
352 
353 /**
354  * @brief Receives a key press event.
355  * @param event The event to handle.
356  */
keyPressEvent(QKeyEvent * event)357 void SpriteView::keyPressEvent(QKeyEvent* event) {
358 
359   if (event->key() == Qt::Key_Escape &&
360       state == State::CHANGING_NUM_FRAMES_COLUMNS) {
361     cancel_state_changing_num_frames_columns();
362   }
363   QGraphicsView::keyPressEvent(event);
364 }
365 
366 /**
367  * @brief Receives a mouse press event.
368  *
369  * Reimplemented to handle the selection.
370  *
371  * @param event The event to handle.
372  */
mousePressEvent(QMouseEvent * event)373 void SpriteView::mousePressEvent(QMouseEvent* event) {
374 
375   if (model == nullptr) {
376     return;
377   }
378 
379   if (state == State::CHANGING_NUM_FRAMES_COLUMNS) {
380     return;
381   }
382 
383   if (event->button() == Qt::LeftButton || event->button() == Qt::RightButton) {
384 
385     // Left or right button: possibly change the selection.
386     QList<QGraphicsItem*> items_under_mouse = items(
387           QRect(event->pos(), QSize(1, 1)),
388           Qt::IntersectsItemBoundingRect  // Pick transparent items too.
389           );
390     QGraphicsItem* item = items_under_mouse.empty() ? nullptr : items_under_mouse.first();
391 
392     if (item != nullptr) {
393       if (!item->isSelected()) {
394         // Select the item.
395         scene->clearSelection();
396         item->setSelected(true);
397       }
398       if (event->button() == Qt::LeftButton &&
399           model->get_selected_index().is_direction_index()) {
400         // Allow to move it.
401         start_state_moving_direction(event->pos());
402       }
403     }
404     else {
405       if (event->button() == Qt::LeftButton) {
406         // Left click outside items: trace a selection rectangle.
407         scene->clearSelection();
408         start_state_drawing_rectangle(event->pos());
409       }
410     }
411   }
412 }
413 
414 /**
415  * @brief Receives a mouse release event.
416  *
417  * Reimplemented to scroll the view when the middle mouse button is pressed.
418  *
419  * @param event The event to handle.
420  */
mouseReleaseEvent(QMouseEvent * event)421 void SpriteView::mouseReleaseEvent(QMouseEvent* event) {
422 
423   if (model == nullptr) {
424     return;
425   }
426 
427   if (state == State::DRAWING_RECTANGLE) {
428     end_state_drawing_rectangle();
429   }
430   else if (state == State::MOVING_DIRECTION) {
431     end_state_moving_direction();
432   }
433   else if (state == State::CHANGING_NUM_FRAMES_COLUMNS) {
434     end_state_changing_num_frames_columns();
435   }
436 
437   QGraphicsView::mouseReleaseEvent(event);
438 }
439 
440 /**
441  * @brief Receives a mouse move event.
442  *
443  * Reimplemented to scroll the view when the middle mouse button is pressed.
444  *
445  * @param event The event to handle.
446  */
mouseMoveEvent(QMouseEvent * event)447 void SpriteView::mouseMoveEvent(QMouseEvent* event) {
448 
449   if (model == nullptr) {
450     return;
451   }
452 
453   bool update_selection_validity = false;
454 
455   if (state == State::DRAWING_RECTANGLE) {
456 
457     // Compute the selected area.
458     QPoint dragging_previous_point = dragging_current_point;
459     dragging_current_point = map_to_scene(event->pos(), true);
460 
461     if (dragging_current_point != dragging_previous_point) {
462 
463       int x = qMin(dragging_current_point.x(), dragging_start_point.x());
464       int y = qMin(dragging_current_point.y(), dragging_start_point.y());
465       int width = qAbs(dragging_current_point.x() - dragging_start_point.x());
466       int height = qAbs(dragging_current_point.y() - dragging_start_point.y());
467 
468       current_area_item.setPos(QPoint(x, y));
469       current_area_item.set_frame_size(QSize(width, height));
470       update_selection_validity = true;
471     }
472   }
473   else if (state == State::MOVING_DIRECTION) {
474 
475     SpriteModel::Index index = model->get_selected_index();
476     if (!index.is_direction_index()) {
477       // Direction was deselected: cancel the movement.
478       end_state_moving_direction();
479     }
480     else {
481       QPoint position = model->get_direction_position(index);
482       QRect previous_rect = current_area_item.get_direction_all_frames_rect();
483 
484       dragging_current_point = Point::floor_8(mapToScene(event->pos()));
485       current_area_item.setPos(QPoint(
486         position.x() + dragging_current_point.x() - dragging_start_point.x(),
487         position.y() + dragging_current_point.y() - dragging_start_point.y()));
488       update_selection_validity = true;
489 
490       // To ensure that the previous area is clean.
491       scene->invalidate(previous_rect);
492     }
493   } else if (state == State::CHANGING_NUM_FRAMES_COLUMNS) {
494 
495     SpriteModel::Index index = model->get_selected_index();
496     if (!index.is_direction_index() && !create_multiframe_direction) {
497       cancel_state_changing_num_frames_columns();
498     }
499     else {
500       dragging_current_point = map_to_scene(event->pos(), false);
501       update_state_changing_num_frames_columns();
502     }
503   }
504 
505   if (update_selection_validity) {
506     QRect rect = current_area_item.get_direction_all_frames_rect();
507     current_area_item.set_valid(rect.isEmpty() || sceneRect().contains(rect));
508   }
509 
510   // The parent class tracks mouse movements for internal needs
511   // such as anchoring the viewport to the mouse when zooming.
512   QGraphicsView::mouseMoveEvent(event);
513 }
514 
515 /**
516  * @brief Receives a context menu event.
517  * @param event The event to handle.
518  */
contextMenuEvent(QContextMenuEvent * event)519 void SpriteView::contextMenuEvent(QContextMenuEvent* event) {
520 
521   if (scene == nullptr) {
522     return;
523   }
524 
525   QPoint where;
526   if (event->pos() != QPoint(0, 0)) {
527     where = event->pos();
528   }
529   else {
530     QList<QGraphicsItem*> selected_items = scene->selectedItems();
531     where = mapFromScene(selected_items.first()->pos() + QPoint(8, 8));
532   }
533 
534   show_context_menu(where);
535 }
536 
537 /**
538  * @brief Shows a context menu with actions relative to the selected directions.
539  *
540  * Does nothing if the view is in read-only mode.
541  *
542  * @param where Where to show the menu, in view coordinates.
543  */
show_context_menu(const QPoint & where)544 void SpriteView::show_context_menu(const QPoint& where) {
545 
546   if (model == nullptr) {
547     return;
548   }
549 
550   SpriteModel::Index index = model->get_selected_index();
551   if (!index.is_direction_index()) {
552     return;
553   }
554 
555   QMenu* menu = new QMenu(this);
556 
557   // Delete direction.
558   menu->addAction(duplicate_direction_action);
559   menu->addSeparator();
560   menu->addAction(change_num_frames_columns_action);
561   menu->addAction(change_num_frames_action);
562   menu->addAction(change_num_columns_action);
563   menu->addSeparator();
564   menu->addAction(delete_direction_action);
565 
566   // Create the menu at 1,1 to avoid the cursor being already in the first item.
567   menu->popup(viewport()->mapToGlobal(where) + QPoint(1, 1));
568 }
569 
570 /**
571  * @brief Sets the normal state.
572  */
start_state_normal()573 void SpriteView::start_state_normal() {
574 
575   this->state = State::NORMAL;
576 }
577 
578 /**
579  * @brief Moves to the state of drawing a rectangle for a selection or a
580  * new direction.
581  * @param initial_point Where the user starts drawing the rectangle,
582  * in view coordinates.
583  */
start_state_drawing_rectangle(const QPoint & initial_point)584 void SpriteView::start_state_drawing_rectangle(const QPoint& initial_point) {
585 
586   state = State::DRAWING_RECTANGLE;
587   dragging_start_point = map_to_scene(initial_point, true);
588   dragging_current_point = dragging_start_point;
589 
590   current_area_item.setPos(dragging_current_point);
591   current_area_item.set_frame_size(QSize(0, 0));
592   current_area_item.set_num_frames(1);
593   current_area_item.set_num_columns(1);
594   current_area_item.set_valid(true);
595   scene->addItem(&current_area_item);
596 }
597 
598 /**
599  * @brief Finishes drawing a rectangle.
600  */
end_state_drawing_rectangle()601 void SpriteView::end_state_drawing_rectangle() {
602 
603   QRect rectangle = current_area_item.get_direction_all_frames_rect();
604   if (!rectangle.isEmpty() &&
605       sceneRect().contains(rectangle) &&
606       !model->get_selected_index().is_direction_index()) {
607 
608     // Context menu to create a direction.
609     QMenu menu;
610     QAction* new_direction_action = new QAction(tr("New direction"), this);
611     connect(new_direction_action, &QAction::triggered, [this, rectangle] {
612       emit add_direction_requested(rectangle, 1, 1);
613     });
614     QAction* new_multiframe_direction_action =
615       new QAction(tr("New multiframe direction"), this);
616     connect(new_multiframe_direction_action, &QAction::triggered, [this] {
617       start_state_changing_num_frames_columns(
618         // TODO: add settings to change the default mode.
619         ChangingNumFramesColumnsMode::CHANGE_BOTH, true);
620     });
621     menu.addAction(new_direction_action);
622     menu.addAction(new_multiframe_direction_action);
623     menu.addSeparator();
624     menu.addAction(tr("Cancel"));
625     menu.exec(cursor().pos() + QPoint(1, 1));
626   }
627 
628   if (state != State::CHANGING_NUM_FRAMES_COLUMNS) {
629     scene->removeItem(&current_area_item);
630     start_state_normal();
631   }
632 }
633 
634 /**
635  * @brief Moves to the state of moving the selected direction.
636  * @param initial_point Where the user starts dragging the direction,
637  * in view coordinates.
638  */
start_state_moving_direction(const QPoint & initial_point)639 void SpriteView::start_state_moving_direction(const QPoint& initial_point) {
640 
641   SpriteModel::Index index = model->get_selected_index();
642   if (!index.is_direction_index()) {
643     return;
644   }
645 
646   state = State::MOVING_DIRECTION;
647   dragging_start_point = Point::floor_8(mapToScene(initial_point));
648   dragging_current_point = dragging_start_point;
649 
650   current_area_item.setPos(model->get_direction_position(index));
651   current_area_item.set_frame_size(model->get_direction_size(index));
652   current_area_item.set_num_frames(model->get_direction_num_frames(index));
653   current_area_item.set_num_columns(model->get_direction_num_columns(index));
654   current_area_item.set_valid(true);
655   scene->addItem(&current_area_item);
656 }
657 
658 /**
659  * @brief Finishes moving a direction.
660  */
end_state_moving_direction()661 void SpriteView::end_state_moving_direction() {
662 
663   SpriteModel::Index index = model->get_selected_index();
664   QRect box = current_area_item.get_direction_all_frames_rect();
665   if (!box.isEmpty() &&
666       sceneRect().contains(box) &&
667       index.is_direction_index() &&
668       box != model->get_direction_all_frames_rect(index)) {
669 
670     // Context menu to move the direction.
671     QMenu menu;
672     QAction* move_direction_action = new QAction(tr("Move here"), this);
673     connect(move_direction_action, &QAction::triggered, [this, box] {
674       emit change_selected_direction_position_requested(box.topLeft());
675     });
676     menu.addAction(move_direction_action);
677     QAction* duplicate_direction_action =
678       new QAction(QIcon(":/images/icon_copy.png"), tr("Duplicate here"), this);
679     connect(duplicate_direction_action, &QAction::triggered, [this, box] {
680       emit duplicate_selected_direction_requested(box.topLeft());
681     });
682     menu.addAction(duplicate_direction_action);
683     menu.addSeparator();
684     menu.addAction(tr("Cancel"));
685     menu.exec(cursor().pos() + QPoint(1, 1));
686   }
687 
688   scene->removeItem(&current_area_item);
689   start_state_normal();
690 }
691 
692 /**
693  * @brief Moves to the state of changing the number of frames and columns.
694  * @param mode The changing mode.
695  * @param create Whether the state change a new direction or the selected one.
696  */
start_state_changing_num_frames_columns(const ChangingNumFramesColumnsMode & mode,bool create)697 void SpriteView::start_state_changing_num_frames_columns(
698   const ChangingNumFramesColumnsMode& mode, bool create) {
699 
700   if (!create) {
701     SpriteModel::Index index = model->get_selected_index();
702     if (!index.is_direction_index()) {
703       return;
704     }
705 
706     current_area_item.setPos(model->get_direction_position(index));
707     current_area_item.set_frame_size(model->get_direction_size(index));
708     current_area_item.set_num_frames(model->get_direction_num_frames(index));
709     current_area_item.set_num_columns(model->get_direction_num_columns(index));
710     scene->addItem(&current_area_item);
711   }
712 
713   state = State::CHANGING_NUM_FRAMES_COLUMNS;
714   changing_mode = mode;
715 
716   create_multiframe_direction = create;
717   current_area_item.set_valid(true);
718 
719   dragging_current_point = map_to_scene(mapFromGlobal(QCursor::pos()), false);
720   update_state_changing_num_frames_columns();
721 }
722 
723 /**
724  * @brief Updates to the state of changing the number of frames and columns.
725  */
update_state_changing_num_frames_columns()726 void SpriteView::update_state_changing_num_frames_columns() {
727 
728   int num_frames = current_area_item.get_num_frames();
729   int num_columns = current_area_item.get_num_columns();
730   compute_num_frames_columns(num_frames, num_columns);
731 
732   current_area_item.set_num_frames(num_frames);
733   current_area_item.set_num_columns(num_columns);
734 
735   // Check validity.
736   QRect rect = current_area_item.get_direction_all_frames_rect();
737   current_area_item.set_valid(!rect.isEmpty() && sceneRect().contains(rect));
738 }
739 
740 /**
741  * @brief Finishes changing the number of frames and columns.
742  */
end_state_changing_num_frames_columns()743 void SpriteView::end_state_changing_num_frames_columns() {
744 
745   QRect rect = current_area_item.get_direction_all_frames_rect();
746   if (sceneRect().contains(rect)) {
747 
748     int num_frames = current_area_item.get_num_frames();
749     int num_columns = current_area_item.get_num_columns();
750     compute_num_frames_columns(num_frames, num_columns);
751 
752     if (create_multiframe_direction) {
753       QRect frame = QRect(
754         current_area_item.pos().toPoint(), current_area_item.get_frame_size());
755       emit add_direction_requested(frame, num_frames, num_columns);
756     }
757     else {
758       emit change_direction_num_frames_columns_requested(
759         num_frames, num_columns);
760     }
761   }
762 
763   cancel_state_changing_num_frames_columns();
764 }
765 
766 /**
767  * @brief Cancels changing the number of frames and columns.
768  */
cancel_state_changing_num_frames_columns()769 void SpriteView::cancel_state_changing_num_frames_columns() {
770 
771   scene->removeItem(&current_area_item);
772   start_state_normal();
773 }
774 
775 /**
776  * @brief Computes the current number of frames and columns for changing state.
777  * @param[out] num_frames The number of frames.
778  * @param[out] num_columns The number of columns.
779  */
compute_num_frames_columns(int & num_frames,int & num_columns)780 void SpriteView::compute_num_frames_columns(int& num_frames, int& num_columns) {
781 
782   QPoint pos = current_area_item.pos().toPoint();
783   QSize size = current_area_item.get_frame_size();
784   int direction_num_frames = current_area_item.get_num_frames();
785   int direction_num_columns = current_area_item.get_num_columns();
786 
787   int column = dragging_current_point.x() + size.width() - pos.x();
788   column = qMax(column, size.width()) / size.width();
789 
790   if (changing_mode == ChangingNumFramesColumnsMode::CHANGE_NUM_COLUMNS) {
791     num_columns = qMin(direction_num_frames, column);
792   } else {
793 
794     int row = dragging_current_point.y() + size.height() - pos.y();
795     row = qMax(row, size.height()) / size.height();
796 
797     if (changing_mode == ChangingNumFramesColumnsMode::CHANGE_NUM_FRAMES) {
798       num_frames = qMin(direction_num_columns, column);
799       num_frames += (row - 1) * direction_num_columns;
800     }
801     else if (changing_mode == ChangingNumFramesColumnsMode::CHANGE_BOTH) {
802       num_columns = column;
803       num_frames = row * num_columns;
804     }
805   }
806 }
807 
808 /**
809  * @brief Creates a selection item.
810  */
DirectionAreaItem()811 SpriteView::DirectionAreaItem::DirectionAreaItem() :
812   frame_size(8, 8),
813   num_frames(1),
814   num_columns(1),
815   is_valid(true) {
816   update_bounding_rect();
817 }
818 
819 /**
820  * @brief Returns the size of frames.
821  * @return The size of frames.
822  */
get_frame_size() const823 QSize SpriteView::DirectionAreaItem::get_frame_size() const {
824   return frame_size;
825 }
826 
827 /**
828  * @brief Returns the number of frames.
829  * @return The number of frames.
830  */
get_num_frames() const831 int SpriteView::DirectionAreaItem::get_num_frames() const {
832   return num_frames;
833 }
834 
835 /**
836  * @brief Returns the number of columns.
837  * @return The number of columns.
838  */
get_num_columns() const839 int SpriteView::DirectionAreaItem::get_num_columns() const {
840   return num_columns;
841 }
842 
843 /**
844  * @brief Changes the size of the frames.
845  * @param size The size.
846  */
set_frame_size(const QSize & size)847 void SpriteView::DirectionAreaItem::set_frame_size(const QSize& size) {
848 
849   frame_size = size;
850   update_bounding_rect();
851 }
852 
853 /**
854  * @brief Change the number of frames.
855  * @param num_frames The number of frames.
856  */
set_num_frames(int num_frames)857 void SpriteView::DirectionAreaItem::set_num_frames(int num_frames) {
858 
859   this->num_frames = qMax(num_frames, 1);
860   update_bounding_rect();
861 }
862 
863 /**
864  * @brief Changes the number of columns.
865  * @param num_columns The number of columns.
866  */
set_num_columns(int num_columns)867 void SpriteView::DirectionAreaItem::set_num_columns(int num_columns) {
868 
869   this->num_columns = qMax(num_columns, 1);
870   update_bounding_rect();
871 }
872 
873 /**
874  * @brief Changes whether the area is valid.
875  * @param valid Whether the area is valid.
876  */
set_valid(bool valid)877 void SpriteView::DirectionAreaItem::set_valid(bool valid) {
878 
879   is_valid = valid;
880 }
881 
882 /**
883  * @brief Returns a rect that contains all frames of a direction.
884  * @return The direction's frames rect.
885  */
get_direction_all_frames_rect() const886 QRect SpriteView::DirectionAreaItem::get_direction_all_frames_rect() const {
887 
888   QRectF rectf = boundingRect();
889   rectf.translate(pos());
890   return rectf.toRect();
891 }
892 
893 /**
894  * @brief Returns the bounding rect.
895  * @return The bouding rect.
896  */
boundingRect() const897 QRectF SpriteView::DirectionAreaItem::boundingRect() const {
898 
899   return bounding_rect;
900 }
901 
902 /**
903  * @brief The paint event.
904  */
paint(QPainter * painter,const QStyleOptionGraphicsItem * option,QWidget * widget)905 void SpriteView::DirectionAreaItem::paint(
906   QPainter* painter, const QStyleOptionGraphicsItem* option,
907   QWidget* widget) {
908 
909   Q_UNUSED(option);
910   Q_UNUSED(widget);
911 
912   if (frame_size.isNull()) {
913     return;
914   }
915 
916   painter->save();
917   painter->setPen(is_valid ? Qt::yellow : Qt::red);
918 
919   if (frame_size.isEmpty()) {
920     painter->drawRect(QRect(QPoint(0, 0), frame_size));
921   }
922 
923   QSize draw_size = frame_size;
924 
925   for (int i = 0; i < num_frames; ++i) {
926     int row = qFloor(i / num_columns);
927     int column = i % num_columns;
928     QPoint pos = QPoint(frame_size.width() * column, frame_size.height() * row);
929     painter->drawRect(QRect(pos, draw_size));
930   }
931 
932   painter->restore();
933 }
934 
935 /**
936  * @brief Updates the bounding rect.
937  */
update_bounding_rect()938 void SpriteView::DirectionAreaItem::update_bounding_rect() {
939 
940   int num_columns = qMin(this->num_columns, num_frames);
941   int num_rows = qFloor((num_frames - 1) / num_columns) + 1;
942 
943   prepareGeometryChange();
944   bounding_rect = QRect(
945     0, 0, frame_size.width() * num_columns, frame_size.height() * num_rows);
946 }
947 
948 }
949