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(¤t_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(¤t_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(¤t_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(¤t_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(¤t_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(¤t_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