diff --git a/app/viewmainpage.cpp b/app/viewmainpage.cpp index 480d3c21..f2985295 100644 --- a/app/viewmainpage.cpp +++ b/app/viewmainpage.cpp @@ -1,872 +1,872 @@ /* 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 "viewmainpage.h" #include "config-gwenview.h" // Qt #include #include #include #include #include #include #include // KDE #include #include #include #include #include #include #include #ifdef KF5Activities_FOUND #include #endif // Local #include "fileoperations.h" #include #include "splitter.h" #include #include #include #include #include #include #include #include #include #include #include #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 const int ViewMainPage::MaxViewCount = 6; /* * Layout of mThumbnailSplitter is: * * +-mThumbnailSplitter------------------------------------------------+ * |+-mAdapterContainer-----------------------------------------------+| * ||+-mDocumentViewContainer----------------------------------------+|| * |||+-DocumentView----------------++-DocumentView-----------------+||| * |||| || |||| * |||| || |||| * |||| || |||| * |||| || |||| * |||| || |||| * |||| || |||| * |||+-----------------------------++------------------------------+||| * ||+---------------------------------------------------------------+|| * ||+-mToolContainer------------------------------------------------+|| * ||| ||| * ||+---------------------------------------------------------------+|| * ||+-mStatusBarContainer-------------------------------------------+|| * |||[mToggleSideBarButton][mToggleThumbnailBarButton] [mZoomWidget]||| * ||+---------------------------------------------------------------+|| * |+-----------------------------------------------------------------+| * |===================================================================| * |+-mThumbnailBar---------------------------------------------------+| * || || * || || * |+-----------------------------------------------------------------+| * +-------------------------------------------------------------------+ */ struct ViewMainPagePrivate { ViewMainPage* q; SlideShow* mSlideShow; KActionCollection* mActionCollection; GvCore* mGvCore; KModelIndexProxyMapper* mDirModelToBarModelProxyMapper; QSplitter *mThumbnailSplitter; QWidget* mAdapterContainer; DocumentViewController* mDocumentViewController; QList mDocumentViews; DocumentViewSynchronizer* mSynchronizer; QToolButton* mToggleSideBarButton; QToolButton* mToggleThumbnailBarButton; ZoomWidget* mZoomWidget; DocumentViewContainer* mDocumentViewContainer; SlideContainer* mToolContainer; QWidget* mStatusBarContainer; ThumbnailBarView* mThumbnailBar; KToggleAction* mToggleThumbnailBarAction; KToggleAction* mSynchronizeAction; QCheckBox* mSynchronizeCheckBox; KSqueezedTextLabel* mDocumentCountLabel; // Activity Resource events reporting needs to be above KPart, // in the shell itself, to avoid problems with other MDI applications // that use this KPart #ifdef KF5Activities_FOUND QHash mActivityResources; #endif bool mCompareMode; ZoomMode::Enum mZoomMode; void setupThumbnailBar() { mThumbnailBar = new ThumbnailBarView; ThumbnailBarItemDelegate* delegate = new ThumbnailBarItemDelegate(mThumbnailBar); mThumbnailBar->setItemDelegate(delegate); mThumbnailBar->setVisible(GwenviewConfig::thumbnailBarIsVisible()); mThumbnailBar->setSelectionMode(QAbstractItemView::ExtendedSelection); } void setupThumbnailBarStyleSheet() { QPalette pal = mGvCore->palette(GvCore::NormalViewPalette); mThumbnailBar->setPalette(pal); Qt::Orientation orientation = mThumbnailBar->orientation(); QColor bgColor = pal.color(QPalette::Normal, QPalette::Base); QColor bgSelColor = pal.color(QPalette::Normal, QPalette::Highlight); QColor bgHovColor = pal.color(QPalette::Normal, QPalette::Highlight); // Avoid dark and bright colors bgColor.setHsv(bgColor.hue(), bgColor.saturation(), (127 + 3 * bgColor.value()) / 4); // Hover uses lighter/faded version of select color. Combine with bgColor to adapt to different backgrounds bgHovColor.setHsv(bgHovColor.hue(), (bgHovColor.saturation() / 2), ((bgHovColor.value() + bgColor.value()) / 2)); QColor leftBorderColor = PaintUtils::adjustedHsv(bgColor, 0, 0, qMin(20, 255 - bgColor.value())); QColor rightBorderColor = PaintUtils::adjustedHsv(bgColor, 0, 0, -qMin(40, bgColor.value())); QColor borderSelColor = PaintUtils::adjustedHsv(bgSelColor, 0, 0, -qMin(60, bgSelColor.value())); QString itemCss = "QListView::item {" " background-color: %1;" " border-left: 1px solid %2;" " border-right: 1px solid %3;" "}"; itemCss = itemCss.arg( StyleSheetUtils::gradient(orientation, bgColor, 46), StyleSheetUtils::gradient(orientation, leftBorderColor, 36), StyleSheetUtils::gradient(orientation, rightBorderColor, 26)); QString itemSelCss = "QListView::item:selected {" " background-color: %1;" " border-left: 1px solid %2;" " border-right: 1px solid %2;" "}"; itemSelCss = itemSelCss.arg( StyleSheetUtils::gradient(orientation, bgSelColor, 56), StyleSheetUtils::rgba(borderSelColor)); QString itemHovCss = "QListView::item:hover:!selected {" " background-color: %1;" " border-left: 1px solid %2;" " border-right: 1px solid %3;" "}"; itemHovCss = itemHovCss.arg( StyleSheetUtils::gradient(orientation, bgHovColor, 56), StyleSheetUtils::rgba(leftBorderColor), StyleSheetUtils::rgba(rightBorderColor)); QString css = itemCss + itemSelCss + itemHovCss; if (orientation == Qt::Vertical) { css.replace("left", "top").replace("right", "bottom"); } mThumbnailBar->setStyleSheet(css); } void setupAdapterContainer() { mAdapterContainer = new QWidget; QVBoxLayout* layout = new QVBoxLayout(mAdapterContainer); layout->setMargin(0); layout->setSpacing(0); mDocumentViewContainer = new DocumentViewContainer; mDocumentViewContainer->setAutoFillBackground(true); mDocumentViewContainer->setBackgroundRole(QPalette::Base); layout->addWidget(mDocumentViewContainer); layout->addWidget(mToolContainer); layout->addWidget(mStatusBarContainer); } void setupDocumentViewController() { mDocumentViewController = new DocumentViewController(mActionCollection, q); mDocumentViewController->setZoomWidget(mZoomWidget); mDocumentViewController->setToolContainer(mToolContainer); mSynchronizer = new DocumentViewSynchronizer(&mDocumentViews, q); } DocumentView* createDocumentView() { DocumentView* view = mDocumentViewContainer->createView(); // Connect context menu // If you need to connect another view signal, make sure it is disconnected in deleteDocumentView QObject::connect(view, &DocumentView::contextMenuRequested, q, &ViewMainPage::showContextMenu); QObject::connect(view, &DocumentView::completed, q, &ViewMainPage::completed); QObject::connect(view, &DocumentView::previousImageRequested, q, &ViewMainPage::previousImageRequested); QObject::connect(view, &DocumentView::nextImageRequested, q, &ViewMainPage::nextImageRequested); QObject::connect(view, &DocumentView::openUrlRequested, q, &ViewMainPage::openUrlRequested); QObject::connect(view, &DocumentView::openDirUrlRequested, q, &ViewMainPage::openDirUrlRequested); QObject::connect(view, &DocumentView::captionUpdateRequested, q, &ViewMainPage::captionUpdateRequested); QObject::connect(view, &DocumentView::toggleFullScreenRequested, q, &ViewMainPage::toggleFullScreenRequested); QObject::connect(view, &DocumentView::focused, q, &ViewMainPage::slotViewFocused); QObject::connect(view, &DocumentView::hudTrashClicked, q, &ViewMainPage::trashView); QObject::connect(view, &DocumentView::hudDeselectClicked, q, &ViewMainPage::deselectView); QObject::connect(view, &DocumentView::videoFinished, mSlideShow, &SlideShow::resumeAndGoToNextUrl); mDocumentViews << view; #ifdef KF5Activities_FOUND mActivityResources.insert(view, new KActivities::ResourceInstance(q->window()->winId(), view)); #endif return view; } void deleteDocumentView(DocumentView* view) { if (mDocumentViewController->view() == view) { mDocumentViewController->setView(nullptr); } // Make sure we do not get notified about this view while it is going away. // mDocumentViewController->deleteView() animates the view deletion so // the view still exists for a short while when we come back to the // event loop) QObject::disconnect(view, nullptr, q, nullptr); QObject::disconnect(view, nullptr, mSlideShow, nullptr); mDocumentViews.removeOne(view); #ifdef KF5Activities_FOUND mActivityResources.remove(view); #endif mDocumentViewContainer->deleteView(view); } void setupToolContainer() { mToolContainer = new SlideContainer; } void setupStatusBar() { mStatusBarContainer = new QWidget; mToggleSideBarButton = new StatusBarToolButton; mToggleThumbnailBarButton = new StatusBarToolButton; mZoomWidget = new ZoomWidget; mSynchronizeCheckBox = new QCheckBox(i18n("Synchronize")); mSynchronizeCheckBox->hide(); mDocumentCountLabel = new KSqueezedTextLabel; mDocumentCountLabel->setAlignment(Qt::AlignCenter); mDocumentCountLabel->setTextElideMode(Qt::ElideRight); QMargins labelMargins = mDocumentCountLabel->contentsMargins(); labelMargins.setLeft(15); labelMargins.setRight(15); mDocumentCountLabel->setContentsMargins(labelMargins); QHBoxLayout* layout = new QHBoxLayout(mStatusBarContainer); layout->setMargin(0); layout->setSpacing(0); layout->addWidget(mToggleSideBarButton); layout->addWidget(mToggleThumbnailBarButton); layout->addStretch(); layout->addWidget(mSynchronizeCheckBox); // Ensure document count label takes up all available space, // so its autohide feature works properly (stretch factor = 1) layout->addWidget(mDocumentCountLabel, 1); layout->addStretch(); layout->addWidget(mZoomWidget); } void setupSplitter() { Qt::Orientation orientation = GwenviewConfig::thumbnailBarOrientation(); mThumbnailSplitter = new Splitter(orientation == Qt::Horizontal ? Qt::Vertical : Qt::Horizontal, q); mThumbnailBar->setOrientation(orientation); mThumbnailBar->setThumbnailAspectRatio(GwenviewConfig::thumbnailAspectRatio()); mThumbnailBar->setRowCount(GwenviewConfig::thumbnailBarRowCount()); mThumbnailSplitter->addWidget(mAdapterContainer); mThumbnailSplitter->addWidget(mThumbnailBar); mThumbnailSplitter->setSizes(GwenviewConfig::thumbnailSplitterSizes()); QVBoxLayout* layout = new QVBoxLayout(q); layout->setMargin(0); layout->addWidget(mThumbnailSplitter); } void saveSplitterConfig() { if (mThumbnailBar->isVisible()) { GwenviewConfig::setThumbnailSplitterSizes(mThumbnailSplitter->sizes()); } } DocumentView* currentView() const { return mDocumentViewController->view(); } void setCurrentView(DocumentView* view) { DocumentView* oldView = currentView(); if (view == oldView) { return; } if (oldView) { oldView->setCurrent(false); #ifdef KF5Activities_FOUND Q_ASSERT(mActivityResources.contains(oldView)); mActivityResources.value(oldView)->notifyFocusedOut(); #endif } view->setCurrent(true); mDocumentViewController->setView(view); mSynchronizer->setCurrentView(view); QModelIndex index = indexForView(view); if (index.isValid()) { // Index may be invalid when Gwenview is started as // `gwenview /foo/image.png` because in this situation it loads image.png // *before* listing /foo (because it matters less to the user) mThumbnailBar->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Current); } #ifdef KF5Activities_FOUND Q_ASSERT(mActivityResources.contains(view)); mActivityResources.value(view)->notifyFocusedIn(); #endif QObject::connect(view, &DocumentView::currentToolChanged, q, &ViewMainPage::updateFocus); } QModelIndex indexForView(DocumentView* view) const { QUrl url = view->url(); if (!url.isValid()) { qWarning() << "View does not display any document!"; return QModelIndex(); } SortedDirModel* dirModel = mGvCore->sortedDirModel(); QModelIndex srcIndex = dirModel->indexForUrl(url); if (!mDirModelToBarModelProxyMapper) { // Delay the initialization of the mapper to its first use because // mThumbnailBar->model() is not set after ViewMainPage ctor is // done. const_cast(this)->mDirModelToBarModelProxyMapper = new KModelIndexProxyMapper(dirModel, mThumbnailBar->model(), q); } QModelIndex index = mDirModelToBarModelProxyMapper->mapLeftToRight(srcIndex); return index; } void applyPalette(bool fullScreenMode) { mDocumentViewContainer->applyPalette(mGvCore->palette(fullScreenMode ? GvCore::FullScreenViewPalette : GvCore::NormalViewPalette)); setupThumbnailBarStyleSheet(); } void updateDocumentCountLabel() { const int current = mThumbnailBar->currentIndex().row() + 1; // zero-based const int total = mThumbnailBar->model()->rowCount(); const QString text = i18nc("@info:status %1 current document index, %2 total documents", "%1 of %2", current, total); mDocumentCountLabel->setText(text); } }; ViewMainPage::ViewMainPage(QWidget* parent, SlideShow* slideShow, KActionCollection* actionCollection, GvCore* gvCore) : QWidget(parent) , d(new ViewMainPagePrivate) { d->q = this; d->mDirModelToBarModelProxyMapper = nullptr; // Initialized later d->mSlideShow = slideShow; d->mActionCollection = actionCollection; d->mGvCore = gvCore; d->mCompareMode = false; QShortcut* enterKeyShortcut = new QShortcut(Qt::Key_Return, this); connect(enterKeyShortcut, &QShortcut::activated, this, &ViewMainPage::slotEnterPressed); d->setupToolContainer(); d->setupStatusBar(); d->setupAdapterContainer(); d->setupThumbnailBar(); d->setupSplitter(); d->setupDocumentViewController(); KActionCategory* view = new KActionCategory(i18nc("@title actions category - means actions changing smth in interface", "View"), actionCollection); d->mToggleThumbnailBarAction = view->add(QStringLiteral("toggle_thumbnailbar")); d->mToggleThumbnailBarAction->setText(i18n("Thumbnail Bar")); d->mToggleThumbnailBarAction->setIcon(QIcon::fromTheme("folder-image")); actionCollection->setDefaultShortcut(d->mToggleThumbnailBarAction, Qt::CTRL + Qt::Key_B); connect(d->mToggleThumbnailBarAction, &KToggleAction::triggered, this, &ViewMainPage::setThumbnailBarVisibility); d->mToggleThumbnailBarButton->setDefaultAction(d->mToggleThumbnailBarAction); d->mSynchronizeAction = view->add("synchronize_views"); d->mSynchronizeAction->setText(i18n("Synchronize")); actionCollection->setDefaultShortcut(d->mSynchronizeAction, Qt::CTRL + Qt::Key_Y); connect(d->mSynchronizeAction, SIGNAL(toggled(bool)), d->mSynchronizer, SLOT(setActive(bool))); // Ensure mSynchronizeAction and mSynchronizeCheckBox are in sync connect(d->mSynchronizeAction, SIGNAL(toggled(bool)), d->mSynchronizeCheckBox, SLOT(setChecked(bool))); connect(d->mSynchronizeCheckBox, SIGNAL(toggled(bool)), d->mSynchronizeAction, SLOT(setChecked(bool))); // Connections for the document count connect(d->mThumbnailBar, &ThumbnailBarView::rowsInsertedSignal, this, &ViewMainPage::slotDirModelItemsAddedOrRemoved); connect(d->mThumbnailBar, &ThumbnailBarView::rowsRemovedSignal, this, &ViewMainPage::slotDirModelItemsAddedOrRemoved); installEventFilter(this); } ViewMainPage::~ViewMainPage() { delete d; } void ViewMainPage::loadConfig() { d->applyPalette(window()->isFullScreen()); // FIXME: Not symetric with saveConfig(). Check if it matters. Q_FOREACH(DocumentView * view, d->mDocumentViews) { view->loadAdapterConfig(); } Qt::Orientation orientation = GwenviewConfig::thumbnailBarOrientation(); d->mThumbnailSplitter->setOrientation(orientation == Qt::Horizontal ? Qt::Vertical : Qt::Horizontal); d->mThumbnailBar->setOrientation(orientation); d->setupThumbnailBarStyleSheet(); d->mThumbnailBar->setVisible(GwenviewConfig::thumbnailBarIsVisible()); d->mToggleThumbnailBarAction->setChecked(GwenviewConfig::thumbnailBarIsVisible()); int oldRowCount = d->mThumbnailBar->rowCount(); int newRowCount = GwenviewConfig::thumbnailBarRowCount(); if (oldRowCount != newRowCount) { d->mThumbnailBar->setUpdatesEnabled(false); int gridSize = d->mThumbnailBar->gridSize().width(); d->mThumbnailBar->setRowCount(newRowCount); // Adjust splitter to ensure thumbnail size remains the same int delta = (newRowCount - oldRowCount) * gridSize; QList sizes = d->mThumbnailSplitter->sizes(); Q_ASSERT(sizes.count() == 2); sizes[0] -= delta; sizes[1] += delta; d->mThumbnailSplitter->setSizes(sizes); d->mThumbnailBar->setUpdatesEnabled(true); } d->mZoomMode = GwenviewConfig::zoomMode(); } void ViewMainPage::saveConfig() { d->saveSplitterConfig(); GwenviewConfig::setThumbnailBarIsVisible(d->mToggleThumbnailBarAction->isChecked()); } void ViewMainPage::setThumbnailBarVisibility(bool visible) { d->saveSplitterConfig(); d->mThumbnailBar->setVisible(visible); } int ViewMainPage::statusBarHeight() const { return d->mStatusBarContainer->height(); } void ViewMainPage::setStatusBarVisible(bool visible) { d->mStatusBarContainer->setVisible(visible); } void ViewMainPage::setFullScreenMode(bool fullScreenMode) { if (fullScreenMode) { d->mThumbnailBar->setVisible(false); } else { d->mThumbnailBar->setVisible(d->mToggleThumbnailBarAction->isChecked()); } d->applyPalette(fullScreenMode); d->mToggleThumbnailBarAction->setEnabled(!fullScreenMode); } ThumbnailBarView* ViewMainPage::thumbnailBar() const { return d->mThumbnailBar; } inline void addActionToMenu(QMenu* menu, KActionCollection* actionCollection, const char* name) { QAction* action = actionCollection->action(name); if (action) { menu->addAction(action); } } void ViewMainPage::showContextMenu() { QMenu menu(this); addActionToMenu(&menu, d->mActionCollection, "fullscreen"); menu.addSeparator(); addActionToMenu(&menu, d->mActionCollection, "go_previous"); addActionToMenu(&menu, d->mActionCollection, "go_next"); if (d->currentView()->canZoom()) { menu.addSeparator(); addActionToMenu(&menu, d->mActionCollection, "view_actual_size"); addActionToMenu(&menu, d->mActionCollection, "view_zoom_to_fit"); addActionToMenu(&menu, d->mActionCollection, "view_zoom_in"); addActionToMenu(&menu, d->mActionCollection, "view_zoom_out"); } if (d->mCompareMode) { menu.addSeparator(); addActionToMenu(&menu, d->mActionCollection, "synchronize_views"); } menu.addSeparator(); addActionToMenu(&menu, d->mActionCollection, "file_copy_to"); addActionToMenu(&menu, d->mActionCollection, "file_move_to"); addActionToMenu(&menu, d->mActionCollection, "file_link_to"); menu.addSeparator(); addActionToMenu(&menu, d->mActionCollection, "file_open_with"); addActionToMenu(&menu, d->mActionCollection, "file_open_containing_folder"); menu.exec(QCursor::pos()); } QSize ViewMainPage::sizeHint() const { return QSize(400, 300); } QSize ViewMainPage::minimumSizeHint() const { if (!layout()) { return QSize(); } QSize minimumSize = layout()->minimumSize(); if (window()->isFullScreen()) { // Check minimum width of the overlay fullscreen bar // since there is no layout link which could do this const FullScreenBar* fullScreenBar = findChild(); if (fullScreenBar && fullScreenBar->layout()) { const int fullScreenBarWidth = fullScreenBar->layout()->minimumSize().width(); if (fullScreenBarWidth > minimumSize.width()) { minimumSize.setWidth(fullScreenBarWidth); } } } return minimumSize; } QUrl ViewMainPage::url() const { GV_RETURN_VALUE_IF_FAIL(d->currentView(), QUrl()); return d->currentView()->url(); } Document::Ptr ViewMainPage::currentDocument() const { if (!d->currentView()) { LOG("!d->documentView()"); return Document::Ptr(); } return d->currentView()->document(); } bool ViewMainPage::isEmpty() const { return !currentDocument(); } RasterImageView* ViewMainPage::imageView() const { if (!d->currentView()) { return nullptr; } return d->currentView()->imageView(); } DocumentView* ViewMainPage::documentView() const { return d->currentView(); } void ViewMainPage::openUrl(const QUrl &url) { openUrls(QList() << url, url); } void ViewMainPage::openUrls(const QList& allUrls, const QUrl ¤tUrl) { DocumentView::Setup setup; QSet urls = allUrls.toSet(); d->mCompareMode = urls.count() > 1; typedef QMap ViewForUrlMap; ViewForUrlMap viewForUrlMap; if (!d->mDocumentViews.isEmpty()) { d->mDocumentViewContainer->updateSetup(d->mDocumentViews.last()); } if (d->mDocumentViews.isEmpty() || d->mZoomMode == ZoomMode::Autofit) { setup.valid = true; setup.zoomToFit = true; } else { setup = d->mDocumentViews.last()->setup(); } // Destroy views which show urls we don't care about, remove from "urls" the // urls which already have a view. Q_FOREACH(DocumentView * view, d->mDocumentViews) { QUrl url = view->url(); if (urls.contains(url)) { // view displays an url we must display, keep it urls.remove(url); viewForUrlMap.insert(url, view); } else { // view url is not interesting, drop it d->deleteDocumentView(view); } } // Create view for remaining urls Q_FOREACH(const QUrl &url, urls) { if (d->mDocumentViews.count() >= MaxViewCount) { qWarning() << "Too many documents to show"; break; } DocumentView* view = d->createDocumentView(); viewForUrlMap.insert(url, view); } // Set sortKey to match url order int sortKey = 0; Q_FOREACH(const QUrl &url, allUrls) { viewForUrlMap[url]->setSortKey(sortKey); ++sortKey; } d->mDocumentViewContainer->updateLayout(); // Load urls for new views. Do it only now because the view must have the // correct size before it starts loading its url. Do not do it later because // view->url() needs to be set for the next loop. ViewForUrlMap::ConstIterator it = viewForUrlMap.constBegin(), end = viewForUrlMap.constEnd(); for (; it != end; ++it) { QUrl url = it.key(); DocumentView* view = it.value(); DocumentView::Setup savedSetup = d->mDocumentViewContainer->savedSetup(url); view->openUrl(url, d->mZoomMode == ZoomMode::Individual && savedSetup.valid ? savedSetup : setup); #ifdef KF5Activities_FOUND d->mActivityResources.value(view)->setUri(url); #endif } // Init views Q_FOREACH(DocumentView * view, d->mDocumentViews) { view->setCompareMode(d->mCompareMode); if (view->url() == currentUrl) { d->setCurrentView(view); } else { view->setCurrent(false); } } d->mSynchronizeCheckBox->setVisible(d->mCompareMode); if (d->mCompareMode) { d->mSynchronizer->setActive(d->mSynchronizeCheckBox->isChecked()); } else { d->mSynchronizer->setActive(false); } d->updateDocumentCountLabel(); d->mDocumentCountLabel->setVisible(!d->mCompareMode); } void ViewMainPage::reload() { DocumentView *view = d->currentView(); if (!view) { return; } Document::Ptr doc = view->document(); if (!doc) { qWarning() << "!doc"; return; } if (doc->isModified()) { KGuiItem cont = KStandardGuiItem::cont(); cont.setText(i18nc("@action:button", "Discard Changes and Reload")); int answer = KMessageBox::warningContinueCancel(this, i18nc("@info", "This image has been modified. Reloading it will discard all your changes."), QString() /* caption */, cont); if (answer != KMessageBox::Continue) { return; } } doc->reload(); // Call openUrl again because DocumentView may need to switch to a new // adapter (for example because document was broken and it is not anymore) d->currentView()->openUrl(doc->url(), d->currentView()->setup()); } void ViewMainPage::reset() { d->mDocumentViewController->reset(); d->mDocumentViewContainer->reset(); d->mDocumentViews.clear(); } void ViewMainPage::slotViewFocused(DocumentView* view) { d->setCurrentView(view); } void ViewMainPage::slotEnterPressed() { DocumentView *view = d->currentView(); if (view) { AbstractRasterImageViewTool *tool = view->currentTool(); if (tool) { QKeyEvent event(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier); tool->keyPressEvent(&event); if (event.isAccepted()) { return; } } } emit goToBrowseModeRequested(); } bool ViewMainPage::eventFilter(QObject* watched, QEvent* event) { if (event->type() == QEvent::ShortcutOverride) { - const QKeyEvent* keyEvent = static_cast(event); - if (keyEvent->key() == Qt::Key_Escape) { + const int key = static_cast(event)->key(); + if (key == Qt::Key_Space || key == Qt::Key_Escape) { const DocumentView* view = d->currentView(); if (view) { AbstractRasterImageViewTool* tool = view->currentTool(); if (tool) { - QKeyEvent toolKeyEvent(QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier); + QKeyEvent toolKeyEvent(QEvent::KeyPress, key, Qt::NoModifier); tool->keyPressEvent(&toolKeyEvent); if (toolKeyEvent.isAccepted()) { event->accept(); } } } } } return QWidget::eventFilter(watched, event); } void ViewMainPage::trashView(DocumentView* view) { QUrl url = view->url(); deselectView(view); FileOperations::trash(QList() << url, this); } void ViewMainPage::deselectView(DocumentView* view) { DocumentView* newCurrentView = nullptr; if (view == d->currentView()) { // We need to find a new view to set as current int idx = d->mDocumentViews.indexOf(view); if (idx + 1 < d->mDocumentViews.count()) { newCurrentView = d->mDocumentViews.at(idx + 1); } else if (idx > 0) { newCurrentView = d->mDocumentViews.at(idx - 1); } else { GV_WARN_AND_RETURN("No view found to set as current"); } } QModelIndex index = d->indexForView(view); QItemSelectionModel* selectionModel = d->mThumbnailBar->selectionModel(); selectionModel->select(index, QItemSelectionModel::Deselect); if (newCurrentView) { d->setCurrentView(newCurrentView); } } QToolButton* ViewMainPage::toggleSideBarButton() const { return d->mToggleSideBarButton; } void ViewMainPage::showMessageWidget(QGraphicsWidget* widget, Qt::Alignment align) { d->mDocumentViewContainer->showMessageWidget(widget, align); } void ViewMainPage::updateFocus(const AbstractRasterImageViewTool* tool) { if (!tool) { d->mDocumentViewContainer->setFocus(); } } void ViewMainPage::slotDirModelItemsAddedOrRemoved() { d->updateDocumentCountLabel(); } } // namespace diff --git a/lib/crop/croptool.cpp b/lib/crop/croptool.cpp index 011e6fc4..786abd28 100644 --- a/lib/crop/croptool.cpp +++ b/lib/crop/croptool.cpp @@ -1,483 +1,490 @@ // 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. */ // Self #include "croptool.h" // Qt #include #include #include #include +#include #include #include // KDE // Local #include #include "cropimageoperation.h" #include "cropwidget.h" #include "gwenviewconfig.h" static const int HANDLE_SIZE = 15; namespace Gwenview { enum CropHandleFlag { CH_None, CH_Top = 1, CH_Left = 2, CH_Right = 4, CH_Bottom = 8, CH_TopLeft = CH_Top | CH_Left, CH_BottomLeft = CH_Bottom | CH_Left, CH_TopRight = CH_Top | CH_Right, CH_BottomRight = CH_Bottom | CH_Right, CH_Content = 16 }; Q_DECLARE_FLAGS(CropHandle, CropHandleFlag) } // namespace inline QPoint boundPointX(const QPoint& point, const QRect& rect) { return QPoint( qBound(rect.left(), point.x(), rect.right()), point.y() ); } inline QPoint boundPointXY(const QPoint& point, const QRect& rect) { return QPoint( qBound(rect.left(), point.x(), rect.right()), qBound(rect.top(), point.y(), rect.bottom()) ); } Q_DECLARE_OPERATORS_FOR_FLAGS(Gwenview::CropHandle) namespace Gwenview { struct CropToolPrivate { CropTool* q; QRect mRect; QList mCropHandleList; CropHandle mMovingHandle; QPoint mLastMouseMovePos; double mCropRatio; double mLockedCropRatio; CropWidget* mCropWidget; QRect viewportCropRect() const { return q->imageView()->mapToView(mRect); } QRect handleViewportRect(CropHandle handle) { QSize viewportSize = q->imageView()->size().toSize(); QRect rect = viewportCropRect(); int left, top; if (handle & CH_Top) { top = rect.top(); } else if (handle & CH_Bottom) { top = rect.bottom() + 1 - HANDLE_SIZE; } else { top = rect.top() + (rect.height() - HANDLE_SIZE) / 2; top = qBound(0, top, viewportSize.height() - HANDLE_SIZE); top = qBound(rect.top() + HANDLE_SIZE, top, rect.bottom() - 2 * HANDLE_SIZE); } if (handle & CH_Left) { left = rect.left(); } else if (handle & CH_Right) { left = rect.right() + 1 - HANDLE_SIZE; } else { left = rect.left() + (rect.width() - HANDLE_SIZE) / 2; left = qBound(0, left, viewportSize.width() - HANDLE_SIZE); left = qBound(rect.left() + HANDLE_SIZE, left, rect.right() - 2 * HANDLE_SIZE); } return QRect(left, top, HANDLE_SIZE, HANDLE_SIZE); } CropHandle handleAt(const QPointF& pos) { Q_FOREACH(const CropHandle & handle, mCropHandleList) { QRectF rect = handleViewportRect(handle); if (rect.contains(pos)) { return handle; } } QRectF rect = viewportCropRect(); if (rect.contains(pos)) { return CH_Content; } return CH_None; } void updateCursor(CropHandle handle, bool buttonDown) { Qt::CursorShape shape; switch (handle) { case CH_TopLeft: case CH_BottomRight: shape = Qt::SizeFDiagCursor; break; case CH_TopRight: case CH_BottomLeft: shape = Qt::SizeBDiagCursor; break; case CH_Left: case CH_Right: shape = Qt::SizeHorCursor; break; case CH_Top: case CH_Bottom: shape = Qt::SizeVerCursor; break; case CH_Content: shape = buttonDown ? Qt::ClosedHandCursor : Qt::OpenHandCursor; break; default: shape = Qt::ArrowCursor; break; } q->imageView()->setCursor(shape); } void keepRectInsideImage() { const QSize imageSize = q->imageView()->documentSize().toSize(); if (mRect.width() > imageSize.width() || mRect.height() > imageSize.height()) { // This can happen when the crop ratio changes QSize rectSize = mRect.size(); rectSize.scale(imageSize, Qt::KeepAspectRatio); mRect.setSize(rectSize); } if (mRect.right() >= imageSize.width()) { mRect.moveRight(imageSize.width() - 1); } else if (mRect.left() < 0) { mRect.moveLeft(0); } if (mRect.bottom() >= imageSize.height()) { mRect.moveBottom(imageSize.height() - 1); } else if (mRect.top() < 0) { mRect.moveTop(0); } } void setupWidget() { RasterImageView* view = q->imageView(); mCropWidget = new CropWidget(nullptr, view, q); QObject::connect(mCropWidget, SIGNAL(cropRequested()), q, SLOT(slotCropRequested())); QObject::connect(mCropWidget, SIGNAL(done()), q, SIGNAL(done())); // This is needed when crop ratio set to Current Image, and the image is rotated QObject::connect(view, &RasterImageView::imageRectUpdated, mCropWidget, &CropWidget::updateCropRatio); } QRect computeVisibleImageRect() const { RasterImageView* view = q->imageView(); const QRect imageRect = QRect(QPoint(0, 0), view->documentSize().toSize()); const QRect viewportRect = view->mapToImage(view->rect().toRect()); return imageRect & viewportRect; } }; CropTool::CropTool(RasterImageView* view) : AbstractRasterImageViewTool(view) , d(new CropToolPrivate) { d->q = this; d->mCropHandleList << CH_Left << CH_Right << CH_Top << CH_Bottom << CH_TopLeft << CH_TopRight << CH_BottomLeft << CH_BottomRight; d->mMovingHandle = CH_None; d->mCropRatio = 0.; d->mLockedCropRatio = 0.; d->mRect = d->computeVisibleImageRect(); d->setupWidget(); } CropTool::~CropTool() { // mCropWidget is a child of its container not of us, so it is not deleted automatically delete d->mCropWidget; delete d; } void CropTool::setCropRatio(double ratio) { d->mCropRatio = ratio; } void CropTool::setRect(const QRect& rect) { QRect oldRect = d->mRect; d->mRect = rect; d->keepRectInsideImage(); if (d->mRect != oldRect) { rectUpdated(d->mRect); } imageView()->update(); } QRect CropTool::rect() const { return d->mRect; } void CropTool::paint(QPainter* painter) { QRect rect = d->viewportCropRect(); QRect imageRect = imageView()->rect().toRect(); static const QColor outerColor = QColor::fromHsvF(0, 0, 0, 0.5); // For some reason nothing gets drawn if borderColor is not fully opaque! //static const QColor borderColor = QColor::fromHsvF(0, 0, 1.0, 0.66); static const QColor borderColor = QColor::fromHsvF(0, 0, 1.0); static const QColor fillColor = QColor::fromHsvF(0, 0, 0.75, 0.66); QRegion outerRegion = QRegion(imageRect) - QRegion(rect); Q_FOREACH(const QRect & outerRect, outerRegion.rects()) { painter->fillRect(outerRect, outerColor); } painter->setPen(borderColor); painter->drawRect(rect); if (d->mMovingHandle == CH_None) { // Only draw handles when user is not resizing painter->setBrush(fillColor); Q_FOREACH(const CropHandle & handle, d->mCropHandleList) { rect = d->handleViewportRect(handle); painter->drawRect(rect); } } } void CropTool::mousePressEvent(QGraphicsSceneMouseEvent* event) { if (event->buttons() != Qt::LeftButton) { event->ignore(); return; } const CropHandle newMovingHandle = d->handleAt(event->pos()); if (event->modifiers() & Qt::ControlModifier && !(newMovingHandle & (CH_Top | CH_Left | CH_Right | CH_Bottom))) { event->ignore(); return; } event->accept(); d->mMovingHandle = newMovingHandle; d->updateCursor(d->mMovingHandle, true /* down */); if (d->mMovingHandle == CH_Content) { d->mLastMouseMovePos = imageView()->mapToImage(event->pos().toPoint()); } // Update to hide handles imageView()->update(); } void CropTool::mouseMoveEvent(QGraphicsSceneMouseEvent* event) { event->accept(); if (event->buttons() != Qt::LeftButton) { return; } const QSize imageSize = imageView()->document()->size(); QPoint point = imageView()->mapToImage(event->pos().toPoint()); int posX = qBound(0, point.x(), imageSize.width() - 1); int posY = qBound(0, point.y(), imageSize.height() - 1); if (d->mMovingHandle == CH_None) { return; } // Adjust edge if (d->mMovingHandle & CH_Top) { d->mRect.setTop(posY); } else if (d->mMovingHandle & CH_Bottom) { d->mRect.setBottom(posY); } if (d->mMovingHandle & CH_Left) { d->mRect.setLeft(posX); } else if (d->mMovingHandle & CH_Right) { d->mRect.setRight(posX); } // Normalize rect and handles (this is useful when user drag the right side // of the crop rect to the left of the left side) if (d->mRect.height() < 0) { d->mMovingHandle = d->mMovingHandle ^(CH_Top | CH_Bottom); } if (d->mRect.width() < 0) { d->mMovingHandle = d->mMovingHandle ^(CH_Left | CH_Right); } d->mRect = d->mRect.normalized(); // Enforce ratio: double ratioToEnforce = d->mCropRatio; // - if user is holding down Ctrl/Shift when resizing rect, lock to current rect ratio if (event->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier) && d->mLockedCropRatio > 0) { ratioToEnforce = d->mLockedCropRatio; } // - if user has restricted the ratio via the GUI if (ratioToEnforce > 0.) { if (d->mMovingHandle == CH_Top || d->mMovingHandle == CH_Bottom) { // Top or bottom int width = int(d->mRect.height() / ratioToEnforce); d->mRect.setWidth(width); } else if (d->mMovingHandle == CH_Left || d->mMovingHandle == CH_Right) { // Left or right int height = int(d->mRect.width() * ratioToEnforce); d->mRect.setHeight(height); } else if (d->mMovingHandle & CH_Top) { // Top left or top right int height = int(d->mRect.width() * ratioToEnforce); d->mRect.setTop(d->mRect.bottom() - height); } else if (d->mMovingHandle & CH_Bottom) { // Bottom left or bottom right int height = int(d->mRect.width() * ratioToEnforce); d->mRect.setHeight(height); } } if (d->mMovingHandle == CH_Content) { d->mRect.translate(point - d->mLastMouseMovePos); d->mLastMouseMovePos = point; } d->keepRectInsideImage(); imageView()->update(); rectUpdated(d->mRect); } void CropTool::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) { event->accept(); d->mMovingHandle = CH_None; d->updateCursor(d->handleAt(event->lastPos()), false); // Update to show handles imageView()->update(); } void CropTool::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event) { if (event->buttons() != Qt::LeftButton || d->handleAt(event->pos()) == CH_None) { event->ignore(); return; } event->accept(); d->mCropWidget->findChild()->accepted(); } void CropTool::hoverMoveEvent(QGraphicsSceneHoverEvent* event) { event->accept(); // Make sure cursor is updated when moving over handles CropHandle handle = d->handleAt(event->lastPos()); d->updateCursor(handle, false /* buttonDown */); } void CropTool::keyPressEvent(QKeyEvent* event) { // Lock crop ratio to current rect when user presses Control or Shift if (event->key() == Qt::Key_Control || event->key() == Qt::Key_Shift) { d->mLockedCropRatio = 1. * d->mRect.height() / d->mRect.width(); } QDialogButtonBox *buttons = d->mCropWidget->findChild(); switch (event->key()) { case Qt::Key_Escape: event->accept(); buttons->rejected(); break; case Qt::Key_Return: - case Qt::Key_Enter: + case Qt::Key_Enter: { event->accept(); - buttons->accepted(); + auto focusButton = static_cast(buttons->focusWidget()); + if (focusButton && buttons->buttonRole(focusButton) == QDialogButtonBox::RejectRole) { + buttons->rejected(); + } else { + buttons->accepted(); + } break; + } default: break; } } void CropTool::toolActivated() { d->mCropWidget->setAdvancedSettingsEnabled(GwenviewConfig::cropAdvancedSettingsEnabled()); d->mCropWidget->setPreserveAspectRatio(GwenviewConfig::cropPreserveAspectRatio()); const int index = GwenviewConfig::cropRatioIndex(); if (index >= 0) { // Preset ratio d->mCropWidget->setCropRatioIndex(index); } else { // Must be a custom ratio, or blank const QSizeF ratio = QSizeF(GwenviewConfig::cropRatioWidth(), GwenviewConfig::cropRatioHeight()); d->mCropWidget->setCropRatio(ratio); } } void CropTool::toolDeactivated() { GwenviewConfig::setCropAdvancedSettingsEnabled(d->mCropWidget->advancedSettingsEnabled()); GwenviewConfig::setCropPreserveAspectRatio(d->mCropWidget->preserveAspectRatio()); GwenviewConfig::setCropRatioIndex(d->mCropWidget->cropRatioIndex()); const QSizeF ratio = d->mCropWidget->cropRatio(); GwenviewConfig::setCropRatioWidth(ratio.width()); GwenviewConfig::setCropRatioHeight(ratio.height()); } void CropTool::slotCropRequested() { CropImageOperation* op = new CropImageOperation(d->mRect); emit imageOperationRequested(op); emit done(); } QWidget* CropTool::widget() const { return d->mCropWidget; } } // namespace diff --git a/lib/redeyereduction/redeyereductiontool.cpp b/lib/redeyereduction/redeyereductiontool.cpp index 2d7e51e0..d7d667c4 100644 --- a/lib/redeyereduction/redeyereductiontool.cpp +++ b/lib/redeyereduction/redeyereductiontool.cpp @@ -1,233 +1,237 @@ // 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. */ // Self #include "redeyereductiontool.h" // Qt #include #include #include #include #include #include // KDE // Local #include #include "gwenviewconfig.h" #include "paintutils.h" #include "redeyereductionimageoperation.h" #include "ui_redeyereductionwidget.h" namespace Gwenview { struct RedEyeReductionWidget : public QWidget, public Ui_RedEyeReductionWidget { RedEyeReductionWidget() { setupUi(this); QPushButton* okButton = mainDialogButtonBox->button(QDialogButtonBox::Ok); okButton->setIcon(QIcon::fromTheme(QStringLiteral("redeyes"))); okButton->setText(i18n("Reduce Red Eye")); } void showNotSetPage() { // Prevent Close button from turning blue upon accepting helpTextLabel->setFocus(); stackedWidget->setCurrentWidget(notSetPage); } void showMainPage() { stackedWidget->setCurrentWidget(mainPage); } }; struct RedEyeReductionToolPrivate { RedEyeReductionTool* q; RedEyeReductionTool::Status mStatus; QPointF mCenter; int mDiameter; RedEyeReductionWidget* mToolWidget; void setupToolWidget() { mToolWidget = new RedEyeReductionWidget; mToolWidget->showNotSetPage(); QObject::connect(mToolWidget->diameterSpinBox, SIGNAL(valueChanged(int)), q, SLOT(setDiameter(int))); QObject::connect(mToolWidget->mainDialogButtonBox, &QDialogButtonBox::accepted, q, &RedEyeReductionTool::slotApplyClicked); QObject::connect(mToolWidget->mainDialogButtonBox, &QDialogButtonBox::rejected, q, &RedEyeReductionTool::done); QObject::connect(mToolWidget->helpDialogButtonBox, &QDialogButtonBox::rejected, q, &RedEyeReductionTool::done); } QRectF rectF() const { if (mStatus == RedEyeReductionTool::NotSet) { return QRectF(); } return QRectF(mCenter.x() - mDiameter / 2, mCenter.y() - mDiameter / 2, mDiameter, mDiameter); } }; RedEyeReductionTool::RedEyeReductionTool(RasterImageView* view) : AbstractRasterImageViewTool(view) , d(new RedEyeReductionToolPrivate) { d->q = this; d->mDiameter = GwenviewConfig::redEyeReductionDiameter(); d->mStatus = NotSet; d->setupToolWidget(); view->document()->startLoadingFullImage(); } RedEyeReductionTool::~RedEyeReductionTool() { GwenviewConfig::setRedEyeReductionDiameter(d->mDiameter); delete d->mToolWidget; delete d; } void RedEyeReductionTool::paint(QPainter* painter) { if (d->mStatus == NotSet) { return; } QRectF docRectF = d->rectF(); imageView()->document()->waitUntilLoaded(); QRect docRect = PaintUtils::containingRect(docRectF); QImage img = imageView()->document()->image().copy(docRect); QRectF imgRectF( docRectF.left() - docRect.left(), docRectF.top() - docRect.top(), docRectF.width(), docRectF.height() ); RedEyeReductionImageOperation::apply(&img, imgRectF); const QRectF viewRectF = imageView()->mapToView(docRectF); painter->drawImage(viewRectF, img, imgRectF); } void RedEyeReductionTool::mousePressEvent(QGraphicsSceneMouseEvent* event) { if (event->buttons() != Qt::LeftButton || event->modifiers() & Qt::ControlModifier) { event->ignore(); return; } event->accept(); if (d->mStatus == NotSet) { d->mToolWidget->diameterSpinBox->setValue(d->mDiameter); d->mToolWidget->showMainPage(); d->mStatus = Adjusting; } d->mCenter = imageView()->mapToImage(event->pos()); imageView()->update(); } void RedEyeReductionTool::mouseMoveEvent(QGraphicsSceneMouseEvent* event) { event->accept(); if (event->buttons() == Qt::NoButton) { return; } d->mCenter = imageView()->mapToImage(event->pos()); imageView()->update(); } void RedEyeReductionTool::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) { // Just prevent the event from reaching the image view event->accept(); } void RedEyeReductionTool::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event) { if (event->buttons() != Qt::LeftButton) { event->ignore(); return; } event->accept(); d->mToolWidget->mainDialogButtonBox->accepted(); } void RedEyeReductionTool::keyPressEvent(QKeyEvent* event) { QDialogButtonBox *buttons = d->mToolWidget->mainDialogButtonBox; switch (event->key()) { case Qt::Key_Escape: event->accept(); buttons->rejected(); break; case Qt::Key_Return: - case Qt::Key_Enter: + case Qt::Key_Enter: { event->accept(); - if (d->mStatus == Adjusting) { + auto focusButton = static_cast(buttons->focusWidget()); + if (focusButton && buttons->buttonRole(focusButton) == QDialogButtonBox::RejectRole) { + buttons->rejected(); + } else if (d->mStatus == Adjusting) { buttons->accepted(); } break; + } default: break; } } void RedEyeReductionTool::toolActivated() { imageView()->setCursor(Qt::CrossCursor); } void RedEyeReductionTool::slotApplyClicked() { QRectF docRectF = d->rectF(); if (!docRectF.isValid()) { qWarning() << "invalid rect"; return; } RedEyeReductionImageOperation* op = new RedEyeReductionImageOperation(docRectF); emit imageOperationRequested(op); d->mStatus = NotSet; d->mToolWidget->showNotSetPage(); } void RedEyeReductionTool::setDiameter(int value) { d->mDiameter = value; imageView()->update(); } QWidget* RedEyeReductionTool::widget() const { return d->mToolWidget; } } // namespace