// vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer SPDX-FileCopyrightText: 2011 Aurélien Gâteau SPDX-FileCopyrightText: 2021 Noah Davis SPDX-License-Identifier: GPL-2.0-or-later */ // Self #include "documentviewcontroller.h" // Local #include "documentview.h" #include "gwenview_lib_debug.h" #include #include #include #include // KF #include #include #include // Qt #include #include #include #include namespace Gwenview { struct DocumentViewControllerPrivate { DocumentViewController *q = nullptr; KActionCollection *mActionCollection = nullptr; DocumentView *mView = nullptr; ZoomWidget *mZoomWidget = nullptr; SlideContainer *mToolContainer = nullptr; QAction *mZoomToFitAction = nullptr; QAction *mZoomToFillAction = nullptr; QAction *mActualSizeAction = nullptr; QAction *mZoomInAction = nullptr; QAction *mZoomOutAction = nullptr; QAction *mToggleBirdEyeViewAction = nullptr; QAction *mBackgroundColorModeAuto = nullptr; QAction *mBackgroundColorModeLight = nullptr; QAction *mBackgroundColorModeNeutral = nullptr; QAction *mBackgroundColorModeDark = nullptr; QList mActions; void setupActions() { auto view = new KActionCategory(i18nc("@title actions category - means actions changing smth in interface", "View"), mActionCollection); mZoomToFitAction = view->addAction(QStringLiteral("view_zoom_to_fit")); view->collection()->setDefaultShortcut(mZoomToFitAction, Qt::Key_F); mZoomToFitAction->setCheckable(true); mZoomToFitAction->setChecked(true); mZoomToFitAction->setText(i18n("Zoom to Fit")); mZoomToFitAction->setIcon(QIcon::fromTheme(QStringLiteral("zoom-fit-best"))); mZoomToFitAction->setIconText(i18nc("@action:button Zoom to fit, shown in status bar, keep it short please", "Fit")); mZoomToFitAction->setToolTip(i18nc("@info:tooltip", "Fit image into the viewing area")); // clang-format off // i18n: "a previous zoom value" is worded in such an unclear way because it can either be the zoom value for the image viewed previously or the // zoom value that was used the last time this same image was viewed. Being more clear about this isn't really necessary here so I kept it short // but a more elaborate translation would also be fine. // The text "in the settings" is supposed to sound like clicking it opens the settings. mZoomToFitAction->setWhatsThis(xi18nc("@info:whatsthis, %1 the action's text", "This fits the image into the available viewing area:" "Images that are bigger than the viewing area are displayed at a smaller size so they fit." "Images that are smaller than the viewing area are displayed at their normal size. If smaller images should instead use all of " "the available viewing area, turn on Enlarge smaller images in the settings." "If \"%1\" is already enabled, pressing it again will switch it off and the image will be displayed at its normal size instead." "\"%1\" is the default zoom mode for images. This can be changed so images are displayed at a previous zoom value instead " "in the settings.", mZoomToFitAction->iconText(), QStringLiteral("gwenview:/config/imageview"))); // Keep the previous link address in sync with MainWindow::Private::SettingsOpenerHelper::eventFilter(). // clang-format on mZoomToFillAction = view->addAction(QStringLiteral("view_zoom_to_fill")); view->collection()->setDefaultShortcut(mZoomToFillAction, Qt::SHIFT | Qt::Key_F); mZoomToFillAction->setCheckable(true); mZoomToFillAction->setText(i18n("Zoom to fill window by fitting to width or height")); mZoomToFillAction->setIcon(QIcon::fromTheme(QStringLiteral("zoom-fit-best"))); mZoomToFillAction->setIconText(i18nc("@action:button Zoom to fill (fit width or height), shown in status bar, keep it short please", "Fill")); mActualSizeAction = view->addAction(KStandardAction::ActualSize); mActualSizeAction->setCheckable(true); mActualSizeAction->setIcon(QIcon::fromTheme(QStringLiteral("zoom-original"))); mActualSizeAction->setIconText(i18nc("Original image size percent value", "100%")); mZoomInAction = view->addAction(KStandardAction::ZoomIn); mZoomOutAction = view->addAction(KStandardAction::ZoomOut); mToggleBirdEyeViewAction = view->addAction(QStringLiteral("view_toggle_birdeyeview")); mToggleBirdEyeViewAction->setCheckable(true); mToggleBirdEyeViewAction->setChecked(GwenviewConfig::birdEyeViewEnabled()); mToggleBirdEyeViewAction->setText(i18n("Show Bird's Eye View When Zoomed In")); mToggleBirdEyeViewAction->setIcon(QIcon::fromTheme(QStringLiteral("zoom"))); mToggleBirdEyeViewAction->setEnabled(mView != nullptr); mBackgroundColorModeAuto = view->addAction(QStringLiteral("view_background_colormode_auto")); mBackgroundColorModeAuto->setCheckable(true); mBackgroundColorModeAuto->setChecked(GwenviewConfig::backgroundColorMode() == DocumentView::BackgroundColorMode::Auto); mBackgroundColorModeAuto->setText(i18nc("@action", "Follow color scheme")); mBackgroundColorModeAuto->setEnabled(mView != nullptr); mBackgroundColorModeLight = view->addAction(QStringLiteral("view_background_colormode_light")); mBackgroundColorModeLight->setCheckable(true); mBackgroundColorModeLight->setChecked(GwenviewConfig::backgroundColorMode() == DocumentView::BackgroundColorMode::Light); mBackgroundColorModeLight->setText(i18nc("@action", "Light Mode")); mBackgroundColorModeLight->setEnabled(mView != nullptr); mBackgroundColorModeNeutral = view->addAction(QStringLiteral("view_background_colormode_neutral")); mBackgroundColorModeNeutral->setCheckable(true); mBackgroundColorModeNeutral->setChecked(GwenviewConfig::backgroundColorMode() == DocumentView::BackgroundColorMode::Neutral); mBackgroundColorModeNeutral->setText(i18nc("@action", "Neutral Mode")); mBackgroundColorModeNeutral->setEnabled(mView != nullptr); mBackgroundColorModeDark = view->addAction(QStringLiteral("view_background_colormode_dark")); mBackgroundColorModeDark->setCheckable(true); mBackgroundColorModeDark->setChecked(GwenviewConfig::backgroundColorMode() == DocumentView::BackgroundColorMode::Dark); mBackgroundColorModeDark->setText(i18nc("@action", "Dark Mode")); mBackgroundColorModeDark->setEnabled(mView != nullptr); setBackgroundColorModeIcons(mBackgroundColorModeAuto, mBackgroundColorModeLight, mBackgroundColorModeNeutral, mBackgroundColorModeDark); auto actionGroup = new QActionGroup(q); actionGroup->addAction(mBackgroundColorModeAuto); actionGroup->addAction(mBackgroundColorModeLight); actionGroup->addAction(mBackgroundColorModeNeutral); actionGroup->addAction(mBackgroundColorModeDark); actionGroup->setExclusive(true); mActions << mZoomToFitAction << mActualSizeAction << mZoomInAction << mZoomOutAction << mZoomToFillAction << mToggleBirdEyeViewAction << mBackgroundColorModeAuto << mBackgroundColorModeLight << mBackgroundColorModeNeutral << mBackgroundColorModeDark; } void setBackgroundColorModeIcons(QAction *autoAction, QAction *lightAction, QAction *neutralAction, QAction *darkAction) const { const bool usingLightTheme = qApp->palette().base().color().lightness() > qApp->palette().text().color().lightness(); const int pixMapWidth(16 * qApp->devicePixelRatio()); // Default icon size in menus is 16 but on toolbars is 22. The icon will only show up in QMenus // unless a user adds the action to their toolbar so we go for 16. QPixmap lightPixmap(pixMapWidth, pixMapWidth); QPixmap neutralPixmap(pixMapWidth, pixMapWidth); QPixmap darkPixmap(pixMapWidth, pixMapWidth); QPixmap autoPixmap(pixMapWidth, pixMapWidth); // Wipe them clean. If we don't do this, the background will have all sorts of weird artifacts. lightPixmap.fill(Qt::transparent); neutralPixmap.fill(Qt::transparent); darkPixmap.fill(Qt::transparent); autoPixmap.fill(Qt::transparent); const QColor &lightColor = usingLightTheme ? qApp->palette().base().color() : qApp->palette().text().color(); const QColor &darkColor = usingLightTheme ? qApp->palette().text().color() : qApp->palette().base().color(); const QColor neutralColor = KColorUtils::mix(lightColor, darkColor, 0.5); paintPixmap(lightPixmap, lightColor); paintPixmap(neutralPixmap, neutralColor); paintPixmap(darkPixmap, darkColor); paintAutoPixmap(autoPixmap, lightColor, darkColor); autoAction->setIcon(autoPixmap); lightAction->setIcon(lightPixmap); neutralAction->setIcon(neutralPixmap); darkAction->setIcon(darkPixmap); } void paintPixmap(QPixmap &pixmap, const QColor &color) const { QPainter painter; painter.begin(&pixmap); painter.setRenderHint(QPainter::Antialiasing); // QPainter isn't good at drawing lines that are exactly 1px thick. const qreal penWidth = qApp->devicePixelRatio() != 1 ? qApp->devicePixelRatio() : qApp->devicePixelRatio() + 0.001; const QColor penColor = KColorUtils::mix(color, qApp->palette().text().color(), 0.3); const QPen pen(penColor, penWidth); const qreal margin = pen.widthF() / 2.0; const QMarginsF penMargins(margin, margin, margin, margin); const QRectF rect = pixmap.rect(); painter.setBrush(color); painter.setPen(pen); painter.drawEllipse(rect.marginsRemoved(penMargins)); painter.end(); } void paintAutoPixmap(QPixmap &pixmap, const QColor &lightColor, const QColor &darkColor) const { QPainter painter; painter.begin(&pixmap); painter.setRenderHint(QPainter::Antialiasing); // QPainter isn't good at drawing lines that are exactly 1px thick. const qreal penWidth = qApp->devicePixelRatio() != 1 ? qApp->devicePixelRatio() : qApp->devicePixelRatio() + 0.001; const QColor lightPenColor = KColorUtils::mix(lightColor, darkColor, 0.3); const QPen lightPen(lightPenColor, penWidth); const QColor darkPenColor = KColorUtils::mix(darkColor, lightColor, 0.3); const QPen darkPen(darkPenColor, penWidth); const qreal margin = lightPen.widthF() / 2.0; const QMarginsF penMargins(margin, margin, margin, margin); QRectF rect = pixmap.rect(); rect = rect.marginsRemoved(penMargins); int lightStartAngle = 45 * 16; int lightSpanAngle = 180 * 16; int darkStartAngle = -135 * 16; int darkSpanAngle = 180 * 16; painter.setBrush(lightColor); painter.setPen(lightPen); painter.drawChord(rect, lightStartAngle, lightSpanAngle); painter.setBrush(darkColor); painter.setPen(darkPen); painter.drawChord(rect, darkStartAngle, darkSpanAngle); painter.end(); } void connectZoomWidget() { if (!mZoomWidget || !mView) { return; } // from mZoomWidget to mView QObject::connect(mZoomWidget, &ZoomWidget::zoomChanged, mView, &DocumentView::setZoom); // from mView to mZoomWidget QObject::connect(mView, &DocumentView::minimumZoomChanged, mZoomWidget, &ZoomWidget::setMinimumZoom); QObject::connect(mView, &DocumentView::zoomChanged, mZoomWidget, &ZoomWidget::setZoom); mZoomWidget->setMinimumZoom(mView->minimumZoom()); mZoomWidget->setZoom(mView->zoom()); } void updateZoomWidgetVisibility() { if (!mZoomWidget) { return; } mZoomWidget->setVisible(mView && mView->canZoom()); } void updateActions() { const bool enabled = mView && mView->isVisible() && mView->canZoom(); for (QAction *action : qAsConst(mActions)) { action->setEnabled(enabled); } } }; DocumentViewController::DocumentViewController(KActionCollection *actionCollection, QObject *parent) : QObject(parent) , d(new DocumentViewControllerPrivate) { d->q = this; d->mActionCollection = actionCollection; d->mView = nullptr; d->mZoomWidget = nullptr; d->mToolContainer = nullptr; d->setupActions(); } DocumentViewController::~DocumentViewController() { delete d; } void DocumentViewController::setView(DocumentView *view) { // Forget old view if (d->mView) { disconnect(d->mView, nullptr, this, nullptr); for (QAction *action : qAsConst(d->mActions)) { disconnect(action, nullptr, d->mView, nullptr); } disconnect(d->mBackgroundColorModeAuto, &QAction::triggered, this, nullptr); disconnect(d->mBackgroundColorModeLight, &QAction::triggered, this, nullptr); disconnect(d->mBackgroundColorModeNeutral, &QAction::triggered, this, nullptr); disconnect(d->mBackgroundColorModeDark, &QAction::triggered, this, nullptr); disconnect(d->mZoomWidget, nullptr, d->mView, nullptr); } // Connect new view d->mView = view; if (!d->mView) { return; } connect(d->mView, &DocumentView::adapterChanged, this, &DocumentViewController::slotAdapterChanged); connect(d->mView, &DocumentView::zoomToFitChanged, this, &DocumentViewController::updateZoomToFitActionFromView); connect(d->mView, &DocumentView::zoomToFillChanged, this, &DocumentViewController::updateZoomToFillActionFromView); connect(d->mView, &DocumentView::currentToolChanged, this, &DocumentViewController::updateTool); connect(d->mZoomToFitAction, &QAction::triggered, d->mView, &DocumentView::toggleZoomToFit); connect(d->mZoomToFillAction, &QAction::triggered, d->mView, &DocumentView::toggleZoomToFill); connect(d->mActualSizeAction, &QAction::triggered, d->mView, &DocumentView::zoomActualSize); connect(d->mZoomInAction, SIGNAL(triggered()), d->mView, SLOT(zoomIn())); connect(d->mZoomOutAction, SIGNAL(triggered()), d->mView, SLOT(zoomOut())); connect(d->mToggleBirdEyeViewAction, &QAction::triggered, d->mView, &DocumentView::toggleBirdEyeView); connect(d->mBackgroundColorModeAuto, &QAction::triggered, this, [this]() { d->mView->setBackgroundColorMode(DocumentView::BackgroundColorMode::Auto); qApp->paletteChanged(qApp->palette()); }); connect(d->mBackgroundColorModeLight, &QAction::triggered, this, [this]() { d->mView->setBackgroundColorMode(DocumentView::BackgroundColorMode::Light); qApp->paletteChanged(qApp->palette()); }); connect(d->mBackgroundColorModeNeutral, &QAction::triggered, this, [this]() { d->mView->setBackgroundColorMode(DocumentView::BackgroundColorMode::Neutral); qApp->paletteChanged(qApp->palette()); }); connect(d->mBackgroundColorModeDark, &QAction::triggered, this, [this]() { d->mView->setBackgroundColorMode(DocumentView::BackgroundColorMode::Dark); qApp->paletteChanged(qApp->palette()); }); d->updateActions(); updateZoomToFitActionFromView(); updateZoomToFillActionFromView(); updateTool(); // Sync zoom widget d->connectZoomWidget(); d->updateZoomWidgetVisibility(); } DocumentView *DocumentViewController::view() const { return d->mView; } void DocumentViewController::setZoomWidget(ZoomWidget *widget) { d->mZoomWidget = widget; d->mZoomWidget->setActions(d->mZoomToFitAction, d->mActualSizeAction, d->mZoomInAction, d->mZoomOutAction, d->mZoomToFillAction); d->mZoomWidget->setMaximumZoom(qreal(DocumentView::MaximumZoom)); d->connectZoomWidget(); d->updateZoomWidgetVisibility(); } ZoomWidget *DocumentViewController::zoomWidget() const { return d->mZoomWidget; } void DocumentViewController::slotAdapterChanged() { d->updateActions(); d->updateZoomWidgetVisibility(); } void DocumentViewController::updateZoomToFitActionFromView() { d->mZoomToFitAction->setChecked(d->mView->zoomToFit()); } void DocumentViewController::updateZoomToFillActionFromView() { d->mZoomToFillAction->setChecked(d->mView->zoomToFill()); } void DocumentViewController::updateTool() { if (!d->mToolContainer) { return; } AbstractRasterImageViewTool *tool = d->mView->currentTool(); if (tool && tool->widget()) { // Use a QueuedConnection to ensure the size of the view has been // updated by the time the slot is called. connect(d->mToolContainer, &SlideContainer::slidedIn, tool, &AbstractRasterImageViewTool::onWidgetSlidedIn, Qt::QueuedConnection); d->mToolContainer->setContent(tool->widget()); d->mToolContainer->slideIn(); } else { d->mToolContainer->slideOut(); } } void DocumentViewController::reset() { setView(nullptr); d->updateActions(); } void DocumentViewController::setToolContainer(SlideContainer *container) { d->mToolContainer = container; } } // namespace #include "moc_documentviewcontroller.cpp"