Stepped keyboard zoom and a steady tab strip (12.9b)

This commit is contained in:
雷电芽衣 committed 2026-09-28 13:18:10 -04:00
1 parent fdb62978c5
commit 9ac3b1b81c
17 files changed
+533 -38

No files matched your search

+11
View File
@@ -315,6 +315,17 @@ void EditorSession::zoom(double value, std::optional<QPointF> anchor)
notify();
}
void EditorSession::zoomKeyboard(int step)
{
if (!m_document)
return;
const double target = viewport.keyboardZoomTarget(step);
if (target == viewport.zoom())
return;
viewport.setZoom(target, viewport.center(), m_document->size());
notify();
}
void EditorSession::setShowsPixelGrid(bool shows)
{
m_showsPixelGrid = shows;
+2
View File
@@ -253,6 +253,8 @@ public:
void createDocument(int width, int height, bool emptyLayer = false);
void fit();
void zoom(double value, std::optional<QPointF> anchor = std::nullopt);
// Swift's stepped keyboard zoom about the view's centre.
void zoomKeyboard(int step);
// The View menu's grid, shown from 800%.
bool showsPixelGrid() const { return m_showsPixelGrid; }
void setShowsPixelGrid(bool shows);
+13
View File
@@ -56,6 +56,19 @@ void CanvasViewport::setZoom(double value, QPointF anchoredAt, QSizeF documentSi
m_followsFit = false;
}
double CanvasViewport::keyboardZoomTarget(int step) const
{
if (step == 0)
return m_zoom;
const double tolerance = std::max(0.000000001, std::abs(m_zoom) * 0.000000001);
if (step > 0) {
const auto next = std::find_if(keyboardZoomLevels.begin(), keyboardZoomLevels.end(), [&](double level) { return level > m_zoom + tolerance; });
return next == keyboardZoomLevels.end() ? m_zoom : *next;
}
const auto next = std::find_if(keyboardZoomLevels.rbegin(), keyboardZoomLevels.rend(), [&](double level) { return level < m_zoom - tolerance; });
return next == keyboardZoomLevels.rend() ? m_zoom : *next;
}
void CanvasViewport::translate(QSizeF by)
{
pan += by;
+6
View File
@@ -2,6 +2,7 @@
#include <QPointF>
#include <QRectF>
#include <QSizeF>
#include <array>
#include <optional>
// Document: top-left pixels. View: widget points. Zoom 1: device pixels.
@@ -9,6 +10,9 @@ class CanvasViewport {
public:
static constexpr double minimumZoom = 0.001;
static constexpr double maximumZoom = 32;
// Swift's keyboard zoom stops, 12.5% to 1600%.
static constexpr std::array keyboardZoomLevels{0.125, 1.0 / 6.0, 0.25, 1.0 / 3.0, 0.5, 2.0 / 3.0, 1.0, 1.25, 1.5,
2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 12.0, 16.0};
QSizeF viewSize{0, 0};
double backingScale = 1;
@@ -25,6 +29,8 @@ public:
void fit(QSizeF documentSize);
void resize(QSizeF to, double backingScale, std::optional<QSizeF> documentSize);
void setZoom(double value, QPointF anchoredAt, QSizeF documentSize);
// The next stop past the zoom, else the zoom.
double keyboardZoomTarget(int step) const;
void translate(QSizeF by);
friend bool operator==(const CanvasViewport &lhs, const CanvasViewport &rhs);
+18
View File
@@ -324,6 +324,8 @@ bool CanvasView::eventFilter(QObject *watched, QEvent *event)
key = typed.get();
// Shift-+ and Shift-− step the blend mode, except while typing.
const bool typing = qobject_cast<QLineEdit *>(QApplication::focusWidget()) || (m_inlineTextEditor && hasFocus());
if (event->type() == QEvent::KeyPress && !typing && handleKeyboardZoom(*key))
return true;
if (event->type() != QEvent::KeyPress || typing || key->modifiers() & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier))
return false;
const bool forward = key->key() == Qt::Key_Plus || key->key() == Qt::Key_Equal;
@@ -335,6 +337,22 @@ bool CanvasView::eventFilter(QObject *watched, QEvent *event)
return QApplication::focusWidget() != this && !m_session.levels() && brushBracket(key->text());
}
// Zoom In's plus, which its Ctrl+= entry cannot hold.
bool CanvasView::handleKeyboardZoom(const QKeyEvent &key)
{
const Qt::KeyboardModifiers held = key.modifiers();
if (!m_session.document() || key.key() != Qt::Key_Plus || !held.testFlag(Qt::ControlModifier) || held & (Qt::AltModifier | Qt::MetaModifier))
return false;
// Only while the entry keeps its default key, as Swift's.
for (const ShortcutDefinition &definition : ShortcutDefinition::all()) {
if (definition.isMenu() && definition.title == QLatin1String("Zoom In") && !(ShortcutSettings::shared().chord(definition) == definition.original))
return false;
}
m_session.zoomKeyboard(1);
synchronizeDisplay();
return true;
}
// A key changed: badge, box and cursor follow at once.
void CanvasView::modifiersChanged()
{
+2
View File
@@ -176,6 +176,8 @@ private:
Qt::KeyboardModifiers heldModifiers() const;
void readModifiers(Qt::KeyboardModifiers modifiers);
void modifiersChanged();
// Swift's zoom keys on key-down; true when it zoomed.
bool handleKeyboardZoom(const QKeyEvent &key);
std::optional<PressTarget> transformPressLayer(QPointF pixel, Qt::KeyboardModifiers modifiers) const;
bool pressMovesLayer(QPointF point, Qt::KeyboardModifiers modifiers) const;
QCursor transformCursor(QPointF point, Qt::KeyboardModifiers modifiers) const;
+7 -2
View File
@@ -129,8 +129,13 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
QMenu *view = bar.addMenu(QStringLiteral("&View"));
add(view, QStringLiteral("fit"), QStringLiteral("Fit Canvas"), QKeySequence(Qt::CTRL | Qt::Key_0), [this] { session().fit(); });
add(view, QStringLiteral("actualPixels"), QStringLiteral("Actual Pixels"), QKeySequence(Qt::CTRL | Qt::Key_1), [this] { session().zoom(1); });
add(view, QStringLiteral("zoomIn"), QStringLiteral("Zoom In"), QKeySequence(Qt::CTRL | Qt::Key_Equal), [this] { session().zoom(session().viewport.zoom() * 1.25); });
add(view, QStringLiteral("zoomOut"), QStringLiteral("Zoom Out"), QKeySequence(Qt::CTRL | Qt::Key_Minus), [this] { session().zoom(session().viewport.zoom() / 1.25); });
// Swift's stops; open text keeps the keys, as NSText does.
const auto zoomBy = [this](int step) {
if (!m_field && !(m_canvas && session().textDraft()))
session().zoomKeyboard(step);
};
add(view, QStringLiteral("zoomIn"), QStringLiteral("Zoom In"), QKeySequence(Qt::CTRL | Qt::Key_Equal), [zoomBy] { zoomBy(1); });
add(view, QStringLiteral("zoomOut"), QStringLiteral("Zoom Out"), QKeySequence(Qt::CTRL | Qt::Key_Minus), [zoomBy] { zoomBy(-1); });
add(view, QStringLiteral("pixelGrid"), QStringLiteral("Pixel Grid (800% and above)"), QKeySequence(), [this] { session().setShowsPixelGrid(!session().showsPixelGrid()); })
->setCheckable(true);
add(view, QStringLiteral("snap"), QStringLiteral("Snap"), QKeySequence(), [this] { session().setSnappingEnabled(!session().snappingEnabled()); })
+49 -13
View File
@@ -4,9 +4,13 @@
#include <QDragEnterEvent>
#include <QDropEvent>
#include <QEvent>
#include <QFontMetricsF>
#include <QHBoxLayout>
#include <QPainter>
#include <QScrollBar>
#include <QStyleOptionToolButton>
#include <QStylePainter>
#include <QTimer>
namespace {
QFont tabFont(bool active)
@@ -16,17 +20,40 @@ QFont tabFont(bool active)
font.setWeight(active ? QFont::DemiBold : QFont::Medium);
return font;
}
// Swift's leading label: text from the left, 8 kept after.
class TabTitle : public QToolButton {
public:
using QToolButton::QToolButton;
protected:
void paintEvent(QPaintEvent *) override
{
QStylePainter painter(this);
QStyleOptionToolButton option;
initStyleOption(&option);
option.text.clear();
painter.drawComplexControl(QStyle::CC_ToolButton, option);
painter.setPen(palette().color(isEnabled() ? QPalette::Active : QPalette::Disabled, QPalette::ButtonText));
painter.drawText(rect().adjusted(0, 0, -8, 0), Qt::AlignLeft | Qt::AlignVCenter, text());
}
};
// Swift's projectTabLabelWidth: the title's own width, 35 to 155.
int labelWidth(const QString &title, bool active, bool modified)
{
const double titleWidth = QFontMetricsF(tabFont(active)).horizontalAdvance(title);
return int(std::min(155.0, std::max(35.0, std::ceil(titleWidth) + (modified ? 10 : 0))));
}
}
ProjectTabButton::ProjectTabButton(ProjectWorkspace &workspace, std::shared_ptr<ProjectTab> tab, QWidget *parent)
: QWidget(parent), tab(std::move(tab)), m_workspace(workspace), m_select(new QToolButton(this)), m_close(new QToolButton(this))
: QWidget(parent), tab(std::move(tab)), m_workspace(workspace), m_select(new TabTitle(this)), m_close(new QToolButton(this))
{
// 28 high: the strip's 34 less its margins.
m_select->setObjectName(QStringLiteral("selectTab"));
m_select->setAutoRaise(true);
m_select->setFocusPolicy(Qt::NoFocus);
m_select->setMinimumWidth(35);
m_select->setMaximumWidth(155);
m_close->setObjectName(QStringLiteral("closeTab"));
m_close->setAutoRaise(true);
m_close->setFocusPolicy(Qt::NoFocus);
@@ -48,9 +75,15 @@ ProjectTabButton::ProjectTabButton(ProjectWorkspace &workspace, std::shared_ptr<
void ProjectTabButton::synchronize()
{
const bool active = m_workspace.selectedID() == tab->id;
// Swift's dot for unsaved changes stands before the title.
m_select->setText((tab->session.isModified() ? QStringLiteral("● ") : QString()) + tab->title());
// The unsaved dot leads; fixed widths move later tabs only.
const bool modified = tab->session.isModified();
const int label = labelWidth(tab->title(), active, modified);
const QString dot = modified ? QStringLiteral("● ") : QString();
const QFontMetrics metrics(tabFont(active));
m_select->setText(dot + metrics.elidedText(tab->title(), Qt::ElideRight, label - (modified ? 10 : 0)));
m_select->setFont(tabFont(active));
// Swift's trailing 8 inside; a character dot outgrows Swift's 10.
m_select->setFixedWidth(label + 8 + (modified ? metrics.horizontalAdvance(dot) - 10 : 0));
m_select->setToolTip(tab->title());
m_select->setEnabled(m_workspace.canSwitch() || active);
m_close->setToolTip(QStringLiteral("Close %1").arg(tab->title()));
@@ -265,18 +298,21 @@ void ProjectTabStrip::synchronize()
bool ProjectTabStrip::eventFilter(QObject *watched, QEvent *event)
{
// A title changed size: the row takes its new width.
if (event->type() == QEvent::LayoutRequest) {
if (event->type() == QEvent::LayoutRequest)
m_row->resize(m_row->sizeHint());
showFront();
}
return QScrollArea::eventFilter(watched, event);
}
// The front tab scrolls into view.
// Only a new front tab scrolls into view.
void ProjectTabStrip::showFront()
{
for (ProjectTabButton *button : buttons()) {
if (button->tab->id == m_workspace.selectedID())
ensureWidgetVisible(button, 0, 0);
}
if (std::exchange(m_shownFront, m_workspace.selectedID()) == m_workspace.selectedID())
return;
// Posted, as Swift's: the row's layout request runs first.
QTimer::singleShot(0, this, [this] {
for (ProjectTabButton *button : buttons()) {
if (button->tab->id == m_workspace.selectedID())
ensureWidgetVisible(button, 0, 0);
}
});
}
+2
View File
@@ -70,4 +70,6 @@ private:
ProjectWorkspace &m_workspace;
QWidget *const m_row;
// Swift's lastSelectedID: only a new front tab scrolls.
std::optional<QUuid> m_shownFront;
};
+2 -8
View File
@@ -41,14 +41,8 @@ ProjectWorkspaceView::ProjectWorkspaceView(ProjectWorkspace &workspace, QWidget
connect(m_newCanvas, &QAction::triggered, this, [this] { m_workspace.current().controller.newCanvas(); });
connect(m_fit, &QAction::triggered, this, [this] { m_workspace.current().session.fit(); });
connect(m_actualPixels, &QAction::triggered, this, [this] { m_workspace.current().session.zoom(1); });
connect(m_zoomIn, &QAction::triggered, this, [this] {
EditorSession &session = m_workspace.current().session;
session.zoom(session.viewport.zoom() * 1.25);
});
connect(m_zoomOut, &QAction::triggered, this, [this] {
EditorSession &session = m_workspace.current().session;
session.zoom(session.viewport.zoom() / 1.25);
});
connect(m_zoomIn, &QAction::triggered, this, [this] { m_workspace.current().session.zoomKeyboard(1); });
connect(m_zoomOut, &QAction::triggered, this, [this] { m_workspace.current().session.zoomKeyboard(-1); });
connect(&m_workspace, &ProjectWorkspace::changed, this, &ProjectWorkspaceView::synchronize);
synchronize();
}