From 6494f61793f36247930d1a596805881387009905 Mon Sep 17 00:00:00 2001 From: Mysterie Cat Dev Date: Sat, 9 May 2026 09:39:29 -0700 Subject: [PATCH 1/9] feat: add flushed indentation to wrapped bullet list text --- src/Widgets/TextView.vala | 44 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/Widgets/TextView.vala b/src/Widgets/TextView.vala index 06a9bcf5..59bfee9b 100644 --- a/src/Widgets/TextView.vala +++ b/src/Widgets/TextView.vala @@ -80,7 +80,21 @@ public class Jorts.TextView : Granite.HyperTextView { GLib.SettingsBindFlags.DEFAULT); } + private void ensure_tags () { + if (buffer.tag_table.lookup ("list_item") == null) { + var layout = this.create_pango_layout (list_item_start); + int width, height; + layout.get_pixel_size (out width, out height); + + buffer.create_tag ("list_item", + "indent", -width, + "left-margin", SPACING_DOUBLE + width + ); + } + } + public void toggle_list () { + ensure_tags (); Gtk.TextIter start, end; buffer.get_selection_bounds (out start, out end); @@ -147,6 +161,13 @@ public class Jorts.TextView : Granite.HyperTextView { buffer.get_iter_at_line_offset (out line_start, line_number, 0); buffer.insert (ref line_start, list_item_start, -1); } + + // Apply hanging indent tag to the line + Gtk.TextIter ls, le; + buffer.get_iter_at_line_offset (out ls, line_number, 0); + le = ls.copy (); + le.forward_to_line_end (); + buffer.apply_tag_by_name ("list_item", ls, le); } } @@ -163,13 +184,19 @@ public class Jorts.TextView : Granite.HyperTextView { * Remove list prefix from line x to line y. Presuppose it is there */ private void remove_prefix (int line_number) { - Gtk.TextIter line_start, prefix_end; + Gtk.TextIter line_start, prefix_end, line_end; var remove_range = list_item_start.char_count (); debug ("doing line " + line_number.to_string ()); buffer.get_iter_at_line_offset (out line_start, line_number, 0); buffer.get_iter_at_line_offset (out prefix_end, line_number, remove_range); buffer.delete (ref line_start, ref prefix_end); + + // Remove hanging indent tag from the line + buffer.get_iter_at_line_offset (out line_start, line_number, 0); + line_end = line_start.copy (); + line_end.forward_to_line_end (); + buffer.remove_tag_by_name ("list_item", line_start, line_end); } /** @@ -177,6 +204,7 @@ public class Jorts.TextView : Granite.HyperTextView { * Some local stuff is deduplicated in the Ifs, because i do not like the idea of getting computation done not needed 98% of the time */ private bool on_key_pressed (uint keyval, uint keycode, Gdk.ModifierType state) { + ensure_tags (); // If backspace on a prefix: Delete the prefix. if (keyval == Gdk.Key.BackSpace) { @@ -197,6 +225,13 @@ public class Jorts.TextView : Granite.HyperTextView { buffer.begin_user_action (); buffer.delete (ref start, ref end); buffer.insert_at_cursor ("\n", -1); + + // The line is now an empty normal line, so remove the hanging indent + buffer.get_iter_at_line_offset (out start, line_number, 0); + end = start.copy (); + end.forward_to_line_end (); + buffer.remove_tag_by_name ("list_item", start, end); + buffer.end_user_action (); } } @@ -212,6 +247,13 @@ public class Jorts.TextView : Granite.HyperTextView { buffer.begin_user_action (); buffer.insert_at_cursor ("\n" + list_item_start, -1); + + // Ensure new line has tag applied since it was just inserted + buffer.get_iter_at_line_offset (out start, line_number + 1, 0); + end = start.copy (); + end.forward_to_line_end (); + buffer.apply_tag_by_name ("list_item", start, end); + buffer.end_user_action (); return true; From 9cd6c18367c24a7b7a31bf31738fa4ed53a4d417 Mon Sep 17 00:00:00 2001 From: Mysterie Cat Dev Date: Sat, 9 May 2026 09:40:08 -0700 Subject: [PATCH 2/9] chore: remove debug print statement --- src/Widgets/TextView.vala | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Widgets/TextView.vala b/src/Widgets/TextView.vala index 59bfee9b..4be94e08 100644 --- a/src/Widgets/TextView.vala +++ b/src/Widgets/TextView.vala @@ -208,7 +208,6 @@ public class Jorts.TextView : Granite.HyperTextView { // If backspace on a prefix: Delete the prefix. if (keyval == Gdk.Key.BackSpace) { - print ("backspace"); Gtk.TextIter start, end; buffer.get_selection_bounds (out start, out end); From e99072dd5879f0851504f7493b12d17d4a30c2b1 Mon Sep 17 00:00:00 2001 From: Mysterie Cat Dev Date: Sat, 9 May 2026 09:40:47 -0700 Subject: [PATCH 3/9] docs: add Fedora dev setup instructions --- docs/development/building.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/development/building.md b/docs/development/building.md index 8caee1e4..7f0f09d8 100644 --- a/docs/development/building.md +++ b/docs/development/building.md @@ -35,7 +35,11 @@ Ubuntu sudo apt install libgranite-7-common libjson-glib-1.0-0 libgee-0.8-2 meson libvala-0.56-0 libportal-gtk4-dev ``` +Fedora +```bash +sudo dnf install granite-7-devel json-glib-devel libgee-devel meson libvala libportal-devel +``` ## Setup Meson From 520e56b0af40230da5086928aee1e504a3210ff9 Mon Sep 17 00:00:00 2001 From: Mysterie Cat Dev Date: Sun, 10 May 2026 04:42:35 -0700 Subject: [PATCH 4/9] fix: recalculate indentation on zoom level change --- src/Services/ZoomController.vala | 1 + src/Widgets/TextView.vala | 22 ++++++++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Services/ZoomController.vala b/src/Services/ZoomController.vala index bb5f5903..b744dbec 100644 --- a/src/Services/ZoomController.vala +++ b/src/Services/ZoomController.vala @@ -105,6 +105,7 @@ public class Jorts.ZoomController : Object { window.remove_css_class (Jorts.Zoom.from_int ( _old_zoom).to_css_class ()); _old_zoom = new_zoom; window.add_css_class (Jorts.Zoom.from_int ( new_zoom).to_css_class ()); + window.textview.refresh_list_item_indentation (); // Adapt headerbar size to avoid weird flickering window.view.headerbar.height_request = Jorts.Zoom.from_int (new_zoom).to_ui_size (); diff --git a/src/Widgets/TextView.vala b/src/Widgets/TextView.vala index 4be94e08..4084bda1 100644 --- a/src/Widgets/TextView.vala +++ b/src/Widgets/TextView.vala @@ -81,16 +81,30 @@ public class Jorts.TextView : Granite.HyperTextView { } private void ensure_tags () { - if (buffer.tag_table.lookup ("list_item") == null) { - var layout = this.create_pango_layout (list_item_start); - int width, height; - layout.get_pixel_size (out width, out height); + if (list_item_start == "") { + return; + } + + var layout = this.create_pango_layout (list_item_start); + int width, height; + layout.get_pixel_size (out width, out height); + var list_item_tag = buffer.tag_table.lookup ("list_item"); + + if (list_item_tag == null) { buffer.create_tag ("list_item", "indent", -width, "left-margin", SPACING_DOUBLE + width ); + return; } + + list_item_tag.indent = -width; + list_item_tag.left_margin = SPACING_DOUBLE + width; + } + + public void refresh_list_item_indentation () { + ensure_tags (); } public void toggle_list () { From 4a0e344d6bf58ca8867b4e2212cc8ba1bd68dc4b Mon Sep 17 00:00:00 2001 From: Mysterie Cat Dev Date: Thu, 14 May 2026 14:47:16 -0700 Subject: [PATCH 5/9] fix: scan for, and reapply, indentations on load --- src/Widgets/TextView.vala | 26 ++++++++++++++++++++++++++ src/Windows/StickyNoteWindow.vala | 1 + 2 files changed, 27 insertions(+) diff --git a/src/Widgets/TextView.vala b/src/Widgets/TextView.vala index 4084bda1..8f7e3c79 100644 --- a/src/Widgets/TextView.vala +++ b/src/Widgets/TextView.vala @@ -107,6 +107,32 @@ public class Jorts.TextView : Granite.HyperTextView { ensure_tags (); } + public void restore_list_item_indentation () { + Gtk.TextIter start, end; + buffer.get_bounds (out start, out end); + buffer.remove_tag_by_name ("list_item", start, end); + + if (list_item_start == "") { + return; + } + + ensure_tags (); + + var line_count = buffer.get_line_count (); + + for (int line_number = 0; line_number < line_count; line_number++) { + if (!this.has_prefix (line_number)) { + continue; + } + + Gtk.TextIter line_start, line_end; + buffer.get_iter_at_line_offset (out line_start, line_number, 0); + line_end = line_start.copy (); + line_end.forward_to_line_end (); + buffer.apply_tag_by_name ("list_item", line_start, line_end); + } + } + public void toggle_list () { ensure_tags (); Gtk.TextIter start, end; diff --git a/src/Windows/StickyNoteWindow.vala b/src/Windows/StickyNoteWindow.vala index 6044ae56..1f82d0ce 100644 --- a/src/Windows/StickyNoteWindow.vala +++ b/src/Windows/StickyNoteWindow.vala @@ -191,6 +191,7 @@ public class Jorts.StickyNoteWindow : Gtk.ApplicationWindow { color_controller.theme = data.theme; zoom_controller.zoom = data.zoom; view.monospace = data.monospace; + textview.restore_list_item_indentation (); } public void has_changed () { From 44ac1e2976211168bbd58341b7660f5c1a11b3f8 Mon Sep 17 00:00:00 2001 From: Mysterie Cat Dev Date: Thu, 14 May 2026 15:12:41 -0700 Subject: [PATCH 6/9] feat: support changing list prefix during runtime --- src/Views/PreferencesView.vala | 21 ++++++++++- src/Widgets/TextView.vala | 67 ++++++++++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/src/Views/PreferencesView.vala b/src/Views/PreferencesView.vala index 1bd2a638..466e7763 100644 --- a/src/Views/PreferencesView.vala +++ b/src/Views/PreferencesView.vala @@ -10,6 +10,16 @@ private Granite.Toast toast; public Gtk.Button close_button; + private void commit_list_prefix (Gtk.Entry entry) { + var current_prefix = Application.gsettings.get_string (KEY_LIST); + + if (entry.text == current_prefix) { + return; + } + + Application.gsettings.set_string (KEY_LIST, entry.text); + } + construct { var overlay = new Gtk.Overlay (); child = overlay; @@ -50,6 +60,15 @@ list_entry.secondary_icon_name = "view-refresh-symbolic"; list_entry.secondary_icon_tooltip_text = _("Reset to default"); list_entry.icon_press.connect (on_reset_prefix); + list_entry.activate.connect (() => { + commit_list_prefix (list_entry); + }); + + var list_entry_focus = new Gtk.EventControllerFocus (); + list_entry_focus.leave.connect (() => { + commit_list_prefix (list_entry); + }); + list_entry.add_controller (list_entry_focus); var list_label = new Granite.HeaderLabel (_("List item prefix")) { mnemonic_widget = list_entry, @@ -63,7 +82,7 @@ Application.gsettings.bind (KEY_LIST, list_entry, "text", - SettingsBindFlags.DEFAULT); + SettingsBindFlags.GET); settingsbox.append (lists_box); diff --git a/src/Widgets/TextView.vala b/src/Widgets/TextView.vala index 8f7e3c79..1dfc287e 100644 --- a/src/Widgets/TextView.vala +++ b/src/Widgets/TextView.vala @@ -12,7 +12,19 @@ public class Jorts.TextView : Granite.HyperTextView { private Gtk.EventControllerKey keyboard; - public string list_item_start {get; set;} + private string _list_item_start = ""; + public string list_item_start { + get { return _list_item_start; } + set { + if (_list_item_start == value) { + return; + } + + var old_prefix = _list_item_start; + _list_item_start = value; + migrate_list_prefixes (old_prefix, value); + } + } public bool on_list_item {public get; private set;} public string text { @@ -159,8 +171,8 @@ public class Jorts.TextView : Granite.HyperTextView { /** * Add the list prefix only to lines who hasnt it already */ - private bool has_prefix (int line_number) { - if (list_item_start == "") {return false;} + private bool has_specific_prefix (int line_number, string prefix) { + if (prefix == "") {return false;} Gtk.TextIter start, end; buffer.get_iter_at_line_offset (out start, line_number, 0); @@ -170,7 +182,54 @@ public class Jorts.TextView : Granite.HyperTextView { var text_in_line = buffer.get_slice (start, end, false); - return text_in_line.has_prefix (list_item_start); + return text_in_line.has_prefix (prefix); + } + + private bool has_prefix (int line_number) { + return has_specific_prefix (line_number, list_item_start); + } + + private void replace_prefix (int line_number, string old_prefix, string new_prefix) { + Gtk.TextIter line_start, prefix_end; + + buffer.get_iter_at_line_offset (out line_start, line_number, 0); + buffer.get_iter_at_line_offset (out prefix_end, line_number, old_prefix.char_count ()); + buffer.delete (ref line_start, ref prefix_end); + + buffer.get_iter_at_line_offset (out line_start, line_number, 0); + buffer.insert (ref line_start, new_prefix, -1); + } + + private void migrate_list_prefixes (string old_prefix, string new_prefix) { + if (old_prefix == "") { + if (new_prefix == "") { + Gtk.TextIter start, end; + buffer.get_bounds (out start, out end); + buffer.remove_tag_by_name ("list_item", start, end); + } + + return; + } + + var line_count = buffer.get_line_count (); + var did_change = false; + + buffer.begin_user_action (); + + for (int line_number = 0; line_number < line_count; line_number++) { + if (!has_specific_prefix (line_number, old_prefix)) { + continue; + } + + replace_prefix (line_number, old_prefix, new_prefix); + did_change = true; + } + + buffer.end_user_action (); + + if (did_change || new_prefix == "") { + restore_list_item_indentation (); + } } /** From 682da9f8620568efe9cecc406ea090ae32b1c05b Mon Sep 17 00:00:00 2001 From: Mysterie Cat Dev Date: Fri, 15 May 2026 09:20:08 -0700 Subject: [PATCH 7/9] chore: updated list item start string --- data/jorts.gschema.xml.in | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/jorts.gschema.xml.in b/data/jorts.gschema.xml.in index 08409008..7026d1f0 100644 --- a/data/jorts.gschema.xml.in +++ b/data/jorts.gschema.xml.in @@ -12,9 +12,9 @@ Whether to hide the actionbar and its buttons - " • " - Hide actionbar - Whether to hide the actionbar and its buttons + "• " + List item start + The string to use at the start of a list item From 10033b724ad7ea8d0841ad9186931419c0b4109d Mon Sep 17 00:00:00 2001 From: Mysterie Cat Dev Date: Fri, 15 May 2026 10:04:56 -0700 Subject: [PATCH 8/9] fix: observe buffer for undo/redo --- src/Widgets/TextView.vala | 47 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/Widgets/TextView.vala b/src/Widgets/TextView.vala index 1dfc287e..f0fb8dbc 100644 --- a/src/Widgets/TextView.vala +++ b/src/Widgets/TextView.vala @@ -12,6 +12,9 @@ public class Jorts.TextView : Granite.HyperTextView { private Gtk.EventControllerKey keyboard; + private Gtk.TextBuffer? observed_buffer; + private ulong buffer_changed_handler_id = 0; + private bool list_item_restore_queued = false; private string _list_item_start = ""; public string list_item_start { get { return _list_item_start; } @@ -87,11 +90,55 @@ public class Jorts.TextView : Granite.HyperTextView { /* CONNECTS AND BINDS */ /***************************************************/ + notify["buffer"].connect (() => { + attach_buffer_observers (); + queue_restore_list_item_indentation (); + }); + attach_buffer_observers (); + Application.gsettings.bind (KEY_LIST, this, "list-item-start", GLib.SettingsBindFlags.DEFAULT); } + private void attach_buffer_observers () { + if (observed_buffer == buffer) { + return; + } + + detach_buffer_observers (); + observed_buffer = buffer; + + buffer_changed_handler_id = observed_buffer.changed.connect_after (queue_restore_list_item_indentation); + } + + private void detach_buffer_observers () { + if (observed_buffer == null) { + return; + } + + if (buffer_changed_handler_id != 0) { + SignalHandler.disconnect (observed_buffer, buffer_changed_handler_id); + buffer_changed_handler_id = 0; + } + + observed_buffer = null; + } + + private void queue_restore_list_item_indentation () { + if (list_item_restore_queued) { + return; + } + + list_item_restore_queued = true; + + Idle.add (() => { + list_item_restore_queued = false; + restore_list_item_indentation (); + return false; + }); + } + private void ensure_tags () { if (list_item_start == "") { return; From 35688a6ec80f19f14bb4125d7d349af633b8b0f0 Mon Sep 17 00:00:00 2001 From: Mysterie Cat Dev Date: Fri, 15 May 2026 10:20:57 -0700 Subject: [PATCH 9/9] fix: reduce sub-pixel rounding for hanging indents --- src/Widgets/TextView.vala | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Widgets/TextView.vala b/src/Widgets/TextView.vala index f0fb8dbc..3a12e49c 100644 --- a/src/Widgets/TextView.vala +++ b/src/Widgets/TextView.vala @@ -144,22 +144,30 @@ public class Jorts.TextView : Granite.HyperTextView { return; } - var layout = this.create_pango_layout (list_item_start); + var measured_prefix = list_item_start.strip (); + + if (measured_prefix == "") { + measured_prefix = list_item_start; + } + + var layout = this.create_pango_layout (measured_prefix); int width, height; layout.get_pixel_size (out width, out height); + var indent_width = int.max (width + 4, 8); + var list_item_tag = buffer.tag_table.lookup ("list_item"); if (list_item_tag == null) { buffer.create_tag ("list_item", - "indent", -width, - "left-margin", SPACING_DOUBLE + width + "indent", -indent_width, + "left-margin", SPACING_DOUBLE + indent_width ); return; } - list_item_tag.indent = -width; - list_item_tag.left_margin = SPACING_DOUBLE + width; + list_item_tag.indent = -indent_width; + list_item_tag.left_margin = SPACING_DOUBLE + indent_width; } public void refresh_list_item_indentation () {