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 diff --git a/src/Services/ZoomController.vala b/src/Services/ZoomController.vala index 753b2ff9..78c9c3b1 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/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 06a9bcf5..3a12e49c 100644 --- a/src/Widgets/TextView.vala +++ b/src/Widgets/TextView.vala @@ -12,7 +12,22 @@ public class Jorts.TextView : Granite.HyperTextView { private Gtk.EventControllerKey keyboard; - public string list_item_start {get; set;} + 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; } + 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 { @@ -75,12 +90,118 @@ 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; + } + + 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", -indent_width, + "left-margin", SPACING_DOUBLE + indent_width + ); + return; + } + + list_item_tag.indent = -indent_width; + list_item_tag.left_margin = SPACING_DOUBLE + indent_width; + } + + public void refresh_list_item_indentation () { + 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; buffer.get_selection_bounds (out start, out end); @@ -105,8 +226,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); @@ -116,7 +237,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 (); + } } /** @@ -147,6 +315,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 +338,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,10 +358,10 @@ 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) { - print ("backspace"); Gtk.TextIter start, end; buffer.get_selection_bounds (out start, out end); @@ -197,6 +378,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 +400,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; diff --git a/src/Windows/StickyNoteWindow.vala b/src/Windows/StickyNoteWindow.vala index 27305c02..441bd37c 100644 --- a/src/Windows/StickyNoteWindow.vala +++ b/src/Windows/StickyNoteWindow.vala @@ -199,6 +199,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 () {