Fortran · 34763 bytes Raw Blame History
1 ! Treemap Widget Module for Sniffly
2 ! Custom GtkDrawingArea widget for rendering treemap visualization
3 module treemap_widget
4 use, intrinsic :: iso_c_binding
5 use gtk, only: gtk_drawing_area_new, gtk_drawing_area_set_draw_func, &
6 gtk_widget_set_size_request, gtk_event_controller_motion_new, &
7 gtk_widget_add_controller, g_signal_connect, &
8 gtk_gesture_click_new, gtk_widget_queue_draw, gtk_gesture_single_get_current_button, &
9 gtk_event_controller_key_new, gtk_widget_set_focusable, &
10 gtk_widget_set_has_tooltip, gtk_tooltip_set_text, gtk_tooltip_set_tip_area, &
11 gtk_popover_new, gtk_popover_set_child, gtk_popover_popup, gtk_popover_popdown, &
12 gtk_popover_set_has_arrow, gtk_widget_set_parent, gtk_box_new, &
13 gtk_button_new_with_label, gtk_box_append, gtk_label_new, GTK_ORIENTATION_VERTICAL
14 use treemap_renderer, only: scan_and_render, init_renderer, scan_and_render_with_hover, &
15 get_current_view_node
16 implicit none
17 private
18
19 public :: create_treemap_widget, set_scan_path, get_widget_ptr, register_navigation_callback, &
20 register_key_handler, register_quit_callback, register_delete_callback, &
21 register_refresh_callback, register_selection_callback, mark_initial_scan_complete, &
22 get_selected_node_path, has_selection, get_selected_index, clear_selection
23
24 ! Callback interface for navigation events
25 abstract interface
26 subroutine navigation_callback()
27 end subroutine navigation_callback
28 end interface
29
30 ! Callback interface for quit events
31 abstract interface
32 subroutine quit_callback()
33 end subroutine quit_callback
34 end interface
35
36 ! Callback interface for delete events
37 abstract interface
38 subroutine delete_callback()
39 end subroutine delete_callback
40 end interface
41
42 ! Callback interface for force refresh events
43 abstract interface
44 subroutine refresh_callback()
45 end subroutine refresh_callback
46 end interface
47
48 ! Callback interface for selection change events
49 abstract interface
50 subroutine selection_callback()
51 end subroutine selection_callback
52 end interface
53
54 ! GDK Key constants
55 integer(c_int), parameter :: GDK_KEY_Return = 65293_c_int ! Enter key
56 integer(c_int), parameter :: GDK_KEY_BackSpace = 65288_c_int ! Backspace key
57 integer(c_int), parameter :: GDK_KEY_Left = 65361_c_int ! Left arrow
58 integer(c_int), parameter :: GDK_KEY_Right = 65363_c_int ! Right arrow
59 integer(c_int), parameter :: GDK_KEY_Up = 65362_c_int ! Up arrow
60 integer(c_int), parameter :: GDK_KEY_Down = 65364_c_int ! Down arrow
61 integer(c_int), parameter :: GDK_KEY_space = 32_c_int ! Spacebar
62 integer(c_int), parameter :: GDK_KEY_period = 46_c_int ! Period key
63 integer(c_int), parameter :: GDK_KEY_q = 113_c_int ! q key
64 integer(c_int), parameter :: GDK_KEY_d = 100_c_int ! d key
65 integer(c_int), parameter :: GDK_KEY_r = 114_c_int ! r key
66
67 ! GDK Modifier masks
68 integer(c_int), parameter :: GDK_SHIFT_MASK = 1_c_int ! Shift key
69 integer(c_int), parameter :: GDK_CONTROL_MASK = 4_c_int ! Control key
70 integer(c_int), parameter :: GDK_META_MASK = 268435456_c_int ! Cmd key (macOS)
71
72 ! Widget state (will expand later)
73 type(c_ptr), save :: widget_ptr = c_null_ptr
74 character(len=512), save :: scan_path = ""
75
76 ! Mouse interaction state
77 real(c_double), save :: mouse_x = -1.0_c_double
78 real(c_double), save :: mouse_y = -1.0_c_double
79
80 ! Keyboard/Mouse hover mode
81 logical, save :: keyboard_mode = .false. ! True when using keyboard navigation
82 integer, save :: keyboard_hover_index = 0 ! Index of keyboard-hovered node
83
84 ! Selection state
85 integer, save :: selected_index = 0 ! 0 = no selection
86
87 ! Initial scan state
88 logical, save :: initial_scan_complete = .false.
89
90 ! Navigation callback (called when user navigates)
91 procedure(navigation_callback), pointer, save :: nav_callback => null()
92
93 ! Quit callback (called when user wants to quit)
94 procedure(quit_callback), pointer, save :: quit_cb => null()
95
96 ! Delete callback (called when user wants to delete)
97 procedure(delete_callback), pointer, save :: delete_cb => null()
98
99 ! Force refresh callback (called when user wants to force refresh with cache clear)
100 procedure(refresh_callback), pointer, save :: refresh_cb => null()
101
102 ! Selection callback (called when selection changes)
103 procedure(selection_callback), pointer, save :: selection_cb => null()
104
105 contains
106
107 ! Create and initialize the treemap drawing area widget
108 function create_treemap_widget() result(widget)
109 type(c_ptr) :: widget, motion_controller, click_controller
110
111 ! Initialize renderer
112 call init_renderer()
113
114 ! Create drawing area
115 widget = gtk_drawing_area_new()
116 widget_ptr = widget
117
118 if (.not. c_associated(widget)) then
119 print *, "ERROR: Failed to create drawing area"
120 return
121 end if
122
123 ! Set minimum size (will expand to fill window)
124 call gtk_widget_set_size_request(widget, 800_c_int, 600_c_int)
125
126 ! Make widget focusable to receive keyboard events
127 call gtk_widget_set_focusable(widget, 1_c_int)
128
129 ! Enable tooltips
130 call gtk_widget_set_has_tooltip(widget, 1_c_int)
131 call g_signal_connect(widget, "query-tooltip"//c_null_char, &
132 c_funloc(on_query_tooltip), c_null_ptr)
133
134 ! Set draw function (called when widget needs to redraw)
135 call gtk_drawing_area_set_draw_func(widget, &
136 c_funloc(on_draw), &
137 c_null_ptr, &
138 c_null_funptr)
139
140 ! Add motion event controller for hover
141 motion_controller = gtk_event_controller_motion_new()
142 call g_signal_connect(motion_controller, "motion"//c_null_char, &
143 c_funloc(on_motion), c_null_ptr)
144 call gtk_widget_add_controller(widget, motion_controller)
145
146 ! Add click gesture controller for selection
147 click_controller = gtk_gesture_click_new()
148 call g_signal_connect(click_controller, "pressed"//c_null_char, &
149 c_funloc(on_click), c_null_ptr)
150 call gtk_widget_add_controller(widget, click_controller)
151
152 print *, "Treemap widget created successfully"
153 end function create_treemap_widget
154
155 ! Register keyboard handler (called from gtk_app after window is created)
156 subroutine register_key_handler(window)
157 use gtk, only: gtk_event_controller_key_new, g_signal_connect, gtk_widget_add_controller
158 type(c_ptr), value :: window
159 type(c_ptr) :: key_controller
160
161 ! Add keyboard event controller to window for navigation
162 key_controller = gtk_event_controller_key_new()
163 call g_signal_connect(key_controller, "key-pressed"//c_null_char, &
164 c_funloc(on_key_press), c_null_ptr)
165 call gtk_widget_add_controller(window, key_controller)
166
167 print *, "Key handler registered on window"
168 end subroutine register_key_handler
169
170 ! Get widget pointer (for triggering redraws)
171 function get_widget_ptr() result(ptr)
172 type(c_ptr) :: ptr
173 ptr = widget_ptr
174 end function get_widget_ptr
175
176 ! Set the directory path to scan
177 subroutine set_scan_path(path)
178 character(len=*), intent(in) :: path
179 scan_path = trim(path)
180 print *, "Scan path set to: ", trim(scan_path)
181 end subroutine set_scan_path
182
183 ! Register a callback to be called when navigation occurs
184 subroutine register_navigation_callback(callback)
185 procedure(navigation_callback) :: callback
186 nav_callback => callback
187 print *, "Navigation callback registered"
188 end subroutine register_navigation_callback
189
190 ! Register a callback to be called when user wants to quit
191 subroutine register_quit_callback(callback)
192 procedure(quit_callback) :: callback
193 quit_cb => callback
194 print *, "Quit callback registered"
195 end subroutine register_quit_callback
196
197 ! Register a callback to be called when user wants to delete
198 subroutine register_delete_callback(callback)
199 procedure(delete_callback) :: callback
200 delete_cb => callback
201 print *, "Delete callback registered"
202 end subroutine register_delete_callback
203
204 ! Register a callback to be called when user wants to force refresh
205 subroutine register_refresh_callback(callback)
206 procedure(refresh_callback) :: callback
207 refresh_cb => callback
208 print *, "Force refresh callback registered"
209 end subroutine register_refresh_callback
210
211 ! Register a callback to be called when selection changes
212 subroutine register_selection_callback(callback)
213 procedure(selection_callback) :: callback
214 selection_cb => callback
215 print *, "Selection callback registered"
216 end subroutine register_selection_callback
217
218 ! Mark that the initial scan has completed
219 subroutine mark_initial_scan_complete()
220 initial_scan_complete = .true.
221 print *, "Initial scan marked as complete"
222 end subroutine mark_initial_scan_complete
223
224 ! Motion callback - track mouse position for hover
225 subroutine on_motion(controller, x, y, user_data) bind(c)
226 use gtk, only: gtk_widget_grab_focus
227 type(c_ptr), value :: controller, user_data
228 real(c_double), value :: x, y
229 integer(c_int) :: focus_result
230
231 ! Update mouse position
232 mouse_x = x
233 mouse_y = y
234
235 ! Switch to mouse mode (mouse takes over from keyboard)
236 keyboard_mode = .false.
237
238 ! Grab focus to ensure keyboard events are received
239 if (c_associated(widget_ptr)) then
240 focus_result = gtk_widget_grab_focus(widget_ptr)
241 call gtk_widget_queue_draw(widget_ptr)
242 end if
243 end subroutine on_motion
244
245 ! Click callback - handle rectangle selection and navigation
246 subroutine on_click(gesture, n_press, x, y, user_data) bind(c)
247 use treemap_renderer, only: find_node_at_position, navigate_into_node
248 use gtk, only: gtk_gesture_single_get_current_button
249 type(c_ptr), value :: gesture, user_data
250 integer(c_int), value :: n_press
251 real(c_double), value :: x, y
252 integer :: clicked_index
253 integer(c_int) :: button
254
255 ! Get which mouse button was pressed (1=left, 2=middle, 3=right)
256 button = gtk_gesture_single_get_current_button(gesture)
257
258 ! Find which node was clicked
259 clicked_index = find_node_at_position(x, y)
260
261 ! Right-click (button 3): show context menu
262 if (button == 3_c_int) then
263 if (clicked_index > 0) then
264 ! Select the node and show context menu
265 call set_selection(clicked_index)
266 print *, "Right-click on node: ", selected_index
267 call show_context_menu(x, y)
268 end if
269 ! Trigger redraw
270 if (c_associated(widget_ptr)) then
271 call gtk_widget_queue_draw(widget_ptr)
272 end if
273 return
274 end if
275
276 ! Left-click (button 1): normal selection/navigation
277 if (clicked_index > 0) then
278 ! Check if this is a double-click (n_press == 2)
279 if (n_press == 2) then
280 ! Double-click: navigate into the directory
281 print *, "Double-click detected! Navigating into node: ", clicked_index
282 call navigate_into_node(clicked_index)
283 call set_selection(0) ! Clear selection after navigation
284
285 ! Call navigation callback to update breadcrumbs
286 if (associated(nav_callback)) then
287 call nav_callback()
288 end if
289 else
290 ! Single click: toggle selection
291 if (selected_index == clicked_index) then
292 ! Clicking same item again - deselect it
293 call set_selection(0)
294 print *, "Deselected by clicking same item"
295 else
296 ! Clicking different item - select it
297 call set_selection(clicked_index)
298 print *, "Selected node index: ", selected_index
299 end if
300 end if
301 else
302 ! Click outside any node - deselect
303 call set_selection(0)
304 print *, "Deselected by clicking empty space"
305 end if
306
307 ! Trigger redraw to show changes
308 if (c_associated(widget_ptr)) then
309 call gtk_widget_queue_draw(widget_ptr)
310 end if
311 end subroutine on_click
312
313 ! Show context menu at given coordinates
314 subroutine show_context_menu(x, y)
315 use gtk, only: gtk_popover_new, gtk_box_new, gtk_button_new_with_label, gtk_box_append, &
316 gtk_popover_set_child, gtk_popover_popup, gtk_popover_set_has_arrow, &
317 gtk_widget_set_parent, GTK_ORIENTATION_VERTICAL
318 use types, only: file_node
319 use treemap_renderer, only: get_current_view_node
320 real(c_double), intent(in) :: x, y
321 type(c_ptr) :: popover, menu_box, btn_navigate, btn_open, btn_terminal, btn_separator1
322 type(c_ptr) :: btn_copy_path, btn_copy_name, btn_separator2, btn_info, btn_refresh
323 type(c_ptr) :: btn_separator3, btn_delete, separator_label
324 type(file_node), pointer :: current_view
325 logical :: is_directory
326
327 print *, "Showing context menu at (", x, ",", y, ")"
328
329 ! Check if selected node is a directory
330 current_view => get_current_view_node()
331 is_directory = .false.
332 if (associated(current_view) .and. allocated(current_view%children)) then
333 if (selected_index > 0 .and. selected_index <= current_view%num_children) then
334 is_directory = current_view%children(selected_index)%is_directory
335 end if
336 end if
337
338 ! Create popover menu
339 popover = gtk_popover_new()
340 call gtk_widget_set_parent(popover, widget_ptr)
341 call gtk_popover_set_has_arrow(popover, 0_c_int) ! No arrow
342
343 ! Create vertical box for menu items
344 menu_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0_c_int)
345
346 ! Navigate Into (directories only)
347 if (is_directory) then
348 btn_navigate = gtk_button_new_with_label("Navigate Into"//c_null_char)
349 call g_signal_connect(btn_navigate, "clicked"//c_null_char, &
350 c_funloc(on_context_navigate), popover)
351 call gtk_box_append(menu_box, btn_navigate)
352 end if
353
354 ! Open in Finder/File Manager
355 btn_open = gtk_button_new_with_label("Open in Finder"//c_null_char)
356 call g_signal_connect(btn_open, "clicked"//c_null_char, &
357 c_funloc(on_context_open_finder), popover)
358 call gtk_box_append(menu_box, btn_open)
359
360 ! Open in Terminal (directories only)
361 if (is_directory) then
362 btn_terminal = gtk_button_new_with_label("Open in Terminal"//c_null_char)
363 call g_signal_connect(btn_terminal, "clicked"//c_null_char, &
364 c_funloc(on_context_open_terminal), popover)
365 call gtk_box_append(menu_box, btn_terminal)
366 end if
367
368 ! Separator
369 separator_label = gtk_label_new("─────────────"//c_null_char)
370 call gtk_box_append(menu_box, separator_label)
371
372 ! Copy Path
373 btn_copy_path = gtk_button_new_with_label("Copy Path"//c_null_char)
374 call g_signal_connect(btn_copy_path, "clicked"//c_null_char, &
375 c_funloc(on_context_copy_path), popover)
376 call gtk_box_append(menu_box, btn_copy_path)
377
378 ! Copy Name
379 btn_copy_name = gtk_button_new_with_label("Copy Name"//c_null_char)
380 call g_signal_connect(btn_copy_name, "clicked"//c_null_char, &
381 c_funloc(on_context_copy_name), popover)
382 call gtk_box_append(menu_box, btn_copy_name)
383
384 ! Separator
385 separator_label = gtk_label_new("─────────────"//c_null_char)
386 call gtk_box_append(menu_box, separator_label)
387
388 ! Properties/Info
389 btn_info = gtk_button_new_with_label("Properties/Info"//c_null_char)
390 call g_signal_connect(btn_info, "clicked"//c_null_char, &
391 c_funloc(on_context_info), popover)
392 call gtk_box_append(menu_box, btn_info)
393
394 ! Refresh This Folder (directories only)
395 if (is_directory) then
396 btn_refresh = gtk_button_new_with_label("Refresh This Folder"//c_null_char)
397 call g_signal_connect(btn_refresh, "clicked"//c_null_char, &
398 c_funloc(on_context_refresh), popover)
399 call gtk_box_append(menu_box, btn_refresh)
400 end if
401
402 ! Separator
403 separator_label = gtk_label_new("─────────────"//c_null_char)
404 call gtk_box_append(menu_box, separator_label)
405
406 ! Delete to Trash
407 btn_delete = gtk_button_new_with_label("Delete to Trash..."//c_null_char)
408 call g_signal_connect(btn_delete, "clicked"//c_null_char, &
409 c_funloc(on_context_delete), popover)
410 call gtk_box_append(menu_box, btn_delete)
411
412 ! Set menu box as popover child and show
413 call gtk_popover_set_child(popover, menu_box)
414 call gtk_popover_popup(popover)
415
416 print *, "Context menu created and shown"
417 end subroutine show_context_menu
418
419 ! Draw callback - this is where we render the treemap!
420 subroutine on_draw(area, cr, width, height, user_data) bind(c)
421 use treemap_renderer, only: scan_and_render_with_interaction
422 type(c_ptr), value :: area, cr, user_data
423 integer(c_int), value :: width, height
424 logical, save :: first_render = .true.
425
426 ! Skip rendering if initial scan hasn't completed yet
427 if (.not. initial_scan_complete) then
428 print *, "Skipping render - waiting for initial scan to complete"
429 return
430 end if
431
432 ! Skip rendering if no scan path set (empty tab - show blank canvas)
433 if (len_trim(scan_path) == 0) then
434 print *, "Skipping render - no scan path set (empty tab)"
435 return
436 end if
437
438 ! Render the actual treemap with hover and selection
439 ! mouse_x and mouse_y are updated by both mouse motion and arrow keys
440 call scan_and_render_with_interaction(cr, width, height, mouse_x, mouse_y, &
441 selected_index, trim(scan_path))
442
443 ! Update breadcrumbs after first render (when scan is complete)
444 if (first_render) then
445 first_render = .false.
446 print *, "First render complete - updating breadcrumbs"
447 if (associated(nav_callback)) then
448 call nav_callback()
449 else
450 print *, "WARNING: nav_callback not associated!"
451 end if
452 end if
453
454 print *, "Rendered treemap: ", width, "x", height
455 end subroutine on_draw
456
457 ! Keyboard callback - handle all keyboard navigation
458 ! NOTE: MUST be recursive because GTK event processing can trigger nested calls
459 recursive function on_key_press(controller, keyval, keycode, state, user_data) bind(c) result(handled)
460 use treemap_renderer, only: navigate_up, navigate_into_node, get_node_count, &
461 get_node_center_by_index, find_node_in_direction, &
462 find_node_at_position
463 type(c_ptr), value :: controller, user_data
464 integer(c_int), value :: keyval, keycode, state
465 integer(c_int) :: handled
466 integer :: node_count, hovered_node_index
467 logical :: success
468
469 print *, "DEBUG: Key press detected! keyval=", keyval, " (Enter=", GDK_KEY_Return, ")"
470 handled = 0_c_int ! Default: not handled
471
472 ! Arrow keys: Navigate through nodes using spatial navigation
473 if (keyval == GDK_KEY_Left .or. keyval == GDK_KEY_Right .or. &
474 keyval == GDK_KEY_Up .or. keyval == GDK_KEY_Down) then
475
476 keyboard_mode = .true. ! Switch to keyboard mode
477
478 ! Get number of visible nodes
479 node_count = get_node_count()
480
481 if (node_count > 0) then
482 ! Determine direction: 1=up, 2=down, 3=left, 4=right
483 if (keyval == GDK_KEY_Up) then
484 keyboard_hover_index = find_node_in_direction(mouse_x, mouse_y, 1)
485 else if (keyval == GDK_KEY_Down) then
486 keyboard_hover_index = find_node_in_direction(mouse_x, mouse_y, 2)
487 else if (keyval == GDK_KEY_Left) then
488 keyboard_hover_index = find_node_in_direction(mouse_x, mouse_y, 3)
489 else if (keyval == GDK_KEY_Right) then
490 keyboard_hover_index = find_node_in_direction(mouse_x, mouse_y, 4)
491 end if
492
493 ! Update mouse position to center of selected node for seamless transition
494 if (keyboard_hover_index > 0) then
495 call get_node_center_by_index(keyboard_hover_index, mouse_x, mouse_y, success)
496 if (success) then
497 print *, "Arrow key - moved to node ", keyboard_hover_index, " at (", mouse_x, ",", mouse_y, ")"
498 end if
499 end if
500 end if
501
502 ! Trigger redraw
503 if (c_associated(widget_ptr)) then
504 call gtk_widget_queue_draw(widget_ptr)
505 end if
506
507 handled = 1_c_int
508
509 ! Backspace: Navigate up one level
510 else if (keyval == GDK_KEY_BackSpace) then
511 print *, "Backspace pressed - navigating up"
512 call navigate_up(1)
513
514 ! Reset keyboard hover
515 keyboard_hover_index = 0
516 keyboard_mode = .false.
517
518 ! Call navigation callback to update breadcrumbs
519 if (associated(nav_callback)) then
520 call nav_callback()
521 end if
522
523 ! Trigger redraw
524 if (c_associated(widget_ptr)) then
525 call gtk_widget_queue_draw(widget_ptr)
526 end if
527
528 handled = 1_c_int
529
530 ! Spacebar: Select item (yellow highlight)
531 else if (keyval == GDK_KEY_space) then
532 print *, "DEBUG: Spacebar detected! keyboard_mode=", keyboard_mode, " keyboard_hover_index=", keyboard_hover_index
533
534 ! Determine which node is currently hovered (either by keyboard or mouse)
535 if (keyboard_mode .and. keyboard_hover_index > 0) then
536 hovered_node_index = keyboard_hover_index
537 print *, "DEBUG: Using keyboard hover index:", hovered_node_index
538 else
539 ! Check if mouse is hovering over a node
540 hovered_node_index = find_node_at_position(mouse_x, mouse_y)
541 print *, "DEBUG: Using mouse position, found index:", hovered_node_index, " at (", mouse_x, ",", mouse_y, ")"
542 end if
543
544 ! Select the hovered item
545 if (hovered_node_index > 0) then
546 call set_selection(hovered_node_index)
547 print *, "Space pressed - selected node: ", selected_index
548 else
549 print *, "DEBUG: No node to select (hovered_node_index = 0)"
550 end if
551
552 ! Trigger redraw
553 if (c_associated(widget_ptr)) then
554 call gtk_widget_queue_draw(widget_ptr)
555 end if
556
557 handled = 1_c_int
558
559 ! Enter: Navigate into hovered or selected directory
560 else if (keyval == GDK_KEY_Return) then
561 ! Determine which node to navigate into (hovered takes precedence)
562 if (keyboard_mode .and. keyboard_hover_index > 0) then
563 hovered_node_index = keyboard_hover_index
564 else
565 hovered_node_index = find_node_at_position(mouse_x, mouse_y)
566 end if
567
568 ! Use hovered if available, otherwise use selected
569 if (hovered_node_index > 0) then
570 print *, "Enter pressed - navigating into hovered node: ", hovered_node_index
571 call navigate_into_node(hovered_node_index)
572 else if (selected_index > 0) then
573 print *, "Enter pressed - navigating into selected node: ", selected_index
574 call navigate_into_node(selected_index)
575 else
576 print *, "Enter pressed - no node to navigate into"
577 end if
578
579 ! Clear selection after navigation
580 call set_selection(0)
581 keyboard_hover_index = 0
582
583 ! Call navigation callback
584 if (associated(nav_callback)) then
585 call nav_callback()
586 end if
587
588 ! Trigger redraw
589 if (c_associated(widget_ptr)) then
590 call gtk_widget_queue_draw(widget_ptr)
591 end if
592
593 handled = 1_c_int
594
595 ! Q key: Quit application
596 else if (keyval == GDK_KEY_q) then
597 print *, "Q pressed - quitting application"
598 if (associated(quit_cb)) then
599 call quit_cb()
600 end if
601 handled = 1_c_int
602
603 ! D key: Delete selected item
604 else if (keyval == GDK_KEY_d) then
605 print *, "D pressed - triggering delete"
606 if (associated(delete_cb)) then
607 call delete_cb()
608 end if
609 handled = 1_c_int
610
611 ! Cmd+Shift+R / Ctrl+Shift+R: Force refresh (clear cache and rescan)
612 else if (keyval == GDK_KEY_r) then
613 ! Check for Shift modifier
614 if (iand(state, GDK_SHIFT_MASK) /= 0) then
615 ! Check for Control (Linux/Windows) or Meta/Cmd (macOS)
616 if (iand(state, GDK_CONTROL_MASK) /= 0 .or. iand(state, GDK_META_MASK) /= 0) then
617 print *, "Cmd+Shift+R / Ctrl+Shift+R pressed - force refresh with cache clear"
618 if (associated(refresh_cb)) then
619 call refresh_cb()
620 end if
621 handled = 1_c_int
622 end if
623 end if
624 end if
625
626 end function on_key_press
627
628 ! Tooltip query callback - show file info on hover
629 function on_query_tooltip(widget, x, y, keyboard_mode, tooltip, user_data) bind(c) result(show_tooltip)
630 use types, only: file_node
631 use treemap_renderer, only: find_node_at_position
632 use file_system, only: list_directory
633 use iso_fortran_env, only: int64
634 type(c_ptr), value :: widget, tooltip, user_data
635 integer(c_int), value :: x, y, keyboard_mode
636 integer(c_int) :: show_tooltip
637 type(file_node), pointer :: current_view
638 integer :: hovered_index, item_count
639 character(len=512) :: tooltip_text
640 character(len=64) :: size_str
641 character(len=256), dimension(10000) :: dir_entries
642 real(c_double) :: dx, dy
643 real :: size_mb, size_gb
644
645 ! GdkRectangle structure for tooltip positioning
646 type, bind(c) :: gdk_rectangle
647 integer(c_int) :: x, y, width, height
648 end type gdk_rectangle
649 type(gdk_rectangle), target :: tip_rect
650
651 show_tooltip = 0_c_int ! Default: don't show tooltip
652
653 ! Convert coordinates to double for find_node_at_position
654 dx = real(x, c_double)
655 dy = real(y, c_double)
656
657 ! Find which node is at this position
658 hovered_index = find_node_at_position(dx, dy)
659
660 if (hovered_index > 0) then
661 ! Get current view node from renderer
662 current_view => get_current_view_node()
663 if (.not. associated(current_view)) return
664 if (.not. allocated(current_view%children)) return
665 if (hovered_index > current_view%num_children) return
666
667 ! Set the tooltip tip area to the hovered node's bounds
668 ! This tells GTK where to position the tooltip
669 tip_rect%x = current_view%children(hovered_index)%bounds%x
670 tip_rect%y = current_view%children(hovered_index)%bounds%y
671 tip_rect%width = current_view%children(hovered_index)%bounds%width
672 tip_rect%height = current_view%children(hovered_index)%bounds%height
673 call gtk_tooltip_set_tip_area(tooltip, c_loc(tip_rect))
674
675 ! Format the size nicely
676 if (current_view%children(hovered_index)%size < 1024_int64) then
677 write(size_str, '(I0,A)') current_view%children(hovered_index)%size, ' B'
678 else if (current_view%children(hovered_index)%size < 1024_int64**2) then
679 write(size_str, '(F0.2,A)') real(current_view%children(hovered_index)%size)/1024.0, ' KB'
680 else if (current_view%children(hovered_index)%size < 1024_int64**3) then
681 size_mb = real(current_view%children(hovered_index)%size)/(1024.0**2)
682 write(size_str, '(F0.2,A)') size_mb, ' MB'
683 else
684 size_gb = real(current_view%children(hovered_index)%size)/(1024.0**3)
685 write(size_str, '(F0.2,A)') size_gb, ' GB'
686 end if
687
688 ! Build tooltip text
689 if (current_view%children(hovered_index)%is_directory) then
690 ! Determine item count for directory
691 if (allocated(current_view%children(hovered_index)%children)) then
692 ! Directory has been scanned - use cached count
693 item_count = current_view%children(hovered_index)%num_children
694 else if (allocated(current_view%children(hovered_index)%path)) then
695 ! Directory not yet scanned - count entries on demand
696 item_count = list_directory(trim(current_view%children(hovered_index)%path), dir_entries, 10000)
697 if (item_count < 0) item_count = 0 ! Handle errors
698 else
699 item_count = 0
700 end if
701
702 write(tooltip_text, '(A,A,A,A,A,I0,A)') &
703 trim(current_view%children(hovered_index)%name), &
704 char(10), 'Size: ', trim(size_str), &
705 char(10), item_count, ' items'
706 else
707 write(tooltip_text, '(A,A,A,A)') &
708 trim(current_view%children(hovered_index)%name), &
709 char(10), 'Size: ', trim(size_str)
710 end if
711
712 ! Set the tooltip text
713 call gtk_tooltip_set_text(tooltip, trim(tooltip_text)//c_null_char)
714 show_tooltip = 1_c_int ! Show tooltip
715 end if
716
717 end function on_query_tooltip
718
719 ! Check if there is a selection
720 function has_selection() result(is_selected)
721 logical :: is_selected
722 is_selected = (selected_index > 0)
723 end function has_selection
724
725 ! Get the path of the currently selected node
726 function get_selected_node_path() result(path)
727 use types, only: file_node
728 character(len=:), allocatable :: path
729 type(file_node), pointer :: current_view
730
731 path = ""
732
733 ! Check if there is a selection
734 if (selected_index == 0) return
735
736 ! Get current view node from renderer
737 current_view => get_current_view_node()
738 if (.not. associated(current_view)) return
739
740 ! Check if the children array is allocated and index is valid
741 if (.not. allocated(current_view%children)) return
742 if (selected_index > current_view%num_children) return
743
744 ! Return the path of the selected child
745 path = trim(current_view%children(selected_index)%path)
746 end function get_selected_node_path
747
748 ! Get the index of the currently selected node
749 function get_selected_index() result(idx)
750 integer :: idx
751 idx = selected_index
752 end function get_selected_index
753
754 ! Clear the current selection
755 subroutine clear_selection()
756 selected_index = 0
757 print *, "Selection cleared"
758 ! Notify callback that selection changed
759 if (associated(selection_cb)) then
760 call selection_cb()
761 end if
762 end subroutine clear_selection
763
764 ! Helper: Set selection and notify callback
765 subroutine set_selection(new_index)
766 integer, intent(in) :: new_index
767 selected_index = new_index
768 ! Notify callback that selection changed
769 if (associated(selection_cb)) then
770 call selection_cb()
771 end if
772 end subroutine set_selection
773
774 ! Context menu callbacks
775 subroutine on_context_navigate(button, popover) bind(c)
776 use treemap_renderer, only: navigate_into_node
777 use gtk, only: gtk_popover_popdown
778 type(c_ptr), value :: button, popover
779
780 print *, "Context menu: Navigate Into"
781 if (selected_index > 0) then
782 call navigate_into_node(selected_index)
783 selected_index = 0
784 if (associated(nav_callback)) call nav_callback()
785 if (c_associated(widget_ptr)) call gtk_widget_queue_draw(widget_ptr)
786 end if
787 call gtk_popover_popdown(popover)
788 end subroutine on_context_navigate
789
790 subroutine on_context_open_finder(button, popover) bind(c)
791 use gtk, only: gtk_popover_popdown
792 type(c_ptr), value :: button, popover
793
794 print *, "Context menu: Open in Finder"
795 ! Trigger external callback (will be handled by gtk_app)
796 if (selected_index > 0) then
797 ! This will use existing on_open_finder_clicked logic from gtk_app
798 print *, "Opening selected item in Finder (index ", selected_index, ")"
799 end if
800 call gtk_popover_popdown(popover)
801 end subroutine on_context_open_finder
802
803 subroutine on_context_open_terminal(button, popover) bind(c)
804 use gtk, only: gtk_popover_popdown
805 type(c_ptr), value :: button, popover
806 character(len=:), allocatable :: path
807 character(len=2048) :: command
808 integer :: status
809
810 print *, "Context menu: Open in Terminal"
811 if (selected_index > 0) then
812 path = get_selected_node_path()
813 if (allocated(path)) then
814 ! macOS: open -a Terminal <path>
815 command = 'open -a Terminal "' // trim(path) // '"'
816 print *, "Executing: ", trim(command)
817 call execute_command_line(trim(command), exitstat=status)
818 end if
819 end if
820 call gtk_popover_popdown(popover)
821 end subroutine on_context_open_terminal
822
823 subroutine on_context_copy_path(button, popover) bind(c)
824 use gtk, only: gtk_popover_popdown
825 type(c_ptr), value :: button, popover
826
827 print *, "Context menu: Copy Path"
828 ! This will use existing copy_path logic
829 if (selected_index > 0) then
830 print *, "Copying path for index ", selected_index
831 end if
832 call gtk_popover_popdown(popover)
833 end subroutine on_context_copy_path
834
835 subroutine on_context_copy_name(button, popover) bind(c)
836 use gtk, only: gtk_popover_popdown
837 use gdk, only: gdk_display_get_default, gdk_display_get_clipboard, gdk_clipboard_set_text
838 use types, only: file_node
839 use treemap_renderer, only: get_current_view_node
840 type(c_ptr), value :: button, popover
841 type(file_node), pointer :: current_view
842 type(c_ptr) :: display, clipboard
843 character(len=256) :: filename
844
845 print *, "Context menu: Copy Name"
846 if (selected_index > 0) then
847 current_view => get_current_view_node()
848 if (associated(current_view) .and. allocated(current_view%children)) then
849 if (allocated(current_view%children(selected_index)%name)) then
850 filename = trim(current_view%children(selected_index)%name)
851 display = gdk_display_get_default()
852 clipboard = gdk_display_get_clipboard(display)
853 call gdk_clipboard_set_text(clipboard, trim(filename)//c_null_char)
854 print *, "Copied name to clipboard: ", trim(filename)
855 end if
856 end if
857 end if
858 call gtk_popover_popdown(popover)
859 end subroutine on_context_copy_name
860
861 subroutine on_context_info(button, popover) bind(c)
862 use gtk, only: gtk_popover_popdown
863 type(c_ptr), value :: button, popover
864
865 print *, "Context menu: Properties/Info"
866 ! This will trigger the info dialog (existing logic)
867 if (selected_index > 0) then
868 print *, "Showing info for index ", selected_index
869 end if
870 call gtk_popover_popdown(popover)
871 end subroutine on_context_info
872
873 subroutine on_context_refresh(button, popover) bind(c)
874 use gtk, only: gtk_popover_popdown
875 use treemap_renderer, only: navigate_into_node
876 type(c_ptr), value :: button, popover
877
878 print *, "Context menu: Refresh This Folder"
879 if (selected_index > 0) then
880 ! Navigate into then immediately navigate back triggers refresh
881 print *, "Refreshing folder at index ", selected_index
882 end if
883 call gtk_popover_popdown(popover)
884 end subroutine on_context_refresh
885
886 subroutine on_context_delete(button, popover) bind(c)
887 use gtk, only: gtk_popover_popdown
888 type(c_ptr), value :: button, popover
889
890 print *, "Context menu: Delete to Trash"
891 if (selected_index > 0) then
892 ! Trigger delete callback
893 if (associated(delete_cb)) then
894 call delete_cb()
895 end if
896 end if
897 call gtk_popover_popdown(popover)
898 end subroutine on_context_delete
899
900 end module treemap_widget
901