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)