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);
221 connect(m_d->multiframeColorSelector->colorLabelSelector(), &KisColorLabelSelectorWidget::currentIndexChanged, this, &KisAnimTimelineFramesView::slotColorLabelChanged);
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 if (current.row() != previous.row()) {
1346 m_d->model->requestNodeChange(current);
1347 }
1348}
1349
1350QItemSelectionModel::SelectionFlags KisAnimTimelineFramesView::selectionCommand(const QModelIndex &index,
1351 const QEvent *event) const
1352{
1353 // WARNING: Copy-pasted from KisNodeView! Please keep in sync!
1354
1365 if (event &&
1366 (event->type() == QEvent::MouseButtonPress ||
1367 event->type() == QEvent::MouseButtonRelease) &&
1368 index.isValid()) {
1369
1370 const QMouseEvent *mevent = static_cast<const QMouseEvent*>(event);
1371
1372 if (mevent->button() == Qt::RightButton &&
1373 selectionModel()->selectedIndexes().contains(index)) {
1374
1375 // Allow calling context menu for multiple layers
1376 return QItemSelectionModel::NoUpdate;
1377 }
1378
1379 if (event->type() == QEvent::MouseButtonPress &&
1380 (mevent->modifiers() & Qt::ControlModifier)) {
1381
1382 return QItemSelectionModel::NoUpdate;
1383 }
1384
1385 if (event->type() == QEvent::MouseButtonRelease &&
1386 (mevent->modifiers() & Qt::ControlModifier)) {
1387
1388 return QItemSelectionModel::Toggle;
1389 }
1390 }
1391
1392 return QAbstractItemView::selectionCommand(index, event);
1393}
1394
1396{
1397 m_d->fps = fps;
1398 m_d->horizontalRuler->setFramePerSecond(fps);
1399}
1400
1402{
1403 QModelIndexList indexes;
1404
1405 if (entireColumn) {
1406 QSet<int> rows;
1407 int minColumn = 0;
1408 int maxColumn = 0;
1409
1410 calculateSelectionMetrics(minColumn, maxColumn, rows, true);
1411
1412 rows.clear();
1413 for (int i = 0; i < m_d->model->rowCount(); i++) {
1414 if (editableOnly &&
1415 !m_d->model->data(m_d->model->index(i, minColumn), KisAnimTimelineFramesModel::FrameEditableRole).toBool()) continue;
1416
1417 for (int column = minColumn; column <= maxColumn; column++) {
1418 indexes << m_d->model->index(i, column);
1419 }
1420 }
1421 } else {
1422 Q_FOREACH (const QModelIndex &index, selectionModel()->selectedIndexes()) {
1423 if (!editableOnly || m_d->model->data(index, KisAnimTimelineFramesModel::FrameEditableRole).toBool()) {
1424 indexes << index;
1425 }
1426 }
1427 }
1428
1429 return indexes;
1430}
1431
1432void KisAnimTimelineFramesView::calculateSelectionMetrics(int &minColumn, int &maxColumn, QSet<int> &rows, bool ignoreEditability) const
1433{
1434 minColumn = std::numeric_limits<int>::max();
1435 maxColumn = std::numeric_limits<int>::min();
1436
1437 Q_FOREACH (const QModelIndex &index, selectionModel()->selectedIndexes()) {
1438 if (!ignoreEditability &&
1439 !m_d->model->data(index, KisAnimTimelineFramesModel::FrameEditableRole).toBool()) continue;
1440
1441 rows.insert(index.row());
1442 minColumn = qMin(minColumn, index.column());
1443 maxColumn = qMax(maxColumn, index.column());
1444 }
1445}
1446
1447void KisAnimTimelineFramesView::insertKeyframes(int count, int timing, TimelineDirection direction, bool entireColumn)
1448{
1449 QSet<int> rows;
1450 int minColumn = 0, maxColumn = 0;
1451
1452 calculateSelectionMetrics(minColumn, maxColumn, rows, entireColumn);
1453 if (minColumn > maxColumn) return;
1454
1455 if (count <= 0) { //Negative count? Use number of selected frames.
1456 count = qMax(1, maxColumn - minColumn + 1);
1457 }
1458
1459 const int insertionColumn =
1460 direction == TimelineDirection::RIGHT ?
1461 maxColumn + 1 : minColumn;
1462
1463 if (entireColumn) {
1464 rows.clear();
1465 for (int i = 0; i < m_d->model->rowCount(); i++) {
1466 if (!m_d->model->data(m_d->model->index(i, insertionColumn), KisAnimTimelineFramesModel::FrameEditableRole).toBool()) continue;
1467 rows.insert(i);
1468 }
1469 }
1470
1471 if (!rows.isEmpty()) {
1472 m_d->model->insertFrames(insertionColumn, QList<int>(rows.begin(), rows.end()), count, timing);
1473 }
1474}
1475
1477{
1478 int count, timing;
1479 TimelineDirection direction;
1480
1481 if (m_d->insertKeyframeDialog->promptUserSettings(count, timing, direction)) {
1482 insertKeyframes(count, timing, direction, entireColumn);
1483 }
1484}
1485
1487{
1488 QModelIndexList indexes;
1489
1490 // Populate indices..
1491 if (!entireColumn) {
1492 Q_FOREACH (const QModelIndex &index, selectionModel()->selectedIndexes()) {
1493 if (m_d->model->data(index, KisAnimTimelineFramesModel::FrameEditableRole).toBool()) {
1494 indexes << index;
1495 }
1496 }
1497 } else {
1498 const int column = selectionModel()->currentIndex().column();
1499
1500 for (int i = 0; i < m_d->model->rowCount(); i++) {
1501 const QModelIndex index = m_d->model->index(i, column);
1502 if (m_d->model->data(index, KisAnimTimelineFramesModel::FrameEditableRole).toBool()) {
1503 indexes << index;
1504 }
1505 }
1506 }
1507
1508 if (!indexes.isEmpty()) {
1509 m_d->model->insertHoldFrames(indexes, count);
1510
1511 // Fan selection based on insertion or deletion.
1512 // This should allow better UI/UX for insertion of keyframes or hold frames.
1513 fanSelectedFrames(indexes, count);
1514
1515 // bulk adding frames can add too many
1516 // trim timeline to clean up extra frames that might have been added
1518 }
1519}
1520
1522{
1523 bool ok = false;
1524 const int count = QInputDialog::getInt(this,
1525 i18nc("@title:window", "Insert or Remove Hold Frames"),
1526 i18nc("@label:spinbox", "Enter number of frames"),
1527 insertion ?
1528 m_d->insertKeyframeDialog->defaultTimingOfAddedFrames() :
1529 m_d->insertKeyframeDialog->defaultNumberOfHoldFramesToRemove(),
1530 1, 10000, 1, &ok);
1531
1532 if (ok) {
1533 if (insertion) {
1534 m_d->insertKeyframeDialog->setDefaultTimingOfAddedFrames(count);
1535 insertOrRemoveHoldFrames(count, entireColumn);
1536 } else {
1537 m_d->insertKeyframeDialog->setDefaultNumberOfHoldFramesToRemove(count);
1538 insertOrRemoveHoldFrames(-count, entireColumn);
1539 }
1540
1541 }
1542}
1543
1544void KisAnimTimelineFramesView::fanSelectedFrames(const QModelIndexList &selection, int count, bool ignoreKeyless) {
1545 QMap<int, QList<int>> indexMap;
1546
1547 QList<QModelIndex> selectedIndices = selection;
1548
1549 foreach (const QModelIndex &index, selectedIndices) {
1550 if (!indexMap.contains(index.row())) {
1551 indexMap.insert(index.row(), QList<int>());
1552 }
1553
1554 if (m_d->model->data(index, KisAnimTimelineFramesModel::FrameExistsRole).value<bool>() || !ignoreKeyless) {
1555 indexMap[index.row()] << index.column();
1556 }
1557 }
1558
1559 KisSignalsBlocker blockSig(selectionModel());
1560 selectionModel()->clearSelection();
1561 foreach (const int &layer, indexMap.keys()) {
1563 int progressIndex = 0;
1564
1565 std::sort(indexMap[layer].begin(), indexMap[layer].end());
1566 for (it = indexMap[layer].constBegin(); it != indexMap[layer].constEnd(); it++) {
1567 const int offsetColumn = *it + (progressIndex * count);
1568 selectionModel()->select(model()->index(layer, offsetColumn), QItemSelectionModel::Select);
1569 progressIndex++;
1570 }
1571 }
1572}
1573
1574void KisAnimTimelineFramesView::cutCopyImpl(bool entireColumn, bool copy)
1575{
1576 const QModelIndexList selectedIndices = calculateSelectionSpan(entireColumn, !copy);
1577 if (selectedIndices.isEmpty()) return;
1578
1579 int minColumn = std::numeric_limits<int>::max();
1580 int minRow = std::numeric_limits<int>::max();
1581 Q_FOREACH (const QModelIndex &index, selectedIndices) {
1582 minRow = qMin(minRow, index.row());
1583 minColumn = qMin(minColumn, index.column());
1584 }
1585
1586 const QModelIndex baseIndex = m_d->model->index(minRow, minColumn);
1587 QMimeData *data = m_d->model->mimeDataExtended(selectedIndices,
1588 baseIndex,
1589 copy ?
1592
1593 if (data) {
1594 QClipboard *cb = QApplication::clipboard();
1595 cb->setMimeData(data);
1596 }
1597}
1598
1600{
1601 const QModelIndexList selectedIndices = calculateSelectionSpan(entireColumn, false);
1602 if (selectedIndices.isEmpty()) return;
1603
1604 int minColumn = std::numeric_limits<int>::max();
1605 int minRow = std::numeric_limits<int>::max();
1606 Q_FOREACH (const QModelIndex &index, selectedIndices) {
1607 minRow = qMin(minRow, index.row());
1608 minColumn = qMin(minColumn, index.column());
1609 }
1610
1611 const QModelIndex baseIndex = m_d->model->index(minRow, minColumn);
1612 QMimeData *data = m_d->model->mimeDataExtended(selectedIndices,
1613 baseIndex,
1615
1616 if (data) {
1617 QClipboard *cb = QApplication::clipboard();
1618 cb->setMimeData(data);
1619 }
1620}
1621
1622void KisAnimTimelineFramesView::createFrameEditingMenuActions(QMenu *menu, bool emptyFrame, bool cloneFrameSelected)
1623{
1625
1626 // calculate if selection range is set. This will determine if the update playback range is available
1627 QSet<int> rows;
1628 int minColumn = 0;
1629 int maxColumn = 0;
1630 calculateSelectionMetrics(minColumn, maxColumn, rows, true);
1631 bool selectionExists = minColumn != maxColumn;
1632
1633 menu->addSection(i18n("Edit Frames:"));
1634 menu->addSeparator();
1635
1636 if (selectionExists) {
1637 KisActionManager::safePopulateMenu(menu, "update_playback_range", m_d->actionMan);
1638 } else {
1639 KisActionManager::safePopulateMenu(menu, "set_start_time", m_d->actionMan);
1640 KisActionManager::safePopulateMenu(menu, "set_end_time", m_d->actionMan);
1641 }
1642
1643 menu->addSeparator();
1644
1645 if (!emptyFrame) {
1646 KisActionManager::safePopulateMenu(menu, "cut_frames", m_d->actionMan);
1647 KisActionManager::safePopulateMenu(menu, "copy_frames", m_d->actionMan);
1648 KisActionManager::safePopulateMenu(menu, "copy_frames_as_clones", m_d->actionMan);
1649 }
1650
1651 KisActionManager::safePopulateMenu(menu, "paste_frames", m_d->actionMan);
1652
1653 if (!emptyFrame && cloneFrameSelected) {
1654 KisActionManager::safePopulateMenu(menu, "make_clones_unique", m_d->actionMan);
1655 }
1656
1657 menu->addSeparator();
1658
1659 { //Frames submenu.
1660 QMenu *frames = menu->addMenu(i18nc("@item:inmenu", "Keyframes"));
1661 KisActionManager::safePopulateMenu(frames, "insert_keyframe_left", m_d->actionMan);
1662 KisActionManager::safePopulateMenu(frames, "insert_keyframe_right", m_d->actionMan);
1663 frames->addSeparator();
1664 KisActionManager::safePopulateMenu(frames, "insert_multiple_keyframes", m_d->actionMan);
1665 }
1666
1667 { //Holds submenu.
1668 QMenu *hold = menu->addMenu(i18nc("@item:inmenu", "Hold Frames"));
1669 KisActionManager::safePopulateMenu(hold, "insert_hold_frame", m_d->actionMan);
1670 KisActionManager::safePopulateMenu(hold, "remove_hold_frame", m_d->actionMan);
1671 hold->addSeparator();
1672 KisActionManager::safePopulateMenu(hold, "insert_multiple_hold_frames", m_d->actionMan);
1673 KisActionManager::safePopulateMenu(hold, "remove_multiple_hold_frames", m_d->actionMan);
1674 }
1675
1676 menu->addSeparator();
1677
1678 if (!emptyFrame) {
1679 KisActionManager::safePopulateMenu(menu, "remove_frames", m_d->actionMan);
1680 }
1681 KisActionManager::safePopulateMenu(menu, "remove_frames_and_pull", m_d->actionMan);
1682
1683 menu->addSeparator();
1684
1685 if (emptyFrame) {
1686 KisActionManager::safePopulateMenu(menu, "add_blank_frame", m_d->actionMan);
1687 KisActionManager::safePopulateMenu(menu, "add_duplicate_frame", m_d->actionMan);
1688 menu->addSeparator();
1689 }
1690}
1691
1693 const int sectionWidth = m_d->horizontalRuler->defaultSectionSize();
1694 return sectionWidth * column;
1695}
1696
1698{
1699#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
1700 QStyleOptionViewItem option = q->viewOptions();
1701#else
1702 QStyleOptionViewItem option;
1703 q->initViewItemOption(&option);
1704#endif
1705 option.locale = q->locale();
1706 option.locale.setNumberOptions(QLocale::OmitGroupSeparator);
1707 option.widget = q;
1708 return option;
1709}
1710
1712{
1713 Q_ASSERT(r);
1714 QRect &rect = *r;
1715 const QRect viewportRect = q->viewport()->rect();
1717 for (int i = 0; i < indexes.count(); ++i) {
1718 const QModelIndex &index = indexes.at(i);
1719 const QRect current = q->visualRect(index);
1720 if (current.intersects(viewportRect)) {
1721 ret += qMakePair(current, index);
1722 rect |= current;
1723 }
1724 }
1725 rect &= viewportRect;
1726 return ret;
1727}
1728
1730{
1731 Q_ASSERT(r);
1732 QItemViewPaintPairs paintPairs = draggablePaintPairs(indexes, r);
1733
1734 if (paintPairs.isEmpty())
1735 return QPixmap();
1736
1737 QPixmap pixmap(r->size());
1738 pixmap.fill(Qt::transparent);
1739
1740 QPainter painter(&pixmap);
1741
1742 QStyleOptionViewItem option = viewOptionsV4();
1743 option.state |= QStyle::State_Selected;
1744
1745 for (int j = 0; j < paintPairs.count(); ++j) {
1746 option.rect = paintPairs.at(j).first.translated(-r->topLeft());
1747 const QModelIndex &current = paintPairs.at(j).second;
1748 //adjustViewOptionsForIndex(&option, current);
1749
1750#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1751 q->itemDelegate(current)->paint(&painter, option, current);
1752#else
1753 q->itemDelegateForIndex(current)->paint(&painter, option, current);
1754#endif
1755 }
1756
1757 return pixmap;
1758}
1759
1760void resizeToMinimalSize(QAbstractButton *w, int minimalSize)
1761{
1762 QSize buttonSize = w->sizeHint();
1763
1764 if (buttonSize.height() > minimalSize) {
1765 buttonSize = QSize(minimalSize, minimalSize);
1766 }
1767
1768 w->resize(buttonSize);
1769}
1770
1771inline bool isIndexDragEnabled(QAbstractItemModel *model, const QModelIndex &index)
1772{
1773 return (model->flags(index) & Qt::ItemIsDragEnabled);
1774}
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