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
4 changes: 4 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+-` change the text size, `Ctrl+0` returns to the default.
`Ctrl+scroll` does the same.
- `Ctrl+?` shows the keyboard shortcut reference.

Unsaved drafts are recovered after an abnormal exit. Omawrite also watches open files
Expand All @@ -30,6 +32,8 @@ and warns before an external change can replace local work.
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.
`Ctrl+=`, `Ctrl+-`, and `Ctrl+scroll` set your own size on top of that, from 67% to
200% in browser-sized steps. It is remembered for the next window you open.

## Requirements

Expand Down
57 changes: 55 additions & 2 deletions src/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ 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 user's own
// text size set with Ctrl+= / Ctrl+- / Ctrl+wheel.
readonly property real textScale: backend.textScale
readonly property int editorFontPixelSize: scaledSize(20)
readonly property int editorWidth: Math.min(
Expand Down Expand Up @@ -178,6 +179,26 @@ ApplicationWindow {
onActivated: shortcutsDialog.open()
}

Shortcut {
// Ctrl++ is a different shortcut to Qt, not a shifted Ctrl+=, and it is
// the only one a layout that puts + on its own key can reach.
sequences: ["Ctrl+=", "Ctrl++"]
context: Qt.ApplicationShortcut
onActivated: backend.increaseTextSize()
}

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

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

Shortcut {
sequence: "Ctrl+O"
context: Qt.ApplicationShortcut
Expand Down Expand Up @@ -331,7 +352,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+= / Ctrl+- Text Size\nCtrl+0 Reset Text Size\nF11 / Super+F Fullscreen\nCtrl+? Shortcuts"
lineHeight: 1.5
}
}
Expand All @@ -341,6 +362,7 @@ ApplicationWindow {

Flickable {
id: editorFlick
objectName: "editorScroll"
anchors.fill: parent
anchors.leftMargin: 24
anchors.rightMargin: 24
Expand Down Expand Up @@ -463,6 +485,12 @@ ApplicationWindow {
// finger scrolling carries pixel-precise pixelDelta.
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: function(wheel) {
if (wheel.modifiers & Qt.ControlModifier) {
editorFlick.zoomByWheel(wheel);
wheel.accepted = true;
return;
}

scrollLinger.restart();
if (wheel.pixelDelta.y !== 0)
editorFlick.scrollTo(editorFlick.clampContentY(editorFlick.contentY - wheel.pixelDelta.y));
Expand All @@ -474,6 +502,31 @@ ApplicationWindow {

onMovementStarted: wheelScroll.stop()

// Ctrl+wheel changes the text size, as it does in a browser. A
// wheel notch is a whole step, but a touchpad trickles in pixels,
// so bank the fractions and step once a whole one is scrolled.
property real zoomNotches: 0

function zoomByWheel(wheel) {
// Finger scrolling synthesizes an angleDelta too, so pixelDelta
// is the one to test first, as in onWheel.
var notches = wheel.pixelDelta.y !== 0
? wheel.pixelDelta.y / 50
: wheel.angleDelta.y / 120;
if (zoomNotches * notches < 0)
zoomNotches = 0;

zoomNotches += notches;
while (zoomNotches >= 1) {
backend.increaseTextSize();
zoomNotches -= 1;
}
while (zoomNotches <= -1) {
backend.decreaseTextSize();
zoomNotches += 1;
}
}

function scrollByWheel(wheel) {
// High-resolution wheels report fractional notches; feed
// those through the same animated path, like Chromium does
Expand Down
59 changes: 56 additions & 3 deletions src/backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,19 @@
#include <QWindow>

#include <algorithm>
#include <iterator>

#include "markdownhighlighter.h"

constexpr qreal typoraLineHeightPercent = 140;
const QString lastSaveDirectorySetting = QStringLiteral("file/lastSaveDirectory");
const QString textZoomSetting = QStringLiteral("text/zoom");

// The user's own text size, multiplied onto the desktop's text scale.
// Chromium's zoom ladder, trimmed to the range the whole window still reads
// well at, so Ctrl+wheel here steps like Ctrl+wheel in a browser.
constexpr qreal textZoomSteps[] = {0.67, 0.75, 0.9, 1.0, 1.1, 1.25, 1.5, 1.75, 2.0};
constexpr int textZoomStepCount = int(std::size(textZoomSteps));

QString Backend::normalizedLinkUrl(const QString &clipboardText) {
QString candidate = clipboardText.trimmed();
Expand Down Expand Up @@ -72,6 +80,15 @@ QString Backend::normalizedLinkUrl(const QString &clipboardText) {
}

Backend::Backend(QObject *parent) : QObject(parent) {
// A settings file truncated by a crash reads back as an empty value, and
// both that and a NaN would clamp to the smallest size rather than fall
// back to the default.
bool zoomOk = false;
const qreal storedZoom = QSettings().value(textZoomSetting, 1.0).toDouble(&zoomOk);
m_textZoom = zoomOk && storedZoom > 0 ? qBound(textZoomSteps[0], storedZoom,
textZoomSteps[textZoomStepCount - 1])
: 1.0;

const QString stateDirectory = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir().mkpath(stateDirectory);
// Claim an orphaned snapshot before taking an empty slot. This ensures a
Expand Down Expand Up @@ -158,11 +175,47 @@ void Backend::setDarkMode(bool darkMode) {
emit darkModeChanged();
}

void Backend::setTextScale(qreal textScale) {
if (qFuzzyCompare(m_textScale, textScale))
void Backend::setDesktopTextScale(qreal textScale) {
if (qFuzzyCompare(m_desktopTextScale, textScale))
return;

m_desktopTextScale = textScale;
emit textScaleChanged();
}

void Backend::increaseTextSize() {
setTextZoom(steppedTextZoom(1));
}

void Backend::decreaseTextSize() {
setTextZoom(steppedTextZoom(-1));
}

void Backend::resetTextSize() {
setTextZoom(1.0);
}

// The neighbouring rung, starting from whichever rung the current zoom sits
// closest to; the ends of the ladder hold.
qreal Backend::steppedTextZoom(int direction) const {
int closest = 0;
for (int step = 1; step < textZoomStepCount; ++step) {
if (qAbs(textZoomSteps[step] - m_textZoom) < qAbs(textZoomSteps[closest] - m_textZoom))
closest = step;
}

return textZoomSteps[qBound(0, closest + direction, textZoomStepCount - 1)];
}

void Backend::setTextZoom(qreal zoom) {
// Report the size even when the ladder has run out, so a keypress that
// changes nothing still says why.
setStatus(QStringLiteral("Text size %1%").arg(qRound(zoom * 100)));
if (qFuzzyCompare(m_textZoom, zoom))
return;

m_textScale = textScale;
m_textZoom = zoom;
QSettings().setValue(textZoomSetting, zoom);
emit textScaleChanged();
}

Expand Down
15 changes: 11 additions & 4 deletions src/backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ 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(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 +43,9 @@ 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);
// What the desktop asks for, times what the user asked for on top of it.
qreal textScale() const { return m_desktopTextScale * m_textZoom; }
void setDesktopTextScale(qreal textScale);
QString themeBackground() const { return m_themeBackground; }
QString themeForeground() const { return m_themeForeground; }
QString themeAccent() const { return m_themeAccent; }
Expand All @@ -64,6 +65,9 @@ class Backend : public QObject {
Q_INVOKABLE void discardRecovery();
Q_INVOKABLE void reloadFromDisk();
Q_INVOKABLE void keepExternalVersion();
Q_INVOKABLE void increaseTextSize();
Q_INVOKABLE void decreaseTextSize();
Q_INVOKABLE void resetTextSize();
Q_INVOKABLE void printDocument();
Q_INVOKABLE void newWindow();
Q_INVOKABLE QString clipboardUrl() const;
Expand Down Expand Up @@ -91,6 +95,8 @@ class Backend : public QObject {

private:
void loadDocumentText(const QString &text);
qreal steppedTextZoom(int direction) const;
void setTextZoom(qreal zoom);
void setFileUrl(const QUrl &url);
void setModified(bool modified);
void setStatus(const QString &status);
Expand All @@ -116,7 +122,8 @@ class Backend : public QObject {
QString m_status;
int m_wordCount = 0;
bool m_darkMode = true;
qreal m_textScale = 1.0;
qreal m_desktopTextScale = 1.0;
qreal m_textZoom = 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.setTextScale(systemTheme.textScale());
// The desktop sets the baseline; the backend folds the user's own text
// size into it and reports the total.
backend.setDesktopTextScale(systemTheme.textScale());
applyInterfaceFont(backend.textScale());
QObject::connect(&systemTheme, &SystemTheme::textScaleChanged, &backend,
[&backend, applyInterfaceFont](qreal textScale) {
applyInterfaceFont(textScale);
backend.setTextScale(textScale);
&Backend::setDesktopTextScale);
QObject::connect(&backend, &Backend::textScaleChanged, &app,
[&backend, applyInterfaceFont]() {
applyInterfaceFont(backend.textScale());
});

QQmlApplicationEngine engine;
Expand Down
Loading