Krita Source Code Documentation
Loading...
Searching...
No Matches
KisAnimTimelineTimeHeader.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2015 Dmitry Kazakov <dimula73@gmail.com>
3 * SPDX-FileCopyrightText: 2021 Eoin O'Neil <eoinoneill1991@gmail.com>
4 * SPDX-FileCopyrightText: 2021 Emmet O'Neill <emmetoneill.pdx@gmail.com>
5 *
6 * SPDX-License-Identifier: GPL-2.0-or-later
7 */
8
10
11#include <limits>
12
13#include <QMenu>
14#include <QAction>
15#include <QPainter>
16#include <QPaintEvent>
17#include <KisPlaybackEngine.h>
18
19#include <klocalizedstring.h>
20
23#include "kis_action.h"
25#include "kis_config.h"
26
27#include "kis_debug.h"
28
30{
32 : model(nullptr)
33 , actionMan(nullptr)
34 , fps(12)
36 {
37 // Compressed configuration writing..
38 const int compressorDelayMS = 100;
40 new KisSignalCompressorWithParam<qreal>(compressorDelayMS,
41 [](qreal zoomValue){
42 KisConfig cfg(false);
43 cfg.setTimelineZoom(zoomValue);
44 },
46 );
47 }
48
51 QScopedPointer<KisSignalCompressorWithParam<qreal>> zoomSaveCompressor;
52
53 int fps;
55
56 qreal offset = 0.0f;
57 const int minSectionSize = 4;
58 const int maxSectionSize = 72;
59 const int unitSectionSize = 18;
60 qreal remainder = 0.0f;
61
62 int calcSpanWidth(const int sectionWidth);
63 QModelIndexList prepareFramesSlab(int startCol, int endCol);
64};
65
67 : QHeaderView(Qt::Horizontal, parent)
68 , m_d(new Private)
69{
70 setSectionResizeMode(QHeaderView::Fixed);
71 setDefaultSectionSize(18);
72 setMinimumSectionSize(8);
73}
74
78
80{
81 m_d->offset = qMax(offset, qreal(0.f));
82 setOffset(m_d->offset);
83 viewport()->update();
84}
85
87{
88 m_d->actionMan = actionManager;
89
91
92 if (actionManager) {
93 KisAction *action;
94
95 action = actionManager->createAction("insert_column_left");
96 connect(action, SIGNAL(triggered()), SIGNAL(sigInsertColumnLeft()));
97
98 action = actionManager->createAction("insert_column_right");
99 connect(action, SIGNAL(triggered()), SIGNAL(sigInsertColumnRight()));
100
101 action = actionManager->createAction("insert_multiple_columns");
102 connect(action, SIGNAL(triggered()), SIGNAL(sigInsertMultipleColumns()));
103
104 action = actionManager->createAction("remove_columns_and_pull");
105 connect(action, SIGNAL(triggered()), SIGNAL(sigRemoveColumnsAndShift()));
106
107 action = actionManager->createAction("remove_columns");
108 connect(action, SIGNAL(triggered()), SIGNAL(sigRemoveColumns()));
109
110 action = actionManager->createAction("insert_hold_column");
111 connect(action, SIGNAL(triggered()), SIGNAL(sigInsertHoldColumns()));
112
113 action = actionManager->createAction("insert_multiple_hold_columns");
114 connect(action, SIGNAL(triggered()), SIGNAL(sigInsertHoldColumnsCustom()));
115
116 action = actionManager->createAction("remove_hold_column");
117 connect(action, SIGNAL(triggered()), SIGNAL(sigRemoveHoldColumns()));
118
119 action = actionManager->createAction("remove_multiple_hold_columns");
120 connect(action, SIGNAL(triggered()), SIGNAL(sigRemoveHoldColumnsCustom()));
121
122 action = actionManager->createAction("mirror_columns");
123 connect(action, SIGNAL(triggered()), SIGNAL(sigMirrorColumns()));
124
125 action = actionManager->createAction("clear_animation_cache");
126 connect(action, SIGNAL(triggered()), SIGNAL(sigClearCache()));
127
128 action = actionManager->createAction("copy_columns_to_clipboard");
129 connect(action, SIGNAL(triggered()), SIGNAL(sigCopyColumns()));
130
131 action = actionManager->createAction("cut_columns_to_clipboard");
132 connect(action, SIGNAL(triggered()), SIGNAL(sigCutColumns()));
133
134 action = actionManager->createAction("paste_columns_from_clipboard");
135 connect(action, SIGNAL(triggered()), SIGNAL(sigPasteColumns()));
136
137 KisConfig cfg(true);
138 setZoom(cfg.timelineZoom());
140 }
141}
142
143
145{
146 QHeaderView::paintEvent(e);
147
148 // Copied from Qt 4.8...
149
150 if (count() == 0)
151 return;
152
153 QPainter painter(viewport());
154 const QPoint offset = dirtyRegionOffset();
155 QRect translatedEventRect = e->rect();
156 translatedEventRect.translate(offset);
157
158 int start = -1;
159 int end = -1;
160 if (orientation() == Qt::Horizontal) {
161 start = visualIndexAt(translatedEventRect.left());
162 end = visualIndexAt(translatedEventRect.right());
163 } else {
164 start = visualIndexAt(translatedEventRect.top());
165 end = visualIndexAt(translatedEventRect.bottom());
166 }
167
168 const bool reverseImpl = orientation() == Qt::Horizontal && isRightToLeft();
169
170 if (reverseImpl) {
171 start = (start == -1 ? count() - 1 : start);
172 end = (end == -1 ? 0 : end);
173 } else {
174 start = (start == -1 ? 0 : start);
175 end = (end == -1 ? count() - 1 : end);
176 }
177
178 int tmp = start;
179 start = qMin(start, end);
180 end = qMax(tmp, end);
181
184
185 const int spanStart = start - start % m_d->fps;
186 const int spanEnd = end - end % m_d->fps + m_d->fps - 1;
187
188 start = spanStart;
189 end = qMin(count() - 1, spanEnd);
190
193
194 QRect currentSectionRect;
195 int logical;
196 const int width = viewport()->width();
197 const int height = viewport()->height();
198
199 for (int i = start; i <= end; ++i) {
200 // DK: cannot copy-paste easily...
201 // if (d->isVisualIndexHidden(i))
202 // continue;
203 painter.save();
204 logical = logicalIndex(i);
205 if (orientation() == Qt::Horizontal) {
206 currentSectionRect.setRect(sectionViewportPosition(logical), 0, sectionSize(logical), height);
207 } else {
208 currentSectionRect.setRect(0, sectionViewportPosition(logical), width, sectionSize(logical));
209 }
210 currentSectionRect.translate(offset);
211
212 QVariant variant = model()->headerData(logical, orientation(),
213 Qt::FontRole);
214 if (variant.isValid() && variant.canConvert<QFont>()) {
215 QFont sectionFont = qvariant_cast<QFont>(variant);
216 painter.setFont(sectionFont);
217 }
218 paintSection1(&painter, currentSectionRect, logical);
219 painter.restore();
220 }
221}
222
223void KisAnimTimelineTimeHeader::paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const
224{
225 // Base paint event should paint nothing in the sections area
226
227 Q_UNUSED(painter);
228 Q_UNUSED(rect);
229 Q_UNUSED(logicalIndex);
230}
231
232void KisAnimTimelineTimeHeader::paintSpan(QPainter *painter, int userFrameId,
233 const QRect &spanRect,
234 bool isIntegralLine,
235 bool isPrevIntegralLine,
236 QStyle *style,
237 const QPalette &palette,
238 const QPen &gridPen) const
239{
240 painter->fillRect(spanRect, palette.brush(QPalette::Button));
241
242 int safeRight = spanRect.right();
243
244 QPen oldPen = painter->pen();
245 painter->setPen(gridPen);
246
247 int adjustedTop = spanRect.top() + (!isIntegralLine ? spanRect.height() / 2 : 0);
248 painter->drawLine(safeRight, adjustedTop, safeRight, spanRect.bottom());
249
250 if (isPrevIntegralLine) {
251 painter->drawLine(spanRect.left() + 1, spanRect.top(), spanRect.left() + 1, spanRect.bottom());
252 }
253
254 painter->setPen(oldPen);
255
256 QString frameIdText = QString::number(userFrameId);
257 QRect textRect(spanRect.topLeft() + QPoint(2, 0), QSize(spanRect.width() - 2, spanRect.height()));
258
259 QStyleOptionHeader opt;
260 initStyleOption(&opt);
261
262 QStyle::State state = QStyle::State_None;
263 if (isEnabled())
264 state |= QStyle::State_Enabled;
265 if (window()->isActiveWindow())
266 state |= QStyle::State_Active;
267 opt.state |= state;
268 opt.selectedPosition = QStyleOptionHeader::NotAdjacent;
269
270 opt.textAlignment = Qt::AlignLeft | Qt::AlignTop;
271 opt.rect = textRect;
272 opt.text = frameIdText;
273 style->drawControl(QStyle::CE_HeaderLabel, &opt, painter, this);
274}
275
277{
278 m_d->zoomSaveCompressor->start(value);
279}
280
282 const int minWidth = 36;
283
284 int spanWidth = this->fps;
285
286 while (spanWidth * sectionWidth < minWidth) {
287 spanWidth *= 2;
288 }
289
290 bool splitHappened = false;
291
292 do {
293 splitHappened = false;
294
295 if (!(spanWidth & 0x1) &&
296 spanWidth * sectionWidth / 2 > minWidth) {
297
298 spanWidth /= 2;
299 splitHappened = true;
300
301 } else if (!(spanWidth % 3) &&
302 spanWidth * sectionWidth / 3 > minWidth) {
303
304 spanWidth /= 3;
305 splitHappened = true;
306
307 } else if (!(spanWidth % 5) &&
308 spanWidth * sectionWidth / 5 > minWidth) {
309
310 spanWidth /= 5;
311 splitHappened = true;
312 }
313
314 } while (splitHappened);
315
316
317 if (sectionWidth > minWidth) {
318 spanWidth = 1;
319 }
320
321 return spanWidth;
322}
323
324void KisAnimTimelineTimeHeader::paintSection1(QPainter *painter, const QRect &rect, int logicalIndex) const
325{
326
327 if (!rect.isValid())
328 return;
329
330 QFontMetrics metrics(this->font());
331 const int textHeight = metrics.height();
332
333 QPoint p1 = rect.topLeft() + QPoint(0, textHeight);
334 QPoint p2 = rect.topRight() + QPoint(0, textHeight);
335
336 QRect frameRect = QRect(p1, QSize(rect.width(), rect.height() - textHeight));
337
338 const int width = rect.width();
339
340 int spanWidth = m_d->calcSpanWidth(width);
341
342 const int internalIndex = logicalIndex % spanWidth;
343 const int userFrameId = logicalIndex;
344
345 const int spanEnd = qMin(count(), logicalIndex + spanWidth);
346 QRect spanRect(rect.topLeft(), QSize(width * (spanEnd - logicalIndex), textHeight));
347
348#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
349 QStyleOptionViewItem option = viewOptions();
350#else
351 QStyleOptionViewItem option;
352 initViewItemOption(&option);
353#endif
354 const int gridHint = style()->styleHint(QStyle::SH_Table_GridLineColor, &option, this);
355 const QColor gridColor = static_cast<QRgb>(gridHint);
356 const QPen gridPen = QPen(gridColor);
357
358 if (!internalIndex) {
359 bool isIntegralLine = (logicalIndex + spanWidth) % m_d->fps == 0;
360 bool isPrevIntegralLine = logicalIndex % m_d->fps == 0;
361 paintSpan(painter, userFrameId, spanRect, isIntegralLine, isPrevIntegralLine, style(), palette(), gridPen);
362 }
363
364 {
365 QBrush fillColor = KisAnimTimelineColors::instance()->headerEmpty();
366
367 QVariant activeValue = model()->headerData(logicalIndex, orientation(),
369
370 QVariant cachedValue = model()->headerData(logicalIndex, orientation(),
372
373 QVariant withinRangeValue = model()->headerData(logicalIndex, orientation(),
375
376 const bool isActive = activeValue.isValid() && activeValue.toBool();
377 const bool isCached = cachedValue.isValid() && cachedValue.toBool();
378 const bool isWithinRange = withinRangeValue.isValid() && withinRangeValue.toBool();
379
380 if (isActive) {
382 } else if (isCached && isWithinRange) {
384 }
385
386 painter->fillRect(frameRect, fillColor);
387
388 QVector<QLine> lines;
389 lines << QLine(p1, p2);
390 lines << QLine(frameRect.topRight(), frameRect.bottomRight());
391 lines << QLine(frameRect.bottomLeft(), frameRect.bottomRight());
392
393 QPen oldPen = painter->pen();
394 painter->setPen(gridPen);
395 painter->drawLines(lines);
396 painter->setPen(oldPen);
397 }
398}
399
401{
402 Q_UNUSED(event);
403
405}
406
408{
409 m_d->fps = fps;
410 update();
411}
412
414{
415 qreal newSectionSize = zoom * m_d->unitSectionSize;
416
417 if (newSectionSize < m_d->minSectionSize) {
418 newSectionSize = m_d->minSectionSize;
419 zoom = qreal(newSectionSize) / m_d->unitSectionSize;
420 } else if (newSectionSize > m_d->maxSectionSize) {
421 newSectionSize = m_d->maxSectionSize;
422 zoom = qreal(newSectionSize) / m_d->unitSectionSize;
423 }
424
425 m_d->remainder = newSectionSize - floor(newSectionSize);
426
427 if (newSectionSize != defaultSectionSize()) {
428 setDefaultSectionSize(newSectionSize);
429 Q_EMIT sigZoomChanged(zoom);
430 return true;
431 }
432
433 return false;
434}
435
437 return (qreal(defaultSectionSize() + m_d->remainder) / m_d->unitSectionSize);
438}
439
441{
442 QFontMetrics metrics(this->font());
443 const int textHeight = metrics.height();
444
445 setMinimumSize(0, 1.5 * textHeight);
446}
447
448void KisAnimTimelineTimeHeader::setModel(QAbstractItemModel *model)
449{
450 KisTimeBasedItemModel *framesModel = qobject_cast<KisTimeBasedItemModel*>(model);
451 m_d->model = framesModel;
452
453 QHeaderView::setModel(model);
454}
455
456int getColumnCount(const QModelIndexList &indexes, int *leftmostCol, int *rightmostCol)
457{
458 QVector<int> columns;
459 int leftmost = std::numeric_limits<int>::max();
460 int rightmost = std::numeric_limits<int>::min();
461
462 Q_FOREACH (const QModelIndex &index, indexes) {
463 leftmost = qMin(leftmost, index.column());
464 rightmost = qMax(rightmost, index.column());
465 if (!columns.contains(index.column())) {
466 columns.append(index.column());
467 }
468 }
469
470 if (leftmostCol) *leftmostCol = leftmost;
471 if (rightmostCol) *rightmostCol = rightmost;
472
473 return columns.size();
474}
475
477{
478 int logical = logicalIndexAt(e->pos());
479 if (logical != -1) {
480 QModelIndexList selectedIndexes = selectionModel()->selectedIndexes();
481 int numSelectedColumns = getColumnCount(selectedIndexes, 0, 0);
482
483 if (e->button() == Qt::RightButton) {
484 if (numSelectedColumns <= 1) {
485 model()->setHeaderData(logical, orientation(), true, KisTimeBasedItemModel::ActiveFrameRole);
486 model()->setHeaderData(logical, orientation(), QVariant(int(SEEK_FINALIZE | SEEK_PUSH_AUDIO)), KisTimeBasedItemModel::ScrubToRole);
487 }
488
489 /* Fix for safe-assert involving kis_animation_curve_docker.
490 * There should probably be a more elegant way for dealing
491 * with reused timeline_ruler_header instances in other
492 * timeline views instead of simply animation_frame_view.
493 *
494 * This works for now though... */
495 if(!m_d->actionMan){
496 return;
497 }
498
499 QMenu menu;
500
501 menu.addSection(i18n("Edit Columns:"));
502 menu.addSeparator();
503
504 KisActionManager::safePopulateMenu(&menu, "cut_columns_to_clipboard", m_d->actionMan);
505 KisActionManager::safePopulateMenu(&menu, "copy_columns_to_clipboard", m_d->actionMan);
506 KisActionManager::safePopulateMenu(&menu, "paste_columns_from_clipboard", m_d->actionMan);
507
508 menu.addSeparator();
509
510 { //Frame Columns Submenu
511 QMenu *frames = menu.addMenu(i18nc("@item:inmenu", "Keyframe Columns"));
512 KisActionManager::safePopulateMenu(frames, "insert_column_left", m_d->actionMan);
513 KisActionManager::safePopulateMenu(frames, "insert_column_right", m_d->actionMan);
514 frames->addSeparator();
515 KisActionManager::safePopulateMenu(frames, "insert_multiple_columns", m_d->actionMan);
516 }
517
518 { //Hold Columns Submenu
519 QMenu *hold = menu.addMenu(i18nc("@item:inmenu", "Hold Frame Columns"));
520 KisActionManager::safePopulateMenu(hold, "insert_hold_column", m_d->actionMan);
521 KisActionManager::safePopulateMenu(hold, "remove_hold_column", m_d->actionMan);
522 hold->addSeparator();
523 KisActionManager::safePopulateMenu(hold, "insert_multiple_hold_columns", m_d->actionMan);
524 KisActionManager::safePopulateMenu(hold, "remove_multiple_hold_columns", m_d->actionMan);
525 }
526
527 menu.addSeparator();
528
529 KisActionManager::safePopulateMenu(&menu, "remove_columns", m_d->actionMan);
530 KisActionManager::safePopulateMenu(&menu, "remove_columns_and_pull", m_d->actionMan);
531
532 if (numSelectedColumns > 1) {
533 menu.addSeparator();
534 KisActionManager::safePopulateMenu(&menu, "mirror_columns", m_d->actionMan);
535 }
536
537 menu.addSeparator();
538
539 KisActionManager::safePopulateMenu(&menu, "clear_animation_cache", m_d->actionMan);
540
541 menu.exec(e->globalPos());
542
543 return;
544
545 } else if (e->button() == Qt::LeftButton) {
546 m_d->lastPressSectionIndex = logical;
547 model()->setHeaderData(logical, orientation(), true, KisTimeBasedItemModel::ActiveFrameRole);
548 }
549 }
550
551 QHeaderView::mousePressEvent(e);
552}
553
555{
556 int logical = logicalIndexAt(e->pos());
557 if (logical != -1) {
558
559 if (e->buttons() & Qt::LeftButton) {
560
561 m_d->model->setScrubState(true);
562 QVariant activeValue = model()->headerData(logical, orientation(), KisTimeBasedItemModel::ActiveFrameRole);
563 KIS_ASSERT(activeValue.type() == QVariant::Bool);
564 if (activeValue.toBool() != true) {
565 model()->setHeaderData(logical, orientation(), true, KisTimeBasedItemModel::ActiveFrameRole);
566 model()->setHeaderData(logical, orientation(), QVariant(int(SEEK_PUSH_AUDIO)), KisTimeBasedItemModel::ScrubToRole);
567 }
568
569 if (m_d->lastPressSectionIndex >= 0 &&
570 logical != m_d->lastPressSectionIndex &&
571 e->modifiers() & Qt::ShiftModifier) {
572
573 const int minCol = qMin(m_d->lastPressSectionIndex, logical);
574 const int maxCol = qMax(m_d->lastPressSectionIndex, logical);
575
576 QItemSelection sel(m_d->model->index(0, minCol), m_d->model->index(0, maxCol));
577 selectionModel()->select(sel,
578 QItemSelectionModel::Columns |
579 QItemSelectionModel::SelectCurrent);
580 }
581
582 }
583
584 }
585
586 QHeaderView::mouseMoveEvent(e);
587}
588
590{
591 const int sectionWidth = defaultSectionSize();
592 return (m_d->offset + width() - 1) / sectionWidth;
593}
594
596{
597 const int sectionWidth = defaultSectionSize();
598 return ceil(qreal(m_d->offset) / sectionWidth);
599}
600
602{
603 const int PADDING = 2;
604 qreal lengthSections = (end + PADDING) - start;
605 qreal desiredZoom = width() / lengthSections;
606
607 setZoom(desiredZoom / m_d->unitSectionSize);
608}
609
611{
612 if (!m_d->model)
613 return;
614
615 if (e->button() == Qt::LeftButton) {
616 int timeUnderMouse = qMax(logicalIndexAt(e->pos()), 0);
617 model()->setHeaderData(timeUnderMouse, orientation(), true, KisTimeBasedItemModel::ActiveFrameRole);
618 if (timeUnderMouse != m_d->model->currentTime()) {
619 model()->setHeaderData(timeUnderMouse, orientation(), QVariant(int(SEEK_PUSH_AUDIO | SEEK_FINALIZE)), KisTimeBasedItemModel::ScrubToRole);
620 }
621 m_d->model->setScrubState(false);
622 }
623
624 QHeaderView::mouseReleaseEvent(e);
625}
626
628{
629 QModelIndexList frames;
630
631 const int numRows = model->rowCount();
632
633 for (int i = 0; i < numRows; i++) {
634 for (int j = startCol; j <= endCol; j++) {
635 QModelIndex index = model->index(i, j);
636 const bool exists = model->data(index, KisTimeBasedItemModel::FrameExistsRole).toBool();
637 if (exists) {
638 frames << index;
639 }
640 }
641 }
642
643 return frames;
644}
float value(const T *src, size_t ch)
int getColumnCount(const QModelIndexList &indexes, int *leftmostCol, int *rightmostCol)
QPointF p2
QPointF p1
@ SEEK_PUSH_AUDIO
@ SEEK_FINALIZE
connect(this, SIGNAL(optionsChanged()), this, SLOT(saveOptions()))
A KisActionManager class keeps track of KisActions. These actions are always associated with the GUI....
KisAction * createAction(const QString &name)
static void safePopulateMenu(QMenu *menu, const QString &actionId, KisActionManager *actionManager)
static KisAnimTimelineColors * instance()
void mouseMoveEvent(QMouseEvent *e) override
void paintSection1(QPainter *painter, const QRect &rect, int logicalIndex) const
void setActionManager(KisActionManager *actionManager)
void sigZoomChanged(qreal zoom)
void zoomToFitFrameRange(int start, int end)
void setModel(QAbstractItemModel *model) override
void mouseReleaseEvent(QMouseEvent *e) override
void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const override
void paintSpan(QPainter *painter, int userFrameId, const QRect &spanRect, bool isIntegralLine, bool isPrevIntegralLine, QStyle *style, const QPalette &palette, const QPen &gridPen) const
void mousePressEvent(QMouseEvent *e) override
void changeEvent(QEvent *event) override
void paintEvent(QPaintEvent *e) override
const QScopedPointer< Private > m_d
qreal timelineZoom(bool defaultValue=false) const
void setTimelineZoom(qreal value)
#define KIS_ASSERT(cond)
Definition kis_assert.h:33
unsigned int QRgb
rgba palette[MAX_PALETTE]
Definition palette.c:35
QScopedPointer< KisSignalCompressorWithParam< qreal > > zoomSaveCompressor
QModelIndexList prepareFramesSlab(int startCol, int endCol)