Middle-button pan, persistent tool settings, Auto Select's foreground rule, the new icon (12.6b)

This commit is contained in:
雷电芽衣 committed 2026-09-27 10:53:42 -04:00
1 parent 5a2fa91bc7
commit 29eab10edc
27 files changed
+390 -22

No files matched your search

+3
View File
@@ -1,4 +1,5 @@
#include "Document/ProjectWorkspace.h"
#include "Document/ToolDefaults.h"
#include "Logging.h"
#include "UI/OmarchyTheme.h"
#include "UI/ProjectWorkspaceView.h"
@@ -11,6 +12,8 @@ int main(int argc, char **argv)
qSetMessagePattern(QStringLiteral("%{time yyyy-MM-dd hh:mm:ss.zzz} %{type} %{category}: %{message}"));
QApplication application(argc, argv);
QApplication::setApplicationName(QStringLiteral("OmaPhoto"));
// Tool toggles outlive the run, as Swift's user defaults.
ToolDefaults::enable();
QApplication::setApplicationVersion(QStringLiteral(OMAPHOTO_VERSION));
// The desktop entry's name: icons and windows find each other.
QGuiApplication::setDesktopFileName(QStringLiteral("io.github.ZacharyZhang_NY.OmaPhoto"));
+2
View File
@@ -85,12 +85,14 @@ void EditorSession::beginTransform(bool persistent)
void EditorSession::setTransformAutoSelect(bool picks)
{
m_transformAutoSelect = picks;
ToolDefaults::set(picks, QStringLiteral("autoSelect"));
notify();
}
void EditorSession::setShowsTransformControls(bool shows)
{
m_showsTransformControls = shows;
ToolDefaults::set(shows, QStringLiteral("transformControls"));
notify();
}
+1
View File
@@ -318,6 +318,7 @@ void EditorSession::zoom(double value, std::optional<QPointF> anchor)
void EditorSession::setShowsPixelGrid(bool shows)
{
m_showsPixelGrid = shows;
ToolDefaults::set(shows, QStringLiteral("pixelGrid"));
notify();
}
+13 -12
View File
@@ -12,6 +12,7 @@
#include "Document/SelectionClipboard.h"
#include "Document/SelectionEdits.h"
#include "Document/SmudgeLiquify.h"
#include "Document/ToolDefaults.h"
#include "Document/DocumentHistory.h"
#include "Document/EditorSession+Jobs.h"
#include "Document/EditorSession+Model.h"
@@ -344,8 +345,8 @@ private:
std::optional<TransformEdit> m_transformEdit;
std::optional<TransformDuplicate> m_transformDuplicate;
bool m_transformAutoSelect = false;
bool m_showsTransformControls = true;
bool m_transformAutoSelect = ToolDefaults::boolean(QStringLiteral("autoSelect"), false);
bool m_showsTransformControls = ToolDefaults::boolean(QStringLiteral("transformControls"), true);
bool m_locksTransformRatio = true;
std::optional<BlendPreview> m_blendPreview;
std::optional<QUuid> m_opacityEditLayerID;
@@ -358,18 +359,18 @@ private:
std::optional<QString> m_importError;
std::optional<QUuid> m_renamingLayerID;
std::optional<QString> m_projectPath;
bool m_showsPixelGrid = true;
bool m_showsPixelGrid = ToolDefaults::boolean(QStringLiteral("pixelGrid"), true);
bool m_snappingEnabled = true;
// The layout grid starts off; guides show; rulers hide.
bool m_showsGrid = false;
bool m_showsGuides = true;
bool m_showsRulers = false;
bool m_snapEnabled = true;
bool m_snapToGuides = true;
bool m_snapToGrid = false;
bool m_snapToLayers = true;
bool m_snapToDocumentBounds = true;
bool m_locksGuides = false;
bool m_showsGrid = ToolDefaults::boolean(QStringLiteral("grid"), false);
bool m_showsGuides = ToolDefaults::boolean(QStringLiteral("guides"), true);
bool m_showsRulers = ToolDefaults::boolean(QStringLiteral("rulers"), false);
bool m_snapEnabled = ToolDefaults::boolean(QStringLiteral("snap"), true);
bool m_snapToGuides = ToolDefaults::boolean(QStringLiteral("snapGuides"), true);
bool m_snapToGrid = ToolDefaults::boolean(QStringLiteral("snapGrid"), false);
bool m_snapToLayers = ToolDefaults::boolean(QStringLiteral("snapLayers"), true);
bool m_snapToDocumentBounds = ToolDefaults::boolean(QStringLiteral("snapBounds"), true);
bool m_locksGuides = ToolDefaults::boolean(QStringLiteral("lockGuides"), false);
std::optional<GuideDrag> m_guideDrag;
bool m_showsSampleRing = true;
std::optional<QRectF> m_cropRect;
+11
View File
@@ -57,54 +57,63 @@ bool LayoutGrid::isMajor(double value)
void EditorSession::setShowsGrid(bool shows)
{
m_showsGrid = shows;
ToolDefaults::set(shows, QStringLiteral("grid"));
notify();
}
void EditorSession::setShowsGuides(bool shows)
{
m_showsGuides = shows;
ToolDefaults::set(shows, QStringLiteral("guides"));
notify();
}
void EditorSession::setShowsRulers(bool shows)
{
m_showsRulers = shows;
ToolDefaults::set(shows, QStringLiteral("rulers"));
notify();
}
void EditorSession::setSnapEnabled(bool enabled)
{
m_snapEnabled = enabled;
ToolDefaults::set(enabled, QStringLiteral("snap"));
notify();
}
void EditorSession::setSnapToGuides(bool snaps)
{
m_snapToGuides = snaps;
ToolDefaults::set(snaps, QStringLiteral("snapGuides"));
notify();
}
void EditorSession::setSnapToGrid(bool snaps)
{
m_snapToGrid = snaps;
ToolDefaults::set(snaps, QStringLiteral("snapGrid"));
notify();
}
void EditorSession::setSnapToLayers(bool snaps)
{
m_snapToLayers = snaps;
ToolDefaults::set(snaps, QStringLiteral("snapLayers"));
notify();
}
void EditorSession::setSnapToDocumentBounds(bool snaps)
{
m_snapToDocumentBounds = snaps;
ToolDefaults::set(snaps, QStringLiteral("snapBounds"));
notify();
}
void EditorSession::setLocksGuides(bool locks)
{
m_locksGuides = locks;
ToolDefaults::set(locks, QStringLiteral("lockGuides"));
notify();
}
@@ -155,6 +164,7 @@ void EditorSession::beginGuideCreation(CanvasGuide::Axis axis, double position)
if (!canEditGuides())
return;
m_showsGuides = true;
ToolDefaults::set(true, QStringLiteral("guides"));
m_guideDrag = GuideDrag{QUuid::createUuid(), axis, snappedGuidePosition(position, axis, std::nullopt), true, std::nullopt};
notify();
}
@@ -225,6 +235,7 @@ void EditorSession::addGuide(const CanvasGuide &guide)
if (!canEditGuides())
return;
m_showsGuides = true;
ToolDefaults::set(true, QStringLiteral("guides"));
beginEdit(QStringLiteral("New Guide"));
m_document->guides.push_back(guide);
endEdit();
+33
View File
@@ -0,0 +1,33 @@
#include "Document/ToolDefaults.h"
#include "Logging.h"
#include <QSettings>
namespace {
const QString prefix = QStringLiteral("tool.");
// Swift's XCTest check: only the app keeps the toggles.
bool enabled = false;
}
void ToolDefaults::enable()
{
enabled = true;
}
bool ToolDefaults::boolean(const QString &key, bool fallback)
{
if (!enabled)
return fallback;
// The settings file keeps text; this run's writes are booleans.
const QString stored = QSettings().value(prefix + key).toString();
if (stored == QLatin1String("true") || stored == QLatin1String("false"))
return stored == QLatin1String("true");
if (!stored.isEmpty())
qCWarning(lcApp).noquote() << "ignoring the tool setting" << key << "of" << stored;
return fallback;
}
void ToolDefaults::set(bool value, const QString &key)
{
if (enabled)
QSettings().setValue(prefix + key, value);
}
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <QString>
// Swift's ToolDefaults: toggles kept for the person, across launches.
namespace ToolDefaults {
// The app turns them on; tests keep the compiled defaults.
void enable();
// The stored toggle, else the fallback.
bool boolean(const QString &key, bool fallback);
void set(bool value, const QString &key);
}
+1 -1
View File
@@ -114,7 +114,7 @@ void CanvasView::endBrushTipDrag(QPointF point)
// The circle at the pointer, a stroke's own size.
void CanvasView::updateBrushCursor()
{
const bool shows = isBrushTool(m_session.tool()) && !m_spaceHeld && !picking();
const bool shows = isBrushTool(m_session.tool()) && !m_spaceHeld && !picking() && !m_middlePanPoint;
const double diameter = m_session.brushStroke() ? m_session.brushStroke()->settings.diameter : m_session.brushSettings().diameter;
// Clone Stamp marks its source; between strokes, previews a click.
std::optional<QPointF> sample;
+4 -2
View File
@@ -250,8 +250,10 @@ QCursor CanvasView::pixelDragCursor(bool duplicate, double ratio)
void CanvasView::updateCursor()
{
const NavigationTool tool = m_session.tool();
// A transform, outline or pixel drag keeps its cursor.
if (m_dragCursor)
// A middle-button pan outranks every drag; another keeps its cursor.
if (m_middlePanPoint)
setCursor(Qt::ClosedHandCursor);
else if (m_dragCursor)
setCursor(*m_dragCursor);
else if (m_selectionDragStart)
setCursor(moveSelectionCursor(devicePixelRatio()));
+29
View File
@@ -114,6 +114,13 @@ void CanvasView::wheelEvent(QWheelEvent *event)
void CanvasView::mousePressEvent(QMouseEvent *event)
{
// The middle button pans in any tool, beside the left.
if (event->button() == Qt::MiddleButton && m_session.document()) {
m_middlePanPoint = event->position();
updateBrushCursor();
updateCursor();
return;
}
if (event->button() == Qt::RightButton && isBrushTool(m_session.tool()) && !m_session.brushStroke() && !m_session.warpStroke() && !m_spaceHeld) {
beginBrushTipDrag(event->position(), event->modifiers());
return;
@@ -199,6 +206,16 @@ void CanvasView::mouseMoveEvent(QMouseEvent *event)
const QPointF point = event->position();
m_hover = point;
readModifiers(event->modifiers());
if (m_middlePanPoint && event->buttons().testFlag(Qt::MiddleButton)) {
m_session.viewport.translate(QSizeF(point.x() - m_middlePanPoint->x(), point.y() - m_middlePanPoint->y()));
m_middlePanPoint = point;
m_session.notify();
if (!event->buttons().testFlag(Qt::LeftButton))
return;
} else if (m_middlePanPoint) {
// Qt can lose a release: the pan ends here.
endMiddlePan();
}
// Without the left button a text gesture's release was lost.
const bool held = event->buttons().testFlag(Qt::LeftButton);
if (m_textBoxAnchor) {
@@ -258,6 +275,10 @@ void CanvasView::mouseMoveEvent(QMouseEvent *event)
void CanvasView::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::MiddleButton && m_middlePanPoint) {
endMiddlePan();
return;
}
if (event->button() == Qt::RightButton && m_brushTipDrag) {
endBrushTipDrag(event->position());
return;
@@ -344,3 +365,11 @@ void CanvasView::leaveEvent(QEvent *event)
updateBrushCursor();
QWidget::leaveEvent(event);
}
// The tool's own cursor and circle come back at once.
void CanvasView::endMiddlePan()
{
m_middlePanPoint.reset();
updateCursor();
updateBrushCursor();
}
+18 -1
View File
@@ -6,6 +6,19 @@
#include <numbers>
namespace {
// Whether `upper` is painted above `lower`: later is higher.
bool above(const std::vector<ImageLayer> &layers, QUuid upper, QUuid lower)
{
const auto index = [&](QUuid id) -> std::optional<size_t> {
for (size_t at = 0; at < layers.size(); ++at)
if (layers[at].id == id)
return at;
return std::nullopt;
};
// Both are drawn: the pointer's layer, the visible active one.
return index(upper).value() > index(lower).value();
}
// Four arrows about `center`, each `reach` long, traced clockwise.
QPainterPath fourArrowPath(QPointF center, double reach, double shaft, double head, double headLength)
{
@@ -149,8 +162,12 @@ std::optional<CanvasView::PressTarget> CanvasView::transformPressLayer(QPointF p
if ((box && box->contains(pixel)) || !(picks && m_session.transformAutoSelect()) || !underPointer)
return PressTarget{*m_session.activeLayerID(), false};
}
if (active && m_session.editedTransform(*active).contains(pixel))
if (active && m_session.editedTransform(*active).contains(pixel)) {
// Auto Select prefers a layer above, as over a background.
if (picks && m_session.transformAutoSelect() && underPointer && above(rendered, *underPointer, active->id))
return PressTarget{*underPointer, true};
return PressTarget{active->id, false};
}
if (picks && (m_session.transformAutoSelect() || control) && underPointer)
return PressTarget{*underPointer, true};
if (active)
+3
View File
@@ -208,6 +208,7 @@ private:
void dragBrushTip(QPointF point, Qt::KeyboardModifiers modifiers);
void endBrushTipDrag(QPointF point);
void updateBrushCursor();
void endMiddlePan();
// Clone Stamp's preview: one click's coverage, and the source there.
QImage cloneTip(double diameter, double hardness);
QImage clonePreview(QPointF center, double diameter, const CanvasDocument &document);
@@ -268,6 +269,8 @@ private:
std::optional<int> m_panKey;
bool m_optionHeld = false;
std::optional<QPointF> m_lastDragPoint;
// Where a middle-button pan last was, its own drag point.
std::optional<QPointF> m_middlePanPoint;
std::optional<ZoomDrag> m_zoomDrag;
bool m_shiftHeld = false;
// Document point where a drag of the outline began.