Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Install via the Omarchy Package Repository via the `omawrite` package. It's inst
- `Ctrl+F` searches the document. Use `Enter` or `Ctrl+G` for the next match and `Shift+Enter` for the previous match.
- `Ctrl+H` opens find and replace.
- `Ctrl+B`, `Ctrl+I`, and `Ctrl+K` insert bold, italic, and link Markdown.
- `Ctrl+=` and `Ctrl+-` make the text bigger and smaller, and `Ctrl+0` returns
it to the default. `Ctrl` and the mouse wheel does the same.
- `Ctrl+?` shows the keyboard shortcut reference.

Unsaved drafts are recovered after an abnormal exit. Omawrite also watches open files
Expand All @@ -31,6 +33,9 @@ Text follows the desktop text size — `omarchy display text size`, or GNOME's
`text-scaling-factor` — and re-flows without a restart. The default of 12px leaves
Omawrite at the size it is designed around; larger and smaller sizes scale from there.

The zoom shortcuts scale on top of that, along the same steps a browser uses, from
half size to triple. Omawrite remembers the zoom between sessions.

## Requirements

- Qt 6: `qt6-base`, `qt6-declarative`, `qt6-quickcontrols2`
Expand Down
12 changes: 9 additions & 3 deletions src/FooterIconButton.qml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Item {
width: 16
height: 16

// The icon paths are drawn on a 16px grid, so a larger button paints the
// same shape at a larger stroke.
readonly property real drawScale: width / 16

ToolTip.visible: hitArea.containsMouse && tooltip.length > 0
ToolTip.text: tooltip

Expand All @@ -29,10 +33,12 @@ Item {
transformOrigin: Item.TopLeft
scale: 1 / dpr
onDprChanged: requestPaint()
onWidthChanged: requestPaint()

onPaint: {
var context = getContext("2d");
context.setTransform(dpr, 0, 0, dpr, 0, 0);
var unit = dpr * control.drawScale;
context.setTransform(unit, 0, 0, unit, 0, 0);
context.clearRect(0, 0, width, height);
context.strokeStyle = control.iconColor;
context.lineWidth = 1.4;
Expand Down Expand Up @@ -76,8 +82,8 @@ Item {
MouseArea {
id: hitArea
anchors.centerIn: parent
width: 28
height: 28
width: control.width + 12
height: control.height + 12
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: control.clicked()
Expand Down
61 changes: 58 additions & 3 deletions src/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ ApplicationWindow {
readonly property color selectionFill: backend.themeSelection
// The desktop's text size knob (GNOME's text-scaling-factor, which
// `omarchy display text size` drives) anchored so its 12px default leaves
// the app at the sizes it was designed around.
// the app at the sizes it was designed around, times the in-app zoom.
readonly property real textScale: backend.textScale
readonly property int editorFontPixelSize: scaledSize(20)
readonly property int editorWidth: Math.min(
Expand Down Expand Up @@ -85,6 +85,29 @@ ApplicationWindow {
return Math.max(1, Math.round(pixels * win.textScale));
}

// One zoom step per wheel notch. Finger scrolling arrives as a stream of
// small pixel deltas instead, so those add up to a notch before they move
// the zoom, which keeps a pinch from running to the end of the ladder.
property real zoomWheelDelta: 0

function zoomByWheel(wheel) {
var delta = wheel.angleDelta.y !== 0 ? wheel.angleDelta.y : wheel.pixelDelta.y * 4;
if (delta === 0)
return;
if (delta > 0 !== zoomWheelDelta > 0)
zoomWheelDelta = 0;

zoomWheelDelta += delta;
while (zoomWheelDelta >= 120) {
zoomWheelDelta -= 120;
backend.zoomIn();
}
while (zoomWheelDelta <= -120) {
zoomWheelDelta += 120;
backend.zoomOut();
}
}

function toggleFullScreen() {
win.visibility = win.visibility === Window.FullScreen
? Window.Windowed
Expand Down Expand Up @@ -196,6 +219,27 @@ ApplicationWindow {
onActivated: backend.saveAsDialog()
}

// Ctrl and the plus key, which the layout may only reach through Shift,
// and Ctrl and minus. Listing a sequence twice makes Qt call the match
// ambiguous and fire nothing, so each spelling appears once.
Shortcut {
sequences: ["Ctrl+=", "Ctrl++"]
context: Qt.ApplicationShortcut
onActivated: backend.zoomIn()
}

Shortcut {
sequence: "Ctrl+-"
context: Qt.ApplicationShortcut
onActivated: backend.zoomOut()
}

Shortcut {
sequence: "Ctrl+0"
context: Qt.ApplicationShortcut
onActivated: backend.resetZoom()
}

Shortcut {
sequence: "Ctrl+P"
context: Qt.ApplicationShortcut
Expand Down Expand Up @@ -331,7 +375,7 @@ ApplicationWindow {
standardButtons: Dialog.Close
anchors.centerIn: parent
contentItem: Label {
text: "Ctrl+S Save\nCtrl+Shift+S Save As\nCtrl+O Open\nCtrl+N New Window\nCtrl+F Find\nCtrl+H Find and Replace\nCtrl+B Bold\nCtrl+I Italic\nCtrl+K Link\nCtrl+P Print\nF11 / Super+F Fullscreen\nCtrl+? Shortcuts"
text: "Ctrl+S Save\nCtrl+Shift+S Save As\nCtrl+O Open\nCtrl+N New Window\nCtrl+F Find\nCtrl+H Find and Replace\nCtrl+B Bold\nCtrl+I Italic\nCtrl+K Link\nCtrl+P Print\nCtrl+= Bigger Text\nCtrl+- Smaller Text\nCtrl+0 Reset Text Size\nF11 / Super+F Fullscreen\nCtrl+? Shortcuts"
lineHeight: 1.5
}
}
Expand Down Expand Up @@ -463,6 +507,13 @@ ApplicationWindow {
// finger scrolling carries pixel-precise pixelDelta.
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: function(wheel) {
// Ctrl and the wheel zooms, as it does in a browser.
if (wheel.modifiers & Qt.ControlModifier) {
win.zoomByWheel(wheel);
wheel.accepted = true;
return;
}

scrollLinger.restart();
if (wheel.pixelDelta.y !== 0)
editorFlick.scrollTo(editorFlick.clampContentY(editorFlick.contentY - wheel.pixelDelta.y));
Expand Down Expand Up @@ -802,11 +853,13 @@ ApplicationWindow {
anchors.bottom: parent.bottom
anchors.leftMargin: 12
anchors.bottomMargin: 10
spacing: 12
spacing: win.scaledSize(12)
opacity: 0.55

FooterIconButton {
objectName: "saveButton"
width: win.scaledSize(16)
height: win.scaledSize(16)
iconName: "save"
iconColor: win.mutedColor
tooltip: "Save"
Expand All @@ -815,6 +868,8 @@ ApplicationWindow {

FooterIconButton {
objectName: "openButton"
width: win.scaledSize(16)
height: win.scaledSize(16)
iconName: "open"
iconColor: win.mutedColor
tooltip: "Open"
Expand Down
56 changes: 53 additions & 3 deletions src/backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@

constexpr qreal typoraLineHeightPercent = 140;
const QString lastSaveDirectorySetting = QStringLiteral("file/lastSaveDirectory");
const QString zoomSetting = QStringLiteral("view/zoom");
// The zoom steps a browser uses, so Ctrl+= and Ctrl+- land where the muscle
// memory from Chromium and Firefox expects.
const qreal zoomSteps[] = {0.5, 0.67, 0.75, 0.9, 1.0, 1.1, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0};
constexpr int zoomStepCount = int(sizeof(zoomSteps) / sizeof(zoomSteps[0]));

QString Backend::normalizedLinkUrl(const QString &clipboardText) {
QString candidate = clipboardText.trimmed();
Expand Down Expand Up @@ -91,6 +96,11 @@ Backend::Backend(QObject *parent) : QObject(parent) {
}
}
}
// A hand-edited settings file could hold anything; keep the restored
// zoom inside the range the shortcuts can reach.
const qreal savedZoom = QSettings().value(zoomSetting, 1.0).toReal();
m_zoom = qBound(zoomSteps[0], savedZoom, zoomSteps[zoomStepCount - 1]);

m_wordCountTimer.setSingleShot(true);
m_wordCountTimer.setInterval(120);
connect(&m_wordCountTimer, &QTimer::timeout, this, &Backend::refreshWordCount);
Expand Down Expand Up @@ -158,11 +168,51 @@ void Backend::setDarkMode(bool darkMode) {
emit darkModeChanged();
}

void Backend::setTextScale(qreal textScale) {
if (qFuzzyCompare(m_textScale, textScale))
void Backend::setSystemTextScale(qreal textScale) {
if (qFuzzyCompare(m_systemTextScale, textScale))
return;

m_systemTextScale = textScale;
emit textScaleChanged();
}

// The next step up (direction 1) or down (-1) from the given zoom, snapping a
// value that sits between two steps onto the ladder.
qreal Backend::steppedZoom(qreal zoom, int direction) {
if (direction > 0) {
for (int step = 0; step < zoomStepCount; ++step) {
if (zoomSteps[step] > zoom + 0.001)
return zoomSteps[step];
}
return zoomSteps[zoomStepCount - 1];
}

for (int step = zoomStepCount - 1; step >= 0; --step) {
if (zoomSteps[step] < zoom - 0.001)
return zoomSteps[step];
}
return zoomSteps[0];
}

void Backend::zoomIn() {
setZoom(steppedZoom(m_zoom, 1));
}

void Backend::zoomOut() {
setZoom(steppedZoom(m_zoom, -1));
}

void Backend::resetZoom() {
setZoom(1.0);
}

void Backend::setZoom(qreal zoom) {
if (qFuzzyCompare(m_zoom, zoom))
return;

m_textScale = textScale;
m_zoom = zoom;
QSettings().setValue(zoomSetting, m_zoom);
setStatus(QStringLiteral("Text size %1%").arg(qRound(m_zoom * 100)));
emit textScaleChanged();
}

Expand Down
18 changes: 14 additions & 4 deletions src/backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ class Backend : public QObject {
Q_PROPERTY(QString status READ status NOTIFY statusChanged)
Q_PROPERTY(int wordCount READ wordCount NOTIFY wordCountChanged)
Q_PROPERTY(bool darkMode READ darkMode WRITE setDarkMode NOTIFY darkModeChanged)
Q_PROPERTY(qreal textScale READ textScale WRITE setTextScale NOTIFY textScaleChanged)
Q_PROPERTY(qreal textScale READ textScale NOTIFY textScaleChanged)
Q_PROPERTY(qreal zoom READ zoom NOTIFY textScaleChanged)
Q_PROPERTY(QString themeBackground READ themeBackground NOTIFY themeColorsChanged)
Q_PROPERTY(QString themeForeground READ themeForeground NOTIFY themeColorsChanged)
Q_PROPERTY(QString themeAccent READ themeAccent NOTIFY themeColorsChanged)
Expand All @@ -43,8 +44,12 @@ class Backend : public QObject {
int wordCount() const { return m_wordCount; }
bool darkMode() const { return m_darkMode; }
void setDarkMode(bool darkMode);
qreal textScale() const { return m_textScale; }
void setTextScale(qreal textScale);
// The desktop text size and the in-app zoom, combined into the one
// factor the interface scales by.
qreal textScale() const { return m_systemTextScale * m_zoom; }
qreal zoom() const { return m_zoom; }
void setSystemTextScale(qreal textScale);
static qreal steppedZoom(qreal zoom, int direction);
QString themeBackground() const { return m_themeBackground; }
QString themeForeground() const { return m_themeForeground; }
QString themeAccent() const { return m_themeAccent; }
Expand All @@ -64,6 +69,9 @@ class Backend : public QObject {
Q_INVOKABLE void discardRecovery();
Q_INVOKABLE void reloadFromDisk();
Q_INVOKABLE void keepExternalVersion();
Q_INVOKABLE void zoomIn();
Q_INVOKABLE void zoomOut();
Q_INVOKABLE void resetZoom();
Q_INVOKABLE void printDocument();
Q_INVOKABLE void newWindow();
Q_INVOKABLE QString clipboardUrl() const;
Expand All @@ -90,6 +98,7 @@ class Backend : public QObject {
void externalChangeDetected(bool deleted, bool locallyModified);

private:
void setZoom(qreal zoom);
void loadDocumentText(const QString &text);
void setFileUrl(const QUrl &url);
void setModified(bool modified);
Expand All @@ -116,7 +125,8 @@ class Backend : public QObject {
QString m_status;
int m_wordCount = 0;
bool m_darkMode = true;
qreal m_textScale = 1.0;
qreal m_systemTextScale = 1.0;
qreal m_zoom = 1.0;
bool m_loading = false;
bool m_closeAfterSave = false;
bool m_formattingTypography = false;
Expand Down
17 changes: 10 additions & 7 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ int main(int argc, char *argv[]) {
QObject::connect(&systemTheme, &SystemTheme::darkModeChanged, &backend,
&Backend::setDarkMode);

// Carry the desktop's text scale into the default font, so the chrome that
// inherits it (dialog titles, buttons) grows along with the writing area.
// Carry the text scale into the default font, so the chrome that inherits
// it (dialog titles, buttons) grows along with the writing area.
const QFont interfaceFont(QStringLiteral("iA Writer Mono S"));
const qreal basePointSize = interfaceFont.pointSizeF() > 0
? interfaceFont.pointSizeF()
Expand All @@ -45,13 +45,16 @@ int main(int argc, char *argv[]) {
scaled.setPointSizeF(basePointSize * textScale);
app.setFont(scaled);
};
applyInterfaceFont(systemTheme.textScale());
backend.setSystemTextScale(systemTheme.textScale());
applyInterfaceFont(backend.textScale());

backend.setTextScale(systemTheme.textScale());
QObject::connect(&systemTheme, &SystemTheme::textScaleChanged, &backend,
[&backend, applyInterfaceFont](qreal textScale) {
applyInterfaceFont(textScale);
backend.setTextScale(textScale);
&Backend::setSystemTextScale);
// Both the desktop text size and the in-app zoom arrive through this one
// signal, so the chrome follows either of them.
QObject::connect(&backend, &Backend::textScaleChanged, &app,
[&backend, applyInterfaceFont]() {
applyInterfaceFont(backend.textScale());
});

QQmlApplicationEngine engine;
Expand Down
Loading