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.

InputHandler(parent)
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
def parent(unknown):

The type of the None singleton.

start_pos
end_pos
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.