File indexing completed on 2024-05-12 04:19:40

0001 // vim: set tabstop=4 shiftwidth=4 expandtab:
0002 /*
0003     Gwenview: an image viewer
0004     SPDX-FileCopyrightText: 2011 Aurélien Gâteau <agateau@kde.org>
0005     SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 // Self
0011 #include "documentviewcontroller.h"
0012 
0013 // Local
0014 #include "documentview.h"
0015 #include "gwenview_lib_debug.h"
0016 #include <lib/documentview/abstractrasterimageviewtool.h>
0017 #include <lib/gwenviewconfig.h>
0018 #include <lib/slidecontainer.h>
0019 #include <lib/zoomwidget.h>
0020 
0021 // KF
0022 #include <KActionCategory>
0023 #include <KColorUtils>
0024 #include <KLocalizedString>
0025 
0026 // Qt
0027 #include <QAction>
0028 #include <QActionGroup>
0029 #include <QApplication>
0030 #include <QPainter>
0031 
0032 namespace Gwenview
0033 {
0034 struct DocumentViewControllerPrivate {
0035     DocumentViewController *q = nullptr;
0036     KActionCollection *mActionCollection = nullptr;
0037     DocumentView *mView = nullptr;
0038     ZoomWidget *mZoomWidget = nullptr;
0039     SlideContainer *mToolContainer = nullptr;
0040 
0041     QAction *mZoomToFitAction = nullptr;
0042     QAction *mZoomToFillAction = nullptr;
0043     QAction *mActualSizeAction = nullptr;
0044     QAction *mZoomInAction = nullptr;
0045     QAction *mZoomOutAction = nullptr;
0046     QAction *mToggleBirdEyeViewAction = nullptr;
0047     QAction *mBackgroundColorModeAuto = nullptr;
0048     QAction *mBackgroundColorModeLight = nullptr;
0049     QAction *mBackgroundColorModeNeutral = nullptr;
0050     QAction *mBackgroundColorModeDark = nullptr;
0051     QList<QAction *> mActions;
0052 
0053     void setupActions()
0054     {
0055         auto view = new KActionCategory(i18nc("@title actions category - means actions changing smth in interface", "View"), mActionCollection);
0056 
0057         mZoomToFitAction = view->addAction(QStringLiteral("view_zoom_to_fit"));
0058         view->collection()->setDefaultShortcut(mZoomToFitAction, Qt::Key_F);
0059         mZoomToFitAction->setCheckable(true);
0060         mZoomToFitAction->setChecked(true);
0061         mZoomToFitAction->setText(i18n("Zoom to Fit"));
0062         mZoomToFitAction->setIcon(QIcon::fromTheme(QStringLiteral("zoom-fit-best")));
0063         mZoomToFitAction->setIconText(i18nc("@action:button Zoom to fit, shown in status bar, keep it short please", "Fit"));
0064         mZoomToFitAction->setToolTip(i18nc("@info:tooltip", "Fit image into the viewing area"));
0065         // clang-format off
0066         // 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
0067         // 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
0068         // but a more elaborate translation would also be fine.
0069         // The text "in the settings" is supposed to sound like clicking it opens the settings.
0070         mZoomToFitAction->setWhatsThis(xi18nc("@info:whatsthis, %1 the action's text", "<para>This fits the image into the available viewing area:<list>"
0071             "<item>Images that are bigger than the viewing area are displayed at a smaller size so they fit.</item>"
0072             "<item>Images that are smaller than the viewing area are displayed at their normal size. If smaller images should instead use all of "
0073             "the available viewing area, turn on <emphasis>Enlarge smaller images</emphasis> <link url='%2'>in the settings</link>.</item></list></para>"
0074             "<para>If \"%1\" is already enabled, pressing it again will switch it off and the image will be displayed at its normal size instead.</para>"
0075             "<para>\"%1\" is the default zoom mode for images. This can be changed so images are displayed at a previous zoom value instead "
0076             "<link url='%2'>in the settings</link>.</para>", mZoomToFitAction->iconText(), QStringLiteral("gwenview:/config/imageview")));
0077             // Keep the previous link address in sync with MainWindow::Private::SettingsOpenerHelper::eventFilter().
0078         // clang-format on
0079 
0080         mZoomToFillAction = view->addAction(QStringLiteral("view_zoom_to_fill"));
0081         view->collection()->setDefaultShortcut(mZoomToFillAction, Qt::SHIFT | Qt::Key_F);
0082         mZoomToFillAction->setCheckable(true);
0083         mZoomToFillAction->setText(i18n("Zoom to fill window by fitting to width or height"));
0084         mZoomToFillAction->setIcon(QIcon::fromTheme(QStringLiteral("zoom-fit-best")));
0085         mZoomToFillAction->setIconText(i18nc("@action:button Zoom to fill (fit width or height), shown in status bar, keep it short please", "Fill"));
0086 
0087         mActualSizeAction = view->addAction(KStandardAction::ActualSize);
0088         mActualSizeAction->setCheckable(true);
0089         mActualSizeAction->setIcon(QIcon::fromTheme(QStringLiteral("zoom-original")));
0090         mActualSizeAction->setIconText(i18nc("Original image size percent value", "100%"));
0091 
0092         mZoomInAction = view->addAction(KStandardAction::ZoomIn);
0093         mZoomOutAction = view->addAction(KStandardAction::ZoomOut);
0094 
0095         mToggleBirdEyeViewAction = view->addAction(QStringLiteral("view_toggle_birdeyeview"));
0096         mToggleBirdEyeViewAction->setCheckable(true);
0097         mToggleBirdEyeViewAction->setChecked(GwenviewConfig::birdEyeViewEnabled());
0098         mToggleBirdEyeViewAction->setText(i18n("Show Bird's Eye View When Zoomed In"));
0099         mToggleBirdEyeViewAction->setIcon(QIcon::fromTheme(QStringLiteral("zoom")));
0100         mToggleBirdEyeViewAction->setEnabled(mView != nullptr);
0101 
0102         mBackgroundColorModeAuto = view->addAction(QStringLiteral("view_background_colormode_auto"));
0103         mBackgroundColorModeAuto->setCheckable(true);
0104         mBackgroundColorModeAuto->setChecked(GwenviewConfig::backgroundColorMode() == DocumentView::BackgroundColorMode::Auto);
0105         mBackgroundColorModeAuto->setText(i18nc("@action", "Follow color scheme"));
0106         mBackgroundColorModeAuto->setEnabled(mView != nullptr);
0107 
0108         mBackgroundColorModeLight = view->addAction(QStringLiteral("view_background_colormode_light"));
0109         mBackgroundColorModeLight->setCheckable(true);
0110         mBackgroundColorModeLight->setChecked(GwenviewConfig::backgroundColorMode() == DocumentView::BackgroundColorMode::Light);
0111         mBackgroundColorModeLight->setText(i18nc("@action", "Light Mode"));
0112         mBackgroundColorModeLight->setEnabled(mView != nullptr);
0113 
0114         mBackgroundColorModeNeutral = view->addAction(QStringLiteral("view_background_colormode_neutral"));
0115         mBackgroundColorModeNeutral->setCheckable(true);
0116         mBackgroundColorModeNeutral->setChecked(GwenviewConfig::backgroundColorMode() == DocumentView::BackgroundColorMode::Neutral);
0117         mBackgroundColorModeNeutral->setText(i18nc("@action", "Neutral Mode"));
0118         mBackgroundColorModeNeutral->setEnabled(mView != nullptr);
0119 
0120         mBackgroundColorModeDark = view->addAction(QStringLiteral("view_background_colormode_dark"));
0121         mBackgroundColorModeDark->setCheckable(true);
0122         mBackgroundColorModeDark->setChecked(GwenviewConfig::backgroundColorMode() == DocumentView::BackgroundColorMode::Dark);
0123         mBackgroundColorModeDark->setText(i18nc("@action", "Dark Mode"));
0124         mBackgroundColorModeDark->setEnabled(mView != nullptr);
0125 
0126         setBackgroundColorModeIcons(mBackgroundColorModeAuto, mBackgroundColorModeLight, mBackgroundColorModeNeutral, mBackgroundColorModeDark);
0127 
0128         auto actionGroup = new QActionGroup(q);
0129         actionGroup->addAction(mBackgroundColorModeAuto);
0130         actionGroup->addAction(mBackgroundColorModeLight);
0131         actionGroup->addAction(mBackgroundColorModeNeutral);
0132         actionGroup->addAction(mBackgroundColorModeDark);
0133         actionGroup->setExclusive(true);
0134 
0135         mActions << mZoomToFitAction << mActualSizeAction << mZoomInAction << mZoomOutAction << mZoomToFillAction << mToggleBirdEyeViewAction
0136                  << mBackgroundColorModeAuto << mBackgroundColorModeLight << mBackgroundColorModeNeutral << mBackgroundColorModeDark;
0137     }
0138 
0139     void setBackgroundColorModeIcons(QAction *autoAction, QAction *lightAction, QAction *neutralAction, QAction *darkAction) const
0140     {
0141         const bool usingLightTheme = qApp->palette().base().color().lightness() > qApp->palette().text().color().lightness();
0142         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
0143                                                               // unless a user adds the action to their toolbar so we go for 16.
0144         QPixmap lightPixmap(pixMapWidth, pixMapWidth);
0145         QPixmap neutralPixmap(pixMapWidth, pixMapWidth);
0146         QPixmap darkPixmap(pixMapWidth, pixMapWidth);
0147         QPixmap autoPixmap(pixMapWidth, pixMapWidth);
0148         // Wipe them clean. If we don't do this, the background will have all sorts of weird artifacts.
0149         lightPixmap.fill(Qt::transparent);
0150         neutralPixmap.fill(Qt::transparent);
0151         darkPixmap.fill(Qt::transparent);
0152         autoPixmap.fill(Qt::transparent);
0153 
0154         const QColor &lightColor = usingLightTheme ? qApp->palette().base().color() : qApp->palette().text().color();
0155         const QColor &darkColor = usingLightTheme ? qApp->palette().text().color() : qApp->palette().base().color();
0156         const QColor neutralColor = KColorUtils::mix(lightColor, darkColor, 0.5);
0157 
0158         paintPixmap(lightPixmap, lightColor);
0159         paintPixmap(neutralPixmap, neutralColor);
0160         paintPixmap(darkPixmap, darkColor);
0161         paintAutoPixmap(autoPixmap, lightColor, darkColor);
0162 
0163         autoAction->setIcon(autoPixmap);
0164         lightAction->setIcon(lightPixmap);
0165         neutralAction->setIcon(neutralPixmap);
0166         darkAction->setIcon(darkPixmap);
0167     }
0168 
0169     void paintPixmap(QPixmap &pixmap, const QColor &color) const
0170     {
0171         QPainter painter;
0172         painter.begin(&pixmap);
0173         painter.setRenderHint(QPainter::Antialiasing);
0174 
0175         // QPainter isn't good at drawing lines that are exactly 1px thick.
0176         const qreal penWidth = qApp->devicePixelRatio() != 1 ? qApp->devicePixelRatio() : qApp->devicePixelRatio() + 0.001;
0177         const QColor penColor = KColorUtils::mix(color, qApp->palette().text().color(), 0.3);
0178         const QPen pen(penColor, penWidth);
0179         const qreal margin = pen.widthF() / 2.0;
0180         const QMarginsF penMargins(margin, margin, margin, margin);
0181         const QRectF rect = pixmap.rect();
0182 
0183         painter.setBrush(color);
0184         painter.setPen(pen);
0185         painter.drawEllipse(rect.marginsRemoved(penMargins));
0186 
0187         painter.end();
0188     }
0189 
0190     void paintAutoPixmap(QPixmap &pixmap, const QColor &lightColor, const QColor &darkColor) const
0191     {
0192         QPainter painter;
0193         painter.begin(&pixmap);
0194         painter.setRenderHint(QPainter::Antialiasing);
0195 
0196         // QPainter isn't good at drawing lines that are exactly 1px thick.
0197         const qreal penWidth = qApp->devicePixelRatio() != 1 ? qApp->devicePixelRatio() : qApp->devicePixelRatio() + 0.001;
0198         const QColor lightPenColor = KColorUtils::mix(lightColor, darkColor, 0.3);
0199         const QPen lightPen(lightPenColor, penWidth);
0200         const QColor darkPenColor = KColorUtils::mix(darkColor, lightColor, 0.3);
0201         const QPen darkPen(darkPenColor, penWidth);
0202 
0203         const qreal margin = lightPen.widthF() / 2.0;
0204         const QMarginsF penMargins(margin, margin, margin, margin);
0205         QRectF rect = pixmap.rect();
0206         rect = rect.marginsRemoved(penMargins);
0207         int lightStartAngle = 45 * 16;
0208         int lightSpanAngle = 180 * 16;
0209         int darkStartAngle = -135 * 16;
0210         int darkSpanAngle = 180 * 16;
0211 
0212         painter.setBrush(lightColor);
0213         painter.setPen(lightPen);
0214         painter.drawChord(rect, lightStartAngle, lightSpanAngle);
0215         painter.setBrush(darkColor);
0216         painter.setPen(darkPen);
0217         painter.drawChord(rect, darkStartAngle, darkSpanAngle);
0218 
0219         painter.end();
0220     }
0221 
0222     void connectZoomWidget()
0223     {
0224         if (!mZoomWidget || !mView) {
0225             return;
0226         }
0227 
0228         // from mZoomWidget to mView
0229         QObject::connect(mZoomWidget, &ZoomWidget::zoomChanged, mView, &DocumentView::setZoom);
0230 
0231         // from mView to mZoomWidget
0232         QObject::connect(mView, &DocumentView::minimumZoomChanged, mZoomWidget, &ZoomWidget::setMinimumZoom);
0233         QObject::connect(mView, &DocumentView::zoomChanged, mZoomWidget, &ZoomWidget::setZoom);
0234 
0235         mZoomWidget->setMinimumZoom(mView->minimumZoom());
0236         mZoomWidget->setZoom(mView->zoom());
0237     }
0238 
0239     void updateZoomWidgetVisibility()
0240     {
0241         if (!mZoomWidget) {
0242             return;
0243         }
0244         mZoomWidget->setVisible(mView && mView->canZoom());
0245     }
0246 
0247     void updateActions()
0248     {
0249         const bool enabled = mView && mView->isVisible() && mView->canZoom();
0250         for (QAction *action : qAsConst(mActions)) {
0251             action->setEnabled(enabled);
0252         }
0253     }
0254 };
0255 
0256 DocumentViewController::DocumentViewController(KActionCollection *actionCollection, QObject *parent)
0257     : QObject(parent)
0258     , d(new DocumentViewControllerPrivate)
0259 {
0260     d->q = this;
0261     d->mActionCollection = actionCollection;
0262     d->mView = nullptr;
0263     d->mZoomWidget = nullptr;
0264     d->mToolContainer = nullptr;
0265 
0266     d->setupActions();
0267 }
0268 
0269 DocumentViewController::~DocumentViewController()
0270 {
0271     delete d;
0272 }
0273 
0274 void DocumentViewController::setView(DocumentView *view)
0275 {
0276     // Forget old view
0277     if (d->mView) {
0278         disconnect(d->mView, nullptr, this, nullptr);
0279         for (QAction *action : qAsConst(d->mActions)) {
0280             disconnect(action, nullptr, d->mView, nullptr);
0281         }
0282         disconnect(d->mBackgroundColorModeAuto, &QAction::triggered, this, nullptr);
0283         disconnect(d->mBackgroundColorModeLight, &QAction::triggered, this, nullptr);
0284         disconnect(d->mBackgroundColorModeNeutral, &QAction::triggered, this, nullptr);
0285         disconnect(d->mBackgroundColorModeDark, &QAction::triggered, this, nullptr);
0286 
0287         disconnect(d->mZoomWidget, nullptr, d->mView, nullptr);
0288     }
0289 
0290     // Connect new view
0291     d->mView = view;
0292     if (!d->mView) {
0293         return;
0294     }
0295     connect(d->mView, &DocumentView::adapterChanged, this, &DocumentViewController::slotAdapterChanged);
0296     connect(d->mView, &DocumentView::zoomToFitChanged, this, &DocumentViewController::updateZoomToFitActionFromView);
0297     connect(d->mView, &DocumentView::zoomToFillChanged, this, &DocumentViewController::updateZoomToFillActionFromView);
0298     connect(d->mView, &DocumentView::currentToolChanged, this, &DocumentViewController::updateTool);
0299 
0300     connect(d->mZoomToFitAction, &QAction::triggered, d->mView, &DocumentView::toggleZoomToFit);
0301     connect(d->mZoomToFillAction, &QAction::triggered, d->mView, &DocumentView::toggleZoomToFill);
0302     connect(d->mActualSizeAction, &QAction::triggered, d->mView, &DocumentView::zoomActualSize);
0303     connect(d->mZoomInAction, SIGNAL(triggered()), d->mView, SLOT(zoomIn()));
0304     connect(d->mZoomOutAction, SIGNAL(triggered()), d->mView, SLOT(zoomOut()));
0305 
0306     connect(d->mToggleBirdEyeViewAction, &QAction::triggered, d->mView, &DocumentView::toggleBirdEyeView);
0307 
0308     connect(d->mBackgroundColorModeAuto, &QAction::triggered, this, [this]() {
0309         d->mView->setBackgroundColorMode(DocumentView::BackgroundColorMode::Auto);
0310         qApp->paletteChanged(qApp->palette());
0311     });
0312     connect(d->mBackgroundColorModeLight, &QAction::triggered, this, [this]() {
0313         d->mView->setBackgroundColorMode(DocumentView::BackgroundColorMode::Light);
0314         qApp->paletteChanged(qApp->palette());
0315     });
0316     connect(d->mBackgroundColorModeNeutral, &QAction::triggered, this, [this]() {
0317         d->mView->setBackgroundColorMode(DocumentView::BackgroundColorMode::Neutral);
0318         qApp->paletteChanged(qApp->palette());
0319     });
0320     connect(d->mBackgroundColorModeDark, &QAction::triggered, this, [this]() {
0321         d->mView->setBackgroundColorMode(DocumentView::BackgroundColorMode::Dark);
0322         qApp->paletteChanged(qApp->palette());
0323     });
0324 
0325     d->updateActions();
0326     updateZoomToFitActionFromView();
0327     updateZoomToFillActionFromView();
0328     updateTool();
0329 
0330     // Sync zoom widget
0331     d->connectZoomWidget();
0332     d->updateZoomWidgetVisibility();
0333 }
0334 
0335 DocumentView *DocumentViewController::view() const
0336 {
0337     return d->mView;
0338 }
0339 
0340 void DocumentViewController::setZoomWidget(ZoomWidget *widget)
0341 {
0342     d->mZoomWidget = widget;
0343 
0344     d->mZoomWidget->setActions(d->mZoomToFitAction, d->mActualSizeAction, d->mZoomInAction, d->mZoomOutAction, d->mZoomToFillAction);
0345 
0346     d->mZoomWidget->setMaximumZoom(qreal(DocumentView::MaximumZoom));
0347 
0348     d->connectZoomWidget();
0349     d->updateZoomWidgetVisibility();
0350 }
0351 
0352 ZoomWidget *DocumentViewController::zoomWidget() const
0353 {
0354     return d->mZoomWidget;
0355 }
0356 
0357 void DocumentViewController::slotAdapterChanged()
0358 {
0359     d->updateActions();
0360     d->updateZoomWidgetVisibility();
0361 }
0362 
0363 void DocumentViewController::updateZoomToFitActionFromView()
0364 {
0365     d->mZoomToFitAction->setChecked(d->mView->zoomToFit());
0366 }
0367 
0368 void DocumentViewController::updateZoomToFillActionFromView()
0369 {
0370     d->mZoomToFillAction->setChecked(d->mView->zoomToFill());
0371 }
0372 
0373 void DocumentViewController::updateTool()
0374 {
0375     if (!d->mToolContainer) {
0376         return;
0377     }
0378     AbstractRasterImageViewTool *tool = d->mView->currentTool();
0379     if (tool && tool->widget()) {
0380         // Use a QueuedConnection to ensure the size of the view has been
0381         // updated by the time the slot is called.
0382         connect(d->mToolContainer, &SlideContainer::slidedIn, tool, &AbstractRasterImageViewTool::onWidgetSlidedIn, Qt::QueuedConnection);
0383         d->mToolContainer->setContent(tool->widget());
0384         d->mToolContainer->slideIn();
0385     } else {
0386         d->mToolContainer->slideOut();
0387     }
0388 }
0389 
0390 void DocumentViewController::reset()
0391 {
0392     setView(nullptr);
0393     d->updateActions();
0394 }
0395 
0396 void DocumentViewController::setToolContainer(SlideContainer *container)
0397 {
0398     d->mToolContainer = container;
0399 }
0400 
0401 } // namespace
0402 
0403 #include "moc_documentviewcontroller.cpp"