Skip to content
Merged
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
6 changes: 6 additions & 0 deletions data/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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'
)
10 changes: 10 additions & 0 deletions data/slate.gschema.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema path="/io/github/wpkelso/slate/" id="io.github.wpkelso.slate">
<key name="match-case" type="b">
<default>false</default>
<summary>Whether to match case in search</summary>
<description>Used by the search function to remember whether the user wanted to match case</description>
</key>
</schema>
</schemalist>
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ subdir('data')
subdir('po')

gnome.post_install(
glib_compile_schemas: true,
gtk_update_icon_cache: true,
update_desktop_database: true
)
3 changes: 2 additions & 1 deletion po/POTFILES
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
src/Application.vala
src/Window.vala
src/Utils.vala
src/Utils.vala
src/SearchButton.vala
12 changes: 12 additions & 0 deletions src/Application.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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", {"<Control>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.search_menu.activate ();
});

SimpleAction quit_action = new SimpleAction ("quit", null);
set_accels_for_action ("app.quit", {"<Control>q"});
add_action (quit_action);
Expand Down
225 changes: 225 additions & 0 deletions src/SearchButton.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2025 William Kelso <wpkelso@posteo.net>
*/

public class SearchButton : Granite.Bin {
public Gtk.TextView textview {get; construct;}
public Gtk.TextBuffer buffer {get; construct;}

public Gtk.MenuButton search_menu;
Gtk.Entry entry_search;
Gtk.ToggleButton toggle_match;
Gtk.Button previous;
Gtk.Button next;
Gtk.Revealer revealer_not_found;

Gtk.TextSearchFlags flags {
get {
if (toggle_match.active) {
return Gtk.TextSearchFlags.VISIBLE_ONLY;
}
return Gtk.TextSearchFlags.CASE_INSENSITIVE;
}
set {
toggle_match.active = (value == Gtk.TextSearchFlags.VISIBLE_ONLY);
}
}

public SearchButton (Gtk.TextView textview) {
Object (textview: textview,
buffer: textview.buffer);
}

construct {
search_menu = new Gtk.MenuButton () {
icon_name = "system-search",
tooltip_markup = Granite.markup_accel_tooltip (
{"<Control>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"),
primary_icon_name = "system-search-symbolic",
secondary_icon_tooltip_markup = Granite.markup_accel_tooltip (
{"<Control>l"},
_("Clear text")
)
};

previous = new Gtk.Button.from_icon_name ("go-up-symbolic") {
tooltip_markup = Granite.markup_accel_tooltip (
{"<Shift>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")
};

search_box.append (entry_search);
search_box.append (previous);
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
};

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.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);});

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 () {
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) {
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;
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) {

// 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 ();
}

if (text in remaining_text) {
found_match = start_selection.backward_search (text, flags,
out match_start, out match_end, start_buffer);
}
}

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;
}
}
10 changes: 10 additions & 0 deletions src/Window.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -65,6 +66,13 @@ public class AppWindow : Gtk.Window {
buf = text_view.buffer;
buf.text = "";

// 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 () {
child = text_view,
Expand All @@ -83,6 +91,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
Expand Down
1 change: 1 addition & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ sources += files(
'Window.vala',
'Application.vala',
'Utils.vala',
'SearchButton.vala',
)