diff --git a/README.md b/README.md index 43893075..23191199 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,23 @@ The plugin adds new shortcuts of the following types: [![PIE MENUS - introducing Shortcut Composer](http://img.youtube.com/vi/hrjBycVYFZM/0.jpg)](https://www.youtube.com/watch?v=hrjBycVYFZM "PIE MENUS - introducing Shortcut Composer") +## What's new in v1.0.1 +Fixes following issues: +- Fix displaying inactive presets and tags +- Detect and support light krita theme, switch between them when changed +- PieWidget as popup - pies no longer recognized by OS as windows +- Allow different parameters for each configuration value +- Use default font to fix MacOS helvetica issue +- Allow using actions when active layer is locked +- Work around krita bug: zoom being dependent on document dpi + ## Requirements -Shortcut Composer **v1.0** Requires krita **5.1.0** or later. +Shortcut Composer **v1.0.1** Requires krita **5.1.0** or later. Supported operating systems: - [x] Windows 10/11 - [x] Linux -- [ ] MacOS (Not tested yet - your feedback is very appreciated) +- [ ] MacOS (Known bug of canvas losing focus after using PieMenu) - [ ] Android (Does not support python plugins yet) ## Installation: diff --git a/shortcut_composer/actions.action b/shortcut_composer/actions.action index 7ff784ca..f2097746 100755 --- a/shortcut_composer/actions.action +++ b/shortcut_composer/actions.action @@ -11,17 +11,14 @@ 1 - 1 1 - 1 1 - 1 @@ -30,12 +27,10 @@ 1 - 1 1 - 1 @@ -44,12 +39,10 @@ 1 - 1 1 - 1 @@ -58,27 +51,22 @@ 1 - 1 1 - 1 1 - 1 1 - 1 1 - 1 @@ -87,32 +75,26 @@ 1 - 1 1 - 1 1 - 1 1 - 1 1 - 1 1 - 1 @@ -121,12 +103,10 @@ 0 - 0 0 - 0 diff --git a/shortcut_composer/api_krita/core_api.py b/shortcut_composer/api_krita/core_api.py index 87187d7b..372fab85 100644 --- a/shortcut_composer/api_krita/core_api.py +++ b/shortcut_composer/api_krita/core_api.py @@ -1,11 +1,12 @@ # SPDX-FileCopyrightText: © 2022 Wojciech Trybus # SPDX-License-Identifier: GPL-3.0-or-later -from krita import Krita as Api, Extension +from krita import Krita as Api, Extension, qApp from typing import Callable, Protocol, Any from PyQt5.QtWidgets import QMainWindow, QDesktopWidget, QWidgetAction -from PyQt5.QtGui import QKeySequence +from PyQt5.QtGui import QKeySequence, QColor +from PyQt5.QtCore import QTimer from .wrappers import ( ToolDescriptor, @@ -25,6 +26,7 @@ class KritaInstance: def __init__(self) -> None: self.instance = Api.instance() self.screen_size = QDesktopWidget().screenGeometry(-1).width() + self.main_window: Any = None def get_active_view(self) -> View: """Return wrapper of krita `View`.""" @@ -89,9 +91,30 @@ def add_extension(self, extension: Extension) -> None: """Add extension/plugin/add-on to krita.""" self.instance.addExtension(extension(self.instance)) + def add_theme_change_callback(self, callback: Callable[[], None]) -> Any: + """ + Add method which should be run after the theme is changed. + + Method is delayed with a timer to allow running it on plugin + initialization phase. + """ + def connect_callback(): + self.main_window = self.instance.activeWindow() + if self.main_window is not None: + self.main_window.themeChanged.connect(callback) + QTimer.singleShot(1000, connect_callback) + + @property + def is_light_theme_active(self) -> bool: + """Return if currently set theme is light using it's main color.""" + main_color: QColor = qApp.palette().window().color() + average = (main_color.red()+main_color.green()+main_color.blue()) // 3 + return average > 128 + class KritaWindow(Protocol): """Krita window received in createActions() of main extension file.""" + def createAction( self, name: str, diff --git a/shortcut_composer/api_krita/pyqt/colorizer.py b/shortcut_composer/api_krita/pyqt/colorizer.py index 03f64956..ebfbcaa4 100644 --- a/shortcut_composer/api_krita/pyqt/colorizer.py +++ b/shortcut_composer/api_krita/pyqt/colorizer.py @@ -1,26 +1,31 @@ # SPDX-FileCopyrightText: © 2022 Wojciech Trybus # SPDX-License-Identifier: GPL-3.0-or-later +from collections import defaultdict from enum import Enum from PyQt5.QtGui import QColor +from api_krita import Krita from ..enums import BlendingMode class Color(Enum): """Named custom colors.""" - ORANGE = QColor(236, 109, 39) + WHITE = QColor(248, 248, 248) LIGHT_GRAY = QColor(170, 170, 170) - BLUE = QColor(110, 217, 224) + DARK_GRAY = QColor(90, 90, 90) + BLACK = QColor(0, 0, 0) + YELLOW = QColor(253, 214, 56) + RED = QColor(245, 49, 116) + ORANGE = QColor(236, 109, 39) GREEN = QColor(169, 224, 69) DARK_GREEN = QColor(119, 184, 55) - RED = QColor(245, 49, 116) - YELLOW = QColor(253, 214, 56) - VIOLET = QColor(173, 133, 251) + BLUE = QColor(110, 217, 224) DARK_BLUE = QColor(42, 160, 251) - WHITE = QColor(248, 248, 242) + VERY_DARK_BLUE = QColor(22, 130, 221) + VIOLET = QColor(173, 133, 251) class Colorizer(QColor): @@ -29,7 +34,9 @@ class Colorizer(QColor): @classmethod def blending_mode(cls, mode: BlendingMode) -> QColor: """Return a QColor associated with blending mode. Gray by default.""" - return cls.BLENDING_MODES_MAP.get(mode, Color.LIGHT_GRAY).value + if Krita.is_light_theme_active: + return cls.BLENDING_MODES_LIGHT[mode].value + return cls.BLENDING_MODES_DARK[mode].value @classmethod def percentage(cls, percent: int) -> QColor: @@ -39,24 +46,46 @@ def percentage(cls, percent: int) -> QColor: @staticmethod def _percentage(percent: int) -> Color: """Mapping of percentage values to custom colors.""" - if percent >= 100: - return Color.DARK_GREEN - if percent >= 80: - return Color.GREEN - if percent >= 50: - return Color.WHITE - if percent > 25: - return Color.LIGHT_GRAY - if percent > 10: - return Color.YELLOW - return Color.RED - - BLENDING_MODES_MAP = { + if Krita.is_light_theme_active: + if percent >= 100: + return Color.DARK_GREEN + if percent >= 80: + return Color.GREEN + if percent >= 50: + return Color.BLACK + if percent > 25: + return Color.DARK_GRAY + if percent > 10: + return Color.ORANGE + return Color.RED + else: + if percent >= 100: + return Color.DARK_GREEN + if percent >= 80: + return Color.GREEN + if percent >= 50: + return Color.WHITE + if percent > 25: + return Color.LIGHT_GRAY + if percent > 10: + return Color.YELLOW + return Color.RED + + BLENDING_MODES_DARK = defaultdict(lambda: Color.LIGHT_GRAY, { BlendingMode.NORMAL: Color.WHITE, BlendingMode.OVERLAY: Color.RED, BlendingMode.SCREEN: Color.GREEN, BlendingMode.COLOR: Color.YELLOW, BlendingMode.ADD: Color.DARK_BLUE, BlendingMode.MULTIPLY: Color.BLUE, - } - """Mapping of blending modes to custom colors.""" + }) + """Mapping of blending modes to custom colors in dark theme.""" + BLENDING_MODES_LIGHT = defaultdict(lambda: Color.DARK_GRAY, { + BlendingMode.NORMAL: Color.BLACK, + BlendingMode.OVERLAY: Color.RED, + BlendingMode.SCREEN: Color.ORANGE, + BlendingMode.COLOR: Color.VIOLET, + BlendingMode.ADD: Color.DARK_BLUE, + BlendingMode.MULTIPLY: Color.VERY_DARK_BLUE, + }) + """Mapping of blending modes to custom colors in light theme.""" diff --git a/shortcut_composer/api_krita/wrappers/canvas.py b/shortcut_composer/api_krita/wrappers/canvas.py index 2ac2ef43..bd54e562 100644 --- a/shortcut_composer/api_krita/wrappers/canvas.py +++ b/shortcut_composer/api_krita/wrappers/canvas.py @@ -2,22 +2,30 @@ # SPDX-License-Identifier: GPL-3.0-or-later from dataclasses import dataclass -from typing import Protocol +from typing import Protocol, Any + +from .document import Document class KritaCanvas(Protocol): """Krita `Canvas` object API.""" + def rotation(self) -> float: ... def setRotation(self, angle_deg: float) -> None: ... def zoomLevel(self) -> float: ... def setZoomLevel(self, zoom: float) -> None: ... + def view(self) -> Any: ... @dataclass class Canvas: + """Wraps krita `Canvas` for typing, docs and PEP8 compatibility.""" canvas: KritaCanvas + def __post_init__(self): + self._zoom_scale = Document(self.canvas.view().document()).dpi/7200 + @property def rotation(self) -> float: """Settable property with rotation in degrees between `0` and `360`.""" @@ -30,8 +38,12 @@ def rotation(self, angle_deg: float) -> None: @property def zoom(self) -> float: - """Settable property with zoom level expressed in %.""" - return self.canvas.zoomLevel()/0.04166666 + """ + Settable property with zoom level expressed in %. + + Add a workaround for zoom detected by krita affected by document dpi. + """ + return self.canvas.zoomLevel() / self._zoom_scale @zoom.setter def zoom(self, zoom: float) -> None: diff --git a/shortcut_composer/api_krita/wrappers/database.py b/shortcut_composer/api_krita/wrappers/database.py index 9a724f94..6fc9730f 100644 --- a/shortcut_composer/api_krita/wrappers/database.py +++ b/shortcut_composer/api_krita/wrappers/database.py @@ -52,7 +52,9 @@ def get_preset_names_from_tag(self, tag_name: str) -> List[str]: ON t.id=rt.tag_id JOIN resources r ON r.id = rt.resource_id - WHERE t.name='{tag_name}' + WHERE + t.name='{tag_name}' + AND rt.active = 1 ''' return self._single_column_query(sql_query, "preset") @@ -61,6 +63,9 @@ def get_brush_tags(self) -> List[str]: sql_query = ''' SELECT t.name AS tag FROM tags t + WHERE + t.active = 1 + AND t.resource_type_id = 5 ''' return self._single_column_query(sql_query, "tag") diff --git a/shortcut_composer/api_krita/wrappers/document.py b/shortcut_composer/api_krita/wrappers/document.py index 6d54e641..7e371896 100644 --- a/shortcut_composer/api_krita/wrappers/document.py +++ b/shortcut_composer/api_krita/wrappers/document.py @@ -9,9 +9,11 @@ class KritaDocument(Protocol): """Krita `Document` object API.""" + def activeNode(self) -> KritaNode: ... def setActiveNode(self, node: KritaNode): ... def topLevelNodes(self) -> List[KritaNode]: ... + def resolution(self) -> int: ... def currentTime(self) -> int: ... def setCurrentTime(self, time: int) -> None: ... def refreshProjection(self) -> None: ... @@ -57,6 +59,11 @@ def recursive_search(nodes: List[Node], found_so_far: List[Node]): return found_so_far return recursive_search(self.get_top_nodes(), []) + @property + def dpi(self): + """Return dpi (dot per inch) of the document.""" + return self.document.resolution() + def refresh(self) -> None: """Refresh OpenGL projection of this document.""" self.document.refreshProjection() diff --git a/shortcut_composer/composer_utils/settings_dialog_utils/spin_boxes_layout.py b/shortcut_composer/composer_utils/settings_dialog_utils/spin_boxes_layout.py index 94356233..90a5cf00 100644 --- a/shortcut_composer/composer_utils/settings_dialog_utils/spin_boxes_layout.py +++ b/shortcut_composer/composer_utils/settings_dialog_utils/spin_boxes_layout.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from typing import Dict, Union +from dataclasses import dataclass from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( QDoubleSpinBox, @@ -19,27 +20,78 @@ class SpinBoxesLayout(QFormLayout): """Dialog zone consisting of spin boxes.""" + @dataclass + class ConfigParams: + """Adds spinbox parametrization to the config field.""" + config: Config + step: float + max_value: float + is_int: bool + def __init__(self) -> None: super().__init__() self._forms: Dict[Config, SpinBox] = {} self._add_label("Common settings") - self._add_row(Config.SHORT_VS_LONG_PRESS_TIME, is_int=False) - self._add_row(Config.FPS_LIMIT, is_int=True) + + self._add_row(self.ConfigParams( + Config.SHORT_VS_LONG_PRESS_TIME, + step=0.05, + max_value=4, + is_int=False)) + + self._add_row(self.ConfigParams( + Config.FPS_LIMIT, + step=5, + max_value=300, + is_int=True)) self._add_label("Cursor trackers") - self._add_row(Config.TRACKER_SENSITIVITY_SCALE, is_int=False) - self._add_row(Config.TRACKER_DEADZONE, is_int=True) + + self._add_row(self.ConfigParams( + Config.TRACKER_SENSITIVITY_SCALE, + step=0.05, + max_value=4, + is_int=False)) + + self._add_row(self.ConfigParams( + Config.TRACKER_DEADZONE, + step=1, + max_value=200, + is_int=True)) self._add_label("Pie menus display") - self._add_row(Config.PIE_GLOBAL_SCALE, is_int=False) - self._add_row(Config.PIE_ICON_GLOBAL_SCALE, is_int=False) - self._add_row(Config.PIE_DEADZONE_GLOBAL_SCALE, is_int=False) - self._add_row(Config.PIE_ANIMATION_TIME, is_int=False) - def _add_row(self, config: Config, is_int: bool) -> None: - """Add a spin box to the layout along with its desctiption.""" - self.addRow(config.value, self._create_form(config, is_int)) + self._add_row(self.ConfigParams( + Config.PIE_GLOBAL_SCALE, + step=0.05, + max_value=4, + is_int=False)) + + self._add_row(self.ConfigParams( + Config.PIE_ICON_GLOBAL_SCALE, + step=0.05, + max_value=4, + is_int=False)) + + self._add_row(self.ConfigParams( + Config.PIE_DEADZONE_GLOBAL_SCALE, + step=0.05, + max_value=4, + is_int=False)) + + self._add_row(self.ConfigParams( + Config.PIE_ANIMATION_TIME, + step=0.01, + max_value=1, + is_int=False)) + + def _add_row(self, config_params: ConfigParams) -> None: + """Add a spin box to the layout along with its description.""" + self.addRow( + config_params.config.value, + self._create_form(config_params) + ) def _add_label(self, text: str): label = QLabel(text) @@ -47,14 +99,15 @@ def _add_label(self, text: str): self.addRow(QSplitter(Qt.Horizontal)) self.addRow(label) - def _create_form(self, config: Config, is_int: bool) -> SpinBox: + def _create_form(self, config_params: ConfigParams) -> SpinBox: """Store and return new spin box for required type (int or float).""" - form = QSpinBox() if is_int else QDoubleSpinBox() - form.setObjectName(config.value) + form = QSpinBox() if config_params.is_int else QDoubleSpinBox() + form.setObjectName(config_params.config.value) form.setMinimum(0) - form.setSingleStep(1 if is_int else 0.05) # type: ignore + form.setMaximum(config_params.max_value) # type: ignore + form.setSingleStep(config_params.step) # type: ignore - self._forms[config] = form + self._forms[config_params.config] = form return form def refresh(self) -> None: diff --git a/shortcut_composer/shortcut_composer.py b/shortcut_composer/shortcut_composer.py index 77786a2b..96efc94e 100755 --- a/shortcut_composer/shortcut_composer.py +++ b/shortcut_composer/shortcut_composer.py @@ -26,6 +26,7 @@ def createActions(self, window) -> None: - Create usual actions for transform modes using `TransformModeActions` - Create usual actions for reloading the extension and settings dialog + - Add a callback to reload plugin when krita theme changes - Create complex action manager which holds and binds them to krita """ self._transform_modes = TransformModeActions(window) @@ -34,6 +35,8 @@ def createActions(self, window) -> None: self._reload_action = self._create_reload_action(window) self._settings_action = self._create_settings_action(window) + Krita.add_theme_change_callback(self._reload_composer) + self._manager = ActionManager(window) self._reload_composer() diff --git a/shortcut_composer/templates/pie_menu.py b/shortcut_composer/templates/pie_menu.py index d13a27f8..501c0472 100644 --- a/shortcut_composer/templates/pie_menu.py +++ b/shortcut_composer/templates/pie_menu.py @@ -83,7 +83,7 @@ def __init__( instructions: List[Instruction] = [], pie_radius_scale: float = 1.0, icon_radius_scale: float = 1.0, - background_color: QColor = QColor(75, 75, 75, 190), + background_color: Optional[QColor] = None, active_color: QColor = QColor(100, 150, 230, 255), short_vs_long_press_time: Optional[float] = None ) -> None: diff --git a/shortcut_composer/templates/pie_menu_utils/label.py b/shortcut_composer/templates/pie_menu_utils/label.py index 1f5886cf..a837dcf4 100644 --- a/shortcut_composer/templates/pie_menu_utils/label.py +++ b/shortcut_composer/templates/pie_menu_utils/label.py @@ -6,7 +6,7 @@ from abc import ABC, abstractmethod from PyQt5.QtCore import QPoint, Qt -from PyQt5.QtGui import QFont, QPixmap, QColor, QIcon +from PyQt5.QtGui import QFont, QPixmap, QColor, QIcon, QFontDatabase from PyQt5.QtWidgets import QLabel, QWidget from api_krita.pyqt import Painter, Text, PixmapTransform @@ -95,7 +95,7 @@ def _create_pyqt_label(self) -> QLabel: label = QLabel(self.widget) label.setText(to_display.value) - label.setFont(QFont('Helvetica', self.style.font_size, QFont.Bold)) + label.setFont(self._font) label.setAlignment(Qt.AlignCenter) label.setGeometry(0, 0, round(heigth*2), round(heigth)) label.move(self.label.center.x()-heigth, @@ -108,6 +108,14 @@ def _create_pyqt_label(self) -> QLabel: label.show() return label + @property + def _font(self) -> QFont: + """Return font which to use in pyqt label.""" + font = QFontDatabase.systemFont(QFontDatabase.TitleFont) + font.setPointSize(self.style.font_size) + font.setBold(True) + return font + @staticmethod def _color_to_str(color: QColor) -> str: return f''' {color.red()}, {color.green()}, {color.blue()}, {color.alpha()}''' diff --git a/shortcut_composer/templates/pie_menu_utils/pie_style.py b/shortcut_composer/templates/pie_menu_utils/pie_style.py index ab523b09..4f09ace8 100644 --- a/shortcut_composer/templates/pie_menu_utils/pie_style.py +++ b/shortcut_composer/templates/pie_menu_utils/pie_style.py @@ -3,6 +3,7 @@ import math import platform +from typing import Optional from dataclasses import dataclass from copy import copy @@ -24,12 +25,19 @@ class PieStyle: fit the given amount of labels. """ - pie_radius_scale: float - icon_radius_scale: float - background_color: QColor - active_color: QColor + def __init__( + self, + pie_radius_scale: float, + icon_radius_scale: float, + background_color: Optional[QColor], + active_color: QColor, + ) -> None: + + self.pie_radius_scale = pie_radius_scale + self.icon_radius_scale = icon_radius_scale + self.background_color = self._pick_background_color(background_color) + self.active_color = active_color - def __post_init__(self): base_size = Krita.screen_size/2560 self.pie_radius = round( @@ -75,9 +83,16 @@ def adapt_to_item_amount(self, amount: int) -> None: max_icon_size = round(self.pie_radius * math.pi / amount) self.icon_radius = min(self.icon_radius, max_icon_size) + def _pick_background_color(self, color: Optional[QColor]) -> QColor: + if color is not None: + return color + if Krita.is_light_theme_active: + return QColor(210, 210, 210, 190) + return QColor(75, 75, 75, 190) + SYSTEM_FONT_SIZE = { "Linux": 0.40, "Windows": 0.25, - "Darwin": 0.25, + "Darwin": 0.6, "": 0.25, } diff --git a/shortcut_composer/templates/pie_menu_utils/pie_widget.py b/shortcut_composer/templates/pie_menu_utils/pie_widget.py index 8ae306e9..969251c2 100644 --- a/shortcut_composer/templates/pie_menu_utils/pie_widget.py +++ b/shortcut_composer/templates/pie_menu_utils/pie_widget.py @@ -41,8 +41,11 @@ def __init__( self._style = style self._label_painters = self._create_label_painters() - flags = self.windowFlags() | Qt.FramelessWindowHint # type: ignore - self.setWindowFlags(flags) + self.setWindowFlags(( + self.windowFlags() | # type: ignore + Qt.Popup | + Qt.FramelessWindowHint | + Qt.NoDropShadowWindowHint)) self.setAttribute(Qt.WA_TranslucentBackground) self.setStyleSheet("background: transparent;") self.setCursor(Qt.CrossCursor)