mirror of https://github.com/aseprite/aseprite.git
4823 lines
140 KiB
C++
4823 lines
140 KiB
C++
// Aseprite
|
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
|
// Copyright (C) 2001-2018 David Capello
|
|
//
|
|
// This program is distributed under the terms of
|
|
// the End-User License Agreement for Aseprite.
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include "app/ui/timeline/timeline.h"
|
|
|
|
#include "app/app.h"
|
|
#include "app/app_menus.h"
|
|
#include "app/cmd/set_tag_range.h"
|
|
#include "app/cmd_transaction.h"
|
|
#include "app/color_utils.h"
|
|
#include "app/commands/command.h"
|
|
#include "app/commands/commands.h"
|
|
#include "app/commands/params.h"
|
|
#include "app/console.h"
|
|
#include "app/context_access.h"
|
|
#include "app/doc.h"
|
|
#include "app/doc_api.h"
|
|
#include "app/doc_event.h"
|
|
#include "app/doc_range_ops.h"
|
|
#include "app/doc_undo.h"
|
|
#include "app/i18n/strings.h"
|
|
#include "app/inline_command_execution.h"
|
|
#include "app/loop_tag.h"
|
|
#include "app/modules/gfx.h"
|
|
#include "app/modules/gui.h"
|
|
#include "app/thumbnails.h"
|
|
#include "app/transaction.h"
|
|
#include "app/tx.h"
|
|
#include "app/ui/app_menuitem.h"
|
|
#include "app/ui/configure_timeline_popup.h"
|
|
#include "app/ui/doc_view.h"
|
|
#include "app/ui/editor/editor.h"
|
|
#include "app/ui/input_chain.h"
|
|
#include "app/ui/skin/skin_theme.h"
|
|
#include "app/ui/status_bar.h"
|
|
#include "app/ui/timeline/doc_providers.h"
|
|
#include "app/ui/workspace.h"
|
|
#include "app/ui_context.h"
|
|
#include "app/util/clipboard.h"
|
|
#include "app/util/layer_boundaries.h"
|
|
#include "app/util/readable_time.h"
|
|
#include "base/convert_to.h"
|
|
#include "base/memory.h"
|
|
#include "base/scoped_value.h"
|
|
#include "doc/doc.h"
|
|
#include "doc/image_ref.h"
|
|
#include "fmt/format.h"
|
|
#include "gfx/point.h"
|
|
#include "gfx/rect.h"
|
|
#include "os/surface.h"
|
|
#include "os/system.h"
|
|
#include "text/font.h"
|
|
#include "text/font_metrics.h"
|
|
#include "ui/ui.h"
|
|
#include "view/layers.h"
|
|
#include "view/timeline_adapter.h"
|
|
#include "view/utils.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <vector>
|
|
|
|
namespace app {
|
|
|
|
using namespace app::skin;
|
|
using namespace gfx;
|
|
using namespace doc;
|
|
using namespace ui;
|
|
|
|
enum {
|
|
PART_NOTHING = 0,
|
|
PART_TOP,
|
|
PART_SEPARATOR,
|
|
PART_HEADER_EYE,
|
|
PART_HEADER_PADLOCK,
|
|
PART_HEADER_CONTINUOUS,
|
|
PART_HEADER_GEAR,
|
|
PART_HEADER_ONIONSKIN,
|
|
PART_HEADER_ONIONSKIN_RANGE_LEFT,
|
|
PART_HEADER_ONIONSKIN_RANGE_RIGHT,
|
|
PART_HEADER_LAYER,
|
|
PART_HEADER_FRAME,
|
|
PART_ROW,
|
|
PART_ROW_EYE_ICON,
|
|
PART_ROW_PADLOCK_ICON,
|
|
PART_ROW_CONTINUOUS_ICON,
|
|
PART_ROW_TEXT,
|
|
PART_CEL,
|
|
PART_RANGE_OUTLINE,
|
|
PART_TAG,
|
|
PART_TAG_LEFT,
|
|
PART_TAG_RIGHT,
|
|
PART_TAGS,
|
|
PART_TAG_BAND,
|
|
PART_TAG_SWITCH_BUTTONS,
|
|
PART_TAG_SWITCH_BAND_BUTTON,
|
|
};
|
|
|
|
struct Timeline::DrawCelData {
|
|
CelIterator begin;
|
|
CelIterator end;
|
|
CelIterator it;
|
|
CelIterator prevIt; // Previous Cel to "it"
|
|
CelIterator nextIt; // Next Cel to "it"
|
|
CelIterator activeIt; // Active Cel iterator
|
|
CelIterator firstLink; // First link to the active cel
|
|
CelIterator lastLink; // Last link to the active cel
|
|
};
|
|
|
|
namespace {
|
|
|
|
template<typename Pred>
|
|
void for_each_expanded_layer(LayerGroup* group,
|
|
Pred&& pred,
|
|
int level = 0,
|
|
LayerFlags flags = LayerFlags(int(LayerFlags::Visible) |
|
|
int(LayerFlags::Editable)))
|
|
{
|
|
if (!group->isVisible())
|
|
flags = static_cast<LayerFlags>(int(flags) & ~int(LayerFlags::Visible));
|
|
|
|
if (!group->isEditable())
|
|
flags = static_cast<LayerFlags>(int(flags) & ~int(LayerFlags::Editable));
|
|
|
|
for (Layer* child : group->layers()) {
|
|
if (child->isGroup() && !child->isCollapsed())
|
|
for_each_expanded_layer<Pred>(static_cast<LayerGroup*>(child),
|
|
std::forward<Pred>(pred),
|
|
level + 1,
|
|
flags);
|
|
|
|
pred(child, level, flags);
|
|
}
|
|
}
|
|
|
|
bool is_copy_key_pressed(ui::Message* msg)
|
|
{
|
|
return msg->ctrlPressed() || // Ctrl is common on Windows
|
|
msg->altPressed(); // Alt is common on Mac OS X
|
|
}
|
|
|
|
bool is_select_layer_in_canvas_key_pressed(ui::Message* msg)
|
|
{
|
|
#ifdef __APPLE__
|
|
return msg->cmdPressed();
|
|
#else
|
|
return msg->ctrlPressed();
|
|
#endif
|
|
}
|
|
|
|
SelectLayerBoundariesOp get_select_layer_in_canvas_op(ui::Message* msg)
|
|
{
|
|
if (msg->altPressed() && msg->shiftPressed())
|
|
return SelectLayerBoundariesOp::INTERSECT;
|
|
else if (msg->shiftPressed())
|
|
return SelectLayerBoundariesOp::ADD;
|
|
else if (msg->altPressed())
|
|
return SelectLayerBoundariesOp::SUBTRACT;
|
|
else
|
|
return SelectLayerBoundariesOp::REPLACE;
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
Timeline::Hit::Hit(int part, layer_t layer, col_t frame, ObjectId tag, int band)
|
|
: part(part)
|
|
, layer(layer)
|
|
, frame(frame)
|
|
, tag(tag)
|
|
, veryBottom(false)
|
|
, band(band)
|
|
{
|
|
}
|
|
|
|
bool Timeline::Hit::operator!=(const Hit& other) const
|
|
{
|
|
return part != other.part || layer != other.layer || frame != other.frame || tag != other.tag ||
|
|
band != other.band;
|
|
}
|
|
|
|
Tag* Timeline::Hit::getTag() const
|
|
{
|
|
return get<Tag>(tag);
|
|
}
|
|
|
|
Timeline::DropTarget::DropTarget()
|
|
{
|
|
hhit = HNone;
|
|
vhit = VNone;
|
|
outside = false;
|
|
}
|
|
|
|
Timeline::DropTarget::DropTarget(const DropTarget& o)
|
|
: hhit(o.hhit)
|
|
, vhit(o.vhit)
|
|
, outside(o.outside)
|
|
{
|
|
}
|
|
|
|
Timeline::Row::Row() : m_layer(nullptr), m_level(0), m_inheritedFlags(LayerFlags::None)
|
|
{
|
|
}
|
|
|
|
Timeline::Row::Row(Layer* layer, const int level, const LayerFlags inheritedFlags)
|
|
: m_layer(layer)
|
|
, m_level(level)
|
|
, m_inheritedFlags(inheritedFlags)
|
|
{
|
|
}
|
|
|
|
bool Timeline::Row::parentVisible() const
|
|
{
|
|
return ((int(m_inheritedFlags) & int(LayerFlags::Visible)) != 0);
|
|
}
|
|
|
|
bool Timeline::Row::parentEditable() const
|
|
{
|
|
return ((int(m_inheritedFlags) & int(LayerFlags::Editable)) != 0);
|
|
}
|
|
|
|
Timeline::Timeline(TooltipManager* tooltipManager)
|
|
: Widget(kGenericWidget)
|
|
, m_adapter(nullptr)
|
|
, m_hbar(HORIZONTAL, this)
|
|
, m_vbar(VERTICAL, this)
|
|
, m_zoom(1.0)
|
|
, m_scaleUpToFit(false)
|
|
, m_context(UIContext::instance())
|
|
, m_editor(NULL)
|
|
, m_document(NULL)
|
|
, m_sprite(NULL)
|
|
, m_rangeLocks(0)
|
|
, m_state(STATE_STANDBY)
|
|
, m_tagBands(0)
|
|
, m_tagFocusBand(-1)
|
|
, m_separator_x(Preferences::instance().general.timelineLayerPanelWidth())
|
|
, m_separator_w(guiscale())
|
|
, m_confPopup(nullptr)
|
|
, m_clipboard_timer(100, this)
|
|
, m_offset_count(0)
|
|
, m_redrawMarchingAntsOnly(false)
|
|
, m_scroll(false)
|
|
, m_fromTimeline(false)
|
|
, m_aniControls(tooltipManager)
|
|
{
|
|
enableFlags(CTRL_RIGHT_CLICK | ALLOW_DROP);
|
|
|
|
m_ctxConn1 = m_context->BeforeCommandExecution.connect(&Timeline::onBeforeCommandExecution, this);
|
|
m_ctxConn2 = m_context->AfterCommandExecution.connect(&Timeline::onAfterCommandExecution, this);
|
|
m_context->documents().add_observer(this);
|
|
m_context->add_observer(this);
|
|
|
|
setDoubleBuffered(true);
|
|
addChild(&m_aniControls);
|
|
addChild(&m_hbar);
|
|
addChild(&m_vbar);
|
|
|
|
m_hbar.setTransparent(true);
|
|
m_vbar.setTransparent(true);
|
|
initTheme();
|
|
}
|
|
|
|
Timeline::~Timeline()
|
|
{
|
|
// Save unscaled separator
|
|
Preferences::instance().general.timelineLayerPanelWidth(m_separator_x);
|
|
|
|
m_clipboard_timer.stop();
|
|
|
|
detachDocument();
|
|
m_context->documents().remove_observer(this);
|
|
m_context->remove_observer(this);
|
|
m_confPopup.reset();
|
|
}
|
|
|
|
void Timeline::setZoom(const double zoom)
|
|
{
|
|
m_zoom = std::clamp(zoom, 1.0, 10.0);
|
|
m_thumbnailsOverlayDirection = gfx::Point(int(frameBoxWidth() * 1.0), int(frameBoxWidth() * 0.5));
|
|
m_thumbnailsOverlayVisible = false;
|
|
|
|
if (m_document)
|
|
regenerateCols();
|
|
}
|
|
|
|
void Timeline::setZoomAndUpdate(const double zoom, const bool updatePref)
|
|
{
|
|
if (zoom != m_zoom) {
|
|
setZoom(zoom);
|
|
regenerateTagBands();
|
|
updateScrollBars();
|
|
invalidate();
|
|
}
|
|
if (updatePref && zoom != docPref().thumbnails.zoom()) {
|
|
docPref().thumbnails.zoom(zoom);
|
|
docPref().thumbnails.enabled(zoom > 1);
|
|
}
|
|
}
|
|
|
|
void Timeline::onThumbnailsPrefChange()
|
|
{
|
|
if (m_scaleUpToFit != docPref().thumbnails.scaleUpToFit()) {
|
|
m_scaleUpToFit = docPref().thumbnails.scaleUpToFit();
|
|
invalidate();
|
|
}
|
|
setZoomAndUpdate(docPref().thumbnails.enabled() ? docPref().thumbnails.zoom() : 1.0, false);
|
|
}
|
|
|
|
void Timeline::updateUsingEditor(Editor* editor)
|
|
{
|
|
// Selecting the same editor we can go to a fast path.
|
|
if (editor == m_editor) {
|
|
// Deselect range.
|
|
if (!timelinePref().keepSelection())
|
|
resetAllRanges();
|
|
|
|
return;
|
|
}
|
|
|
|
m_aniControls.updateUsingEditor(editor);
|
|
|
|
// Save active m_tagFocusBand into the old focused editor
|
|
if (m_editor)
|
|
m_editor->setTagFocusBand(m_tagFocusBand);
|
|
m_tagFocusBand = -1;
|
|
|
|
detachDocument();
|
|
|
|
// The range is reset in detachDocument()
|
|
ASSERT(!m_range.enabled());
|
|
|
|
// We always update the editor. In this way the timeline keeps in
|
|
// sync with the active editor.
|
|
m_editor = editor;
|
|
if (!m_editor)
|
|
return; // No editor specified.
|
|
|
|
m_editor->add_observer(this);
|
|
m_tagFocusBand = m_editor->tagFocusBand();
|
|
|
|
// Update active doc/sprite/layer/frame
|
|
m_document = m_editor->document();
|
|
m_document->add_observer(this);
|
|
m_sprite = m_editor->sprite();
|
|
m_layer = m_editor->layer();
|
|
|
|
// Re-create the TimelineAdapter.
|
|
updateTimelineAdapter(false);
|
|
m_frame = m_adapter->toColFrame(fr_t(m_editor->frame()));
|
|
|
|
DocumentPreferences& docPref = Preferences::instance().document(m_document);
|
|
m_thumbnailsPrefConn = docPref.thumbnails.AfterChange.connect(
|
|
[this] { onThumbnailsPrefChange(); });
|
|
setZoom(docPref.thumbnails.enabled() ? docPref.thumbnails.zoom() : 1.0);
|
|
m_scaleUpToFit = docPref.thumbnails.scaleUpToFit();
|
|
|
|
m_state = STATE_STANDBY;
|
|
m_hot.part = PART_NOTHING;
|
|
m_clk.part = PART_NOTHING;
|
|
|
|
m_firstFrameConn =
|
|
Preferences::instance().document(m_document).timeline.firstFrame.AfterChange.connect([this] {
|
|
invalidate();
|
|
});
|
|
|
|
m_onionskinConn = docPref.onionskin.AfterChange.connect([this] { invalidate(); });
|
|
|
|
setFocusStop(true);
|
|
regenerateCols();
|
|
regenerateRows();
|
|
|
|
if (DocView* view = m_editor->getDocView())
|
|
setViewScroll(view->timelineScroll());
|
|
|
|
showCurrentCel();
|
|
}
|
|
|
|
void Timeline::detachDocument()
|
|
{
|
|
resetAllRanges();
|
|
|
|
if (m_confPopup && m_confPopup->isVisible())
|
|
m_confPopup->closeWindow(nullptr);
|
|
|
|
m_firstFrameConn.disconnect();
|
|
m_onionskinConn.disconnect();
|
|
|
|
if (m_document) {
|
|
m_thumbnailsPrefConn.disconnect();
|
|
m_document->remove_observer(this);
|
|
m_document = nullptr;
|
|
}
|
|
|
|
// Reset all pointers to this document, we don't want to store a
|
|
// pointer to a layer of a document that we are not observing
|
|
// anymore (because the document might be deleted soon).
|
|
m_sprite = nullptr;
|
|
m_layer = nullptr;
|
|
|
|
if (m_editor) {
|
|
if (DocView* view = m_editor->getDocView())
|
|
view->setTimelineScroll(viewScroll());
|
|
|
|
m_editor->remove_observer(this);
|
|
m_editor = nullptr;
|
|
}
|
|
|
|
m_adapter.reset();
|
|
invalidate();
|
|
}
|
|
|
|
bool Timeline::isMovingCel() const
|
|
{
|
|
return (m_state == STATE_MOVING_RANGE && m_range.type() == Range::kCels);
|
|
}
|
|
|
|
bool Timeline::selectedLayersBounds(const SelectedLayers& layers,
|
|
layer_t* first,
|
|
layer_t* last) const
|
|
{
|
|
if (layers.empty())
|
|
return false;
|
|
|
|
*first = *last = getLayerIndex(*layers.begin());
|
|
|
|
for (auto layer : layers) {
|
|
layer_t i = getLayerIndex(layer);
|
|
if (*first > i)
|
|
*first = i;
|
|
if (*last < i)
|
|
*last = i;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void Timeline::setLayer(Layer* layer)
|
|
{
|
|
ASSERT(m_editor != NULL);
|
|
|
|
invalidateLayer(m_layer);
|
|
invalidateLayer(layer);
|
|
|
|
m_layer = layer;
|
|
|
|
// Expand all parents
|
|
if (m_layer) {
|
|
LayerGroup* group = m_layer->parent();
|
|
while (group != m_layer->sprite()->root()) {
|
|
// Expand this group
|
|
group->setCollapsed(false);
|
|
group = group->parent();
|
|
}
|
|
regenerateRows();
|
|
invalidate();
|
|
}
|
|
|
|
if (m_editor->layer() != layer)
|
|
m_editor->setLayer(m_layer);
|
|
}
|
|
|
|
void Timeline::setFrame(col_t frame, bool byUser)
|
|
{
|
|
ASSERT(m_editor);
|
|
ASSERT(m_adapter);
|
|
|
|
frame = std::clamp(frame, firstFrame(), lastFrame());
|
|
|
|
if (m_layer) {
|
|
Cel* oldCel = m_layer->cel(m_adapter->toRealFrame(m_frame));
|
|
Cel* newCel = m_layer->cel(m_adapter->toRealFrame(frame));
|
|
std::size_t oldLinks = (oldCel ? oldCel->links() : 0);
|
|
std::size_t newLinks = (newCel ? newCel->links() : 0);
|
|
if ((oldLinks && !newCel) || (newLinks && !oldCel) ||
|
|
((oldLinks || newLinks) && (oldCel->data() != newCel->data()))) {
|
|
invalidateLayer(m_layer);
|
|
}
|
|
}
|
|
|
|
invalidateFrame(m_frame);
|
|
invalidateFrame(frame);
|
|
|
|
gfx::Rect onionRc = getOnionskinFramesBounds();
|
|
|
|
m_frame = frame;
|
|
|
|
// Invalidate the onionskin handles area
|
|
onionRc |= getOnionskinFramesBounds();
|
|
if (!onionRc.isEmpty())
|
|
invalidateRect(onionRc.offset(origin()));
|
|
|
|
const frame_t realFrame = m_adapter->toRealFrame(m_frame);
|
|
if (m_editor->frame() != realFrame) {
|
|
const bool isPlaying = m_editor->isPlaying();
|
|
|
|
if (isPlaying)
|
|
m_editor->stop();
|
|
|
|
m_editor->setFrame(realFrame);
|
|
|
|
if (isPlaying)
|
|
m_editor->play(false,
|
|
Preferences::instance().editor.playAll(),
|
|
Preferences::instance().editor.playSubtags());
|
|
}
|
|
}
|
|
|
|
view::RealRange Timeline::realRange() const
|
|
{
|
|
ASSERT(m_adapter != nullptr);
|
|
if (!m_adapter)
|
|
return view::RealRange();
|
|
return view::to_real_range(m_adapter.get(), m_range);
|
|
}
|
|
|
|
void Timeline::prepareToMoveRange()
|
|
{
|
|
ASSERT(m_range.enabled());
|
|
|
|
layer_t i = 0;
|
|
for (auto layer : m_range.selectedLayers().toBrowsableLayerList()) {
|
|
if (layer == m_layer)
|
|
break;
|
|
++i;
|
|
}
|
|
|
|
col_t j = col_t(0);
|
|
for (auto frame : m_range.selectedFrames()) {
|
|
if (frame == m_frame)
|
|
break;
|
|
j = col_t(j + 1);
|
|
}
|
|
|
|
m_moveRangeData.activeRelativeLayer = i;
|
|
m_moveRangeData.activeRelativeFrame = j;
|
|
}
|
|
|
|
void Timeline::moveRange(const VirtualRange& range)
|
|
{
|
|
regenerateCols();
|
|
regenerateRows();
|
|
|
|
// We have to change the range before we generate an
|
|
// onActiveSiteChange() event so observers (like cel properties
|
|
// dialog) know the new selected range.
|
|
m_range = range;
|
|
|
|
layer_t i = 0;
|
|
for (auto layer : range.selectedLayers().toBrowsableLayerList()) {
|
|
if (i == m_moveRangeData.activeRelativeLayer) {
|
|
setLayer(layer);
|
|
break;
|
|
}
|
|
++i;
|
|
}
|
|
|
|
col_t j = col_t(0);
|
|
for (auto frame : range.selectedFrames()) {
|
|
if (j == m_moveRangeData.activeRelativeFrame) {
|
|
setFrame(col_t(frame), true);
|
|
break;
|
|
}
|
|
j = col_t(j + 1);
|
|
}
|
|
|
|
// Select the range again (it might be lost between all the
|
|
// setLayer()/setFrame() calls).
|
|
m_range = range;
|
|
}
|
|
|
|
void Timeline::setVirtualRange(const VirtualRange& range)
|
|
{
|
|
m_range = range;
|
|
invalidate();
|
|
}
|
|
|
|
void Timeline::setRealRange(const RealRange& range)
|
|
{
|
|
m_range = view::to_virtual_range(m_adapter.get(), range);
|
|
invalidate();
|
|
}
|
|
|
|
void Timeline::activateClipboardRange()
|
|
{
|
|
m_clipboard_timer.start();
|
|
invalidate();
|
|
}
|
|
|
|
Tag* Timeline::getTagByFrame(const frame_t frame, const bool getLoopTagIfNone)
|
|
{
|
|
if (!m_sprite)
|
|
return nullptr;
|
|
|
|
if (m_tagFocusBand < 0) {
|
|
Tag* tag = get_animation_tag(m_sprite, frame);
|
|
if (!tag && getLoopTagIfNone)
|
|
tag = get_loop_tag(m_sprite);
|
|
return tag;
|
|
}
|
|
|
|
for (Tag* tag : m_sprite->tags()) {
|
|
if (frame >= tag->fromFrame() && frame <= tag->toFrame() && m_tagBand[tag] == m_tagFocusBand) {
|
|
return tag;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
bool Timeline::onProcessMessage(Message* msg)
|
|
{
|
|
switch (msg->type()) {
|
|
case kFocusEnterMessage: App::instance()->inputChain().prioritize(this, msg); break;
|
|
|
|
case kTimerMessage:
|
|
if (static_cast<TimerMessage*>(msg)->timer() == &m_clipboard_timer) {
|
|
Doc* clipboard_document;
|
|
DocRange clipboard_range;
|
|
Clipboard::instance()->getDocumentRangeInfo(&clipboard_document, &clipboard_range);
|
|
|
|
if (isVisible() && m_document && m_document == clipboard_document) {
|
|
// Set offset to make selection-movement effect
|
|
if (m_offset_count < 7)
|
|
m_offset_count++;
|
|
else
|
|
m_offset_count = 0;
|
|
|
|
bool redrawOnlyMarchingAnts = getUpdateRegion().isEmpty();
|
|
invalidateRect(gfx::Rect(getRangeBounds(clipboard_range)).offset(origin()));
|
|
if (redrawOnlyMarchingAnts)
|
|
m_redrawMarchingAntsOnly = true;
|
|
}
|
|
else if (m_clipboard_timer.isRunning()) {
|
|
m_clipboard_timer.stop();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case kMouseDownMessage: {
|
|
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
|
|
|
|
if (!m_document)
|
|
break;
|
|
|
|
if (mouseMsg->middle() || os::System::instance()->isKeyPressed(kKeySpace)) {
|
|
captureMouse();
|
|
m_state = STATE_SCROLLING;
|
|
m_oldPos = static_cast<MouseMessage*>(msg)->position();
|
|
return true;
|
|
}
|
|
|
|
// As we can ctrl+click color bar + timeline, now we have to
|
|
// re-prioritize timeline on each click.
|
|
App::instance()->inputChain().prioritize(this, msg);
|
|
|
|
// Update hot part (as the user might have left clicked with
|
|
// Ctrl on OS X, which it's converted to a right-click and it's
|
|
// interpreted as other action by the Timeline::hitTest())
|
|
setHot(hitTest(msg, mouseMsg->position() - bounds().origin()));
|
|
|
|
// Clicked-part = hot-part.
|
|
m_clk = m_hot;
|
|
|
|
captureMouse();
|
|
|
|
switch (m_hot.part) {
|
|
case PART_SEPARATOR: m_state = STATE_MOVING_SEPARATOR; break;
|
|
|
|
case PART_HEADER_EYE: {
|
|
ASSERT(m_sprite);
|
|
if (!m_sprite)
|
|
break;
|
|
|
|
bool regenRows = false;
|
|
bool newVisibleState = !allLayersVisible();
|
|
for (Layer* topLayer : m_sprite->root()->layers()) {
|
|
if (topLayer->isVisible() != newVisibleState) {
|
|
m_document->setLayerVisibilityWithNotifications(topLayer, newVisibleState);
|
|
|
|
if (topLayer->isGroup())
|
|
regenRows = true;
|
|
}
|
|
}
|
|
|
|
if (regenRows) {
|
|
regenerateRows();
|
|
invalidate();
|
|
}
|
|
|
|
// Redraw all views.
|
|
m_document->notifyGeneralUpdate();
|
|
break;
|
|
}
|
|
|
|
case PART_HEADER_PADLOCK: {
|
|
ASSERT(m_sprite);
|
|
if (!m_sprite)
|
|
break;
|
|
|
|
bool regenRows = false;
|
|
bool newEditableState = !allLayersUnlocked();
|
|
for (Layer* topLayer : m_sprite->root()->layers()) {
|
|
if (topLayer->isEditable() != newEditableState) {
|
|
topLayer->setEditable(newEditableState);
|
|
if (topLayer->isGroup()) {
|
|
regenRows = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (regenRows) {
|
|
regenerateRows();
|
|
invalidate();
|
|
}
|
|
break;
|
|
}
|
|
|
|
case PART_HEADER_CONTINUOUS: {
|
|
bool newContinuousState = !allLayersContinuous();
|
|
for (size_t i = 0; i < m_rows.size(); i++)
|
|
m_rows[i].layer()->setContinuous(newContinuousState);
|
|
invalidate();
|
|
break;
|
|
}
|
|
|
|
case PART_HEADER_ONIONSKIN: {
|
|
docPref().onionskin.active(!docPref().onionskin.active());
|
|
invalidate();
|
|
break;
|
|
}
|
|
case PART_HEADER_ONIONSKIN_RANGE_LEFT: {
|
|
m_state = STATE_MOVING_ONIONSKIN_RANGE_LEFT;
|
|
m_origFrames = docPref().onionskin.prevFrames();
|
|
break;
|
|
}
|
|
case PART_HEADER_ONIONSKIN_RANGE_RIGHT: {
|
|
m_state = STATE_MOVING_ONIONSKIN_RANGE_RIGHT;
|
|
m_origFrames = docPref().onionskin.nextFrames();
|
|
break;
|
|
}
|
|
case PART_HEADER_FRAME: {
|
|
bool selectFrame = (mouseMsg->left() || !isFrameActive(m_clk.frame));
|
|
|
|
if (selectFrame) {
|
|
m_state = STATE_SELECTING_FRAMES;
|
|
|
|
handleRangeMouseDown(msg, Range::kFrames, m_layer, m_clk.frame);
|
|
|
|
setFrame(m_clk.frame, true);
|
|
}
|
|
break;
|
|
}
|
|
case PART_ROW_TEXT: {
|
|
base::ScopedValue lock(m_fromTimeline, true);
|
|
const layer_t old_layer = getLayerIndex(m_layer);
|
|
const bool selectLayer = (mouseMsg->left() || !isLayerActive(m_clk.layer));
|
|
const bool selectLayerInCanvas = (m_clk.layer != -1 && mouseMsg->left() &&
|
|
is_select_layer_in_canvas_key_pressed(mouseMsg));
|
|
|
|
if (selectLayerInCanvas) {
|
|
select_layer_boundaries(m_rows[m_clk.layer].layer(),
|
|
m_adapter->toRealFrame(m_frame),
|
|
get_select_layer_in_canvas_op(mouseMsg));
|
|
}
|
|
else if (selectLayer) {
|
|
m_state = STATE_SELECTING_LAYERS;
|
|
|
|
handleRangeMouseDown(msg, Range::kLayers, m_rows[m_clk.layer].layer(), m_frame);
|
|
|
|
// Did the user select another layer?
|
|
if (old_layer != m_clk.layer) {
|
|
setLayer(m_rows[m_clk.layer].layer());
|
|
invalidate();
|
|
}
|
|
}
|
|
|
|
// Change the scroll to show the new selected layer/cel.
|
|
showCel(m_clk.layer, m_frame);
|
|
break;
|
|
}
|
|
|
|
case PART_ROW_EYE_ICON:
|
|
if (validLayer(m_clk.layer)) {
|
|
Row& row = m_rows[m_clk.layer];
|
|
Layer* layer = row.layer();
|
|
ASSERT(layer)
|
|
|
|
// Hide everything or restore alternative state
|
|
bool oneWithInternalState = false;
|
|
if (msg->altPressed()) {
|
|
for (const Row& row : m_rows) {
|
|
const Layer* l = row.layer();
|
|
if (l->hasFlags(LayerFlags::Internal_WasVisible)) {
|
|
oneWithInternalState = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If there is one layer with the internal state, restore the previous visible state
|
|
if (oneWithInternalState) {
|
|
for (Row& row : m_rows) {
|
|
Layer* l = row.layer();
|
|
if (l->hasFlags(LayerFlags::Internal_WasVisible)) {
|
|
m_document->setLayerVisibilityWithNotifications(l, true);
|
|
l->switchFlags(LayerFlags::Internal_WasVisible, false);
|
|
}
|
|
else {
|
|
m_document->setLayerVisibilityWithNotifications(l, false);
|
|
}
|
|
}
|
|
}
|
|
// In other case, hide everything
|
|
else {
|
|
for (Row& row : m_rows) {
|
|
Layer* l = row.layer();
|
|
l->switchFlags(LayerFlags::Internal_WasVisible, l->isVisible());
|
|
m_document->setLayerVisibilityWithNotifications(l, false);
|
|
}
|
|
}
|
|
|
|
regenerateRows();
|
|
invalidate();
|
|
|
|
m_document->notifyGeneralUpdate();
|
|
}
|
|
|
|
if (layer->isVisible() && !oneWithInternalState)
|
|
m_state = STATE_HIDING_LAYERS;
|
|
else
|
|
m_state = STATE_SHOWING_LAYERS;
|
|
|
|
setLayerVisibleFlag(m_clk.layer, m_state == STATE_SHOWING_LAYERS);
|
|
}
|
|
break;
|
|
|
|
case PART_ROW_PADLOCK_ICON:
|
|
if (validLayer(m_hot.layer)) {
|
|
Row& row = m_rows[m_clk.layer];
|
|
Layer* layer = row.layer();
|
|
ASSERT(layer);
|
|
if (layer->isEditable())
|
|
m_state = STATE_LOCKING_LAYERS;
|
|
else
|
|
m_state = STATE_UNLOCKING_LAYERS;
|
|
|
|
setLayerEditableFlag(m_clk.layer, m_state == STATE_UNLOCKING_LAYERS);
|
|
}
|
|
break;
|
|
|
|
case PART_ROW_CONTINUOUS_ICON:
|
|
if (validLayer(m_hot.layer)) {
|
|
Row& row = m_rows[m_clk.layer];
|
|
Layer* layer = row.layer();
|
|
ASSERT(layer);
|
|
|
|
if (layer->isImage()) {
|
|
if (layer->isContinuous())
|
|
m_state = STATE_DISABLING_CONTINUOUS_LAYERS;
|
|
else
|
|
m_state = STATE_ENABLING_CONTINUOUS_LAYERS;
|
|
|
|
setLayerContinuousFlag(m_clk.layer, m_state == STATE_ENABLING_CONTINUOUS_LAYERS);
|
|
}
|
|
else if (layer->isGroup()) {
|
|
if (layer->isCollapsed())
|
|
m_state = STATE_EXPANDING_LAYERS;
|
|
else
|
|
m_state = STATE_COLLAPSING_LAYERS;
|
|
|
|
setLayerCollapsedFlag(m_clk.layer, m_state == STATE_COLLAPSING_LAYERS);
|
|
updateByMousePos(msg, mousePosInClientBounds());
|
|
|
|
// The m_clk might have changed because we've
|
|
// expanded/collapsed a group just right now (i.e. we've
|
|
// called regenerateRows())
|
|
m_clk = m_hot;
|
|
|
|
ASSERT(m_rows[m_clk.layer].layer() == layer);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case PART_CEL: {
|
|
base::ScopedValue lock(m_fromTimeline, true);
|
|
const layer_t old_layer = getLayerIndex(m_layer);
|
|
const bool selectCel = (mouseMsg->left() || !isLayerActive(m_clk.layer) ||
|
|
!isFrameActive(m_clk.frame));
|
|
const bool selectCelInCanvas = (m_clk.layer != -1 && mouseMsg->left() &&
|
|
is_select_layer_in_canvas_key_pressed(mouseMsg));
|
|
const col_t old_frame = m_frame;
|
|
|
|
if (selectCelInCanvas) {
|
|
select_layer_boundaries(m_rows[m_clk.layer].layer(),
|
|
m_clk.frame,
|
|
get_select_layer_in_canvas_op(mouseMsg));
|
|
}
|
|
else {
|
|
if (selectCel) {
|
|
m_state = STATE_SELECTING_CELS;
|
|
|
|
handleRangeMouseDown(msg, Range::kCels, m_rows[m_clk.layer].layer(), m_clk.frame);
|
|
}
|
|
|
|
// Select the new clicked-part.
|
|
if (old_layer != m_clk.layer || old_frame != m_clk.frame) {
|
|
setLayer(m_rows[m_clk.layer].layer());
|
|
setFrame(m_clk.frame, true);
|
|
invalidate();
|
|
}
|
|
}
|
|
|
|
// Change the scroll to show the new selected cel.
|
|
showCel(m_clk.layer, m_frame);
|
|
invalidate();
|
|
break;
|
|
}
|
|
case PART_RANGE_OUTLINE:
|
|
m_state = STATE_MOVING_RANGE;
|
|
|
|
// If we select the outline of a cels range, we have to
|
|
// recalculate the dragged cel (m_clk) using a special
|
|
// hitTestCel() and limiting the clicked cel inside the
|
|
// range bounds.
|
|
if (m_range.type() == Range::kCels) {
|
|
m_clk = hitTestCel(mouseMsg->position() - bounds().origin());
|
|
|
|
if (m_range.layers() > 0) {
|
|
layer_t layerFirst, layerLast;
|
|
if (selectedLayersBounds(m_range.selectedLayers(), &layerFirst, &layerLast)) {
|
|
layer_t layerIdx = m_clk.layer;
|
|
layerIdx = std::clamp(layerIdx, layerFirst, layerLast);
|
|
m_clk.layer = layerIdx;
|
|
}
|
|
}
|
|
|
|
if (m_clk.frame < m_range.firstFrame())
|
|
m_clk.frame = col_t(m_range.firstFrame());
|
|
else if (m_clk.frame > m_range.lastFrame())
|
|
m_clk.frame = col_t(m_range.lastFrame());
|
|
}
|
|
break;
|
|
|
|
case PART_TAG:
|
|
m_state = STATE_MOVING_TAG;
|
|
m_resizeTagData.reset(*m_adapter, m_clk.tag);
|
|
break;
|
|
case PART_TAG_LEFT:
|
|
m_state = STATE_RESIZING_TAG_LEFT;
|
|
m_resizeTagData.reset(*m_adapter, m_clk.tag);
|
|
// TODO reduce the scope of the invalidation
|
|
invalidate();
|
|
break;
|
|
case PART_TAG_RIGHT:
|
|
m_state = STATE_RESIZING_TAG_RIGHT;
|
|
m_resizeTagData.reset(*m_adapter, m_clk.tag);
|
|
invalidate();
|
|
break;
|
|
}
|
|
|
|
// Redraw the new clicked part (header, layer or cel).
|
|
invalidateHit(m_clk);
|
|
break;
|
|
}
|
|
|
|
case kMouseLeaveMessage: {
|
|
if (m_hot.part != PART_NOTHING) {
|
|
invalidateHit(m_hot);
|
|
m_hot = Hit();
|
|
}
|
|
break;
|
|
}
|
|
|
|
case kMouseMoveMessage: {
|
|
if (!m_document)
|
|
break;
|
|
|
|
gfx::Point mousePos = static_cast<MouseMessage*>(msg)->position() - bounds().origin();
|
|
|
|
Hit hit;
|
|
setHot(hit = hitTest(msg, mousePos));
|
|
|
|
if (hasCapture()) {
|
|
switch (m_state) {
|
|
case STATE_SCROLLING: {
|
|
gfx::Point absMousePos = static_cast<MouseMessage*>(msg)->position();
|
|
setViewScroll(viewScroll() -
|
|
gfx::Point((absMousePos.x - m_oldPos.x), (absMousePos.y - m_oldPos.y)));
|
|
|
|
m_oldPos = absMousePos;
|
|
return true;
|
|
}
|
|
|
|
case STATE_MOVING_ONIONSKIN_RANGE_LEFT: {
|
|
gfx::Rect onionRc = getOnionskinFramesBounds();
|
|
|
|
int newValue = m_origFrames + (m_clk.frame - hit.frame);
|
|
docPref().onionskin.prevFrames(std::max(0, newValue));
|
|
|
|
onionRc |= getOnionskinFramesBounds();
|
|
invalidateRect(onionRc.offset(origin()));
|
|
return true;
|
|
}
|
|
|
|
case STATE_MOVING_ONIONSKIN_RANGE_RIGHT: {
|
|
gfx::Rect onionRc = getOnionskinFramesBounds();
|
|
|
|
int newValue = m_origFrames - (m_clk.frame - hit.frame);
|
|
docPref().onionskin.nextFrames(std::max(0, newValue));
|
|
|
|
onionRc |= getOnionskinFramesBounds();
|
|
invalidateRect(onionRc.offset(origin()));
|
|
return true;
|
|
}
|
|
|
|
case STATE_SHOWING_LAYERS:
|
|
case STATE_HIDING_LAYERS:
|
|
m_clk = hit;
|
|
if (hit.part == PART_ROW_EYE_ICON) {
|
|
setLayerVisibleFlag(hit.layer, m_state == STATE_SHOWING_LAYERS);
|
|
}
|
|
break;
|
|
|
|
case STATE_LOCKING_LAYERS:
|
|
case STATE_UNLOCKING_LAYERS:
|
|
m_clk = hit;
|
|
if (hit.part == PART_ROW_PADLOCK_ICON) {
|
|
setLayerEditableFlag(hit.layer, m_state == STATE_UNLOCKING_LAYERS);
|
|
}
|
|
break;
|
|
|
|
case STATE_ENABLING_CONTINUOUS_LAYERS:
|
|
case STATE_DISABLING_CONTINUOUS_LAYERS:
|
|
m_clk = hit;
|
|
if (hit.part == PART_ROW_CONTINUOUS_ICON) {
|
|
setLayerContinuousFlag(hit.layer, m_state == STATE_ENABLING_CONTINUOUS_LAYERS);
|
|
}
|
|
break;
|
|
|
|
case STATE_EXPANDING_LAYERS:
|
|
case STATE_COLLAPSING_LAYERS:
|
|
m_clk = hit;
|
|
if (hit.part == PART_ROW_CONTINUOUS_ICON) {
|
|
setLayerCollapsedFlag(hit.layer, m_state == STATE_COLLAPSING_LAYERS);
|
|
updateByMousePos(msg, mousePosInClientBounds());
|
|
}
|
|
break;
|
|
}
|
|
|
|
// If the mouse pressed the mouse's button in the separator,
|
|
// we shouldn't change the hot (so the separator can be
|
|
// tracked to the mouse's released).
|
|
if (m_clk.part == PART_SEPARATOR) {
|
|
setSeparatorX(mousePos.x);
|
|
layout();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
updateDropRange(mousePos);
|
|
|
|
if (hasCapture()) {
|
|
switch (m_state) {
|
|
case STATE_DRAGGING_EXTERNAL_RANGE:
|
|
case STATE_MOVING_RANGE: {
|
|
col_t newFrame;
|
|
if (m_range.type() == Range::kLayers) {
|
|
// If we are moving only layers we don't change the
|
|
// current frame.
|
|
newFrame = m_frame;
|
|
}
|
|
else {
|
|
col_t firstDrawableFrame;
|
|
col_t lastDrawableFrame;
|
|
getDrawableFrames(&firstDrawableFrame, &lastDrawableFrame);
|
|
|
|
if (hit.frame < firstDrawableFrame)
|
|
newFrame = col_t(firstDrawableFrame - 1);
|
|
else if (hit.frame > lastDrawableFrame)
|
|
newFrame = col_t(lastDrawableFrame + 1);
|
|
else
|
|
newFrame = hit.frame;
|
|
}
|
|
|
|
layer_t newLayer = -1;
|
|
if (m_range.type() == Range::kFrames) {
|
|
// If we are moving only frames we don't change the
|
|
// current layer.
|
|
newLayer = getLayerIndex(m_layer);
|
|
}
|
|
else {
|
|
newLayer = hit.layer;
|
|
|
|
layer_t firstDrawableLayer;
|
|
layer_t lastDrawableLayer;
|
|
if (getDrawableLayers(&firstDrawableLayer, &lastDrawableLayer)) {
|
|
if (hit.layer < firstDrawableLayer)
|
|
newLayer = firstDrawableLayer - 1;
|
|
else if (hit.layer > lastDrawableLayer)
|
|
newLayer = lastDrawableLayer + 1;
|
|
}
|
|
}
|
|
|
|
if (newLayer != -1)
|
|
showCel(newLayer, newFrame);
|
|
break;
|
|
}
|
|
|
|
case STATE_SELECTING_LAYERS: {
|
|
Layer* hitLayer = m_rows[hit.layer].layer();
|
|
if (m_layer != hitLayer ||
|
|
// Hit other part? useful to enable the range when
|
|
// selectOnDrag is enabled.
|
|
m_clk.part != hit.part) {
|
|
if (m_layer != hitLayer) {
|
|
m_clk.layer = hit.layer;
|
|
}
|
|
else {
|
|
hitLayer = m_rows[m_clk.layer].layer();
|
|
}
|
|
|
|
// We have to change the range before we generate an
|
|
// onActiveSiteChange() event so observers (like cel
|
|
// properties dialog) know the new selected range.
|
|
handleRangeMouseMove(hitLayer, m_frame);
|
|
setLayer(hitLayer);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case STATE_SELECTING_FRAMES: {
|
|
if (m_clk.frame != hit.frame || m_clk.part != hit.part) {
|
|
if (m_clk.frame != hit.frame)
|
|
m_clk.frame = hit.frame;
|
|
|
|
invalidateRange();
|
|
|
|
handleRangeMouseMove(m_layer, m_clk.frame);
|
|
setFrame(m_clk.frame, true);
|
|
|
|
invalidateRange();
|
|
}
|
|
break;
|
|
}
|
|
|
|
case STATE_SELECTING_CELS: {
|
|
Layer* hitLayer = m_rows[hit.layer].layer();
|
|
if ((m_layer != hitLayer) || (m_frame != hit.frame) || (m_clk.part != hit.part)) {
|
|
if (m_layer != hitLayer)
|
|
m_clk.layer = hit.layer;
|
|
else
|
|
hitLayer = m_rows[m_clk.layer].layer();
|
|
|
|
if (m_clk.frame != hit.frame)
|
|
m_clk.frame = hit.frame;
|
|
|
|
handleRangeMouseMove(hitLayer, m_clk.frame);
|
|
setLayer(hitLayer);
|
|
setFrame(m_clk.frame, true);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case STATE_MOVING_TAG:
|
|
// TODO
|
|
break;
|
|
|
|
case STATE_RESIZING_TAG_LEFT:
|
|
case STATE_RESIZING_TAG_RIGHT: {
|
|
auto tag = doc::get<doc::Tag>(m_resizeTagData.tag);
|
|
if (tag) {
|
|
switch (m_state) {
|
|
case STATE_RESIZING_TAG_LEFT:
|
|
m_resizeTagData.from =
|
|
std::clamp(hit.frame, col_t(0), m_adapter->toColFrame(fr_t(tag->toFrame())));
|
|
break;
|
|
case STATE_RESIZING_TAG_RIGHT:
|
|
m_resizeTagData.to = std::clamp(hit.frame,
|
|
m_adapter->toColFrame(fr_t(tag->fromFrame())),
|
|
lastFrame());
|
|
break;
|
|
}
|
|
invalidate();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
updateStatusBar(msg);
|
|
updateCelOverlayBounds(hit);
|
|
return true;
|
|
}
|
|
|
|
case kMouseUpMessage:
|
|
if (hasCapture()) {
|
|
ASSERT(m_document != NULL);
|
|
|
|
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
|
|
|
|
if (m_state == STATE_SCROLLING) {
|
|
m_state = STATE_STANDBY;
|
|
releaseMouse();
|
|
return true;
|
|
}
|
|
|
|
bool regenRows = false;
|
|
bool relayout = false;
|
|
setHot(hitTest(msg, mouseMsg->position() - bounds().origin()));
|
|
|
|
switch (m_hot.part) {
|
|
case PART_HEADER_GEAR: {
|
|
gfx::Rect gearBounds = getPartBounds(Hit(PART_HEADER_GEAR)).offset(bounds().origin());
|
|
|
|
if (!m_confPopup) {
|
|
m_confPopup.reset(new ConfigureTimelinePopup);
|
|
m_confPopup->remapWindow();
|
|
}
|
|
|
|
if (!m_confPopup->isVisible()) {
|
|
gfx::Rect bounds = m_confPopup->bounds();
|
|
ui::fit_bounds(display(), BOTTOM, gearBounds, bounds);
|
|
ui::fit_bounds(display(), m_confPopup.get(), bounds);
|
|
m_confPopup->openWindow();
|
|
}
|
|
else {
|
|
m_confPopup->closeWindow(nullptr);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case PART_HEADER_FRAME:
|
|
// Show the frame pop-up menu.
|
|
if (mouseMsg->right()) {
|
|
if (m_clk.frame == m_hot.frame) {
|
|
Menu* popupMenu = AppMenus::instance()->getFramePopupMenu();
|
|
if (popupMenu) {
|
|
popupMenu->showPopup(mouseMsg->position(), display());
|
|
|
|
m_state = STATE_STANDBY;
|
|
invalidate();
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case PART_ROW_TEXT:
|
|
// Show the layer pop-up menu.
|
|
if (mouseMsg->right()) {
|
|
if (m_clk.layer == m_hot.layer) {
|
|
Menu* popupMenu = AppMenus::instance()->getLayerPopupMenu();
|
|
if (popupMenu) {
|
|
popupMenu->showPopup(mouseMsg->position(), display());
|
|
|
|
m_state = STATE_STANDBY;
|
|
invalidate();
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case PART_CEL: {
|
|
// Show the cel pop-up menu.
|
|
if (mouseMsg->right()) {
|
|
Menu* popupMenu = (m_state == STATE_MOVING_RANGE && m_range.type() == Range::kCels &&
|
|
(m_hot.layer != m_clk.layer || m_hot.frame != m_clk.frame)) ?
|
|
AppMenus::instance()->getCelMovementPopupMenu() :
|
|
AppMenus::instance()->getCelPopupMenu();
|
|
if (popupMenu) {
|
|
popupMenu->showPopup(mouseMsg->position(), display());
|
|
|
|
// Do not drop in this function, the drop is done from
|
|
// the menu in case we've used the
|
|
// CelMovementPopupMenu
|
|
m_state = STATE_STANDBY;
|
|
invalidate();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case PART_TAG: {
|
|
m_resizeTagData.reset(); // Reset resize info
|
|
|
|
Tag* tag = m_clk.getTag();
|
|
if (tag) {
|
|
Params params;
|
|
params.set("id", base::convert_to<std::string>(tag->id()).c_str());
|
|
|
|
// As the m_clk.tag can be deleted with
|
|
// RemoveTag command, we've to clean all references
|
|
// to it from Hit() structures.
|
|
cleanClk();
|
|
m_hot = m_clk;
|
|
|
|
if (mouseMsg->right()) {
|
|
Menu* popupMenu = AppMenus::instance()->getTagPopupMenu();
|
|
if (popupMenu) {
|
|
AppMenuItem::setContextParams(params);
|
|
popupMenu->showPopup(mouseMsg->position(), display());
|
|
AppMenuItem::setContextParams(Params());
|
|
|
|
m_state = STATE_STANDBY;
|
|
invalidate();
|
|
}
|
|
}
|
|
else if (mouseMsg->left()) {
|
|
Command* command = Commands::instance()->byId(CommandId::FrameTagProperties());
|
|
m_context->executeCommand(command, params);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case PART_TAG_SWITCH_BAND_BUTTON:
|
|
if (m_clk.band >= 0) {
|
|
focusTagBand(m_clk.band);
|
|
regenRows = true;
|
|
relayout = true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (regenRows) {
|
|
regenerateRows();
|
|
invalidate();
|
|
}
|
|
if (relayout)
|
|
layout();
|
|
|
|
switch (m_state) {
|
|
case STATE_MOVING_RANGE:
|
|
if (m_dropRange.type() != Range::kNone) {
|
|
dropRange(is_copy_key_pressed(mouseMsg) ? Timeline::kCopy : Timeline::kMove);
|
|
}
|
|
break;
|
|
|
|
case STATE_MOVING_TAG: m_resizeTagData.reset(); break;
|
|
|
|
case STATE_RESIZING_TAG_LEFT:
|
|
case STATE_RESIZING_TAG_RIGHT: {
|
|
auto tag = doc::get<doc::Tag>(m_resizeTagData.tag);
|
|
if (tag) {
|
|
if ((m_state == STATE_RESIZING_TAG_LEFT &&
|
|
tag->fromFrame() != m_resizeTagData.from) ||
|
|
(m_state == STATE_RESIZING_TAG_RIGHT && tag->toFrame() != m_resizeTagData.to)) {
|
|
try {
|
|
InlineCommandExecution inlineCmd(m_context);
|
|
ContextWriter writer(m_context);
|
|
Tx tx(writer, Strings::commands_FrameTagProperties());
|
|
tx(new cmd::SetTagRange(tag,
|
|
(m_state == STATE_RESIZING_TAG_LEFT ?
|
|
m_adapter->toRealFrame(m_resizeTagData.from) :
|
|
tag->fromFrame()),
|
|
(m_state == STATE_RESIZING_TAG_RIGHT ?
|
|
m_adapter->toRealFrame(m_resizeTagData.to) :
|
|
tag->toFrame())));
|
|
tx.commit();
|
|
}
|
|
catch (const base::Exception& e) {
|
|
Console::showException(e);
|
|
}
|
|
|
|
regenerateRows();
|
|
}
|
|
}
|
|
m_resizeTagData.reset();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Clean the clicked-part & redraw the hot-part.
|
|
cleanClk();
|
|
|
|
if (hasCapture())
|
|
invalidate();
|
|
else
|
|
invalidateHit(m_hot);
|
|
|
|
// Restore the cursor.
|
|
m_state = STATE_STANDBY;
|
|
setCursor(msg, hitTest(msg, mouseMsg->position() - bounds().origin()));
|
|
|
|
releaseMouse();
|
|
updateStatusBar(msg);
|
|
return true;
|
|
}
|
|
break;
|
|
|
|
case kDoubleClickMessage:
|
|
switch (m_hot.part) {
|
|
case PART_ROW_TEXT: {
|
|
Command* command = Commands::instance()->byId(CommandId::LayerProperties());
|
|
|
|
m_context->executeCommand(command);
|
|
return true;
|
|
}
|
|
|
|
case PART_HEADER_FRAME: {
|
|
Command* command = Commands::instance()->byId(CommandId::FrameProperties());
|
|
Params params;
|
|
params.set("frame", "current");
|
|
|
|
m_context->executeCommand(command, params);
|
|
return true;
|
|
}
|
|
|
|
case PART_CEL: {
|
|
Command* command = Commands::instance()->byId(CommandId::CelProperties());
|
|
|
|
m_context->executeCommand(command);
|
|
return true;
|
|
}
|
|
|
|
case PART_TAG_BAND:
|
|
if (m_hot.band >= 0) {
|
|
focusTagBand(m_hot.band);
|
|
regenerateRows();
|
|
invalidate();
|
|
layout();
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case kKeyDownMessage: {
|
|
bool used = false;
|
|
|
|
switch (static_cast<KeyMessage*>(msg)->scancode()) {
|
|
case kKeyEsc:
|
|
if (m_state == STATE_STANDBY) {
|
|
clearAndInvalidateRange();
|
|
}
|
|
else {
|
|
m_state = STATE_STANDBY;
|
|
}
|
|
|
|
// Don't use this key, so it's caught by CancelCommand.
|
|
// TODO The deselection of the current range should be
|
|
// handled in CancelCommand itself.
|
|
// used = true;
|
|
break;
|
|
|
|
case kKeySpace: {
|
|
// If we receive a key down event when the Space bar is
|
|
// pressed (because the Timeline has the keyboard focus) but
|
|
// we don't have the mouse inside, we don't consume this
|
|
// event so the Space bar can be used by the Editor to
|
|
// activate the hand/pan/scroll tool.
|
|
if (!hasMouse())
|
|
break;
|
|
|
|
m_scroll = true;
|
|
used = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
updateByMousePos(msg, mousePosInClientBounds());
|
|
if (used)
|
|
return true;
|
|
|
|
break;
|
|
}
|
|
|
|
case kKeyUpMessage: {
|
|
bool used = false;
|
|
|
|
switch (static_cast<KeyMessage*>(msg)->scancode()) {
|
|
case kKeySpace: {
|
|
m_scroll = false;
|
|
used = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
updateByMousePos(msg, mousePosInClientBounds());
|
|
if (used)
|
|
return true;
|
|
|
|
break;
|
|
}
|
|
|
|
case kMouseWheelMessage:
|
|
if (m_document) {
|
|
gfx::Point delta = static_cast<MouseMessage*>(msg)->wheelDelta();
|
|
const bool precise = static_cast<MouseMessage*>(msg)->preciseWheel();
|
|
|
|
// Zoom timeline
|
|
if (msg->ctrlPressed() || // TODO configurable
|
|
msg->cmdPressed()) {
|
|
double dz = delta.x + delta.y;
|
|
|
|
if (precise) {
|
|
dz /= 1.5;
|
|
if (dz < -1.0)
|
|
dz = -1.0;
|
|
else if (dz > 1.0)
|
|
dz = 1.0;
|
|
}
|
|
|
|
setZoomAndUpdate(m_zoom - dz, true);
|
|
}
|
|
else {
|
|
if (!precise) {
|
|
delta.x *= frameBoxWidth();
|
|
delta.y *= layerBoxHeight();
|
|
|
|
if (delta.x == 0 && // On macOS shift already changes the wheel axis
|
|
msg->shiftPressed()) {
|
|
if (std::fabs(delta.y) > delta.x)
|
|
std::swap(delta.x, delta.y);
|
|
}
|
|
|
|
if (msg->altPressed()) {
|
|
delta.x *= 3;
|
|
delta.y *= 3;
|
|
}
|
|
}
|
|
setViewScroll(viewScroll() + delta);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case kSetCursorMessage:
|
|
if (m_document) {
|
|
setCursor(msg, m_hot);
|
|
return true;
|
|
}
|
|
break;
|
|
|
|
case kTouchMagnifyMessage:
|
|
setZoomAndUpdate(m_zoom + m_zoom * static_cast<ui::TouchMessage*>(msg)->magnification(),
|
|
true);
|
|
break;
|
|
}
|
|
|
|
return Widget::onProcessMessage(msg);
|
|
}
|
|
|
|
void Timeline::handleRangeMouseDown(const ui::Message* msg,
|
|
const Range::Type rangeType,
|
|
doc::Layer* fromLayer,
|
|
const col_t fromFrame)
|
|
{
|
|
// With Ctrl+click (Win/Linux) or Shift+click (OS X) we can
|
|
// select non-adjacents layer/frame ranges
|
|
const bool hasKeyModifier =
|
|
#if !defined(__APPLE__)
|
|
msg->ctrlPressed() ||
|
|
#endif
|
|
msg->shiftPressed();
|
|
|
|
// Clear the range (i.e. "start range from scratch") if the shift
|
|
// key isn't pressed, or if it shouldn't act as a "keep selection"
|
|
// modifier (selectOnClickWithKey = false)
|
|
if (!hasKeyModifier || !timelinePref().selectOnClickWithKey()) {
|
|
clearAndInvalidateRange();
|
|
|
|
// If the Shift key is pressed here, it means that
|
|
// selectOnClickWithKey=false, so Shift+click works as if it
|
|
// doesn't select a range at all.
|
|
if (hasKeyModifier) {
|
|
// Here we clear the start range too and return (so Shift+click
|
|
// acts like a single click without selecting the range).
|
|
m_startRange.clearRange();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Start the range on mouse down/click.
|
|
if ((timelinePref().selectOnClick()) ||
|
|
(timelinePref().selectOnClickWithKey() && hasKeyModifier)) {
|
|
// If Shift key is pressed, and we are just starting the range,
|
|
// add the current location in the range too just in case that
|
|
// we've clicked a layer/frame different from the active one.
|
|
if (hasKeyModifier && !m_range.enabled()) {
|
|
m_range.startRange(m_layer, m_frame, rangeType);
|
|
m_range.endRange(m_layer, m_frame);
|
|
}
|
|
// Start the range with the clicked fromLayer/Frame position.
|
|
m_range.startRange(fromLayer, frame_t(fromFrame), rangeType);
|
|
m_startRange = m_range;
|
|
}
|
|
// If selectOnClick/WithKey are disabled, we start the range on
|
|
// drag, but we've to indicate from where we're starting
|
|
// (m_startRange).
|
|
else if (timelinePref().selectOnDrag()) {
|
|
m_startRange.clearRange();
|
|
m_startRange.startRange(fromLayer, frame_t(fromFrame), rangeType);
|
|
}
|
|
else {
|
|
m_startRange = m_range;
|
|
}
|
|
|
|
invalidateRange();
|
|
}
|
|
|
|
void Timeline::handleRangeMouseMove(doc::Layer* fromLayer, const col_t fromFrame)
|
|
{
|
|
// Indicate the range end if it's already enabled by the mouse down
|
|
// event, or in other case, check the if selectOnDrag=true to enable
|
|
// the range when we move the mouse.
|
|
if (m_range.enabled() || timelinePref().selectOnDrag()) {
|
|
m_range = m_startRange;
|
|
m_range.endRange(fromLayer, fromFrame);
|
|
}
|
|
}
|
|
|
|
void Timeline::onInitTheme(ui::InitThemeEvent& ev)
|
|
{
|
|
Widget::onInitTheme(ev);
|
|
|
|
auto theme = SkinTheme::get(this);
|
|
int barsize = theme->dimensions.miniScrollbarSize();
|
|
m_hbar.setBarWidth(barsize);
|
|
m_vbar.setBarWidth(barsize);
|
|
m_hbar.setStyle(theme->styles.transparentScrollbar());
|
|
m_vbar.setStyle(theme->styles.transparentScrollbar());
|
|
m_hbar.setThumbStyle(theme->styles.transparentScrollbarThumb());
|
|
m_vbar.setThumbStyle(theme->styles.transparentScrollbarThumb());
|
|
|
|
if (m_confPopup)
|
|
m_confPopup->initTheme();
|
|
|
|
m_separator_w = guiscale();
|
|
}
|
|
|
|
void Timeline::onInvalidateRegion(const gfx::Region& region)
|
|
{
|
|
Widget::onInvalidateRegion(region);
|
|
m_redrawMarchingAntsOnly = false;
|
|
}
|
|
|
|
void Timeline::onSizeHint(SizeHintEvent& ev)
|
|
{
|
|
// This doesn't matter, the AniEditor'll use the entire screen anyway.
|
|
ev.setSizeHint(Size(32, 32));
|
|
}
|
|
|
|
void Timeline::onResize(ui::ResizeEvent& ev)
|
|
{
|
|
gfx::Rect rc = ev.bounds();
|
|
setBoundsQuietly(rc);
|
|
|
|
gfx::Size sz = m_aniControls.sizeHint();
|
|
m_aniControls.setBounds(gfx::Rect(
|
|
rc.x,
|
|
rc.y + (visibleTagBands() - 1) * oneTagHeight(),
|
|
(!m_sprite || m_sprite->tags().empty() ? std::min(sz.w, rc.w) : std::min(sz.w, separatorX())),
|
|
oneTagHeight()));
|
|
|
|
updateScrollBars();
|
|
}
|
|
|
|
void Timeline::onPaint(ui::PaintEvent& ev)
|
|
{
|
|
Graphics* g = ev.graphics();
|
|
bool noDoc = (m_document == NULL);
|
|
if (noDoc)
|
|
goto paintNoDoc;
|
|
|
|
try {
|
|
// Lock the sprite to read/render it. Here we don't wait if the
|
|
// document is locked (e.g. a filter is being applied to the
|
|
// sprite) to avoid locking the UI.
|
|
const DocReader docReader(m_document, 0);
|
|
|
|
if (m_redrawMarchingAntsOnly) {
|
|
drawClipboardRange(g);
|
|
m_redrawMarchingAntsOnly = false;
|
|
return;
|
|
}
|
|
|
|
layer_t layer, firstLayer, lastLayer;
|
|
col_t frame, firstFrame, lastFrame;
|
|
|
|
const bool hasLayers = getDrawableLayers(&firstLayer, &lastLayer);
|
|
getDrawableFrames(&firstFrame, &lastFrame);
|
|
|
|
drawTop(g);
|
|
|
|
// Draw the header for layers.
|
|
drawHeader(g);
|
|
|
|
// Draw the header for each visible frame.
|
|
{
|
|
IntersectClip clip(g, getFrameHeadersBounds());
|
|
if (clip) {
|
|
for (frame = firstFrame; frame <= lastFrame; frame = col_t(frame + 1))
|
|
drawHeaderFrame(g, frame);
|
|
|
|
// Draw onionskin indicators.
|
|
gfx::Rect bounds = getOnionskinFramesBounds();
|
|
if (!bounds.isEmpty()) {
|
|
drawPart(g,
|
|
bounds,
|
|
nullptr,
|
|
skinTheme()->styles.timelineOnionskinRange(),
|
|
false,
|
|
false,
|
|
false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw each visible layer.
|
|
if (hasLayers) {
|
|
DrawCelData data;
|
|
const view::fr_t realActiveFrame = m_adapter->toRealFrame(m_frame);
|
|
for (layer = lastLayer; layer >= firstLayer; --layer) {
|
|
{
|
|
IntersectClip clip(g, getLayerHeadersBounds());
|
|
if (clip)
|
|
drawLayer(g, layer);
|
|
}
|
|
|
|
IntersectClip clip(g, getCelsBounds());
|
|
if (!clip)
|
|
continue;
|
|
|
|
Layer* layerPtr = getLayer(layer);
|
|
if (!layerPtr || !layerPtr->isImage()) {
|
|
// Draw empty cels
|
|
for (frame = firstFrame; frame <= lastFrame; frame = col_t(frame + 1)) {
|
|
drawCel(g, layer, frame, nullptr, nullptr);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// TODO Draw the set of cels by "column blocks" (set of
|
|
// consecutive frame columns)
|
|
|
|
// Get the first CelIterator to be drawn (it is the first cel with cel->frame >=
|
|
// first_frame)
|
|
LayerImage* layerImagePtr = static_cast<LayerImage*>(layerPtr);
|
|
data.begin = layerImagePtr->getCelBegin();
|
|
data.end = layerImagePtr->getCelEnd();
|
|
|
|
const frame_t firstRealFrame(m_adapter->toRealFrame(firstFrame));
|
|
const frame_t lastRealFrame(m_adapter->toRealFrame(lastFrame));
|
|
data.it = layerImagePtr->findFirstCelIteratorAfter(firstRealFrame - 1);
|
|
|
|
if (firstRealFrame > 0 && data.it != data.begin)
|
|
data.prevIt = data.it - 1;
|
|
else
|
|
data.prevIt = data.end;
|
|
data.nextIt = (data.it != data.end ? data.it + 1 : data.end);
|
|
|
|
// Calculate link range for the active cel
|
|
data.firstLink = data.end;
|
|
data.lastLink = data.end;
|
|
|
|
if (layerPtr == m_layer) {
|
|
data.activeIt = layerImagePtr->findCelIterator(frame_t(realActiveFrame));
|
|
if (data.activeIt != data.end) {
|
|
data.firstLink = data.activeIt;
|
|
data.lastLink = data.activeIt;
|
|
|
|
ObjectId imageId = (*data.activeIt)->image()->id();
|
|
|
|
auto it2 = data.activeIt;
|
|
if (it2 != data.begin) {
|
|
do {
|
|
--it2;
|
|
if ((*it2)->image()->id() == imageId) {
|
|
data.firstLink = it2;
|
|
if ((*it2)->frame() < firstRealFrame)
|
|
break;
|
|
}
|
|
} while (it2 != data.begin);
|
|
}
|
|
|
|
it2 = data.activeIt;
|
|
while (it2 != data.end) {
|
|
if ((*it2)->image()->id() == imageId) {
|
|
data.lastLink = it2;
|
|
if ((*it2)->frame() > lastRealFrame)
|
|
break;
|
|
}
|
|
++it2;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
data.activeIt = data.end;
|
|
|
|
// Draw every visible cel for each layer.
|
|
for (frame = firstFrame; frame <= lastFrame; frame = col_t(frame + 1)) {
|
|
const view::fr_t realFrame = m_adapter->toRealFrame(frame);
|
|
Cel* cel = (data.it != data.end && (*data.it)->frame() == realFrame ? *data.it : nullptr);
|
|
|
|
drawCel(g, layer, frame, cel, &data);
|
|
|
|
if (cel) {
|
|
data.prevIt = data.it;
|
|
data.it = data.nextIt; // Point to next cel
|
|
if (data.nextIt != data.end)
|
|
++data.nextIt;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
drawPaddings(g);
|
|
drawTags(g);
|
|
drawRangeOutline(g);
|
|
drawClipboardRange(g);
|
|
drawCelOverlay(g);
|
|
|
|
#if 0 // Use this code to debug the calculated m_dropRange by updateDropRange()
|
|
{
|
|
g->drawRect(gfx::rgba(255, 255, 0), getRangeBounds(m_range));
|
|
g->drawRect(gfx::rgba(255, 0, 0), getRangeBounds(m_dropRange));
|
|
}
|
|
#endif
|
|
}
|
|
catch (const LockedDocException&) {
|
|
// The sprite is locked, so we defer the rendering of the sprite
|
|
// for later.
|
|
noDoc = true;
|
|
defer_invalid_rect(g->getClipBounds().offset(bounds().origin()));
|
|
}
|
|
|
|
paintNoDoc:;
|
|
if (noDoc)
|
|
drawPart(g, clientBounds(), nullptr, skinTheme()->styles.timelinePadding());
|
|
}
|
|
|
|
void Timeline::onBeforeCommandExecution(CommandExecutionEvent& ev)
|
|
{
|
|
m_savedVersion = (m_document ? m_document->sprite()->version() : 0);
|
|
}
|
|
|
|
void Timeline::onAfterCommandExecution(CommandExecutionEvent& ev)
|
|
{
|
|
if (!m_document)
|
|
return;
|
|
|
|
// TODO Improve this: check if the structure of layers/frames has changed
|
|
const doc::ObjectVersion currentVersion = m_document->sprite()->version();
|
|
if (m_savedVersion != currentVersion) {
|
|
regenerateCols();
|
|
regenerateRows();
|
|
showCurrentCel();
|
|
invalidate();
|
|
}
|
|
}
|
|
|
|
void Timeline::onActiveSiteChange(const Site& site)
|
|
{
|
|
if (m_adapter && hasMouse()) {
|
|
updateStatusBarForFrame(m_adapter->toColFrame(fr_t(site.frame())), nullptr, site.cel());
|
|
}
|
|
}
|
|
|
|
void Timeline::onRemoveDocument(Doc* document)
|
|
{
|
|
if (document == m_document) {
|
|
detachDocument();
|
|
}
|
|
}
|
|
|
|
void Timeline::onGeneralUpdate(DocEvent& ev)
|
|
{
|
|
invalidate();
|
|
}
|
|
|
|
void Timeline::onAddLayer(DocEvent& ev)
|
|
{
|
|
ASSERT(ev.layer() != NULL);
|
|
|
|
setLayer(ev.layer());
|
|
|
|
regenerateCols();
|
|
regenerateRows();
|
|
showCurrentCel();
|
|
clearClipboardRange();
|
|
invalidate();
|
|
}
|
|
|
|
// TODO similar to ActiveSiteHandler::onBeforeRemoveLayer() and Editor::onBeforeRemoveLayer()
|
|
void Timeline::onBeforeRemoveLayer(DocEvent& ev)
|
|
{
|
|
Layer* layerToSelect = view::candidate_if_layer_is_deleted(m_layer, ev.layer());
|
|
if (m_layer != layerToSelect)
|
|
setLayer(layerToSelect);
|
|
|
|
// Remove layer from ranges
|
|
m_range.eraseAndAdjust(ev.layer());
|
|
m_startRange.eraseAndAdjust(ev.layer());
|
|
m_dropRange.eraseAndAdjust(ev.layer());
|
|
|
|
ASSERT(!m_range.contains(ev.layer()));
|
|
ASSERT(!m_startRange.contains(ev.layer()));
|
|
ASSERT(!m_dropRange.contains(ev.layer()));
|
|
}
|
|
|
|
// We have to regenerate the layer rows (m_rows) after the layer is
|
|
// removed from the sprite.
|
|
void Timeline::onAfterRemoveLayer(DocEvent& ev)
|
|
{
|
|
regenerateCols();
|
|
regenerateRows();
|
|
showCurrentCel();
|
|
clearClipboardRange();
|
|
invalidate();
|
|
}
|
|
|
|
void Timeline::onAddFrame(DocEvent& ev)
|
|
{
|
|
const col_t col = m_adapter->toColFrame(fr_t(ev.frame()));
|
|
setFrame(col, false);
|
|
|
|
regenerateCols();
|
|
showCurrentCel();
|
|
clearClipboardRange();
|
|
invalidate();
|
|
}
|
|
|
|
// TODO similar to ActiveSiteHandler::onRemoveFrame()
|
|
void Timeline::onRemoveFrame(DocEvent& ev)
|
|
{
|
|
const col_t evCol = m_adapter->toColFrame(fr_t(ev.frame()));
|
|
|
|
// In case that the column isn't visible we can just ignore this event
|
|
if (evCol == kNoCol) {
|
|
// TODO Check if we can just ignore this
|
|
}
|
|
// Adjust current frame of all editors that are in a frame more
|
|
// advanced that the removed one.
|
|
else if (m_frame > evCol) {
|
|
setFrame(col_t(m_frame - 1), false);
|
|
}
|
|
// If the editor was in the previous "last frame" (current value of
|
|
// totalFrames()), we've to adjust it to the new last frame
|
|
// (lastFrame())
|
|
else if (m_frame >= m_adapter->totalFrames()) {
|
|
setFrame(lastFrame(), false);
|
|
}
|
|
|
|
// Disable the selected range when we remove frames
|
|
if (m_range.enabled())
|
|
clearAndInvalidateRange();
|
|
|
|
regenerateCols();
|
|
showCurrentCel();
|
|
clearClipboardRange();
|
|
invalidate();
|
|
}
|
|
|
|
void Timeline::onAddCel(DocEvent& ev)
|
|
{
|
|
invalidateLayer(ev.layer());
|
|
}
|
|
|
|
void Timeline::onAfterRemoveCel(DocEvent& ev)
|
|
{
|
|
ui::execute_now_or_enqueue([this, ev] { invalidateLayer(ev.layer()); });
|
|
}
|
|
|
|
void Timeline::onLayerNameChange(DocEvent& ev)
|
|
{
|
|
invalidate();
|
|
}
|
|
|
|
void Timeline::onAddTag(DocEvent& ev)
|
|
{
|
|
if (m_tagFocusBand >= 0) {
|
|
m_tagFocusBand = -1;
|
|
|
|
regenerateTagBands();
|
|
updateScrollBars();
|
|
layout();
|
|
}
|
|
invalidate();
|
|
}
|
|
|
|
void Timeline::onRemoveTag(DocEvent& ev)
|
|
{
|
|
if (m_adapter && m_adapter->isViewingTag(ev.tag())) {
|
|
updateTimelineAdapter(true);
|
|
}
|
|
|
|
onAddTag(ev);
|
|
}
|
|
|
|
void Timeline::onTagChange(DocEvent& ev)
|
|
{
|
|
invalidateHit(Hit(PART_TAGS));
|
|
}
|
|
|
|
void Timeline::onTagRename(DocEvent& ev)
|
|
{
|
|
invalidateHit(Hit(PART_TAGS));
|
|
}
|
|
|
|
void Timeline::onLayerCollapsedChanged(DocEvent& ev)
|
|
{
|
|
regenerateRows();
|
|
invalidate();
|
|
}
|
|
|
|
void Timeline::onAfterLayerVisibilityChange(DocEvent& ev)
|
|
{
|
|
layer_t layerIdx = getLayerIndex(ev.layer());
|
|
if (layerIdx >= 0)
|
|
invalidateRect(getPartBounds(Hit(PART_ROW_EYE_ICON, layerIdx)).offset(origin()));
|
|
if (docPref().onionskin.active())
|
|
m_document->notifyGeneralUpdate();
|
|
}
|
|
|
|
void Timeline::onStateChanged(Editor* editor)
|
|
{
|
|
m_aniControls.updateUsingEditor(editor);
|
|
}
|
|
|
|
void Timeline::onAfterFrameChanged(Editor* editor)
|
|
{
|
|
if (m_fromTimeline)
|
|
return;
|
|
|
|
setFrame(m_adapter->toColFrame(fr_t(editor->frame())), false);
|
|
|
|
if (!hasCapture() && !editor->keepTimelineRange())
|
|
clearAndInvalidateRange();
|
|
|
|
regenerateCols();
|
|
showCurrentCel();
|
|
}
|
|
|
|
void Timeline::onAfterLayerChanged(Editor* editor)
|
|
{
|
|
if (m_fromTimeline)
|
|
return;
|
|
|
|
if (!hasCapture())
|
|
m_range.clearRange();
|
|
|
|
setLayer(editor->layer());
|
|
showCurrentCel();
|
|
}
|
|
|
|
void Timeline::onDestroyEditor(Editor* editor)
|
|
{
|
|
ASSERT(m_editor == editor);
|
|
if (m_editor == editor) {
|
|
m_editor->remove_observer(this);
|
|
m_editor = nullptr;
|
|
}
|
|
}
|
|
|
|
void Timeline::setCursor(ui::Message* msg, const Hit& hit)
|
|
{
|
|
// Scrolling.
|
|
if (m_state == STATE_SCROLLING || m_scroll) {
|
|
ui::set_mouse_cursor(kScrollCursor);
|
|
}
|
|
// Moving.
|
|
else if (m_state == STATE_MOVING_RANGE) {
|
|
if (msg && is_copy_key_pressed(msg))
|
|
ui::set_mouse_cursor(kArrowPlusCursor);
|
|
else
|
|
ui::set_mouse_cursor(kMoveCursor);
|
|
}
|
|
// Dragging elements from external source.
|
|
else if (m_state == STATE_DRAGGING_EXTERNAL_RANGE) {
|
|
ui::set_mouse_cursor(kArrowPlusCursor);
|
|
}
|
|
// Normal state.
|
|
else if (hit.part == PART_HEADER_ONIONSKIN_RANGE_LEFT ||
|
|
m_state == STATE_MOVING_ONIONSKIN_RANGE_LEFT) {
|
|
ui::set_mouse_cursor(kSizeWCursor);
|
|
}
|
|
else if (hit.part == PART_HEADER_ONIONSKIN_RANGE_RIGHT ||
|
|
m_state == STATE_MOVING_ONIONSKIN_RANGE_RIGHT) {
|
|
ui::set_mouse_cursor(kSizeECursor);
|
|
}
|
|
else if (hit.part == PART_RANGE_OUTLINE) {
|
|
if (msg && is_copy_key_pressed(msg))
|
|
ui::set_mouse_cursor(kArrowPlusCursor);
|
|
else
|
|
ui::set_mouse_cursor(kMoveCursor);
|
|
}
|
|
else if (hit.part == PART_SEPARATOR) {
|
|
ui::set_mouse_cursor(kSizeWECursor);
|
|
}
|
|
else if (hit.part == PART_TAG) {
|
|
ui::set_mouse_cursor(kHandCursor);
|
|
}
|
|
else if (hit.part == PART_TAG_RIGHT) {
|
|
ui::set_mouse_cursor(kSizeECursor);
|
|
}
|
|
else if (hit.part == PART_TAG_LEFT) {
|
|
ui::set_mouse_cursor(kSizeWCursor);
|
|
}
|
|
else {
|
|
ui::set_mouse_cursor(kArrowCursor);
|
|
}
|
|
}
|
|
|
|
bool Timeline::getDrawableLayers(layer_t* firstDrawableLayer, layer_t* lastDrawableLayer)
|
|
{
|
|
if (m_rows.empty())
|
|
return false; // No layers
|
|
|
|
layer_t i = lastLayer() - ((viewScroll().y + getCelsBounds().h) / layerBoxHeight());
|
|
i = std::clamp(i, firstLayer(), lastLayer());
|
|
|
|
layer_t j = lastLayer() - viewScroll().y / layerBoxHeight();
|
|
;
|
|
if (!m_rows.empty())
|
|
j = std::clamp(j, firstLayer(), lastLayer());
|
|
else
|
|
j = -1;
|
|
|
|
*firstDrawableLayer = i;
|
|
*lastDrawableLayer = j;
|
|
return true;
|
|
}
|
|
|
|
void Timeline::getDrawableFrames(col_t* firstFrame, col_t* lastFrame)
|
|
{
|
|
const int availW = (clientBounds().w - m_separator_x);
|
|
|
|
*firstFrame = getFrameInXPos(viewScroll().x);
|
|
*lastFrame = getFrameInXPos(viewScroll().x + availW);
|
|
}
|
|
|
|
// Gets the range of columns used by the tag. Returns false if the tag
|
|
// is not visible (or is partially visible) with the current timeline
|
|
// adapter/view.
|
|
bool Timeline::getTagFrames(const doc::Tag* tag, col_t* fromFrame, col_t* toFrame) const
|
|
{
|
|
ASSERT(tag);
|
|
|
|
*fromFrame = m_adapter->toColFrame(fr_t(tag->fromFrame()));
|
|
*toFrame = m_adapter->toColFrame(fr_t(tag->toFrame()));
|
|
|
|
if (m_resizeTagData.tag == tag->id()) {
|
|
*fromFrame = m_resizeTagData.from;
|
|
*toFrame = m_resizeTagData.to;
|
|
}
|
|
|
|
// TODO show partial tags
|
|
return (*fromFrame != kNoCol && *toFrame != kNoCol);
|
|
}
|
|
|
|
void Timeline::drawPart(ui::Graphics* g,
|
|
const gfx::Rect& bounds,
|
|
const std::string* text,
|
|
ui::Style* style,
|
|
const bool is_active,
|
|
const bool is_hover,
|
|
const bool is_clicked,
|
|
const bool is_disabled)
|
|
{
|
|
IntersectClip clip(g, bounds);
|
|
if (!clip)
|
|
return;
|
|
|
|
PaintWidgetPartInfo info;
|
|
info.text = text;
|
|
info.styleFlags = (is_active ? ui::Style::Layer::kFocus : 0) |
|
|
(is_hover ? ui::Style::Layer::kMouse : 0) |
|
|
(is_clicked ? ui::Style::Layer::kSelected : 0) |
|
|
(is_disabled ? ui::Style::Layer::kDisabled : 0);
|
|
|
|
text::FontMetrics metrics;
|
|
font()->metrics(&metrics);
|
|
info.baseline = guiscaled_center(bounds.y, bounds.h, metrics.descent - metrics.ascent) -
|
|
metrics.ascent;
|
|
|
|
theme()->paintWidgetPart(g, style, bounds, info);
|
|
}
|
|
|
|
void Timeline::drawClipboardRange(ui::Graphics* g)
|
|
{
|
|
Doc* clipboard_document;
|
|
DocRange clipboard_range;
|
|
Clipboard::instance()->getDocumentRangeInfo(&clipboard_document, &clipboard_range);
|
|
|
|
if (!m_document || clipboard_document != m_document || !m_clipboard_timer.isRunning())
|
|
return;
|
|
|
|
IntersectClip clip(g, getRangeClipBounds(clipboard_range));
|
|
if (clip) {
|
|
ui::Paint paint;
|
|
paint.style(ui::Paint::Stroke);
|
|
ui::set_checkered_paint_mode(paint,
|
|
m_offset_count,
|
|
gfx::rgba(0, 0, 0, 255),
|
|
gfx::rgba(255, 255, 255, 255));
|
|
g->drawRect(getRangeBounds(clipboard_range), paint);
|
|
}
|
|
}
|
|
|
|
void Timeline::drawTop(ui::Graphics* g)
|
|
{
|
|
g->fillRect(skinTheme()->colors.workspace(), getPartBounds(Hit(PART_TOP)));
|
|
}
|
|
|
|
void Timeline::drawHeader(ui::Graphics* g)
|
|
{
|
|
auto& styles = skinTheme()->styles;
|
|
bool allInvisible = allLayersInvisible();
|
|
bool allLocked = allLayersLocked();
|
|
bool allContinuous = allLayersContinuous();
|
|
|
|
drawPart(g,
|
|
getPartBounds(Hit(PART_HEADER_EYE)),
|
|
nullptr,
|
|
allInvisible ? styles.timelineClosedEye() : styles.timelineOpenEye(),
|
|
m_clk.part == PART_HEADER_EYE,
|
|
m_hot.part == PART_HEADER_EYE,
|
|
m_clk.part == PART_HEADER_EYE);
|
|
|
|
drawPart(g,
|
|
getPartBounds(Hit(PART_HEADER_PADLOCK)),
|
|
nullptr,
|
|
allLocked ? styles.timelineClosedPadlock() : styles.timelineOpenPadlock(),
|
|
m_clk.part == PART_HEADER_PADLOCK,
|
|
m_hot.part == PART_HEADER_PADLOCK,
|
|
m_clk.part == PART_HEADER_PADLOCK);
|
|
|
|
drawPart(g,
|
|
getPartBounds(Hit(PART_HEADER_CONTINUOUS)),
|
|
nullptr,
|
|
allContinuous ? styles.timelineContinuous() : styles.timelineDiscontinuous(),
|
|
m_clk.part == PART_HEADER_CONTINUOUS,
|
|
m_hot.part == PART_HEADER_CONTINUOUS,
|
|
m_clk.part == PART_HEADER_CONTINUOUS);
|
|
|
|
drawPart(g,
|
|
getPartBounds(Hit(PART_HEADER_GEAR)),
|
|
nullptr,
|
|
styles.timelineGear(),
|
|
m_clk.part == PART_HEADER_GEAR,
|
|
m_hot.part == PART_HEADER_GEAR,
|
|
m_clk.part == PART_HEADER_GEAR);
|
|
|
|
drawPart(g,
|
|
getPartBounds(Hit(PART_HEADER_ONIONSKIN)),
|
|
NULL,
|
|
styles.timelineOnionskin(),
|
|
docPref().onionskin.active() || (m_clk.part == PART_HEADER_ONIONSKIN),
|
|
m_hot.part == PART_HEADER_ONIONSKIN,
|
|
m_clk.part == PART_HEADER_ONIONSKIN);
|
|
|
|
// Empty header space.
|
|
drawPart(g,
|
|
getPartBounds(Hit(PART_HEADER_LAYER)),
|
|
NULL,
|
|
styles.timelineBox(),
|
|
false,
|
|
false,
|
|
false);
|
|
}
|
|
|
|
void Timeline::drawHeaderFrame(ui::Graphics* g, col_t col)
|
|
{
|
|
bool is_active = isFrameActive(col);
|
|
bool is_hover = (m_hot.part == PART_HEADER_FRAME && m_hot.frame == col);
|
|
bool is_clicked = (m_clk.part == PART_HEADER_FRAME && m_clk.frame == col);
|
|
gfx::Rect bounds = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), col));
|
|
IntersectClip clip(g, bounds);
|
|
if (!clip)
|
|
return;
|
|
|
|
// Draw the header for the layers.
|
|
const fr_t frame = m_adapter->toRealFrame(col);
|
|
const int n = (docPref().timeline.firstFrame() + frame);
|
|
std::string text = base::convert_to<std::string, int>(n % 100);
|
|
if (n >= 100 && (n % 100) < 10)
|
|
text.insert(0, 1, '0');
|
|
|
|
drawPart(g,
|
|
bounds,
|
|
&text,
|
|
skinTheme()->styles.timelineHeaderFrame(),
|
|
is_active,
|
|
is_hover,
|
|
is_clicked);
|
|
}
|
|
|
|
void Timeline::drawLayer(ui::Graphics* g, const int layerIdx)
|
|
{
|
|
// It can happen when the m_rows is empty (e.g. the sprite doesn't
|
|
// have layers)
|
|
if (layerIdx < 0 || layerIdx >= m_rows.size())
|
|
return;
|
|
|
|
auto& styles = skinTheme()->styles;
|
|
Layer* layer = m_rows[layerIdx].layer();
|
|
bool is_active = isLayerActive(layerIdx);
|
|
bool hotlayer = (m_hot.layer == layerIdx);
|
|
bool clklayer = (m_clk.layer == layerIdx);
|
|
gfx::Rect bounds = getPartBounds(Hit(PART_ROW, layerIdx, firstFrame()));
|
|
IntersectClip clip(g, bounds);
|
|
if (!clip)
|
|
return;
|
|
|
|
// Draw the eye (visible flag).
|
|
bounds = getPartBounds(Hit(PART_ROW_EYE_ICON, layerIdx));
|
|
drawPart(g,
|
|
bounds,
|
|
nullptr,
|
|
(layer->isVisible() ? styles.timelineOpenEye() : styles.timelineClosedEye()),
|
|
is_active || (clklayer && m_clk.part == PART_ROW_EYE_ICON),
|
|
(hotlayer && m_hot.part == PART_ROW_EYE_ICON),
|
|
(clklayer && m_clk.part == PART_ROW_EYE_ICON),
|
|
!m_rows[layerIdx].parentVisible());
|
|
|
|
// Draw the padlock (editable flag).
|
|
bounds = getPartBounds(Hit(PART_ROW_PADLOCK_ICON, layerIdx));
|
|
drawPart(g,
|
|
bounds,
|
|
nullptr,
|
|
(layer->isEditable() ? styles.timelineOpenPadlock() : styles.timelineClosedPadlock()),
|
|
is_active || (clklayer && m_clk.part == PART_ROW_PADLOCK_ICON),
|
|
(hotlayer && m_hot.part == PART_ROW_PADLOCK_ICON),
|
|
(clklayer && m_clk.part == PART_ROW_PADLOCK_ICON),
|
|
!m_rows[layerIdx].parentEditable());
|
|
|
|
// Draw the continuous flag/group icon.
|
|
bounds = getPartBounds(Hit(PART_ROW_CONTINUOUS_ICON, layerIdx));
|
|
if (layer->isImage()) {
|
|
drawPart(g,
|
|
bounds,
|
|
nullptr,
|
|
layer->isContinuous() ? styles.timelineContinuous() : styles.timelineDiscontinuous(),
|
|
is_active || (clklayer && m_clk.part == PART_ROW_CONTINUOUS_ICON),
|
|
(hotlayer && m_hot.part == PART_ROW_CONTINUOUS_ICON),
|
|
(clklayer && m_clk.part == PART_ROW_CONTINUOUS_ICON));
|
|
}
|
|
else if (layer->isGroup()) {
|
|
drawPart(g,
|
|
bounds,
|
|
nullptr,
|
|
layer->isCollapsed() ? styles.timelineClosedGroup() : styles.timelineOpenGroup(),
|
|
is_active || (clklayer && m_clk.part == PART_ROW_CONTINUOUS_ICON),
|
|
(hotlayer && m_hot.part == PART_ROW_CONTINUOUS_ICON),
|
|
(clklayer && m_clk.part == PART_ROW_CONTINUOUS_ICON));
|
|
}
|
|
|
|
// Get the layer's name bounds.
|
|
bounds = getPartBounds(Hit(PART_ROW_TEXT, layerIdx));
|
|
|
|
// Draw layer name.
|
|
doc::color_t layerColor = layer->userData().color();
|
|
gfx::Rect textBounds = bounds;
|
|
if (m_rows[layerIdx].level() > 0) {
|
|
const int frameBoxWithWithoutZoom = skinTheme()->dimensions.timelineBaseSize();
|
|
const int w = m_rows[layerIdx].level() * frameBoxWithWithoutZoom;
|
|
textBounds.x += w;
|
|
textBounds.w -= w;
|
|
}
|
|
|
|
// Layer name background
|
|
drawPart(g,
|
|
bounds,
|
|
nullptr,
|
|
styles.timelineLayer(),
|
|
is_active || (clklayer && m_clk.part == PART_ROW_TEXT),
|
|
(hotlayer && m_hot.part == PART_ROW_TEXT),
|
|
(clklayer && m_clk.part == PART_ROW_TEXT));
|
|
|
|
if (doc::rgba_geta(layerColor) > 0) {
|
|
// Fill with an user-defined custom color.
|
|
auto b2 = textBounds;
|
|
b2.shrink(1 * guiscale()).inflate(1 * guiscale());
|
|
g->fillRect(gfx::rgba(doc::rgba_getr(layerColor),
|
|
doc::rgba_getg(layerColor),
|
|
doc::rgba_getb(layerColor),
|
|
doc::rgba_geta(layerColor)),
|
|
b2);
|
|
}
|
|
|
|
// Tilemap icon
|
|
if (layer->isTilemap()) {
|
|
drawPart(g,
|
|
textBounds,
|
|
nullptr,
|
|
styles.timelineTilemapLayer(),
|
|
is_active || (clklayer && m_clk.part == PART_ROW_TEXT),
|
|
(hotlayer && m_hot.part == PART_ROW_TEXT),
|
|
(clklayer && m_clk.part == PART_ROW_TEXT));
|
|
|
|
gfx::Size sz = skinTheme()->calcSizeHint(this, skinTheme()->styles.timelineTilemapLayer());
|
|
textBounds.x += sz.w;
|
|
textBounds.w -= sz.w;
|
|
}
|
|
|
|
// Layer text
|
|
drawPart(g,
|
|
textBounds,
|
|
&layer->name(),
|
|
styles.timelineLayerTextOnly(),
|
|
is_active,
|
|
(hotlayer && m_hot.part == PART_ROW_TEXT),
|
|
(clklayer && m_clk.part == PART_ROW_TEXT));
|
|
|
|
if (layer->isBackground()) {
|
|
int s = ui::guiscale();
|
|
g->fillRect(is_active ? skinTheme()->colors.timelineClickedText() :
|
|
skinTheme()->colors.timelineNormalText(),
|
|
gfx::Rect(bounds.x + 4 * s,
|
|
bounds.y + bounds.h - 2 * s,
|
|
font()->textLength(layer->name().c_str()),
|
|
s));
|
|
}
|
|
else if (layer->isReference()) {
|
|
int s = ui::guiscale();
|
|
g->fillRect(is_active ? skinTheme()->colors.timelineClickedText() :
|
|
skinTheme()->colors.timelineNormalText(),
|
|
gfx::Rect(bounds.x + 4 * s,
|
|
bounds.y + bounds.h / 2,
|
|
font()->textLength(layer->name().c_str()),
|
|
s));
|
|
}
|
|
|
|
// If this layer wasn't clicked but there are another layer clicked,
|
|
// we have to draw some indicators to show that the user can move
|
|
// layers.
|
|
if (hotlayer && !is_active && m_clk.part == PART_ROW_TEXT) {
|
|
// TODO this should be skinneable
|
|
g->fillRect(skinTheme()->colors.timelineActive(), gfx::Rect(bounds.x, bounds.y, bounds.w, 2));
|
|
}
|
|
}
|
|
|
|
void Timeline::drawCel(ui::Graphics* g,
|
|
const layer_t layerIndex,
|
|
const col_t col,
|
|
const Cel* cel,
|
|
const DrawCelData* data)
|
|
{
|
|
auto& styles = skinTheme()->styles;
|
|
Layer* layer = getLayer(layerIndex);
|
|
Image* image = (cel ? cel->image() : nullptr);
|
|
bool is_hover = (m_hot.part == PART_CEL && m_hot.layer == layerIndex && m_hot.frame == col);
|
|
const bool is_active = isCelActive(layerIndex, col);
|
|
const bool is_loosely_active = isCelLooselyActive(layerIndex, col);
|
|
const bool is_empty = (image == nullptr);
|
|
gfx::Rect bounds = getPartBounds(Hit(PART_CEL, layerIndex, col));
|
|
gfx::Rect full_bounds = bounds;
|
|
IntersectClip clip(g, bounds);
|
|
if (!clip)
|
|
return;
|
|
|
|
const fr_t frame = m_adapter->toRealFrame(col);
|
|
|
|
// Draw background
|
|
if (layer == m_layer && col == m_frame)
|
|
drawPart(g,
|
|
bounds,
|
|
nullptr,
|
|
m_range.enabled() ? styles.timelineFocusedCel() : styles.timelineSelectedCel(),
|
|
false,
|
|
is_hover,
|
|
true);
|
|
else if (m_range.enabled() && is_active)
|
|
drawPart(g, bounds, nullptr, styles.timelineSelectedCel(), false, is_hover, true);
|
|
else
|
|
drawPart(g, bounds, nullptr, styles.timelineBox(), is_loosely_active, is_hover);
|
|
|
|
// Fill with an user-defined custom color.
|
|
if (cel && cel->data()) {
|
|
doc::color_t celColor = cel->data()->userData().color();
|
|
if (doc::rgba_geta(celColor) > 0) {
|
|
auto b2 = bounds;
|
|
b2.shrink(1 * guiscale()).inflate(1 * guiscale());
|
|
g->fillRect(gfx::rgba(doc::rgba_getr(celColor),
|
|
doc::rgba_getg(celColor),
|
|
doc::rgba_getb(celColor),
|
|
doc::rgba_geta(celColor)),
|
|
b2);
|
|
}
|
|
}
|
|
|
|
// Draw keyframe shape
|
|
|
|
ui::Style* style = nullptr;
|
|
bool fromLeft = false;
|
|
bool fromRight = false;
|
|
if (is_empty || !data) {
|
|
style = styles.timelineEmptyFrame();
|
|
}
|
|
else {
|
|
// Calculate which cel is next to this one (in previous and next
|
|
// frame).
|
|
Cel* left = (data->prevIt != data->end ? *data->prevIt : nullptr);
|
|
Cel* right = (data->nextIt != data->end ? *data->nextIt : nullptr);
|
|
if (left && left->frame() != frame - 1)
|
|
left = nullptr;
|
|
if (right && right->frame() != frame + 1)
|
|
right = nullptr;
|
|
|
|
ObjectId leftImg = (left ? left->image()->id() : 0);
|
|
ObjectId rightImg = (right ? right->image()->id() : 0);
|
|
fromLeft = (leftImg == cel->image()->id());
|
|
fromRight = (rightImg == cel->image()->id());
|
|
|
|
if (fromLeft && fromRight)
|
|
style = styles.timelineFromBoth();
|
|
else if (fromLeft)
|
|
style = styles.timelineFromLeft();
|
|
else if (fromRight)
|
|
style = styles.timelineFromRight();
|
|
else
|
|
style = styles.timelineKeyframe();
|
|
}
|
|
|
|
drawPart(g, bounds, nullptr, style, is_loosely_active, is_hover);
|
|
|
|
// Draw thumbnail
|
|
if ((docPref().thumbnails.enabled() && m_zoom > 1) && image) {
|
|
gfx::Rect thumb_bounds = gfx::Rect(bounds).shrink(skinTheme()->calcBorder(this, style));
|
|
|
|
if (!thumb_bounds.isEmpty()) {
|
|
if (os::SurfaceRef surface =
|
|
thumb::get_cel_thumbnail(g->display(), cel, m_scaleUpToFit, thumb_bounds.size())) {
|
|
const int t = std::clamp(thumb_bounds.w / 8, 4, 16);
|
|
draw_checkered_grid(g, thumb_bounds, gfx::Size(t, t), docPref());
|
|
|
|
g->drawRgbaSurface(surface.get(),
|
|
thumb_bounds.center().x - surface->width() / 2,
|
|
thumb_bounds.center().y - surface->height() / 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw decorators to link the activeCel with its links.
|
|
if (data && data->activeIt != data->end)
|
|
drawCelLinkDecorators(g, full_bounds, cel, col, is_loosely_active, is_hover, data);
|
|
|
|
// Draw 'z' if this cel has a custom z-index (non-zero)
|
|
if (cel && cel->zIndex() != 0) {
|
|
drawPart(g, bounds, nullptr, styles.timelineZindex(), is_loosely_active, is_hover);
|
|
}
|
|
}
|
|
|
|
void Timeline::updateCelOverlayBounds(const Hit& hit)
|
|
{
|
|
gfx::Rect rc;
|
|
|
|
if (docPref().thumbnails.overlayEnabled() && hit.part == PART_CEL) {
|
|
m_thumbnailsOverlayHit = hit;
|
|
|
|
int max_size = headerBoxWidth() * docPref().thumbnails.overlaySize();
|
|
int width, height;
|
|
if (m_sprite->width() > m_sprite->height()) {
|
|
width = max_size;
|
|
height = max_size * m_sprite->height() / m_sprite->width();
|
|
}
|
|
else {
|
|
width = max_size * m_sprite->width() / m_sprite->height();
|
|
height = max_size;
|
|
}
|
|
|
|
gfx::Rect client_bounds = clientBounds();
|
|
gfx::Point center = client_bounds.center();
|
|
|
|
gfx::Rect bounds_cel = getPartBounds(m_thumbnailsOverlayHit);
|
|
rc = gfx::Rect(bounds_cel.x + m_thumbnailsOverlayDirection.x,
|
|
bounds_cel.y + m_thumbnailsOverlayDirection.y,
|
|
width,
|
|
height);
|
|
|
|
if (!client_bounds.contains(rc)) {
|
|
m_thumbnailsOverlayDirection = gfx::Point(
|
|
bounds_cel.x < center.x ? (int)(frameBoxWidth() * 1.0) : -width,
|
|
bounds_cel.y < center.y ? (int)(frameBoxWidth() * 0.5) :
|
|
-height + (int)(frameBoxWidth() * 0.5));
|
|
rc.setOrigin(gfx::Point(bounds_cel.x + m_thumbnailsOverlayDirection.x,
|
|
bounds_cel.y + m_thumbnailsOverlayDirection.y));
|
|
}
|
|
}
|
|
else {
|
|
rc = gfx::Rect(0, 0, 0, 0);
|
|
}
|
|
|
|
if (rc == m_thumbnailsOverlayBounds)
|
|
return;
|
|
|
|
if (!m_thumbnailsOverlayBounds.isEmpty())
|
|
invalidateRect(gfx::Rect(m_thumbnailsOverlayBounds).offset(origin()));
|
|
if (!rc.isEmpty())
|
|
invalidateRect(gfx::Rect(rc).offset(origin()));
|
|
|
|
m_thumbnailsOverlayVisible = !rc.isEmpty();
|
|
m_thumbnailsOverlayBounds = rc;
|
|
}
|
|
|
|
void Timeline::drawCelOverlay(ui::Graphics* g)
|
|
{
|
|
if (!m_thumbnailsOverlayVisible)
|
|
return;
|
|
|
|
Layer* layer = m_rows[m_thumbnailsOverlayHit.layer].layer();
|
|
Cel* cel = layer->cel(m_thumbnailsOverlayHit.frame);
|
|
if (!cel)
|
|
return;
|
|
|
|
Image* image = cel->image();
|
|
if (!image)
|
|
return;
|
|
|
|
IntersectClip clip(g, m_thumbnailsOverlayBounds);
|
|
if (!clip)
|
|
return;
|
|
|
|
gfx::Rect rc = m_sprite->bounds().fitIn(gfx::Rect(m_thumbnailsOverlayBounds).shrink(1));
|
|
if (os::SurfaceRef surface =
|
|
thumb::get_cel_thumbnail(g->display(), cel, m_scaleUpToFit, rc.size())) {
|
|
draw_checkered_grid(g, rc, gfx::Size(8, 8) * ui::guiscale(), docPref());
|
|
|
|
g->drawRgbaSurface(surface.get(),
|
|
rc.center().x - surface->width() / 2,
|
|
rc.center().y - surface->height() / 2);
|
|
g->drawRect(gfx::rgba(0, 0, 0, 128), m_thumbnailsOverlayBounds);
|
|
}
|
|
}
|
|
|
|
void Timeline::drawCelLinkDecorators(ui::Graphics* g,
|
|
const gfx::Rect& bounds,
|
|
const Cel* cel,
|
|
const col_t col,
|
|
const bool is_active,
|
|
const bool is_hover,
|
|
const DrawCelData* data)
|
|
{
|
|
auto& styles = skinTheme()->styles;
|
|
ObjectId imageId = (*data->activeIt)->image()->id();
|
|
|
|
ui::Style* style1 = nullptr;
|
|
ui::Style* style2 = nullptr;
|
|
|
|
// Links at the left or right side
|
|
fr_t frame = m_adapter->toRealFrame(col);
|
|
bool left = (data->firstLink != data->end ? frame > (*data->firstLink)->frame() : false);
|
|
bool right = (data->lastLink != data->end ? frame < (*data->lastLink)->frame() : false);
|
|
|
|
if (cel && cel->image()->id() == imageId) {
|
|
if (left) {
|
|
Cel* prevCel = m_layer->cel(cel->frame() - 1);
|
|
if (!prevCel || prevCel->image()->id() != imageId)
|
|
style1 = styles.timelineLeftLink();
|
|
}
|
|
if (right) {
|
|
Cel* nextCel = m_layer->cel(cel->frame() + 1);
|
|
if (!nextCel || nextCel->image()->id() != imageId)
|
|
style2 = styles.timelineRightLink();
|
|
}
|
|
}
|
|
else {
|
|
if (left && right)
|
|
style1 = styles.timelineBothLinks();
|
|
}
|
|
|
|
if (style1)
|
|
drawPart(g, bounds, nullptr, style1, is_active, is_hover);
|
|
if (style2)
|
|
drawPart(g, bounds, nullptr, style2, is_active, is_hover);
|
|
}
|
|
|
|
void Timeline::drawTags(ui::Graphics* g)
|
|
{
|
|
IntersectClip clip(g, getPartBounds(Hit(PART_TAGS)));
|
|
if (!clip)
|
|
return;
|
|
|
|
SkinTheme* theme = skinTheme();
|
|
auto& styles = theme->styles;
|
|
|
|
g->fillRect(theme->colors.workspace(),
|
|
gfx::Rect(0,
|
|
font()->lineHeight(),
|
|
clientBounds().w,
|
|
theme->dimensions.timelineTagsAreaHeight()));
|
|
|
|
// Draw active frame tag band
|
|
if (m_hot.band >= 0 && m_tagBands > 1 && m_tagFocusBand < 0) {
|
|
gfx::Rect bandBounds = getPartBounds(Hit(PART_TAG_BAND, -1, kNoCol, doc::NullId, m_hot.band));
|
|
g->fillRect(theme->colors.timelineBandHighlight(), bandBounds);
|
|
}
|
|
|
|
int passes = (m_tagFocusBand >= 0 ? 2 : 1);
|
|
for (int pass = 0; pass < passes; ++pass) {
|
|
for (Tag* tag : m_sprite->tags()) {
|
|
int band = -1;
|
|
if (m_tagFocusBand >= 0) {
|
|
auto it = m_tagBand.find(tag);
|
|
if (it != m_tagBand.end()) {
|
|
band = it->second;
|
|
if ((pass == 0 && band == m_tagFocusBand) || (pass == 1 && band != m_tagFocusBand))
|
|
continue;
|
|
}
|
|
}
|
|
|
|
col_t fromFrame, toFrame;
|
|
if (!getTagFrames(tag, &fromFrame, &toFrame))
|
|
continue;
|
|
|
|
gfx::Rect bounds1 = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), fromFrame));
|
|
gfx::Rect bounds2 = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), toFrame));
|
|
gfx::Rect bounds = bounds1.createUnion(bounds2);
|
|
gfx::Rect tagBounds = getPartBounds(Hit(PART_TAG, 0, kNoCol, tag->id()));
|
|
bounds.h = bounds.y2() - tagBounds.y2();
|
|
bounds.y = tagBounds.y2();
|
|
|
|
int dx = 0, dw = 0;
|
|
if (m_dropTarget.outside && m_dropTarget.hhit != DropTarget::HNone &&
|
|
m_dropRange.type() == DocRange::kFrames) {
|
|
switch (m_dropTarget.hhit) {
|
|
case DropTarget::Before:
|
|
if (m_dropRange.firstFrame() == fromFrame) {
|
|
dx = +frameBoxWidth() / 4;
|
|
dw = -frameBoxWidth() / 4;
|
|
}
|
|
else if (m_dropRange.firstFrame() - 1 == toFrame) {
|
|
dw = -frameBoxWidth() / 4;
|
|
}
|
|
break;
|
|
case DropTarget::After:
|
|
if (m_dropRange.lastFrame() == toFrame) {
|
|
dw = -frameBoxWidth() / 4;
|
|
}
|
|
else if (m_dropRange.lastFrame() + 1 == fromFrame) {
|
|
dx = +frameBoxWidth() / 4;
|
|
dw = -frameBoxWidth() / 4;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
bounds.x += dx;
|
|
bounds.w += dw;
|
|
tagBounds.x += dx;
|
|
|
|
const gfx::Color tagColor = (m_tagFocusBand < 0 || pass == 1) ?
|
|
tag->color() :
|
|
theme->colors.timelineBandBg();
|
|
gfx::Color bg = tagColor;
|
|
|
|
// Draw the tag braces
|
|
drawTagBraces(g, bg, bounds, bounds);
|
|
if ((m_clk.part == PART_TAG_LEFT && m_clk.tag == tag->id()) ||
|
|
(m_clk.part != PART_TAG_LEFT && m_hot.part == PART_TAG_LEFT && m_hot.tag == tag->id())) {
|
|
if (m_clk.part == PART_TAG_LEFT)
|
|
bg = color_utils::blackandwhite_neg(tagColor);
|
|
else
|
|
bg = Timeline::highlightColor(tagColor);
|
|
drawTagBraces(g, bg, bounds, gfx::Rect(bounds.x, bounds.y, frameBoxWidth() / 2, bounds.h));
|
|
}
|
|
else if ((m_clk.part == PART_TAG_RIGHT && m_clk.tag == tag->id()) ||
|
|
(m_clk.part != PART_TAG_RIGHT && m_hot.part == PART_TAG_RIGHT &&
|
|
m_hot.tag == tag->id())) {
|
|
if (m_clk.part == PART_TAG_RIGHT)
|
|
bg = color_utils::blackandwhite_neg(tagColor);
|
|
else
|
|
bg = Timeline::highlightColor(tagColor);
|
|
drawTagBraces(
|
|
g,
|
|
bg,
|
|
bounds,
|
|
gfx::Rect(bounds.x2() - frameBoxWidth() / 2, bounds.y, frameBoxWidth() / 2, bounds.h));
|
|
}
|
|
|
|
// Draw tag text
|
|
if (m_tagFocusBand < 0 || pass == 1) {
|
|
bounds = tagBounds;
|
|
|
|
if (m_clk.part == PART_TAG && m_clk.tag == tag->id())
|
|
bg = color_utils::blackandwhite_neg(tagColor);
|
|
else if (m_hot.part == PART_TAG && m_hot.tag == tag->id())
|
|
bg = Timeline::highlightColor(tagColor);
|
|
else
|
|
bg = tagColor;
|
|
g->fillRect(bg, bounds);
|
|
|
|
bounds.y += 2 * ui::guiscale();
|
|
bounds.x += 2 * ui::guiscale();
|
|
g->drawText(tag->name(),
|
|
color_utils::blackandwhite_neg(bg),
|
|
gfx::ColorNone,
|
|
bounds.origin());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw button to expand/collapse the active band
|
|
if (m_hot.band >= 0 && m_tagBands > 1) {
|
|
gfx::Rect butBounds = getPartBounds(
|
|
Hit(PART_TAG_SWITCH_BAND_BUTTON, -1, kNoCol, doc::NullId, m_hot.band));
|
|
PaintWidgetPartInfo info;
|
|
if (m_hot.part == PART_TAG_SWITCH_BAND_BUTTON) {
|
|
info.styleFlags |= ui::Style::Layer::kMouse;
|
|
if (hasCapture())
|
|
info.styleFlags |= ui::Style::Layer::kSelected;
|
|
}
|
|
theme->paintWidgetPart(g, styles.timelineSwitchBandButton(), butBounds, info);
|
|
}
|
|
}
|
|
|
|
void Timeline::drawTagBraces(ui::Graphics* g,
|
|
gfx::Color tagColor,
|
|
const gfx::Rect& bounds,
|
|
const gfx::Rect& clipBounds)
|
|
{
|
|
IntersectClip clip(g, clipBounds);
|
|
if (clip) {
|
|
SkinTheme* theme = skinTheme();
|
|
auto& styles = theme->styles;
|
|
for (auto& layer : styles.timelineLoopRange()->layers()) {
|
|
if (layer.type() == Style::Layer::Type::kBackground ||
|
|
layer.type() == Style::Layer::Type::kBackgroundBorder ||
|
|
layer.type() == Style::Layer::Type::kBorder) {
|
|
const_cast<Style::Layer*>(&layer)->setColor(tagColor);
|
|
}
|
|
}
|
|
drawPart(g, bounds, nullptr, styles.timelineLoopRange());
|
|
}
|
|
}
|
|
|
|
void Timeline::paintDropFrameDeco(ui::Graphics* g, PaintWidgetPartInfo info, gfx::Rect dropBounds)
|
|
{
|
|
auto& styles = skinTheme()->styles;
|
|
int w = 5 * guiscale(); // TODO get width from the skin info
|
|
|
|
if (m_dropTarget.hhit == DropTarget::Before)
|
|
dropBounds.x -= w / 2;
|
|
else if (m_dropRange == m_range)
|
|
dropBounds.x = dropBounds.x + getRangeBounds(m_range).w - w / 2;
|
|
else
|
|
dropBounds.x = dropBounds.x + dropBounds.w - w / 2;
|
|
|
|
dropBounds.w = w;
|
|
|
|
info.styleFlags = 0;
|
|
theme()->paintWidgetPart(g, styles.timelineDropFrameDeco(), dropBounds, info);
|
|
}
|
|
|
|
void Timeline::paintDropLayerDeco(ui::Graphics* g,
|
|
const PaintWidgetPartInfo& info,
|
|
gfx::Rect dropBounds)
|
|
{
|
|
auto& styles = skinTheme()->styles;
|
|
int h = 5 * guiscale(); // TODO get height from the skin info
|
|
|
|
if (m_dropTarget.vhit == DropTarget::Top)
|
|
dropBounds.y -= h / 2;
|
|
else if (m_dropRange == m_range)
|
|
dropBounds.y = dropBounds.y + getRangeBounds(m_range).h - h / 2;
|
|
else
|
|
dropBounds.y = dropBounds.y + dropBounds.h - h / 2;
|
|
|
|
dropBounds.h = h;
|
|
|
|
theme()->paintWidgetPart(g, styles.timelineDropLayerDeco(), dropBounds, info);
|
|
}
|
|
|
|
void Timeline::drawRangeOutline(ui::Graphics* g)
|
|
{
|
|
auto& styles = skinTheme()->styles;
|
|
|
|
IntersectClip clip(g, getRangeClipBounds(m_range).enlarge(outlineWidth()));
|
|
if (!clip)
|
|
return;
|
|
|
|
PaintWidgetPartInfo info;
|
|
info.styleFlags = (m_range.enabled() ? ui::Style::Layer::kFocus : 0) |
|
|
(m_hot.part == PART_RANGE_OUTLINE ? ui::Style::Layer::kMouse : 0);
|
|
|
|
gfx::Rect bounds = getPartBounds(Hit(PART_RANGE_OUTLINE));
|
|
theme()->paintWidgetPart(g, styles.timelineRangeOutline(), bounds, info);
|
|
|
|
gfx::Rect dropBounds = getRangeBounds(m_dropRange);
|
|
|
|
switch (m_dropRange.type()) {
|
|
case Range::kCels: {
|
|
gfx::Rect outlineBounds(dropBounds);
|
|
outlineBounds.enlarge(outlineWidth());
|
|
info.styleFlags = ui::Style::Layer::kFocus;
|
|
theme()->paintWidgetPart(g, styles.timelineRangeOutline(), outlineBounds, info);
|
|
|
|
if (m_state == STATE_DRAGGING_EXTERNAL_RANGE)
|
|
paintDropLayerDeco(g, info, getSelectedLayersBounds(m_dropRange));
|
|
|
|
break;
|
|
}
|
|
|
|
case Range::kFrames: {
|
|
paintDropFrameDeco(g, info, dropBounds);
|
|
break;
|
|
}
|
|
|
|
case Range::kLayers: {
|
|
paintDropLayerDeco(g, info, dropBounds);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Timeline::drawPaddings(ui::Graphics* g)
|
|
{
|
|
auto& styles = skinTheme()->styles;
|
|
|
|
gfx::Rect client = clientBounds();
|
|
gfx::Rect bottomLayer;
|
|
gfx::Rect lastFrame;
|
|
int top = topHeight();
|
|
|
|
if (!m_rows.empty()) {
|
|
bottomLayer = getPartBounds(Hit(PART_ROW, firstLayer()));
|
|
lastFrame = getPartBounds(Hit(PART_CEL, firstLayer(), this->lastFrame()));
|
|
}
|
|
else {
|
|
bottomLayer = getPartBounds(Hit(PART_HEADER_LAYER));
|
|
lastFrame = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), this->lastFrame()));
|
|
}
|
|
|
|
drawPart(g,
|
|
gfx::Rect(lastFrame.x + lastFrame.w,
|
|
client.y + top,
|
|
client.w - (lastFrame.x + lastFrame.w),
|
|
bottomLayer.y + bottomLayer.h),
|
|
NULL,
|
|
styles.timelinePaddingTr());
|
|
|
|
drawPart(g,
|
|
gfx::Rect(client.x,
|
|
bottomLayer.y + bottomLayer.h,
|
|
lastFrame.x + lastFrame.w - client.x,
|
|
client.h - (bottomLayer.y + bottomLayer.h)),
|
|
NULL,
|
|
styles.timelinePaddingBl());
|
|
|
|
drawPart(g,
|
|
gfx::Rect(lastFrame.x + lastFrame.w,
|
|
bottomLayer.y + bottomLayer.h,
|
|
client.w - (lastFrame.x + lastFrame.w),
|
|
client.h - (bottomLayer.y + bottomLayer.h)),
|
|
NULL,
|
|
styles.timelinePaddingBr());
|
|
}
|
|
|
|
gfx::Rect Timeline::getLayerHeadersBounds() const
|
|
{
|
|
gfx::Rect rc = clientBounds();
|
|
rc.w = separatorX();
|
|
int h = topHeight() + headerBoxHeight();
|
|
rc.y += h;
|
|
rc.h -= h;
|
|
return rc;
|
|
}
|
|
|
|
gfx::Rect Timeline::getFrameHeadersBounds() const
|
|
{
|
|
gfx::Rect rc = clientBounds();
|
|
rc.x += separatorX();
|
|
rc.y += topHeight();
|
|
rc.w -= separatorX();
|
|
rc.h = headerBoxHeight();
|
|
return rc;
|
|
}
|
|
|
|
gfx::Rect Timeline::getOnionskinFramesBounds() const
|
|
{
|
|
DocumentPreferences& docPref = this->docPref();
|
|
if (!docPref.onionskin.active())
|
|
return gfx::Rect();
|
|
|
|
col_t firstFrame = col_t(m_frame - docPref.onionskin.prevFrames());
|
|
col_t lastFrame = col_t(m_frame + docPref.onionskin.nextFrames());
|
|
|
|
if (firstFrame < this->firstFrame())
|
|
firstFrame = this->firstFrame();
|
|
|
|
if (lastFrame > this->lastFrame())
|
|
lastFrame = this->lastFrame();
|
|
|
|
return getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), firstFrame))
|
|
.createUnion(getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), lastFrame)));
|
|
}
|
|
|
|
gfx::Rect Timeline::getCelsBounds() const
|
|
{
|
|
gfx::Rect rc = clientBounds();
|
|
rc.x += separatorX();
|
|
rc.w -= separatorX();
|
|
rc.y += headerBoxHeight() + topHeight();
|
|
rc.h -= headerBoxHeight() + topHeight();
|
|
return rc;
|
|
}
|
|
|
|
gfx::Rect Timeline::getPartBounds(const Hit& hit) const
|
|
{
|
|
gfx::Rect bounds = clientBounds();
|
|
int y = topHeight();
|
|
|
|
switch (hit.part) {
|
|
case PART_NOTHING: break;
|
|
|
|
case PART_TOP: return gfx::Rect(bounds.x, bounds.y, bounds.w, y);
|
|
|
|
case PART_SEPARATOR:
|
|
return gfx::Rect(bounds.x + separatorX(),
|
|
bounds.y + y,
|
|
separatorX() + m_separator_w,
|
|
bounds.h - y);
|
|
|
|
case PART_HEADER_EYE:
|
|
return gfx::Rect(bounds.x + headerBoxWidth() * 0,
|
|
bounds.y + y,
|
|
headerBoxWidth(),
|
|
headerBoxHeight());
|
|
|
|
case PART_HEADER_PADLOCK:
|
|
return gfx::Rect(bounds.x + headerBoxWidth() * 1,
|
|
bounds.y + y,
|
|
headerBoxWidth(),
|
|
headerBoxHeight());
|
|
|
|
case PART_HEADER_CONTINUOUS:
|
|
return gfx::Rect(bounds.x + headerBoxWidth() * 2,
|
|
bounds.y + y,
|
|
headerBoxWidth(),
|
|
headerBoxHeight());
|
|
|
|
case PART_HEADER_GEAR:
|
|
return gfx::Rect(bounds.x + headerBoxWidth() * 3,
|
|
bounds.y + y,
|
|
headerBoxWidth(),
|
|
headerBoxHeight());
|
|
|
|
case PART_HEADER_ONIONSKIN:
|
|
return gfx::Rect(bounds.x + headerBoxWidth() * 4,
|
|
bounds.y + y,
|
|
headerBoxWidth(),
|
|
headerBoxHeight());
|
|
|
|
case PART_HEADER_LAYER:
|
|
return gfx::Rect(bounds.x + headerBoxWidth() * 5,
|
|
bounds.y + y,
|
|
separatorX() - headerBoxWidth() * 5,
|
|
headerBoxHeight());
|
|
|
|
case PART_HEADER_FRAME: {
|
|
col_t frame = std::max(firstFrame(), hit.frame);
|
|
return gfx::Rect(
|
|
bounds.x + separatorX() + m_separator_w - guiscale() + getFrameXPos(frame) - viewScroll().x,
|
|
bounds.y + y,
|
|
getFrameWidth(frame),
|
|
headerBoxHeight());
|
|
}
|
|
|
|
case PART_ROW:
|
|
if (validLayer(hit.layer)) {
|
|
return gfx::Rect(bounds.x,
|
|
bounds.y + y + headerBoxHeight() +
|
|
layerBoxHeight() * (lastLayer() - hit.layer) - viewScroll().y,
|
|
separatorX(),
|
|
layerBoxHeight());
|
|
}
|
|
break;
|
|
|
|
case PART_ROW_EYE_ICON:
|
|
if (validLayer(hit.layer)) {
|
|
return gfx::Rect(bounds.x,
|
|
bounds.y + y + headerBoxHeight() +
|
|
layerBoxHeight() * (lastLayer() - hit.layer) - viewScroll().y,
|
|
headerBoxWidth(),
|
|
layerBoxHeight());
|
|
}
|
|
break;
|
|
|
|
case PART_ROW_PADLOCK_ICON:
|
|
if (validLayer(hit.layer)) {
|
|
return gfx::Rect(bounds.x + headerBoxWidth(),
|
|
bounds.y + y + headerBoxHeight() +
|
|
layerBoxHeight() * (lastLayer() - hit.layer) - viewScroll().y,
|
|
headerBoxWidth(),
|
|
layerBoxHeight());
|
|
}
|
|
break;
|
|
|
|
case PART_ROW_CONTINUOUS_ICON:
|
|
if (validLayer(hit.layer)) {
|
|
return gfx::Rect(bounds.x + 2 * headerBoxWidth(),
|
|
bounds.y + y + headerBoxHeight() +
|
|
layerBoxHeight() * (lastLayer() - hit.layer) - viewScroll().y,
|
|
headerBoxWidth(),
|
|
layerBoxHeight());
|
|
}
|
|
break;
|
|
|
|
case PART_ROW_TEXT:
|
|
if (validLayer(hit.layer)) {
|
|
int x = headerBoxWidth() * 3;
|
|
return gfx::Rect(bounds.x + x,
|
|
bounds.y + y + headerBoxHeight() +
|
|
layerBoxHeight() * (lastLayer() - hit.layer) - viewScroll().y,
|
|
separatorX() - x,
|
|
layerBoxHeight());
|
|
}
|
|
break;
|
|
|
|
case PART_CEL:
|
|
if (validLayer(hit.layer) && hit.frame >= frame_t(0)) {
|
|
return gfx::Rect(bounds.x + separatorX() + m_separator_w - guiscale() +
|
|
getFrameXPos(hit.frame) - viewScroll().x,
|
|
bounds.y + y + headerBoxHeight() +
|
|
layerBoxHeight() * (lastLayer() - hit.layer) - viewScroll().y,
|
|
getFrameWidth(hit.frame),
|
|
layerBoxHeight());
|
|
}
|
|
break;
|
|
|
|
case PART_RANGE_OUTLINE: {
|
|
gfx::Rect rc = getRangeBounds(m_range);
|
|
int s = outlineWidth();
|
|
rc.enlarge(gfx::Border(s - guiscale(), s - guiscale(), s, s));
|
|
if (rc.x < bounds.x)
|
|
rc.offset(s, 0).inflate(-s, 0);
|
|
if (rc.y < bounds.y)
|
|
rc.offset(0, s).inflate(0, -s);
|
|
return rc;
|
|
}
|
|
|
|
case PART_TAG: {
|
|
Tag* tag = hit.getTag();
|
|
if (tag) {
|
|
col_t fromFrame, toFrame;
|
|
getTagFrames(tag, &fromFrame, &toFrame);
|
|
|
|
gfx::Rect bounds1 = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), fromFrame));
|
|
gfx::Rect bounds2 = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), toFrame));
|
|
gfx::Rect bounds = bounds1.createUnion(bounds2);
|
|
bounds.y -= skinTheme()->dimensions.timelineTagsAreaHeight();
|
|
|
|
int textHeight = font()->lineHeight();
|
|
bounds.y -= textHeight + 2 * ui::guiscale();
|
|
bounds.x += 3 * ui::guiscale();
|
|
bounds.w = font()->textLength(tag->name().c_str()) + 4 * ui::guiscale();
|
|
bounds.h = font()->lineHeight() + 2 * ui::guiscale();
|
|
|
|
if (m_tagFocusBand < 0) {
|
|
auto it = m_tagBand.find(tag);
|
|
if (it != m_tagBand.end()) {
|
|
int dy = (m_tagBands - it->second - 1) * oneTagHeight();
|
|
bounds.y -= dy;
|
|
}
|
|
}
|
|
|
|
return bounds;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case PART_TAGS:
|
|
return gfx::Rect(bounds.x + separatorX() + m_separator_w - guiscale(),
|
|
bounds.y,
|
|
bounds.w - separatorX() - m_separator_w + guiscale(),
|
|
y);
|
|
|
|
case PART_TAG_BAND:
|
|
return gfx::Rect(bounds.x + separatorX() + m_separator_w - guiscale(),
|
|
bounds.y + (m_tagFocusBand < 0 ? oneTagHeight() * std::max(0, hit.band) : 0),
|
|
bounds.w - separatorX() - m_separator_w + guiscale(),
|
|
oneTagHeight());
|
|
|
|
case PART_TAG_SWITCH_BUTTONS: {
|
|
gfx::Size sz = theme()->calcSizeHint(this, skinTheme()->styles.timelineSwitchBandButton());
|
|
|
|
return gfx::Rect(bounds.x + bounds.w - sz.w, bounds.y, sz.w, y);
|
|
}
|
|
|
|
case PART_TAG_SWITCH_BAND_BUTTON: {
|
|
gfx::Size sz = theme()->calcSizeHint(this, skinTheme()->styles.timelineSwitchBandButton());
|
|
|
|
return gfx::Rect(bounds.x + bounds.w - sz.w - 2 * ui::guiscale(),
|
|
bounds.y +
|
|
(m_tagFocusBand < 0 ? oneTagHeight() * std::max(0, hit.band) : 0) +
|
|
oneTagHeight() / 2 - sz.h / 2,
|
|
sz.w,
|
|
sz.h);
|
|
}
|
|
}
|
|
|
|
return gfx::Rect();
|
|
}
|
|
|
|
gfx::Rect Timeline::getSelectedFramesBounds(const Range& range) const
|
|
{
|
|
gfx::Rect rc;
|
|
for (frame_t frame : range.selectedFrames()) {
|
|
rc |= getPartBounds(Hit(PART_HEADER_FRAME, 0, col_t(frame)));
|
|
rc |= getPartBounds(Hit(PART_CEL, 0, col_t(frame)));
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
gfx::Rect Timeline::getSelectedLayersBounds(const Range& range) const
|
|
{
|
|
gfx::Rect rc;
|
|
for (auto* layer : range.selectedLayers()) {
|
|
layer_t layerIdx = getLayerIndex(layer);
|
|
rc |= getPartBounds(Hit(PART_ROW_TEXT, layerIdx));
|
|
rc |= getPartBounds(Hit(PART_CEL, layerIdx, lastFrame()));
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
gfx::Rect Timeline::getRangeBounds(const Range& range) const
|
|
{
|
|
gfx::Rect rc;
|
|
switch (range.type()) {
|
|
case Range::kNone:
|
|
// Return empty rectangle
|
|
break;
|
|
case Range::kCels:
|
|
for (auto* layer : range.selectedLayers()) {
|
|
layer_t layerIdx = getLayerIndex(layer);
|
|
for (frame_t frame : range.selectedFrames())
|
|
rc |= getPartBounds(Hit(PART_CEL, layerIdx, col_t(frame)));
|
|
}
|
|
break;
|
|
case Range::kFrames: return getSelectedFramesBounds(range);
|
|
|
|
case Range::kLayers: return getSelectedLayersBounds(range);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
gfx::Rect Timeline::getRangeClipBounds(const Range& range) const
|
|
{
|
|
gfx::Rect celBounds = getCelsBounds();
|
|
gfx::Rect clipBounds, unionBounds;
|
|
switch (range.type()) {
|
|
case Range::kCels: {
|
|
clipBounds = celBounds;
|
|
if (m_state == STATE_DRAGGING_EXTERNAL_RANGE)
|
|
clipBounds |= getLayerHeadersBounds();
|
|
break;
|
|
}
|
|
case Range::kFrames: {
|
|
clipBounds = getFrameHeadersBounds();
|
|
|
|
unionBounds = (clipBounds | celBounds);
|
|
clipBounds.y = unionBounds.y;
|
|
clipBounds.h = unionBounds.h;
|
|
break;
|
|
}
|
|
case Range::kLayers: {
|
|
clipBounds = getLayerHeadersBounds();
|
|
|
|
unionBounds = (clipBounds | celBounds);
|
|
clipBounds.x = unionBounds.x;
|
|
clipBounds.w = unionBounds.w;
|
|
break;
|
|
}
|
|
}
|
|
return clipBounds;
|
|
}
|
|
|
|
int Timeline::getFrameXPos(const col_t frame) const
|
|
{
|
|
if (frame < 0)
|
|
return 0;
|
|
return frame * frameBoxWidth();
|
|
}
|
|
|
|
int Timeline::getFrameWidth(const col_t frame) const
|
|
{
|
|
(void)frame; // TODO receive the frame future different width per frame.
|
|
return frameBoxWidth();
|
|
}
|
|
|
|
view::col_t Timeline::getFrameInXPos(const int x) const
|
|
{
|
|
return col_t(x / frameBoxWidth());
|
|
}
|
|
|
|
void Timeline::invalidateHit(const Hit& hit)
|
|
{
|
|
if (hit.band >= 0) {
|
|
Hit hit2 = hit;
|
|
hit2.part = PART_TAG_BAND;
|
|
invalidateRect(getPartBounds(hit2).offset(origin()));
|
|
}
|
|
|
|
invalidateRect(getPartBounds(hit).offset(origin()));
|
|
}
|
|
|
|
void Timeline::invalidateLayer(const Layer* layer)
|
|
{
|
|
if (layer == nullptr)
|
|
return;
|
|
|
|
layer_t layerIdx = getLayerIndex(layer);
|
|
if (layerIdx < firstLayer())
|
|
return;
|
|
|
|
gfx::Rect rc = getPartBounds(Hit(PART_ROW, layerIdx));
|
|
gfx::Rect rcCels;
|
|
rcCels |= getPartBounds(Hit(PART_CEL, layerIdx, firstFrame()));
|
|
rcCels |= getPartBounds(Hit(PART_CEL, layerIdx, lastFrame()));
|
|
rcCels &= getCelsBounds();
|
|
rc |= rcCels;
|
|
rc.offset(origin());
|
|
invalidateRect(rc);
|
|
}
|
|
|
|
void Timeline::invalidateFrame(const col_t frame)
|
|
{
|
|
if (!validFrame(frame))
|
|
return;
|
|
|
|
gfx::Rect rc = getPartBounds(Hit(PART_HEADER_FRAME, -1, frame));
|
|
gfx::Rect rcCels;
|
|
rcCels |= getPartBounds(Hit(PART_CEL, firstLayer(), frame));
|
|
rcCels |= getPartBounds(Hit(PART_CEL, lastLayer(), frame));
|
|
rcCels &= getCelsBounds();
|
|
rc |= rcCels;
|
|
rc.offset(origin());
|
|
invalidateRect(rc);
|
|
}
|
|
|
|
void Timeline::regenerateCols()
|
|
{
|
|
ASSERT(m_document);
|
|
ASSERT(m_sprite);
|
|
|
|
m_ncols = m_adapter->totalFrames();
|
|
m_ncols = std::max(col_t(1), m_ncols);
|
|
}
|
|
|
|
void Timeline::regenerateRows()
|
|
{
|
|
ASSERT(m_document);
|
|
ASSERT(m_sprite);
|
|
|
|
size_t nlayers = 0;
|
|
for_each_expanded_layer(m_sprite->root(),
|
|
[&nlayers](Layer* layer, int level, LayerFlags flags) { ++nlayers; });
|
|
|
|
if (m_rows.size() != nlayers) {
|
|
if (nlayers > 0)
|
|
m_rows.resize(nlayers);
|
|
else
|
|
m_rows.clear();
|
|
}
|
|
|
|
size_t i = 0;
|
|
for_each_expanded_layer(m_sprite->root(), [&i, this](Layer* layer, int level, LayerFlags flags) {
|
|
m_rows[i++] = Row(layer, level, flags);
|
|
});
|
|
|
|
regenerateTagBands();
|
|
updateScrollBars();
|
|
}
|
|
|
|
void Timeline::regenerateTagBands()
|
|
{
|
|
const bool oldEmptyTagBand = m_tagBand.empty();
|
|
|
|
// TODO improve this implementation
|
|
std::vector<unsigned char> tagsPerFrame(m_adapter->totalFrames(), 0);
|
|
std::vector<Tag*> bands(4, nullptr);
|
|
m_tagBand.clear();
|
|
for (Tag* tag : m_sprite->tags()) {
|
|
col_t fromFrame, toFrame;
|
|
if (!getTagFrames(tag, &fromFrame, &toFrame))
|
|
continue; // Ignore tags that are not inside the timeline adapter/view
|
|
|
|
int b = 0;
|
|
for (; b < int(bands.size()); ++b) {
|
|
if (!bands[b] || fromFrame > calcTagVisibleToFrame(bands[b])) {
|
|
bands[b] = tag;
|
|
m_tagBand[tag] = b;
|
|
break;
|
|
}
|
|
}
|
|
if (b == int(bands.size()))
|
|
m_tagBand[tag] = tagsPerFrame[fromFrame];
|
|
|
|
toFrame = calcTagVisibleToFrame(tag);
|
|
if (toFrame >= col_t(tagsPerFrame.size()))
|
|
tagsPerFrame.resize(toFrame + 1, 0);
|
|
for (col_t f = fromFrame; f <= toFrame; f = col_t(f + 1)) {
|
|
ASSERT(f < frame_t(tagsPerFrame.size()));
|
|
if (tagsPerFrame[f] < 255)
|
|
++tagsPerFrame[f];
|
|
}
|
|
}
|
|
|
|
const int oldVisibleBands = visibleTagBands();
|
|
m_tagBands = 0;
|
|
for (int i : tagsPerFrame)
|
|
m_tagBands = std::max(m_tagBands, i);
|
|
|
|
if (m_tagFocusBand >= m_tagBands)
|
|
m_tagFocusBand = -1;
|
|
|
|
if (oldVisibleBands != visibleTagBands() ||
|
|
// This case is to re-layout the timeline when the AniControl
|
|
// can use more/less space because there weren't tags and now
|
|
// there tags, or viceversa.
|
|
oldEmptyTagBand != m_tagBand.empty()) {
|
|
layout();
|
|
}
|
|
}
|
|
|
|
int Timeline::visibleTagBands() const
|
|
{
|
|
if (m_tagBands > 1 && m_tagFocusBand == -1)
|
|
return m_tagBands;
|
|
else
|
|
return 1;
|
|
}
|
|
|
|
void Timeline::updateScrollBars()
|
|
{
|
|
gfx::Rect rc = bounds();
|
|
m_viewportArea = getCelsBounds().offset(rc.origin());
|
|
ui::setup_scrollbars(getScrollableSize(), m_viewportArea, *this, m_hbar, m_vbar);
|
|
|
|
setViewScroll(viewScroll());
|
|
}
|
|
|
|
void Timeline::updateByMousePos(ui::Message* msg, const gfx::Point& mousePos)
|
|
{
|
|
Hit hit = hitTest(msg, mousePos);
|
|
if (hasMouse())
|
|
setCursor(msg, hit);
|
|
setHot(hit);
|
|
}
|
|
|
|
Timeline::Hit Timeline::hitTest(ui::Message* msg, const gfx::Point& mousePos)
|
|
{
|
|
Hit hit(PART_NOTHING);
|
|
if (!m_document)
|
|
return hit;
|
|
|
|
if (m_clk.part == PART_SEPARATOR) {
|
|
hit.part = PART_SEPARATOR;
|
|
}
|
|
else {
|
|
gfx::Point scroll = viewScroll();
|
|
int top = topHeight();
|
|
|
|
hit.layer = lastLayer() -
|
|
((mousePos.y - top - headerBoxHeight() + scroll.y) / layerBoxHeight());
|
|
|
|
hit.frame = getFrameInXPos(mousePos.x - separatorX() - m_separator_w + scroll.x);
|
|
|
|
if (hasCapture()) {
|
|
if (!m_rows.empty())
|
|
hit.layer = std::clamp(hit.layer, firstLayer(), lastLayer());
|
|
else
|
|
hit.layer = -1;
|
|
|
|
if (isMovingCel())
|
|
hit.frame = std::max(firstFrame(), hit.frame);
|
|
else
|
|
hit.frame = std::clamp(hit.frame, firstFrame(), lastFrame());
|
|
}
|
|
else {
|
|
if (hit.layer > lastLayer())
|
|
hit.layer = -1;
|
|
if (hit.frame > lastFrame())
|
|
hit.frame = kNoCol;
|
|
}
|
|
|
|
// Flag which indicates that we are in the are below the Background layer/last layer area
|
|
if (hit.layer < 0)
|
|
hit.veryBottom = true;
|
|
|
|
// Is the mouse over onionskin handles?
|
|
gfx::Rect bounds = getOnionskinFramesBounds();
|
|
if (!bounds.isEmpty() && gfx::Rect(bounds.x, bounds.y, 3, bounds.h).contains(mousePos)) {
|
|
hit.part = PART_HEADER_ONIONSKIN_RANGE_LEFT;
|
|
}
|
|
else if (!bounds.isEmpty() &&
|
|
gfx::Rect(bounds.x + bounds.w - 3, bounds.y, 3, bounds.h).contains(mousePos)) {
|
|
hit.part = PART_HEADER_ONIONSKIN_RANGE_RIGHT;
|
|
}
|
|
// Is the mouse on the frame tags area?
|
|
else if (getPartBounds(Hit(PART_TAGS)).contains(mousePos)) {
|
|
// Mouse in switch band button
|
|
if (hit.part == PART_NOTHING) {
|
|
if (m_tagFocusBand < 0) {
|
|
for (int band = 0; band < m_tagBands; ++band) {
|
|
gfx::Rect bounds = getPartBounds(
|
|
Hit(PART_TAG_SWITCH_BAND_BUTTON, 0, kNoCol, doc::NullId, band));
|
|
if (bounds.contains(mousePos)) {
|
|
hit.part = PART_TAG_SWITCH_BAND_BUTTON;
|
|
hit.band = band;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
gfx::Rect bounds = getPartBounds(
|
|
Hit(PART_TAG_SWITCH_BAND_BUTTON, 0, kNoCol, doc::NullId, m_tagFocusBand));
|
|
if (bounds.contains(mousePos)) {
|
|
hit.part = PART_TAG_SWITCH_BAND_BUTTON;
|
|
hit.band = m_tagFocusBand;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mouse in frame tags
|
|
if (hit.part == PART_NOTHING) {
|
|
for (Tag* tag : m_sprite->tags()) {
|
|
col_t fromFrame, toFrame;
|
|
if (!getTagFrames(tag, &fromFrame, &toFrame))
|
|
continue;
|
|
|
|
const int band = m_tagBand[tag];
|
|
|
|
// Skip unfocused bands
|
|
if (m_tagFocusBand >= 0 && m_tagFocusBand != band) {
|
|
continue;
|
|
}
|
|
|
|
gfx::Rect tagBounds = getPartBounds(Hit(PART_TAG, 0, kNoCol, tag->id()));
|
|
if (tagBounds.contains(mousePos)) {
|
|
hit.part = PART_TAG;
|
|
hit.tag = tag->id();
|
|
hit.band = band;
|
|
break;
|
|
}
|
|
// Check if we are in the left/right handles to resize the tag
|
|
else {
|
|
gfx::Rect bounds1 = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), fromFrame));
|
|
gfx::Rect bounds2 = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), toFrame));
|
|
gfx::Rect bounds = bounds1.createUnion(bounds2);
|
|
bounds.h = bounds.y2() - tagBounds.y2();
|
|
bounds.y = tagBounds.y2();
|
|
|
|
gfx::Rect bandBounds = getPartBounds(Hit(PART_TAG_BAND, 0, kNoCol, doc::NullId, band));
|
|
|
|
const int fw = frameBoxWidth() / 2;
|
|
if (gfx::Rect(bounds.x2() - fw, bounds.y, fw, bounds.h).contains(mousePos)) {
|
|
hit.part = PART_TAG_RIGHT;
|
|
hit.tag = tag->id();
|
|
hit.band = band;
|
|
// If we are in the band, we hit this tag, in other
|
|
// case, we can try to hit other tag that might be a
|
|
// better match.
|
|
if (bandBounds.contains(mousePos))
|
|
break;
|
|
}
|
|
else if (gfx::Rect(bounds.x, bounds.y, fw, bounds.h).contains(mousePos)) {
|
|
hit.part = PART_TAG_LEFT;
|
|
hit.tag = tag->id();
|
|
hit.band = band;
|
|
if (bandBounds.contains(mousePos))
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mouse in bands
|
|
if (hit.part == PART_NOTHING) {
|
|
if (m_tagFocusBand < 0) {
|
|
for (int band = 0; band < m_tagBands; ++band) {
|
|
gfx::Rect bounds = getPartBounds(Hit(PART_TAG_BAND, 0, kNoCol, doc::NullId, band));
|
|
if (bounds.contains(mousePos)) {
|
|
hit.part = PART_TAG_BAND;
|
|
hit.band = band;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
gfx::Rect bounds = getPartBounds(
|
|
Hit(PART_TAG_BAND, 0, kNoCol, doc::NullId, m_tagFocusBand));
|
|
if (bounds.contains(mousePos)) {
|
|
hit.part = PART_TAG_BAND;
|
|
hit.band = m_tagFocusBand;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Is the mouse on the separator.
|
|
else if (mousePos.x > separatorX() - 4 && mousePos.x <= separatorX()) {
|
|
hit.part = PART_SEPARATOR;
|
|
}
|
|
// Is the mouse on the headers?
|
|
else if (mousePos.y >= top && mousePos.y < top + headerBoxHeight()) {
|
|
if (mousePos.x < separatorX()) {
|
|
if (getPartBounds(Hit(PART_HEADER_EYE)).contains(mousePos))
|
|
hit.part = PART_HEADER_EYE;
|
|
else if (getPartBounds(Hit(PART_HEADER_PADLOCK)).contains(mousePos))
|
|
hit.part = PART_HEADER_PADLOCK;
|
|
else if (getPartBounds(Hit(PART_HEADER_CONTINUOUS)).contains(mousePos))
|
|
hit.part = PART_HEADER_CONTINUOUS;
|
|
else if (getPartBounds(Hit(PART_HEADER_GEAR)).contains(mousePos))
|
|
hit.part = PART_HEADER_GEAR;
|
|
else if (getPartBounds(Hit(PART_HEADER_ONIONSKIN)).contains(mousePos))
|
|
hit.part = PART_HEADER_ONIONSKIN;
|
|
else if (getPartBounds(Hit(PART_HEADER_LAYER)).contains(mousePos))
|
|
hit.part = PART_HEADER_LAYER;
|
|
}
|
|
else {
|
|
hit.part = PART_HEADER_FRAME;
|
|
}
|
|
}
|
|
// Activate a flag in case that the hit is in the header area (AniControls and tags).
|
|
else if (mousePos.y < top + headerBoxHeight())
|
|
hit.part = PART_TOP;
|
|
// Is the mouse on a layer's label?
|
|
else if (mousePos.x < separatorX()) {
|
|
if (getPartBounds(Hit(PART_ROW_EYE_ICON, hit.layer)).contains(mousePos))
|
|
hit.part = PART_ROW_EYE_ICON;
|
|
else if (getPartBounds(Hit(PART_ROW_PADLOCK_ICON, hit.layer)).contains(mousePos))
|
|
hit.part = PART_ROW_PADLOCK_ICON;
|
|
else if (getPartBounds(Hit(PART_ROW_CONTINUOUS_ICON, hit.layer)).contains(mousePos))
|
|
hit.part = PART_ROW_CONTINUOUS_ICON;
|
|
else if (getPartBounds(Hit(PART_ROW_TEXT, hit.layer)).contains(mousePos))
|
|
hit.part = PART_ROW_TEXT;
|
|
else
|
|
hit.part = PART_ROW;
|
|
}
|
|
else if (validLayer(hit.layer) && validFrame(hit.frame)) {
|
|
hit.part = PART_CEL;
|
|
}
|
|
else
|
|
hit.part = PART_NOTHING;
|
|
|
|
if (!hasCapture() && msg) {
|
|
gfx::Rect outline = getPartBounds(Hit(PART_RANGE_OUTLINE));
|
|
if (outline.contains(mousePos)) {
|
|
auto mouseMsg = dynamic_cast<MouseMessage*>(msg);
|
|
|
|
if ( // With Ctrl and Alt key we can drag the range from any place (not necessary from the
|
|
// outline.
|
|
is_copy_key_pressed(msg) ||
|
|
// Drag with right-click
|
|
(m_state == STATE_STANDBY && mouseMsg && mouseMsg->right()) ||
|
|
// Drag with left-click only if we are inside the range edges
|
|
(Preferences::instance().timeline.dragAndDropFromEdges() &&
|
|
!gfx::Rect(outline).shrink(2 * outlineWidth()).contains(mousePos))) {
|
|
hit.part = PART_RANGE_OUTLINE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return hit;
|
|
}
|
|
|
|
Timeline::Hit Timeline::hitTestCel(const gfx::Point& mousePos)
|
|
{
|
|
Hit hit(PART_NOTHING);
|
|
if (!m_document)
|
|
return hit;
|
|
|
|
gfx::Point scroll = viewScroll();
|
|
int top = topHeight();
|
|
|
|
hit.layer = lastLayer() - ((mousePos.y - top - headerBoxHeight() + scroll.y) / layerBoxHeight());
|
|
|
|
hit.frame = getFrameInXPos(mousePos.x - separatorX() - m_separator_w + scroll.x);
|
|
|
|
if (!m_rows.empty())
|
|
hit.layer = std::clamp(hit.layer, firstLayer(), lastLayer());
|
|
else
|
|
hit.layer = -1;
|
|
|
|
hit.frame = std::max(firstFrame(), hit.frame);
|
|
|
|
return hit;
|
|
}
|
|
|
|
void Timeline::setHot(const Hit& hit)
|
|
{
|
|
// If the part, layer or frame change.
|
|
if (m_hot != hit) {
|
|
// Invalidate the whole control.
|
|
if (isMovingCel()) {
|
|
invalidate();
|
|
}
|
|
// Invalidate the old and new 'hot' thing.
|
|
else {
|
|
invalidateHit(m_hot);
|
|
invalidateHit(hit);
|
|
}
|
|
|
|
// Change the new 'hot' thing.
|
|
m_hot = hit;
|
|
}
|
|
}
|
|
|
|
void Timeline::updateStatusBar(ui::Message* msg)
|
|
{
|
|
if (!hasMouse())
|
|
return;
|
|
|
|
StatusBar* sb = StatusBar::instance();
|
|
|
|
if (m_state == STATE_MOVING_RANGE && msg) {
|
|
const char* verb = is_copy_key_pressed(msg) ? "Copy" : "Move";
|
|
|
|
switch (m_range.type()) {
|
|
case Range::kCels: sb->setStatusText(0, fmt::format("{} cels", verb)); return;
|
|
|
|
case Range::kFrames:
|
|
if (validFrame(m_hot.frame)) {
|
|
if (m_dropTarget.hhit == DropTarget::Before) {
|
|
sb->setStatusText(
|
|
0,
|
|
fmt::format("{} before frame {}", verb, int(m_dropRange.firstFrame() + 1)));
|
|
return;
|
|
}
|
|
else if (m_dropTarget.hhit == DropTarget::After) {
|
|
sb->setStatusText(
|
|
0,
|
|
fmt::format("{} after frame {}", verb, int(m_dropRange.lastFrame() + 1)));
|
|
return;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case Range::kLayers: {
|
|
layer_t firstLayer;
|
|
layer_t lastLayer;
|
|
if (!selectedLayersBounds(m_dropRange.selectedLayers(), &firstLayer, &lastLayer))
|
|
break;
|
|
|
|
if (m_dropTarget.vhit == DropTarget::VeryBottom) {
|
|
sb->setStatusText(0, fmt::format("{} at the very bottom", verb));
|
|
return;
|
|
}
|
|
|
|
layer_t layerIdx = -1;
|
|
if (m_dropTarget.vhit == DropTarget::Bottom || m_dropTarget.vhit == DropTarget::FirstChild)
|
|
layerIdx = firstLayer;
|
|
else if (m_dropTarget.vhit == DropTarget::Top)
|
|
layerIdx = lastLayer;
|
|
|
|
Layer* layer = (validLayer(layerIdx) ? m_rows[layerIdx].layer() : nullptr);
|
|
if (layer) {
|
|
switch (m_dropTarget.vhit) {
|
|
case DropTarget::Bottom:
|
|
sb->setStatusText(0, fmt::format("{} below layer '{}'", verb, layer->name()));
|
|
return;
|
|
case DropTarget::Top:
|
|
sb->setStatusText(0, fmt::format("{} above layer '{}'", verb, layer->name()));
|
|
return;
|
|
case DropTarget::FirstChild:
|
|
sb->setStatusText(
|
|
0,
|
|
fmt::format("{} as first child of group '{}'", verb, layer->name()));
|
|
return;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
Layer* layer = (validLayer(m_hot.layer) ? m_rows[m_hot.layer].layer() : nullptr);
|
|
|
|
switch (m_hot.part) {
|
|
case PART_HEADER_ONIONSKIN: {
|
|
sb->setStatusText(
|
|
0,
|
|
fmt::format("Onionskin is {}", docPref().onionskin.active() ? "enabled" : "disabled"));
|
|
return;
|
|
}
|
|
|
|
case PART_ROW_TEXT:
|
|
if (layer != NULL) {
|
|
sb->setStatusText(0,
|
|
fmt::format("{} '{}' [{}{}]",
|
|
layer->isReference() ? "Reference layer" : "Layer",
|
|
layer->name(),
|
|
layer->isVisible() ? "visible" : "hidden",
|
|
layer->isEditable() ? "" : " locked"));
|
|
return;
|
|
}
|
|
break;
|
|
|
|
case PART_ROW_EYE_ICON:
|
|
if (layer != NULL) {
|
|
sb->setStatusText(0,
|
|
fmt::format("Layer '{}' is {}",
|
|
layer->name(),
|
|
layer->isVisible() ? "visible" : "hidden"));
|
|
return;
|
|
}
|
|
break;
|
|
|
|
case PART_ROW_PADLOCK_ICON:
|
|
if (layer != NULL) {
|
|
sb->setStatusText(
|
|
0,
|
|
fmt::format("Layer '{}' is {}",
|
|
layer->name(),
|
|
layer->isEditable() ? "unlocked (editable)" : "locked (read-only)"));
|
|
return;
|
|
}
|
|
break;
|
|
|
|
case PART_ROW_CONTINUOUS_ICON:
|
|
if (layer) {
|
|
if (layer->isImage())
|
|
sb->setStatusText(0,
|
|
fmt::format("Layer '{}' is {} ({})",
|
|
layer->name(),
|
|
layer->isContinuous() ? "continuous" : "discontinuous",
|
|
layer->isContinuous() ? "prefer linked cels/frames" :
|
|
"prefer individual cels/frames"));
|
|
else if (layer->isGroup())
|
|
sb->setStatusText(0, fmt::format("Group '{}'", layer->name()));
|
|
return;
|
|
}
|
|
break;
|
|
|
|
case PART_HEADER_FRAME:
|
|
case PART_CEL:
|
|
case PART_TAG: {
|
|
col_t frame = m_frame;
|
|
if (validFrame(m_hot.frame))
|
|
frame = m_hot.frame;
|
|
|
|
updateStatusBarForFrame(frame, m_hot.getTag(), (layer ? layer->cel(frame) : nullptr));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
sb->showDefaultText();
|
|
}
|
|
|
|
void Timeline::updateStatusBarForFrame(const col_t col, const Tag* tag, const Cel* cel)
|
|
{
|
|
if (!m_sprite || !m_adapter)
|
|
return;
|
|
|
|
std::string buf;
|
|
frame_t base = docPref().timeline.firstFrame();
|
|
const fr_t frame = m_adapter->toRealFrame(col);
|
|
frame_t firstFrame = frame;
|
|
frame_t lastFrame = frame;
|
|
|
|
if (tag) {
|
|
firstFrame = tag->fromFrame();
|
|
lastFrame = tag->toFrame();
|
|
}
|
|
else if (m_range.enabled() && m_range.frames() > 1) {
|
|
firstFrame = m_adapter->toRealFrame(col_t(m_range.firstFrame()));
|
|
lastFrame = m_adapter->toRealFrame(col_t(m_range.lastFrame()));
|
|
}
|
|
|
|
buf += fmt::format(":frame: {}", int(base + frame));
|
|
if (firstFrame != lastFrame) {
|
|
buf += fmt::format(" [{}...{}]", int(base + firstFrame), int(base + lastFrame));
|
|
}
|
|
|
|
buf += fmt::format(" :clock: {}", human_readable_time(m_adapter->frameDuration(col)));
|
|
if (firstFrame != lastFrame) {
|
|
buf += fmt::format(" [{}]",
|
|
tag ? human_readable_time(tagFramesDuration(tag)) :
|
|
human_readable_time(selectedFramesDuration()));
|
|
}
|
|
if (m_sprite->totalFrames() > 1)
|
|
buf += fmt::format("/{}", human_readable_time(m_sprite->totalAnimationDuration()));
|
|
|
|
if (cel) {
|
|
buf += fmt::format(" Cel :pos: {} {} :size: {} {}",
|
|
cel->bounds().x,
|
|
cel->bounds().y,
|
|
cel->bounds().w,
|
|
cel->bounds().h);
|
|
|
|
if (cel->links() > 0) {
|
|
buf += fmt::format(" Links {}", int(cel->links()));
|
|
}
|
|
}
|
|
|
|
StatusBar::instance()->setStatusText(0, buf);
|
|
}
|
|
|
|
void Timeline::showCel(layer_t layer, col_t frame)
|
|
{
|
|
gfx::Point scroll = viewScroll();
|
|
|
|
gfx::Rect viewport = m_viewportArea;
|
|
|
|
// Add the horizontal bar space to the viewport area if the viewport
|
|
// is not big enough to show one cel.
|
|
if (m_hbar.isVisible() && viewport.h < layerBoxHeight())
|
|
viewport.h += m_vbar.getBarWidth();
|
|
|
|
gfx::Rect celBounds(viewport.x + getFrameXPos(frame) - scroll.x,
|
|
viewport.y + layerBoxHeight() * (lastLayer() - layer) - scroll.y,
|
|
getFrameWidth(frame),
|
|
layerBoxHeight());
|
|
|
|
const bool isPlaying = m_editor->isPlaying();
|
|
|
|
// Here we use <= instead of < to avoid jumping between this
|
|
// condition and the "else if" one when we are playing the
|
|
// animation.
|
|
if (celBounds.x <= viewport.x) {
|
|
scroll.x -= viewport.x - celBounds.x;
|
|
if (isPlaying)
|
|
scroll.x -= viewport.w / 2 - celBounds.w / 2;
|
|
}
|
|
else if (celBounds.x2() > viewport.x2()) {
|
|
scroll.x += celBounds.x2() - viewport.x2();
|
|
if (isPlaying)
|
|
scroll.x += viewport.w / 2 - celBounds.w / 2;
|
|
}
|
|
|
|
if (celBounds.y <= viewport.y) {
|
|
scroll.y -= viewport.y - celBounds.y;
|
|
}
|
|
else if (celBounds.y2() > viewport.y2()) {
|
|
scroll.y += celBounds.y2() - viewport.y2();
|
|
}
|
|
|
|
setViewScroll(scroll);
|
|
}
|
|
|
|
void Timeline::showCurrentCel()
|
|
{
|
|
layer_t layer = getLayerIndex(m_layer);
|
|
if (layer >= firstLayer())
|
|
showCel(layer, m_frame);
|
|
}
|
|
|
|
void Timeline::focusTagBand(int band)
|
|
{
|
|
if (m_tagFocusBand < 0) {
|
|
m_tagFocusBand = band;
|
|
}
|
|
else {
|
|
m_tagFocusBand = -1;
|
|
}
|
|
}
|
|
|
|
void Timeline::cleanClk()
|
|
{
|
|
invalidateHit(m_clk);
|
|
m_clk = Hit(PART_NOTHING);
|
|
}
|
|
|
|
gfx::Size Timeline::getScrollableSize() const
|
|
{
|
|
if (m_sprite) {
|
|
return gfx::Size(getFrameXPos(m_adapter->totalFrames()) + getCelsBounds().w / 2,
|
|
(m_rows.size() + 1) * layerBoxHeight());
|
|
}
|
|
else
|
|
return gfx::Size(0, 0);
|
|
}
|
|
|
|
gfx::Point Timeline::getMaxScrollablePos() const
|
|
{
|
|
if (m_sprite) {
|
|
gfx::Size size = getScrollableSize();
|
|
int max_scroll_x = size.w - getCelsBounds().w + 1 * guiscale();
|
|
int max_scroll_y = size.h - getCelsBounds().h + 1 * guiscale();
|
|
max_scroll_x = std::max(0, max_scroll_x);
|
|
max_scroll_y = std::max(0, max_scroll_y);
|
|
return gfx::Point(max_scroll_x, max_scroll_y);
|
|
}
|
|
else
|
|
return gfx::Point(0, 0);
|
|
}
|
|
|
|
bool Timeline::allLayersVisible()
|
|
{
|
|
for (Layer* topLayer : m_sprite->root()->layers())
|
|
if (!topLayer->isVisible())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Timeline::allLayersInvisible()
|
|
{
|
|
for (Layer* topLayer : m_sprite->root()->layers())
|
|
if (topLayer->isVisible())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Timeline::allLayersLocked()
|
|
{
|
|
for (Layer* topLayer : m_sprite->root()->layers())
|
|
if (topLayer->isEditable())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Timeline::allLayersUnlocked()
|
|
{
|
|
for (Layer* topLayer : m_sprite->root()->layers())
|
|
if (!topLayer->isEditable())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Timeline::allLayersContinuous()
|
|
{
|
|
for (size_t i = 0; i < m_rows.size(); i++)
|
|
if (!m_rows[i].layer()->isContinuous())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Timeline::allLayersDiscontinuous()
|
|
{
|
|
for (size_t i = 0; i < m_rows.size(); i++)
|
|
if (m_rows[i].layer()->isContinuous())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
doc::Layer* Timeline::getLayer(int layerIndex) const
|
|
{
|
|
if (layerIndex >= 0 && layerIndex < m_rows.size())
|
|
return m_rows[layerIndex].layer();
|
|
else {
|
|
// Only possible when m_rows is empty
|
|
ASSERT(m_rows.size() == 0);
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
layer_t Timeline::getLayerIndex(const Layer* layer) const
|
|
{
|
|
for (int i = 0; i < (int)m_rows.size(); i++)
|
|
if (m_rows[i].layer() == layer)
|
|
return i;
|
|
|
|
return -1;
|
|
}
|
|
|
|
bool Timeline::isLayerActive(const layer_t layerIndex) const
|
|
{
|
|
Layer* layer = getLayer(layerIndex);
|
|
if (!layer)
|
|
return false;
|
|
|
|
if (layer == m_layer)
|
|
return true;
|
|
else
|
|
return m_range.contains(layer);
|
|
}
|
|
|
|
bool Timeline::isFrameActive(const col_t frame) const
|
|
{
|
|
if (frame == m_frame)
|
|
return true;
|
|
else
|
|
return m_range.contains(frame_t(frame));
|
|
}
|
|
|
|
bool Timeline::isCelActive(const layer_t layerIdx, const col_t frame) const
|
|
{
|
|
Layer* layer = getLayer(layerIdx);
|
|
if (!layer)
|
|
return false;
|
|
|
|
if (m_range.enabled())
|
|
return m_range.contains(layer, frame);
|
|
else
|
|
return (layer == m_layer && frame == m_frame);
|
|
}
|
|
|
|
bool Timeline::isCelLooselyActive(const layer_t layerIdx, const col_t frame) const
|
|
{
|
|
Layer* layer = getLayer(layerIdx);
|
|
if (!layer)
|
|
return false;
|
|
|
|
if (m_range.enabled())
|
|
return (m_range.contains(layer) || m_range.contains(frame));
|
|
else
|
|
return (layer == m_layer || frame == m_frame);
|
|
}
|
|
|
|
void Timeline::dropRange(DropOp op)
|
|
{
|
|
bool copy = (op == Timeline::kCopy);
|
|
VirtualRange newFromRange;
|
|
DocRangePlace place = kDocRangeAfter;
|
|
RealRange dropRange = view::to_real_range(m_adapter.get(), m_dropRange);
|
|
bool outside = m_dropTarget.outside;
|
|
|
|
switch (m_range.type()) {
|
|
case Range::kFrames:
|
|
if (m_dropTarget.hhit == DropTarget::Before)
|
|
place = kDocRangeBefore;
|
|
break;
|
|
|
|
case Range::kLayers:
|
|
switch (m_dropTarget.vhit) {
|
|
case DropTarget::Bottom: place = kDocRangeBefore; break;
|
|
case DropTarget::FirstChild: place = kDocRangeFirstChild; break;
|
|
case DropTarget::VeryBottom:
|
|
place = kDocRangeBefore;
|
|
{
|
|
Layer* layer = m_sprite->root()->firstLayer();
|
|
dropRange.clearRange();
|
|
dropRange.startRange(layer, -1, Range::kLayers);
|
|
dropRange.endRange(layer, -1);
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
|
|
prepareToMoveRange();
|
|
|
|
try {
|
|
TagsHandling tagsHandling = (outside ? kFitOutsideTags : kFitInsideTags);
|
|
const RealRange realRange = this->realRange();
|
|
|
|
invalidateRange();
|
|
if (copy)
|
|
newFromRange = copy_range(m_document, realRange, dropRange, place, tagsHandling);
|
|
else
|
|
newFromRange = move_range(m_document, realRange, dropRange, place, tagsHandling);
|
|
|
|
// If we drop a cel in the same frame (but in another layer),
|
|
// document views are not updated, so we are forcing the updating of
|
|
// all views.
|
|
m_document->notifyGeneralUpdate();
|
|
|
|
newFromRange = view::to_virtual_range(m_adapter.get(), newFromRange);
|
|
moveRange(newFromRange);
|
|
|
|
invalidateRange();
|
|
|
|
if (m_range.type() == Range::kFrames && m_sprite && !m_sprite->tags().empty()) {
|
|
invalidateRect(getFrameHeadersBounds().offset(origin()));
|
|
}
|
|
|
|
// Update the sprite position after the command was executed
|
|
// TODO improve this workaround
|
|
Cmd* cmd = m_document->undoHistory()->lastExecutedCmd();
|
|
if (auto cmdTx = dynamic_cast<CmdTransaction*>(cmd))
|
|
cmdTx->updateSpritePositionAfter();
|
|
}
|
|
catch (const std::exception& ex) {
|
|
Console::showException(ex);
|
|
}
|
|
}
|
|
|
|
gfx::Size Timeline::visibleSize() const
|
|
{
|
|
return getCelsBounds().size();
|
|
}
|
|
|
|
gfx::Point Timeline::viewScroll() const
|
|
{
|
|
return gfx::Point(m_hbar.getPos(), m_vbar.getPos());
|
|
}
|
|
|
|
void Timeline::setViewScroll(const gfx::Point& pt)
|
|
{
|
|
const gfx::Point oldScroll = viewScroll();
|
|
const gfx::Point maxPos = getMaxScrollablePos();
|
|
gfx::Point newScroll = pt;
|
|
newScroll.x = std::clamp(newScroll.x, 0, maxPos.x);
|
|
newScroll.y = std::clamp(newScroll.y, 0, maxPos.y);
|
|
|
|
if (newScroll.y != oldScroll.y) {
|
|
gfx::Rect rc;
|
|
rc = getLayerHeadersBounds();
|
|
rc.offset(origin());
|
|
invalidateRect(rc);
|
|
}
|
|
|
|
if (newScroll != oldScroll) {
|
|
gfx::Rect rc;
|
|
if (m_tagBands > 0)
|
|
rc |= getPartBounds(Hit(PART_TAG_BAND));
|
|
if (m_range.enabled())
|
|
rc |= getRangeBounds(m_range).enlarge(outlineWidth());
|
|
rc |= getFrameHeadersBounds();
|
|
rc |= getCelsBounds();
|
|
rc.offset(origin());
|
|
invalidateRect(rc);
|
|
}
|
|
|
|
m_hbar.setPos(newScroll.x);
|
|
m_vbar.setPos(newScroll.y);
|
|
}
|
|
|
|
void Timeline::lockRange()
|
|
{
|
|
++m_rangeLocks;
|
|
}
|
|
|
|
void Timeline::unlockRange()
|
|
{
|
|
--m_rangeLocks;
|
|
}
|
|
|
|
void Timeline::updateDropRange(const gfx::Point& pt)
|
|
{
|
|
const DropTarget oldDropTarget = m_dropTarget;
|
|
m_dropTarget.hhit = DropTarget::HNone;
|
|
m_dropTarget.vhit = DropTarget::VNone;
|
|
m_dropTarget.outside = false;
|
|
|
|
if (m_state != STATE_MOVING_RANGE && m_state != STATE_DRAGGING_EXTERNAL_RANGE) {
|
|
m_dropRange.clearRange();
|
|
return;
|
|
}
|
|
|
|
switch (m_range.type()) {
|
|
case Range::kCels:
|
|
m_dropRange = m_range;
|
|
m_dropRange.displace(m_hot.layer - m_clk.layer, m_hot.frame - m_clk.frame);
|
|
break;
|
|
|
|
case Range::kFrames:
|
|
case Range::kLayers:
|
|
m_dropRange.clearRange();
|
|
if (!m_rows.empty()) {
|
|
auto* layer =
|
|
(m_hot.layer >= 0 && m_hot.layer < m_rows.size() ? m_rows[m_hot.layer].layer() : nullptr);
|
|
m_dropRange.startRange(layer, m_hot.frame, m_range.type());
|
|
m_dropRange.endRange(layer, m_hot.frame);
|
|
}
|
|
break;
|
|
}
|
|
|
|
gfx::Rect bounds = getRangeBounds(m_dropRange);
|
|
|
|
if (pt.x < bounds.x + bounds.w / 2) {
|
|
m_dropTarget.hhit = DropTarget::Before;
|
|
m_dropTarget.outside = (pt.x < bounds.x + 2);
|
|
}
|
|
else {
|
|
m_dropTarget.hhit = DropTarget::After;
|
|
m_dropTarget.outside = (pt.x >= bounds.x2() - 2);
|
|
}
|
|
|
|
if (m_hot.veryBottom)
|
|
m_dropTarget.vhit = DropTarget::VeryBottom;
|
|
else if (pt.y < bounds.y + bounds.h / 2)
|
|
m_dropTarget.vhit = DropTarget::Top;
|
|
// Special drop target for expanded groups
|
|
else if (m_range.type() == Range::kLayers && m_hot.layer >= 0 &&
|
|
m_hot.layer < int(m_rows.size()) && m_rows[m_hot.layer].layer()->isGroup() &&
|
|
static_cast<LayerGroup*>(m_rows[m_hot.layer].layer())->isExpanded()) {
|
|
m_dropTarget.vhit = DropTarget::FirstChild;
|
|
}
|
|
else {
|
|
m_dropTarget.vhit = DropTarget::Bottom;
|
|
}
|
|
|
|
if (oldDropTarget != m_dropTarget)
|
|
invalidate();
|
|
}
|
|
|
|
void Timeline::clearClipboardRange()
|
|
{
|
|
Doc* clipboard_document;
|
|
DocRange clipboard_range;
|
|
auto clipboard = Clipboard::instance();
|
|
clipboard->getDocumentRangeInfo(&clipboard_document, &clipboard_range);
|
|
|
|
if (!m_document || clipboard_document != m_document)
|
|
return;
|
|
|
|
clipboard->clearContent();
|
|
m_clipboard_timer.stop();
|
|
}
|
|
|
|
void Timeline::resetAllRanges()
|
|
{
|
|
invalidateRange();
|
|
m_range.clearRange();
|
|
m_startRange.clearRange();
|
|
m_dropRange.clearRange();
|
|
}
|
|
|
|
void Timeline::invalidateRange()
|
|
{
|
|
if (m_range.enabled()) {
|
|
for (const Layer* layer : m_range.selectedLayers())
|
|
invalidateLayer(layer);
|
|
for (const frame_t frame : m_range.selectedFrames())
|
|
invalidateFrame(col_t(frame));
|
|
|
|
invalidateHit(Hit(PART_RANGE_OUTLINE));
|
|
}
|
|
}
|
|
|
|
void Timeline::clearAndInvalidateRange()
|
|
{
|
|
if (m_range.enabled()) {
|
|
notify_observers(&TimelineObserver::onBeforeRangeChanged, this);
|
|
|
|
invalidateRange();
|
|
m_range.clearRange();
|
|
}
|
|
}
|
|
|
|
void Timeline::refresh()
|
|
{
|
|
regenerateRows();
|
|
invalidate();
|
|
}
|
|
|
|
app::gen::GlobalPref::Timeline& Timeline::timelinePref() const
|
|
{
|
|
return Preferences::instance().timeline;
|
|
}
|
|
|
|
DocumentPreferences& Timeline::docPref() const
|
|
{
|
|
return Preferences::instance().document(m_document);
|
|
}
|
|
|
|
skin::SkinTheme* Timeline::skinTheme() const
|
|
{
|
|
return SkinTheme::get(this);
|
|
}
|
|
|
|
int Timeline::headerBoxWidth() const
|
|
{
|
|
return skinTheme()->dimensions.timelineBaseSize();
|
|
}
|
|
|
|
int Timeline::headerBoxHeight() const
|
|
{
|
|
return skinTheme()->dimensions.timelineBaseSize();
|
|
}
|
|
|
|
int Timeline::layerBoxHeight() const
|
|
{
|
|
return int(zoom() * skinTheme()->dimensions.timelineBaseSize());
|
|
}
|
|
|
|
int Timeline::frameBoxWidth() const
|
|
{
|
|
return int(zoom() * skinTheme()->dimensions.timelineBaseSize());
|
|
}
|
|
|
|
int Timeline::outlineWidth() const
|
|
{
|
|
return skinTheme()->dimensions.timelineOutlineWidth();
|
|
}
|
|
|
|
int Timeline::oneTagHeight() const
|
|
{
|
|
return font()->lineHeight() + 2 * ui::guiscale() +
|
|
skinTheme()->dimensions.timelineTagsAreaHeight();
|
|
}
|
|
|
|
double Timeline::zoom() const
|
|
{
|
|
if (docPref().thumbnails.enabled())
|
|
return m_zoom;
|
|
else
|
|
return 1.0;
|
|
}
|
|
|
|
// Returns the last frame where the frame tag (or tag label) is
|
|
// visible in the timeline.
|
|
view::col_t Timeline::calcTagVisibleToFrame(Tag* tag) const
|
|
{
|
|
col_t frame = getFrameInXPos(getFrameXPos(m_adapter->toColFrame(fr_t(tag->fromFrame()))) +
|
|
font()->textLength(tag->name()));
|
|
|
|
return std::max(frame, m_adapter->toColFrame(fr_t(tag->toFrame())));
|
|
}
|
|
|
|
view::col_t Timeline::lastFrame() const
|
|
{
|
|
return std::max(col_t(0), col_t(m_adapter->totalFrames() - 1));
|
|
}
|
|
|
|
int Timeline::topHeight() const
|
|
{
|
|
int h = 0;
|
|
if (m_document && m_sprite) {
|
|
h += skinTheme()->dimensions.timelineTopBorder();
|
|
h += oneTagHeight() * visibleTagBands();
|
|
}
|
|
return h;
|
|
}
|
|
|
|
void Timeline::onNewInputPriority(InputChainElement* element, const ui::Message* msg)
|
|
{
|
|
// It looks like the user wants to execute commands targeting the
|
|
// ColorBar instead of the Timeline. Here we disable the selected
|
|
// range, so commands like Clear, Copy, Cut, etc. don't target the
|
|
// Timeline and they are sent to the active sprite editor.
|
|
//
|
|
// If the Workspace is selected (an sprite Editor), maybe the user
|
|
// want to move the X/Y position of all cels in the Timeline range.
|
|
// That is why we don't disable the range in this case.
|
|
Workspace* workspace = dynamic_cast<Workspace*>(element);
|
|
if (!workspace) {
|
|
// With Ctrl or Shift we can combine ColorBar selection + Timeline
|
|
// selection.
|
|
if (msg && (msg->ctrlPressed() || msg->shiftPressed()))
|
|
return;
|
|
|
|
if (element != this && m_rangeLocks == 0) {
|
|
if (!Preferences::instance().timeline.keepSelection()) {
|
|
invalidateRange();
|
|
m_range.clearRange();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Timeline::onCanCut(Context* ctx)
|
|
{
|
|
return false; // TODO
|
|
}
|
|
|
|
bool Timeline::onCanCopy(Context* ctx)
|
|
{
|
|
return m_range.enabled() && ctx->checkFlags(ContextFlags::HasActiveDocument);
|
|
}
|
|
|
|
bool Timeline::onCanPaste(Context* ctx)
|
|
{
|
|
return (ctx->clipboard()->format() == ClipboardFormat::DocRange &&
|
|
ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable));
|
|
}
|
|
|
|
bool Timeline::onCanClear(Context* ctx)
|
|
{
|
|
return (m_document && m_sprite && m_range.enabled());
|
|
}
|
|
|
|
bool Timeline::onCut(Context* ctx)
|
|
{
|
|
return false; // TODO
|
|
}
|
|
|
|
bool Timeline::onCopy(Context* ctx)
|
|
{
|
|
if (m_range.enabled()) {
|
|
const ContextReader reader(ctx);
|
|
if (reader.document()) {
|
|
ctx->clipboard()->copyRange(reader, m_range);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Timeline::onPaste(Context* ctx, const gfx::Point* position)
|
|
{
|
|
auto clipboard = ctx->clipboard();
|
|
if (clipboard->format() == ClipboardFormat::DocRange) {
|
|
// After a paste action, we just remove the marching ant
|
|
// (following paste commands will paste the same source range, but
|
|
// we just disable the marching ants effect).
|
|
if (m_clipboard_timer.isRunning()) {
|
|
m_clipboard_timer.stop();
|
|
m_redrawMarchingAntsOnly = false;
|
|
invalidate();
|
|
}
|
|
|
|
clipboard->paste(ctx, true);
|
|
return true;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
|
|
bool Timeline::onClear(Context* ctx)
|
|
{
|
|
if (!m_document || !m_sprite || !m_range.enabled() ||
|
|
// If the mask is visible the delete command will be handled by
|
|
// the Editor
|
|
m_document->isMaskVisible())
|
|
return false;
|
|
|
|
Command* cmd = nullptr;
|
|
|
|
switch (m_range.type()) {
|
|
case DocRange::kCels: cmd = Commands::instance()->byId(CommandId::ClearCel()); break;
|
|
case DocRange::kFrames: cmd = Commands::instance()->byId(CommandId::RemoveFrame()); break;
|
|
case DocRange::kLayers: cmd = Commands::instance()->byId(CommandId::RemoveLayer()); break;
|
|
}
|
|
|
|
if (cmd) {
|
|
ctx->executeCommand(cmd);
|
|
return true;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
|
|
void Timeline::onCancel(Context* ctx)
|
|
{
|
|
if (m_rangeLocks == 0)
|
|
clearAndInvalidateRange();
|
|
|
|
clearClipboardRange();
|
|
invalidate();
|
|
}
|
|
|
|
void Timeline::onDragEnter(ui::DragEvent& e)
|
|
{
|
|
Widget::onDragEnter(e);
|
|
|
|
m_state = STATE_DRAGGING_EXTERNAL_RANGE;
|
|
setCursor(nullptr, Hit());
|
|
}
|
|
|
|
void Timeline::onDragLeave(ui::DragEvent& e)
|
|
{
|
|
Widget::onDragLeave(e);
|
|
|
|
m_state = STATE_STANDBY;
|
|
m_range.clearRange();
|
|
m_dropRange.clearRange();
|
|
invalidate();
|
|
flushRedraw();
|
|
flushMessages();
|
|
updateByMousePos(nullptr, e.position());
|
|
}
|
|
|
|
void Timeline::onDrag(ui::DragEvent& e)
|
|
{
|
|
Widget::onDrag(e);
|
|
|
|
m_range.clearRange();
|
|
setHot(hitTest(nullptr, e.position()));
|
|
switch (m_hot.part) {
|
|
case PART_NOTHING: invalidate(); [[fallthrough]];
|
|
case PART_ROW:
|
|
case PART_ROW_EYE_ICON:
|
|
case PART_ROW_CONTINUOUS_ICON:
|
|
case PART_ROW_PADLOCK_ICON:
|
|
case PART_ROW_TEXT: {
|
|
m_range.startRange(nullptr, -1, Range::kLayers);
|
|
break;
|
|
}
|
|
case PART_CEL:
|
|
m_range.startRange(m_rows[m_hot.layer].layer(), m_hot.frame, Range::kCels);
|
|
m_range.endRange(m_rows[m_hot.layer].layer(), m_hot.frame);
|
|
m_clk = m_hot;
|
|
invalidate();
|
|
break;
|
|
case PART_HEADER_FRAME: m_range.startRange(nullptr, -1, Range::kFrames); break;
|
|
}
|
|
|
|
updateDropRange(e.position());
|
|
flushRedraw();
|
|
flushMessages();
|
|
}
|
|
|
|
void Timeline::onDrop(ui::DragEvent& e)
|
|
{
|
|
using InsertionPoint = docapi::InsertionPoint;
|
|
using DroppedOn = docapi::DroppedOn;
|
|
Widget::onDrop(e);
|
|
|
|
// Determine at which frame and layer the content was dropped on.
|
|
frame_t frame = m_frame;
|
|
layer_t layerIndex = m_sprite->root()->getLayerIndex(m_layer);
|
|
InsertionPoint insert = InsertionPoint::BeforeLayer;
|
|
DroppedOn droppedOn = DroppedOn::Unspecified;
|
|
TRACE("m_dropRange.type() %d\n", m_dropRange.type());
|
|
switch (m_dropRange.type()) {
|
|
case Range::kCels:
|
|
frame = m_hot.frame;
|
|
if (!m_hot.veryBottom)
|
|
layerIndex = m_hot.layer;
|
|
droppedOn = DroppedOn::Cel;
|
|
insert = (m_dropTarget.vhit == DropTarget::Top ? InsertionPoint::AfterLayer :
|
|
InsertionPoint::BeforeLayer);
|
|
TRACE("m_hot part=%d veryBottom=%d frame=%d layer=%d\n",
|
|
m_hot.part,
|
|
m_hot.veryBottom,
|
|
m_hot.frame,
|
|
m_hot.layer);
|
|
break;
|
|
case Range::kFrames:
|
|
frame = m_dropRange.firstFrame();
|
|
droppedOn = DroppedOn::Frame;
|
|
if (m_dropTarget.hhit == DropTarget::After)
|
|
frame++;
|
|
break;
|
|
case Range::kLayers:
|
|
droppedOn = DroppedOn::Layer;
|
|
if (m_dropTarget.vhit != DropTarget::VeryBottom && !m_dropRange.selectedLayers().empty()) {
|
|
auto* selectedLayer = *m_dropRange.selectedLayers().begin();
|
|
layerIndex = getLayerIndex(selectedLayer);
|
|
}
|
|
insert = (m_dropTarget.vhit == DropTarget::Top ? InsertionPoint::AfterLayer :
|
|
InsertionPoint::BeforeLayer);
|
|
break;
|
|
}
|
|
|
|
#if _DEBUG
|
|
LOG(LogLevel::VERBOSE, "Dropped at frame: %d, and layerIndex: %d\n", frame, layerIndex);
|
|
#endif
|
|
|
|
if (e.hasPaths() || e.hasImage()) {
|
|
bool droppedImage = e.hasImage() && !e.hasPaths();
|
|
base::paths paths = e.getPaths();
|
|
auto surface = e.getImage();
|
|
|
|
execute_from_ui_thread([=] {
|
|
std::string txmsg;
|
|
std::unique_ptr<docapi::DocProvider> docProvider = nullptr;
|
|
if (droppedImage) {
|
|
txmsg = "Dropped image on timeline";
|
|
doc::ImageRef image = nullptr;
|
|
convert_surface_to_image(surface.get(), 0, 0, surface->width(), surface->height(), image);
|
|
docProvider = std::make_unique<DocProviderFromImage>(image);
|
|
}
|
|
else {
|
|
txmsg = "Dropped paths on timeline";
|
|
docProvider = std::make_unique<DocProviderFromPaths>(m_document->context(), paths);
|
|
}
|
|
|
|
Tx tx(m_document, txmsg);
|
|
DocApi docApi(m_document, tx);
|
|
docApi.dropDocumentsOnTimeline(m_document, frame, layerIndex, insert, droppedOn, *docProvider);
|
|
tx.commit();
|
|
m_document->notifyGeneralUpdate();
|
|
});
|
|
e.handled(true);
|
|
}
|
|
|
|
m_state = STATE_STANDBY;
|
|
m_range.clearRange();
|
|
m_dropRange.clearRange();
|
|
invalidate();
|
|
flushRedraw();
|
|
flushMessages();
|
|
}
|
|
|
|
int Timeline::tagFramesDuration(const Tag* tag) const
|
|
{
|
|
ASSERT(m_sprite);
|
|
ASSERT(tag);
|
|
|
|
const col_t fromFrame = m_adapter->toColFrame(fr_t(tag->fromFrame()));
|
|
const col_t toFrame = m_adapter->toColFrame(fr_t(tag->toFrame()));
|
|
int duration = 0;
|
|
for (col_t f = fromFrame; f <= toFrame; f = col_t(f + 1)) {
|
|
duration += m_adapter->frameDuration(f);
|
|
}
|
|
return duration;
|
|
}
|
|
|
|
int Timeline::selectedFramesDuration() const
|
|
{
|
|
ASSERT(m_adapter);
|
|
|
|
const col_t nframes = m_adapter->totalFrames();
|
|
int duration = 0;
|
|
for (col_t f = col_t(0); f < nframes; f = col_t(f + 1)) {
|
|
if (isFrameActive(f))
|
|
duration += m_adapter->frameDuration(f);
|
|
}
|
|
return duration; // TODO cache this value
|
|
}
|
|
|
|
void Timeline::setLayerVisibleFlag(const layer_t l, const bool state)
|
|
{
|
|
if (!validLayer(l))
|
|
return;
|
|
|
|
Row& row = m_rows[l];
|
|
Layer* layer = row.layer();
|
|
ASSERT(layer);
|
|
if (!layer)
|
|
return;
|
|
|
|
bool regenRows = false;
|
|
|
|
if (layer->isVisible() != state) {
|
|
m_document->setLayerVisibilityWithNotifications(layer, state);
|
|
|
|
// Regenerate rows because might change the flag of the children
|
|
// (the flag is propagated to the children in m_inheritedFlags).
|
|
if (layer->isGroup() && layer->isExpanded()) {
|
|
regenRows = true;
|
|
}
|
|
|
|
// Show parents too
|
|
if (!row.parentVisible() && state) {
|
|
layer = layer->parent();
|
|
while (layer) {
|
|
if (!layer->isVisible()) {
|
|
m_document->setLayerVisibilityWithNotifications(layer, true);
|
|
regenRows = true;
|
|
}
|
|
layer = layer->parent();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (regenRows) {
|
|
regenerateRows();
|
|
invalidate();
|
|
}
|
|
}
|
|
|
|
void Timeline::setLayerEditableFlag(const layer_t l, const bool state)
|
|
{
|
|
Row& row = m_rows[l];
|
|
Layer* layer = row.layer();
|
|
ASSERT(layer);
|
|
if (!layer)
|
|
return;
|
|
|
|
bool regenRows = false;
|
|
|
|
if (layer->isEditable() != state) {
|
|
m_document->setLayerEditableWithNotifications(layer, state);
|
|
|
|
if (layer->isGroup() && layer->isExpanded())
|
|
regenRows = true;
|
|
|
|
// Make parents editable too
|
|
if (!row.parentEditable() && state) {
|
|
layer = layer->parent();
|
|
while (layer) {
|
|
if (!layer->isEditable()) {
|
|
m_document->setLayerEditableWithNotifications(layer, true);
|
|
regenRows = true;
|
|
}
|
|
layer = layer->parent();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (regenRows) {
|
|
regenerateRows();
|
|
invalidate();
|
|
}
|
|
}
|
|
|
|
void Timeline::setLayerContinuousFlag(const layer_t l, const bool state)
|
|
{
|
|
Layer* layer = m_rows[l].layer();
|
|
ASSERT(layer);
|
|
if (!layer)
|
|
return;
|
|
|
|
if (layer->isImage() && layer->isContinuous() != state) {
|
|
layer->setContinuous(state);
|
|
invalidate();
|
|
}
|
|
}
|
|
|
|
void Timeline::setLayerCollapsedFlag(const layer_t l, const bool state)
|
|
{
|
|
Layer* layer = m_rows[l].layer();
|
|
ASSERT(layer);
|
|
if (!layer)
|
|
return;
|
|
|
|
if (layer->isGroup() && layer->isCollapsed() != state) {
|
|
layer->setCollapsed(state);
|
|
m_document->notifyLayerGroupCollapseChange(layer);
|
|
}
|
|
}
|
|
|
|
int Timeline::separatorX() const
|
|
{
|
|
return std::clamp(m_separator_x * guiscale(),
|
|
headerBoxWidth(),
|
|
std::max(bounds().w - guiscale(), headerBoxWidth()));
|
|
}
|
|
|
|
void Timeline::setSeparatorX(int newValue)
|
|
{
|
|
m_separator_x = std::max(0, newValue) / guiscale();
|
|
}
|
|
|
|
// Re-constructs the timeline adapter
|
|
void Timeline::updateTimelineAdapter(bool allTags)
|
|
{
|
|
m_adapter = std::make_unique<view::FullSpriteTimelineAdapter>(m_sprite);
|
|
invalidate();
|
|
}
|
|
|
|
// static
|
|
gfx::Color Timeline::highlightColor(const gfx::Color color)
|
|
{
|
|
int r, g, b;
|
|
r = gfx::getr(color) + 64; // TODO make this customizable in the theme XML?
|
|
g = gfx::getg(color) + 64;
|
|
b = gfx::getb(color) + 64;
|
|
r = std::clamp(r, 0, 255);
|
|
g = std::clamp(g, 0, 255);
|
|
b = std::clamp(b, 0, 255);
|
|
return gfx::rgba(r, g, b, gfx::geta(color));
|
|
}
|
|
|
|
} // namespace app
|