summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/gridview.c38
-rw-r--r--src/navigator.c65
-rw-r--r--src/navigator.h5
-rw-r--r--src/shortcuts.c1
-rw-r--r--src/window.c40
5 files changed, 139 insertions, 10 deletions
diff --git a/src/gridview.c b/src/gridview.c
index 69a62c1..7d7cc62 100644
--- a/src/gridview.c
+++ b/src/gridview.c
@@ -196,6 +196,37 @@ _on_flow_key(GtkEventControllerKey *p_key, guint u_kv, guint u_kc,
return (FALSE);
}
+/* Middle-click on a grid cell toggles its mark (pointer-accessible
+ * marks): select the cell, sync navigator.current to it, then dispatch the
+ * shared "win.mark" action (the window updates the header + badge exactly
+ * as `v` does). */
+static void
+_on_flow_middle_pressed(GtkGesture *p_g, gint i_n_press, gdouble d_x,
+ gdouble d_y, gpointer p_data) {
+ (void)i_n_press;
+ if (gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(p_g)) !=
+ GDK_BUTTON_MIDDLE) {
+ return;
+ }
+ GgazeGrid *p_grid = GGAZE_GRID(p_data);
+ if (p_grid->p_nav == NULL || p_grid->p_flow == NULL) {
+ return;
+ }
+ GtkFlowBoxChild *p_child = gtk_flow_box_get_child_at_pos(
+ GTK_FLOW_BOX(p_grid->p_flow), (gint)d_x, (gint)d_y);
+ if (p_child == NULL) {
+ return;
+ }
+ /* Select + sync navigator.current so win.mark targets this cell, then
+ * dispatch the shared toggle action (handles badge + header update). */
+ gtk_flow_box_select_child(GTK_FLOW_BOX(p_grid->p_flow), p_child);
+ GFile *p_file = (GFile *)g_object_get_data(G_OBJECT(p_child), "file");
+ if (p_file != NULL) {
+ navigator_set_current_file(p_grid->p_nav, p_file);
+ }
+ gtk_widget_activate_action(GTK_WIDGET(p_grid), "win.mark", NULL);
+}
+
/* --- refresh / rebuild --------------------------------------------------- */
static void
@@ -476,6 +507,13 @@ ggaze_grid_init(GgazeGrid *p_grid) {
GtkEventController *p_key = gtk_event_controller_key_new();
g_signal_connect(p_key, "key-pressed", G_CALLBACK(_on_flow_key), p_grid);
gtk_widget_add_controller(p_grid->p_flow, p_key);
+ /* Middle-click on a cell toggles its mark (pointer-accessible marks). */
+ GtkGesture *p_middle = gtk_gesture_click_new();
+ gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(p_middle),
+ GDK_BUTTON_MIDDLE);
+ g_signal_connect(p_middle, "pressed", G_CALLBACK(_on_flow_middle_pressed),
+ p_grid);
+ gtk_widget_add_controller(p_grid->p_flow, GTK_EVENT_CONTROLLER(p_middle));
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(p_grid->p_scrolled),
p_grid->p_flow);
}
diff --git a/src/navigator.c b/src/navigator.c
index 8752e2d..38f02b1 100644
--- a/src/navigator.c
+++ b/src/navigator.c
@@ -34,15 +34,16 @@ typedef struct {
} Entry;
struct _Navigator {
- GObject parent_instance;
- GFile *p_dir; /* owned */
- GPtrArray *p_files; /* GFile* (owned refs), sorted/filtered */
- gint i_current; /* -1 if empty */
- GgazeSort e_sort;
- gboolean b_wrap;
- gboolean b_hide_raw;
- GHashTable *p_marks; /* GFile* (owned refs) -> presence */
- GHashTable *p_removed; /* GFile* (owned refs): trashed/deleted, dimmed */
+ GObject parent_instance;
+ GFile *p_dir; /* owned */
+ GPtrArray *p_files; /* GFile* (owned refs), sorted/filtered */
+ gint i_current; /* -1 if empty */
+ GgazeSort e_sort;
+ gboolean b_wrap;
+ gboolean b_hide_raw;
+ GHashTable *p_marks; /* GFile* (owned refs) -> presence */
+ GHashTable *p_removed; /* GFile* (owned refs): trashed/deleted, dimmed */
+ GFile *p_last_mark; /* anchor for `V` range-mark (owned ref, NULL=none) */
GFileMonitor *p_monitor;
guint u_debounce_ms;
guint u_debounce_id; /* 0 = none pending */
@@ -277,6 +278,13 @@ _relist(Navigator *p_nav) {
g_hash_table_iter_remove(&iter);
}
}
+ /* The range anchor must also be a live marked file: drop it if its file
+ * left the listing (external delete / rescan), so `V` no-ops instead of
+ * range-marking from a stale path. */
+ if (p_nav->p_last_mark != NULL &&
+ _find_index_by_file(p_nav, p_nav->p_last_mark) < 0) {
+ g_clear_object(&p_nav->p_last_mark);
+ }
/* Preserve removed (dimmed) items: keep them in the listing even if absent
* from disk (trashed/deleted this session); un-remove any that reappeared.
@@ -359,6 +367,7 @@ navigator_dispose(GObject *p_obj) {
g_clear_pointer(&p_nav->p_removed, g_hash_table_unref);
g_clear_pointer(&p_nav->p_files, g_ptr_array_unref);
g_clear_object(&p_nav->p_dir);
+ g_clear_object(&p_nav->p_last_mark);
G_OBJECT_CLASS(navigator_parent_class)->dispose(p_obj);
}
@@ -386,6 +395,7 @@ navigator_init(Navigator *p_nav) {
p_nav->b_wrap = TRUE;
p_nav->b_hide_raw = TRUE;
p_nav->u_debounce_ms = GGAZE_DEFAULT_DEBOUNCE_MS;
+ p_nav->p_last_mark = NULL;
}
/* --- public API ---------------------------------------------------------- */
@@ -476,8 +486,22 @@ void
navigator_mark_removed(Navigator *p_nav, GFile *p_file) {
g_return_if_fail(GGAZE_IS_NAVIGATOR(p_nav));
g_return_if_fail(G_IS_FILE(p_file));
- if (!g_hash_table_contains(p_nav->p_removed, p_file)) {
+ /* Decision Q: marks clear on trash/delete. Drop this file's mark (and
+ * the range anchor if it was the anchor) without touching unrelated
+ * marks, so mark-based ops never target paths that no longer exist. */
+ gboolean b_mark_changed = g_hash_table_remove(p_nav->p_marks, p_file);
+ if (p_nav->p_last_mark != NULL && g_file_equal(p_nav->p_last_mark, p_file)) {
+ g_clear_object(&p_nav->p_last_mark);
+ b_mark_changed = TRUE;
+ }
+ gboolean b_removed_added = !g_hash_table_contains(p_nav->p_removed, p_file);
+ if (b_removed_added) {
g_hash_table_add(p_nav->p_removed, g_object_ref(p_file));
+ }
+ /* Emit when the removed set or the marks actually changed, so grid badges
+ * and the header never go stale (e.g. re-marking a file already in
+ * p_removed, then removing it again). */
+ if (b_removed_added || b_mark_changed) {
_emit_changed(p_nav);
}
}
@@ -630,8 +654,16 @@ navigator_toggle_mark(Navigator *p_nav, GFile *p_file) {
g_return_if_fail(G_IS_FILE(p_file));
if (g_hash_table_contains(p_nav->p_marks, p_file)) {
g_hash_table_remove(p_nav->p_marks, p_file);
+ /* If the unmarked file was the range anchor, drop it: a subsequent `V`
+ * has no meaningful anchor once the anchor itself is unmarked. */
+ if (p_nav->p_last_mark != NULL &&
+ g_file_equal(p_nav->p_last_mark, p_file)) {
+ g_clear_object(&p_nav->p_last_mark);
+ }
} else {
g_hash_table_add(p_nav->p_marks, g_object_ref(p_file));
+ /* Remember this as the anchor for a later `V` range-mark. */
+ g_set_object(&p_nav->p_last_mark, p_file);
}
}
@@ -675,6 +707,7 @@ void
navigator_clear_marks(Navigator *p_nav) {
g_return_if_fail(GGAZE_IS_NAVIGATOR(p_nav));
g_hash_table_remove_all(p_nav->p_marks);
+ g_clear_object(&p_nav->p_last_mark);
_emit_changed(p_nav);
}
@@ -696,6 +729,15 @@ navigator_get_marks(Navigator *p_nav) {
return (g_list_reverse(p_out));
}
+/* The anchor set by the last `v` toggle-on, used by `V` range-mark. Borrowed
+ * (owned by the navigator); NULL if no mark has been toggled on yet, or if the
+ * anchor was unmarked / removed / cleared. Survives re-sort (path-based). */
+GFile *
+navigator_get_last_mark(Navigator *p_nav) {
+ g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), NULL);
+ return (p_nav->p_last_mark);
+}
+
/* --- mutations ----------------------------------------------------------- */
void
@@ -715,6 +757,9 @@ navigator_remove(Navigator *p_nav, GFile *p_file) {
}
/* Clear its mark first (decision Q) while the file ref is still valid. */
g_hash_table_remove(p_nav->p_marks, p_file);
+ if (p_nav->p_last_mark != NULL && g_file_equal(p_nav->p_last_mark, p_file)) {
+ g_clear_object(&p_nav->p_last_mark);
+ }
g_ptr_array_remove_index(p_nav->p_files, (guint)i);
if (p_nav->p_files->len == 0) {
p_nav->i_current = -1;
diff --git a/src/navigator.h b/src/navigator.h
index db781ab..3946a21 100644
--- a/src/navigator.h
+++ b/src/navigator.h
@@ -78,6 +78,11 @@ void navigator_clear_marks(Navigator *p_nav);
guint navigator_get_mark_count(Navigator *p_nav);
GList *navigator_get_marks(Navigator *p_nav); /* (transfer full) GFile* refs */
+/* Borrowed pointer to the last file marked via a `v` toggle-on - the anchor
+ * for `V` range-mark (NULL if none / cleared). Survives re-sort (path-based).
+ */
+GFile *navigator_get_last_mark(Navigator *p_nav); /* (transfer none) */
+
/* --- mutations ---------------------------------------------------------- */
/* Re-read the directory; if the current file is gone, fall back to nearest;
* emit "changed". */
diff --git a/src/shortcuts.c b/src/shortcuts.c
index 0d86bb4..cfecc95 100644
--- a/src/shortcuts.c
+++ b/src/shortcuts.c
@@ -34,6 +34,7 @@ static const ShortcutEntry SHORTCUTS[] = {
{GDK_KEY_u, 0, "win.undo"},
{GDK_KEY_t, 0, "win.toggle-view"},
{GDK_KEY_v, 0, "win.mark"},
+ {GDK_KEY_V, GDK_SHIFT_MASK, "win.mark-range"},
{GDK_KEY_a, GDK_CONTROL_MASK, "win.mark-all"},
{GDK_KEY_a, 0, "win.enhance"},
{GDK_KEY_s, 0, "win.enhance-save"},
diff --git a/src/window.c b/src/window.c
index 65ef229..26beb57 100644
--- a/src/window.c
+++ b/src/window.c
@@ -462,6 +462,38 @@ _action_mark_all(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
_update_header(p_win);
}
+/* `V` range-mark: mark every file from the last `v`-toggled mark (the anchor)
+ * to the highlighted grid cell / current large-view image, inclusive. No-op
+ * if no mark has been toggled on yet (no anchor). Emits "changed" so grid
+ * badges and the header mark count refresh. */
+static void
+_action_mark_range(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
+ (void)p_a;
+ (void)p_v;
+ GgazeWindow *p_win = GGAZE_WINDOW(p_data);
+ if (p_win->p_nav == NULL) {
+ return;
+ }
+ GFile *p_anchor = navigator_get_last_mark(p_win->p_nav);
+ if (p_anchor == NULL) {
+ return;
+ }
+ GFile *p_target = NULL;
+ const char *c_cur =
+ gtk_stack_get_visible_child_name(GTK_STACK(p_win->p_stack));
+ if (g_strcmp0(c_cur, "grid") == 0 && p_win->p_grid != NULL) {
+ p_target = ggaze_grid_get_selected_file(p_win->p_grid);
+ }
+ if (p_target == NULL) {
+ p_target = navigator_get_current(p_win->p_nav);
+ }
+ if (p_target == NULL) {
+ return;
+ }
+ navigator_mark_range(p_win->p_nav, p_anchor, p_target);
+ _update_header(p_win);
+}
+
/* GtkBuilder UI for the shortcuts overlay (?). Accel strings use gtk
* accelerator syntax: "h Left" means h OR Left triggers it. */
static const char *SHORTCUTS_UI =
@@ -557,6 +589,13 @@ static const char *SHORTCUTS_UI =
" </child>\n"
" <child>\n"
" <object class=\"GtkShortcutsShortcut\">\n"
+ " <property name=\"accelerator\">Shift+V</property>\n"
+ " <property name=\"title\">Range-mark from last mark "
+ "to current</property>\n"
+ " </object>\n"
+ " </child>\n"
+ " <child>\n"
+ " <object class=\"GtkShortcutsShortcut\">\n"
" <property name=\"accelerator\">Ctrl+a</property>\n"
" <property name=\"title\">Mark all</property>\n"
" </object>\n"
@@ -1083,6 +1122,7 @@ static const GActionEntry ACTIONS[] = {
{.name = "toggle-view", .activate = _action_toggle_view},
{.name = "mark", .activate = _action_mark},
{.name = "mark-all", .activate = _action_mark_all},
+ {.name = "mark-range", .activate = _action_mark_range},
{.name = "shortcuts", .activate = _action_shortcuts},
{.name = "enhance-1", .activate = _action_enhance_n},
{.name = "enhance-2", .activate = _action_enhance_n},