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"