// Aseprite // Copyright (C) 2001-2015 David Capello // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "app/ui/timeline.h" #include "app/app.h" #include "app/app_menus.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/document.h" #include "app/document_api.h" #include "app/document_range_ops.h" #include "app/document_undo.h" #include "app/loop_tag.h" #include "app/modules/editors.h" #include "app/modules/gfx.h" #include "app/modules/gui.h" #include "app/transaction.h" #include "app/ui/app_menuitem.h" #include "app/ui/configure_timeline_popup.h" #include "app/ui/document_view.h" #include "app/ui/editor/editor.h" #include "app/ui/skin/skin_theme.h" #include "app/ui/skin/style.h" #include "app/ui/status_bar.h" #include "app/ui_context.h" #include "app/util/clipboard.h" #include "base/convert_to.h" #include "base/memory.h" #include "doc/doc.h" #include "doc/document_event.h" #include "doc/frame_tag.h" #include "gfx/point.h" #include "gfx/rect.h" #include "she/font.h" #include "ui/ui.h" #include #include // Size of the thumbnail in the screen (width x height), the really // size of the thumbnail bitmap is specified in the // 'generate_thumbnail' routine. #define THUMBSIZE (12*guiscale()) // Height of the headers. #define HDRSIZE THUMBSIZE // Width of the frames. #define FRMSIZE THUMBSIZE // Height of the layers. #define LAYSIZE THUMBSIZE // Space between icons and other information in the layer. #define ICONSEP (2*guiscale()) #define OUTLINE_WIDTH (2*guiscale()) // TODO theme specific // Space between the icon-bitmap and the edge of the surrounding button. #define ICONBORDER 0 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_HEADER_FRAME_TAGS, PART_LAYER, PART_LAYER_EYE_ICON, PART_LAYER_PADLOCK_ICON, PART_LAYER_CONTINUOUS_ICON, PART_LAYER_TEXT, PART_CEL, PART_RANGE_OUTLINE, PART_FRAME_TAG, }; Timeline::Timeline() : Widget(kGenericWidget) , m_context(UIContext::instance()) , m_editor(NULL) , m_document(NULL) , m_sprite(NULL) , m_scroll_x(0) , m_scroll_y(0) , m_separator_x(100 * guiscale()) , m_separator_w(1) , m_confPopup(NULL) , m_clipboard_timer(100, this) , m_offset_count(0) , m_scroll(false) { m_ctxConn = m_context->AfterCommandExecution.connect(&Timeline::onAfterCommandExecution, this); m_context->documents().addObserver(this); setDoubleBuffered(true); } Timeline::~Timeline() { m_clipboard_timer.stop(); detachDocument(); m_context->documents().removeObserver(this); delete m_confPopup; } void Timeline::updateUsingEditor(Editor* editor) { // As a sprite editor was selected, it looks like the user wants to // execute commands targetting the editor 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. m_range.disableRange(); invalidate(); detachDocument(); // We always update the editor. In this way the timeline keeps in // sync with the active editor. m_editor = editor; if (m_editor) m_editor->addObserver(this); else return; // No editor specified. DocumentLocation location; DocumentView* view = m_editor->getDocumentView(); view->getDocumentLocation(&location); location.document()->addObserver(this); // If we are already in the same position as the "editor", we don't // need to update the at all timeline. if (m_document == location.document() && m_sprite == location.sprite() && m_layer == location.layer() && m_frame == location.frame()) return; m_document = location.document(); m_sprite = location.sprite(); m_layer = location.layer(); m_frame = location.frame(); m_state = STATE_STANDBY; m_hot.part = PART_NOTHING; m_clk.part = PART_NOTHING; setFocusStop(true); regenerateLayers(); } void Timeline::detachDocument() { if (m_document) { m_document->removeObserver(this); m_document = NULL; } if (m_editor) { m_editor->removeObserver(this); m_editor = NULL; } invalidate(); } bool Timeline::isMovingCel() const { return (m_state == STATE_MOVING_RANGE && m_range.type() == Range::kCels); } void Timeline::setLayer(Layer* layer) { ASSERT(m_editor != NULL); m_layer = layer; invalidate(); if (m_editor->layer() != layer) m_editor->setLayer(m_layer); } void Timeline::setFrame(frame_t frame) { ASSERT(m_editor != NULL); // ASSERT(frame >= 0 && frame < m_sprite->totalFrames()); if (frame < 0) frame = firstFrame(); else if (frame >= m_sprite->totalFrames()) frame = frame_t(m_sprite->totalFrames()-1); m_frame = frame; invalidate(); if (m_editor->frame() != frame) m_editor->setFrame(m_frame); } void Timeline::activateClipboardRange() { m_clipboard_timer.start(); invalidate(); } bool Timeline::onProcessMessage(Message* msg) { switch (msg->type()) { case kTimerMessage: if (static_cast(msg)->timer() == &m_clipboard_timer) { Document* clipboard_document; DocumentRange clipboard_range; clipboard::get_document_range_info( &clipboard_document, &clipboard_range); if (isVisible() && m_document && clipboard_document == m_document) { // Set offset to make selection-movement effect if (m_offset_count < 7) m_offset_count++; else m_offset_count = 0; } else if (m_clipboard_timer.isRunning()) { m_clipboard_timer.stop(); } invalidate(); } break; case kMouseDownMessage: { MouseMessage* mouseMsg = static_cast(msg); if (!m_document) break; if (mouseMsg->middle() || she::is_key_pressed(kKeySpace)) { captureMouse(); m_state = STATE_SCROLLING; m_oldPos = static_cast(msg)->position(); return true; } // 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_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) { setFrame(m_clk.frame); m_state = STATE_SELECTING_FRAMES; m_range.startRange(getLayerIndex(m_layer), m_clk.frame, Range::kFrames); } break; } case PART_LAYER_TEXT: { LayerIndex old_layer = getLayerIndex(m_layer); bool selectLayer = (mouseMsg->left() || !isLayerActive(m_clk.layer)); if (selectLayer) { // Did the user select another layer? if (old_layer != m_clk.layer) { setLayer(m_layers[m_clk.layer]); invalidate(); } } // Change the scroll to show the new selected layer/cel. showCel(m_clk.layer, m_frame); if (selectLayer) { m_state = STATE_SELECTING_LAYERS; m_range.startRange(m_clk.layer, m_frame, Range::kLayers); } break; } case PART_LAYER_EYE_ICON: break; case PART_LAYER_PADLOCK_ICON: break; case PART_LAYER_CONTINUOUS_ICON: break; case PART_CEL: { LayerIndex old_layer = getLayerIndex(m_layer); bool selectCel = (mouseMsg->left() || !isLayerActive(m_clk.layer) || !isFrameActive(m_clk.frame)); frame_t old_frame = m_frame; // Select the new clicked-part. if (old_layer != m_clk.layer || old_frame != m_clk.frame) { setLayer(m_layers[m_clk.layer]); setFrame(m_clk.frame); invalidate(); } // Change the scroll to show the new selected cel. showCel(m_clk.layer, m_frame); if (selectCel) { m_state = STATE_SELECTING_CELS; m_range.startRange(m_clk.layer, m_clk.frame, Range::kCels); } invalidate(); break; } case PART_RANGE_OUTLINE: m_state = STATE_MOVING_RANGE; 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(msg)->position() - getBounds().getOrigin(); Hit hit; setHot(hit = hitTest(msg, mousePos)); if (hasCapture()) { switch (m_state) { case STATE_SCROLLING: { gfx::Point absMousePos = static_cast(msg)->position(); setScroll( m_scroll_x - (absMousePos.x - m_oldPos.x), m_scroll_y - (absMousePos.y - m_oldPos.y)); m_oldPos = absMousePos; return true; } case STATE_MOVING_ONIONSKIN_RANGE_LEFT: { int newValue = m_origFrames + (m_clk.frame - hit.frame); docPref().onionskin.prevFrames(MAX(0, newValue)); invalidate(); return true; } case STATE_MOVING_ONIONSKIN_RANGE_RIGHT: int newValue = m_origFrames - (m_clk.frame - hit.frame); docPref().onionskin.nextFrames(MAX(0, newValue)); invalidate(); return true; } // 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) { m_separator_x = MAX(0, mousePos.x); invalidate(); return true; } } updateDropRange(mousePos); if (hasCapture()) { switch (m_state) { case STATE_SELECTING_LAYERS: { if (m_layer != m_layers[hit.layer]) { m_range.endRange(hit.layer, m_frame); setLayer(m_layers[m_clk.layer = hit.layer]); } break; } case STATE_SELECTING_FRAMES: { m_range.endRange(getLayerIndex(m_layer), hit.frame); setFrame(m_clk.frame = hit.frame); break; } case STATE_SELECTING_CELS: if ((m_layer != m_layers[hit.layer]) || (m_frame != hit.frame)) { m_range.endRange(hit.layer, hit.frame); setLayer(m_layers[m_clk.layer = hit.layer]); setFrame(m_clk.frame = hit.frame); } break; } } updateStatusBar(msg); return true; } case kMouseUpMessage: if (hasCapture()) { ASSERT(m_document != NULL); MouseMessage* mouseMsg = static_cast(msg); if (m_state == STATE_SCROLLING) { m_state = STATE_STANDBY; releaseMouse(); return true; } switch (m_hot.part) { case PART_NOTHING: case PART_SEPARATOR: case PART_HEADER_LAYER: // Do nothing. break; case PART_HEADER_EYE: { bool newVisibleState = !allLayersVisible(); for (size_t i=0; isetVisible(newVisibleState); // Redraw all views. m_document->notifyGeneralUpdate(); break; } case PART_HEADER_PADLOCK: { bool newEditableState = !allLayersUnlocked(); for (size_t i=0; isetEditable(newEditableState); break; } case PART_HEADER_CONTINUOUS: { bool newContinuousState = !allLayersContinuous(); for (size_t i=0; isetContinuous(newContinuousState); break; } case PART_HEADER_GEAR: { gfx::Rect gearBounds = getPartBounds(Hit(PART_HEADER_GEAR)).offset(getBounds().getOrigin()); if (!m_confPopup) { ConfigureTimelinePopup* popup = new ConfigureTimelinePopup(); popup->remapWindow(); m_confPopup = popup; } if (!m_confPopup->isVisible()) { m_confPopup->moveWindow(gfx::Rect( gearBounds.x, gearBounds.y-m_confPopup->getBounds().h, m_confPopup->getBounds().w, m_confPopup->getBounds().h)); m_confPopup->openWindow(); } else m_confPopup->closeWindow(NULL); break; } case PART_HEADER_ONIONSKIN: { docPref().onionskin.active(!docPref().onionskin.active()); break; } case PART_HEADER_FRAME: // Show the frame pop-up menu. if (mouseMsg->right()) { if (m_clk.frame == m_hot.frame) { Menu* popup_menu = AppMenus::instance()->getFramePopupMenu(); if (popup_menu) popup_menu->showPopup(mouseMsg->position()); } } break; case PART_LAYER_TEXT: // Show the layer pop-up menu. if (mouseMsg->right()) { if (m_clk.layer == m_hot.layer) { Menu* popup_menu = AppMenus::instance()->getLayerPopupMenu(); if (popup_menu != NULL) popup_menu->showPopup(mouseMsg->position()); } } break; case PART_LAYER_EYE_ICON: // Hide/show layer. if (m_hot.layer == m_clk.layer && validLayer(m_hot.layer)) { Layer* layer = m_layers[m_clk.layer]; ASSERT(layer != NULL); layer->setVisible(!layer->isVisible()); // Redraw all views. m_document->notifyGeneralUpdate(); } break; case PART_LAYER_PADLOCK_ICON: // Lock/unlock layer. if (m_hot.layer == m_clk.layer && validLayer(m_hot.layer)) { Layer* layer = m_layers[m_clk.layer]; ASSERT(layer != NULL); layer->setEditable(!layer->isEditable()); } break; case PART_LAYER_CONTINUOUS_ICON: if (m_hot.layer == m_clk.layer && validLayer(m_hot.layer)) { Layer* layer = m_layers[m_clk.layer]; ASSERT(layer != NULL); layer->setContinuous(!layer->isContinuous()); } break; case PART_CEL: { // Show the cel pop-up menu. if (mouseMsg->right()) { Menu* popup_menu = (m_state == STATE_MOVING_RANGE && m_range.type() == Range::kCels) ? AppMenus::instance()->getCelMovementPopupMenu(): AppMenus::instance()->getCelPopupMenu(); if (popup_menu) popup_menu->showPopup(mouseMsg->position()); } break; } case PART_FRAME_TAG: if (m_clk.frameTag) { Params params; params.set("id", base::convert_to(m_clk.frameTag->id()).c_str()); // As the m_clk.frameTag can be deleted with // RemoveFrameTag command, we've to clean all references // to it from Hit() structures. cleanClk(); m_hot = m_clk; if (mouseMsg->right()) { Menu* popup_menu = AppMenus::instance()->getFrameTagPopupMenu(); if (popup_menu) { AppMenuItem::setContextParams(params); popup_menu->showPopup(mouseMsg->position()); } } else if (mouseMsg->left()) { Command* command = CommandsModule::instance() ->getCommandByName(CommandId::FrameTagProperties); UIContext::instance()->executeCommand(command, params); } } break; } if (mouseMsg->left() && m_state == STATE_MOVING_RANGE && m_dropRange.type() != Range::kNone) { dropRange(isCopyKeyPressed(mouseMsg) ? Timeline::kCopy: Timeline::kMove); } // 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())); releaseMouse(); updateStatusBar(msg); return true; } break; case kDoubleClickMessage: switch (m_hot.part) { case PART_LAYER_TEXT: { Command* command = CommandsModule::instance() ->getCommandByName(CommandId::LayerProperties); UIContext::instance()->executeCommand(command); return true; } case PART_HEADER_FRAME: { Command* command = CommandsModule::instance() ->getCommandByName(CommandId::FrameProperties); Params params; params.set("frame", "current"); UIContext::instance()->executeCommand(command, params); return true; } case PART_CEL: { Command* command = CommandsModule::instance() ->getCommandByName(CommandId::CelProperties); UIContext::instance()->executeCommand(command); return true; } } break; case kKeyDownMessage: { bool used = false; switch (static_cast(msg)->scancode()) { case kKeyEsc: if (m_state == STATE_STANDBY) { m_range.disableRange(); invalidate(); } else { m_state = STATE_STANDBY; } used = true; break; case kKeySpace: { m_scroll = true; used = true; break; } } updateByMousePos(msg, ui::get_mouse_position() - getBounds().getOrigin()); if (used) return true; break; } case kKeyUpMessage: { bool used = false; switch (static_cast(msg)->scancode()) { case kKeySpace: { m_scroll = false; // We have to clear all the kKeySpace keys in buffer. she::clear_keyboard_buffer(); used = true; break; } } updateByMousePos(msg, ui::get_mouse_position() - getBounds().getOrigin()); if (used) return true; break; } case kMouseWheelMessage: if (m_document) { int dz = static_cast(msg)->wheelDelta().y; int dx = 0; int dy = 0; dx += static_cast(msg)->wheelDelta().x; if (msg->ctrlPressed()) dx = dz * FRMSIZE; else dy = dz * LAYSIZE; if (msg->shiftPressed()) { dx *= 3; dy *= 3; } setScroll(m_scroll_x+dx, m_scroll_y+dy); } break; case kSetCursorMessage: if (m_document) { gfx::Point mousePos = static_cast(msg)->position(); setCursor(msg, m_hot); return true; } break; } return Widget::onProcessMessage(msg); } void Timeline::onPreferredSize(PreferredSizeEvent& ev) { // This doesn't matter, the AniEditor'll use the entire screen anyway. ev.setPreferredSize(Size(32, 32)); } void Timeline::onPaint(ui::PaintEvent& ev) { Graphics* g = ev.getGraphics(); bool noDoc = (m_document == NULL); if (noDoc) goto paintNoDoc; try { // Lock the sprite to read/render it. const DocumentReader documentReader(m_document); LayerIndex layer, first_layer, last_layer; frame_t frame, first_frame, last_frame; getDrawableLayers(g, &first_layer, &last_layer); getDrawableFrames(g, &first_frame, &last_frame); drawTop(g); // Draw the header for layers. drawHeader(g); // Draw the header for each visible frame. { IntersectClip clip(g, getFrameHeadersBounds()); if (clip) { for (frame=first_frame; frame<=last_frame; ++frame) drawHeaderFrame(g, frame); // Draw onionskin indicators. gfx::Rect bounds = getOnionskinFramesBounds(); if (!bounds.isEmpty()) { drawPart(g, bounds, NULL, skinTheme()->styles.timelineOnionskinRange(), false, false, false); } } } // Draw each visible layer. for (layer=last_layer; layer>=first_layer; --layer) { { IntersectClip clip(g, getLayerHeadersBounds()); if (clip) drawLayer(g, layer); } // Get the first CelIterator to be drawn (it is the first cel with cel->frame >= first_frame) CelIterator it, end; Layer* layerPtr = m_layers[layer]; if (layerPtr->isImage()) { it = static_cast(layerPtr)->getCelBegin(); end = static_cast(layerPtr)->getCelEnd(); for (; it != end && (*it)->frame() < first_frame; ++it) ; } IntersectClip clip(g, getCelsBounds()); if (!clip) continue; // Draw every visible cel for each layer. for (frame=first_frame; frame<=last_frame; ++frame) { Cel* cel = (layerPtr->isImage() && it != end && (*it)->frame() == frame ? *it: NULL); drawCel(g, layer, frame, cel); if (cel) ++it; // Go to next cel } } drawPaddings(g); drawFrameTags(g); drawRangeOutline(g); drawClipboardRange(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 LockedDocumentException&) { noDoc = true; } paintNoDoc:; if (noDoc) drawPart(g, getClientBounds(), NULL, skinTheme()->styles.timelinePadding()); } void Timeline::onAfterCommandExecution(Command* command) { if (!m_document) return; regenerateLayers(); showCurrentCel(); invalidate(); } void Timeline::onRemoveDocument(doc::Document* document) { if (document == m_document) detachDocument(); } void Timeline::onAddLayer(doc::DocumentEvent& ev) { ASSERT(ev.layer() != NULL); setLayer(ev.layer()); regenerateLayers(); showCurrentCel(); invalidate(); } void Timeline::onAfterRemoveLayer(doc::DocumentEvent& ev) { Sprite* sprite = ev.sprite(); Layer* layer = ev.layer(); // If the layer that was removed is the selected one if (layer == getLayer()) { LayerFolder* parent = layer->parent(); Layer* layer_select = NULL; // Select previous layer, or next layer, or the parent (if it is // not the main layer of sprite set). if (layer->getPrevious()) layer_select = layer->getPrevious(); else if (layer->getNext()) layer_select = layer->getNext(); else if (parent != sprite->folder()) layer_select = parent; setLayer(layer_select); } regenerateLayers(); showCurrentCel(); invalidate(); } void Timeline::onAddFrame(doc::DocumentEvent& ev) { setFrame(ev.frame()); showCurrentCel(); clearClipboardRange(); invalidate(); } void Timeline::onRemoveFrame(doc::DocumentEvent& ev) { // Adjust current frame of all editors that are in a frame more // advanced that the removed one. if (getFrame() > ev.frame()) { setFrame(getFrame()-1); } // 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 (getFrame() >= sprite()->totalFrames()) { setFrame(sprite()->lastFrame()); } showCurrentCel(); clearClipboardRange(); invalidate(); } void Timeline::onSelectionChanged(doc::DocumentEvent& ev) { m_range.disableRange(); clearClipboardRange(); invalidate(); } void Timeline::onAfterFrameChanged(Editor* editor) { setFrame(editor->frame()); if (!hasCapture()) m_range.disableRange(); showCurrentCel(); clearClipboardRange(); invalidate(); } void Timeline::onAfterLayerChanged(Editor* editor) { setLayer(editor->layer()); if (!hasCapture()) m_range.disableRange(); showCurrentCel(); clearClipboardRange(); invalidate(); } 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 (isCopyKeyPressed(msg)) ui::set_mouse_cursor(kArrowPlusCursor); else ui::set_mouse_cursor(kMoveCursor); } // 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) { ui::set_mouse_cursor(kMoveCursor); } else if (hit.part == PART_SEPARATOR) { ui::set_mouse_cursor(kSizeWECursor); } else if (hit.part == PART_FRAME_TAG) { ui::set_mouse_cursor(kHandCursor); } else { ui::set_mouse_cursor(kArrowCursor); } } void Timeline::getDrawableLayers(ui::Graphics* g, LayerIndex* first_layer, LayerIndex* last_layer) { int hpx = (getClientBounds().h - HDRSIZE); LayerIndex i = lastLayer() - LayerIndex((m_scroll_y+hpx) / LAYSIZE); i = MID(firstLayer(), i, lastLayer()); LayerIndex j = i + LayerIndex(hpx / LAYSIZE); if (!m_layers.empty()) j = MID(firstLayer(), j, lastLayer()); else j = LayerIndex::NoLayer; *first_layer = i; *last_layer = j; } void Timeline::getDrawableFrames(ui::Graphics* g, frame_t* first_frame, frame_t* last_frame) { *first_frame = frame_t((m_separator_w + m_scroll_x) / FRMSIZE); *last_frame = *first_frame + frame_t((getClientBounds().w - m_separator_w) / FRMSIZE); } void Timeline::drawPart(ui::Graphics* g, const gfx::Rect& bounds, const char* text, Style* style, bool is_active, bool is_hover, bool is_clicked) { IntersectClip clip(g, bounds); if (!clip) return; Style::State state; if (is_active) state += Style::active(); if (is_hover) state += Style::hover(); if (is_clicked) state += Style::clicked(); style->paint(g, bounds, text, state); } void Timeline::drawClipboardRange(ui::Graphics* g) { Document* clipboard_document; DocumentRange clipboard_range; clipboard::get_document_range_info( &clipboard_document, &clipboard_range); if (!m_document || clipboard_document != m_document) return; if (!m_clipboard_timer.isRunning()) m_clipboard_timer.start(); CheckedDrawMode checked(g, m_offset_count); g->drawRect(0, getRangeBounds(clipboard_range)); } void Timeline::drawTop(ui::Graphics* g) { g->fillRect(skinTheme()->colors.workspace(), getPartBounds(Hit(PART_TOP))); } void Timeline::drawHeader(ui::Graphics* g) { SkinTheme::Styles& styles = skinTheme()->styles; bool allInvisible = allLayersInvisible(); bool allLocked = allLayersLocked(); bool allContinuous = allLayersContinuous(); drawPart(g, getPartBounds(Hit(PART_HEADER_EYE)), NULL, 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)), NULL, 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)), NULL, 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)), NULL, styles.timelineGear(), false, 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_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, frame_t frame) { bool is_active = isFrameActive(frame); bool is_hover = (m_hot.part == PART_HEADER_FRAME && m_hot.frame == frame); bool is_clicked = (m_clk.part == PART_HEADER_FRAME && m_clk.frame == frame); gfx::Rect bounds = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), frame)); IntersectClip clip(g, bounds); if (!clip) return; // Draw the header for the layers. char buf[256]; std::sprintf(buf, "%d", (frame+1)%100); // Draw only the first two digits. she::Font* oldFont = g->getFont(); g->setFont(skinTheme()->getMiniFont()); drawPart(g, bounds, buf, skinTheme()->styles.timelineBox(), is_active, is_hover, is_clicked); g->setFont(oldFont); } void Timeline::drawLayer(ui::Graphics* g, LayerIndex layerIdx) { SkinTheme::Styles& styles = skinTheme()->styles; Layer* layer = m_layers[layerIdx]; bool is_active = isLayerActive(layerIdx); bool hotlayer = (m_hot.layer == layerIdx); bool clklayer = (m_clk.layer == layerIdx); gfx::Rect bounds = getPartBounds(Hit(PART_LAYER, layerIdx, firstFrame())); IntersectClip clip(g, bounds); if (!clip) return; // Draw the eye (visible flag). bounds = getPartBounds(Hit(PART_LAYER_EYE_ICON, layerIdx)); drawPart(g, bounds, NULL, layer->isVisible() ? styles.timelineOpenEye(): styles.timelineClosedEye(), is_active, (hotlayer && m_hot.part == PART_LAYER_EYE_ICON), (clklayer && m_clk.part == PART_LAYER_EYE_ICON)); // Draw the padlock (editable flag). bounds = getPartBounds(Hit(PART_LAYER_PADLOCK_ICON, layerIdx)); drawPart(g, bounds, NULL, layer->isEditable() ? styles.timelineOpenPadlock(): styles.timelineClosedPadlock(), is_active, (hotlayer && m_hot.part == PART_LAYER_PADLOCK_ICON), (clklayer && m_clk.part == PART_LAYER_PADLOCK_ICON)); // Draw the continuous flag. bounds = getPartBounds(Hit(PART_LAYER_CONTINUOUS_ICON, layerIdx)); drawPart(g, bounds, NULL, layer->isContinuous() ? styles.timelineContinuous(): styles.timelineDiscontinuous(), is_active, (hotlayer && m_hot.part == PART_LAYER_CONTINUOUS_ICON), (clklayer && m_clk.part == PART_LAYER_CONTINUOUS_ICON)); // Draw the layer's name. bounds = getPartBounds(Hit(PART_LAYER_TEXT, layerIdx)); drawPart(g, bounds, layer->name().c_str(), styles.timelineLayer(), is_active, (hotlayer && m_hot.part == PART_LAYER_TEXT), (clklayer && m_clk.part == PART_LAYER_TEXT)); // 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_LAYER_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, LayerIndex layerIndex, frame_t frame, Cel* cel) { SkinTheme::Styles& styles = skinTheme()->styles; Layer* layer = m_layers[layerIndex]; Image* image = (cel ? cel->image(): NULL); bool is_hover = (m_hot.part == PART_CEL && m_hot.layer == layerIndex && m_hot.frame == frame); bool is_active = (isLayerActive(layerIndex) || isFrameActive(frame)); bool is_empty = (image == NULL); gfx::Rect bounds = getPartBounds(Hit(PART_CEL, layerIndex, frame)); IntersectClip clip(g, bounds); if (!clip) return; if (layer == m_layer && frame == m_frame) drawPart(g, bounds, NULL, styles.timelineSelectedCel(), false, false, true); else drawPart(g, bounds, NULL, styles.timelineBox(), is_active, is_hover); skin::Style* style; bool fromLeft = false; bool fromRight = false; if (is_empty) { style = styles.timelineEmptyFrame(); } else { Cel* left = (layer->isImage() ? layer->cel(frame-1): NULL); Cel* right = (layer->isImage() ? layer->cel(frame+1): NULL); 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, NULL, style, is_active, is_hover); // Draw decorators to link the activeCel with its links. if (layer == m_layer) { Cel* activeCel = m_layer->cel(m_frame); if (activeCel) drawCelLinkDecorators(g, bounds, cel, activeCel, frame, is_active, is_hover); } } void Timeline::drawCelLinkDecorators(ui::Graphics* g, const gfx::Rect& bounds, Cel* cel, Cel* activeCel, frame_t frame, bool is_active, bool is_hover) { SkinTheme::Styles& styles = skinTheme()->styles; ObjectId imageId = activeCel->image()->id(); // Link in some cel at the left side bool left = false; for (frame_t fr=frame-1; fr>=0; --fr) { Cel* c = m_layer->cel(fr); if (c && c->image()->id() == imageId) { left = true; break; } } // Link in some cel at the right side bool right = false; for (frame_t fr=frame+1; frtotalFrames(); ++fr) { Cel* c = m_layer->cel(fr); if (c && c->image()->id() == imageId) { right = true; break; } } if (!cel || cel->image()->id() != imageId) { if (left && right) drawPart(g, bounds, NULL, styles.timelineBothLinks(), is_active, is_hover); } else { if (left) { Cel* prevCel = m_layer->cel(cel->frame()-1); if (!prevCel || prevCel->image()->id() != imageId) drawPart(g, bounds, NULL, styles.timelineLeftLink(), is_active, is_hover); } if (right) { Cel* nextCel = m_layer->cel(cel->frame()+1); if (!nextCel || nextCel->image()->id() != imageId) drawPart(g, bounds, NULL, styles.timelineRightLink(), is_active, is_hover); } } } void Timeline::drawFrameTags(ui::Graphics* g) { IntersectClip clip(g, getPartBounds(Hit(PART_HEADER_FRAME_TAGS))); if (!clip) return; SkinTheme* theme = skinTheme(); SkinTheme::Styles& styles = theme->styles; if (!m_sprite->frameTags().empty()) { g->fillRect(theme->colors.workspace(), gfx::Rect( 0, getFont()->height(), getClientBounds().w, theme->dimensions.timelineTagsAreaHeight())); } for (FrameTag* frameTag : m_sprite->frameTags()) { gfx::Rect bounds1 = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), frameTag->fromFrame())); gfx::Rect bounds2 = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), frameTag->toFrame())); gfx::Rect bounds = bounds1.createUnion(bounds2); bounds.y -= theme->dimensions.timelineTagsAreaHeight(); { IntersectClip clip(g, bounds); if (clip) drawPart(g, bounds, NULL, styles.timelineLoopRange()); } { bounds = getPartBounds(Hit(PART_FRAME_TAG, LayerIndex(0), 0, frameTag)); gfx::Color bg = frameTag->color(); if (m_clk.part == PART_FRAME_TAG && m_clk.frameTag == frameTag) { bg = color_utils::blackandwhite_neg(bg); } else if (m_hot.part == PART_FRAME_TAG && m_hot.frameTag == frameTag) { int r, g, b; r = gfx::getr(bg)+32; g = gfx::getg(bg)+32; b = gfx::getb(bg)+32; r = MID(0, r, 255); g = MID(0, g, 255); b = MID(0, b, 255); bg = gfx::rgba(r, g, b, gfx::geta(bg)); } g->fillRect(bg, bounds); bounds.y += 2*ui::guiscale(); bounds.x += 2*ui::guiscale(); g->drawString( frameTag->name(), color_utils::blackandwhite_neg(bg), gfx::ColorNone, bounds.getOrigin()); } } } void Timeline::drawRangeOutline(ui::Graphics* g) { SkinTheme::Styles& styles = skinTheme()->styles; gfx::Rect clipBounds; switch (m_range.type()) { case Range::kCels: clipBounds = getCelsBounds(); break; case Range::kFrames: clipBounds = getFrameHeadersBounds(); break; case Range::kLayers: clipBounds = getLayerHeadersBounds(); break; } IntersectClip clip(g, clipBounds.enlarge(OUTLINE_WIDTH)); if (!clip) return; Style::State state; if (m_range.enabled()) state += Style::active(); if (m_hot.part == PART_RANGE_OUTLINE) state += Style::hover(); gfx::Rect bounds = getPartBounds(Hit(PART_RANGE_OUTLINE)); styles.timelineRangeOutline()->paint(g, bounds, NULL, state); Range drop = m_dropRange; gfx::Rect dropBounds = getRangeBounds(drop); switch (drop.type()) { case Range::kCels: { dropBounds = dropBounds.enlarge(OUTLINE_WIDTH); styles.timelineRangeOutline()->paint(g, dropBounds, NULL, Style::active()); break; } case Range::kFrames: { int w = 5 * guiscale(); // TODO get width from the skin info if (m_dropTarget.hhit == DropTarget::Before) dropBounds.x -= w/2; else if (drop == m_range) dropBounds.x = dropBounds.x + getRangeBounds(m_range).w - w/2; else dropBounds.x = dropBounds.x + dropBounds.w - w/2; dropBounds.w = w; styles.timelineDropFrameDeco()->paint(g, dropBounds, NULL, Style::State()); break; } case Range::kLayers: { int h = 5 * guiscale(); // TODO get height from the skin info if (m_dropTarget.vhit == DropTarget::Top) dropBounds.y -= h/2; else if (drop == m_range) dropBounds.y = dropBounds.y + getRangeBounds(m_range).h - h/2; else dropBounds.y = dropBounds.y + dropBounds.h - h/2; dropBounds.h = h; styles.timelineDropLayerDeco()->paint(g, dropBounds, NULL, Style::State()); break; } } } void Timeline::drawPaddings(ui::Graphics* g) { SkinTheme::Styles& styles = skinTheme()->styles; gfx::Rect client = getClientBounds(); gfx::Rect bottomLayer; gfx::Rect lastFrame; int top = topHeight(); if (!m_layers.empty()) { bottomLayer = getPartBounds(Hit(PART_LAYER, 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 = getClientBounds(); rc.w = m_separator_x; int h = topHeight() + HDRSIZE; rc.y += h; rc.h -= h; return rc; } gfx::Rect Timeline::getFrameHeadersBounds() const { gfx::Rect rc = getClientBounds(); rc.x += m_separator_x; rc.y += topHeight(); rc.w -= m_separator_x; rc.h = HDRSIZE; return rc; } gfx::Rect Timeline::getOnionskinFramesBounds() const { DocumentPreferences& docPref = this->docPref(); if (!docPref.onionskin.active()) return gfx::Rect(); frame_t firstFrame = m_frame - docPref.onionskin.prevFrames(); frame_t lastFrame = 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 = getClientBounds(); rc.x += m_separator_x; rc.w -= m_separator_x; rc.y += HDRSIZE + topHeight(); rc.h -= HDRSIZE - topHeight(); return rc; } gfx::Rect Timeline::getPartBounds(const Hit& hit) const { gfx::Rect bounds = getClientBounds(); 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 + m_separator_x, bounds.y + y, m_separator_x + m_separator_w, bounds.h - y); case PART_HEADER_EYE: return gfx::Rect(bounds.x + FRMSIZE*0, bounds.y + y, FRMSIZE, HDRSIZE); case PART_HEADER_PADLOCK: return gfx::Rect(bounds.x + FRMSIZE*1, bounds.y + y, FRMSIZE, HDRSIZE); case PART_HEADER_CONTINUOUS: return gfx::Rect(bounds.x + FRMSIZE*2, bounds.y + y, FRMSIZE, HDRSIZE); case PART_HEADER_GEAR: return gfx::Rect(bounds.x + FRMSIZE*3, bounds.y + y, FRMSIZE, HDRSIZE); case PART_HEADER_ONIONSKIN: return gfx::Rect(bounds.x + FRMSIZE*4, bounds.y + y, FRMSIZE, HDRSIZE); case PART_HEADER_LAYER: return gfx::Rect(bounds.x + FRMSIZE*5, bounds.y + y, m_separator_x - FRMSIZE*5, HDRSIZE); case PART_HEADER_FRAME: if (validFrame(hit.frame)) { return gfx::Rect( bounds.x + m_separator_x + m_separator_w - 1 + FRMSIZE*hit.frame - m_scroll_x, bounds.y + y, FRMSIZE, HDRSIZE); } break; case PART_HEADER_FRAME_TAGS: return gfx::Rect( bounds.x + m_separator_x + m_separator_w - 1, bounds.y, bounds.w - m_separator_x - m_separator_w + 1, y); case PART_LAYER: if (validLayer(hit.layer)) { return gfx::Rect(bounds.x, bounds.y + y + HDRSIZE + LAYSIZE*(lastLayer()-hit.layer) - m_scroll_y, m_separator_x, LAYSIZE); } break; case PART_LAYER_EYE_ICON: if (validLayer(hit.layer)) { return gfx::Rect(bounds.x, bounds.y + y + HDRSIZE + LAYSIZE*(lastLayer()-hit.layer) - m_scroll_y, FRMSIZE, LAYSIZE); } break; case PART_LAYER_PADLOCK_ICON: if (validLayer(hit.layer)) { return gfx::Rect(bounds.x + FRMSIZE, bounds.y + y + HDRSIZE + LAYSIZE*(lastLayer()-hit.layer) - m_scroll_y, FRMSIZE, LAYSIZE); } break; case PART_LAYER_CONTINUOUS_ICON: if (validLayer(hit.layer)) { return gfx::Rect(bounds.x + 2*FRMSIZE, bounds.y + y + HDRSIZE + LAYSIZE*(lastLayer()-hit.layer) - m_scroll_y, FRMSIZE, LAYSIZE); } break; case PART_LAYER_TEXT: if (validLayer(hit.layer)) { int x = FRMSIZE*3; return gfx::Rect(bounds.x + x, bounds.y + y + HDRSIZE + LAYSIZE*(lastLayer()-hit.layer) - m_scroll_y, m_separator_x - x, LAYSIZE); } break; case PART_CEL: if (validLayer(hit.layer) && hit.frame >= frame_t(0)) { return gfx::Rect( bounds.x + m_separator_x + m_separator_w - 1 + FRMSIZE*hit.frame - m_scroll_x, bounds.y + y + HDRSIZE + LAYSIZE*(lastLayer()-hit.layer) - m_scroll_y, FRMSIZE, LAYSIZE); } break; case PART_RANGE_OUTLINE: { gfx::Rect rc = getRangeBounds(m_range); int s = OUTLINE_WIDTH; rc.enlarge(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_FRAME_TAG: if (hit.frameTag) { gfx::Rect bounds1 = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), hit.frameTag->fromFrame())); gfx::Rect bounds2 = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), hit.frameTag->toFrame())); gfx::Rect bounds = bounds1.createUnion(bounds2); bounds.y -= skinTheme()->dimensions.timelineTagsAreaHeight(); int textHeight = getFont()->height(); bounds.y -= textHeight + 2*ui::guiscale(); bounds.x += 3*ui::guiscale(); bounds.w = getFont()->textLength(hit.frameTag->name().c_str()) + 4*ui::guiscale(); bounds.h = getFont()->height() + 2*ui::guiscale(); return bounds; } break; } return gfx::Rect(); } gfx::Rect Timeline::getRangeBounds(const Range& range) const { gfx::Rect rc; switch (range.type()) { case Range::kNone: break; // Return empty rectangle case Range::kCels: rc = getPartBounds(Hit(PART_CEL, range.layerBegin(), range.frameBegin())).createUnion( getPartBounds(Hit(PART_CEL, range.layerEnd(), range.frameEnd()))); break; case Range::kFrames: rc = getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), range.frameBegin())).createUnion( getPartBounds(Hit(PART_HEADER_FRAME, firstLayer(), range.frameEnd()))); break; case Range::kLayers: rc = getPartBounds(Hit(PART_LAYER, range.layerBegin())).createUnion( getPartBounds(Hit(PART_LAYER, range.layerEnd()))); break; } return rc; } void Timeline::invalidateHit(const Hit& hit) { invalidateRect(getPartBounds(hit).offset(getOrigin())); } void Timeline::regenerateLayers() { ASSERT(m_document != NULL); ASSERT(m_sprite != NULL); size_t nlayers = m_sprite->countLayers(); if (m_layers.size() != nlayers) { if (nlayers > 0) m_layers.resize(nlayers, NULL); else m_layers.clear(); } for (size_t c=0; cindexToLayer(LayerIndex(c)); } void Timeline::updateByMousePos(ui::Message* msg, const gfx::Point& mousePos) { Hit hit = hitTest(msg, mousePos); if (hasMouseOver()) setCursor(msg, hit); setHot(hit); } Timeline::Hit Timeline::hitTest(ui::Message* msg, const gfx::Point& mousePos) { Hit hit( PART_NOTHING, LayerIndex::NoLayer, frame_t((mousePos.x - m_separator_x - m_separator_w + m_scroll_x) / FRMSIZE)); if (!m_document) return hit; if (m_clk.part == PART_SEPARATOR) { hit.part = PART_SEPARATOR; } else { int top = topHeight(); hit.layer = lastLayer() - LayerIndex( (mousePos.y - top - HDRSIZE + m_scroll_y) / LAYSIZE); hit.frame = frame_t((mousePos.x - m_separator_x - m_separator_w + m_scroll_x) / FRMSIZE); if (hasCapture()) { hit.layer = MID(firstLayer(), hit.layer, lastLayer()); if (isMovingCel()) hit.frame = MAX(firstFrame(), hit.frame); else hit.frame = MID(firstFrame(), hit.frame, lastFrame()); } else { if (hit.layer > lastLayer()) hit.layer = LayerIndex::NoLayer; if (hit.frame > lastFrame()) hit.frame = frame_t(-1); } // 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 separator. else if (mousePos.x > m_separator_x-4 && mousePos.x <= m_separator_x) { hit.part = PART_SEPARATOR; } // Is the mouse on the frame tags area? else if (getPartBounds(Hit(PART_HEADER_FRAME_TAGS)).contains(mousePos)) { for (FrameTag* frameTag : m_sprite->frameTags()) { gfx::Rect bounds = getPartBounds(Hit(PART_FRAME_TAG, LayerIndex(0), 0, frameTag)); if (bounds.contains(mousePos)) { hit.part = PART_FRAME_TAG; hit.frameTag = frameTag; break; } } } // Is the mouse on the headers? else if (mousePos.y >= top && mousePos.y < top+HDRSIZE) { if (mousePos.x < m_separator_x) { 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; } } else { // Is the mouse on a layer's label? if (mousePos.x < m_separator_x) { if (getPartBounds(Hit(PART_LAYER_EYE_ICON, hit.layer)).contains(mousePos)) hit.part = PART_LAYER_EYE_ICON; else if (getPartBounds(Hit(PART_LAYER_PADLOCK_ICON, hit.layer)).contains(mousePos)) hit.part = PART_LAYER_PADLOCK_ICON; else if (getPartBounds(Hit(PART_LAYER_CONTINUOUS_ICON, hit.layer)).contains(mousePos)) hit.part = PART_LAYER_CONTINUOUS_ICON; else if (getPartBounds(Hit(PART_LAYER_TEXT, hit.layer)).contains(mousePos)) hit.part = PART_LAYER_TEXT; else hit.part = PART_LAYER; } else if (validLayer(hit.layer) && validFrame(hit.frame)) { hit.part = PART_CEL; } else hit.part = PART_NOTHING; } if (!hasCapture()) { gfx::Rect outline = getPartBounds(Hit(PART_RANGE_OUTLINE)); if (outline.contains(mousePos)) { // With Ctrl and Alt key we can drag the range from any place (not necessary from the outline. if (isCopyKeyPressed(msg) || !gfx::Rect(outline).shrink(2*OUTLINE_WIDTH).contains(mousePos)) { hit.part = PART_RANGE_OUTLINE; } } } } return hit; } void Timeline::setHot(const Hit& hit) { // If the part, layer or frame change. if (m_hot != hit) { // Invalidate the whole control. if (m_state == STATE_MOVING_RANGE || hit.part == PART_RANGE_OUTLINE || m_hot.part == PART_RANGE_OUTLINE) { 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) { StatusBar* sb = StatusBar::instance(); if (m_state == STATE_MOVING_RANGE) { const char* verb = isCopyKeyPressed(msg) ? "Copy": "Move"; switch (m_range.type()) { case Range::kCels: sb->setStatusText(0, "%s cels", verb); break; case Range::kFrames: if (validFrame(m_hot.frame)) { if (m_dropTarget.hhit == DropTarget::Before) { sb->setStatusText(0, "%s before frame %d", verb, int(m_dropRange.frameBegin()+1)); return; } else if (m_dropTarget.hhit == DropTarget::After) { sb->setStatusText(0, "%s after frame %d", verb, int(m_dropRange.frameEnd()+1)); return; } } break; case Range::kLayers: { int layerIdx = -1; if (m_dropTarget.vhit == DropTarget::Bottom) layerIdx = m_dropRange.layerBegin(); else if (m_dropTarget.vhit == DropTarget::Top) layerIdx = m_dropRange.layerEnd(); Layer* layer = ((layerIdx >= 0 && layerIdx < (int)m_layers.size()) ? m_layers[layerIdx]: NULL); if (layer) { if (m_dropTarget.vhit == DropTarget::Bottom) { sb->setStatusText(0, "%s at bottom of layer %s", verb, layer->name().c_str()); return; } else if (m_dropTarget.vhit == DropTarget::Top) { sb->setStatusText(0, "%s at top of layer %s", verb, layer->name().c_str()); return; } } break; } } } else { Layer* layer = (validLayer(m_hot.layer) ? m_layers[m_hot.layer]: NULL); switch (m_hot.part) { case PART_HEADER_ONIONSKIN: { sb->setStatusText(0, "Onionskin is %s", docPref().onionskin.active() ? "enabled": "disabled"); return; } case PART_LAYER_TEXT: if (layer != NULL) { sb->setStatusText(0, "Layer '%s' [%s%s]", layer->name().c_str(), layer->isVisible() ? "visible": "hidden", layer->isEditable() ? "": " locked"); return; } break; case PART_LAYER_EYE_ICON: if (layer != NULL) { sb->setStatusText(0, "Layer '%s' is %s", layer->name().c_str(), layer->isVisible() ? "visible": "hidden"); return; } break; case PART_LAYER_PADLOCK_ICON: if (layer != NULL) { sb->setStatusText(0, "Layer '%s' is %s", layer->name().c_str(), layer->isEditable() ? "unlocked (editable)": "locked (read-only)"); return; } break; case PART_LAYER_CONTINUOUS_ICON: if (layer != NULL) { sb->setStatusText(0, "Layer '%s' is %s (%s)", layer->name().c_str(), layer->isContinuous() ? "continuous": "discontinuous", layer->isContinuous() ? "prefer linked cels/frames": "prefer individual cels/frames"); return; } break; case PART_HEADER_FRAME: if (validFrame(m_hot.frame)) { sb->setStatusText(0, "Frame %d [%d msecs]", (int)m_hot.frame+1, m_sprite->frameDuration(m_hot.frame)); return; } break; case PART_CEL: if (layer) { Cel* cel = (layer->isImage() ? layer->cel(m_hot.frame): NULL); StatusBar::instance()->setStatusText(0, "%s at frame %d" #ifdef _DEBUG " (Image %d)" #endif , cel ? "Cel": "Empty cel" , (int)m_hot.frame+1 #ifdef _DEBUG , (cel ? cel->image()->id(): 0) #endif ); return; } break; } } sb->clearText(); } void Timeline::centerCel(LayerIndex layer, frame_t frame) { int target_x = (getBounds().x + m_separator_x + m_separator_w + getBounds().x2())/2 - FRMSIZE/2; int target_y = (getBounds().y + HDRSIZE + getBounds().y2())/2 - LAYSIZE/2; int scroll_x = getBounds().x + m_separator_x + m_separator_w + FRMSIZE*frame - target_x; int scroll_y = getBounds().y + HDRSIZE + LAYSIZE*(lastLayer() - layer) - target_y; setScroll(scroll_x, scroll_y); } void Timeline::showCel(LayerIndex layer, frame_t frame) { int scroll_x, scroll_y; int x1, y1, x2, y2; x1 = getBounds().x + m_separator_x + m_separator_w + FRMSIZE*frame - m_scroll_x; y1 = getBounds().y + HDRSIZE + LAYSIZE*(lastLayer() - layer) - m_scroll_y; x2 = x1 + FRMSIZE - 1; y2 = y1 + LAYSIZE - 1; scroll_x = m_scroll_x; scroll_y = m_scroll_y; if (x1 < getBounds().x + m_separator_x + m_separator_w) { scroll_x -= (getBounds().x + m_separator_x + m_separator_w) - (x1); } else if (x2 > getBounds().x2()-1) { scroll_x += (x2) - (getBounds().x2()-1); } if (y1 < getBounds().y + HDRSIZE) { scroll_y -= (getBounds().y + HDRSIZE) - (y1); } else if (y2 > getBounds().y2()-1) { scroll_y += (y2) - (getBounds().y2()-1); } if (scroll_x != m_scroll_x || scroll_y != m_scroll_y) setScroll(scroll_x, scroll_y); } void Timeline::showCurrentCel() { LayerIndex layer = getLayerIndex(m_layer); if (layer >= firstLayer()) showCel(layer, m_frame); } void Timeline::cleanClk() { invalidateHit(m_clk); m_clk = Hit(PART_NOTHING); } void Timeline::setScroll(int x, int y) { int max_scroll_x = m_sprite->totalFrames() * FRMSIZE - getBounds().w/2; int max_scroll_y = m_layers.size() * LAYSIZE - getBounds().h/2; max_scroll_x = MAX(0, max_scroll_x); max_scroll_y = MAX(0, max_scroll_y); m_scroll_x = MID(0, x, max_scroll_x); m_scroll_y = MID(0, y, max_scroll_y); invalidate(); } bool Timeline::allLayersVisible() { for (size_t i=0; iisVisible()) return false; return true; } bool Timeline::allLayersInvisible() { for (size_t i=0; iisVisible()) return false; return true; } bool Timeline::allLayersLocked() { for (size_t i=0; iisEditable()) return false; return true; } bool Timeline::allLayersUnlocked() { for (size_t i=0; iisEditable()) return false; return true; } bool Timeline::allLayersContinuous() { for (size_t i=0; iisContinuous()) return false; return true; } bool Timeline::allLayersDiscontinuous() { for (size_t i=0; iisContinuous()) return false; return true; } LayerIndex Timeline::getLayerIndex(const Layer* layer) const { for (int i=0; i<(int)m_layers.size(); i++) if (m_layers[i] == layer) { ASSERT(m_sprite->layerToIndex(layer) == LayerIndex(i)); return LayerIndex(i); } return LayerIndex::NoLayer; } bool Timeline::isLayerActive(LayerIndex layerIndex) const { if (layerIndex == getLayerIndex(m_layer)) return true; else return m_range.inRange(layerIndex); } bool Timeline::isFrameActive(frame_t frame) const { if (frame == m_frame) return true; else return m_range.inRange(frame); } void Timeline::dropRange(DropOp op) { bool copy = (op == Timeline::kCopy); Range newFromRange; DocumentRangePlace place = kDocumentRangeAfter; switch (m_range.type()) { case Range::kFrames: if (m_dropTarget.hhit == DropTarget::Before) place = kDocumentRangeBefore; break; case Range::kLayers: if (m_dropTarget.vhit == DropTarget::Bottom) place = kDocumentRangeBefore; break; } int activeRelativeLayer = getLayerIndex(m_layer) - m_range.layerBegin(); frame_t activeRelativeFrame = m_frame - m_range.frameBegin(); try { if (copy) newFromRange = copy_range(m_document, m_range, m_dropRange, place); else newFromRange = move_range(m_document, m_range, m_dropRange, place); regenerateLayers(); m_range = newFromRange; if (m_range.layerBegin() >= LayerIndex(0)) setLayer(m_layers[m_range.layerBegin() + activeRelativeLayer]); if (m_range.frameBegin() >= frame_t(0)) setFrame(m_range.frameBegin() + activeRelativeFrame); } catch (const std::exception& e) { ui::Alert::show("Problem<<%s||&OK", e.what()); } // 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(); invalidate(); } void Timeline::updateDropRange(const gfx::Point& pt) { DropTarget::HHit oldHHit = m_dropTarget.hhit; DropTarget::VHit oldVHit = m_dropTarget.vhit; m_dropTarget.hhit = DropTarget::HNone; m_dropTarget.vhit = DropTarget::VNone; if (m_state != STATE_MOVING_RANGE) { m_dropRange.disableRange(); return; } switch (m_range.type()) { case Range::kCels: { frame_t dx = m_hot.frame - m_clk.frame; LayerIndex dy = m_hot.layer - m_clk.layer; LayerIndex layerIdx; frame_t frame; layerIdx = dy+m_range.layerBegin(); layerIdx = MID(firstLayer(), layerIdx, LayerIndex(m_layers.size() - m_range.layers())); frame = dx+m_range.frameBegin(); frame = MAX(firstFrame(), frame); m_dropRange.startRange(layerIdx, frame, m_range.type()); m_dropRange.endRange( layerIdx+LayerIndex(m_range.layers()-1), frame+m_range.frames()-1); break; } case Range::kFrames: { frame_t frame = m_hot.frame; frame_t frameEnd = frame; if (frame >= m_range.frameBegin() && frame <= m_range.frameEnd()) { frame = m_range.frameBegin(); frameEnd = frame + m_range.frames() - 1; } LayerIndex layerIdx = getLayerIndex(m_layer); m_dropRange.startRange(layerIdx, frame, m_range.type()); m_dropRange.endRange(layerIdx, frameEnd); break; } case Range::kLayers: { LayerIndex layer = m_hot.layer; LayerIndex layerEnd = layer; if (layer >= m_range.layerBegin() && layer <= m_range.layerEnd()) { layer = m_range.layerBegin(); layerEnd = layer + LayerIndex(m_range.layers() - 1); } m_dropRange.startRange(layer, m_frame, m_range.type()); m_dropRange.endRange(layerEnd, m_frame); break; } } gfx::Rect bounds = getRangeBounds(m_dropRange); if (pt.x < bounds.x + bounds.w/2) m_dropTarget.hhit = DropTarget::Before; else m_dropTarget.hhit = DropTarget::After; if (pt.y < bounds.y + bounds.h/2) m_dropTarget.vhit = DropTarget::Top; else m_dropTarget.vhit = DropTarget::Bottom; if (oldHHit != m_dropTarget.hhit || oldVHit != m_dropTarget.vhit) { invalidate(); } } void Timeline::clearClipboardRange() { Document* clipboard_document; DocumentRange clipboard_range; clipboard::get_document_range_info( &clipboard_document, &clipboard_range); if (!m_document || clipboard_document != m_document) return; clipboard::clear_content(); m_clipboard_timer.stop(); } bool Timeline::isCopyKeyPressed(ui::Message* msg) { return msg->ctrlPressed() || // Ctrl is common on Windows msg->altPressed(); // Alt is common on Mac OS X } DocumentPreferences& Timeline::docPref() const { return App::instance()->preferences().document(m_document); } skin::SkinTheme* Timeline::skinTheme() const { return static_cast(getTheme()); } int Timeline::topHeight() const { int h = 0; if (m_document && m_sprite) { h += skinTheme()->dimensions.timelineTopBorder(); if (!m_sprite->frameTags().empty()) { h += getFont()->height(); h += skinTheme()->dimensions.timelineTagsAreaHeight(); } } return h; } } // namespace app