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
66namespace app {
67
68using namespace app::skin;
69using namespace gfx;
70using namespace doc;
71using namespace ui;
72
73enum {
74 PART_NOTHING = 0,
75 PART_TOP,
76 PART_SEPARATOR,
77 PART_HEADER_EYE,
78 PART_HEADER_PADLOCK,
79 PART_HEADER_CONTINUOUS,
80 PART_HEADER_GEAR,
81 PART_HEADER_ONIONSKIN,
82 PART_HEADER_ONIONSKIN_RANGE_LEFT,
83 PART_HEADER_ONIONSKIN_RANGE_RIGHT,
84 PART_HEADER_LAYER,
85 PART_HEADER_FRAME,
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
102struct 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
113namespace {
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
167Timeline::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
181bool 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
191Tag* Timeline::Hit::getTag() const
192{
193 return get<Tag>(tag);
194}
195
196Timeline::DropTarget::DropTarget()
197{
198 hhit = HNone;
199 vhit = VNone;
200 outside = false;
201}
202
203Timeline::DropTarget::DropTarget(const DropTarget& o)
204 : hhit(o.hhit)
205 , vhit(o.vhit)
206 , outside(o.outside)
207{
208}
209
210Timeline::Row::Row()
211 : m_layer(nullptr),
212 m_level(0),
213 m_inheritedFlags(LayerFlags::None)
214{
215}
216
217Timeline::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
226bool Timeline::Row::parentVisible() const
227{
228 return ((int(m_inheritedFlags) & int(LayerFlags::Visible)) != 0);
229}
230
231bool Timeline::Row::parentEditable() const
232{
233 return ((int(m_inheritedFlags) & int(LayerFlags::Editable)) != 0);
234}
235
236Timeline::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
279Timeline::~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
292void 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
299void 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
314void Timeline::onThumbnailsPrefChange()
315{
316 setZoomAndUpdate(
317 docPref().thumbnails.enabled() ?
318 docPref().thumbnails.zoom(): 1.0,
319 false);
320}
321
322void 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
398void 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
425bool Timeline::isMovingCel() const
426{
427 return (m_state == STATE_MOVING_RANGE &&
428 m_range.type() == Range::kCels);
429}
430
431bool 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
448void 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
473void 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
520void 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
542void 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
574void Timeline::setRange(const Range& range)
575{
576 m_range = range;
577 invalidate();
578}
579
580void Timeline::activateClipboardRange()
581{
582 m_clipboard_timer.start();
583 invalidate();
584}
585
586Tag* 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
610bool 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* popupMenu = 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* popupMenu = 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* popupMenu =
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* popupMenu = 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
1571void 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
1588void Timeline::onInvalidateRegion(const gfx::Region& region)
1589{
1590 Widget::onInvalidateRegion(region);
1591 m_redrawMarchingAntsOnly = false;
1592}
1593
1594void 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
1600void 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
1617void 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
1773paintNoDoc:;
1774 if (noDoc)
1775 drawPart(
1776 g, clientBounds(), nullptr,
1777 skinTheme()->styles.timelinePadding());
1778}
1779
1780void Timeline::onBeforeCommandExecution(CommandExecutionEvent& ev)
1781{
1782 m_savedVersion = (m_document ? m_document->sprite()->version(): 0);
1783}
1784
1785void 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
1799void Timeline::onActiveSiteChange(const Site& site)
1800{
1801 if (hasMouse()) {
1802 updateStatusBarForFrame(site.frame(), nullptr, site.cel());
1803 }
1804}
1805
1806void Timeline::onRemoveDocument(Doc* document)
1807{
1808 if (document == m_document) {
1809 detachDocument();
1810 }
1811}
1812
1813void Timeline::onGeneralUpdate(DocEvent& ev)
1814{
1815 invalidate();
1816}
1817
1818void 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()
1831void 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.
1849void Timeline::onAfterRemoveLayer(DocEvent& ev)
1850{
1851 regenerateRows();
1852 showCurrentCel();
1853 clearClipboardRange();
1854 invalidate();
1855}
1856
1857void 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()
1867void 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
1890void Timeline::onAddCel(DocEvent& ev)
1891{
1892 invalidateLayer(ev.layer());
1893}
1894
1895void Timeline::onAfterRemoveCel(DocEvent& ev)
1896{
1897 invalidateLayer(ev.layer());
1898}
1899
1900void Timeline::onLayerNameChange(DocEvent& ev)
1901{
1902 invalidate();
1903}
1904
1905void Timeline::onAddTag(DocEvent& ev)
1906{
1907 if (m_tagFocusBand >= 0) {
1908 m_tagFocusBand = -1;
1909 regenerateRows();
1910 layout();
1911 }
1912}
1913
1914void Timeline::onRemoveTag(DocEvent& ev)
1915{
1916 onAddTag(ev);
1917}
1918
1919void Timeline::onTagChange(DocEvent& ev)
1920{
1921 invalidateHit(Hit(PART_TAGS));
1922}
1923
1924void Timeline::onTagRename(DocEvent& ev)
1925{
1926 invalidateHit(Hit(PART_TAGS));
1927}
1928
1929void Timeline::onStateChanged(Editor* editor)
1930{
1931 m_aniControls.updateUsingEditor(editor);
1932}
1933
1934void 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
1947void 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
1959void 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
1968void 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
2010void 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
2027void 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
2034void 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
2056void 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
2081void Timeline::drawTop(ui::Graphics* g)
2082{
2083 g->fillRect(skinTheme()->colors.workspace(),
2084 getPartBounds(Hit(PART_TOP)));
2085}
2086
2087void Timeline::drawHeader(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
2133void Timeline::drawHeaderFrame(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
2154void 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
2295void 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
2392void 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
2445void 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
2475void 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
2510void 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
2660void 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
2680void 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
2747void 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
2783gfx::Rect Timeline::getLayerHeadersBounds() 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
2793gfx::Rect Timeline::getFrameHeadersBounds() 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
2803gfx::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
2822gfx::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
2832gfx::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
3013gfx::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
3045gfx::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
3073void 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
3084void 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
3103void 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
3118void 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
3148void 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
3198int Timeline::visibleTagBands() const
3199{
3200 if (m_tagBands > 1 && m_tagFocusBand == -1)
3201 return m_tagBands;
3202 else
3203 return 1;
3204}
3205
3206void 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
3218void 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
3226Timeline::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
3445Timeline::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
3471void 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
3490void 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
3644void 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
3707void 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
3749void Timeline::showCurrentCel()
3750{
3751 layer_t layer = getLayerIndex(m_layer);
3752 if (layer >= firstLayer())
3753 showCel(layer, m_frame);
3754}
3755
3756void Timeline::focusTagBand(int band)
3757{
3758 if (m_tagFocusBand < 0) {
3759 m_tagFocusBand = band;
3760 }
3761 else {
3762 m_tagFocusBand = -1;
3763 }
3764}
3765
3766void Timeline::cleanClk()
3767{
3768 invalidateHit(m_clk);
3769 m_clk = Hit(PART_NOTHING);
3770}
3771
3772gfx::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
3783gfx::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
3797bool Timeline::allLayersVisible()
3798{
3799 for (Layer* topLayer : m_sprite->root()->layers())
3800 if (!topLayer->isVisible())
3801 return false;
3802
3803 return true;
3804}
3805
3806bool Timeline::allLayersInvisible()
3807{
3808 for (Layer* topLayer : m_sprite->root()->layers())
3809 if (topLayer->isVisible())
3810 return false;
3811
3812 return true;
3813}
3814
3815bool Timeline::allLayersLocked()
3816{
3817 for (Layer* topLayer : m_sprite->root()->layers())
3818 if (topLayer->isEditable())
3819 return false;
3820
3821 return true;
3822}
3823
3824bool Timeline::allLayersUnlocked()
3825{
3826 for (Layer* topLayer : m_sprite->root()->layers())
3827 if (!topLayer->isEditable())
3828 return false;
3829
3830 return true;
3831}
3832
3833bool 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
3842bool 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
3851layer_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
3860bool 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
3868bool 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
3876bool 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
3885bool 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
3895void 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
3971gfx::Size Timeline::visibleSize() const
3972{
3973 return getCelsBounds().size();
3974}
3975
3976gfx::Point Timeline::viewScroll() const
3977{
3978 return gfx::Point(m_hbar.getPos(), m_vbar.getPos());
3979}
3980
3981void 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
4013void Timeline::lockRange()
4014{
4015 ++m_rangeLocks;
4016}
4017
4018void Timeline::unlockRange()
4019{
4020 --m_rangeLocks;
4021}
4022
4023void 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
4082void 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
4098void 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
4110void Timeline::clearAndInvalidateRange()
4111{
4112 if (m_range.enabled()) {
4113 notify_observers(&TimelineObserver::onBeforeRangeChanged, this);
4114
4115 invalidateRange();
4116 m_range.clearRange();
4117 }
4118}
4119
4120DocumentPreferences& Timeline::docPref() const
4121{
4122 return Preferences::instance().document(m_document);
4123}
4124
4125skin::SkinTheme* Timeline::skinTheme() const
4126{
4127 return SkinTheme::get(this);
4128}
4129
4130gfx::Size Timeline::celBoxSize() const
4131{
4132 return gfx::Size(frameBoxWidth(), layerBoxHeight());
4133}
4134
4135int Timeline::headerBoxWidth() const
4136{
4137 return skinTheme()->dimensions.timelineBaseSize();
4138}
4139
4140int Timeline::headerBoxHeight() const
4141{
4142 return skinTheme()->dimensions.timelineBaseSize();
4143}
4144
4145int Timeline::layerBoxHeight() const
4146{
4147 return int(zoom()*skinTheme()->dimensions.timelineBaseSize());
4148}
4149
4150int Timeline::frameBoxWidth() const
4151{
4152 return int(zoom()*skinTheme()->dimensions.timelineBaseSize());
4153}
4154
4155int Timeline::outlineWidth() const
4156{
4157 return skinTheme()->dimensions.timelineOutlineWidth();
4158}
4159
4160int Timeline::oneTagHeight() const
4161{
4162 return
4163 font()->height() +
4164 2*ui::guiscale() +
4165 skinTheme()->dimensions.timelineTagsAreaHeight();
4166}
4167
4168double 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.
4178int Timeline::calcTagVisibleToFrame(Tag* tag) const
4179{
4180 return
4181 std::max(tag->toFrame(),
4182 tag->fromFrame() +
4183 font()->textLength(tag->name())/frameBoxWidth());
4184}
4185
4186int 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
4196void 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
4223bool Timeline::onCanCut(Context* ctx)
4224{
4225 return false; // TODO
4226}
4227
4228bool Timeline::onCanCopy(Context* ctx)
4229{
4230 return
4231 m_range.enabled() &&
4232 ctx->checkFlags(ContextFlags::HasActiveDocument);
4233}
4234
4235bool Timeline::onCanPaste(Context* ctx)
4236{
4237 return
4238 (ctx->clipboard()->format() == ClipboardFormat::DocRange &&
4239 ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable));
4240}
4241
4242bool Timeline::onCanClear(Context* ctx)
4243{
4244 return (m_document && m_sprite && m_range.enabled());
4245}
4246
4247bool Timeline::onCut(Context* ctx)
4248{
4249 return false; // TODO
4250}
4251
4252bool 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
4264bool 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
4275bool 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
4307void Timeline::onCancel(Context* ctx)
4308{
4309 if (m_rangeLocks == 0)
4310 clearAndInvalidateRange();
4311
4312 clearClipboardRange();
4313 invalidate();
4314}
4315
4316int 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
4329int 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
4341void 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
4388void 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
4423void 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
4436void 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
4451int Timeline::separatorX() const
4452{
4453 return std::clamp(m_separator_x,
4454 headerBoxWidth(),
4455 std::max(bounds().w-guiscale(),
4456 headerBoxWidth()));
4457}
4458
4459void Timeline::setSeparatorX(int newValue)
4460{
4461 m_separator_x = std::max(0, newValue);
4462}
4463
4464//static
4465gfx::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