1 /*
2  * SPDX-FileCopyrightText: 2014 Emmanuel Pescosta <emmanuelpescosta099@gmail.com>
3  * SPDX-FileCopyrightText: 2020 Felix Ernst <fe.a.ernst@gmail.com>
4  *
5  * SPDX-License-Identifier: GPL-2.0-or-later
6  */
7 
8 #include "dolphintabpage.h"
9 
10 #include "dolphin_generalsettings.h"
11 #include "dolphinviewcontainer.h"
12 #include "global.h"
13 
14 #include <QVariantAnimation>
15 #include <QGridLayout>
16 #include <QWidgetAction>
17 #include <QStyle>
18 
DolphinTabPage(const QUrl & primaryUrl,const QUrl & secondaryUrl,QWidget * parent)19 DolphinTabPage::DolphinTabPage(const QUrl &primaryUrl, const QUrl &secondaryUrl, QWidget* parent) :
20     QWidget(parent),
21     m_expandingContainer{nullptr},
22     m_primaryViewActive(true),
23     m_splitViewEnabled(false),
24     m_active(true)
25 {
26     QGridLayout *layout = new QGridLayout(this);
27     layout->setSpacing(0);
28     layout->setContentsMargins(0, 0, 0, 0);
29 
30     m_splitter = new DolphinTabPageSplitter(Qt::Horizontal, this);
31     m_splitter->setChildrenCollapsible(false);
32     connect(m_splitter, &QSplitter::splitterMoved,
33             this, &DolphinTabPage::splitterMoved);
34     layout->addWidget(m_splitter, 1, 0);
35     layout->setRowStretch(1, 1);
36 
37     // Create a new primary view
38     m_primaryViewContainer = createViewContainer(primaryUrl);
39     connect(m_primaryViewContainer->view(), &DolphinView::urlChanged,
40             this, &DolphinTabPage::activeViewUrlChanged);
41     connect(m_primaryViewContainer->view(), &DolphinView::redirection,
42             this, &DolphinTabPage::slotViewUrlRedirection);
43 
44     m_splitter->addWidget(m_primaryViewContainer);
45     m_primaryViewContainer->show();
46 
47     if (secondaryUrl.isValid() || GeneralSettings::splitView()) {
48         // Provide a secondary view, if the given secondary url is valid or if the
49         // startup settings are set this way (use the url of the primary view).
50         m_splitViewEnabled = true;
51         const QUrl& url = secondaryUrl.isValid() ? secondaryUrl : primaryUrl;
52         m_secondaryViewContainer = createViewContainer(url);
53         m_splitter->addWidget(m_secondaryViewContainer);
54         m_secondaryViewContainer->show();
55     }
56 
57     m_primaryViewContainer->setActive(true);
58 }
59 
primaryViewActive() const60 bool DolphinTabPage::primaryViewActive() const
61 {
62     return m_primaryViewActive;
63 }
64 
splitViewEnabled() const65 bool DolphinTabPage::splitViewEnabled() const
66 {
67     return m_splitViewEnabled;
68 }
69 
setSplitViewEnabled(bool enabled,Animated animated,const QUrl & secondaryUrl)70 void DolphinTabPage::setSplitViewEnabled(bool enabled, Animated animated, const QUrl &secondaryUrl)
71 {
72     if (m_splitViewEnabled != enabled) {
73         m_splitViewEnabled = enabled;
74         if (animated == WithAnimation && (
75             style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, this) < 1 ||
76             GlobalConfig::animationDurationFactor() <= 0.0)) {
77             animated = WithoutAnimation;
78         }
79         if (m_expandViewAnimation) {
80             m_expandViewAnimation->stop(); // deletes because of QAbstractAnimation::DeleteWhenStopped.
81             if (animated == WithoutAnimation) {
82                 slotAnimationFinished();
83             }
84         }
85 
86         if (enabled) {
87             QList<int> splitterSizes = m_splitter->sizes();
88             const QUrl& url = (secondaryUrl.isEmpty()) ? m_primaryViewContainer->url() : secondaryUrl;
89             m_secondaryViewContainer = createViewContainer(url);
90 
91             auto secondaryNavigator = m_navigatorsWidget->secondaryUrlNavigator();
92             if (!secondaryNavigator) {
93                 m_navigatorsWidget->createSecondaryUrlNavigator();
94                 secondaryNavigator = m_navigatorsWidget->secondaryUrlNavigator();
95             }
96             m_secondaryViewContainer->connectUrlNavigator(secondaryNavigator);
97             m_navigatorsWidget->setSecondaryNavigatorVisible(true);
98             m_navigatorsWidget->followViewContainersGeometry(m_primaryViewContainer,
99                                                              m_secondaryViewContainer);
100 
101             m_splitter->addWidget(m_secondaryViewContainer);
102             m_secondaryViewContainer->setActive(true);
103 
104             if (animated == WithAnimation) {
105                 m_secondaryViewContainer->setMinimumWidth(1);
106                 splitterSizes.append(1);
107                 m_splitter->setSizes(splitterSizes);
108                 startExpandViewAnimation(m_secondaryViewContainer);
109             }
110             m_secondaryViewContainer->show();
111         } else {
112             m_navigatorsWidget->setSecondaryNavigatorVisible(false);
113             m_secondaryViewContainer->disconnectUrlNavigator();
114 
115             DolphinViewContainer* view;
116             if (GeneralSettings::closeActiveSplitView()) {
117                 view = activeViewContainer();
118                 if (m_primaryViewActive) {
119                     m_primaryViewContainer->disconnectUrlNavigator();
120                     m_secondaryViewContainer->connectUrlNavigator(
121                             m_navigatorsWidget->primaryUrlNavigator());
122 
123                     // If the primary view is active, we have to swap the pointers
124                     // because the secondary view will be the new primary view.
125                     qSwap(m_primaryViewContainer, m_secondaryViewContainer);
126                     m_primaryViewActive = false;
127                 }
128             } else {
129                 view = m_primaryViewActive ? m_secondaryViewContainer : m_primaryViewContainer;
130                 if (!m_primaryViewActive) {
131                     m_primaryViewContainer->disconnectUrlNavigator();
132                     m_secondaryViewContainer->connectUrlNavigator(
133                             m_navigatorsWidget->primaryUrlNavigator());
134 
135                     // If the secondary view is active, we have to swap the pointers
136                     // because the secondary view will be the new primary view.
137                     qSwap(m_primaryViewContainer, m_secondaryViewContainer);
138                     m_primaryViewActive = true;
139                 }
140             }
141             m_primaryViewContainer->setActive(true);
142             m_navigatorsWidget->followViewContainersGeometry(m_primaryViewContainer, nullptr);
143 
144             if (animated == WithoutAnimation) {
145                 view->close();
146                 view->deleteLater();
147             } else {
148                 // Kill it but keep it as a zombie for the closing animation.
149                 m_secondaryViewContainer = nullptr;
150                 view->blockSignals(true);
151                 view->view()->blockSignals(true);
152                 view->setDisabled(true);
153                 startExpandViewAnimation(m_primaryViewContainer);
154             }
155         }
156     }
157 }
158 
primaryViewContainer() const159 DolphinViewContainer* DolphinTabPage::primaryViewContainer() const
160 {
161     return m_primaryViewContainer;
162 }
163 
secondaryViewContainer() const164 DolphinViewContainer* DolphinTabPage::secondaryViewContainer() const
165 {
166     return m_secondaryViewContainer;
167 }
168 
activeViewContainer() const169 DolphinViewContainer* DolphinTabPage::activeViewContainer() const
170 {
171     return m_primaryViewActive ? m_primaryViewContainer :
172                                  m_secondaryViewContainer;
173 }
174 
selectedItems() const175 KFileItemList DolphinTabPage::selectedItems() const
176 {
177     KFileItemList items = m_primaryViewContainer->view()->selectedItems();
178     if (m_splitViewEnabled) {
179         items += m_secondaryViewContainer->view()->selectedItems();
180     }
181     return items;
182 }
183 
selectedItemsCount() const184 int DolphinTabPage::selectedItemsCount() const
185 {
186     int selectedItemsCount = m_primaryViewContainer->view()->selectedItemsCount();
187     if (m_splitViewEnabled) {
188         selectedItemsCount += m_secondaryViewContainer->view()->selectedItemsCount();
189     }
190     return selectedItemsCount;
191 }
192 
connectNavigators(DolphinNavigatorsWidgetAction * navigatorsWidget)193 void DolphinTabPage::connectNavigators(DolphinNavigatorsWidgetAction *navigatorsWidget)
194 {
195     insertNavigatorsWidget(navigatorsWidget);
196     m_navigatorsWidget = navigatorsWidget;
197     auto primaryNavigator = navigatorsWidget->primaryUrlNavigator();
198     m_primaryViewContainer->connectUrlNavigator(primaryNavigator);
199     if (m_splitViewEnabled) {
200         auto secondaryNavigator = navigatorsWidget->secondaryUrlNavigator();
201         m_secondaryViewContainer->connectUrlNavigator(secondaryNavigator);
202     }
203     m_navigatorsWidget->followViewContainersGeometry(m_primaryViewContainer,
204                                                      m_secondaryViewContainer);
205 }
206 
disconnectNavigators()207 void DolphinTabPage::disconnectNavigators()
208 {
209     m_navigatorsWidget = nullptr;
210     m_primaryViewContainer->disconnectUrlNavigator();
211     if (m_splitViewEnabled) {
212         m_secondaryViewContainer->disconnectUrlNavigator();
213     }
214 }
215 
insertNavigatorsWidget(DolphinNavigatorsWidgetAction * navigatorsWidget)216 void DolphinTabPage::insertNavigatorsWidget(DolphinNavigatorsWidgetAction* navigatorsWidget)
217 {
218     QGridLayout *gridLayout = static_cast<QGridLayout *>(layout());
219     if (navigatorsWidget->isInToolbar()) {
220         gridLayout->setRowMinimumHeight(0, 0);
221     } else {
222         // We set a row minimum height, so the height does not visibly change whenever
223         // navigatorsWidget is inserted which happens every time the current tab is changed.
224         gridLayout->setRowMinimumHeight(0, navigatorsWidget->primaryUrlNavigator()->height());
225         gridLayout->addWidget(navigatorsWidget->requestWidget(this), 0, 0);
226     }
227 }
228 
markUrlsAsSelected(const QList<QUrl> & urls)229 void DolphinTabPage::markUrlsAsSelected(const QList<QUrl>& urls)
230 {
231     m_primaryViewContainer->view()->markUrlsAsSelected(urls);
232     if (m_splitViewEnabled) {
233         m_secondaryViewContainer->view()->markUrlsAsSelected(urls);
234     }
235 }
236 
markUrlAsCurrent(const QUrl & url)237 void DolphinTabPage::markUrlAsCurrent(const QUrl& url)
238 {
239     m_primaryViewContainer->view()->markUrlAsCurrent(url);
240     if (m_splitViewEnabled) {
241         m_secondaryViewContainer->view()->markUrlAsCurrent(url);
242     }
243 }
244 
refreshViews()245 void DolphinTabPage::refreshViews()
246 {
247     m_primaryViewContainer->readSettings();
248     if (m_splitViewEnabled) {
249         m_secondaryViewContainer->readSettings();
250     }
251 }
252 
saveState() const253 QByteArray DolphinTabPage::saveState() const
254 {
255     QByteArray state;
256     QDataStream stream(&state, QIODevice::WriteOnly);
257 
258     stream << quint32(2); // Tab state version
259 
260     stream << m_splitViewEnabled;
261 
262     stream << m_primaryViewContainer->url();
263     stream << m_primaryViewContainer->urlNavigatorInternalWithHistory()->isUrlEditable();
264     m_primaryViewContainer->view()->saveState(stream);
265 
266     if (m_splitViewEnabled) {
267         stream << m_secondaryViewContainer->url();
268         stream << m_secondaryViewContainer->urlNavigatorInternalWithHistory()->isUrlEditable();
269         m_secondaryViewContainer->view()->saveState(stream);
270     }
271 
272     stream << m_primaryViewActive;
273     stream << m_splitter->saveState();
274 
275     return state;
276 }
277 
restoreState(const QByteArray & state)278 void DolphinTabPage::restoreState(const QByteArray& state)
279 {
280     if (state.isEmpty()) {
281         return;
282     }
283 
284     QByteArray sd = state;
285     QDataStream stream(&sd, QIODevice::ReadOnly);
286 
287     // Read the version number of the tab state and check if the version is supported.
288     quint32 version = 0;
289     stream >> version;
290     if (version != 2) {
291         // The version of the tab state isn't supported, we can't restore it.
292         return;
293     }
294 
295     bool isSplitViewEnabled = false;
296     stream >> isSplitViewEnabled;
297     setSplitViewEnabled(isSplitViewEnabled, WithoutAnimation);
298 
299     QUrl primaryUrl;
300     stream >> primaryUrl;
301     m_primaryViewContainer->setUrl(primaryUrl);
302     bool primaryUrlEditable;
303     stream >> primaryUrlEditable;
304     m_primaryViewContainer->urlNavigatorInternalWithHistory()->setUrlEditable(primaryUrlEditable);
305     m_primaryViewContainer->view()->restoreState(stream);
306 
307     if (isSplitViewEnabled) {
308         QUrl secondaryUrl;
309         stream >> secondaryUrl;
310         m_secondaryViewContainer->setUrl(secondaryUrl);
311         bool secondaryUrlEditable;
312         stream >> secondaryUrlEditable;
313         m_secondaryViewContainer->urlNavigatorInternalWithHistory()->setUrlEditable(secondaryUrlEditable);
314         m_secondaryViewContainer->view()->restoreState(stream);
315     }
316 
317     stream >> m_primaryViewActive;
318     if (m_primaryViewActive) {
319         m_primaryViewContainer->setActive(true);
320     } else {
321         Q_ASSERT(m_splitViewEnabled);
322         m_secondaryViewContainer->setActive(true);
323     }
324 
325     QByteArray splitterState;
326     stream >> splitterState;
327     m_splitter->restoreState(splitterState);
328 }
329 
setActive(bool active)330 void DolphinTabPage::setActive(bool active)
331 {
332     if (active) {
333         m_active = active;
334     } else {
335         // we should bypass changing active view in split mode
336         m_active = !m_splitViewEnabled;
337     }
338     // we want view to fire activated when goes from false to true
339     activeViewContainer()->setActive(active);
340 }
341 
slotAnimationFinished()342 void DolphinTabPage::slotAnimationFinished()
343 {
344     for (int i = 0; i < m_splitter->count(); ++i) {
345         QWidget *viewContainer = m_splitter->widget(i);
346         if (viewContainer != m_primaryViewContainer &&
347             viewContainer != m_secondaryViewContainer) {
348             viewContainer->close();
349             viewContainer->deleteLater();
350         }
351     }
352     for (int i = 0; i < m_splitter->count(); ++i) {
353         QWidget *viewContainer = m_splitter->widget(i);
354         viewContainer->setMinimumWidth(viewContainer->minimumSizeHint().width());
355     }
356     m_expandingContainer = nullptr;
357 }
358 
slotAnimationValueChanged(const QVariant & value)359 void DolphinTabPage::slotAnimationValueChanged(const QVariant& value)
360 {
361     Q_CHECK_PTR(m_expandingContainer);
362     const int indexOfExpandingContainer = m_splitter->indexOf(m_expandingContainer);
363     int indexOfNonExpandingContainer = -1;
364     if (m_expandingContainer == m_primaryViewContainer) {
365         indexOfNonExpandingContainer = m_splitter->indexOf(m_secondaryViewContainer);
366     } else {
367         indexOfNonExpandingContainer = m_splitter->indexOf(m_primaryViewContainer);
368     }
369     std::vector<QWidget *> widgetsToRemove;
370     const QList<int> oldSplitterSizes = m_splitter->sizes();
371     QList<int> newSplitterSizes{oldSplitterSizes};
372     int expansionWidthNeeded = value.toInt() - oldSplitterSizes.at(indexOfExpandingContainer);
373 
374     // Reduce the size of the other widgets to make space for the expandingContainer.
375     for (int i = m_splitter->count() - 1; i >= 0; --i) {
376         if (m_splitter->widget(i) == m_primaryViewContainer ||
377             m_splitter->widget(i) == m_secondaryViewContainer) {
378             continue;
379         }
380         newSplitterSizes[i] = oldSplitterSizes.at(i) - expansionWidthNeeded;
381         expansionWidthNeeded = 0;
382         if (indexOfNonExpandingContainer != -1) {
383             // Make sure every zombie container is at least slightly reduced in size
384             // so it doesn't seem like they are here to stay.
385             newSplitterSizes[i]--;
386             newSplitterSizes[indexOfNonExpandingContainer]++;
387         }
388         if (newSplitterSizes.at(i) <= 0) {
389             expansionWidthNeeded -= newSplitterSizes.at(i);
390             newSplitterSizes[i] = 0;
391             widgetsToRemove.emplace_back(m_splitter->widget(i));
392         }
393     }
394     if (expansionWidthNeeded > 1 && indexOfNonExpandingContainer != -1) {
395         Q_ASSERT(m_splitViewEnabled);
396         newSplitterSizes[indexOfNonExpandingContainer] -= expansionWidthNeeded;
397     }
398     newSplitterSizes[indexOfExpandingContainer] = value.toInt();
399     m_splitter->setSizes(newSplitterSizes);
400     while (!widgetsToRemove.empty()) {
401         widgetsToRemove.back()->close();
402         widgetsToRemove.back()->deleteLater();
403         widgetsToRemove.pop_back();
404     }
405 }
406 
407 
slotViewActivated()408 void DolphinTabPage::slotViewActivated()
409 {
410     const DolphinView* oldActiveView = activeViewContainer()->view();
411 
412     // Set the view, which was active before, to inactive
413     // and update the active view type, if tab is active
414     if (m_active) {
415         if (m_splitViewEnabled) {
416             activeViewContainer()->setActive(false);
417             m_primaryViewActive = !m_primaryViewActive;
418         } else {
419             m_primaryViewActive = true;
420             if (m_secondaryViewContainer) {
421                 m_secondaryViewContainer->setActive(false);
422             }
423         }
424     }
425 
426     const DolphinView* newActiveView = activeViewContainer()->view();
427 
428     if (newActiveView == oldActiveView) {
429         return;
430     }
431 
432     disconnect(oldActiveView, &DolphinView::urlChanged,
433                this, &DolphinTabPage::activeViewUrlChanged);
434     disconnect(oldActiveView, &DolphinView::redirection,
435                this, &DolphinTabPage::slotViewUrlRedirection);
436     connect(newActiveView, &DolphinView::urlChanged,
437             this, &DolphinTabPage::activeViewUrlChanged);
438     connect(newActiveView, &DolphinView::redirection,
439             this, &DolphinTabPage::slotViewUrlRedirection);
440     Q_EMIT activeViewChanged(activeViewContainer());
441     Q_EMIT activeViewUrlChanged(activeViewContainer()->url());
442 }
443 
slotViewUrlRedirection(const QUrl & oldUrl,const QUrl & newUrl)444 void DolphinTabPage::slotViewUrlRedirection(const QUrl& oldUrl, const QUrl& newUrl)
445 {
446     Q_UNUSED(oldUrl)
447 
448     Q_EMIT activeViewUrlChanged(newUrl);
449 }
450 
switchActiveView()451 void DolphinTabPage::switchActiveView()
452 {
453     if (!m_splitViewEnabled) {
454         return;
455     }
456     if (m_primaryViewActive) {
457        m_secondaryViewContainer->setActive(true);
458     } else {
459        m_primaryViewContainer->setActive(true);
460     }
461 }
462 
createViewContainer(const QUrl & url) const463 DolphinViewContainer* DolphinTabPage::createViewContainer(const QUrl& url) const
464 {
465     DolphinViewContainer* container = new DolphinViewContainer(url, m_splitter);
466     container->setActive(false);
467 
468     const DolphinView* view = container->view();
469     connect(view, &DolphinView::activated,
470             this, &DolphinTabPage::slotViewActivated);
471 
472     connect(view, &DolphinView::toggleActiveViewRequested,
473             this, &DolphinTabPage::switchActiveView);
474 
475     return container;
476 }
477 
startExpandViewAnimation(DolphinViewContainer * expandingContainer)478 void DolphinTabPage::startExpandViewAnimation(DolphinViewContainer *expandingContainer)
479 {
480     Q_CHECK_PTR(expandingContainer);
481     Q_ASSERT(expandingContainer == m_primaryViewContainer ||
482              expandingContainer == m_secondaryViewContainer);
483     m_expandingContainer = expandingContainer;
484 
485     m_expandViewAnimation = new QVariantAnimation(m_splitter);
486     m_expandViewAnimation->setDuration(2 *
487             style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, this) *
488             GlobalConfig::animationDurationFactor());
489     for (int i = 0; i < m_splitter->count(); ++i) {
490         m_splitter->widget(i)->setMinimumWidth(1);
491     }
492     connect(m_expandViewAnimation, &QAbstractAnimation::finished,
493             this, &DolphinTabPage::slotAnimationFinished);
494     connect(m_expandViewAnimation, &QVariantAnimation::valueChanged,
495             this, &DolphinTabPage::slotAnimationValueChanged);
496 
497     m_expandViewAnimation->setStartValue(expandingContainer->width());
498     if (m_splitViewEnabled) { // A new viewContainer is being opened.
499         m_expandViewAnimation->setEndValue(m_splitter->width() / 2);
500         m_expandViewAnimation->setEasingCurve(QEasingCurve::OutCubic);
501     } else { // A viewContainer is being closed.
502         m_expandViewAnimation->setEndValue(m_splitter->width());
503         m_expandViewAnimation->setEasingCurve(QEasingCurve::InCubic);
504     }
505     m_expandViewAnimation->start(QAbstractAnimation::DeleteWhenStopped);
506 }
507 
DolphinTabPageSplitterHandle(Qt::Orientation orientation,QSplitter * parent)508 DolphinTabPageSplitterHandle::DolphinTabPageSplitterHandle(Qt::Orientation orientation, QSplitter *parent)
509     : QSplitterHandle(orientation, parent)
510     , m_mouseReleaseWasReceived(false)
511 {}
512 
event(QEvent * event)513 bool DolphinTabPageSplitterHandle::event(QEvent *event)
514 {
515     switch (event->type()) {
516     case QEvent::MouseButtonPress:
517         m_mouseReleaseWasReceived = false;
518         break;
519     case QEvent::MouseButtonRelease:
520         if (m_mouseReleaseWasReceived) {
521             resetSplitterSizes();
522         }
523         m_mouseReleaseWasReceived = !m_mouseReleaseWasReceived;
524         break;
525     case QEvent::MouseButtonDblClick:
526         m_mouseReleaseWasReceived = false;
527         resetSplitterSizes();
528         break;
529     default:
530         break;
531     }
532 
533     return QSplitterHandle::event(event);
534 }
535 
resetSplitterSizes()536 void DolphinTabPageSplitterHandle::resetSplitterSizes()
537 {
538     QList<int> splitterSizes = splitter()->sizes();
539     std::fill(splitterSizes.begin(), splitterSizes.end(), 0);
540     splitter()->setSizes(splitterSizes);
541 }
542 
DolphinTabPageSplitter(Qt::Orientation orientation,QWidget * parent)543 DolphinTabPageSplitter::DolphinTabPageSplitter(Qt::Orientation orientation, QWidget *parent)
544     : QSplitter(orientation, parent)
545 {}
546 
createHandle()547 QSplitterHandle* DolphinTabPageSplitter::createHandle()
548 {
549     return new DolphinTabPageSplitterHandle(orientation(), this);
550 }
551