diff --git a/libs/widgetutils/CMakeLists.txt b/libs/widgetutils/CMakeLists.txt index 48499f117e..bff769c68b 100644 --- a/libs/widgetutils/CMakeLists.txt +++ b/libs/widgetutils/CMakeLists.txt @@ -1,143 +1,144 @@ add_subdirectory(tests) configure_file(xmlgui/config-xmlgui.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-xmlgui.h ) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/config) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/xmlgui) set(kritawidgetutils_LIB_SRCS WidgetUtilsDebug.cpp kis_icon_utils.cpp kis_action_registry.cpp KisActionsSnapshot.cpp KoGroupButton.cpp KoProgressProxy.cpp KoFakeProgressProxy.cpp KoProgressBar.cpp KoProgressUpdater.cpp KoUpdater.cpp KoUpdaterPrivate_p.cpp KoProperties.cpp KoFileDialog.cpp KisKineticScroller.cpp KoCheckerBoardPainter.cpp KoItemToolTip.cpp KisSqueezedComboBox.cpp KisDialogStateSaver.cpp KisPopupButton.cpp kis_cursor.cc kis_cursor_cache.cpp kis_double_parse_spin_box.cpp kis_double_parse_unit_spin_box.cpp kis_int_parse_spin_box.cpp kis_num_parser.cpp kis_slider_spin_box.cpp + kis_zoom_scrollbar.cpp kis_spin_box_unit_manager.cpp config/kcolorscheme.cpp config/kcolorschememanager.cpp config/khelpclient.cpp config/klanguagebutton.cpp config/krecentfilesaction.cpp config/kstandardaction.cpp xmlgui/KisShortcutsEditorItem.cpp xmlgui/KisShortcutEditWidget.cpp xmlgui/KisShortcutsEditorDelegate.cpp xmlgui/KisShortcutsDialog.cpp xmlgui/KisShortcutsDialog_p.cpp xmlgui/KisShortcutsEditor.cpp xmlgui/KisShortcutsEditor_p.cpp xmlgui/kshortcutschemeseditor.cpp xmlgui/kshortcutschemeshelper.cpp xmlgui/kaboutkdedialog_p.cpp xmlgui/kactioncategory.cpp xmlgui/kactioncollection.cpp xmlgui/kbugreport.cpp xmlgui/kcheckaccelerators.cpp xmlgui/kedittoolbar.cpp xmlgui/kgesture.cpp xmlgui/kgesturemap.cpp xmlgui/khelpmenu.cpp xmlgui/kkeysequencewidget.cpp xmlgui/kmainwindow.cpp xmlgui/kmenumenuhandler_p.cpp xmlgui/kshortcutwidget.cpp xmlgui/kswitchlanguagedialog_p.cpp xmlgui/ktoggletoolbaraction.cpp xmlgui/ktoolbar.cpp xmlgui/ktoolbarhandler.cpp xmlgui/kundoactions.cpp xmlgui/kxmlguibuilder.cpp xmlgui/kxmlguiclient.cpp xmlgui/kxmlguifactory.cpp xmlgui/kxmlguifactory_p.cpp xmlgui/kxmlguiversionhandler.cpp xmlgui/kxmlguiwindow.cpp ) if (HAVE_DBUS) set(kritawidgetutils_LIB_SRCS ${kritawidgetutils_LIB_SRCS} xmlgui/kmainwindowiface.cpp ) endif() ki18n_wrap_ui(kritawidgetutils_LIB_SRCS xmlgui/KisShortcutsDialog.ui xmlgui/kshortcutwidget.ui ) qt5_add_resources(kritawidgetutils_LIB_SRCS xmlgui/kxmlgui.qrc) add_library(kritawidgetutils SHARED ${kritawidgetutils_LIB_SRCS}) target_include_directories(kritawidgetutils PUBLIC $ $ ) generate_export_header(kritawidgetutils BASE_NAME kritawidgetutils) if (HAVE_DBUS) set (KRITA_WIDGET_UTILS_EXTRA_LIBS ${KRITA_WIDGET_UTILS_EXTRA_LIBS} Qt5::DBus) endif () if (APPLE) find_library(FOUNDATION_LIBRARY Foundation) set(KRITA_WIDGET_UTILS_EXTRA_LIBS ${KRITA_WIDGET_UTILS_EXTRA_LIBS} ${FOUNDATION_LIBRARY}) endif () target_link_libraries(kritawidgetutils PUBLIC Qt5::Widgets Qt5::Gui Qt5::Xml Qt5::Core KF5::ItemViews kritaglobal kritaresources PRIVATE Qt5::PrintSupport KF5::I18n KF5::ConfigCore KF5::CoreAddons KF5::ConfigGui KF5::GuiAddons KF5::WidgetsAddons KF5::WindowSystem kritaplugin ${KRITA_WIDGET_UTILS_EXTRA_LIBS} ) set_target_properties(kritawidgetutils PROPERTIES VERSION ${GENERIC_KRITA_LIB_VERSION} SOVERSION ${GENERIC_KRITA_LIB_SOVERSION} ) install(TARGETS kritawidgetutils ${INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/libs/widgetutils/kis_zoom_scrollbar.cpp b/libs/widgetutils/kis_zoom_scrollbar.cpp new file mode 100644 index 0000000000..545a82d1e5 --- /dev/null +++ b/libs/widgetutils/kis_zoom_scrollbar.cpp @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2020 Eoin O'Neill + * Copyright (c) 2020 Emmet O'Neill + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "kis_zoom_scrollbar.h" + +#include "kis_global.h" +#include "kis_debug.h" +#include +#include + +KisZoomableScrollbar::KisZoomableScrollbar(QWidget *parent) + : QScrollBar(parent) + , lastKnownPosition(0,0) + , accelerationAccumulator(0,0) + , scrollSubPixelAccumulator(0.0f) + , zoomPerpendicularityThreshold(0.75f) + , catchTeleportCorrection(false) +{ +} + +KisZoomableScrollbar::KisZoomableScrollbar(Qt::Orientation orientation, QWidget *parent) + : KisZoomableScrollbar(parent) +{ + setOrientation(orientation); +} + +KisZoomableScrollbar::~KisZoomableScrollbar() +{ + +} + +bool KisZoomableScrollbar::catchTeleports(QMouseEvent *event) { + if (catchTeleportCorrection) { + catchTeleportCorrection = false; + event->accept(); + return true; + } + + return false; +} + +void KisZoomableScrollbar::handleWrap( const QPoint &accel, const QPoint &mouseCoord) +{ + QRect windowRect = window()->geometry(); + windowRect = kisGrowRect(windowRect, -2); + const int windowWidth = windowRect.width(); + const int windowHeight = windowRect.height(); + const int windowX = windowRect.x(); + const int windowY = windowRect.y(); + const bool xWrap = true; + const bool yWrap = true; + + if (!windowRect.contains(mouseCoord)) { + int x = mouseCoord.x(); + int y = mouseCoord.y(); + + if (x < windowX && xWrap ) { + x += (windowWidth - 2); + } else if (x > (windowX + windowWidth) && xWrap ) { + x -= (windowWidth - 2); + } + + if (y < windowY && yWrap) { + y += (windowHeight - 2); + } else if (y > (windowY + windowHeight) && yWrap) { + y -= (windowHeight - 2); + } + + QCursor::setPos(x, y); + lastKnownPosition = QPoint(x, y) - accel; + + //Important -- teleportation needs to caught to prevent high-acceleration + //values from QCursor::setPos being read in this event. + catchTeleportCorrection = true; + } +} + +void KisZoomableScrollbar::handleScroll(const QPoint &accel) +{ + const qreal sliderMovementPix = (orientation() == Qt::Horizontal) ? accel.x() * devicePixelRatio() : accel.y() * devicePixelRatio(); + const qreal zoomMovementPix = (orientation() == Qt::Horizontal) ? -accel.y() : -accel.x(); + const qreal documentLength = maximum() - minimum() + pageStep(); + const qreal widgetLength = (orientation() == Qt::Horizontal) ? width() * devicePixelRatio() : height() * devicePixelRatio(); + const qreal widgetThickness = (orientation() == Qt::Horizontal) ? height() * devicePixelRatio() : width() * devicePixelRatio(); + + const QVector2D perpendicularDirection = (orientation() == Qt::Horizontal) ? QVector2D(0, 1) : QVector2D(1, 0); + const float perpendicularity = QVector2D::dotProduct(perpendicularDirection.normalized(), accelerationAccumulator.normalized()); + + if (abs(perpendicularity) > zoomPerpendicularityThreshold && zoomMovementPix != 0) { + zoom(qreal(zoomMovementPix) / (widgetThickness * 2)); + } else if (sliderMovementPix != 0) { + const int currentPosition = sliderPosition(); + scrollSubPixelAccumulator += (documentLength) * (sliderMovementPix / widgetLength); + + setSliderPosition(currentPosition + scrollSubPixelAccumulator); + if (currentPosition + scrollSubPixelAccumulator > maximum() || + currentPosition + scrollSubPixelAccumulator < minimum()) { + overscroll(scrollSubPixelAccumulator); + } + + const int sign = (scrollSubPixelAccumulator > 0) - (scrollSubPixelAccumulator < 0); + scrollSubPixelAccumulator -= floor(abs(scrollSubPixelAccumulator)) * sign; + } +} + +void KisZoomableScrollbar::tabletEvent(QTabletEvent *event) { + if ( event->type() == QTabletEvent::TabletMove && isSliderDown() ) { + QPoint globalMouseCoord = mapToGlobal(event->pos()); + QPoint accel = globalMouseCoord - lastKnownPosition; + accelerationAccumulator += QVector2D(accel); + + if( accelerationAccumulator.length() > 5) { + accelerationAccumulator = accelerationAccumulator.normalized(); + } + + handleScroll(accel); + lastKnownPosition = globalMouseCoord; + event->accept(); + } else { + + if (event->type() == QTabletEvent::TabletPress) { + QPoint globalMouseCoord = mapToGlobal(event->pos()); + lastKnownPosition = globalMouseCoord; + } + + QScrollBar::tabletEvent(event); + } +} + +void KisZoomableScrollbar::mousePressEvent(QMouseEvent *event) +{ + const bool wasSliderDownBefore = isSliderDown(); + QScrollBar::mousePressEvent(event); + + if( isSliderDown() && !wasSliderDownBefore ){ + lastKnownPosition = mapToGlobal(event->pos()); + accelerationAccumulator = QVector2D(0,0); + setCursor(Qt::BlankCursor); + } else { + setCursor(Qt::ArrowCursor); + } + +} + + +void KisZoomableScrollbar::mouseMoveEvent(QMouseEvent *event) +{ + if (isSliderDown()) { + QPoint globalMouseCoord = mapToGlobal(event->pos()); + + QPoint accel = globalMouseCoord - lastKnownPosition; + accelerationAccumulator += QVector2D(accel); + + if (catchTeleports(event)){ + return; + } + + if( accelerationAccumulator.length() > 5 ) { + accelerationAccumulator = accelerationAccumulator.normalized(); + } + + handleScroll(accel); + lastKnownPosition = globalMouseCoord; + handleWrap(accel, mapToGlobal(event->pos())); + event->accept(); + } else { + QScrollBar::mouseMoveEvent(event); + } +} + +void KisZoomableScrollbar::mouseReleaseEvent(QMouseEvent *event) +{ + setCursor(Qt::ArrowCursor); + QScrollBar::mouseReleaseEvent(event); + +} + +void KisZoomableScrollbar::wheelEvent(QWheelEvent *event) { + const int delta = (event->angleDelta().y() / 8) * singleStep() * -1; + const int currentPosition = sliderPosition(); + + if (currentPosition + delta > maximum() || currentPosition + delta < minimum()){ + overscroll(delta); + } + + QScrollBar::wheelEvent(event); +} + +void KisZoomableScrollbar::setZoomDeadzone(float value) +{ + zoomPerpendicularityThreshold = value; +} diff --git a/libs/widgetutils/kis_zoom_scrollbar.h b/libs/widgetutils/kis_zoom_scrollbar.h new file mode 100644 index 0000000000..897e6c2e35 --- /dev/null +++ b/libs/widgetutils/kis_zoom_scrollbar.h @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2020 Eoin O'Neill + * Copyright (c) 2020 Emmet O'Neill + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + + +#ifndef KIS_ZOOM_SCROLLBAR_H +#define KIS_ZOOM_SCROLLBAR_H + +#include +#include + +#include + +class KRITAWIDGETUTILS_EXPORT KisZoomableScrollbar : public QScrollBar +{ + Q_OBJECT + +private: + QPoint lastKnownPosition; + QVector2D accelerationAccumulator; + qreal scrollSubPixelAccumulator; + qreal zoomPerpendicularityThreshold; + bool catchTeleportCorrection = false; + +public: + KisZoomableScrollbar(QWidget* parent = 0); + KisZoomableScrollbar(Qt::Orientation orientation, QWidget * parent = 0); + ~KisZoomableScrollbar(); + + //Catch for teleportation from one side of the screen to the other. + bool catchTeleports(QMouseEvent* event); + + //Window-space wrapping for mouse dragging. Allows for blender-like + //infinite mouse scrolls. + void handleWrap(const QPoint &accel, const QPoint &globalMouseCoord); + + //Scroll based on a mouse acceleration value. + void handleScroll(const QPoint &accel); + + void tabletEvent(QTabletEvent *event) override; + virtual void mousePressEvent(QMouseEvent *event) override; + virtual void mouseMoveEvent(QMouseEvent *event) override; + virtual void mouseReleaseEvent(QMouseEvent *event) override; + + virtual void wheelEvent(QWheelEvent *event) override; + + void setZoomDeadzone(float value); + +Q_SIGNALS: + void zoom(qreal delta); + void overscroll(int delta); +}; + +#endif // KIS_ZOOM_SCROLLBAR_H diff --git a/plugins/dockers/animation/kis_time_based_item_model.cpp b/plugins/dockers/animation/kis_time_based_item_model.cpp index a4318146dd..4418654766 100644 --- a/plugins/dockers/animation/kis_time_based_item_model.cpp +++ b/plugins/dockers/animation/kis_time_based_item_model.cpp @@ -1,532 +1,551 @@ /* * Copyright (c) 2016 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_time_based_item_model.h" #include #include #include "kis_animation_frame_cache.h" #include "kis_animation_player.h" #include "kis_signal_compressor_with_param.h" #include "kis_image.h" #include "kis_image_animation_interface.h" #include "kis_time_range.h" #include "kis_animation_utils.h" #include "kis_keyframe_channel.h" #include "kis_processing_applicator.h" #include "KisImageBarrierLockerWithFeedback.h" #include "commands_new/kis_switch_current_time_command.h" #include "kis_command_utils.h" #include "KisPart.h" #include "kis_animation_cache_populator.h" struct KisTimeBasedItemModel::Private { Private() : animationPlayer(0) , numFramesOverride(0) , activeFrameIndex(0) , scrubInProgress(false) , scrubStartFrame(-1) {} KisImageWSP image; KisAnimationFrameCacheWSP framesCache; QPointer animationPlayer; QVector cachedFrames; int numFramesOverride; int activeFrameIndex; bool scrubInProgress; int scrubStartFrame; QScopedPointer > scrubbingCompressor; int baseNumFrames() const { auto imageSP = image.toStrongRef(); if (!imageSP) return 0; KisImageAnimationInterface *i = imageSP->animationInterface(); if (!i) return 1; return i->totalLength(); } int effectiveNumFrames() const { if (image.isNull()) return 0; return qMax(baseNumFrames(), numFramesOverride); } int framesPerSecond() { return image->animationInterface()->framerate(); } }; KisTimeBasedItemModel::KisTimeBasedItemModel(QObject *parent) : QAbstractTableModel(parent) , m_d(new Private()) { KisConfig cfg(true); using namespace std::placeholders; std::function callback( std::bind(&KisTimeBasedItemModel::slotInternalScrubPreviewRequested, this, _1)); m_d->scrubbingCompressor.reset( new KisSignalCompressorWithParam(cfg.scrubbingUpdatesDelay(), callback, KisSignalCompressor::FIRST_ACTIVE)); } KisTimeBasedItemModel::~KisTimeBasedItemModel() {} void KisTimeBasedItemModel::setImage(KisImageWSP image) { KisImageWSP oldImage = m_d->image; m_d->image = image; if (image) { KisImageAnimationInterface *ai = image->animationInterface(); connect(ai, SIGNAL(sigFramerateChanged()), SLOT(slotFramerateChanged())); connect(ai, SIGNAL(sigUiTimeChanged(int)), SLOT(slotCurrentTimeChanged(int))); + connect(ai, SIGNAL(sigFullClipRangeChanged()), SLOT(slotClipRangeChanged())); } if (image != oldImage) { beginResetModel(); endResetModel(); } } void KisTimeBasedItemModel::setFrameCache(KisAnimationFrameCacheSP cache) { if (KisAnimationFrameCacheSP(m_d->framesCache) == cache) return; if (m_d->framesCache) { m_d->framesCache->disconnect(this); } m_d->framesCache = cache; if (m_d->framesCache) { connect(m_d->framesCache, SIGNAL(changed()), SLOT(slotCacheChanged())); } } void KisTimeBasedItemModel::setAnimationPlayer(KisAnimationPlayer *player) { if (m_d->animationPlayer == player) return; if (m_d->animationPlayer) { m_d->animationPlayer->disconnect(this); } m_d->animationPlayer = player; if (m_d->animationPlayer) { connect(m_d->animationPlayer, SIGNAL(sigPlaybackStopped()), SLOT(slotPlaybackStopped())); connect(m_d->animationPlayer, SIGNAL(sigFrameChanged()), SLOT(slotPlaybackFrameChanged())); } } void KisTimeBasedItemModel::setLastVisibleFrame(int time) { - const int growThreshold = m_d->effectiveNumFrames() - 3; + const int growThreshold = m_d->effectiveNumFrames() - 1; const int growValue = time + 8; - const int shrinkThreshold = m_d->effectiveNumFrames() - 12; + const int shrinkThreshold = m_d->effectiveNumFrames() - 3; const int shrinkValue = qMax(m_d->baseNumFrames(), qMin(growValue, shrinkThreshold)); - const bool canShrink = m_d->effectiveNumFrames() > m_d->baseNumFrames(); + const bool canShrink = m_d->baseNumFrames() < m_d->effectiveNumFrames(); if (time >= growThreshold) { beginInsertColumns(QModelIndex(), m_d->effectiveNumFrames(), growValue - 1); m_d->numFramesOverride = growValue; endInsertColumns(); } else if (time < shrinkThreshold && canShrink) { beginRemoveColumns(QModelIndex(), shrinkValue, m_d->effectiveNumFrames() - 1); m_d->numFramesOverride = shrinkValue; endRemoveColumns(); } } int KisTimeBasedItemModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return m_d->effectiveNumFrames(); } QVariant KisTimeBasedItemModel::data(const QModelIndex &index, int role) const { switch (role) { case ActiveFrameRole: { return index.column() == m_d->activeFrameIndex; } } return QVariant(); } bool KisTimeBasedItemModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid()) return false; switch (role) { case ActiveFrameRole: { setHeaderData(index.column(), Qt::Horizontal, value, role); break; } } return false; } QVariant KisTimeBasedItemModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { switch (role) { case ActiveFrameRole: return section == m_d->activeFrameIndex; case FrameCachedRole: return m_d->cachedFrames.size() > section ? m_d->cachedFrames[section] : false; case FramesPerSecondRole: return m_d->framesPerSecond(); } } return QVariant(); } bool KisTimeBasedItemModel::setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role) { if (orientation == Qt::Horizontal) { switch (role) { case ActiveFrameRole: if (value.toBool() && section != m_d->activeFrameIndex) { int prevFrame = m_d->activeFrameIndex; m_d->activeFrameIndex = section; scrubTo(m_d->activeFrameIndex, m_d->scrubInProgress); /** * Optimization Hack Alert: * * ideally, we should emit all four signals, but... The * point is this code is used in a tight loop during * playback, so it should run as fast as possible. To tell * the story short, commenting out these three lines makes * playback run 15% faster ;) */ if (m_d->scrubInProgress) { //emit dataChanged(this->index(0, prevFrame), this->index(rowCount() - 1, prevFrame)); emit dataChanged(this->index(0, m_d->activeFrameIndex), this->index(rowCount() - 1, m_d->activeFrameIndex)); //emit headerDataChanged (Qt::Horizontal, prevFrame, prevFrame); //emit headerDataChanged (Qt::Horizontal, m_d->activeFrameIndex, m_d->activeFrameIndex); } else { emit dataChanged(this->index(0, prevFrame), this->index(rowCount() - 1, prevFrame)); emit dataChanged(this->index(0, m_d->activeFrameIndex), this->index(rowCount() - 1, m_d->activeFrameIndex)); emit headerDataChanged (Qt::Horizontal, prevFrame, prevFrame); emit headerDataChanged (Qt::Horizontal, m_d->activeFrameIndex, m_d->activeFrameIndex); } } } } return false; } bool KisTimeBasedItemModel::removeFrames(const QModelIndexList &indexes) { KisAnimationUtils::FrameItemList frameItems; { KisImageBarrierLockerWithFeedback locker(m_d->image); Q_FOREACH (const QModelIndex &index, indexes) { int time = index.column(); Q_FOREACH(KisKeyframeChannel *channel, channelsAt(index)) { if (channel->keyframeAt(time)) { frameItems << KisAnimationUtils::FrameItem(channel->node(), channel->id(), index.column()); } } } } if (frameItems.isEmpty()) return false; KisAnimationUtils::removeKeyframes(m_d->image, frameItems); return true; } KUndo2Command* KisTimeBasedItemModel::createOffsetFramesCommand(QModelIndexList srcIndexes, const QPoint &offset, bool copyFrames, bool moveEmptyFrames, KUndo2Command *parentCommand) { if (srcIndexes.isEmpty()) return 0; if (offset.isNull()) return 0; KisAnimationUtils::sortPointsForSafeMove(&srcIndexes, offset); KisAnimationUtils::FrameItemList srcFrameItems; KisAnimationUtils::FrameItemList dstFrameItems; Q_FOREACH (const QModelIndex &srcIndex, srcIndexes) { QModelIndex dstIndex = index( srcIndex.row() + offset.y(), srcIndex.column() + offset.x()); KisNodeSP srcNode = nodeAt(srcIndex); KisNodeSP dstNode = nodeAt(dstIndex); if (!srcNode || !dstNode) return 0; Q_FOREACH(KisKeyframeChannel *channel, channelsAt(srcIndex)) { if (moveEmptyFrames || channel->keyframeAt(srcIndex.column())) { srcFrameItems << KisAnimationUtils::FrameItem(srcNode, channel->id(), srcIndex.column()); dstFrameItems << KisAnimationUtils::FrameItem(dstNode, channel->id(), dstIndex.column()); } } } KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(srcFrameItems.size() == dstFrameItems.size(), 0); if (srcFrameItems.isEmpty()) return 0; return KisAnimationUtils::createMoveKeyframesCommand(srcFrameItems, dstFrameItems, copyFrames, moveEmptyFrames, parentCommand); } bool KisTimeBasedItemModel::removeFramesAndOffset(QModelIndexList indicesToRemove) { if (indicesToRemove.isEmpty()) return true; std::sort(indicesToRemove.begin(), indicesToRemove.end(), [] (const QModelIndex &lhs, const QModelIndex &rhs) { return lhs.column() > rhs.column(); }); const int minColumn = indicesToRemove.last().column(); KUndo2Command *parentCommand = new KUndo2Command(kundo2_i18np("Remove frame and shift", "Remove %1 frames and shift", indicesToRemove.size())); { KisImageBarrierLockerWithFeedback locker(m_d->image); Q_FOREACH (const QModelIndex &index, indicesToRemove) { QModelIndexList indicesToOffset; for (int column = index.column() + 1; column < columnCount(); column++) { indicesToOffset << this->index(index.row(), column); } createOffsetFramesCommand(indicesToOffset, QPoint(-1, 0), false, true, parentCommand); } const int oldTime = m_d->image->animationInterface()->currentUITime(); const int newTime = minColumn; new KisSwitchCurrentTimeCommand(m_d->image->animationInterface(), oldTime, newTime, parentCommand); } KisProcessingApplicator::runSingleCommandStroke(m_d->image, parentCommand, KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); return true; } bool KisTimeBasedItemModel::mirrorFrames(QModelIndexList indexes) { QScopedPointer parentCommand(new KUndo2Command(kundo2_i18n("Mirror Frames"))); { KisImageBarrierLockerWithFeedback locker(m_d->image); QMap rowsList; Q_FOREACH (const QModelIndex &index, indexes) { rowsList[index.row()].append(index); } Q_FOREACH (int row, rowsList.keys()) { QModelIndexList &list = rowsList[row]; KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!list.isEmpty(), false); std::sort(list.begin(), list.end(), [] (const QModelIndex &lhs, const QModelIndex &rhs) { return lhs.column() < rhs.column(); }); auto srcIt = list.begin(); auto dstIt = list.end(); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(srcIt != dstIt, false); --dstIt; QList channels = channelsAt(*srcIt).values(); while (srcIt < dstIt) { Q_FOREACH (KisKeyframeChannel *channel, channels) { channel->swapFrames(srcIt->column(), dstIt->column(), parentCommand.data()); } srcIt++; dstIt--; } } } KisProcessingApplicator::runSingleCommandStroke(m_d->image, new KisCommandUtils::SkipFirstRedoWrapper(parentCommand.take()), KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); return true; } void KisTimeBasedItemModel::slotInternalScrubPreviewRequested(int time) { if (m_d->animationPlayer && !m_d->animationPlayer->isPlaying()) { m_d->animationPlayer->displayFrame(time); } } void KisTimeBasedItemModel::setScrubState(bool active) { if (!m_d->scrubInProgress && active) { const int currentFrame = m_d->image->animationInterface()->currentUITime(); const bool hasCurrentFrameInCache = m_d->framesCache->frameStatus(currentFrame) == KisAnimationFrameCache::Cached; if(!hasCurrentFrameInCache) { KisPart::instance()->prioritizeFrameForCache(m_d->image, currentFrame); } m_d->scrubStartFrame = m_d->activeFrameIndex; m_d->scrubInProgress = true; } if (m_d->scrubInProgress && !active) { m_d->scrubInProgress = false; if (m_d->scrubStartFrame >= 0 && m_d->scrubStartFrame != m_d->activeFrameIndex) { scrubTo(m_d->activeFrameIndex, false); } m_d->scrubStartFrame = -1; } } +bool KisTimeBasedItemModel::isScrubbing() +{ + return m_d->scrubInProgress; +} + void KisTimeBasedItemModel::scrubTo(int time, bool preview) { if (m_d->animationPlayer && m_d->animationPlayer->isPlaying()) return; KIS_ASSERT_RECOVER_RETURN(m_d->image); if (preview) { if (m_d->animationPlayer) { m_d->scrubbingCompressor->start(time); } } else { m_d->image->animationInterface()->requestTimeSwitchWithUndo(time); } } void KisTimeBasedItemModel::slotCurrentTimeChanged(int time) { if (time != m_d->activeFrameIndex) { setHeaderData(time, Qt::Horizontal, true, ActiveFrameRole); } } void KisTimeBasedItemModel::slotFramerateChanged() { emit headerDataChanged(Qt::Horizontal, 0, columnCount() - 1); } +void KisTimeBasedItemModel::slotClipRangeChanged() +{ + if (m_d->image && m_d->image->animationInterface() ) { + const KisImageAnimationInterface* const interface = m_d->image->animationInterface(); + const int lastFrame = interface->playbackRange().end(); + if ( lastFrame > m_d->numFramesOverride) { + beginInsertColumns(QModelIndex(), m_d->numFramesOverride, interface->playbackRange().end()); + m_d->numFramesOverride = interface->playbackRange().end(); + endInsertColumns(); + } + } +} + void KisTimeBasedItemModel::slotCacheChanged() { const int numFrames = columnCount(); m_d->cachedFrames.resize(numFrames); for (int i = 0; i < numFrames; i++) { m_d->cachedFrames[i] = m_d->framesCache->frameStatus(i) == KisAnimationFrameCache::Cached; } emit headerDataChanged(Qt::Horizontal, 0, numFrames); } void KisTimeBasedItemModel::slotPlaybackFrameChanged() { if (!m_d->animationPlayer->isPlaying()) return; setData(index(0, m_d->animationPlayer->visibleFrame()), true, ActiveFrameRole); } void KisTimeBasedItemModel::slotPlaybackStopped() { setData(index(0, m_d->image->animationInterface()->currentUITime()), true, ActiveFrameRole); } void KisTimeBasedItemModel::setPlaybackRange(const KisTimeRange &range) { if (m_d->image.isNull()) return; KisImageAnimationInterface *i = m_d->image->animationInterface(); i->setPlaybackRange(range); } bool KisTimeBasedItemModel::isPlaybackActive() const { return m_d->animationPlayer && m_d->animationPlayer->isPlaying(); } bool KisTimeBasedItemModel::isPlaybackPaused() const { return m_d->animationPlayer && m_d->animationPlayer->isPaused(); } void KisTimeBasedItemModel::stopPlayback() const { KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->animationPlayer); m_d->animationPlayer->stop(); } int KisTimeBasedItemModel::currentTime() const { return m_d->image->animationInterface()->currentUITime(); } KisImageWSP KisTimeBasedItemModel::image() const { return m_d->image; } diff --git a/plugins/dockers/animation/kis_time_based_item_model.h b/plugins/dockers/animation/kis_time_based_item_model.h index 5e3dc7b33f..bb6d002257 100644 --- a/plugins/dockers/animation/kis_time_based_item_model.h +++ b/plugins/dockers/animation/kis_time_based_item_model.h @@ -1,107 +1,109 @@ /* * Copyright (c) 2016 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_TIME_BASED_ITEM_MODEL_H #define _KIS_TIME_BASED_ITEM_MODEL_H #include #include #include #include "kritaanimationdocker_export.h" #include "kis_types.h" class KisTimeRange; class KisAnimationPlayer; class KisKeyframeChannel; class KRITAANIMATIONDOCKER_EXPORT KisTimeBasedItemModel : public QAbstractTableModel { Q_OBJECT public: KisTimeBasedItemModel(QObject *parent); ~KisTimeBasedItemModel() override; void setImage(KisImageWSP image); void setFrameCache(KisAnimationFrameCacheSP cache); void setAnimationPlayer(KisAnimationPlayer *player); void setLastVisibleFrame(int time); int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; bool setData(const QModelIndex &index, const QVariant &value, int role) override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role) override; bool removeFrames(const QModelIndexList &indexes); bool removeFramesAndOffset(QModelIndexList indicesToRemove); bool mirrorFrames(QModelIndexList indexes); void setScrubState(bool active); + bool isScrubbing(); void scrubTo(int time, bool preview); void setPlaybackRange(const KisTimeRange &range); bool isPlaybackActive() const; bool isPlaybackPaused() const; void stopPlayback() const; int currentTime() const; enum ItemDataRole { ActiveFrameRole = Qt::UserRole + 101, FrameExistsRole, SpecialKeyframeExists, FrameCachedRole, FrameEditableRole, FramesPerSecondRole, UserRole, FrameHasContent // is it an empty frame with nothing in it? }; protected: virtual KisNodeSP nodeAt(QModelIndex index) const = 0; virtual QMap channelsAt(QModelIndex index) const = 0; KisImageWSP image() const; KUndo2Command* createOffsetFramesCommand(QModelIndexList srcIndexes, const QPoint &offset, bool copyFrames, bool moveEmptyFrames, KUndo2Command *parentCommand = 0); protected Q_SLOTS: void slotCurrentTimeChanged(int time); private Q_SLOTS: void slotFramerateChanged(); + void slotClipRangeChanged(); void slotCacheChanged(); void slotInternalScrubPreviewRequested(int time); void slotPlaybackFrameChanged(); void slotPlaybackStopped(); private: struct Private; const QScopedPointer m_d; }; #endif diff --git a/plugins/dockers/animation/timeline_frames_model.h b/plugins/dockers/animation/timeline_frames_model.h index 68e36134a7..0e7f735c80 100644 --- a/plugins/dockers/animation/timeline_frames_model.h +++ b/plugins/dockers/animation/timeline_frames_model.h @@ -1,157 +1,158 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __TIMELINE_FRAMES_MODEL_H #define __TIMELINE_FRAMES_MODEL_H #include #include #include "kritaanimationdocker_export.h" #include "kis_node_model.h" #include "kis_types.h" #include "kis_node.h" #include "timeline_node_list_keeper.h" class KisNodeDummy; class KisDummiesFacadeBase; class KisAnimationPlayer; class KisNodeDisplayModeAdapter; class KRITAANIMATIONDOCKER_EXPORT TimelineFramesModel : public TimelineNodeListKeeper::ModelWithExternalNotifications { Q_OBJECT public: enum MimeCopyPolicy { UndefinedPolicy = 0, MoveFramesPolicy, CopyFramesPolicy }; public: TimelineFramesModel(QObject *parent); ~TimelineFramesModel() override; bool hasConnectionToCanvas() const; void setDummiesFacade(KisDummiesFacadeBase *dummiesFacade, KisImageSP image, KisNodeDisplayModeAdapter *displayModeAdapter); bool canDropFrameData(const QMimeData *data, const QModelIndex &index); bool insertOtherLayer(int index, int dstRow); int activeLayerRow() const; bool createFrame(const QModelIndex &dstIndex); bool copyFrame(const QModelIndex &dstIndex); bool insertFrames(int dstColumn, const QList &dstRows, int count, int timing = 1); bool insertHoldFrames(QModelIndexList selectedIndexes, int count); QString audioChannelFileName() const; void setAudioChannelFileName(const QString &fileName); bool isAudioMuted() const; void setAudioMuted(bool value); qreal audioVolume() const; void setAudioVolume(qreal value); void setFullClipRangeStart(int column); void setFullClipRangeEnd(int column); void setLastClickedIndex(const QModelIndex &index); int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; bool setData(const QModelIndex &index, const QVariant &value, int role) override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role) override; Qt::DropActions supportedDragActions() const override; Qt::DropActions supportedDropActions() const override; QStringList mimeTypes() const override; QMimeData *mimeData(const QModelIndexList &indexes) const override; QMimeData *mimeDataExtended(const QModelIndexList &indexes, const QModelIndex &baseIndex, MimeCopyPolicy copyPolicy) const; bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; bool dropMimeDataExtended(const QMimeData *data, Qt::DropAction action, const QModelIndex &parent, bool *dataMoved = 0); Qt::ItemFlags flags(const QModelIndex &index) const override; bool insertRows(int row, int count, const QModelIndex &parent) override; bool removeRows(int row, int count, const QModelIndex &parent) override; enum ItemDataRole { ActiveLayerRole = KisTimeBasedItemModel::UserRole, TimelinePropertiesRole, OtherLayersRole, PinnedToTimelineRole, FrameColorLabelIndexRole }; // metatype is added by the original implementation typedef KisBaseNode::Property Property; typedef KisBaseNode::PropertyList PropertyList; typedef TimelineNodeListKeeper::OtherLayer OtherLayer; typedef TimelineNodeListKeeper::OtherLayersList OtherLayersList; struct NodeManipulationInterface { virtual ~NodeManipulationInterface() {} virtual KisLayerSP addPaintLayer() const = 0; virtual void removeNode(KisNodeSP node) const = 0; virtual bool setNodeProperties(KisNodeSP node, KisImageSP image, KisBaseNode::PropertyList properties) const = 0; }; /** * NOTE: the model has an ownership over the interface, that is it'll * be deleted automatically later */ void setNodeManipulationInterface(NodeManipulationInterface *iface); KisNodeSP nodeAt(QModelIndex index) const override; protected: QMap channelsAt(QModelIndex index) const override; private Q_SLOTS: void slotDummyChanged(KisNodeDummy *dummy); void slotImageContentChanged(); void processUpdateQueue(); public Q_SLOTS: void slotCurrentNodeChanged(KisNodeSP node); Q_SIGNALS: void requestCurrentNodeChanged(KisNodeSP node); void sigInfiniteTimelineUpdateNeeded(); void sigAudioChannelChanged(); void sigEnsureRowVisible(int row); + void sigFullClipRangeChanged(); private: struct Private; const QScopedPointer m_d; }; #endif /* __TIMELINE_FRAMES_MODEL_H */ diff --git a/plugins/dockers/animation/timeline_frames_view.cpp b/plugins/dockers/animation/timeline_frames_view.cpp index b692d64c89..56e602f4f6 100644 --- a/plugins/dockers/animation/timeline_frames_view.cpp +++ b/plugins/dockers/animation/timeline_frames_view.cpp @@ -1,1552 +1,1654 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "timeline_frames_view.h" #include "timeline_frames_model.h" #include "timeline_ruler_header.h" #include "timeline_layers_header.h" #include "timeline_insert_keyframe_dialog.h" #include "timeline_frames_item_delegate.h" #include #include #include #include #include #include #include #include #include #include +#include +#include #include "config-qtmultimedia.h" #include "KSharedConfig" #include "KisKineticScroller.h" #include "kis_zoom_button.h" #include "kis_icon_utils.h" #include "kis_animation_utils.h" #include "kis_custom_modifiers_catcher.h" #include "kis_action.h" #include "kis_signal_compressor.h" #include "kis_time_range.h" #include "kis_color_label_selector_widget.h" #include "kis_keyframe_channel.h" #include "kis_slider_spin_box.h" -#include -#include -#include - -#include -#include +#include "kis_signals_blocker.h" +#include "kis_image_config.h" +#include "kis_zoom_scrollbar.h" +#include "KisImportExportManager.h" +#include "KoFileDialog.h" +#include "KisIconToolTip.h" typedef QPair QItemViewPaintPair; typedef QList QItemViewPaintPairs; struct TimelineFramesView::Private { Private(TimelineFramesView *_q) : q(_q), fps(1), - zoomStillPointIndex(-1), - zoomStillPointOriginalOffset(0), dragInProgress(false), dragWasSuccessful(false), modifiersCatcher(0), - selectionChangedCompressor(300, KisSignalCompressor::FIRST_INACTIVE) - {} + selectionChangedCompressor(300, KisSignalCompressor::FIRST_INACTIVE), + geometryChangedCompressor(300, KisSignalCompressor::FIRST_INACTIVE), + kineticScrollInfiniteFrameUpdater() + { + kineticScrollInfiniteFrameUpdater.setTimerType(Qt::CoarseTimer); + } TimelineFramesView *q; TimelineFramesModel *model; TimelineRulerHeader *horizontalRuler; TimelineLayersHeader *layersHeader; int fps; - int zoomStillPointIndex; - int zoomStillPointOriginalOffset; QPoint initialDragPanValue; QPoint initialDragPanPos; QToolButton *addLayersButton; KisAction *pinLayerToTimelineAction; QToolButton *audioOptionsButton; KisColorLabelSelectorWidget *colorSelector; QWidgetAction *colorSelectorAction; KisColorLabelSelectorWidget *multiframeColorSelector; QWidgetAction *multiframeColorSelectorAction; QMenu *audioOptionsMenu; QAction *openAudioAction; QAction *audioMuteAction; KisSliderSpinBox *volumeSlider; QMenu *layerEditingMenu; QMenu *existingLayersMenu; TimelineInsertKeyframeDialog *insertKeyframeDialog; KisZoomButton *zoomDragButton; bool dragInProgress; bool dragWasSuccessful; KisCustomModifiersCatcher *modifiersCatcher; QPoint lastPressedPosition; Qt::KeyboardModifiers lastPressedModifier; + + QTimer kineticScrollInfiniteFrameUpdater; + KisSignalCompressor selectionChangedCompressor; + KisSignalCompressor geometryChangedCompressor; QStyleOptionViewItem viewOptionsV4() const; QItemViewPaintPairs draggablePaintPairs(const QModelIndexList &indexes, QRect *r) const; QPixmap renderToPixmap(const QModelIndexList &indexes, QRect *r) const; KisIconToolTip tip; KisActionManager *actionMan = 0; }; TimelineFramesView::TimelineFramesView(QWidget *parent) : QTableView(parent), m_d(new Private(this)) { m_d->modifiersCatcher = new KisCustomModifiersCatcher(this); m_d->modifiersCatcher->addModifier("pan-zoom", Qt::Key_Space); m_d->modifiersCatcher->addModifier("offset-frame", Qt::Key_Alt); setCornerButtonEnabled(false); setSelectionBehavior(QAbstractItemView::SelectItems); setSelectionMode(QAbstractItemView::ExtendedSelection); setItemDelegate(new TimelineFramesItemDelegate(this)); setDragEnabled(true); setDragDropMode(QAbstractItemView::DragDrop); setAcceptDrops(true); setDropIndicatorShown(true); setDefaultDropAction(Qt::MoveAction); m_d->horizontalRuler = new TimelineRulerHeader(this); this->setHorizontalHeader(m_d->horizontalRuler); + KisZoomableScrollbar* hZoomableBar = new KisZoomableScrollbar(this); + setHorizontalScrollBar(hZoomableBar); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + hZoomableBar->setEnabled(false); + setVerticalScrollBar(new KisZoomableScrollbar(this)); + + connect(hZoomableBar, SIGNAL(zoom(qreal)), this, SLOT(slotScrollbarZoom(qreal))); + connect(m_d->horizontalRuler, SIGNAL(sigInsertColumnLeft()), SLOT(slotInsertKeyframeColumnLeft())); connect(m_d->horizontalRuler, SIGNAL(sigInsertColumnRight()), SLOT(slotInsertKeyframeColumnRight())); connect(m_d->horizontalRuler, SIGNAL(sigInsertMultipleColumns()), SLOT(slotInsertMultipleKeyframeColumns())); connect(m_d->horizontalRuler, SIGNAL(sigRemoveColumns()), SLOT(slotRemoveSelectedColumns())); connect(m_d->horizontalRuler, SIGNAL(sigRemoveColumnsAndShift()), SLOT(slotRemoveSelectedColumnsAndShift())); connect(m_d->horizontalRuler, SIGNAL(sigInsertHoldColumns()), SLOT(slotInsertHoldFrameColumn())); connect(m_d->horizontalRuler, SIGNAL(sigRemoveHoldColumns()), SLOT(slotRemoveHoldFrameColumn())); connect(m_d->horizontalRuler, SIGNAL(sigInsertHoldColumnsCustom()), SLOT(slotInsertMultipleHoldFrameColumns())); connect(m_d->horizontalRuler, SIGNAL(sigRemoveHoldColumnsCustom()), SLOT(slotRemoveMultipleHoldFrameColumns())); connect(m_d->horizontalRuler, SIGNAL(sigMirrorColumns()), SLOT(slotMirrorColumns())); connect(m_d->horizontalRuler, SIGNAL(sigCopyColumns()), SLOT(slotCopyColumns())); connect(m_d->horizontalRuler, SIGNAL(sigCutColumns()), SLOT(slotCutColumns())); connect(m_d->horizontalRuler, SIGNAL(sigPasteColumns()), SLOT(slotPasteColumns())); + connect(m_d->horizontalRuler, SIGNAL(geometriesChanged()), &m_d->geometryChangedCompressor, SLOT(start())); + m_d->layersHeader = new TimelineLayersHeader(this); m_d->layersHeader->setSectionResizeMode(QHeaderView::Fixed); m_d->layersHeader->setDefaultSectionSize(24); m_d->layersHeader->setMinimumWidth(60); m_d->layersHeader->setHighlightSections(true); - this->setVerticalHeader(m_d->layersHeader); - connect(horizontalScrollBar(), SIGNAL(valueChanged(int)), SLOT(slotUpdateInfiniteFramesCount())); - connect(horizontalScrollBar(), SIGNAL(sliderReleased()), SLOT(slotUpdateInfiniteFramesCount())); + connect(m_d->layersHeader, SIGNAL(geometriesChanged()), &m_d->geometryChangedCompressor, SLOT(start())); + + connect(&m_d->geometryChangedCompressor, SIGNAL(timeout()), SLOT(slotRealignScrollBars())); + + connect(hZoomableBar, SIGNAL(overscroll(int)), SLOT(slotUpdateInfiniteFramesCount())); + connect(hZoomableBar, SIGNAL(sliderReleased()), SLOT(slotUpdateInfiniteFramesCount())); /********** Layer Menu ***********************************************************/ m_d->layerEditingMenu = new QMenu(this); m_d->layerEditingMenu->addSection(i18n("Edit Layers:")); m_d->layerEditingMenu->addSeparator(); m_d->layerEditingMenu->addAction(KisAnimationUtils::newLayerActionName, this, SLOT(slotAddNewLayer())); m_d->layerEditingMenu->addAction(KisAnimationUtils::removeLayerActionName, this, SLOT(slotRemoveLayer())); m_d->layerEditingMenu->addSeparator(); m_d->existingLayersMenu = m_d->layerEditingMenu->addMenu(KisAnimationUtils::pinExistingLayerActionName); connect(m_d->existingLayersMenu, SIGNAL(aboutToShow()), SLOT(slotUpdateLayersMenu())); connect(m_d->existingLayersMenu, SIGNAL(triggered(QAction*)), SLOT(slotAddExistingLayer(QAction*))); connect(m_d->layersHeader, SIGNAL(sigRequestContextMenu(QPoint)), SLOT(slotLayerContextMenuRequested(QPoint))); m_d->addLayersButton = new QToolButton(this); m_d->addLayersButton->setAutoRaise(true); m_d->addLayersButton->setIcon(KisIconUtils::loadIcon("addlayer")); m_d->addLayersButton->setIconSize(QSize(20, 20)); m_d->addLayersButton->setPopupMode(QToolButton::InstantPopup); m_d->addLayersButton->setMenu(m_d->layerEditingMenu); /********** Audio Channel Menu *******************************************************/ m_d->audioOptionsButton = new QToolButton(this); m_d->audioOptionsButton->setAutoRaise(true); m_d->audioOptionsButton->setIcon(KisIconUtils::loadIcon("audio-none")); m_d->audioOptionsButton->setIconSize(QSize(20, 20)); // very small on windows if not explicitly set m_d->audioOptionsButton->setPopupMode(QToolButton::InstantPopup); m_d->audioOptionsMenu = new QMenu(this); m_d->audioOptionsMenu->addSection(i18n("Edit Audio:")); m_d->audioOptionsMenu->addSeparator(); #ifndef HAVE_QT_MULTIMEDIA m_d->audioOptionsMenu->addSection(i18nc("@item:inmenu", "Audio playback is not supported in this build!")); #endif m_d->openAudioAction= new QAction("XXX", this); connect(m_d->openAudioAction, SIGNAL(triggered()), this, SLOT(slotSelectAudioChannelFile())); m_d->audioOptionsMenu->addAction(m_d->openAudioAction); m_d->audioMuteAction = new QAction(i18nc("@item:inmenu", "Mute"), this); m_d->audioMuteAction->setCheckable(true); connect(m_d->audioMuteAction, SIGNAL(triggered(bool)), SLOT(slotAudioChannelMute(bool))); m_d->audioOptionsMenu->addAction(m_d->audioMuteAction); m_d->audioOptionsMenu->addAction(i18nc("@item:inmenu", "Remove audio"), this, SLOT(slotAudioChannelRemove())); m_d->audioOptionsMenu->addSeparator(); m_d->volumeSlider = new KisSliderSpinBox(this); m_d->volumeSlider->setRange(0, 100); m_d->volumeSlider->setSuffix(i18n("%")); m_d->volumeSlider->setPrefix(i18nc("@item:inmenu, slider", "Volume:")); m_d->volumeSlider->setSingleStep(1); m_d->volumeSlider->setPageStep(10); m_d->volumeSlider->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); connect(m_d->volumeSlider, SIGNAL(valueChanged(int)), SLOT(slotAudioVolumeChanged(int))); QWidgetAction *volumeAction = new QWidgetAction(m_d->audioOptionsMenu); volumeAction->setDefaultWidget(m_d->volumeSlider); m_d->audioOptionsMenu->addAction(volumeAction); m_d->audioOptionsButton->setMenu(m_d->audioOptionsMenu); /********** Frame Editing Context Menu ***********************************************/ m_d->colorSelector = new KisColorLabelSelectorWidget(this); m_d->colorSelectorAction = new QWidgetAction(this); m_d->colorSelectorAction->setDefaultWidget(m_d->colorSelector); connect(m_d->colorSelector, &KisColorLabelSelectorWidget::currentIndexChanged, this, &TimelineFramesView::slotColorLabelChanged); m_d->multiframeColorSelector = new KisColorLabelSelectorWidget(this); m_d->multiframeColorSelectorAction = new QWidgetAction(this); m_d->multiframeColorSelectorAction->setDefaultWidget(m_d->multiframeColorSelector); connect(m_d->multiframeColorSelector, &KisColorLabelSelectorWidget::currentIndexChanged, this, &TimelineFramesView::slotColorLabelChanged); /********** Insert Keyframes Dialog **************************************************/ m_d->insertKeyframeDialog = new TimelineInsertKeyframeDialog(this); /********** Zoom Button **************************************************************/ m_d->zoomDragButton = new KisZoomButton(this); m_d->zoomDragButton->setAutoRaise(true); m_d->zoomDragButton->setIcon(KisIconUtils::loadIcon("zoom-horizontal")); m_d->zoomDragButton->setIconSize(QSize(20, 20)); // this icon is very small on windows if no explicitly set m_d->zoomDragButton->setToolTip(i18nc("@info:tooltip", "Zoom Timeline. Hold down and drag left or right.")); m_d->zoomDragButton->setPopupMode(QToolButton::InstantPopup); connect(m_d->zoomDragButton, SIGNAL(zoomLevelChanged(qreal)), SLOT(slotZoomButtonChanged(qreal))); - connect(m_d->zoomDragButton, SIGNAL(zoomStarted(qreal)), SLOT(slotZoomButtonPressed(qreal))); setFramesPerSecond(12); setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); { QScroller *scroller = KisKineticScroller::createPreconfiguredScroller(this); if( scroller ) { + QScrollerProperties props = scroller->scrollerProperties(); + connect(scroller, SIGNAL(stateChanged(QScroller::State)), this, SLOT(slotScrollerStateChanged(QScroller::State))); + + connect(&m_d->kineticScrollInfiniteFrameUpdater, &QTimer::timeout, [this, scroller](){ + slotUpdateInfiniteFramesCount(); + scroller->resendPrepareEvent(); + }); + + props.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff); + props.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff); + + scroller->setScrollerProperties(props); } } connect(&m_d->selectionChangedCompressor, SIGNAL(timeout()), SLOT(slotSelectionChanged())); connect(&m_d->selectionChangedCompressor, SIGNAL(timeout()), SLOT(slotUpdateFrameActions())); { QClipboard *cb = QApplication::clipboard(); connect(cb, SIGNAL(dataChanged()), SLOT(slotUpdateFrameActions())); } } TimelineFramesView::~TimelineFramesView() { } void TimelineFramesView::setActionManager(KisActionManager *actionManager) { m_d->actionMan = actionManager; m_d->horizontalRuler->setActionManager(actionManager); if (actionManager) { KisAction *action = 0; action = m_d->actionMan->createAction("add_blank_frame"); connect(action, SIGNAL(triggered()), SLOT(slotAddBlankFrame())); action = m_d->actionMan->createAction("add_duplicate_frame"); connect(action, SIGNAL(triggered()), SLOT(slotAddDuplicateFrame())); action = m_d->actionMan->createAction("insert_keyframe_left"); connect(action, SIGNAL(triggered()), SLOT(slotInsertKeyframeLeft())); action = m_d->actionMan->createAction("insert_keyframe_right"); connect(action, SIGNAL(triggered()), SLOT(slotInsertKeyframeRight())); action = m_d->actionMan->createAction("insert_multiple_keyframes"); connect(action, SIGNAL(triggered()), SLOT(slotInsertMultipleKeyframes())); action = m_d->actionMan->createAction("remove_frames_and_pull"); connect(action, SIGNAL(triggered()), SLOT(slotRemoveSelectedFramesAndShift())); action = m_d->actionMan->createAction("remove_frames"); connect(action, SIGNAL(triggered()), SLOT(slotRemoveSelectedFrames())); action = m_d->actionMan->createAction("insert_hold_frame"); connect(action, SIGNAL(triggered()), SLOT(slotInsertHoldFrame())); action = m_d->actionMan->createAction("insert_multiple_hold_frames"); connect(action, SIGNAL(triggered()), SLOT(slotInsertMultipleHoldFrames())); action = m_d->actionMan->createAction("remove_hold_frame"); connect(action, SIGNAL(triggered()), SLOT(slotRemoveHoldFrame())); action = m_d->actionMan->createAction("remove_multiple_hold_frames"); connect(action, SIGNAL(triggered()), SLOT(slotRemoveMultipleHoldFrames())); action = m_d->actionMan->createAction("mirror_frames"); connect(action, SIGNAL(triggered()), SLOT(slotMirrorFrames())); action = m_d->actionMan->createAction("copy_frames_to_clipboard"); connect(action, SIGNAL(triggered()), SLOT(slotCopyFrames())); action = m_d->actionMan->createAction("cut_frames_to_clipboard"); connect(action, SIGNAL(triggered()), SLOT(slotCutFrames())); action = m_d->actionMan->createAction("paste_frames_from_clipboard"); connect(action, SIGNAL(triggered()), SLOT(slotPasteFrames())); action = m_d->actionMan->createAction("set_start_time"); connect(action, SIGNAL(triggered()), SLOT(slotSetStartTimeToCurrentPosition())); action = m_d->actionMan->createAction("set_end_time"); connect(action, SIGNAL(triggered()), SLOT(slotSetEndTimeToCurrentPosition())); action = m_d->actionMan->createAction("update_playback_range"); connect(action, SIGNAL(triggered()), SLOT(slotUpdatePlackbackRange())); action = m_d->actionMan->actionByName("pin_to_timeline"); m_d->pinLayerToTimelineAction = action; m_d->layerEditingMenu->addAction(action); } } void resizeToMinimalSize(QAbstractButton *w, int minimalSize) { QSize buttonSize = w->sizeHint(); if (buttonSize.height() > minimalSize) { buttonSize = QSize(minimalSize, minimalSize); } w->resize(buttonSize); } void TimelineFramesView::updateGeometries() { QTableView::updateGeometries(); const int availableHeight = m_d->horizontalRuler->height(); const int margin = 2; const int minimalSize = availableHeight - 2 * margin; resizeToMinimalSize(m_d->addLayersButton, minimalSize); resizeToMinimalSize(m_d->audioOptionsButton, minimalSize); resizeToMinimalSize(m_d->zoomDragButton, minimalSize); int x = 2 * margin; int y = (availableHeight - minimalSize) / 2; m_d->addLayersButton->move(x, 2 * y); m_d->audioOptionsButton->move(x + minimalSize + 2 * margin, 2 * y); const int availableWidth = m_d->layersHeader->width(); x = availableWidth - margin - minimalSize; m_d->zoomDragButton->move(x, 2 * y); } void TimelineFramesView::setModel(QAbstractItemModel *model) { TimelineFramesModel *framesModel = qobject_cast(model); m_d->model = framesModel; + horizontalScrollBar()->setEnabled((framesModel != nullptr)); + QTableView::setModel(model); connect(m_d->model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)), this, SLOT(slotHeaderDataChanged(Qt::Orientation,int,int))); connect(m_d->model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(slotDataChanged(QModelIndex,QModelIndex))); connect(m_d->model, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(slotReselectCurrentIndex())); connect(m_d->model, SIGNAL(sigInfiniteTimelineUpdateNeeded()), this, SLOT(slotUpdateInfiniteFramesCount())); connect(m_d->model, SIGNAL(sigAudioChannelChanged()), this, SLOT(slotUpdateAudioActions())); connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), &m_d->selectionChangedCompressor, SLOT(start())); connect(m_d->model, SIGNAL(sigEnsureRowVisible(int)), SLOT(slotEnsureRowVisible(int))); slotUpdateAudioActions(); } void TimelineFramesView::setFramesPerSecond(int fps) { m_d->fps = fps; m_d->horizontalRuler->setFramePerSecond(fps); - - // For some reason simple update sometimes doesn't work here, so - // reset the whole header - // - // m_d->horizontalRuler->reset(); } -void TimelineFramesView::slotZoomButtonPressed(qreal staticPoint) +void TimelineFramesView::slotZoomButtonChanged(qreal zoomLevel) { - m_d->zoomStillPointIndex = - qIsNaN(staticPoint) ? currentIndex().column() : staticPoint; + const int originalFirstColumn = estimateFirstVisibleColumn(); + if (m_d->horizontalRuler->setZoom(zoomLevel)) { + m_d->zoomDragButton->setZoomLevel(m_d->horizontalRuler->zoom()); - const int w = m_d->horizontalRuler->defaultSectionSize(); + if (estimateLastVisibleColumn() >= m_d->model->columnCount()) { + slotUpdateInfiniteFramesCount(); + } - m_d->zoomStillPointOriginalOffset = - w * m_d->zoomStillPointIndex - - horizontalScrollBar()->value(); + viewport()->update(); + horizontalScrollBar()->setValue(scrollPositionFromColumn(originalFirstColumn)); + } } -void TimelineFramesView::slotZoomButtonChanged(qreal zoomLevel) +void TimelineFramesView::slotScrollbarZoom(qreal zoom) { - if (m_d->horizontalRuler->setZoom(zoomLevel)) { - slotUpdateInfiniteFramesCount(); + const int originalFirstColumn = estimateFirstVisibleColumn(); + if (m_d->horizontalRuler->setZoom(m_d->horizontalRuler->zoom() + zoom)) { + m_d->zoomDragButton->setZoomLevel(m_d->horizontalRuler->zoom()); - const int w = m_d->horizontalRuler->defaultSectionSize(); - horizontalScrollBar()->setValue(w * m_d->zoomStillPointIndex - m_d->zoomStillPointOriginalOffset); + if (estimateLastVisibleColumn() >= m_d->model->columnCount()) { + slotUpdateInfiniteFramesCount(); + } viewport()->update(); + horizontalScrollBar()->setValue(scrollPositionFromColumn(originalFirstColumn)); } } void TimelineFramesView::slotColorLabelChanged(int label) { Q_FOREACH(QModelIndex index, selectedIndexes()) { m_d->model->setData(index, label, TimelineFramesModel::FrameColorLabelIndexRole); } KisImageConfig(false).setDefaultFrameColorLabel(label); } void TimelineFramesView::slotSelectAudioChannelFile() { if (!m_d->model) return; QString defaultDir = QStandardPaths::writableLocation(QStandardPaths::MusicLocation); const QString currentFile = m_d->model->audioChannelFileName(); QDir baseDir = QFileInfo(currentFile).absoluteDir(); if (baseDir.exists()) { defaultDir = baseDir.absolutePath(); } const QString result = KisImportExportManager::askForAudioFileName(defaultDir, this); const QFileInfo info(result); if (info.exists()) { m_d->model->setAudioChannelFileName(info.absoluteFilePath()); } } void TimelineFramesView::slotAudioChannelMute(bool value) { if (!m_d->model) return; if (value != m_d->model->isAudioMuted()) { m_d->model->setAudioMuted(value); } } void TimelineFramesView::slotUpdateIcons() { m_d->addLayersButton->setIcon(KisIconUtils::loadIcon("addlayer")); m_d->audioOptionsButton->setIcon(KisIconUtils::loadIcon("audio-none")); m_d->zoomDragButton->setIcon(KisIconUtils::loadIcon("zoom-horizontal")); } void TimelineFramesView::slotAudioChannelRemove() { if (!m_d->model) return; m_d->model->setAudioChannelFileName(QString()); } void TimelineFramesView::slotUpdateAudioActions() { if (!m_d->model) return; const QString currentFile = m_d->model->audioChannelFileName(); if (currentFile.isEmpty()) { m_d->openAudioAction->setText(i18nc("@item:inmenu", "Open audio...")); } else { QFileInfo info(currentFile); m_d->openAudioAction->setText(i18nc("@item:inmenu", "Change audio (%1)...", info.fileName())); } m_d->audioMuteAction->setChecked(m_d->model->isAudioMuted()); QIcon audioIcon; if (currentFile.isEmpty()) { audioIcon = KisIconUtils::loadIcon("audio-none"); } else { if (m_d->model->isAudioMuted()) { audioIcon = KisIconUtils::loadIcon("audio-volume-mute"); } else { audioIcon = KisIconUtils::loadIcon("audio-volume-high"); } } m_d->audioOptionsButton->setIcon(audioIcon); m_d->volumeSlider->setEnabled(!m_d->model->isAudioMuted()); KisSignalsBlocker b(m_d->volumeSlider); m_d->volumeSlider->setValue(qRound(m_d->model->audioVolume() * 100.0)); } void TimelineFramesView::slotAudioVolumeChanged(int value) { m_d->model->setAudioVolume(qreal(value) / 100.0); } void TimelineFramesView::slotUpdateInfiniteFramesCount() { - if (horizontalScrollBar()->isSliderDown()) return; + const int lastVisibleFrame = estimateLastVisibleColumn(); - const int sectionWidth = m_d->horizontalRuler->defaultSectionSize(); - const int calculatedIndex = - (horizontalScrollBar()->value() + - m_d->horizontalRuler->width() - 1) / sectionWidth; + m_d->model->setLastVisibleFrame(lastVisibleFrame); - m_d->model->setLastVisibleFrame(calculatedIndex); } void TimelineFramesView::slotScrollerStateChanged( QScroller::State state ) { + + if (state == QScroller::Dragging || state == QScroller::Scrolling ) { + m_d->kineticScrollInfiniteFrameUpdater.start(16); + } else { + m_d->kineticScrollInfiniteFrameUpdater.stop(); + } + KisKineticScroller::updateCursor(this, state); } +void TimelineFramesView::slotUpdateDragInfiniteFramesCount() { + if(m_d->dragInProgress || + (m_d->model->isScrubbing() && horizontalScrollBar()->sliderPosition() == horizontalScrollBar()->maximum()) ) { + slotUpdateInfiniteFramesCount(); + } +} + +void TimelineFramesView::slotRealignScrollBars() { + QScrollBar* hBar = horizontalScrollBar(); + QScrollBar* vBar = verticalScrollBar(); + + QSize desiredScrollArea = QSize(width() - verticalHeader()->width(), height() - horizontalHeader()->height()); + + // Compensate for corner gap... + if (hBar->isVisible() && vBar->isVisible()) { + desiredScrollArea -= QSize(vBar->width(), hBar->height()); + } + + hBar->parentWidget()->layout()->setAlignment(Qt::AlignRight); + hBar->setMaximumWidth(desiredScrollArea.width()); + hBar->setMinimumWidth(desiredScrollArea.width()); + + + vBar->parentWidget()->layout()->setAlignment(Qt::AlignBottom); + vBar->setMaximumHeight(desiredScrollArea.height()); + vBar->setMinimumHeight(desiredScrollArea.height()); +} + void TimelineFramesView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) { QTableView::currentChanged(current, previous); if (previous.column() != current.column()) { m_d->model->setData(previous, false, TimelineFramesModel::ActiveFrameRole); m_d->model->setData(current, true, TimelineFramesModel::ActiveFrameRole); } } QItemSelectionModel::SelectionFlags TimelineFramesView::selectionCommand(const QModelIndex &index, const QEvent *event) const { // WARNING: Copy-pasted from KisNodeView! Please keep in sync! /** * Qt has a bug: when we Ctrl+click on an item, the item's * selections gets toggled on mouse *press*, whereas usually it is * done on mouse *release*. Therefore the user cannot do a * Ctrl+D&D with the default configuration. This code fixes the * problem by manually returning QItemSelectionModel::NoUpdate * flag when the user clicks on an item and returning * QItemSelectionModel::Toggle on release. */ if (event && (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease) && index.isValid()) { const QMouseEvent *mevent = static_cast(event); if (mevent->button() == Qt::RightButton && selectionModel()->selectedIndexes().contains(index)) { // Allow calling context menu for multiple layers return QItemSelectionModel::NoUpdate; } if (event->type() == QEvent::MouseButtonPress && (mevent->modifiers() & Qt::ControlModifier)) { return QItemSelectionModel::NoUpdate; } if (event->type() == QEvent::MouseButtonRelease && (mevent->modifiers() & Qt::ControlModifier)) { return QItemSelectionModel::Toggle; } } return QAbstractItemView::selectionCommand(index, event); } void TimelineFramesView::slotSelectionChanged() { int minColumn = std::numeric_limits::max(); int maxColumn = std::numeric_limits::min(); foreach (const QModelIndex &idx, selectedIndexes()) { if (idx.column() > maxColumn) { maxColumn = idx.column(); } if (idx.column() < minColumn) { minColumn = idx.column(); } } KisTimeRange range; if (maxColumn > minColumn) { range = KisTimeRange(minColumn, maxColumn - minColumn + 1); } if (m_d->model->isPlaybackPaused()) { m_d->model->stopPlayback(); } m_d->model->setPlaybackRange(range); } void TimelineFramesView::slotReselectCurrentIndex() { QModelIndex index = currentIndex(); currentChanged(index, index); } void TimelineFramesView::slotEnsureRowVisible(int row) { QModelIndex index = currentIndex(); if (!index.isValid() || row < 0) return; index = m_d->model->index(row, index.column()); scrollTo(index); } void TimelineFramesView::slotDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) { if (m_d->model->isPlaybackActive()) return; int selectedColumn = -1; for (int j = topLeft.column(); j <= bottomRight.column(); j++) { QVariant value = m_d->model->data( m_d->model->index(topLeft.row(), j), TimelineFramesModel::ActiveFrameRole); if (value.isValid() && value.toBool()) { selectedColumn = j; break; } } QModelIndex index = currentIndex(); if (!index.isValid() && selectedColumn < 0) { return; } if (selectedColumn == -1) { selectedColumn = index.column(); } if (selectedColumn != index.column() && !m_d->dragInProgress) { int row= index.isValid() ? index.row() : 0; selectionModel()->setCurrentIndex(m_d->model->index(row, selectedColumn), QItemSelectionModel::ClearAndSelect); } } void TimelineFramesView::slotHeaderDataChanged(Qt::Orientation orientation, int first, int last) { Q_UNUSED(first); Q_UNUSED(last); if (orientation == Qt::Horizontal) { const int newFps = m_d->model->headerData(0, Qt::Horizontal, TimelineFramesModel::FramesPerSecondRole).toInt(); if (newFps != m_d->fps) { setFramesPerSecond(newFps); } } } void TimelineFramesView::rowsInserted(const QModelIndex& parent, int start, int end) { QTableView::rowsInserted(parent, start, end); } inline bool isIndexDragEnabled(QAbstractItemModel *model, const QModelIndex &index) { return (model->flags(index) & Qt::ItemIsDragEnabled); } QStyleOptionViewItem TimelineFramesView::Private::viewOptionsV4() const { QStyleOptionViewItem option = q->viewOptions(); option.locale = q->locale(); option.locale.setNumberOptions(QLocale::OmitGroupSeparator); option.widget = q; return option; } QItemViewPaintPairs TimelineFramesView::Private::draggablePaintPairs(const QModelIndexList &indexes, QRect *r) const { Q_ASSERT(r); QRect &rect = *r; const QRect viewportRect = q->viewport()->rect(); QItemViewPaintPairs ret; for (int i = 0; i < indexes.count(); ++i) { const QModelIndex &index = indexes.at(i); const QRect current = q->visualRect(index); if (current.intersects(viewportRect)) { ret += qMakePair(current, index); rect |= current; } } rect &= viewportRect; return ret; } QPixmap TimelineFramesView::Private::renderToPixmap(const QModelIndexList &indexes, QRect *r) const { Q_ASSERT(r); QItemViewPaintPairs paintPairs = draggablePaintPairs(indexes, r); if (paintPairs.isEmpty()) return QPixmap(); QPixmap pixmap(r->size()); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); QStyleOptionViewItem option = viewOptionsV4(); option.state |= QStyle::State_Selected; for (int j = 0; j < paintPairs.count(); ++j) { option.rect = paintPairs.at(j).first.translated(-r->topLeft()); const QModelIndex ¤t = paintPairs.at(j).second; //adjustViewOptionsForIndex(&option, current); q->itemDelegate(current)->paint(&painter, option, current); } return pixmap; } void TimelineFramesView::startDrag(Qt::DropActions supportedActions) { QModelIndexList indexes = selectionModel()->selectedIndexes(); if (!indexes.isEmpty() && m_d->modifiersCatcher->modifierPressed("offset-frame")) { QVector rows; int leftmostColumn = std::numeric_limits::max(); Q_FOREACH (const QModelIndex &index, indexes) { leftmostColumn = qMin(leftmostColumn, index.column()); if (!rows.contains(index.row())) { rows.append(index.row()); } } const int lastColumn = m_d->model->columnCount() - 1; selectionModel()->clear(); Q_FOREACH (const int row, rows) { QItemSelection sel(m_d->model->index(row, leftmostColumn), m_d->model->index(row, lastColumn)); selectionModel()->select(sel, QItemSelectionModel::Select); } supportedActions = Qt::MoveAction; { QModelIndexList indexes = selectedIndexes(); for(int i = indexes.count() - 1 ; i >= 0; --i) { if (!isIndexDragEnabled(m_d->model, indexes.at(i))) indexes.removeAt(i); } selectionModel()->clear(); if (indexes.count() > 0) { QMimeData *data = m_d->model->mimeData(indexes); if (!data) return; QRect rect; QPixmap pixmap = m_d->renderToPixmap(indexes, &rect); rect.adjust(horizontalOffset(), verticalOffset(), 0, 0); QDrag *drag = new QDrag(this); drag->setPixmap(pixmap); drag->setMimeData(data); drag->setHotSpot(m_d->lastPressedPosition - rect.topLeft()); drag->exec(supportedActions, Qt::MoveAction); setCurrentIndex(currentIndex()); } } } else { /** * Workaround for Qt5's bug: if we start a dragging action right during * Shift-selection, Qt will get crazy. We cannot workaround it easily, * because we would need to fork mouseMoveEvent() for that (where the * decision about drag state is done). So we just abort dragging in that * case. * * BUG:373067 */ if (m_d->lastPressedModifier & Qt::ShiftModifier) { return; } /** * Workaround for Qt5's bugs: * * 1) Qt doesn't treat selection the selection on D&D * correctly, so we save it in advance and restore * afterwards. * * 2) There is a private variable in QAbstractItemView: * QAbstractItemView::Private::currentSelectionStartIndex. * It is initialized *only* when the setCurrentIndex() is called * explicitly on the view object, not on the selection model. * Therefore we should explicitly call setCurrentIndex() after * D&D, even if it already has *correct* value! * * 2) We should also call selectionModel()->select() * explicitly. There are two reasons for it: 1) Qt doesn't * maintain selection over D&D; 2) when reselecting single * element after D&D, Qt goes crazy, because it tries to * read *global* keyboard modifiers. Therefore if we are * dragging with Shift or Ctrl pressed it'll get crazy. So * just reset it explicitly. */ QModelIndexList selectionBefore = selectionModel()->selectedIndexes(); QModelIndex currentBefore = selectionModel()->currentIndex(); // initialize a global status variable m_d->dragWasSuccessful = false; QAbstractItemView::startDrag(supportedActions); QModelIndex newCurrent; QPoint selectionOffset; if (m_d->dragWasSuccessful) { newCurrent = currentIndex(); selectionOffset = QPoint(newCurrent.column() - currentBefore.column(), newCurrent.row() - currentBefore.row()); } else { newCurrent = currentBefore; selectionOffset = QPoint(); } setCurrentIndex(newCurrent); selectionModel()->clearSelection(); Q_FOREACH (const QModelIndex &idx, selectionBefore) { QModelIndex newIndex = model()->index(idx.row() + selectionOffset.y(), idx.column() + selectionOffset.x()); selectionModel()->select(newIndex, QItemSelectionModel::Select); } } } void TimelineFramesView::dragEnterEvent(QDragEnterEvent *event) { m_d->dragInProgress = true; m_d->model->setScrubState(true); QTableView::dragEnterEvent(event); } void TimelineFramesView::dragMoveEvent(QDragMoveEvent *event) { m_d->dragInProgress = true; m_d->model->setScrubState(true); QTableView::dragMoveEvent(event); if (event->isAccepted()) { QModelIndex index = indexAt(event->pos()); + if (!m_d->model->canDropFrameData(event->mimeData(), index)) { event->ignore(); } else { selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate); } } } void TimelineFramesView::dropEvent(QDropEvent *event) { m_d->dragInProgress = false; m_d->model->setScrubState(false); if (event->keyboardModifiers() & Qt::ControlModifier) { event->setDropAction(Qt::CopyAction); } QAbstractItemView::dropEvent(event); m_d->dragWasSuccessful = event->isAccepted(); } void TimelineFramesView::dragLeaveEvent(QDragLeaveEvent *event) { m_d->dragInProgress = false; m_d->model->setScrubState(false); QAbstractItemView::dragLeaveEvent(event); } void TimelineFramesView::createFrameEditingMenuActions(QMenu *menu, bool addFrameCreationActions) { slotUpdateFrameActions(); // calculate if selection range is set. This will determine if the update playback range is available QSet rows; int minColumn = 0; int maxColumn = 0; calculateSelectionMetrics(minColumn, maxColumn, rows); bool selectionExists = minColumn != maxColumn; menu->addSection(i18n("Edit Frames:")); menu->addSeparator(); if (selectionExists) { KisActionManager::safePopulateMenu(menu, "update_playback_range", m_d->actionMan); } else { KisActionManager::safePopulateMenu(menu, "set_start_time", m_d->actionMan); KisActionManager::safePopulateMenu(menu, "set_end_time", m_d->actionMan); } menu->addSeparator(); KisActionManager::safePopulateMenu(menu, "cut_frames_to_clipboard", m_d->actionMan); KisActionManager::safePopulateMenu(menu, "copy_frames_to_clipboard", m_d->actionMan); KisActionManager::safePopulateMenu(menu, "paste_frames_from_clipboard", m_d->actionMan); menu->addSeparator(); { //Frames submenu. QMenu *frames = menu->addMenu(i18nc("@item:inmenu", "Keyframes")); KisActionManager::safePopulateMenu(frames, "insert_keyframe_left", m_d->actionMan); KisActionManager::safePopulateMenu(frames, "insert_keyframe_right", m_d->actionMan); frames->addSeparator(); KisActionManager::safePopulateMenu(frames, "insert_multiple_keyframes", m_d->actionMan); } { //Holds submenu. QMenu *hold = menu->addMenu(i18nc("@item:inmenu", "Hold Frames")); KisActionManager::safePopulateMenu(hold, "insert_hold_frame", m_d->actionMan); KisActionManager::safePopulateMenu(hold, "remove_hold_frame", m_d->actionMan); hold->addSeparator(); KisActionManager::safePopulateMenu(hold, "insert_multiple_hold_frames", m_d->actionMan); KisActionManager::safePopulateMenu(hold, "remove_multiple_hold_frames", m_d->actionMan); } menu->addSeparator(); KisActionManager::safePopulateMenu(menu, "remove_frames", m_d->actionMan); KisActionManager::safePopulateMenu(menu, "remove_frames_and_pull", m_d->actionMan); menu->addSeparator(); if (addFrameCreationActions) { KisActionManager::safePopulateMenu(menu, "add_blank_frame", m_d->actionMan); KisActionManager::safePopulateMenu(menu, "add_duplicate_frame", m_d->actionMan); menu->addSeparator(); } } void TimelineFramesView::mousePressEvent(QMouseEvent *event) { QPersistentModelIndex index = indexAt(event->pos()); if (m_d->modifiersCatcher->modifierPressed("pan-zoom")) { if (event->button() == Qt::RightButton) { // TODO: try calculate index under mouse cursor even when // it is outside any visible row qreal staticPoint = index.isValid() ? index.column() : currentIndex().column(); m_d->zoomDragButton->beginZoom(event->pos(), staticPoint); } else if (event->button() == Qt::LeftButton) { m_d->initialDragPanPos = event->pos(); m_d->initialDragPanValue = QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()); } event->accept(); } else if (event->button() == Qt::RightButton) { int numSelectedItems = selectionModel()->selectedIndexes().size(); if (index.isValid() && numSelectedItems <= 1 && m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) { model()->setData(index, true, TimelineFramesModel::ActiveLayerRole); model()->setData(index, true, TimelineFramesModel::ActiveFrameRole); setCurrentIndex(index); if (model()->data(index, TimelineFramesModel::FrameExistsRole).toBool() || model()->data(index, TimelineFramesModel::SpecialKeyframeExists).toBool()) { { KisSignalsBlocker b(m_d->colorSelector); QVariant colorLabel = index.data(TimelineFramesModel::FrameColorLabelIndexRole); int labelIndex = colorLabel.isValid() ? colorLabel.toInt() : 0; m_d->colorSelector->setCurrentIndex(labelIndex); } QMenu menu; createFrameEditingMenuActions(&menu, false); menu.addSeparator(); menu.addAction(m_d->colorSelectorAction); menu.exec(event->globalPos()); } else { { KisSignalsBlocker b(m_d->colorSelector); const int labelIndex = KisImageConfig(true).defaultFrameColorLabel(); m_d->colorSelector->setCurrentIndex(labelIndex); } QMenu menu; createFrameEditingMenuActions(&menu, true); menu.addSeparator(); menu.addAction(m_d->colorSelectorAction); menu.exec(event->globalPos()); } } else if (numSelectedItems > 1) { int labelIndex = -1; bool haveFrames = false; Q_FOREACH(QModelIndex index, selectedIndexes()) { haveFrames |= index.data(TimelineFramesModel::FrameExistsRole).toBool(); QVariant colorLabel = index.data(TimelineFramesModel::FrameColorLabelIndexRole); if (colorLabel.isValid()) { if (labelIndex == -1) { // First label labelIndex = colorLabel.toInt(); } else if (labelIndex != colorLabel.toInt()) { // Mixed colors in selection labelIndex = -1; break; } } } if (haveFrames) { KisSignalsBlocker b(m_d->multiframeColorSelector); m_d->multiframeColorSelector->setCurrentIndex(labelIndex); } QMenu menu; createFrameEditingMenuActions(&menu, false); menu.addSeparator(); KisActionManager::safePopulateMenu(&menu, "mirror_frames", m_d->actionMan); menu.addSeparator(); menu.addAction(m_d->multiframeColorSelectorAction); menu.exec(event->globalPos()); } } else if (event->button() == Qt::MidButton) { QModelIndex index = model()->buddy(indexAt(event->pos())); if (index.isValid()) { QStyleOptionViewItem option = viewOptions(); option.rect = visualRect(index); // The offset of the headers is needed to get the correct position inside the view. m_d->tip.showTip(this, event->pos() + QPoint(verticalHeader()->width(), horizontalHeader()->height()), option, index); } event->accept(); } else { if (index.isValid()) { m_d->model->setLastClickedIndex(index); } m_d->lastPressedPosition = QPoint(horizontalOffset(), verticalOffset()) + event->pos(); m_d->lastPressedModifier = event->modifiers(); QAbstractItemView::mousePressEvent(event); } } void TimelineFramesView::mouseMoveEvent(QMouseEvent *e) { if (m_d->modifiersCatcher->modifierPressed("pan-zoom")) { if (e->buttons() & Qt::RightButton) { m_d->zoomDragButton->continueZoom(e->pos()); } else if (e->buttons() & Qt::LeftButton) { QPoint diff = e->pos() - m_d->initialDragPanPos; QPoint offset = QPoint(m_d->initialDragPanValue.x() - diff.x(), m_d->initialDragPanValue.y() - diff.y()); const int height = m_d->layersHeader->defaultSectionSize(); + if (m_d->initialDragPanValue.x() - diff.x() > horizontalScrollBar()->maximum() || m_d->initialDragPanValue.x() - diff.x() > horizontalScrollBar()->minimum() ){ + KisZoomableScrollbar* zoombar = static_cast(horizontalScrollBar()); + zoombar->overscroll(-diff.x()); + } + horizontalScrollBar()->setValue(offset.x()); verticalScrollBar()->setValue(offset.y() / height); } e->accept(); } else if (e->buttons() == Qt::MidButton) { QModelIndex index = model()->buddy(indexAt(e->pos())); if (index.isValid()) { QStyleOptionViewItem option = viewOptions(); option.rect = visualRect(index); // The offset of the headers is needed to get the correct position inside the view. m_d->tip.showTip(this, e->pos() + QPoint(verticalHeader()->width(), horizontalHeader()->height()), option, index); } e->accept(); } else { m_d->model->setScrubState(true); QTableView::mouseMoveEvent(e); } } void TimelineFramesView::mouseReleaseEvent(QMouseEvent *e) { if (m_d->modifiersCatcher->modifierPressed("pan-zoom")) { e->accept(); } else { m_d->model->setScrubState(false); QTableView::mouseReleaseEvent(e); } } void TimelineFramesView::wheelEvent(QWheelEvent *e) { QModelIndex index = currentIndex(); int column= -1; + if (verticalHeader()->rect().contains(verticalHeader()->mapFromGlobal(e->globalPos()))) { + QTableView::wheelEvent(e); + return; + } + if (index.isValid()) { column= index.column() + ((e->delta() > 0) ? 1 : -1); } if (column >= 0 && !m_d->dragInProgress) { setCurrentIndex(m_d->model->index(index.row(), column)); } } +void TimelineFramesView::resizeEvent(QResizeEvent *e) +{ + updateGeometries(); + slotUpdateInfiniteFramesCount(); +} + void TimelineFramesView::slotUpdateLayersMenu() { QAction *action = 0; m_d->existingLayersMenu->clear(); QVariant value = model()->headerData(0, Qt::Vertical, TimelineFramesModel::OtherLayersRole); if (value.isValid()) { TimelineFramesModel::OtherLayersList list = value.value(); int i = 0; Q_FOREACH (const TimelineFramesModel::OtherLayer &l, list) { action = m_d->existingLayersMenu->addAction(l.name); action->setData(i++); } } } void TimelineFramesView::slotUpdateFrameActions() { if (!m_d->actionMan) return; const QModelIndexList editableIndexes = calculateSelectionSpan(false, true); const bool hasEditableFrames = !editableIndexes.isEmpty(); bool hasExistingFrames = false; Q_FOREACH (const QModelIndex &index, editableIndexes) { if (model()->data(index, TimelineFramesModel::FrameExistsRole).toBool()) { hasExistingFrames = true; break; } } auto enableAction = [this] (const QString &id, bool value) { KisAction *action = m_d->actionMan->actionByName(id); KIS_SAFE_ASSERT_RECOVER_RETURN(action); action->setEnabled(value); }; enableAction("add_blank_frame", hasEditableFrames); enableAction("add_duplicate_frame", hasEditableFrames); enableAction("insert_keyframe_left", hasEditableFrames); enableAction("insert_keyframe_right", hasEditableFrames); enableAction("insert_multiple_keyframes", hasEditableFrames); enableAction("remove_frames", hasEditableFrames && hasExistingFrames); enableAction("remove_frames_and_pull", hasEditableFrames); enableAction("insert_hold_frame", hasEditableFrames); enableAction("insert_multiple_hold_frames", hasEditableFrames); enableAction("remove_hold_frame", hasEditableFrames); enableAction("remove_multiple_hold_frames", hasEditableFrames); enableAction("mirror_frames", hasEditableFrames && editableIndexes.size() > 1); enableAction("copy_frames_to_clipboard", true); enableAction("cut_frames_to_clipboard", hasEditableFrames); } void TimelineFramesView::slotSetStartTimeToCurrentPosition() { m_d->model->setFullClipRangeStart(this->currentIndex().column()); } void TimelineFramesView::slotSetEndTimeToCurrentPosition() { m_d->model->setFullClipRangeEnd(this->currentIndex().column()); } void TimelineFramesView::slotUpdatePlackbackRange() { QSet rows; int minColumn = 0; int maxColumn = 0; calculateSelectionMetrics(minColumn, maxColumn, rows); m_d->model->setFullClipRangeStart(minColumn); m_d->model->setFullClipRangeEnd(maxColumn); } void TimelineFramesView::slotLayerContextMenuRequested(const QPoint &globalPos) { m_d->layerEditingMenu->exec(globalPos); } void TimelineFramesView::slotAddNewLayer() { QModelIndex index = currentIndex(); const int newRow = index.isValid() ? index.row() : 0; model()->insertRow(newRow); } void TimelineFramesView::slotAddExistingLayer(QAction *action) { QVariant value = action->data(); if (value.isValid()) { QModelIndex index = currentIndex(); const int newRow = index.isValid() ? index.row() + 1 : 0; m_d->model->insertOtherLayer(value.toInt(), newRow); } } void TimelineFramesView::slotRemoveLayer() { QModelIndex index = currentIndex(); if (!index.isValid()) return; model()->removeRow(index.row()); } void TimelineFramesView::slotAddBlankFrame() { QModelIndex index = currentIndex(); if (!index.isValid() || !m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) { return; } m_d->model->createFrame(index); } void TimelineFramesView::slotAddDuplicateFrame() { QModelIndex index = currentIndex(); if (!index.isValid() || !m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) { return; } m_d->model->copyFrame(index); } void TimelineFramesView::calculateSelectionMetrics(int &minColumn, int &maxColumn, QSet &rows) const { minColumn = std::numeric_limits::max(); maxColumn = std::numeric_limits::min(); Q_FOREACH (const QModelIndex &index, selectionModel()->selectedIndexes()) { if (!m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) continue; rows.insert(index.row()); minColumn = qMin(minColumn, index.column()); maxColumn = qMax(maxColumn, index.column()); } } void TimelineFramesView::insertKeyframes(int count, int timing, TimelineDirection direction, bool entireColumn) { QSet rows; int minColumn = 0, maxColumn = 0; calculateSelectionMetrics(minColumn, maxColumn, rows); if (count <= 0) { //Negative count? Use number of selected frames. count = qMax(1, maxColumn - minColumn + 1); } const int insertionColumn = direction == TimelineDirection::RIGHT ? maxColumn + 1 : minColumn; if (entireColumn) { rows.clear(); for (int i = 0; i < m_d->model->rowCount(); i++) { if (!m_d->model->data(m_d->model->index(i, insertionColumn), TimelineFramesModel::FrameEditableRole).toBool()) continue; rows.insert(i); } } if (!rows.isEmpty()) { #if QT_VERSION >= QT_VERSION_CHECK(5,14,0) m_d->model->insertFrames(insertionColumn, QList(rows.begin(), rows.end()), count, timing); #else m_d->model->insertFrames(insertionColumn, QList::fromSet(rows), count, timing); #endif } } void TimelineFramesView::insertMultipleKeyframes(bool entireColumn) { int count, timing; TimelineDirection direction; if (m_d->insertKeyframeDialog->promptUserSettings(count, timing, direction)) { insertKeyframes(count, timing, direction, entireColumn); } } QModelIndexList TimelineFramesView::calculateSelectionSpan(bool entireColumn, bool editableOnly) const { QModelIndexList indexes; if (entireColumn) { QSet rows; int minColumn = 0; int maxColumn = 0; calculateSelectionMetrics(minColumn, maxColumn, rows); rows.clear(); for (int i = 0; i < m_d->model->rowCount(); i++) { if (editableOnly && !m_d->model->data(m_d->model->index(i, minColumn), TimelineFramesModel::FrameEditableRole).toBool()) continue; for (int column = minColumn; column <= maxColumn; column++) { indexes << m_d->model->index(i, column); } } } else { Q_FOREACH (const QModelIndex &index, selectionModel()->selectedIndexes()) { if (!editableOnly || m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) { indexes << index; } } } return indexes; } +int TimelineFramesView::estimateLastVisibleColumn() +{ + const int sectionWidth = m_d->horizontalRuler->defaultSectionSize(); + const int calculatedIndex = + (horizontalScrollBar()->value() + + m_d->horizontalRuler->width() - 1) / sectionWidth; + return calculatedIndex; +} + +int TimelineFramesView::estimateFirstVisibleColumn() +{ + const int sectionWidth = m_d->horizontalRuler->defaultSectionSize(); + const int calculatedIndex = ceil( qreal(horizontalScrollBar()->value()) / sectionWidth ); + return calculatedIndex; +} + +int TimelineFramesView::scrollPositionFromColumn(int column) { + const int sectionWidth = m_d->horizontalRuler->defaultSectionSize(); + return sectionWidth * column; +} + void TimelineFramesView::slotRemoveSelectedFrames(bool entireColumn, bool pull) { const QModelIndexList selectedIndices = calculateSelectionSpan(entireColumn); if (!selectedIndices.isEmpty()) { if (pull) { m_d->model->removeFramesAndOffset(selectedIndices); } else { m_d->model->removeFrames(selectedIndices); } } } void TimelineFramesView::insertOrRemoveHoldFrames(int count, bool entireColumn) { QModelIndexList indexes; if (!entireColumn) { Q_FOREACH (const QModelIndex &index, selectionModel()->selectedIndexes()) { if (m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) { indexes << index; } } } else { const int column = selectionModel()->currentIndex().column(); for (int i = 0; i < m_d->model->rowCount(); i++) { const QModelIndex index = m_d->model->index(i, column); if (m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) { indexes << index; } } } if (!indexes.isEmpty()) { // add extra columns to the end of the timeline if we are adding hold frames // they will be truncated if we don't do this if (count > 0) { // Scan all the layers and find out what layer has the most keyframes // only keep a reference of layer that has the most keyframes int keyframesInLayerNode = 0; Q_FOREACH (const QModelIndex &index, indexes) { KisNodeSP layerNode = m_d->model->nodeAt(index); KisKeyframeChannel *channel = layerNode->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (!channel) continue; if (keyframesInLayerNode < channel->allKeyframeIds().count()) { keyframesInLayerNode = channel->allKeyframeIds().count(); } } m_d->model->setLastVisibleFrame(m_d->model->columnCount() + count*keyframesInLayerNode); } m_d->model->insertHoldFrames(indexes, count); // bulk adding frames can add too many // trim timeline to clean up extra frames that might have been added slotUpdateInfiniteFramesCount(); } } void TimelineFramesView::insertOrRemoveMultipleHoldFrames(bool insertion, bool entireColumn) { bool ok = false; const int count = QInputDialog::getInt(this, i18nc("@title:window", "Insert or Remove Hold Frames"), i18nc("@label:spinbox", "Enter number of frames"), insertion ? m_d->insertKeyframeDialog->defaultTimingOfAddedFrames() : m_d->insertKeyframeDialog->defaultNumberOfHoldFramesToRemove(), 1, 10000, 1, &ok); if (ok) { if (insertion) { m_d->insertKeyframeDialog->setDefaultTimingOfAddedFrames(count); insertOrRemoveHoldFrames(count, entireColumn); } else { m_d->insertKeyframeDialog->setDefaultNumberOfHoldFramesToRemove(count); insertOrRemoveHoldFrames(-count, entireColumn); } } } void TimelineFramesView::slotMirrorFrames(bool entireColumn) { const QModelIndexList indexes = calculateSelectionSpan(entireColumn); if (!indexes.isEmpty()) { m_d->model->mirrorFrames(indexes); } } void TimelineFramesView::cutCopyImpl(bool entireColumn, bool copy) { const QModelIndexList selectedIndices = calculateSelectionSpan(entireColumn, !copy); if (selectedIndices.isEmpty()) return; int minColumn = std::numeric_limits::max(); int minRow = std::numeric_limits::max(); Q_FOREACH (const QModelIndex &index, selectedIndices) { minRow = qMin(minRow, index.row()); minColumn = qMin(minColumn, index.column()); } const QModelIndex baseIndex = m_d->model->index(minRow, minColumn); QMimeData *data = m_d->model->mimeDataExtended(selectedIndices, baseIndex, copy ? TimelineFramesModel::CopyFramesPolicy : TimelineFramesModel::MoveFramesPolicy); if (data) { QClipboard *cb = QApplication::clipboard(); cb->setMimeData(data); } } void TimelineFramesView::slotPasteFrames(bool entireColumn) { const QModelIndex currentIndex = !entireColumn ? this->currentIndex() : m_d->model->index(0, this->currentIndex().column()); if (!currentIndex.isValid()) return; QClipboard *cb = QApplication::clipboard(); const QMimeData *data = cb->mimeData(); if (data && data->hasFormat("application/x-krita-frame")) { bool dataMoved = false; bool result = m_d->model->dropMimeDataExtended(data, Qt::MoveAction, currentIndex, &dataMoved); if (result && dataMoved) { cb->clear(); } } } bool TimelineFramesView::viewportEvent(QEvent *event) { if (event->type() == QEvent::ToolTip && model()) { QHelpEvent *he = static_cast(event); QModelIndex index = model()->buddy(indexAt(he->pos())); if (index.isValid()) { QStyleOptionViewItem option = viewOptions(); option.rect = visualRect(index); // The offset of the headers is needed to get the correct position inside the view. m_d->tip.showTip(this, he->pos() + QPoint(verticalHeader()->width(), horizontalHeader()->height()), option, index); return true; } } return QTableView::viewportEvent(event); } diff --git a/plugins/dockers/animation/timeline_frames_view.h b/plugins/dockers/animation/timeline_frames_view.h index 323fee46b6..4263f1817d 100644 --- a/plugins/dockers/animation/timeline_frames_view.h +++ b/plugins/dockers/animation/timeline_frames_view.h @@ -1,190 +1,199 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __TIMELINE_FRAMES_VIEW_H #define __TIMELINE_FRAMES_VIEW_H #include #include #include +#include #include "kis_action_manager.h" #include "kritaanimationdocker_export.h" class KisAction; class TimelineWidget; enum TimelineDirection : short { LEFT = -1, BEFORE = -1, RIGHT = 1, AFTER = 1 }; - class KRITAANIMATIONDOCKER_EXPORT TimelineFramesView : public QTableView { Q_OBJECT public: TimelineFramesView(QWidget *parent); ~TimelineFramesView() override; void setModel(QAbstractItemModel *model) override; void updateGeometries() override; void setPinToTimeline(KisAction *action); void setActionManager(KisActionManager *actionManager); public Q_SLOTS: void slotSelectionChanged(); void slotUpdateIcons(); private Q_SLOTS: void slotUpdateLayersMenu(); void slotUpdateFrameActions(); void slotSetStartTimeToCurrentPosition(); void slotSetEndTimeToCurrentPosition(); void slotUpdatePlackbackRange(); // Layer void slotAddNewLayer(); void slotAddExistingLayer(QAction *action); void slotDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); void slotRemoveLayer(); void slotLayerContextMenuRequested(const QPoint &globalPos); // New, Insert and Remove Frames void slotAddBlankFrame(); void slotAddDuplicateFrame(); void slotInsertKeyframeLeft() {insertKeyframes(-1, 1, TimelineDirection::LEFT, false);} void slotInsertKeyframeRight() {insertKeyframes(-1, 1, TimelineDirection::RIGHT, false);} void slotInsertKeyframeColumnLeft() {insertKeyframes(-1, 1, TimelineDirection::LEFT, true);} void slotInsertKeyframeColumnRight() {insertKeyframes(-1, 1, TimelineDirection::RIGHT, true);} void slotInsertMultipleKeyframes() {insertMultipleKeyframes(false);} void slotInsertMultipleKeyframeColumns() {insertMultipleKeyframes(true);} void slotRemoveSelectedFrames(bool entireColumn = false, bool pull = false); void slotRemoveSelectedFramesAndShift() {slotRemoveSelectedFrames(false, true);} void slotRemoveSelectedColumns() {slotRemoveSelectedFrames(true);} void slotRemoveSelectedColumnsAndShift() {slotRemoveSelectedFrames(true, true);} void slotInsertHoldFrame() {insertOrRemoveHoldFrames(1);} void slotRemoveHoldFrame() {insertOrRemoveHoldFrames(-1);} void slotInsertHoldFrameColumn() {insertOrRemoveHoldFrames(1,true);} void slotRemoveHoldFrameColumn() {insertOrRemoveHoldFrames(-1,true);} void slotInsertMultipleHoldFrames() {insertOrRemoveMultipleHoldFrames(true);} void slotRemoveMultipleHoldFrames() {insertOrRemoveMultipleHoldFrames(false);} void slotInsertMultipleHoldFrameColumns() {insertOrRemoveMultipleHoldFrames(true, true);} void slotRemoveMultipleHoldFrameColumns() {insertOrRemoveMultipleHoldFrames(false, true);} void slotMirrorFrames(bool entireColumn = false); void slotMirrorColumns() {slotMirrorFrames(true);} // Copy-paste void slotCopyFrames() {cutCopyImpl(false, true);} void slotCutFrames() {cutCopyImpl(false, false);} void slotCopyColumns() {cutCopyImpl(true, true);} void slotCutColumns() {cutCopyImpl(true, false);} void slotPasteFrames(bool entireColumn = false); void slotPasteColumns() {slotPasteFrames(true);} void slotReselectCurrentIndex(); void slotUpdateInfiniteFramesCount(); void slotHeaderDataChanged(Qt::Orientation orientation, int first, int last); - void slotZoomButtonPressed(qreal staticPoint); void slotZoomButtonChanged(qreal value); + void slotScrollbarZoom(qreal zoom); + void slotColorLabelChanged(int); void slotEnsureRowVisible(int row); // Audio void slotSelectAudioChannelFile(); void slotAudioChannelMute(bool value); void slotAudioChannelRemove(); void slotUpdateAudioActions(); void slotAudioVolumeChanged(int value); // DragScroll void slotScrollerStateChanged(QScroller::State state); + void slotUpdateDragInfiniteFramesCount(); + + void slotRealignScrollBars(); private: void setFramesPerSecond(int fps); void calculateSelectionMetrics(int &minColumn, int &maxColumn, QSet &rows) const; /* Insert new keyframes/columns. * * count - Number of frames to add. If <0, use number of currently SELECTED frames. * timing - Animation timing of frames to be added (on 1s, 2s, 3s, etc.) * direction - Insert frames before (left) or after (right) selection scrubber. * entireColumn - Create frames on all layers (rows) instead of just the active layer? */ void insertKeyframes(int count = 1, int timing = 1, TimelineDirection direction = TimelineDirection::LEFT, bool entireColumn = false); void insertMultipleKeyframes(bool entireColumn = false); void insertOrRemoveHoldFrames(int count, bool entireColumn = false); void insertOrRemoveMultipleHoldFrames(bool insertion, bool entireColumn = false); void cutCopyImpl(bool entireColumn, bool copy); void createFrameEditingMenuActions(QMenu *menu, bool addFrameCreationActions); QModelIndexList calculateSelectionSpan(bool entireColumn, bool editableOnly = true) const; + int estimateLastVisibleColumn(); + int estimateFirstVisibleColumn(); + int scrollPositionFromColumn( int column ); + protected: QItemSelectionModel::SelectionFlags selectionCommand(const QModelIndex &index, const QEvent *event) const override; void currentChanged(const QModelIndex ¤t, const QModelIndex &previous) override; void startDrag(Qt::DropActions supportedActions) override; void dragEnterEvent(QDragEnterEvent *event) override; void dragMoveEvent(QDragMoveEvent *event) override; void dropEvent(QDropEvent *event) override; void dragLeaveEvent(QDragLeaveEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; void wheelEvent(QWheelEvent *e) override; + void resizeEvent(QResizeEvent *e) override; void rowsInserted(const QModelIndex &parent, int start, int end) override; bool viewportEvent(QEvent *event) override; private: struct Private; const QScopedPointer m_d; }; #endif /* __TIMELINE_FRAMES_VIEW_H */ diff --git a/plugins/dockers/animation/timeline_ruler_header.cpp b/plugins/dockers/animation/timeline_ruler_header.cpp index ee61db82c0..ee59ee8914 100644 --- a/plugins/dockers/animation/timeline_ruler_header.cpp +++ b/plugins/dockers/animation/timeline_ruler_header.cpp @@ -1,551 +1,561 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "timeline_ruler_header.h" #include #include #include #include #include #include #include "kis_time_based_item_model.h" #include "timeline_color_scheme.h" #include "kis_action.h" #include "kis_debug.h" struct TimelineRulerHeader::Private { Private() : fps(12), lastPressSectionIndex(-1) {} int fps; KisTimeBasedItemModel *model; int lastPressSectionIndex; int calcSpanWidth(const int sectionWidth); QModelIndexList prepareFramesSlab(int startCol, int endCol); KisActionManager* actionMan = 0; + const int minSectionSize = 4; + const int maxSectionSize = 72; + const int unitSectionSize = 18; + qreal remainder = 0.0f; }; TimelineRulerHeader::TimelineRulerHeader(QWidget *parent) : QHeaderView(Qt::Horizontal, parent), m_d(new Private) { setSectionResizeMode(QHeaderView::Fixed); setDefaultSectionSize(18); setMinimumSectionSize(8); } TimelineRulerHeader::~TimelineRulerHeader() { } void TimelineRulerHeader::setActionManager(KisActionManager *actionManager) { m_d->actionMan = actionManager; if (actionManager) { KisAction *action; action = actionManager->createAction("insert_column_left"); connect(action, SIGNAL(triggered()), SIGNAL(sigInsertColumnLeft())); action = actionManager->createAction("insert_column_right"); connect(action, SIGNAL(triggered()), SIGNAL(sigInsertColumnRight())); action = actionManager->createAction("insert_multiple_columns"); connect(action, SIGNAL(triggered()), SIGNAL(sigInsertMultipleColumns())); action = actionManager->createAction("remove_columns_and_pull"); connect(action, SIGNAL(triggered()), SIGNAL(sigRemoveColumnsAndShift())); action = actionManager->createAction("remove_columns"); connect(action, SIGNAL(triggered()), SIGNAL(sigRemoveColumns())); action = actionManager->createAction("insert_hold_column"); connect(action, SIGNAL(triggered()), SIGNAL(sigInsertHoldColumns())); action = actionManager->createAction("insert_multiple_hold_columns"); connect(action, SIGNAL(triggered()), SIGNAL(sigInsertHoldColumnsCustom())); action = actionManager->createAction("remove_hold_column"); connect(action, SIGNAL(triggered()), SIGNAL(sigRemoveHoldColumns())); action = actionManager->createAction("remove_multiple_hold_columns"); connect(action, SIGNAL(triggered()), SIGNAL(sigRemoveHoldColumnsCustom())); action = actionManager->createAction("mirror_columns"); connect(action, SIGNAL(triggered()), SIGNAL(sigMirrorColumns())); action = actionManager->createAction("copy_columns_to_clipboard"); connect(action, SIGNAL(triggered()), SIGNAL(sigCopyColumns())); action = actionManager->createAction("cut_columns_to_clipboard"); connect(action, SIGNAL(triggered()), SIGNAL(sigCutColumns())); action = actionManager->createAction("paste_columns_from_clipboard"); connect(action, SIGNAL(triggered()), SIGNAL(sigPasteColumns())); } } void TimelineRulerHeader::paintEvent(QPaintEvent *e) { QHeaderView::paintEvent(e); // Copied from Qt 4.8... if (count() == 0) return; QPainter painter(viewport()); const QPoint offset = dirtyRegionOffset(); QRect translatedEventRect = e->rect(); translatedEventRect.translate(offset); int start = -1; int end = -1; if (orientation() == Qt::Horizontal) { start = visualIndexAt(translatedEventRect.left()); end = visualIndexAt(translatedEventRect.right()); } else { start = visualIndexAt(translatedEventRect.top()); end = visualIndexAt(translatedEventRect.bottom()); } const bool reverseImpl = orientation() == Qt::Horizontal && isRightToLeft(); if (reverseImpl) { start = (start == -1 ? count() - 1 : start); end = (end == -1 ? 0 : end); } else { start = (start == -1 ? 0 : start); end = (end == -1 ? count() - 1 : end); } int tmp = start; start = qMin(start, end); end = qMax(tmp, end); /////////////////////////////////////////////////// /// Krita specific code. We should update in spans! const int spanStart = start - start % m_d->fps; const int spanEnd = end - end % m_d->fps + m_d->fps - 1; start = spanStart; end = qMin(count() - 1, spanEnd); /// End of Krita specific code /////////////////////////////////////////////////// QRect currentSectionRect; int logical; const int width = viewport()->width(); const int height = viewport()->height(); for (int i = start; i <= end; ++i) { // DK: cannot copy-paste easily... // if (d->isVisualIndexHidden(i)) // continue; painter.save(); logical = logicalIndex(i); if (orientation() == Qt::Horizontal) { currentSectionRect.setRect(sectionViewportPosition(logical), 0, sectionSize(logical), height); } else { currentSectionRect.setRect(0, sectionViewportPosition(logical), width, sectionSize(logical)); } currentSectionRect.translate(offset); QVariant variant = model()->headerData(logical, orientation(), Qt::FontRole); if (variant.isValid() && variant.canConvert()) { QFont sectionFont = qvariant_cast(variant); painter.setFont(sectionFont); } paintSection1(&painter, currentSectionRect, logical); painter.restore(); } } void TimelineRulerHeader::paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const { // Base paint event should paint nothing in the sections area Q_UNUSED(painter); Q_UNUSED(rect); Q_UNUSED(logicalIndex); } void TimelineRulerHeader::paintSpan(QPainter *painter, int userFrameId, const QRect &spanRect, bool isIntegralLine, bool isPrevIntegralLine, QStyle *style, const QPalette &palette, const QPen &gridPen) const { painter->fillRect(spanRect, palette.brush(QPalette::Button)); int safeRight = spanRect.right(); QPen oldPen = painter->pen(); painter->setPen(gridPen); int adjustedTop = spanRect.top() + (!isIntegralLine ? spanRect.height() / 2 : 0); painter->drawLine(safeRight, adjustedTop, safeRight, spanRect.bottom()); if (isPrevIntegralLine) { painter->drawLine(spanRect.left() + 1, spanRect.top(), spanRect.left() + 1, spanRect.bottom()); } painter->setPen(oldPen); QString frameIdText = QString::number(userFrameId); QRect textRect(spanRect.topLeft() + QPoint(2, 0), QSize(spanRect.width() - 2, spanRect.height())); QStyleOptionHeader opt; initStyleOption(&opt); QStyle::State state = QStyle::State_None; if (isEnabled()) state |= QStyle::State_Enabled; if (window()->isActiveWindow()) state |= QStyle::State_Active; opt.state |= state; opt.selectedPosition = QStyleOptionHeader::NotAdjacent; opt.textAlignment = Qt::AlignLeft | Qt::AlignTop; opt.rect = textRect; opt.text = frameIdText; style->drawControl(QStyle::CE_HeaderLabel, &opt, painter, this); } int TimelineRulerHeader::Private::calcSpanWidth(const int sectionWidth) { const int minWidth = 36; int spanWidth = this->fps; while (spanWidth * sectionWidth < minWidth) { spanWidth *= 2; } bool splitHappened = false; do { splitHappened = false; if (!(spanWidth & 0x1) && spanWidth * sectionWidth / 2 > minWidth) { spanWidth /= 2; splitHappened = true; } else if (!(spanWidth % 3) && spanWidth * sectionWidth / 3 > minWidth) { spanWidth /= 3; splitHappened = true; } else if (!(spanWidth % 5) && spanWidth * sectionWidth / 5 > minWidth) { spanWidth /= 5; splitHappened = true; } } while (splitHappened); if (sectionWidth > minWidth) { spanWidth = 1; } return spanWidth; } void TimelineRulerHeader::paintSection1(QPainter *painter, const QRect &rect, int logicalIndex) const { if (!rect.isValid()) return; QFontMetrics metrics(this->font()); const int textHeight = metrics.height(); QPoint p1 = rect.topLeft() + QPoint(0, textHeight); QPoint p2 = rect.topRight() + QPoint(0, textHeight); QRect frameRect = QRect(p1, QSize(rect.width(), rect.height() - textHeight)); const int width = rect.width(); int spanWidth = m_d->calcSpanWidth(width); const int internalIndex = logicalIndex % spanWidth; const int userFrameId = logicalIndex; const int spanEnd = qMin(count(), logicalIndex + spanWidth); QRect spanRect(rect.topLeft(), QSize(width * (spanEnd - logicalIndex), textHeight)); QStyleOptionViewItem option = viewOptions(); const int gridHint = style()->styleHint(QStyle::SH_Table_GridLineColor, &option, this); const QColor gridColor = static_cast(gridHint); const QPen gridPen = QPen(gridColor); if (!internalIndex) { bool isIntegralLine = (logicalIndex + spanWidth) % m_d->fps == 0; bool isPrevIntegralLine = logicalIndex % m_d->fps == 0; paintSpan(painter, userFrameId, spanRect, isIntegralLine, isPrevIntegralLine, style(), palette(), gridPen); } { QBrush fillColor = TimelineColorScheme::instance()->headerEmpty(); QVariant activeValue = model()->headerData(logicalIndex, orientation(), KisTimeBasedItemModel::ActiveFrameRole); QVariant cachedValue = model()->headerData(logicalIndex, orientation(), KisTimeBasedItemModel::FrameCachedRole); if (activeValue.isValid() && activeValue.toBool()) { fillColor = TimelineColorScheme::instance()->headerActive(); } else if (cachedValue.isValid() && cachedValue.toBool()) { fillColor = TimelineColorScheme::instance()->headerCachedFrame(); } painter->fillRect(frameRect, fillColor); QVector lines; lines << QLine(p1, p2); lines << QLine(frameRect.topRight(), frameRect.bottomRight()); lines << QLine(frameRect.bottomLeft(), frameRect.bottomRight()); QPen oldPen = painter->pen(); painter->setPen(gridPen); painter->drawLines(lines); painter->setPen(oldPen); } } void TimelineRulerHeader::changeEvent(QEvent *event) { Q_UNUSED(event); updateMinimumSize(); } void TimelineRulerHeader::setFramePerSecond(int fps) { m_d->fps = fps; update(); } bool TimelineRulerHeader::setZoom(qreal zoom) { - const int minSectionSize = 4; - const int unitSectionSize = 18; - - int newSectionSize = zoom * unitSectionSize; - - if (newSectionSize < minSectionSize) { - newSectionSize = minSectionSize; - zoom = qreal(newSectionSize) / unitSectionSize; + qreal newSectionSize = zoom * m_d->unitSectionSize; + + if (newSectionSize < m_d->minSectionSize) { + newSectionSize = m_d->minSectionSize; + zoom = qreal(newSectionSize) / m_d->unitSectionSize; + } else if (newSectionSize > m_d->maxSectionSize) { + newSectionSize = m_d->maxSectionSize; + zoom = qreal(newSectionSize) / m_d->unitSectionSize; } + m_d->remainder = newSectionSize - floor(newSectionSize); + if (newSectionSize != defaultSectionSize()) { setDefaultSectionSize(newSectionSize); return true; } return false; } +qreal TimelineRulerHeader::zoom() { + return (qreal(defaultSectionSize() + m_d->remainder) / m_d->unitSectionSize); +} + void TimelineRulerHeader::updateMinimumSize() { QFontMetrics metrics(this->font()); const int textHeight = metrics.height(); setMinimumSize(0, 1.5 * textHeight); } void TimelineRulerHeader::setModel(QAbstractItemModel *model) { KisTimeBasedItemModel *framesModel = qobject_cast(model); m_d->model = framesModel; QHeaderView::setModel(model); } int getColumnCount(const QModelIndexList &indexes, int *leftmostCol, int *rightmostCol) { QVector columns; int leftmost = std::numeric_limits::max(); int rightmost = std::numeric_limits::min(); Q_FOREACH (const QModelIndex &index, indexes) { leftmost = qMin(leftmost, index.column()); rightmost = qMax(rightmost, index.column()); if (!columns.contains(index.column())) { columns.append(index.column()); } } if (leftmostCol) *leftmostCol = leftmost; if (rightmostCol) *rightmostCol = rightmost; return columns.size(); } void TimelineRulerHeader::mousePressEvent(QMouseEvent *e) { int logical = logicalIndexAt(e->pos()); if (logical != -1) { QModelIndexList selectedIndexes = selectionModel()->selectedIndexes(); int numSelectedColumns = getColumnCount(selectedIndexes, 0, 0); if (e->button() == Qt::RightButton) { if (numSelectedColumns <= 1) { model()->setHeaderData(logical, orientation(), true, KisTimeBasedItemModel::ActiveFrameRole); } /* Fix for safe-assert involving kis_animation_curve_docker. * There should probably be a more elagant way for dealing * with reused timeline_ruler_header instances in other * timeline views instead of simply animation_frame_view. * * This works for now though... */ if(!m_d->actionMan){ return; } QMenu menu; menu.addSection(i18n("Edit Columns:")); menu.addSeparator(); KisActionManager::safePopulateMenu(&menu, "cut_columns_to_clipboard", m_d->actionMan); KisActionManager::safePopulateMenu(&menu, "copy_columns_to_clipboard", m_d->actionMan); KisActionManager::safePopulateMenu(&menu, "paste_columns_from_clipboard", m_d->actionMan); menu.addSeparator(); { //Frame Columns Submenu QMenu *frames = menu.addMenu(i18nc("@item:inmenu", "Keyframe Columns")); KisActionManager::safePopulateMenu(frames, "insert_column_left", m_d->actionMan); KisActionManager::safePopulateMenu(frames, "insert_column_right", m_d->actionMan); frames->addSeparator(); KisActionManager::safePopulateMenu(frames, "insert_multiple_columns", m_d->actionMan); } { //Hold Columns Submenu QMenu *hold = menu.addMenu(i18nc("@item:inmenu", "Hold Frame Columns")); KisActionManager::safePopulateMenu(hold, "insert_hold_column", m_d->actionMan); KisActionManager::safePopulateMenu(hold, "remove_hold_column", m_d->actionMan); hold->addSeparator(); KisActionManager::safePopulateMenu(hold, "insert_multiple_hold_columns", m_d->actionMan); KisActionManager::safePopulateMenu(hold, "remove_multiple_hold_columns", m_d->actionMan); } menu.addSeparator(); KisActionManager::safePopulateMenu(&menu, "remove_columns", m_d->actionMan); KisActionManager::safePopulateMenu(&menu, "remove_columns_and_pull", m_d->actionMan); if (numSelectedColumns > 1) { menu.addSeparator(); KisActionManager::safePopulateMenu(&menu, "mirror_columns", m_d->actionMan); } menu.exec(e->globalPos()); return; } else if (e->button() == Qt::LeftButton) { m_d->lastPressSectionIndex = logical; model()->setHeaderData(logical, orientation(), true, KisTimeBasedItemModel::ActiveFrameRole); } } QHeaderView::mousePressEvent(e); } void TimelineRulerHeader::mouseMoveEvent(QMouseEvent *e) { int logical = logicalIndexAt(e->pos()); if (logical != -1) { if (e->buttons() & Qt::LeftButton) { m_d->model->setScrubState(true); model()->setHeaderData(logical, orientation(), true, KisTimeBasedItemModel::ActiveFrameRole); if (m_d->lastPressSectionIndex >= 0 && logical != m_d->lastPressSectionIndex && e->modifiers() & Qt::ShiftModifier) { const int minCol = qMin(m_d->lastPressSectionIndex, logical); const int maxCol = qMax(m_d->lastPressSectionIndex, logical); QItemSelection sel(m_d->model->index(0, minCol), m_d->model->index(0, maxCol)); selectionModel()->select(sel, QItemSelectionModel::Columns | QItemSelectionModel::SelectCurrent); } } } QHeaderView::mouseMoveEvent(e); } void TimelineRulerHeader::mouseReleaseEvent(QMouseEvent *e) { if (e->button() == Qt::LeftButton) { m_d->model->setScrubState(false); } QHeaderView::mouseReleaseEvent(e); } QModelIndexList TimelineRulerHeader::Private::prepareFramesSlab(int startCol, int endCol) { QModelIndexList frames; const int numRows = model->rowCount(); for (int i = 0; i < numRows; i++) { for (int j = startCol; j <= endCol; j++) { QModelIndex index = model->index(i, j); const bool exists = model->data(index, KisTimeBasedItemModel::FrameExistsRole).toBool(); if (exists) { frames << index; } } } return frames; } diff --git a/plugins/dockers/animation/timeline_ruler_header.h b/plugins/dockers/animation/timeline_ruler_header.h index 290de58e25..9291bbb0c5 100644 --- a/plugins/dockers/animation/timeline_ruler_header.h +++ b/plugins/dockers/animation/timeline_ruler_header.h @@ -1,90 +1,91 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef TIMELINE_RULER_HEADER_H #define TIMELINE_RULER_HEADER_H #include #include #include "kis_action_manager.h" class QPaintEvent; class TimelineRulerHeader : public QHeaderView { Q_OBJECT public: TimelineRulerHeader(QWidget *parent = 0); ~TimelineRulerHeader() override; void setFramePerSecond(int fps); bool setZoom(qreal zoomLevel); + qreal zoom(); void setModel(QAbstractItemModel *model) override; void setActionManager(KisActionManager *actionManager); void mouseMoveEvent(QMouseEvent *e) override; protected: void mousePressEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; void paintEvent(QPaintEvent *e) override; void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const override; void paintSection1(QPainter *painter, const QRect &rect, int logicalIndex) const; void changeEvent(QEvent *event) override; private: void updateMinimumSize(); void paintSpan(QPainter *painter, int userFrameId, const QRect &spanRect, bool isIntegralLine, bool isPrevIntegralLine, QStyle *style, const QPalette &palette, const QPen &gridPen) const; Q_SIGNALS: void sigInsertColumnLeft(); void sigInsertColumnRight(); void sigInsertMultipleColumns(); void sigRemoveColumns(); void sigRemoveColumnsAndShift(); void sigInsertHoldColumns(); void sigRemoveHoldColumns(); void sigInsertHoldColumnsCustom(); void sigRemoveHoldColumnsCustom(); void sigMirrorColumns(); void sigCutColumns(); void sigCopyColumns(); void sigPasteColumns(); private: struct Private; const QScopedPointer m_d; }; #endif // TIMELINE_RULER_HEADER_H