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