diff --git a/interfaces/viewerinterface.h b/interfaces/viewerinterface.h index adec6a9c1..d9d616bcb 100644 --- a/interfaces/viewerinterface.h +++ b/interfaces/viewerinterface.h @@ -79,6 +79,13 @@ public: */ virtual bool openNewFilesInTabs() const = 0; + /** + * Returns the sidebar container. + * + * @since 23.04 + */ + virtual QWidget *getSideContainer() const = 0; + // SIGNALS /** * The signal 'openSourceReference' is emitted whenever the user has triggered a source diff --git a/part/part.cpp b/part/part.cpp index e23eb138b..280580acd 100644 --- a/part/part.cpp +++ b/part/part.cpp @@ -1107,6 +1107,11 @@ bool Part::openNewFilesInTabs() const return Okular::Settings::self()->shellOpenFileInTabs(); } +QWidget *Part::getSideContainer() const +{ + return m_sidebar->getSideContainer(); +} + bool Part::activateTabIfAlreadyOpenFile() const { return Okular::Settings::self()->switchToTabIfOpen(); diff --git a/part/part.h b/part/part.h index d8e86f2b1..c880d4747 100644 --- a/part/part.h +++ b/part/part.h @@ -147,6 +147,7 @@ public: bool areSourceLocationsShownGraphically() const override; void setShowSourceLocationsGraphically(bool show) override; bool openNewFilesInTabs() const override; + QWidget *getSideContainer() const override; Q_INVOKABLE bool activateTabIfAlreadyOpenFile() const; void setModified(bool modified) override; diff --git a/part/sidebar.cpp b/part/sidebar.cpp index f71750cd6..8c74ddacf 100644 --- a/part/sidebar.cpp +++ b/part/sidebar.cpp @@ -166,6 +166,11 @@ void Sidebar::moveSplitter(int sideWidgetSize) d->splitter->setSizes(splitterSizeList); } +QWidget *Sidebar::getSideContainer() const +{ + return d->sideContainer; +} + void Sidebar::splitterMoved(int /*pos*/, int index) { // if the side panel has been resized, save splitter sizes diff --git a/part/sidebar.h b/part/sidebar.h index e2d392b52..09661e4d2 100644 --- a/part/sidebar.h +++ b/part/sidebar.h @@ -33,6 +33,8 @@ public: void moveSplitter(int sideWidgetSize); + QWidget *getSideContainer() const; + Q_SIGNALS: void urlsDropped(const QList &urls); diff --git a/shell/shell.cpp b/shell/shell.cpp index 585e3059c..e87bdb33e 100644 --- a/shell/shell.cpp +++ b/shell/shell.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -62,6 +63,86 @@ static const char *shouldShowToolBarComingFromFullScreen = "shouldShowToolBarCom static const char *const SESSION_URL_KEY = "Urls"; static const char *const SESSION_TAB_KEY = "ActiveTab"; +static constexpr char SIDEBAR_LOCKED_KEY[] = "LockSidebar"; +static constexpr char SIDEBAR_VISIBLE_KEY[] = "ShowSidebar"; + +/** + * Groups sidebar containers in a QDockWidget. + * + * This control groups all the sidebar containers provided by each tab (the Part object), + * allowing the user to dock it to the left and right sides of the window, + * or detach it from the window altogether. + */ +class Sidebar : public QDockWidget +{ + Q_OBJECT + +public: + explicit Sidebar(QWidget *parent = nullptr) + : QDockWidget(parent) + { + setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); + setFeatures(defaultFeatures()); + + m_stackedWidget = new QStackedWidget; + setWidget(m_stackedWidget); + } + + bool isLocked() const + { + return features().testFlag(NoDockWidgetFeatures); + } + + void setLocked(bool locked) + { + setFeatures(locked ? NoDockWidgetFeatures : defaultFeatures()); + + // show titlebar only if not locked + if (locked) { + if (!m_dumbTitleWidget) { + m_dumbTitleWidget = new QWidget; + } + setTitleBarWidget(m_dumbTitleWidget); + } else { + setTitleBarWidget(nullptr); + } + } + + int indexOf(QWidget *widget) const + { + return m_stackedWidget->indexOf(widget); + } + + void addWidget(QWidget *widget) + { + m_stackedWidget->addWidget(widget); + } + + void removeWidget(QWidget *widget) + { + m_stackedWidget->removeWidget(widget); + } + + void setCurrentWidget(QWidget *widget) + { + m_stackedWidget->setCurrentWidget(widget); + } + +private: + static DockWidgetFeatures defaultFeatures() + { + DockWidgetFeatures dockFeatures = DockWidgetClosable | DockWidgetMovable; + if (!KWindowSystem::isPlatformWayland()) { // TODO : Remove this check when QTBUG-87332 is fixed + dockFeatures |= DockWidgetFloatable; + } + + return dockFeatures; + } + + QStackedWidget *m_stackedWidget = nullptr; + QWidget *m_dumbTitleWidget = nullptr; +}; + Shell::Shell(const QString &serializedOptions) : KParts::MainWindow() , m_menuBarWasShown(true) @@ -126,12 +207,27 @@ Shell::Shell(const QString &serializedOptions) connect(m_tabWidget, &QTabWidget::tabCloseRequested, this, &Shell::closeTab); connect(m_tabWidget->tabBar(), &QTabBar::tabMoved, this, &Shell::moveTabData); + m_sidebar = new Sidebar; + m_sidebar->setObjectName(QStringLiteral("okular_sidebar")); + m_sidebar->setContextMenuPolicy(Qt::ActionsContextMenu); + m_sidebar->setWindowTitle(i18n("Sidebar")); + connect(m_sidebar, &QDockWidget::visibilityChanged, this, [this](bool visible) { + // sync sidebar visibility with the m_showSidebarAction only if welcome screen is hidden + if (m_showSidebarAction && m_centralStackedWidget->currentWidget() != m_welcomeScreen) { + m_showSidebarAction->setChecked(visible); + } + }); + addDockWidget(Qt::LeftDockWidgetArea, m_sidebar); + // then, setup our actions setupActions(); connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &QObject::deleteLater); // and integrate the part's GUI with the shell's setupGUI(Keys | ToolBar | Save); + // NOTE : apply default sidebar width only after calling setupGUI(...) + resizeDocks({m_sidebar}, {200}, Qt::Horizontal); + m_tabs.append(TabState(firstPart)); m_tabWidget->addTab(firstPart->widget(), QString()); // triggers setActiveTab that calls createGUI( part ) @@ -367,11 +463,25 @@ void Shell::readSettings() m_menuBarWasShown = group.readEntry(shouldShowMenuBarComingFromFullScreen, true); m_toolBarWasShown = group.readEntry(shouldShowToolBarComingFromFullScreen, true); } + + const KConfigGroup sidebarGroup = KSharedConfig::openConfig()->group("General"); + m_sidebar->setVisible(sidebarGroup.readEntry(SIDEBAR_VISIBLE_KEY, true)); + m_sidebar->setLocked(sidebarGroup.readEntry(SIDEBAR_LOCKED_KEY, true)); + + m_showSidebarAction->setChecked(m_sidebar->isVisibleTo(this)); + m_lockSidebarAction->setChecked(m_sidebar->isLocked()); } void Shell::writeSettings() { saveRecents(); + + KConfigGroup sidebarGroup = KSharedConfig::openConfig()->group("General"); + sidebarGroup.writeEntry(SIDEBAR_LOCKED_KEY, m_sidebar->isLocked()); + // NOTE : Consider whether the m_showSidebarAction is checked, because + // the sidebar can be forcibly hidden if the welcome screen is displayed + sidebarGroup.writeEntry(SIDEBAR_VISIBLE_KEY, m_sidebar->isVisibleTo(this) || m_showSidebarAction->isChecked()); + KConfigGroup group = KSharedConfig::openConfig()->group("Desktop Entry"); group.writeEntry("FullScreen", m_fullScreenAction->isChecked()); if (m_fullScreenAction->isChecked()) { @@ -425,6 +535,19 @@ void Shell::setupActions() m_undoCloseTab->setIcon(QIcon::fromTheme(QStringLiteral("edit-undo"))); m_undoCloseTab->setEnabled(false); connect(m_undoCloseTab, &QAction::triggered, this, &Shell::undoCloseTab); + + m_showSidebarAction = actionCollection()->addAction(QStringLiteral("okular_show_sidebar")); + m_showSidebarAction->setCheckable(true); + m_showSidebarAction->setIcon(QIcon::fromTheme(QStringLiteral("sidebar-show-symbolic"))); + m_showSidebarAction->setText(i18n("Show Sidebar")); + connect(m_showSidebarAction, &QAction::triggered, m_sidebar, &Sidebar::setVisible); + + m_lockSidebarAction = actionCollection()->addAction(QStringLiteral("okular_lock_sidebar")); + m_lockSidebarAction->setCheckable(true); + m_lockSidebarAction->setIcon(QIcon::fromTheme(QStringLiteral("lock"))); + m_lockSidebarAction->setText(i18n("Lock Sidebar")); + connect(m_lockSidebarAction, &QAction::triggered, m_sidebar, &Sidebar::setLocked); + m_sidebar->addAction(m_lockSidebarAction); } void Shell::saveProperties(KConfigGroup &group) @@ -673,7 +796,24 @@ bool Shell::queryClose() void Shell::setActiveTab(int tab) { m_tabWidget->setCurrentIndex(tab); + + // NOTE : createGUI(...) breaks the visibility of the sidebar, so we need + // to save and restore it + const bool isSidebarVisible = m_sidebar->isVisible(); createGUI(m_tabs[tab].part); + m_sidebar->setVisible(isSidebarVisible); + + // dock KPart's sidebar if new and make it current + Okular::ViewerInterface *iPart = qobject_cast(m_tabs[tab].part); + Q_ASSERT(iPart); + QWidget *sideContainer = iPart->getSideContainer(); + if (m_sidebar->indexOf(sideContainer) == -1) { + m_sidebar->addWidget(sideContainer); + if (m_sidebar->maximumWidth() > sideContainer->maximumWidth()) { + m_sidebar->setMaximumWidth(sideContainer->maximumWidth()); + } + } + m_sidebar->setCurrentWidget(sideContainer); m_printAction->setEnabled(m_tabs[tab].printEnabled); m_closeAction->setEnabled(m_tabs[tab].closeEnabled); @@ -689,6 +829,13 @@ void Shell::closeTab(int tab) part->factory()->removeClient(part); } part->disconnect(); + + Okular::ViewerInterface *iPart = qobject_cast(m_tabs[tab].part); + Q_ASSERT(iPart); + QWidget *sideContainer = iPart->getSideContainer(); + m_sidebar->removeWidget(sideContainer); + connect(part, &QObject::destroyed, sideContainer, &QObject::deleteLater); + part->deleteLater(); m_tabs.removeAt(tab); m_tabWidget->removeTab(tab); @@ -776,7 +923,7 @@ void Shell::applyOptionsToPart(QObject *part, const QString &serializedOptions) } } -void Shell::connectPart(QObject *part) +void Shell::connectPart(const KParts::ReadWritePart *part) { // We're abusing the fact we know the part is our part here connect(this, SIGNAL(moveSplitter(int)), part, SLOT(moveSplitter(int))); // clazy:exclude=old-style-connect @@ -788,6 +935,12 @@ void Shell::connectPart(QObject *part) // Otherwise the QSize,QSize gets turned into QSize, QSize that is not normalized signals and is slightly slower connect(part, SIGNAL(fitWindowToPage(QSize,QSize)), this, SLOT(slotFitWindowToPage(QSize,QSize))); // clazy:exclude=old-style-connect // clang-format on + + // since sidebar is now docked to main window, we use another action + // to show/hide it, so we should hide a similar KPart's action + if (QAction *action = part->actionCollection()->action(QStringLiteral("show_leftpanel"))) { + action->setVisible(false); + } } void Shell::print() @@ -903,12 +1056,17 @@ void Shell::slotFitWindowToPage(const QSize pageViewSize, const QSize pageSize) void Shell::hideWelcomeScreen() { + m_sidebar->setVisible(m_showSidebarAction->isChecked()); m_centralStackedWidget->setCurrentWidget(m_tabWidget); + m_showSidebarAction->setEnabled(true); } void Shell::showWelcomeScreen() { + m_showSidebarAction->setEnabled(false); m_centralStackedWidget->setCurrentWidget(m_welcomeScreen); + m_sidebar->setVisible(false); + refreshRecentsOnWelcomeScreen(); } @@ -927,4 +1085,6 @@ void Shell::forgetRecentItem(QUrl const &url) } } +#include "shell.moc" + /* kate: replace-tabs on; indent-width 4; */ diff --git a/shell/shell.h b/shell/shell.h index 2aec58bd1..951371f53 100644 --- a/shell/shell.h +++ b/shell/shell.h @@ -24,6 +24,7 @@ #include "welcomescreen.h" +class Sidebar; class KRecentFilesAction; class KToggleAction; class QTabWidget; @@ -155,7 +156,7 @@ private: void setupActions(); void openNewTab(const QUrl &url, const QString &serializedOptions); void applyOptionsToPart(QObject *part, const QString &serializedOptions); - void connectPart(QObject *part); + void connectPart(const KParts::ReadWritePart *part); int findTabIndex(QObject *sender) const; int findTabIndex(const QUrl &url) const; @@ -177,6 +178,7 @@ private: KToggleAction *m_openInTab; WelcomeScreen *m_welcomeScreen; QStackedWidget *m_centralStackedWidget; + Sidebar *m_sidebar = nullptr; struct TabState { explicit TabState(KParts::ReadWritePart *p) @@ -194,6 +196,8 @@ private: QAction *m_nextTabAction; QAction *m_prevTabAction; QAction *m_undoCloseTab; + QAction *m_showSidebarAction = nullptr; + QAction *m_lockSidebarAction = nullptr; #ifndef Q_OS_WIN KActivities::ResourceInstance *m_activityResource; diff --git a/shell/shell.rc b/shell/shell.rc index 97a948700..0600f5d7a 100644 --- a/shell/shell.rc +++ b/shell/shell.rc @@ -1,6 +1,6 @@ - + @@ -13,6 +13,7 @@ + @@ -21,8 +22,12 @@ + Main Toolbar + + +