24
24
from .image_handler import ImageHandler
25
25
from .ui_components import create_list_row_widget , show_help_window , show_preview_window
26
26
from .ui_builder import build_main_window_content
27
+ from .utils import fuzzy_search
27
28
28
29
from gi .repository import Gdk , GLib , Gtk , Pango # noqa: E402
29
30
@@ -48,7 +49,7 @@ def __init__(self, application_window: Gtk.ApplicationWindow):
48
49
self ._search_timer_id = None
49
50
self ._vadjustment_handler_id = None
50
51
self ._is_wayland = "wayland" in os .environ .get ("XDG_SESSION_TYPE" , "" ).lower ()
51
- log .info (f"Detected session type: { 'Wayland' if self ._is_wayland else 'X11' } " )
52
+ log .debug (f"Detected session type: { 'Wayland' if self ._is_wayland else 'X11' } " )
52
53
53
54
self .data_manager = DataManager (update_callback = self ._on_history_updated )
54
55
self .image_handler = ImageHandler (IMAGE_CACHE_MAX_SIZE or 50 )
@@ -151,24 +152,15 @@ def _focus_first_item(self):
151
152
152
153
def update_filtered_items (self ):
153
154
"""Filters master list based on search and pin status, then updates UI."""
154
- self .filtered_items = []
155
- search_term_lower = self .search_term .lower ()
156
-
157
- for index , item in enumerate (self .items ):
158
- is_pinned = item .get ("pinned" , False )
159
- if self .show_only_pinned and not is_pinned :
160
- continue
161
-
162
- if search_term_lower :
163
- item_value = item .get ("value" , "" ).lower ()
164
- match = search_term_lower in item_value
165
- if not match and item .get ("filePath" ):
166
- match = search_term_lower in item .get ("filePath" , "" ).lower ()
167
- if not match :
168
- continue
169
-
170
- self .filtered_items .append ({"original_index" : index , "item" : item })
171
155
156
+ self .filtered_items = fuzzy_search (
157
+ items = self .items ,
158
+ search_term = self .search_term ,
159
+ value_key = "value" ,
160
+ path_key = "filePath" ,
161
+ pinned_key = "pinned" ,
162
+ show_only_pinned = self .show_only_pinned ,
163
+ )
172
164
self .populate_list_view ()
173
165
self .update_status_label ()
174
166
GLib .idle_add (self .check_load_more )
@@ -716,7 +708,7 @@ def copy_image_to_clipboard(self, image_path):
716
708
self .flash_status (f"Error copying image: { str (e )[:100 ]} " )
717
709
return False
718
710
719
- def copy_selected_item_to_clipboard (self ):
711
+ def copy_selected_item_to_clipboard (self , with_paste_simulation = False ):
720
712
"""Copies the selected item to the system clipboard and closes the window."""
721
713
selected_row = self .list_box .get_selected_row ()
722
714
exit_timeout = 150
@@ -768,15 +760,19 @@ def close_window_callback(window):
768
760
self .flash_status ("Cannot copy null text value." )
769
761
770
762
if copy_successful :
771
- if ENTER_TO_PASTE :
763
+ if ENTER_TO_PASTE or with_paste_simulation :
772
764
log .debug ("Hiding window and scheduling paste simulation." )
773
765
self .window .hide ()
774
- GLib .timeout_add (
775
- PASTE_SIMULATION_DELAY_MS or 150 ,
776
- self ._trigger_paste_simulation_and_quit ,
777
- )
766
+ GLib .timeout_add (
767
+ PASTE_SIMULATION_DELAY_MS or 150 ,
768
+ self ._trigger_paste_simulation_and_quit ,
769
+ )
770
+ else :
771
+ GLib .timeout_add (100 , self ._quit_application )
778
772
else :
779
- GLib .timeout_add (100 , self ._quit_application )
773
+ log .error ("Copy operation failed." )
774
+ self .flash_status ("Error: Copy operation failed." )
775
+ GLib .timeout_add (exit_timeout , close_window_callback , self .window )
780
776
781
777
except Exception as e :
782
778
log .error (f"Unexpected error during copy selection: { e } " , exc_info = True )
@@ -808,7 +804,6 @@ def _quit_application(self):
808
804
809
805
def paste_from_clipboard_simulated (self ):
810
806
"""Pastes FROM the clipboard by simulating key presses (Ctrl+V)."""
811
- print (self ._is_wayland , "Is Wayland" )
812
807
if self ._is_wayland :
813
808
cmd_str = str (PASTE_SIMULATION_CMD_WAYLAND )
814
809
tool_name = "wtype"
@@ -1030,49 +1025,80 @@ def on_key_press(self, widget, event):
1030
1025
return True
1031
1026
1032
1027
if keyval in [Gdk .KEY_Up , Gdk .KEY_Down , Gdk .KEY_Page_Up , Gdk .KEY_Page_Down ]:
1033
- if len (self .list_box .get_children ()) > 0 :
1034
- self .list_box .grab_focus ()
1028
+ focusable_elements = self .list_box .get_children ()
1029
+ if not focusable_elements :
1030
+ return False
1035
1031
1036
- if keyval == Gdk .KEY_Down :
1037
- current = self .list_box .get_selected_row ()
1038
- if current :
1039
- index = current .get_index ()
1040
- next_row = self .list_box .get_row_at_index (index + 1 )
1041
- if next_row :
1042
- self .list_box .select_row (next_row )
1043
- else :
1044
- first_row = self .list_box .get_row_at_index (0 )
1045
- if first_row :
1046
- self .list_box .select_row (first_row )
1047
- elif keyval == Gdk .KEY_Up :
1048
- current = self .list_box .get_selected_row ()
1049
- if current :
1050
- index = current .get_index ()
1051
- if index > 0 :
1052
- prev_row = self .list_box .get_row_at_index (index - 1 )
1053
- if prev_row :
1054
- self .list_box .select_row (prev_row )
1055
-
1056
- if keyval in [Gdk .KEY_Up , Gdk .KEY_Down ]:
1057
- return True
1032
+ current_focus = self .window .get_focus ()
1033
+ current_index = (
1034
+ focusable_elements .index (current_focus )
1035
+ if current_focus in focusable_elements
1036
+ else - 1
1037
+ )
1058
1038
1059
- return False
1060
- return True
1039
+ target_index = current_index
1040
+ if keyval == Gdk .KEY_Down :
1041
+ target_index = 0 if current_index == - 1 else current_index + 1
1042
+ elif keyval == Gdk .KEY_Up :
1043
+ target_index = (
1044
+ len (focusable_elements ) - 1
1045
+ if current_index == - 1
1046
+ else current_index - 1
1047
+ )
1048
+ elif keyval == Gdk .KEY_Page_Down :
1049
+ target_index = (
1050
+ 0
1051
+ if current_index == - 1
1052
+ else min (current_index + 5 , len (focusable_elements ) - 1 )
1053
+ )
1054
+ elif keyval == Gdk .KEY_Page_Up :
1055
+ target_index = (
1056
+ len (focusable_elements ) - 1
1057
+ if current_index == - 1
1058
+ else max (current_index - 5 , 0 )
1059
+ )
1060
+
1061
+ if 0 <= target_index < len (focusable_elements ):
1062
+ row = focusable_elements [target_index ]
1063
+ self .list_box .select_row (row )
1064
+ row .grab_focus ()
1065
+ allocation = row .get_allocation ()
1066
+ adj = self .scrolled_window .get_vadjustment ()
1067
+ if adj :
1068
+ adj .set_value (
1069
+ min (allocation .y , adj .get_upper () - adj .get_page_size ())
1070
+ )
1071
+ return True
1072
+
1073
+ return False
1061
1074
1062
1075
selected_row = self .list_box .get_selected_row ()
1063
1076
1077
+ if keyval == Gdk .KEY_Return :
1078
+ if selected_row :
1079
+ self .on_row_activated (self .list_box , shift and not ENTER_TO_PASTE )
1080
+ elif self .list_box .get_children ():
1081
+ first_row = self .list_box .get_row_at_index (0 )
1082
+ if first_row :
1083
+ self .list_box .select_row (first_row )
1084
+ first_row .grab_focus ()
1085
+ self .on_row_activated (self .list_box )
1086
+ else :
1087
+ self .search_entry .grab_focus ()
1088
+ return True
1089
+
1064
1090
# Navigation Aliases
1065
1091
if keyval == Gdk .KEY_k :
1066
- return self .list_box .emit (
1067
- "move-cursor" , Gtk .MovementStep .DISPLAY_LINES , - 1 , False
1068
- )
1092
+ return self .list_box .emit ("move-cursor" , Gtk .MovementStep .DISPLAY_LINES , - 1 )
1069
1093
if keyval == Gdk .KEY_j :
1070
- return self .list_box .emit (
1071
- "move-cursor" , Gtk .MovementStep .DISPLAY_LINES , 1 , False
1072
- )
1094
+ return self .list_box .emit ("move-cursor" , Gtk .MovementStep .DISPLAY_LINES , 1 )
1073
1095
1074
1096
# Actions
1075
- if keyval == Gdk .KEY_slash or keyval == Gdk .KEY_f :
1097
+ if (
1098
+ keyval == Gdk .KEY_slash
1099
+ or keyval == Gdk .KEY_f
1100
+ and not self .search_entry .has_focus ()
1101
+ ):
1076
1102
self .search_entry .grab_focus ()
1077
1103
self .search_entry .select_region (0 , - 1 )
1078
1104
return True
@@ -1128,19 +1154,12 @@ def on_key_press(self, widget, event):
1128
1154
self .update_zoom ()
1129
1155
return True
1130
1156
1131
- # Enter/Activation (let row-activated signal handle)
1132
- if keyval == Gdk .KEY_Return or keyval == Gdk .KEY_KP_Enter :
1133
- if selected_row :
1134
- return False
1135
- else :
1136
- return True
1137
-
1138
1157
return False
1139
1158
1140
- def on_row_activated (self , list_box , row ):
1159
+ def on_row_activated (self , row , with_paste_simulation = False ):
1141
1160
"""Handles double-click or Enter on a list row."""
1142
1161
log .debug (f"Row activated: original_index={ getattr (row , 'item_index' , 'N/A' )} " )
1143
- self .copy_selected_item_to_clipboard ()
1162
+ self .copy_selected_item_to_clipboard (with_paste_simulation )
1144
1163
1145
1164
def on_search_changed (self , entry ):
1146
1165
"""Handles changes in the search entry, debounced."""
0 commit comments