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