diff --git a/CMakeLists.txt b/CMakeLists.txt index 883efd0d..64c9aafa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,156 +1,158 @@ cmake_minimum_required (VERSION 3.0 FATAL_ERROR) # KDE Application Version, managed by release script set (KDE_APPLICATIONS_VERSION_MAJOR "18") set (KDE_APPLICATIONS_VERSION_MINOR "03") set (KDE_APPLICATIONS_VERSION_MICRO "70") set (KDE_APPLICATIONS_VERSION "${KDE_APPLICATIONS_VERSION_MAJOR}.${KDE_APPLICATIONS_VERSION_MINOR}.${KDE_APPLICATIONS_VERSION_MICRO}") project(gwenview VERSION ${KDE_APPLICATIONS_VERSION}) set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake ) set (QT_MIN_VERSION "5.6.0") option(RUN_UNINSTALL "test run uninstalled apps (experimental)" FALSE) set(ECM_VERSION 1.7.0) # ECM setup if (RUN_UNINSTALL) set(ECM_VERSION "5.38.0") endif() find_package(ECM ${ECM_VERSION} REQUIRED NO_MODULE) # Ccache support if (UNIX OR APPLE) find_program(CCACHE_FOUND ccache) set(CCACHE_SUPPORT OFF CACHE BOOL "Enable ccache support") if (CCACHE_FOUND AND CCACHE_SUPPORT MATCHES ON) set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache) set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ccache) endif () endif () set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR}) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(ECMOptionalAddSubdirectory) include(ECMInstallIcons) include(ECMSetupVersion) include(ECMMarkNonGuiExecutable) include(ECMGenerateHeaders) include(ECMAddAppIcon) include(GenerateExportHeader) include(FeatureSummary) ## Generate header with version number ecm_setup_version(${KDE_APPLICATIONS_VERSION} VARIABLE_PREFIX GWENVIEW VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/lib/gwenview_version.h" ) ## CMake options set(GWENVIEW_SEMANTICINFO_BACKEND_NONE OFF) set(GWENVIEW_SEMANTICINFO_BACKEND_FAKE OFF) set(GWENVIEW_SEMANTICINFO_BACKEND_BALOO OFF) set(GWENVIEW_SEMANTICINFO_BACKEND "Baloo" CACHE STRING "Semantic info backend for Gwenview (Baloo/Fake/None)") # Init GWENVIEW_SEMANTICINFO_BACKEND_* vars if(GWENVIEW_SEMANTICINFO_BACKEND STREQUAL "None") set(GWENVIEW_SEMANTICINFO_BACKEND_NONE ON) elseif(GWENVIEW_SEMANTICINFO_BACKEND STREQUAL "Fake") set(GWENVIEW_SEMANTICINFO_BACKEND_FAKE ON) else() set(GWENVIEW_SEMANTICINFO_BACKEND_BALOO ON) endif() find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED Core Widgets Concurrent Svg OpenGL PrintSupport) +find_package(Qt5DBus ${QT_MIN_VERSION} CONFIG QUIET) +set(HAVE_QTDBUS ${Qt5DBus_FOUND}) find_package(Phonon4Qt5 4.6.60 NO_MODULE REQUIRED) include_directories(BEFORE ${PHONON_INCLUDES}) add_definitions(-DPHONON_LIB_SONAME=\"${PHONON_LIB_SONAME}\") find_package(KF5 5.25 REQUIRED COMPONENTS KIO Activities ItemModels I18n DocTools Parts WindowSystem IconThemes Notifications ) ## Dependencies find_package(JPEG) set_package_properties(JPEG PROPERTIES URL "http://libjpeg.sourceforge.net/" DESCRIPTION "JPEG image manipulation support" TYPE REQUIRED) find_package(PNG) set_package_properties(PNG PROPERTIES URL "http://www.libpng.org" DESCRIPTION "PNG image manipulation support" TYPE REQUIRED) find_package(Exiv2) set_package_properties(Exiv2 PROPERTIES URL "http://www.exiv2.org" DESCRIPTION "image metadata support" TYPE REQUIRED) find_package(CFitsio) set_package_properties(CFitsio PROPERTIES URL "http://heasarc.gsfc.nasa.gov/fitsio/fitsio.html" DESCRIPTION "FITS format support" TYPE OPTIONAL) if(CFITSIO_FOUND) set(HAVE_FITS true) endif() find_package(KF5Kipi) if (KF5Kipi_FOUND) set(KIPI_FOUND true) endif() # set_package_properties(Kipi PROPERTIES URL"http://www.kipi-plugins.org" DESCRIPTION "Provides various image manipulation and export features" TYPE OPTIONAL) find_package(LCMS2) set_package_properties(LCMS2 PROPERTIES URL "http://www.littlecms.com" DESCRIPTION "Color management engine" TYPE REQUIRED) if (GWENVIEW_SEMANTICINFO_BACKEND_BALOO) find_package(KF5Baloo 5.1.90) set_package_properties(KF5Baloo PROPERTIES URL "https://projects.kde.org/projects/kde/kdelibs/baloo" DESCRIPTION "Desktop-wide semantic information support" TYPE OPTIONAL) if (NOT KF5Baloo_FOUND) message (STATUS "You have selected Baloo for semantic backend, but required version was not found. Overriding the backend to None") unset(GWENVIEW_SEMANTICINFO_BACKEND_BALOO) set(GWENVIEW_SEMANTICINFO_BACKEND_NONE ON) endif () endif () find_package(KF5KDcraw) if (KF5KDcraw_FOUND) add_definitions(-DKDCRAW_FOUND) endif() #set_package_properties(KDCRAW_FOUND PROPERTIES URL "http://www.kde.org" DESCRIPTION "KDE Dcraw library" TYPE OPTIONAL) find_package(X11) if(X11_FOUND) find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED X11Extras) set(HAVE_X11 1) endif() configure_file(config-gwenview.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-gwenview.h) include_directories(${CMAKE_CURRENT_BINARY_DIR}) add_definitions(-DQT_NO_URL_CAST_FROM_STRING) ## dirs to build add_subdirectory(lib) add_subdirectory(app) add_subdirectory(importer) add_subdirectory(part) add_subdirectory(tests) add_subdirectory(icons) add_subdirectory(images) add_subdirectory(cursors) add_subdirectory(color-schemes) add_subdirectory(doc) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/app/mainwindow.cpp b/app/mainwindow.cpp index 30d751d3..8eddade1 100644 --- a/app/mainwindow.cpp +++ b/app/mainwindow.cpp @@ -1,1657 +1,1670 @@ /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "mainwindow.h" #include #include "dialogguard.h" // Qt #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_OSX #include #endif // KDE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Local #include "configdialog.h" #include "documentinfoprovider.h" #include "viewmainpage.h" #include "fileopscontextmanageritem.h" #include "folderviewcontextmanageritem.h" #include "fullscreencontent.h" #include "gvcore.h" #include "imageopscontextmanageritem.h" #include "infocontextmanageritem.h" #ifdef KIPI_FOUND #include "kipiexportaction.h" #include "kipiinterface.h" #endif #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE #include "semanticinfocontextmanageritem.h" #endif #include "preloader.h" #include "savebar.h" #include "sidebar.h" #include "splitter.h" #include "startmainpage.h" #include "thumbnailviewhelper.h" #include "browsemainpage.h" #include #include #include #include #include #include #include #include #include +#ifdef HAVE_QTDBUS +#include +#endif #include #include #include #include #include #include #include #include namespace Gwenview { #undef ENABLE_LOG #undef LOG //#define ENABLE_LOG #ifdef ENABLE_LOG #define LOG(x) qDebug() << x #else #define LOG(x) ; #endif static const int BROWSE_PRELOAD_DELAY = 1000; static const int VIEW_PRELOAD_DELAY = 100; static const char* SESSION_CURRENT_PAGE_KEY = "Page"; static const char* SESSION_URL_KEY = "Url"; enum MainPageId { StartMainPageId, BrowseMainPageId, ViewMainPageId }; struct MainWindowState { bool mToolBarVisible; }; /* Layout of the main window looks like this: .-mCentralSplitter-----------------------------. |.-mSideBar--. .-mContentWidget---------------.| || | |.-mSaveBar-------------------.|| || | || ||| || | |'----------------------------'|| || | |.-mViewStackedWidget---------.|| || | || ||| || | || ||| || | || ||| || | || ||| || | |'----------------------------'|| |'-----------' '------------------------------'| '----------------------------------------------' */ struct MainWindow::Private { GvCore* mGvCore; MainWindow* q; QSplitter* mCentralSplitter; QWidget* mContentWidget; ViewMainPage* mViewMainPage; KUrlNavigator* mUrlNavigator; ThumbnailView* mThumbnailView; ThumbnailView* mActiveThumbnailView; DocumentInfoProvider* mDocumentInfoProvider; ThumbnailViewHelper* mThumbnailViewHelper; QPointer mThumbnailProvider; BrowseMainPage* mBrowseMainPage; StartMainPage* mStartMainPage; SideBar* mSideBar; QStackedWidget* mViewStackedWidget; FullScreenContent* mFullScreenContent; SaveBar* mSaveBar; bool mStartSlideShowWhenDirListerCompleted; SlideShow* mSlideShow; +#ifdef HAVE_QTDBUS + Mpris2Service* mMpris2Service; +#endif Preloader* mPreloader; bool mPreloadDirectionIsForward; #ifdef KIPI_FOUND KIPIInterface* mKIPIInterface; #endif QActionGroup* mViewModeActionGroup; KRecentFilesAction* mFileOpenRecentAction; QAction * mBrowseAction; QAction * mViewAction; QAction * mGoUpAction; QAction * mGoToPreviousAction; QAction * mGoToNextAction; QAction * mGoToFirstAction; QAction * mGoToLastAction; KToggleAction* mToggleSideBarAction; QAction* mFullScreenAction; QAction * mToggleSlideShowAction; KToggleAction* mShowMenuBarAction; KToggleAction* mShowStatusBarAction; #ifdef KIPI_FOUND KIPIExportAction* mKIPIExportAction; #endif SortedDirModel* mDirModel; DocumentOnlyProxyModel* mThumbnailBarModel; KLinkItemSelectionModel* mThumbnailBarSelectionModel; ContextManager* mContextManager; MainWindowState mStateBeforeFullScreen; QString mCaption; MainPageId mCurrentMainPageId; QDateTime mFullScreenLeftAt; KNotificationRestrictions* mNotificationRestrictions; void setupContextManager() { mContextManager = new ContextManager(mDirModel, q); connect(mContextManager, SIGNAL(selectionChanged()), q, SLOT(slotSelectionChanged())); connect(mContextManager, SIGNAL(currentDirUrlChanged(QUrl)), q, SLOT(slotCurrentDirUrlChanged(QUrl))); } void setupWidgets() { mFullScreenContent = new FullScreenContent(q, mGvCore); connect(mContextManager, SIGNAL(currentUrlChanged(QUrl)), mFullScreenContent, SLOT(setCurrentUrl(QUrl))); mCentralSplitter = new Splitter(Qt::Horizontal, q); q->setCentralWidget(mCentralSplitter); // Left side of splitter mSideBar = new SideBar(mCentralSplitter); // Right side of splitter mContentWidget = new QWidget(mCentralSplitter); mSaveBar = new SaveBar(mContentWidget, q->actionCollection()); connect(mContextManager, SIGNAL(currentUrlChanged(QUrl)), mSaveBar, SLOT(setCurrentUrl(QUrl))); mViewStackedWidget = new QStackedWidget(mContentWidget); QVBoxLayout* layout = new QVBoxLayout(mContentWidget); layout->addWidget(mSaveBar); layout->addWidget(mViewStackedWidget); layout->setMargin(0); layout->setSpacing(0); //// mStartSlideShowWhenDirListerCompleted = false; mSlideShow = new SlideShow(q); connect(mContextManager, SIGNAL(currentUrlChanged(QUrl)), mSlideShow, SLOT(setCurrentUrl(QUrl))); setupThumbnailView(mViewStackedWidget); setupViewMainPage(mViewStackedWidget); setupStartMainPage(mViewStackedWidget); mViewStackedWidget->addWidget(mBrowseMainPage); mViewStackedWidget->addWidget(mViewMainPage); mViewStackedWidget->addWidget(mStartMainPage); mViewStackedWidget->setCurrentWidget(mBrowseMainPage); mCentralSplitter->setStretchFactor(0, 0); mCentralSplitter->setStretchFactor(1, 1); mThumbnailView->setFocus(); connect(mSaveBar, SIGNAL(requestSaveAll()), mGvCore, SLOT(saveAll())); connect(mSaveBar, SIGNAL(goToUrl(QUrl)), q, SLOT(goToUrl(QUrl))); connect(mSlideShow, SIGNAL(goToUrl(QUrl)), q, SLOT(goToUrl(QUrl))); } void setupThumbnailView(QWidget* parent) { Q_ASSERT(mContextManager); mBrowseMainPage = new BrowseMainPage(parent, q->actionCollection(), mGvCore); mThumbnailView = mBrowseMainPage->thumbnailView(); mThumbnailView->setSelectionModel(mContextManager->selectionModel()); mUrlNavigator = mBrowseMainPage->urlNavigator(); mDocumentInfoProvider = new DocumentInfoProvider(mDirModel); mThumbnailView->setDocumentInfoProvider(mDocumentInfoProvider); mThumbnailViewHelper = new ThumbnailViewHelper(mDirModel, q->actionCollection()); connect(mContextManager, SIGNAL(currentDirUrlChanged(QUrl)), mThumbnailViewHelper, SLOT(setCurrentDirUrl(QUrl))); mThumbnailView->setThumbnailViewHelper(mThumbnailViewHelper); mThumbnailBarSelectionModel = new KLinkItemSelectionModel(mThumbnailBarModel, mContextManager->selectionModel(), q); // Connect thumbnail view connect(mThumbnailView, SIGNAL(indexActivated(QModelIndex)), q, SLOT(slotThumbnailViewIndexActivated(QModelIndex))); // Connect delegate QAbstractItemDelegate* delegate = mThumbnailView->itemDelegate(); connect(delegate, SIGNAL(saveDocumentRequested(QUrl)), mGvCore, SLOT(save(QUrl))); connect(delegate, SIGNAL(rotateDocumentLeftRequested(QUrl)), mGvCore, SLOT(rotateLeft(QUrl))); connect(delegate, SIGNAL(rotateDocumentRightRequested(QUrl)), mGvCore, SLOT(rotateRight(QUrl))); connect(delegate, SIGNAL(showDocumentInFullScreenRequested(QUrl)), q, SLOT(showDocumentInFullScreen(QUrl))); connect(delegate, SIGNAL(setDocumentRatingRequested(QUrl,int)), mGvCore, SLOT(setRating(QUrl,int))); // Connect url navigator connect(mUrlNavigator, SIGNAL(urlChanged(QUrl)), q, SLOT(openDirUrl(QUrl))); } void setupViewMainPage(QWidget* parent) { mViewMainPage = new ViewMainPage(parent, mSlideShow, q->actionCollection(), mGvCore); connect(mViewMainPage, SIGNAL(captionUpdateRequested(QString)), q, SLOT(slotUpdateCaption(QString))); connect(mViewMainPage, SIGNAL(completed()), q, SLOT(slotPartCompleted())); connect(mViewMainPage, SIGNAL(previousImageRequested()), q, SLOT(goToPrevious())); connect(mViewMainPage, SIGNAL(nextImageRequested()), q, SLOT(goToNext())); setupThumbnailBar(mViewMainPage->thumbnailBar()); } void setupThumbnailBar(ThumbnailView* bar) { Q_ASSERT(mThumbnailBarModel); Q_ASSERT(mThumbnailBarSelectionModel); Q_ASSERT(mDocumentInfoProvider); Q_ASSERT(mThumbnailViewHelper); bar->setModel(mThumbnailBarModel); bar->setSelectionModel(mThumbnailBarSelectionModel); bar->setDocumentInfoProvider(mDocumentInfoProvider); bar->setThumbnailViewHelper(mThumbnailViewHelper); } void setupStartMainPage(QWidget* parent) { mStartMainPage = new StartMainPage(parent, mGvCore); connect(mStartMainPage, SIGNAL(urlSelected(QUrl)), q, SLOT(slotStartMainPageUrlSelected(QUrl))); } void installDisabledActionShortcutMonitor(QAction* action, const char* slot) { DisabledActionShortcutMonitor* monitor = new DisabledActionShortcutMonitor(action, q); connect(monitor, SIGNAL(activated()), q, slot); } void setupActions() { KActionCollection* actionCollection = q->actionCollection(); KActionCategory* file = new KActionCategory(i18nc("@title actions category", "File"), actionCollection); KActionCategory* view = new KActionCategory(i18nc("@title actions category - means actions changing smth in interface", "View"), actionCollection); file->addAction(KStandardAction::Save, q, SLOT(saveCurrent())); file->addAction(KStandardAction::SaveAs, q, SLOT(saveCurrentAs())); file->addAction(KStandardAction::Open, q, SLOT(openFile())); mFileOpenRecentAction = KStandardAction::openRecent(q, SLOT(openUrl(QUrl)), q); file->addAction("file_open_recent", mFileOpenRecentAction); file->addAction(KStandardAction::Print, q, SLOT(print())); file->addAction(KStandardAction::Quit, qApp, SLOT(closeAllWindows())); QAction * action = file->addAction("reload", q, SLOT(reload())); action->setText(i18nc("@action reload the currently viewed image", "Reload")); action->setIcon(QIcon::fromTheme("view-refresh")); actionCollection->setDefaultShortcuts(action, KStandardShortcut::reload()); mBrowseAction = view->addAction("browse"); mBrowseAction->setText(i18nc("@action:intoolbar Switch to file list", "Browse")); mBrowseAction->setToolTip(i18nc("@info:tooltip", "Browse folders for images")); mBrowseAction->setCheckable(true); mBrowseAction->setIcon(QIcon::fromTheme("view-list-icons")); actionCollection->setDefaultShortcut(mBrowseAction, Qt::Key_Escape); connect(mViewMainPage, SIGNAL(goToBrowseModeRequested()), mBrowseAction, SLOT(trigger())); mViewAction = view->addAction("view"); mViewAction->setText(i18nc("@action:intoolbar Switch to image view", "View")); mViewAction->setToolTip(i18nc("@info:tooltip", "View selected images")); mViewAction->setIcon(QIcon::fromTheme("view-preview")); mViewAction->setCheckable(true); mViewModeActionGroup = new QActionGroup(q); mViewModeActionGroup->addAction(mBrowseAction); mViewModeActionGroup->addAction(mViewAction); connect(mViewModeActionGroup, SIGNAL(triggered(QAction*)), q, SLOT(setActiveViewModeAction(QAction*))); mFullScreenAction = KStandardAction::fullScreen(q, &MainWindow::toggleFullScreen, q, actionCollection); QList shortcuts = mFullScreenAction->shortcuts(); shortcuts.append(QKeySequence(Qt::Key_F11)); actionCollection->setDefaultShortcuts(mFullScreenAction, shortcuts); connect(mViewMainPage, SIGNAL(toggleFullScreenRequested()), mFullScreenAction, SLOT(trigger())); QAction * leaveFullScreenAction = view->addAction("leave_fullscreen", q, SLOT(leaveFullScreen())); leaveFullScreenAction->setIcon(QIcon::fromTheme("view-restore")); leaveFullScreenAction->setPriority(QAction::LowPriority); leaveFullScreenAction->setText(i18nc("@action", "Leave Fullscreen Mode")); mGoToPreviousAction = view->addAction("go_previous", q, SLOT(goToPrevious())); mGoToPreviousAction->setPriority(QAction::LowPriority); mGoToPreviousAction->setIcon(QIcon::fromTheme("go-previous-view")); mGoToPreviousAction->setText(i18nc("@action Go to previous image", "Previous")); mGoToPreviousAction->setToolTip(i18nc("@info:tooltip", "Go to previous image")); actionCollection->setDefaultShortcut(mGoToPreviousAction, Qt::Key_Backspace); installDisabledActionShortcutMonitor(mGoToPreviousAction, SLOT(showFirstDocumentReached())); mGoToNextAction = view->addAction("go_next", q, SLOT(goToNext())); mGoToNextAction->setPriority(QAction::LowPriority); mGoToNextAction->setIcon(QIcon::fromTheme("go-next-view")); mGoToNextAction->setText(i18nc("@action Go to next image", "Next")); mGoToNextAction->setToolTip(i18nc("@info:tooltip", "Go to next image")); actionCollection->setDefaultShortcut(mGoToNextAction, Qt::Key_Space); installDisabledActionShortcutMonitor(mGoToNextAction, SLOT(showLastDocumentReached())); mGoToFirstAction = view->addAction("go_first", q, SLOT(goToFirst())); mGoToFirstAction->setPriority(QAction::LowPriority); mGoToFirstAction->setIcon(QIcon::fromTheme("go-first-view")); mGoToFirstAction->setText(i18nc("@action Go to first image", "First")); mGoToFirstAction->setToolTip(i18nc("@info:tooltip", "Go to first image")); actionCollection->setDefaultShortcut(mGoToFirstAction, Qt::Key_Home); mGoToLastAction = view->addAction("go_last", q, SLOT(goToLast())); mGoToLastAction->setPriority(QAction::LowPriority); mGoToLastAction->setIcon(QIcon::fromTheme("go-last-view")); mGoToLastAction->setText(i18nc("@action Go to last image", "Last")); mGoToLastAction->setToolTip(i18nc("@info:tooltip", "Go to last image")); actionCollection->setDefaultShortcut(mGoToLastAction, Qt::Key_End); mPreloadDirectionIsForward = true; mGoUpAction = view->addAction(KStandardAction::Up, q, SLOT(goUp())); action = view->addAction("go_start_page", q, SLOT(showStartMainPage())); action->setPriority(QAction::LowPriority); action->setIcon(QIcon::fromTheme("go-home")); action->setText(i18nc("@action", "Start Page")); action->setToolTip(i18nc("@info:tooltip", "Open the start page")); mToggleSideBarAction = view->add("toggle_sidebar"); connect(mToggleSideBarAction, &KToggleAction::triggered, q, &MainWindow::toggleSideBar); mToggleSideBarAction->setIcon(QIcon::fromTheme("view-sidetree")); actionCollection->setDefaultShortcut(mToggleSideBarAction, Qt::Key_F4); mToggleSideBarAction->setText(i18nc("@action", "Sidebar")); connect(mBrowseMainPage->toggleSideBarButton(), SIGNAL(clicked()), mToggleSideBarAction, SLOT(trigger())); connect(mViewMainPage->toggleSideBarButton(), SIGNAL(clicked()), mToggleSideBarAction, SLOT(trigger())); mToggleSlideShowAction = view->addAction("toggle_slideshow", q, SLOT(toggleSlideShow())); q->updateSlideShowAction(); connect(mSlideShow, SIGNAL(stateChanged(bool)), q, SLOT(updateSlideShowAction())); q->setStandardToolBarMenuEnabled(true); mShowMenuBarAction = static_cast(view->addAction(KStandardAction::ShowMenubar, q, SLOT(toggleMenuBar()))); mShowStatusBarAction = static_cast(view->addAction(KStandardAction::ShowStatusbar, q, SLOT(toggleStatusBar(bool)))); actionCollection->setDefaultShortcut(mShowStatusBarAction, Qt::Key_F3); view->addAction(KStandardAction::KeyBindings, q->guiFactory(), SLOT(configureShortcuts())); view->addAction(KStandardAction::Preferences, q, SLOT(showConfigDialog())); view->addAction(KStandardAction::ConfigureToolbars, q, SLOT(configureToolbars())); #ifdef KIPI_FOUND mKIPIExportAction = new KIPIExportAction(q); actionCollection->addAction("kipi_export", mKIPIExportAction); #endif } void setupUndoActions() { // There is no KUndoGroup similar to KUndoStack. This code basically // does the same as KUndoStack, but for the KUndoGroup actions. QUndoGroup* undoGroup = DocumentFactory::instance()->undoGroup(); QAction* action; KActionCollection* actionCollection = q->actionCollection(); KActionCategory* edit = new KActionCategory(i18nc("@title actions category - means actions changing smth in interface", "Edit"), actionCollection); action = undoGroup->createRedoAction(actionCollection); action->setObjectName(KStandardAction::name(KStandardAction::Redo)); action->setIcon(QIcon::fromTheme("edit-redo")); action->setIconText(i18n("Redo")); actionCollection->setDefaultShortcuts(action, KStandardShortcut::redo()); edit->addAction(action->objectName(), action); action = undoGroup->createUndoAction(actionCollection); action->setObjectName(KStandardAction::name(KStandardAction::Undo)); action->setIcon(QIcon::fromTheme("edit-undo")); action->setIconText(i18n("Undo")); actionCollection->setDefaultShortcuts(action, KStandardShortcut::undo()); edit->addAction(action->objectName(), action); } void setupContextManagerItems() { Q_ASSERT(mContextManager); KActionCollection* actionCollection = q->actionCollection(); // Create context manager items FolderViewContextManagerItem* folderViewItem = new FolderViewContextManagerItem(mContextManager); connect(folderViewItem, &FolderViewContextManagerItem::urlChanged, q, &MainWindow::folderViewUrlChanged); InfoContextManagerItem* infoItem = new InfoContextManagerItem(mContextManager); #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE SemanticInfoContextManagerItem* semanticInfoItem = 0; semanticInfoItem = new SemanticInfoContextManagerItem(mContextManager, actionCollection, mViewMainPage); #endif ImageOpsContextManagerItem* imageOpsItem = new ImageOpsContextManagerItem(mContextManager, q); FileOpsContextManagerItem* fileOpsItem = new FileOpsContextManagerItem(mContextManager, mThumbnailView, actionCollection, q); // Fill sidebar SideBarPage* page; page = new SideBarPage(i18n("Folders")); page->setObjectName(QLatin1String("folders")); page->addWidget(folderViewItem->widget()); page->layout()->setMargin(0); mSideBar->addPage(page); page = new SideBarPage(i18n("Information")); page->setObjectName(QLatin1String("information")); page->addWidget(infoItem->widget()); #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE if (semanticInfoItem) { page->addWidget(semanticInfoItem->widget()); } #endif mSideBar->addPage(page); page = new SideBarPage(i18n("Operations")); page->setObjectName(QLatin1String("operations")); page->addWidget(imageOpsItem->widget()); page->addWidget(fileOpsItem->widget()); page->addStretch(); mSideBar->addPage(page); } void initDirModel() { mDirModel->setKindFilter( MimeTypeUtils::KIND_DIR | MimeTypeUtils::KIND_ARCHIVE | MimeTypeUtils::KIND_RASTER_IMAGE | MimeTypeUtils::KIND_SVG_IMAGE | MimeTypeUtils::KIND_VIDEO); connect(mDirModel, SIGNAL(rowsInserted(QModelIndex,int,int)), q, SLOT(slotDirModelNewItems())); connect(mDirModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), q, SLOT(updatePreviousNextActions())); connect(mDirModel, SIGNAL(modelReset()), q, SLOT(updatePreviousNextActions())); connect(mDirModel->dirLister(), SIGNAL(completed()), q, SLOT(slotDirListerCompleted())); } void setupThumbnailBarModel() { mThumbnailBarModel = new DocumentOnlyProxyModel(q); mThumbnailBarModel->setSourceModel(mDirModel); } bool indexIsDirOrArchive(const QModelIndex& index) const { Q_ASSERT(index.isValid()); KFileItem item = mDirModel->itemForIndex(index); return ArchiveUtils::fileItemIsDirOrArchive(item); } void goTo(const QModelIndex& index) { if (!index.isValid()) { return; } mThumbnailView->setCurrentIndex(index); mThumbnailView->scrollTo(index); } void goTo(int offset) { mPreloadDirectionIsForward = offset > 0; QModelIndex index = mContextManager->selectionModel()->currentIndex(); index = mDirModel->index(index.row() + offset, 0); if (index.isValid() && !indexIsDirOrArchive(index)) { goTo(index); } } void goToFirstDocument() { QModelIndex index; for (int row = 0;; ++row) { index = mDirModel->index(row, 0); if (!index.isValid()) { return; } if (!indexIsDirOrArchive(index)) { break; } } goTo(index); } void goToLastDocument() { QModelIndex index = mDirModel->index(mDirModel->rowCount() - 1, 0); goTo(index); } void setupFullScreenContent() { mFullScreenContent->init(q->actionCollection(), mViewMainPage, mSlideShow); setupThumbnailBar(mFullScreenContent->thumbnailBar()); } inline void setActionEnabled(const char* name, bool enabled) { QAction* action = q->actionCollection()->action(name); if (action) { action->setEnabled(enabled); } else { qWarning() << "Action" << name << "not found"; } } void setActionsDisabledOnStartMainPageEnabled(bool enabled) { mBrowseAction->setEnabled(enabled); mViewAction->setEnabled(enabled); mToggleSideBarAction->setEnabled(enabled); mShowStatusBarAction->setEnabled(enabled); mFullScreenAction->setEnabled(enabled); mToggleSlideShowAction->setEnabled(enabled); setActionEnabled("reload", enabled); setActionEnabled("go_start_page", enabled); setActionEnabled("add_folder_to_places", enabled); } void updateActions() { bool isRasterImage = false; bool canSave = false; bool isModified = false; const QUrl url = mContextManager->currentUrl(); if (url.isValid()) { isRasterImage = mContextManager->currentUrlIsRasterImage(); canSave = isRasterImage; isModified = DocumentFactory::instance()->load(url)->isModified(); if (mCurrentMainPageId != ViewMainPageId && mContextManager->selectedFileItemList().count() != 1) { // Saving only makes sense if exactly one image is selected canSave = false; } } KActionCollection* actionCollection = q->actionCollection(); actionCollection->action("file_save")->setEnabled(canSave && isModified); actionCollection->action("file_save_as")->setEnabled(canSave); actionCollection->action("file_print")->setEnabled(isRasterImage); } bool sideBarVisibility() const { switch (mCurrentMainPageId) { case StartMainPageId: GV_WARN_AND_RETURN_VALUE(false, "Sidebar not implemented on start page"); break; case BrowseMainPageId: return GwenviewConfig::sideBarVisibleBrowseMode(); break; case ViewMainPageId: return q->isFullScreen() ? GwenviewConfig::sideBarVisibleViewModeFullScreen() : GwenviewConfig::sideBarVisibleViewMode(); break; } return false; } void saveSideBarVisibility(const bool visible) { switch (mCurrentMainPageId) { case StartMainPageId: GV_WARN_AND_RETURN("Sidebar not implemented on start page"); break; case BrowseMainPageId: GwenviewConfig::setSideBarVisibleBrowseMode(visible); break; case ViewMainPageId: q->isFullScreen() ? GwenviewConfig::setSideBarVisibleViewModeFullScreen(visible) : GwenviewConfig::setSideBarVisibleViewMode(visible); break; } } bool statusBarVisibility() const { switch (mCurrentMainPageId) { case StartMainPageId: GV_WARN_AND_RETURN_VALUE(false, "Statusbar not implemented on start page"); break; case BrowseMainPageId: return GwenviewConfig::statusBarVisibleBrowseMode(); break; case ViewMainPageId: return q->isFullScreen() ? GwenviewConfig::statusBarVisibleViewModeFullScreen() : GwenviewConfig::statusBarVisibleViewMode(); break; } return false; } void saveStatusBarVisibility(const bool visible) { switch (mCurrentMainPageId) { case StartMainPageId: GV_WARN_AND_RETURN("Statusbar not implemented on start page"); break; case BrowseMainPageId: GwenviewConfig::setStatusBarVisibleBrowseMode(visible); break; case ViewMainPageId: q->isFullScreen() ? GwenviewConfig::setStatusBarVisibleViewModeFullScreen(visible) : GwenviewConfig::setStatusBarVisibleViewMode(visible); break; } } void setScreenSaverEnabled(bool enabled) { // Always delete mNotificationRestrictions, it does not hurt delete mNotificationRestrictions; if (!enabled) { mNotificationRestrictions = new KNotificationRestrictions(KNotificationRestrictions::ScreenSaver, q); } else { mNotificationRestrictions = 0; } } void assignThumbnailProviderToThumbnailView(ThumbnailView* thumbnailView) { GV_RETURN_IF_FAIL(thumbnailView); if (mActiveThumbnailView) { mActiveThumbnailView->setThumbnailProvider(0); } thumbnailView->setThumbnailProvider(mThumbnailProvider); mActiveThumbnailView = thumbnailView; if (mActiveThumbnailView->isVisible()) { mThumbnailProvider->stop(); mActiveThumbnailView->generateThumbnailsForItems(); } } void autoAssignThumbnailProvider() { if (mCurrentMainPageId == ViewMainPageId) { if (q->windowState() & Qt::WindowFullScreen) { assignThumbnailProviderToThumbnailView(mFullScreenContent->thumbnailBar()); } else { assignThumbnailProviderToThumbnailView(mViewMainPage->thumbnailBar()); } } else if (mCurrentMainPageId == BrowseMainPageId) { assignThumbnailProviderToThumbnailView(mThumbnailView); } else if (mCurrentMainPageId == StartMainPageId) { assignThumbnailProviderToThumbnailView(mStartMainPage->recentFoldersView()); } } }; MainWindow::MainWindow() : KXmlGuiWindow(), d(new MainWindow::Private) { d->q = this; d->mCurrentMainPageId = StartMainPageId; d->mDirModel = new SortedDirModel(this); d->setupContextManager(); d->setupThumbnailBarModel(); d->mGvCore = new GvCore(this, d->mDirModel); d->mPreloader = new Preloader(this); d->mNotificationRestrictions = 0; d->mThumbnailProvider = new ThumbnailProvider(); d->mActiveThumbnailView = 0; d->initDirModel(); d->setupWidgets(); d->setupActions(); d->setupUndoActions(); d->setupContextManagerItems(); d->setupFullScreenContent(); + +#ifdef HAVE_QTDBUS + d->mMpris2Service = new Mpris2Service(d->mSlideShow, d->mContextManager, + d->mToggleSlideShowAction, d->mFullScreenAction, + d->mGoToPreviousAction, d->mGoToNextAction, this); +#endif + d->updateActions(); updatePreviousNextActions(); d->mSaveBar->initActionDependentWidgets(); createGUI(); loadConfig(); connect(DocumentFactory::instance(), SIGNAL(modifiedDocumentListChanged()), SLOT(slotModifiedDocumentListChanged())); #ifdef KIPI_FOUND d->mKIPIInterface = new KIPIInterface(this); d->mKIPIExportAction->setKIPIInterface(d->mKIPIInterface); #endif setAutoSaveSettings(); #ifdef Q_OS_OSX qApp->installEventFilter(this); #endif } MainWindow::~MainWindow() { if (GwenviewConfig::deleteThumbnailCacheOnExit()) { QDir dir(ThumbnailProvider::thumbnailBaseDir()); if (dir.exists()) { dir.removeRecursively(); } } delete d->mThumbnailProvider; delete d; } ContextManager* MainWindow::contextManager() const { return d->mContextManager; } ViewMainPage* MainWindow::viewMainPage() const { return d->mViewMainPage; } void MainWindow::setCaption(const QString& caption) { // Keep a trace of caption to use it in slotModifiedDocumentListChanged() d->mCaption = caption; KXmlGuiWindow::setCaption(caption); } void MainWindow::setCaption(const QString& caption, bool modified) { d->mCaption = caption; KXmlGuiWindow::setCaption(caption, modified); } void MainWindow::slotUpdateCaption(const QString& caption) { const QUrl url = d->mContextManager->currentUrl(); const QList list = DocumentFactory::instance()->modifiedDocumentList(); setCaption(caption, list.contains(url)); } void MainWindow::slotModifiedDocumentListChanged() { d->updateActions(); slotUpdateCaption(d->mCaption); } void MainWindow::setInitialUrl(const QUrl &_url) { Q_ASSERT(_url.isValid()); QUrl url = UrlUtils::fixUserEnteredUrl(_url); if (UrlUtils::urlIsDirectory(url)) { d->mBrowseAction->trigger(); openDirUrl(url); } else { openUrl(url); } } void MainWindow::startSlideShow() { d->mViewAction->trigger(); // We need to wait until we have listed all images in the dirlister to // start the slideshow because the SlideShow objects needs an image list to // work. d->mStartSlideShowWhenDirListerCompleted = true; } void MainWindow::setActiveViewModeAction(QAction* action) { if (action == d->mViewAction) { d->mCurrentMainPageId = ViewMainPageId; // Switching to view mode d->mViewStackedWidget->setCurrentWidget(d->mViewMainPage); openSelectedDocuments(); d->mPreloadDirectionIsForward = true; QTimer::singleShot(VIEW_PRELOAD_DELAY, this, SLOT(preloadNextUrl())); } else { d->mCurrentMainPageId = BrowseMainPageId; // Switching to browse mode d->mViewStackedWidget->setCurrentWidget(d->mBrowseMainPage); if (!d->mViewMainPage->isEmpty() && KProtocolManager::supportsListing(d->mViewMainPage->url())) { // Reset the view to spare resources, but don't do it if we can't // browse the url, otherwise if the user starts Gwenview this way: // gwenview http://example.com/example.png // and switch to browse mode, switching back to view mode won't bring // his image back. d->mViewMainPage->reset(); } setCaption(QString()); } d->autoAssignThumbnailProvider(); toggleSideBar(d->sideBarVisibility()); toggleStatusBar(d->statusBarVisibility()); emit viewModeChanged(); } void MainWindow::slotThumbnailViewIndexActivated(const QModelIndex& index) { if (!index.isValid()) { return; } KFileItem item = d->mDirModel->itemForIndex(index); if (item.isDir()) { // Item is a dir, open it openDirUrl(item.url()); } else { QString protocol = ArchiveUtils::protocolForMimeType(item.mimetype()); if (!protocol.isEmpty()) { // Item is an archive, tweak url then open it QUrl url = item.url(); url.setScheme(protocol); openDirUrl(url); } else { // Item is a document, switch to view mode d->mViewAction->trigger(); } } } void MainWindow::openSelectedDocuments() { if (d->mCurrentMainPageId != ViewMainPageId) { return; } int count = 0; QList urls; const auto list = d->mContextManager->selectedFileItemList(); for (const auto &item : list) { if (!item.isNull() && !ArchiveUtils::fileItemIsDirOrArchive(item)) { urls << item.url(); ++count; if (count == ViewMainPage::MaxViewCount) { break; } } } if (urls.isEmpty()) { // Selection contains no fitting items // Switch back to browsing mode d->mBrowseAction->trigger(); d->mViewMainPage->reset(); return; } QUrl currentUrl = d->mContextManager->currentUrl(); if (currentUrl.isEmpty() || !urls.contains(currentUrl)) { // There is no current URL or it doesn't belong to selection // This can happen when user manually selects a group of items currentUrl = urls.first(); } d->mViewMainPage->openUrls(urls, currentUrl); } void MainWindow::goUp() { if (d->mCurrentMainPageId == BrowseMainPageId) { QUrl url = d->mContextManager->currentDirUrl(); url = KIO::upUrl(url); openDirUrl(url); } else { d->mBrowseAction->trigger(); } } void MainWindow::showStartMainPage() { d->mCurrentMainPageId = StartMainPageId; d->setActionsDisabledOnStartMainPageEnabled(false); d->mSideBar->hide(); d->mViewStackedWidget->setCurrentWidget(d->mStartMainPage); d->updateActions(); updatePreviousNextActions(); d->mContextManager->setCurrentDirUrl(QUrl()); d->mContextManager->setCurrentUrl(QUrl()); d->autoAssignThumbnailProvider(); } void MainWindow::slotStartMainPageUrlSelected(const QUrl &url) { d->setActionsDisabledOnStartMainPageEnabled(true); if (d->mBrowseAction->isChecked()) { // Silently uncheck the action so that setInitialUrl() does the right thing SignalBlocker blocker(d->mBrowseAction); d->mBrowseAction->setChecked(false); } setInitialUrl(url); } void MainWindow::openDirUrl(const QUrl &url) { const QUrl currentUrl = d->mContextManager->currentDirUrl(); if (url == currentUrl) { return; } if (url.isParentOf(currentUrl)) { // Keep first child between url and currentUrl selected // If currentPath is "/home/user/photos/2008/event" // and wantedPath is "/home/user/photos" // pathToSelect should be "/home/user/photos/2008" // To anyone considering using QUrl::toLocalFile() instead of // QUrl::path() here. Please don't, using QUrl::path() is the right // thing to do here. const QString currentPath = QDir::cleanPath(currentUrl.adjusted(QUrl::StripTrailingSlash).path()); const QString wantedPath = QDir::cleanPath(url.adjusted(QUrl::StripTrailingSlash).path()); const QChar separator('/'); const int slashCount = wantedPath.count(separator); const QString pathToSelect = currentPath.section(separator, 0, slashCount + 1); QUrl urlToSelect = url; urlToSelect.setPath(pathToSelect); d->mContextManager->setUrlToSelect(urlToSelect); } d->mThumbnailProvider->stop(); d->mContextManager->setCurrentDirUrl(url); d->mGvCore->addUrlToRecentFolders(url); d->mViewMainPage->reset(); } void MainWindow::folderViewUrlChanged(const QUrl &url) { const QUrl currentUrl = d->mContextManager->currentDirUrl(); if (url == currentUrl) { switch (d->mCurrentMainPageId) { case ViewMainPageId: d->mBrowseAction->trigger(); break; case BrowseMainPageId: d->mViewAction->trigger(); break; case StartMainPageId: break; } } else { openDirUrl(url); } } void MainWindow::toggleSideBar(bool visible) { d->mToggleSideBarAction->setChecked(visible); d->saveSideBarVisibility(visible); d->mSideBar->setVisible(visible); const QString text = QApplication::isRightToLeft() ? QString::fromUtf8(visible ? "▮→" : "▮←") : QString::fromUtf8(visible ? "▮←" : "▮→"); const QString toolTip = visible ? i18nc("@info:tooltip", "Hide sidebar") : i18nc("@info:tooltip", "Show sidebar"); const QList buttonList { d->mBrowseMainPage->toggleSideBarButton(), d->mViewMainPage->toggleSideBarButton() }; for (auto button : buttonList) { button->setText(text); button->setToolTip(toolTip); } } void MainWindow::toggleStatusBar(bool visible) { d->mShowStatusBarAction->setChecked(visible); d->saveStatusBarVisibility(visible); d->mViewMainPage->setStatusBarVisible(visible); d->mBrowseMainPage->setStatusBarVisible(visible); } void MainWindow::slotPartCompleted() { d->updateActions(); const QUrl url = d->mContextManager->currentUrl(); if (!url.isEmpty() && GwenviewConfig::historyEnabled()) { d->mFileOpenRecentAction->addUrl(url); d->mGvCore->addUrlToRecentFiles(url); } if (KProtocolManager::supportsListing(url)) { const QUrl dirUrl = d->mContextManager->currentDirUrl(); d->mGvCore->addUrlToRecentFolders(dirUrl); } } void MainWindow::slotSelectionChanged() { if (d->mCurrentMainPageId == ViewMainPageId) { // The user selected a new file in the thumbnail view, since the // document view is visible, let's show it openSelectedDocuments(); } else { // No document view, we need to load the document to set the undo group // of document factory to the correct QUndoStack QModelIndex index = d->mThumbnailView->currentIndex(); KFileItem item; if (index.isValid()) { item = d->mDirModel->itemForIndex(index); } QUndoGroup* undoGroup = DocumentFactory::instance()->undoGroup(); if (!item.isNull() && !ArchiveUtils::fileItemIsDirOrArchive(item)) { QUrl url = item.url(); Document::Ptr doc = DocumentFactory::instance()->load(url); undoGroup->addStack(doc->undoStack()); undoGroup->setActiveStack(doc->undoStack()); } else { undoGroup->setActiveStack(nullptr); } } // Update UI d->updateActions(); updatePreviousNextActions(); // Start preloading int preloadDelay = d->mCurrentMainPageId == ViewMainPageId ? VIEW_PRELOAD_DELAY : BROWSE_PRELOAD_DELAY; QTimer::singleShot(preloadDelay, this, SLOT(preloadNextUrl())); } void MainWindow::slotCurrentDirUrlChanged(const QUrl &url) { if (url.isValid()) { d->mUrlNavigator->setLocationUrl(url); d->mGoUpAction->setEnabled(url.path() != "/"); } else { d->mGoUpAction->setEnabled(false); } } void MainWindow::slotDirModelNewItems() { if (d->mContextManager->selectionModel()->hasSelection()) { updatePreviousNextActions(); } } void MainWindow::slotDirListerCompleted() { if (d->mStartSlideShowWhenDirListerCompleted) { d->mStartSlideShowWhenDirListerCompleted = false; QTimer::singleShot(0, d->mToggleSlideShowAction, SLOT(trigger())); } if (d->mContextManager->selectionModel()->hasSelection()) { updatePreviousNextActions(); } else { d->goToFirstDocument(); if (!d->mContextManager->selectionModel()->hasSelection()) { const QModelIndex index = d->mThumbnailView->model()->index(0, 0); if (index.isValid()) { d->mThumbnailView->setCurrentIndex(index); } } } d->mThumbnailView->scrollToSelectedIndex(); d->mViewMainPage->thumbnailBar()->scrollToSelectedIndex(); d->mFullScreenContent->thumbnailBar()->scrollToSelectedIndex(); } void MainWindow::goToPrevious() { d->goTo(-1); } void MainWindow::goToNext() { d->goTo(1); } void MainWindow::goToFirst() { d->goToFirstDocument(); } void MainWindow::goToLast() { d->goToLastDocument(); } void MainWindow::goToUrl(const QUrl &url) { if (d->mCurrentMainPageId == ViewMainPageId) { d->mViewMainPage->openUrl(url); } QUrl dirUrl = url; dirUrl = dirUrl.adjusted(QUrl::RemoveFilename); dirUrl.setPath(dirUrl.path() + ""); if (dirUrl != d->mContextManager->currentDirUrl()) { d->mContextManager->setCurrentDirUrl(dirUrl); d->mGvCore->addUrlToRecentFolders(dirUrl); } d->mContextManager->setUrlToSelect(url); } void MainWindow::updatePreviousNextActions() { bool hasPrevious; bool hasNext; QModelIndex currentIndex = d->mContextManager->selectionModel()->currentIndex(); if (currentIndex.isValid() && !d->indexIsDirOrArchive(currentIndex)) { int row = currentIndex.row(); QModelIndex prevIndex = d->mDirModel->index(row - 1, 0); QModelIndex nextIndex = d->mDirModel->index(row + 1, 0); hasPrevious = prevIndex.isValid() && !d->indexIsDirOrArchive(prevIndex); hasNext = nextIndex.isValid() && !d->indexIsDirOrArchive(nextIndex); } else { hasPrevious = false; hasNext = false; } d->mGoToPreviousAction->setEnabled(hasPrevious); d->mGoToNextAction->setEnabled(hasNext); d->mGoToFirstAction->setEnabled(hasPrevious); d->mGoToLastAction->setEnabled(hasNext); } void MainWindow::leaveFullScreen() { if (d->mFullScreenAction->isChecked()) { d->mFullScreenAction->trigger(); } } void MainWindow::toggleFullScreen(bool checked) { setUpdatesEnabled(false); if (checked) { // Save MainWindow config now, this way if we quit while in // fullscreen, we are sure latest MainWindow changes are remembered. KConfigGroup saveConfigGroup = autoSaveConfigGroup(); if (!isFullScreen()) { // Save state if window manager did not already switch to fullscreen. saveMainWindowSettings(saveConfigGroup); d->mStateBeforeFullScreen.mToolBarVisible = toolBar()->isVisible(); } setAutoSaveSettings(saveConfigGroup, false); resetAutoSaveSettings(); // Go full screen KToggleFullScreenAction::setFullScreen(this, true); menuBar()->hide(); toolBar()->hide(); qApp->setProperty("KDE_COLOR_SCHEME_PATH", d->mGvCore->fullScreenPaletteName()); QApplication::setPalette(d->mGvCore->palette(GvCore::FullScreenPalette)); d->setScreenSaverEnabled(false); } else { setAutoSaveSettings(); // Back to normal qApp->setProperty("KDE_COLOR_SCHEME_PATH", QVariant()); QApplication::setPalette(d->mGvCore->palette(GvCore::NormalPalette)); d->mSlideShow->stop(); KToggleFullScreenAction::setFullScreen(this, false); menuBar()->setVisible(d->mShowMenuBarAction->isChecked()); toolBar()->setVisible(d->mStateBeforeFullScreen.mToolBarVisible); d->setScreenSaverEnabled(true); // See resizeEvent d->mFullScreenLeftAt = QDateTime::currentDateTime(); } d->mFullScreenContent->setFullScreenMode(checked); d->mBrowseMainPage->setFullScreenMode(checked); d->mViewMainPage->setFullScreenMode(checked); d->mSaveBar->setFullScreenMode(checked); toggleSideBar(d->sideBarVisibility()); toggleStatusBar(d->statusBarVisibility()); setUpdatesEnabled(true); d->autoAssignThumbnailProvider(); } void MainWindow::saveCurrent() { d->mGvCore->save(d->mContextManager->currentUrl()); } void MainWindow::saveCurrentAs() { d->mGvCore->saveAs(d->mContextManager->currentUrl()); } void MainWindow::reload() { if (d->mCurrentMainPageId == ViewMainPageId) { d->mViewMainPage->reload(); } else { d->mBrowseMainPage->reload(); } } void MainWindow::openFile() { QUrl dirUrl = d->mContextManager->currentDirUrl(); DialogGuard dialog(this); dialog->selectUrl(dirUrl); dialog->setWindowTitle(i18nc("@title:window", "Open Image")); const QStringList mimeFilter = MimeTypeUtils::imageMimeTypes(); dialog->setMimeTypeFilters(mimeFilter); dialog->setAcceptMode(QFileDialog::AcceptOpen); if (!dialog->exec()) { return; } if (!dialog->selectedUrls().isEmpty()) { openUrl(dialog->selectedUrls().first()); } } void MainWindow::openUrl(const QUrl& url) { d->setActionsDisabledOnStartMainPageEnabled(true); d->mContextManager->setUrlToSelect(url); d->mViewAction->trigger(); } void MainWindow::showDocumentInFullScreen(const QUrl &url) { d->mContextManager->setUrlToSelect(url); d->mViewAction->trigger(); d->mFullScreenAction->trigger(); } void MainWindow::toggleSlideShow() { if (d->mSlideShow->isRunning()) { d->mSlideShow->stop(); } else { if (!d->mViewAction->isChecked()) { d->mViewAction->trigger(); } if (!d->mFullScreenAction->isChecked()) { d->mFullScreenAction->trigger(); } QList list; for (int pos = 0; pos < d->mDirModel->rowCount(); ++pos) { QModelIndex index = d->mDirModel->index(pos, 0); KFileItem item = d->mDirModel->itemForIndex(index); MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item); switch (kind) { case MimeTypeUtils::KIND_SVG_IMAGE: case MimeTypeUtils::KIND_RASTER_IMAGE: case MimeTypeUtils::KIND_VIDEO: list << item.url(); break; default: break; } } d->mSlideShow->start(list); } updateSlideShowAction(); } void MainWindow::updateSlideShowAction() { if (d->mSlideShow->isRunning()) { d->mToggleSlideShowAction->setText(i18n("Stop Slideshow")); d->mToggleSlideShowAction->setIcon(QIcon::fromTheme("media-playback-pause")); } else { d->mToggleSlideShowAction->setText(i18n("Start Slideshow")); d->mToggleSlideShowAction->setIcon(QIcon::fromTheme("media-playback-start")); } } bool MainWindow::queryClose() { saveConfig(); QList list = DocumentFactory::instance()->modifiedDocumentList(); if (list.size() == 0) { return true; } KGuiItem yes(i18n("Save All Changes"), "document-save-all"); KGuiItem no(i18n("Discard Changes"), "delete"); QString msg = i18np("One image has been modified.", "%1 images have been modified.", list.size()) + '\n' + i18n("If you quit now, your changes will be lost."); int answer = KMessageBox::warningYesNoCancel( this, msg, QString() /* caption */, yes, no); switch (answer) { case KMessageBox::Yes: d->mGvCore->saveAll(); // We need to wait a bit because the DocumentFactory is notified about // saved documents through a queued connection. qApp->processEvents(); return DocumentFactory::instance()->modifiedDocumentList().isEmpty(); case KMessageBox::No: return true; default: // cancel return false; } } void MainWindow::showConfigDialog() { // Save first so changes like thumbnail zoom level are not lost when reloading config saveConfig(); DialogGuard dialog(this); connect(dialog.data(), SIGNAL(settingsChanged(QString)), SLOT(loadConfig())); dialog->exec(); } void MainWindow::toggleMenuBar() { if (!d->mFullScreenAction->isChecked()) { menuBar()->setVisible(d->mShowMenuBarAction->isChecked()); } } void MainWindow::loadConfig() { d->mDirModel->setBlackListedExtensions(GwenviewConfig::blackListedExtensions()); d->mDirModel->adjustKindFilter(MimeTypeUtils::KIND_VIDEO, GwenviewConfig::listVideos()); if (GwenviewConfig::historyEnabled()) { d->mFileOpenRecentAction->loadEntries(KConfigGroup(KSharedConfig::openConfig(), "Recent Files")); foreach(const QUrl& url, d->mFileOpenRecentAction->urls()) { d->mGvCore->addUrlToRecentFiles(url); } } else { d->mFileOpenRecentAction->clear(); } d->mFileOpenRecentAction->setVisible(GwenviewConfig::historyEnabled()); d->mStartMainPage->loadConfig(); d->mViewMainPage->loadConfig(); d->mBrowseMainPage->loadConfig(); d->mContextManager->loadConfig(); d->mSideBar->loadConfig(); } void MainWindow::saveConfig() { d->mFileOpenRecentAction->saveEntries(KConfigGroup(KSharedConfig::openConfig(), "Recent Files")); d->mViewMainPage->saveConfig(); d->mBrowseMainPage->saveConfig(); d->mContextManager->saveConfig(); GwenviewConfig::setFullScreenModeActive(isFullScreen()); } void MainWindow::print() { if (!d->mContextManager->currentUrlIsRasterImage()) { return; } Document::Ptr doc = DocumentFactory::instance()->load(d->mContextManager->currentUrl()); PrintHelper printHelper(this); printHelper.print(doc); } void MainWindow::preloadNextUrl() { static bool disablePreload = qgetenv("GV_MAX_UNREFERENCED_IMAGES") == "0"; if (disablePreload) { qDebug() << "Preloading disabled"; return; } QItemSelection selection = d->mContextManager->selectionModel()->selection(); if (selection.size() != 1) { return; } QModelIndexList indexList = selection.indexes(); if (indexList.isEmpty()) { return; } QModelIndex index = indexList.at(0); if (!index.isValid()) { return; } if (d->mCurrentMainPageId == ViewMainPageId) { // If we are in view mode, preload the next url, otherwise preload the // selected one int offset = d->mPreloadDirectionIsForward ? 1 : -1; index = d->mDirModel->sibling(index.row() + offset, index.column(), index); if (!index.isValid()) { return; } } KFileItem item = d->mDirModel->itemForIndex(index); if (!ArchiveUtils::fileItemIsDirOrArchive(item)) { QUrl url = item.url(); if (url.isLocalFile()) { QSize size = d->mViewStackedWidget->size(); d->mPreloader->preload(url, size); } } } QSize MainWindow::sizeHint() const { return KXmlGuiWindow::sizeHint().expandedTo(QSize(750, 500)); } void MainWindow::showEvent(QShowEvent *event) { // We need to delay initializing the action state until the menu bar has // been initialized, that's why it's done only in the showEvent() d->mShowMenuBarAction->setChecked(menuBar()->isVisible()); KXmlGuiWindow::showEvent(event); } void MainWindow::resizeEvent(QResizeEvent* event) { KXmlGuiWindow::resizeEvent(event); // This is a small hack to execute code after leaving fullscreen, and after // the window has been resized back to its former size. if (d->mFullScreenLeftAt.isValid() && d->mFullScreenLeftAt.secsTo(QDateTime::currentDateTime()) < 2) { if (d->mCurrentMainPageId == BrowseMainPageId) { d->mThumbnailView->scrollToSelectedIndex(); } d->mFullScreenLeftAt = QDateTime(); } } bool MainWindow::eventFilter(QObject *obj, QEvent *event) { Q_UNUSED(obj); Q_UNUSED(event); #ifdef Q_OS_OSX /** * handle Mac OS X file open events (only exist on OS X) */ if (event->type() == QEvent::FileOpen) { QFileOpenEvent *fileOpenEvent = static_cast(event); openUrl(fileOpenEvent->url()); return true; } #endif return false; } void MainWindow::setDistractionFreeMode(bool value) { d->mFullScreenContent->setDistractionFreeMode(value); } void MainWindow::saveProperties(KConfigGroup& group) { group.writeEntry(SESSION_CURRENT_PAGE_KEY, int(d->mCurrentMainPageId)); group.writeEntry(SESSION_URL_KEY, d->mContextManager->currentUrl().toString()); } void MainWindow::readProperties(const KConfigGroup& group) { MainPageId pageId = MainPageId(group.readEntry(SESSION_CURRENT_PAGE_KEY, int(StartMainPageId))); if (pageId == StartMainPageId) { d->mCurrentMainPageId = StartMainPageId; showStartMainPage(); } else if (pageId == BrowseMainPageId) { d->mBrowseAction->trigger(); } else { d->mViewAction->trigger(); } QUrl url = group.readEntry(SESSION_URL_KEY, QUrl()); if (!url.isValid()) { qWarning() << "Invalid url!"; return; } goToUrl(url); } void MainWindow::showFirstDocumentReached() { if (d->mCurrentMainPageId != ViewMainPageId) { return; } HudButtonBox* dlg = new HudButtonBox; dlg->setText(i18n("You reached the first document, what do you want to do?")); dlg->addButton(i18n("Stay There")); dlg->addAction(d->mGoToLastAction, i18n("Go to the Last Document")); dlg->addAction(d->mBrowseAction, i18n("Go Back to the Document List")); dlg->addCountDown(15000); d->mViewMainPage->showMessageWidget(dlg, Qt::AlignCenter); } void MainWindow::showLastDocumentReached() { if (d->mCurrentMainPageId != ViewMainPageId) { return; } HudButtonBox* dlg = new HudButtonBox; dlg->setText(i18n("You reached the last document, what do you want to do?")); dlg->addButton(i18n("Stay There")); dlg->addAction(d->mGoToFirstAction, i18n("Go to the First Document")); dlg->addAction(d->mBrowseAction, i18n("Go Back to the Document List")); dlg->addCountDown(15000); d->mViewMainPage->showMessageWidget(dlg, Qt::AlignCenter); } } // namespace diff --git a/config-gwenview.h.cmake b/config-gwenview.h.cmake index 80dc9cac..29a5a601 100644 --- a/config-gwenview.h.cmake +++ b/config-gwenview.h.cmake @@ -1,7 +1,8 @@ #cmakedefine GWENVIEW_SEMANTICINFO_BACKEND_NONE 1 #cmakedefine GWENVIEW_SEMANTICINFO_BACKEND_FAKE 1 #cmakedefine GWENVIEW_SEMANTICINFO_BACKEND_BALOO 1 #cmakedefine KIPI_FOUND 1 #define GV_TEST_DATA_DIR "@CMAKE_CURRENT_SOURCE_DIR@/tests/data" #cmakedefine HAVE_X11 ${HAVE_X11} #cmakedefine HAVE_FITS ${HAVE_FITS} +#cmakedefine HAVE_QTDBUS ${HAVE_QTDBUS} diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index ef62cf7b..5f135a4d 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -1,280 +1,295 @@ project(gwenviewlib) add_definitions(-DTRANSLATION_DOMAIN="gwenview") set(LIBGWENVIEW_VERSION "4.97.0") # Extract version of libjpeg so that we can use the appropriate dir # See bug #227313 message(STATUS "Looking for libjpeg version in ${JPEG_INCLUDE_DIR}/jpeglib.h") # Due to the large variety of different headers the version data might be # found in (between libjpeg, libjpeg-turbo and various multilib header # forwarding schemes seen in distros), have a simple program print out the # right version. set(JPEGLIB_VERSION_CHECK_PATH "${CMAKE_CURRENT_BINARY_DIR}/jpeglib-version-check.c") file(WRITE ${JPEGLIB_VERSION_CHECK_PATH} " #include #include #include int main(void) { printf(\"%d\\\n\", JPEG_LIB_VERSION); } ") try_run(JPEGLIB_RUN_RESULT JPEGLIB_COMPILE_RESULT ${CMAKE_CURRENT_BINARY_DIR} ${JPEGLIB_VERSION_CHECK_PATH} CMAKE_FLAGS -DINCLUDE_DIRECTORIES:PATH=${JPEG_INCLUDE_DIR} RUN_OUTPUT_VARIABLE jpeglib_version) if ((${JPEGLIB_COMPILE_RESULT} EQUAL FALSE) OR ("${JPEGLIB_RUN_RESULT}" EQUAL FAILED_TO_RUN) OR "${jpeglib_version}" STREQUAL "") message(FATAL_ERROR "Could not find jpeglib.h. This file comes with libjpeg.") endif() if ("${jpeglib_version}" LESS 80) set(GV_JPEG_DIR libjpeg-62) endif() if ("${jpeglib_version}" EQUAL 80) set(GV_JPEG_DIR libjpeg-80) endif() if ("${jpeglib_version}" EQUAL 90) set(GV_JPEG_DIR libjpeg-90) endif() if ("${GV_JPEG_DIR}" STREQUAL "") message(FATAL_ERROR "Unknown libjpeg version: ${jpeglib_version}") endif() message(STATUS "libjpeg version: ${jpeglib_version}") add_definitions(-Dlibjpeg_EXPORTS) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/${GV_JPEG_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_CURRENT_BINARY_DIR} ${EXIV2_INCLUDE_DIR} ${JPEG_INCLUDE_DIR} ${PNG_INCLUDE_DIRS} ) if(HAVE_FITS) include_directories( ${CFITSIO_INCLUDE_DIR} ) endif() # For config-gwenview.h include_directories( ${CMAKE_CURRENT_BINARY_DIR}/.. ) set(gwenviewlib_SRCS cms/iccjpeg.c cms/cmsprofile.cpp cms/cmsprofile_png.cpp contextmanager.cpp crop/cropwidget.cpp crop/cropimageoperation.cpp crop/croptool.cpp document/abstractdocumentimpl.cpp document/documentjob.cpp document/animateddocumentloadedimpl.cpp document/document.cpp document/documentfactory.cpp document/documentloadedimpl.cpp document/emptydocumentimpl.cpp document/jpegdocumentloadedimpl.cpp document/loadingdocumentimpl.cpp document/loadingjob.cpp document/savejob.cpp document/svgdocumentloadedimpl.cpp document/videodocumentloadedimpl.cpp documentview/abstractdocumentviewadapter.cpp documentview/abstractimageview.cpp documentview/abstractrasterimageviewtool.cpp documentview/birdeyeview.cpp documentview/documentview.cpp documentview/documentviewcontroller.cpp documentview/documentviewsynchronizer.cpp documentview/loadingindicator.cpp documentview/messageviewadapter.cpp documentview/rasterimageview.cpp documentview/rasterimageviewadapter.cpp documentview/svgviewadapter.cpp documentview/videoviewadapter.cpp about.cpp abstractimageoperation.cpp disabledactionshortcutmonitor.cpp documentonlyproxymodel.cpp documentview/documentviewcontainer.cpp binder.cpp eventwatcher.cpp historymodel.cpp recentfilesmodel.cpp archiveutils.cpp datewidget.cpp exiv2imageloader.cpp flowlayout.cpp fullscreenbar.cpp hud/hudbutton.cpp hud/hudbuttonbox.cpp hud/hudcountdown.cpp hud/hudlabel.cpp hud/hudmessagebubble.cpp hud/hudslider.cpp hud/hudtheme.cpp hud/hudwidget.cpp graphicswidgetfloater.cpp imageformats/imageformats.cpp # imageformats/jpegplugin.cpp # imageformats/jpeghandler.cpp imagemetainfomodel.cpp imagescaler.cpp imageutils.cpp invisiblebuttongroup.cpp iodevicejpegsourcemanager.cpp jpegcontent.cpp kindproxymodel.cpp semanticinfo/sorteddirmodel.cpp memoryutils.cpp mimetypeutils.cpp paintutils.cpp placetreemodel.cpp preferredimagemetainfomodel.cpp print/printhelper.cpp print/printoptionspage.cpp recursivedirmodel.cpp shadowfilter.cpp slidecontainer.cpp slideshow.cpp statusbartoolbutton.cpp stylesheetutils.cpp redeyereduction/redeyereductionimageoperation.cpp redeyereduction/redeyereductiontool.cpp resize/resizeimageoperation.cpp resize/resizeimagedialog.cpp thumbnailprovider/thumbnailgenerator.cpp thumbnailprovider/thumbnailprovider.cpp thumbnailprovider/thumbnailwriter.cpp thumbnailview/abstractthumbnailviewhelper.cpp thumbnailview/abstractdocumentinfoprovider.cpp thumbnailview/contextbarbutton.cpp thumbnailview/dragpixmapgenerator.cpp thumbnailview/itemeditor.cpp thumbnailview/previewitemdelegate.cpp thumbnailview/thumbnailbarview.cpp thumbnailview/thumbnailslider.cpp thumbnailview/thumbnailview.cpp thumbnailview/tooltipwidget.cpp timeutils.cpp transformimageoperation.cpp urlutils.cpp widgetfloater.cpp zoomslider.cpp zoomwidget.cpp ${GV_JPEG_DIR}/transupp.c ) +if(HAVE_QTDBUS) + set(gwenviewlib_SRCS + ${gwenviewlib_SRCS} + mpris2/lockscreenwatcher.cpp + mpris2/dbusabstractadaptor.cpp + mpris2/mpris2service.cpp + mpris2/mprismediaplayer2.cpp + mpris2/mprismediaplayer2player.cpp + ) + qt5_add_dbus_interface(gwenviewlib_SRCS mpris2/org.freedesktop.ScreenSaver.xml screensaverdbusinterface) +endif() + if(HAVE_FITS) set(gwenviewlib_SRCS ${gwenviewlib_SRCS} imageformats/fitsplugin.cpp imageformats/fitshandler.cpp imageformats/fitsformat/bayer.c imageformats/fitsformat/fitsdata.cpp ) endif() if (NOT GWENVIEW_SEMANTICINFO_BACKEND_NONE) set(gwenviewlib_SRCS ${gwenviewlib_SRCS} semanticinfo/abstractsemanticinfobackend.cpp semanticinfo/semanticinfodirmodel.cpp semanticinfo/tagitemdelegate.cpp semanticinfo/tagmodel.cpp semanticinfo/tagwidget.cpp ) endif() if (GWENVIEW_SEMANTICINFO_BACKEND_FAKE) set(gwenviewlib_SRCS ${gwenviewlib_SRCS} semanticinfo/fakesemanticinfobackend.cpp ) endif() if (GWENVIEW_SEMANTICINFO_BACKEND_BALOO) set(gwenviewlib_SRCS ${gwenviewlib_SRCS} semanticinfo/baloosemanticinfobackend.cpp ) endif() kde_source_files_enable_exceptions( exiv2imageloader.cpp imagemetainfomodel.cpp timeutils.cpp ) ki18n_wrap_ui(gwenviewlib_SRCS crop/cropwidget.ui documentview/messageview.ui print/printoptionspage.ui redeyereduction/redeyereductionwidget.ui resize/resizeimagewidget.ui ) kconfig_add_kcfg_files(gwenviewlib_SRCS gwenviewconfig.kcfgc ) add_library(gwenviewlib SHARED ${gwenviewlib_SRCS}) generate_export_header(gwenviewlib BASE_NAME gwenviewlib) set_target_properties(gwenviewlib PROPERTIES VERSION ${LIBGWENVIEW_VERSION} SOVERSION 5) if (WIN32) set_target_properties(gwenviewlib PROPERTIES COMPILE_FLAGS -DJPEG_STATIC) endif() target_link_libraries(gwenviewlib Qt5::Concurrent Qt5::Svg Qt5::OpenGL Qt5::PrintSupport KF5::KIOCore KF5::KIOWidgets KF5::KIOFileWidgets KF5::I18n KF5::WindowSystem KF5::IconThemes ${JPEG_LIBRARY} ${EXIV2_LIBRARIES} ${PNG_LIBRARIES} ${LCMS2_LIBRARIES} ${PHONON_LIBRARY} ) +if(HAVE_QTDBUS) + target_link_libraries(gwenviewlib Qt5::DBus) +endif() if(HAVE_FITS) target_link_libraries(gwenviewlib ${CFITSIO_LIBRARIES}) endif() if (WIN32) target_link_libraries(gwenviewlib ${EXPAT_LIBRARIES}) endif() if (KF5KDcraw_FOUND) target_link_libraries(gwenviewlib KF5::KDcraw) endif() if (HAVE_X11) target_link_libraries(gwenviewlib Qt5::X11Extras ${X11_X11_LIB}) endif() if (GWENVIEW_SEMANTICINFO_BACKEND_BALOO) target_link_libraries(gwenviewlib KF5::Baloo KF5::FileMetaData ) endif() install(TARGETS gwenviewlib ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP) diff --git a/lib/mpris2/dbusabstractadaptor.cpp b/lib/mpris2/dbusabstractadaptor.cpp new file mode 100644 index 00000000..4a7e986c --- /dev/null +++ b/lib/mpris2/dbusabstractadaptor.cpp @@ -0,0 +1,74 @@ +/* +Gwenview: an image viewer +Copyright 2018 Friedrich W. H. Kossebau + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#include "dbusabstractadaptor.h" + +// Qt +#include +#include +#include + +namespace Gwenview +{ + +DBusAbstractAdaptor::DBusAbstractAdaptor(const QString &objectDBusPath, QObject *parent) + : QDBusAbstractAdaptor(parent) + , mObjectPath(objectDBusPath) +{ + Q_ASSERT(!mObjectPath.isEmpty()); +} + +void DBusAbstractAdaptor::signalPropertyChange(const QString &propertyName, const QVariant &value) +{ + const bool firstChange = mChangedProperties.isEmpty(); + + mChangedProperties.insert(propertyName, value); + + if (firstChange) { + // trigger signal emission on next event loop + QMetaObject::invokeMethod(this, "emitPropertiesChangeDBusSignal", Qt::QueuedConnection); + } +} + + +void DBusAbstractAdaptor::emitPropertiesChangeDBusSignal() +{ + if (mChangedProperties.isEmpty()) { + return; + } + + const QMetaObject* metaObject = this->metaObject(); + const int dBusInterfaceNameIndex = metaObject->indexOfClassInfo("D-Bus Interface"); + Q_ASSERT(dBusInterfaceNameIndex >= 0); + const char* dBusInterfaceName = metaObject->classInfo(dBusInterfaceNameIndex).value(); + + QDBusMessage signalMessage = QDBusMessage::createSignal(mObjectPath, + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("PropertiesChanged")); + signalMessage + << dBusInterfaceName + << mChangedProperties + << QStringList(); + + QDBusConnection::sessionBus().send(signalMessage); + + mChangedProperties.clear(); +} + +} diff --git a/lib/mpris2/dbusabstractadaptor.h b/lib/mpris2/dbusabstractadaptor.h new file mode 100644 index 00000000..ea404660 --- /dev/null +++ b/lib/mpris2/dbusabstractadaptor.h @@ -0,0 +1,74 @@ +/* +Gwenview: an image viewer +Copyright 2018 Friedrich W. H. Kossebau + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#ifndef DBUSABSTRACTADAPTOR_H +#define DBUSABSTRACTADAPTOR_H + +// Qt +#include +#include + +namespace Gwenview +{ + +/** + * Extension of QDBusAbstractAdaptor for proper signalling of D-Bus object property changes + * + * QDBusAbstractAdaptor seems to fail on mapping QObject properties + * to D-Bus object properties when it comes to signalling changes to a property. + * The NOTIFY entry of Q_PROPERTY is not turned into respective D-Bus signalling of a + * property change. So we have to do this explicitly ourselves, instead of using a normal + * QObject signal and expecting the adaptor to translate it. + * + * To reduce D-Bus traffic, all registered property changes are accumulated and squashed + * between event loops where then the D-Bus signal is emitted. + */ +class DBusAbstractAdaptor : public QDBusAbstractAdaptor +{ + Q_OBJECT + +public: + /** + * Ideally we could query the D-Bus path of the object when used, but no idea yet how to do that. + * So one has to additionally pass here the D-Bus path at which the object is registered + * for which this interface is added. + * + * @param objectDBusPath D-Bus name of the property + * @param parent memory management parent or nullptr + */ + explicit DBusAbstractAdaptor(const QString &objectDBusPath, QObject *parent); + +protected: + /** + * @param propertyName D-Bus name of the property + * @param value the new value of the property + */ + void signalPropertyChange(const QString &propertyName, const QVariant &value); + +private Q_SLOTS: + void emitPropertiesChangeDBusSignal(); + +private: + QVariantMap mChangedProperties; + const QString mObjectPath; +}; + +} + +#endif // DBUSABSTRACTADAPTOR_H diff --git a/lib/mpris2/lockscreenwatcher.cpp b/lib/mpris2/lockscreenwatcher.cpp new file mode 100644 index 00000000..09a3c224 --- /dev/null +++ b/lib/mpris2/lockscreenwatcher.cpp @@ -0,0 +1,158 @@ +/* +Gwenview: an image viewer +Copyright 2013 Martin Gräßlin +Copyright 2018 Friedrich W. H. Kossebau + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#include "lockscreenwatcher.h" + +// lib +#include +// Qt +#include +#include +#include + +namespace Gwenview +{ + +using DBusBoolReplyWatcher = QFutureWatcher>; +using DBusStringReplyWatcher = QFutureWatcher>; + +inline QString screenSaverServiceName() { return QStringLiteral("org.freedesktop.ScreenSaver"); } + + +LockScreenWatcher::LockScreenWatcher(QObject *parent) + : QObject(parent) + , mScreenSaverInterface(nullptr) + , mLocked(false) +{ + QDBusServiceWatcher *screenLockServiceWatcher = new QDBusServiceWatcher(this); + connect(screenLockServiceWatcher, &QDBusServiceWatcher::serviceOwnerChanged, + this, &LockScreenWatcher::onScreenSaverServiceOwnerChanged); + screenLockServiceWatcher->setWatchMode(QDBusServiceWatcher::WatchForOwnerChange); + screenLockServiceWatcher->addWatchedService(screenSaverServiceName()); + + DBusBoolReplyWatcher *watcher = new DBusBoolReplyWatcher(this); + connect(watcher, &DBusBoolReplyWatcher::finished, + this, &LockScreenWatcher::onServiceRegisteredQueried); + connect(watcher, &DBusBoolReplyWatcher::canceled, + watcher, &DBusBoolReplyWatcher::deleteLater); + + watcher->setFuture(QtConcurrent::run(QDBusConnection::sessionBus().interface(), + &QDBusConnectionInterface::isServiceRegistered, + screenSaverServiceName())); +} + +LockScreenWatcher::~LockScreenWatcher() = default; + +bool LockScreenWatcher::isLocked() const +{ + return mLocked; +} + +void LockScreenWatcher::onScreenSaverServiceOwnerChanged(const QString &serviceName, + const QString &oldOwner, const QString &newOwner) +{ + Q_UNUSED(oldOwner) + + if (serviceName != screenSaverServiceName()) { + return; + } + + delete mScreenSaverInterface; + mScreenSaverInterface = nullptr; + + if (!newOwner.isEmpty()) { + mScreenSaverInterface = new OrgFreedesktopScreenSaverInterface(newOwner, QStringLiteral("/ScreenSaver"), + QDBusConnection::sessionBus(), this); + connect(mScreenSaverInterface, &OrgFreedesktopScreenSaverInterface::ActiveChanged, + this, &LockScreenWatcher::onScreenSaverActiveChanged); + + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(mScreenSaverInterface->GetActive(), this); + connect(watcher, &QDBusPendingCallWatcher::finished, + this, &LockScreenWatcher::onActiveQueried); + } else { + if (mLocked) { + // reset + mLocked = false; + emit isLockedChanged(mLocked); + } + } +} + +void LockScreenWatcher::onServiceRegisteredQueried() +{ + DBusBoolReplyWatcher *watcher = dynamic_cast(sender()); + if (!watcher) { + return; + } + + const QDBusReply &reply = watcher->result(); + + if (reply.isValid() && reply.value()) { + DBusStringReplyWatcher *ownerWatcher = new DBusStringReplyWatcher(this); + connect(ownerWatcher, &DBusStringReplyWatcher::finished, + this, &LockScreenWatcher::onServiceOwnerQueried); + connect(ownerWatcher, &DBusStringReplyWatcher::canceled, + ownerWatcher, &DBusStringReplyWatcher::deleteLater); + ownerWatcher->setFuture(QtConcurrent::run(QDBusConnection::sessionBus().interface(), + &QDBusConnectionInterface::serviceOwner, + screenSaverServiceName())); + } + + watcher->deleteLater(); +} + +void LockScreenWatcher::onServiceOwnerQueried() +{ + DBusStringReplyWatcher *watcher = dynamic_cast(sender()); + if (!watcher) { + return; + } + + const QDBusReply reply = watcher->result(); + + if (reply.isValid()) { + onScreenSaverServiceOwnerChanged(screenSaverServiceName(), QString(), reply.value()); + } + + watcher->deleteLater(); +} + +void LockScreenWatcher::onActiveQueried(QDBusPendingCallWatcher *watcher) +{ + QDBusPendingReply reply = *watcher; + if (!reply.isError()) { + onScreenSaverActiveChanged(reply.value()); + } + + watcher->deleteLater(); +} + +void LockScreenWatcher::onScreenSaverActiveChanged(bool isActive) +{ + if (mLocked == isActive) { + return; + } + + mLocked = isActive; + + emit isLockedChanged(mLocked); +} + +} diff --git a/lib/mpris2/lockscreenwatcher.h b/lib/mpris2/lockscreenwatcher.h new file mode 100644 index 00000000..fd5f5c64 --- /dev/null +++ b/lib/mpris2/lockscreenwatcher.h @@ -0,0 +1,61 @@ +/* +Gwenview: an image viewer +Copyright 2018 Friedrich W. H. Kossebau + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#ifndef LOCKSCREENWATCHER_H +#define LOCKSCREENWATCHER_H + +// Qt +#include + +class OrgFreedesktopScreenSaverInterface; +class QDBusPendingCallWatcher; + +namespace Gwenview +{ + +class LockScreenWatcher : public QObject +{ + Q_OBJECT + +public: + explicit LockScreenWatcher(QObject *parent); + ~LockScreenWatcher() override; + +public: + bool isLocked() const; + +Q_SIGNALS: + void isLockedChanged(bool locked); + +private: + void onScreenSaverActiveChanged(bool isActive); + void onActiveQueried(QDBusPendingCallWatcher *watcher); + void onScreenSaverServiceOwnerChanged(const QString &serviceName, + const QString &oldOwner, const QString &newOwner); + void onServiceRegisteredQueried(); + void onServiceOwnerQueried(); + +private: + OrgFreedesktopScreenSaverInterface *mScreenSaverInterface; + bool mLocked; +}; + +} + +#endif diff --git a/lib/mpris2/mpris2service.cpp b/lib/mpris2/mpris2service.cpp new file mode 100644 index 00000000..bc21c701 --- /dev/null +++ b/lib/mpris2/mpris2service.cpp @@ -0,0 +1,113 @@ +/* +Gwenview: an image viewer +Copyright 2018 Friedrich W. H. Kossebau + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#include "mpris2service.h" + +// lib +#include "lockscreenwatcher.h" +#include "mprismediaplayer2.h" +#include "mprismediaplayer2player.h" +#include +// Qt +#include +// std +#include + +namespace Gwenview +{ + +inline QString mediaPlayer2ObjectPath() { return QStringLiteral("/org/mpris/MediaPlayer2"); } + + +Mpris2Service::Mpris2Service(SlideShow* slideShow, ContextManager* contextManager, + QAction* toggleSlideShowAction, QAction* fullScreenAction, + QAction* previousAction, QAction* nextAction, + QObject* parent) + : QObject(parent) +{ + new MprisMediaPlayer2(mediaPlayer2ObjectPath(), fullScreenAction, this); + new MprisMediaPlayer2Player(mediaPlayer2ObjectPath(), slideShow, contextManager, + toggleSlideShowAction, fullScreenAction, previousAction, nextAction, + this); + + // To avoid appearing in the media controller on the lock screen, + // which might be not expected or wanted for Gwenview, + // the MPRIS service is unregistered while the lockscreen is active. + LockScreenWatcher *lockScreenWatcher = new LockScreenWatcher(this); + connect(lockScreenWatcher, &LockScreenWatcher::isLockedChanged, + this, &Mpris2Service::onLockScreenLockedChanged); + + if (!lockScreenWatcher->isLocked()) { + registerOnDBus(); + } +} + +Mpris2Service::~Mpris2Service() +{ + unregisterOnDBus(); +} + +void Mpris2Service::registerOnDBus() +{ + QDBusConnection sessionBus = QDBusConnection::sessionBus(); + + // try to register MPRIS presentation object + // to be done before registering the service name, so it is already present + // when controllers react to the service name having appeared + const bool objectRegistered = sessionBus.registerObject(mediaPlayer2ObjectPath(), this, QDBusConnection::ExportAdaptors); + + // try to register MPRIS presentation service + if (objectRegistered) { + mMpris2ServiceName = QStringLiteral("org.mpris.MediaPlayer2.Gwenview"); + + bool serviceRegistered = QDBusConnection::sessionBus().registerService(mMpris2ServiceName); + + // Perhaps not the first instance? Try again with another name, as specified by MPRIS2 spec: + if (!serviceRegistered) { + mMpris2ServiceName = mMpris2ServiceName + QLatin1String(".instance") + QString::number(getpid()); + serviceRegistered = QDBusConnection::sessionBus().registerService(mMpris2ServiceName); + } + if (!serviceRegistered) { + mMpris2ServiceName.clear(); + sessionBus.unregisterObject(mediaPlayer2ObjectPath()); + } + } +} + +void Mpris2Service::unregisterOnDBus() +{ + if (mMpris2ServiceName.isEmpty()) { + return; + } + + QDBusConnection sessionBus = QDBusConnection::sessionBus(); + sessionBus.unregisterService(mMpris2ServiceName); + sessionBus.unregisterObject(mediaPlayer2ObjectPath()); +} + +void Mpris2Service::onLockScreenLockedChanged(bool isLocked) +{ + if (isLocked) { + unregisterOnDBus(); + } else { + registerOnDBus(); + } +} + +} diff --git a/lib/mpris2/mpris2service.h b/lib/mpris2/mpris2service.h new file mode 100644 index 00000000..22e0ac2e --- /dev/null +++ b/lib/mpris2/mpris2service.h @@ -0,0 +1,57 @@ +/* +Gwenview: an image viewer +Copyright 2018 Friedrich W. H. Kossebau + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#ifndef MPRIS2SERVICE_H +#define MPRIS2SERVICE_H + +#include +// Qt +#include +#include + +class QAction; + +namespace Gwenview +{ +class SlideShow; +class ContextManager; + +class GWENVIEWLIB_EXPORT Mpris2Service : public QObject +{ + Q_OBJECT + +public: + Mpris2Service(SlideShow* slideShow, ContextManager* contextManager, + QAction* toggleSlideShowAction, QAction* fullScreenAction, + QAction* previousAction, QAction* nextAction, QObject* parent); + ~Mpris2Service() override; + +private: + void registerOnDBus(); + void unregisterOnDBus(); + + void onLockScreenLockedChanged(bool isLocked); + +private: + QString mMpris2ServiceName; +}; + +} + +#endif diff --git a/lib/mpris2/mprismediaplayer2.cpp b/lib/mpris2/mprismediaplayer2.cpp new file mode 100644 index 00000000..9a233924 --- /dev/null +++ b/lib/mpris2/mprismediaplayer2.cpp @@ -0,0 +1,116 @@ +/* +Gwenview: an image viewer +Copyright 2018 Friedrich W. H. Kossebau + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#include "mprismediaplayer2.h" + +// Qt +#include +#include + +namespace Gwenview +{ + +MprisMediaPlayer2::MprisMediaPlayer2(const QString &objectDBusPath, + QAction* fullScreenAction, + QObject* parent) + : DBusAbstractAdaptor(objectDBusPath, parent) + , mFullScreenAction(fullScreenAction) +{ + connect(mFullScreenAction, &QAction::toggled, + this, &MprisMediaPlayer2::onFullScreenActionToggled); +} + +MprisMediaPlayer2::~MprisMediaPlayer2() +{ +} + +bool MprisMediaPlayer2::canQuit() const +{ + return true; +} + +bool MprisMediaPlayer2::canRaise() const +{ + // spec: "If true, calling Raise will cause the media application to attempt to bring its user interface to the front, + // Which seems to be about the app specific control UI, less about the rendered media (display). + // Could perhaps be supported by pulling in the toppanel when invoked? + return false; +} + +bool MprisMediaPlayer2::canSetFullscreen() const +{ + return true; +} + +bool MprisMediaPlayer2::hasTrackList() const +{ + return false; +} + +void MprisMediaPlayer2::Quit() +{ + QGuiApplication::quit(); +} + +void MprisMediaPlayer2::Raise() +{ +} + +QString MprisMediaPlayer2::identity() const +{ + return QGuiApplication::applicationDisplayName(); +} + +#ifdef GV_SUPPORT_MPRIS_DESKTOPENTRY +QString MprisMediaPlayer2::desktopEntry() const +{ + return QGuiApplication::desktopFileName(); +} +#endif + +QStringList MprisMediaPlayer2::supportedUriSchemes() const +{ + return QStringList(); +} + +QStringList MprisMediaPlayer2::supportedMimeTypes() const +{ + return QStringList(); +} + +bool MprisMediaPlayer2::isFullscreen() const +{ + return mFullScreenAction->isChecked(); +} + +void MprisMediaPlayer2::setFullscreen(bool isFullscreen) +{ + if (isFullscreen == mFullScreenAction->isChecked()) { + return; + } + + mFullScreenAction->trigger(); +} + +void MprisMediaPlayer2::onFullScreenActionToggled(bool checked) +{ + signalPropertyChange("Fullscreen", checked); +} + +} diff --git a/lib/mpris2/mprismediaplayer2.h b/lib/mpris2/mprismediaplayer2.h new file mode 100644 index 00000000..4c5fa8f5 --- /dev/null +++ b/lib/mpris2/mprismediaplayer2.h @@ -0,0 +1,92 @@ +/* +Gwenview: an image viewer +Copyright 2018 Friedrich W. H. Kossebau + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#ifndef MPRISMEDIAPLAYER2_H +#define MPRISMEDIAPLAYER2_H + +#include "dbusabstractadaptor.h" +// Qt +#include + +class QAction; + +// Used QGuiApplication::desktopFileName() only available with Qt 5.7 +// DesktopEntry is an optional property for MPRIS MediaPlayer2, +// so simply only supported for Qt 5.7 & later +#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0) +#define GV_SUPPORT_MPRIS_DESKTOPENTRY 1 +#endif + +namespace Gwenview +{ + +// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html +class MprisMediaPlayer2 : public DBusAbstractAdaptor +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2") + + Q_PROPERTY(bool CanQuit READ canQuit CONSTANT) + Q_PROPERTY(bool CanRaise READ canRaise CONSTANT) + Q_PROPERTY(bool CanSetFullscreen READ canSetFullscreen CONSTANT) + + Q_PROPERTY(QString Identity READ identity CONSTANT) +#ifdef GV_SUPPORT_MPRIS_DESKTOPENTRY + Q_PROPERTY(QString DesktopEntry READ desktopEntry CONSTANT) +#endif + + Q_PROPERTY(bool HasTrackList READ hasTrackList CONSTANT) + Q_PROPERTY(bool Fullscreen READ isFullscreen WRITE setFullscreen) + + Q_PROPERTY(QStringList SupportedUriSchemes READ supportedUriSchemes CONSTANT) + Q_PROPERTY(QStringList SupportedMimeTypes READ supportedMimeTypes CONSTANT) + +public: + MprisMediaPlayer2(const QString &objectDBusPath, QAction* fullScreenAction, QObject* parent); + ~MprisMediaPlayer2() override; + +public Q_SLOTS: // D-Bus API + void Quit(); + void Raise(); + +private: + bool canQuit() const; + bool canRaise() const; + bool canSetFullscreen() const; + bool hasTrackList() const; + + QString identity() const; +#ifdef GV_SUPPORT_MPRIS_DESKTOPENTRY + QString desktopEntry() const; +#endif + bool isFullscreen() const; + void setFullscreen(bool isFullscreen); + + QStringList supportedUriSchemes() const; + QStringList supportedMimeTypes() const; + + void onFullScreenActionToggled(bool checked); + +private: + QAction* mFullScreenAction; +}; + +} + +#endif diff --git a/lib/mpris2/mprismediaplayer2player.cpp b/lib/mpris2/mprismediaplayer2player.cpp new file mode 100644 index 00000000..85ab098b --- /dev/null +++ b/lib/mpris2/mprismediaplayer2player.cpp @@ -0,0 +1,378 @@ +/* +Gwenview: an image viewer +Copyright 2018 Friedrich W. H. Kossebau + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#include "mprismediaplayer2player.h" + +#include + +// lib +#include +#include +#include +#include +#include +#include +#include +// KF +#include +#include +// Qt +#include +#include +#include + +namespace Gwenview +{ + +static const double MAX_RATE = 1.0; +static const double MIN_RATE = 1.0; + + +MprisMediaPlayer2Player::MprisMediaPlayer2Player(const QString &objectDBusPath, + SlideShow* slideShow, + ContextManager* contextManager, + QAction* toggleSlideShowAction, QAction* fullScreenAction, + QAction* previousAction, QAction* nextAction, + QObject* parent) + : DBusAbstractAdaptor(objectDBusPath, parent) + , mSlideShow(slideShow) + , mContextManager(contextManager) + , mToggleSlideShowAction(toggleSlideShowAction) + , mFullScreenAction(fullScreenAction) + , mPreviousAction(previousAction) + , mNextAction(nextAction) + , mSlideShowEnabled(mToggleSlideShowAction->isEnabled()) + , mPreviousEnabled(mPreviousAction->isEnabled()) + , mNextEnabled(mNextAction->isEnabled()) +{ + updatePlaybackStatus(); + + connect(mSlideShow, &SlideShow::stateChanged, + this, &MprisMediaPlayer2Player::onSlideShowStateChanged); + connect(mContextManager, &ContextManager::currentUrlChanged, + this, &MprisMediaPlayer2Player::onCurrentUrlChanged); + connect(mSlideShow->randomAction(), &QAction::toggled, + this, &MprisMediaPlayer2Player::onRandomActionToggled); + connect(mToggleSlideShowAction, &QAction::changed, + this, &MprisMediaPlayer2Player::onToggleSlideShowActionChanged); + connect(mFullScreenAction, &QAction::toggled, + this, &MprisMediaPlayer2Player::onFullScreenActionToggled); + connect(mNextAction, &QAction::changed, + this, &MprisMediaPlayer2Player::onNextActionChanged); + connect(mPreviousAction, &QAction::changed, + this, &MprisMediaPlayer2Player::onPreviousActionChanged); +} + +MprisMediaPlayer2Player::~MprisMediaPlayer2Player() +{ +} + +bool MprisMediaPlayer2Player::updatePlaybackStatus() +{ + const QString newStatus = + (!mSlideShowEnabled || !mFullScreenAction->isChecked()) ? + QStringLiteral("Stopped") : + mSlideShow->isRunning() ? + QStringLiteral("Playing") : + /* else */ + QStringLiteral("Paused"); + + const bool changed = (newStatus != mPlaybackStatus); + if (changed) { + mPlaybackStatus = newStatus; + } + + return changed; +} + +QString MprisMediaPlayer2Player::playbackStatus() const +{ + return mPlaybackStatus; +} + +bool MprisMediaPlayer2Player::canGoNext() const +{ + return mNextEnabled; +} + +void MprisMediaPlayer2Player::Next() +{ + mNextAction->trigger(); +} + + +bool MprisMediaPlayer2Player::canGoPrevious() const +{ + return mPreviousEnabled; +} + +void MprisMediaPlayer2Player::Previous() +{ + mPreviousAction->trigger(); +} + +bool MprisMediaPlayer2Player::canPause() const +{ + return mSlideShowEnabled; +} + +void MprisMediaPlayer2Player::Pause() +{ + mSlideShow->stop(); +} + +void MprisMediaPlayer2Player::PlayPause() +{ + mToggleSlideShowAction->trigger(); +} + +void MprisMediaPlayer2Player::Stop() +{ + if (mFullScreenAction->isChecked()) { + mFullScreenAction->trigger(); + } +} + +bool MprisMediaPlayer2Player::canPlay() const +{ + return mSlideShowEnabled; +} + +void MprisMediaPlayer2Player::Play() +{ + if (mSlideShow->isRunning()) { + return; + } + mToggleSlideShowAction->trigger(); +} + +double MprisMediaPlayer2Player::volume() const +{ + return 0; +} + +void MprisMediaPlayer2Player::setVolume(double volume) +{ + Q_UNUSED(volume); +} + +void MprisMediaPlayer2Player::setShuffle(bool isShuffle) +{ + mSlideShow->randomAction()->setChecked(isShuffle); +} + +QVariantMap MprisMediaPlayer2Player::metadata() const +{ + return mMetaData; +} + +qlonglong MprisMediaPlayer2Player::position() const +{ + return 0; +} + +double MprisMediaPlayer2Player::rate() const +{ + return 1.0; +} + +void MprisMediaPlayer2Player::setRate(double newRate) +{ + Q_UNUSED(newRate); +} + +double MprisMediaPlayer2Player::minimumRate() const +{ + return MIN_RATE; +} + +double MprisMediaPlayer2Player::maximumRate() const +{ + return MAX_RATE; +} + +bool MprisMediaPlayer2Player::isShuffle() const +{ + return mSlideShow->randomAction()->isChecked(); +} + + +bool MprisMediaPlayer2Player::canSeek() const +{ + return false; +} + +bool MprisMediaPlayer2Player::canControl() const +{ + return true; +} + +void MprisMediaPlayer2Player::Seek(qlonglong offset) +{ + Q_UNUSED(offset); +} + +void MprisMediaPlayer2Player::SetPosition(const QDBusObjectPath& trackId, qlonglong pos) +{ + Q_UNUSED(trackId); + Q_UNUSED(pos); +} + +void MprisMediaPlayer2Player::OpenUri(const QString& uri) +{ + Q_UNUSED(uri); +} + +void MprisMediaPlayer2Player::onSlideShowStateChanged() +{ + if (!updatePlaybackStatus()) { + return; + } + + signalPropertyChange("PlaybackStatus", mPlaybackStatus); +} + +void MprisMediaPlayer2Player::onCurrentUrlChanged(const QUrl& url) +{ + if (url.isEmpty()) { + mCurrentDocument = Document::Ptr(); + } else { + mCurrentDocument = DocumentFactory::instance()->load(url); + connect(mCurrentDocument.data(), &Document::metaInfoUpdated, + this, &MprisMediaPlayer2Player::onMetaInfoUpdated); + } + + onMetaInfoUpdated(); +} + +void MprisMediaPlayer2Player::onMetaInfoUpdated() +{ + QVariantMap updatedMetaData; + + if (mCurrentDocument) { + const QUrl url = mCurrentDocument->url(); + ImageMetaInfoModel* metaInfoModel = mCurrentDocument->metaInfo(); + + // We need some unique id mapping to urls. The index in the list is not reliable, + // as images can be added/removed during a running slideshow + // To allow some bidrectional mapping, convert the url to base64 to encode it for + // matching the D-Bus object path spec + const QString slideId = QString::fromLatin1(url.toString().toUtf8().toBase64(QByteArray::OmitTrailingEquals)); + const QDBusObjectPath trackId(QLatin1String("/org/kde/gwenview/imagelist/") + slideId); + updatedMetaData.insert(QStringLiteral("mpris:trackid"), + QVariant::fromValue(trackId)); + + // TODO: for videos also get and report the length + if (MimeTypeUtils::urlKind(url) != MimeTypeUtils::KIND_VIDEO) { + // TODO: implement other MPRIS API for position + // convert seconds in microseconds + // const qlonglong duration = qlonglong(mSlideShow->interval() * 1000000); + // updatedMetaData.insert(QStringLiteral("mpris:length"), duration); + } + + // TODO: update on metadata changes, given user can edit most of this data + + const QString name = metaInfoModel->getValueForKey(QStringLiteral("General.Name")); + updatedMetaData.insert(QStringLiteral("xesam:title"), name); + const QString comment = metaInfoModel->getValueForKey(QStringLiteral("General.Comment")); + if (!comment.isEmpty()) { + updatedMetaData.insert(QStringLiteral("xesam:comment"), comment); + } + updatedMetaData.insert(QStringLiteral("xesam:url"), url.toString()); + // slight bending of semantics :) + const KFileItem folderItem(mContextManager->currentDirUrl()); + updatedMetaData.insert(QStringLiteral("xesam:album"), folderItem.text()); + // TODO: hook up with thumbnail cache and pass that as arturl + // updatedMetaData.insert(QStringLiteral("mpris:artUrl"), url.toString()); + +#ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE + const QModelIndex index = mContextManager->dirModel()->indexForUrl(url); + if (index.isValid()) { + const double rating = index.data(SemanticInfoDirModel::RatingRole).toInt() / 10.0; + updatedMetaData.insert(QStringLiteral("xesam:userRating"), rating); + } +#endif + // consider export of other metadata where mapping works + } + + if (updatedMetaData != mMetaData) { + mMetaData = updatedMetaData; + + signalPropertyChange("Metadata", mMetaData); + } +} + +void MprisMediaPlayer2Player::onRandomActionToggled(bool checked) +{ + signalPropertyChange("Shuffle", checked); +} + +void MprisMediaPlayer2Player::onFullScreenActionToggled() +{ + if (!updatePlaybackStatus()) { + return; + } + + signalPropertyChange("PlaybackStatus", mPlaybackStatus); +} + +void MprisMediaPlayer2Player::onToggleSlideShowActionChanged() +{ + const bool isEnabled = mToggleSlideShowAction->isEnabled(); + if (mSlideShowEnabled == isEnabled) { + return; + } + + mSlideShowEnabled = isEnabled; + + const bool playbackStatusChanged = updatePlaybackStatus(); + + signalPropertyChange("CanPlay", mSlideShowEnabled); + signalPropertyChange("CanPause", mSlideShowEnabled); + if (playbackStatusChanged) { + signalPropertyChange("PlaybackStatus", mPlaybackStatus); + } +} + +void MprisMediaPlayer2Player::onNextActionChanged() +{ + const bool isEnabled = mNextAction->isEnabled(); + if (mNextEnabled == isEnabled) { + return; + } + + mNextEnabled = isEnabled; + + signalPropertyChange("CanGoNext", mNextEnabled); +} + + +void MprisMediaPlayer2Player::onPreviousActionChanged() +{ + const bool isEnabled = mPreviousAction->isEnabled(); + if (mPreviousEnabled == isEnabled) { + return; + } + + mPreviousEnabled = isEnabled; + + signalPropertyChange("CanGoPrevious", mPreviousEnabled); +} + +} diff --git a/lib/mpris2/mprismediaplayer2player.h b/lib/mpris2/mprismediaplayer2player.h new file mode 100644 index 00000000..fe98f360 --- /dev/null +++ b/lib/mpris2/mprismediaplayer2player.h @@ -0,0 +1,128 @@ +/* +Gwenview: an image viewer +Copyright 2018 Friedrich W. H. Kossebau + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#ifndef MPRISMEDIAPLAYER2PLAYER_H +#define MPRISMEDIAPLAYER2PLAYER_H + +#include "dbusabstractadaptor.h" +// lib +#include + +class QDBusObjectPath; +class QAction; + +namespace Gwenview +{ +class SlideShow; +class ContextManager; + +// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html +class MprisMediaPlayer2Player : public DBusAbstractAdaptor +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2.Player") + + Q_PROPERTY(QString PlaybackStatus READ playbackStatus) + Q_PROPERTY(double Rate READ rate WRITE setRate) + Q_PROPERTY(QVariantMap Metadata READ metadata) + Q_PROPERTY(double Volume READ volume WRITE setVolume) + Q_PROPERTY(qlonglong Position READ position) + Q_PROPERTY(double MinimumRate READ minimumRate CONSTANT) + Q_PROPERTY(double MaximumRate READ maximumRate CONSTANT) + Q_PROPERTY(bool Shuffle READ isShuffle WRITE setShuffle) + + Q_PROPERTY(bool CanControl READ canControl CONSTANT) + Q_PROPERTY(bool CanPlay READ canPlay) + Q_PROPERTY(bool CanPause READ canPause) + Q_PROPERTY(bool CanGoNext READ canGoNext) + Q_PROPERTY(bool CanGoPrevious READ canGoPrevious) + Q_PROPERTY(bool CanSeek READ canSeek CONSTANT) + +public: + MprisMediaPlayer2Player(const QString &objectDBusPath, + SlideShow* slideShow, ContextManager* contextManager, + QAction* toggleSlideShowAction, QAction* fullScreenAction, + QAction* previousAction, QAction* nextAction, QObject* parent); + ~MprisMediaPlayer2Player() override; + +public Q_SLOTS: // D-Bus API + void Next(); + void Previous(); + void Pause(); + void PlayPause(); + void Stop(); + void Play(); + void Seek(qlonglong Offset); + void SetPosition(const QDBusObjectPath& trackId, qlonglong pos); + void OpenUri(const QString& uri); + +Q_SIGNALS: // D-Bus API + void Seeked(qlonglong Position) const; + +private: + QString playbackStatus() const; + double rate() const; + QVariantMap metadata() const; + double volume() const; + qlonglong position() const; + double minimumRate() const; + double maximumRate() const; + bool isShuffle() const; + + bool canGoNext() const; + bool canGoPrevious() const; + bool canPlay() const; + bool canPause() const; + bool canSeek() const; + bool canControl() const; + + void setRate(double newRate); + void setVolume(double volume); + void setShuffle(bool isShuffle); + + bool updatePlaybackStatus(); + + void onSlideShowStateChanged(); + void onCurrentUrlChanged(const QUrl& url); + void onRandomActionToggled(bool checked); + void onFullScreenActionToggled(); + void onToggleSlideShowActionChanged(); + void onPreviousActionChanged(); + void onNextActionChanged(); + void onMetaInfoUpdated(); + +private: + SlideShow* mSlideShow; + ContextManager* mContextManager; + QAction* mToggleSlideShowAction; + QAction* mFullScreenAction; + QAction* mPreviousAction; + QAction* mNextAction; + + bool mSlideShowEnabled; + bool mPreviousEnabled; + bool mNextEnabled; + QString mPlaybackStatus; + QVariantMap mMetaData; + Document::Ptr mCurrentDocument; +}; + +} + +#endif diff --git a/lib/mpris2/org.freedesktop.ScreenSaver.xml b/lib/mpris2/org.freedesktop.ScreenSaver.xml new file mode 100644 index 00000000..5efd9433 --- /dev/null +++ b/lib/mpris2/org.freedesktop.ScreenSaver.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/slideshow.cpp b/lib/slideshow.cpp index f35c3b14..c3676ede 100644 --- a/lib/slideshow.cpp +++ b/lib/slideshow.cpp @@ -1,303 +1,308 @@ /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "slideshow.h" // libc #include // STL #include // Qt #include #include #include // KDE #include // Local #include #include namespace Gwenview { #undef ENABLE_LOG #undef LOG //#define ENABLE_LOG #ifdef ENABLE_LOG #define LOG(x) qDebug() << x #else #define LOG(x) ; #endif enum State { Stopped, Started, WaitForEndOfUrl }; /** * This class generate random numbers which are not the same between two runs * of Gwenview. See bug #132334 */ class RandomNumberGenerator { public: RandomNumberGenerator() : mSeed(time(0)) { } int operator()(int n) { return rand_r(&mSeed) % n; } private: unsigned int mSeed; }; struct SlideShowPrivate { QTimer* mTimer; State mState; QVector mUrls; QVector mShuffledUrls; QVector::ConstIterator mStartIt; QUrl mCurrentUrl; QUrl mLastShuffledUrl; QAction* mLoopAction; QAction* mRandomAction; QUrl findNextUrl() { if (GwenviewConfig::random()) { return findNextRandomUrl(); } else { return findNextOrderedUrl(); } } QUrl findNextOrderedUrl() { QVector::ConstIterator it = qFind(mUrls.constBegin(), mUrls.constEnd(), mCurrentUrl); GV_RETURN_VALUE_IF_FAIL2(it != mUrls.constEnd(), QUrl(), "Current url not found in list."); ++it; if (GwenviewConfig::loop()) { // Looping, if we reach the end, start again if (it == mUrls.constEnd()) { it = mUrls.constBegin(); } } else { // Not looping, have we reached the end? // FIXME: stopAtEnd if (/*(it==mUrls.end() && GwenviewConfig::stopAtEnd()) ||*/ it == mStartIt) { it = mUrls.constEnd(); } } if (it != mUrls.constEnd()) { return *it; } else { return QUrl(); } } void initShuffledUrls() { mShuffledUrls = mUrls; RandomNumberGenerator generator; std::random_shuffle(mShuffledUrls.begin(), mShuffledUrls.end(), generator); // Ensure the first url is different from the previous last one, so that // last url does not stay visible twice longer than usual if (mLastShuffledUrl == mShuffledUrls.first() && mShuffledUrls.count() > 1) { qSwap(mShuffledUrls[0], mShuffledUrls[1]); } mLastShuffledUrl = mShuffledUrls.last(); } QUrl findNextRandomUrl() { if (mShuffledUrls.empty()) { if (GwenviewConfig::loop()) { initShuffledUrls(); } else { return QUrl(); } } QUrl url = mShuffledUrls.last(); mShuffledUrls.pop_back(); return url; } void updateTimerInterval() { mTimer->setInterval(int(GwenviewConfig::interval() * 1000)); } void doStart() { if (MimeTypeUtils::urlKind(mCurrentUrl) == MimeTypeUtils::KIND_VIDEO) { LOG("mState = WaitForEndOfUrl"); // Just in case mTimer->stop(); mState = WaitForEndOfUrl; } else { LOG("mState = Started"); mTimer->start(); mState = Started; } } }; SlideShow::SlideShow(QObject* parent) : QObject(parent) , d(new SlideShowPrivate) { d->mState = Stopped; d->mTimer = new QTimer(this); connect(d->mTimer, &QTimer::timeout, this, &SlideShow::goToNextUrl); d->mLoopAction = new QAction(this); d->mLoopAction->setText(i18nc("@item:inmenu toggle loop in slideshow", "Loop")); d->mLoopAction->setCheckable(true); connect(d->mLoopAction, &QAction::triggered, this, &SlideShow::updateConfig); d->mRandomAction = new QAction(this); d->mRandomAction->setText(i18nc("@item:inmenu toggle random order in slideshow", "Random")); d->mRandomAction->setCheckable(true); connect(d->mRandomAction, &QAction::toggled, this, &SlideShow::slotRandomActionToggled); connect(d->mRandomAction, &QAction::triggered, this, &SlideShow::updateConfig); d->mLoopAction->setChecked(GwenviewConfig::loop()); d->mRandomAction->setChecked(GwenviewConfig::random()); } SlideShow::~SlideShow() { GwenviewConfig::self()->save(); delete d; } QAction* SlideShow::loopAction() const { return d->mLoopAction; } QAction* SlideShow::randomAction() const { return d->mRandomAction; } void SlideShow::start(const QList& urls) { d->mUrls.resize(urls.size()); qCopy(urls.begin(), urls.end(), d->mUrls.begin()); d->mStartIt = qFind(d->mUrls.constBegin(), d->mUrls.constEnd(), d->mCurrentUrl); if (d->mStartIt == d->mUrls.constEnd()) { qWarning() << "Current url not found in list, aborting.\n"; return; } if (GwenviewConfig::random()) { d->initShuffledUrls(); } d->updateTimerInterval(); d->mTimer->setSingleShot(false); d->doStart(); stateChanged(true); } void SlideShow::setInterval(int intervalInSeconds) { GwenviewConfig::setInterval(double(intervalInSeconds)); d->updateTimerInterval(); } +int SlideShow::interval() const +{ + return GwenviewConfig::interval(); +} + void SlideShow::stop() { LOG("Stopping timer"); d->mTimer->stop(); d->mState = Stopped; stateChanged(false); } void SlideShow::resumeAndGoToNextUrl() { LOG(""); if (d->mState == WaitForEndOfUrl) { goToNextUrl(); } } void SlideShow::goToNextUrl() { LOG(""); QUrl url = d->findNextUrl(); LOG("url:" << url); if (!url.isValid()) { stop(); return; } goToUrl(url); } void SlideShow::setCurrentUrl(const QUrl &url) { LOG(url); if (d->mCurrentUrl == url) { return; } d->mCurrentUrl = url; // Restart timer to avoid showing new url for the remaining time of the old // url if (d->mState != Stopped) { d->doStart(); } } bool SlideShow::isRunning() const { return d->mState != Stopped; } void SlideShow::updateConfig() { GwenviewConfig::setLoop(d->mLoopAction->isChecked()); GwenviewConfig::setRandom(d->mRandomAction->isChecked()); } void SlideShow::slotRandomActionToggled(bool on) { if (on && d->mState != Stopped) { d->initShuffledUrls(); } } } // namespace diff --git a/lib/slideshow.h b/lib/slideshow.h index c3c10abe..44d739a9 100644 --- a/lib/slideshow.h +++ b/lib/slideshow.h @@ -1,81 +1,83 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef SLIDESHOW_H #define SLIDESHOW_H #include // Qt #include // KDE #include class QAction; namespace Gwenview { struct SlideShowPrivate; class GWENVIEWLIB_EXPORT SlideShow : public QObject { Q_OBJECT public: SlideShow(QObject* parent); virtual ~SlideShow(); void start(const QList& urls); void stop(); QAction* loopAction() const; QAction* randomAction() const; /** @return true if the slideshow is running */ bool isRunning() const; + int interval() const; + public Q_SLOTS: void setInterval(int); void setCurrentUrl(const QUrl &url); /** * Resume slideshow and go to next url. */ void resumeAndGoToNextUrl(); Q_SIGNALS: void goToUrl(const QUrl&); /** * Slideshow has been started or stopped */ void stateChanged(bool running); private Q_SLOTS: void goToNextUrl(); void updateConfig(); void slotRandomActionToggled(bool on); private: SlideShowPrivate* const d; }; } // namespace #endif // SLIDESHOW_H