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