Stepped keyboard zoom and a steady tab strip (12.9b)
This commit is contained in:
1 parent
fdb62978c5
commit
9ac3b1b81c
17 files changed
+533
-38
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user