]> cloud.milkyroute.net Git - dolphin.git/commitdiff
Animate split view mode toggling
authorFelix Ernst <fe.a.ernst@gmail.com>
Sat, 2 Jan 2021 17:48:52 +0000 (17:48 +0000)
committerElvis Angelaccio <elvis.angelaccio@kde.org>
Sat, 2 Jan 2021 17:48:52 +0000 (17:48 +0000)
Have the secondary ViewContainer slide into/out of view when split view mode is switched on or off by the user.

This should help users understand what split view mode is about. Without the animation it might seem like the only thing the button does is creating a weird vertical line in the middle of the view or something. With the animation it should be clear that the second view is a separate entity that was added. The closing animation will help users understand which of the ViewContainers was just closed.

src/dolphinmainwindow.cpp
src/dolphintabpage.cpp
src/dolphintabpage.h
src/global.cpp
src/global.h

index 321670b58777438bd17b3b05d9263d1aef215e25..3377918cee40a53fa4849660ba2b99a5e9a41908 100644 (file)
@@ -807,7 +807,7 @@ void DolphinMainWindow::invertSelection()
 void DolphinMainWindow::toggleSplitView()
 {
     DolphinTabPage* tabPage = m_tabWidget->currentTabPage();
-    tabPage->setSplitViewEnabled(!tabPage->splitViewEnabled());
+    tabPage->setSplitViewEnabled(!tabPage->splitViewEnabled(), WithAnimation);
 
     updateViewActions();
 }
@@ -815,8 +815,8 @@ void DolphinMainWindow::toggleSplitView()
 void DolphinMainWindow::toggleSplitStash()
 {
     DolphinTabPage* tabPage = m_tabWidget->currentTabPage();
-    tabPage->setSplitViewEnabled(false);
-    tabPage->setSplitViewEnabled(true, QUrl("stash:/"));
+    tabPage->setSplitViewEnabled(false, WithAnimation);
+    tabPage->setSplitViewEnabled(true, WithAnimation, QUrl("stash:/"));
 }
 
 void DolphinMainWindow::reloadView()
@@ -2143,7 +2143,7 @@ void DolphinMainWindow::refreshViews()
         // The startup settings have been changed by the user (see bug #254947).
         // Synchronize the split-view setting with the active view:
         const bool splitView = GeneralSettings::splitView();
-        m_tabWidget->currentTabPage()->setSplitViewEnabled(splitView);
+        m_tabWidget->currentTabPage()->setSplitViewEnabled(splitView, WithAnimation);
         updateSplitAction();
         updateWindowTitle();
     }
index 138822cfd36a009c254f465d055956d93b49dbe0..a90e8e7f0c6753a0f6a1eb3148dffd3650601027 100644 (file)
@@ -1,5 +1,6 @@
 /*
  * SPDX-FileCopyrightText: 2014 Emmanuel Pescosta <emmanuelpescosta099@gmail.com>
+ * SPDX-FileCopyrightText: 2020 Felix Ernst <fe.a.ernst@gmail.com>
  *
  * SPDX-License-Identifier: GPL-2.0-or-later
  */
@@ -8,13 +9,17 @@
 
 #include "dolphin_generalsettings.h"
 #include "dolphinviewcontainer.h"
+#include "global.h"
 
+#include <QVariantAnimation>
 #include <QSplitter>
 #include <QGridLayout>
 #include <QWidgetAction>
+#include <QStyle>
 
 DolphinTabPage::DolphinTabPage(const QUrl &primaryUrl, const QUrl &secondaryUrl, QWidget* parent) :
     QWidget(parent),
+    m_expandingContainer{nullptr},
     m_primaryViewActive(true),
     m_splitViewEnabled(false),
     m_active(true)
@@ -65,12 +70,24 @@ bool DolphinTabPage::splitViewEnabled() const
     return m_splitViewEnabled;
 }
 
-void DolphinTabPage::setSplitViewEnabled(bool enabled, const QUrl &secondaryUrl)
+void DolphinTabPage::setSplitViewEnabled(bool enabled, Animated animated, const QUrl &secondaryUrl)
 {
     if (m_splitViewEnabled != enabled) {
         m_splitViewEnabled = enabled;
+        if (animated == WithAnimation && (
+            style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, this) < 1 ||
+            GlobalConfig::animationDurationFactor() <= 0.0)) {
+            animated = WithoutAnimation;
+        }
+        if (m_expandViewAnimation) {
+            m_expandViewAnimation->stop(); // deletes because of QAbstractAnimation::DeleteWhenStopped.
+            if (animated == WithoutAnimation) {
+                slotAnimationFinished();
+            }
+        }
 
         if (enabled) {
+            QList<int> splitterSizes = m_splitter->sizes();
             const QUrl& url = (secondaryUrl.isEmpty()) ? m_primaryViewContainer->url() : secondaryUrl;
             m_secondaryViewContainer = createViewContainer(url);
 
@@ -84,8 +101,15 @@ void DolphinTabPage::setSplitViewEnabled(bool enabled, const QUrl &secondaryUrl)
 
             m_splitter->addWidget(m_secondaryViewContainer);
             m_secondaryViewContainer->installEventFilter(this);
-            m_secondaryViewContainer->show();
             m_secondaryViewContainer->setActive(true);
+
+            if (animated == WithAnimation) {
+                m_secondaryViewContainer->setMinimumWidth(1);
+                splitterSizes.append(1);
+                m_splitter->setSizes(splitterSizes);
+                startExpandViewAnimation(m_secondaryViewContainer);
+            }
+            m_secondaryViewContainer->show();
         } else {
             m_navigatorsWidget->setSecondaryNavigatorVisible(false);
             m_secondaryViewContainer->disconnectUrlNavigator();
@@ -117,8 +141,18 @@ void DolphinTabPage::setSplitViewEnabled(bool enabled, const QUrl &secondaryUrl)
                 }
             }
             m_primaryViewContainer->setActive(true);
-            view->close();
-            view->deleteLater();
+
+            if (animated == WithoutAnimation) {
+                view->close();
+                view->deleteLater();
+            } else {
+                // Kill it but keep it as a zombie for the closing animation.
+                m_secondaryViewContainer = nullptr;
+                view->blockSignals(true);
+                view->view()->blockSignals(true);
+                view->setDisabled(true);
+                startExpandViewAnimation(m_primaryViewContainer);
+            }
         }
     }
 }
@@ -204,7 +238,7 @@ void DolphinTabPage::insertNavigatorsWidget(DolphinNavigatorsWidgetAction* navig
 
 void DolphinTabPage::resizeNavigators() const
 {
-    if (!m_splitViewEnabled) {
+    if (!m_secondaryViewContainer) {
         m_navigatorsWidget->followViewContainerGeometry(
                 m_primaryViewContainer->mapToGlobal(QPoint(0,0)).x(),
                 m_primaryViewContainer->width());
@@ -285,7 +319,7 @@ void DolphinTabPage::restoreState(const QByteArray& state)
 
     bool isSplitViewEnabled = false;
     stream >> isSplitViewEnabled;
-    setSplitViewEnabled(isSplitViewEnabled);
+    setSplitViewEnabled(isSplitViewEnabled, WithoutAnimation);
 
     QUrl primaryUrl;
     stream >> primaryUrl;
@@ -329,7 +363,7 @@ void DolphinTabPage::restoreStateV1(const QByteArray& state)
 
     bool isSplitViewEnabled = false;
     stream >> isSplitViewEnabled;
-    setSplitViewEnabled(isSplitViewEnabled);
+    setSplitViewEnabled(isSplitViewEnabled, WithoutAnimation);
 
     QUrl primaryUrl;
     stream >> primaryUrl;
@@ -372,6 +406,72 @@ void DolphinTabPage::setActive(bool active)
     activeViewContainer()->setActive(active);
 }
 
+void DolphinTabPage::slotAnimationFinished()
+{
+    for (int i = 0; i < m_splitter->count(); ++i) {
+        QWidget *viewContainer = m_splitter->widget(i);
+        if (viewContainer != m_primaryViewContainer &&
+            viewContainer != m_secondaryViewContainer) {
+            viewContainer->close();
+            viewContainer->deleteLater();
+        }
+    }
+    for (int i = 0; i < m_splitter->count(); ++i) {
+        QWidget *viewContainer = m_splitter->widget(i);
+        viewContainer->setMinimumWidth(viewContainer->minimumSizeHint().width());
+    }
+    m_expandingContainer = nullptr;
+}
+
+void DolphinTabPage::slotAnimationValueChanged(const QVariant& value)
+{
+    Q_CHECK_PTR(m_expandingContainer);
+    const int indexOfExpandingContainer = m_splitter->indexOf(m_expandingContainer);
+    int indexOfNonExpandingContainer = -1;
+    if (m_expandingContainer == m_primaryViewContainer) {
+        indexOfNonExpandingContainer = m_splitter->indexOf(m_secondaryViewContainer);
+    } else {
+        indexOfNonExpandingContainer = m_splitter->indexOf(m_primaryViewContainer);
+    }
+    std::vector<QWidget *> widgetsToRemove;
+    const QList<int> oldSplitterSizes = m_splitter->sizes();
+    QList<int> newSplitterSizes{oldSplitterSizes};
+    int expansionWidthNeeded = value.toInt() - oldSplitterSizes.at(indexOfExpandingContainer);
+
+    // Reduce the size of the other widgets to make space for the expandingContainer.
+    for (int i = m_splitter->count() - 1; i >= 0; --i) {
+        if (m_splitter->widget(i) == m_primaryViewContainer ||
+            m_splitter->widget(i) == m_secondaryViewContainer) {
+            continue;
+        }
+        newSplitterSizes[i] = oldSplitterSizes.at(i) - expansionWidthNeeded;
+        expansionWidthNeeded = 0;
+        if (indexOfNonExpandingContainer != -1) {
+            // Make sure every zombie container is at least slightly reduced in size
+            // so it doesn't seem like they are here to stay.
+            newSplitterSizes[i]--;
+            newSplitterSizes[indexOfNonExpandingContainer]++;
+        }
+        if (newSplitterSizes.at(i) <= 0) {
+            expansionWidthNeeded -= newSplitterSizes.at(i);
+            newSplitterSizes[i] = 0;
+            widgetsToRemove.emplace_back(m_splitter->widget(i));
+        }
+    }
+    if (expansionWidthNeeded > 1 && indexOfNonExpandingContainer != -1) {
+        Q_ASSERT(m_splitViewEnabled);
+        newSplitterSizes[indexOfNonExpandingContainer] -= expansionWidthNeeded;
+    }
+    newSplitterSizes[indexOfExpandingContainer] = value.toInt();
+    m_splitter->setSizes(newSplitterSizes);
+    while (!widgetsToRemove.empty()) {
+        widgetsToRemove.back()->close();
+        widgetsToRemove.back()->deleteLater();
+        widgetsToRemove.pop_back();
+    }
+}
+
+
 void DolphinTabPage::slotViewActivated()
 {
     const DolphinView* oldActiveView = activeViewContainer()->view();
@@ -441,3 +541,33 @@ DolphinViewContainer* DolphinTabPage::createViewContainer(const QUrl& url) const
 
     return container;
 }
+
+void DolphinTabPage::startExpandViewAnimation(DolphinViewContainer *expandingContainer)
+{
+    Q_CHECK_PTR(expandingContainer);
+    Q_ASSERT(expandingContainer == m_primaryViewContainer ||
+             expandingContainer == m_secondaryViewContainer);
+    m_expandingContainer = expandingContainer;
+
+    m_expandViewAnimation = new QVariantAnimation(m_splitter);
+    m_expandViewAnimation->setDuration(2 *
+            style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, this) *
+            GlobalConfig::animationDurationFactor());
+    for (int i = 0; i < m_splitter->count(); ++i) {
+        m_splitter->widget(i)->setMinimumWidth(1);
+    }
+    connect(m_expandViewAnimation, &QAbstractAnimation::finished,
+            this, &DolphinTabPage::slotAnimationFinished);
+    connect(m_expandViewAnimation, &QVariantAnimation::valueChanged,
+            this, &DolphinTabPage::slotAnimationValueChanged);
+
+    m_expandViewAnimation->setStartValue(expandingContainer->width());
+    if (m_splitViewEnabled) { // A new viewContainer is being opened.
+        m_expandViewAnimation->setEndValue(m_splitter->width() / 2);
+        m_expandViewAnimation->setEasingCurve(QEasingCurve::OutCubic);
+    } else { // A viewContainer is being closed.
+        m_expandViewAnimation->setEndValue(m_splitter->width());
+        m_expandViewAnimation->setEasingCurve(QEasingCurve::InCubic);
+    }
+    m_expandViewAnimation->start(QAbstractAnimation::DeleteWhenStopped);
+}
index 63a246328f074bdfa4eb78355ecd8ddd51ba1703..e90bf99bf586eaba5f22dcc8c725b6696a1b9a7b 100644 (file)
@@ -1,5 +1,6 @@
 /*
  * SPDX-FileCopyrightText: 2014 Emmanuel Pescosta <emmanuelpescosta099@gmail.com>
+ * SPDX-FileCopyrightText: 2020 Felix Ernst <fe.a.ernst@gmail.com>
  *
  * SPDX-License-Identifier: GPL-2.0-or-later
  */
 class DolphinNavigatorsWidgetAction;
 class DolphinViewContainer;
 class QSplitter;
+class QVariantAnimation;
 class KFileItemList;
 
+enum Animated {
+    WithAnimation,
+    WithoutAnimation
+};
+
 class DolphinTabPage : public QWidget
 {
     Q_OBJECT
@@ -36,9 +43,15 @@ public:
     /**
      * Enables or disables the split view mode.
      *
-     * If \a enabled is true, it creates a secondary view with the url of the primary view.
+     * @param enabled      If true, creates a secondary viewContainer in this tab.
+     *                     Otherwise deletes it.
+     * @param animated     Decides wether the effects of this method call should
+     *                     happen instantly or be transitioned to smoothly.
+     * @param secondaryUrl If \p enabled is true, the new viewContainer will be opened at this
+     *                     parameter. The default value will set the Url of the new viewContainer
+     *                     to be the same as the existing one.
      */
-    void setSplitViewEnabled(bool enabled, const QUrl &secondaryUrl = QUrl());
+    void setSplitViewEnabled(bool enabled, Animated animated, const QUrl &secondaryUrl = QUrl());
 
     /**
      * @return The primary view container.
@@ -147,6 +160,17 @@ signals:
     void splitterMoved(int pos, int index);
 
 private slots:
+    /**
+     * Deletes all zombie viewContainers that were used for the animation
+     * and resets the minimum size of the others to a sane value.
+     */
+    void slotAnimationFinished();
+
+    /**
+     * This method is called for every frame of the m_expandViewAnimation.
+     */
+    void slotAnimationValueChanged(const QVariant &value);
+
     /**
      * Handles the view activated event.
      *
@@ -170,6 +194,16 @@ private:
      */
     DolphinViewContainer* createViewContainer(const QUrl& url) const;
 
+    /**
+     * Starts an animation that transitions between split view mode states.
+     *
+     * One of the viewContainers is always being expanded when toggling so
+     * this method can animate both opening and closing of viewContainers.
+     * @param expandingContainer The container that will increase in size
+     *                           over the course of the animation.
+     */
+    void startExpandViewAnimation(DolphinViewContainer *expandingContainer);
+
 private:
     QSplitter* m_splitter;
 
@@ -177,6 +211,9 @@ private:
     QPointer<DolphinViewContainer> m_primaryViewContainer;
     QPointer<DolphinViewContainer> m_secondaryViewContainer;
 
+    DolphinViewContainer *m_expandingContainer;
+    QPointer<QVariantAnimation> m_expandViewAnimation;
+
     bool m_primaryViewActive;
     bool m_splitViewEnabled;
     bool m_active;
index 1018c7d4c496a3a5e9bce32bf9601ef5bba91aa7..3d17a733bca243110621a75069adbd2c5c5ec146 100644 (file)
@@ -10,6 +10,7 @@
 #include "dolphindebug.h"
 #include "dolphinmainwindowinterface.h"
 
+#include <KConfigWatcher>
 #include <KDialogJobUiDelegate>
 #include <KIO/ApplicationLauncherJob>
 #include <KService>
@@ -138,3 +139,29 @@ QVector<QPair<QSharedPointer<OrgKdeDolphinMainWindowInterface>, QStringList>> Do
 
     return dolphinInterfaces;
 }
+
+double GlobalConfig::animationDurationFactor()
+{
+    if (s_animationDurationFactor >= 0.0) {
+        return s_animationDurationFactor;
+    }
+    // This is the first time this method is called.
+    auto kdeGlobalsConfig = KConfigGroup(KSharedConfig::openConfig(), QStringLiteral("KDE"));
+    updateAnimationDurationFactor(kdeGlobalsConfig, {"AnimationDurationFactor"});
+
+    KConfigWatcher::Ptr configWatcher = KConfigWatcher::create(KSharedConfig::openConfig());
+    connect(configWatcher.data(), &KConfigWatcher::configChanged,
+            &GlobalConfig::updateAnimationDurationFactor);
+    return s_animationDurationFactor;
+}
+
+void GlobalConfig::updateAnimationDurationFactor(const KConfigGroup &group, const QByteArrayList &names)
+{
+    if (group.name() == QLatin1String("KDE") &&
+        names.contains(QByteArrayLiteral("AnimationDurationFactor"))) {
+        s_animationDurationFactor = std::max(0.0,
+                group.readEntry("AnimationDurationFactor", 1.0));
+    }
+}
+
+double GlobalConfig::s_animationDurationFactor = -1.0;
index 65247351a7476d1ee0d9a219251b60a3ffd42c75..088e9c5b66b20f58e2dc686f801ea3e899493171 100644 (file)
@@ -11,6 +11,7 @@
 #include <QUrl>
 #include <QWidget>
 
+class KConfigGroup;
 class OrgKdeDolphinMainWindowInterface;
 
 namespace Dolphin {
@@ -52,4 +53,26 @@ namespace Dolphin {
     const int LAYOUT_SPACING_SMALL = 2;
 }
 
+class GlobalConfig : public QObject
+{
+    Q_OBJECT
+
+public:
+    GlobalConfig() = delete;
+
+    /**
+     * @return a value from the global KDE config that should be
+     *         multiplied with every animation duration once.
+     *         0.0 is returned if animations are globally turned off.
+     *         1.0 is the default value.
+     */
+    static double animationDurationFactor();
+
+private:
+    static void updateAnimationDurationFactor(const KConfigGroup &group, const QByteArrayList &names);
+
+private:
+    static double s_animationDurationFactor;
+};
+
 #endif //GLOBAL_H