224 lines
8.0 KiB
Python
Raw Normal View History

2025-03-07 08:03:18 +01:00
# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus <wojtryb@gmail.com>
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import List, Type, TypeVar, Generic, Optional
from functools import cached_property
from enum import Enum
from PyQt5.QtCore import QPoint
from PyQt5.QtGui import QColor
from api_krita import Krita
from core_components import Controller, Instruction
from .pie_menu_utils.settings_gui import (
PieSettings,
NumericPieSettings,
PresetPieSettings,
EnumPieSettings)
from .pie_menu_utils import (
NonPresetPieConfig,
PresetPieConfig,
PieManager,
PieConfig,
PieWidget,
PieStyle,
Label)
from .pie_menu_utils.widget_utils import EditMode, PieButton
from .raw_instructions import RawInstructions
T = TypeVar('T')
class PieMenu(RawInstructions, Generic[T]):
"""
Pick value by hovering over a pie menu widget.
- Widget is displayed under the cursor between key press and release
- Moving mouse in a direction of a value activates in on key release
- When the mouse was not moved past deadzone, value is not changed
- Edit button activates mode in which pie does not hide on key
release and can be configured
### Arguments:
- `name` -- unique name of action. Must match the
definition in shortcut_composer.action file
- `controller` -- defines which krita property will be modified
- `values` -- default list of values to display in pie
- `instructions` -- (optional) list of additional instructions to
perform on key press and release
- `pie_radius_scale` -- (optional) default widget size multiplier
- `icon_radius_scale` -- (optional) default icons size multiplier
- `background_color` -- (optional) default rgba color of background
- `active_color` -- (optional) default rgba color of active pie
- `short_vs_long_press_time` -- (optional) time [s] that specifies
if key press is short or long
### Action implementation example:
Action is meant to change opacity of current layer to one of
predefined values using the pie menu widget.
```python
templates.PieMenu(
name="Pick active layer opacity",
controller=controllers.LayerOpacityController(),
values=[100, 90, 80, 70, 60, 50, 40, 30, 20, 10],
pie_radius_scale=1.3 # 30% larger menu
icon_radius_scale=0.9 # 10% smaller icons
background_color=QColor(255, 0, 0, 128) # 50% red
active_color=QColor(0, 0, 255) # 100% blue
)
```
"""
def __init__(
self, *,
name: str,
controller: Controller[T],
values: List[T],
instructions: Optional[List[Instruction]] = None,
pie_radius_scale: float = 1.0,
icon_radius_scale: float = 1.0,
background_color: Optional[QColor] = None,
active_color: QColor = QColor(100, 150, 230, 255),
save_local: bool = False,
short_vs_long_press_time: Optional[float] = None
) -> None:
super().__init__(name, instructions, short_vs_long_press_time)
self._controller = controller
def _dispatch_config_type() -> Type[PieConfig[T]]:
if issubclass(self._controller.TYPE, str):
return PresetPieConfig # type: ignore
return NonPresetPieConfig
self._config = _dispatch_config_type()(**{
"name": f"ShortcutComposer: {name}",
"values": values,
"pie_radius_scale": pie_radius_scale,
"icon_radius_scale": icon_radius_scale,
"save_local": save_local,
"background_color": background_color,
"active_color": active_color})
self._config.ORDER.register_callback(self._reset_labels)
self._labels: List[Label] = []
self._edit_mode = EditMode(self)
self._style = PieStyle(items=self._labels, pie_config=self._config)
@cached_property
def pie_widget(self) -> PieWidget:
"""Qwidget of the Pie for selecting values."""
return PieWidget(
style=self._style,
labels=self._labels,
config=self._config)
@cached_property
def pie_settings(self) -> PieSettings:
"""Create and return the right settings based on labels type."""
if issubclass(self._controller.TYPE, str):
return PresetPieSettings(self._config, self._style) # type: ignore
elif issubclass(self._controller.TYPE, float):
return NumericPieSettings(self._config, self._style)
elif issubclass(self._controller.TYPE, Enum):
return EnumPieSettings(
self._controller, self._config, self._style) # type: ignore
raise ValueError(f"Unknown pie config {self._config}")
@cached_property
def pie_manager(self) -> PieManager:
"""Manager which shows, hides and moves Pie widget and its settings."""
return PieManager(pie_widget=self.pie_widget)
@cached_property
def settings_button(self):
"""Button with which user can enter the edit mode."""
settings_button = PieButton(
icon=Krita.get_icon("properties"),
icon_scale=1.1,
parent=self.pie_widget,
radius_callback=lambda: self._style.setting_button_radius,
style=self._style,
config=self._config)
settings_button.clicked.connect(lambda: self._edit_mode.set(True))
return settings_button
@cached_property
def accept_button(self):
"""Button displayed in edit mode, which allows to hide the pie."""
accept_button = PieButton(
icon=Krita.get_icon("dialog-ok"),
icon_scale=1.5,
parent=self.pie_widget,
radius_callback=lambda: self._style.accept_button_radius,
style=self._style,
config=self._config)
accept_button.clicked.connect(lambda: self._edit_mode.set(False))
accept_button.hide()
return accept_button
def _move_buttons(self):
"""Move accept and setting buttons to their correct positions."""
self.accept_button.move_center(self.pie_widget.center)
self.settings_button.move(QPoint(
self.pie_widget.width()-self.settings_button.width(),
self.pie_widget.height()-self.settings_button.height()))
def on_key_press(self) -> None:
"""Handle the event of user pressing the action key."""
super().on_key_press()
if self.pie_widget.isVisible():
return
self._controller.refresh()
self._reset_labels()
self.pie_widget.label_holder.reset() # HACK: should be automatic
self._move_buttons()
self.pie_manager.start()
def on_every_key_release(self) -> None:
"""
Handle the key release event.
Ignore if in edit mode. Otherwise, stop the manager and set the
selected value if deadzone was reached.
"""
super().on_every_key_release()
if self._edit_mode.get():
return
self.pie_manager.stop()
if label := self.pie_widget.active:
self._controller.set_value(label.value)
INVALID_VALUES: 'set[T]' = set()
def _reset_labels(self) -> None:
"""Replace list values with newly created labels."""
values = self._config.values()
# Workaround of krita tags sometimes returning invalid presets
# Bad values are remembered in class attribute and filtered out
filtered_values = [v for v in values if v not in self.INVALID_VALUES]
current_values = [label.value for label in self._labels]
# Method is expensive, and should not be performed when values
# did not in fact change.
if filtered_values == current_values:
return
self._labels.clear()
for value in values:
label = Label.from_value(value, self._controller)
if label is not None:
self._labels.append(label)
else:
self.INVALID_VALUES.add(value)
self._config.refresh_order()