]> cloud.milkyroute.net Git - dolphin.git/blob - src/dolphintabwidget.cpp
Improve copying and moving items between panels
[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 *inactiveView;
339 if (tabPage->primaryViewActive()) {
340 inactiveView = tabPage->secondaryViewContainer()->view();
341 } else {
342 inactiveView = tabPage->primaryViewContainer()->view();
343 }
344 inactiveView->copySelectedItems(selectedItems, inactiveView->url());
345 }
346
347 void DolphinTabWidget::moveToInactiveSplitView()
348 {
349 const DolphinTabPage *tabPage = currentTabPage();
350 if (!tabPage->splitViewEnabled()) {
351 return;
352 }
353
354 const KFileItemList selectedItems = tabPage->activeViewContainer()->view()->selectedItems();
355 if (selectedItems.isEmpty()) {
356 return;
357 }
358
359 DolphinView *inactiveView;
360 if (tabPage->primaryViewActive()) {
361 inactiveView = tabPage->secondaryViewContainer()->view();
362 } else {
363 inactiveView = tabPage->primaryViewContainer()->view();
364 }
365 inactiveView->moveSelectedItems(selectedItems, inactiveView->url());
366 }
367
368 void DolphinTabWidget::detachTab(int index)
369 {
370 Q_ASSERT(index >= 0);
371
372 QStringList args;
373
374 const DolphinTabPage *tabPage = tabPageAt(index);
375 args << tabPage->primaryViewContainer()->url().url();
376 if (tabPage->splitViewEnabled()) {
377 args << tabPage->secondaryViewContainer()->url().url();
378 args << QStringLiteral("--split");
379 }
380 args << QStringLiteral("--new-window");
381
382 KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob("dolphin", args, this);
383 job->setDesktopName(QStringLiteral("org.kde.dolphin"));
384 job->start();
385
386 closeTab(index);
387 }
388
389 void DolphinTabWidget::openNewActivatedTab(int index)
390 {
391 Q_ASSERT(index >= 0);
392 const DolphinTabPage *tabPage = tabPageAt(index);
393 openNewActivatedTab(tabPage->activeViewContainer()->url());
394 }
395
396 void DolphinTabWidget::tabDropEvent(int index, QDropEvent *event)
397 {
398 if (index >= 0) {
399 DolphinView *view = tabPageAt(index)->activeViewContainer()->view();
400 view->dropUrls(view->url(), event, view);
401 } else {
402 const auto urls = event->mimeData()->urls();
403
404 for (const QUrl &url : urls) {
405 auto *job = KIO::statDetails(url, KIO::StatJob::SourceSide, KIO::StatDetail::StatBasic, KIO::JobFlag::HideProgressInfo);
406 connect(job, &KJob::result, this, [this, job]() {
407 if (!job->error() && job->statResult().isDir()) {
408 openNewTab(job->url(), QUrl(), NewTabPosition::AtEnd);
409 }
410 });
411 }
412 }
413 }
414
415 void DolphinTabWidget::tabUrlChanged(const QUrl &url)
416 {
417 const int index = indexOf(qobject_cast<QWidget *>(sender()));
418 if (index >= 0) {
419 tabBar()->setTabText(index, tabName(tabPageAt(index)));
420 tabBar()->setTabToolTip(index, url.toDisplayString(QUrl::PreferLocalFile));
421 if (tabBar()->isVisible()) {
422 // ensure the path url ends with a slash to have proper folder icon for remote folders
423 const QUrl pathUrl = QUrl(url.adjusted(QUrl::StripTrailingSlash).toString(QUrl::FullyEncoded).append("/"));
424 tabBar()->setTabIcon(index, QIcon::fromTheme(KIO::iconNameForUrl(pathUrl)));
425 } else {
426 // Mark as dirty, actually load once the tab bar actually gets shown
427 tabBar()->setTabIcon(index, QIcon());
428 }
429
430 // Emit the currentUrlChanged signal if the url of the current tab has been changed.
431 if (index == currentIndex()) {
432 Q_EMIT currentUrlChanged(url);
433 }
434 }
435 }
436
437 void DolphinTabWidget::currentTabChanged(int index)
438 {
439 DolphinTabPage *tabPage = tabPageAt(index);
440 if (tabPage == m_lastViewedTab) {
441 return;
442 }
443 if (m_lastViewedTab) {
444 m_lastViewedTab->disconnectNavigators();
445 m_lastViewedTab->setActive(false);
446 }
447 if (tabPage->splitViewEnabled() && !m_navigatorsWidget->secondaryUrlNavigator()) {
448 m_navigatorsWidget->createSecondaryUrlNavigator();
449 }
450 DolphinViewContainer *viewContainer = tabPage->activeViewContainer();
451 Q_EMIT activeViewChanged(viewContainer);
452 Q_EMIT currentUrlChanged(viewContainer->url());
453 tabPage->setActive(true);
454 tabPage->connectNavigators(m_navigatorsWidget);
455 m_navigatorsWidget->setSecondaryNavigatorVisible(tabPage->splitViewEnabled());
456 m_lastViewedTab = tabPage;
457 }
458
459 void DolphinTabWidget::tabInserted(int index)
460 {
461 QTabWidget::tabInserted(index);
462
463 if (count() > 1) {
464 // Resolve all pending tab icons
465 for (int i = 0; i < count(); ++i) {
466 const QUrl url = tabPageAt(i)->activeViewContainer()->url();
467 if (tabBar()->tabIcon(i).isNull()) {
468 // ensure the path url ends with a slash to have proper folder icon for remote folders
469 const QUrl pathUrl = QUrl(url.adjusted(QUrl::StripTrailingSlash).toString(QUrl::FullyEncoded).append("/"));
470 tabBar()->setTabIcon(i, QIcon::fromTheme(KIO::iconNameForUrl(pathUrl)));
471 }
472 if (tabBar()->tabToolTip(i).isEmpty()) {
473 tabBar()->setTabToolTip(index, url.toDisplayString(QUrl::PreferLocalFile));
474 }
475 }
476
477 tabBar()->show();
478 }
479
480 Q_EMIT tabCountChanged(count());
481 }
482
483 void DolphinTabWidget::tabRemoved(int index)
484 {
485 QTabWidget::tabRemoved(index);
486
487 // If only one tab is left, then remove the tab entry so that
488 // closing the last tab is not possible.
489 if (count() < 2) {
490 tabBar()->hide();
491 }
492
493 Q_EMIT tabCountChanged(count());
494 }
495
496 QString DolphinTabWidget::tabName(DolphinTabPage *tabPage) const
497 {
498 if (!tabPage) {
499 return QString();
500 }
501 // clang-format off
502 QString name;
503 if (tabPage->splitViewEnabled()) {
504 if (tabPage->primaryViewActive()) {
505 // 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
506 // left in the tab name. In right to left languages the primary view would be on the right so the tab name should match.
507 name = i18nc("@title:tab Active primary view | (Inactive secondary view)", "%1 | (%2)", tabPage->primaryViewContainer()->caption(), tabPage->secondaryViewContainer()->caption());
508 } else {
509 // 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
510 // left in the tab name. In right to left languages the primary view would be on the right so the tab name should match.
511 name = i18nc("@title:tab (Inactive primary view) | Active secondary view", "(%1) | %2", tabPage->primaryViewContainer()->caption(), tabPage->secondaryViewContainer()->caption());
512 }
513 } else {
514 name = tabPage->activeViewContainer()->caption();
515 }
516 // clang-format on
517
518 // Make sure that a '&' inside the directory name is displayed correctly
519 // and not misinterpreted as a keyboard shortcut in QTabBar::setTabText()
520 return name.replace('&', QLatin1String("&&"));
521 }
522
523 DolphinViewContainer *DolphinTabWidget::viewContainerAt(DolphinTabWidget::ViewIndex viewIndex) const
524 {
525 const auto tabPage = tabPageAt(viewIndex.tabIndex);
526 if (!tabPage) {
527 return nullptr;
528 }
529 return viewIndex.isInPrimaryView ? tabPage->primaryViewContainer() : tabPage->secondaryViewContainer();
530 }
531
532 DolphinViewContainer *DolphinTabWidget::activateViewContainerAt(DolphinTabWidget::ViewIndex viewIndex)
533 {
534 activateTab(viewIndex.tabIndex);
535 auto viewContainer = viewContainerAt(viewIndex);
536 if (!viewContainer) {
537 return nullptr;
538 }
539 viewContainer->setActive(true);
540 return viewContainer;
541 }
542
543 const std::optional<const DolphinTabWidget::ViewIndex> DolphinTabWidget::viewOpenAtDirectory(const QUrl &directory) const
544 {
545 int i = currentIndex();
546 if (i < 0) {
547 return std::nullopt;
548 }
549 // loop over the tabs starting from the current one
550 do {
551 const auto tabPage = tabPageAt(i);
552 if (tabPage->primaryViewContainer()->url() == directory) {
553 return std::optional(ViewIndex{i, true});
554 }
555
556 if (tabPage->splitViewEnabled() && tabPage->secondaryViewContainer()->url() == directory) {
557 return std::optional(ViewIndex{i, false});
558 }
559
560 i = (i + 1) % count();
561 } while (i != currentIndex());
562
563 return std::nullopt;
564 }
565
566 const std::optional<const DolphinTabWidget::ViewIndex> DolphinTabWidget::viewShowingItem(const QUrl &item) const
567 {
568 // The item might not be loaded yet even though it exists. So instead
569 // we check if the folder containing the item is showing its contents.
570 const QUrl dirContainingItem(item.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
571
572 // The dirContainingItem is either open directly or expanded in a tree-style view mode.
573 // Is dirContainingitem the base url of a view?
574 auto viewOpenAtContainingDirectory = viewOpenAtDirectory(dirContainingItem);
575 if (viewOpenAtContainingDirectory.has_value()) {
576 return viewOpenAtContainingDirectory;
577 }
578
579 // Is dirContainingItem expanded in some tree-style view?
580 // The rest of this method is about figuring this out.
581
582 int i = currentIndex();
583 if (i < 0) {
584 return std::nullopt;
585 }
586 // loop over the tabs starting from the current one
587 do {
588 const auto tabPage = tabPageAt(i);
589 if (tabPage->primaryViewContainer()->url().isParentOf(item)) {
590 const KFileItem fileItemContainingItem = tabPage->primaryViewContainer()->view()->items().findByUrl(dirContainingItem);
591 if (!fileItemContainingItem.isNull() && tabPage->primaryViewContainer()->view()->isExpanded(fileItemContainingItem)) {
592 return std::optional(ViewIndex{i, true});
593 }
594 }
595
596 if (tabPage->splitViewEnabled() && tabPage->secondaryViewContainer()->url().isParentOf(item)) {
597 const KFileItem fileItemContainingItem = tabPage->secondaryViewContainer()->view()->items().findByUrl(dirContainingItem);
598 if (!fileItemContainingItem.isNull() && tabPage->secondaryViewContainer()->view()->isExpanded(fileItemContainingItem)) {
599 return std::optional(ViewIndex{i, false});
600 }
601 }
602
603 i = (i + 1) % count();
604 } while (i != currentIndex());
605
606 return std::nullopt;
607 }