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