diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/gridview.c | 38 | ||||
| -rw-r--r-- | src/navigator.c | 65 | ||||
| -rw-r--r-- | src/navigator.h | 5 | ||||
| -rw-r--r-- | src/shortcuts.c | 1 | ||||
| -rw-r--r-- | src/window.c | 40 |
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}, |
