creatumlibre.ui.input.input_handler
1from PyQt6.QtCore import QEvent, QObject 2from PyQt6.QtGui import QKeySequence 3 4from creatumlibre.ui.dialogs.object_manager_dialog import ObjectManagerDialog 5from creatumlibre.ui.manager.image_handler import ImageHandler 6from creatumlibre.ui.manager.object_manager import ObjectManager 7from creatumlibre.ui.mode.ui_input_mode import InputMode 8 9 10class InputHandler(QObject): 11 """Handles global key and mouse events.""" 12 13 def __init__(self, parent): 14 super().__init__() 15 self.parent = parent # Reference to UI mode for event interpretation 16 self.start_pos = None 17 self.end_pos = None 18 19 def map_event_to_image_coordinates(self, event) -> tuple[int, int] | None: 20 """Get the real position of the mouse within the image QLabel.""" 21 if (active_tab := self.parent.tab_manager.get_active_tab()) is None: 22 return None 23 24 if (label_widget := active_tab.get("widget")) is None: 25 return None 26 27 global_pos = event.globalPosition().toPoint() 28 local_pos = label_widget.mapFromGlobal(global_pos) 29 30 # Center offset: compensate for QLabel centering the image 31 pixmap = label_widget.pixmap() 32 if pixmap is None: 33 return None 34 35 offset_x = max(0, (label_widget.width() - pixmap.width()) // 2) 36 offset_y = max(0, (label_widget.height() - pixmap.height()) // 2) 37 38 real_x = local_pos.x() - offset_x 39 real_y = local_pos.y() - offset_y 40 41 # Optional: also return unscaled pixel in original image space 42 zoom = active_tab.get("manager").zoom_factor 43 orig_x = int(real_x / zoom) 44 orig_y = int(real_y / zoom) 45 46 return orig_x, orig_y 47 48 def eventFilter(self, _, event): 49 """Intercept global events and delegate handling.""" 50 51 if event.type() == QEvent.Type.KeyPress: 52 self.handle_key_press(event) 53 return True 54 55 if self.parent.ui_input_mode.get_mode() == InputMode.IDLE: 56 if event.type() == QEvent.Type.MouseButtonPress: 57 self.handle_mouse_press(event) 58 return False 59 60 if self.parent.ui_input_mode.get_mode() == InputMode.MOVE_OBJECTS: 61 if event.type() == QEvent.Type.MouseMove: 62 self.handle_mouse_move(event) 63 elif event.type() == QEvent.Type.MouseButtonRelease: 64 self.handle_mouse_release(event) 65 66 if self.parent.ui_input_mode.get_mode() == InputMode.SELECT_REGION: 67 if event.type() == QEvent.Type.MouseButtonPress: 68 self.handle_mouse_press(event) 69 elif event.type() == QEvent.Type.MouseMove: 70 self.handle_mouse_move(event) 71 elif event.type() == QEvent.Type.MouseButtonRelease: 72 self.handle_mouse_release(event) 73 elif event.type() == QEvent.Type.KeyPress: 74 self.handle_key_press(event) 75 return False # Allows event to propagate if unhandled 76 77 def handle_mouse_press(self, event): 78 """Handles mouse press based on current mode.""" 79 mode = self.parent.ui_input_mode.get_mode() 80 if mode == InputMode.SELECT_REGION: 81 self.start_pos = self.map_event_to_image_coordinates(event) 82 print(f"Selection started at {self.start_pos}") 83 elif mode == InputMode.IDLE: # select objects 84 self.start_pos = self.map_event_to_image_coordinates(event) 85 if (active_tab := self.parent.tab_manager.get_active_tab()) is None: 86 return 87 if active_tab["manager"].set_selected_object_by_click(self.start_pos): 88 self.parent.ui_input_mode.set_mode(InputMode.MOVE_OBJECTS) 89 self.parent.tab_manager.refresh_active_tab_display() 90 91 print(f"IDLE Object Selection at {self.start_pos}") 92 93 def handle_mouse_move(self, event): 94 """Handles mouse movement feedback.""" 95 mode = self.parent.ui_input_mode.get_mode() 96 if mode == InputMode.SELECT_REGION and self.start_pos: 97 self.end_pos = self.map_event_to_image_coordinates(event) 98 self.parent.update() 99 if (active_tab := self.parent.tab_manager.get_active_tab()) is None: 100 return 101 tmp_object = self.create_new_image_object_from_selection(active_tab) 102 self.parent.tab_manager.refresh_active_tab_display() 103 active_tab["manager"].delete_object(tmp_object) 104 105 elif mode == InputMode.MOVE_OBJECTS and self.start_pos: 106 # paint everything 107 self.end_pos = self.map_event_to_image_coordinates(event) 108 # update pos of dragged objects 109 dx = self.end_pos[0] - self.start_pos[0] 110 dy = self.end_pos[1] - self.start_pos[1] 111 if (active_tab := self.parent.tab_manager.get_active_tab()) is None: 112 return 113 active_tab["manager"].update_selected_position(dx, dy) 114 self.parent.tab_manager.refresh_active_tab_display() 115 116 def handle_mouse_release(self, event): 117 """Handles mouse release actions.""" 118 mode = self.parent.ui_input_mode.get_mode() 119 if mode == InputMode.SELECT_REGION: 120 self.parent.ui_input_mode.set_mode(InputMode.IDLE) 121 self.end_pos = self.map_event_to_image_coordinates( 122 event 123 ) # Set final end position 124 125 if (active_tab := self.parent.tab_manager.get_active_tab()) is None: 126 return 127 128 self.create_new_image_object_from_selection(active_tab) 129 130 elif mode == InputMode.MOVE_OBJECTS and self.start_pos: 131 self.parent.ui_input_mode.set_mode(InputMode.IDLE) 132 # Maybe change later: but for now: release all objects 133 if (active_tab := self.parent.tab_manager.get_active_tab()) is None: 134 return 135 active_tab["manager"].clear_selection() 136 137 def create_new_image_object_from_selection( 138 self, active_tab: ObjectManager 139 ) -> ImageHandler: 140 """create new image object from selection""" 141 x, y, w, h = self.process_rect_selection() # to base-image 142 ## create image (ImageHandler) 143 parent_object = active_tab["manager"].get_active_object() 144 145 # set the selection mask 146 parent_object.region_manager.set_bounding_rect(x, y, w, h) 147 148 new_image_object = parent_object.extract_selection_as_new_image() 149 active_tab["manager"].add_object(new_image_object) # new object in ObjectList 150 return new_image_object 151 152 def handle_key_press(self, event): 153 """Handles key interactions based on mode.""" 154 print("Key press detected") 155 156 modifiers = event.modifiers().value 157 key_seq = QKeySequence(event.key() | modifiers) 158 159 if ( 160 is_cut := key_seq.matches(QKeySequence.StandardKey.Cut) 161 == QKeySequence.SequenceMatch.ExactMatch 162 ) or key_seq.matches( 163 QKeySequence.StandardKey.Copy 164 ) == QKeySequence.SequenceMatch.ExactMatch: 165 166 active_tab = self.parent.tab_manager.get_active_tab() 167 if active_tab is None: 168 print("No active tab") 169 return 170 print("Opening Object Manager Dialog (Cmd+X/C)") 171 self.parent.dialog_manager.show(ObjectManagerDialog(active_tab["manager"])) 172 # copy or cut object without "promoted" flag 173 active_tab["manager"].copy_promoted(is_cut) 174 active_tab["manager"].delete_promoted() 175 self.parent.dialog_manager.update(ObjectManagerDialog) 176 177 def process_rect_selection(self): 178 """Convert QPoint to integers and ensure correct ordering.""" 179 x1, y1 = self.start_pos[0], self.start_pos[1] 180 x2, y2 = self.end_pos[0], self.end_pos[1] 181 182 # Ensure coordinates are ordered correctly 183 x_start, x_end = sorted([x1, x2]) 184 y_start, y_end = sorted([y1, y2]) 185 width, height = x_end - x_start, y_end - y_start 186 return x_start, y_start, width, height
class
InputHandler(PyQt6.QtCore.QObject):
11class InputHandler(QObject): 12 """Handles global key and mouse events.""" 13 14 def __init__(self, parent): 15 super().__init__() 16 self.parent = parent # Reference to UI mode for event interpretation 17 self.start_pos = None 18 self.end_pos = None 19 20 def map_event_to_image_coordinates(self, event) -> tuple[int, int] | None: 21 """Get the real position of the mouse within the image QLabel.""" 22 if (active_tab := self.parent.tab_manager.get_active_tab()) is None: 23 return None 24 25 if (label_widget := active_tab.get("widget")) is None: 26 return None 27 28 global_pos = event.globalPosition().toPoint() 29 local_pos = label_widget.mapFromGlobal(global_pos) 30 31 # Center offset: compensate for QLabel centering the image 32 pixmap = label_widget.pixmap() 33 if pixmap is None: 34 return None 35 36 offset_x = max(0, (label_widget.width() - pixmap.width()) // 2) 37 offset_y = max(0, (label_widget.height() - pixmap.height()) // 2) 38 39 real_x = local_pos.x() - offset_x 40 real_y = local_pos.y() - offset_y 41 42 # Optional: also return unscaled pixel in original image space 43 zoom = active_tab.get("manager").zoom_factor 44 orig_x = int(real_x / zoom) 45 orig_y = int(real_y / zoom) 46 47 return orig_x, orig_y 48 49 def eventFilter(self, _, event): 50 """Intercept global events and delegate handling.""" 51 52 if event.type() == QEvent.Type.KeyPress: 53 self.handle_key_press(event) 54 return True 55 56 if self.parent.ui_input_mode.get_mode() == InputMode.IDLE: 57 if event.type() == QEvent.Type.MouseButtonPress: 58 self.handle_mouse_press(event) 59 return False 60 61 if self.parent.ui_input_mode.get_mode() == InputMode.MOVE_OBJECTS: 62 if event.type() == QEvent.Type.MouseMove: 63 self.handle_mouse_move(event) 64 elif event.type() == QEvent.Type.MouseButtonRelease: 65 self.handle_mouse_release(event) 66 67 if self.parent.ui_input_mode.get_mode() == InputMode.SELECT_REGION: 68 if event.type() == QEvent.Type.MouseButtonPress: 69 self.handle_mouse_press(event) 70 elif event.type() == QEvent.Type.MouseMove: 71 self.handle_mouse_move(event) 72 elif event.type() == QEvent.Type.MouseButtonRelease: 73 self.handle_mouse_release(event) 74 elif event.type() == QEvent.Type.KeyPress: 75 self.handle_key_press(event) 76 return False # Allows event to propagate if unhandled 77 78 def handle_mouse_press(self, event): 79 """Handles mouse press based on current mode.""" 80 mode = self.parent.ui_input_mode.get_mode() 81 if mode == InputMode.SELECT_REGION: 82 self.start_pos = self.map_event_to_image_coordinates(event) 83 print(f"Selection started at {self.start_pos}") 84 elif mode == InputMode.IDLE: # select objects 85 self.start_pos = self.map_event_to_image_coordinates(event) 86 if (active_tab := self.parent.tab_manager.get_active_tab()) is None: 87 return 88 if active_tab["manager"].set_selected_object_by_click(self.start_pos): 89 self.parent.ui_input_mode.set_mode(InputMode.MOVE_OBJECTS) 90 self.parent.tab_manager.refresh_active_tab_display() 91 92 print(f"IDLE Object Selection at {self.start_pos}") 93 94 def handle_mouse_move(self, event): 95 """Handles mouse movement feedback.""" 96 mode = self.parent.ui_input_mode.get_mode() 97 if mode == InputMode.SELECT_REGION and self.start_pos: 98 self.end_pos = self.map_event_to_image_coordinates(event) 99 self.parent.update() 100 if (active_tab := self.parent.tab_manager.get_active_tab()) is None: 101 return 102 tmp_object = self.create_new_image_object_from_selection(active_tab) 103 self.parent.tab_manager.refresh_active_tab_display() 104 active_tab["manager"].delete_object(tmp_object) 105 106 elif mode == InputMode.MOVE_OBJECTS and self.start_pos: 107 # paint everything 108 self.end_pos = self.map_event_to_image_coordinates(event) 109 # update pos of dragged objects 110 dx = self.end_pos[0] - self.start_pos[0] 111 dy = self.end_pos[1] - self.start_pos[1] 112 if (active_tab := self.parent.tab_manager.get_active_tab()) is None: 113 return 114 active_tab["manager"].update_selected_position(dx, dy) 115 self.parent.tab_manager.refresh_active_tab_display() 116 117 def handle_mouse_release(self, event): 118 """Handles mouse release actions.""" 119 mode = self.parent.ui_input_mode.get_mode() 120 if mode == InputMode.SELECT_REGION: 121 self.parent.ui_input_mode.set_mode(InputMode.IDLE) 122 self.end_pos = self.map_event_to_image_coordinates( 123 event 124 ) # Set final end position 125 126 if (active_tab := self.parent.tab_manager.get_active_tab()) is None: 127 return 128 129 self.create_new_image_object_from_selection(active_tab) 130 131 elif mode == InputMode.MOVE_OBJECTS and self.start_pos: 132 self.parent.ui_input_mode.set_mode(InputMode.IDLE) 133 # Maybe change later: but for now: release all objects 134 if (active_tab := self.parent.tab_manager.get_active_tab()) is None: 135 return 136 active_tab["manager"].clear_selection() 137 138 def create_new_image_object_from_selection( 139 self, active_tab: ObjectManager 140 ) -> ImageHandler: 141 """create new image object from selection""" 142 x, y, w, h = self.process_rect_selection() # to base-image 143 ## create image (ImageHandler) 144 parent_object = active_tab["manager"].get_active_object() 145 146 # set the selection mask 147 parent_object.region_manager.set_bounding_rect(x, y, w, h) 148 149 new_image_object = parent_object.extract_selection_as_new_image() 150 active_tab["manager"].add_object(new_image_object) # new object in ObjectList 151 return new_image_object 152 153 def handle_key_press(self, event): 154 """Handles key interactions based on mode.""" 155 print("Key press detected") 156 157 modifiers = event.modifiers().value 158 key_seq = QKeySequence(event.key() | modifiers) 159 160 if ( 161 is_cut := key_seq.matches(QKeySequence.StandardKey.Cut) 162 == QKeySequence.SequenceMatch.ExactMatch 163 ) or key_seq.matches( 164 QKeySequence.StandardKey.Copy 165 ) == QKeySequence.SequenceMatch.ExactMatch: 166 167 active_tab = self.parent.tab_manager.get_active_tab() 168 if active_tab is None: 169 print("No active tab") 170 return 171 print("Opening Object Manager Dialog (Cmd+X/C)") 172 self.parent.dialog_manager.show(ObjectManagerDialog(active_tab["manager"])) 173 # copy or cut object without "promoted" flag 174 active_tab["manager"].copy_promoted(is_cut) 175 active_tab["manager"].delete_promoted() 176 self.parent.dialog_manager.update(ObjectManagerDialog) 177 178 def process_rect_selection(self): 179 """Convert QPoint to integers and ensure correct ordering.""" 180 x1, y1 = self.start_pos[0], self.start_pos[1] 181 x2, y2 = self.end_pos[0], self.end_pos[1] 182 183 # Ensure coordinates are ordered correctly 184 x_start, x_end = sorted([x1, x2]) 185 y_start, y_end = sorted([y1, y2]) 186 width, height = x_end - x_start, y_end - y_start 187 return x_start, y_start, width, height
Handles global key and mouse events.
def
map_event_to_image_coordinates(self, event) -> tuple[int, int] | None:
20 def map_event_to_image_coordinates(self, event) -> tuple[int, int] | None: 21 """Get the real position of the mouse within the image QLabel.""" 22 if (active_tab := self.parent.tab_manager.get_active_tab()) is None: 23 return None 24 25 if (label_widget := active_tab.get("widget")) is None: 26 return None 27 28 global_pos = event.globalPosition().toPoint() 29 local_pos = label_widget.mapFromGlobal(global_pos) 30 31 # Center offset: compensate for QLabel centering the image 32 pixmap = label_widget.pixmap() 33 if pixmap is None: 34 return None 35 36 offset_x = max(0, (label_widget.width() - pixmap.width()) // 2) 37 offset_y = max(0, (label_widget.height() - pixmap.height()) // 2) 38 39 real_x = local_pos.x() - offset_x 40 real_y = local_pos.y() - offset_y 41 42 # Optional: also return unscaled pixel in original image space 43 zoom = active_tab.get("manager").zoom_factor 44 orig_x = int(real_x / zoom) 45 orig_y = int(real_y / zoom) 46 47 return orig_x, orig_y
Get the real position of the mouse within the image QLabel.
def
eventFilter(self, _, event):
49 def eventFilter(self, _, event): 50 """Intercept global events and delegate handling.""" 51 52 if event.type() == QEvent.Type.KeyPress: 53 self.handle_key_press(event) 54 return True 55 56 if self.parent.ui_input_mode.get_mode() == InputMode.IDLE: 57 if event.type() == QEvent.Type.MouseButtonPress: 58 self.handle_mouse_press(event) 59 return False 60 61 if self.parent.ui_input_mode.get_mode() == InputMode.MOVE_OBJECTS: 62 if event.type() == QEvent.Type.MouseMove: 63 self.handle_mouse_move(event) 64 elif event.type() == QEvent.Type.MouseButtonRelease: 65 self.handle_mouse_release(event) 66 67 if self.parent.ui_input_mode.get_mode() == InputMode.SELECT_REGION: 68 if event.type() == QEvent.Type.MouseButtonPress: 69 self.handle_mouse_press(event) 70 elif event.type() == QEvent.Type.MouseMove: 71 self.handle_mouse_move(event) 72 elif event.type() == QEvent.Type.MouseButtonRelease: 73 self.handle_mouse_release(event) 74 elif event.type() == QEvent.Type.KeyPress: 75 self.handle_key_press(event) 76 return False # Allows event to propagate if unhandled
Intercept global events and delegate handling.
def
handle_mouse_press(self, event):
78 def handle_mouse_press(self, event): 79 """Handles mouse press based on current mode.""" 80 mode = self.parent.ui_input_mode.get_mode() 81 if mode == InputMode.SELECT_REGION: 82 self.start_pos = self.map_event_to_image_coordinates(event) 83 print(f"Selection started at {self.start_pos}") 84 elif mode == InputMode.IDLE: # select objects 85 self.start_pos = self.map_event_to_image_coordinates(event) 86 if (active_tab := self.parent.tab_manager.get_active_tab()) is None: 87 return 88 if active_tab["manager"].set_selected_object_by_click(self.start_pos): 89 self.parent.ui_input_mode.set_mode(InputMode.MOVE_OBJECTS) 90 self.parent.tab_manager.refresh_active_tab_display() 91 92 print(f"IDLE Object Selection at {self.start_pos}")
Handles mouse press based on current mode.
def
handle_mouse_move(self, event):
94 def handle_mouse_move(self, event): 95 """Handles mouse movement feedback.""" 96 mode = self.parent.ui_input_mode.get_mode() 97 if mode == InputMode.SELECT_REGION and self.start_pos: 98 self.end_pos = self.map_event_to_image_coordinates(event) 99 self.parent.update() 100 if (active_tab := self.parent.tab_manager.get_active_tab()) is None: 101 return 102 tmp_object = self.create_new_image_object_from_selection(active_tab) 103 self.parent.tab_manager.refresh_active_tab_display() 104 active_tab["manager"].delete_object(tmp_object) 105 106 elif mode == InputMode.MOVE_OBJECTS and self.start_pos: 107 # paint everything 108 self.end_pos = self.map_event_to_image_coordinates(event) 109 # update pos of dragged objects 110 dx = self.end_pos[0] - self.start_pos[0] 111 dy = self.end_pos[1] - self.start_pos[1] 112 if (active_tab := self.parent.tab_manager.get_active_tab()) is None: 113 return 114 active_tab["manager"].update_selected_position(dx, dy) 115 self.parent.tab_manager.refresh_active_tab_display()
Handles mouse movement feedback.
def
handle_mouse_release(self, event):
117 def handle_mouse_release(self, event): 118 """Handles mouse release actions.""" 119 mode = self.parent.ui_input_mode.get_mode() 120 if mode == InputMode.SELECT_REGION: 121 self.parent.ui_input_mode.set_mode(InputMode.IDLE) 122 self.end_pos = self.map_event_to_image_coordinates( 123 event 124 ) # Set final end position 125 126 if (active_tab := self.parent.tab_manager.get_active_tab()) is None: 127 return 128 129 self.create_new_image_object_from_selection(active_tab) 130 131 elif mode == InputMode.MOVE_OBJECTS and self.start_pos: 132 self.parent.ui_input_mode.set_mode(InputMode.IDLE) 133 # Maybe change later: but for now: release all objects 134 if (active_tab := self.parent.tab_manager.get_active_tab()) is None: 135 return 136 active_tab["manager"].clear_selection()
Handles mouse release actions.
def
create_new_image_object_from_selection( self, active_tab: creatumlibre.ui.manager.object_manager.ObjectManager) -> creatumlibre.ui.manager.image_handler.ImageHandler:
138 def create_new_image_object_from_selection( 139 self, active_tab: ObjectManager 140 ) -> ImageHandler: 141 """create new image object from selection""" 142 x, y, w, h = self.process_rect_selection() # to base-image 143 ## create image (ImageHandler) 144 parent_object = active_tab["manager"].get_active_object() 145 146 # set the selection mask 147 parent_object.region_manager.set_bounding_rect(x, y, w, h) 148 149 new_image_object = parent_object.extract_selection_as_new_image() 150 active_tab["manager"].add_object(new_image_object) # new object in ObjectList 151 return new_image_object
create new image object from selection
def
handle_key_press(self, event):
153 def handle_key_press(self, event): 154 """Handles key interactions based on mode.""" 155 print("Key press detected") 156 157 modifiers = event.modifiers().value 158 key_seq = QKeySequence(event.key() | modifiers) 159 160 if ( 161 is_cut := key_seq.matches(QKeySequence.StandardKey.Cut) 162 == QKeySequence.SequenceMatch.ExactMatch 163 ) or key_seq.matches( 164 QKeySequence.StandardKey.Copy 165 ) == QKeySequence.SequenceMatch.ExactMatch: 166 167 active_tab = self.parent.tab_manager.get_active_tab() 168 if active_tab is None: 169 print("No active tab") 170 return 171 print("Opening Object Manager Dialog (Cmd+X/C)") 172 self.parent.dialog_manager.show(ObjectManagerDialog(active_tab["manager"])) 173 # copy or cut object without "promoted" flag 174 active_tab["manager"].copy_promoted(is_cut) 175 active_tab["manager"].delete_promoted() 176 self.parent.dialog_manager.update(ObjectManagerDialog)
Handles key interactions based on mode.
def
process_rect_selection(self):
178 def process_rect_selection(self): 179 """Convert QPoint to integers and ensure correct ordering.""" 180 x1, y1 = self.start_pos[0], self.start_pos[1] 181 x2, y2 = self.end_pos[0], self.end_pos[1] 182 183 # Ensure coordinates are ordered correctly 184 x_start, x_end = sorted([x1, x2]) 185 y_start, y_end = sorted([y1, y2]) 186 width, height = x_end - x_start, y_end - y_start 187 return x_start, y_start, width, height
Convert QPoint to integers and ensure correct ordering.