diff --git a/beeref/items.py b/beeref/items.py index 6a94bbf..7e0ecfe 100644 --- a/beeref/items.py +++ b/beeref/items.py @@ -43,9 +43,6 @@ def register_item(cls): class BeeItemMixin(SelectableMixin): """Base for all items added by the user.""" - def sample_color_at(self, pos): - return None - def set_pos_center(self, pos): """Sets the position using the item's center as the origin point.""" diff --git a/beeref/selection.py b/beeref/selection.py index 69e0827..ebd1180 100644 --- a/beeref/selection.py +++ b/beeref/selection.py @@ -130,6 +130,9 @@ class BaseItemMixin: if self.scene(): self.scene().cursor_cleared.emit() + def sample_color_at(self, pos): + return None + class SelectableMixin(BaseItemMixin): """Common code for selectable items: Selection outline, handles etc.""" @@ -640,7 +643,15 @@ class MultiSelectItem(SelectableMixin, def selection_action_items(self): """The items affected by selection actions like scaling and rotating. """ - return list(self.scene().selectedItems()) + if self.scene(): + return list(self.scene().selectedItems()) + return [] + + def lower_behind_selection(self): + items = self.selection_action_items() + if items: + min_z = min(item.zValue() for item in items) + self.setZValue(min_z - self.scene().Z_STEP) def fit_selection_area(self, rect): """Updates itself to fit the given selection area.""" diff --git a/beeref/view.py b/beeref/view.py index a266b17..a8fda7b 100644 --- a/beeref/view.py +++ b/beeref/view.py @@ -110,6 +110,8 @@ class BeeGraphicsView(MainControlsMixin, if hasattr(self, 'sample_color_widget'): self.sample_color_widget.hide() del self.sample_color_widget + if self.scene.has_multi_selection(): + self.scene.multi_select_item.bring_to_front() def update_window_title(self): clean = self.undo_stack.isClean() @@ -358,6 +360,11 @@ class BeeGraphicsView(MainControlsMixin, self.viewport().setCursor(Qt.CursorShape.CrossCursor) self.active_mode = self.SAMPLE_COLOR_MODE + if self.scene.has_multi_selection(): + # We don't want to sample the multi select item, so + # temporarily send it to the back: + self.scene.multi_select_item.lower_behind_selection() + pos = self.mapFromGlobal(self.cursor().pos()) self.sample_color_widget = widgets.SampleColorWidget( self, diff --git a/tests/selection/test_multi_select_item.py b/tests/selection/test_multi_select_item.py index bc18728..322210a 100644 --- a/tests/selection/test_multi_select_item.py +++ b/tests/selection/test_multi_select_item.py @@ -47,6 +47,46 @@ def test_selection_action_items(view): assert action_items == {item1, item2, view.scene.multi_select_item} +def test_lower_behind_selection_when_selection(view): + item1 = BeePixmapItem(QtGui.QImage()) + view.scene.addItem(item1) + item1.setSelected(True) + item2 = BeePixmapItem(QtGui.QImage()) + view.scene.addItem(item2) + item2.setSelected(True) + item3 = BeePixmapItem(QtGui.QImage()) + view.scene.addItem(item3) + item3.setSelected(False) + + item1.setZValue(3) + item2.setZValue(4) + item3.setZValue(1) + + view.scene.multi_select_item.setZValue(5) + view.scene.multi_select_item.lower_behind_selection() + assert view.scene.multi_select_item.zValue() == 2.999 + + +def test_lower_behind_selection_when_no_selection(view): + item1 = BeePixmapItem(QtGui.QImage()) + view.scene.addItem(item1) + item1.setSelected(False) + item2 = BeePixmapItem(QtGui.QImage()) + view.scene.addItem(item2) + item2.setSelected(False) + item3 = BeePixmapItem(QtGui.QImage()) + view.scene.addItem(item3) + item3.setSelected(False) + + item1.setZValue(3) + item2.setZValue(4) + item3.setZValue(1) + + view.scene.multi_select_item.setZValue(5) + view.scene.multi_select_item.lower_behind_selection() + assert view.scene.multi_select_item.zValue() == 5 + + def test_fit_selection_area(): item = MultiSelectItem() item.setScale(5) diff --git a/tests/test_view.py b/tests/test_view.py index 3bee979..497cc0f 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -658,6 +658,24 @@ def test_on_action_sample_color(view): view.cancel_active_modes.assert_called_once_with() +def test_on_action_sample_color_when_multi_selection(view, item): + view.scene.addItem(item) + item.setSelected(True) + item2 = BeeTextItem('foo') + view.scene.addItem(item2) + item2.setSelected(True) + + view.cancel_active_modes = MagicMock() + view.scene.multi_select_item.lower_behind_selection = MagicMock() + view.on_action_sample_color() + assert view.active_mode == view.SAMPLE_COLOR_MODE + assert isinstance(view.sample_color_widget, widgets.SampleColorWidget) + assert view.viewport().cursor() == Qt.CursorShape.CrossCursor + view.cancel_active_modes.assert_called_once_with() + view.scene.multi_select_item.lower_behind_selection\ + .assert_called_once_with() + + @patch('PyQt6.QtWidgets.QWidget.create') @patch('PyQt6.QtWidgets.QWidget.destroy') @patch('PyQt6.QtWidgets.QWidget.show') @@ -815,6 +833,26 @@ def test_cancel_active_modes_when_sample_color_mode(view): assert view.viewport().cursor() == Qt.CursorShape.ArrowCursor +def test_cancel_sample_color_mode_when_multi_selection(view, item): + view.scene.addItem(item) + item.setSelected(True) + item2 = BeeTextItem('foo') + view.scene.addItem(item2) + item2.setSelected(True) + + view.scene.multi_select_item.bring_to_front = MagicMock() + view.active_mode = view.SAMPLE_COLOR_MODE + view.sample_color_widget = widgets.SampleColorWidget( + view, MagicMock(), MagicMock()) + view.viewport().setCursor(Qt.CursorShape.CrossCursor) + view.cancel_active_modes() + + assert view.active_mode is None + assert hasattr(view, 'sample_color_widget') is False + assert view.viewport().cursor() == Qt.CursorShape.ArrowCursor + view.scene.multi_select_item.bring_to_front.assert_called_once() + + @patch('PyQt6.QtGui.QUndoStack.isClean', return_value=True) def test_update_window_title_no_changes_no_filename(clear_mock, view): view.filename = None