Skip to content

Commit

Permalink
first pass
Browse files Browse the repository at this point in the history
  • Loading branch information
rbreu committed Feb 25, 2024
1 parent 51b9baf commit 0c414f4
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 77 deletions.
7 changes: 7 additions & 0 deletions beeref/actions/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,13 @@ def __getitem__(self, key):
'callback': 'on_action_show_color_gamut',
'group': 'active_when_single_image',
}),
Action({
'id': 'sample_color',
'text': 'Sample Color',
'shortcuts': ['S'],
'callback': 'on_action_sample_color',
'group': 'active_when_items_in_scene',
}),
Action({
'id': 'crop',
'text': '&Crop',
Expand Down
1 change: 1 addition & 0 deletions beeref/actions/menu_structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
'grayscale',
MENU_SEPARATOR,
'show_color_gamut',
'sample_color',
],
},
{
Expand Down
2 changes: 1 addition & 1 deletion beeref/fileio/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class ExporterBase:

def __init__(self, scene):
self.scene = scene
self.scene.cancel_crop_mode()
self.scene.cancel_active_modes()
self.scene.deselect_all_items()
# Selection outlines/handles will be rendered to the exported
# image, so deselect first. (Alternatively, pass an attribute
Expand Down
17 changes: 16 additions & 1 deletion beeref/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ 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."""

Expand All @@ -64,7 +67,7 @@ def selection_action_items(self):
def on_selected_change(self, value):
if (value and self.scene()
and not self.scene().has_selection()
and not self.scene().rubberband_active):
and not self.scene().active_mode is None):
self.bring_to_front()

def update_from_data(self, **kwargs):
Expand Down Expand Up @@ -140,6 +143,18 @@ def grayscale(self, value):

self.update()

def sample_color_at(self, pos):
ipos = self.mapFromScene(pos)
if self.grayscale:
pm = self._grayscale_pixmap
else:
pm = self.pixmap()
img = pm.toImage()

color = img.pixelColor(int(ipos.x()), int(ipos.y()))
if color.alpha():
return color

def bounding_rect_unselected(self):
if self.crop_mode:
return QtWidgets.QGraphicsPixmapItem.boundingRect(self)
Expand Down
68 changes: 42 additions & 26 deletions beeref/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
cursor_changed = QtCore.pyqtSignal(QtGui.QCursor)
cursor_cleared = QtCore.pyqtSignal()

MOVE_MODE = 1
RUBBERBAND_MODE = 2

def __init__(self, undo_stack):
super().__init__()
self.move_active = False
self.rubberband_active = False
self.active_mode = None
self.undo_stack = undo_stack
self.max_z = 0
self.min_z = 0
Expand Down Expand Up @@ -66,6 +68,19 @@ def removeItem(self, item):
logger.debug(f'Removing item {item}')
super().removeItem(item)

def cancel_active_modes(self):
"""Cancels ongoing crop modes, rubberband modes etc, if there are
any.
"""
self.cancel_crop_mode()
self.end_rubberband_mode()

def end_rubberband_mode(self):
logger.debug('Ending rubberband selection')
if self.rubberband_item.scene():
self.removeItem(self.rubberband_item)
self.active_mode = None

def cancel_crop_mode(self):
"""Cancels an ongoing crop mode, if there is any."""
if self.crop_item:
Expand All @@ -84,7 +99,7 @@ def paste_from_internal_clipboard(self, position):
self.undo_stack.push(commands.InsertItems(self, copies, position))

def raise_to_top(self):
self.cancel_crop_mode()
self.cancel_active_modes()
items = self.selectedItems(user_only=True)
z_values = map(lambda i: i.zValue(), items)
delta = self.max_z + self.Z_STEP - min(z_values)
Expand All @@ -93,7 +108,7 @@ def raise_to_top(self):
item.setZValue(item.zValue() + delta)

def lower_to_bottom(self):
self.cancel_crop_mode()
self.cancel_active_modes()
items = self.selectedItems(user_only=True)
z_values = map(lambda i: i.zValue(), items)
delta = self.min_z - self.Z_STEP - max(z_values)
Expand All @@ -109,7 +124,7 @@ def normalize_width_or_height(self, mode):
:param mode: "width" or "height".
"""

self.cancel_crop_mode()
self.cancel_active_modes()
values = []
items = self.selectedItems(user_only=True)
for item in items:
Expand Down Expand Up @@ -141,7 +156,7 @@ def normalize_size(self):
Size meaning the area = widh * height.
"""

self.cancel_crop_mode()
self.cancel_active_modes()
sizes = []
items = self.selectedItems(user_only=True)
for item in items:
Expand All @@ -164,7 +179,7 @@ def normalize_size(self):
def arrange(self, vertical=False):
"""Arrange items in a line (horizontally or vertically)."""

self.cancel_crop_mode()
self.cancel_active_modes()

items = self.selectedItems(user_only=True)
if len(items) < 2:
Expand Down Expand Up @@ -205,7 +220,7 @@ def arrange(self, vertical=False):
positions))

def arrange_optimal(self):
self.cancel_crop_mode()
self.cancel_active_modes()

items = self.selectedItems(user_only=True)
if len(items) < 2:
Expand Down Expand Up @@ -243,7 +258,7 @@ def arrange_optimal(self):

def flip_items(self, vertical=False):
"""Flip selected items."""
self.cancel_crop_mode()
self.cancel_active_modes()
self.undo_stack.push(
commands.FlipItems(self.selectedItems(user_only=True),
self.get_selection_center(),
Expand All @@ -259,15 +274,20 @@ def crop_items(self):
if item.is_image:
item.enter_crop_mode()

def sample_color_at(self, position):
item_at_pos = self.itemAt(position, self.views()[0].transform())
if item_at_pos:
return item_at_pos.sample_color_at(position)

def select_all_items(self):
self.cancel_crop_mode()
self.cancel_active_modes()
path = QtGui.QPainterPath()
path.addRect(self.itemsBoundingRect())
# This is faster than looping through all items and calling setSelected
self.setSelectionArea(path)

def deselect_all_items(self):
self.cancel_crop_mode()
self.cancel_active_modes()
self.clearSelection()

def has_selection(self):
Expand Down Expand Up @@ -316,16 +336,16 @@ def mousePressEvent(self, event):
super().mousePressEvent(event)
return
if item_at_pos:
self.move_active = True
self.active_mode = self.MOVE_MODE
elif self.items():
self.rubberband_active = True
self.active_mode = self.RUBBERBAND_MODE

super().mousePressEvent(event)

def mouseDoubleClickEvent(self, event):
self.cancel_active_modes()
item = self.itemAt(event.scenePos(), self.views()[0].transform())
if item:
self.move_active = False
if not item.isSelected():
item.setSelected(True)
if item.is_editable:
Expand All @@ -339,7 +359,7 @@ def mouseDoubleClickEvent(self, event):
super().mouseDoubleClickEvent(event)

def mouseMoveEvent(self, event):
if self.rubberband_active:
if self.active_mode == self.RUBBERBAND_MODE:
if not self.rubberband_item.scene():
logger.debug('Activating rubberband selection')
self.addItem(self.rubberband_item)
Expand All @@ -350,22 +370,19 @@ def mouseMoveEvent(self, event):
super().mouseMoveEvent(event)

def mouseReleaseEvent(self, event):
if self.rubberband_active:
if self.rubberband_item.scene():
logger.debug('Ending rubberband selection')
self.removeItem(self.rubberband_item)
self.rubberband_active = False
if (self.move_active
if self.active_mode == self.RUBBERBAND_MODE:
self.end_rubberband_mode()
if (self.active_mode == self.MOVE_MODE
and self.has_selection()
and not self.multi_select_item.is_action_active()
and not self.selectedItems()[0].is_action_active()):
and self.multi_select_item.active_mode is None
and self.selectedItems()[0].active_mode is None):
delta = event.scenePos() - self.event_start
if not delta.isNull():
self.undo_stack.push(
commands.MoveItemsBy(self.selectedItems(),
delta,
ignore_first_redo=True))
self.move_active = False
self.active_mode = None
super().mouseReleaseEvent(event)

def selectedItems(self, user_only=False):
Expand Down Expand Up @@ -446,8 +463,7 @@ def on_selection_change(self):

def on_change(self, region):
if (self.multi_select_item.scene()
and not self.multi_select_item.scale_active
and not self.multi_select_item.rotate_active):
and self.multi_select_item.active_mode is None):
self.multi_select_item.fit_selection_area(
self.itemsBoundingRect(selection_only=True))

Expand Down
42 changes: 18 additions & 24 deletions beeref/selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,26 +140,20 @@ class SelectableMixin(BaseItemMixin):
SELECT_ROTATE_SIZE = 10 # size of hover area for rotating
SELECT_FREE_CENTER = 20 # size of handle-free area in the center

SCALE_MODE = 1
ROTATE_MODE = 2
FLIP_MODE = 3

def init_selectable(self):
self.setAcceptHoverEvents(True)
self.setFlags(
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)

self.viewport_scale = 1
self.reset_actions()
self.active_mode = None
self.is_editable = False

def reset_actions(self):
self.scale_active = False
self.rotate_active = False
self.flip_active = False

def is_action_active(self):
return any((self.scale_active,
self.rotate_active,
self.flip_active))

def fixed_length_for_viewport(self, value):
"""The interactable areas need to stay the same size on the
screen so we need to adjust the values according to the scale
Expand Down Expand Up @@ -423,7 +417,7 @@ def mousePressEvent(self, event):
# Check if we are in one of the corner's scale areas
if self.get_scale_bounds(corner).contains(event.pos()):
# Start scale action for this corner
self.scale_active = True
self.active_mode = self.SCALE_MODE
self.event_direction = self.get_direction_from_center(
event.scenePos())
self.event_anchor = self.mapToScene(
Expand All @@ -435,7 +429,7 @@ def mousePressEvent(self, event):
# Check if we are in one of the corner's rotate areas
if self.get_rotate_bounds(corner).contains(event.pos()):
# Start rotate action
self.rotate_active = True
self.active_mode = self.ROTATE_MODE
self.event_anchor = self.center_scene_coords
self.rotate_start_angle = self.get_rotate_angle(
event.scenePos())
Expand All @@ -446,7 +440,7 @@ def mousePressEvent(self, event):
# Check if we are in one of the flip edges:
for edge in self.get_flip_bounds():
if edge['rect'].contains(event.pos()):
self.flip_active = True
self.active_mode = self.FLIP_MODE
event.accept()
self.scene().undo_stack.push(
commands.FlipItems(
Expand Down Expand Up @@ -549,14 +543,14 @@ def mouseMoveEvent(self, event):
if (event.scenePos() - self.event_start).manhattanLength() > 5:
self.scene().views()[0].reset_previous_transform()

if self.scale_active:
if self.active_mode == self.SCALE_MODE:
factor = self.get_scale_factor(event)
for item in self.selection_action_items():
item.setScale(item.scale_orig_factor * factor,
item.mapFromScene(self.event_anchor))
event.accept()
return
if self.rotate_active:
if self.active_mode == self.ROTATE_MODE:
snap = (event.modifiers() == Qt.KeyboardModifier.ControlModifier
or event.modifiers() == Qt.KeyboardModifier.ShiftModifier)
delta = self.get_rotate_delta(event.scenePos(), snap)
Expand All @@ -566,7 +560,7 @@ def mouseMoveEvent(self, event):
item.mapFromScene(self.event_anchor))
event.accept()
return
if self.flip_active:
if self.active_mode == self.FLIP_MODE:
# We have already flipped on MousePress, but we
# still need to accept the event here as to not
# initiate an item move
Expand All @@ -576,7 +570,7 @@ def mouseMoveEvent(self, event):
super().mouseMoveEvent(event)

def mouseReleaseEvent(self, event):
if self.scale_active:
if self.active_mode == self.SCALE_MODE:
if self.get_scale_factor(event) != 1:
self.scene().undo_stack.push(
commands.ScaleItemsBy(
Expand All @@ -585,9 +579,9 @@ def mouseReleaseEvent(self, event):
self.event_anchor,
ignore_first_redo=True))
event.accept()
self.reset_actions()
self.active_mode = None
return
elif self.rotate_active:
elif self.active_mode == self.ROTATE_MODE:
self.scene().on_selection_change()
if self.get_rotate_delta(event.scenePos()) != 0:
self.scene().undo_stack.push(
Expand All @@ -597,18 +591,18 @@ def mouseReleaseEvent(self, event):
self.event_anchor,
ignore_first_redo=True))
event.accept()
self.reset_actions()
self.active_mode = None
return
elif self.flip_active:
elif self.active_mode == self.FLIP_MODE:
for edge in self.get_flip_bounds():
if edge['rect'].contains(event.pos()):
# We have already flipped on MousePress, but we
# still need to accept the event here as to not
# initiate an item move
event.accept()
self.reset_actions()
self.active_mode = None
return
self.reset_actions()
self.active_mode = None
super().mouseReleaseEvent(event)

def on_view_scale_change(self):
Expand Down
Loading

0 comments on commit 0c414f4

Please sign in to comment.