404 lines
17 KiB
C++
404 lines
17 KiB
C++
// 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"
|