1 | // Aseprite |
2 | // Copyright (C) 2018-2022 Igara Studio S.A. |
3 | // Copyright (C) 2001-2018 David Capello |
4 | // |
5 | // This program is distributed under the terms of |
6 | // the End-User License Agreement for Aseprite. |
7 | |
8 | #ifdef HAVE_CONFIG_H |
9 | #include "config.h" |
10 | #endif |
11 | |
12 | #include "app/ui/timeline/timeline.h" |
13 | |
14 | #include "app/app.h" |
15 | #include "app/app_menus.h" |
16 | #include "app/cmd/set_tag_range.h" |
17 | #include "app/cmd_transaction.h" |
18 | #include "app/color_utils.h" |
19 | #include "app/commands/command.h" |
20 | #include "app/commands/commands.h" |
21 | #include "app/commands/params.h" |
22 | #include "app/console.h" |
23 | #include "app/context_access.h" |
24 | #include "app/doc.h" |
25 | #include "app/doc_api.h" |
26 | #include "app/doc_event.h" |
27 | #include "app/doc_range_ops.h" |
28 | #include "app/doc_undo.h" |
29 | #include "app/i18n/strings.h" |
30 | #include "app/loop_tag.h" |
31 | #include "app/modules/editors.h" |
32 | #include "app/modules/gfx.h" |
33 | #include "app/modules/gui.h" |
34 | #include "app/thumbnails.h" |
35 | #include "app/transaction.h" |
36 | #include "app/tx.h" |
37 | #include "app/ui/app_menuitem.h" |
38 | #include "app/ui/configure_timeline_popup.h" |
39 | #include "app/ui/doc_view.h" |
40 | #include "app/ui/editor/editor.h" |
41 | #include "app/ui/input_chain.h" |
42 | #include "app/ui/skin/skin_theme.h" |
43 | #include "app/ui/status_bar.h" |
44 | #include "app/ui/workspace.h" |
45 | #include "app/ui_context.h" |
46 | #include "app/util/clipboard.h" |
47 | #include "app/util/layer_boundaries.h" |
48 | #include "app/util/layer_utils.h" |
49 | #include "app/util/readable_time.h" |
50 | #include "base/convert_to.h" |
51 | #include "base/memory.h" |
52 | #include "base/scoped_value.h" |
53 | #include "doc/doc.h" |
54 | #include "fmt/format.h" |
55 | #include "gfx/point.h" |
56 | #include "gfx/rect.h" |
57 | #include "os/font.h" |
58 | #include "os/surface.h" |
59 | #include "os/system.h" |
60 | #include "ui/ui.h" |
61 | |
62 | #include <algorithm> |
63 | #include <cstdio> |
64 | #include <vector> |
65 | |
66 | namespace app { |
67 | |
68 | using namespace app::skin; |
69 | using namespace gfx; |
70 | using namespace doc; |
71 | using namespace ui; |
72 | |
73 | enum { |
74 | PART_NOTHING = 0, |
75 | PART_TOP, |
76 | PART_SEPARATOR, |
77 | , |
78 | , |
79 | , |
80 | , |
81 | , |
82 | , |
83 | , |
84 | , |
85 | , |
86 | PART_ROW, |
87 | PART_ROW_EYE_ICON, |
88 | PART_ROW_PADLOCK_ICON, |
89 | PART_ROW_CONTINUOUS_ICON, |
90 | PART_ROW_TEXT, |
91 | PART_CEL, |
92 | PART_RANGE_OUTLINE, |
93 | PART_TAG, |
94 | PART_TAG_LEFT, |
95 | PART_TAG_RIGHT, |
96 | PART_TAGS, |
97 | PART_TAG_BAND, |
98 | PART_TAG_SWITCH_BUTTONS, |
99 | PART_TAG_SWITCH_BAND_BUTTON, |
100 | }; |
101 | |
102 | struct Timeline::DrawCelData { |
103 | CelIterator begin; |
104 | CelIterator end; |
105 | CelIterator it; |
106 | CelIterator prevIt; // Previous Cel to "it" |
107 | CelIterator nextIt; // Next Cel to "it" |
108 | CelIterator activeIt; // Active Cel iterator |
109 | CelIterator firstLink; // First link to the active cel |
110 | CelIterator lastLink; // Last link to the active cel |
111 | }; |
112 | |
113 | namespace { |
114 | |
115 | template<typename Pred> |
116 | void for_each_expanded_layer(LayerGroup* group, |
117 | Pred&& pred, |
118 | int level = 0, |
119 | LayerFlags flags = |
120 | LayerFlags(int(LayerFlags::Visible) | |
121 | int(LayerFlags::Editable))) { |
122 | if (!group->isVisible()) |
123 | flags = static_cast<LayerFlags>(int(flags) & ~int(LayerFlags::Visible)); |
124 | |
125 | if (!group->isEditable()) |
126 | flags = static_cast<LayerFlags>(int(flags) & ~int(LayerFlags::Editable)); |
127 | |
128 | for (Layer* child : group->layers()) { |
129 | if (child->isGroup() && !child->isCollapsed()) |
130 | for_each_expanded_layer<Pred>( |
131 | static_cast<LayerGroup*>(child), |
132 | std::forward<Pred>(pred), |
133 | level+1, |
134 | flags); |
135 | |
136 | pred(child, level, flags); |
137 | } |
138 | } |
139 | |
140 | bool is_copy_key_pressed(ui::Message* msg) { |
141 | return |
142 | msg->ctrlPressed() || // Ctrl is common on Windows |
143 | msg->altPressed(); // Alt is common on Mac OS X |
144 | } |
145 | |
146 | bool is_select_layer_in_canvas_key_pressed(ui::Message* msg) { |
147 | #ifdef __APPLE__ |
148 | return msg->cmdPressed(); |
149 | #else |
150 | return msg->ctrlPressed(); |
151 | #endif |
152 | } |
153 | |
154 | SelectLayerBoundariesOp get_select_layer_in_canvas_op(ui::Message* msg) { |
155 | if (msg->altPressed() && msg->shiftPressed()) |
156 | return SelectLayerBoundariesOp::INTERSECT; |
157 | else if (msg->shiftPressed()) |
158 | return SelectLayerBoundariesOp::ADD; |
159 | else if (msg->altPressed()) |
160 | return SelectLayerBoundariesOp::SUBTRACT; |
161 | else |
162 | return SelectLayerBoundariesOp::REPLACE; |
163 | } |
164 | |
165 | } // anonymous namespace |
166 | |
167 | Timeline::Hit::Hit(int part, |
168 | layer_t layer, |
169 | frame_t frame, |
170 | ObjectId tag, |
171 | int band) |
172 | : part(part), |
173 | layer(layer), |
174 | frame(frame), |
175 | tag(tag), |
176 | veryBottom(false), |
177 | band(band) |
178 | { |
179 | } |
180 | |
181 | bool Timeline::Hit::operator!=(const Hit& other) const |
182 | { |
183 | return |
184 | part != other.part || |
185 | layer != other.layer || |
186 | frame != other.frame || |
187 | tag != other.tag || |
188 | band != other.band; |
189 | } |
190 | |
191 | Tag* Timeline::Hit::getTag() const |
192 | { |
193 | return get<Tag>(tag); |
194 | } |
195 | |
196 | Timeline::DropTarget::DropTarget() |
197 | { |
198 | hhit = HNone; |
199 | vhit = VNone; |
200 | outside = false; |
201 | } |
202 | |
203 | Timeline::DropTarget::DropTarget(const DropTarget& o) |
204 | : hhit(o.hhit) |
205 | , vhit(o.vhit) |
206 | , outside(o.outside) |
207 | { |
208 | } |
209 | |
210 | Timeline::Row::Row() |
211 | : m_layer(nullptr), |
212 | m_level(0), |
213 | m_inheritedFlags(LayerFlags::None) |
214 | { |
215 | } |
216 | |
217 | Timeline::Row::Row(Layer* layer, |
218 | const int level, |
219 | const LayerFlags inheritedFlags) |
220 | : m_layer(layer), |
221 | m_level(level), |
222 | m_inheritedFlags(inheritedFlags) |
223 | { |
224 | } |
225 | |
226 | bool Timeline::Row::parentVisible() const |
227 | { |
228 | return ((int(m_inheritedFlags) & int(LayerFlags::Visible)) != 0); |
229 | } |
230 | |
231 | bool Timeline::Row::parentEditable() const |
232 | { |
233 | return ((int(m_inheritedFlags) & int(LayerFlags::Editable)) != 0); |
234 | } |
235 | |
236 | Timeline::Timeline(TooltipManager* tooltipManager) |
237 | : Widget(kGenericWidget) |
238 | , m_hbar(HORIZONTAL, this) |
239 | , m_vbar(VERTICAL, this) |
240 | , m_zoom(1.0) |
241 | , m_context(UIContext::instance()) |
242 | , m_editor(NULL) |
243 | , m_document(NULL) |
244 | , m_sprite(NULL) |
245 | , m_rangeLocks(0) |
246 | , m_state(STATE_STANDBY) |
247 | , m_tagBands(0) |
248 | , m_tagFocusBand(-1) |
249 | , m_separator_x( |
250 | Preferences::instance().general.timelineLayerPanelWidth() * guiscale()) |
251 | , m_separator_w(1) |
252 | , m_confPopup(nullptr) |
253 | , m_clipboard_timer(100, this) |
254 | , m_offset_count(0) |
255 | , m_redrawMarchingAntsOnly(false) |
256 | , m_scroll(false) |
257 | , m_fromTimeline(false) |
258 | , m_aniControls(tooltipManager) |
259 | { |
260 | enableFlags(CTRL_RIGHT_CLICK); |
261 | |
262 | m_ctxConn1 = m_context->BeforeCommandExecution.connect( |
263 | &Timeline::onBeforeCommandExecution, this); |
264 | m_ctxConn2 = m_context->AfterCommandExecution.connect( |
265 | &Timeline::onAfterCommandExecution, this); |
266 | m_context->documents().add_observer(this); |
267 | m_context->add_observer(this); |
268 | |
269 | setDoubleBuffered(true); |
270 | addChild(&m_aniControls); |
271 | addChild(&m_hbar); |
272 | addChild(&m_vbar); |
273 | |
274 | m_hbar.setTransparent(true); |
275 | m_vbar.setTransparent(true); |
276 | initTheme(); |
277 | } |
278 | |
279 | Timeline::~Timeline() |
280 | { |
281 | Preferences::instance().general.timelineLayerPanelWidth( |
282 | m_separator_x / guiscale()); |
283 | |
284 | m_clipboard_timer.stop(); |
285 | |
286 | detachDocument(); |
287 | m_context->documents().remove_observer(this); |
288 | m_context->remove_observer(this); |
289 | m_confPopup.reset(); |
290 | } |
291 | |
292 | void Timeline::setZoom(const double zoom) |
293 | { |
294 | m_zoom = std::clamp(zoom, 1.0, 10.0); |
295 | m_thumbnailsOverlayDirection = gfx::Point(int(frameBoxWidth()*1.0), int(frameBoxWidth()*0.5)); |
296 | m_thumbnailsOverlayVisible = false; |
297 | } |
298 | |
299 | void Timeline::setZoomAndUpdate(const double zoom, |
300 | const bool updatePref) |
301 | { |
302 | if (zoom != m_zoom) { |
303 | setZoom(zoom); |
304 | regenerateTagBands(); |
305 | updateScrollBars(); |
306 | invalidate(); |
307 | } |
308 | if (updatePref && zoom != docPref().thumbnails.zoom()) { |
309 | docPref().thumbnails.zoom(zoom); |
310 | docPref().thumbnails.enabled(zoom > 1); |
311 | } |
312 | } |
313 | |
314 | void Timeline::onThumbnailsPrefChange() |
315 | { |
316 | setZoomAndUpdate( |
317 | docPref().thumbnails.enabled() ? |
318 | docPref().thumbnails.zoom(): 1.0, |
319 | false); |
320 | } |
321 | |
322 | void Timeline::updateUsingEditor(Editor* editor) |
323 | { |
324 | // TODO if editor == m_editor, avoid doing a lot of extra work here |
325 | |
326 | m_aniControls.updateUsingEditor(editor); |
327 | |
328 | DocRange oldRange; |
329 | if (editor != m_editor) { |
330 | // Save active m_tagFocusBand into the old focused editor |
331 | if (m_editor) |
332 | m_editor->setTagFocusBand(m_tagFocusBand); |
333 | m_tagFocusBand = -1; |
334 | } |
335 | else { |
336 | oldRange = m_range; |
337 | } |
338 | |
339 | detachDocument(); |
340 | |
341 | if (Preferences::instance().timeline.keepSelection()) |
342 | m_range = oldRange; |
343 | else { |
344 | // The range is reset in detachDocument() |
345 | ASSERT(!m_range.enabled()); |
346 | } |
347 | |
348 | // We always update the editor. In this way the timeline keeps in |
349 | // sync with the active editor. |
350 | m_editor = editor; |
351 | if (!m_editor) |
352 | return; // No editor specified. |
353 | |
354 | m_editor->add_observer(this); |
355 | m_tagFocusBand = m_editor->tagFocusBand(); |
356 | |
357 | Site site; |
358 | DocView* view = m_editor->getDocView(); |
359 | view->getSite(&site); |
360 | |
361 | site.document()->add_observer(this); |
362 | |
363 | Doc* app_document = site.document(); |
364 | DocumentPreferences& docPref = Preferences::instance().document(app_document); |
365 | |
366 | m_thumbnailsPrefConn = docPref.thumbnails.AfterChange.connect( |
367 | [this]{ onThumbnailsPrefChange(); }); |
368 | |
369 | setZoom( |
370 | docPref.thumbnails.enabled() ? |
371 | docPref.thumbnails.zoom(): 1.0); |
372 | |
373 | // If we are already in the same position as the "editor", we don't |
374 | // need to update the at all timeline. |
375 | if (m_document == site.document() && |
376 | m_sprite == site.sprite() && |
377 | m_layer == site.layer() && |
378 | m_frame == site.frame()) |
379 | return; |
380 | |
381 | m_document = site.document(); |
382 | m_sprite = site.sprite(); |
383 | m_layer = site.layer(); |
384 | m_frame = site.frame(); |
385 | m_state = STATE_STANDBY; |
386 | m_hot.part = PART_NOTHING; |
387 | m_clk.part = PART_NOTHING; |
388 | |
389 | m_firstFrameConn = Preferences::instance().document(m_document) |
390 | .timeline.firstFrame.AfterChange.connect([this]{ invalidate(); }); |
391 | |
392 | setFocusStop(true); |
393 | regenerateRows(); |
394 | setViewScroll(viewScroll()); |
395 | showCurrentCel(); |
396 | } |
397 | |
398 | void Timeline::detachDocument() |
399 | { |
400 | m_firstFrameConn.disconnect(); |
401 | |
402 | if (m_document) { |
403 | m_thumbnailsPrefConn.disconnect(); |
404 | m_document->remove_observer(this); |
405 | m_document = nullptr; |
406 | } |
407 | |
408 | // Reset all pointers to this document, even DocRanges, we don't |
409 | // want to store a pointer to a layer of a document that we are not |
410 | // observing anymore (because the document might be deleted soon). |
411 | m_sprite = nullptr; |
412 | m_layer = nullptr; |
413 | m_range.clearRange(); |
414 | m_startRange.clearRange(); |
415 | m_dropRange.clearRange(); |
416 | |
417 | if (m_editor) { |
418 | m_editor->remove_observer(this); |
419 | m_editor = nullptr; |
420 | } |
421 | |
422 | invalidate(); |
423 | } |
424 | |
425 | bool Timeline::isMovingCel() const |
426 | { |
427 | return (m_state == STATE_MOVING_RANGE && |
428 | m_range.type() == Range::kCels); |
429 | } |
430 | |
431 | bool Timeline::selectedLayersBounds(const SelectedLayers& layers, |
432 | layer_t* first, layer_t* last) const |
433 | { |
434 | if (layers.empty()) |
435 | return false; |
436 | |
437 | *first = *last = getLayerIndex(*layers.begin()); |
438 | |
439 | for (auto layer : layers) { |
440 | layer_t i = getLayerIndex(layer); |
441 | if (*first > i) *first = i; |
442 | if (*last < i) *last = i; |
443 | } |
444 | |
445 | return true; |
446 | } |
447 | |
448 | void Timeline::setLayer(Layer* layer) |
449 | { |
450 | ASSERT(m_editor != NULL); |
451 | |
452 | invalidateLayer(m_layer); |
453 | invalidateLayer(layer); |
454 | |
455 | m_layer = layer; |
456 | |
457 | // Expand all parents |
458 | if (m_layer) { |
459 | LayerGroup* group = m_layer->parent(); |
460 | while (group != m_layer->sprite()->root()) { |
461 | // Expand this group |
462 | group->setCollapsed(false); |
463 | group = group->parent(); |
464 | } |
465 | regenerateRows(); |
466 | invalidate(); |
467 | } |
468 | |
469 | if (m_editor->layer() != layer) |
470 | m_editor->setLayer(m_layer); |
471 | } |
472 | |
473 | void Timeline::setFrame(frame_t frame, bool byUser) |
474 | { |
475 | ASSERT(m_editor != NULL); |
476 | // ASSERT(frame >= 0 && frame < m_sprite->totalFrames()); |
477 | |
478 | if (frame < 0) |
479 | frame = firstFrame(); |
480 | else if (frame >= m_sprite->totalFrames()) |
481 | frame = frame_t(m_sprite->totalFrames()-1); |
482 | |
483 | if (m_layer) { |
484 | Cel* oldCel = m_layer->cel(m_frame); |
485 | Cel* newCel = m_layer->cel(frame); |
486 | std::size_t oldLinks = (oldCel ? oldCel->links(): 0); |
487 | std::size_t newLinks = (newCel ? newCel->links(): 0); |
488 | if ((oldLinks && !newCel) || |
489 | (newLinks && !oldCel) || |
490 | ((oldLinks || newLinks) && (oldCel->data() != newCel->data()))) |
491 | invalidateLayer(m_layer); |
492 | } |
493 | |
494 | invalidateFrame(m_frame); |
495 | invalidateFrame(frame); |
496 | |
497 | gfx::Rect onionRc = getOnionskinFramesBounds(); |
498 | |
499 | m_frame = frame; |
500 | |
501 | // Invalidate the onionskin handles area |
502 | onionRc |= getOnionskinFramesBounds(); |
503 | if (!onionRc.isEmpty()) |
504 | invalidateRect(onionRc.offset(origin())); |
505 | |
506 | if (m_editor->frame() != frame) { |
507 | const bool isPlaying = m_editor->isPlaying(); |
508 | |
509 | if (isPlaying) |
510 | m_editor->stop(); |
511 | |
512 | m_editor->setFrame(m_frame); |
513 | |
514 | if (isPlaying) |
515 | m_editor->play(false, |
516 | Preferences::instance().editor.playAll()); |
517 | } |
518 | } |
519 | |
520 | void Timeline::prepareToMoveRange() |
521 | { |
522 | ASSERT(m_range.enabled()); |
523 | |
524 | layer_t i = 0; |
525 | for (auto layer : m_range.selectedLayers().toBrowsableLayerList()) { |
526 | if (layer == m_layer) |
527 | break; |
528 | ++i; |
529 | } |
530 | |
531 | frame_t j = 0; |
532 | for (auto frame : m_range.selectedFrames()) { |
533 | if (frame == m_frame) |
534 | break; |
535 | ++j; |
536 | } |
537 | |
538 | m_moveRangeData.activeRelativeLayer = i; |
539 | m_moveRangeData.activeRelativeFrame = j; |
540 | } |
541 | |
542 | void Timeline::moveRange(const Range& range) |
543 | { |
544 | regenerateRows(); |
545 | |
546 | // We have to change the range before we generate an |
547 | // onActiveSiteChange() event so observers (like cel properties |
548 | // dialog) know the new selected range. |
549 | m_range = range; |
550 | |
551 | layer_t i = 0; |
552 | for (auto layer : range.selectedLayers().toBrowsableLayerList()) { |
553 | if (i == m_moveRangeData.activeRelativeLayer) { |
554 | setLayer(layer); |
555 | break; |
556 | } |
557 | ++i; |
558 | } |
559 | |
560 | frame_t j = 0; |
561 | for (auto frame : range.selectedFrames()) { |
562 | if (j == m_moveRangeData.activeRelativeFrame) { |
563 | setFrame(frame, true); |
564 | break; |
565 | } |
566 | ++j; |
567 | } |
568 | |
569 | // Select the range again (it might be lost between all the |
570 | // setLayer()/setFrame() calls). |
571 | m_range = range; |
572 | } |
573 | |
574 | void Timeline::setRange(const Range& range) |
575 | { |
576 | m_range = range; |
577 | invalidate(); |
578 | } |
579 | |
580 | void Timeline::activateClipboardRange() |
581 | { |
582 | m_clipboard_timer.start(); |
583 | invalidate(); |
584 | } |
585 | |
586 | Tag* Timeline::getTagByFrame(const frame_t frame, |
587 | const bool getLoopTagIfNone) |
588 | { |
589 | if (!m_sprite) |
590 | return nullptr; |
591 | |
592 | if (m_tagFocusBand < 0) { |
593 | Tag* tag = get_animation_tag(m_sprite, frame); |
594 | if (!tag && getLoopTagIfNone) |
595 | tag = get_loop_tag(m_sprite); |
596 | return tag; |
597 | } |
598 | |
599 | for (Tag* tag : m_sprite->tags()) { |
600 | if (frame >= tag->fromFrame() && |
601 | frame <= tag->toFrame() && |
602 | m_tagBand[tag] == m_tagFocusBand) { |
603 | return tag; |
604 | } |
605 | } |
606 | |
607 | return nullptr; |
608 | } |
609 | |
610 | bool Timeline::onProcessMessage(Message* msg) |
611 | { |
612 | switch (msg->type()) { |
613 | |
614 | case kFocusEnterMessage: |
615 | App::instance()->inputChain().prioritize(this, msg); |
616 | break; |
617 | |
618 | case kTimerMessage: |
619 | if (static_cast<TimerMessage*>(msg)->timer() == &m_clipboard_timer) { |
620 | Doc* clipboard_document; |
621 | DocRange clipboard_range; |
622 | Clipboard::instance()->getDocumentRangeInfo( |
623 | &clipboard_document, |
624 | &clipboard_range); |
625 | |
626 | if (isVisible() && |
627 | m_document && |
628 | m_document == clipboard_document) { |
629 | // Set offset to make selection-movement effect |
630 | if (m_offset_count < 7) |
631 | m_offset_count++; |
632 | else |
633 | m_offset_count = 0; |
634 | |
635 | bool redrawOnlyMarchingAnts = getUpdateRegion().isEmpty(); |
636 | invalidateRect(gfx::Rect(getRangeBounds(clipboard_range)).offset(origin())); |
637 | if (redrawOnlyMarchingAnts) |
638 | m_redrawMarchingAntsOnly = true; |
639 | } |
640 | else if (m_clipboard_timer.isRunning()) { |
641 | m_clipboard_timer.stop(); |
642 | } |
643 | } |
644 | break; |
645 | |
646 | case kMouseDownMessage: { |
647 | MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg); |
648 | |
649 | if (!m_document) |
650 | break; |
651 | |
652 | if (mouseMsg->middle() || |
653 | os::instance()->isKeyPressed(kKeySpace)) { |
654 | captureMouse(); |
655 | m_state = STATE_SCROLLING; |
656 | m_oldPos = static_cast<MouseMessage*>(msg)->position(); |
657 | return true; |
658 | } |
659 | |
660 | // As we can ctrl+click color bar + timeline, now we have to |
661 | // re-prioritize timeline on each click. |
662 | App::instance()->inputChain().prioritize(this, msg); |
663 | |
664 | // Update hot part (as the user might have left clicked with |
665 | // Ctrl on OS X, which it's converted to a right-click and it's |
666 | // interpreted as other action by the Timeline::hitTest()) |
667 | setHot(hitTest(msg, mouseMsg->position() - bounds().origin())); |
668 | |
669 | // Clicked-part = hot-part. |
670 | m_clk = m_hot; |
671 | |
672 | // With Ctrl+click (Win/Linux) or Shift+click (OS X) we can |
673 | // select non-adjacents layer/frame ranges |
674 | bool clearRange = |
675 | #if !defined(__APPLE__) |
676 | !msg->ctrlPressed() && |
677 | #endif |
678 | !msg->shiftPressed(); |
679 | |
680 | captureMouse(); |
681 | |
682 | switch (m_hot.part) { |
683 | |
684 | case PART_SEPARATOR: |
685 | m_state = STATE_MOVING_SEPARATOR; |
686 | break; |
687 | |
688 | case PART_HEADER_EYE: { |
689 | ASSERT(m_sprite); |
690 | if (!m_sprite) |
691 | break; |
692 | |
693 | bool regenRows = false; |
694 | bool newVisibleState = !allLayersVisible(); |
695 | for (Layer* topLayer : m_sprite->root()->layers()) { |
696 | if (topLayer->isVisible() != newVisibleState) { |
697 | topLayer->setVisible(newVisibleState); |
698 | if (topLayer->isGroup()) |
699 | regenRows = true; |
700 | } |
701 | } |
702 | |
703 | if (regenRows) { |
704 | regenerateRows(); |
705 | invalidate(); |
706 | } |
707 | |
708 | // Redraw all views. |
709 | m_document->notifyGeneralUpdate(); |
710 | break; |
711 | } |
712 | |
713 | case PART_HEADER_PADLOCK: { |
714 | ASSERT(m_sprite); |
715 | if (!m_sprite) |
716 | break; |
717 | |
718 | bool regenRows = false; |
719 | bool newEditableState = !allLayersUnlocked(); |
720 | for (Layer* topLayer : m_sprite->root()->layers()) { |
721 | if (topLayer->isEditable() != newEditableState) { |
722 | topLayer->setEditable(newEditableState); |
723 | if (topLayer->isGroup()) { |
724 | regenRows = true; |
725 | } |
726 | } |
727 | } |
728 | |
729 | if (regenRows) { |
730 | regenerateRows(); |
731 | invalidate(); |
732 | } |
733 | break; |
734 | } |
735 | |
736 | case PART_HEADER_CONTINUOUS: { |
737 | bool newContinuousState = !allLayersContinuous(); |
738 | for (size_t i=0; i<m_rows.size(); i++) |
739 | m_rows[i].layer()->setContinuous(newContinuousState); |
740 | invalidate(); |
741 | break; |
742 | } |
743 | |
744 | case PART_HEADER_ONIONSKIN: { |
745 | docPref().onionskin.active(!docPref().onionskin.active()); |
746 | invalidate(); |
747 | break; |
748 | } |
749 | case PART_HEADER_ONIONSKIN_RANGE_LEFT: { |
750 | m_state = STATE_MOVING_ONIONSKIN_RANGE_LEFT; |
751 | m_origFrames = docPref().onionskin.prevFrames(); |
752 | break; |
753 | } |
754 | case PART_HEADER_ONIONSKIN_RANGE_RIGHT: { |
755 | m_state = STATE_MOVING_ONIONSKIN_RANGE_RIGHT; |
756 | m_origFrames = docPref().onionskin.nextFrames(); |
757 | break; |
758 | } |
759 | case PART_HEADER_FRAME: { |
760 | bool selectFrame = (mouseMsg->left() || !isFrameActive(m_clk.frame)); |
761 | |
762 | if (selectFrame) { |
763 | m_state = STATE_SELECTING_FRAMES; |
764 | if (clearRange) |
765 | clearAndInvalidateRange(); |
766 | m_range.startRange(m_layer, m_clk.frame, Range::kFrames); |
767 | m_startRange = m_range; |
768 | invalidateRange(); |
769 | |
770 | setFrame(m_clk.frame, true); |
771 | } |
772 | break; |
773 | } |
774 | case PART_ROW_TEXT: { |
775 | base::ScopedValue<bool> lock(m_fromTimeline, true, false); |
776 | const layer_t old_layer = getLayerIndex(m_layer); |
777 | const bool selectLayer = (mouseMsg->left() || !isLayerActive(m_clk.layer)); |
778 | const bool selectLayerInCanvas = |
779 | (m_clk.layer != -1 && |
780 | mouseMsg->left() && |
781 | is_select_layer_in_canvas_key_pressed(mouseMsg)); |
782 | |
783 | if (selectLayerInCanvas) { |
784 | select_layer_boundaries(m_rows[m_clk.layer].layer(), m_frame, |
785 | get_select_layer_in_canvas_op(mouseMsg)); |
786 | } |
787 | else if (selectLayer) { |
788 | m_state = STATE_SELECTING_LAYERS; |
789 | if (clearRange) |
790 | clearAndInvalidateRange(); |
791 | m_range.startRange(m_rows[m_clk.layer].layer(), |
792 | m_frame, Range::kLayers); |
793 | m_startRange = m_range; |
794 | invalidateRange(); |
795 | |
796 | // Did the user select another layer? |
797 | if (old_layer != m_clk.layer) { |
798 | setLayer(m_rows[m_clk.layer].layer()); |
799 | invalidate(); |
800 | } |
801 | } |
802 | |
803 | // Change the scroll to show the new selected layer/cel. |
804 | showCel(m_clk.layer, m_frame); |
805 | break; |
806 | } |
807 | |
808 | case PART_ROW_EYE_ICON: |
809 | if (validLayer(m_clk.layer)) { |
810 | Row& row = m_rows[m_clk.layer]; |
811 | Layer* layer = row.layer(); |
812 | ASSERT(layer) |
813 | |
814 | // Hide everything or restore alternative state |
815 | bool oneWithInternalState = false; |
816 | if (msg->altPressed()) { |
817 | for (const Row& row : m_rows) { |
818 | const Layer* l = row.layer(); |
819 | if (l->hasFlags(LayerFlags::Internal_WasVisible)) { |
820 | oneWithInternalState = true; |
821 | break; |
822 | } |
823 | } |
824 | |
825 | // If there is one layer with the internal state, restore the previous visible state |
826 | if (oneWithInternalState) { |
827 | for (Row& row : m_rows) { |
828 | Layer* l = row.layer(); |
829 | if (l->hasFlags(LayerFlags::Internal_WasVisible)) { |
830 | l->setVisible(true); |
831 | l->switchFlags(LayerFlags::Internal_WasVisible, false); |
832 | } |
833 | else { |
834 | l->setVisible(false); |
835 | } |
836 | } |
837 | } |
838 | // In other case, hide everything |
839 | else { |
840 | for (Row& row : m_rows) { |
841 | Layer* l = row.layer(); |
842 | l->switchFlags(LayerFlags::Internal_WasVisible, l->isVisible()); |
843 | l->setVisible(false); |
844 | } |
845 | } |
846 | |
847 | regenerateRows(); |
848 | invalidate(); |
849 | |
850 | m_document->notifyGeneralUpdate(); |
851 | } |
852 | |
853 | if (layer->isVisible() && !oneWithInternalState) |
854 | m_state = STATE_HIDING_LAYERS; |
855 | else |
856 | m_state = STATE_SHOWING_LAYERS; |
857 | |
858 | setLayerVisibleFlag(m_clk.layer, m_state == STATE_SHOWING_LAYERS); |
859 | } |
860 | break; |
861 | |
862 | case PART_ROW_PADLOCK_ICON: |
863 | if (validLayer(m_hot.layer)) { |
864 | Row& row = m_rows[m_clk.layer]; |
865 | Layer* layer = row.layer(); |
866 | ASSERT(layer); |
867 | if (layer->isEditable()) |
868 | m_state = STATE_LOCKING_LAYERS; |
869 | else |
870 | m_state = STATE_UNLOCKING_LAYERS; |
871 | |
872 | setLayerEditableFlag(m_clk.layer, m_state == STATE_UNLOCKING_LAYERS); |
873 | } |
874 | break; |
875 | |
876 | case PART_ROW_CONTINUOUS_ICON: |
877 | if (validLayer(m_hot.layer)) { |
878 | Row& row = m_rows[m_clk.layer]; |
879 | Layer* layer = row.layer(); |
880 | ASSERT(layer); |
881 | |
882 | if (layer->isImage()) { |
883 | if (layer->isContinuous()) |
884 | m_state = STATE_DISABLING_CONTINUOUS_LAYERS; |
885 | else |
886 | m_state = STATE_ENABLING_CONTINUOUS_LAYERS; |
887 | |
888 | setLayerContinuousFlag(m_clk.layer, m_state == STATE_ENABLING_CONTINUOUS_LAYERS); |
889 | } |
890 | else if (layer->isGroup()) { |
891 | if (layer->isCollapsed()) |
892 | m_state = STATE_EXPANDING_LAYERS; |
893 | else |
894 | m_state = STATE_COLLAPSING_LAYERS; |
895 | |
896 | setLayerCollapsedFlag(m_clk.layer, m_state == STATE_COLLAPSING_LAYERS); |
897 | updateByMousePos(msg, mousePosInClientBounds()); |
898 | |
899 | // The m_clk might have changed because we've |
900 | // expanded/collapsed a group just right now (i.e. we've |
901 | // called regenerateRows()) |
902 | m_clk = m_hot; |
903 | |
904 | ASSERT(m_rows[m_clk.layer].layer() == layer); |
905 | } |
906 | } |
907 | break; |
908 | |
909 | case PART_CEL: { |
910 | base::ScopedValue<bool> lock(m_fromTimeline, true, false); |
911 | const layer_t old_layer = getLayerIndex(m_layer); |
912 | const bool selectCel = (mouseMsg->left() |
913 | || !isLayerActive(m_clk.layer) |
914 | || !isFrameActive(m_clk.frame)); |
915 | const bool selectCelInCanvas = |
916 | (m_clk.layer != -1 && |
917 | mouseMsg->left() && |
918 | is_select_layer_in_canvas_key_pressed(mouseMsg)); |
919 | const frame_t old_frame = m_frame; |
920 | |
921 | if (selectCelInCanvas) { |
922 | select_layer_boundaries(m_rows[m_clk.layer].layer(), |
923 | m_clk.frame, |
924 | get_select_layer_in_canvas_op(mouseMsg)); |
925 | } |
926 | else { |
927 | if (selectCel) { |
928 | m_state = STATE_SELECTING_CELS; |
929 | if (clearRange) |
930 | clearAndInvalidateRange(); |
931 | m_range.startRange(m_rows[m_clk.layer].layer(), |
932 | m_clk.frame, Range::kCels); |
933 | m_startRange = m_range; |
934 | invalidateRange(); |
935 | } |
936 | |
937 | // Select the new clicked-part. |
938 | if (old_layer != m_clk.layer |
939 | || old_frame != m_clk.frame) { |
940 | setLayer(m_rows[m_clk.layer].layer()); |
941 | setFrame(m_clk.frame, true); |
942 | invalidate(); |
943 | } |
944 | } |
945 | |
946 | // Change the scroll to show the new selected cel. |
947 | showCel(m_clk.layer, m_frame); |
948 | invalidate(); |
949 | break; |
950 | } |
951 | case PART_RANGE_OUTLINE: |
952 | m_state = STATE_MOVING_RANGE; |
953 | |
954 | // If we select the outline of a cels range, we have to |
955 | // recalculate the dragged cel (m_clk) using a special |
956 | // hitTestCel() and limiting the clicked cel inside the |
957 | // range bounds. |
958 | if (m_range.type() == Range::kCels) { |
959 | m_clk = hitTestCel(mouseMsg->position() - bounds().origin()); |
960 | |
961 | if (m_range.layers() > 0) { |
962 | layer_t layerFirst, layerLast; |
963 | if (selectedLayersBounds(selectedLayers(), |
964 | &layerFirst, &layerLast)) { |
965 | layer_t layerIdx = m_clk.layer; |
966 | layerIdx = std::clamp(layerIdx, layerFirst, layerLast); |
967 | m_clk.layer = layerIdx; |
968 | } |
969 | } |
970 | |
971 | if (m_clk.frame < m_range.firstFrame()) |
972 | m_clk.frame = m_range.firstFrame(); |
973 | else if (m_clk.frame > m_range.lastFrame()) |
974 | m_clk.frame = m_range.lastFrame(); |
975 | } |
976 | break; |
977 | |
978 | case PART_TAG: |
979 | m_state = STATE_MOVING_TAG; |
980 | m_resizeTagData.reset(m_clk.tag); |
981 | break; |
982 | case PART_TAG_LEFT: |
983 | m_state = STATE_RESIZING_TAG_LEFT; |
984 | m_resizeTagData.reset(m_clk.tag); |
985 | // TODO reduce the scope of the invalidation |
986 | invalidate(); |
987 | break; |
988 | case PART_TAG_RIGHT: |
989 | m_state = STATE_RESIZING_TAG_RIGHT; |
990 | m_resizeTagData.reset(m_clk.tag); |
991 | invalidate(); |
992 | break; |
993 | |
994 | } |
995 | |
996 | // Redraw the new clicked part (header, layer or cel). |
997 | invalidateHit(m_clk); |
998 | break; |
999 | } |
1000 | |
1001 | case kMouseLeaveMessage: { |
1002 | if (m_hot.part != PART_NOTHING) { |
1003 | invalidateHit(m_hot); |
1004 | m_hot = Hit(); |
1005 | } |
1006 | break; |
1007 | } |
1008 | |
1009 | case kMouseMoveMessage: { |
1010 | if (!m_document) |
1011 | break; |
1012 | |
1013 | gfx::Point mousePos = static_cast<MouseMessage*>(msg)->position() |
1014 | - bounds().origin(); |
1015 | |
1016 | Hit hit; |
1017 | setHot(hit = hitTest(msg, mousePos)); |
1018 | |
1019 | if (hasCapture()) { |
1020 | switch (m_state) { |
1021 | |
1022 | case STATE_SCROLLING: { |
1023 | gfx::Point absMousePos = static_cast<MouseMessage*>(msg)->position(); |
1024 | setViewScroll( |
1025 | viewScroll() - gfx::Point( |
1026 | (absMousePos.x - m_oldPos.x), |
1027 | (absMousePos.y - m_oldPos.y))); |
1028 | |
1029 | m_oldPos = absMousePos; |
1030 | return true; |
1031 | } |
1032 | |
1033 | case STATE_MOVING_ONIONSKIN_RANGE_LEFT: { |
1034 | gfx::Rect onionRc = getOnionskinFramesBounds(); |
1035 | |
1036 | int newValue = m_origFrames + (m_clk.frame - hit.frame); |
1037 | docPref().onionskin.prevFrames(std::max(0, newValue)); |
1038 | |
1039 | onionRc |= getOnionskinFramesBounds(); |
1040 | invalidateRect(onionRc.offset(origin())); |
1041 | return true; |
1042 | } |
1043 | |
1044 | case STATE_MOVING_ONIONSKIN_RANGE_RIGHT: { |
1045 | gfx::Rect onionRc = getOnionskinFramesBounds(); |
1046 | |
1047 | int newValue = m_origFrames - (m_clk.frame - hit.frame); |
1048 | docPref().onionskin.nextFrames(std::max(0, newValue)); |
1049 | |
1050 | onionRc |= getOnionskinFramesBounds(); |
1051 | invalidateRect(onionRc.offset(origin())); |
1052 | return true; |
1053 | } |
1054 | |
1055 | case STATE_SHOWING_LAYERS: |
1056 | case STATE_HIDING_LAYERS: |
1057 | m_clk = hit; |
1058 | if (hit.part == PART_ROW_EYE_ICON) { |
1059 | setLayerVisibleFlag(hit.layer, m_state == STATE_SHOWING_LAYERS); |
1060 | } |
1061 | break; |
1062 | |
1063 | case STATE_LOCKING_LAYERS: |
1064 | case STATE_UNLOCKING_LAYERS: |
1065 | m_clk = hit; |
1066 | if (hit.part == PART_ROW_PADLOCK_ICON) { |
1067 | setLayerEditableFlag(hit.layer, m_state == STATE_UNLOCKING_LAYERS); |
1068 | } |
1069 | break; |
1070 | |
1071 | case STATE_ENABLING_CONTINUOUS_LAYERS: |
1072 | case STATE_DISABLING_CONTINUOUS_LAYERS: |
1073 | m_clk = hit; |
1074 | if (hit.part == PART_ROW_CONTINUOUS_ICON) { |
1075 | setLayerContinuousFlag(hit.layer, m_state == STATE_ENABLING_CONTINUOUS_LAYERS); |
1076 | } |
1077 | break; |
1078 | |
1079 | case STATE_EXPANDING_LAYERS: |
1080 | case STATE_COLLAPSING_LAYERS: |
1081 | m_clk = hit; |
1082 | if (hit.part == PART_ROW_CONTINUOUS_ICON) { |
1083 | setLayerCollapsedFlag(hit.layer, m_state == STATE_COLLAPSING_LAYERS); |
1084 | updateByMousePos(msg, mousePosInClientBounds()); |
1085 | } |
1086 | break; |
1087 | |
1088 | } |
1089 | |
1090 | // If the mouse pressed the mouse's button in the separator, |
1091 | // we shouldn't change the hot (so the separator can be |
1092 | // tracked to the mouse's released). |
1093 | if (m_clk.part == PART_SEPARATOR) { |
1094 | setSeparatorX(mousePos.x); |
1095 | layout(); |
1096 | return true; |
1097 | } |
1098 | } |
1099 | |
1100 | updateDropRange(mousePos); |
1101 | |
1102 | if (hasCapture()) { |
1103 | switch (m_state) { |
1104 | |
1105 | case STATE_MOVING_RANGE: { |
1106 | frame_t newFrame; |
1107 | if (m_range.type() == Range::kLayers) { |
1108 | // If we are moving only layers we don't change the |
1109 | // current frame. |
1110 | newFrame = m_frame; |
1111 | } |
1112 | else { |
1113 | frame_t firstDrawableFrame; |
1114 | frame_t lastDrawableFrame; |
1115 | getDrawableFrames(&firstDrawableFrame, &lastDrawableFrame); |
1116 | |
1117 | if (hit.frame < firstDrawableFrame) |
1118 | newFrame = firstDrawableFrame - 1; |
1119 | else if (hit.frame > lastDrawableFrame) |
1120 | newFrame = lastDrawableFrame + 1; |
1121 | else |
1122 | newFrame = hit.frame; |
1123 | } |
1124 | |
1125 | layer_t newLayer; |
1126 | if (m_range.type() == Range::kFrames) { |
1127 | // If we are moving only frames we don't change the |
1128 | // current layer. |
1129 | newLayer = getLayerIndex(m_layer); |
1130 | } |
1131 | else { |
1132 | layer_t firstDrawableLayer; |
1133 | layer_t lastDrawableLayer; |
1134 | getDrawableLayers(&firstDrawableLayer, &lastDrawableLayer); |
1135 | |
1136 | if (hit.layer < firstDrawableLayer) |
1137 | newLayer = firstDrawableLayer - 1; |
1138 | else if (hit.layer > lastDrawableLayer) |
1139 | newLayer = lastDrawableLayer + 1; |
1140 | else |
1141 | newLayer = hit.layer; |
1142 | } |
1143 | |
1144 | showCel(newLayer, newFrame); |
1145 | break; |
1146 | } |
1147 | |
1148 | case STATE_SELECTING_LAYERS: { |
1149 | Layer* hitLayer = m_rows[hit.layer].layer(); |
1150 | if (m_layer != hitLayer) { |
1151 | m_clk.layer = hit.layer; |
1152 | |
1153 | // We have to change the range before we generate an |
1154 | // onActiveSiteChange() event so observers (like cel |
1155 | // properties dialog) know the new selected range. |
1156 | m_range = m_startRange; |
1157 | m_range.endRange(hitLayer, m_frame); |
1158 | |
1159 | setLayer(hitLayer); |
1160 | } |
1161 | break; |
1162 | } |
1163 | |
1164 | case STATE_SELECTING_FRAMES: { |
1165 | invalidateRange(); |
1166 | |
1167 | m_range = m_startRange; |
1168 | m_range.endRange(m_layer, hit.frame); |
1169 | |
1170 | setFrame(m_clk.frame = hit.frame, true); |
1171 | |
1172 | invalidateRange(); |
1173 | break; |
1174 | } |
1175 | |
1176 | case STATE_SELECTING_CELS: { |
1177 | Layer* hitLayer = m_rows[hit.layer].layer(); |
1178 | if ((m_layer != hitLayer) || (m_frame != hit.frame)) { |
1179 | m_clk.layer = hit.layer; |
1180 | |
1181 | m_range = m_startRange; |
1182 | m_range.endRange(hitLayer, hit.frame); |
1183 | |
1184 | setLayer(hitLayer); |
1185 | setFrame(m_clk.frame = hit.frame, true); |
1186 | } |
1187 | break; |
1188 | } |
1189 | |
1190 | case STATE_MOVING_TAG: |
1191 | // TODO |
1192 | break; |
1193 | |
1194 | case STATE_RESIZING_TAG_LEFT: |
1195 | case STATE_RESIZING_TAG_RIGHT: { |
1196 | auto tag = doc::get<doc::Tag>(m_resizeTagData.tag); |
1197 | if (tag) { |
1198 | switch (m_state) { |
1199 | case STATE_RESIZING_TAG_LEFT: |
1200 | m_resizeTagData.from = std::clamp(hit.frame, 0, tag->toFrame()); |
1201 | break; |
1202 | case STATE_RESIZING_TAG_RIGHT: |
1203 | m_resizeTagData.to = std::clamp(hit.frame, tag->fromFrame(), m_sprite->lastFrame()); |
1204 | break; |
1205 | } |
1206 | invalidate(); |
1207 | } |
1208 | break; |
1209 | } |
1210 | |
1211 | } |
1212 | } |
1213 | |
1214 | updateStatusBar(msg); |
1215 | updateCelOverlayBounds(hit); |
1216 | return true; |
1217 | } |
1218 | |
1219 | case kMouseUpMessage: |
1220 | if (hasCapture()) { |
1221 | ASSERT(m_document != NULL); |
1222 | |
1223 | MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg); |
1224 | |
1225 | if (m_state == STATE_SCROLLING) { |
1226 | m_state = STATE_STANDBY; |
1227 | releaseMouse(); |
1228 | return true; |
1229 | } |
1230 | |
1231 | bool regenRows = false; |
1232 | bool relayout = false; |
1233 | setHot(hitTest(msg, mouseMsg->position() - bounds().origin())); |
1234 | |
1235 | switch (m_hot.part) { |
1236 | |
1237 | case PART_HEADER_GEAR: { |
1238 | gfx::Rect gearBounds = |
1239 | getPartBounds(Hit(PART_HEADER_GEAR)).offset(bounds().origin()); |
1240 | |
1241 | if (!m_confPopup) { |
1242 | m_confPopup.reset(new ConfigureTimelinePopup); |
1243 | m_confPopup->remapWindow(); |
1244 | } |
1245 | |
1246 | if (!m_confPopup->isVisible()) { |
1247 | gfx::Rect bounds = m_confPopup->bounds(); |
1248 | ui::fit_bounds(display(), BOTTOM, gearBounds, bounds); |
1249 | ui::fit_bounds(display(), m_confPopup.get(), bounds); |
1250 | m_confPopup->openWindow(); |
1251 | } |
1252 | else { |
1253 | m_confPopup->closeWindow(nullptr); |
1254 | } |
1255 | break; |
1256 | } |
1257 | |
1258 | case PART_HEADER_FRAME: |
1259 | // Show the frame pop-up menu. |
1260 | if (mouseMsg->right()) { |
1261 | if (m_clk.frame == m_hot.frame) { |
1262 | Menu* = AppMenus::instance()->getFramePopupMenu(); |
1263 | if (popupMenu) { |
1264 | popupMenu->showPopup(mouseMsg->position(), display()); |
1265 | |
1266 | m_state = STATE_STANDBY; |
1267 | invalidate(); |
1268 | } |
1269 | } |
1270 | } |
1271 | break; |
1272 | |
1273 | case PART_ROW_TEXT: |
1274 | // Show the layer pop-up menu. |
1275 | if (mouseMsg->right()) { |
1276 | if (m_clk.layer == m_hot.layer) { |
1277 | Menu* = AppMenus::instance()->getLayerPopupMenu(); |
1278 | if (popupMenu) { |
1279 | popupMenu->showPopup(mouseMsg->position(), display()); |
1280 | |
1281 | m_state = STATE_STANDBY; |
1282 | invalidate(); |
1283 | } |
1284 | } |
1285 | } |
1286 | break; |
1287 | |
1288 | case PART_CEL: { |
1289 | // Show the cel pop-up menu. |
1290 | if (mouseMsg->right()) { |
1291 | Menu* = |
1292 | (m_state == STATE_MOVING_RANGE && |
1293 | m_range.type() == Range::kCels && |
1294 | (m_hot.layer != m_clk.layer || |
1295 | m_hot.frame != m_clk.frame)) ? |
1296 | AppMenus::instance()->getCelMovementPopupMenu(): |
1297 | AppMenus::instance()->getCelPopupMenu(); |
1298 | if (popupMenu) { |
1299 | popupMenu->showPopup(mouseMsg->position(), display()); |
1300 | |
1301 | // Do not drop in this function, the drop is done from |
1302 | // the menu in case we've used the |
1303 | // CelMovementPopupMenu |
1304 | m_state = STATE_STANDBY; |
1305 | invalidate(); |
1306 | } |
1307 | } |
1308 | break; |
1309 | } |
1310 | |
1311 | case PART_TAG: { |
1312 | Tag* tag = m_clk.getTag(); |
1313 | if (tag) { |
1314 | Params params; |
1315 | params.set("id" , base::convert_to<std::string>(tag->id()).c_str()); |
1316 | |
1317 | // As the m_clk.tag can be deleted with |
1318 | // RemoveTag command, we've to clean all references |
1319 | // to it from Hit() structures. |
1320 | cleanClk(); |
1321 | m_hot = m_clk; |
1322 | |
1323 | if (mouseMsg->right()) { |
1324 | Menu* = AppMenus::instance()->getTagPopupMenu(); |
1325 | if (popupMenu) { |
1326 | AppMenuItem::setContextParams(params); |
1327 | popupMenu->showPopup(mouseMsg->position(), display()); |
1328 | AppMenuItem::setContextParams(Params()); |
1329 | |
1330 | m_state = STATE_STANDBY; |
1331 | invalidate(); |
1332 | } |
1333 | } |
1334 | else if (mouseMsg->left()) { |
1335 | Command* command = Commands::instance() |
1336 | ->byId(CommandId::FrameTagProperties()); |
1337 | UIContext::instance()->executeCommand(command, params); |
1338 | } |
1339 | } |
1340 | break; |
1341 | } |
1342 | |
1343 | case PART_TAG_SWITCH_BAND_BUTTON: |
1344 | if (m_clk.band >= 0) { |
1345 | focusTagBand(m_clk.band); |
1346 | regenRows = true; |
1347 | relayout = true; |
1348 | } |
1349 | break; |
1350 | |
1351 | } |
1352 | |
1353 | if (regenRows) { |
1354 | regenerateRows(); |
1355 | invalidate(); |
1356 | } |
1357 | if (relayout) |
1358 | layout(); |
1359 | |
1360 | switch (m_state) { |
1361 | case STATE_MOVING_RANGE: |
1362 | if (m_dropRange.type() != Range::kNone) { |
1363 | dropRange(is_copy_key_pressed(mouseMsg) ? |
1364 | Timeline::kCopy: |
1365 | Timeline::kMove); |
1366 | } |
1367 | break; |
1368 | |
1369 | case STATE_MOVING_TAG: |
1370 | m_resizeTagData.reset(); |
1371 | break; |
1372 | |
1373 | case STATE_RESIZING_TAG_LEFT: |
1374 | case STATE_RESIZING_TAG_RIGHT: { |
1375 | auto tag = doc::get<doc::Tag>(m_resizeTagData.tag); |
1376 | if (tag) { |
1377 | if ((m_state == STATE_RESIZING_TAG_LEFT && tag->fromFrame() != m_resizeTagData.from) || |
1378 | (m_state == STATE_RESIZING_TAG_RIGHT && tag->toFrame() != m_resizeTagData.to)) { |
1379 | Tx tx(UIContext::instance(), Strings::commands_FrameTagProperties()); |
1380 | tx(new cmd::SetTagRange( |
1381 | tag, |
1382 | (m_state == STATE_RESIZING_TAG_LEFT ? m_resizeTagData.from: tag->fromFrame()), |
1383 | (m_state == STATE_RESIZING_TAG_RIGHT ? m_resizeTagData.to: tag->toFrame()))); |
1384 | tx.commit(); |
1385 | |
1386 | regenerateRows(); |
1387 | } |
1388 | } |
1389 | m_resizeTagData.reset(); |
1390 | break; |
1391 | } |
1392 | |
1393 | } |
1394 | |
1395 | // Clean the clicked-part & redraw the hot-part. |
1396 | cleanClk(); |
1397 | |
1398 | if (hasCapture()) |
1399 | invalidate(); |
1400 | else |
1401 | invalidateHit(m_hot); |
1402 | |
1403 | // Restore the cursor. |
1404 | m_state = STATE_STANDBY; |
1405 | setCursor(msg, hitTest(msg, mouseMsg->position() - bounds().origin())); |
1406 | |
1407 | releaseMouse(); |
1408 | updateStatusBar(msg); |
1409 | return true; |
1410 | } |
1411 | break; |
1412 | |
1413 | case kDoubleClickMessage: |
1414 | switch (m_hot.part) { |
1415 | |
1416 | case PART_ROW_TEXT: { |
1417 | Command* command = Commands::instance() |
1418 | ->byId(CommandId::LayerProperties()); |
1419 | |
1420 | UIContext::instance()->executeCommand(command); |
1421 | return true; |
1422 | } |
1423 | |
1424 | case PART_HEADER_FRAME: { |
1425 | Command* command = Commands::instance() |
1426 | ->byId(CommandId::FrameProperties()); |
1427 | Params params; |
1428 | params.set("frame" , "current" ); |
1429 | |
1430 | UIContext::instance()->executeCommand(command, params); |
1431 | return true; |
1432 | } |
1433 | |
1434 | case PART_CEL: { |
1435 | Command* command = Commands::instance() |
1436 | ->byId(CommandId::CelProperties()); |
1437 | |
1438 | UIContext::instance()->executeCommand(command); |
1439 | return true; |
1440 | } |
1441 | |
1442 | case PART_TAG_BAND: |
1443 | if (m_hot.band >= 0) { |
1444 | focusTagBand(m_hot.band); |
1445 | regenerateRows(); |
1446 | invalidate(); |
1447 | layout(); |
1448 | return true; |
1449 | } |
1450 | break; |
1451 | |
1452 | } |
1453 | break; |
1454 | |
1455 | case kKeyDownMessage: { |
1456 | bool used = false; |
1457 | |
1458 | switch (static_cast<KeyMessage*>(msg)->scancode()) { |
1459 | |
1460 | case kKeyEsc: |
1461 | if (m_state == STATE_STANDBY) { |
1462 | clearAndInvalidateRange(); |
1463 | } |
1464 | else { |
1465 | m_state = STATE_STANDBY; |
1466 | } |
1467 | |
1468 | // Don't use this key, so it's caught by CancelCommand. |
1469 | // TODO The deselection of the current range should be |
1470 | // handled in CancelCommand itself. |
1471 | //used = true; |
1472 | break; |
1473 | |
1474 | case kKeySpace: { |
1475 | // If we receive a key down event when the Space bar is |
1476 | // pressed (because the Timeline has the keyboard focus) but |
1477 | // we don't have the mouse inside, we don't consume this |
1478 | // event so the Space bar can be used by the Editor to |
1479 | // activate the hand/pan/scroll tool. |
1480 | if (!hasMouse()) |
1481 | break; |
1482 | |
1483 | m_scroll = true; |
1484 | used = true; |
1485 | break; |
1486 | } |
1487 | } |
1488 | |
1489 | updateByMousePos(msg, mousePosInClientBounds()); |
1490 | if (used) |
1491 | return true; |
1492 | |
1493 | break; |
1494 | } |
1495 | |
1496 | case kKeyUpMessage: { |
1497 | bool used = false; |
1498 | |
1499 | switch (static_cast<KeyMessage*>(msg)->scancode()) { |
1500 | |
1501 | case kKeySpace: { |
1502 | m_scroll = false; |
1503 | used = true; |
1504 | break; |
1505 | } |
1506 | } |
1507 | |
1508 | updateByMousePos(msg, mousePosInClientBounds()); |
1509 | if (used) |
1510 | return true; |
1511 | |
1512 | break; |
1513 | } |
1514 | |
1515 | case kMouseWheelMessage: |
1516 | if (m_document) { |
1517 | gfx::Point delta = static_cast<MouseMessage*>(msg)->wheelDelta(); |
1518 | const bool precise = static_cast<MouseMessage*>(msg)->preciseWheel(); |
1519 | |
1520 | // Zoom timeline |
1521 | if (msg->ctrlPressed() || // TODO configurable |
1522 | msg->cmdPressed()) { |
1523 | double dz = delta.x + delta.y; |
1524 | |
1525 | if (precise) { |
1526 | dz /= 1.5; |
1527 | if (dz < -1.0) dz = -1.0; |
1528 | else if (dz > 1.0) dz = 1.0; |
1529 | } |
1530 | |
1531 | setZoomAndUpdate(m_zoom - dz, true); |
1532 | } |
1533 | else { |
1534 | if (!precise) { |
1535 | delta.x *= frameBoxWidth(); |
1536 | delta.y *= layerBoxHeight(); |
1537 | |
1538 | if (delta.x == 0 && // On macOS shift already changes the wheel axis |
1539 | msg->shiftPressed()) { |
1540 | if (std::fabs(delta.y) > delta.x) |
1541 | std::swap(delta.x, delta.y); |
1542 | } |
1543 | |
1544 | if (msg->altPressed()) { |
1545 | delta.x *= 3; |
1546 | delta.y *= 3; |
1547 | } |
1548 | } |
1549 | setViewScroll(viewScroll() + delta); |
1550 | } |
1551 | } |
1552 | break; |
1553 | |
1554 | case kSetCursorMessage: |
1555 | if (m_document) { |
1556 | setCursor(msg, m_hot); |
1557 | return true; |
1558 | } |
1559 | break; |
1560 | |
1561 | case kTouchMagnifyMessage: |
1562 | setZoomAndUpdate( |
1563 | m_zoom + m_zoom * static_cast<ui::TouchMessage*>(msg)->magnification(), |
1564 | true); |
1565 | break; |
1566 | } |
1567 | |
1568 | return Widget::onProcessMessage(msg); |
1569 | } |
1570 | |
1571 | void Timeline::onInitTheme(ui::InitThemeEvent& ev) |
1572 | { |
1573 | Widget::onInitTheme(ev); |
1574 | |
1575 | auto theme = SkinTheme::get(this); |
1576 | int barsize = theme->dimensions.miniScrollbarSize(); |
1577 | m_hbar.setBarWidth(barsize); |
1578 | m_vbar.setBarWidth(barsize); |
1579 | m_hbar.setStyle(theme->styles.transparentScrollbar()); |
1580 | m_vbar.setStyle(theme->styles.transparentScrollbar()); |
1581 | m_hbar.setThumbStyle(theme->styles.transparentScrollbarThumb()); |
1582 | m_vbar.setThumbStyle(theme->styles.transparentScrollbarThumb()); |
1583 | |
1584 | if (m_confPopup) |
1585 | m_confPopup->initTheme(); |
1586 | } |
1587 | |
1588 | void Timeline::onInvalidateRegion(const gfx::Region& region) |
1589 | { |
1590 | Widget::onInvalidateRegion(region); |
1591 | m_redrawMarchingAntsOnly = false; |
1592 | } |
1593 | |
1594 | void Timeline::onSizeHint(SizeHintEvent& ev) |
1595 | { |
1596 | // This doesn't matter, the AniEditor'll use the entire screen anyway. |
1597 | ev.setSizeHint(Size(32, 32)); |
1598 | } |
1599 | |
1600 | void Timeline::onResize(ui::ResizeEvent& ev) |
1601 | { |
1602 | gfx::Rect rc = ev.bounds(); |
1603 | setBoundsQuietly(rc); |
1604 | |
1605 | gfx::Size sz = m_aniControls.sizeHint(); |
1606 | m_aniControls.setBounds( |
1607 | gfx::Rect( |
1608 | rc.x, |
1609 | rc.y+(visibleTagBands()-1)*oneTagHeight(), |
1610 | (!m_sprite || m_sprite->tags().empty() ? std::min(sz.w, rc.w): |
1611 | std::min(sz.w, separatorX())), |
1612 | oneTagHeight())); |
1613 | |
1614 | updateScrollBars(); |
1615 | } |
1616 | |
1617 | void Timeline::onPaint(ui::PaintEvent& ev) |
1618 | { |
1619 | Graphics* g = ev.graphics(); |
1620 | bool noDoc = (m_document == NULL); |
1621 | if (noDoc) |
1622 | goto paintNoDoc; |
1623 | |
1624 | try { |
1625 | // Lock the sprite to read/render it. Here we don't wait if the |
1626 | // document is locked (e.g. a filter is being applied to the |
1627 | // sprite) to avoid locking the UI. |
1628 | const DocReader docReader(m_document, 0); |
1629 | |
1630 | if (m_redrawMarchingAntsOnly) { |
1631 | drawClipboardRange(g); |
1632 | m_redrawMarchingAntsOnly = false; |
1633 | return; |
1634 | } |
1635 | |
1636 | layer_t layer, firstLayer, lastLayer; |
1637 | frame_t frame, firstFrame, lastFrame; |
1638 | |
1639 | getDrawableLayers(&firstLayer, &lastLayer); |
1640 | getDrawableFrames(&firstFrame, &lastFrame); |
1641 | |
1642 | drawTop(g); |
1643 | |
1644 | // Draw the header for layers. |
1645 | drawHeader(g); |
1646 | |
1647 | // Draw the header for each visible frame. |
1648 | { |
1649 | IntersectClip clip(g, getFrameHeadersBounds()); |
1650 | if (clip) { |
1651 | for (frame=firstFrame; frame<=lastFrame; ++frame) |
1652 | drawHeaderFrame(g, frame); |
1653 | |
1654 | // Draw onionskin indicators. |
1655 | gfx::Rect bounds = getOnionskinFramesBounds(); |
1656 | if (!bounds.isEmpty()) { |
1657 | drawPart( |
1658 | g, bounds, nullptr, |
1659 | skinTheme()->styles.timelineOnionskinRange(), |
1660 | false, false, false); |
1661 | } |
1662 | } |
1663 | } |
1664 | |
1665 | // Draw each visible layer. |
1666 | DrawCelData data; |
1667 | for (layer=lastLayer; layer>=firstLayer; --layer) { |
1668 | { |
1669 | IntersectClip clip(g, getLayerHeadersBounds()); |
1670 | if (clip) |
1671 | drawLayer(g, layer); |
1672 | } |
1673 | |
1674 | IntersectClip clip(g, getCelsBounds()); |
1675 | if (!clip) |
1676 | continue; |
1677 | |
1678 | Layer* layerPtr = m_rows[layer].layer(); |
1679 | if (!layerPtr->isImage()) { |
1680 | // Draw empty cels |
1681 | for (frame=firstFrame; frame<=lastFrame; ++frame) { |
1682 | drawCel(g, layer, frame, nullptr, nullptr); |
1683 | } |
1684 | continue; |
1685 | } |
1686 | |
1687 | // Get the first CelIterator to be drawn (it is the first cel with cel->frame >= first_frame) |
1688 | LayerImage* layerImagePtr = static_cast<LayerImage*>(layerPtr); |
1689 | data.begin = layerImagePtr->getCelBegin(); |
1690 | data.end = layerImagePtr->getCelEnd(); |
1691 | data.it = layerImagePtr->findFirstCelIteratorAfter(firstFrame-1); |
1692 | if (firstFrame > 0 && data.it != data.begin) |
1693 | data.prevIt = data.it-1; |
1694 | else |
1695 | data.prevIt = data.end; |
1696 | data.nextIt = (data.it != data.end ? data.it+1: data.end); |
1697 | |
1698 | // Calculate link range for the active cel |
1699 | data.firstLink = data.end; |
1700 | data.lastLink = data.end; |
1701 | |
1702 | if (layerPtr == m_layer) { |
1703 | data.activeIt = layerImagePtr->findCelIterator(m_frame); |
1704 | if (data.activeIt != data.end) { |
1705 | data.firstLink = data.activeIt; |
1706 | data.lastLink = data.activeIt; |
1707 | |
1708 | ObjectId imageId = (*data.activeIt)->image()->id(); |
1709 | |
1710 | auto it2 = data.activeIt; |
1711 | if (it2 != data.begin) { |
1712 | do { |
1713 | --it2; |
1714 | if ((*it2)->image()->id() == imageId) { |
1715 | data.firstLink = it2; |
1716 | if ((*data.firstLink)->frame() < firstFrame) |
1717 | break; |
1718 | } |
1719 | } while (it2 != data.begin); |
1720 | } |
1721 | |
1722 | it2 = data.activeIt; |
1723 | while (it2 != data.end) { |
1724 | if ((*it2)->image()->id() == imageId) { |
1725 | data.lastLink = it2; |
1726 | if ((*data.lastLink)->frame() > lastFrame) |
1727 | break; |
1728 | } |
1729 | ++it2; |
1730 | } |
1731 | } |
1732 | } |
1733 | else |
1734 | data.activeIt = data.end; |
1735 | |
1736 | // Draw every visible cel for each layer. |
1737 | for (frame=firstFrame; frame<=lastFrame; ++frame) { |
1738 | Cel* cel = |
1739 | (data.it != data.end && |
1740 | (*data.it)->frame() == frame ? *data.it: nullptr); |
1741 | |
1742 | drawCel(g, layer, frame, cel, &data); |
1743 | |
1744 | if (cel) { |
1745 | data.prevIt = data.it; |
1746 | data.it = data.nextIt; // Point to next cel |
1747 | if (data.nextIt != data.end) |
1748 | ++data.nextIt; |
1749 | } |
1750 | } |
1751 | } |
1752 | |
1753 | drawPaddings(g); |
1754 | drawTags(g); |
1755 | drawRangeOutline(g); |
1756 | drawClipboardRange(g); |
1757 | drawCelOverlay(g); |
1758 | |
1759 | #if 0 // Use this code to debug the calculated m_dropRange by updateDropRange() |
1760 | { |
1761 | g->drawRect(gfx::rgba(255, 255, 0), getRangeBounds(m_range)); |
1762 | g->drawRect(gfx::rgba(255, 0, 0), getRangeBounds(m_dropRange)); |
1763 | } |
1764 | #endif |
1765 | } |
1766 | catch (const LockedDocException&) { |
1767 | // The sprite is locked, so we defer the rendering of the sprite |
1768 | // for later. |
1769 | noDoc = true; |
1770 | defer_invalid_rect(g->getClipBounds().offset(bounds().origin())); |
1771 | } |
1772 | |
1773 | paintNoDoc:; |
1774 | if (noDoc) |
1775 | drawPart( |
1776 | g, clientBounds(), nullptr, |
1777 | skinTheme()->styles.timelinePadding()); |
1778 | } |
1779 | |
1780 | void Timeline::onBeforeCommandExecution(CommandExecutionEvent& ev) |
1781 | { |
1782 | m_savedVersion = (m_document ? m_document->sprite()->version(): 0); |
1783 | } |
1784 | |
1785 | void Timeline::onAfterCommandExecution(CommandExecutionEvent& ev) |
1786 | { |
1787 | if (!m_document) |
1788 | return; |
1789 | |
1790 | // TODO Improve this: check if the structure of layers/frames has changed |
1791 | const doc::ObjectVersion currentVersion = m_document->sprite()->version(); |
1792 | if (m_savedVersion != currentVersion) { |
1793 | regenerateRows(); |
1794 | showCurrentCel(); |
1795 | invalidate(); |
1796 | } |
1797 | } |
1798 | |
1799 | void Timeline::onActiveSiteChange(const Site& site) |
1800 | { |
1801 | if (hasMouse()) { |
1802 | updateStatusBarForFrame(site.frame(), nullptr, site.cel()); |
1803 | } |
1804 | } |
1805 | |
1806 | void Timeline::onRemoveDocument(Doc* document) |
1807 | { |
1808 | if (document == m_document) { |
1809 | detachDocument(); |
1810 | } |
1811 | } |
1812 | |
1813 | void Timeline::onGeneralUpdate(DocEvent& ev) |
1814 | { |
1815 | invalidate(); |
1816 | } |
1817 | |
1818 | void Timeline::onAddLayer(DocEvent& ev) |
1819 | { |
1820 | ASSERT(ev.layer() != NULL); |
1821 | |
1822 | setLayer(ev.layer()); |
1823 | |
1824 | regenerateRows(); |
1825 | showCurrentCel(); |
1826 | clearClipboardRange(); |
1827 | invalidate(); |
1828 | } |
1829 | |
1830 | // TODO similar to ActiveSiteHandler::onBeforeRemoveLayer() and Editor::onBeforeRemoveLayer() |
1831 | void Timeline::onBeforeRemoveLayer(DocEvent& ev) |
1832 | { |
1833 | Layer* layerToSelect = candidate_if_layer_is_deleted(m_layer, ev.layer()); |
1834 | if (m_layer != layerToSelect) |
1835 | setLayer(layerToSelect); |
1836 | |
1837 | // Remove layer from ranges |
1838 | m_range.eraseAndAdjust(ev.layer()); |
1839 | m_startRange.eraseAndAdjust(ev.layer()); |
1840 | m_dropRange.eraseAndAdjust(ev.layer()); |
1841 | |
1842 | ASSERT(!m_range.contains(ev.layer())); |
1843 | ASSERT(!m_startRange.contains(ev.layer())); |
1844 | ASSERT(!m_dropRange.contains(ev.layer())); |
1845 | } |
1846 | |
1847 | // We have to regenerate the layer rows (m_rows) after the layer is |
1848 | // removed from the sprite. |
1849 | void Timeline::onAfterRemoveLayer(DocEvent& ev) |
1850 | { |
1851 | regenerateRows(); |
1852 | showCurrentCel(); |
1853 | clearClipboardRange(); |
1854 | invalidate(); |
1855 | } |
1856 | |
1857 | void Timeline::onAddFrame(DocEvent& ev) |
1858 | { |
1859 | setFrame(ev.frame(), false); |
1860 | |
1861 | showCurrentCel(); |
1862 | clearClipboardRange(); |
1863 | invalidate(); |
1864 | } |
1865 | |
1866 | // TODO similar to ActiveSiteHandler::onRemoveFrame() |
1867 | void Timeline::onRemoveFrame(DocEvent& ev) |
1868 | { |
1869 | // Adjust current frame of all editors that are in a frame more |
1870 | // advanced that the removed one. |
1871 | if (getFrame() > ev.frame()) { |
1872 | setFrame(getFrame()-1, false); |
1873 | } |
1874 | // If the editor was in the previous "last frame" (current value of |
1875 | // totalFrames()), we've to adjust it to the new last frame |
1876 | // (lastFrame()) |
1877 | else if (getFrame() >= sprite()->totalFrames()) { |
1878 | setFrame(sprite()->lastFrame(), false); |
1879 | } |
1880 | |
1881 | // Disable the selected range when we remove frames |
1882 | if (m_range.enabled()) |
1883 | clearAndInvalidateRange(); |
1884 | |
1885 | showCurrentCel(); |
1886 | clearClipboardRange(); |
1887 | invalidate(); |
1888 | } |
1889 | |
1890 | void Timeline::onAddCel(DocEvent& ev) |
1891 | { |
1892 | invalidateLayer(ev.layer()); |
1893 | } |
1894 | |
1895 | void Timeline::onAfterRemoveCel(DocEvent& ev) |
1896 | { |
1897 | invalidateLayer(ev.layer()); |
1898 | } |
1899 | |
1900 | void Timeline::onLayerNameChange(DocEvent& ev) |
1901 | { |
1902 | invalidate(); |
1903 | } |
1904 | |
1905 | void Timeline::onAddTag(DocEvent& ev) |
1906 | { |
1907 | if (m_tagFocusBand >= 0) { |
1908 | m_tagFocusBand = -1; |
1909 | regenerateRows(); |
1910 | layout(); |
1911 | } |
1912 | } |
1913 | |
1914 | void Timeline::onRemoveTag(DocEvent& ev) |
1915 | { |
1916 | onAddTag(ev); |
1917 | } |
1918 | |
1919 | void Timeline::onTagChange(DocEvent& ev) |
1920 | { |
1921 | invalidateHit(Hit(PART_TAGS)); |
1922 | } |
1923 | |
1924 | void Timeline::onTagRename(DocEvent& ev) |
1925 | { |
1926 | invalidateHit(Hit(PART_TAGS)); |
1927 | } |
1928 | |
1929 | void Timeline::onStateChanged(Editor* editor) |
1930 | { |
1931 | m_aniControls.updateUsingEditor(editor); |
1932 | } |
1933 | |
1934 | void Timeline::onAfterFrameChanged(Editor* editor) |
1935 | { |
1936 | if (m_fromTimeline) |
1937 | return; |
1938 | |
1939 | setFrame(editor->frame(), false); |
1940 | |
1941 | if (!hasCapture() && !editor->keepTimelineRange()) |
1942 | clearAndInvalidateRange(); |
1943 | |
1944 | showCurrentCel(); |
1945 | } |
1946 | |
1947 | void Timeline::onAfterLayerChanged(Editor* editor) |
1948 | { |
1949 | if (m_fromTimeline) |
1950 | return; |
1951 | |
1952 | if (!hasCapture()) |
1953 | m_range.clearRange(); |
1954 | |
1955 | setLayer(editor->layer()); |
1956 | showCurrentCel(); |
1957 | } |
1958 | |
1959 | void Timeline::onDestroyEditor(Editor* editor) |
1960 | { |
1961 | ASSERT(m_editor == editor); |
1962 | if (m_editor == editor) { |
1963 | m_editor->remove_observer(this); |
1964 | m_editor = nullptr; |
1965 | } |
1966 | } |
1967 | |
1968 | void Timeline::setCursor(ui::Message* msg, const Hit& hit) |
1969 | { |
1970 | // Scrolling. |
1971 | if (m_state == STATE_SCROLLING || m_scroll) { |
1972 | ui::set_mouse_cursor(kScrollCursor); |
1973 | } |
1974 | // Moving. |
1975 | else if (m_state == STATE_MOVING_RANGE) { |
1976 | if (is_copy_key_pressed(msg)) |
1977 | ui::set_mouse_cursor(kArrowPlusCursor); |
1978 | else |
1979 | ui::set_mouse_cursor(kMoveCursor); |
1980 | } |
1981 | // Normal state. |
1982 | else if (hit.part == PART_HEADER_ONIONSKIN_RANGE_LEFT |
1983 | || m_state == STATE_MOVING_ONIONSKIN_RANGE_LEFT) { |
1984 | ui::set_mouse_cursor(kSizeWCursor); |
1985 | } |
1986 | else if (hit.part == PART_HEADER_ONIONSKIN_RANGE_RIGHT |
1987 | || m_state == STATE_MOVING_ONIONSKIN_RANGE_RIGHT) { |
1988 | ui::set_mouse_cursor(kSizeECursor); |
1989 | } |
1990 | else if (hit.part == PART_RANGE_OUTLINE) { |
1991 | ui::set_mouse_cursor(kMoveCursor); |
1992 | } |
1993 | else if (hit.part == PART_SEPARATOR) { |
1994 | ui::set_mouse_cursor(kSizeWECursor); |
1995 | } |
1996 | else if (hit.part == PART_TAG) { |
1997 | ui::set_mouse_cursor(kHandCursor); |
1998 | } |
1999 | else if (hit.part == PART_TAG_RIGHT) { |
2000 | ui::set_mouse_cursor(kSizeECursor); |
2001 | } |
2002 | else if (hit.part == PART_TAG_LEFT) { |
2003 | ui::set_mouse_cursor(kSizeWCursor); |
2004 | } |
2005 | else { |
2006 | ui::set_mouse_cursor(kArrowCursor); |
2007 | } |
2008 | } |
2009 | |
2010 | void Timeline::getDrawableLayers(layer_t* firstDrawableLayer, |
2011 | layer_t* lastDrawableLayer) |
2012 | { |
2013 | layer_t i = lastLayer() |
2014 | - ((viewScroll().y + getCelsBounds().h) / layerBoxHeight()); |
2015 | i = std::clamp(i, firstLayer(), lastLayer()); |
2016 | |
2017 | layer_t j = lastLayer() - viewScroll().y / layerBoxHeight();; |
2018 | if (!m_rows.empty()) |
2019 | j = std::clamp(j, firstLayer(), lastLayer()); |
2020 | else |
2021 | j = -1; |
2022 | |
2023 | *firstDrawableLayer = i; |
2024 | *lastDrawableLayer = j; |
2025 | } |
2026 | |
2027 | void Timeline::getDrawableFrames(frame_t* firstFrame, frame_t* lastFrame) |
2028 | { |
2029 | *firstFrame = frame_t(viewScroll().x / frameBoxWidth()); |
2030 | *lastFrame = frame_t((viewScroll().x |
2031 | + getCelsBounds().w) / frameBoxWidth()); |
2032 | } |
2033 | |
2034 | void Timeline::drawPart(ui::Graphics* g, const gfx::Rect& bounds, |
2035 | const std::string* text, ui::Style* style, |
2036 | const bool is_active, |
2037 | const bool is_hover, |
2038 | const bool is_clicked, |
2039 | const bool is_disabled) |
2040 | { |
2041 | IntersectClip clip(g, bounds); |
2042 | if (!clip) |
2043 | return; |
2044 | |
2045 | PaintWidgetPartInfo info; |
2046 | info.text = text; |
2047 | info.styleFlags = |
2048 | (is_active ? ui::Style::Layer::kFocus: 0) | |
2049 | (is_hover ? ui::Style::Layer::kMouse: 0) | |
2050 | (is_clicked ? ui::Style::Layer::kSelected: 0) | |
2051 | (is_disabled ? ui::Style::Layer::kDisabled: 0); |
2052 | |
2053 | theme()->paintWidgetPart(g, style, bounds, info); |
2054 | } |
2055 | |
2056 | void Timeline::drawClipboardRange(ui::Graphics* g) |
2057 | { |
2058 | Doc* clipboard_document; |
2059 | DocRange clipboard_range; |
2060 | Clipboard::instance()->getDocumentRangeInfo( |
2061 | &clipboard_document, |
2062 | &clipboard_range); |
2063 | |
2064 | if (!m_document || clipboard_document != m_document) |
2065 | return; |
2066 | |
2067 | if (!m_clipboard_timer.isRunning()) |
2068 | m_clipboard_timer.start(); |
2069 | |
2070 | IntersectClip clip(g, getRangeClipBounds(clipboard_range)); |
2071 | if (clip) { |
2072 | ui::Paint paint; |
2073 | paint.style(ui::Paint::Stroke); |
2074 | ui::set_checkered_paint_mode(paint, m_offset_count, |
2075 | gfx::rgba(0, 0, 0, 255), |
2076 | gfx::rgba(255, 255, 255, 255)); |
2077 | g->drawRect(getRangeBounds(clipboard_range), paint); |
2078 | } |
2079 | } |
2080 | |
2081 | void Timeline::drawTop(ui::Graphics* g) |
2082 | { |
2083 | g->fillRect(skinTheme()->colors.workspace(), |
2084 | getPartBounds(Hit(PART_TOP))); |
2085 | } |
2086 | |
2087 | void Timeline::(ui::Graphics* g) |
2088 | { |
2089 | auto& styles = skinTheme()->styles; |
2090 | bool allInvisible = allLayersInvisible(); |
2091 | bool allLocked = allLayersLocked(); |
2092 | bool allContinuous = allLayersContinuous(); |
2093 | |
2094 | drawPart(g, getPartBounds(Hit(PART_HEADER_EYE)), |
2095 | nullptr, |
2096 | allInvisible ? styles.timelineClosedEye(): styles.timelineOpenEye(), |
2097 | m_clk.part == PART_HEADER_EYE, |
2098 | m_hot.part == PART_HEADER_EYE, |
2099 | m_clk.part == PART_HEADER_EYE); |
2100 | |
2101 | drawPart(g, getPartBounds(Hit(PART_HEADER_PADLOCK)), |
2102 | nullptr, |
2103 | allLocked ? styles.timelineClosedPadlock(): styles.timelineOpenPadlock(), |
2104 | m_clk.part == PART_HEADER_PADLOCK, |
2105 | m_hot.part == PART_HEADER_PADLOCK, |
2106 | m_clk.part == PART_HEADER_PADLOCK); |
2107 | |
2108 | drawPart(g, getPartBounds(Hit(PART_HEADER_CONTINUOUS)), |
2109 | nullptr, |
2110 | allContinuous ? styles.timelineContinuous(): styles.timelineDiscontinuous(), |
2111 | m_clk.part == PART_HEADER_CONTINUOUS, |
2112 | m_hot.part == PART_HEADER_CONTINUOUS, |
2113 | m_clk.part == PART_HEADER_CONTINUOUS); |
2114 | |
2115 | drawPart(g, getPartBounds(Hit(PART_HEADER_GEAR)), |
2116 | nullptr, |
2117 | styles.timelineGear(), |
2118 | m_clk.part == PART_HEADER_GEAR, |
2119 | m_hot.part == PART_HEADER_GEAR, |
2120 | m_clk.part == PART_HEADER_GEAR); |
2121 | |
2122 | drawPart(g, getPartBounds(Hit(PART_HEADER_ONIONSKIN)), |
2123 | NULL, styles.timelineOnionskin(), |
2124 | docPref().onionskin.active() || (m_clk.part == PART_HEADER_ONIONSKIN), |
2125 | m_hot.part == PART_HEADER_ONIONSKIN, |
2126 | m_clk.part == PART_HEADER_ONIONSKIN); |
2127 | |
2128 | // Empty header space. |
2129 | drawPart(g, getPartBounds(Hit(PART_HEADER_LAYER)), |
2130 | NULL, styles.timelineBox(), false, false, false); |
2131 | } |
2132 | |
2133 | void Timeline::(ui::Graphics* g, frame_t frame) |
2134 | { |
2135 | bool is_active = isFrameActive(frame); |
2136 | bool is_hover = (m_hot.part == PART_HEADER_FRAME && m_hot.frame == frame); |
2137 | bool is_clicked = (m_clk.part == PART_HEADER_FRAME && m_clk.frame == frame); |
2138 | gfx::Rect bounds = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), frame)); |
2139 | IntersectClip clip(g, bounds); |
2140 | if (!clip) |
2141 | return; |
2142 | |
2143 | // Draw the header for the layers. |
2144 | const int n = (docPref().timeline.firstFrame()+frame); |
2145 | std::string text = base::convert_to<std::string, int>(n % 100); |
2146 | if (n >= 100 && (n % 100) < 10) |
2147 | text.insert(0, 1, '0'); |
2148 | |
2149 | drawPart(g, bounds, &text, |
2150 | skinTheme()->styles.timelineHeaderFrame(), |
2151 | is_active, is_hover, is_clicked); |
2152 | } |
2153 | |
2154 | void Timeline::drawLayer(ui::Graphics* g, int layerIdx) |
2155 | { |
2156 | ASSERT(layerIdx >= 0 && layerIdx < int(m_rows.size())); |
2157 | if (layerIdx < 0 || layerIdx >= m_rows.size()) |
2158 | return; |
2159 | |
2160 | auto& styles = skinTheme()->styles; |
2161 | Layer* layer = m_rows[layerIdx].layer(); |
2162 | bool is_active = isLayerActive(layerIdx); |
2163 | bool hotlayer = (m_hot.layer == layerIdx); |
2164 | bool clklayer = (m_clk.layer == layerIdx); |
2165 | gfx::Rect bounds = getPartBounds(Hit(PART_ROW, layerIdx, firstFrame())); |
2166 | IntersectClip clip(g, bounds); |
2167 | if (!clip) |
2168 | return; |
2169 | |
2170 | // Draw the eye (visible flag). |
2171 | bounds = getPartBounds(Hit(PART_ROW_EYE_ICON, layerIdx)); |
2172 | drawPart( |
2173 | g, bounds, nullptr, |
2174 | (layer->isVisible() ? styles.timelineOpenEye(): |
2175 | styles.timelineClosedEye()), |
2176 | is_active || (clklayer && m_clk.part == PART_ROW_EYE_ICON), |
2177 | (hotlayer && m_hot.part == PART_ROW_EYE_ICON), |
2178 | (clklayer && m_clk.part == PART_ROW_EYE_ICON), |
2179 | !m_rows[layerIdx].parentVisible()); |
2180 | |
2181 | // Draw the padlock (editable flag). |
2182 | bounds = getPartBounds(Hit(PART_ROW_PADLOCK_ICON, layerIdx)); |
2183 | drawPart( |
2184 | g, bounds, nullptr, |
2185 | (layer->isEditable() ? styles.timelineOpenPadlock(): |
2186 | styles.timelineClosedPadlock()), |
2187 | is_active || (clklayer && m_clk.part == PART_ROW_PADLOCK_ICON), |
2188 | (hotlayer && m_hot.part == PART_ROW_PADLOCK_ICON), |
2189 | (clklayer && m_clk.part == PART_ROW_PADLOCK_ICON), |
2190 | !m_rows[layerIdx].parentEditable()); |
2191 | |
2192 | // Draw the continuous flag/group icon. |
2193 | bounds = getPartBounds(Hit(PART_ROW_CONTINUOUS_ICON, layerIdx)); |
2194 | if (layer->isImage()) { |
2195 | drawPart(g, bounds, nullptr, |
2196 | layer->isContinuous() ? styles.timelineContinuous(): |
2197 | styles.timelineDiscontinuous(), |
2198 | is_active || (clklayer && m_clk.part == PART_ROW_CONTINUOUS_ICON), |
2199 | (hotlayer && m_hot.part == PART_ROW_CONTINUOUS_ICON), |
2200 | (clklayer && m_clk.part == PART_ROW_CONTINUOUS_ICON)); |
2201 | } |
2202 | else if (layer->isGroup()) { |
2203 | drawPart(g, bounds, nullptr, |
2204 | layer->isCollapsed() ? styles.timelineClosedGroup(): |
2205 | styles.timelineOpenGroup(), |
2206 | is_active || (clklayer && m_clk.part == PART_ROW_CONTINUOUS_ICON), |
2207 | (hotlayer && m_hot.part == PART_ROW_CONTINUOUS_ICON), |
2208 | (clklayer && m_clk.part == PART_ROW_CONTINUOUS_ICON)); |
2209 | } |
2210 | |
2211 | // Get the layer's name bounds. |
2212 | bounds = getPartBounds(Hit(PART_ROW_TEXT, layerIdx)); |
2213 | |
2214 | // Draw layer name. |
2215 | doc::color_t layerColor = layer->userData().color(); |
2216 | gfx::Rect textBounds = bounds; |
2217 | if (m_rows[layerIdx].level() > 0) { |
2218 | const int frameBoxWithWithoutZoom = |
2219 | skinTheme()->dimensions.timelineBaseSize(); |
2220 | const int w = m_rows[layerIdx].level()*frameBoxWithWithoutZoom; |
2221 | textBounds.x += w; |
2222 | textBounds.w -= w; |
2223 | } |
2224 | |
2225 | // Layer name background |
2226 | drawPart(g, bounds, nullptr, styles.timelineLayer(), |
2227 | is_active || (clklayer && m_clk.part == PART_ROW_TEXT), |
2228 | (hotlayer && m_hot.part == PART_ROW_TEXT), |
2229 | (clklayer && m_clk.part == PART_ROW_TEXT)); |
2230 | |
2231 | if (doc::rgba_geta(layerColor) > 0) { |
2232 | // Fill with an user-defined custom color. |
2233 | auto b2 = textBounds; |
2234 | b2.shrink(1*guiscale()).inflate(1*guiscale()); |
2235 | g->fillRect(gfx::rgba(doc::rgba_getr(layerColor), |
2236 | doc::rgba_getg(layerColor), |
2237 | doc::rgba_getb(layerColor), |
2238 | doc::rgba_geta(layerColor)), |
2239 | b2); |
2240 | } |
2241 | |
2242 | // Tilemap icon |
2243 | if (layer->isTilemap()) { |
2244 | drawPart(g, textBounds, nullptr, styles.timelineTilemapLayer(), |
2245 | is_active || (clklayer && m_clk.part == PART_ROW_TEXT), |
2246 | (hotlayer && m_hot.part == PART_ROW_TEXT), |
2247 | (clklayer && m_clk.part == PART_ROW_TEXT)); |
2248 | |
2249 | gfx::Size sz = skinTheme()->calcSizeHint( |
2250 | this, skinTheme()->styles.timelineTilemapLayer()); |
2251 | textBounds.x += sz.w; |
2252 | textBounds.w -= sz.w; |
2253 | } |
2254 | |
2255 | // Layer text |
2256 | drawPart(g, textBounds, |
2257 | &layer->name(), |
2258 | styles.timelineLayerTextOnly(), |
2259 | is_active, |
2260 | (hotlayer && m_hot.part == PART_ROW_TEXT), |
2261 | (clklayer && m_clk.part == PART_ROW_TEXT)); |
2262 | |
2263 | if (layer->isBackground()) { |
2264 | int s = ui::guiscale(); |
2265 | g->fillRect( |
2266 | is_active ? |
2267 | skinTheme()->colors.timelineClickedText(): |
2268 | skinTheme()->colors.timelineNormalText(), |
2269 | gfx::Rect(bounds.x+4*s, |
2270 | bounds.y+bounds.h-2*s, |
2271 | font()->textLength(layer->name().c_str()), s)); |
2272 | } |
2273 | else if (layer->isReference()) { |
2274 | int s = ui::guiscale(); |
2275 | g->fillRect( |
2276 | is_active ? |
2277 | skinTheme()->colors.timelineClickedText(): |
2278 | skinTheme()->colors.timelineNormalText(), |
2279 | gfx::Rect(bounds.x+4*s, |
2280 | bounds.y+bounds.h/2, |
2281 | font()->textLength(layer->name().c_str()), s)); |
2282 | } |
2283 | |
2284 | // If this layer wasn't clicked but there are another layer clicked, |
2285 | // we have to draw some indicators to show that the user can move |
2286 | // layers. |
2287 | if (hotlayer && !is_active && m_clk.part == PART_ROW_TEXT) { |
2288 | // TODO this should be skinneable |
2289 | g->fillRect( |
2290 | skinTheme()->colors.timelineActive(), |
2291 | gfx::Rect(bounds.x, bounds.y, bounds.w, 2)); |
2292 | } |
2293 | } |
2294 | |
2295 | void Timeline::drawCel(ui::Graphics* g, layer_t layerIndex, frame_t frame, Cel* cel, DrawCelData* data) |
2296 | { |
2297 | auto& styles = skinTheme()->styles; |
2298 | Layer* layer = m_rows[layerIndex].layer(); |
2299 | Image* image = (cel ? cel->image(): nullptr); |
2300 | bool is_hover = (m_hot.part == PART_CEL && |
2301 | m_hot.layer == layerIndex && |
2302 | m_hot.frame == frame); |
2303 | const bool is_active = isCelActive(layerIndex, frame); |
2304 | const bool is_loosely_active = isCelLooselyActive(layerIndex, frame); |
2305 | const bool is_empty = (image == nullptr); |
2306 | gfx::Rect bounds = getPartBounds(Hit(PART_CEL, layerIndex, frame)); |
2307 | gfx::Rect full_bounds = bounds; |
2308 | IntersectClip clip(g, bounds); |
2309 | if (!clip) |
2310 | return; |
2311 | |
2312 | // Draw background |
2313 | if (layer == m_layer && frame == m_frame) |
2314 | drawPart(g, bounds, nullptr, |
2315 | m_range.enabled() ? styles.timelineFocusedCel(): |
2316 | styles.timelineSelectedCel(), false, is_hover, true); |
2317 | else if (m_range.enabled() && is_active) |
2318 | drawPart(g, bounds, nullptr, styles.timelineSelectedCel(), false, is_hover, true); |
2319 | else |
2320 | drawPart(g, bounds, nullptr, styles.timelineBox(), is_loosely_active, is_hover); |
2321 | |
2322 | // Fill with an user-defined custom color. |
2323 | if (cel && cel->data()) { |
2324 | doc::color_t celColor = cel->data()->userData().color(); |
2325 | if (doc::rgba_geta(celColor) > 0) { |
2326 | auto b2 = bounds; |
2327 | b2.shrink(1 * guiscale()).inflate(1 * guiscale()); |
2328 | g->fillRect(gfx::rgba(doc::rgba_getr(celColor), |
2329 | doc::rgba_getg(celColor), |
2330 | doc::rgba_getb(celColor), |
2331 | doc::rgba_geta(celColor)), |
2332 | b2); |
2333 | } |
2334 | } |
2335 | |
2336 | // Draw keyframe shape |
2337 | |
2338 | ui::Style* style = nullptr; |
2339 | bool fromLeft = false; |
2340 | bool fromRight = false; |
2341 | if (is_empty || !data) { |
2342 | style = styles.timelineEmptyFrame(); |
2343 | } |
2344 | else { |
2345 | // Calculate which cel is next to this one (in previous and next |
2346 | // frame). |
2347 | Cel* left = (data->prevIt != data->end ? *data->prevIt: nullptr); |
2348 | Cel* right = (data->nextIt != data->end ? *data->nextIt: nullptr); |
2349 | if (left && left->frame() != frame-1) left = nullptr; |
2350 | if (right && right->frame() != frame+1) right = nullptr; |
2351 | |
2352 | ObjectId leftImg = (left ? left->image()->id(): 0); |
2353 | ObjectId rightImg = (right ? right->image()->id(): 0); |
2354 | fromLeft = (leftImg == cel->image()->id()); |
2355 | fromRight = (rightImg == cel->image()->id()); |
2356 | |
2357 | if (fromLeft && fromRight) |
2358 | style = styles.timelineFromBoth(); |
2359 | else if (fromLeft) |
2360 | style = styles.timelineFromLeft(); |
2361 | else if (fromRight) |
2362 | style = styles.timelineFromRight(); |
2363 | else |
2364 | style = styles.timelineKeyframe(); |
2365 | } |
2366 | |
2367 | drawPart(g, bounds, nullptr, style, is_loosely_active, is_hover); |
2368 | |
2369 | // Draw thumbnail |
2370 | if ((docPref().thumbnails.enabled() && m_zoom > 1) && image) { |
2371 | gfx::Rect thumb_bounds = |
2372 | gfx::Rect(bounds).shrink( |
2373 | skinTheme()->calcBorder(this, style)); |
2374 | |
2375 | if (!thumb_bounds.isEmpty()) { |
2376 | if (os::SurfaceRef surface = thumb::get_cel_thumbnail(cel, thumb_bounds.size())) { |
2377 | const int t = std::clamp(thumb_bounds.w/8, 4, 16); |
2378 | draw_checkered_grid(g, thumb_bounds, gfx::Size(t, t), docPref()); |
2379 | |
2380 | g->drawRgbaSurface(surface.get(), |
2381 | thumb_bounds.center().x-surface->width()/2, |
2382 | thumb_bounds.center().y-surface->height()/2); |
2383 | } |
2384 | } |
2385 | } |
2386 | |
2387 | // Draw decorators to link the activeCel with its links. |
2388 | if (data && data->activeIt != data->end) |
2389 | drawCelLinkDecorators(g, full_bounds, cel, frame, is_loosely_active, is_hover, data); |
2390 | } |
2391 | |
2392 | void Timeline::updateCelOverlayBounds(const Hit& hit) |
2393 | { |
2394 | gfx::Rect rc; |
2395 | |
2396 | if (docPref().thumbnails.overlayEnabled() && hit.part == PART_CEL) { |
2397 | m_thumbnailsOverlayHit = hit; |
2398 | |
2399 | int max_size = headerBoxWidth() * docPref().thumbnails.overlaySize(); |
2400 | int width, height; |
2401 | if (m_sprite->width() > m_sprite->height()) { |
2402 | width = max_size; |
2403 | height = max_size * m_sprite->height() / m_sprite->width(); |
2404 | } |
2405 | else { |
2406 | width = max_size * m_sprite->width() / m_sprite->height(); |
2407 | height = max_size; |
2408 | } |
2409 | |
2410 | gfx::Rect client_bounds = clientBounds(); |
2411 | gfx::Point center = client_bounds.center(); |
2412 | |
2413 | gfx::Rect bounds_cel = getPartBounds(m_thumbnailsOverlayHit); |
2414 | rc = gfx::Rect( |
2415 | bounds_cel.x + m_thumbnailsOverlayDirection.x, |
2416 | bounds_cel.y + m_thumbnailsOverlayDirection.y, |
2417 | width, |
2418 | height); |
2419 | |
2420 | if (!client_bounds.contains(rc)) { |
2421 | m_thumbnailsOverlayDirection = gfx::Point( |
2422 | bounds_cel.x < center.x ? (int)(frameBoxWidth()*1.0) : -width, |
2423 | bounds_cel.y < center.y ? (int)(frameBoxWidth()*0.5) : -height+(int)(frameBoxWidth()*0.5)); |
2424 | rc.setOrigin(gfx::Point( |
2425 | bounds_cel.x + m_thumbnailsOverlayDirection.x, |
2426 | bounds_cel.y + m_thumbnailsOverlayDirection.y)); |
2427 | } |
2428 | } |
2429 | else { |
2430 | rc = gfx::Rect(0, 0, 0, 0); |
2431 | } |
2432 | |
2433 | if (rc == m_thumbnailsOverlayBounds) |
2434 | return; |
2435 | |
2436 | if (!m_thumbnailsOverlayBounds.isEmpty()) |
2437 | invalidateRect(gfx::Rect(m_thumbnailsOverlayBounds).offset(origin())); |
2438 | if (!rc.isEmpty()) |
2439 | invalidateRect(gfx::Rect(rc).offset(origin())); |
2440 | |
2441 | m_thumbnailsOverlayVisible = !rc.isEmpty(); |
2442 | m_thumbnailsOverlayBounds = rc; |
2443 | } |
2444 | |
2445 | void Timeline::drawCelOverlay(ui::Graphics* g) |
2446 | { |
2447 | if (!m_thumbnailsOverlayVisible) |
2448 | return; |
2449 | |
2450 | Layer* layer = m_rows[m_thumbnailsOverlayHit.layer].layer(); |
2451 | Cel* cel = layer->cel(m_thumbnailsOverlayHit.frame); |
2452 | if (!cel) |
2453 | return; |
2454 | |
2455 | Image* image = cel->image(); |
2456 | if (!image) |
2457 | return; |
2458 | |
2459 | IntersectClip clip(g, m_thumbnailsOverlayBounds); |
2460 | if (!clip) |
2461 | return; |
2462 | |
2463 | gfx::Rect rc = m_sprite->bounds().fitIn( |
2464 | gfx::Rect(m_thumbnailsOverlayBounds).shrink(1)); |
2465 | if (os::SurfaceRef surface = thumb::get_cel_thumbnail(cel, rc.size())) { |
2466 | draw_checkered_grid(g, rc, gfx::Size(8, 8)*ui::guiscale(), docPref()); |
2467 | |
2468 | g->drawRgbaSurface(surface.get(), |
2469 | rc.center().x-surface->width()/2, |
2470 | rc.center().y-surface->height()/2); |
2471 | g->drawRect(gfx::rgba(0, 0, 0, 128), m_thumbnailsOverlayBounds); |
2472 | } |
2473 | } |
2474 | |
2475 | void Timeline::drawCelLinkDecorators(ui::Graphics* g, const gfx::Rect& bounds, |
2476 | Cel* cel, frame_t frame, bool is_active, bool is_hover, |
2477 | DrawCelData* data) |
2478 | { |
2479 | auto& styles = skinTheme()->styles; |
2480 | ObjectId imageId = (*data->activeIt)->image()->id(); |
2481 | |
2482 | ui::Style* style1 = nullptr; |
2483 | ui::Style* style2 = nullptr; |
2484 | |
2485 | // Links at the left or right side |
2486 | bool left = (data->firstLink != data->end ? frame > (*data->firstLink)->frame(): false); |
2487 | bool right = (data->lastLink != data->end ? frame < (*data->lastLink)->frame(): false); |
2488 | |
2489 | if (cel && cel->image()->id() == imageId) { |
2490 | if (left) { |
2491 | Cel* prevCel = m_layer->cel(cel->frame()-1); |
2492 | if (!prevCel || prevCel->image()->id() != imageId) |
2493 | style1 = styles.timelineLeftLink(); |
2494 | } |
2495 | if (right) { |
2496 | Cel* nextCel = m_layer->cel(cel->frame()+1); |
2497 | if (!nextCel || nextCel->image()->id() != imageId) |
2498 | style2 = styles.timelineRightLink(); |
2499 | } |
2500 | } |
2501 | else { |
2502 | if (left && right) |
2503 | style1 = styles.timelineBothLinks(); |
2504 | } |
2505 | |
2506 | if (style1) drawPart(g, bounds, nullptr, style1, is_active, is_hover); |
2507 | if (style2) drawPart(g, bounds, nullptr, style2, is_active, is_hover); |
2508 | } |
2509 | |
2510 | void Timeline::drawTags(ui::Graphics* g) |
2511 | { |
2512 | IntersectClip clip(g, getPartBounds(Hit(PART_TAGS))); |
2513 | if (!clip) |
2514 | return; |
2515 | |
2516 | SkinTheme* theme = skinTheme(); |
2517 | auto& styles = theme->styles; |
2518 | |
2519 | g->fillRect(theme->colors.workspace(), |
2520 | gfx::Rect( |
2521 | 0, font()->height(), |
2522 | clientBounds().w, |
2523 | theme->dimensions.timelineTagsAreaHeight())); |
2524 | |
2525 | // Draw active frame tag band |
2526 | if (m_hot.band >= 0 && |
2527 | m_tagBands > 1 && |
2528 | m_tagFocusBand < 0) { |
2529 | gfx::Rect bandBounds = |
2530 | getPartBounds(Hit(PART_TAG_BAND, -1, 0, |
2531 | doc::NullId, m_hot.band)); |
2532 | g->fillRect(theme->colors.timelineBandHighlight(), bandBounds); |
2533 | } |
2534 | |
2535 | int passes = (m_tagFocusBand >= 0 ? 2: 1); |
2536 | for (int pass=0; pass<passes; ++pass) { |
2537 | for (Tag* tag : m_sprite->tags()) { |
2538 | int band = -1; |
2539 | if (m_tagFocusBand >= 0) { |
2540 | auto it = m_tagBand.find(tag); |
2541 | if (it != m_tagBand.end()) { |
2542 | band = it->second; |
2543 | if ((pass == 0 && band == m_tagFocusBand) || |
2544 | (pass == 1 && band != m_tagFocusBand)) |
2545 | continue; |
2546 | } |
2547 | } |
2548 | |
2549 | doc::frame_t fromFrame = tag->fromFrame(); |
2550 | doc::frame_t toFrame = tag->toFrame(); |
2551 | if (m_resizeTagData.tag == tag->id()) { |
2552 | fromFrame = m_resizeTagData.from; |
2553 | toFrame = m_resizeTagData.to; |
2554 | } |
2555 | |
2556 | gfx::Rect bounds1 = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), fromFrame)); |
2557 | gfx::Rect bounds2 = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), toFrame)); |
2558 | gfx::Rect bounds = bounds1.createUnion(bounds2); |
2559 | gfx::Rect tagBounds = getPartBounds(Hit(PART_TAG, 0, 0, tag->id())); |
2560 | bounds.h = bounds.y2() - tagBounds.y2(); |
2561 | bounds.y = tagBounds.y2(); |
2562 | |
2563 | int dx = 0, dw = 0; |
2564 | if (m_dropTarget.outside && |
2565 | m_dropTarget.hhit != DropTarget::HNone && |
2566 | m_dropRange.type() == DocRange::kFrames) { |
2567 | switch (m_dropTarget.hhit) { |
2568 | case DropTarget::Before: |
2569 | if (m_dropRange.firstFrame() == tag->fromFrame()) { |
2570 | dx = +frameBoxWidth()/4; |
2571 | dw = -frameBoxWidth()/4; |
2572 | } |
2573 | else if (m_dropRange.firstFrame()-1 == tag->toFrame()) { |
2574 | dw = -frameBoxWidth()/4; |
2575 | } |
2576 | break; |
2577 | case DropTarget::After: |
2578 | if (m_dropRange.lastFrame() == tag->toFrame()) { |
2579 | dw = -frameBoxWidth()/4; |
2580 | } |
2581 | else if (m_dropRange.lastFrame()+1 == tag->fromFrame()) { |
2582 | dx = +frameBoxWidth()/4; |
2583 | dw = -frameBoxWidth()/4; |
2584 | } |
2585 | break; |
2586 | } |
2587 | } |
2588 | bounds.x += dx; |
2589 | bounds.w += dw; |
2590 | tagBounds.x += dx; |
2591 | |
2592 | const gfx::Color tagColor = |
2593 | (m_tagFocusBand < 0 || pass == 1) ? |
2594 | tag->color(): theme->colors.timelineBandBg(); |
2595 | gfx::Color bg = tagColor; |
2596 | |
2597 | // Draw the tag braces |
2598 | drawTagBraces(g, bg, bounds, bounds); |
2599 | if ((m_clk.part == PART_TAG_LEFT && m_clk.tag == tag->id()) || |
2600 | (m_clk.part != PART_TAG_LEFT && |
2601 | m_hot.part == PART_TAG_LEFT && m_hot.tag == tag->id())) { |
2602 | if (m_clk.part == PART_TAG_LEFT) |
2603 | bg = color_utils::blackandwhite_neg(tagColor); |
2604 | else |
2605 | bg = Timeline::highlightColor(tagColor); |
2606 | drawTagBraces(g, bg, bounds, gfx::Rect(bounds.x, bounds.y, |
2607 | frameBoxWidth()/2, bounds.h)); |
2608 | } |
2609 | else if ((m_clk.part == PART_TAG_RIGHT && m_clk.tag == tag->id()) || |
2610 | (m_clk.part != PART_TAG_RIGHT && |
2611 | m_hot.part == PART_TAG_RIGHT && m_hot.tag == tag->id())) { |
2612 | if (m_clk.part == PART_TAG_RIGHT) |
2613 | bg = color_utils::blackandwhite_neg(tagColor); |
2614 | else |
2615 | bg = Timeline::highlightColor(tagColor); |
2616 | drawTagBraces(g, bg, bounds, |
2617 | gfx::Rect(bounds.x2()-frameBoxWidth()/2, bounds.y, |
2618 | frameBoxWidth()/2, bounds.h)); |
2619 | } |
2620 | |
2621 | // Draw tag text |
2622 | if (m_tagFocusBand < 0 || pass == 1) { |
2623 | bounds = tagBounds; |
2624 | |
2625 | if (m_clk.part == PART_TAG && m_clk.tag == tag->id()) |
2626 | bg = color_utils::blackandwhite_neg(tagColor); |
2627 | else if (m_hot.part == PART_TAG && m_hot.tag == tag->id()) |
2628 | bg = Timeline::highlightColor(tagColor); |
2629 | else |
2630 | bg = tagColor; |
2631 | g->fillRect(bg, bounds); |
2632 | |
2633 | bounds.y += 2*ui::guiscale(); |
2634 | bounds.x += 2*ui::guiscale(); |
2635 | g->drawText( |
2636 | tag->name(), |
2637 | color_utils::blackandwhite_neg(bg), |
2638 | gfx::ColorNone, |
2639 | bounds.origin()); |
2640 | } |
2641 | } |
2642 | } |
2643 | |
2644 | // Draw button to expand/collapse the active band |
2645 | if (m_hot.band >= 0 && m_tagBands > 1) { |
2646 | gfx::Rect butBounds = |
2647 | getPartBounds(Hit(PART_TAG_SWITCH_BAND_BUTTON, -1, 0, |
2648 | doc::NullId, m_hot.band)); |
2649 | PaintWidgetPartInfo info; |
2650 | if (m_hot.part == PART_TAG_SWITCH_BAND_BUTTON) { |
2651 | info.styleFlags |= ui::Style::Layer::kMouse; |
2652 | if (hasCapture()) |
2653 | info.styleFlags |= ui::Style::Layer::kSelected; |
2654 | } |
2655 | theme->paintWidgetPart(g, styles.timelineSwitchBandButton(), |
2656 | butBounds, info); |
2657 | } |
2658 | } |
2659 | |
2660 | void Timeline::drawTagBraces(ui::Graphics* g, |
2661 | gfx::Color tagColor, |
2662 | const gfx::Rect& bounds, |
2663 | const gfx::Rect& clipBounds) |
2664 | { |
2665 | IntersectClip clip(g, clipBounds); |
2666 | if (clip) { |
2667 | SkinTheme* theme = skinTheme(); |
2668 | auto& styles = theme->styles; |
2669 | for (auto& layer : styles.timelineLoopRange()->layers()) { |
2670 | if (layer.type() == Style::Layer::Type::kBackground || |
2671 | layer.type() == Style::Layer::Type::kBackgroundBorder || |
2672 | layer.type() == Style::Layer::Type::kBorder) { |
2673 | const_cast<Style::Layer*>(&layer)->setColor(tagColor); |
2674 | } |
2675 | } |
2676 | drawPart(g, bounds, nullptr, styles.timelineLoopRange()); |
2677 | } |
2678 | } |
2679 | |
2680 | void Timeline::drawRangeOutline(ui::Graphics* g) |
2681 | { |
2682 | auto& styles = skinTheme()->styles; |
2683 | |
2684 | IntersectClip clip(g, getRangeClipBounds(m_range).enlarge(outlineWidth())); |
2685 | if (!clip) |
2686 | return; |
2687 | |
2688 | PaintWidgetPartInfo info; |
2689 | info.styleFlags = |
2690 | (m_range.enabled() ? ui::Style::Layer::kFocus: 0) | |
2691 | (m_hot.part == PART_RANGE_OUTLINE ? ui::Style::Layer::kMouse: 0); |
2692 | |
2693 | gfx::Rect bounds = getPartBounds(Hit(PART_RANGE_OUTLINE)); |
2694 | theme()->paintWidgetPart( |
2695 | g, styles.timelineRangeOutline(), bounds, info); |
2696 | |
2697 | Range drop = m_dropRange; |
2698 | gfx::Rect dropBounds = getRangeBounds(drop); |
2699 | |
2700 | switch (drop.type()) { |
2701 | |
2702 | case Range::kCels: { |
2703 | dropBounds = dropBounds.enlarge(outlineWidth()); |
2704 | info.styleFlags = ui::Style::Layer::kFocus; |
2705 | theme()->paintWidgetPart( |
2706 | g, styles.timelineRangeOutline(), dropBounds, info); |
2707 | break; |
2708 | } |
2709 | |
2710 | case Range::kFrames: { |
2711 | int w = 5 * guiscale(); // TODO get width from the skin info |
2712 | |
2713 | if (m_dropTarget.hhit == DropTarget::Before) |
2714 | dropBounds.x -= w/2; |
2715 | else if (drop == m_range) |
2716 | dropBounds.x = dropBounds.x + getRangeBounds(m_range).w - w/2; |
2717 | else |
2718 | dropBounds.x = dropBounds.x + dropBounds.w - w/2; |
2719 | |
2720 | dropBounds.w = w; |
2721 | |
2722 | info.styleFlags = 0; |
2723 | theme()->paintWidgetPart( |
2724 | g, styles.timelineDropFrameDeco(), dropBounds, info); |
2725 | break; |
2726 | } |
2727 | |
2728 | case Range::kLayers: { |
2729 | int h = 5 * guiscale(); // TODO get height from the skin info |
2730 | |
2731 | if (m_dropTarget.vhit == DropTarget::Top) |
2732 | dropBounds.y -= h/2; |
2733 | else if (drop == m_range) |
2734 | dropBounds.y = dropBounds.y + getRangeBounds(m_range).h - h/2; |
2735 | else |
2736 | dropBounds.y = dropBounds.y + dropBounds.h - h/2; |
2737 | |
2738 | dropBounds.h = h; |
2739 | |
2740 | theme()->paintWidgetPart( |
2741 | g, styles.timelineDropLayerDeco(), dropBounds, info); |
2742 | break; |
2743 | } |
2744 | } |
2745 | } |
2746 | |
2747 | void Timeline::drawPaddings(ui::Graphics* g) |
2748 | { |
2749 | auto& styles = skinTheme()->styles; |
2750 | |
2751 | gfx::Rect client = clientBounds(); |
2752 | gfx::Rect bottomLayer; |
2753 | gfx::Rect lastFrame; |
2754 | int top = topHeight(); |
2755 | |
2756 | if (!m_rows.empty()) { |
2757 | bottomLayer = getPartBounds(Hit(PART_ROW, firstLayer())); |
2758 | lastFrame = getPartBounds(Hit(PART_CEL, firstLayer(), this->lastFrame())); |
2759 | } |
2760 | else { |
2761 | bottomLayer = getPartBounds(Hit(PART_HEADER_LAYER)); |
2762 | lastFrame = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), this->lastFrame())); |
2763 | } |
2764 | |
2765 | drawPart(g, |
2766 | gfx::Rect(lastFrame.x+lastFrame.w, client.y + top, |
2767 | client.w - (lastFrame.x+lastFrame.w), |
2768 | bottomLayer.y+bottomLayer.h), |
2769 | NULL, styles.timelinePaddingTr()); |
2770 | |
2771 | drawPart(g, |
2772 | gfx::Rect(client.x, bottomLayer.y+bottomLayer.h, |
2773 | lastFrame.x+lastFrame.w - client.x, client.h - (bottomLayer.y+bottomLayer.h)), |
2774 | NULL, styles.timelinePaddingBl()); |
2775 | |
2776 | drawPart(g, |
2777 | gfx::Rect(lastFrame.x+lastFrame.w, bottomLayer.y+bottomLayer.h, |
2778 | client.w - (lastFrame.x+lastFrame.w), |
2779 | client.h - (bottomLayer.y+bottomLayer.h)), |
2780 | NULL, styles.timelinePaddingBr()); |
2781 | } |
2782 | |
2783 | gfx::Rect Timeline::() const |
2784 | { |
2785 | gfx::Rect rc = clientBounds(); |
2786 | rc.w = separatorX(); |
2787 | int h = topHeight() + headerBoxHeight(); |
2788 | rc.y += h; |
2789 | rc.h -= h; |
2790 | return rc; |
2791 | } |
2792 | |
2793 | gfx::Rect Timeline::() const |
2794 | { |
2795 | gfx::Rect rc = clientBounds(); |
2796 | rc.x += separatorX(); |
2797 | rc.y += topHeight(); |
2798 | rc.w -= separatorX(); |
2799 | rc.h = headerBoxHeight(); |
2800 | return rc; |
2801 | } |
2802 | |
2803 | gfx::Rect Timeline::getOnionskinFramesBounds() const |
2804 | { |
2805 | DocumentPreferences& docPref = this->docPref(); |
2806 | if (!docPref.onionskin.active()) |
2807 | return gfx::Rect(); |
2808 | |
2809 | frame_t firstFrame = m_frame - docPref.onionskin.prevFrames(); |
2810 | frame_t lastFrame = m_frame + docPref.onionskin.nextFrames(); |
2811 | |
2812 | if (firstFrame < this->firstFrame()) |
2813 | firstFrame = this->firstFrame(); |
2814 | |
2815 | if (lastFrame > this->lastFrame()) |
2816 | lastFrame = this->lastFrame(); |
2817 | |
2818 | return getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), firstFrame)) |
2819 | .createUnion(getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), lastFrame))); |
2820 | } |
2821 | |
2822 | gfx::Rect Timeline::getCelsBounds() const |
2823 | { |
2824 | gfx::Rect rc = clientBounds(); |
2825 | rc.x += separatorX(); |
2826 | rc.w -= separatorX(); |
2827 | rc.y += headerBoxHeight() + topHeight(); |
2828 | rc.h -= headerBoxHeight() + topHeight(); |
2829 | return rc; |
2830 | } |
2831 | |
2832 | gfx::Rect Timeline::getPartBounds(const Hit& hit) const |
2833 | { |
2834 | gfx::Rect bounds = clientBounds(); |
2835 | int y = topHeight(); |
2836 | |
2837 | switch (hit.part) { |
2838 | |
2839 | case PART_NOTHING: |
2840 | break; |
2841 | |
2842 | case PART_TOP: |
2843 | return gfx::Rect(bounds.x, bounds.y, bounds.w, y); |
2844 | |
2845 | case PART_SEPARATOR: |
2846 | return gfx::Rect(bounds.x + separatorX(), bounds.y + y, |
2847 | separatorX() + m_separator_w, bounds.h - y); |
2848 | |
2849 | case PART_HEADER_EYE: |
2850 | return gfx::Rect(bounds.x + headerBoxWidth()*0, bounds.y + y, |
2851 | headerBoxWidth(), headerBoxHeight()); |
2852 | |
2853 | case PART_HEADER_PADLOCK: |
2854 | return gfx::Rect(bounds.x + headerBoxWidth()*1, bounds.y + y, |
2855 | headerBoxWidth(), headerBoxHeight()); |
2856 | |
2857 | case PART_HEADER_CONTINUOUS: |
2858 | return gfx::Rect(bounds.x + headerBoxWidth()*2, bounds.y + y, |
2859 | headerBoxWidth(), headerBoxHeight()); |
2860 | |
2861 | case PART_HEADER_GEAR: |
2862 | return gfx::Rect(bounds.x + headerBoxWidth()*3, bounds.y + y, |
2863 | headerBoxWidth(), headerBoxHeight()); |
2864 | |
2865 | case PART_HEADER_ONIONSKIN: |
2866 | return gfx::Rect(bounds.x + headerBoxWidth()*4, bounds.y + y, |
2867 | headerBoxWidth(), headerBoxHeight()); |
2868 | |
2869 | case PART_HEADER_LAYER: |
2870 | return gfx::Rect(bounds.x + headerBoxWidth()*5, bounds.y + y, |
2871 | separatorX() - headerBoxWidth()*5, headerBoxHeight()); |
2872 | |
2873 | case PART_HEADER_FRAME: |
2874 | return gfx::Rect( |
2875 | bounds.x + separatorX() + m_separator_w - 1 |
2876 | + frameBoxWidth()*std::max(firstFrame(), hit.frame) - viewScroll().x, |
2877 | bounds.y + y, frameBoxWidth(), headerBoxHeight()); |
2878 | |
2879 | case PART_ROW: |
2880 | if (validLayer(hit.layer)) { |
2881 | return gfx::Rect(bounds.x, |
2882 | bounds.y + y + headerBoxHeight() + layerBoxHeight()*(lastLayer()-hit.layer) - viewScroll().y, |
2883 | separatorX(), layerBoxHeight()); |
2884 | } |
2885 | break; |
2886 | |
2887 | case PART_ROW_EYE_ICON: |
2888 | if (validLayer(hit.layer)) { |
2889 | return gfx::Rect(bounds.x, |
2890 | bounds.y + y + headerBoxHeight() + layerBoxHeight()*(lastLayer()-hit.layer) - viewScroll().y, |
2891 | headerBoxWidth(), layerBoxHeight()); |
2892 | } |
2893 | break; |
2894 | |
2895 | case PART_ROW_PADLOCK_ICON: |
2896 | if (validLayer(hit.layer)) { |
2897 | return gfx::Rect(bounds.x + headerBoxWidth(), |
2898 | bounds.y + y + headerBoxHeight() + layerBoxHeight()*(lastLayer()-hit.layer) - viewScroll().y, |
2899 | headerBoxWidth(), layerBoxHeight()); |
2900 | } |
2901 | break; |
2902 | |
2903 | case PART_ROW_CONTINUOUS_ICON: |
2904 | if (validLayer(hit.layer)) { |
2905 | return gfx::Rect(bounds.x + 2* headerBoxWidth(), |
2906 | bounds.y + y + headerBoxHeight() + layerBoxHeight()*(lastLayer()-hit.layer) - viewScroll().y, |
2907 | headerBoxWidth(), layerBoxHeight()); |
2908 | } |
2909 | break; |
2910 | |
2911 | case PART_ROW_TEXT: |
2912 | if (validLayer(hit.layer)) { |
2913 | int x = headerBoxWidth()*3; |
2914 | return gfx::Rect(bounds.x + x, |
2915 | bounds.y + y + headerBoxHeight() + layerBoxHeight()*(lastLayer()-hit.layer) - viewScroll().y, |
2916 | separatorX() - x, layerBoxHeight()); |
2917 | } |
2918 | break; |
2919 | |
2920 | case PART_CEL: |
2921 | if (validLayer(hit.layer) && hit.frame >= frame_t(0)) { |
2922 | return gfx::Rect( |
2923 | bounds.x + separatorX() + m_separator_w - 1 + frameBoxWidth()*hit.frame - viewScroll().x, |
2924 | bounds.y + y + headerBoxHeight() + layerBoxHeight()*(lastLayer()-hit.layer) - viewScroll().y, |
2925 | frameBoxWidth(), layerBoxHeight()); |
2926 | } |
2927 | break; |
2928 | |
2929 | case PART_RANGE_OUTLINE: { |
2930 | gfx::Rect rc = getRangeBounds(m_range); |
2931 | int s = outlineWidth(); |
2932 | rc.enlarge(gfx::Border(s-1, s-1, s, s)); |
2933 | if (rc.x < bounds.x) rc.offset(s, 0).inflate(-s, 0); |
2934 | if (rc.y < bounds.y) rc.offset(0, s).inflate(0, -s); |
2935 | return rc; |
2936 | } |
2937 | |
2938 | case PART_TAG: { |
2939 | Tag* tag = hit.getTag(); |
2940 | if (tag) { |
2941 | doc::frame_t fromFrame = tag->fromFrame(); |
2942 | doc::frame_t toFrame = tag->toFrame(); |
2943 | if (m_resizeTagData.tag == tag->id()) { |
2944 | fromFrame = m_resizeTagData.from; |
2945 | toFrame = m_resizeTagData.to; |
2946 | } |
2947 | |
2948 | gfx::Rect bounds1 = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), fromFrame)); |
2949 | gfx::Rect bounds2 = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), toFrame)); |
2950 | gfx::Rect bounds = bounds1.createUnion(bounds2); |
2951 | bounds.y -= skinTheme()->dimensions.timelineTagsAreaHeight(); |
2952 | |
2953 | int textHeight = font()->height(); |
2954 | bounds.y -= textHeight + 2*ui::guiscale(); |
2955 | bounds.x += 3*ui::guiscale(); |
2956 | bounds.w = font()->textLength(tag->name().c_str()) + 4*ui::guiscale(); |
2957 | bounds.h = font()->height() + 2*ui::guiscale(); |
2958 | |
2959 | if (m_tagFocusBand < 0) { |
2960 | auto it = m_tagBand.find(tag); |
2961 | if (it != m_tagBand.end()) { |
2962 | int dy = (m_tagBands-it->second-1)*oneTagHeight(); |
2963 | bounds.y -= dy; |
2964 | } |
2965 | } |
2966 | |
2967 | return bounds; |
2968 | } |
2969 | break; |
2970 | } |
2971 | |
2972 | case PART_TAGS: |
2973 | return gfx::Rect( |
2974 | bounds.x + separatorX() + m_separator_w - 1, |
2975 | bounds.y, |
2976 | bounds.w - separatorX() - m_separator_w + 1, y); |
2977 | |
2978 | case PART_TAG_BAND: |
2979 | return gfx::Rect( |
2980 | bounds.x + separatorX() + m_separator_w - 1, |
2981 | bounds.y |
2982 | + (m_tagFocusBand < 0 ? oneTagHeight() * std::max(0, hit.band): 0), |
2983 | bounds.w - separatorX() - m_separator_w + 1, |
2984 | oneTagHeight()); |
2985 | |
2986 | case PART_TAG_SWITCH_BUTTONS: { |
2987 | gfx::Size sz = theme()->calcSizeHint( |
2988 | this, skinTheme()->styles.timelineSwitchBandButton()); |
2989 | |
2990 | return gfx::Rect( |
2991 | bounds.x + bounds.w - sz.w, |
2992 | bounds.y, |
2993 | sz.w, y); |
2994 | } |
2995 | |
2996 | case PART_TAG_SWITCH_BAND_BUTTON: { |
2997 | gfx::Size sz = theme()->calcSizeHint( |
2998 | this, skinTheme()->styles.timelineSwitchBandButton()); |
2999 | |
3000 | return gfx::Rect( |
3001 | bounds.x + bounds.w - sz.w - 2*ui::guiscale(), |
3002 | bounds.y |
3003 | + (m_tagFocusBand < 0 ? oneTagHeight() * std::max(0, hit.band): 0) |
3004 | + oneTagHeight()/2 - sz.h/2, |
3005 | sz.w, sz.h); |
3006 | } |
3007 | |
3008 | } |
3009 | |
3010 | return gfx::Rect(); |
3011 | } |
3012 | |
3013 | gfx::Rect Timeline::getRangeBounds(const Range& range) const |
3014 | { |
3015 | gfx::Rect rc; |
3016 | switch (range.type()) { |
3017 | case Range::kNone: |
3018 | // Return empty rectangle |
3019 | break; |
3020 | case Range::kCels: |
3021 | for (auto layer : range.selectedLayers()) { |
3022 | layer_t layerIdx = getLayerIndex(layer); |
3023 | for (auto frame : range.selectedFrames()) |
3024 | rc |= getPartBounds(Hit(PART_CEL, layerIdx, frame)); |
3025 | } |
3026 | break; |
3027 | case Range::kFrames: { |
3028 | for (auto frame : range.selectedFrames()) { |
3029 | rc |= getPartBounds(Hit(PART_HEADER_FRAME, 0, frame)); |
3030 | rc |= getPartBounds(Hit(PART_CEL, 0, frame)); |
3031 | } |
3032 | break; |
3033 | } |
3034 | case Range::kLayers: |
3035 | for (auto layer : range.selectedLayers()) { |
3036 | layer_t layerIdx = getLayerIndex(layer); |
3037 | rc |= getPartBounds(Hit(PART_ROW_TEXT, layerIdx)); |
3038 | rc |= getPartBounds(Hit(PART_CEL, layerIdx, m_sprite->lastFrame())); |
3039 | } |
3040 | break; |
3041 | } |
3042 | return rc; |
3043 | } |
3044 | |
3045 | gfx::Rect Timeline::getRangeClipBounds(const Range& range) const |
3046 | { |
3047 | gfx::Rect celBounds = getCelsBounds(); |
3048 | gfx::Rect clipBounds, unionBounds; |
3049 | switch (range.type()) { |
3050 | case Range::kCels: |
3051 | clipBounds = celBounds; |
3052 | break; |
3053 | case Range::kFrames: { |
3054 | clipBounds = getFrameHeadersBounds(); |
3055 | |
3056 | unionBounds = (clipBounds | celBounds); |
3057 | clipBounds.y = unionBounds.y; |
3058 | clipBounds.h = unionBounds.h; |
3059 | break; |
3060 | } |
3061 | case Range::kLayers: { |
3062 | clipBounds = getLayerHeadersBounds(); |
3063 | |
3064 | unionBounds = (clipBounds | celBounds); |
3065 | clipBounds.x = unionBounds.x; |
3066 | clipBounds.w = unionBounds.w; |
3067 | break; |
3068 | } |
3069 | } |
3070 | return clipBounds; |
3071 | } |
3072 | |
3073 | void Timeline::invalidateHit(const Hit& hit) |
3074 | { |
3075 | if (hit.band >= 0) { |
3076 | Hit hit2 = hit; |
3077 | hit2.part = PART_TAG_BAND; |
3078 | invalidateRect(getPartBounds(hit2).offset(origin())); |
3079 | } |
3080 | |
3081 | invalidateRect(getPartBounds(hit).offset(origin())); |
3082 | } |
3083 | |
3084 | void Timeline::invalidateLayer(const Layer* layer) |
3085 | { |
3086 | if (layer == nullptr) |
3087 | return; |
3088 | |
3089 | layer_t layerIdx = getLayerIndex(layer); |
3090 | if (layerIdx < firstLayer()) |
3091 | return; |
3092 | |
3093 | gfx::Rect rc = getPartBounds(Hit(PART_ROW, layerIdx)); |
3094 | gfx::Rect rcCels; |
3095 | rcCels |= getPartBounds(Hit(PART_CEL, layerIdx, firstFrame())); |
3096 | rcCels |= getPartBounds(Hit(PART_CEL, layerIdx, lastFrame())); |
3097 | rcCels &= getCelsBounds(); |
3098 | rc |= rcCels; |
3099 | rc.offset(origin()); |
3100 | invalidateRect(rc); |
3101 | } |
3102 | |
3103 | void Timeline::invalidateFrame(const frame_t frame) |
3104 | { |
3105 | if (!validFrame(frame)) |
3106 | return; |
3107 | |
3108 | gfx::Rect rc = getPartBounds(Hit(PART_HEADER_FRAME, -1, frame)); |
3109 | gfx::Rect rcCels; |
3110 | rcCels |= getPartBounds(Hit(PART_CEL, firstLayer(), frame)); |
3111 | rcCels |= getPartBounds(Hit(PART_CEL, lastLayer(), frame)); |
3112 | rcCels &= getCelsBounds(); |
3113 | rc |= rcCels; |
3114 | rc.offset(origin()); |
3115 | invalidateRect(rc); |
3116 | } |
3117 | |
3118 | void Timeline::regenerateRows() |
3119 | { |
3120 | ASSERT(m_document); |
3121 | ASSERT(m_sprite); |
3122 | |
3123 | size_t nlayers = 0; |
3124 | for_each_expanded_layer( |
3125 | m_sprite->root(), |
3126 | [&nlayers](Layer* layer, int level, LayerFlags flags) { |
3127 | ++nlayers; |
3128 | }); |
3129 | |
3130 | if (m_rows.size() != nlayers) { |
3131 | if (nlayers > 0) |
3132 | m_rows.resize(nlayers); |
3133 | else |
3134 | m_rows.clear(); |
3135 | } |
3136 | |
3137 | size_t i = 0; |
3138 | for_each_expanded_layer( |
3139 | m_sprite->root(), |
3140 | [&i, this](Layer* layer, int level, LayerFlags flags) { |
3141 | m_rows[i++] = Row(layer, level, flags); |
3142 | }); |
3143 | |
3144 | regenerateTagBands(); |
3145 | updateScrollBars(); |
3146 | } |
3147 | |
3148 | void Timeline::regenerateTagBands() |
3149 | { |
3150 | const bool oldEmptyTagBand = m_tagBand.empty(); |
3151 | |
3152 | // TODO improve this implementation |
3153 | std::vector<unsigned char> tagsPerFrame(m_sprite->totalFrames(), 0); |
3154 | std::vector<Tag*> bands(4, nullptr); |
3155 | m_tagBand.clear(); |
3156 | for (Tag* tag : m_sprite->tags()) { |
3157 | frame_t f = tag->fromFrame(); |
3158 | |
3159 | int b=0; |
3160 | for (; b<int(bands.size()); ++b) { |
3161 | if (!bands[b] || |
3162 | tag->fromFrame() > calcTagVisibleToFrame(bands[b])) { |
3163 | bands[b] = tag; |
3164 | m_tagBand[tag] = b; |
3165 | break; |
3166 | } |
3167 | } |
3168 | if (b == int(bands.size())) |
3169 | m_tagBand[tag] = tagsPerFrame[f]; |
3170 | |
3171 | frame_t toFrame = calcTagVisibleToFrame(tag); |
3172 | if (toFrame >= frame_t(tagsPerFrame.size())) |
3173 | tagsPerFrame.resize(toFrame+1, 0); |
3174 | for (; f<=toFrame; ++f) { |
3175 | ASSERT(f < frame_t(tagsPerFrame.size())); |
3176 | if (tagsPerFrame[f] < 255) |
3177 | ++tagsPerFrame[f]; |
3178 | } |
3179 | } |
3180 | |
3181 | const int oldVisibleBands = visibleTagBands(); |
3182 | m_tagBands = 0; |
3183 | for (int i : tagsPerFrame) |
3184 | m_tagBands = std::max(m_tagBands, i); |
3185 | |
3186 | if (m_tagFocusBand >= m_tagBands) |
3187 | m_tagFocusBand = -1; |
3188 | |
3189 | if (oldVisibleBands != visibleTagBands() || |
3190 | // This case is to re-layout the timeline when the AniControl |
3191 | // can use more/less space because there weren't tags and now |
3192 | // there tags, or viceversa. |
3193 | oldEmptyTagBand != m_tagBand.empty()) { |
3194 | layout(); |
3195 | } |
3196 | } |
3197 | |
3198 | int Timeline::visibleTagBands() const |
3199 | { |
3200 | if (m_tagBands > 1 && m_tagFocusBand == -1) |
3201 | return m_tagBands; |
3202 | else |
3203 | return 1; |
3204 | } |
3205 | |
3206 | void Timeline::updateScrollBars() |
3207 | { |
3208 | gfx::Rect rc = bounds(); |
3209 | m_viewportArea = getCelsBounds().offset(rc.origin()); |
3210 | ui::setup_scrollbars(getScrollableSize(), |
3211 | m_viewportArea, *this, |
3212 | m_hbar, |
3213 | m_vbar); |
3214 | |
3215 | setViewScroll(viewScroll()); |
3216 | } |
3217 | |
3218 | void Timeline::updateByMousePos(ui::Message* msg, const gfx::Point& mousePos) |
3219 | { |
3220 | Hit hit = hitTest(msg, mousePos); |
3221 | if (hasMouseOver()) |
3222 | setCursor(msg, hit); |
3223 | setHot(hit); |
3224 | } |
3225 | |
3226 | Timeline::Hit Timeline::hitTest(ui::Message* msg, const gfx::Point& mousePos) |
3227 | { |
3228 | Hit hit(PART_NOTHING, -1, -1); |
3229 | if (!m_document) |
3230 | return hit; |
3231 | |
3232 | if (m_clk.part == PART_SEPARATOR) { |
3233 | hit.part = PART_SEPARATOR; |
3234 | } |
3235 | else { |
3236 | gfx::Point scroll = viewScroll(); |
3237 | int top = topHeight(); |
3238 | |
3239 | hit.layer = lastLayer() - |
3240 | ((mousePos.y |
3241 | - top |
3242 | - headerBoxHeight() |
3243 | + scroll.y) / layerBoxHeight()); |
3244 | |
3245 | hit.frame = frame_t((mousePos.x |
3246 | - separatorX() |
3247 | - m_separator_w |
3248 | + scroll.x) / frameBoxWidth()); |
3249 | |
3250 | // Flag which indicates that we are in the are below the Background layer/last layer area |
3251 | if (hit.layer < 0) |
3252 | hit.veryBottom = true; |
3253 | |
3254 | if (hasCapture()) { |
3255 | hit.layer = std::clamp(hit.layer, firstLayer(), lastLayer()); |
3256 | if (isMovingCel()) |
3257 | hit.frame = std::max(firstFrame(), hit.frame); |
3258 | else |
3259 | hit.frame = std::clamp(hit.frame, firstFrame(), lastFrame()); |
3260 | } |
3261 | else { |
3262 | if (hit.layer > lastLayer()) hit.layer = -1; |
3263 | if (hit.frame > lastFrame()) hit.frame = -1; |
3264 | } |
3265 | |
3266 | // Is the mouse over onionskin handles? |
3267 | gfx::Rect bounds = getOnionskinFramesBounds(); |
3268 | if (!bounds.isEmpty() && gfx::Rect(bounds.x, bounds.y, 3, bounds.h).contains(mousePos)) { |
3269 | hit.part = PART_HEADER_ONIONSKIN_RANGE_LEFT; |
3270 | } |
3271 | else if (!bounds.isEmpty() && gfx::Rect(bounds.x+bounds.w-3, bounds.y, 3, bounds.h).contains(mousePos)) { |
3272 | hit.part = PART_HEADER_ONIONSKIN_RANGE_RIGHT; |
3273 | } |
3274 | // Is the mouse on the frame tags area? |
3275 | else if (getPartBounds(Hit(PART_TAGS)).contains(mousePos)) { |
3276 | // Mouse in switch band button |
3277 | if (hit.part == PART_NOTHING) { |
3278 | if (m_tagFocusBand < 0) { |
3279 | for (int band=0; band<m_tagBands; ++band) { |
3280 | gfx::Rect bounds = getPartBounds( |
3281 | Hit(PART_TAG_SWITCH_BAND_BUTTON, 0, 0, |
3282 | doc::NullId, band)); |
3283 | if (bounds.contains(mousePos)) { |
3284 | hit.part = PART_TAG_SWITCH_BAND_BUTTON; |
3285 | hit.band = band; |
3286 | break; |
3287 | } |
3288 | } |
3289 | } |
3290 | else { |
3291 | gfx::Rect bounds = getPartBounds( |
3292 | Hit(PART_TAG_SWITCH_BAND_BUTTON, 0, 0, |
3293 | doc::NullId, m_tagFocusBand)); |
3294 | if (bounds.contains(mousePos)) { |
3295 | hit.part = PART_TAG_SWITCH_BAND_BUTTON; |
3296 | hit.band = m_tagFocusBand; |
3297 | } |
3298 | } |
3299 | } |
3300 | |
3301 | // Mouse in frame tags |
3302 | if (hit.part == PART_NOTHING) { |
3303 | for (Tag* tag : m_sprite->tags()) { |
3304 | const int band = m_tagBand[tag]; |
3305 | |
3306 | // Skip unfocused bands |
3307 | if (m_tagFocusBand >= 0 && |
3308 | m_tagFocusBand != band) { |
3309 | continue; |
3310 | } |
3311 | |
3312 | gfx::Rect tagBounds = getPartBounds(Hit(PART_TAG, 0, 0, tag->id())); |
3313 | if (tagBounds.contains(mousePos)) { |
3314 | hit.part = PART_TAG; |
3315 | hit.tag = tag->id(); |
3316 | hit.band = band; |
3317 | break; |
3318 | } |
3319 | // Check if we are in the left/right handles to resize the tag |
3320 | else { |
3321 | gfx::Rect bounds1 = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), tag->fromFrame())); |
3322 | gfx::Rect bounds2 = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), tag->toFrame())); |
3323 | gfx::Rect bounds = bounds1.createUnion(bounds2); |
3324 | bounds.h = bounds.y2() - tagBounds.y2(); |
3325 | bounds.y = tagBounds.y2(); |
3326 | |
3327 | gfx::Rect bandBounds = getPartBounds(Hit(PART_TAG_BAND, 0, 0, doc::NullId, band)); |
3328 | |
3329 | const int fw = frameBoxWidth()/2; |
3330 | if (gfx::Rect(bounds.x2()-fw, bounds.y, fw, bounds.h).contains(mousePos)) { |
3331 | hit.part = PART_TAG_RIGHT; |
3332 | hit.tag = tag->id(); |
3333 | hit.band = band; |
3334 | // If we are in the band, we hit this tag, in other |
3335 | // case, we can try to hit other tag that might be a |
3336 | // better match. |
3337 | if (bandBounds.contains(mousePos)) |
3338 | break; |
3339 | } |
3340 | else if (gfx::Rect(bounds.x, bounds.y, fw, bounds.h).contains(mousePos)) { |
3341 | hit.part = PART_TAG_LEFT; |
3342 | hit.tag = tag->id(); |
3343 | hit.band = band; |
3344 | if (bandBounds.contains(mousePos)) |
3345 | break; |
3346 | } |
3347 | } |
3348 | } |
3349 | } |
3350 | |
3351 | // Mouse in bands |
3352 | if (hit.part == PART_NOTHING) { |
3353 | if (m_tagFocusBand < 0) { |
3354 | for (int band=0; band<m_tagBands; ++band) { |
3355 | gfx::Rect bounds = getPartBounds( |
3356 | Hit(PART_TAG_BAND, 0, 0, |
3357 | doc::NullId, band)); |
3358 | if (bounds.contains(mousePos)) { |
3359 | hit.part = PART_TAG_BAND; |
3360 | hit.band = band; |
3361 | break; |
3362 | } |
3363 | } |
3364 | } |
3365 | else { |
3366 | gfx::Rect bounds = getPartBounds( |
3367 | Hit(PART_TAG_BAND, 0, 0, |
3368 | doc::NullId, m_tagFocusBand)); |
3369 | if (bounds.contains(mousePos)) { |
3370 | hit.part = PART_TAG_BAND; |
3371 | hit.band = m_tagFocusBand; |
3372 | } |
3373 | } |
3374 | } |
3375 | } |
3376 | // Is the mouse on the separator. |
3377 | else if (mousePos.x > separatorX()-4 && |
3378 | mousePos.x <= separatorX()) { |
3379 | hit.part = PART_SEPARATOR; |
3380 | } |
3381 | // Is the mouse on the headers? |
3382 | else if (mousePos.y >= top && mousePos.y < top+headerBoxHeight()) { |
3383 | if (mousePos.x < separatorX()) { |
3384 | if (getPartBounds(Hit(PART_HEADER_EYE)).contains(mousePos)) |
3385 | hit.part = PART_HEADER_EYE; |
3386 | else if (getPartBounds(Hit(PART_HEADER_PADLOCK)).contains(mousePos)) |
3387 | hit.part = PART_HEADER_PADLOCK; |
3388 | else if (getPartBounds(Hit(PART_HEADER_CONTINUOUS)).contains(mousePos)) |
3389 | hit.part = PART_HEADER_CONTINUOUS; |
3390 | else if (getPartBounds(Hit(PART_HEADER_GEAR)).contains(mousePos)) |
3391 | hit.part = PART_HEADER_GEAR; |
3392 | else if (getPartBounds(Hit(PART_HEADER_ONIONSKIN)).contains(mousePos)) |
3393 | hit.part = PART_HEADER_ONIONSKIN; |
3394 | else if (getPartBounds(Hit(PART_HEADER_LAYER)).contains(mousePos)) |
3395 | hit.part = PART_HEADER_LAYER; |
3396 | } |
3397 | else { |
3398 | hit.part = PART_HEADER_FRAME; |
3399 | } |
3400 | } |
3401 | // Activate a flag in case that the hit is in the header area (AniControls and tags). |
3402 | else if (mousePos.y < top+headerBoxHeight()) |
3403 | hit.part = PART_TOP; |
3404 | // Is the mouse on a layer's label? |
3405 | else if (mousePos.x < separatorX()) { |
3406 | if (getPartBounds(Hit(PART_ROW_EYE_ICON, hit.layer)).contains(mousePos)) |
3407 | hit.part = PART_ROW_EYE_ICON; |
3408 | else if (getPartBounds(Hit(PART_ROW_PADLOCK_ICON, hit.layer)).contains(mousePos)) |
3409 | hit.part = PART_ROW_PADLOCK_ICON; |
3410 | else if (getPartBounds(Hit(PART_ROW_CONTINUOUS_ICON, hit.layer)).contains(mousePos)) |
3411 | hit.part = PART_ROW_CONTINUOUS_ICON; |
3412 | else if (getPartBounds(Hit(PART_ROW_TEXT, hit.layer)).contains(mousePos)) |
3413 | hit.part = PART_ROW_TEXT; |
3414 | else |
3415 | hit.part = PART_ROW; |
3416 | } |
3417 | else if (validLayer(hit.layer) && validFrame(hit.frame)) { |
3418 | hit.part = PART_CEL; |
3419 | } |
3420 | else |
3421 | hit.part = PART_NOTHING; |
3422 | |
3423 | if (!hasCapture()) { |
3424 | gfx::Rect outline = getPartBounds(Hit(PART_RANGE_OUTLINE)); |
3425 | if (outline.contains(mousePos)) { |
3426 | auto mouseMsg = dynamic_cast<MouseMessage*>(msg); |
3427 | |
3428 | if (// With Ctrl and Alt key we can drag the range from any place (not necessary from the outline. |
3429 | is_copy_key_pressed(msg) || |
3430 | // Drag with right-click |
3431 | (m_state == STATE_STANDBY && |
3432 | mouseMsg && |
3433 | mouseMsg->right()) || |
3434 | // Drag with left-click only if we are inside the range edges |
3435 | !gfx::Rect(outline).shrink(2*outlineWidth()).contains(mousePos)) { |
3436 | hit.part = PART_RANGE_OUTLINE; |
3437 | } |
3438 | } |
3439 | } |
3440 | } |
3441 | |
3442 | return hit; |
3443 | } |
3444 | |
3445 | Timeline::Hit Timeline::hitTestCel(const gfx::Point& mousePos) |
3446 | { |
3447 | Hit hit(PART_NOTHING, -1, -1); |
3448 | if (!m_document) |
3449 | return hit; |
3450 | |
3451 | gfx::Point scroll = viewScroll(); |
3452 | int top = topHeight(); |
3453 | |
3454 | hit.layer = lastLayer() - ( |
3455 | (mousePos.y |
3456 | - top |
3457 | - headerBoxHeight() |
3458 | + scroll.y) / layerBoxHeight()); |
3459 | |
3460 | hit.frame = frame_t((mousePos.x |
3461 | - separatorX() |
3462 | - m_separator_w |
3463 | + scroll.x) / frameBoxWidth()); |
3464 | |
3465 | hit.layer = std::clamp(hit.layer, firstLayer(), lastLayer()); |
3466 | hit.frame = std::max(firstFrame(), hit.frame); |
3467 | |
3468 | return hit; |
3469 | } |
3470 | |
3471 | void Timeline::setHot(const Hit& hit) |
3472 | { |
3473 | // If the part, layer or frame change. |
3474 | if (m_hot != hit) { |
3475 | // Invalidate the whole control. |
3476 | if (isMovingCel()) { |
3477 | invalidate(); |
3478 | } |
3479 | // Invalidate the old and new 'hot' thing. |
3480 | else { |
3481 | invalidateHit(m_hot); |
3482 | invalidateHit(hit); |
3483 | } |
3484 | |
3485 | // Change the new 'hot' thing. |
3486 | m_hot = hit; |
3487 | } |
3488 | } |
3489 | |
3490 | void Timeline::updateStatusBar(ui::Message* msg) |
3491 | { |
3492 | if (!hasMouse()) |
3493 | return; |
3494 | |
3495 | StatusBar* sb = StatusBar::instance(); |
3496 | |
3497 | if (m_state == STATE_MOVING_RANGE && msg) { |
3498 | const char* verb = is_copy_key_pressed(msg) ? "Copy" : "Move" ; |
3499 | |
3500 | switch (m_range.type()) { |
3501 | |
3502 | case Range::kCels: |
3503 | sb->setStatusText(0, fmt::format("{} cels" , verb)); |
3504 | return; |
3505 | |
3506 | case Range::kFrames: |
3507 | if (validFrame(m_hot.frame)) { |
3508 | if (m_dropTarget.hhit == DropTarget::Before) { |
3509 | sb->setStatusText(0, fmt::format("{} before frame {}" , |
3510 | verb, int(m_dropRange.firstFrame()+1))); |
3511 | return; |
3512 | } |
3513 | else if (m_dropTarget.hhit == DropTarget::After) { |
3514 | sb->setStatusText(0, fmt::format("{} after frame {}" , |
3515 | verb, int(m_dropRange.lastFrame()+1))); |
3516 | return; |
3517 | } |
3518 | } |
3519 | break; |
3520 | |
3521 | case Range::kLayers: { |
3522 | layer_t firstLayer; |
3523 | layer_t lastLayer; |
3524 | if (!selectedLayersBounds(m_dropRange.selectedLayers(), |
3525 | &firstLayer, &lastLayer)) |
3526 | break; |
3527 | |
3528 | if (m_dropTarget.vhit == DropTarget::VeryBottom) { |
3529 | sb->setStatusText(0, fmt::format("{} at the very bottom" , verb)); |
3530 | return; |
3531 | } |
3532 | |
3533 | layer_t layerIdx = -1; |
3534 | if (m_dropTarget.vhit == DropTarget::Bottom || |
3535 | m_dropTarget.vhit == DropTarget::FirstChild) |
3536 | layerIdx = firstLayer; |
3537 | else if (m_dropTarget.vhit == DropTarget::Top) |
3538 | layerIdx = lastLayer; |
3539 | |
3540 | Layer* layer = (validLayer(layerIdx) ? m_rows[layerIdx].layer(): nullptr); |
3541 | if (layer) { |
3542 | switch (m_dropTarget.vhit) { |
3543 | case DropTarget::Bottom: |
3544 | sb->setStatusText(0, fmt::format("{} below layer '{}'" , |
3545 | verb, layer->name())); |
3546 | return; |
3547 | case DropTarget::Top: |
3548 | sb->setStatusText(0, fmt::format("{} above layer '{}'" , |
3549 | verb, layer->name())); |
3550 | return; |
3551 | case DropTarget::FirstChild: |
3552 | sb->setStatusText(0, fmt::format("{} as first child of group '{}'" , |
3553 | verb, layer->name())); |
3554 | return; |
3555 | } |
3556 | } |
3557 | break; |
3558 | } |
3559 | |
3560 | } |
3561 | } |
3562 | else { |
3563 | Layer* layer = (validLayer(m_hot.layer) ? m_rows[m_hot.layer].layer(): |
3564 | nullptr); |
3565 | |
3566 | switch (m_hot.part) { |
3567 | |
3568 | case PART_HEADER_ONIONSKIN: { |
3569 | sb->setStatusText(0, fmt::format("Onionskin is {}" , |
3570 | docPref().onionskin.active() |
3571 | ? "enabled" : "disabled" )); |
3572 | return; |
3573 | } |
3574 | |
3575 | case PART_ROW_TEXT: |
3576 | if (layer != NULL) { |
3577 | sb->setStatusText( |
3578 | 0, fmt::format("{} '{}' [{}{}]" , |
3579 | layer->isReference() ? "Reference layer" : "Layer" , |
3580 | layer->name(), |
3581 | layer->isVisible() ? "visible" : "hidden" , |
3582 | layer->isEditable() ? "" : " locked" )); |
3583 | return; |
3584 | } |
3585 | break; |
3586 | |
3587 | case PART_ROW_EYE_ICON: |
3588 | if (layer != NULL) { |
3589 | sb->setStatusText( |
3590 | 0, fmt::format("Layer '{}' is {}" , |
3591 | layer->name(), |
3592 | layer->isVisible() ? "visible" : "hidden" )); |
3593 | return; |
3594 | } |
3595 | break; |
3596 | |
3597 | case PART_ROW_PADLOCK_ICON: |
3598 | if (layer != NULL) { |
3599 | sb->setStatusText( |
3600 | 0, fmt::format("Layer '{}' is {}" , |
3601 | layer->name(), |
3602 | layer->isEditable() ? "unlocked (editable)" : |
3603 | "locked (read-only)" )); |
3604 | return; |
3605 | } |
3606 | break; |
3607 | |
3608 | case PART_ROW_CONTINUOUS_ICON: |
3609 | if (layer) { |
3610 | if (layer->isImage()) |
3611 | sb->setStatusText( |
3612 | 0, fmt::format("Layer '{}' is {} ({})" , |
3613 | layer->name(), |
3614 | layer->isContinuous() ? "continuous" : |
3615 | "discontinuous" , |
3616 | layer->isContinuous() ? "prefer linked cels/frames" : |
3617 | "prefer individual cels/frames" )); |
3618 | else if (layer->isGroup()) |
3619 | sb->setStatusText( |
3620 | 0, fmt::format("Group '{}'" , layer->name())); |
3621 | return; |
3622 | } |
3623 | break; |
3624 | |
3625 | case PART_HEADER_FRAME: |
3626 | case PART_CEL: |
3627 | case PART_TAG: { |
3628 | frame_t frame = m_frame; |
3629 | if (validFrame(m_hot.frame)) |
3630 | frame = m_hot.frame; |
3631 | |
3632 | updateStatusBarForFrame( |
3633 | frame, |
3634 | m_hot.getTag(), |
3635 | (layer ? layer->cel(frame) : nullptr)); |
3636 | return; |
3637 | } |
3638 | } |
3639 | } |
3640 | |
3641 | sb->showDefaultText(); |
3642 | } |
3643 | |
3644 | void Timeline::updateStatusBarForFrame(const frame_t frame, |
3645 | const Tag* tag, |
3646 | const Cel* cel) |
3647 | { |
3648 | if (!m_sprite) |
3649 | return; |
3650 | |
3651 | char buf[256] = { 0 }; |
3652 | frame_t base = docPref().timeline.firstFrame(); |
3653 | frame_t firstFrame = frame; |
3654 | frame_t lastFrame = frame; |
3655 | |
3656 | if (tag) { |
3657 | firstFrame = tag->fromFrame(); |
3658 | lastFrame = tag->toFrame(); |
3659 | } |
3660 | else if (m_range.enabled() && |
3661 | m_range.frames() > 1) { |
3662 | firstFrame = m_range.firstFrame(); |
3663 | lastFrame = m_range.lastFrame(); |
3664 | } |
3665 | |
3666 | std::sprintf( |
3667 | buf+std::strlen(buf), ":frame: %d" , |
3668 | base+frame); |
3669 | if (firstFrame != lastFrame) { |
3670 | std::sprintf( |
3671 | buf+std::strlen(buf), " [%d...%d]" , |
3672 | int(base+firstFrame), |
3673 | int(base+lastFrame)); |
3674 | } |
3675 | |
3676 | std::sprintf( |
3677 | buf+std::strlen(buf), " :clock: %s" , |
3678 | human_readable_time(m_sprite->frameDuration(frame)).c_str()); |
3679 | if (firstFrame != lastFrame) { |
3680 | std::sprintf( |
3681 | buf+std::strlen(buf), " [%s]" , |
3682 | tag ? |
3683 | human_readable_time(tagFramesDuration(tag)).c_str(): |
3684 | human_readable_time(selectedFramesDuration()).c_str()); |
3685 | } |
3686 | if (m_sprite->totalFrames() > 1) |
3687 | std::sprintf( |
3688 | buf+std::strlen(buf), "/%s" , |
3689 | human_readable_time(m_sprite->totalAnimationDuration()).c_str()); |
3690 | |
3691 | if (cel) { |
3692 | std::sprintf( |
3693 | buf+std::strlen(buf), " Cel :pos: %d %d :size: %d %d" , |
3694 | cel->bounds().x, cel->bounds().y, |
3695 | cel->bounds().w, cel->bounds().h); |
3696 | |
3697 | if (cel->links() > 0) { |
3698 | std::sprintf( |
3699 | buf+std::strlen(buf), " Links %d" , |
3700 | int(cel->links())); |
3701 | } |
3702 | } |
3703 | |
3704 | StatusBar::instance()->setStatusText(0, buf); |
3705 | } |
3706 | |
3707 | void Timeline::showCel(layer_t layer, frame_t frame) |
3708 | { |
3709 | gfx::Point scroll = viewScroll(); |
3710 | |
3711 | gfx::Rect viewport = m_viewportArea; |
3712 | |
3713 | // Add the horizontal bar space to the viewport area if the viewport |
3714 | // is not big enough to show one cel. |
3715 | if (m_hbar.isVisible() && viewport.h < layerBoxHeight()) |
3716 | viewport.h += m_vbar.getBarWidth(); |
3717 | |
3718 | gfx::Rect celBounds( |
3719 | viewport.x + frameBoxWidth()*frame - scroll.x, |
3720 | viewport.y + layerBoxHeight()*(lastLayer() - layer) - scroll.y, |
3721 | frameBoxWidth(), layerBoxHeight()); |
3722 | |
3723 | const bool isPlaying = m_editor->isPlaying(); |
3724 | |
3725 | // Here we use <= instead of < to avoid jumping between this |
3726 | // condition and the "else if" one when we are playing the |
3727 | // animation. |
3728 | if (celBounds.x <= viewport.x) { |
3729 | scroll.x -= viewport.x - celBounds.x; |
3730 | if (isPlaying) |
3731 | scroll.x -= viewport.w/2 - celBounds.w/2; |
3732 | } |
3733 | else if (celBounds.x2() > viewport.x2()) { |
3734 | scroll.x += celBounds.x2() - viewport.x2(); |
3735 | if (isPlaying) |
3736 | scroll.x += viewport.w/2 - celBounds.w/2; |
3737 | } |
3738 | |
3739 | if (celBounds.y <= viewport.y) { |
3740 | scroll.y -= viewport.y - celBounds.y; |
3741 | } |
3742 | else if (celBounds.y2() > viewport.y2()) { |
3743 | scroll.y += celBounds.y2() - viewport.y2(); |
3744 | } |
3745 | |
3746 | setViewScroll(scroll); |
3747 | } |
3748 | |
3749 | void Timeline::showCurrentCel() |
3750 | { |
3751 | layer_t layer = getLayerIndex(m_layer); |
3752 | if (layer >= firstLayer()) |
3753 | showCel(layer, m_frame); |
3754 | } |
3755 | |
3756 | void Timeline::focusTagBand(int band) |
3757 | { |
3758 | if (m_tagFocusBand < 0) { |
3759 | m_tagFocusBand = band; |
3760 | } |
3761 | else { |
3762 | m_tagFocusBand = -1; |
3763 | } |
3764 | } |
3765 | |
3766 | void Timeline::cleanClk() |
3767 | { |
3768 | invalidateHit(m_clk); |
3769 | m_clk = Hit(PART_NOTHING); |
3770 | } |
3771 | |
3772 | gfx::Size Timeline::getScrollableSize() const |
3773 | { |
3774 | if (m_sprite) { |
3775 | return gfx::Size( |
3776 | m_sprite->totalFrames() * frameBoxWidth() + getCelsBounds().w/2, |
3777 | (m_rows.size()+1) * layerBoxHeight()); |
3778 | } |
3779 | else |
3780 | return gfx::Size(0, 0); |
3781 | } |
3782 | |
3783 | gfx::Point Timeline::getMaxScrollablePos() const |
3784 | { |
3785 | if (m_sprite) { |
3786 | gfx::Size size = getScrollableSize(); |
3787 | int max_scroll_x = size.w - getCelsBounds().w + 1*guiscale(); |
3788 | int max_scroll_y = size.h - getCelsBounds().h + 1*guiscale(); |
3789 | max_scroll_x = std::max(0, max_scroll_x); |
3790 | max_scroll_y = std::max(0, max_scroll_y); |
3791 | return gfx::Point(max_scroll_x, max_scroll_y); |
3792 | } |
3793 | else |
3794 | return gfx::Point(0, 0); |
3795 | } |
3796 | |
3797 | bool Timeline::allLayersVisible() |
3798 | { |
3799 | for (Layer* topLayer : m_sprite->root()->layers()) |
3800 | if (!topLayer->isVisible()) |
3801 | return false; |
3802 | |
3803 | return true; |
3804 | } |
3805 | |
3806 | bool Timeline::allLayersInvisible() |
3807 | { |
3808 | for (Layer* topLayer : m_sprite->root()->layers()) |
3809 | if (topLayer->isVisible()) |
3810 | return false; |
3811 | |
3812 | return true; |
3813 | } |
3814 | |
3815 | bool Timeline::allLayersLocked() |
3816 | { |
3817 | for (Layer* topLayer : m_sprite->root()->layers()) |
3818 | if (topLayer->isEditable()) |
3819 | return false; |
3820 | |
3821 | return true; |
3822 | } |
3823 | |
3824 | bool Timeline::allLayersUnlocked() |
3825 | { |
3826 | for (Layer* topLayer : m_sprite->root()->layers()) |
3827 | if (!topLayer->isEditable()) |
3828 | return false; |
3829 | |
3830 | return true; |
3831 | } |
3832 | |
3833 | bool Timeline::allLayersContinuous() |
3834 | { |
3835 | for (size_t i=0; i<m_rows.size(); i++) |
3836 | if (!m_rows[i].layer()->isContinuous()) |
3837 | return false; |
3838 | |
3839 | return true; |
3840 | } |
3841 | |
3842 | bool Timeline::allLayersDiscontinuous() |
3843 | { |
3844 | for (size_t i=0; i<m_rows.size(); i++) |
3845 | if (m_rows[i].layer()->isContinuous()) |
3846 | return false; |
3847 | |
3848 | return true; |
3849 | } |
3850 | |
3851 | layer_t Timeline::getLayerIndex(const Layer* layer) const |
3852 | { |
3853 | for (int i=0; i<(int)m_rows.size(); i++) |
3854 | if (m_rows[i].layer() == layer) |
3855 | return i; |
3856 | |
3857 | return -1; |
3858 | } |
3859 | |
3860 | bool Timeline::isLayerActive(const layer_t layerIndex) const |
3861 | { |
3862 | if (layerIndex == getLayerIndex(m_layer)) |
3863 | return true; |
3864 | else |
3865 | return m_range.contains(m_rows[layerIndex].layer()); |
3866 | } |
3867 | |
3868 | bool Timeline::isFrameActive(const frame_t frame) const |
3869 | { |
3870 | if (frame == m_frame) |
3871 | return true; |
3872 | else |
3873 | return m_range.contains(frame); |
3874 | } |
3875 | |
3876 | bool Timeline::isCelActive(const layer_t layerIdx, const frame_t frame) const |
3877 | { |
3878 | if (m_range.enabled()) |
3879 | return m_range.contains(m_rows[layerIdx].layer(), frame); |
3880 | else |
3881 | return (layerIdx == getLayerIndex(m_layer) && |
3882 | frame == m_frame); |
3883 | } |
3884 | |
3885 | bool Timeline::isCelLooselyActive(const layer_t layerIdx, const frame_t frame) const |
3886 | { |
3887 | if (m_range.enabled()) |
3888 | return (m_range.contains(m_rows[layerIdx].layer()) || |
3889 | m_range.contains(frame)); |
3890 | else |
3891 | return (layerIdx == getLayerIndex(m_layer) || |
3892 | frame == m_frame); |
3893 | } |
3894 | |
3895 | void Timeline::dropRange(DropOp op) |
3896 | { |
3897 | bool copy = (op == Timeline::kCopy); |
3898 | Range newFromRange; |
3899 | DocRangePlace place = kDocRangeAfter; |
3900 | Range dropRange = m_dropRange; |
3901 | bool outside = m_dropTarget.outside; |
3902 | |
3903 | switch (m_range.type()) { |
3904 | |
3905 | case Range::kFrames: |
3906 | if (m_dropTarget.hhit == DropTarget::Before) |
3907 | place = kDocRangeBefore; |
3908 | break; |
3909 | |
3910 | case Range::kLayers: |
3911 | switch (m_dropTarget.vhit) { |
3912 | case DropTarget::Bottom: |
3913 | place = kDocRangeBefore; |
3914 | break; |
3915 | case DropTarget::FirstChild: |
3916 | place = kDocRangeFirstChild; |
3917 | break; |
3918 | case DropTarget::VeryBottom: |
3919 | place = kDocRangeBefore; |
3920 | { |
3921 | Layer* layer = m_sprite->root()->firstLayer(); |
3922 | dropRange.clearRange(); |
3923 | dropRange.startRange(layer, -1, Range::kLayers); |
3924 | dropRange.endRange(layer, -1); |
3925 | } |
3926 | break; |
3927 | } |
3928 | break; |
3929 | } |
3930 | |
3931 | prepareToMoveRange(); |
3932 | |
3933 | try { |
3934 | TagsHandling tagsHandling = (outside ? kFitOutsideTags: |
3935 | kFitInsideTags); |
3936 | |
3937 | invalidateRange(); |
3938 | if (copy) |
3939 | newFromRange = copy_range(m_document, m_range, dropRange, |
3940 | place, tagsHandling); |
3941 | else |
3942 | newFromRange = move_range(m_document, m_range, dropRange, |
3943 | place, tagsHandling); |
3944 | |
3945 | // If we drop a cel in the same frame (but in another layer), |
3946 | // document views are not updated, so we are forcing the updating of |
3947 | // all views. |
3948 | m_document->notifyGeneralUpdate(); |
3949 | |
3950 | moveRange(newFromRange); |
3951 | |
3952 | invalidateRange(); |
3953 | |
3954 | if (m_range.type() == Range::kFrames && |
3955 | m_sprite && |
3956 | !m_sprite->tags().empty()) { |
3957 | invalidateRect(getFrameHeadersBounds().offset(origin())); |
3958 | } |
3959 | |
3960 | // Update the sprite position after the command was executed |
3961 | // TODO improve this workaround |
3962 | Cmd* cmd = m_document->undoHistory()->lastExecutedCmd(); |
3963 | if (auto cmdTx = dynamic_cast<CmdTransaction*>(cmd)) |
3964 | cmdTx->updateSpritePositionAfter(); |
3965 | } |
3966 | catch (const std::exception& ex) { |
3967 | Console::showException(ex); |
3968 | } |
3969 | } |
3970 | |
3971 | gfx::Size Timeline::visibleSize() const |
3972 | { |
3973 | return getCelsBounds().size(); |
3974 | } |
3975 | |
3976 | gfx::Point Timeline::viewScroll() const |
3977 | { |
3978 | return gfx::Point(m_hbar.getPos(), m_vbar.getPos()); |
3979 | } |
3980 | |
3981 | void Timeline::setViewScroll(const gfx::Point& pt) |
3982 | { |
3983 | const gfx::Point oldScroll = viewScroll(); |
3984 | const gfx::Point maxPos = getMaxScrollablePos(); |
3985 | gfx::Point newScroll = pt; |
3986 | newScroll.x = std::clamp(newScroll.x, 0, maxPos.x); |
3987 | newScroll.y = std::clamp(newScroll.y, 0, maxPos.y); |
3988 | |
3989 | if (newScroll.y != oldScroll.y) { |
3990 | gfx::Rect rc; |
3991 | rc = getLayerHeadersBounds(); |
3992 | rc.offset(origin()); |
3993 | invalidateRect(rc); |
3994 | } |
3995 | |
3996 | if (newScroll != oldScroll) { |
3997 | gfx::Rect rc; |
3998 | if (m_tagBands > 0) |
3999 | rc |= getPartBounds(Hit(PART_TAG_BAND)); |
4000 | if (m_range.enabled()) |
4001 | rc |= getRangeBounds(m_range).enlarge(outlineWidth()); |
4002 | rc |= getFrameHeadersBounds(); |
4003 | rc |= getCelsBounds(); |
4004 | rc.offset(origin()); |
4005 | invalidateRect(rc); |
4006 | } |
4007 | |
4008 | m_hbar.setPos(newScroll.x); |
4009 | m_vbar.setPos(newScroll.y); |
4010 | } |
4011 | |
4012 | |
4013 | void Timeline::lockRange() |
4014 | { |
4015 | ++m_rangeLocks; |
4016 | } |
4017 | |
4018 | void Timeline::unlockRange() |
4019 | { |
4020 | --m_rangeLocks; |
4021 | } |
4022 | |
4023 | void Timeline::updateDropRange(const gfx::Point& pt) |
4024 | { |
4025 | const DropTarget oldDropTarget = m_dropTarget; |
4026 | m_dropTarget.hhit = DropTarget::HNone; |
4027 | m_dropTarget.vhit = DropTarget::VNone; |
4028 | m_dropTarget.outside = false; |
4029 | |
4030 | if (m_state != STATE_MOVING_RANGE) { |
4031 | m_dropRange.clearRange(); |
4032 | return; |
4033 | } |
4034 | |
4035 | switch (m_range.type()) { |
4036 | |
4037 | case Range::kCels: |
4038 | m_dropRange = m_range; |
4039 | m_dropRange.displace(m_hot.layer - m_clk.layer, |
4040 | m_hot.frame - m_clk.frame); |
4041 | break; |
4042 | |
4043 | case Range::kFrames: |
4044 | case Range::kLayers: |
4045 | m_dropRange.clearRange(); |
4046 | m_dropRange.startRange(m_rows[m_hot.layer].layer(), m_hot.frame, m_range.type()); |
4047 | m_dropRange.endRange(m_rows[m_hot.layer].layer(), m_hot.frame); |
4048 | break; |
4049 | } |
4050 | |
4051 | gfx::Rect bounds = getRangeBounds(m_dropRange); |
4052 | |
4053 | if (pt.x < bounds.x + bounds.w/2) { |
4054 | m_dropTarget.hhit = DropTarget::Before; |
4055 | m_dropTarget.outside = (pt.x < bounds.x+2); |
4056 | } |
4057 | else { |
4058 | m_dropTarget.hhit = DropTarget::After; |
4059 | m_dropTarget.outside = (pt.x >= bounds.x2()-2); |
4060 | } |
4061 | |
4062 | if (m_hot.veryBottom) |
4063 | m_dropTarget.vhit = DropTarget::VeryBottom; |
4064 | else if (pt.y < bounds.y + bounds.h/2) |
4065 | m_dropTarget.vhit = DropTarget::Top; |
4066 | // Special drop target for expanded groups |
4067 | else if (m_range.type() == Range::kLayers && |
4068 | m_hot.layer >= 0 && |
4069 | m_hot.layer < int(m_rows.size()) && |
4070 | m_rows[m_hot.layer].layer()->isGroup() && |
4071 | static_cast<LayerGroup*>(m_rows[m_hot.layer].layer())->isExpanded()) { |
4072 | m_dropTarget.vhit = DropTarget::FirstChild; |
4073 | } |
4074 | else { |
4075 | m_dropTarget.vhit = DropTarget::Bottom; |
4076 | } |
4077 | |
4078 | if (oldDropTarget != m_dropTarget) |
4079 | invalidate(); |
4080 | } |
4081 | |
4082 | void Timeline::clearClipboardRange() |
4083 | { |
4084 | Doc* clipboard_document; |
4085 | DocRange clipboard_range; |
4086 | auto clipboard = Clipboard::instance(); |
4087 | clipboard->getDocumentRangeInfo( |
4088 | &clipboard_document, |
4089 | &clipboard_range); |
4090 | |
4091 | if (!m_document || clipboard_document != m_document) |
4092 | return; |
4093 | |
4094 | clipboard->clearContent(); |
4095 | m_clipboard_timer.stop(); |
4096 | } |
4097 | |
4098 | void Timeline::invalidateRange() |
4099 | { |
4100 | if (m_range.enabled()) { |
4101 | for (const Layer* layer : m_range.selectedLayers()) |
4102 | invalidateLayer(layer); |
4103 | for (const frame_t frame : m_range.selectedFrames()) |
4104 | invalidateFrame(frame); |
4105 | |
4106 | invalidateHit(Hit(PART_RANGE_OUTLINE)); |
4107 | } |
4108 | } |
4109 | |
4110 | void Timeline::clearAndInvalidateRange() |
4111 | { |
4112 | if (m_range.enabled()) { |
4113 | notify_observers(&TimelineObserver::onBeforeRangeChanged, this); |
4114 | |
4115 | invalidateRange(); |
4116 | m_range.clearRange(); |
4117 | } |
4118 | } |
4119 | |
4120 | DocumentPreferences& Timeline::docPref() const |
4121 | { |
4122 | return Preferences::instance().document(m_document); |
4123 | } |
4124 | |
4125 | skin::SkinTheme* Timeline::skinTheme() const |
4126 | { |
4127 | return SkinTheme::get(this); |
4128 | } |
4129 | |
4130 | gfx::Size Timeline::celBoxSize() const |
4131 | { |
4132 | return gfx::Size(frameBoxWidth(), layerBoxHeight()); |
4133 | } |
4134 | |
4135 | int Timeline::() const |
4136 | { |
4137 | return skinTheme()->dimensions.timelineBaseSize(); |
4138 | } |
4139 | |
4140 | int Timeline::() const |
4141 | { |
4142 | return skinTheme()->dimensions.timelineBaseSize(); |
4143 | } |
4144 | |
4145 | int Timeline::layerBoxHeight() const |
4146 | { |
4147 | return int(zoom()*skinTheme()->dimensions.timelineBaseSize()); |
4148 | } |
4149 | |
4150 | int Timeline::frameBoxWidth() const |
4151 | { |
4152 | return int(zoom()*skinTheme()->dimensions.timelineBaseSize()); |
4153 | } |
4154 | |
4155 | int Timeline::outlineWidth() const |
4156 | { |
4157 | return skinTheme()->dimensions.timelineOutlineWidth(); |
4158 | } |
4159 | |
4160 | int Timeline::oneTagHeight() const |
4161 | { |
4162 | return |
4163 | font()->height() + |
4164 | 2*ui::guiscale() + |
4165 | skinTheme()->dimensions.timelineTagsAreaHeight(); |
4166 | } |
4167 | |
4168 | double Timeline::zoom() const |
4169 | { |
4170 | if (docPref().thumbnails.enabled()) |
4171 | return m_zoom; |
4172 | else |
4173 | return 1.0; |
4174 | } |
4175 | |
4176 | // Returns the last frame where the frame tag (or frame tag label) |
4177 | // is visible in the timeline. |
4178 | int Timeline::calcTagVisibleToFrame(Tag* tag) const |
4179 | { |
4180 | return |
4181 | std::max(tag->toFrame(), |
4182 | tag->fromFrame() + |
4183 | font()->textLength(tag->name())/frameBoxWidth()); |
4184 | } |
4185 | |
4186 | int Timeline::topHeight() const |
4187 | { |
4188 | int h = 0; |
4189 | if (m_document && m_sprite) { |
4190 | h += skinTheme()->dimensions.timelineTopBorder(); |
4191 | h += oneTagHeight() * visibleTagBands(); |
4192 | } |
4193 | return h; |
4194 | } |
4195 | |
4196 | void Timeline::onNewInputPriority(InputChainElement* element, |
4197 | const ui::Message* msg) |
4198 | { |
4199 | // It looks like the user wants to execute commands targetting the |
4200 | // ColorBar instead of the Timeline. Here we disable the selected |
4201 | // range, so commands like Clear, Copy, Cut, etc. don't target the |
4202 | // Timeline and they are sent to the active sprite editor. |
4203 | // |
4204 | // If the Workspace is selected (an sprite Editor), maybe the user |
4205 | // want to move the X/Y position of all cels in the Timeline range. |
4206 | // That is why we don't disable the range in this case. |
4207 | Workspace* workspace = dynamic_cast<Workspace*>(element); |
4208 | if (!workspace) { |
4209 | // With Ctrl or Shift we can combine ColorBar selection + Timeline |
4210 | // selection. |
4211 | if (msg && (msg->ctrlPressed() || msg->shiftPressed())) |
4212 | return; |
4213 | |
4214 | if (element != this && m_rangeLocks == 0) { |
4215 | if (!Preferences::instance().timeline.keepSelection()) { |
4216 | m_range.clearRange(); |
4217 | invalidate(); |
4218 | } |
4219 | } |
4220 | } |
4221 | } |
4222 | |
4223 | bool Timeline::onCanCut(Context* ctx) |
4224 | { |
4225 | return false; // TODO |
4226 | } |
4227 | |
4228 | bool Timeline::onCanCopy(Context* ctx) |
4229 | { |
4230 | return |
4231 | m_range.enabled() && |
4232 | ctx->checkFlags(ContextFlags::HasActiveDocument); |
4233 | } |
4234 | |
4235 | bool Timeline::onCanPaste(Context* ctx) |
4236 | { |
4237 | return |
4238 | (ctx->clipboard()->format() == ClipboardFormat::DocRange && |
4239 | ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable)); |
4240 | } |
4241 | |
4242 | bool Timeline::onCanClear(Context* ctx) |
4243 | { |
4244 | return (m_document && m_sprite && m_range.enabled()); |
4245 | } |
4246 | |
4247 | bool Timeline::onCut(Context* ctx) |
4248 | { |
4249 | return false; // TODO |
4250 | } |
4251 | |
4252 | bool Timeline::onCopy(Context* ctx) |
4253 | { |
4254 | if (m_range.enabled()) { |
4255 | const ContextReader reader(ctx); |
4256 | if (reader.document()) { |
4257 | ctx->clipboard()->copyRange(reader, m_range); |
4258 | return true; |
4259 | } |
4260 | } |
4261 | return false; |
4262 | } |
4263 | |
4264 | bool Timeline::onPaste(Context* ctx) |
4265 | { |
4266 | auto clipboard = ctx->clipboard(); |
4267 | if (clipboard->format() == ClipboardFormat::DocRange) { |
4268 | clipboard->paste(ctx, true); |
4269 | return true; |
4270 | } |
4271 | else |
4272 | return false; |
4273 | } |
4274 | |
4275 | bool Timeline::onClear(Context* ctx) |
4276 | { |
4277 | if (!m_document || |
4278 | !m_sprite || |
4279 | !m_range.enabled() || |
4280 | // If the mask is visible the delete command will be handled by |
4281 | // the Editor |
4282 | m_document->isMaskVisible()) |
4283 | return false; |
4284 | |
4285 | Command* cmd = nullptr; |
4286 | |
4287 | switch (m_range.type()) { |
4288 | case DocRange::kCels: |
4289 | cmd = Commands::instance()->byId(CommandId::ClearCel()); |
4290 | break; |
4291 | case DocRange::kFrames: |
4292 | cmd = Commands::instance()->byId(CommandId::RemoveFrame()); |
4293 | break; |
4294 | case DocRange::kLayers: |
4295 | cmd = Commands::instance()->byId(CommandId::RemoveLayer()); |
4296 | break; |
4297 | } |
4298 | |
4299 | if (cmd) { |
4300 | ctx->executeCommand(cmd); |
4301 | return true; |
4302 | } |
4303 | else |
4304 | return false; |
4305 | } |
4306 | |
4307 | void Timeline::onCancel(Context* ctx) |
4308 | { |
4309 | if (m_rangeLocks == 0) |
4310 | clearAndInvalidateRange(); |
4311 | |
4312 | clearClipboardRange(); |
4313 | invalidate(); |
4314 | } |
4315 | |
4316 | int Timeline::tagFramesDuration(const Tag* tag) const |
4317 | { |
4318 | ASSERT(m_sprite); |
4319 | ASSERT(tag); |
4320 | |
4321 | int duration = 0; |
4322 | for (frame_t f=tag->fromFrame(); |
4323 | f<tag->toFrame(); ++f) { |
4324 | duration += m_sprite->frameDuration(f); |
4325 | } |
4326 | return duration; |
4327 | } |
4328 | |
4329 | int Timeline::selectedFramesDuration() const |
4330 | { |
4331 | ASSERT(m_sprite); |
4332 | |
4333 | int duration = 0; |
4334 | for (frame_t f=0; f<m_sprite->totalFrames(); ++f) { |
4335 | if (isFrameActive(f)) |
4336 | duration += m_sprite->frameDuration(f); |
4337 | } |
4338 | return duration; // TODO cache this value |
4339 | } |
4340 | |
4341 | void Timeline::setLayerVisibleFlag(const layer_t l, const bool state) |
4342 | { |
4343 | if (!validLayer(l)) |
4344 | return; |
4345 | |
4346 | Row& row = m_rows[l]; |
4347 | Layer* layer = row.layer(); |
4348 | ASSERT(layer); |
4349 | if (!layer) |
4350 | return; |
4351 | |
4352 | bool redrawEditors = false; |
4353 | bool regenRows = false; |
4354 | |
4355 | if (layer->isVisible() != state) { |
4356 | layer->setVisible(state); |
4357 | redrawEditors = true; |
4358 | |
4359 | // Regenerate rows because might change the flag of the children |
4360 | // (the flag is propagated to the children in m_inheritedFlags). |
4361 | if (layer->isGroup() && layer->isExpanded()) { |
4362 | regenRows = true; |
4363 | } |
4364 | |
4365 | // Show parents too |
4366 | if (!row.parentVisible() && state) { |
4367 | layer = layer->parent(); |
4368 | while (layer) { |
4369 | if (!layer->isVisible()) { |
4370 | layer->setVisible(true); |
4371 | regenRows = true; |
4372 | redrawEditors = true; |
4373 | } |
4374 | layer = layer->parent(); |
4375 | } |
4376 | } |
4377 | } |
4378 | |
4379 | if (regenRows) { |
4380 | regenerateRows(); |
4381 | invalidate(); |
4382 | } |
4383 | |
4384 | if (redrawEditors) |
4385 | m_document->notifyGeneralUpdate(); |
4386 | } |
4387 | |
4388 | void Timeline::setLayerEditableFlag(const layer_t l, const bool state) |
4389 | { |
4390 | Row& row = m_rows[l]; |
4391 | Layer* layer = row.layer(); |
4392 | ASSERT(layer); |
4393 | if (!layer) |
4394 | return; |
4395 | |
4396 | bool regenRows = false; |
4397 | |
4398 | if (layer->isEditable() != state) { |
4399 | layer->setEditable(state); |
4400 | |
4401 | if (layer->isGroup() && layer->isExpanded()) |
4402 | regenRows = true; |
4403 | |
4404 | // Make parents editable too |
4405 | if (!row.parentEditable() && state) { |
4406 | layer = layer->parent(); |
4407 | while (layer) { |
4408 | if (!layer->isEditable()) { |
4409 | layer->setEditable(true); |
4410 | regenRows = true; |
4411 | } |
4412 | layer = layer->parent(); |
4413 | } |
4414 | } |
4415 | } |
4416 | |
4417 | if (regenRows) { |
4418 | regenerateRows(); |
4419 | invalidate(); |
4420 | } |
4421 | } |
4422 | |
4423 | void Timeline::setLayerContinuousFlag(const layer_t l, const bool state) |
4424 | { |
4425 | Layer* layer = m_rows[l].layer(); |
4426 | ASSERT(layer); |
4427 | if (!layer) |
4428 | return; |
4429 | |
4430 | if (layer->isImage() && layer->isContinuous() != state) { |
4431 | layer->setContinuous(state); |
4432 | invalidate(); |
4433 | } |
4434 | } |
4435 | |
4436 | void Timeline::setLayerCollapsedFlag(const layer_t l, const bool state) |
4437 | { |
4438 | Layer* layer = m_rows[l].layer(); |
4439 | ASSERT(layer); |
4440 | if (!layer) |
4441 | return; |
4442 | |
4443 | if (layer->isGroup() && layer->isCollapsed() != state) { |
4444 | layer->setCollapsed(state); |
4445 | |
4446 | regenerateRows(); |
4447 | invalidate(); |
4448 | } |
4449 | } |
4450 | |
4451 | int Timeline::separatorX() const |
4452 | { |
4453 | return std::clamp(m_separator_x, |
4454 | headerBoxWidth(), |
4455 | std::max(bounds().w-guiscale(), |
4456 | headerBoxWidth())); |
4457 | } |
4458 | |
4459 | void Timeline::setSeparatorX(int newValue) |
4460 | { |
4461 | m_separator_x = std::max(0, newValue); |
4462 | } |
4463 | |
4464 | //static |
4465 | gfx::Color Timeline::highlightColor(const gfx::Color color) |
4466 | { |
4467 | int r, g, b; |
4468 | r = gfx::getr(color)+64; // TODO make this customizable in the theme XML? |
4469 | g = gfx::getg(color)+64; |
4470 | b = gfx::getb(color)+64; |
4471 | r = std::clamp(r, 0, 255); |
4472 | g = std::clamp(g, 0, 255); |
4473 | b = std::clamp(b, 0, 255); |
4474 | return gfx::rgba(r, g, b, gfx::geta(color)); |
4475 | } |
4476 | |
4477 | } // namespace app |
4478 | |