| 1 | ! GTK4 Application Module for Sniffly |
| 2 | ! Handles application initialization and main window setup |
| 3 | module gtk_app |
| 4 | use, intrinsic :: iso_c_binding |
| 5 | use gtk, only: gtk_init, gtk_application_new, gtk_application_window_new, & |
| 6 | gtk_window_set_title, gtk_window_set_default_size, & |
| 7 | gtk_window_present, G_APPLICATION_DEFAULT_FLAGS, & |
| 8 | gtk_application_get_active_window, gtk_window_destroy, & |
| 9 | gtk_window_set_child, g_signal_connect, & |
| 10 | gtk_box_new, gtk_box_append, gtk_box_remove, GTK_ORIENTATION_VERTICAL, & |
| 11 | GTK_ORIENTATION_HORIZONTAL, gtk_button_new_with_label, & |
| 12 | gtk_widget_set_hexpand, gtk_widget_set_vexpand, gtk_widget_get_first_child, & |
| 13 | gtk_label_new, gtk_label_set_text, gtk_widget_set_halign, & |
| 14 | GTK_ALIGN_START, gtk_progress_bar_new, gtk_progress_bar_set_fraction, & |
| 15 | gtk_progress_bar_set_text, gtk_progress_bar_set_show_text, & |
| 16 | gtk_widget_set_visible, gtk_widget_set_sensitive, & |
| 17 | gtk_button_new, gtk_button_set_icon_name, gtk_widget_set_tooltip_text, & |
| 18 | gtk_entry_new, gtk_entry_buffer_set_text, gtk_entry_get_buffer, & |
| 19 | gtk_editable_set_editable, gtk_editable_get_text, & |
| 20 | gtk_entry_set_placeholder_text, gtk_widget_add_css_class, & |
| 21 | gtk_widget_remove_css_class, gtk_css_provider_new, & |
| 22 | gtk_css_provider_load_from_string, gtk_style_context_add_provider_for_display |
| 23 | use gdk, only: gdk_display_get_default, gdk_display_get_clipboard, gdk_clipboard_set_text |
| 24 | use g, only: g_application_run, g_idle_add, g_timeout_add_seconds_once |
| 25 | use treemap_widget, only: create_treemap_widget, set_scan_path, register_navigation_callback, & |
| 26 | register_key_handler, register_quit_callback, register_delete_callback, & |
| 27 | register_refresh_callback, register_selection_callback, mark_initial_scan_complete, & |
| 28 | has_selection, get_selected_node_path |
| 29 | use breadcrumb_widget, only: create_breadcrumb_widget, update_breadcrumb_cache, & |
| 30 | set_navigation_callback, get_previous_breadcrumb_path, & |
| 31 | clear_previous_breadcrumb_path |
| 32 | use treemap_renderer, only: register_progress_callback, scan_directory, set_redraw_widget, & |
| 33 | register_scan_completion_callback, set_renderer_state_from_tab |
| 34 | use tab_manager, only: tab_state, init_tab_manager, create_tab, get_active_tab, & |
| 35 | switch_to_tab, num_tabs, active_tab_index, get_path_basename |
| 36 | use tab_widget, only: create_tab_bar, refresh_tab_bar, register_tab_switch_callback, & |
| 37 | update_tab_visual_states |
| 38 | implicit none |
| 39 | private |
| 40 | |
| 41 | public :: sniffly_app_run, sniffly_app_quit, sniffly_set_scan_path, & |
| 42 | sniffly_update_status, sniffly_show_error, breadcrumb_callback, & |
| 43 | sniffly_update_progress, sniffly_show_progress, sniffly_hide_progress, & |
| 44 | sniffly_update_status_bar_stats, get_forward_path, update_ui_for_active_tab |
| 45 | |
| 46 | ! Application constants |
| 47 | character(len=*), parameter :: APP_ID = "org.fortrangoingonforty.sniffly" |
| 48 | character(len=*), parameter :: APP_TITLE = "Sniffly - Disk Space Analyzer" |
| 49 | integer, parameter :: DEFAULT_WIDTH = 1024 |
| 50 | integer, parameter :: DEFAULT_HEIGHT = 768 |
| 51 | |
| 52 | ! Global application pointer (will be set in activate callback) |
| 53 | type(c_ptr), save :: app_ptr = c_null_ptr |
| 54 | type(c_ptr), save :: main_window_ptr = c_null_ptr |
| 55 | type(c_ptr), save :: status_label_ptr = c_null_ptr |
| 56 | type(c_ptr), save :: progress_bar_ptr = c_null_ptr |
| 57 | type(c_ptr), save :: path_entry_ptr = c_null_ptr |
| 58 | |
| 59 | ! Shutdown flag - set when app is closing to prevent widget access |
| 60 | logical, save :: app_is_shutting_down = .false. |
| 61 | |
| 62 | ! Global scan path (can be set via command line) |
| 63 | character(len=512), save :: global_scan_path = "" |
| 64 | |
| 65 | ! Scan path for async initial scan |
| 66 | character(len=512), save :: pending_scan_path = "" |
| 67 | |
| 68 | ! Navigation history for Back/Forward buttons |
| 69 | integer, parameter :: MAX_HISTORY = 50 |
| 70 | character(len=512), dimension(MAX_HISTORY), save :: nav_history |
| 71 | integer, save :: nav_history_count = 0 |
| 72 | integer, save :: nav_history_pos = 0 ! Current position in history (0 = no history) |
| 73 | logical, save :: navigating_history = .false. ! Flag: are we using back/forward? |
| 74 | |
| 75 | ! Button pointers for enabling/disabling |
| 76 | type(c_ptr), save :: back_btn_ptr = c_null_ptr |
| 77 | type(c_ptr), save :: forward_btn_ptr = c_null_ptr |
| 78 | type(c_ptr), save :: cancel_scan_btn_ptr = c_null_ptr |
| 79 | |
| 80 | ! Selection-dependent button pointers |
| 81 | type(c_ptr), save :: info_btn_ptr = c_null_ptr |
| 82 | type(c_ptr), save :: copy_path_btn_ptr = c_null_ptr |
| 83 | type(c_ptr), save :: open_finder_btn_ptr = c_null_ptr |
| 84 | type(c_ptr), save :: delete_btn_ptr = c_null_ptr |
| 85 | |
| 86 | ! Open directory button pointer (for pulsing on empty tabs) |
| 87 | type(c_ptr), save :: open_dir_btn_ptr = c_null_ptr |
| 88 | |
| 89 | ! Drawing area pointer (for redrawing treemap) |
| 90 | type(c_ptr), save :: drawing_area_ptr = c_null_ptr |
| 91 | |
| 92 | ! Pending navigation state for synthetic paths |
| 93 | logical, save :: pending_synthetic_nav = .false. |
| 94 | character(len=512), save :: pending_synthetic_child_name = "" |
| 95 | integer, save :: synthetic_nav_attempts = 0 ! Count attempts to avoid infinite loops |
| 96 | |
| 97 | contains |
| 98 | |
| 99 | ! Run the Sniffly GTK application |
| 100 | function sniffly_app_run() result(status) |
| 101 | integer :: status |
| 102 | integer(c_int) :: c_status |
| 103 | |
| 104 | ! Initialize GTK |
| 105 | call gtk_init() |
| 106 | |
| 107 | ! Create application |
| 108 | app_ptr = gtk_application_new(APP_ID//c_null_char, & |
| 109 | G_APPLICATION_DEFAULT_FLAGS) |
| 110 | |
| 111 | if (.not. c_associated(app_ptr)) then |
| 112 | print *, "ERROR: Failed to create GTK application" |
| 113 | status = 1 |
| 114 | return |
| 115 | end if |
| 116 | |
| 117 | ! Connect activate signal (called when app starts) |
| 118 | call g_signal_connect(app_ptr, "activate"//c_null_char, & |
| 119 | c_funloc(on_activate), c_null_ptr) |
| 120 | |
| 121 | ! Run the application (enters main loop) |
| 122 | c_status = g_application_run(app_ptr, 0, [c_null_ptr]) |
| 123 | status = int(c_status) |
| 124 | end function sniffly_app_run |
| 125 | |
| 126 | ! Quit the application |
| 127 | subroutine sniffly_app_quit() |
| 128 | use progressive_scanner, only: stop_progressive_scan |
| 129 | ! Set shutdown flag first to prevent callbacks from accessing widgets |
| 130 | app_is_shutting_down = .true. |
| 131 | |
| 132 | ! Stop any active scans before destroying window |
| 133 | call stop_progressive_scan() |
| 134 | |
| 135 | if (c_associated(main_window_ptr)) then |
| 136 | call gtk_window_destroy(main_window_ptr) |
| 137 | end if |
| 138 | |
| 139 | ! Nullify all widget pointers to prevent access after destruction |
| 140 | main_window_ptr = c_null_ptr |
| 141 | status_label_ptr = c_null_ptr |
| 142 | progress_bar_ptr = c_null_ptr |
| 143 | path_entry_ptr = c_null_ptr |
| 144 | back_btn_ptr = c_null_ptr |
| 145 | forward_btn_ptr = c_null_ptr |
| 146 | cancel_scan_btn_ptr = c_null_ptr |
| 147 | open_dir_btn_ptr = c_null_ptr |
| 148 | end subroutine sniffly_app_quit |
| 149 | |
| 150 | ! Set the directory path to scan (call before sniffly_app_run) |
| 151 | subroutine sniffly_set_scan_path(path) |
| 152 | character(len=*), intent(in) :: path |
| 153 | ! Store in legacy global for now - will be used to create first tab in on_activate |
| 154 | global_scan_path = trim(path) |
| 155 | print *, "Initial scan path set to: ", trim(global_scan_path) |
| 156 | end subroutine sniffly_set_scan_path |
| 157 | |
| 158 | ! Callback when window close button (X) is clicked |
| 159 | function on_window_close_request(window, user_data) bind(c) result(stop_propagation) |
| 160 | use progressive_scanner, only: stop_progressive_scan |
| 161 | type(c_ptr), value :: window, user_data |
| 162 | integer(c_int) :: stop_propagation |
| 163 | |
| 164 | print *, "Window close button clicked - stopping scan and cleaning up" |
| 165 | |
| 166 | ! Set shutdown flag first to prevent callbacks from accessing widgets |
| 167 | app_is_shutting_down = .true. |
| 168 | |
| 169 | ! Stop any active scans |
| 170 | call stop_progressive_scan() |
| 171 | |
| 172 | ! Nullify all widget pointers to prevent access after destruction |
| 173 | ! (The window will be destroyed by GTK after we return FALSE) |
| 174 | status_label_ptr = c_null_ptr |
| 175 | progress_bar_ptr = c_null_ptr |
| 176 | path_entry_ptr = c_null_ptr |
| 177 | back_btn_ptr = c_null_ptr |
| 178 | forward_btn_ptr = c_null_ptr |
| 179 | open_dir_btn_ptr = c_null_ptr |
| 180 | main_window_ptr = c_null_ptr |
| 181 | |
| 182 | ! Return FALSE (0) to allow the window to close |
| 183 | stop_propagation = 0_c_int |
| 184 | end function on_window_close_request |
| 185 | |
| 186 | ! Callback when application activates (startup) |
| 187 | subroutine on_activate(app, user_data) bind(c) |
| 188 | type(c_ptr), value :: app, user_data |
| 189 | type(c_ptr) :: drawing_area, main_box, toolbar, open_dir_btn, scan_btn, cancel_scan_btn, back_btn, forward_btn, up_btn, open_finder_btn, copy_path_btn, info_btn, toggle_dotfiles_btn, toggle_ext_btn, toggle_render_btn, delete_btn, status_bar, breadcrumb_widget, breadcrumb_row, tab_bar |
| 190 | character(len=512) :: scan_path |
| 191 | integer(c_int) :: idle_id |
| 192 | integer :: first_tab_index |
| 193 | |
| 194 | ! Initialize tab manager |
| 195 | call init_tab_manager() |
| 196 | print *, "Tab manager initialized" |
| 197 | |
| 198 | ! Create main window |
| 199 | main_window_ptr = gtk_application_window_new(app) |
| 200 | |
| 201 | if (.not. c_associated(main_window_ptr)) then |
| 202 | print *, "ERROR: Failed to create main window" |
| 203 | return |
| 204 | end if |
| 205 | |
| 206 | ! Set up window properties |
| 207 | call gtk_window_set_title(main_window_ptr, APP_TITLE//c_null_char) |
| 208 | call gtk_window_set_default_size(main_window_ptr, & |
| 209 | int(DEFAULT_WIDTH, c_int), & |
| 210 | int(DEFAULT_HEIGHT, c_int)) |
| 211 | |
| 212 | ! Connect close-request signal to handle window close button (X) |
| 213 | call g_signal_connect(main_window_ptr, "close-request"//c_null_char, & |
| 214 | c_funloc(on_window_close_request), c_null_ptr) |
| 215 | |
| 216 | ! Use global scan path or home directory |
| 217 | if (len_trim(global_scan_path) > 0) then |
| 218 | scan_path = global_scan_path |
| 219 | print *, "Using specified directory: ", trim(scan_path) |
| 220 | else |
| 221 | ! No directory specified - use home directory and prompt user |
| 222 | scan_path = get_home_directory() |
| 223 | print *, "No directory specified, using home directory: ", trim(scan_path) |
| 224 | print *, "Click the folder icon to select a different directory" |
| 225 | end if |
| 226 | |
| 227 | ! Create first tab with initial scan path |
| 228 | first_tab_index = create_tab(scan_path) |
| 229 | if (first_tab_index < 0) then |
| 230 | print *, "ERROR: Failed to create initial tab" |
| 231 | return |
| 232 | end if |
| 233 | print *, "Created initial tab ", first_tab_index, " for: ", trim(scan_path) |
| 234 | |
| 235 | ! Create main vertical box (toolbar + treemap) |
| 236 | main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0_c_int) |
| 237 | |
| 238 | ! Create toolbar (horizontal box) |
| 239 | toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5_c_int) |
| 240 | |
| 241 | ! Create Open Directory button with folder icon |
| 242 | open_dir_btn = gtk_button_new() |
| 243 | open_dir_btn_ptr = open_dir_btn ! Store for later access (pulsing on empty tabs) |
| 244 | call gtk_button_set_icon_name(open_dir_btn, "folder-open"//c_null_char) |
| 245 | call gtk_widget_set_tooltip_text(open_dir_btn, "Open Directory (Ctrl+O)"//c_null_char) |
| 246 | call g_signal_connect(open_dir_btn, "clicked"//c_null_char, & |
| 247 | c_funloc(on_open_dir_clicked), c_null_ptr) |
| 248 | call gtk_box_append(toolbar, open_dir_btn) |
| 249 | |
| 250 | ! Create path display entry (read-only) |
| 251 | path_entry_ptr = gtk_entry_new() |
| 252 | call gtk_editable_set_editable(path_entry_ptr, 0_c_int) ! Make read-only |
| 253 | call gtk_widget_set_hexpand(path_entry_ptr, 1_c_int) ! Expand to fill space |
| 254 | call gtk_widget_set_tooltip_text(path_entry_ptr, "Current scan path"//c_null_char) |
| 255 | call gtk_box_append(toolbar, path_entry_ptr) |
| 256 | |
| 257 | ! Create Scan button with refresh icon |
| 258 | scan_btn = gtk_button_new() |
| 259 | call gtk_button_set_icon_name(scan_btn, "view-refresh"//c_null_char) |
| 260 | call gtk_widget_set_tooltip_text(scan_btn, "Rescan Current Directory"//c_null_char) |
| 261 | call g_signal_connect(scan_btn, "clicked"//c_null_char, & |
| 262 | c_funloc(on_scan_clicked), c_null_ptr) |
| 263 | call gtk_box_append(toolbar, scan_btn) |
| 264 | |
| 265 | ! Create Cancel Scan button with X icon |
| 266 | cancel_scan_btn = gtk_button_new() |
| 267 | call gtk_button_set_icon_name(cancel_scan_btn, "window-close"//c_null_char) |
| 268 | call gtk_widget_set_tooltip_text(cancel_scan_btn, "Cancel Scan"//c_null_char) |
| 269 | call g_signal_connect(cancel_scan_btn, "clicked"//c_null_char, & |
| 270 | c_funloc(on_cancel_scan_clicked), c_null_ptr) |
| 271 | call gtk_box_append(toolbar, cancel_scan_btn) |
| 272 | cancel_scan_btn_ptr = cancel_scan_btn ! Store for enabling/disabling |
| 273 | |
| 274 | ! Create Back button (navigate to previous directory in history) |
| 275 | back_btn = gtk_button_new() |
| 276 | call gtk_button_set_icon_name(back_btn, "go-previous"//c_null_char) |
| 277 | call gtk_widget_set_tooltip_text(back_btn, "Navigate Back"//c_null_char) |
| 278 | call g_signal_connect(back_btn, "clicked"//c_null_char, & |
| 279 | c_funloc(on_back_clicked), c_null_ptr) |
| 280 | call gtk_box_append(toolbar, back_btn) |
| 281 | back_btn_ptr = back_btn ! Store for enabling/disabling |
| 282 | |
| 283 | ! Create Forward button (navigate to next directory in history) |
| 284 | forward_btn = gtk_button_new() |
| 285 | call gtk_button_set_icon_name(forward_btn, "go-next"//c_null_char) |
| 286 | call gtk_widget_set_tooltip_text(forward_btn, "Navigate Forward"//c_null_char) |
| 287 | call g_signal_connect(forward_btn, "clicked"//c_null_char, & |
| 288 | c_funloc(on_forward_clicked), c_null_ptr) |
| 289 | call gtk_box_append(toolbar, forward_btn) |
| 290 | forward_btn_ptr = forward_btn ! Store for enabling/disabling |
| 291 | |
| 292 | ! Create Up to Parent button (navigate to parent directory) |
| 293 | up_btn = gtk_button_new() |
| 294 | call gtk_button_set_icon_name(up_btn, "go-up"//c_null_char) |
| 295 | call gtk_widget_set_tooltip_text(up_btn, "Navigate to Parent Directory (Backspace)"//c_null_char) |
| 296 | call g_signal_connect(up_btn, "clicked"//c_null_char, & |
| 297 | c_funloc(on_up_clicked), c_null_ptr) |
| 298 | call gtk_box_append(toolbar, up_btn) |
| 299 | |
| 300 | ! Initialize Back/Forward button states (disabled until history exists) |
| 301 | call update_history_buttons() |
| 302 | |
| 303 | ! Initialize Cancel Scan button state (disabled and grey until scan starts) |
| 304 | call update_cancel_scan_button_state() |
| 305 | |
| 306 | ! Initialize selection-dependent button states (disabled until selection exists) |
| 307 | call update_selection_buttons() |
| 308 | |
| 309 | ! Create progress bar (always visible but starts at 0%) |
| 310 | ! Place it in toolbar, expanded to fill remaining space (pushes to right) |
| 311 | progress_bar_ptr = gtk_progress_bar_new() |
| 312 | call gtk_progress_bar_set_show_text(progress_bar_ptr, 1_c_int) ! Show percentage text |
| 313 | call gtk_widget_set_hexpand(progress_bar_ptr, 1_c_int) ! Expand horizontally to fill space |
| 314 | call gtk_progress_bar_set_fraction(progress_bar_ptr, 0.0_c_double) ! Start at 0% |
| 315 | call gtk_box_append(toolbar, progress_bar_ptr) |
| 316 | |
| 317 | ! Create Open in Finder button (floated right after progress bar) |
| 318 | open_finder_btn = gtk_button_new() |
| 319 | call gtk_button_set_icon_name(open_finder_btn, "document-open"//c_null_char) |
| 320 | call gtk_widget_set_tooltip_text(open_finder_btn, "Open in Finder/File Manager"//c_null_char) |
| 321 | call g_signal_connect(open_finder_btn, "clicked"//c_null_char, & |
| 322 | c_funloc(on_open_finder_clicked), c_null_ptr) |
| 323 | call gtk_box_append(toolbar, open_finder_btn) |
| 324 | open_finder_btn_ptr = open_finder_btn ! Store for enabling/disabling |
| 325 | |
| 326 | ! Create Copy Path button |
| 327 | copy_path_btn = gtk_button_new() |
| 328 | call gtk_button_set_icon_name(copy_path_btn, "edit-copy"//c_null_char) |
| 329 | call gtk_widget_set_tooltip_text(copy_path_btn, "Copy Path to Clipboard"//c_null_char) |
| 330 | call g_signal_connect(copy_path_btn, "clicked"//c_null_char, & |
| 331 | c_funloc(on_copy_path_clicked), c_null_ptr) |
| 332 | call gtk_box_append(toolbar, copy_path_btn) |
| 333 | copy_path_btn_ptr = copy_path_btn ! Store for enabling/disabling |
| 334 | |
| 335 | ! Create Properties/Info button (opens macOS Get Info window) |
| 336 | info_btn = gtk_button_new() |
| 337 | call gtk_button_set_icon_name(info_btn, "dialog-information"//c_null_char) |
| 338 | call gtk_widget_set_tooltip_text(info_btn, "Show in Finder Info"//c_null_char) |
| 339 | call g_signal_connect(info_btn, "clicked"//c_null_char, & |
| 340 | c_funloc(on_info_clicked), c_null_ptr) |
| 341 | call gtk_box_append(toolbar, info_btn) |
| 342 | info_btn_ptr = info_btn ! Store for enabling/disabling |
| 343 | |
| 344 | ! View Toggle Buttons (Phase 3 & 5 features) |
| 345 | |
| 346 | ! Toggle Dotfiles button |
| 347 | toggle_dotfiles_btn = gtk_button_new() |
| 348 | call gtk_button_set_icon_name(toggle_dotfiles_btn, "view-reveal-symbolic"//c_null_char) |
| 349 | call gtk_widget_set_tooltip_text(toggle_dotfiles_btn, "Toggle Hidden Files/Dotfiles"//c_null_char) |
| 350 | call g_signal_connect(toggle_dotfiles_btn, "clicked"//c_null_char, & |
| 351 | c_funloc(on_toggle_dotfiles_clicked), c_null_ptr) |
| 352 | call gtk_box_append(toolbar, toggle_dotfiles_btn) |
| 353 | |
| 354 | ! Toggle File Extensions button |
| 355 | toggle_ext_btn = gtk_button_new() |
| 356 | call gtk_button_set_icon_name(toggle_ext_btn, "text-x-generic-symbolic"//c_null_char) |
| 357 | call gtk_widget_set_tooltip_text(toggle_ext_btn, "Toggle File Extensions in Labels"//c_null_char) |
| 358 | call g_signal_connect(toggle_ext_btn, "clicked"//c_null_char, & |
| 359 | c_funloc(on_toggle_extensions_clicked), c_null_ptr) |
| 360 | call gtk_box_append(toolbar, toggle_ext_btn) |
| 361 | |
| 362 | ! Toggle Render Mode button (Flat vs Cushioned) |
| 363 | toggle_render_btn = gtk_button_new() |
| 364 | call gtk_button_set_icon_name(toggle_render_btn, "view-grid-symbolic"//c_null_char) |
| 365 | call gtk_widget_set_tooltip_text(toggle_render_btn, "Toggle Flat/3D Rendering Mode"//c_null_char) |
| 366 | call g_signal_connect(toggle_render_btn, "clicked"//c_null_char, & |
| 367 | c_funloc(on_toggle_render_mode_clicked), c_null_ptr) |
| 368 | call gtk_box_append(toolbar, toggle_render_btn) |
| 369 | |
| 370 | ! Create Delete button |
| 371 | delete_btn = gtk_button_new() |
| 372 | call gtk_button_set_icon_name(delete_btn, "user-trash"//c_null_char) |
| 373 | call gtk_widget_set_tooltip_text(delete_btn, "Delete to Trash (D key)"//c_null_char) |
| 374 | call g_signal_connect(delete_btn, "clicked"//c_null_char, & |
| 375 | c_funloc(on_delete_clicked), c_null_ptr) |
| 376 | call gtk_box_append(toolbar, delete_btn) |
| 377 | delete_btn_ptr = delete_btn ! Store for enabling/disabling |
| 378 | |
| 379 | ! Add toolbar to main box |
| 380 | call gtk_box_append(main_box, toolbar) |
| 381 | |
| 382 | ! Create horizontal box for breadcrumb row (breadcrumb + tab bar) |
| 383 | breadcrumb_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5_c_int) |
| 384 | |
| 385 | ! Create custom Cairo breadcrumb widget |
| 386 | breadcrumb_widget = create_breadcrumb_widget() |
| 387 | if (.not. c_associated(breadcrumb_widget)) then |
| 388 | print *, "ERROR: Failed to create breadcrumb widget" |
| 389 | return |
| 390 | end if |
| 391 | |
| 392 | ! Register navigation callback for breadcrumb |
| 393 | call set_navigation_callback(breadcrumb_callback) |
| 394 | |
| 395 | ! Make breadcrumb expand to fill space (pushes tab bar to right) |
| 396 | call gtk_widget_set_hexpand(breadcrumb_widget, 1_c_int) |
| 397 | |
| 398 | ! Add breadcrumb widget to breadcrumb row |
| 399 | call gtk_box_append(breadcrumb_row, breadcrumb_widget) |
| 400 | |
| 401 | ! Create tab bar (on right side of breadcrumb row) |
| 402 | tab_bar = create_tab_bar() |
| 403 | if (c_associated(tab_bar)) then |
| 404 | call gtk_box_append(breadcrumb_row, tab_bar) |
| 405 | ! Populate with tabs |
| 406 | call refresh_tab_bar() |
| 407 | ! Register callback for tab switching to update UI |
| 408 | call register_tab_switch_callback(update_ui_for_active_tab) |
| 409 | print *, "Tab bar added to breadcrumb row" |
| 410 | else |
| 411 | print *, "ERROR: Failed to create tab bar" |
| 412 | end if |
| 413 | |
| 414 | ! Add breadcrumb row to main box |
| 415 | call gtk_box_append(main_box, breadcrumb_row) |
| 416 | |
| 417 | ! Create treemap drawing area widget |
| 418 | drawing_area = create_treemap_widget() |
| 419 | |
| 420 | if (.not. c_associated(drawing_area)) then |
| 421 | print *, "ERROR: Failed to create treemap widget" |
| 422 | return |
| 423 | end if |
| 424 | |
| 425 | ! Store pointer for later use (e.g., tab switching redraw) |
| 426 | drawing_area_ptr = drawing_area |
| 427 | |
| 428 | ! Make drawing area expand to fill space |
| 429 | call gtk_widget_set_hexpand(drawing_area, 1_c_int) |
| 430 | call gtk_widget_set_vexpand(drawing_area, 1_c_int) |
| 431 | |
| 432 | ! Register widget with renderer for progressive scan redraws |
| 433 | call set_redraw_widget(drawing_area) |
| 434 | |
| 435 | ! Set the scan path |
| 436 | call set_scan_path(scan_path) |
| 437 | |
| 438 | ! Update path entry to show initial scan path |
| 439 | call update_path_entry(scan_path) |
| 440 | |
| 441 | ! Register navigation callback for breadcrumb updates |
| 442 | call register_navigation_callback(breadcrumb_callback) |
| 443 | |
| 444 | ! Register quit callback |
| 445 | call register_quit_callback(quit_callback_wrapper) |
| 446 | |
| 447 | ! Register delete callback |
| 448 | call register_delete_callback(delete_callback_wrapper) |
| 449 | |
| 450 | ! Register force refresh callback |
| 451 | call register_refresh_callback(refresh_callback_wrapper) |
| 452 | |
| 453 | ! Register selection change callback for button state updates |
| 454 | call register_selection_callback(update_selection_buttons) |
| 455 | print *, "Selection callback registered" |
| 456 | |
| 457 | ! Register progress callbacks |
| 458 | call register_progress_callback(sniffly_show_progress, sniffly_hide_progress, & |
| 459 | sniffly_update_progress) |
| 460 | |
| 461 | ! Register scan completion callback (wrapped to update button state) |
| 462 | call register_scan_completion_callback(scan_complete_callback_wrapper) |
| 463 | print *, "Scan completion callback registered" |
| 464 | |
| 465 | ! Add drawing area to main box |
| 466 | call gtk_box_append(main_box, drawing_area) |
| 467 | |
| 468 | ! Create status bar (horizontal box with label only) |
| 469 | status_bar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5_c_int) |
| 470 | status_label_ptr = gtk_label_new("Preparing to scan..."//c_null_char) |
| 471 | call gtk_widget_set_halign(status_label_ptr, GTK_ALIGN_START) |
| 472 | call gtk_box_append(status_bar, status_label_ptr) |
| 473 | |
| 474 | ! Add status bar to main box |
| 475 | call gtk_box_append(main_box, status_bar) |
| 476 | |
| 477 | ! Add main box to window |
| 478 | call gtk_window_set_child(main_window_ptr, main_box) |
| 479 | |
| 480 | ! Register keyboard handler on window (not widget) for global keyboard capture |
| 481 | call register_key_handler(main_window_ptr) |
| 482 | |
| 483 | ! Load custom CSS (including pulsing animation for suggested-action) |
| 484 | call load_custom_css() |
| 485 | |
| 486 | ! Show the window first with "Scanning..." status |
| 487 | call gtk_window_present(main_window_ptr) |
| 488 | |
| 489 | ! Store scan path for idle callback |
| 490 | pending_scan_path = scan_path |
| 491 | |
| 492 | ! Schedule initial scan to run when GTK is idle (after window is shown) |
| 493 | idle_id = g_idle_add(c_funloc(perform_initial_scan), c_null_ptr) |
| 494 | |
| 495 | print *, "Sniffly started successfully! Scan will begin shortly..." |
| 496 | print *, "Window size: ", DEFAULT_WIDTH, "x", DEFAULT_HEIGHT |
| 497 | end subroutine on_activate |
| 498 | |
| 499 | ! Load custom CSS for animations and styling |
| 500 | subroutine load_custom_css() |
| 501 | type(c_ptr) :: css_provider, display |
| 502 | character(len=:), allocatable :: css_data |
| 503 | |
| 504 | ! CSS with pulsing animation for suggested-action class (empty tabs) |
| 505 | css_data = & |
| 506 | "@keyframes pulse { " // & |
| 507 | "0% { opacity: 1.0; } " // & |
| 508 | "50% { opacity: 0.5; } " // & |
| 509 | "100% { opacity: 1.0; } " // & |
| 510 | "} " // & |
| 511 | ".suggested-action { " // & |
| 512 | "animation: pulse 1.5s ease-in-out infinite; " // & |
| 513 | "}" |
| 514 | |
| 515 | ! Create CSS provider |
| 516 | css_provider = gtk_css_provider_new() |
| 517 | |
| 518 | ! Load CSS from string |
| 519 | call gtk_css_provider_load_from_string(css_provider, trim(css_data)//c_null_char) |
| 520 | |
| 521 | ! Get default display |
| 522 | display = gdk_display_get_default() |
| 523 | |
| 524 | ! Add CSS provider to display (600 = GTK_STYLE_PROVIDER_PRIORITY_APPLICATION) |
| 525 | call gtk_style_context_add_provider_for_display(display, css_provider, 600_c_int) |
| 526 | |
| 527 | print *, "Custom CSS loaded (pulsing animation for empty tabs)" |
| 528 | end subroutine load_custom_css |
| 529 | |
| 530 | ! Callback when Open Directory button is clicked |
| 531 | ! NOTE: Uses system command for file picking until GTK4 file dialog bindings are available |
| 532 | subroutine on_open_dir_clicked(button, user_data) bind(c) |
| 533 | type(c_ptr), value :: button, user_data |
| 534 | type(tab_state), pointer :: tab |
| 535 | character(len=1024) :: selected_path |
| 536 | integer :: status |
| 537 | |
| 538 | print *, "Open Directory button clicked!" |
| 539 | |
| 540 | ! Get active tab |
| 541 | tab => get_active_tab() |
| 542 | if (.not. associated(tab)) then |
| 543 | print *, "ERROR: No active tab in on_open_dir_clicked" |
| 544 | return |
| 545 | end if |
| 546 | |
| 547 | ! Call helper to show native file picker |
| 548 | call show_native_directory_picker(selected_path, status) |
| 549 | |
| 550 | if (status == 0 .and. len_trim(selected_path) > 0) then |
| 551 | print *, "Selected directory: ", trim(selected_path) |
| 552 | |
| 553 | ! Update tab scan path (but don't scan yet) |
| 554 | ! Remove trailing slash if present (C code doesn't like it) |
| 555 | if (len_trim(selected_path) > 1 .and. selected_path(len_trim(selected_path):len_trim(selected_path)) == '/') then |
| 556 | tab%scan_path = trim(selected_path(1:len_trim(selected_path)-1)) |
| 557 | print *, "DEBUG: Removed trailing slash from path" |
| 558 | else |
| 559 | tab%scan_path = trim(selected_path) |
| 560 | end if |
| 561 | print *, "DEBUG: Set tab scan_path to: '", trim(tab%scan_path), "'" |
| 562 | call set_scan_path(trim(tab%scan_path)) |
| 563 | |
| 564 | ! Update path display entry |
| 565 | call update_path_entry(trim(tab%scan_path)) |
| 566 | |
| 567 | print *, "Path updated. Click Scan button to scan: ", trim(tab%scan_path) |
| 568 | else |
| 569 | print *, "Directory selection cancelled or failed" |
| 570 | end if |
| 571 | end subroutine on_open_dir_clicked |
| 572 | |
| 573 | ! Callback when Scan button is clicked |
| 574 | subroutine on_scan_clicked(button, user_data) bind(c) |
| 575 | type(c_ptr), value :: button, user_data |
| 576 | type(tab_state), pointer :: tab |
| 577 | |
| 578 | ! Get active tab |
| 579 | tab => get_active_tab() |
| 580 | if (.not. associated(tab)) then |
| 581 | print *, "ERROR: No active tab in on_scan_clicked" |
| 582 | return |
| 583 | end if |
| 584 | |
| 585 | if (len_trim(tab%scan_path) == 0) then |
| 586 | call sniffly_show_error("No directory to scan") |
| 587 | return |
| 588 | end if |
| 589 | |
| 590 | ! Trigger a rescan of the current path (uses cache for speed) |
| 591 | call sniffly_update_status("Rescanning...") |
| 592 | call trigger_rescan(tab%scan_path) |
| 593 | end subroutine on_scan_clicked |
| 594 | |
| 595 | ! Callback when Cancel Scan button is clicked |
| 596 | subroutine on_cancel_scan_clicked(button, user_data) bind(c) |
| 597 | use progressive_scanner, only: stop_progressive_scan, is_scan_active |
| 598 | type(c_ptr), value :: button, user_data |
| 599 | |
| 600 | print *, "Cancel scan button clicked!" |
| 601 | |
| 602 | if (is_scan_active()) then |
| 603 | call sniffly_update_status("Cancelling scan...") |
| 604 | call stop_progressive_scan() |
| 605 | ! Update button states (cancel button disabled, back/forward re-enabled if history exists) |
| 606 | call update_cancel_scan_button_state() |
| 607 | call update_history_buttons() |
| 608 | call sniffly_update_status("Scan cancelled") |
| 609 | end if |
| 610 | end subroutine on_cancel_scan_clicked |
| 611 | |
| 612 | ! Callback when Open in Finder button is clicked |
| 613 | subroutine on_open_finder_clicked(button, user_data) bind(c) |
| 614 | type(c_ptr), value :: button, user_data |
| 615 | type(tab_state), pointer :: tab |
| 616 | character(len=:), allocatable :: selected_path |
| 617 | |
| 618 | print *, "Open in Finder button clicked!" |
| 619 | |
| 620 | ! Check if there's a selection |
| 621 | if (has_selection()) then |
| 622 | ! Get the selected node path |
| 623 | selected_path = get_selected_node_path() |
| 624 | if (len_trim(selected_path) == 0) then |
| 625 | print *, "Invalid selection path" |
| 626 | return |
| 627 | end if |
| 628 | print *, "Opening selected item in Finder: ", trim(selected_path) |
| 629 | else |
| 630 | ! No selection - use current directory |
| 631 | ! Get active tab |
| 632 | tab => get_active_tab() |
| 633 | if (.not. associated(tab)) then |
| 634 | print *, "ERROR: No active tab in on_open_finder_clicked" |
| 635 | return |
| 636 | end if |
| 637 | |
| 638 | if (len_trim(tab%scan_path) == 0) then |
| 639 | print *, "No current directory to open" |
| 640 | call sniffly_show_error("No directory to open in Finder") |
| 641 | return |
| 642 | end if |
| 643 | selected_path = trim(tab%scan_path) |
| 644 | print *, "Opening current directory in Finder: ", trim(selected_path) |
| 645 | end if |
| 646 | |
| 647 | call open_in_file_manager(selected_path) |
| 648 | call sniffly_update_status("Opened in Finder: " // trim(selected_path)) |
| 649 | end subroutine on_open_finder_clicked |
| 650 | |
| 651 | ! Callback when Copy Path button is clicked |
| 652 | subroutine on_copy_path_clicked(button, user_data) bind(c) |
| 653 | type(c_ptr), value :: button, user_data |
| 654 | character(len=:), allocatable :: selected_path |
| 655 | type(c_ptr) :: display, clipboard |
| 656 | |
| 657 | print *, "Copy Path button clicked!" |
| 658 | |
| 659 | ! Check if there's a selection |
| 660 | if (.not. has_selection()) then |
| 661 | print *, "No selection - cannot copy path" |
| 662 | call sniffly_show_error("No selection to copy") |
| 663 | return |
| 664 | end if |
| 665 | |
| 666 | ! Get the selected node path |
| 667 | selected_path = get_selected_node_path() |
| 668 | |
| 669 | if (len_trim(selected_path) == 0) then |
| 670 | print *, "Invalid selection path" |
| 671 | call sniffly_show_error("Invalid selection path") |
| 672 | return |
| 673 | end if |
| 674 | |
| 675 | print *, "Copying path to clipboard: ", trim(selected_path) |
| 676 | |
| 677 | ! Get the default display |
| 678 | display = gdk_display_get_default() |
| 679 | if (.not. c_associated(display)) then |
| 680 | print *, "ERROR: Failed to get default display" |
| 681 | call sniffly_show_error("Failed to access clipboard") |
| 682 | return |
| 683 | end if |
| 684 | |
| 685 | ! Get the clipboard from the display |
| 686 | clipboard = gdk_display_get_clipboard(display) |
| 687 | if (.not. c_associated(clipboard)) then |
| 688 | print *, "ERROR: Failed to get clipboard" |
| 689 | call sniffly_show_error("Failed to access clipboard") |
| 690 | return |
| 691 | end if |
| 692 | |
| 693 | ! Convert Fortran string to C string and set clipboard |
| 694 | call gdk_clipboard_set_text(clipboard, trim(selected_path)//c_null_char) |
| 695 | |
| 696 | ! Update status |
| 697 | call sniffly_update_status("Path copied to clipboard: " // trim(selected_path)) |
| 698 | print *, "Path copied successfully!" |
| 699 | end subroutine on_copy_path_clicked |
| 700 | |
| 701 | ! Helper: Build selection info string for status bar |
| 702 | function build_selection_info() result(info_text) |
| 703 | use iso_fortran_env, only: int64 |
| 704 | use treemap_renderer, only: get_current_view_node |
| 705 | use treemap_widget, only: get_selected_index |
| 706 | use types, only: file_node |
| 707 | use file_system, only: list_directory |
| 708 | character(len=1024) :: info_text |
| 709 | character(len=20) :: size_str |
| 710 | type(file_node), pointer :: view_node |
| 711 | integer(int64) :: size_bytes |
| 712 | integer :: item_count, selected_idx |
| 713 | character(len=256), dimension(10000) :: entries |
| 714 | |
| 715 | info_text = "" |
| 716 | |
| 717 | ! Get the selected node |
| 718 | view_node => get_current_view_node() |
| 719 | if (.not. associated(view_node)) then |
| 720 | return |
| 721 | end if |
| 722 | |
| 723 | ! Get the selected child index (1-based) |
| 724 | selected_idx = get_selected_index() |
| 725 | if (selected_idx < 1 .or. selected_idx > view_node%num_children) then |
| 726 | return |
| 727 | end if |
| 728 | |
| 729 | ! Get details from selected child |
| 730 | size_bytes = view_node%children(selected_idx)%size |
| 731 | |
| 732 | ! Check if this is a grouped "[N small files]" node |
| 733 | if (index(view_node%children(selected_idx)%name, '[') == 1 .and. & |
| 734 | index(view_node%children(selected_idx)%name, 'small files]') > 0) then |
| 735 | item_count = -1 ! Special marker for grouped nodes |
| 736 | else if (view_node%children(selected_idx)%is_directory) then |
| 737 | ! For regular directories, count entries on-demand |
| 738 | item_count = list_directory(view_node%children(selected_idx)%path, entries, 10000) |
| 739 | else |
| 740 | item_count = 0 ! Files don't have children |
| 741 | end if |
| 742 | |
| 743 | ! Format size |
| 744 | if (size_bytes < 1024_int64) then |
| 745 | write(size_str, '(I0,A)') size_bytes, ' B' |
| 746 | else if (size_bytes < 1024_int64**2) then |
| 747 | write(size_str, '(F0.2,A)') real(size_bytes)/1024.0, ' KB' |
| 748 | else if (size_bytes < 1024_int64**3) then |
| 749 | write(size_str, '(F0.2,A)') real(size_bytes)/(1024.0**2), ' MB' |
| 750 | else |
| 751 | write(size_str, '(F0.2,A)') real(size_bytes)/(1024.0**3), ' GB' |
| 752 | end if |
| 753 | |
| 754 | ! Build info text |
| 755 | if (item_count == -1) then |
| 756 | ! Grouped small files - name already contains the count |
| 757 | write(info_text, '(A,A,A)') & |
| 758 | trim(view_node%children(selected_idx)%name), ' | ', trim(size_str) |
| 759 | else if (view_node%children(selected_idx)%is_directory) then |
| 760 | ! Regular directory |
| 761 | write(info_text, '(A,A,A,A,I0,A)') & |
| 762 | trim(view_node%children(selected_idx)%name), ' | ', trim(size_str), ' | ', item_count, ' items' |
| 763 | else |
| 764 | ! Regular file |
| 765 | write(info_text, '(A,A,A,A)') & |
| 766 | trim(view_node%children(selected_idx)%name), ' | ', trim(size_str), ' | File' |
| 767 | end if |
| 768 | end function build_selection_info |
| 769 | |
| 770 | ! Callback when Properties/Info button is clicked - opens macOS Get Info window |
| 771 | subroutine on_info_clicked(button, user_data) bind(c) |
| 772 | type(c_ptr), value :: button, user_data |
| 773 | character(len=512) :: selected_path |
| 774 | character(len=1024) :: applescript_cmd |
| 775 | integer :: exit_status |
| 776 | |
| 777 | print *, "Info button clicked - opening macOS Get Info window" |
| 778 | |
| 779 | ! Check if there's a selection |
| 780 | if (.not. has_selection()) then |
| 781 | call sniffly_show_error("No selection to show info for") |
| 782 | return |
| 783 | end if |
| 784 | |
| 785 | ! Get the selected node path |
| 786 | selected_path = get_selected_node_path() |
| 787 | if (len_trim(selected_path) == 0) then |
| 788 | print *, "Invalid selection path" |
| 789 | call sniffly_show_error("Invalid selection") |
| 790 | return |
| 791 | end if |
| 792 | |
| 793 | print *, "Opening Get Info for: ", trim(selected_path) |
| 794 | |
| 795 | ! Build AppleScript command to open Get Info window |
| 796 | ! We escape single quotes in the path by replacing ' with '\'' |
| 797 | write(applescript_cmd, '(A,A,A)') & |
| 798 | 'osascript -e ''tell application "Finder" to open information window of (POSIX file "', & |
| 799 | trim(selected_path), '" as alias)''' |
| 800 | |
| 801 | ! Execute the command |
| 802 | call execute_command_line(trim(applescript_cmd), exitstat=exit_status) |
| 803 | |
| 804 | if (exit_status /= 0) then |
| 805 | print *, "ERROR: Failed to open Get Info window (exit status:", exit_status, ")" |
| 806 | call sniffly_show_error("Failed to open Get Info window") |
| 807 | else |
| 808 | print *, "Successfully opened Get Info window" |
| 809 | end if |
| 810 | end subroutine on_info_clicked |
| 811 | |
| 812 | ! Helper: Update Back/Forward button states |
| 813 | subroutine update_history_buttons() |
| 814 | use gtk, only: gtk_widget_set_sensitive |
| 815 | use progressive_scanner, only: is_scan_active |
| 816 | type(tab_state), pointer :: tab |
| 817 | logical :: scan_active |
| 818 | |
| 819 | ! Guard against accessing widgets during shutdown |
| 820 | if (app_is_shutting_down) return |
| 821 | if (.not. c_associated(back_btn_ptr) .or. .not. c_associated(forward_btn_ptr)) return |
| 822 | |
| 823 | ! Get active tab |
| 824 | tab => get_active_tab() |
| 825 | if (.not. associated(tab)) then |
| 826 | ! No active tab - disable buttons |
| 827 | call gtk_widget_set_sensitive(back_btn_ptr, 0_c_int) |
| 828 | call gtk_widget_set_sensitive(forward_btn_ptr, 0_c_int) |
| 829 | return |
| 830 | end if |
| 831 | |
| 832 | ! Check if scan is active - disable buttons during scan |
| 833 | scan_active = is_scan_active() |
| 834 | |
| 835 | print *, "=== UPDATE_HISTORY_BUTTONS ===" |
| 836 | print *, " pos=", tab%nav_history_pos, " count=", tab%nav_history_count, " scan_active=", scan_active |
| 837 | |
| 838 | ! Enable Back if we're not at the start of history AND scan is not active |
| 839 | if (tab%nav_history_pos > 1 .and. .not. scan_active) then |
| 840 | print *, " Enabling Back (pos > 1 and scan not active)" |
| 841 | call gtk_widget_set_sensitive(back_btn_ptr, 1_c_int) |
| 842 | else |
| 843 | print *, " Disabling Back (pos=", tab%nav_history_pos, " or scan active)" |
| 844 | call gtk_widget_set_sensitive(back_btn_ptr, 0_c_int) |
| 845 | end if |
| 846 | |
| 847 | ! Enable Forward if we're not at the end of history AND scan is not active |
| 848 | if (tab%nav_history_pos > 0 .and. tab%nav_history_pos < tab%nav_history_count .and. .not. scan_active) then |
| 849 | print *, " Enabling Forward (pos < count and scan not active)" |
| 850 | call gtk_widget_set_sensitive(forward_btn_ptr, 1_c_int) |
| 851 | else |
| 852 | print *, " Disabling Forward (pos=", tab%nav_history_pos, " count=", tab%nav_history_count, " or scan active)" |
| 853 | call gtk_widget_set_sensitive(forward_btn_ptr, 0_c_int) |
| 854 | end if |
| 855 | end subroutine update_history_buttons |
| 856 | |
| 857 | ! Helper: Update Cancel Scan button state based on scan status |
| 858 | subroutine update_cancel_scan_button_state() |
| 859 | use progressive_scanner, only: is_scan_active |
| 860 | use gtk, only: gtk_widget_set_sensitive |
| 861 | |
| 862 | ! Guard against accessing widgets during shutdown |
| 863 | if (app_is_shutting_down) return |
| 864 | if (.not. c_associated(cancel_scan_btn_ptr)) return |
| 865 | |
| 866 | print *, "DEBUG: Updating cancel button state, scan active =", is_scan_active() |
| 867 | |
| 868 | if (is_scan_active()) then |
| 869 | ! Scan is active - enable button and make it red |
| 870 | print *, "DEBUG: Enabling cancel button (making it red)" |
| 871 | call gtk_widget_add_css_class(cancel_scan_btn_ptr, "destructive-action"//c_null_char) |
| 872 | call gtk_widget_set_sensitive(cancel_scan_btn_ptr, 1_c_int) |
| 873 | else |
| 874 | ! No scan active - remove red styling first, then disable button |
| 875 | print *, "DEBUG: Disabling cancel button (making it grey)" |
| 876 | call gtk_widget_remove_css_class(cancel_scan_btn_ptr, "destructive-action"//c_null_char) |
| 877 | call gtk_widget_set_sensitive(cancel_scan_btn_ptr, 0_c_int) |
| 878 | end if |
| 879 | end subroutine update_cancel_scan_button_state |
| 880 | |
| 881 | ! Helper: Update selection-dependent button states and display selection info |
| 882 | subroutine update_selection_buttons() |
| 883 | use gtk, only: gtk_widget_set_sensitive |
| 884 | logical :: has_sel |
| 885 | character(len=1024) :: sel_info |
| 886 | |
| 887 | ! Guard against accessing widgets during shutdown |
| 888 | if (app_is_shutting_down) return |
| 889 | if (.not. c_associated(info_btn_ptr)) return |
| 890 | |
| 891 | ! Check if there's a selection |
| 892 | has_sel = has_selection() |
| 893 | |
| 894 | print *, "DEBUG: update_selection_buttons() called, has_selection =", has_sel |
| 895 | |
| 896 | ! Enable/disable selection-dependent buttons |
| 897 | if (has_sel) then |
| 898 | print *, "DEBUG: Enabling selection-dependent buttons" |
| 899 | call gtk_widget_set_sensitive(info_btn_ptr, 1_c_int) |
| 900 | call gtk_widget_set_sensitive(copy_path_btn_ptr, 1_c_int) |
| 901 | call gtk_widget_set_sensitive(delete_btn_ptr, 1_c_int) |
| 902 | |
| 903 | ! Auto-display selection info in status bar |
| 904 | sel_info = build_selection_info() |
| 905 | if (len_trim(sel_info) > 0) then |
| 906 | call sniffly_update_status(trim(sel_info)) |
| 907 | end if |
| 908 | else |
| 909 | print *, "DEBUG: Disabling selection-dependent buttons" |
| 910 | call gtk_widget_set_sensitive(info_btn_ptr, 0_c_int) |
| 911 | call gtk_widget_set_sensitive(copy_path_btn_ptr, 0_c_int) |
| 912 | call gtk_widget_set_sensitive(delete_btn_ptr, 0_c_int) |
| 913 | |
| 914 | ! Clear selection info from status bar when deselected |
| 915 | call sniffly_update_status("") |
| 916 | end if |
| 917 | |
| 918 | ! Open in Finder button is always enabled (defaults to current directory) |
| 919 | call gtk_widget_set_sensitive(open_finder_btn_ptr, 1_c_int) |
| 920 | end subroutine update_selection_buttons |
| 921 | |
| 922 | ! Update UI to reflect the active tab's state (called when switching tabs) |
| 923 | subroutine update_ui_for_active_tab() |
| 924 | use gtk, only: gtk_widget_queue_draw |
| 925 | use types, only: file_node |
| 926 | type(tab_state), pointer :: tab |
| 927 | type(file_node), pointer :: current_view |
| 928 | |
| 929 | print *, "=== UPDATE_UI_FOR_ACTIVE_TAB ===" |
| 930 | |
| 931 | ! Get active tab |
| 932 | tab => get_active_tab() |
| 933 | if (.not. associated(tab)) then |
| 934 | print *, "ERROR: No active tab" |
| 935 | return |
| 936 | end if |
| 937 | |
| 938 | print *, " Active tab index: ", active_tab_index |
| 939 | print *, " Tab has_data: ", tab%has_data |
| 940 | print *, " Tab scan_path: ", trim(tab%scan_path) |
| 941 | |
| 942 | ! Sync treemap widget's scan path with active tab's scan path |
| 943 | call set_scan_path(trim(tab%scan_path)) |
| 944 | |
| 945 | ! Sync renderer state with active tab (CRITICAL for correct rendering) |
| 946 | call set_renderer_state_from_tab(tab%root_node, tab%current_view_node, tab%has_data) |
| 947 | |
| 948 | ! Check if tab has data |
| 949 | if (.not. tab%has_data) then |
| 950 | print *, " Tab has no data yet - showing empty tab UI" |
| 951 | |
| 952 | ! Add blue suggested-action class to open-dir button to draw attention |
| 953 | if (c_associated(open_dir_btn_ptr)) then |
| 954 | call gtk_widget_add_css_class(open_dir_btn_ptr, "suggested-action"//c_null_char) |
| 955 | end if |
| 956 | |
| 957 | ! Clear breadcrumb display |
| 958 | call update_breadcrumb_cache("") |
| 959 | |
| 960 | ! Clear path entry |
| 961 | call update_path_entry("") |
| 962 | |
| 963 | ! Update status bar to guide user |
| 964 | call sniffly_update_status("No directory selected - click the folder icon to choose a directory") |
| 965 | |
| 966 | ! Clear the treemap drawing (redraw with no data will show blank) |
| 967 | if (c_associated(drawing_area_ptr)) then |
| 968 | call gtk_widget_queue_draw(drawing_area_ptr) |
| 969 | end if |
| 970 | |
| 971 | return |
| 972 | end if |
| 973 | |
| 974 | ! Tab has data - remove suggested-action class from open-dir button |
| 975 | if (c_associated(open_dir_btn_ptr)) then |
| 976 | call gtk_widget_remove_css_class(open_dir_btn_ptr, "suggested-action"//c_null_char) |
| 977 | end if |
| 978 | |
| 979 | ! Get the current view node |
| 980 | current_view => tab%current_view_node |
| 981 | if (.not. associated(current_view)) then |
| 982 | print *, "ERROR: Tab has_data=true but current_view_node not associated" |
| 983 | return |
| 984 | end if |
| 985 | |
| 986 | print *, " Updating breadcrumb for: ", trim(current_view%path) |
| 987 | |
| 988 | ! Update breadcrumb cache |
| 989 | call update_breadcrumb_cache(trim(current_view%path)) |
| 990 | |
| 991 | ! Trigger treemap redraw |
| 992 | if (c_associated(drawing_area_ptr)) then |
| 993 | call gtk_widget_queue_draw(drawing_area_ptr) |
| 994 | print *, " Triggered treemap redraw" |
| 995 | end if |
| 996 | |
| 997 | ! Update navigation buttons |
| 998 | call update_history_buttons() |
| 999 | |
| 1000 | ! Update path entry |
| 1001 | call update_path_entry(trim(current_view%path)) |
| 1002 | |
| 1003 | print *, "=== UI UPDATE COMPLETE ===" |
| 1004 | end subroutine update_ui_for_active_tab |
| 1005 | |
| 1006 | ! Helper: Add path to navigation history |
| 1007 | subroutine add_to_history(path) |
| 1008 | character(len=*), intent(in) :: path |
| 1009 | type(tab_state), pointer :: tab |
| 1010 | integer :: i |
| 1011 | |
| 1012 | print *, "=== ADD_TO_HISTORY CALLED ===" |
| 1013 | print *, " Path: ", trim(path) |
| 1014 | |
| 1015 | ! Get active tab |
| 1016 | tab => get_active_tab() |
| 1017 | if (.not. associated(tab)) then |
| 1018 | print *, "ERROR: No active tab in add_to_history" |
| 1019 | return |
| 1020 | end if |
| 1021 | |
| 1022 | print *, " Before: pos=", tab%nav_history_pos, " count=", tab%nav_history_count |
| 1023 | if (tab%nav_history_count > 0) then |
| 1024 | print *, " Current history:" |
| 1025 | do i = 1, tab%nav_history_count |
| 1026 | if (i == tab%nav_history_pos) then |
| 1027 | print *, " [", i, "] (CURRENT) ", trim(tab%nav_history(i)) |
| 1028 | else |
| 1029 | print *, " [", i, "] ", trim(tab%nav_history(i)) |
| 1030 | end if |
| 1031 | end do |
| 1032 | end if |
| 1033 | |
| 1034 | ! Don't add if it's the same as current position |
| 1035 | if (tab%nav_history_pos > 0 .and. tab%nav_history_pos <= tab%nav_history_count) then |
| 1036 | if (trim(tab%nav_history(tab%nav_history_pos)) == trim(path)) then |
| 1037 | print *, " Path same as current position - not adding" |
| 1038 | return |
| 1039 | end if |
| 1040 | end if |
| 1041 | |
| 1042 | ! If we're in the middle of history, discard forward history |
| 1043 | if (tab%nav_history_pos > 0 .and. tab%nav_history_pos < tab%nav_history_count) then |
| 1044 | print *, " In middle of history - truncating forward history" |
| 1045 | print *, " Truncating count from", tab%nav_history_count, "to", tab%nav_history_pos |
| 1046 | tab%nav_history_count = tab%nav_history_pos |
| 1047 | end if |
| 1048 | |
| 1049 | ! Add to history |
| 1050 | if (tab%nav_history_count < MAX_HISTORY) then |
| 1051 | tab%nav_history_count = tab%nav_history_count + 1 |
| 1052 | tab%nav_history(tab%nav_history_count) = trim(path) |
| 1053 | print *, " Added to history at position", tab%nav_history_count |
| 1054 | else |
| 1055 | ! Shift history left and add at end |
| 1056 | print *, " History full - shifting left" |
| 1057 | do i = 1, MAX_HISTORY - 1 |
| 1058 | tab%nav_history(i) = tab%nav_history(i + 1) |
| 1059 | end do |
| 1060 | tab%nav_history(MAX_HISTORY) = trim(path) |
| 1061 | end if |
| 1062 | |
| 1063 | tab%nav_history_pos = tab%nav_history_count |
| 1064 | print *, " After: pos=", tab%nav_history_pos, " count=", tab%nav_history_count |
| 1065 | print *, "=== END ADD_TO_HISTORY ===" |
| 1066 | call update_history_buttons() |
| 1067 | end subroutine add_to_history |
| 1068 | |
| 1069 | ! Navigate to a synthetic path (grouped small files node) by tree traversal |
| 1070 | subroutine navigate_to_synthetic_path(synthetic_path) |
| 1071 | use treemap_renderer, only: scan_directory |
| 1072 | character(len=*), intent(in) :: synthetic_path |
| 1073 | character(len=512) :: parent_path, node_name |
| 1074 | integer :: last_sep, i |
| 1075 | |
| 1076 | ! Extract parent path and node name |
| 1077 | last_sep = 0 |
| 1078 | do i = len_trim(synthetic_path), 1, -1 |
| 1079 | if (synthetic_path(i:i) == '/') then |
| 1080 | last_sep = i |
| 1081 | exit |
| 1082 | end if |
| 1083 | end do |
| 1084 | |
| 1085 | if (last_sep > 0) then |
| 1086 | parent_path = synthetic_path(1:last_sep-1) |
| 1087 | node_name = synthetic_path(last_sep+1:len_trim(synthetic_path)) |
| 1088 | else |
| 1089 | print *, "ERROR: Invalid synthetic path format" |
| 1090 | return |
| 1091 | end if |
| 1092 | |
| 1093 | print *, " Parent path: ", trim(parent_path) |
| 1094 | print *, " Node name: ", trim(node_name) |
| 1095 | |
| 1096 | ! Set up pending navigation |
| 1097 | pending_synthetic_nav = .true. |
| 1098 | pending_synthetic_child_name = trim(node_name) |
| 1099 | |
| 1100 | ! Navigate to parent directory - after scan completes, breadcrumb_callback will handle navigation |
| 1101 | call set_scan_path(trim(parent_path)) |
| 1102 | call update_path_entry(trim(parent_path)) |
| 1103 | call trigger_rescan(parent_path) |
| 1104 | call sniffly_update_status("Navigating to grouped files...") |
| 1105 | end subroutine navigate_to_synthetic_path |
| 1106 | |
| 1107 | ! Complete pending synthetic navigation (called after scan completes) |
| 1108 | subroutine complete_synthetic_navigation() |
| 1109 | use treemap_renderer, only: get_current_view_node, navigate_into_node, invalidate_layout |
| 1110 | use treemap_widget, only: get_widget_ptr |
| 1111 | use gtk, only: gtk_widget_queue_draw |
| 1112 | use types, only: file_node |
| 1113 | type(file_node), pointer :: view_node |
| 1114 | type(c_ptr) :: widget |
| 1115 | integer :: i |
| 1116 | |
| 1117 | if (.not. pending_synthetic_nav) return |
| 1118 | |
| 1119 | synthetic_nav_attempts = synthetic_nav_attempts + 1 |
| 1120 | print *, "=== COMPLETING SYNTHETIC NAVIGATION (attempt ", synthetic_nav_attempts, ") ===" |
| 1121 | print *, " Looking for child: ", trim(pending_synthetic_child_name) |
| 1122 | |
| 1123 | view_node => get_current_view_node() |
| 1124 | if (.not. associated(view_node)) then |
| 1125 | print *, "ERROR: No current view node" |
| 1126 | pending_synthetic_nav = .false. |
| 1127 | synthetic_nav_attempts = 0 |
| 1128 | return |
| 1129 | end if |
| 1130 | |
| 1131 | if (.not. allocated(view_node%children)) then |
| 1132 | print *, "ERROR: Current node has no children" |
| 1133 | pending_synthetic_nav = .false. |
| 1134 | synthetic_nav_attempts = 0 |
| 1135 | return |
| 1136 | end if |
| 1137 | |
| 1138 | print *, " Current view has ", view_node%num_children, " children" |
| 1139 | |
| 1140 | ! If we have way too many children, grouping hasn't happened yet - wait for next callback |
| 1141 | if (view_node%num_children > 100 .and. synthetic_nav_attempts < 5) then |
| 1142 | print *, " Too many children (", view_node%num_children, ") - grouping not done yet, waiting..." |
| 1143 | return ! Keep pending flag true, will retry on next callback |
| 1144 | end if |
| 1145 | |
| 1146 | ! Find child with matching name |
| 1147 | do i = 1, view_node%num_children |
| 1148 | if (allocated(view_node%children(i)%name)) then |
| 1149 | if (trim(view_node%children(i)%name) == trim(pending_synthetic_child_name)) then |
| 1150 | print *, " Found child at index ", i, ": '", trim(view_node%children(i)%name), "'" |
| 1151 | ! Navigate into the child |
| 1152 | call navigate_into_node(i) |
| 1153 | |
| 1154 | ! Update UI |
| 1155 | call invalidate_layout() |
| 1156 | widget = get_widget_ptr() |
| 1157 | if (c_associated(widget)) then |
| 1158 | call gtk_widget_queue_draw(widget) |
| 1159 | end if |
| 1160 | |
| 1161 | ! Trigger breadcrumb update |
| 1162 | call breadcrumb_callback() |
| 1163 | |
| 1164 | pending_synthetic_nav = .false. |
| 1165 | synthetic_nav_attempts = 0 |
| 1166 | call sniffly_update_status("Navigated to grouped files") |
| 1167 | return |
| 1168 | end if |
| 1169 | end if |
| 1170 | end do |
| 1171 | |
| 1172 | ! Give up after several attempts |
| 1173 | if (synthetic_nav_attempts >= 5) then |
| 1174 | print *, "ERROR: Could not find child after ", synthetic_nav_attempts, " attempts" |
| 1175 | print *, " Searched for: '", trim(pending_synthetic_child_name), "'" |
| 1176 | pending_synthetic_nav = .false. |
| 1177 | synthetic_nav_attempts = 0 |
| 1178 | call sniffly_show_error("Could not find grouped files node") |
| 1179 | else |
| 1180 | print *, " Child not found yet, will retry on next callback" |
| 1181 | end if |
| 1182 | end subroutine complete_synthetic_navigation |
| 1183 | |
| 1184 | ! Callback when Back button is clicked |
| 1185 | subroutine on_back_clicked(button, user_data) bind(c) |
| 1186 | use progressive_scanner, only: is_scan_active |
| 1187 | type(c_ptr), value :: button, user_data |
| 1188 | type(tab_state), pointer :: tab |
| 1189 | |
| 1190 | print *, "=== BACK BUTTON CLICKED ===" |
| 1191 | |
| 1192 | ! Get active tab |
| 1193 | tab => get_active_tab() |
| 1194 | if (.not. associated(tab)) then |
| 1195 | print *, "ERROR: No active tab in on_back_clicked" |
| 1196 | return |
| 1197 | end if |
| 1198 | |
| 1199 | print *, " Before: pos=", tab%nav_history_pos, " count=", tab%nav_history_count |
| 1200 | |
| 1201 | ! Block navigation if scan is active |
| 1202 | if (is_scan_active()) then |
| 1203 | call sniffly_show_error("Cannot navigate: Scan in progress") |
| 1204 | return |
| 1205 | end if |
| 1206 | |
| 1207 | if (tab%nav_history_pos > 1) then |
| 1208 | tab%nav_history_pos = tab%nav_history_pos - 1 |
| 1209 | tab%scan_path = trim(tab%nav_history(tab%nav_history_pos)) |
| 1210 | print *, " Moving back to pos=", tab%nav_history_pos |
| 1211 | print *, " Path: ", trim(tab%scan_path) |
| 1212 | tab%navigating_history = .true. ! Set flag before triggering rescan |
| 1213 | call set_scan_path(trim(tab%scan_path)) |
| 1214 | call update_path_entry(trim(tab%scan_path)) |
| 1215 | call trigger_rescan(tab%scan_path) |
| 1216 | call update_history_buttons() |
| 1217 | call sniffly_update_status("Navigated back to: " // trim(tab%scan_path)) |
| 1218 | end if |
| 1219 | end subroutine on_back_clicked |
| 1220 | |
| 1221 | ! Callback when Forward button is clicked |
| 1222 | subroutine on_forward_clicked(button, user_data) bind(c) |
| 1223 | use progressive_scanner, only: is_scan_active |
| 1224 | type(c_ptr), value :: button, user_data |
| 1225 | type(tab_state), pointer :: tab |
| 1226 | logical :: is_synthetic |
| 1227 | |
| 1228 | print *, "=== FORWARD BUTTON CLICKED ===" |
| 1229 | |
| 1230 | ! Get active tab |
| 1231 | tab => get_active_tab() |
| 1232 | if (.not. associated(tab)) then |
| 1233 | print *, "ERROR: No active tab in on_forward_clicked" |
| 1234 | return |
| 1235 | end if |
| 1236 | |
| 1237 | print *, " Before: pos=", tab%nav_history_pos, " count=", tab%nav_history_count |
| 1238 | |
| 1239 | ! Block navigation if scan is active |
| 1240 | if (is_scan_active()) then |
| 1241 | call sniffly_show_error("Cannot navigate: Scan in progress") |
| 1242 | return |
| 1243 | end if |
| 1244 | |
| 1245 | if (tab%nav_history_pos > 0 .and. tab%nav_history_pos < tab%nav_history_count) then |
| 1246 | tab%nav_history_pos = tab%nav_history_pos + 1 |
| 1247 | tab%scan_path = trim(tab%nav_history(tab%nav_history_pos)) |
| 1248 | print *, " Moving forward to pos=", tab%nav_history_pos |
| 1249 | print *, " Path: ", trim(tab%scan_path) |
| 1250 | |
| 1251 | tab%navigating_history = .true. ! Set flag before triggering rescan |
| 1252 | |
| 1253 | ! Check if this is a synthetic path (grouped small files node) |
| 1254 | is_synthetic = (index(tab%scan_path, '[') > 0 .and. & |
| 1255 | index(tab%scan_path, 'small files]') > 0) |
| 1256 | |
| 1257 | if (is_synthetic) then |
| 1258 | print *, " Synthetic path detected - navigating by tree traversal" |
| 1259 | call navigate_to_synthetic_path(tab%scan_path) |
| 1260 | else |
| 1261 | call set_scan_path(trim(tab%scan_path)) |
| 1262 | call update_path_entry(trim(tab%scan_path)) |
| 1263 | call trigger_rescan(tab%scan_path) |
| 1264 | end if |
| 1265 | |
| 1266 | call update_history_buttons() |
| 1267 | call sniffly_update_status("Navigated forward to: " // trim(tab%scan_path)) |
| 1268 | end if |
| 1269 | end subroutine on_forward_clicked |
| 1270 | |
| 1271 | ! Callback when Up to Parent button is clicked |
| 1272 | subroutine on_up_clicked(button, user_data) bind(c) |
| 1273 | use treemap_renderer, only: navigate_up |
| 1274 | use gtk, only: gtk_widget_queue_draw |
| 1275 | type(c_ptr), value :: button, user_data |
| 1276 | |
| 1277 | ! Use the existing navigate_up functionality from treemap_renderer |
| 1278 | call navigate_up() |
| 1279 | |
| 1280 | ! Update breadcrumbs and history |
| 1281 | call breadcrumb_callback() |
| 1282 | |
| 1283 | ! Trigger redraw |
| 1284 | if (c_associated(main_window_ptr)) then |
| 1285 | call gtk_widget_queue_draw(main_window_ptr) |
| 1286 | end if |
| 1287 | |
| 1288 | call sniffly_update_status("Navigated to parent directory") |
| 1289 | end subroutine on_up_clicked |
| 1290 | |
| 1291 | ! Callback when Delete button is clicked |
| 1292 | subroutine on_delete_clicked(button, user_data) bind(c) |
| 1293 | use treemap_widget, only: get_selected_index |
| 1294 | type(c_ptr), value :: button, user_data |
| 1295 | character(len=:), allocatable :: selected_path |
| 1296 | integer :: confirm_result, selected_idx |
| 1297 | |
| 1298 | print *, "Delete button clicked!" |
| 1299 | |
| 1300 | ! Check if there's a selection |
| 1301 | if (.not. has_selection()) then |
| 1302 | print *, "No selection - cannot delete" |
| 1303 | return |
| 1304 | end if |
| 1305 | |
| 1306 | ! Get the selected node path and index |
| 1307 | selected_path = get_selected_node_path() |
| 1308 | selected_idx = get_selected_index() |
| 1309 | |
| 1310 | if (len_trim(selected_path) == 0) then |
| 1311 | print *, "Invalid selection path" |
| 1312 | return |
| 1313 | end if |
| 1314 | |
| 1315 | print *, "Preparing to delete: ", trim(selected_path) |
| 1316 | |
| 1317 | ! Show confirmation dialog |
| 1318 | call show_delete_confirmation(selected_path, confirm_result) |
| 1319 | |
| 1320 | if (confirm_result == 1) then |
| 1321 | print *, "Delete confirmed - proceeding" |
| 1322 | call delete_to_trash(selected_path, selected_idx) |
| 1323 | else |
| 1324 | print *, "Delete cancelled by user" |
| 1325 | end if |
| 1326 | end subroutine on_delete_clicked |
| 1327 | |
| 1328 | ! Callback when Toggle Dotfiles button is clicked |
| 1329 | subroutine on_toggle_dotfiles_clicked(button, user_data) bind(c) |
| 1330 | use treemap_renderer, only: toggle_hidden_files |
| 1331 | type(c_ptr), value :: button, user_data |
| 1332 | type(tab_state), pointer :: tab |
| 1333 | |
| 1334 | ! Get active tab |
| 1335 | tab => get_active_tab() |
| 1336 | if (.not. associated(tab)) then |
| 1337 | print *, "ERROR: No active tab in on_toggle_dotfiles_clicked" |
| 1338 | return |
| 1339 | end if |
| 1340 | |
| 1341 | call toggle_hidden_files() |
| 1342 | call sniffly_update_status("Toggled hidden files visibility - rescanning...") |
| 1343 | |
| 1344 | ! Trigger rescan to apply the filter |
| 1345 | if (len_trim(tab%scan_path) > 0) then |
| 1346 | call trigger_rescan(tab%scan_path) |
| 1347 | end if |
| 1348 | end subroutine on_toggle_dotfiles_clicked |
| 1349 | |
| 1350 | ! Callback when Toggle File Extensions button is clicked |
| 1351 | subroutine on_toggle_extensions_clicked(button, user_data) bind(c) |
| 1352 | use treemap_renderer, only: toggle_file_extensions |
| 1353 | type(c_ptr), value :: button, user_data |
| 1354 | |
| 1355 | call toggle_file_extensions() |
| 1356 | call sniffly_update_status("Toggled file extensions visibility") |
| 1357 | end subroutine on_toggle_extensions_clicked |
| 1358 | |
| 1359 | ! Callback when Toggle Render Mode button is clicked |
| 1360 | subroutine on_toggle_render_mode_clicked(button, user_data) bind(c) |
| 1361 | use treemap_renderer, only: toggle_render_mode |
| 1362 | type(c_ptr), value :: button, user_data |
| 1363 | |
| 1364 | call toggle_render_mode() |
| 1365 | call sniffly_update_status("Toggled render mode (flat vs cushioned)") |
| 1366 | end subroutine on_toggle_render_mode_clicked |
| 1367 | |
| 1368 | ! Commented out unused helper function - was used by removed search/filter feature |
| 1369 | ! Uncomment if needed in future |
| 1370 | |
| 1371 | ! ! Helper to convert C string to Fortran string |
| 1372 | ! subroutine c_f_string(c_str_ptr, f_str) |
| 1373 | ! type(c_ptr), intent(in) :: c_str_ptr |
| 1374 | ! character(len=*), intent(out) :: f_str |
| 1375 | ! character(len=1, kind=c_char), pointer :: c_chars(:) |
| 1376 | ! integer :: i, str_len |
| 1377 | ! |
| 1378 | ! f_str = "" |
| 1379 | ! if (.not. c_associated(c_str_ptr)) return |
| 1380 | ! |
| 1381 | ! ! Get string length |
| 1382 | ! str_len = 0 |
| 1383 | ! do i = 1, len(f_str) |
| 1384 | ! call c_f_pointer(c_str_ptr, c_chars, [i]) |
| 1385 | ! if (c_chars(i) == c_null_char) exit |
| 1386 | ! str_len = i |
| 1387 | ! end do |
| 1388 | ! |
| 1389 | ! ! Copy characters |
| 1390 | ! if (str_len > 0) then |
| 1391 | ! call c_f_pointer(c_str_ptr, c_chars, [str_len]) |
| 1392 | ! do i = 1, str_len |
| 1393 | ! f_str(i:i) = c_chars(i) |
| 1394 | ! end do |
| 1395 | ! end if |
| 1396 | ! end subroutine c_f_string |
| 1397 | |
| 1398 | ! Detect if we're running on macOS |
| 1399 | function is_macos() result(is_mac) |
| 1400 | logical :: is_mac |
| 1401 | logical :: file_exists |
| 1402 | |
| 1403 | ! Check for macOS-specific directory |
| 1404 | inquire(file='/Applications', exist=file_exists) |
| 1405 | is_mac = file_exists |
| 1406 | end function is_macos |
| 1407 | |
| 1408 | ! Show native OS directory picker using system commands |
| 1409 | ! This is a workaround until GTK4 file dialog bindings are available |
| 1410 | subroutine show_native_directory_picker(path, status) |
| 1411 | character(len=*), intent(out) :: path |
| 1412 | integer, intent(out) :: status |
| 1413 | character(len=2048) :: command, temp_file |
| 1414 | integer :: unit, ios |
| 1415 | logical :: file_exists |
| 1416 | |
| 1417 | path = "" |
| 1418 | status = -1 |
| 1419 | |
| 1420 | ! Create temp file for output |
| 1421 | temp_file = "/tmp/sniffly_picker.txt" |
| 1422 | |
| 1423 | ! Platform-specific command - detect at runtime |
| 1424 | if (is_macos()) then |
| 1425 | ! macOS: Use osascript to show native folder picker |
| 1426 | command = 'osascript -e ''POSIX path of (choose folder with prompt "Select directory to scan:")'' > ' & |
| 1427 | // trim(temp_file) // ' 2>&1' |
| 1428 | else |
| 1429 | ! Linux: Try zenity, fallback to kdialog |
| 1430 | command = 'zenity --file-selection --directory > ' // trim(temp_file) // & |
| 1431 | ' 2>&1 || kdialog --getexistingdirectory . > ' // trim(temp_file) // ' 2>&1' |
| 1432 | end if |
| 1433 | |
| 1434 | print *, "Executing: ", trim(command) |
| 1435 | |
| 1436 | ! Execute command |
| 1437 | call execute_command_line(trim(command), exitstat=status) |
| 1438 | |
| 1439 | ! Read result from temp file |
| 1440 | inquire(file=trim(temp_file), exist=file_exists) |
| 1441 | if (file_exists) then |
| 1442 | open(newunit=unit, file=trim(temp_file), status='old', action='read', iostat=ios) |
| 1443 | if (ios == 0) then |
| 1444 | read(unit, '(A)', iostat=ios) path |
| 1445 | close(unit) |
| 1446 | |
| 1447 | ! Remove temp file |
| 1448 | call execute_command_line('rm -f ' // trim(temp_file)) |
| 1449 | |
| 1450 | ! Trim whitespace and check if valid |
| 1451 | path = trim(adjustl(path)) |
| 1452 | if (len_trim(path) > 0) then |
| 1453 | status = 0 |
| 1454 | print *, "Got path: ", trim(path) |
| 1455 | else |
| 1456 | status = 1 |
| 1457 | end if |
| 1458 | else |
| 1459 | close(unit) |
| 1460 | status = 1 |
| 1461 | end if |
| 1462 | else |
| 1463 | status = 1 |
| 1464 | end if |
| 1465 | end subroutine show_native_directory_picker |
| 1466 | |
| 1467 | ! Update the path display entry with a new path |
| 1468 | subroutine update_path_entry(path) |
| 1469 | character(len=*), intent(in) :: path |
| 1470 | type(c_ptr) :: buffer |
| 1471 | |
| 1472 | ! Guard against accessing widgets during shutdown |
| 1473 | if (app_is_shutting_down) return |
| 1474 | if (.not. c_associated(path_entry_ptr)) return |
| 1475 | |
| 1476 | ! Get the entry buffer and set the text |
| 1477 | buffer = gtk_entry_get_buffer(path_entry_ptr) |
| 1478 | call gtk_entry_buffer_set_text(buffer, trim(path)//c_null_char, & |
| 1479 | int(len_trim(path), c_int)) |
| 1480 | end subroutine update_path_entry |
| 1481 | |
| 1482 | ! Open a file or folder in the OS file manager (Finder on macOS, file browser on Linux) |
| 1483 | subroutine open_in_file_manager(path) |
| 1484 | character(len=*), intent(in) :: path |
| 1485 | character(len=2048) :: command |
| 1486 | integer :: status |
| 1487 | |
| 1488 | if (is_macos()) then |
| 1489 | ! macOS: Use 'open -R' to reveal in Finder |
| 1490 | command = 'open -R "' // trim(path) // '"' |
| 1491 | else |
| 1492 | ! Linux: Use xdg-open to open in default file manager |
| 1493 | command = 'xdg-open "' // trim(path) // '"' |
| 1494 | end if |
| 1495 | |
| 1496 | print *, "Executing: ", trim(command) |
| 1497 | call execute_command_line(trim(command), exitstat=status) |
| 1498 | |
| 1499 | if (status /= 0) then |
| 1500 | print *, "Warning: Failed to open file manager (exit status: ", status, ")" |
| 1501 | else |
| 1502 | print *, "Successfully opened in file manager" |
| 1503 | end if |
| 1504 | end subroutine open_in_file_manager |
| 1505 | |
| 1506 | ! Show native delete confirmation dialog using system commands |
| 1507 | subroutine show_delete_confirmation(path, result) |
| 1508 | character(len=*), intent(in) :: path |
| 1509 | integer, intent(out) :: result |
| 1510 | character(len=2048) :: command |
| 1511 | integer :: status |
| 1512 | |
| 1513 | result = 0 ! Default to cancel |
| 1514 | |
| 1515 | if (is_macos()) then |
| 1516 | ! macOS: Use osascript to show native dialog |
| 1517 | command = 'osascript -e ''display dialog "Are you sure you want to delete:\n' & |
| 1518 | // trim(path) // '\n\nThis will move the item to Trash." ' & |
| 1519 | // 'buttons {"Cancel", "Delete"} default button "Cancel" ' & |
| 1520 | // 'with icon caution'' > /dev/null 2>&1' |
| 1521 | else |
| 1522 | ! Linux: Use zenity for confirmation dialog |
| 1523 | command = 'zenity --question --title="Confirm Delete" --text="Are you sure you want to delete:\n' & |
| 1524 | // trim(path) // '\n\nThis will move the item to Trash." 2>&1' |
| 1525 | end if |
| 1526 | |
| 1527 | print *, "Showing confirmation dialog for: ", trim(path) |
| 1528 | call execute_command_line(trim(command), exitstat=status) |
| 1529 | |
| 1530 | ! Both macOS and Linux: exit status 0 means confirmed |
| 1531 | if (status == 0) then |
| 1532 | result = 1 ! Confirmed |
| 1533 | end if |
| 1534 | |
| 1535 | print *, "Confirmation result: ", result |
| 1536 | end subroutine show_delete_confirmation |
| 1537 | |
| 1538 | ! Delete file or folder to system trash (macOS/Linux) |
| 1539 | subroutine delete_to_trash(path, selected_idx) |
| 1540 | use gtk, only: gtk_widget_queue_draw |
| 1541 | use treemap_renderer, only: remove_selected_node_from_view, invalidate_layout |
| 1542 | use treemap_widget, only: clear_selection |
| 1543 | character(len=*), intent(in) :: path |
| 1544 | integer, intent(in) :: selected_idx |
| 1545 | character(len=2048) :: command |
| 1546 | integer :: status |
| 1547 | |
| 1548 | if (is_macos()) then |
| 1549 | ! macOS: Use osascript to move to Trash via Finder |
| 1550 | command = 'osascript -e ''tell application "Finder" to delete POSIX file "' & |
| 1551 | // trim(path) // '"'' > /dev/null 2>&1' |
| 1552 | else |
| 1553 | ! Linux: Use gio trash (GNOME), fallback to trash-cli |
| 1554 | command = 'gio trash "' // trim(path) // '" 2>&1 || trash "' // trim(path) // '" 2>&1' |
| 1555 | end if |
| 1556 | |
| 1557 | print *, "Deleting to trash: ", trim(path) |
| 1558 | call execute_command_line(trim(command), exitstat=status) |
| 1559 | |
| 1560 | if (status == 0) then |
| 1561 | print *, "Successfully moved to trash: ", trim(path) |
| 1562 | |
| 1563 | ! Clear the selection first (before modifying tree) |
| 1564 | call clear_selection() |
| 1565 | |
| 1566 | ! Remove the node from the current view by marking it as deleted |
| 1567 | call remove_selected_node_from_view(selected_idx) |
| 1568 | |
| 1569 | ! Force layout recalculation |
| 1570 | call invalidate_layout() |
| 1571 | |
| 1572 | ! Trigger redraw to show the updated view |
| 1573 | if (c_associated(main_window_ptr)) then |
| 1574 | call gtk_widget_queue_draw(main_window_ptr) |
| 1575 | end if |
| 1576 | |
| 1577 | print *, "View updated - deleted node removed" |
| 1578 | else |
| 1579 | print *, "ERROR: Failed to move to trash (exit status: ", status, ")" |
| 1580 | print *, "You may need to delete manually or check permissions" |
| 1581 | end if |
| 1582 | end subroutine delete_to_trash |
| 1583 | |
| 1584 | ! Update status bar with scan information |
| 1585 | subroutine sniffly_update_status(message) |
| 1586 | character(len=*), intent(in) :: message |
| 1587 | ! Guard against accessing widgets during shutdown |
| 1588 | if (app_is_shutting_down) return |
| 1589 | if (c_associated(status_label_ptr)) then |
| 1590 | call gtk_label_set_text(status_label_ptr, trim(message)//c_null_char) |
| 1591 | end if |
| 1592 | end subroutine sniffly_update_status |
| 1593 | |
| 1594 | ! Timeout callback to clear status message (called after 5 seconds) |
| 1595 | function clear_status_message(user_data) bind(c) result(continue) |
| 1596 | type(c_ptr), value :: user_data |
| 1597 | integer(c_int) :: continue |
| 1598 | |
| 1599 | ! Clear the status message |
| 1600 | if (.not. app_is_shutting_down .and. c_associated(status_label_ptr)) then |
| 1601 | call gtk_label_set_text(status_label_ptr, ""//c_null_char) |
| 1602 | end if |
| 1603 | |
| 1604 | ! Return 0 to indicate the timeout should not repeat (one-shot) |
| 1605 | continue = 0_c_int |
| 1606 | end function clear_status_message |
| 1607 | |
| 1608 | ! Show error message in status bar, auto-dismiss after 5 seconds |
| 1609 | subroutine sniffly_show_error(message) |
| 1610 | character(len=*), intent(in) :: message |
| 1611 | integer(c_int) :: timeout_id |
| 1612 | |
| 1613 | ! Display the error message |
| 1614 | call sniffly_update_status(message) |
| 1615 | |
| 1616 | ! Schedule message to be cleared after 5 seconds |
| 1617 | ! Note: g_timeout_add_seconds_once is a one-shot timer that calls the callback once |
| 1618 | timeout_id = g_timeout_add_seconds_once(5_c_int, c_funloc(clear_status_message), c_null_ptr) |
| 1619 | end subroutine sniffly_show_error |
| 1620 | |
| 1621 | ! Update status bar with file count and size statistics |
| 1622 | subroutine sniffly_update_status_bar_stats() |
| 1623 | use types, only: file_node |
| 1624 | use treemap_renderer, only: get_current_view_node, get_node_count |
| 1625 | use iso_fortran_env, only: int64 |
| 1626 | type(file_node), pointer :: current_view |
| 1627 | integer :: item_count, total_files |
| 1628 | integer(int64) :: total_size |
| 1629 | character(len=256) :: status_text |
| 1630 | character(len=64) :: size_str |
| 1631 | real :: size_kb, size_mb, size_gb |
| 1632 | |
| 1633 | ! Guard against accessing widgets during shutdown |
| 1634 | if (app_is_shutting_down) return |
| 1635 | if (.not. c_associated(status_label_ptr)) return |
| 1636 | |
| 1637 | ! Get current view node |
| 1638 | current_view => get_current_view_node() |
| 1639 | if (.not. associated(current_view)) then |
| 1640 | call gtk_label_set_text(status_label_ptr, "No data"//c_null_char) |
| 1641 | return |
| 1642 | end if |
| 1643 | |
| 1644 | ! Get statistics from current view |
| 1645 | item_count = get_node_count() |
| 1646 | total_size = current_view%size |
| 1647 | |
| 1648 | ! Count total files recursively |
| 1649 | total_files = count_files_recursive(current_view) |
| 1650 | |
| 1651 | ! Format size nicely |
| 1652 | if (total_size < 1024_int64) then |
| 1653 | write(size_str, '(I0,A)') total_size, ' B' |
| 1654 | else if (total_size < 1024_int64**2) then |
| 1655 | size_kb = real(total_size) / 1024.0 |
| 1656 | write(size_str, '(F0.2,A)') size_kb, ' KB' |
| 1657 | else if (total_size < 1024_int64**3) then |
| 1658 | size_mb = real(total_size) / (1024.0**2) |
| 1659 | write(size_str, '(F0.2,A)') size_mb, ' MB' |
| 1660 | else |
| 1661 | size_gb = real(total_size) / (1024.0**3) |
| 1662 | write(size_str, '(F0.2,A)') size_gb, ' GB' |
| 1663 | end if |
| 1664 | |
| 1665 | ! Build status text |
| 1666 | write(status_text, '(I0,A,I0,A,A)') item_count, ' items (', total_files, ' files) - ', trim(size_str) |
| 1667 | |
| 1668 | ! Update status label |
| 1669 | call gtk_label_set_text(status_label_ptr, trim(status_text)//c_null_char) |
| 1670 | end subroutine sniffly_update_status_bar_stats |
| 1671 | |
| 1672 | ! Helper function to recursively count all files in a tree |
| 1673 | recursive function count_files_recursive(node) result(count) |
| 1674 | use types, only: file_node |
| 1675 | type(file_node), intent(in) :: node |
| 1676 | integer :: count, i |
| 1677 | |
| 1678 | count = 0 |
| 1679 | |
| 1680 | if (node%is_directory) then |
| 1681 | ! For directories, count all children recursively |
| 1682 | if (allocated(node%children)) then |
| 1683 | do i = 1, node%num_children |
| 1684 | count = count + count_files_recursive(node%children(i)) |
| 1685 | end do |
| 1686 | end if |
| 1687 | else |
| 1688 | ! For files, count this file |
| 1689 | count = 1 |
| 1690 | end if |
| 1691 | end function count_files_recursive |
| 1692 | |
| 1693 | ! Get forward path (if we navigated backwards and there's a forward history) |
| 1694 | function get_forward_path() result(fwd_path) |
| 1695 | type(tab_state), pointer :: tab |
| 1696 | character(len=512) :: fwd_path |
| 1697 | |
| 1698 | fwd_path = "" |
| 1699 | |
| 1700 | ! Get active tab |
| 1701 | tab => get_active_tab() |
| 1702 | if (.not. associated(tab)) then |
| 1703 | return |
| 1704 | end if |
| 1705 | |
| 1706 | if (tab%nav_history_pos > 0 .and. tab%nav_history_pos < tab%nav_history_count) then |
| 1707 | fwd_path = trim(tab%nav_history(tab%nav_history_pos + 1)) |
| 1708 | print *, "Forward path available: ", trim(fwd_path) |
| 1709 | end if |
| 1710 | end function get_forward_path |
| 1711 | |
| 1712 | ! Callback wrapper for navigation events (no arguments) |
| 1713 | subroutine breadcrumb_callback() |
| 1714 | use treemap_renderer, only: get_current_view_node |
| 1715 | use types, only: file_node |
| 1716 | type(file_node), pointer :: current_view |
| 1717 | type(tab_state), pointer :: tab |
| 1718 | character(len=512) :: fwd_path, prev_breadcrumb_path |
| 1719 | integer :: i, matched_pos, current_len |
| 1720 | logical :: was_navigating_history, is_breadcrumb_lookahead |
| 1721 | |
| 1722 | print *, "=== BREADCRUMB_CALLBACK ===" |
| 1723 | |
| 1724 | ! Get active tab |
| 1725 | tab => get_active_tab() |
| 1726 | if (.not. associated(tab)) then |
| 1727 | print *, "ERROR: No active tab in breadcrumb_callback" |
| 1728 | return |
| 1729 | end if |
| 1730 | |
| 1731 | print *, " navigating_history flag at entry: ", tab%navigating_history |
| 1732 | |
| 1733 | ! Initialize variables |
| 1734 | fwd_path = "" |
| 1735 | is_breadcrumb_lookahead = .false. |
| 1736 | |
| 1737 | ! Save flag state |
| 1738 | was_navigating_history = tab%navigating_history |
| 1739 | |
| 1740 | ! Sync tab scan_path with the current view node's path |
| 1741 | current_view => get_current_view_node() |
| 1742 | if (associated(current_view) .and. allocated(current_view%path)) then |
| 1743 | tab%scan_path = trim(current_view%path) |
| 1744 | |
| 1745 | ! Update tab label to reflect new path |
| 1746 | tab%label = get_path_basename(trim(tab%scan_path)) |
| 1747 | |
| 1748 | print *, " Synced tab scan_path to: ", trim(tab%scan_path) |
| 1749 | print *, " Updated tab label to: ", trim(tab%label) |
| 1750 | print *, " Current nav_history_pos: ", tab%nav_history_pos, " nav_history_count: ", tab%nav_history_count |
| 1751 | |
| 1752 | ! Check for breadcrumb-based lookahead first (when clicking up in breadcrumb) |
| 1753 | prev_breadcrumb_path = get_previous_breadcrumb_path() |
| 1754 | if (len_trim(prev_breadcrumb_path) > 0) then |
| 1755 | print *, " Previous breadcrumb path: ", trim(prev_breadcrumb_path) |
| 1756 | ! Check if current path is a prefix of previous path (navigating up) |
| 1757 | current_len = len_trim(tab%scan_path) |
| 1758 | if (len_trim(prev_breadcrumb_path) > current_len) then |
| 1759 | if (prev_breadcrumb_path(1:current_len) == tab%scan_path(1:current_len)) then |
| 1760 | ! We navigated to a parent directory via breadcrumb |
| 1761 | fwd_path = trim(prev_breadcrumb_path) |
| 1762 | is_breadcrumb_lookahead = .true. |
| 1763 | print *, " Breadcrumb-based lookahead detected: ", trim(fwd_path) |
| 1764 | ! Clear the saved path |
| 1765 | call clear_previous_breadcrumb_path() |
| 1766 | end if |
| 1767 | else |
| 1768 | ! Not a parent navigation, clear the saved path |
| 1769 | call clear_previous_breadcrumb_path() |
| 1770 | end if |
| 1771 | end if |
| 1772 | |
| 1773 | ! Check if this path matches any entry in history (for breadcrumb clicks) |
| 1774 | ! This syncs nav_history_pos with breadcrumb navigation |
| 1775 | ! Only do this if we're NOT already in a history navigation (back/forward button) |
| 1776 | if (.not. was_navigating_history .and. tab%nav_history_count > 0) then |
| 1777 | matched_pos = 0 |
| 1778 | do i = 1, tab%nav_history_count |
| 1779 | if (trim(tab%nav_history(i)) == trim(tab%scan_path)) then |
| 1780 | matched_pos = i |
| 1781 | print *, " Found path in history at position ", i |
| 1782 | exit |
| 1783 | end if |
| 1784 | end do |
| 1785 | |
| 1786 | if (matched_pos > 0 .and. matched_pos /= tab%nav_history_pos) then |
| 1787 | print *, " Breadcrumb navigation: syncing history pos from ", tab%nav_history_pos, " to ", matched_pos |
| 1788 | tab%nav_history_pos = matched_pos |
| 1789 | tab%navigating_history = .true. ! Mark as history navigation to skip add_to_history |
| 1790 | else if (matched_pos > 0) then |
| 1791 | print *, " Path matches current history position - no sync needed" |
| 1792 | else |
| 1793 | print *, " Path not found in history - will add as new entry" |
| 1794 | end if |
| 1795 | end if |
| 1796 | |
| 1797 | ! Get history-based forward path (if available and no breadcrumb lookahead) |
| 1798 | if (len_trim(fwd_path) == 0) then |
| 1799 | fwd_path = get_forward_path() |
| 1800 | if (len_trim(fwd_path) > 0) then |
| 1801 | print *, " History-based forward path available: ", trim(fwd_path) |
| 1802 | else |
| 1803 | print *, " No forward path available" |
| 1804 | end if |
| 1805 | end if |
| 1806 | |
| 1807 | ! Update breadcrumb widget with new path and forward lookahead |
| 1808 | if (len_trim(fwd_path) > 0) then |
| 1809 | call update_breadcrumb_cache(trim(current_view%path), trim(fwd_path)) |
| 1810 | else |
| 1811 | call update_breadcrumb_cache(trim(current_view%path)) |
| 1812 | end if |
| 1813 | end if |
| 1814 | |
| 1815 | call sniffly_update_status_bar_stats() |
| 1816 | |
| 1817 | ! Add to history if this is a new navigation |
| 1818 | ! Skip only if: (1) using back/forward buttons OR (2) history-based forward path exists |
| 1819 | ! BUT: breadcrumb-based lookahead IS a new navigation and should be added! |
| 1820 | if (.not. tab%navigating_history .and. (len_trim(fwd_path) == 0 .or. is_breadcrumb_lookahead)) then |
| 1821 | ! New navigation (including breadcrumb navigation with lookahead) |
| 1822 | if (len_trim(tab%scan_path) > 0) then |
| 1823 | print *, " Calling add_to_history (new navigation, breadcrumb_lookahead=", is_breadcrumb_lookahead, ")" |
| 1824 | call add_to_history(tab%scan_path) |
| 1825 | end if |
| 1826 | else |
| 1827 | if (tab%navigating_history) then |
| 1828 | print *, " Skipping add_to_history (history navigation mode)" |
| 1829 | else |
| 1830 | print *, " Skipping add_to_history (history-based forward context exists)" |
| 1831 | end if |
| 1832 | end if |
| 1833 | |
| 1834 | ! ALWAYS reset the flag at the end (ensure it doesn't stick) |
| 1835 | print *, " Resetting navigating_history flag to false" |
| 1836 | tab%navigating_history = .false. |
| 1837 | |
| 1838 | ! Update button states now that history may have changed |
| 1839 | call update_history_buttons() |
| 1840 | |
| 1841 | ! Refresh tab bar to show updated label |
| 1842 | call refresh_tab_bar() |
| 1843 | call update_tab_visual_states() |
| 1844 | end subroutine breadcrumb_callback |
| 1845 | |
| 1846 | ! Show progress bar (now just resets to prepare for updates) |
| 1847 | subroutine sniffly_show_progress() |
| 1848 | ! Guard against accessing widgets during shutdown |
| 1849 | if (app_is_shutting_down) return |
| 1850 | if (c_associated(progress_bar_ptr)) then |
| 1851 | call gtk_progress_bar_set_fraction(progress_bar_ptr, 0.0_c_double) |
| 1852 | call gtk_progress_bar_set_text(progress_bar_ptr, "0%"//c_null_char) |
| 1853 | end if |
| 1854 | ! Update cancel button when progress bar is shown (scan starting) |
| 1855 | call update_cancel_scan_button_state() |
| 1856 | end subroutine sniffly_show_progress |
| 1857 | |
| 1858 | ! Hide progress bar (now just resets to 0%) |
| 1859 | subroutine sniffly_hide_progress() |
| 1860 | ! Guard against accessing widgets during shutdown |
| 1861 | if (app_is_shutting_down) return |
| 1862 | if (c_associated(progress_bar_ptr)) then |
| 1863 | call gtk_progress_bar_set_fraction(progress_bar_ptr, 0.0_c_double) |
| 1864 | call gtk_progress_bar_set_text(progress_bar_ptr, ""//c_null_char) |
| 1865 | end if |
| 1866 | ! Update cancel button when progress bar is hidden (scan likely stopped) |
| 1867 | call update_cancel_scan_button_state() |
| 1868 | end subroutine sniffly_hide_progress |
| 1869 | |
| 1870 | ! Update progress bar and status text |
| 1871 | ! fraction: 0.0 to 1.0 |
| 1872 | ! message: status text to show |
| 1873 | subroutine sniffly_update_progress(fraction, message) |
| 1874 | real(c_double), intent(in) :: fraction |
| 1875 | character(len=*), intent(in) :: message |
| 1876 | character(len=32) :: percent_str |
| 1877 | integer :: percent_int |
| 1878 | |
| 1879 | ! Guard against accessing widgets during shutdown |
| 1880 | if (app_is_shutting_down) return |
| 1881 | |
| 1882 | if (c_associated(progress_bar_ptr)) then |
| 1883 | ! Update progress bar fraction |
| 1884 | call gtk_progress_bar_set_fraction(progress_bar_ptr, fraction) |
| 1885 | |
| 1886 | ! Set percentage text |
| 1887 | percent_int = int(fraction * 100.0_c_double) |
| 1888 | write(percent_str, '(I0,A)') percent_int, '%' |
| 1889 | call gtk_progress_bar_set_text(progress_bar_ptr, trim(percent_str)//c_null_char) |
| 1890 | end if |
| 1891 | |
| 1892 | ! Update status label |
| 1893 | if (c_associated(status_label_ptr)) then |
| 1894 | call gtk_label_set_text(status_label_ptr, trim(message)//c_null_char) |
| 1895 | end if |
| 1896 | |
| 1897 | ! Note: We update widgets but GTK will handle rendering in its own event loop |
| 1898 | ! Frequent calls to this function will keep the UI updated |
| 1899 | end subroutine sniffly_update_progress |
| 1900 | |
| 1901 | ! Callback wrapper for quit events (no arguments) |
| 1902 | subroutine quit_callback_wrapper() |
| 1903 | use progressive_scanner, only: stop_progressive_scan |
| 1904 | ! Stop any active scans before quitting |
| 1905 | call stop_progressive_scan() |
| 1906 | call sniffly_app_quit() |
| 1907 | end subroutine quit_callback_wrapper |
| 1908 | |
| 1909 | ! Callback wrapper for delete events (no arguments) |
| 1910 | subroutine delete_callback_wrapper() |
| 1911 | use treemap_widget, only: get_selected_index |
| 1912 | character(len=:), allocatable :: selected_path |
| 1913 | integer :: confirm_result, selected_idx |
| 1914 | |
| 1915 | print *, "Delete callback triggered from keyboard" |
| 1916 | |
| 1917 | ! Check if there's a selection |
| 1918 | if (.not. has_selection()) then |
| 1919 | print *, "No selection - cannot delete" |
| 1920 | return |
| 1921 | end if |
| 1922 | |
| 1923 | ! Get the selected node path and index |
| 1924 | selected_path = get_selected_node_path() |
| 1925 | selected_idx = get_selected_index() |
| 1926 | |
| 1927 | if (len_trim(selected_path) == 0) then |
| 1928 | print *, "Invalid selection path" |
| 1929 | return |
| 1930 | end if |
| 1931 | |
| 1932 | print *, "Preparing to delete: ", trim(selected_path) |
| 1933 | |
| 1934 | ! Show confirmation dialog |
| 1935 | call show_delete_confirmation(selected_path, confirm_result) |
| 1936 | |
| 1937 | if (confirm_result == 1) then |
| 1938 | print *, "Delete confirmed - proceeding" |
| 1939 | call delete_to_trash(selected_path, selected_idx) |
| 1940 | else |
| 1941 | print *, "Delete cancelled by user" |
| 1942 | end if |
| 1943 | end subroutine delete_callback_wrapper |
| 1944 | |
| 1945 | ! Callback wrapper for force refresh events (clear cache and rescan) |
| 1946 | subroutine refresh_callback_wrapper() |
| 1947 | use treemap_renderer, only: clear_cache, invalidate_layout |
| 1948 | type(tab_state), pointer :: tab |
| 1949 | |
| 1950 | print *, "Force refresh triggered from keyboard shortcut" |
| 1951 | |
| 1952 | ! Get active tab |
| 1953 | tab => get_active_tab() |
| 1954 | if (.not. associated(tab)) then |
| 1955 | print *, "ERROR: No active tab in refresh_callback_wrapper" |
| 1956 | return |
| 1957 | end if |
| 1958 | |
| 1959 | ! Clear the directory cache |
| 1960 | call clear_cache() |
| 1961 | call invalidate_layout() |
| 1962 | |
| 1963 | ! Update status |
| 1964 | call sniffly_update_status("Clearing cache and rescanning...") |
| 1965 | |
| 1966 | ! Trigger rescan if we have a path |
| 1967 | if (len_trim(tab%scan_path) > 0) then |
| 1968 | call trigger_rescan(tab%scan_path) |
| 1969 | else |
| 1970 | call sniffly_show_error("No directory to scan") |
| 1971 | end if |
| 1972 | end subroutine refresh_callback_wrapper |
| 1973 | |
| 1974 | ! Callback wrapper for scan completion |
| 1975 | subroutine scan_complete_callback_wrapper() |
| 1976 | use treemap_widget, only: mark_initial_scan_complete |
| 1977 | use treemap_renderer, only: get_root_node, get_current_view_node |
| 1978 | use types, only: file_node |
| 1979 | type(tab_state), pointer :: tab |
| 1980 | type(file_node), pointer :: root, current_view |
| 1981 | |
| 1982 | print *, "=== SCAN COMPLETE CALLBACK FIRED ===" |
| 1983 | |
| 1984 | ! Call the original completion callback |
| 1985 | call mark_initial_scan_complete() |
| 1986 | |
| 1987 | ! Sync active tab's state with renderer (tab now has data) |
| 1988 | tab => get_active_tab() |
| 1989 | if (associated(tab)) then |
| 1990 | root => get_root_node() |
| 1991 | current_view => get_current_view_node() |
| 1992 | if (associated(root)) then |
| 1993 | tab%has_data = .true. |
| 1994 | tab%root_node => root |
| 1995 | tab%current_view_node => current_view |
| 1996 | print *, "=== SYNCED TAB STATE AFTER SCAN COMPLETE ===" |
| 1997 | end if |
| 1998 | end if |
| 1999 | |
| 2000 | ! Update cancel button (scan is done, should be disabled and grey) |
| 2001 | print *, "=== UPDATING CANCEL BUTTON FROM COMPLETION CALLBACK ===" |
| 2002 | call update_cancel_scan_button_state() |
| 2003 | |
| 2004 | ! Re-enable back/forward buttons if there's history |
| 2005 | print *, "=== RE-ENABLING NAVIGATION BUTTONS ===" |
| 2006 | call update_history_buttons() |
| 2007 | |
| 2008 | ! Update UI for active tab (removes blue pulsing if tab now has data) |
| 2009 | print *, "=== UPDATING UI FOR ACTIVE TAB (SCAN COMPLETE) ===" |
| 2010 | call update_ui_for_active_tab() |
| 2011 | |
| 2012 | ! Complete any pending synthetic navigation |
| 2013 | if (pending_synthetic_nav) then |
| 2014 | print *, "=== PENDING SYNTHETIC NAV - COMPLETING ===" |
| 2015 | call complete_synthetic_navigation() |
| 2016 | end if |
| 2017 | end subroutine scan_complete_callback_wrapper |
| 2018 | |
| 2019 | ! Trigger a rescan of the given directory (for UI buttons) |
| 2020 | subroutine trigger_rescan(path) |
| 2021 | use gtk, only: gtk_widget_queue_draw |
| 2022 | use g, only: g_main_context_default, g_main_context_iteration |
| 2023 | use treemap_renderer, only: invalidate_layout |
| 2024 | character(len=*), intent(in) :: path |
| 2025 | character(len=:), allocatable :: normalized_path |
| 2026 | type(c_ptr) :: context |
| 2027 | integer :: i |
| 2028 | integer :: path_len |
| 2029 | |
| 2030 | print *, "=== TRIGGER_RESCAN ENTERED ===" |
| 2031 | print *, "Triggering rescan of: '", trim(path), "'" |
| 2032 | print *, "Path length: ", len_trim(path) |
| 2033 | |
| 2034 | ! Remove trailing slash if present (C code doesn't like it) |
| 2035 | path_len = len_trim(path) |
| 2036 | if (path_len > 1 .and. path(path_len:path_len) == '/') then |
| 2037 | normalized_path = trim(path(1:path_len-1)) |
| 2038 | print *, "DEBUG: Removed trailing slash. New path: '", normalized_path, "'" |
| 2039 | else |
| 2040 | normalized_path = trim(path) |
| 2041 | end if |
| 2042 | |
| 2043 | ! Process pending GTK events before starting scan |
| 2044 | context = g_main_context_default() |
| 2045 | do i = 1, 10 |
| 2046 | do while (g_main_context_iteration(context, 0_c_int) /= 0_c_int) |
| 2047 | end do |
| 2048 | end do |
| 2049 | |
| 2050 | print *, "=== ABOUT TO CALL scan_directory ===" |
| 2051 | ! Scan the directory (this will show progress via callbacks) |
| 2052 | call scan_directory(normalized_path) |
| 2053 | print *, "=== RETURNED FROM scan_directory ===" |
| 2054 | |
| 2055 | ! Update button states (cancel button enabled, navigation disabled during scan) |
| 2056 | call update_cancel_scan_button_state() |
| 2057 | call update_history_buttons() |
| 2058 | |
| 2059 | ! Process events after scan to update UI |
| 2060 | do i = 1, 10 |
| 2061 | do while (g_main_context_iteration(context, 0_c_int) /= 0_c_int) |
| 2062 | end do |
| 2063 | end do |
| 2064 | |
| 2065 | ! Invalidate layout to force recalculation |
| 2066 | call invalidate_layout() |
| 2067 | |
| 2068 | ! Update breadcrumbs and status via callback (handles forward path lookahead) |
| 2069 | call breadcrumb_callback() |
| 2070 | |
| 2071 | ! Trigger redraw to show the scanned data |
| 2072 | if (c_associated(main_window_ptr)) then |
| 2073 | call gtk_widget_queue_draw(main_window_ptr) |
| 2074 | end if |
| 2075 | |
| 2076 | print *, "=== RESCAN COMPLETE ===" |
| 2077 | end subroutine trigger_rescan |
| 2078 | |
| 2079 | ! Idle callback for async initial scan |
| 2080 | function perform_initial_scan(user_data) bind(c) result(continue) |
| 2081 | use gtk, only: gtk_widget_queue_draw |
| 2082 | use treemap_renderer, only: invalidate_layout |
| 2083 | type(c_ptr), value :: user_data |
| 2084 | integer(c_int) :: continue |
| 2085 | |
| 2086 | print *, "Performing initial scan in idle callback..." |
| 2087 | call scan_directory(pending_scan_path) |
| 2088 | |
| 2089 | ! Note: mark_initial_scan_complete() is now called by the progressive scanner |
| 2090 | ! when the scan actually completes (not immediately when it starts) |
| 2091 | |
| 2092 | ! Invalidate layout to force recalculation |
| 2093 | call invalidate_layout() |
| 2094 | |
| 2095 | ! Update breadcrumbs after scan (use pending_scan_path) |
| 2096 | if (len_trim(pending_scan_path) > 0) then |
| 2097 | call update_breadcrumb_cache(trim(pending_scan_path)) |
| 2098 | end if |
| 2099 | |
| 2100 | ! Update status bar with file statistics |
| 2101 | call sniffly_update_status_bar_stats() |
| 2102 | |
| 2103 | ! Trigger redraw to show the scanned data |
| 2104 | if (c_associated(main_window_ptr)) then |
| 2105 | call gtk_widget_queue_draw(main_window_ptr) |
| 2106 | end if |
| 2107 | |
| 2108 | ! Return 0 to indicate this callback should not be called again |
| 2109 | continue = 0_c_int |
| 2110 | end function perform_initial_scan |
| 2111 | |
| 2112 | ! Get user's home directory |
| 2113 | function get_home_directory() result(home_path) |
| 2114 | character(len=512) :: home_path |
| 2115 | character(len=512) :: env_value |
| 2116 | integer :: status |
| 2117 | |
| 2118 | ! Try to get HOME environment variable |
| 2119 | call get_environment_variable("HOME", env_value, status=status) |
| 2120 | if (status == 0) then |
| 2121 | home_path = trim(env_value) |
| 2122 | else |
| 2123 | ! Fallback to /Users/username on macOS or /home/username on Linux |
| 2124 | home_path = "/Users" |
| 2125 | end if |
| 2126 | end function get_home_directory |
| 2127 | |
| 2128 | end module gtk_app |
| 2129 |