File indexing completed on 2024-04-28 15:52:02
0001 /* 0002 SPDX-FileCopyrightText: 2002 Wilco Greven <greven@kde.org> 0003 SPDX-FileCopyrightText: 2002 Chris Cheney <ccheney@cheney.cx> 0004 SPDX-FileCopyrightText: 2003 Benjamin Meyer <benjamin@csh.rit.edu> 0005 SPDX-FileCopyrightText: 2003-2004 Christophe Devriese <Christophe.Devriese@student.kuleuven.ac.be> 0006 SPDX-FileCopyrightText: 2003 Laurent Montel <montel@kde.org> 0007 SPDX-FileCopyrightText: 2003-2004 Albert Astals Cid <aacid@kde.org> 0008 SPDX-FileCopyrightText: 2003 Luboš Luňák <l.lunak@kde.org> 0009 SPDX-FileCopyrightText: 2003 Malcolm Hunter <malcolm.hunter@gmx.co.uk> 0010 SPDX-FileCopyrightText: 2004 Dominique Devriese <devriese@kde.org> 0011 SPDX-FileCopyrightText: 2004 Dirk Mueller <mueller@kde.org> 0012 0013 Work sponsored by the LiMux project of the city of Munich: 0014 SPDX-FileCopyrightText: 2017 Klarälvdalens Datakonsult AB a KDAB Group company <info@kdab.com> 0015 0016 SPDX-License-Identifier: GPL-2.0-or-later 0017 */ 0018 0019 #include "shell.h" 0020 0021 // qt/kde includes 0022 #include <KActionCollection> 0023 #include <KConfigGroup> 0024 #include <KIO/Global> 0025 #include <KLocalizedString> 0026 #include <KMessageBox> 0027 #include <KPluginFactory> 0028 #include <KRecentFilesAction> 0029 #include <KSharedConfig> 0030 #include <KStandardAction> 0031 #ifndef Q_OS_WIN 0032 #include <KStartupInfo> 0033 #include <KWindowInfo> 0034 #endif 0035 #include <KToggleFullScreenAction> 0036 #include <KToolBar> 0037 #include <KUrlMimeData> 0038 #include <KWindowSystem> 0039 #include <KXMLGUIFactory> 0040 #include <QApplication> 0041 #if HAVE_DBUS 0042 #include <QDBusConnection> 0043 #endif // HAVE_DBUS 0044 #include <QDockWidget> 0045 #include <QDragMoveEvent> 0046 #include <QFileDialog> 0047 #include <QJsonArray> 0048 #include <QMenuBar> 0049 #include <QMimeData> 0050 #include <QObject> 0051 #include <QPointer> 0052 #include <QScreen> 0053 #include <QTabBar> 0054 #include <QTabWidget> 0055 #include <QTimer> 0056 #ifdef WITH_KACTIVITIES 0057 #include <PlasmaActivities/ResourceInstance> 0058 #endif 0059 0060 #include <kio_version.h> 0061 #include <kxmlgui_version.h> 0062 0063 // local includes 0064 #include "../interfaces/viewerinterface.h" 0065 #include "kdocumentviewer.h" 0066 #include "shellutils.h" 0067 0068 static const char *shouldShowMenuBarComingFromFullScreen = "shouldShowMenuBarComingFromFullScreen"; 0069 static const char *shouldShowToolBarComingFromFullScreen = "shouldShowToolBarComingFromFullScreen"; 0070 0071 static const char *const SESSION_URL_KEY = "Urls"; 0072 static const char *const SESSION_TAB_KEY = "ActiveTab"; 0073 0074 static constexpr char SIDEBAR_LOCKED_KEY[] = "LockSidebar"; 0075 static constexpr char SIDEBAR_VISIBLE_KEY[] = "ShowSidebar"; 0076 0077 static inline QString DesktopEntryGroupKey() 0078 { 0079 return QStringLiteral("Desktop Entry"); 0080 } 0081 static inline QString RecentFilesGroupKey() 0082 { 0083 return QStringLiteral("Recent Files"); 0084 } 0085 static inline QString GeneralGroupKey() 0086 { 0087 return QStringLiteral("General"); 0088 } 0089 0090 class ResizableStackedWidget : public QStackedWidget 0091 { 0092 Q_OBJECT 0093 0094 public: 0095 QSize sizeHint() const override 0096 { 0097 return currentWidget()->sizeHint(); 0098 } 0099 QSize minimumSizeHint() const override 0100 { 0101 return currentWidget()->minimumSizeHint(); 0102 } 0103 }; 0104 0105 /** 0106 * Groups sidebar containers in a QDockWidget. 0107 * 0108 * This control groups all the sidebar containers provided by each tab (the Part object), 0109 * allowing the user to dock it to the left and right sides of the window, 0110 * or detach it from the window altogether. 0111 */ 0112 class Sidebar : public QDockWidget 0113 { 0114 Q_OBJECT 0115 0116 public: 0117 explicit Sidebar(QWidget *parent = nullptr) 0118 : QDockWidget(parent) 0119 { 0120 setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); 0121 setFeatures(defaultFeatures()); 0122 0123 m_stackedWidget = new QStackedWidget; 0124 setWidget(m_stackedWidget); 0125 // It seems that without requesting a specific minimum size, Qt 0126 // somehow calculates a (0,-1) minimum size, and then Qt gets angry 0127 // that negative sizes is not possible. 0128 setMinimumSize(10, 10); 0129 } 0130 0131 bool isLocked() const 0132 { 0133 return features().testFlag(NoDockWidgetFeatures); 0134 } 0135 0136 void setLocked(bool locked) 0137 { 0138 setFeatures(locked ? NoDockWidgetFeatures : defaultFeatures()); 0139 0140 // show titlebar only if not locked 0141 if (locked) { 0142 if (!m_dumbTitleWidget) { 0143 m_dumbTitleWidget = new QWidget; 0144 } 0145 setTitleBarWidget(m_dumbTitleWidget); 0146 } else { 0147 setTitleBarWidget(nullptr); 0148 } 0149 } 0150 0151 int indexOf(QWidget *widget) const 0152 { 0153 return m_stackedWidget->indexOf(widget); 0154 } 0155 0156 void addWidget(QWidget *widget) 0157 { 0158 m_stackedWidget->addWidget(widget); 0159 } 0160 0161 void removeWidget(QWidget *widget) 0162 { 0163 m_stackedWidget->removeWidget(widget); 0164 } 0165 0166 void setCurrentWidget(QWidget *widget) 0167 { 0168 m_stackedWidget->setCurrentWidget(widget); 0169 } 0170 0171 private: 0172 static DockWidgetFeatures defaultFeatures() 0173 { 0174 DockWidgetFeatures dockFeatures = DockWidgetClosable | DockWidgetMovable; 0175 if (!KWindowSystem::isPlatformWayland()) { // TODO : Remove this check when QTBUG-87332 is fixed 0176 dockFeatures |= DockWidgetFloatable; 0177 } 0178 0179 return dockFeatures; 0180 } 0181 0182 QStackedWidget *m_stackedWidget = nullptr; 0183 QWidget *m_dumbTitleWidget = nullptr; 0184 }; 0185 0186 Shell::Shell(const QString &serializedOptions) 0187 : KParts::MainWindow() 0188 , m_menuBarWasShown(true) 0189 , m_toolBarWasShown(true) 0190 , m_isValid(true) 0191 { 0192 setObjectName(QStringLiteral("okular::Shell#")); 0193 setContextMenuPolicy(Qt::NoContextMenu); 0194 // otherwise .rc file won't be found by unit test 0195 setComponentName(QStringLiteral("okular"), QString()); 0196 // set the shell's ui resource file 0197 setXMLFile(QStringLiteral("shell.rc")); 0198 m_fileformatsscanned = false; 0199 m_showMenuBarAction = nullptr; 0200 // this routine will find and load our Part. it finds the Part by 0201 // name which is a bad idea usually.. but it's alright in this 0202 // case since our Part is made for this Shell 0203 0204 const auto result = KPluginFactory::loadFactory(KPluginMetaData(QStringLiteral("kf6/parts/okularpart"))); 0205 0206 if (!result) { 0207 // if we couldn't find our Part, we exit since the Shell by 0208 // itself can't do anything useful 0209 m_isValid = false; 0210 KMessageBox::error(this, i18n("Unable to find the Okular component: %1", result.errorString)); 0211 return; 0212 } else { 0213 m_partFactory = result.plugin; 0214 } 0215 0216 // now that the Part plugin is loaded, create the part 0217 KParts::ReadWritePart *const firstPart = m_partFactory->create<KParts::ReadWritePart>(this); 0218 if (firstPart) { 0219 // Setup the central widget 0220 m_centralStackedWidget = new ResizableStackedWidget(); 0221 setCentralWidget(m_centralStackedWidget); 0222 0223 // Setup the welcome screen 0224 m_welcomeScreen = new WelcomeScreen(this); 0225 connect(m_welcomeScreen, &WelcomeScreen::openClicked, this, &Shell::fileOpen); 0226 connect(m_welcomeScreen, &WelcomeScreen::closeClicked, this, &Shell::hideWelcomeScreen); 0227 connect(m_welcomeScreen, &WelcomeScreen::recentItemClicked, this, [this](const QUrl &url) { openUrl(url); }); 0228 connect(m_welcomeScreen, &WelcomeScreen::forgetRecentItem, this, &Shell::forgetRecentItem); 0229 m_centralStackedWidget->addWidget(m_welcomeScreen); 0230 0231 m_welcomeScreen->installEventFilter(this); 0232 0233 // Setup tab bar 0234 m_tabWidget = new QTabWidget(this); 0235 m_tabWidget->setTabsClosable(true); 0236 m_tabWidget->setElideMode(Qt::ElideRight); 0237 m_tabWidget->tabBar()->hide(); 0238 m_tabWidget->setDocumentMode(true); 0239 m_tabWidget->setMovable(true); 0240 0241 m_tabWidget->setAcceptDrops(true); 0242 m_tabWidget->tabBar()->installEventFilter(this); 0243 0244 m_centralStackedWidget->addWidget(m_tabWidget); 0245 0246 connect(m_tabWidget, &QTabWidget::currentChanged, this, &Shell::setActiveTab); 0247 connect(m_tabWidget, &QTabWidget::tabCloseRequested, this, &Shell::closeTab); 0248 connect(m_tabWidget->tabBar(), &QTabBar::tabMoved, this, &Shell::moveTabData); 0249 0250 m_sidebar = new Sidebar; 0251 m_sidebar->setObjectName(QStringLiteral("okular_sidebar")); 0252 m_sidebar->setContextMenuPolicy(Qt::ActionsContextMenu); 0253 m_sidebar->setWindowTitle(i18n("Sidebar")); 0254 connect(m_sidebar, &QDockWidget::visibilityChanged, this, [this](bool visible) { 0255 // sync sidebar visibility with the m_showSidebarAction only if welcome screen is hidden 0256 if (m_showSidebarAction && m_centralStackedWidget->currentWidget() != m_welcomeScreen) { 0257 m_showSidebarAction->setChecked(visible); 0258 } 0259 if (m_centralStackedWidget->currentWidget() == m_welcomeScreen) { 0260 // MainWindow tries hard to make its child dockwidgets shown, but during 0261 // welcome screen we don't want to see the sidebar, 0262 // so try a bit more to actually hide it. 0263 m_sidebar->hide(); 0264 } 0265 }); 0266 addDockWidget(Qt::LeftDockWidgetArea, m_sidebar); 0267 0268 // then, setup our actions 0269 setupActions(); 0270 connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &QObject::deleteLater); 0271 // and integrate the part's GUI with the shell's 0272 setupGUI(Keys | ToolBar | Save); 0273 0274 // NOTE : apply default sidebar width only after calling setupGUI(...) 0275 resizeDocks({m_sidebar}, {200}, Qt::Horizontal); 0276 0277 m_tabs.append(TabState(firstPart)); 0278 m_tabWidget->addTab(firstPart->widget(), QString()); // triggers setActiveTab that calls createGUI( part ) 0279 0280 connectPart(firstPart); 0281 0282 readSettings(); 0283 0284 m_unique = ShellUtils::unique(serializedOptions); 0285 #if HAVE_DBUS 0286 if (m_unique) { 0287 m_unique = QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.okular")); 0288 if (!m_unique) { 0289 KMessageBox::information(this, i18n("There is already a unique Okular instance running. This instance won't be the unique one.")); 0290 } 0291 } else { 0292 QString serviceName = QStringLiteral("org.kde.okular-") + QString::number(qApp->applicationPid()); 0293 QDBusConnection::sessionBus().registerService(serviceName); 0294 } 0295 if (ShellUtils::noRaise(serializedOptions)) { 0296 setAttribute(Qt::WA_ShowWithoutActivating); 0297 } 0298 0299 { 0300 const QString editorCmd = ShellUtils::editorCmd(serializedOptions); 0301 if (!editorCmd.isEmpty()) { 0302 QMetaObject::invokeMethod(firstPart, "setEditorCmd", Q_ARG(QString, editorCmd)); 0303 } 0304 } 0305 0306 QDBusConnection::sessionBus().registerObject(QStringLiteral("/okularshell"), this, QDBusConnection::ExportScriptableSlots); 0307 #endif // HAVE_DBUS 0308 0309 // Make sure that the welcome scren is visible on startup. 0310 showWelcomeScreen(); 0311 } else { 0312 m_isValid = false; 0313 KMessageBox::error(this, i18n("Unable to find the Okular component.")); 0314 } 0315 0316 connect(guiFactory(), &KXMLGUIFactory::shortcutsSaved, this, &Shell::reloadAllXML); 0317 } 0318 0319 void Shell::reloadAllXML() 0320 { 0321 for (const TabState &tab : std::as_const(m_tabs)) { 0322 tab.part->reloadXML(); 0323 } 0324 } 0325 0326 void Shell::keyPressEvent(QKeyEvent *e) 0327 { 0328 if (e->key() == Qt::Key_Escape && window()->isFullScreen()) { 0329 setFullScreen(false); 0330 } 0331 } 0332 0333 bool Shell::eventFilter(QObject *obj, QEvent *event) 0334 { 0335 QDragMoveEvent *dmEvent = dynamic_cast<QDragMoveEvent *>(event); 0336 if (dmEvent) { 0337 bool accept = dmEvent->mimeData()->hasUrls(); 0338 event->setAccepted(accept); 0339 return accept; 0340 } 0341 0342 QDropEvent *dEvent = dynamic_cast<QDropEvent *>(event); 0343 if (dEvent) { 0344 const QList<QUrl> list = KUrlMimeData::urlsFromMimeData(dEvent->mimeData()); 0345 handleDroppedUrls(list); 0346 dEvent->setAccepted(true); 0347 return true; 0348 } 0349 0350 // Handle middle button click events on the tab bar 0351 if (obj == m_tabWidget->tabBar() && event->type() == QEvent::MouseButtonRelease) { 0352 QMouseEvent *mEvent = static_cast<QMouseEvent *>(event); 0353 if (mEvent->button() == Qt::MiddleButton) { 0354 int tabIndex = m_tabWidget->tabBar()->tabAt(mEvent->pos()); 0355 if (tabIndex != -1) { 0356 closeTab(tabIndex); 0357 return true; 0358 } 0359 } 0360 } 0361 return KParts::MainWindow::eventFilter(obj, event); 0362 } 0363 0364 bool Shell::isValid() const 0365 { 0366 return m_isValid; 0367 } 0368 0369 void Shell::showOpenRecentMenu() 0370 { 0371 m_recent->menu()->popup(QCursor::pos()); 0372 } 0373 0374 Shell::~Shell() 0375 { 0376 if (!m_tabs.empty()) { 0377 writeSettings(); 0378 for (const TabState &tab : std::as_const(m_tabs)) { 0379 tab.part->closeUrl(false); 0380 } 0381 m_tabs.clear(); 0382 } 0383 #if HAVE_DBUS 0384 if (m_unique) { 0385 QDBusConnection::sessionBus().unregisterService(QStringLiteral("org.kde.okular")); 0386 } 0387 #endif // HAVE_DBUS 0388 0389 delete m_tabWidget; 0390 } 0391 0392 // Open a new document if we have space for it 0393 // This can hang if called on a unique instance and openUrl pops a messageBox 0394 bool Shell::openDocument(const QUrl &url, const QString &serializedOptions) 0395 { 0396 if (m_tabs.size() <= 0) { 0397 return false; 0398 } 0399 0400 hideWelcomeScreen(); 0401 0402 KParts::ReadWritePart *const part = m_tabs[0].part; 0403 0404 // Return false if we can't open new tabs and the only part is occupied 0405 if (!qobject_cast<Okular::ViewerInterface *>(part)->openNewFilesInTabs() && !part->url().isEmpty() && !ShellUtils::unique(serializedOptions)) { 0406 return false; 0407 } 0408 0409 openUrl(url, serializedOptions); 0410 0411 return true; 0412 } 0413 0414 bool Shell::openDocument(const QString &urlString, const QString &serializedOptions) 0415 { 0416 return openDocument(QUrl(urlString), serializedOptions); 0417 } 0418 0419 bool Shell::canOpenDocs(int numDocs, int desktop) 0420 { 0421 if (m_tabs.size() <= 0 || numDocs <= 0 || m_unique) { 0422 return false; 0423 } 0424 0425 KParts::ReadWritePart *const part = m_tabs[0].part; 0426 const bool allowTabs = qobject_cast<Okular::ViewerInterface *>(part)->openNewFilesInTabs(); 0427 0428 if (!allowTabs && (numDocs > 1 || !part->url().isEmpty())) { 0429 return false; 0430 } 0431 0432 #ifndef Q_OS_WIN 0433 const KWindowInfo winfo(window()->effectiveWinId(), NET::WMDesktop); 0434 if (winfo.desktop() != desktop) { 0435 return false; 0436 } 0437 #endif 0438 0439 return true; 0440 } 0441 0442 void Shell::openUrl(const QUrl &url, const QString &serializedOptions) 0443 { 0444 hideWelcomeScreen(); 0445 0446 const int activeTab = m_tabWidget->currentIndex(); 0447 KParts::ReadWritePart *const activePart = m_tabs[activeTab].part; 0448 if (!activePart->url().isEmpty()) { 0449 if (m_unique) { 0450 applyOptionsToPart(activePart, serializedOptions); 0451 activePart->openUrl(url); 0452 } else { 0453 if (qobject_cast<Okular::ViewerInterface *>(activePart)->openNewFilesInTabs()) { 0454 openNewTab(url, serializedOptions); 0455 } else { 0456 Shell *newShell = new Shell(serializedOptions); 0457 newShell->show(); 0458 newShell->openUrl(url, serializedOptions); 0459 } 0460 } 0461 } else { 0462 m_tabWidget->setTabText(activeTab, url.fileName()); 0463 m_tabWidget->setTabToolTip(activeTab, url.fileName()); 0464 0465 applyOptionsToPart(activePart, serializedOptions); 0466 bool openOk = activePart->openUrl(url); 0467 const bool isstdin = url.fileName() == QLatin1String("-") || url.scheme() == QLatin1String("fd"); 0468 if (!isstdin) { 0469 if (openOk) { 0470 #ifdef WITH_KACTIVITIES 0471 KActivities::ResourceInstance::notifyAccessed(url); 0472 #endif 0473 m_recent->addUrl(url); 0474 } else { 0475 m_recent->removeUrl(url); 0476 closeTab(activeTab); 0477 } 0478 } 0479 } 0480 } 0481 0482 void Shell::closeUrl() 0483 { 0484 closeTab(m_tabWidget->currentIndex()); 0485 0486 // When closing the current tab two things can happen: 0487 // * the focus was on the tab 0488 // * the focus was somewhere in the toolbar 0489 // we don't have other places that accept focus 0490 // * If it was on the tab, logic says it should go back to the next current tab 0491 // * If it was on the toolbar, we could leave it there, but since we redo the menus/toolbars for the new tab, it gets kind of lost 0492 // so it's easier to set it to the next current tab which also makes sense as consistency 0493 if (m_tabWidget->count() >= 0) { 0494 KParts::ReadWritePart *const newPart = m_tabs[m_tabWidget->currentIndex()].part; 0495 newPart->widget()->setFocus(); 0496 } 0497 } 0498 0499 void Shell::readSettings() 0500 { 0501 m_recent->loadEntries(KSharedConfig::openConfig()->group(RecentFilesGroupKey())); 0502 m_recent->setEnabled(true); // force enabling 0503 0504 const KConfigGroup group = KSharedConfig::openConfig()->group(DesktopEntryGroupKey()); 0505 bool fullScreen = group.readEntry("FullScreen", false); 0506 setFullScreen(fullScreen); 0507 0508 if (fullScreen) { 0509 m_menuBarWasShown = group.readEntry(shouldShowMenuBarComingFromFullScreen, true); 0510 m_toolBarWasShown = group.readEntry(shouldShowToolBarComingFromFullScreen, true); 0511 } 0512 0513 const KConfigGroup sidebarGroup = KSharedConfig::openConfig()->group(GeneralGroupKey()); 0514 m_sidebar->setVisible(sidebarGroup.readEntry(SIDEBAR_VISIBLE_KEY, true)); 0515 m_sidebar->setLocked(sidebarGroup.readEntry(SIDEBAR_LOCKED_KEY, true)); 0516 0517 m_showSidebarAction->setChecked(m_sidebar->isVisibleTo(this)); 0518 m_lockSidebarAction->setChecked(m_sidebar->isLocked()); 0519 } 0520 0521 void Shell::writeSettings() 0522 { 0523 saveRecents(); 0524 0525 KConfigGroup sidebarGroup = KSharedConfig::openConfig()->group(GeneralGroupKey()); 0526 sidebarGroup.writeEntry(SIDEBAR_LOCKED_KEY, m_sidebar->isLocked()); 0527 // NOTE : Consider whether the m_showSidebarAction is checked, because 0528 // the sidebar can be forcibly hidden if the welcome screen is displayed 0529 sidebarGroup.writeEntry(SIDEBAR_VISIBLE_KEY, m_sidebar->isVisibleTo(this) || m_showSidebarAction->isChecked()); 0530 0531 KConfigGroup group = KSharedConfig::openConfig()->group(DesktopEntryGroupKey()); 0532 group.writeEntry("FullScreen", m_fullScreenAction->isChecked()); 0533 if (m_fullScreenAction->isChecked()) { 0534 group.writeEntry(shouldShowMenuBarComingFromFullScreen, m_menuBarWasShown); 0535 group.writeEntry(shouldShowToolBarComingFromFullScreen, m_toolBarWasShown); 0536 } 0537 KSharedConfig::openConfig()->sync(); 0538 } 0539 0540 void Shell::saveRecents() 0541 { 0542 m_recent->saveEntries(KSharedConfig::openConfig()->group(RecentFilesGroupKey())); 0543 } 0544 0545 void Shell::setupActions() 0546 { 0547 KStandardAction::open(this, SLOT(fileOpen()), actionCollection()); 0548 m_recent = KStandardAction::openRecent(this, SLOT(openUrl(QUrl)), actionCollection()); 0549 m_recent->setToolBarMode(KRecentFilesAction::MenuMode); 0550 connect(m_recent, &QAction::triggered, this, &Shell::showOpenRecentMenu); 0551 connect(m_recent, &KRecentFilesAction::recentListCleared, this, &Shell::refreshRecentsOnWelcomeScreen); 0552 connect(m_welcomeScreen, &WelcomeScreen::forgetAllRecents, m_recent, &KRecentFilesAction::clear); 0553 m_recent->setToolTip(i18n("Click to open a file\nClick and hold to open a recent file")); 0554 m_recent->setWhatsThis(i18n("<b>Click</b> to open a file or <b>Click and hold</b> to select a recent file")); 0555 m_printAction = KStandardAction::print(this, SLOT(print()), actionCollection()); 0556 m_printAction->setEnabled(false); 0557 m_closeAction = KStandardAction::close(this, SLOT(closeUrl()), actionCollection()); 0558 m_closeAction->setEnabled(false); 0559 KStandardAction::quit(this, SLOT(close()), actionCollection()); 0560 0561 setStandardToolBarMenuEnabled(true); 0562 0563 m_showMenuBarAction = KStandardAction::showMenubar(this, SLOT(slotShowMenubar()), actionCollection()); 0564 m_fullScreenAction = KStandardAction::fullScreen(this, SLOT(slotUpdateFullScreen()), this, actionCollection()); 0565 0566 m_nextTabAction = actionCollection()->addAction(QStringLiteral("tab-next")); 0567 m_nextTabAction->setText(i18n("Next Tab")); 0568 actionCollection()->setDefaultShortcuts(m_nextTabAction, KStandardShortcut::tabNext()); 0569 m_nextTabAction->setEnabled(false); 0570 connect(m_nextTabAction, &QAction::triggered, this, &Shell::activateNextTab); 0571 0572 m_prevTabAction = actionCollection()->addAction(QStringLiteral("tab-previous")); 0573 m_prevTabAction->setText(i18n("Previous Tab")); 0574 actionCollection()->setDefaultShortcuts(m_prevTabAction, KStandardShortcut::tabPrev()); 0575 m_prevTabAction->setEnabled(false); 0576 connect(m_prevTabAction, &QAction::triggered, this, &Shell::activatePrevTab); 0577 0578 m_undoCloseTab = actionCollection()->addAction(QStringLiteral("undo-close-tab")); 0579 m_undoCloseTab->setText(i18n("Undo close tab")); 0580 actionCollection()->setDefaultShortcut(m_undoCloseTab, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_T)); 0581 m_undoCloseTab->setIcon(QIcon::fromTheme(QStringLiteral("edit-undo"))); 0582 m_undoCloseTab->setEnabled(false); 0583 connect(m_undoCloseTab, &QAction::triggered, this, &Shell::undoCloseTab); 0584 0585 m_lockSidebarAction = actionCollection()->addAction(QStringLiteral("okular_lock_sidebar")); 0586 m_lockSidebarAction->setCheckable(true); 0587 m_lockSidebarAction->setIcon(QIcon::fromTheme(QStringLiteral("lock"))); 0588 m_lockSidebarAction->setText(i18n("Lock Sidebar")); 0589 connect(m_lockSidebarAction, &QAction::triggered, m_sidebar, &Sidebar::setLocked); 0590 m_sidebar->addAction(m_lockSidebarAction); 0591 } 0592 0593 void Shell::saveProperties(KConfigGroup &group) 0594 { 0595 if (!m_isValid) { // part couldn't be loaded, nothing to save 0596 return; 0597 } 0598 0599 // Gather lists of settings to preserve 0600 QStringList urls; 0601 for (const TabState &tab : std::as_const(m_tabs)) { 0602 urls.append(tab.part->url().url()); 0603 } 0604 group.writePathEntry(SESSION_URL_KEY, urls); 0605 group.writeEntry(SESSION_TAB_KEY, m_tabWidget->currentIndex()); 0606 } 0607 0608 void Shell::readProperties(const KConfigGroup &group) 0609 { 0610 // Reopen documents based on saved settings 0611 QStringList urls = group.readPathEntry(SESSION_URL_KEY, QStringList()); 0612 int desiredTab = group.readEntry<int>(SESSION_TAB_KEY, 0); 0613 0614 while (!urls.isEmpty()) { 0615 openUrl(QUrl(urls.takeFirst())); 0616 } 0617 0618 if (desiredTab < m_tabs.size()) { 0619 setActiveTab(desiredTab); 0620 } 0621 } 0622 0623 void Shell::fileOpen() 0624 { 0625 // this slot is called whenever the File->Open menu is selected, 0626 // the Open shortcut is pressed (usually CTRL+O) or the Open toolbar 0627 // button is clicked 0628 const int activeTab = m_tabWidget->currentIndex(); 0629 if (!m_fileformatsscanned) { 0630 const KDocumentViewer *const doc = qobject_cast<KDocumentViewer *>(m_tabs[activeTab].part); 0631 Q_ASSERT(doc); 0632 0633 m_fileformats = doc->supportedMimeTypes(); 0634 0635 m_fileformatsscanned = true; 0636 } 0637 0638 QUrl startDir; 0639 const KParts::ReadWritePart *const curPart = m_tabs[activeTab].part; 0640 if (curPart->url().isLocalFile()) { 0641 startDir = KIO::upUrl(curPart->url()); 0642 } 0643 if (startDir.isEmpty() || (startDir == QUrl::fromLocalFile(QDir::rootPath()))) { 0644 startDir = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); 0645 } 0646 0647 QPointer<QFileDialog> dlg(new QFileDialog(this)); 0648 dlg->setDirectoryUrl(startDir); 0649 dlg->setAcceptMode(QFileDialog::AcceptOpen); 0650 dlg->setOption(QFileDialog::HideNameFilterDetails, true); 0651 dlg->setFileMode(QFileDialog::ExistingFiles); // Allow selection of more than one file 0652 0653 QMimeDatabase mimeDatabase; 0654 // Unfortunately non Plasma file dialogs don't support the "All supported files" when using 0655 // setMimeTypeFilters instead of setNameFilters, so for those use setNameFilters which is a bit 0656 // worse because doesn't show you pdf files named bla.blo when you say "show me the pdf files", but 0657 // that's solvable by choosing "All Files" and it's not that common while it's more convenient to 0658 // only get shown the files that the application can open by default instead of all of them 0659 const bool useMimeTypeFilters = qgetenv("XDG_CURRENT_DESKTOP").toLower() == "kde"; 0660 if (useMimeTypeFilters) { 0661 QStringList mimetypes; 0662 for (const QString &mimeName : std::as_const(m_fileformats)) { 0663 QMimeType mimeType = mimeDatabase.mimeTypeForName(mimeName); 0664 mimetypes << mimeType.name(); 0665 } 0666 mimetypes.prepend(QStringLiteral("application/octet-stream")); 0667 dlg->setMimeTypeFilters(mimetypes); 0668 } else { 0669 QSet<QString> globPatterns; 0670 QMap<QString, QStringList> namedGlobs; 0671 for (const QString &mimeName : std::as_const(m_fileformats)) { 0672 QMimeType mimeType = mimeDatabase.mimeTypeForName(mimeName); 0673 const QStringList globs(mimeType.globPatterns()); 0674 if (globs.isEmpty()) { 0675 continue; 0676 } 0677 0678 globPatterns.unite(QSet<QString>(globs.begin(), globs.end())); 0679 0680 namedGlobs[mimeType.comment()].append(globs); 0681 } 0682 QStringList namePatterns; 0683 for (auto it = namedGlobs.cbegin(); it != namedGlobs.cend(); ++it) { 0684 namePatterns.append(it.key() + QLatin1String(" (") + it.value().join(QLatin1Char(' ')) + QLatin1Char(')')); 0685 } 0686 0687 const QStringList allGlobPatterns = globPatterns.values(); 0688 namePatterns.prepend(i18n("All files (*)")); 0689 namePatterns.prepend(i18n("All supported files (%1)", allGlobPatterns.join(QLatin1Char(' ')))); 0690 dlg->setNameFilters(namePatterns); 0691 } 0692 0693 dlg->setWindowTitle(i18n("Open Document")); 0694 if (dlg->exec() && dlg) { 0695 const QList<QUrl> urlList = dlg->selectedUrls(); 0696 for (const QUrl &url : urlList) { 0697 openUrl(url); 0698 } 0699 } 0700 0701 if (dlg) { 0702 delete dlg.data(); 0703 } 0704 } 0705 0706 void Shell::tryRaise(const QString &startupId) 0707 { 0708 #ifndef Q_OS_WIN 0709 if (KWindowSystem::isPlatformWayland()) { 0710 KWindowSystem::setCurrentXdgActivationToken(startupId); 0711 } else if (KWindowSystem::isPlatformX11()) { 0712 KStartupInfo::setNewStartupId(window()->windowHandle(), startupId.toUtf8()); 0713 } 0714 #else 0715 Q_UNUSED(startupId); 0716 #endif 0717 0718 KWindowSystem::activateWindow(window()->windowHandle()); 0719 } 0720 0721 // only called when starting the program 0722 void Shell::setFullScreen(bool useFullScreen) 0723 { 0724 if (useFullScreen) { 0725 setWindowState(windowState() | Qt::WindowFullScreen); // set 0726 } else { 0727 setWindowState(windowState() & ~Qt::WindowFullScreen); // reset 0728 } 0729 } 0730 0731 void Shell::setCaption(const QString &caption) 0732 { 0733 bool modified = false; 0734 0735 const int activeTab = m_tabWidget->currentIndex(); 0736 if (activeTab != -1) { 0737 KParts::ReadWritePart *const activePart = m_tabs[activeTab].part; 0738 QString tabCaption = activePart->url().fileName(); 0739 if (activePart->isModified()) { 0740 modified = true; 0741 if (!tabCaption.isEmpty()) { 0742 tabCaption.append(QStringLiteral(" *")); 0743 } 0744 } 0745 0746 m_tabWidget->setTabText(activeTab, tabCaption); 0747 } 0748 0749 setCaption(caption, modified); 0750 } 0751 0752 void Shell::showEvent(QShowEvent *e) 0753 { 0754 if (!menuBar()->isNativeMenuBar() && m_showMenuBarAction) { 0755 m_showMenuBarAction->setChecked(menuBar()->isVisible()); 0756 } 0757 0758 KParts::MainWindow::showEvent(e); 0759 } 0760 0761 void Shell::slotUpdateFullScreen() 0762 { 0763 if (m_fullScreenAction->isChecked()) { 0764 m_menuBarWasShown = !menuBar()->isHidden(); 0765 menuBar()->hide(); 0766 0767 m_toolBarWasShown = !toolBar()->isHidden(); 0768 toolBar()->hide(); 0769 0770 KToggleFullScreenAction::setFullScreen(this, true); 0771 } else { 0772 if (m_menuBarWasShown) { 0773 menuBar()->show(); 0774 } 0775 if (m_toolBarWasShown) { 0776 toolBar()->show(); 0777 } 0778 KToggleFullScreenAction::setFullScreen(this, false); 0779 } 0780 } 0781 0782 void Shell::slotShowMenubar() 0783 { 0784 if (menuBar()->isHidden()) { 0785 menuBar()->show(); 0786 } else { 0787 menuBar()->hide(); 0788 } 0789 } 0790 0791 QSize Shell::sizeHint() const 0792 { 0793 const QSize baseSize = QApplication::primaryScreen()->availableSize() * 0.6; 0794 // Set an arbitrary yet sensible sane minimum size for very small screens; 0795 // for example we don't want people using 1366x768 screens to get a tiny 0796 // default window size of 820 x 460 which will elide most of the toolbar buttons. 0797 return baseSize.expandedTo(QSize(1000, 700)); 0798 } 0799 0800 bool Shell::queryClose() 0801 { 0802 if (m_tabs.count() > 1) { 0803 const QString dontAskAgainName = QStringLiteral("ShowTabWarning"); 0804 KMessageBox::ButtonCode dummy = KMessageBox::PrimaryAction; 0805 if (KMessageBox::shouldBeShownTwoActions(dontAskAgainName, dummy)) { 0806 QDialog *dialog = new QDialog(this); 0807 dialog->setWindowTitle(i18n("Confirm Close")); 0808 0809 QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog); 0810 buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::No); 0811 KGuiItem::assign(buttonBox->button(QDialogButtonBox::Yes), KGuiItem(i18n("Close Tabs"), QStringLiteral("tab-close"))); 0812 KGuiItem::assign(buttonBox->button(QDialogButtonBox::No), KStandardGuiItem::cancel()); 0813 0814 bool checkboxResult = true; 0815 const int result = KMessageBox::createKMessageBox(dialog, 0816 buttonBox, 0817 QMessageBox::Question, 0818 i18n("You are about to close %1 tabs. Are you sure you want to continue?", m_tabs.count()), 0819 QStringList(), 0820 i18n("Warn me when I attempt to close multiple tabs"), 0821 &checkboxResult, 0822 KMessageBox::Notify); 0823 0824 if (!checkboxResult) { 0825 KMessageBox::saveDontShowAgainTwoActions(dontAskAgainName, dummy); 0826 } 0827 0828 if (result != QDialogButtonBox::Yes) { 0829 return false; 0830 } 0831 } 0832 } 0833 0834 for (int i = 0; i < m_tabs.size(); ++i) { 0835 KParts::ReadWritePart *const part = m_tabs[i].part; 0836 0837 // To resolve confusion about multiple modified docs, switch to relevant tab 0838 if (part->isModified()) { 0839 setActiveTab(i); 0840 } 0841 0842 if (!part->queryClose()) { 0843 return false; 0844 } 0845 } 0846 return true; 0847 } 0848 0849 void Shell::setActiveTab(int tab) 0850 { 0851 if (m_showSidebarAction) { 0852 m_showSidebarAction->disconnect(); 0853 } 0854 0855 m_tabWidget->setCurrentIndex(tab); 0856 0857 // NOTE : createGUI(...) breaks the visibility of the sidebar, so we need 0858 // to save and restore it 0859 const bool isSidebarVisible = m_sidebar->isVisible(); 0860 createGUI(m_tabs[tab].part); 0861 m_sidebar->setVisible(isSidebarVisible); 0862 0863 // dock KPart's sidebar if new and make it current 0864 Okular::ViewerInterface *iPart = qobject_cast<Okular::ViewerInterface *>(m_tabs[tab].part); 0865 Q_ASSERT(iPart); 0866 QWidget *sideContainer = iPart->getSideContainer(); 0867 if (m_sidebar->indexOf(sideContainer) == -1) { 0868 m_sidebar->addWidget(sideContainer); 0869 if (m_sidebar->maximumWidth() > sideContainer->maximumWidth()) { 0870 m_sidebar->setMaximumWidth(sideContainer->maximumWidth()); 0871 } 0872 } 0873 m_sidebar->setCurrentWidget(sideContainer); 0874 0875 m_showSidebarAction = m_tabs[tab].part->actionCollection()->action(QStringLiteral("show_leftpanel")); 0876 Q_ASSERT(m_showSidebarAction); 0877 m_showSidebarAction->disconnect(); 0878 m_showSidebarAction->setChecked(m_sidebar->isVisibleTo(this)); 0879 connect(m_showSidebarAction, &QAction::triggered, m_sidebar, &Sidebar::setVisible); 0880 0881 m_printAction->setEnabled(m_tabs[tab].printEnabled); 0882 m_closeAction->setEnabled(m_tabs[tab].closeEnabled); 0883 } 0884 0885 void Shell::closeTab(int tab) 0886 { 0887 KParts::ReadWritePart *const part = m_tabs[tab].part; 0888 QUrl url = part->url(); 0889 bool closeSuccess = part->closeUrl(); 0890 if (closeSuccess && m_tabs.count() > 1) { 0891 if (part->factory()) { 0892 part->factory()->removeClient(part); 0893 } 0894 part->disconnect(); 0895 0896 Okular::ViewerInterface *iPart = qobject_cast<Okular::ViewerInterface *>(m_tabs[tab].part); 0897 Q_ASSERT(iPart); 0898 QWidget *sideContainer = iPart->getSideContainer(); 0899 m_sidebar->removeWidget(sideContainer); 0900 connect(part, &QObject::destroyed, sideContainer, &QObject::deleteLater); 0901 0902 part->deleteLater(); 0903 m_tabs.removeAt(tab); 0904 m_tabWidget->removeTab(tab); 0905 m_undoCloseTab->setEnabled(true); 0906 m_closedTabUrls.append(url); 0907 0908 if (m_tabWidget->count() == 1) { 0909 m_tabWidget->tabBar()->hide(); 0910 m_nextTabAction->setEnabled(false); 0911 m_prevTabAction->setEnabled(false); 0912 } 0913 } else if (closeSuccess && m_tabs.count() == 1) { 0914 // Show welcome screen when the last tab is closed. 0915 0916 showWelcomeScreen(); 0917 } 0918 } 0919 0920 void Shell::openNewTab(const QUrl &url, const QString &serializedOptions) 0921 { 0922 const int previousActiveTab = m_tabWidget->currentIndex(); 0923 KParts::ReadWritePart *const activePart = m_tabs[previousActiveTab].part; 0924 0925 hideWelcomeScreen(); 0926 0927 bool activateTabIfAlreadyOpen; 0928 QMetaObject::invokeMethod(activePart, "activateTabIfAlreadyOpenFile", Q_RETURN_ARG(bool, activateTabIfAlreadyOpen)); 0929 0930 if (activateTabIfAlreadyOpen) { 0931 const int tabIndex = findTabIndex(url); 0932 0933 if (tabIndex >= 0) { 0934 setActiveTab(tabIndex); 0935 m_recent->addUrl(url); 0936 return; 0937 } 0938 } 0939 0940 // Tabs are hidden when there's only one, so show it 0941 if (m_tabs.size() == 1) { 0942 m_tabWidget->tabBar()->show(); 0943 m_nextTabAction->setEnabled(true); 0944 m_prevTabAction->setEnabled(true); 0945 } 0946 0947 const int newIndex = m_tabs.size(); 0948 0949 // Make new part 0950 m_tabs.append(TabState(m_partFactory->create<KParts::ReadWritePart>(this))); 0951 connectPart(m_tabs[newIndex].part); 0952 0953 // Update GUI 0954 KParts::ReadWritePart *const part = m_tabs[newIndex].part; 0955 m_tabWidget->addTab(part->widget(), url.fileName()); 0956 m_tabWidget->setTabToolTip(newIndex, url.fileName()); 0957 0958 applyOptionsToPart(part, serializedOptions); 0959 0960 setActiveTab(m_tabs.size() - 1); 0961 0962 if (part->openUrl(url)) { 0963 m_recent->addUrl(url); 0964 } else { 0965 setActiveTab(previousActiveTab); 0966 closeTab(m_tabs.size() - 1); 0967 m_recent->removeUrl(url); 0968 } 0969 } 0970 0971 void Shell::applyOptionsToPart(QObject *part, const QString &serializedOptions) 0972 { 0973 KDocumentViewer *const doc = qobject_cast<KDocumentViewer *>(part); 0974 const QString find = ShellUtils::find(serializedOptions); 0975 if (ShellUtils::startInPresentation(serializedOptions)) { 0976 doc->startPresentation(); 0977 } 0978 if (ShellUtils::showPrintDialog(serializedOptions)) { 0979 QMetaObject::invokeMethod(part, "enableStartWithPrint"); 0980 } 0981 if (ShellUtils::showPrintDialogAndExit(serializedOptions)) { 0982 QMetaObject::invokeMethod(part, "enableExitAfterPrint"); 0983 } 0984 if (!find.isEmpty()) { 0985 QMetaObject::invokeMethod(part, "enableStartWithFind", Q_ARG(QString, find)); 0986 } 0987 } 0988 0989 void Shell::connectPart(const KParts::ReadWritePart *part) 0990 { 0991 // We're abusing the fact we know the part is our part here 0992 connect(this, SIGNAL(moveSplitter(int)), part, SLOT(moveSplitter(int))); // clazy:exclude=old-style-connect 0993 connect(part, SIGNAL(enablePrintAction(bool)), this, SLOT(setPrintEnabled(bool))); // clazy:exclude=old-style-connect 0994 connect(part, SIGNAL(enableCloseAction(bool)), this, SLOT(setCloseEnabled(bool))); // clazy:exclude=old-style-connect 0995 connect(part, SIGNAL(mimeTypeChanged(QMimeType)), this, SLOT(setTabIcon(QMimeType))); // clazy:exclude=old-style-connect 0996 connect(part, SIGNAL(urlsDropped(QList<QUrl>)), this, SLOT(handleDroppedUrls(QList<QUrl>))); // clazy:exclude=old-style-connect 0997 // clang-format off 0998 // Otherwise the QSize,QSize gets turned into QSize, QSize that is not normalized signals and is slightly slower 0999 connect(part, SIGNAL(fitWindowToPage(QSize,QSize)), this, SLOT(slotFitWindowToPage(QSize,QSize))); // clazy:exclude=old-style-connect 1000 // clang-format on 1001 } 1002 1003 void Shell::print() 1004 { 1005 QMetaObject::invokeMethod(m_tabs[m_tabWidget->currentIndex()].part, "slotPrint"); 1006 } 1007 1008 void Shell::setPrintEnabled(bool enabled) 1009 { 1010 int i = findTabIndex(sender()); 1011 if (i != -1) { 1012 m_tabs[i].printEnabled = enabled; 1013 if (i == m_tabWidget->currentIndex()) { 1014 m_printAction->setEnabled(enabled); 1015 } 1016 } 1017 } 1018 1019 void Shell::setCloseEnabled(bool enabled) 1020 { 1021 int i = findTabIndex(sender()); 1022 if (i != -1) { 1023 m_tabs[i].closeEnabled = enabled; 1024 if (i == m_tabWidget->currentIndex()) { 1025 m_closeAction->setEnabled(enabled); 1026 } 1027 } 1028 } 1029 1030 void Shell::activateNextTab() 1031 { 1032 if (m_tabs.size() < 2) { 1033 return; 1034 } 1035 1036 const int activeTab = m_tabWidget->currentIndex(); 1037 const int nextTab = (activeTab == m_tabs.size() - 1) ? 0 : activeTab + 1; 1038 1039 setActiveTab(nextTab); 1040 } 1041 1042 void Shell::activatePrevTab() 1043 { 1044 if (m_tabs.size() < 2) { 1045 return; 1046 } 1047 1048 const int activeTab = m_tabWidget->currentIndex(); 1049 const int prevTab = (activeTab == 0) ? m_tabs.size() - 1 : activeTab - 1; 1050 1051 setActiveTab(prevTab); 1052 } 1053 1054 void Shell::undoCloseTab() 1055 { 1056 if (m_closedTabUrls.isEmpty()) { 1057 return; 1058 } 1059 1060 const QUrl lastTabUrl = m_closedTabUrls.takeLast(); 1061 1062 if (m_closedTabUrls.isEmpty()) { 1063 m_undoCloseTab->setEnabled(false); 1064 } 1065 1066 openUrl(lastTabUrl); 1067 } 1068 1069 void Shell::setTabIcon(const QMimeType &mimeType) 1070 { 1071 int i = findTabIndex(sender()); 1072 if (i != -1) { 1073 m_tabWidget->setTabIcon(i, QIcon::fromTheme(mimeType.iconName())); 1074 } 1075 } 1076 1077 int Shell::findTabIndex(QObject *sender) const 1078 { 1079 for (int i = 0; i < m_tabs.size(); ++i) { 1080 if (m_tabs[i].part == sender) { 1081 return i; 1082 } 1083 } 1084 return -1; 1085 } 1086 1087 int Shell::findTabIndex(const QUrl &url) const 1088 { 1089 auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&url](const TabState state) { return state.part->url() == url; }); 1090 return (it != m_tabs.end()) ? std::distance(m_tabs.begin(), it) : -1; 1091 } 1092 1093 void Shell::handleDroppedUrls(const QList<QUrl> &urls) 1094 { 1095 for (const QUrl &url : urls) { 1096 openUrl(url); 1097 } 1098 } 1099 1100 void Shell::moveTabData(int from, int to) 1101 { 1102 m_tabs.move(from, to); 1103 } 1104 1105 void Shell::slotFitWindowToPage(const QSize pageViewSize, const QSize pageSize) 1106 { 1107 const int xOffset = pageViewSize.width() - pageSize.width(); 1108 const int yOffset = pageViewSize.height() - pageSize.height(); 1109 showNormal(); 1110 resize(width() - xOffset, height() - yOffset); 1111 Q_EMIT moveSplitter(pageSize.width()); 1112 } 1113 1114 void Shell::hideWelcomeScreen() 1115 { 1116 m_sidebar->setVisible(m_showSidebarAction->isChecked()); 1117 m_centralStackedWidget->setCurrentWidget(m_tabWidget); 1118 m_showSidebarAction->setEnabled(true); 1119 } 1120 1121 void Shell::showWelcomeScreen() 1122 { 1123 m_showSidebarAction->setEnabled(false); 1124 m_centralStackedWidget->setCurrentWidget(m_welcomeScreen); 1125 m_sidebar->setVisible(false); 1126 1127 refreshRecentsOnWelcomeScreen(); 1128 } 1129 1130 void Shell::refreshRecentsOnWelcomeScreen() 1131 { 1132 saveRecents(); 1133 m_welcomeScreen->loadRecents(); 1134 } 1135 1136 void Shell::forgetRecentItem(QUrl const &url) 1137 { 1138 if (m_recent != nullptr) { 1139 m_recent->removeUrl(url); 1140 saveRecents(); 1141 refreshRecentsOnWelcomeScreen(); 1142 } 1143 } 1144 1145 #include "shell.moc" 1146 1147 /* kate: replace-tabs on; indent-width 4; */