From a090120a26eac10e9552c25414bd15c8190162ef Mon Sep 17 00:00:00 2001 From: teamcons Date: Wed, 28 Jan 2026 20:36:06 +0100 Subject: [PATCH 01/18] Introduce a SearchButton --- src/SearchButton.vala | 125 ++++++++++++++++++++++++++++++++++++++++++ src/meson.build | 1 + 2 files changed, 126 insertions(+) create mode 100644 src/SearchButton.vala diff --git a/src/SearchButton.vala b/src/SearchButton.vala new file mode 100644 index 0000000..dc52cda --- /dev/null +++ b/src/SearchButton.vala @@ -0,0 +1,125 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 William Kelso + */ + +public class SearchButton : Gtk.Box { + + Gtk.Entry entry_search; + Gtk.ToggleButton toggle_search; + Gtk.ToggleButton toggle_match; + Gtk.Button previous; + Gtk.Button next; + public Gtk.TextBuffer buffer {get; construct;} + + public bool active { + get {return toggle_search.active;} + set {toggle_search.active = value;} + } + + Gtk.TextSearchFlags flags { + get { + if (toggle_match.active) { + return Gtk.TextSearchFlags.TEXT_ONLY; + } + return Gtk.TextSearchFlags.CASE_INSENSITIVE; + } + set { + toggle_match.active = (value == Gtk.TextSearchFlags.TEXT_ONLY); + } + } + + public SearchButton (Gtk.TextBuffer buffer) { + Object (buffer: buffer); + } + + construct { + orientation = Gtk.Orientation.HORIZONTAL; + spacing = 3; + + toggle_search = new Gtk.ToggleButton () { + icon_name = "system-search", + tooltip_markup = Granite.markup_accel_tooltip ( + {"f"}, + _("Search") + ) + }; + + var search_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); + search_box.add_css_class (Granite.STYLE_CLASS_LINKED); + + entry_search = new Gtk.Entry () { + placeholder_text = _("Enter search term"), + secondary_icon_tooltip_text = _("Clear text"), + }; + + previous = new Gtk.Button.from_icon_name ("go-down-symbolic"); + next = new Gtk.Button.from_icon_name ("go-up-symbolic"); + toggle_match = new Gtk.ToggleButton () { + icon_name = "font-select-symbolic", + tooltip_text = _("Match case") + }; + + search_box.append (entry_search); + search_box.append (previous); + search_box.append (next); + search_box.append (toggle_match); + + var revealer_search = new Gtk.Revealer () { + child = search_box, + transition_type = Gtk.RevealerTransitionType.SLIDE_LEFT + }; + + append (revealer_search); + append (toggle_search); + + + /* ---------------- CONNECTS AND BINDS ---------------- */ + toggle_search.bind_property ("active", + revealer_search, "reveal-child", + GLib.BindingFlags.SYNC_CREATE); + + entry_search.changed.connect (on_entry_changed); + entry_search.changed.connect (() => {search_text ();}); + entry_search.icon_release.connect (on_clear_clicked); + + previous.clicked.connect (() => {search_text (false);}); + next.clicked.connect (() => {search_text (true);}); + + revealer_search.notify["reveal-child"].connect (() => {entry_search.grab_focus ();}); + } + + private void on_entry_changed () { + if (entry_search.text_length > 0) { + entry_search.secondary_icon_name = "edit-clear-symbolic"; + + } else { + entry_search.secondary_icon_name = ""; + } + } + + private void on_clear_clicked () { + entry_search.text = ""; + } + + private void search_text (bool? forward = true) { + Gtk.TextIter start_selection, end_selection; + buffer.get_selection_bounds (out start_selection, out end_selection); + + Gtk.TextIter start_buffer, end_buffer; + buffer.get_bounds (out start_buffer, out end_buffer); + + Gtk.TextIter match_start, match_end; + bool found_match; + + if (forward) { + found_match = end_selection.forward_search (entry_search.text, flags, + out match_start, out match_end, start_buffer); + } else { + found_match = start_selection.backward_search (entry_search.text, flags, + out match_start, out match_end, end_buffer); + } + + buffer.select_range (match_start, match_end); + } +} diff --git a/src/meson.build b/src/meson.build index f5f77ac..aafbb2d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -2,4 +2,5 @@ sources += files( 'Window.vala', 'Application.vala', 'Utils.vala', + 'SearchButton.vala', ) From dcd76a752ebc27d88990a1db92f9e47479533c42 Mon Sep 17 00:00:00 2001 From: teamcons Date: Wed, 28 Jan 2026 20:36:25 +0100 Subject: [PATCH 02/18] Initial (fucky) implementation in the UI --- src/Application.vala | 12 ++++++++++++ src/Window.vala | 3 +++ 2 files changed, 15 insertions(+) diff --git a/src/Application.vala b/src/Application.vala index 9e91f07..723437f 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -63,6 +63,18 @@ public class Application : Gtk.Application { window.on_save_as (); }); + SimpleAction toggle_search_action = new SimpleAction ("search", null); + set_accels_for_action ("app.search", {"f"}); + add_action (toggle_search_action); + toggle_search_action.activate.connect (() => { + unowned var window = this.get_active_window () as AppWindow; + if (window == null) { + return; + } + + window.search.active = !window.search.active; + }); + SimpleAction quit_action = new SimpleAction ("quit", null); set_accels_for_action ("app.quit", {"q"}); add_action (quit_action); diff --git a/src/Window.vala b/src/Window.vala index 60e1481..971b147 100644 --- a/src/Window.vala +++ b/src/Window.vala @@ -7,6 +7,7 @@ public class AppWindow : Gtk.Window { public File file { get; set; } private Gtk.TextBuffer buf; private Gtk.HeaderBar header; + public SearchButton search; public string file_name { get; set; } // Add a debounce so we aren't writing the entire buffer every character input @@ -65,6 +66,8 @@ public class AppWindow : Gtk.Window { buf = text_view.buffer; buf.text = ""; + search = new SearchButton (buf); + header.pack_end (search); var scrolled_view = new Gtk.ScrolledWindow () { child = text_view, From 671e29a910b8f6c45f92ffd47536994fed32a02c Mon Sep 17 00:00:00 2001 From: teamcons Date: Wed, 28 Jan 2026 21:49:51 +0100 Subject: [PATCH 03/18] Move to a popover, scroll to selection, and less crashy but still crashy --- src/Application.vala | 2 +- src/SearchButton.vala | 73 ++++++++++++++++++++++++++----------------- src/Window.vala | 2 +- 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/src/Application.vala b/src/Application.vala index 723437f..c25fd91 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -72,7 +72,7 @@ public class Application : Gtk.Application { return; } - window.search.active = !window.search.active; + window.search.search_menu.activate (); }); SimpleAction quit_action = new SimpleAction ("quit", null); diff --git a/src/SearchButton.vala b/src/SearchButton.vala index dc52cda..a379e1c 100644 --- a/src/SearchButton.vala +++ b/src/SearchButton.vala @@ -5,18 +5,14 @@ public class SearchButton : Gtk.Box { + public Gtk.MenuButton search_menu; Gtk.Entry entry_search; - Gtk.ToggleButton toggle_search; Gtk.ToggleButton toggle_match; Gtk.Button previous; Gtk.Button next; + public Gtk.TextView textview {get; construct;} public Gtk.TextBuffer buffer {get; construct;} - public bool active { - get {return toggle_search.active;} - set {toggle_search.active = value;} - } - Gtk.TextSearchFlags flags { get { if (toggle_match.active) { @@ -29,15 +25,18 @@ public class SearchButton : Gtk.Box { } } - public SearchButton (Gtk.TextBuffer buffer) { - Object (buffer: buffer); + public SearchButton (Gtk.TextView textview) { + Object ( + textview: textview, + buffer: textview.buffer + ); } construct { orientation = Gtk.Orientation.HORIZONTAL; - spacing = 3; + spacing = 0; - toggle_search = new Gtk.ToggleButton () { + search_menu = new Gtk.MenuButton () { icon_name = "system-search", tooltip_markup = Granite.markup_accel_tooltip ( {"f"}, @@ -45,7 +44,12 @@ public class SearchButton : Gtk.Box { ) }; - var search_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); + var search_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) { + margin_start = 10, + margin_end = 10, + margin_top = 10, + margin_bottom = 10 + }; search_box.add_css_class (Granite.STYLE_CLASS_LINKED); entry_search = new Gtk.Entry () { @@ -53,8 +57,8 @@ public class SearchButton : Gtk.Box { secondary_icon_tooltip_text = _("Clear text"), }; - previous = new Gtk.Button.from_icon_name ("go-down-symbolic"); - next = new Gtk.Button.from_icon_name ("go-up-symbolic"); + previous = new Gtk.Button.from_icon_name ("go-up-symbolic"); + next = new Gtk.Button.from_icon_name ("go-down-symbolic"); toggle_match = new Gtk.ToggleButton () { icon_name = "font-select-symbolic", tooltip_text = _("Match case") @@ -65,28 +69,27 @@ public class SearchButton : Gtk.Box { search_box.append (next); search_box.append (toggle_match); - var revealer_search = new Gtk.Revealer () { - child = search_box, - transition_type = Gtk.RevealerTransitionType.SLIDE_LEFT + + var popover = new Gtk.Popover () { + child = search_box }; - append (revealer_search); - append (toggle_search); + search_menu.popover = popover; + append (search_menu); /* ---------------- CONNECTS AND BINDS ---------------- */ - toggle_search.bind_property ("active", - revealer_search, "reveal-child", - GLib.BindingFlags.SYNC_CREATE); + entry_search.changed.connect (on_entry_changed); - entry_search.changed.connect (() => {search_text ();}); entry_search.icon_release.connect (on_clear_clicked); previous.clicked.connect (() => {search_text (false);}); next.clicked.connect (() => {search_text (true);}); + entry_search.activate.connect (() => {search_text (true);}); + - revealer_search.notify["reveal-child"].connect (() => {entry_search.grab_focus ();}); + popover.show.connect (() => {entry_search.grab_focus ();}); } private void on_entry_changed () { @@ -103,23 +106,37 @@ public class SearchButton : Gtk.Box { } private void search_text (bool? forward = true) { + print ("\nSearch start"); + Gtk.TextIter start_selection, end_selection; buffer.get_selection_bounds (out start_selection, out end_selection); + print ("\nbounds start"); + Gtk.TextIter start_buffer, end_buffer; buffer.get_bounds (out start_buffer, out end_buffer); Gtk.TextIter match_start, match_end; bool found_match; + var text = entry_search.text; if (forward) { - found_match = end_selection.forward_search (entry_search.text, flags, - out match_start, out match_end, start_buffer); + print ("\nfw"); + found_match = end_selection.forward_search (text, flags, + out match_start, out match_end, null); } else { - found_match = start_selection.backward_search (entry_search.text, flags, - out match_start, out match_end, end_buffer); + + print ("\nbackward"); + found_match = start_selection.backward_search (text, flags, + out match_start, out match_end, null); + } + + print ("\nFound: " + found_match.to_string () + " at? " + match_start.get_offset ().to_string ()); + + if (found_match) { + buffer.select_range (match_start, match_end); + textview.scroll_to_iter (match_start, 0, false, 0.5f, 0.5f); } - buffer.select_range (match_start, match_end); } } diff --git a/src/Window.vala b/src/Window.vala index 971b147..0ed497b 100644 --- a/src/Window.vala +++ b/src/Window.vala @@ -66,7 +66,7 @@ public class AppWindow : Gtk.Window { buf = text_view.buffer; buf.text = ""; - search = new SearchButton (buf); + search = new SearchButton (text_view); header.pack_end (search); var scrolled_view = new Gtk.ScrolledWindow () { From 6416937a41f3a573c09ad75b8846ced00fe3ad30 Mon Sep 17 00:00:00 2001 From: teamcons Date: Wed, 28 Jan 2026 22:01:21 +0100 Subject: [PATCH 04/18] Add match-key setting --- data/meson.build | 6 ++++++ data/slate.gschema.xml | 10 ++++++++++ meson.build | 1 + src/SearchButton.vala | 19 ++++++++++++------- 4 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 data/slate.gschema.xml diff --git a/data/meson.build b/data/meson.build index d6912b7..074472f 100644 --- a/data/meson.build +++ b/data/meson.build @@ -24,3 +24,9 @@ install_data( rename: meson.project_name() + '.metainfo.xml', install_dir: join_paths(get_option('datadir'), 'metainfo'), ) + +install_data( + 'slate.gschema.xml', + install_dir: get_option('datadir') / 'glib-2.0' / 'schemas', + rename: meson.project_name() + '.gschema.xml' +) \ No newline at end of file diff --git a/data/slate.gschema.xml b/data/slate.gschema.xml new file mode 100644 index 0000000..3d06fbe --- /dev/null +++ b/data/slate.gschema.xml @@ -0,0 +1,10 @@ + + + + + false + Whether to match case in search + Used by the search function to remember whether the user wanted to match case + + + \ No newline at end of file diff --git a/meson.build b/meson.build index fd07101..f4b6f96 100644 --- a/meson.build +++ b/meson.build @@ -50,6 +50,7 @@ subdir('data') subdir('po') gnome.post_install( + glib_compile_schemas: true, gtk_update_icon_cache: true, update_desktop_database: true ) diff --git a/src/SearchButton.vala b/src/SearchButton.vala index a379e1c..68c05ad 100644 --- a/src/SearchButton.vala +++ b/src/SearchButton.vala @@ -88,8 +88,12 @@ public class SearchButton : Gtk.Box { next.clicked.connect (() => {search_text (true);}); entry_search.activate.connect (() => {search_text (true);}); - popover.show.connect (() => {entry_search.grab_focus ();}); + + var settings = new GLib.Settings ("io.github.wpkelso.slate"); + settings.bind ("match-case", + toggle_match, "active", + GLib.SettingsBindFlags.DEFAULT); } private void on_entry_changed () { @@ -121,16 +125,17 @@ public class SearchButton : Gtk.Box { var text = entry_search.text; if (forward) { - print ("\nfw"); - found_match = end_selection.forward_search (text, flags, + print ("\nfw"); + found_match = end_selection.forward_search (text, flags, out match_start, out match_end, null); + } else { - print ("\nbackward"); - found_match = start_selection.backward_search (text, flags, - out match_start, out match_end, null); - } + print ("\nbackward"); + found_match = start_selection.backward_search (text, flags, + out match_start, out match_end, null); + } print ("\nFound: " + found_match.to_string () + " at? " + match_start.get_offset ().to_string ()); if (found_match) { From 505d17fa492d7873903247f544d02dc0aec58f7e Mon Sep 17 00:00:00 2001 From: teamcons Date: Wed, 28 Jan 2026 22:08:02 +0100 Subject: [PATCH 05/18] Add tooltips to next and previous --- src/SearchButton.vala | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/SearchButton.vala b/src/SearchButton.vala index 68c05ad..4461728 100644 --- a/src/SearchButton.vala +++ b/src/SearchButton.vala @@ -57,8 +57,14 @@ public class SearchButton : Gtk.Box { secondary_icon_tooltip_text = _("Clear text"), }; - previous = new Gtk.Button.from_icon_name ("go-up-symbolic"); - next = new Gtk.Button.from_icon_name ("go-down-symbolic"); + previous = new Gtk.Button.from_icon_name ("go-up-symbolic") { + tooltip_text = _("Search for an earlier match") + }; + + next = new Gtk.Button.from_icon_name ("go-down-symbolic") { + tooltip_text = _("Search for a later match") + }; + toggle_match = new Gtk.ToggleButton () { icon_name = "font-select-symbolic", tooltip_text = _("Match case") From 3d57cba5c93622a2634cb13905b47f814f4653c2 Mon Sep 17 00:00:00 2001 From: teamcons Date: Wed, 28 Jan 2026 22:35:12 +0100 Subject: [PATCH 06/18] Fix crashing --- src/SearchButton.vala | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/SearchButton.vala b/src/SearchButton.vala index 4461728..8fc3816 100644 --- a/src/SearchButton.vala +++ b/src/SearchButton.vala @@ -116,37 +116,43 @@ public class SearchButton : Gtk.Box { } private void search_text (bool? forward = true) { - print ("\nSearch start"); - - Gtk.TextIter start_selection, end_selection; - buffer.get_selection_bounds (out start_selection, out end_selection); - - print ("\nbounds start"); Gtk.TextIter start_buffer, end_buffer; buffer.get_bounds (out start_buffer, out end_buffer); - Gtk.TextIter match_start, match_end; - bool found_match; - var text = entry_search.text; + // Selection_bounds can leave the variables untouched, which can lead to a crash + Gtk.TextIter start_selection = start_buffer.copy (); + Gtk.TextIter end_selection = start_buffer.copy (); + buffer.get_selection_bounds (out start_selection, out end_selection); - if (forward) { - print ("\nfw"); - found_match = end_selection.forward_search (text, flags, - out match_start, out match_end, null); + Gtk.TextIter match_start = start_selection.copy (); + Gtk.TextIter match_end = end_selection.copy (); + bool found_match = false; - } else { + if (forward) { - print ("\nbackward"); - found_match = start_selection.backward_search (text, flags, - out match_start, out match_end, null); + //We have to check quick n' dirty behorehand because forward/backward_search prefers to crash the app than return false + var remaining_text = buffer.get_slice (end_selection, end_buffer, true); + if (entry_search.text in remaining_text) { + found_match = end_selection.forward_search (entry_search.text, flags, + out match_start, out match_end, end_buffer); + } + } else { + var remaining_text = buffer.get_slice (start_buffer, start_selection, true); + if (entry_search.text in remaining_text) { + found_match = start_selection.backward_search (entry_search.text, flags, + out match_start, out match_end, start_buffer); + } } - print ("\nFound: " + found_match.to_string () + " at? " + match_start.get_offset ().to_string ()); + print ("\nFound: " + found_match.to_string () + " at? " + match_start.get_offset ().to_string ()); if (found_match) { buffer.select_range (match_start, match_end); textview.scroll_to_iter (match_start, 0, false, 0.5f, 0.5f); + entry_search.remove_css_class (Granite.STYLE_CLASS_ERROR); + } else { + entry_search.add_css_class (Granite.STYLE_CLASS_ERROR); } } From 1335692d4880ec12066d7e1ad2d83ea66c6cd1a0 Mon Sep 17 00:00:00 2001 From: teamcons Date: Wed, 28 Jan 2026 23:30:27 +0100 Subject: [PATCH 07/18] fix broken insensitive case, display a message when things break --- src/SearchButton.vala | 75 +++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/src/SearchButton.vala b/src/SearchButton.vala index 8fc3816..81cb2ce 100644 --- a/src/SearchButton.vala +++ b/src/SearchButton.vala @@ -10,18 +10,20 @@ public class SearchButton : Gtk.Box { Gtk.ToggleButton toggle_match; Gtk.Button previous; Gtk.Button next; + Gtk.Revealer revealer_not_found; public Gtk.TextView textview {get; construct;} public Gtk.TextBuffer buffer {get; construct;} Gtk.TextSearchFlags flags { get { if (toggle_match.active) { - return Gtk.TextSearchFlags.TEXT_ONLY; + return Gtk.TextSearchFlags.VISIBLE_ONLY; + } else { + return Gtk.TextSearchFlags.CASE_INSENSITIVE; } - return Gtk.TextSearchFlags.CASE_INSENSITIVE; } set { - toggle_match.active = (value == Gtk.TextSearchFlags.TEXT_ONLY); + toggle_match.active = (value == Gtk.TextSearchFlags.VISIBLE_ONLY); } } @@ -44,12 +46,7 @@ public class SearchButton : Gtk.Box { ) }; - var search_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) { - margin_start = 10, - margin_end = 10, - margin_top = 10, - margin_bottom = 10 - }; + var search_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); search_box.add_css_class (Granite.STYLE_CLASS_LINKED); entry_search = new Gtk.Entry () { @@ -75,9 +72,27 @@ public class SearchButton : Gtk.Box { search_box.append (next); search_box.append (toggle_match); + var label_not_found = new Gtk.Label (_("Search term could not be found")); + label_not_found.add_css_class (Granite.STYLE_CLASS_ERROR); + + revealer_not_found = new Gtk.Revealer () { + child = label_not_found, + transition_type = Gtk.RevealerTransitionType.SLIDE_DOWN, + reveal_child = false + }; + + var popover_box = new Gtk.Box (VERTICAL, 5) { + margin_start = 10, + margin_end = 10, + margin_top = 10, + margin_bottom = 5 + }; + + popover_box.append (search_box); + popover_box.append (revealer_not_found); var popover = new Gtk.Popover () { - child = search_box + child = popover_box }; search_menu.popover = popover; @@ -116,32 +131,49 @@ public class SearchButton : Gtk.Box { } private void search_text (bool? forward = true) { + if (entry_search.text == "") { + return; + }; Gtk.TextIter start_buffer, end_buffer; buffer.get_bounds (out start_buffer, out end_buffer); // Selection_bounds can leave the variables untouched, which can lead to a crash - Gtk.TextIter start_selection = start_buffer.copy (); - Gtk.TextIter end_selection = start_buffer.copy (); + Gtk.TextIter start_selection = start_buffer; + Gtk.TextIter end_selection = start_buffer; buffer.get_selection_bounds (out start_selection, out end_selection); - Gtk.TextIter match_start = start_selection.copy (); - Gtk.TextIter match_end = end_selection.copy (); + Gtk.TextIter match_start = start_selection; + Gtk.TextIter match_end = end_selection; bool found_match = false; if (forward) { - //We have to check quick n' dirty behorehand because forward/backward_search prefers to crash the app than return false + //We have to check quick n' dirty beforehand because forward/backward_search prefers to crash the app than return false + //Also we have to account checking depending on case sensitiveness + //TODO: Fix this workaround var remaining_text = buffer.get_slice (end_selection, end_buffer, true); - if (entry_search.text in remaining_text) { - found_match = end_selection.forward_search (entry_search.text, flags, + var text = entry_search.text; + if (flags == Gtk.TextSearchFlags.CASE_INSENSITIVE) { + text = text.casefold (); + remaining_text = remaining_text.casefold (); + } + + if (text in remaining_text) { + found_match = end_selection.forward_search (text, flags, out match_start, out match_end, end_buffer); } } else { var remaining_text = buffer.get_slice (start_buffer, start_selection, true); - if (entry_search.text in remaining_text) { - found_match = start_selection.backward_search (entry_search.text, flags, + var text = entry_search.text; + if (flags == Gtk.TextSearchFlags.CASE_INSENSITIVE) { + text = text.casefold (); + remaining_text = remaining_text.casefold (); + } + + if (text in remaining_text) { + found_match = start_selection.backward_search (text, flags, out match_start, out match_end, start_buffer); } } @@ -150,9 +182,10 @@ public class SearchButton : Gtk.Box { if (found_match) { buffer.select_range (match_start, match_end); textview.scroll_to_iter (match_start, 0, false, 0.5f, 0.5f); - entry_search.remove_css_class (Granite.STYLE_CLASS_ERROR); + + revealer_not_found.reveal_child = false; } else { - entry_search.add_css_class (Granite.STYLE_CLASS_ERROR); + revealer_not_found.reveal_child = true; } } From db2c8427b731d78d43c96a7c07a50cf1327e9d70 Mon Sep 17 00:00:00 2001 From: teamcons Date: Thu, 29 Jan 2026 00:16:02 +0100 Subject: [PATCH 08/18] search icon --- src/SearchButton.vala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SearchButton.vala b/src/SearchButton.vala index 81cb2ce..9895298 100644 --- a/src/SearchButton.vala +++ b/src/SearchButton.vala @@ -52,6 +52,7 @@ public class SearchButton : Gtk.Box { entry_search = new Gtk.Entry () { placeholder_text = _("Enter search term"), secondary_icon_tooltip_text = _("Clear text"), + primary_icon_name = "system-search-symbolic" }; previous = new Gtk.Button.from_icon_name ("go-up-symbolic") { From e096d9a3ee6d6f277af207f79f963c5c3a128f43 Mon Sep 17 00:00:00 2001 From: teamcons Date: Sun, 1 Feb 2026 12:49:33 +0100 Subject: [PATCH 09/18] Allow searching from start if user is already at the end --- src/SearchButton.vala | 14 ++++++++++++-- src/Window.vala | 2 ++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/SearchButton.vala b/src/SearchButton.vala index 9895298..b9c778b 100644 --- a/src/SearchButton.vala +++ b/src/SearchButton.vala @@ -101,8 +101,6 @@ public class SearchButton : Gtk.Box { /* ---------------- CONNECTS AND BINDS ---------------- */ - - entry_search.changed.connect (on_entry_changed); entry_search.icon_release.connect (on_clear_clicked); @@ -150,6 +148,14 @@ public class SearchButton : Gtk.Box { if (forward) { + // If the cursor is at the end (like on app start or when the user is typing), we may want to search from the start + // Gated by Forward so the user can still go backward from the end if they want to + if (end_buffer.is_cursor_position ()) { + buffer.place_cursor (start_buffer); + start_selection = start_buffer; + end_selection = start_buffer; + } + //We have to check quick n' dirty beforehand because forward/backward_search prefers to crash the app than return false //Also we have to account checking depending on case sensitiveness //TODO: Fix this workaround @@ -166,6 +172,10 @@ public class SearchButton : Gtk.Box { } } else { + + //We have to check quick n' dirty beforehand because forward/backward_search prefers to crash the app than return false + //Also we have to account checking depending on case sensitiveness + //TODO: Fix this workaround var remaining_text = buffer.get_slice (start_buffer, start_selection, true); var text = entry_search.text; if (flags == Gtk.TextSearchFlags.CASE_INSENSITIVE) { diff --git a/src/Window.vala b/src/Window.vala index 0ed497b..a727ed6 100644 --- a/src/Window.vala +++ b/src/Window.vala @@ -86,6 +86,8 @@ public class AppWindow : Gtk.Window { open_file (file); + text_view.grab_focus (); + debug ("Connecting signals"); // Signal callbacks are heavily derived from similar operations in // elementary/code From 37b8672982d318e498c0562380b192d5ad195010 Mon Sep 17 00:00:00 2001 From: teamcons Date: Sun, 1 Feb 2026 23:15:06 +0100 Subject: [PATCH 10/18] Fix regression: search stuck after first hit. Add a debounce after text entry --- src/SearchButton.vala | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/SearchButton.vala b/src/SearchButton.vala index b9c778b..5df6eec 100644 --- a/src/SearchButton.vala +++ b/src/SearchButton.vala @@ -4,6 +4,8 @@ */ public class SearchButton : Gtk.Box { + public Gtk.TextView textview {get; construct;} + public Gtk.TextBuffer buffer {get; construct;} public Gtk.MenuButton search_menu; Gtk.Entry entry_search; @@ -11,8 +13,10 @@ public class SearchButton : Gtk.Box { Gtk.Button previous; Gtk.Button next; Gtk.Revealer revealer_not_found; - public Gtk.TextView textview {get; construct;} - public Gtk.TextBuffer buffer {get; construct;} + + // Add a debounce for search + static int interval = 500; // ms + static uint debounce_timer_id = 0; Gtk.TextSearchFlags flags { get { @@ -102,6 +106,7 @@ public class SearchButton : Gtk.Box { /* ---------------- CONNECTS AND BINDS ---------------- */ entry_search.changed.connect (on_entry_changed); + entry_search.changed.connect (search_after_typing); entry_search.icon_release.connect (on_clear_clicked); previous.clicked.connect (() => {search_text (false);}); @@ -125,6 +130,22 @@ public class SearchButton : Gtk.Box { } } + + public void search_after_typing () { + debug ("The buffer has been modified, starting the debounce timer"); + + if (debounce_timer_id != 0) { + GLib.Source.remove (debounce_timer_id); + } + + debounce_timer_id = Timeout.add (interval, () => { + debounce_timer_id = 0; + search_text (true); + return GLib.Source.REMOVE; + }); + + } + private void on_clear_clicked () { entry_search.text = ""; } @@ -150,7 +171,8 @@ public class SearchButton : Gtk.Box { // If the cursor is at the end (like on app start or when the user is typing), we may want to search from the start // Gated by Forward so the user can still go backward from the end if they want to - if (end_buffer.is_cursor_position ()) { + if (start_selection.is_end ()) { + print ("\nTHIS IS END: " + buffer.cursor_position.to_string ()); buffer.place_cursor (start_buffer); start_selection = start_buffer; end_selection = start_buffer; @@ -173,6 +195,8 @@ public class SearchButton : Gtk.Box { } else { + print ("go backward"); + //We have to check quick n' dirty beforehand because forward/backward_search prefers to crash the app than return false //Also we have to account checking depending on case sensitiveness //TODO: Fix this workaround @@ -189,7 +213,10 @@ public class SearchButton : Gtk.Box { } } - print ("\nFound: " + found_match.to_string () + " at? " + match_start.get_offset ().to_string ()); + debug ("Cursor: " + buffer.cursor_position.to_string ()); + debug ("Start and end selection: " + start_selection.get_offset ().to_string () + "|" + end_selection.get_offset ().to_string ()); + debug ("Found: " + found_match.to_string () + " at? " + match_start.get_offset ().to_string () + "|" + match_end.get_offset ().to_string ()); + if (found_match) { buffer.select_range (match_start, match_end); textview.scroll_to_iter (match_start, 0, false, 0.5f, 0.5f); From 34cc1114db3c5830673a445f709e536526dc7ab7 Mon Sep 17 00:00:00 2001 From: teamcons Date: Sun, 1 Feb 2026 23:18:24 +0100 Subject: [PATCH 11/18] subclass bin instead of box --- src/SearchButton.vala | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/SearchButton.vala b/src/SearchButton.vala index 5df6eec..049dfd5 100644 --- a/src/SearchButton.vala +++ b/src/SearchButton.vala @@ -3,7 +3,7 @@ * SPDX-FileCopyrightText: 2025 William Kelso */ -public class SearchButton : Gtk.Box { +public class SearchButton : Granite.Bin { public Gtk.TextView textview {get; construct;} public Gtk.TextBuffer buffer {get; construct;} @@ -39,9 +39,6 @@ public class SearchButton : Gtk.Box { } construct { - orientation = Gtk.Orientation.HORIZONTAL; - spacing = 0; - search_menu = new Gtk.MenuButton () { icon_name = "system-search", tooltip_markup = Granite.markup_accel_tooltip ( @@ -101,7 +98,7 @@ public class SearchButton : Gtk.Box { }; search_menu.popover = popover; - append (search_menu); + child = search_menu; /* ---------------- CONNECTS AND BINDS ---------------- */ From aa2ce1c982acbf65c39901a6f4d0677e685d64d7 Mon Sep 17 00:00:00 2001 From: teamcons Date: Sun, 1 Feb 2026 23:24:20 +0100 Subject: [PATCH 12/18] Improve style a touch --- src/SearchButton.vala | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/SearchButton.vala b/src/SearchButton.vala index 049dfd5..2ea60a6 100644 --- a/src/SearchButton.vala +++ b/src/SearchButton.vala @@ -22,9 +22,8 @@ public class SearchButton : Granite.Bin { get { if (toggle_match.active) { return Gtk.TextSearchFlags.VISIBLE_ONLY; - } else { - return Gtk.TextSearchFlags.CASE_INSENSITIVE; } + return Gtk.TextSearchFlags.CASE_INSENSITIVE; } set { toggle_match.active = (value == Gtk.TextSearchFlags.VISIBLE_ONLY); @@ -32,10 +31,8 @@ public class SearchButton : Granite.Bin { } public SearchButton (Gtk.TextView textview) { - Object ( - textview: textview, - buffer: textview.buffer - ); + Object (textview: textview, + buffer: textview.buffer); } construct { @@ -127,10 +124,7 @@ public class SearchButton : Granite.Bin { } } - public void search_after_typing () { - debug ("The buffer has been modified, starting the debounce timer"); - if (debounce_timer_id != 0) { GLib.Source.remove (debounce_timer_id); } @@ -217,8 +211,8 @@ public class SearchButton : Granite.Bin { if (found_match) { buffer.select_range (match_start, match_end); textview.scroll_to_iter (match_start, 0, false, 0.5f, 0.5f); - revealer_not_found.reveal_child = false; + } else { revealer_not_found.reveal_child = true; } From 5ad21c26bc58811df891ed7105bddc8c23732f75 Mon Sep 17 00:00:00 2001 From: teamcons Date: Mon, 2 Feb 2026 00:10:50 +0100 Subject: [PATCH 13/18] Use accels for clear, backward and forward --- src/SearchButton.vala | 56 +++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/src/SearchButton.vala b/src/SearchButton.vala index 2ea60a6..ab75eef 100644 --- a/src/SearchButton.vala +++ b/src/SearchButton.vala @@ -14,10 +14,6 @@ public class SearchButton : Granite.Bin { Gtk.Button next; Gtk.Revealer revealer_not_found; - // Add a debounce for search - static int interval = 500; // ms - static uint debounce_timer_id = 0; - Gtk.TextSearchFlags flags { get { if (toggle_match.active) { @@ -49,16 +45,25 @@ public class SearchButton : Granite.Bin { entry_search = new Gtk.Entry () { placeholder_text = _("Enter search term"), - secondary_icon_tooltip_text = _("Clear text"), - primary_icon_name = "system-search-symbolic" + primary_icon_name = "system-search-symbolic", + secondary_icon_tooltip_markup = Granite.markup_accel_tooltip ( + {"l"}, + _("Clear text") + ) }; previous = new Gtk.Button.from_icon_name ("go-up-symbolic") { - tooltip_text = _("Search for an earlier match") + tooltip_markup = Granite.markup_accel_tooltip ( + {"Return"}, + _("Search for an earlier match") + ) }; next = new Gtk.Button.from_icon_name ("go-down-symbolic") { - tooltip_text = _("Search for a later match") + tooltip_markup = Granite.markup_accel_tooltip ( + {"Return"}, + _("Search for a later match") + ) }; toggle_match = new Gtk.ToggleButton () { @@ -97,10 +102,15 @@ public class SearchButton : Granite.Bin { search_menu.popover = popover; child = search_menu; + // We use a keypress controller to capture Shift+Enter + var keypress_controller = new Gtk.EventControllerKey (); + entry_search.add_controller (keypress_controller); + /* ---------------- CONNECTS AND BINDS ---------------- */ + keypress_controller.key_pressed.connect (on_key_press_event); + entry_search.changed.connect (on_entry_changed); - entry_search.changed.connect (search_after_typing); entry_search.icon_release.connect (on_clear_clicked); previous.clicked.connect (() => {search_text (false);}); @@ -124,19 +134,6 @@ public class SearchButton : Granite.Bin { } } - public void search_after_typing () { - if (debounce_timer_id != 0) { - GLib.Source.remove (debounce_timer_id); - } - - debounce_timer_id = Timeout.add (interval, () => { - debounce_timer_id = 0; - search_text (true); - return GLib.Source.REMOVE; - }); - - } - private void on_clear_clicked () { entry_search.text = ""; } @@ -163,7 +160,6 @@ public class SearchButton : Granite.Bin { // If the cursor is at the end (like on app start or when the user is typing), we may want to search from the start // Gated by Forward so the user can still go backward from the end if they want to if (start_selection.is_end ()) { - print ("\nTHIS IS END: " + buffer.cursor_position.to_string ()); buffer.place_cursor (start_buffer); start_selection = start_buffer; end_selection = start_buffer; @@ -185,9 +181,6 @@ public class SearchButton : Granite.Bin { } } else { - - print ("go backward"); - //We have to check quick n' dirty beforehand because forward/backward_search prefers to crash the app than return false //Also we have to account checking depending on case sensitiveness //TODO: Fix this workaround @@ -216,6 +209,17 @@ public class SearchButton : Granite.Bin { } else { revealer_not_found.reveal_child = true; } + } + + public bool on_key_press_event (uint keyval, uint keycode, Gdk.ModifierType state) { + if (keyval == Gdk.Key.Return && state == Gdk.ModifierType.SHIFT_MASK) { + search_text (false); + } + + if (keyval == Gdk.Key.l && state == Gdk.ModifierType.CONTROL_MASK) { + on_clear_clicked (); + } + return Gdk.EVENT_PROPAGATE; } } From 31162275629975322de0b1e697d4081136b57023 Mon Sep 17 00:00:00 2001 From: teamcons Date: Mon, 2 Feb 2026 12:27:37 +0100 Subject: [PATCH 14/18] Introduce a toolbar box --- src/Window.vala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Window.vala b/src/Window.vala index a727ed6..19c17cc 100644 --- a/src/Window.vala +++ b/src/Window.vala @@ -67,7 +67,10 @@ public class AppWindow : Gtk.Window { buf.text = ""; search = new SearchButton (text_view); - header.pack_end (search); + + var toolbar_box = new Gtk.Box (HORIZONTAL, 8); + toolbar_box.append (search); + header.pack_end (toolbar_box); var scrolled_view = new Gtk.ScrolledWindow () { child = text_view, From 3a71490cf7dcf7fddce29d9065679e63607cb114 Mon Sep 17 00:00:00 2001 From: teamcons Date: Mon, 2 Feb 2026 12:31:28 +0100 Subject: [PATCH 15/18] Add to potfiles --- po/POTFILES | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/po/POTFILES b/po/POTFILES index 5b1199c..4f5eab3 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -1,3 +1,4 @@ src/Application.vala src/Window.vala -src/Utils.vala \ No newline at end of file +src/Utils.vala +src/SearchButton.vala \ No newline at end of file From 976c6f7306f66a70b64135d82c0ff34b2d78e1b2 Mon Sep 17 00:00:00 2001 From: teamcons Date: Mon, 2 Feb 2026 12:56:11 +0100 Subject: [PATCH 16/18] Introduce toolbar box --- src/Window.vala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Window.vala b/src/Window.vala index 19c17cc..3926192 100644 --- a/src/Window.vala +++ b/src/Window.vala @@ -66,10 +66,12 @@ public class AppWindow : Gtk.Window { buf = text_view.buffer; buf.text = ""; - search = new SearchButton (text_view); - + // TODO: use Granite.Box (HORIZONTAL, HALF) when granite-7.7.0 is released var toolbar_box = new Gtk.Box (HORIZONTAL, 8); + + search = new SearchButton (text_view); toolbar_box.append (search); + header.pack_end (toolbar_box); var scrolled_view = new Gtk.ScrolledWindow () { From 22d42f5311cbbb7ab72d679ceec014d536f746e0 Mon Sep 17 00:00:00 2001 From: teamcons Date: Wed, 28 Jan 2026 21:49:51 +0100 Subject: [PATCH 17/18] Move to a popover, scroll to selection, and less crashy but still crashy --- src/SearchButton.vala | 152 +++++++++++------------------------------- 1 file changed, 38 insertions(+), 114 deletions(-) diff --git a/src/SearchButton.vala b/src/SearchButton.vala index ab75eef..d431d7c 100644 --- a/src/SearchButton.vala +++ b/src/SearchButton.vala @@ -7,12 +7,14 @@ public class SearchButton : Granite.Bin { public Gtk.TextView textview {get; construct;} public Gtk.TextBuffer buffer {get; construct;} + public Gtk.MenuButton search_menu; public Gtk.MenuButton search_menu; Gtk.Entry entry_search; Gtk.ToggleButton toggle_match; Gtk.Button previous; Gtk.Button next; - Gtk.Revealer revealer_not_found; + public Gtk.TextView textview {get; construct;} + public Gtk.TextBuffer buffer {get; construct;} Gtk.TextSearchFlags flags { get { @@ -27,11 +29,16 @@ public class SearchButton : Granite.Bin { } public SearchButton (Gtk.TextView textview) { - Object (textview: textview, - buffer: textview.buffer); + Object ( + textview: textview, + buffer: textview.buffer + ); } construct { + orientation = Gtk.Orientation.HORIZONTAL; + spacing = 0; + search_menu = new Gtk.MenuButton () { icon_name = "system-search", tooltip_markup = Granite.markup_accel_tooltip ( @@ -40,7 +47,12 @@ public class SearchButton : Granite.Bin { ) }; - var search_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); + var search_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) { + margin_start = 10, + margin_end = 10, + margin_top = 10, + margin_bottom = 10 + }; search_box.add_css_class (Granite.STYLE_CLASS_LINKED); entry_search = new Gtk.Entry () { @@ -52,20 +64,8 @@ public class SearchButton : Granite.Bin { ) }; - previous = new Gtk.Button.from_icon_name ("go-up-symbolic") { - tooltip_markup = Granite.markup_accel_tooltip ( - {"Return"}, - _("Search for an earlier match") - ) - }; - - next = new Gtk.Button.from_icon_name ("go-down-symbolic") { - tooltip_markup = Granite.markup_accel_tooltip ( - {"Return"}, - _("Search for a later match") - ) - }; - + previous = new Gtk.Button.from_icon_name ("go-up-symbolic"); + next = new Gtk.Button.from_icon_name ("go-down-symbolic"); toggle_match = new Gtk.ToggleButton () { icon_name = "font-select-symbolic", tooltip_text = _("Match case") @@ -76,39 +76,17 @@ public class SearchButton : Granite.Bin { search_box.append (next); search_box.append (toggle_match); - var label_not_found = new Gtk.Label (_("Search term could not be found")); - label_not_found.add_css_class (Granite.STYLE_CLASS_ERROR); - - revealer_not_found = new Gtk.Revealer () { - child = label_not_found, - transition_type = Gtk.RevealerTransitionType.SLIDE_DOWN, - reveal_child = false - }; - - var popover_box = new Gtk.Box (VERTICAL, 5) { - margin_start = 10, - margin_end = 10, - margin_top = 10, - margin_bottom = 5 - }; - - popover_box.append (search_box); - popover_box.append (revealer_not_found); var popover = new Gtk.Popover () { - child = popover_box + child = search_box }; search_menu.popover = popover; - child = search_menu; - - // We use a keypress controller to capture Shift+Enter - var keypress_controller = new Gtk.EventControllerKey (); - entry_search.add_controller (keypress_controller); + append (search_menu); /* ---------------- CONNECTS AND BINDS ---------------- */ - keypress_controller.key_pressed.connect (on_key_press_event); + entry_search.changed.connect (on_entry_changed); entry_search.icon_release.connect (on_clear_clicked); @@ -117,12 +95,8 @@ public class SearchButton : Granite.Bin { next.clicked.connect (() => {search_text (true);}); entry_search.activate.connect (() => {search_text (true);}); - popover.show.connect (() => {entry_search.grab_focus ();}); - var settings = new GLib.Settings ("io.github.wpkelso.slate"); - settings.bind ("match-case", - toggle_match, "active", - GLib.SettingsBindFlags.DEFAULT); + popover.show.connect (() => {entry_search.grab_focus ();}); } private void on_entry_changed () { @@ -139,87 +113,37 @@ public class SearchButton : Granite.Bin { } private void search_text (bool? forward = true) { - if (entry_search.text == "") { - return; - }; - - Gtk.TextIter start_buffer, end_buffer; - buffer.get_bounds (out start_buffer, out end_buffer); + print ("\nSearch start"); - // Selection_bounds can leave the variables untouched, which can lead to a crash - Gtk.TextIter start_selection = start_buffer; - Gtk.TextIter end_selection = start_buffer; + Gtk.TextIter start_selection, end_selection; buffer.get_selection_bounds (out start_selection, out end_selection); - Gtk.TextIter match_start = start_selection; - Gtk.TextIter match_end = end_selection; - bool found_match = false; + print ("\nbounds start"); - if (forward) { - - // If the cursor is at the end (like on app start or when the user is typing), we may want to search from the start - // Gated by Forward so the user can still go backward from the end if they want to - if (start_selection.is_end ()) { - buffer.place_cursor (start_buffer); - start_selection = start_buffer; - end_selection = start_buffer; - } - - //We have to check quick n' dirty beforehand because forward/backward_search prefers to crash the app than return false - //Also we have to account checking depending on case sensitiveness - //TODO: Fix this workaround - var remaining_text = buffer.get_slice (end_selection, end_buffer, true); - var text = entry_search.text; - if (flags == Gtk.TextSearchFlags.CASE_INSENSITIVE) { - text = text.casefold (); - remaining_text = remaining_text.casefold (); - } + Gtk.TextIter start_buffer, end_buffer; + buffer.get_bounds (out start_buffer, out end_buffer); - if (text in remaining_text) { - found_match = end_selection.forward_search (text, flags, - out match_start, out match_end, end_buffer); - } + Gtk.TextIter match_start, match_end; + bool found_match; + var text = entry_search.text; + if (forward) { + print ("\nfw"); + found_match = end_selection.forward_search (text, flags, + out match_start, out match_end, null); } else { - //We have to check quick n' dirty beforehand because forward/backward_search prefers to crash the app than return false - //Also we have to account checking depending on case sensitiveness - //TODO: Fix this workaround - var remaining_text = buffer.get_slice (start_buffer, start_selection, true); - var text = entry_search.text; - if (flags == Gtk.TextSearchFlags.CASE_INSENSITIVE) { - text = text.casefold (); - remaining_text = remaining_text.casefold (); - } - if (text in remaining_text) { - found_match = start_selection.backward_search (text, flags, - out match_start, out match_end, start_buffer); - } + print ("\nbackward"); + found_match = start_selection.backward_search (text, flags, + out match_start, out match_end, null); } - debug ("Cursor: " + buffer.cursor_position.to_string ()); - debug ("Start and end selection: " + start_selection.get_offset ().to_string () + "|" + end_selection.get_offset ().to_string ()); - debug ("Found: " + found_match.to_string () + " at? " + match_start.get_offset ().to_string () + "|" + match_end.get_offset ().to_string ()); + print ("\nFound: " + found_match.to_string () + " at? " + match_start.get_offset ().to_string ()); if (found_match) { buffer.select_range (match_start, match_end); textview.scroll_to_iter (match_start, 0, false, 0.5f, 0.5f); - revealer_not_found.reveal_child = false; - - } else { - revealer_not_found.reveal_child = true; - } - } - - public bool on_key_press_event (uint keyval, uint keycode, Gdk.ModifierType state) { - if (keyval == Gdk.Key.Return && state == Gdk.ModifierType.SHIFT_MASK) { - search_text (false); - } - - if (keyval == Gdk.Key.l && state == Gdk.ModifierType.CONTROL_MASK) { - on_clear_clicked (); } - return Gdk.EVENT_PROPAGATE; } } From 9f28a75212f9588381ef5fcb29327cf3bc460570 Mon Sep 17 00:00:00 2001 From: teamcons Date: Mon, 2 Feb 2026 13:16:37 +0100 Subject: [PATCH 18/18] Clean aftermath of botched rebase --- src/SearchButton.vala | 152 +++++++++++++++++++++++++++++++----------- 1 file changed, 114 insertions(+), 38 deletions(-) diff --git a/src/SearchButton.vala b/src/SearchButton.vala index d431d7c..ab75eef 100644 --- a/src/SearchButton.vala +++ b/src/SearchButton.vala @@ -7,14 +7,12 @@ public class SearchButton : Granite.Bin { public Gtk.TextView textview {get; construct;} public Gtk.TextBuffer buffer {get; construct;} - public Gtk.MenuButton search_menu; public Gtk.MenuButton search_menu; Gtk.Entry entry_search; Gtk.ToggleButton toggle_match; Gtk.Button previous; Gtk.Button next; - public Gtk.TextView textview {get; construct;} - public Gtk.TextBuffer buffer {get; construct;} + Gtk.Revealer revealer_not_found; Gtk.TextSearchFlags flags { get { @@ -29,16 +27,11 @@ public class SearchButton : Granite.Bin { } public SearchButton (Gtk.TextView textview) { - Object ( - textview: textview, - buffer: textview.buffer - ); + Object (textview: textview, + buffer: textview.buffer); } construct { - orientation = Gtk.Orientation.HORIZONTAL; - spacing = 0; - search_menu = new Gtk.MenuButton () { icon_name = "system-search", tooltip_markup = Granite.markup_accel_tooltip ( @@ -47,12 +40,7 @@ public class SearchButton : Granite.Bin { ) }; - var search_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) { - margin_start = 10, - margin_end = 10, - margin_top = 10, - margin_bottom = 10 - }; + var search_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); search_box.add_css_class (Granite.STYLE_CLASS_LINKED); entry_search = new Gtk.Entry () { @@ -64,8 +52,20 @@ public class SearchButton : Granite.Bin { ) }; - previous = new Gtk.Button.from_icon_name ("go-up-symbolic"); - next = new Gtk.Button.from_icon_name ("go-down-symbolic"); + previous = new Gtk.Button.from_icon_name ("go-up-symbolic") { + tooltip_markup = Granite.markup_accel_tooltip ( + {"Return"}, + _("Search for an earlier match") + ) + }; + + next = new Gtk.Button.from_icon_name ("go-down-symbolic") { + tooltip_markup = Granite.markup_accel_tooltip ( + {"Return"}, + _("Search for a later match") + ) + }; + toggle_match = new Gtk.ToggleButton () { icon_name = "font-select-symbolic", tooltip_text = _("Match case") @@ -76,17 +76,39 @@ public class SearchButton : Granite.Bin { search_box.append (next); search_box.append (toggle_match); + var label_not_found = new Gtk.Label (_("Search term could not be found")); + label_not_found.add_css_class (Granite.STYLE_CLASS_ERROR); + + revealer_not_found = new Gtk.Revealer () { + child = label_not_found, + transition_type = Gtk.RevealerTransitionType.SLIDE_DOWN, + reveal_child = false + }; + + var popover_box = new Gtk.Box (VERTICAL, 5) { + margin_start = 10, + margin_end = 10, + margin_top = 10, + margin_bottom = 5 + }; + + popover_box.append (search_box); + popover_box.append (revealer_not_found); var popover = new Gtk.Popover () { - child = search_box + child = popover_box }; search_menu.popover = popover; - append (search_menu); + child = search_menu; + // We use a keypress controller to capture Shift+Enter + var keypress_controller = new Gtk.EventControllerKey (); + entry_search.add_controller (keypress_controller); - /* ---------------- CONNECTS AND BINDS ---------------- */ + /* ---------------- CONNECTS AND BINDS ---------------- */ + keypress_controller.key_pressed.connect (on_key_press_event); entry_search.changed.connect (on_entry_changed); entry_search.icon_release.connect (on_clear_clicked); @@ -95,8 +117,12 @@ public class SearchButton : Granite.Bin { next.clicked.connect (() => {search_text (true);}); entry_search.activate.connect (() => {search_text (true);}); - popover.show.connect (() => {entry_search.grab_focus ();}); + + var settings = new GLib.Settings ("io.github.wpkelso.slate"); + settings.bind ("match-case", + toggle_match, "active", + GLib.SettingsBindFlags.DEFAULT); } private void on_entry_changed () { @@ -113,37 +139,87 @@ public class SearchButton : Granite.Bin { } private void search_text (bool? forward = true) { - print ("\nSearch start"); - - Gtk.TextIter start_selection, end_selection; - buffer.get_selection_bounds (out start_selection, out end_selection); - - print ("\nbounds start"); + if (entry_search.text == "") { + return; + }; Gtk.TextIter start_buffer, end_buffer; buffer.get_bounds (out start_buffer, out end_buffer); - Gtk.TextIter match_start, match_end; - bool found_match; - var text = entry_search.text; + // Selection_bounds can leave the variables untouched, which can lead to a crash + Gtk.TextIter start_selection = start_buffer; + Gtk.TextIter end_selection = start_buffer; + buffer.get_selection_bounds (out start_selection, out end_selection); + + Gtk.TextIter match_start = start_selection; + Gtk.TextIter match_end = end_selection; + bool found_match = false; if (forward) { - print ("\nfw"); - found_match = end_selection.forward_search (text, flags, - out match_start, out match_end, null); + + // If the cursor is at the end (like on app start or when the user is typing), we may want to search from the start + // Gated by Forward so the user can still go backward from the end if they want to + if (start_selection.is_end ()) { + buffer.place_cursor (start_buffer); + start_selection = start_buffer; + end_selection = start_buffer; + } + + //We have to check quick n' dirty beforehand because forward/backward_search prefers to crash the app than return false + //Also we have to account checking depending on case sensitiveness + //TODO: Fix this workaround + var remaining_text = buffer.get_slice (end_selection, end_buffer, true); + var text = entry_search.text; + if (flags == Gtk.TextSearchFlags.CASE_INSENSITIVE) { + text = text.casefold (); + remaining_text = remaining_text.casefold (); + } + + if (text in remaining_text) { + found_match = end_selection.forward_search (text, flags, + out match_start, out match_end, end_buffer); + } + } else { + //We have to check quick n' dirty beforehand because forward/backward_search prefers to crash the app than return false + //Also we have to account checking depending on case sensitiveness + //TODO: Fix this workaround + var remaining_text = buffer.get_slice (start_buffer, start_selection, true); + var text = entry_search.text; + if (flags == Gtk.TextSearchFlags.CASE_INSENSITIVE) { + text = text.casefold (); + remaining_text = remaining_text.casefold (); + } - print ("\nbackward"); - found_match = start_selection.backward_search (text, flags, - out match_start, out match_end, null); + if (text in remaining_text) { + found_match = start_selection.backward_search (text, flags, + out match_start, out match_end, start_buffer); + } } - print ("\nFound: " + found_match.to_string () + " at? " + match_start.get_offset ().to_string ()); + debug ("Cursor: " + buffer.cursor_position.to_string ()); + debug ("Start and end selection: " + start_selection.get_offset ().to_string () + "|" + end_selection.get_offset ().to_string ()); + debug ("Found: " + found_match.to_string () + " at? " + match_start.get_offset ().to_string () + "|" + match_end.get_offset ().to_string ()); if (found_match) { buffer.select_range (match_start, match_end); textview.scroll_to_iter (match_start, 0, false, 0.5f, 0.5f); + revealer_not_found.reveal_child = false; + + } else { + revealer_not_found.reveal_child = true; + } + } + + public bool on_key_press_event (uint keyval, uint keycode, Gdk.ModifierType state) { + if (keyval == Gdk.Key.Return && state == Gdk.ModifierType.SHIFT_MASK) { + search_text (false); + } + + if (keyval == Gdk.Key.l && state == Gdk.ModifierType.CONTROL_MASK) { + on_clear_clicked (); } + return Gdk.EVENT_PROPAGATE; } }