Commit 2c829f4e authored by Noah Davis's avatar Noah Davis 🌵
Browse files

ZoomWidget: Remove buttons, use combobox instead of spinbox

supports custom input

generates zoom presets

Also don't go to browse mode when enter is pressed because it conflicts with pressing enter to confirm values
parent 976978b9
......@@ -781,7 +781,6 @@ void ViewMainPage::slotEnterPressed()
}
}
}
emit goToBrowseModeRequested();
}
bool ViewMainPage::eventFilter(QObject* watched, QEvent* event)
......
......@@ -140,6 +140,7 @@ set(gwenviewlib_SRCS
transformimageoperation.cpp
urlutils.cpp
widgetfloater.cpp
zoomcombobox/zoomcombobox.cpp
zoomslider.cpp
zoomwidget.cpp
scrollerutils.cpp
......
......@@ -34,7 +34,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA
#include <QStandardPaths>
#include <QPainter>
#include <QApplication>
namespace Gwenview
{
......@@ -277,6 +276,9 @@ bool AbstractImageView::zoomToFill() const
void AbstractImageView::setZoomToFit(bool on)
{
if (d->mZoomToFit == on) {
return;
}
d->mZoomToFit = on;
if (on) {
d->mZoomToFill = false;
......@@ -290,6 +292,9 @@ void AbstractImageView::setZoomToFit(bool on)
void AbstractImageView::setZoomToFill(bool on, const QPointF& center)
{
if (d->mZoomToFill == on) {
return;
}
d->mZoomToFill = on;
if (on) {
d->mZoomToFit = false;
......
......@@ -36,7 +36,9 @@ class AlphaBackgroundItem;
struct AbstractImageViewPrivate;
/**
*
* The abstract base class used to implement common functionality of raster and vector image views
* like for example zooming, computing the area where the image will be displayed in the view and
* dealing with a background for transparent areas.
*/
class AbstractImageView : public QGraphicsWidget
{
......@@ -127,8 +129,11 @@ public Q_SLOTS:
void updateCursor();
Q_SIGNALS:
/** Emitted when the zoom mode changes to or from "Fit". */
void zoomToFitChanged(bool);
/** Emitted when the zoom mode changes to or from "Fill". */
void zoomToFillChanged(bool);
/** Emitted when the zoom value changes in any way. */
void zoomChanged(qreal);
void zoomInRequested(const QPointF&);
void zoomOutRequested(const QPointF&);
......
......@@ -79,7 +79,7 @@ struct DocumentViewControllerPrivate
mActualSizeAction = view->addAction(KStandardAction::ActualSize);
mActualSizeAction->setCheckable(true);
mActualSizeAction->setIcon(QIcon::fromTheme(QStringLiteral("zoom-original")));
mActualSizeAction->setIconText(i18nc("@action:button Zoom to original size, shown in status bar, keep it short please", "100%"));
mActualSizeAction->setIconText(QLocale().toString(100).append(QLocale().percent()));
mZoomInAction = view->addAction(KStandardAction::ZoomIn);
mZoomOutAction = view->addAction(KStandardAction::ZoomOut);
......@@ -100,8 +100,10 @@ struct DocumentViewControllerPrivate
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);
......@@ -165,10 +167,13 @@ void DocumentViewController::setView(DocumentView* view)
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, SIGNAL(triggered()),
d->mView, SLOT(zoomActualSize()));
connect(d->mZoomToFitAction, &QAction::toggled, d->mView, &DocumentView::setZoomToFit);
connect(d->mZoomToFillAction, &QAction::toggled, d->mView, &DocumentView::setZoomToFill);
connect(d->mActualSizeAction, &QAction::toggled, d->mView, [this](bool checked){
if (checked) {
d->mView->setZoom(1.0);
}
});
connect(d->mZoomInAction, SIGNAL(triggered()),
d->mView, SLOT(zoomIn()));
connect(d->mZoomOutAction, SIGNAL(triggered()),
......
// SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
#include "zoomcombobox.h"
#include "zoomcombobox_p.h"
#include <QAction>
#include <QAbstractItemView>
#include <QEvent>
#include <QLineEdit>
#include <QMouseEvent>
#include <cmath>
using namespace Gwenview;
ZoomValidator::ZoomValidator(qreal minimum, qreal maximum, ZoomComboBox *q, ZoomComboBoxPrivate *d, QWidget* parent)
: QValidator(parent)
, m_minimum(minimum)
, m_maximum(maximum)
, m_zoomComboBox(q)
, m_zoomComboBoxPrivate(d)
{
}
ZoomValidator::~ZoomValidator() noexcept
{
}
qreal ZoomValidator::minimum() const
{
return m_minimum;
}
void ZoomValidator::setMinimum(const qreal minimum)
{
if (m_minimum == minimum) {
return;
}
m_minimum = minimum;
Q_EMIT changed();
}
qreal ZoomValidator::maximum() const
{
return m_maximum;
}
void ZoomValidator::setMaximum(const qreal maximum)
{
if (m_maximum == maximum) {
return;
}
m_maximum = maximum;
Q_EMIT changed();
}
QValidator::State ZoomValidator::validate(QString& input, int& pos) const
{
Q_UNUSED(pos)
if (m_zoomComboBox->findText(input, Qt::MatchFixedString) > -1) {
return QValidator::Acceptable;
}
QString copy = input.trimmed();
copy.remove(locale().groupSeparator());
copy.remove(locale().percent());
const bool startsWithNumber = copy.constBegin()->isNumber();
if (startsWithNumber || copy.isEmpty()) {
return QValidator::Intermediate;
}
QValidator::State state;
bool ok = false;
int value = locale().toInt(copy, &ok);
if (!ok || value < std::ceil(m_minimum * 100) || value > std::floor(m_maximum * 100)) {
state = QValidator::Intermediate;
} else {
state = QValidator::Acceptable;
}
return state;
}
ZoomComboBoxPrivate::ZoomComboBoxPrivate(ZoomComboBox *q)
: q_ptr(q)
, validator(new ZoomValidator(0, 0, q, this))
{
}
ZoomComboBox::ZoomComboBox(QWidget* parent)
: QComboBox(parent)
, d_ptr(new ZoomComboBoxPrivate(this))
{
Q_D(ZoomComboBox);
d->validator->setObjectName(QLatin1String("zoomValidator"));
setValidator(d->validator);
setEditable(true);
setInsertPolicy(QComboBox::NoInsert);
// QLocale::percent() will return a QString in Qt 6.
// Qt encourages using QString(locale().percent()) in QLocale documentation.
int percentLength = QString(locale().percent()).length();
setMinimumContentsLength(locale().toString(9999).length() + percentLength);
connect(lineEdit(), &QLineEdit::textEdited, this, [this, d](const QString &text){
const bool startsWithNumber = text.constBegin()->isNumber();
int matches = 0;
for (int i = 0; i < count(); ++i) {
if (itemText(i).startsWith(text, Qt::CaseInsensitive)) {
matches += 1;
}
}
Qt::MatchFlags matchFlags = startsWithNumber || matches > 1 ? Qt::MatchFixedString : Qt::MatchStartsWith;
const int textIndex = findText(text, matchFlags);
if (textIndex < 0) {
setValue(valueFromText(text));
}
activateAndChangeZoomTo(textIndex);
lineEdit()->setCursorPosition(lineEdit()->cursorPosition() - 1);
});
connect(this, qOverload<int>(&ZoomComboBox::highlighted),
this, &ZoomComboBox::changeZoomTo);
view()->installEventFilter(this);
connect(this, qOverload<int>(&ZoomComboBox::activated),
this, &ZoomComboBox::activateAndChangeZoomTo);
}
ZoomComboBox::~ZoomComboBox() noexcept
{
}
void ZoomComboBox::setActions(QAction* zoomToFitAction, QAction* zoomToFillAction, QAction* actualSizeAction)
{
Q_D(ZoomComboBox);
d->setActions(zoomToFitAction, zoomToFillAction, actualSizeAction);
connect(zoomToFitAction, &QAction::toggled,
this, &ZoomComboBox::updateDisplayedText);
connect(zoomToFillAction, &QAction::toggled,
this, &ZoomComboBox::updateDisplayedText);
}
void ZoomComboBoxPrivate::setActions(QAction* zoomToFitAction, QAction* zoomToFillAction, QAction* actualSizeAction)
{
Q_Q(ZoomComboBox);
q->clear();
q->addItem(zoomToFitAction->iconText(), QVariant::fromValue(zoomToFitAction)); // index = 0
q->addItem(zoomToFillAction->iconText(), QVariant::fromValue(zoomToFillAction)); // index = 1
q->addItem(actualSizeAction->iconText(), QVariant::fromValue(actualSizeAction)); // index = 2
mZoomToFitAction = zoomToFitAction;
mZoomToFillAction = zoomToFillAction;
mActualSizeAction = actualSizeAction;
}
qreal ZoomComboBox::value() const
{
Q_D(const ZoomComboBox);
return d->value;
}
void ZoomComboBox::setValue(qreal value)
{
Q_D(ZoomComboBox);
d->value = value;
updateDisplayedText();
}
qreal ZoomComboBox::minimum() const
{
Q_D(const ZoomComboBox);
return d->validator->minimum();
}
void ZoomComboBox::setMinimum(qreal minimum)
{
Q_D(ZoomComboBox);
if (this->minimum() == minimum) {
return;
}
d->validator->setMinimum(minimum);
setValue(qMax(minimum, d->value));
// Generate zoom presets below 100%
// FIXME: combobox value gets reset to last index value when this code runs
const int zoomToFillActionIndex = findData(QVariant::fromValue(d->mZoomToFillAction));
const int actualSizeActionIndex = findData(QVariant::fromValue(d->mActualSizeAction));
for (int i = actualSizeActionIndex - 1; i > zoomToFillActionIndex; --i) {
removeItem(i);
}
qreal value = minimum;
for (int i = zoomToFillActionIndex + 1; value < 1.0; ++i) {
insertItem(i, textFromValue(value), QVariant::fromValue(value));
value *= 2;
}
}
qreal ZoomComboBox::maximum() const
{
Q_D(const ZoomComboBox);
return d->validator->maximum();
}
void ZoomComboBox::setMaximum(qreal maximum)
{
Q_D(ZoomComboBox);
if (this->maximum() == maximum) {
return;
}
d->validator->setMaximum(maximum);
setValue(qMin(d->value, maximum));
// Generate zoom presets above 100%
// NOTE: This probably has the same problem as setMinimum(),
// but the problem is never enountered since max zoom doesn't actually change
const int actualSizeActionIndex = findData(QVariant::fromValue(d->mActualSizeAction));
const int count = this->count();
for(int i = actualSizeActionIndex + 1; i < count; ++i) {
removeItem(i);
}
qreal value = 2.0;
while (value < maximum) {
addItem(textFromValue(value), QVariant::fromValue(value));
value *= 2;
}
if (value >= maximum) {
addItem(textFromValue(maximum), QVariant::fromValue(maximum));
}
}
qreal ZoomComboBox::valueFromText(const QString& text, bool *ok) const
{
Q_D(const ZoomComboBox);
QString copy = text;
copy.remove(locale().groupSeparator());
copy.remove(locale().percent());
return qreal(locale().toInt(copy, ok)) / 100.0;
}
QString ZoomComboBox::textFromValue(const qreal value) const
{
Q_D(const ZoomComboBox);
QString text = locale().toString(qRound(value * 100));
d->validator->fixup(text);
text.remove(locale().groupSeparator());
return text.append(locale().percent());
}
void ZoomComboBox::updateDisplayedText()
{
Q_D(ZoomComboBox);
if (d->mZoomToFitAction->isChecked()) {
lineEdit()->setText(d->mZoomToFitAction->iconText());
} else if (d->mZoomToFillAction->isChecked()) {
lineEdit()->setText(d->mZoomToFillAction->iconText());
} else if (d->mActualSizeAction->isChecked()) {
lineEdit()->setText(d->mActualSizeAction->iconText());
} else {
const QString currentZoomValueText(textFromValue(d->value));
lineEdit()->setText(currentZoomValueText);
d->lastSelectedIndex = -1;
d->lastCustomZoomValue = d->value;
}
}
bool ZoomComboBox::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::Hide) {
Q_D(ZoomComboBox);
changeZoomTo(d->lastSelectedIndex);
}
return false;
}
void ZoomComboBox::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton){
Q_D(ZoomComboBox);
if (d->mZoomToFitAction->isChecked()) {
setCurrentIndex(0);
d->lastSelectedIndex = 0;
} else if (d->mZoomToFillAction->isChecked()) {
setCurrentIndex(1);
d->lastSelectedIndex = 1;
} else if (d->mActualSizeAction->isChecked()) {
setCurrentIndex(2);
d->lastSelectedIndex = 2;
} else {
d->lastSelectedIndex = -1;
}
}
QComboBox::mousePressEvent(event);
}
void ZoomComboBox::changeZoomTo(int index) {
if (index < 0) {
Q_D(ZoomComboBox);
Q_EMIT zoomChanged(d->lastCustomZoomValue);
return;
}
QVariant itemData = this->itemData(index);
QAction *action = itemData.value<QAction*>();
if (action) {
if (action->isCheckable()) {
action->setChecked(true);
} else {
action->trigger();
}
} else if (itemData.canConvert(QMetaType::QReal)) {
Q_EMIT zoomChanged(itemData.toReal());
}
}
void ZoomComboBox::activateAndChangeZoomTo(int index)
{
Q_D(ZoomComboBox);
d->lastSelectedIndex = index;
changeZoomTo(index);
}
// SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
#ifndef GWENVIEW_ZOOMCOMBOBOX_H
#define GWENVIEW_ZOOMCOMBOBOX_H
#include <lib/gwenviewlib_export.h>
#include <QComboBox>
namespace Gwenview {
class ZoomComboBoxPrivate;
/**
* QComboBox subclass designed to be somewhat similar to QSpinBox.
* Allows the user to use non-integer combobox list items,
* but only accepts integers as custom input.
*
* This class is structured in a way so that changes to the zoom done through
* user interaction are signalled to the outside. On the other hand changes
* done to the visual state of this class without user interaction will not
* lead to emitted zoom changes/signals from this class.
*/
class GWENVIEWLIB_EXPORT ZoomComboBox : public QComboBox
{
Q_OBJECT
Q_PROPERTY(qreal value READ value WRITE setValue)
Q_PROPERTY(qreal minimum READ minimum WRITE setMinimum)
Q_PROPERTY(qreal maximum READ maximum WRITE setMaximum)
public:
explicit ZoomComboBox(QWidget *parent = nullptr);
~ZoomComboBox() override;
void setActions(QAction *zoomToFitAction, QAction *zoomToFillAction, QAction *actualSizeAction);
qreal value() const;
/**
* Called when the value that is being displayed should change.
* Calling this method doesn't affect the zoom of the currently viewed image.
*/
void setValue(const qreal value);
qreal minimum() const;
void setMinimum(const qreal minimum);
qreal maximum() const;
void setMaximum(const qreal maximum);
/// Gets an integer value from text.
qreal valueFromText(const QString &text, bool *ok = nullptr) const;
/// Gets appropriately decorated text from an integer value.
QString textFromValue(const qreal value) const;
void updateDisplayedText();
Q_SIGNALS:
void zoomChanged(qreal zoom);
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
private Q_SLOTS:
/**
* This method changes the zoom mode or value of the currently displayed image in response to
* user interaction with the Combobox' dropdown menu.
* @param index of the zoom mode or value that the user interacted with.
*/
void changeZoomTo(int index);
/**
* Sets the index as the fallback when the popup menu is closed without selection in the
* future and then calls changeZoomTo().
* @see changeZoomTo()
*/
void activateAndChangeZoomTo(int index);
private:
const std::unique_ptr<ZoomComboBoxPrivate> d_ptr;
Q_DECLARE_PRIVATE(ZoomComboBox)
Q_DISABLE_COPY(ZoomComboBox)
};
}
#endif // GWENVIEW_ZOOMCOMBOBOX_H
// SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
#ifndef GWENVIEW_ZOOMCOMBOBOX_P_H
#define GWENVIEW_ZOOMCOMBOBOX_P_H
#include "zoomcombobox.h"
namespace Gwenview {
class ZoomValidator : public QValidator
{
Q_OBJECT
Q_PROPERTY(qreal minimum READ minimum WRITE setMinimum NOTIFY changed)
Q_PROPERTY(qreal maximum READ maximum WRITE setMaximum NOTIFY changed)
public:
explicit ZoomValidator(qreal minimum, qreal maximum, ZoomComboBox *q, ZoomComboBoxPrivate *d, QWidget* parent = nullptr);
~ZoomValidator() override;
qreal minimum() const;
void setMinimum(const qreal minimum);
qreal maximum() const;
void setMaximum(const qreal maximum);
QValidator::State validate(QString &input, int &pos) const override;
private:
qreal m_minimum;
qreal m_maximum;
ZoomComboBox *m_zoomComboBox;
ZoomComboBoxPrivate *m_zoomComboBoxPrivate;
Q_DISABLE_COPY(ZoomValidator)
};
class ZoomComboBoxPrivate
{
Q_DECLARE_PUBLIC(ZoomComboBox)
public:
ZoomComboBoxPrivate(ZoomComboBox *q);
void setActions(QAction *zoomToFitAction, QAction *zoomToFillAction, QAction *actualSizeAction);
public:
ZoomComboBox *const q_ptr;
QAction *mZoomToFitAction = nullptr;
QAction *mZoomToFillAction = nullptr;
QAction *mActualSizeAction = nullptr;
qreal value = 1.0;
ZoomValidator *validator = nullptr;
int lastSelectedIndex = 0;
qreal lastCustomZoomValue = 1.0;
};
}
#endif // GWENVIEW_ZOOMCOMBOBOX_P_H
......@@ -26,16 +26,14 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA
// Qt
#include <QAction>
#include <QApplication>
#include <QSpinBox>
#include <QHBoxLayout>
#include <QSlider>
// KF
// Local
#include "zoomslider.h"
#include "signalblocker.h"
#include "statusbartoolbutton.h"
#include "zoomcombobox/zoomcombobox.h"
#include "zoomslider.h"
namespace Gwenview
{
......@@ -57,14 +55,9 @@ struct ZoomWidgetPrivate
{
ZoomWidget* q;
StatusBarToolButton* mZoomToFitButton;
StatusBarToolButton* mActualSizeButton;
StatusBarToolButton* mZoomToFillButton;
ZoomSlider* mZoomSlider;
QSpinBox* mZoomSpinBox;