404 lines
17 KiB
C++
Raw Normal View History

2024-06-29 11:52:32 +06:00
// vim: set tabstop=4 shiftwidth=4 expandtab:
/*
Gwenview: an image viewer
SPDX-FileCopyrightText: 2011 Aurélien Gâteau <agateau@kde.org>
SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
// Self
#include "documentviewcontroller.h"
// Local
#include "documentview.h"
#include "gwenview_lib_debug.h"
#include <lib/documentview/abstractrasterimageviewtool.h>
#include <lib/gwenviewconfig.h>
#include <lib/slidecontainer.h>
#include <lib/zoomwidget.h>
// KF
#include <KActionCategory>
#include <KColorUtils>
#include <KLocalizedString>
// Qt
#include <QAction>
#include <QActionGroup>
#include <QApplication>
#include <QPainter>
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<QAction *> 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", "<para>This fits the image into the available viewing area:<list>"
"<item>Images that are bigger than the viewing area are displayed at a smaller size so they fit.</item>"
"<item>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 <emphasis>Enlarge smaller images</emphasis> <link url='%2'>in the settings</link>.</item></list></para>"
"<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>"
"<para>\"%1\" is the default zoom mode for images. This can be changed so images are displayed at a previous zoom value instead "
"<link url='%2'>in the settings</link>.</para>", 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"