1 /*
2  * SPDX-FileCopyrightText: 2014 Emmanuel Pescosta <emmanuelpescosta099@gmail.com>
3  *
4  * SPDX-License-Identifier: GPL-2.0-or-later
5  */
6 
7 #include "dolphintabwidget.h"
8 
9 #include "dolphin_generalsettings.h"
10 #include "dolphintabbar.h"
11 #include "dolphinviewcontainer.h"
12 
13 #include <KConfigGroup>
14 #include <KShell>
15 #include <kio/global.h>
16 #include <KIO/CommandLauncherJob>
17 #include <KAcceleratorManager>
18 
19 #include <QApplication>
20 #include <QDropEvent>
21 
DolphinTabWidget(DolphinNavigatorsWidgetAction * navigatorsWidget,QWidget * parent)22 DolphinTabWidget::DolphinTabWidget(DolphinNavigatorsWidgetAction *navigatorsWidget, QWidget* parent) :
23     QTabWidget(parent),
24     m_lastViewedTab(nullptr),
25     m_navigatorsWidget{navigatorsWidget}
26 {
27     KAcceleratorManager::setNoAccel(this);
28 
29     connect(this, &DolphinTabWidget::tabCloseRequested,
30             this, QOverload<int>::of(&DolphinTabWidget::closeTab));
31     connect(this, &DolphinTabWidget::currentChanged,
32             this, &DolphinTabWidget::currentTabChanged);
33 
34     DolphinTabBar* tabBar = new DolphinTabBar(this);
35     connect(tabBar, &DolphinTabBar::openNewActivatedTab,
36             this, QOverload<int>::of(&DolphinTabWidget::openNewActivatedTab));
37     connect(tabBar, &DolphinTabBar::tabDropEvent,
38             this, &DolphinTabWidget::tabDropEvent);
39     connect(tabBar, &DolphinTabBar::tabDetachRequested,
40             this, &DolphinTabWidget::detachTab);
41     tabBar->hide();
42 
43     setTabBar(tabBar);
44     setDocumentMode(true);
45     setElideMode(Qt::ElideRight);
46     setUsesScrollButtons(true);
47 }
48 
currentTabPage() const49 DolphinTabPage* DolphinTabWidget::currentTabPage() const
50 {
51     return tabPageAt(currentIndex());
52 }
53 
nextTabPage() const54 DolphinTabPage* DolphinTabWidget::nextTabPage() const
55 {
56     const int index = currentIndex() + 1;
57     return tabPageAt(index < count() ? index : 0);
58 }
59 
prevTabPage() const60 DolphinTabPage* DolphinTabWidget::prevTabPage() const
61 {
62     const int index = currentIndex() - 1;
63     return tabPageAt(index >= 0 ? index : (count() - 1));
64 }
65 
tabPageAt(const int index) const66 DolphinTabPage* DolphinTabWidget::tabPageAt(const int index) const
67 {
68     return static_cast<DolphinTabPage*>(widget(index));
69 }
70 
saveProperties(KConfigGroup & group) const71 void DolphinTabWidget::saveProperties(KConfigGroup& group) const
72 {
73     const int tabCount = count();
74     group.writeEntry("Tab Count", tabCount);
75     group.writeEntry("Active Tab Index", currentIndex());
76 
77     for (int i = 0; i < tabCount; ++i) {
78         const DolphinTabPage* tabPage = tabPageAt(i);
79         group.writeEntry("Tab Data " % QString::number(i), tabPage->saveState());
80     }
81 }
82 
readProperties(const KConfigGroup & group)83 void DolphinTabWidget::readProperties(const KConfigGroup& group)
84 {
85     const int tabCount = group.readEntry("Tab Count", 0);
86     for (int i = 0; i < tabCount; ++i) {
87         if (i >= count()) {
88             openNewActivatedTab();
89         }
90         const QByteArray state = group.readEntry("Tab Data " % QString::number(i), QByteArray());
91         tabPageAt(i)->restoreState(state);
92     }
93 
94     const int index = group.readEntry("Active Tab Index", 0);
95     setCurrentIndex(index);
96 }
97 
refreshViews()98 void DolphinTabWidget::refreshViews()
99 {
100     // Left-elision is better when showing full paths, since you care most
101     // about the current directory which is on the right
102     if (GeneralSettings::showFullPathInTitlebar()) {
103         setElideMode(Qt::ElideLeft);
104     } else {
105         setElideMode(Qt::ElideRight);
106     }
107 
108     const int tabCount = count();
109     for (int i = 0; i < tabCount; ++i) {
110         tabBar()->setTabText(i, tabName(tabPageAt(i)));
111         tabPageAt(i)->refreshViews();
112     }
113 }
114 
isUrlOpen(const QUrl & url) const115 bool DolphinTabWidget::isUrlOpen(const QUrl &url) const
116 {
117     return indexByUrl(url).first >= 0;
118 }
119 
openNewActivatedTab()120 void DolphinTabWidget::openNewActivatedTab()
121 {
122     std::unique_ptr<DolphinUrlNavigator::VisualState> oldNavigatorState;
123     if (currentTabPage()->primaryViewActive() || !m_navigatorsWidget->secondaryUrlNavigator()) {
124         oldNavigatorState = m_navigatorsWidget->primaryUrlNavigator()->visualState();
125     } else {
126         oldNavigatorState = m_navigatorsWidget->secondaryUrlNavigator()->visualState();
127     }
128 
129     const DolphinViewContainer* oldActiveViewContainer = currentTabPage()->activeViewContainer();
130     Q_ASSERT(oldActiveViewContainer);
131 
132     openNewActivatedTab(oldActiveViewContainer->url());
133 
134     DolphinViewContainer* newActiveViewContainer = currentTabPage()->activeViewContainer();
135     Q_ASSERT(newActiveViewContainer);
136 
137     // The URL navigator of the new tab should have the same editable state
138     // as the current tab
139     newActiveViewContainer->urlNavigator()->setVisualState(*oldNavigatorState.get());
140 
141     // Always focus the new tab's view
142     newActiveViewContainer->view()->setFocus();
143 }
144 
openNewActivatedTab(const QUrl & primaryUrl,const QUrl & secondaryUrl)145 void DolphinTabWidget::openNewActivatedTab(const QUrl& primaryUrl, const QUrl& secondaryUrl)
146 {
147     openNewTab(primaryUrl, secondaryUrl);
148     if (GeneralSettings::openNewTabAfterLastTab()) {
149         setCurrentIndex(count() - 1);
150     } else {
151         setCurrentIndex(currentIndex() + 1);
152     }
153 }
154 
openNewTab(const QUrl & primaryUrl,const QUrl & secondaryUrl)155 void DolphinTabWidget::openNewTab(const QUrl& primaryUrl, const QUrl& secondaryUrl)
156 {
157     QWidget* focusWidget = QApplication::focusWidget();
158 
159     DolphinTabPage* tabPage = new DolphinTabPage(primaryUrl, secondaryUrl, this);
160     tabPage->setActive(false);
161     connect(tabPage, &DolphinTabPage::activeViewChanged,
162             this, &DolphinTabWidget::activeViewChanged);
163     connect(tabPage, &DolphinTabPage::activeViewUrlChanged,
164             this, &DolphinTabWidget::tabUrlChanged);
165     connect(tabPage->activeViewContainer(), &DolphinViewContainer::captionChanged, this, [this, tabPage]() {
166         const int tabIndex = indexOf(tabPage);
167         Q_ASSERT(tabIndex >= 0);
168         tabBar()->setTabText(tabIndex, tabName(tabPage));
169     });
170 
171     int newTabIndex = -1;
172     if (!GeneralSettings::openNewTabAfterLastTab()) {
173         newTabIndex = currentIndex() + 1;
174     }
175 
176     insertTab(newTabIndex, tabPage, QIcon() /* loaded in tabInserted */, tabName(tabPage));
177 
178     if (focusWidget) {
179         // The DolphinViewContainer grabbed the keyboard focus. As the tab is opened
180         // in background, assure that the previous focused widget gets the focus back.
181         focusWidget->setFocus();
182     }
183 }
184 
openDirectories(const QList<QUrl> & dirs,bool splitView)185 void DolphinTabWidget::openDirectories(const QList<QUrl>& dirs, bool splitView)
186 {
187     Q_ASSERT(dirs.size() > 0);
188 
189     bool somethingWasAlreadyOpen = false;
190 
191     QList<QUrl>::const_iterator it = dirs.constBegin();
192     while (it != dirs.constEnd()) {
193         const QUrl& primaryUrl = *(it++);
194         const QPair<int, bool> indexInfo = indexByUrl(primaryUrl);
195         const int index = indexInfo.first;
196         const bool isInPrimaryView = indexInfo.second;
197 
198         // When the user asks for a URL that's already open, activate it instead
199         // of opening a second copy
200         if (index >= 0) {
201             somethingWasAlreadyOpen = true;
202             activateTab(index);
203             const auto tabPage = tabPageAt(index);
204             if (isInPrimaryView) {
205                 tabPage->primaryViewContainer()->setActive(true);
206             } else {
207                 tabPage->secondaryViewContainer()->setActive(true);
208             }
209             // BUG: 417230
210             // Required for updateViewState() call in openFiles() to work as expected
211             // If there is a selection, updateViewState() calls are effectively a no-op
212             tabPage->activeViewContainer()->view()->clearSelection();
213         } else if (splitView && (it != dirs.constEnd())) {
214             const QUrl& secondaryUrl = *(it++);
215             if (somethingWasAlreadyOpen) {
216                 openNewTab(primaryUrl, secondaryUrl);
217             } else {
218                 openNewActivatedTab(primaryUrl, secondaryUrl);
219             }
220         } else {
221             if (somethingWasAlreadyOpen) {
222                 openNewTab(primaryUrl);
223             } else {
224                 openNewActivatedTab(primaryUrl);
225             }
226         }
227     }
228 }
229 
openFiles(const QList<QUrl> & files,bool splitView)230 void DolphinTabWidget::openFiles(const QList<QUrl>& files, bool splitView)
231 {
232     Q_ASSERT(files.size() > 0);
233 
234     // Get all distinct directories from 'files' and open a tab
235     // for each directory. If the "split view" option is enabled, two
236     // directories are shown inside one tab (see openDirectories()).
237     QList<QUrl> dirs;
238     for (const QUrl& url : files) {
239         const QUrl dir(url.adjusted(QUrl::RemoveFilename));
240         if (!dirs.contains(dir)) {
241             dirs.append(dir);
242         }
243     }
244 
245     const int oldTabCount = count();
246     openDirectories(dirs, splitView);
247     const int tabCount = count();
248 
249     // Select the files. Although the files can be split between several
250     // tabs, there is no need to split 'files' accordingly, as
251     // the DolphinView will just ignore invalid selections.
252     for (int i = 0; i < tabCount; ++i) {
253         DolphinTabPage* tabPage = tabPageAt(i);
254         tabPage->markUrlsAsSelected(files);
255         tabPage->markUrlAsCurrent(files.first());
256         if (i < oldTabCount) {
257             // Force selection of file if directory was already open, BUG: 417230
258             tabPage->activeViewContainer()->view()->updateViewState();
259         }
260     }
261 }
262 
closeTab()263 void DolphinTabWidget::closeTab()
264 {
265     closeTab(currentIndex());
266 }
267 
closeTab(const int index)268 void DolphinTabWidget::closeTab(const int index)
269 {
270     Q_ASSERT(index >= 0);
271     Q_ASSERT(index < count());
272 
273     if (count() < 2) {
274         // Close Dolphin when closing the last tab.
275         parentWidget()->close();
276         return;
277     }
278 
279     DolphinTabPage* tabPage = tabPageAt(index);
280     Q_EMIT rememberClosedTab(tabPage->activeViewContainer()->url(), tabPage->saveState());
281 
282     removeTab(index);
283     tabPage->deleteLater();
284 }
285 
activateTab(const int index)286 void DolphinTabWidget::activateTab(const int index)
287 {
288     if (index < count()) {
289         setCurrentIndex(index);
290     }
291 }
292 
activateLastTab()293 void DolphinTabWidget::activateLastTab()
294 {
295     setCurrentIndex(count() - 1);
296 }
297 
activateNextTab()298 void DolphinTabWidget::activateNextTab()
299 {
300     const int index = currentIndex() + 1;
301     setCurrentIndex(index < count() ? index : 0);
302 }
303 
activatePrevTab()304 void DolphinTabWidget::activatePrevTab()
305 {
306     const int index = currentIndex() - 1;
307     setCurrentIndex(index >= 0 ? index : (count() - 1));
308 }
309 
restoreClosedTab(const QByteArray & state)310 void DolphinTabWidget::restoreClosedTab(const QByteArray& state)
311 {
312     openNewActivatedTab();
313     currentTabPage()->restoreState(state);
314 }
315 
copyToInactiveSplitView()316 void DolphinTabWidget::copyToInactiveSplitView()
317 {
318     const DolphinTabPage* tabPage = tabPageAt(currentIndex());
319     DolphinViewContainer* activeViewContainer = currentTabPage()->activeViewContainer();
320     if (!tabPage->splitViewEnabled() || activeViewContainer->view()->selectedItems().isEmpty()) {
321         return;
322     }
323 
324     if (tabPage->primaryViewActive()) {
325         // copy from left panel to right
326         activeViewContainer->view()->copySelectedItems(activeViewContainer->view()->selectedItems(), tabPage->secondaryViewContainer()->url());
327     } else {
328         // copy from right panel to left
329         activeViewContainer->view()->copySelectedItems(activeViewContainer->view()->selectedItems(), tabPage->primaryViewContainer()->url());
330     }
331 }
332 
moveToInactiveSplitView()333 void DolphinTabWidget::moveToInactiveSplitView()
334 {
335     const DolphinTabPage* tabPage = tabPageAt(currentIndex());
336     DolphinViewContainer* activeViewContainer = currentTabPage()->activeViewContainer();
337     if (!tabPage->splitViewEnabled() || activeViewContainer->view()->selectedItems().isEmpty()) {
338         return;
339     }
340 
341     if (tabPage->primaryViewActive()) {
342         // move from left panel to right
343         activeViewContainer->view()->moveSelectedItems(activeViewContainer->view()->selectedItems(), tabPage->secondaryViewContainer()->url());
344     } else {
345         // move from right panel to left
346         activeViewContainer->view()->moveSelectedItems(activeViewContainer->view()->selectedItems(), tabPage->primaryViewContainer()->url());
347     }
348 }
349 
detachTab(int index)350 void DolphinTabWidget::detachTab(int index)
351 {
352     Q_ASSERT(index >= 0);
353 
354     QStringList args;
355 
356     const DolphinTabPage* tabPage = tabPageAt(index);
357     args << tabPage->primaryViewContainer()->url().url();
358     if (tabPage->splitViewEnabled()) {
359         args << tabPage->secondaryViewContainer()->url().url();
360         args << QStringLiteral("--split");
361     }
362     args << QStringLiteral("--new-window");
363 
364     KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob("dolphin", args, this);
365     job->setDesktopName(QStringLiteral("org.kde.dolphin"));
366     job->start();
367 
368     closeTab(index);
369 }
370 
openNewActivatedTab(int index)371 void DolphinTabWidget::openNewActivatedTab(int index)
372 {
373     Q_ASSERT(index >= 0);
374     const DolphinTabPage* tabPage = tabPageAt(index);
375     openNewActivatedTab(tabPage->activeViewContainer()->url());
376 }
377 
tabDropEvent(int index,QDropEvent * event)378 void DolphinTabWidget::tabDropEvent(int index, QDropEvent* event)
379 {
380     if (index >= 0) {
381         DolphinView* view = tabPageAt(index)->activeViewContainer()->view();
382         view->dropUrls(view->url(), event, view);
383     }
384 }
385 
tabUrlChanged(const QUrl & url)386 void DolphinTabWidget::tabUrlChanged(const QUrl& url)
387 {
388     const int index = indexOf(qobject_cast<QWidget*>(sender()));
389     if (index >= 0) {
390         tabBar()->setTabText(index, tabName(tabPageAt(index)));
391         tabBar()->setTabToolTip(index, url.toDisplayString(QUrl::PreferLocalFile));
392         if (tabBar()->isVisible()) {
393             // ensure the path url ends with a slash to have proper folder icon for remote folders
394             const QUrl pathUrl = QUrl(url.adjusted(QUrl::StripTrailingSlash).toString(QUrl::FullyEncoded).append("/"));
395             tabBar()->setTabIcon(index, QIcon::fromTheme(KIO::iconNameForUrl(pathUrl)));
396         } else {
397             // Mark as dirty, actually load once the tab bar actually gets shown
398             tabBar()->setTabIcon(index, QIcon());
399         }
400 
401         // Emit the currentUrlChanged signal if the url of the current tab has been changed.
402         if (index == currentIndex()) {
403             Q_EMIT currentUrlChanged(url);
404         }
405     }
406 }
407 
currentTabChanged(int index)408 void DolphinTabWidget::currentTabChanged(int index)
409 {
410     DolphinTabPage *tabPage = tabPageAt(index);
411     if (tabPage == m_lastViewedTab) {
412         return;
413     }
414     if (m_lastViewedTab) {
415         m_lastViewedTab->disconnectNavigators();
416         m_lastViewedTab->setActive(false);
417     }
418     if (tabPage->splitViewEnabled() && !m_navigatorsWidget->secondaryUrlNavigator()) {
419         m_navigatorsWidget->createSecondaryUrlNavigator();
420     }
421     DolphinViewContainer* viewContainer = tabPage->activeViewContainer();
422     Q_EMIT activeViewChanged(viewContainer);
423     Q_EMIT currentUrlChanged(viewContainer->url());
424     tabPage->setActive(true);
425     tabPage->connectNavigators(m_navigatorsWidget);
426     m_navigatorsWidget->setSecondaryNavigatorVisible(tabPage->splitViewEnabled());
427     m_lastViewedTab = tabPage;
428 }
429 
tabInserted(int index)430 void DolphinTabWidget::tabInserted(int index)
431 {
432     QTabWidget::tabInserted(index);
433 
434     if (count() > 1) {
435         // Resolve all pending tab icons
436         for (int i = 0; i < count(); ++i) {
437             const QUrl url = tabPageAt(i)->activeViewContainer()->url();
438             if (tabBar()->tabIcon(i).isNull()) {
439                 // ensure the path url ends with a slash to have proper folder icon for remote folders
440                 const QUrl pathUrl = QUrl(url.adjusted(QUrl::StripTrailingSlash).toString(QUrl::FullyEncoded).append("/"));
441                 tabBar()->setTabIcon(i, QIcon::fromTheme(KIO::iconNameForUrl(pathUrl)));
442             }
443             if (tabBar()->tabToolTip(i).isEmpty()) {
444                 tabBar()->setTabToolTip(index, url.toDisplayString(QUrl::PreferLocalFile));
445             }
446         }
447 
448         tabBar()->show();
449     }
450 
451     Q_EMIT tabCountChanged(count());
452 }
453 
tabRemoved(int index)454 void DolphinTabWidget::tabRemoved(int index)
455 {
456     QTabWidget::tabRemoved(index);
457 
458     // If only one tab is left, then remove the tab entry so that
459     // closing the last tab is not possible.
460     if (count() < 2) {
461         tabBar()->hide();
462     }
463 
464     Q_EMIT tabCountChanged(count());
465 }
466 
tabName(DolphinTabPage * tabPage) const467 QString DolphinTabWidget::tabName(DolphinTabPage* tabPage) const
468 {
469     if (!tabPage) {
470         return QString();
471     }
472     QString name = tabPage->activeViewContainer()->caption();
473     // Make sure that a '&' inside the directory name is displayed correctly
474     // and not misinterpreted as a keyboard shortcut in QTabBar::setTabText()
475     return name.replace('&', QLatin1String("&&"));
476 }
477 
indexByUrl(const QUrl & url) const478 QPair<int, bool> DolphinTabWidget::indexByUrl(const QUrl& url) const
479 {
480     for (int i = 0; i < count(); i++) {
481         const auto tabPage = tabPageAt(i);
482         if (url == tabPage->primaryViewContainer()->url()) {
483             return qMakePair(i, true);
484         }
485 
486         if (tabPage->splitViewEnabled() && url == tabPage->secondaryViewContainer()->url()) {
487             return qMakePair(i, false);
488         }
489     }
490     return qMakePair(-1, false);
491 }
492