]> cloud.milkyroute.net Git - dolphin.git/blob - src/dolphintabwidget.cpp
Merge branch 'master' into kf6
[dolphin.git] / src / dolphintabwidget.cpp
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 <KAcceleratorManager>
14 #include <KConfigGroup>
15 #include <KIO/CommandLauncherJob>
16 #include <KLocalizedString>
17 #include <KShell>
18 #include <kio/global.h>
19
20 #include <QApplication>
21 #include <QDropEvent>
22
23 DolphinTabWidget::DolphinTabWidget(DolphinNavigatorsWidgetAction *navigatorsWidget, QWidget *parent)
24 : QTabWidget(parent)
25 , m_lastViewedTab(nullptr)
26 , m_navigatorsWidget{navigatorsWidget}
27 {
28 KAcceleratorManager::setNoAccel(this);
29
30 connect(this, &DolphinTabWidget::tabCloseRequested, this, QOverload<int>::of(&DolphinTabWidget::closeTab));
31 connect(this, &DolphinTabWidget::currentChanged, this, &DolphinTabWidget::currentTabChanged);
32
33 DolphinTabBar *tabBar = new DolphinTabBar(this);
34 connect(tabBar, &DolphinTabBar::openNewActivatedTab, this, QOverload<int>::of(&DolphinTabWidget::openNewActivatedTab));
35 connect(tabBar, &DolphinTabBar::tabDropEvent, this, &DolphinTabWidget::tabDropEvent);
36 connect(tabBar, &DolphinTabBar::tabDetachRequested, this, &DolphinTabWidget::detachTab);
37 tabBar->hide();
38
39 setTabBar(tabBar);
40 setDocumentMode(true);
41 setElideMode(Qt::ElideRight);
42 setUsesScrollButtons(true);
43 }
44
45 DolphinTabPage *DolphinTabWidget::currentTabPage() const
46 {
47 return tabPageAt(currentIndex());
48 }
49
50 DolphinTabPage *DolphinTabWidget::nextTabPage() const
51 {
52 const int index = currentIndex() + 1;
53 return tabPageAt(index < count() ? index : 0);
54 }
55
56 DolphinTabPage *DolphinTabWidget::prevTabPage() const
57 {
58 const int index = currentIndex() - 1;
59 return tabPageAt(index >= 0 ? index : (count() - 1));
60 }
61
62 DolphinTabPage *DolphinTabWidget::tabPageAt(const int index) const
63 {
64 return static_cast<DolphinTabPage *>(widget(index));
65 }
66
67 void DolphinTabWidget::saveProperties(KConfigGroup &group) const
68 {
69 const int tabCount = count();
70 group.writeEntry("Tab Count", tabCount);
71 group.writeEntry("Active Tab Index", currentIndex());
72
73 for (int i = 0; i < tabCount; ++i) {
74 const DolphinTabPage *tabPage = tabPageAt(i);
75 group.writeEntry("Tab Data " % QString::number(i), tabPage->saveState());
76 }
77 }
78
79 void DolphinTabWidget::readProperties(const KConfigGroup &group)
80 {
81 const int tabCount = group.readEntry("Tab Count", 0);
82 for (int i = 0; i < tabCount; ++i) {
83 if (i >= count()) {
84 openNewActivatedTab();
85 }
86 const QByteArray state = group.readEntry("Tab Data " % QString::number(i), QByteArray());
87 tabPageAt(i)->restoreState(state);
88 }
89
90 const int index = group.readEntry("Active Tab Index", 0);
91 setCurrentIndex(index);
92 }
93
94 void DolphinTabWidget::refreshViews()
95 {
96 // Left-elision is better when showing full paths, since you care most
97 // about the current directory which is on the right
98 if (GeneralSettings::showFullPathInTitlebar()) {
99 setElideMode(Qt::ElideLeft);
100 } else {
101 setElideMode(Qt::ElideRight);
102 }
103
104 const int tabCount = count();
105 for (int i = 0; i < tabCount; ++i) {
106 tabBar()->setTabText(i, tabName(tabPageAt(i)));
107 tabPageAt(i)->refreshViews();
108 }
109 }
110
111 bool DolphinTabWidget::isUrlOpen(const QUrl &url) const
112 {
113 return viewOpenAtDirectory(url).has_value();
114 }
115
116 bool DolphinTabWidget::isItemVisibleInAnyView(const QUrl &urlOfItem) const
117 {
118 return viewShowingItem(urlOfItem).has_value();
119 }
120
121 void DolphinTabWidget::openNewActivatedTab()
122 {
123 std::unique_ptr<DolphinUrlNavigator::VisualState> oldNavigatorState;
124 if (currentTabPage()->primaryViewActive() || !m_navigatorsWidget->secondaryUrlNavigator()) {
125 oldNavigatorState = m_navigatorsWidget->primaryUrlNavigator()->visualState();
126 } else {
127 oldNavigatorState = m_navigatorsWidget->secondaryUrlNavigator()->visualState();
128 }
129
130 const DolphinViewContainer *oldActiveViewContainer = currentTabPage()->activeViewContainer();
131 Q_ASSERT(oldActiveViewContainer);
132
133 openNewActivatedTab(oldActiveViewContainer->url());
134
135 DolphinViewContainer *newActiveViewContainer = currentTabPage()->activeViewContainer();
136 Q_ASSERT(newActiveViewContainer);
137
138 // The URL navigator of the new tab should have the same editable state
139 // as the current tab
140 newActiveViewContainer->urlNavigator()->setVisualState(*oldNavigatorState.get());
141
142 // Always focus the new tab's view
143 newActiveViewContainer->view()->setFocus();
144 }
145
146 void DolphinTabWidget::openNewActivatedTab(const QUrl &primaryUrl, const QUrl &secondaryUrl)
147 {
148 openNewTab(primaryUrl, secondaryUrl);
149 if (GeneralSettings::openNewTabAfterLastTab()) {
150 setCurrentIndex(count() - 1);
151 } else {
152 setCurrentIndex(currentIndex() + 1);
153 }
154 }
155
156 void DolphinTabWidget::openNewTab(const QUrl &primaryUrl, const QUrl &secondaryUrl, DolphinTabWidget::NewTabPosition position)
157 {
158 QWidget *focusWidget = QApplication::focusWidget();
159
160 DolphinTabPage *tabPage = new DolphinTabPage(primaryUrl, secondaryUrl, this);
161 tabPage->setActive(false);
162 connect(tabPage, &DolphinTabPage::activeViewChanged, this, &DolphinTabWidget::activeViewChanged);
163 connect(tabPage, &DolphinTabPage::activeViewUrlChanged, this, &DolphinTabWidget::tabUrlChanged);
164 connect(tabPage->activeViewContainer(), &DolphinViewContainer::captionChanged, this, [this, tabPage]() {
165 const int tabIndex = indexOf(tabPage);
166 Q_ASSERT(tabIndex >= 0);
167 tabBar()->setTabText(tabIndex, tabName(tabPage));
168 });
169
170 if (position == NewTabPosition::FollowSetting) {
171 if (GeneralSettings::openNewTabAfterLastTab()) {
172 position = NewTabPosition::AtEnd;
173 } else {
174 position = NewTabPosition::AfterCurrent;
175 }
176 }
177
178 int newTabIndex = -1;
179 if (position == NewTabPosition::AfterCurrent || (position == NewTabPosition::FollowSetting && !GeneralSettings::openNewTabAfterLastTab())) {
180 newTabIndex = currentIndex() + 1;
181 }
182
183 insertTab(newTabIndex, tabPage, QIcon() /* loaded in tabInserted */, tabName(tabPage));
184
185 if (focusWidget) {
186 // The DolphinViewContainer grabbed the keyboard focus. As the tab is opened
187 // in background, assure that the previous focused widget gets the focus back.
188 focusWidget->setFocus();
189 }
190 }
191
192 void DolphinTabWidget::openDirectories(const QList<QUrl> &dirs, bool splitView)
193 {
194 Q_ASSERT(dirs.size() > 0);
195
196 bool somethingWasAlreadyOpen = false;
197
198 QList<QUrl>::const_iterator it = dirs.constBegin();
199 while (it != dirs.constEnd()) {
200 const QUrl &primaryUrl = *(it++);
201 const std::optional<ViewIndex> viewIndexAtDirectory = viewOpenAtDirectory(primaryUrl);
202
203 // When the user asks for a URL that's already open,
204 // activate it instead of opening a new tab
205 if (viewIndexAtDirectory.has_value()) {
206 somethingWasAlreadyOpen = true;
207 activateViewContainerAt(viewIndexAtDirectory.value());
208 } else if (splitView && (it != dirs.constEnd())) {
209 const QUrl &secondaryUrl = *(it++);
210 if (somethingWasAlreadyOpen) {
211 openNewTab(primaryUrl, secondaryUrl);
212 } else {
213 openNewActivatedTab(primaryUrl, secondaryUrl);
214 }
215 } else {
216 if (somethingWasAlreadyOpen) {
217 openNewTab(primaryUrl);
218 } else {
219 openNewActivatedTab(primaryUrl);
220 }
221 }
222 }
223 }
224
225 void DolphinTabWidget::openFiles(const QList<QUrl> &files, bool splitView)
226 {
227 Q_ASSERT(files.size() > 0);
228
229 // Get all distinct directories from 'files'.
230 QList<QUrl> dirsThatNeedToBeOpened;
231 QList<QUrl> dirsThatWereAlreadyOpen;
232 for (const QUrl &file : files) {
233 const QUrl dir(file.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
234 if (dirsThatNeedToBeOpened.contains(dir) || dirsThatWereAlreadyOpen.contains(dir)) {
235 continue;
236 }
237
238 // The selecting of files that we do later will not work in views that already have items selected.
239 // So we check if dir is already open and clear the selection if it is. BUG: 417230
240 // We also make sure the view will be activated.
241 auto viewIndex = viewShowingItem(file);
242 if (viewIndex.has_value()) {
243 viewContainerAt(viewIndex.value())->view()->clearSelection();
244 activateViewContainerAt(viewIndex.value());
245 dirsThatWereAlreadyOpen.append(dir);
246 } else {
247 dirsThatNeedToBeOpened.append(dir);
248 }
249 }
250
251 const int oldTabCount = count();
252 // Open a tab for each directory. If the "split view" option is enabled,
253 // two directories are shown inside one tab (see openDirectories()).
254 if (dirsThatNeedToBeOpened.size() > 0) {
255 openDirectories(dirsThatNeedToBeOpened, splitView);
256 }
257 const int tabCount = count();
258
259 // Select the files. Although the files can be split between several
260 // tabs, there is no need to split 'files' accordingly, as
261 // the DolphinView will just ignore invalid selections.
262 for (int i = 0; i < tabCount; ++i) {
263 DolphinTabPage *tabPage = tabPageAt(i);
264 tabPage->markUrlsAsSelected(files);
265 tabPage->markUrlAsCurrent(files.first());
266 if (i < oldTabCount) {
267 // Force selection of file if directory was already open, BUG: 417230
268 tabPage->activeViewContainer()->view()->updateViewState();
269 }
270 }
271 }
272
273 void DolphinTabWidget::closeTab()
274 {
275 closeTab(currentIndex());
276 }
277
278 void DolphinTabWidget::closeTab(const int index)
279 {
280 Q_ASSERT(index >= 0);
281 Q_ASSERT(index < count());
282
283 if (count() < 2) {
284 // Close Dolphin when closing the last tab.
285 parentWidget()->close();
286 return;
287 }
288
289 DolphinTabPage *tabPage = tabPageAt(index);
290 Q_EMIT rememberClosedTab(tabPage->activeViewContainer()->url(), tabPage->saveState());
291
292 removeTab(index);
293 tabPage->deleteLater();
294 }
295
296 void DolphinTabWidget::activateTab(const int index)
297 {
298 if (index < count()) {
299 setCurrentIndex(index);
300 }
301 }
302
303 void DolphinTabWidget::activateLastTab()
304 {
305 setCurrentIndex(count() - 1);
306 }
307
308 void DolphinTabWidget::activateNextTab()
309 {
310 const int index = currentIndex() + 1;
311 setCurrentIndex(index < count() ? index : 0);
312 }
313
314 void DolphinTabWidget::activatePrevTab()
315 {
316 const int index = currentIndex() - 1;
317 setCurrentIndex(index >= 0 ? index : (count() - 1));
318 }
319
320 void DolphinTabWidget::restoreClosedTab(const QByteArray &state)
321 {
322 openNewActivatedTab();
323 currentTabPage()->restoreState(state);
324 }
325
326 void DolphinTabWidget::copyToInactiveSplitView()
327 {
328 const DolphinTabPage *tabPage = currentTabPage();
329 if (!tabPage->splitViewEnabled()) {
330 return;
331 }
332
333 const KFileItemList selectedItems = tabPage->activeViewContainer()->view()->selectedItems();
334 if (selectedItems.isEmpty()) {
335 return;
336 }
337
338 DolphinView *const inactiveView = tabPage->inactiveViewContainer()->view();
339 inactiveView->copySelectedItems(selectedItems, inactiveView->url());
340 }
341
342 void DolphinTabWidget::moveToInactiveSplitView()
343 {
344 const DolphinTabPage *tabPage = currentTabPage();
345 if (!tabPage->splitViewEnabled()) {
346 return;
347 }
348
349 const KFileItemList selectedItems = tabPage->activeViewContainer()->view()->selectedItems();
350 if (selectedItems.isEmpty()) {
351 return;
352 }
353
354 DolphinView *const inactiveView = tabPage->inactiveViewContainer()->view();
355 inactiveView->moveSelectedItems(selectedItems, inactiveView->url());
356 }
357
358 void DolphinTabWidget::detachTab(int index)
359 {
360 Q_ASSERT(index >= 0);
361
362 QStringList args;
363
364 const DolphinTabPage *tabPage = tabPageAt(index);
365 args << tabPage->primaryViewContainer()->url().url();
366 if (tabPage->splitViewEnabled()) {
367 args << tabPage->secondaryViewContainer()->url().url();
368 args << QStringLiteral("--split");
369 }
370 args << QStringLiteral("--new-window");
371
372 KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob("dolphin", args, this);
373 job->setDesktopName(QStringLiteral("org.kde.dolphin"));
374 job->start();
375
376 closeTab(index);
377 }
378
379 void DolphinTabWidget::openNewActivatedTab(int index)
380 {
381 Q_ASSERT(index >= 0);
382 const DolphinTabPage *tabPage = tabPageAt(index);
383 openNewActivatedTab(tabPage->activeViewContainer()->url());
384 }
385
386 void DolphinTabWidget::tabDropEvent(int index, QDropEvent *event)
387 {
388 if (index >= 0) {
389 DolphinView *view = tabPageAt(index)->activeViewContainer()->view();
390 view->dropUrls(view->url(), event, view);
391 } else {
392 const auto urls = event->mimeData()->urls();
393
394 for (const QUrl &url : urls) {
395 auto *job = KIO::stat(url, KIO::StatJob::SourceSide, KIO::StatDetail::StatBasic, KIO::JobFlag::HideProgressInfo);
396 connect(job, &KJob::result, this, [this, job]() {
397 if (!job->error() && job->statResult().isDir()) {
398 openNewTab(job->url(), QUrl(), NewTabPosition::AtEnd);
399 }
400 });
401 }
402 }
403 }
404
405 void DolphinTabWidget::tabUrlChanged(const QUrl &url)
406 {
407 const int index = indexOf(qobject_cast<QWidget *>(sender()));
408 if (index >= 0) {
409 tabBar()->setTabText(index, tabName(tabPageAt(index)));
410 tabBar()->setTabToolTip(index, url.toDisplayString(QUrl::PreferLocalFile));
411 if (tabBar()->isVisible()) {
412 // ensure the path url ends with a slash to have proper folder icon for remote folders
413 const QUrl pathUrl = QUrl(url.adjusted(QUrl::StripTrailingSlash).toString(QUrl::FullyEncoded).append("/"));
414 tabBar()->setTabIcon(index, QIcon::fromTheme(KIO::iconNameForUrl(pathUrl)));
415 } else {
416 // Mark as dirty, actually load once the tab bar actually gets shown
417 tabBar()->setTabIcon(index, QIcon());
418 }
419
420 // Emit the currentUrlChanged signal if the url of the current tab has been changed.
421 if (index == currentIndex()) {
422 Q_EMIT currentUrlChanged(url);
423 }
424 }
425 }
426
427 void DolphinTabWidget::currentTabChanged(int index)
428 {
429 DolphinTabPage *tabPage = tabPageAt(index);
430 if (tabPage == m_lastViewedTab) {
431 return;
432 }
433 if (m_lastViewedTab) {
434 m_lastViewedTab->disconnectNavigators();
435 m_lastViewedTab->setActive(false);
436 }
437 if (tabPage->splitViewEnabled() && !m_navigatorsWidget->secondaryUrlNavigator()) {
438 m_navigatorsWidget->createSecondaryUrlNavigator();
439 }
440 DolphinViewContainer *viewContainer = tabPage->activeViewContainer();
441 Q_EMIT activeViewChanged(viewContainer);
442 Q_EMIT currentUrlChanged(viewContainer->url());
443 tabPage->setActive(true);
444 tabPage->connectNavigators(m_navigatorsWidget);
445 m_navigatorsWidget->setSecondaryNavigatorVisible(tabPage->splitViewEnabled());
446 m_lastViewedTab = tabPage;
447 }
448
449 void DolphinTabWidget::tabInserted(int index)
450 {
451 QTabWidget::tabInserted(index);
452
453 if (count() > 1) {
454 // Resolve all pending tab icons
455 for (int i = 0; i < count(); ++i) {
456 const QUrl url = tabPageAt(i)->activeViewContainer()->url();
457 if (tabBar()->tabIcon(i).isNull()) {
458 // ensure the path url ends with a slash to have proper folder icon for remote folders
459 const QUrl pathUrl = QUrl(url.adjusted(QUrl::StripTrailingSlash).toString(QUrl::FullyEncoded).append("/"));
460 tabBar()->setTabIcon(i, QIcon::fromTheme(KIO::iconNameForUrl(pathUrl)));
461 }
462 if (tabBar()->tabToolTip(i).isEmpty()) {
463 tabBar()->setTabToolTip(index, url.toDisplayString(QUrl::PreferLocalFile));
464 }
465 }
466
467 tabBar()->show();
468 }
469
470 Q_EMIT tabCountChanged(count());
471 }
472
473 void DolphinTabWidget::tabRemoved(int index)
474 {
475 QTabWidget::tabRemoved(index);
476
477 // If only one tab is left, then remove the tab entry so that
478 // closing the last tab is not possible.
479 if (count() < 2) {
480 tabBar()->hide();
481 }
482
483 Q_EMIT tabCountChanged(count());
484 }
485
486 QString DolphinTabWidget::tabName(DolphinTabPage *tabPage) const
487 {
488 if (!tabPage) {
489 return QString();
490 }
491 // clang-format off
492 QString name;
493 if (tabPage->splitViewEnabled()) {
494 if (tabPage->primaryViewActive()) {
495 // i18n: %1 is the primary view and %2 the secondary view. For left to right languages the primary view is on the left so we also want it to be on the
496 // left in the tab name. In right to left languages the primary view would be on the right so the tab name should match.
497 name = i18nc("@title:tab Active primary view | (Inactive secondary view)", "%1 | (%2)", tabPage->primaryViewContainer()->caption(), tabPage->secondaryViewContainer()->caption());
498 } else {
499 // i18n: %1 is the primary view and %2 the secondary view. For left to right languages the primary view is on the left so we also want it to be on the
500 // left in the tab name. In right to left languages the primary view would be on the right so the tab name should match.
501 name = i18nc("@title:tab (Inactive primary view) | Active secondary view", "(%1) | %2", tabPage->primaryViewContainer()->caption(), tabPage->secondaryViewContainer()->caption());
502 }
503 } else {
504 name = tabPage->activeViewContainer()->caption();
505 }
506 // clang-format on
507
508 // Make sure that a '&' inside the directory name is displayed correctly
509 // and not misinterpreted as a keyboard shortcut in QTabBar::setTabText()
510 return name.replace('&', QLatin1String("&&"));
511 }
512
513 DolphinViewContainer *DolphinTabWidget::viewContainerAt(DolphinTabWidget::ViewIndex viewIndex) const
514 {
515 const auto tabPage = tabPageAt(viewIndex.tabIndex);
516 if (!tabPage) {
517 return nullptr;
518 }
519 return viewIndex.isInPrimaryView ? tabPage->primaryViewContainer() : tabPage->secondaryViewContainer();
520 }
521
522 DolphinViewContainer *DolphinTabWidget::activateViewContainerAt(DolphinTabWidget::ViewIndex viewIndex)
523 {
524 activateTab(viewIndex.tabIndex);
525 auto viewContainer = viewContainerAt(viewIndex);
526 if (!viewContainer) {
527 return nullptr;
528 }
529 viewContainer->setActive(true);
530 return viewContainer;
531 }
532
533 const std::optional<const DolphinTabWidget::ViewIndex> DolphinTabWidget::viewOpenAtDirectory(const QUrl &directory) const
534 {
535 int i = currentIndex();
536 if (i < 0) {
537 return std::nullopt;
538 }
539 // loop over the tabs starting from the current one
540 do {
541 const auto tabPage = tabPageAt(i);
542 if (tabPage->primaryViewContainer()->url() == directory) {
543 return std::optional(ViewIndex{i, true});
544 }
545
546 if (tabPage->splitViewEnabled() && tabPage->secondaryViewContainer()->url() == directory) {
547 return std::optional(ViewIndex{i, false});
548 }
549
550 i = (i + 1) % count();
551 } while (i != currentIndex());
552
553 return std::nullopt;
554 }
555
556 const std::optional<const DolphinTabWidget::ViewIndex> DolphinTabWidget::viewShowingItem(const QUrl &item) const
557 {
558 // The item might not be loaded yet even though it exists. So instead
559 // we check if the folder containing the item is showing its contents.
560 const QUrl dirContainingItem(item.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
561
562 // The dirContainingItem is either open directly or expanded in a tree-style view mode.
563 // Is dirContainingitem the base url of a view?
564 auto viewOpenAtContainingDirectory = viewOpenAtDirectory(dirContainingItem);
565 if (viewOpenAtContainingDirectory.has_value()) {
566 return viewOpenAtContainingDirectory;
567 }
568
569 // Is dirContainingItem expanded in some tree-style view?
570 // The rest of this method is about figuring this out.
571
572 int i = currentIndex();
573 if (i < 0) {
574 return std::nullopt;
575 }
576 // loop over the tabs starting from the current one
577 do {
578 const auto tabPage = tabPageAt(i);
579 if (tabPage->primaryViewContainer()->url().isParentOf(item)) {
580 const KFileItem fileItemContainingItem = tabPage->primaryViewContainer()->view()->items().findByUrl(dirContainingItem);
581 if (!fileItemContainingItem.isNull() && tabPage->primaryViewContainer()->view()->isExpanded(fileItemContainingItem)) {
582 return std::optional(ViewIndex{i, true});
583 }
584 }
585
586 if (tabPage->splitViewEnabled() && tabPage->secondaryViewContainer()->url().isParentOf(item)) {
587 const KFileItem fileItemContainingItem = tabPage->secondaryViewContainer()->view()->items().findByUrl(dirContainingItem);
588 if (!fileItemContainingItem.isNull() && tabPage->secondaryViewContainer()->view()->isExpanded(fileItemContainingItem)) {
589 return std::optional(ViewIndex{i, false});
590 }
591 }
592
593 i = (i + 1) % count();
594 } while (i != currentIndex());
595
596 return std::nullopt;
597 }
598
599 #include "moc_dolphintabwidget.cpp"