Krita Source Code Documentation
Loading...
Searching...
No Matches
KisAnimTimelineFramesView.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2015 Dmitry Kazakov <dimula73@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
8
14#include "KisPlaybackEngine.h"
15
16#include <QPainter>
17#include <QApplication>
18#include <QDropEvent>
19#include <QMenu>
20#include <QScrollBar>
21#include <QScroller>
22#include <QDrag>
23#include <QKeySequence>
24#include <QInputDialog>
25#include <QClipboard>
26#include <QMimeData>
27#include <QLayout>
28#include <QScreen>
29
30#include "KSharedConfig"
31#include "KisKineticScroller.h"
32
33#include "kis_zoom_button.h"
34#include "kis_icon_utils.h"
35#include "KisAnimUtils.h"
37#include "kis_canvas2.h"
39#include "kis_action.h"
41#include "kis_time_span.h"
45#include "kis_slider_spin_box.h"
46#include "kis_signals_blocker.h"
47#include "kis_image_config.h"
50#include "KoFileDialog.h"
51#include "KisIconToolTip.h"
52
53typedef QPair<QRect, QModelIndex> QItemViewPaintPair;
55
56void resizeToMinimalSize(QAbstractButton *w, int minimalSize);
57inline bool isIndexDragEnabled(QAbstractItemModel *model, const QModelIndex &index);
58
60{
62 : q(_q)
63 , model(nullptr)
64 , horizontalRuler(nullptr)
65 , layersHeader(nullptr)
66 , addLayersButton(nullptr)
68 , colorSelector(nullptr)
69 , colorSelectorAction(nullptr)
72 , layerEditingMenu(nullptr)
73 , existingLayersMenu(nullptr)
74 , insertKeyframeDialog(nullptr)
75 , zoomDragButton(nullptr)
76 , canvas(nullptr)
77 , fps(1)
78 , dragInProgress(false)
79 , dragWasSuccessful(false)
83 , tip(_q)
84 {
85 kineticScrollInfiniteFrameUpdater.setTimerType(Qt::CoarseTimer);
86 }
87
90
93
94 QToolButton* addLayersButton;
96
98 QWidgetAction* colorSelectorAction;
101
106
108
109 int fps;
112
115
118 Qt::KeyboardModifiers lastPressedModifier;
119
121
123
124 QStyleOptionViewItem viewOptionsV4() const;
125 QItemViewPaintPairs draggablePaintPairs(const QModelIndexList &indexes, QRect *r) const;
126 QPixmap renderToPixmap(const QModelIndexList &indexes, QRect *r) const;
127
129
131};
132
134 : QTableView(parent),
135 m_d(new Private(this))
136{
137 m_d->modifiersCatcher = new KisCustomModifiersCatcher(this);
138 m_d->modifiersCatcher->addModifier("pan-zoom", Qt::Key_Space);
139 m_d->modifiersCatcher->addModifier("offset-frame", Qt::Key_Shift);
140
141 setCornerButtonEnabled(false);
142 setSelectionBehavior(QAbstractItemView::SelectItems);
143 setSelectionMode(QAbstractItemView::ExtendedSelection);
144
145 setItemDelegate(new KisAnimTimelineFrameDelegate(this));
146
147 setDragEnabled(true);
148 setDragDropMode(QAbstractItemView::DragDrop);
149 setAcceptDrops(true);
150 setDropIndicatorShown(true);
151 setDefaultDropAction(Qt::MoveAction);
152
153 m_d->horizontalRuler = new KisAnimTimelineTimeHeader(this);
154 this->setHorizontalHeader(m_d->horizontalRuler);
155
156 connect(m_d->horizontalRuler, SIGNAL(sigInsertColumnLeft()), SLOT(slotInsertKeyframeColumnLeft()));
157 connect(m_d->horizontalRuler, SIGNAL(sigInsertColumnRight()), SLOT(slotInsertKeyframeColumnRight()));
158
159 connect(m_d->horizontalRuler, SIGNAL(sigInsertMultipleColumns()), SLOT(slotInsertMultipleKeyframeColumns()));
160
161 connect(m_d->horizontalRuler, SIGNAL(sigRemoveColumns()), SLOT(slotRemoveSelectedColumns()));
162 connect(m_d->horizontalRuler, SIGNAL(sigRemoveColumnsAndShift()), SLOT(slotRemoveSelectedColumnsAndShift()));
163
164 connect(m_d->horizontalRuler, SIGNAL(sigInsertHoldColumns()), SLOT(slotInsertHoldFrameColumn()));
165 connect(m_d->horizontalRuler, SIGNAL(sigRemoveHoldColumns()), SLOT(slotRemoveHoldFrameColumn()));
166
167 connect(m_d->horizontalRuler, SIGNAL(sigInsertHoldColumnsCustom()), SLOT(slotInsertMultipleHoldFrameColumns()));
168 connect(m_d->horizontalRuler, SIGNAL(sigRemoveHoldColumnsCustom()), SLOT(slotRemoveMultipleHoldFrameColumns()));
169
170 connect(m_d->horizontalRuler, SIGNAL(sigMirrorColumns()), SLOT(slotMirrorColumns()));
171 connect(m_d->horizontalRuler, SIGNAL(sigClearCache()), SLOT(slotClearCache()));
172
173 connect(m_d->horizontalRuler, SIGNAL(sigCopyColumns()), SLOT(slotCopyColumns()));
174 connect(m_d->horizontalRuler, SIGNAL(sigCutColumns()), SLOT(slotCutColumns()));
175 connect(m_d->horizontalRuler, SIGNAL(sigPasteColumns()), SLOT(slotPasteColumns()));
176
177 m_d->layersHeader = new KisAnimTimelineLayersHeader(this);
178
179 m_d->layersHeader->setSectionResizeMode(QHeaderView::Fixed);
180
181 m_d->layersHeader->setDefaultSectionSize(24);
182 m_d->layersHeader->setMinimumWidth(60);
183 m_d->layersHeader->setHighlightSections(true);
184 this->setVerticalHeader(m_d->layersHeader);
185
186 /********** Layer Menu ***********************************************************/
187
188 m_d->layerEditingMenu = new QMenu(this);
189 m_d->layerEditingMenu->addSection(i18n("Edit Layers:"));
190 m_d->layerEditingMenu->addSeparator();
191
192 m_d->layerEditingMenu->addAction(KisAnimUtils::newLayerActionName, this, SLOT(slotAddNewLayer()));
193 m_d->layerEditingMenu->addAction(KisAnimUtils::removeLayerActionName, this, SLOT(slotRemoveLayer()));
194 m_d->layerEditingMenu->addSeparator();
195 m_d->existingLayersMenu = m_d->layerEditingMenu->addMenu(KisAnimUtils::pinExistingLayerActionName);
196
197 connect(m_d->existingLayersMenu, SIGNAL(aboutToShow()), SLOT(slotUpdateLayersMenu()));
198 connect(m_d->existingLayersMenu, SIGNAL(triggered(QAction*)), SLOT(slotAddExistingLayer(QAction*)));
199
200 connect(m_d->layersHeader, SIGNAL(sigRequestContextMenu(QPoint)), SLOT(slotLayerContextMenuRequested(QPoint)));
201
202 m_d->addLayersButton = new QToolButton(this);
203 m_d->addLayersButton->setAutoRaise(true);
204 m_d->addLayersButton->setIcon(KisIconUtils::loadIcon("list-add-22"));
205 m_d->addLayersButton->setIconSize(QSize(22, 22));
206 m_d->addLayersButton->setPopupMode(QToolButton::InstantPopup);
207 m_d->addLayersButton->setMenu(m_d->layerEditingMenu);
208
209 /********** Frame Editing Context Menu ***********************************************/
210
211 m_d->colorSelector = new KisColorLabelSelectorWidgetMenuWrapper(this);
212 MouseClickIgnore* clickIgnore = new MouseClickIgnore(m_d->colorSelector);
213 m_d->colorSelector->installEventFilter(clickIgnore);
214 m_d->colorSelectorAction = new QWidgetAction(this);
215 m_d->colorSelectorAction->setDefaultWidget(m_d->colorSelector);
217
218 m_d->multiframeColorSelector = new KisColorLabelSelectorWidgetMenuWrapper(this);
219 m_d->multiframeColorSelector->installEventFilter(clickIgnore);
220 m_d->multiframeColorSelectorAction = new QWidgetAction(this);
221 m_d->multiframeColorSelectorAction->setDefaultWidget(m_d->multiframeColorSelector);
222 connect(m_d->multiframeColorSelector->colorLabelSelector(), &KisColorLabelSelectorWidget::currentIndexChanged, this, &KisAnimTimelineFramesView::slotColorLabelChanged);
223
224 /********** Insert Keyframes Dialog **************************************************/
225
226 m_d->insertKeyframeDialog = new TimelineInsertKeyframeDialog(this);
227
228 /********** Zoom Button **************************************************************/
229
230 m_d->zoomDragButton = new KisZoomButton(this);
231 m_d->zoomDragButton->setAutoRaise(true);
232 m_d->zoomDragButton->setIcon(KisIconUtils::loadIcon("zoom-horizontal"));
233 m_d->zoomDragButton->setIconSize(QSize(22, 22));
234
235 m_d->zoomDragButton->setToolTip(i18nc("@info:tooltip", "Zoom Timeline. Hold down and drag left or right."));
236 m_d->zoomDragButton->setPopupMode(QToolButton::InstantPopup);
237 connect(m_d->zoomDragButton, SIGNAL(zoom(qreal)), SLOT(slotZoom(qreal)));
238
239 /********** Zoom Scrollbar **************************************************************/
240
241 KisZoomableScrollBar* hZoomableBar = new KisZoomableScrollBar(this);
242 setHorizontalScrollBar(hZoomableBar);
243 setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
244 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
245 setVerticalScrollBar(new KisZoomableScrollBar(this));
246 hZoomableBar->setEnabled(false);
247
248 connect(hZoomableBar, &KisZoomableScrollBar::valueChanged, m_d->horizontalRuler, &KisAnimTimelineTimeHeader::setPixelOffset);
249 connect(hZoomableBar, SIGNAL(zoom(qreal)), this, SLOT(slotZoom(qreal)));
250 connect(hZoomableBar, SIGNAL(overscroll(qreal)), SLOT(slotUpdateInfiniteFramesCount()));
251 connect(hZoomableBar, SIGNAL(sliderReleased()), SLOT(slotUpdateInfiniteFramesCount()));
252
253 /********** Kinetic Scrolling **************************************************************/
254
255 // Kinetic scrolling via left-click renders the timeline effectively
256 // unusable, you end up scrolling the timeline every time you try to move a
257 // key frame or attempt to use the zoom slider. So we don't enable kinetic
258 // scrolling in that case and require the use of the scrollbar instead.
259 if (KisKineticScroller::getConfiguredGestureType() != QScroller::LeftMouseButtonGesture) {
260 QScroller *scroller = KisKineticScroller::createPreconfiguredScroller(this);
261 if (scroller) {
262 connect(scroller, SIGNAL(stateChanged(QScroller::State)),
263 this, SLOT(slotScrollerStateChanged(QScroller::State)));
264
265 connect(&m_d->kineticScrollInfiniteFrameUpdater, &QTimer::timeout, [this, scroller](){
266 slotUpdateInfiniteFramesCount();
267 scroller->resendPrepareEvent();
268 });
269
270 QScrollerProperties props = scroller->scrollerProperties();
271 props.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
272 props.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
273 scroller->setScrollerProperties(props);
274 }
275 }
276
277 connect(&m_d->selectionChangedCompressor, SIGNAL(timeout()),
278 SLOT(slotSelectionChanged()));
279 connect(&m_d->selectionChangedCompressor, SIGNAL(timeout()),
280 SLOT(slotUpdateFrameActions()));
281
282 {
283 QClipboard *cb = QApplication::clipboard();
284 connect(cb, SIGNAL(dataChanged()), SLOT(slotUpdateFrameActions()));
285 }
286
288}
289
293
294void KisAnimTimelineFramesView::setModel(QAbstractItemModel *model)
295{
296 KisAnimTimelineFramesModel *framesModel = qobject_cast<KisAnimTimelineFramesModel*>(model);
297 m_d->model = framesModel;
298
299 QTableView::setModel(model);
300
301 connect(m_d->model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)),
302 this, SLOT(slotHeaderDataChanged(Qt::Orientation,int,int)));
303
304 connect(m_d->model, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
305 this, SLOT(slotDataChanged(QModelIndex,QModelIndex)));
306
307 connect(m_d->model, SIGNAL(rowsRemoved(QModelIndex,int,int)),
308 this, SLOT(slotReselectCurrentIndex()));
309
310 connect(m_d->model, SIGNAL(sigInfiniteTimelineUpdateNeeded()),
311 this, SLOT(slotUpdateInfiniteFramesCount()));
312
313 connect(m_d->model, SIGNAL(requestTransferSelectionBetweenRows(int,int)),
314 this, SLOT(slotTryTransferSelectionBetweenRows(int,int)));
315
316 connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
317 &m_d->selectionChangedCompressor, SLOT(start()));
318
319 connect(m_d->model, SIGNAL(sigEnsureRowVisible(int)), SLOT(slotEnsureRowVisible(int)));
320}
321
323{
324 m_d->actionMan = actionManager;
325 m_d->horizontalRuler->setActionManager(actionManager);
326
327 if (actionManager) {
328 KisAction *action = 0;
329
330 action = m_d->actionMan->createAction("add_blank_frame");
331 connect(action, SIGNAL(triggered()), SLOT(slotAddBlankFrame()));
332
333 action = m_d->actionMan->createAction("add_duplicate_frame");
334 connect(action, SIGNAL(triggered()), SLOT(slotAddDuplicateFrame()));
335
336 action = m_d->actionMan->createAction("insert_keyframe_left");
337 connect(action, SIGNAL(triggered()), SLOT(slotInsertKeyframeLeft()));
338
339 action = m_d->actionMan->createAction("insert_keyframe_right");
340 connect(action, SIGNAL(triggered()), SLOT(slotInsertKeyframeRight()));
341
342 action = m_d->actionMan->createAction("insert_multiple_keyframes");
343 connect(action, SIGNAL(triggered()), SLOT(slotInsertMultipleKeyframes()));
344
345 action = m_d->actionMan->createAction("remove_frames_and_pull");
346 connect(action, SIGNAL(triggered()), SLOT(slotRemoveSelectedFramesAndShift()));
347
348 action = m_d->actionMan->createAction("remove_frames");
349 connect(action, SIGNAL(triggered()), SLOT(slotRemoveSelectedFrames()));
350
351 action = m_d->actionMan->createAction("insert_hold_frame");
352 connect(action, SIGNAL(triggered()), SLOT(slotInsertHoldFrame()));
353
354 action = m_d->actionMan->createAction("insert_multiple_hold_frames");
355 connect(action, SIGNAL(triggered()), SLOT(slotInsertMultipleHoldFrames()));
356
357 action = m_d->actionMan->createAction("remove_hold_frame");
358 connect(action, SIGNAL(triggered()), SLOT(slotRemoveHoldFrame()));
359
360 action = m_d->actionMan->createAction("remove_multiple_hold_frames");
361 connect(action, SIGNAL(triggered()), SLOT(slotRemoveMultipleHoldFrames()));
362
363 action = m_d->actionMan->createAction("mirror_frames");
364 connect(action, SIGNAL(triggered()), SLOT(slotMirrorFrames()));
365
366 action = m_d->actionMan->createAction("copy_frames");
367 connect(action, SIGNAL(triggered()), SLOT(slotCopyFrames()));
368
369 action = m_d->actionMan->createAction("copy_frames_as_clones");
370 connect(action, &KisAction::triggered, [this](){clone(false);});
371
372 action = m_d->actionMan->createAction("make_clones_unique");
373 connect(action, SIGNAL(triggered()), SLOT(slotMakeClonesUnique()));
374
375 action = m_d->actionMan->createAction("cut_frames");
376 connect(action, SIGNAL(triggered()), SLOT(slotCutFrames()));
377
378 action = m_d->actionMan->createAction("paste_frames");
379 connect(action, SIGNAL(triggered()), SLOT(slotPasteFrames()));
380
381 action = m_d->actionMan->createAction("set_start_time");
382 connect(action, SIGNAL(triggered()), SLOT(slotSetStartTimeToCurrentPosition()));
383
384 action = m_d->actionMan->createAction("set_end_time");
385 connect(action, SIGNAL(triggered()), SLOT(slotSetEndTimeToCurrentPosition()));
386
387 action = m_d->actionMan->createAction("update_playback_range");
388 connect(action, SIGNAL(triggered()), SLOT(slotUpdatePlaybackRange()));
389
390 action = m_d->actionMan->actionByName("pin_to_timeline");
391 m_d->pinLayerToTimelineAction = action;
392 m_d->layerEditingMenu->addAction(action);
393 }
394}
395
397{
398 QTableView::updateGeometries();
399
400 const int availableHeight = m_d->horizontalRuler->height();
401 const int margin = 2;
402 const int minimalSize = availableHeight - 2 * margin;
403
404 resizeToMinimalSize(m_d->addLayersButton, minimalSize);
405 resizeToMinimalSize(m_d->zoomDragButton, minimalSize);
406
407 int x = 2 * margin;
408 int y = (availableHeight - minimalSize) / 2;
409 m_d->addLayersButton->move(x, 2 * y);
410
411 const int availableWidth = m_d->layersHeader->width();
412
413 x = availableWidth - margin - minimalSize;
414 m_d->zoomDragButton->move(x, 2 * y);
415}
416
418{
419 if (m_d->canvas) {
420 KisCanvas2* canvas2 = dynamic_cast<KisCanvas2*>(m_d->canvas);
421 if (canvas2) {
422 KisCanvasAnimationState* state = canvas2->animationState();
423 state->disconnect(this);
424 }
425 }
426
427 m_d->canvas = canvas;
428
429 horizontalScrollBar()->setEnabled(m_d->canvas != nullptr);
430}
431
433{
434 m_d->addLayersButton->setIcon(KisIconUtils::loadIcon("list-add-22"));
435 m_d->zoomDragButton->setIcon(KisIconUtils::loadIcon("zoom-horizontal"));
436}
437
439{
440 QAction *action = 0;
441
442 m_d->existingLayersMenu->clear();
443
444 QVariant value = model()->headerData(0, Qt::Vertical, KisAnimTimelineFramesModel::OtherLayersRole);
445 if (value.isValid()) {
447
448 int i = 0;
449 Q_FOREACH (const KisAnimTimelineFramesModel::OtherLayer &l, list) {
450 action = m_d->existingLayersMenu->addAction(l.name);
451 action->setData(i++);
452 }
453 }
454}
455
457{
458 if (!m_d->actionMan) return;
459
460 const QModelIndexList editableIndexes = calculateSelectionSpan(false, true);
461 const bool hasEditableFrames = !editableIndexes.isEmpty();
462
463 bool hasExistingFrames = false;
464 Q_FOREACH (const QModelIndex &index, editableIndexes) {
465 if (model()->data(index, KisAnimTimelineFramesModel::FrameExistsRole).toBool()) {
466 hasExistingFrames = true;
467 break;
468 }
469 }
470
471 auto enableAction = [this] (const QString &id, bool value) {
472 KisAction *action = m_d->actionMan->actionByName(id);
474 action->setEnabled(value);
475 };
476
477 enableAction("add_blank_frame", hasEditableFrames);
478 enableAction("add_duplicate_frame", hasEditableFrames);
479
480 enableAction("insert_keyframe_left", hasEditableFrames);
481 enableAction("insert_keyframe_right", hasEditableFrames);
482 enableAction("insert_multiple_keyframes", hasEditableFrames);
483
484 enableAction("remove_frames", hasEditableFrames && hasExistingFrames);
485 enableAction("remove_frames_and_pull", hasEditableFrames);
486
487 enableAction("insert_hold_frame", hasEditableFrames);
488 enableAction("insert_multiple_hold_frames", hasEditableFrames);
489
490 enableAction("remove_hold_frame", hasEditableFrames);
491 enableAction("remove_multiple_hold_frames", hasEditableFrames);
492
493 enableAction("mirror_frames", hasEditableFrames && editableIndexes.size() > 1);
494
495 enableAction("copy_frames", true);
496 enableAction("cut_frames", hasEditableFrames);
497}
498
500{
501 int minColumn = std::numeric_limits<int>::max();
502 int maxColumn = std::numeric_limits<int>::min();
503
504 calculateActiveLayerSelectedTimes(selectedIndexes());
505
506 foreach (const QModelIndex &idx, selectedIndexes()) {
507 if (idx.column() > maxColumn) {
508 maxColumn = idx.column();
509 }
510
511 if (idx.column() < minColumn) {
512 minColumn = idx.column();
513 }
514 }
515
516 KisTimeSpan range;
517 if (maxColumn > minColumn) {
518 range = KisTimeSpan::fromTimeWithDuration(minColumn, maxColumn - minColumn + 1);
519 }
520
521 m_d->model->setPlaybackRange(range);
522}
523
525{
526 QModelIndex index = currentIndex();
527 currentChanged(index, index);
528}
529
531{
532 //If there's only one selected index, or less, just select the current index if valid...
533 QModelIndex current = model()->index(toRow, m_d->model->currentTime());
534 if (selectedIndexes().count() <= 1) {
535 if (selectedIndexes().count() != 1 ||
536 (selectedIndexes().first().column() == current.column() &&
537 selectedIndexes().first().row() == fromRow)) {
538 setCurrentIndex(current);
539 }
540
541 }
542}
543
545{
546 m_d->model->setDocumentClipRangeStart(this->currentIndex().column());
547}
548
550{
551 m_d->model->setDocumentClipRangeEnd(this->currentIndex().column());
552}
553
555{
556 QSet<int> rows;
557 int minColumn = 0;
558 int maxColumn = 0;
559
560 calculateSelectionMetrics(minColumn, maxColumn, rows, true);
561
562 m_d->model->setDocumentClipRangeStart(minColumn);
563 m_d->model->setDocumentClipRangeEnd(maxColumn);
564}
565
567{
568 const int lastVisibleFrame = m_d->horizontalRuler->estimateLastVisibleColumn();
569 m_d->model->setLastVisibleFrame(lastVisibleFrame);
570}
571
572void KisAnimTimelineFramesView::slotDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
573{
574 if (m_d->model->isPlaybackActive()) return;
575
576 int selectedColumn = -1;
577
578 for (int j = topLeft.column(); j <= bottomRight.column(); j++) {
579 QVariant value = m_d->model->data(
580 m_d->model->index(topLeft.row(), j),
582
583 if (value.isValid() && value.toBool()) {
584 selectedColumn = j;
585 break;
586 }
587 }
588
589 QModelIndex index = currentIndex();
590
591 if (!index.isValid() && selectedColumn < 0) {
592 return;
593 }
594
595 if (selectionModel()->selectedIndexes().count() > 1) return;
596
597 if (selectedColumn == -1) {
598 selectedColumn = index.column();
599 }
600
601 if (selectedColumn != index.column() && !m_d->dragInProgress && !m_d->model->isScrubbing()) {
602 int row = index.isValid() ? index.row() : 0;
603 // Todo: This is causing double audio pushes. We should fix this eventually.
604 selectionModel()->setCurrentIndex(m_d->model->index(row, selectedColumn), QItemSelectionModel::ClearAndSelect);
605 }
606}
607
608void KisAnimTimelineFramesView::slotHeaderDataChanged(Qt::Orientation orientation, int first, int last)
609{
610 Q_UNUSED(first);
611 Q_UNUSED(last);
612
613 if (orientation == Qt::Horizontal) {
614 const int newFps = m_d->model->headerData(0, Qt::Horizontal, KisAnimTimelineFramesModel::FramesPerSecondRole).toInt();
615
616 if (newFps != m_d->fps) {
617 setFramesPerSecond(newFps);
618 }
619 } else {
620 calculateActiveLayerSelectedTimes(selectedIndexes());
621 }
622}
623
625{
626 Q_FOREACH(QModelIndex index, selectedIndexes()) {
627 m_d->model->setData(index, label, KisAnimTimelineFramesModel::FrameColorLabelIndexRole);
628 }
630}
631
633{
634 QModelIndex index = currentIndex();
635 const int newRow = index.isValid() ? index.row() : 0;
636 model()->insertRow(newRow);
637}
638
640{
641 QVariant value = action->data();
642
643 if (value.isValid()) {
644 QModelIndex index = currentIndex();
645 const int newRow = index.isValid() ? index.row() + 1 : 0;
646
647 m_d->model->insertOtherLayer(value.toInt(), newRow);
648 }
649}
650
652{
653 QModelIndex index = currentIndex();
654 if (!index.isValid()) return;
655 model()->removeRow(index.row());
656}
657
659{
660 m_d->layerEditingMenu->exec(globalPos);
661}
662
664{
665 QModelIndexList selectedIndices = calculateSelectionSpan(false);
666 Q_FOREACH(const QModelIndex &index, selectedIndices) {
667 if (!index.isValid() ||
668 !m_d->model->data(index, KisAnimTimelineFramesModel::FrameEditableRole).toBool()) {
669 selectedIndices.removeOne(index);
670 }
671 }
672
673 m_d->model->createFrame(selectedIndices);
674}
675
677{
678 QModelIndex index = currentIndex();
679 if (!index.isValid() ||
680 !m_d->model->data(index, KisAnimTimelineFramesModel::FrameEditableRole).toBool()) {
681
682 return;
683 }
684
685 m_d->model->copyFrame(index);
686}
687
689{
690 const QModelIndexList selectedIndices = calculateSelectionSpan(entireColumn);
691
692 if (!selectedIndices.isEmpty()) {
693 if (pull) {
694 m_d->model->removeFramesAndOffset(selectedIndices);
695 } else {
696 m_d->model->removeFrames(selectedIndices);
697 }
698 }
699}
700
702{
703 const QModelIndexList indexes = calculateSelectionSpan(entireColumn);
704
705 if (!indexes.isEmpty()) {
706 m_d->model->mirrorFrames(indexes);
707 }
708}
709
711 m_d->model->clearEntireCache();
712}
713
715{
716 const QModelIndex currentIndex =
717 !entireColumn ? this->currentIndex() : m_d->model->index(0, this->currentIndex().column());
718
719 if (!currentIndex.isValid()) return;
720
721 QClipboard *cb = QApplication::clipboard();
722 const QMimeData *data = cb->mimeData();
723
724 if (data && data->hasFormat("application/x-krita-frame")) {
725
726 bool dataMoved = false;
727 bool result = m_d->model->dropMimeDataExtended(data, Qt::MoveAction, currentIndex, &dataMoved);
728
729 if (result && dataMoved) {
730 cb->clear();
731 }
732 }
733}
734
736{
737 if (!m_d->model) return;
738
739 const QModelIndexList indices = calculateSelectionSpan(false);
740 m_d->model->makeClonesUnique(indices);
741}
742
744{
745 if (!m_d->model) return;
746
747 QString defaultDir = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
748
749 const QString currentFile = m_d->model->audioChannelFileName();
750 QDir baseDir = QFileInfo(currentFile).absoluteDir();
751 if (baseDir.exists()) {
752 defaultDir = baseDir.absolutePath();
753 }
754
755 const QString result = KisImportExportManager::askForAudioFileName(defaultDir, this);
756 const QFileInfo info(result);
757
758 if (info.exists()) {
759 m_d->model->setAudioChannelFileName(info);
760 }
761}
762
764{
765 if (!m_d->model) return;
766
767 if (value != m_d->model->isAudioMuted()) {
768 m_d->model->setAudioMuted(value);
769 }
770}
771
773{
774 if (!m_d->model) return;
775 m_d->model->setAudioChannelFileName(QFileInfo());
776}
777
779{
780 m_d->model->setAudioVolume(qreal(value) / 100.0);
781}
782
784
785 if (state == QScroller::Dragging || state == QScroller::Scrolling ) {
786 m_d->kineticScrollInfiniteFrameUpdater.start(16);
787 } else {
788 m_d->kineticScrollInfiniteFrameUpdater.stop();
789 }
790
792}
793
795{
796 const int originalFirstColumn = m_d->horizontalRuler->estimateFirstVisibleColumn();
797 if (m_d->horizontalRuler->setZoom(m_d->horizontalRuler->zoom() + zoom)) {
798 const int newLastColumn = m_d->horizontalRuler->estimateFirstVisibleColumn();
799 if (newLastColumn >= m_d->model->columnCount()) {
801 }
802 viewport()->update();
803 horizontalScrollBar()->setValue(scrollPositionFromColumn(originalFirstColumn));
804 }
805}
806
808 if(m_d->dragInProgress ||
809 (m_d->model->isScrubbing() && horizontalScrollBar()->sliderPosition() == horizontalScrollBar()->maximum()) ) {
811 }
812}
813
815 QScrollBar* hBar = horizontalScrollBar();
816 QScrollBar* vBar = verticalScrollBar();
817
818 QSize desiredScrollArea = QSize(width() - verticalHeader()->width(), height() - horizontalHeader()->height());
819
820 // Compensate for corner gap...
821 if (hBar->isVisible() && vBar->isVisible()) {
822 desiredScrollArea -= QSize(vBar->width(), hBar->height());
823 }
824
825 hBar->parentWidget()->layout()->setAlignment(Qt::AlignRight);
826 hBar->setMaximumWidth(desiredScrollArea.width());
827 hBar->setMinimumWidth(desiredScrollArea.width());
828
829 vBar->parentWidget()->layout()->setAlignment(Qt::AlignBottom);
830 vBar->setMaximumHeight(desiredScrollArea.height());
831 vBar->setMinimumHeight(desiredScrollArea.height());
832}
833
835{
836 QModelIndex index = currentIndex();
837 if (!index.isValid() || row < 0) return;
838
839 index = m_d->model->index(row, index.column());
840
841 // WORKAROUND BUG:437029
842 // Delay's UI scrolling by 1/60 of a second to compensate for
843 // inconsistent dummy indexing caused by a brief period where
844 // two unpinned dummies exist on the timeline simultaneously.
845 QTimer::singleShot(16, Qt::PreciseTimer, this, [this, index](){
846 scrollTo(index);
847 });
848}
849
851{
852 QModelIndex startIndex = currentIndex().siblingAtColumn(start);
853 if (!startIndex.isValid() || start < 0) return;
854
855 KIS_ASSERT(start == 0); //TODO: forcing start to 0 for now, next let's scroll bar to also fit starting value correctly.
856
857 m_d->horizontalRuler->zoomToFitFrameRange(start, end);
858 //scrollTo(startIndex, ScrollHint::PositionAtBottom);
859}
860
862{
863 QSet<int> activeLayerSelectedTimes;
864 Q_FOREACH (const QModelIndex& index, selection) {
865 if (index.data(KisAnimTimelineFramesModel::ActiveLayerRole).toBool()) {
866 activeLayerSelectedTimes.insert(index.column());
867 }
868 }
869
870 m_d->model->setActiveLayerSelectedTimes(activeLayerSelectedTimes);
871}
872
874{
875 // Seems to have been copied over from KisResourceItemListView.
876 // These tooltips currently give bogus info (empty thumbnail and resource location), so removed for now.
877 // TODO: Implement meaningful tooltips if there's demand, probably including frame thumbnails.
878
879 /*
880 if (event->type() == QEvent::ToolTip && model()) {
881 QHelpEvent *he = static_cast<QHelpEvent *>(event);
882 QModelIndex index = model()->buddy(indexAt(he->pos()));
883 if (index.isValid()) {
884#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
885 QStyleOptionViewItem option = viewOptions();
886#else
887 QStyleOptionViewItem option;
888 initViewItemOption(&option);
889#endif
890 option.rect = visualRect(index);
891 // The offset of the headers is needed to get the correct position inside the view.
892 m_d->tip.showTip(this, he->pos() + QPoint(verticalHeader()->width(), horizontalHeader()->height()), option, index);
893 return true;
894 }
895 }
896 */
897
898 return QTableView::viewportEvent(event);
899}
900
902{
903 QPersistentModelIndex index = indexAt(event->pos());
904
905 if (m_d->modifiersCatcher->modifierPressed("pan-zoom")) {
906 if (event->button() == Qt::RightButton) {
907 // TODO: try calculate index under mouse cursor even when
908 // it is outside any visible row
909 // qreal staticPoint = index.isValid() ? index.column() : currentIndex().column();
910 // m_d->zoomDragButton->beginZoom(event->pos(), staticPoint);
911 } else if (event->button() == Qt::LeftButton) {
912 m_d->initialDragPanPos = event->pos();
913 m_d->initialDragPanValue =
914 QPoint(horizontalScrollBar()->value(),
915 verticalScrollBar()->value());
916 }
917 event->accept();
918
919 } else if (event->button() == Qt::RightButton) {
920 int numSelectedItems = selectionModel()->selectedIndexes().size();
921
922 if (index.isValid() &&
923 numSelectedItems <= 1 &&
924 m_d->model->data(index, KisAnimTimelineFramesModel::FrameEditableRole).toBool()) {
925
926 model()->setData(index, true, KisAnimTimelineFramesModel::ActiveLayerRole);
927 model()->setData(index, true, KisAnimTimelineFramesModel::ActiveFrameRole);
928 model()->setData(index, QVariant(int(SEEK_FINALIZE | SEEK_PUSH_AUDIO)), KisAnimTimelineFramesModel::ScrubToRole);
929 setCurrentIndex(index);
930
931 if (model()->data(index, KisAnimTimelineFramesModel::FrameExistsRole).toBool() ||
932 model()->data(index, KisAnimTimelineFramesModel::SpecialKeyframeExists).toBool()) {
933
934 {
935 KisSignalsBlocker b(m_d->colorSelector->colorLabelSelector());
936 QVariant colorLabel = index.data(KisAnimTimelineFramesModel::FrameColorLabelIndexRole);
937 int labelIndex = colorLabel.isValid() ? colorLabel.toInt() : 0;
938 m_d->colorSelector->colorLabelSelector()->setCurrentIndex(labelIndex);
939 }
940
941 const bool hasClones = model()->data(index, KisAnimTimelineFramesModel::CloneCount).toInt() > 0;
942
943 QMenu menu;
944 createFrameEditingMenuActions(&menu, false, hasClones);
945 menu.addSeparator();
946 menu.addAction(m_d->colorSelectorAction);
947 menu.exec(event->globalPos());
948
949 } else {
950 {
951 KisSignalsBlocker b(m_d->colorSelector->colorLabelSelector());
952 const int labelIndex = KisImageConfig(true).defaultFrameColorLabel();
953 m_d->colorSelector->colorLabelSelector()->setCurrentIndex(labelIndex);
954 }
955
956 QMenu menu;
957 createFrameEditingMenuActions(&menu, true, false);
958 menu.addSeparator();
959 menu.addAction(m_d->colorSelectorAction);
960 menu.exec(event->globalPos());
961 }
962 } else if (numSelectedItems > 1) {
963 int labelIndex = -1;
964 bool firstKeyframe = true;
965 bool hasKeyframes = false;
966 bool containsClones = false;
967 Q_FOREACH(QModelIndex index, selectedIndexes()) {
968 hasKeyframes |= index.data(KisAnimTimelineFramesModel::FrameExistsRole).toBool();
969 containsClones |= (index.data(KisAnimTimelineFramesModel::CloneCount).toInt() > 0);
970
971 QVariant colorLabel = index.data(KisAnimTimelineFramesModel::FrameColorLabelIndexRole);
972 if (colorLabel.isValid()) {
973 if (firstKeyframe) {
974 labelIndex = colorLabel.toInt();
975 } else if (labelIndex != colorLabel.toInt()) {
976 // Mixed colors in selection
977 labelIndex = -1;
978 }
979
980 firstKeyframe = false;
981 }
982
983 if (!firstKeyframe
984 && hasKeyframes
985 && containsClones
986 && labelIndex == -1) {
987 break; // Break out early if we find all of the above.
988 }
989 }
990
991 if (hasKeyframes) {
992 KisSignalsBlocker b(m_d->multiframeColorSelector->colorLabelSelector());
993 m_d->multiframeColorSelector->colorLabelSelector()->setCurrentIndex(labelIndex);
994 }
995
996 QMenu menu;
997 createFrameEditingMenuActions(&menu, false, containsClones);
998 menu.addSeparator();
999 KisActionManager::safePopulateMenu(&menu, "mirror_frames", m_d->actionMan);
1000 menu.addSeparator();
1001 menu.addAction(m_d->multiframeColorSelectorAction);
1002 menu.exec(event->globalPos());
1003 }
1004
1005 } else if (event->button() == Qt::MiddleButton) {
1006 QModelIndex index = model()->buddy(indexAt(event->pos()));
1007 if (index.isValid()) {
1008#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
1009 QStyleOptionViewItem option = viewOptions();
1010#else
1011 QStyleOptionViewItem option;
1012 initViewItemOption(&option);
1013#endif
1014 option.rect = visualRect(index);
1015 // The offset of the headers is needed to get the correct position inside the view.
1016 m_d->tip.showTip(event->pos() + QPoint(verticalHeader()->width(), horizontalHeader()->height()), option, index);
1017 }
1018 event->accept();
1019
1020 } else {
1021 if (index.isValid()) {
1022 m_d->model->setLastClickedIndex(index);
1023 }
1024
1025 m_d->lastPressedPosition = QPoint(horizontalOffset(), verticalOffset()) + event->pos();
1026 m_d->lastPressedModifier = event->modifiers();
1027
1028 m_d->initialDragPanPos = event->pos();
1029
1030 QAbstractItemView::mousePressEvent(event);
1031 }
1032}
1033
1035 QPersistentModelIndex index = indexAt(event->pos());
1036
1037 if (index.isValid()) {
1038 if (event->modifiers() & Qt::AltModifier) {
1039 selectRow(index.row());
1040 } else {
1041 selectColumn(index.column());
1042 }
1043 }
1044
1045 QAbstractItemView::mouseDoubleClickEvent(event);
1046}
1047
1049{
1050 // Custom keyframe dragging distance based on zoom level.
1051 if (state() == DraggingState &&
1052 (horizontalHeader()->defaultSectionSize() / 2) < QApplication::startDragDistance() ) {
1053
1054 const QPoint dragVector = e->pos() - m_d->initialDragPanPos;
1055 if (dragVector.manhattanLength() >= (horizontalHeader()->defaultSectionSize() / 2)) {
1056 startDrag(model()->supportedDragActions());
1057 setState(NoState);
1058 stopAutoScroll();
1059 }
1060 }
1061
1062 if (m_d->modifiersCatcher->modifierPressed("pan-zoom")) {
1063 if (e->buttons() & Qt::RightButton) {
1064 // m_d->zoomDragButton->continueZoom(e->pos());
1065 } else if (e->buttons() & Qt::LeftButton) {
1066
1067 QPoint diff = e->pos() - m_d->initialDragPanPos;
1068 QPoint offset = QPoint(m_d->initialDragPanValue.x() - diff.x(),
1069 m_d->initialDragPanValue.y() - diff.y());
1070
1071 const int height = m_d->layersHeader->defaultSectionSize();
1072
1073 if (m_d->initialDragPanValue.x() - diff.x() > horizontalScrollBar()->maximum() || m_d->initialDragPanValue.x() - diff.x() > horizontalScrollBar()->minimum() ){
1074 KisZoomableScrollBar* zoombar = static_cast<KisZoomableScrollBar*>(horizontalScrollBar());
1075 zoombar->overscroll(-diff.x());
1076 }
1077
1078 horizontalScrollBar()->setValue(offset.x());
1079 verticalScrollBar()->setValue(offset.y() / height);
1080 }
1081
1082 e->accept();
1083 } else if (e->buttons() == Qt::MiddleButton) {
1084 QModelIndex index = model()->buddy(indexAt(e->pos()));
1085 if (index.isValid()) {
1086#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
1087 QStyleOptionViewItem option = viewOptions();
1088#else
1089 QStyleOptionViewItem option;
1090 initViewItemOption(&option);
1091#endif
1092 option.rect = visualRect(index);
1093 // The offset of the headers is needed to get the correct position inside the view.
1094 m_d->tip.showTip(e->pos() + QPoint(verticalHeader()->width(), horizontalHeader()->height()), option, index);
1095 }
1096
1097 e->accept();
1098 } else {
1099 m_d->model->setScrubState(true);
1100 QTableView::mouseMoveEvent(e);
1101 }
1102}
1103
1105{
1106 if (m_d->modifiersCatcher->modifierPressed("pan-zoom")) {
1107 e->accept();
1108 } else {
1109 m_d->model->setScrubState(false);
1110 QTableView::mouseReleaseEvent(e);
1111 }
1112}
1113
1114void KisAnimTimelineFramesView::startDrag(Qt::DropActions supportedActions)
1115{
1116 QModelIndexList indexes = selectionModel()->selectedIndexes();
1117
1118 if (!indexes.isEmpty() && m_d->modifiersCatcher->modifierPressed("offset-frame")) {
1119 QVector<int> rows;
1120 int leftmostColumn = std::numeric_limits<int>::max();
1121
1122 Q_FOREACH (const QModelIndex &index, indexes) {
1123 leftmostColumn = qMin(leftmostColumn, index.column());
1124 if (!rows.contains(index.row())) {
1125 rows.append(index.row());
1126 }
1127 }
1128
1129 const int lastColumn = m_d->model->columnCount() - 1;
1130
1131 selectionModel()->clear();
1132 Q_FOREACH (const int row, rows) {
1133 QItemSelection sel(m_d->model->index(row, leftmostColumn), m_d->model->index(row, lastColumn));
1134 selectionModel()->select(sel, QItemSelectionModel::Select);
1135 }
1136
1137 supportedActions = Qt::MoveAction;
1138
1139 {
1140 QModelIndexList indexes = selectedIndexes();
1141 for(int i = indexes.count() - 1 ; i >= 0; --i) {
1142 if (!isIndexDragEnabled(m_d->model, indexes.at(i)))
1143 indexes.removeAt(i);
1144 }
1145
1146 selectionModel()->clear();
1147
1148 if (indexes.count() > 0) {
1149 QMimeData *data = m_d->model->mimeData(indexes);
1150 if (!data)
1151 return;
1152 QRect rect;
1153 QPixmap pixmap = m_d->renderToPixmap(indexes, &rect);
1154 rect.adjust(horizontalOffset(), verticalOffset(), 0, 0);
1155 QDrag *drag = new QDrag(this);
1156 drag->setPixmap(pixmap);
1157 drag->setMimeData(data);
1158 drag->setHotSpot(m_d->lastPressedPosition - rect.topLeft());
1159 drag->exec(supportedActions, Qt::MoveAction);
1160 setCurrentIndex(currentIndex());
1161 }
1162 }
1163 } else {
1173 if (m_d->lastPressedModifier & Qt::ShiftModifier) {
1174 return;
1175 }
1176
1200 QModelIndexList selectionBefore = selectionModel()->selectedIndexes();
1201 QModelIndex currentBefore = selectionModel()->currentIndex();
1202
1203 // initialize a global status variable
1204 m_d->dragWasSuccessful = false;
1205 QAbstractItemView::startDrag(supportedActions);
1206
1207 QModelIndex newCurrent;
1208 QPoint selectionOffset;
1209
1210 if (m_d->dragWasSuccessful) {
1211 newCurrent = currentIndex();
1212 selectionOffset = QPoint(newCurrent.column() - currentBefore.column(),
1213 newCurrent.row() - currentBefore.row());
1214 } else {
1215 newCurrent = currentBefore;
1216 selectionOffset = QPoint();
1217 }
1218
1219 setCurrentIndex(newCurrent);
1220 selectionModel()->clearSelection();
1221 Q_FOREACH (const QModelIndex &idx, selectionBefore) {
1222 QModelIndex newIndex =
1223 model()->index(idx.row() + selectionOffset.y(),
1224 idx.column() + selectionOffset.x());
1225 selectionModel()->select(newIndex, QItemSelectionModel::Select);
1226 }
1227 }
1228}
1229
1231{
1232 m_d->dragInProgress = true;
1233 m_d->model->setScrubState(true);
1234
1235 QTableView::dragEnterEvent(event);
1236}
1237
1239{
1240 m_d->dragInProgress = true;
1241 m_d->model->setScrubState(true);
1242
1243 QAbstractItemView::dragMoveEvent(event);
1244
1245 // Let's check for moving within a selection --
1246 // We want to override the built in qt behavior that
1247 // denies drag events when dragging within a selection...
1248 if (!event->isAccepted() && selectionModel()->isSelected(indexAt(event->pos()))) {
1249 event->setAccepted(true);
1250 }
1251
1252 if (event->isAccepted()) {
1253 QModelIndex index = indexAt(event->pos());
1254
1255 if (!m_d->model->canDropFrameData(event->mimeData(), index)) {
1256 event->ignore();
1257 } else {
1258 selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
1259 }
1260 }
1261}
1262
1264{
1265 m_d->dragInProgress = false;
1266 m_d->model->setScrubState(false);
1267
1268 QAbstractItemView::dragLeaveEvent(event);
1269}
1270
1272{
1273 m_d->dragInProgress = false;
1274 m_d->model->setScrubState(false);
1275
1276 if (event->keyboardModifiers() & Qt::ControlModifier) {
1277 event->setDropAction(Qt::CopyAction);
1278 } else if (event->keyboardModifiers() & Qt::AltModifier) {
1279 event->setDropAction(Qt::LinkAction);
1280 }
1281
1282 QAbstractItemView::dropEvent(event);
1283
1284 // Override drop event to accept drops within selected range.0
1285 QModelIndex index = indexAt(event->pos());
1286 if (!event->isAccepted() && selectionModel()->isSelected(index)) {
1287 event->setAccepted(true);
1288 const Qt::DropAction action = event->dropAction();
1289 const int row = event->pos().y();
1290 const int column = event->pos().x();
1291 if (m_d->model->dropMimeData(event->mimeData(), action, row, column, index)) {
1292 event->acceptProposedAction();
1293 }
1294 }
1295
1296 m_d->dragWasSuccessful = event->isAccepted();
1297}
1298
1300{
1301 const int scrollDirection = e->angleDelta().y() > 0 ? 1 : -1;
1302 bool mouseOverLayerPanel = verticalHeader()->geometry().contains(verticalHeader()->mapFromGlobal(e->globalPosition().toPoint()));
1303
1304 if (mouseOverLayerPanel) {
1305 QTableView::wheelEvent(e);
1306 } else { // Mouse is over frames table view...
1307 QModelIndex index = currentIndex();
1308 int column= -1;
1309
1310 if (index.isValid()) {
1311 column = index.column() + scrollDirection;
1312 }
1313
1314 if (column >= 0 && !m_d->dragInProgress) {
1316 setCurrentIndex(m_d->model->index(index.row(), column));
1317 }
1318 }
1319}
1320
1322{
1323 Q_UNUSED(event);
1324
1327}
1328
1329void KisAnimTimelineFramesView::rowsInserted(const QModelIndex& parent, int start, int end)
1330{
1331 QTableView::rowsInserted(parent, start, end);
1332}
1333
1334void KisAnimTimelineFramesView::currentChanged(const QModelIndex &current, const QModelIndex &previous)
1335{
1336 QTableView::currentChanged(current, previous);
1337
1338 if (previous.column() != current.column()) {
1339 m_d->model->setData(previous, false, KisAnimTimelineFramesModel::ActiveFrameRole);
1340 m_d->model->setData(current, true, KisAnimTimelineFramesModel::ActiveFrameRole);
1341 if ( current.column() != m_d->model->currentTime() ) {
1342 m_d->model->setData(current, QVariant(int(SEEK_FINALIZE | SEEK_PUSH_AUDIO)), KisAnimTimelineFramesModel::ScrubToRole);
1343 }
1344 }
1345}
1346
1347QItemSelectionModel::SelectionFlags KisAnimTimelineFramesView::selectionCommand(const QModelIndex &index,
1348 const QEvent *event) const
1349{
1350 // WARNING: Copy-pasted from KisNodeView! Please keep in sync!
1351
1362 if (event &&
1363 (event->type() == QEvent::MouseButtonPress ||
1364 event->type() == QEvent::MouseButtonRelease) &&
1365 index.isValid()) {
1366
1367 const QMouseEvent *mevent = static_cast<const QMouseEvent*>(event);
1368
1369 if (mevent->button() == Qt::RightButton &&
1370 selectionModel()->selectedIndexes().contains(index)) {
1371
1372 // Allow calling context menu for multiple layers
1373 return QItemSelectionModel::NoUpdate;
1374 }
1375
1376 if (event->type() == QEvent::MouseButtonPress &&
1377 (mevent->modifiers() & Qt::ControlModifier)) {
1378
1379 return QItemSelectionModel::NoUpdate;
1380 }
1381
1382 if (event->type() == QEvent::MouseButtonRelease &&
1383 (mevent->modifiers() & Qt::ControlModifier)) {
1384
1385 return QItemSelectionModel::Toggle;
1386 }
1387 }
1388
1389 return QAbstractItemView::selectionCommand(index, event);
1390}
1391
1393{
1394 m_d->fps = fps;
1395 m_d->horizontalRuler->setFramePerSecond(fps);
1396}
1397
1399{
1400 QModelIndexList indexes;
1401
1402 if (entireColumn) {
1403 QSet<int> rows;
1404 int minColumn = 0;
1405 int maxColumn = 0;
1406
1407 calculateSelectionMetrics(minColumn, maxColumn, rows, true);
1408
1409 rows.clear();
1410 for (int i = 0; i < m_d->model->rowCount(); i++) {
1411 if (editableOnly &&
1412 !m_d->model->data(m_d->model->index(i, minColumn), KisAnimTimelineFramesModel::FrameEditableRole).toBool()) continue;
1413
1414 for (int column = minColumn; column <= maxColumn; column++) {
1415 indexes << m_d->model->index(i, column);
1416 }
1417 }
1418 } else {
1419 Q_FOREACH (const QModelIndex &index, selectionModel()->selectedIndexes()) {
1420 if (!editableOnly || m_d->model->data(index, KisAnimTimelineFramesModel::FrameEditableRole).toBool()) {
1421 indexes << index;
1422 }
1423 }
1424 }
1425
1426 return indexes;
1427}
1428
1429void KisAnimTimelineFramesView::calculateSelectionMetrics(int &minColumn, int &maxColumn, QSet<int> &rows, bool ignoreEditability) const
1430{
1431 minColumn = std::numeric_limits<int>::max();
1432 maxColumn = std::numeric_limits<int>::min();
1433
1434 Q_FOREACH (const QModelIndex &index, selectionModel()->selectedIndexes()) {
1435 if (!ignoreEditability &&
1436 !m_d->model->data(index, KisAnimTimelineFramesModel::FrameEditableRole).toBool()) continue;
1437
1438 rows.insert(index.row());
1439 minColumn = qMin(minColumn, index.column());
1440 maxColumn = qMax(maxColumn, index.column());
1441 }
1442}
1443
1444void KisAnimTimelineFramesView::insertKeyframes(int count, int timing, TimelineDirection direction, bool entireColumn)
1445{
1446 QSet<int> rows;
1447 int minColumn = 0, maxColumn = 0;
1448
1449 calculateSelectionMetrics(minColumn, maxColumn, rows, entireColumn);
1450 if (minColumn > maxColumn) return;
1451
1452 if (count <= 0) { //Negative count? Use number of selected frames.
1453 count = qMax(1, maxColumn - minColumn + 1);
1454 }
1455
1456 const int insertionColumn =
1457 direction == TimelineDirection::RIGHT ?
1458 maxColumn + 1 : minColumn;
1459
1460 if (entireColumn) {
1461 rows.clear();
1462 for (int i = 0; i < m_d->model->rowCount(); i++) {
1463 if (!m_d->model->data(m_d->model->index(i, insertionColumn), KisAnimTimelineFramesModel::FrameEditableRole).toBool()) continue;
1464 rows.insert(i);
1465 }
1466 }
1467
1468 if (!rows.isEmpty()) {
1469 m_d->model->insertFrames(insertionColumn, QList<int>(rows.begin(), rows.end()), count, timing);
1470 }
1471}
1472
1474{
1475 int count, timing;
1476 TimelineDirection direction;
1477
1478 if (m_d->insertKeyframeDialog->promptUserSettings(count, timing, direction)) {
1479 insertKeyframes(count, timing, direction, entireColumn);
1480 }
1481}
1482
1484{
1485 QModelIndexList indexes;
1486
1487 // Populate indices..
1488 if (!entireColumn) {
1489 Q_FOREACH (const QModelIndex &index, selectionModel()->selectedIndexes()) {
1490 if (m_d->model->data(index, KisAnimTimelineFramesModel::FrameEditableRole).toBool()) {
1491 indexes << index;
1492 }
1493 }
1494 } else {
1495 const int column = selectionModel()->currentIndex().column();
1496
1497 for (int i = 0; i < m_d->model->rowCount(); i++) {
1498 const QModelIndex index = m_d->model->index(i, column);
1499 if (m_d->model->data(index, KisAnimTimelineFramesModel::FrameEditableRole).toBool()) {
1500 indexes << index;
1501 }
1502 }
1503 }
1504
1505 if (!indexes.isEmpty()) {
1506 m_d->model->insertHoldFrames(indexes, count);
1507
1508 // Fan selection based on insertion or deletion.
1509 // This should allow better UI/UX for insertion of keyframes or hold frames.
1510 fanSelectedFrames(indexes, count);
1511
1512 // bulk adding frames can add too many
1513 // trim timeline to clean up extra frames that might have been added
1515 }
1516}
1517
1519{
1520 bool ok = false;
1521 const int count = QInputDialog::getInt(this,
1522 i18nc("@title:window", "Insert or Remove Hold Frames"),
1523 i18nc("@label:spinbox", "Enter number of frames"),
1524 insertion ?
1525 m_d->insertKeyframeDialog->defaultTimingOfAddedFrames() :
1526 m_d->insertKeyframeDialog->defaultNumberOfHoldFramesToRemove(),
1527 1, 10000, 1, &ok);
1528
1529 if (ok) {
1530 if (insertion) {
1531 m_d->insertKeyframeDialog->setDefaultTimingOfAddedFrames(count);
1532 insertOrRemoveHoldFrames(count, entireColumn);
1533 } else {
1534 m_d->insertKeyframeDialog->setDefaultNumberOfHoldFramesToRemove(count);
1535 insertOrRemoveHoldFrames(-count, entireColumn);
1536 }
1537
1538 }
1539}
1540
1541void KisAnimTimelineFramesView::fanSelectedFrames(const QModelIndexList &selection, int count, bool ignoreKeyless) {
1542 QMap<int, QList<int>> indexMap;
1543
1544 QList<QModelIndex> selectedIndices = selection;
1545
1546 foreach (const QModelIndex &index, selectedIndices) {
1547 if (!indexMap.contains(index.row())) {
1548 indexMap.insert(index.row(), QList<int>());
1549 }
1550
1551 if (m_d->model->data(index, KisAnimTimelineFramesModel::FrameExistsRole).value<bool>() || !ignoreKeyless) {
1552 indexMap[index.row()] << index.column();
1553 }
1554 }
1555
1556 KisSignalsBlocker blockSig(selectionModel());
1557 selectionModel()->clearSelection();
1558 foreach (const int &layer, indexMap.keys()) {
1560 int progressIndex = 0;
1561
1562 std::sort(indexMap[layer].begin(), indexMap[layer].end());
1563 for (it = indexMap[layer].constBegin(); it != indexMap[layer].constEnd(); it++) {
1564 const int offsetColumn = *it + (progressIndex * count);
1565 selectionModel()->select(model()->index(layer, offsetColumn), QItemSelectionModel::Select);
1566 progressIndex++;
1567 }
1568 }
1569}
1570
1571void KisAnimTimelineFramesView::cutCopyImpl(bool entireColumn, bool copy)
1572{
1573 const QModelIndexList selectedIndices = calculateSelectionSpan(entireColumn, !copy);
1574 if (selectedIndices.isEmpty()) return;
1575
1576 int minColumn = std::numeric_limits<int>::max();
1577 int minRow = std::numeric_limits<int>::max();
1578 Q_FOREACH (const QModelIndex &index, selectedIndices) {
1579 minRow = qMin(minRow, index.row());
1580 minColumn = qMin(minColumn, index.column());
1581 }
1582
1583 const QModelIndex baseIndex = m_d->model->index(minRow, minColumn);
1584 QMimeData *data = m_d->model->mimeDataExtended(selectedIndices,
1585 baseIndex,
1586 copy ?
1589
1590 if (data) {
1591 QClipboard *cb = QApplication::clipboard();
1592 cb->setMimeData(data);
1593 }
1594}
1595
1597{
1598 const QModelIndexList selectedIndices = calculateSelectionSpan(entireColumn, false);
1599 if (selectedIndices.isEmpty()) return;
1600
1601 int minColumn = std::numeric_limits<int>::max();
1602 int minRow = std::numeric_limits<int>::max();
1603 Q_FOREACH (const QModelIndex &index, selectedIndices) {
1604 minRow = qMin(minRow, index.row());
1605 minColumn = qMin(minColumn, index.column());
1606 }
1607
1608 const QModelIndex baseIndex = m_d->model->index(minRow, minColumn);
1609 QMimeData *data = m_d->model->mimeDataExtended(selectedIndices,
1610 baseIndex,
1612
1613 if (data) {
1614 QClipboard *cb = QApplication::clipboard();
1615 cb->setMimeData(data);
1616 }
1617}
1618
1619void KisAnimTimelineFramesView::createFrameEditingMenuActions(QMenu *menu, bool emptyFrame, bool cloneFrameSelected)
1620{
1622
1623 // calculate if selection range is set. This will determine if the update playback range is available
1624 QSet<int> rows;
1625 int minColumn = 0;
1626 int maxColumn = 0;
1627 calculateSelectionMetrics(minColumn, maxColumn, rows, true);
1628 bool selectionExists = minColumn != maxColumn;
1629
1630 menu->addSection(i18n("Edit Frames:"));
1631 menu->addSeparator();
1632
1633 if (selectionExists) {
1634 KisActionManager::safePopulateMenu(menu, "update_playback_range", m_d->actionMan);
1635 } else {
1636 KisActionManager::safePopulateMenu(menu, "set_start_time", m_d->actionMan);
1637 KisActionManager::safePopulateMenu(menu, "set_end_time", m_d->actionMan);
1638 }
1639
1640 menu->addSeparator();
1641
1642 if (!emptyFrame) {
1643 KisActionManager::safePopulateMenu(menu, "cut_frames", m_d->actionMan);
1644 KisActionManager::safePopulateMenu(menu, "copy_frames", m_d->actionMan);
1645 KisActionManager::safePopulateMenu(menu, "copy_frames_as_clones", m_d->actionMan);
1646 }
1647
1648 KisActionManager::safePopulateMenu(menu, "paste_frames", m_d->actionMan);
1649
1650 if (!emptyFrame && cloneFrameSelected) {
1651 KisActionManager::safePopulateMenu(menu, "make_clones_unique", m_d->actionMan);
1652 }
1653
1654 menu->addSeparator();
1655
1656 { //Frames submenu.
1657 QMenu *frames = menu->addMenu(i18nc("@item:inmenu", "Keyframes"));
1658 KisActionManager::safePopulateMenu(frames, "insert_keyframe_left", m_d->actionMan);
1659 KisActionManager::safePopulateMenu(frames, "insert_keyframe_right", m_d->actionMan);
1660 frames->addSeparator();
1661 KisActionManager::safePopulateMenu(frames, "insert_multiple_keyframes", m_d->actionMan);
1662 }
1663
1664 { //Holds submenu.
1665 QMenu *hold = menu->addMenu(i18nc("@item:inmenu", "Hold Frames"));
1666 KisActionManager::safePopulateMenu(hold, "insert_hold_frame", m_d->actionMan);
1667 KisActionManager::safePopulateMenu(hold, "remove_hold_frame", m_d->actionMan);
1668 hold->addSeparator();
1669 KisActionManager::safePopulateMenu(hold, "insert_multiple_hold_frames", m_d->actionMan);
1670 KisActionManager::safePopulateMenu(hold, "remove_multiple_hold_frames", m_d->actionMan);
1671 }
1672
1673 menu->addSeparator();
1674
1675 if (!emptyFrame) {
1676 KisActionManager::safePopulateMenu(menu, "remove_frames", m_d->actionMan);
1677 }
1678 KisActionManager::safePopulateMenu(menu, "remove_frames_and_pull", m_d->actionMan);
1679
1680 menu->addSeparator();
1681
1682 if (emptyFrame) {
1683 KisActionManager::safePopulateMenu(menu, "add_blank_frame", m_d->actionMan);
1684 KisActionManager::safePopulateMenu(menu, "add_duplicate_frame", m_d->actionMan);
1685 menu->addSeparator();
1686 }
1687}
1688
1690 const int sectionWidth = m_d->horizontalRuler->defaultSectionSize();
1691 return sectionWidth * column;
1692}
1693
1695{
1696#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
1697 QStyleOptionViewItem option = q->viewOptions();
1698#else
1699 QStyleOptionViewItem option;
1700 q->initViewItemOption(&option);
1701#endif
1702 option.locale = q->locale();
1703 option.locale.setNumberOptions(QLocale::OmitGroupSeparator);
1704 option.widget = q;
1705 return option;
1706}
1707
1709{
1710 Q_ASSERT(r);
1711 QRect &rect = *r;
1712 const QRect viewportRect = q->viewport()->rect();
1714 for (int i = 0; i < indexes.count(); ++i) {
1715 const QModelIndex &index = indexes.at(i);
1716 const QRect current = q->visualRect(index);
1717 if (current.intersects(viewportRect)) {
1718 ret += qMakePair(current, index);
1719 rect |= current;
1720 }
1721 }
1722 rect &= viewportRect;
1723 return ret;
1724}
1725
1727{
1728 Q_ASSERT(r);
1729 QItemViewPaintPairs paintPairs = draggablePaintPairs(indexes, r);
1730
1731 if (paintPairs.isEmpty())
1732 return QPixmap();
1733
1734 QPixmap pixmap(r->size());
1735 pixmap.fill(Qt::transparent);
1736
1737 QPainter painter(&pixmap);
1738
1739 QStyleOptionViewItem option = viewOptionsV4();
1740 option.state |= QStyle::State_Selected;
1741
1742 for (int j = 0; j < paintPairs.count(); ++j) {
1743 option.rect = paintPairs.at(j).first.translated(-r->topLeft());
1744 const QModelIndex &current = paintPairs.at(j).second;
1745 //adjustViewOptionsForIndex(&option, current);
1746
1747#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1748 q->itemDelegate(current)->paint(&painter, option, current);
1749#else
1750 q->itemDelegateForIndex(current)->paint(&painter, option, current);
1751#endif
1752 }
1753
1754 return pixmap;
1755}
1756
1757void resizeToMinimalSize(QAbstractButton *w, int minimalSize)
1758{
1759 QSize buttonSize = w->sizeHint();
1760
1761 if (buttonSize.height() > minimalSize) {
1762 buttonSize = QSize(minimalSize, minimalSize);
1763 }
1764
1765 w->resize(buttonSize);
1766}
1767
1768inline bool isIndexDragEnabled(QAbstractItemModel *model, const QModelIndex &index)
1769{
1770 return (model->flags(index) & Qt::ItemIsDragEnabled);
1771}
float value(const T *src, size_t ch)
bool isIndexDragEnabled(QAbstractItemModel *model, const QModelIndex &index)
void resizeToMinimalSize(QAbstractButton *w, int minimalSize)
QPair< QRect, QModelIndex > QItemViewPaintPair
QList< QItemViewPaintPair > QItemViewPaintPairs
@ SEEK_PUSH_AUDIO
@ SEEK_FINALIZE
static int buttonSize(int screen)
Definition KoToolBox.cpp:41
A KisActionManager class keeps track of KisActions. These actions are always associated with the GUI....
static void safePopulateMenu(QMenu *menu, const QString &actionId, KisActionManager *actionManager)
void insertOrRemoveMultipleHoldFrames(bool insertion, bool entireColumn=false)
void slotRemoveSelectedFrames(bool entireColumn=false, bool pull=false)
void slotFitViewToFrameRange(int start, int end)
void slotCanvasUpdate(class KoCanvasBase *canvas)
void mouseReleaseEvent(QMouseEvent *e) override
void insertOrRemoveHoldFrames(int count, bool entireColumn=false)
void dragEnterEvent(QDragEnterEvent *event) override
void wheelEvent(QWheelEvent *e) override
void setActionManager(KisActionManager *actionManager)
void dropEvent(QDropEvent *event) override
void insertKeyframes(int count=1, int timing=1, TimelineDirection direction=TimelineDirection::LEFT, bool entireColumn=false)
void slotDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
void dragLeaveEvent(QDragLeaveEvent *event) override
void calculateSelectionMetrics(int &minColumn, int &maxColumn, QSet< int > &rows, bool ignoreEditability) const
void startDrag(Qt::DropActions supportedActions) override
void slotHeaderDataChanged(Qt::Orientation orientation, int first, int last)
void dragMoveEvent(QDragMoveEvent *event) override
void slotMirrorFrames(bool entireColumn=false)
void mouseMoveEvent(QMouseEvent *e) override
bool viewportEvent(QEvent *event) override
void rowsInserted(const QModelIndex &parent, int start, int end) override
const QScopedPointer< Private > m_d
void resizeEvent(QResizeEvent *e) override
void currentChanged(const QModelIndex &current, const QModelIndex &previous) override
void mousePressEvent(QMouseEvent *event) override
void createFrameEditingMenuActions(QMenu *menu, bool emptyFrame, bool cloneFrameSelected)
void cutCopyImpl(bool entireColumn, bool copy)
void slotScrollerStateChanged(QScroller::State state)
void slotTryTransferSelectionBetweenRows(int fromRow, int toRow)
QItemSelectionModel::SelectionFlags selectionCommand(const QModelIndex &index, const QEvent *event) const override
void fanSelectedFrames(const QModelIndexList &selection, int count, bool ignoreKeyless=true)
void slotLayerContextMenuRequested(const QPoint &globalPos)
void mouseDoubleClickEvent(QMouseEvent *event) override
void setModel(QAbstractItemModel *model) override
void insertMultipleKeyframes(bool entireColumn=false)
void slotPasteFrames(bool entireColumn=false)
QModelIndexList calculateSelectionSpan(bool entireColumn, bool editableOnly=true) const
void calculateActiveLayerSelectedTimes(const QModelIndexList &selection)
KisCanvasAnimationState * animationState() const
The KisCanvasAnimationState class stores all of the canvas-specific animation state.
void currentIndexChanged(int index)
The KisCustomModifiersCatcher class is a special utility class that tracks custom modifiers pressed....
int defaultFrameColorLabel() const
void setDefaultFrameColorLabel(int label)
static QString askForAudioFileName(const QString &defaultDir, QWidget *parent)
static KisTimeSpan fromTimeWithDuration(int start, int duration)
void overscroll(qreal delta)
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
#define KIS_ASSERT(cond)
Definition kis_assert.h:33
const QString newLayerActionName
const QString removeLayerActionName
const QString pinExistingLayerActionName
QIcon loadIcon(const QString &name)
KRITAWIDGETUTILS_EXPORT QScroller::ScrollerGestureType getConfiguredGestureType()
KRITAWIDGETUTILS_EXPORT void updateCursor(QWidget *source, QScroller::State state)
KRITAWIDGETUTILS_EXPORT QScroller * createPreconfiguredScroller(QAbstractScrollArea *target)
TimelineInsertKeyframeDialog * insertKeyframeDialog
QPixmap renderToPixmap(const QModelIndexList &indexes, QRect *r) const
KisColorLabelSelectorWidgetMenuWrapper * multiframeColorSelector
QItemViewPaintPairs draggablePaintPairs(const QModelIndexList &indexes, QRect *r) const
KisColorLabelSelectorWidgetMenuWrapper * colorSelector