]> cloud.milkyroute.net Git - dolphin.git/blob - src/dolphintabwidget.cpp
GIT_SILENT Update Appstream for new release
[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 indexByUrl(url).first >= 0;
118 }
119
120 bool DolphinTabWidget::isUrlOrParentOpen(const QUrl &url) const
121 {
122 return indexByUrl(url, ReturnIndexForOpenedParentAlso).first >= 0;
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, bool skipChildUrls)
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 QPair<int, bool> indexInfo = indexByUrl(primaryUrl, skipChildUrls ? ReturnIndexForOpenedParentAlso : ReturnIndexForOpenedUrlOnly);
208 const int index = indexInfo.first;
209 const bool isInPrimaryView = indexInfo.second;
210
211 // When the user asks for a URL that's already open (or it's parent is open if skipChildUrls is set),
212 // activate it instead of opening a new tab
213 if (index >= 0) {
214 somethingWasAlreadyOpen = true;
215 activateTab(index);
216 const auto tabPage = tabPageAt(index);
217 if (isInPrimaryView) {
218 tabPage->primaryViewContainer()->setActive(true);
219 } else {
220 tabPage->secondaryViewContainer()->setActive(true);
221 }
222 // BUG: 417230
223 // Required for updateViewState() call in openFiles() to work as expected
224 // If there is a selection, updateViewState() calls are effectively a no-op
225 tabPage->activeViewContainer()->view()->clearSelection();
226 } else if (splitView && (it != dirs.constEnd())) {
227 const QUrl& secondaryUrl = *(it++);
228 if (somethingWasAlreadyOpen) {
229 openNewTab(primaryUrl, secondaryUrl);
230 } else {
231 openNewActivatedTab(primaryUrl, secondaryUrl);
232 }
233 } else {
234 if (somethingWasAlreadyOpen) {
235 openNewTab(primaryUrl);
236 } else {
237 openNewActivatedTab(primaryUrl);
238 }
239 }
240 }
241 }
242
243 void DolphinTabWidget::openFiles(const QList<QUrl>& files, bool splitView)
244 {
245 Q_ASSERT(files.size() > 0);
246
247 // Get all distinct directories from 'files' and open a tab
248 // for each directory. If the "split view" option is enabled, two
249 // directories are shown inside one tab (see openDirectories()).
250 QList<QUrl> dirs;
251 for (const QUrl& url : files) {
252 const QUrl dir(url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
253 if (!dirs.contains(dir)) {
254 dirs.append(dir);
255 }
256 }
257
258 const int oldTabCount = count();
259 openDirectories(dirs, splitView, true);
260 const int tabCount = count();
261
262 // Select the files. Although the files can be split between several
263 // tabs, there is no need to split 'files' accordingly, as
264 // the DolphinView will just ignore invalid selections.
265 for (int i = 0; i < tabCount; ++i) {
266 DolphinTabPage* tabPage = tabPageAt(i);
267 tabPage->markUrlsAsSelected(files);
268 tabPage->markUrlAsCurrent(files.first());
269 if (i < oldTabCount) {
270 // Force selection of file if directory was already open, BUG: 417230
271 tabPage->activeViewContainer()->view()->updateViewState();
272 }
273 }
274 }
275
276 void DolphinTabWidget::closeTab()
277 {
278 closeTab(currentIndex());
279 }
280
281 void DolphinTabWidget::closeTab(const int index)
282 {
283 Q_ASSERT(index >= 0);
284 Q_ASSERT(index < count());
285
286 if (count() < 2) {
287 // Close Dolphin when closing the last tab.
288 parentWidget()->close();
289 return;
290 }
291
292 DolphinTabPage* tabPage = tabPageAt(index);
293 Q_EMIT rememberClosedTab(tabPage->activeViewContainer()->url(), tabPage->saveState());
294
295 removeTab(index);
296 tabPage->deleteLater();
297 }
298
299 void DolphinTabWidget::activateTab(const int index)
300 {
301 if (index < count()) {
302 setCurrentIndex(index);
303 }
304 }
305
306 void DolphinTabWidget::activateLastTab()
307 {
308 setCurrentIndex(count() - 1);
309 }
310
311 void DolphinTabWidget::activateNextTab()
312 {
313 const int index = currentIndex() + 1;
314 setCurrentIndex(index < count() ? index : 0);
315 }
316
317 void DolphinTabWidget::activatePrevTab()
318 {
319 const int index = currentIndex() - 1;
320 setCurrentIndex(index >= 0 ? index : (count() - 1));
321 }
322
323 void DolphinTabWidget::restoreClosedTab(const QByteArray& state)
324 {
325 openNewActivatedTab();
326 currentTabPage()->restoreState(state);
327 }
328
329 void DolphinTabWidget::copyToInactiveSplitView()
330 {
331 const DolphinTabPage* tabPage = tabPageAt(currentIndex());
332 DolphinViewContainer* activeViewContainer = currentTabPage()->activeViewContainer();
333 if (!tabPage->splitViewEnabled() || activeViewContainer->view()->selectedItems().isEmpty()) {
334 return;
335 }
336
337 if (tabPage->primaryViewActive()) {
338 // copy from left panel to right
339 activeViewContainer->view()->copySelectedItems(activeViewContainer->view()->selectedItems(), tabPage->secondaryViewContainer()->url());
340 } else {
341 // copy from right panel to left
342 activeViewContainer->view()->copySelectedItems(activeViewContainer->view()->selectedItems(), tabPage->primaryViewContainer()->url());
343 }
344 }
345
346 void DolphinTabWidget::moveToInactiveSplitView()
347 {
348 const DolphinTabPage* tabPage = tabPageAt(currentIndex());
349 DolphinViewContainer* activeViewContainer = currentTabPage()->activeViewContainer();
350 if (!tabPage->splitViewEnabled() || activeViewContainer->view()->selectedItems().isEmpty()) {
351 return;
352 }
353
354 if (tabPage->primaryViewActive()) {
355 // move from left panel to right
356 activeViewContainer->view()->moveSelectedItems(activeViewContainer->view()->selectedItems(), tabPage->secondaryViewContainer()->url());
357 } else {
358 // move from right panel to left
359 activeViewContainer->view()->moveSelectedItems(activeViewContainer->view()->selectedItems(), tabPage->primaryViewContainer()->url());
360 }
361 }
362
363 void DolphinTabWidget::detachTab(int index)
364 {
365 Q_ASSERT(index >= 0);
366
367 QStringList args;
368
369 const DolphinTabPage* tabPage = tabPageAt(index);
370 args << tabPage->primaryViewContainer()->url().url();
371 if (tabPage->splitViewEnabled()) {
372 args << tabPage->secondaryViewContainer()->url().url();
373 args << QStringLiteral("--split");
374 }
375 args << QStringLiteral("--new-window");
376
377 KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob("dolphin", args, this);
378 job->setDesktopName(QStringLiteral("org.kde.dolphin"));
379 job->start();
380
381 closeTab(index);
382 }
383
384 void DolphinTabWidget::openNewActivatedTab(int index)
385 {
386 Q_ASSERT(index >= 0);
387 const DolphinTabPage* tabPage = tabPageAt(index);
388 openNewActivatedTab(tabPage->activeViewContainer()->url());
389 }
390
391 void DolphinTabWidget::tabDropEvent(int index, QDropEvent* event)
392 {
393 if (index >= 0) {
394 DolphinView* view = tabPageAt(index)->activeViewContainer()->view();
395 view->dropUrls(view->url(), event, view);
396 } else {
397 const auto urls = event->mimeData()->urls();
398
399 for (const QUrl &url : urls) {
400 auto *job = KIO::statDetails(url, KIO::StatJob::SourceSide, KIO::StatDetail::StatBasic, KIO::JobFlag::HideProgressInfo);
401 connect(job, &KJob::result, this, [this, job]() {
402 if (!job->error() && job->statResult().isDir()) {
403 openNewTab(job->url(), QUrl(), NewTabPosition::AtEnd);
404 }
405 });
406 }
407 }
408 }
409
410 void DolphinTabWidget::tabUrlChanged(const QUrl& url)
411 {
412 const int index = indexOf(qobject_cast<QWidget*>(sender()));
413 if (index >= 0) {
414 tabBar()->setTabText(index, tabName(tabPageAt(index)));
415 tabBar()->setTabToolTip(index, url.toDisplayString(QUrl::PreferLocalFile));
416 if (tabBar()->isVisible()) {
417 // ensure the path url ends with a slash to have proper folder icon for remote folders
418 const QUrl pathUrl = QUrl(url.adjusted(QUrl::StripTrailingSlash).toString(QUrl::FullyEncoded).append("/"));
419 tabBar()->setTabIcon(index, QIcon::fromTheme(KIO::iconNameForUrl(pathUrl)));
420 } else {
421 // Mark as dirty, actually load once the tab bar actually gets shown
422 tabBar()->setTabIcon(index, QIcon());
423 }
424
425 // Emit the currentUrlChanged signal if the url of the current tab has been changed.
426 if (index == currentIndex()) {
427 Q_EMIT currentUrlChanged(url);
428 }
429 }
430 }
431
432 void DolphinTabWidget::currentTabChanged(int index)
433 {
434 DolphinTabPage *tabPage = tabPageAt(index);
435 if (tabPage == m_lastViewedTab) {
436 return;
437 }
438 if (m_lastViewedTab) {
439 m_lastViewedTab->disconnectNavigators();
440 m_lastViewedTab->setActive(false);
441 }
442 if (tabPage->splitViewEnabled() && !m_navigatorsWidget->secondaryUrlNavigator()) {
443 m_navigatorsWidget->createSecondaryUrlNavigator();
444 }
445 DolphinViewContainer* viewContainer = tabPage->activeViewContainer();
446 Q_EMIT activeViewChanged(viewContainer);
447 Q_EMIT currentUrlChanged(viewContainer->url());
448 tabPage->setActive(true);
449 tabPage->connectNavigators(m_navigatorsWidget);
450 m_navigatorsWidget->setSecondaryNavigatorVisible(tabPage->splitViewEnabled());
451 m_lastViewedTab = tabPage;
452 }
453
454 void DolphinTabWidget::tabInserted(int index)
455 {
456 QTabWidget::tabInserted(index);
457
458 if (count() > 1) {
459 // Resolve all pending tab icons
460 for (int i = 0; i < count(); ++i) {
461 const QUrl url = tabPageAt(i)->activeViewContainer()->url();
462 if (tabBar()->tabIcon(i).isNull()) {
463 // ensure the path url ends with a slash to have proper folder icon for remote folders
464 const QUrl pathUrl = QUrl(url.adjusted(QUrl::StripTrailingSlash).toString(QUrl::FullyEncoded).append("/"));
465 tabBar()->setTabIcon(i, QIcon::fromTheme(KIO::iconNameForUrl(pathUrl)));
466 }
467 if (tabBar()->tabToolTip(i).isEmpty()) {
468 tabBar()->setTabToolTip(index, url.toDisplayString(QUrl::PreferLocalFile));
469 }
470 }
471
472 tabBar()->show();
473 }
474
475 Q_EMIT tabCountChanged(count());
476 }
477
478 void DolphinTabWidget::tabRemoved(int index)
479 {
480 QTabWidget::tabRemoved(index);
481
482 // If only one tab is left, then remove the tab entry so that
483 // closing the last tab is not possible.
484 if (count() < 2) {
485 tabBar()->hide();
486 }
487
488 Q_EMIT tabCountChanged(count());
489 }
490
491 QString DolphinTabWidget::tabName(DolphinTabPage* tabPage) const
492 {
493 if (!tabPage) {
494 return QString();
495 }
496 QString name = tabPage->activeViewContainer()->caption();
497 // Make sure that a '&' inside the directory name is displayed correctly
498 // and not misinterpreted as a keyboard shortcut in QTabBar::setTabText()
499 return name.replace('&', QLatin1String("&&"));
500 }
501
502 QPair<int, bool> DolphinTabWidget::indexByUrl(const QUrl& url, ChildUrlBehavior childUrlBehavior) const
503 {
504 int i = currentIndex();
505 if (i < 0) {
506 return qMakePair(-1, false);
507 }
508 // loop over the tabs starting from the current one
509 do {
510 const auto tabPage = tabPageAt(i);
511 if (tabPage->primaryViewContainer()->url() == url ||
512 (childUrlBehavior == ReturnIndexForOpenedParentAlso && tabPage->primaryViewContainer()->url().isParentOf(url))) {
513 return qMakePair(i, true);
514 }
515
516 if (tabPage->splitViewEnabled() &&
517 (url == tabPage->secondaryViewContainer()->url() ||
518 (childUrlBehavior == ReturnIndexForOpenedParentAlso && tabPage->secondaryViewContainer()->url().isParentOf(url)))) {
519 return qMakePair(i, false);
520 }
521
522 i = (i + 1) % count();
523 }
524 while (i != currentIndex());
525
526 return qMakePair(-1, false);
527 }