diff --git a/README.md b/README.md index 29415720..3f65c589 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Shortcut composer **v1.3.2** +# Shortcut composer **v1.4.0** [![python](https://img.shields.io/badge/Python-3.8-3776AB.svg?style=flat&logo=python&logoColor=white)](https://www.python.org) [![Code style: black](https://img.shields.io/badge/code%20style-autopep8-333333.svg)](https://pypi.org/project/autopep8/) @@ -28,16 +28,19 @@ The plugin adds new shortcuts of the following types: - [Join community discussion 👥](https://krita-artists.org/t/shortcut-composer-v1-2-2-plugin-for-pie-menus-multiple-key-assignment-mouse-trackers-and-more/55314) - [Report a bug 🦗](https://github.com/wojtryb/Shortcut-Composer/issues) - [Request a new feature 💡](https://github.com/wojtryb/Shortcut-Composer/discussions) +- [What's new in latest version? ⭐](https://github.com/wojtryb/Shortcut-Composer/releases) -## What's new in the latest release? - -Watch the video below, or read the [changelog](https://github.com/wojtryb/Shortcut-Composer/releases). +## Changelog videos [![PIE MENUS - introducing Shortcut Composer](https://user-images.githubusercontent.com/51094047/244950488-83bd44ff-87f6-4b95-82c7-0f5031bb1b8e.png)](https://www.youtube.com/watch?v=eHK5LBMNiU0 "Managing BRUSHES with Shortcut Composer 1.3") +[![PIE MENUS - introducing Shortcut Composer](https://github-production-user-asset-6210df.s3.amazonaws.com/51094047/238015603-3143fc2d-0fa7-4da1-868d-2ec054ccaeb3.png)](https://www.youtube.com/watch?v=Tkf2-U0OyG4 "PIE MENUS - introducing Shortcut Composer") + +[![PIE MENUS - release video](https://github-production-user-asset-6210df.s3.amazonaws.com/51094047/238179887-87c00d86-0e65-46c2-94c4-52bb02c99501.png)](https://youtu.be/hrjBycVYFZM "PIE MENUS - introducing Shortcut Composer") + ## Requirements - Version of krita on plugin release: **5.1.5** -- Required version of krita: **5.1.0** +- Required version of krita: **5.1.0** or later OS support state: - [x] Windows (10, 11) diff --git a/shortcut_composer/INFO.py b/shortcut_composer/INFO.py index ad6b66bb..d87a0d9a 100644 --- a/shortcut_composer/INFO.py +++ b/shortcut_composer/INFO.py @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus # SPDX-License-Identifier: GPL-3.0-or-later -__version__ = "1.3.2" +__version__ = "1.4.0" __author__ = "Wojciech Trybus" __license__ = "GPL-3.0-or-later" diff --git a/shortcut_composer/__init__.py b/shortcut_composer/__init__.py index fb94de0c..f8149c85 100755 --- a/shortcut_composer/__init__.py +++ b/shortcut_composer/__init__.py @@ -14,10 +14,8 @@ sys.path.append(directory := os.path.dirname(__file__)) -from .shortcut_composer import ShortcutComposer -from .api_krita import Krita -from .composer_utils.compatibility_fix import fix_config -fix_config() +from .shortcut_composer import ShortcutComposer # noqa +from .api_krita import Krita # noqa Krita.add_extension(ShortcutComposer) sys.path.remove(directory) diff --git a/shortcut_composer/actions.action b/shortcut_composer/actions.action index 5adc32a6..4ac0d54f 100755 --- a/shortcut_composer/actions.action +++ b/shortcut_composer/actions.action @@ -90,6 +90,21 @@ krita_tool_grid + + 1 + palette-edit + + + + 1 + palette-edit + + + + 1 + palette-edit + + 1 format-text-bold diff --git a/shortcut_composer/actions.py b/shortcut_composer/actions.py index f8ed3c4e..4f82b13e 100644 --- a/shortcut_composer/actions.py +++ b/shortcut_composer/actions.py @@ -14,10 +14,11 @@ from PyQt5.QtGui import QColor -from api_krita.enums import Tool, Toggle, BlendingMode, TransformMode +from api_krita.enums import Action, Tool, Toggle, BlendingMode, TransformMode from core_components import instructions, controllers from data_components import ( CurrentLayerStack, + DeadzoneStrategy, PickStrategy, Slider, Range, @@ -124,7 +125,7 @@ def create_actions() -> List[templates.RawInstructions]: return [ # Scroll timeline by sliding the cursor horizontally or # animated layers by sliding it vertically - + # # Use TemporaryOn instruction to temporarily isolate active layer templates.CursorTracker( name="Scroll timeline or animated layers", @@ -141,7 +142,7 @@ def create_actions() -> List[templates.RawInstructions]: return [ # Scroll brush sizes by sliding the cursor horizontally or # brush opacity layers by sliding it vertically - + # # Opacity is contiguous from 10% to 100%, sizes come from a list # Switch 1% of opacity every 5 px (instead of default 50 px) templates.CursorTracker( @@ -179,7 +180,7 @@ def create_actions() -> List[templates.RawInstructions]: return [ ), ), - # Use pie menu to pick one of the sporadically used tools. + # Use pie menu to pick one of the tools. templates.PieMenu( name="Pick misc tools", controller=controllers.ToolController(), @@ -190,7 +191,53 @@ def create_actions() -> List[templates.RawInstructions]: return [ Tool.MULTI_BRUSH, Tool.ASSISTANTS, ], - pie_radius_scale=0.9 + pie_radius_scale=0.9, + ), + + + # Use pie menu to pick one of the actions + templates.PieMenu( + name="Activate krita action (red)", + controller=controllers.ActionController(), + values=[ + Action.HORIZONTAL_MIRROR_TOOL, + Action.WRAP_AROUND_MODE, + Action.ERASER, + Action.PRESERVE_ALPHA, + Action.MIRROR_CANVAS, + ], + background_color=QColor(95, 65, 65, 190), + active_color=QColor(200, 70, 70), + ), + + # Use pie menu to pick one of the actions + templates.PieMenu( + name="Activate krita action (green)", + controller=controllers.ActionController(), + values=[ + Action.CREATE_BLANK_FRAME, + Action.CREATE_DUPLICATE_FRAME, + Action.REMOVE_KEYFRAME, + Action.VIEW_ONION_SKIN, + Action.RECORD_TIMELAPSE, + ], + background_color=QColor(65, 95, 65, 190), + active_color=QColor(70, 200, 70), + ), + + # Use pie menu to pick one of the actions + templates.PieMenu( + name="Activate krita action (blue)", + controller=controllers.ActionController(), + values=[ + Action.ADD_PAINT_LAYER, + Action.TOGGLE_LAYER_VISIBILITY, + Action.TOGGLE_LAYER_ALPHA_INHERITANCE, + Action.TOGGLE_LAYER_ALPHA, + Action.TOGGLE_LAYER_LOCK, + ], + background_color=QColor(70, 70, 105, 190), + active_color=QColor(110, 160, 235), ), # Use pie menu to pick one of the brush blending modes. @@ -199,6 +246,7 @@ def create_actions() -> List[templates.RawInstructions]: return [ name="Pick painting blending modes", controller=controllers.BlendingModeController(), instructions=[instructions.SetBrushOnNonPaintable()], + deadzone_strategy=DeadzoneStrategy.PICK_TOP, values=[ BlendingMode.NORMAL, BlendingMode.OVERLAY, @@ -208,7 +256,7 @@ def create_actions() -> List[templates.RawInstructions]: return [ BlendingMode.SCREEN, BlendingMode.DARKEN, BlendingMode.LIGHTEN, - ] + ], ), # Use pie menu to create painting layer with selected blending mode. @@ -232,6 +280,7 @@ def create_actions() -> List[templates.RawInstructions]: return [ templates.PieMenu( name="Pick transform tool modes", controller=controllers.TransformModeController(), + deadzone_strategy=DeadzoneStrategy.PICK_TOP, values=[ TransformMode.FREE, TransformMode.PERSPECTIVE, @@ -242,43 +291,50 @@ def create_actions() -> List[templates.RawInstructions]: return [ ] ), - # Use pie menu to pick one of presets from tag specified in settings. + # Use pie menu to pick one of stored presets. # Set tool to FREEHAND BRUSH if current tool does not allow to paint templates.PieMenu( name="Pick brush presets (red)", controller=controllers.PresetController(), instructions=[instructions.SetBrushOnNonPaintable()], + deadzone_strategy=DeadzoneStrategy.PICK_PREVIOUS, values=Tag("★ My Favorites"), background_color=QColor(95, 65, 65, 190), active_color=QColor(200, 70, 70), ), - # Use pie menu to pick one of presets from tag specified in settings. + # Use pie menu to pick one of stored presets. # Set tool to FREEHAND BRUSH if current tool does not allow to paint templates.PieMenu( name="Pick brush presets (green)", controller=controllers.PresetController(), instructions=[instructions.SetBrushOnNonPaintable()], + deadzone_strategy=DeadzoneStrategy.PICK_PREVIOUS, values=Tag("RGBA"), background_color=QColor(65, 95, 65, 190), active_color=QColor(70, 200, 70), ), - # Use pie menu to pick one of presets from tag specified in settings. + # Use pie menu to pick one of stored presets. # Set tool to FREEHAND BRUSH if current tool does not allow to paint templates.PieMenu( name="Pick brush presets (blue)", controller=controllers.PresetController(), instructions=[instructions.SetBrushOnNonPaintable()], + deadzone_strategy=DeadzoneStrategy.PICK_PREVIOUS, values=Tag("Erasers"), background_color=QColor(70, 70, 105, 190), active_color=QColor(110, 160, 235), ), + # Use pie menu to pick one of stored presets. + # By default, preset names are stored in .kra document. + # Set tool to FREEHAND BRUSH if current tool does not allow to paint templates.PieMenu( name="Pick local brush presets", controller=controllers.PresetController(), instructions=[instructions.SetBrushOnNonPaintable()], + deadzone_strategy=DeadzoneStrategy.PICK_PREVIOUS, values=[], save_local=True, active_color=QColor(234, 172, 0), diff --git a/shortcut_composer/api_krita/actions/transform_actions.py b/shortcut_composer/api_krita/actions/transform_actions.py index 9e0b5b0f..05bb2504 100644 --- a/shortcut_composer/api_krita/actions/transform_actions.py +++ b/shortcut_composer/api_krita/actions/transform_actions.py @@ -38,15 +38,13 @@ def _create_actions(self, window) -> None: TransformMode.WARP: self.set_warp, TransformMode.CAGE: self.set_cage, TransformMode.LIQUIFY: self.set_liquify, - TransformMode.MESH: self.set_mesh, - } + TransformMode.MESH: self.set_mesh} for action, implementation in _ACTION_MAP.items(): self._actions[action] = Krita.create_action( window=window, name=action.value, - callback=implementation - ) + callback=implementation) def _set_mode(self, mode: TransformMode) -> None: """Set a passed mode. Implementation of the new krita tool.""" @@ -59,6 +57,7 @@ def _set_mode(self, mode: TransformMode) -> None: self._delayed_click(mode) def _delayed_click(self, mode: TransformMode) -> None: + """Trigger an mode after short period of time to workaround a bug.""" method = partial(self._finder.activate_mode, mode=mode, apply=False) QTimer.singleShot(40, method) diff --git a/shortcut_composer/api_krita/core_api.py b/shortcut_composer/api_krita/core_api.py index 08358070..ea47c9b6 100644 --- a/shortcut_composer/api_krita/core_api.py +++ b/shortcut_composer/api_krita/core_api.py @@ -9,7 +9,7 @@ QDesktopWidget, QWidgetAction, QMdiArea) -from PyQt5.QtGui import QKeySequence, QColor, QIcon +from PyQt5.QtGui import QKeySequence, QColor, QIcon, QPalette from PyQt5.QtCore import QTimer from .wrappers import ( @@ -129,10 +129,18 @@ def connect_callback(): self.main_window.themeChanged.connect(callback) QTimer.singleShot(1000, connect_callback) + def get_main_color_from_theme(self) -> QColor: + """Return main color of the current theme.""" + return qApp.palette().color(QPalette.Window) + + def get_active_color_from_theme(self) -> QColor: + """Return active color of the current theme.""" + return qApp.palette().color(QPalette.Highlight) + @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() + main_color = self.get_main_color_from_theme() return main_color.value() > 128 diff --git a/shortcut_composer/api_krita/enums/__init__.py b/shortcut_composer/api_krita/enums/__init__.py index 113d3df3..2ec6cc56 100644 --- a/shortcut_composer/api_krita/enums/__init__.py +++ b/shortcut_composer/api_krita/enums/__init__.py @@ -6,7 +6,15 @@ from .transform_mode import TransformMode from .blending_mode import BlendingMode from .node_types import NodeType +from .action import Action from .toggle import Toggle from .tool import Tool -__all__ = ["TransformMode", "BlendingMode", "NodeType", "Toggle", "Tool"] +__all__ = [ + "TransformMode", + "BlendingMode", + "NodeType", + "Action", + "Toggle", + "Tool" +] diff --git a/shortcut_composer/api_krita/enums/action.py b/shortcut_composer/api_krita/enums/action.py new file mode 100644 index 00000000..6402c87f --- /dev/null +++ b/shortcut_composer/api_krita/enums/action.py @@ -0,0 +1,577 @@ +# SPDX-FileCopyrightText: © 2022 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +from krita import Krita as Api + +from PyQt5.QtGui import QIcon +from .helpers import EnumGroup, Group + + +class Action(EnumGroup): + """ + Contains actions of Krita exposed to the plugin. + + Example usage: `Action.UNDO` + """ + + _file = Group("File") + NEW = "file_new" + OPEN = "file_open" + QUIT = "file_quit" + SAVE = "file_save" + SAVE_AS = "file_save_as" + UNDO = "edit_undo" + REDO = "edit_redo" + FULL_SCREEN_MODE = "fullscreen" + IMPORT_ANIMATION_FRAMES = "file_import_animation" + IMPORT_VIDEO_ANIMATION = "file_import_video_animation" + RENDER_ANIMATION = "render_animation" + RENDER_ANIMATION_AGAIN = "render_animation_again" + CLOSE_ALL = "file_close_all" + OPEN_EXISTING_DOCUMENT_AS_UNTITLED_DOCUMENT = "file_import_file" + EXPORT = "file_export_file" + EXPORT_ADVANCED = "file_export_advanced" + DOCUMENT_INFORMATION = "file_documentinfo" + SAVE_INCREMENTAL_VERSION = "save_incremental_version" + SAVE_INCREMENTAL_BACKUP = "save_incremental_backup" + CREATE_TEMPLATE_FROM_IMAGE = "create_template" + CREATE_COPY_FROM_CURRENT_IMAGE = "create_copy" + + _canvas_navigation = Group("Canvas navigation") + ROTATE_CANVAS_RIGHT = "rotate_canvas_right" + ROTATE_CANVAS_LEFT = "rotate_canvas_left" + RESET_CANVAS_ROTATION = "reset_canvas_rotation" + INSTANT_PREVIEW_MODE = "level_of_detail_mode" + SHOW_STATUS_BAR = "showStatusBar" + SHOW_CANVAS_ONLY = "view_show_canvas_only" + RULERS_TRACK_POINTER = "rulers_track_mouse" + RESET_ZOOM = "zoom_to_100pct" + ZOOM_IN = "view_zoom_in" + ZOOM_OUT = "view_zoom_out" + TOGGLE_ZOOM_FIT_TO_PAGE = "toggle_zoom_to_fit" + MIRROR_VIEW_AROUND_CURSOR = "mirror_canvas_around_cursor" + + _canvas_toggles = Group("Canvas toggles") + TOGGLE_BRUSH_OUTLINE = "toggle_brush_outline" + SHOW_GUIDES = "view_show_guides" + LOCK_GUIDES = "view_lock_guides" + SHOW_PIXEL_GRID = "view_pixel_grid" + SNAP_TO_GUIDES = "view_snap_to_guides" + SNAP_ORTHOGONAL = "view_snap_orthogonal" + SNAP_NODE = "view_snap_node" + SNAP_EXTENSION = "view_snap_extension" + SNAP_INTERSECTION = "view_snap_intersection" + SNAP_BOUNDING_BOX = "view_snap_bounding_box" + SNAP_IMAGE_BOUNDS = "view_snap_image_bounds" + SNAP_IMAGE_CENTRE = "view_snap_image_center" + SNAP_PIXEL = "view_snap_to_pixel" + + _filters_shortcuts = Group("Filters shortcuts") + APPLY_FILTER_AGAIN = "filter_apply_again" + APPLY_FILTER_AGAIN_REPROMPT = "filter_apply_reprompt" + FILTER_ASC_CDL = "krita_filter_asc-cdl" + AUTO_CONTRAST = "krita_filter_autocontrast" + BLUR = "krita_filter_blur" + BURN = "krita_filter_burn" + COLOUR_BALANCE = "krita_filter_colorbalance" + COLOUR_TO_ALPHA = "krita_filter_colortoalpha" + COLOUR_TRANSFER = "krita_filter_colortransfer" + CROSS_CHANNEL_ADJUSTMENT_CURVES = "krita_filter_crosschannel" + DESATURATE = "krita_filter_desaturate" + DODGE = "krita_filter_dodge" + EDGE_DETECTION = "krita_filter_edge detection" + EMBOSS_WITH_VARIABLE_DEPTH = "krita_filter_emboss" + EMBOSS_IN_ALL_DIRECTIONS = "krita_filter_emboss all directions" + EMBOSS_HORIZONTAL_VERTICAL = "krita_filter_emboss horizontal and vertical" + EMBOSS_HORIZONTAL_ONLY = "krita_filter_emboss horizontal only" + EMBOSS_LAPLACIAN = "krita_filter_emboss laplascian" + EMBOSS_VERTICAL_ONLY = "krita_filter_emboss vertical only" + GAUSSIAN_BLUR = "krita_filter_gaussian blur" + GAUSSIAN_HIGH_PASS = "krita_filter_gaussianhighpass" + GAUSSIAN_NOISE_REDUCTION = "krita_filter_gaussiannoisereducer" + GRADIENT_MAP = "krita_filter_gradientmap" + HALFTONE = "krita_filter_halftone" + HEIGHT_TO_NORMAL_MAP = "krita_filter_height to normal" + HSV_ADJUSTMENT = "krita_filter_hsvadjustment" + INDEX_COLOURS = "krita_filter_indexcolors" + INVERT = "krita_filter_invert" + LENS_BLUR = "krita_filter_lens blur" + LEVELS = "krita_filter_levels" + MAXIMISE_CHANNEL = "krita_filter_maximize" + MEAN_REMOVAL = "krita_filter_mean removal" + MINIMISE_CHANNEL = "krita_filter_minimize" + MOTION_BLUR = "krita_filter_motion blur" + RANDOM_NOISE = "krita_filter_noise" + NORMALISE = "krita_filter_normalize" + OILPAINT = "krita_filter_oilpaint" + PALETTISE = "krita_filter_palettize" + COLOUR_ADJUSTMENT_CURVES = "krita_filter_perchannel" + PHONG_BUMPMAP = "krita_filter_phongbumpmap" + PIXELISE = "krita_filter_pixelize" + POSTERISE = "krita_filter_posterize" + RAINDROPS = "krita_filter_raindrops" + RANDOM_PICK = "krita_filter_randompick" + ROUND_CORNERS = "krita_filter_roundcorners" + SHARPEN = "krita_filter_sharpen" + SMALL_TILES = "krita_filter_smalltiles" + THRESHOLD = "krita_filter_threshold" + UNSHARP_MASK = "krita_filter_unsharp" + WAVE = "krita_filter_wave" + WAVELET_NOISE_REDUCER = "krita_filter_waveletnoisereducer" + + _edit_document = Group("Edit document") + CUT = "edit_cut" + COPY = "edit_copy" + PASTE = "edit_paste" + COPY_SHARP = "copy_sharp" + CUT_SHARP = "cut_sharp" + PASTE_INTO_NEW_IMAGE = "paste_new" + PASTE_AT_CURSOR = "paste_at" + PASTE_INTO_ACTIVE_LAYER = "paste_into" + PASTE_AS_REFERENCE_IMAGE = "paste_as_reference" + PASTE_SHAPE_STYLE = "paste_shape_style" + COPY_MERGED = "copy_merged" + FLATTEN_IMAGE = "flatten_image" + MERGE_WITH_LAYER_BELOW = "merge_layer" + FLATTEN_LAYER = "flatten_layer" + + _filling = Group("Filling") + FILL_WITH_FOREGROUND_COLOUR = "fill_selection_foreground_color" + FILL_WITH_BACKGROUND_COLOUR = "fill_selection_background_color" + FILL_WITH_PATTERN = "fill_selection_pattern" + FILL_WITH_FOREGROUND_COLOUR_OPACITY = \ + "fill_selection_foreground_color_opacity" + FILL_WITH_BACKGROUND_COLOUR_OPACITY = \ + "fill_selection_background_color_opacity" + FILL_WITH_PATTERN_OPACITY = "fill_selection_pattern_opacity" + STROKE_SELECTED_SHAPES = "stroke_shapes" + + _selection = Group("Selection") + SELECT_ALL = "select_all" + DESELECT = "deselect" + CLEAR = "clear" + RESELECT = "reselect" + INVERT_SELECTION = "invert_selection" + SELECTION_MODE_ADD = "selection_tool_mode_add" + SELECTION_MODE_REPLACE = "selection_tool_mode_replace" + SELECTION_MODE_SUBTRACT = "selection_tool_mode_subtract" + SELECTION_MODE_INTERSECT = "selection_tool_mode_intersect" + DISPLAY_SELECTION = "toggle_display_selection" + TRIM_TO_SELECTION = "resizeimagetoselection" + EDIT_SELECTION = "edit_selection" + CONVERT_TO_VECTOR_SELECTION = "convert_to_vector_selection" + CONVERT_TO_RASTER_SELECTION = "convert_to_raster_selection" + CONVERT_SHAPES_TO_VECTOR_SELECTION = "convert_shapes_to_vector_selection" + CONVERT_TO_SHAPE = "convert_selection_to_shape" + TOGGLE_SELECTION_DISPLAY_MODE = "toggle-selection-overlay-mode" + STROKE_SELECTION = "stroke_selection" + COPY_SELECTION_TO_NEW_LAYER = "copy_selection_to_new_layer" + CUT_SELECTION_TO_NEW_LAYER = "cut_selection_to_new_layer" + SELECT_FROM_COLOUR_RANGE = "colorrange" + SELECT_OPAQUE_REPLACE = "selectopaque" + SELECT_OPAQUE_ADD = "selectopaque_add" + SELECT_OPAQUE_SUBTRACT = "selectopaque_subtract" + SELECT_OPAQUE_INTERSECT = "selectopaque_intersect" + GROW_SELECTION = "growselection" + SHRINK_SELECTION = "shrinkselection" + BORDER_SELECTION = "borderselection" + FEATHER_SELECTION = "featherselection" + SMOOTH = "smoothselection" + + _layer_stack = Group("Layer stack") + LAYER_PROPERTIES = "layer_properties" + COPY_LAYER = "copy_layer_clipboard" + CUT_LAYER = "cut_layer_clipboard" + PASTE_LAYER = "paste_layer_from_clipboard" + DUPLICATE_LAYER_OR_MASK = "duplicatelayer" + RENAME_CURRENT_LAYER = "RenameCurrentLayer" + REMOVE_LAYER = "remove_layer" + QUICK_GROUP = "create_quick_group" + QUICK_CLIPPING_GROUP = "create_quick_clipping_group" + QUICK_UNGROUP = "quick_ungroup" + MIRROR_LAYER_HORIZONTALLY = "mirrorNodeX" + MIRROR_LAYER_VERTICALLY = "mirrorNodeY" + MIRROR_ALL_LAYERS_HORIZONTALLY = "mirrorAllNodesX" + MIRROR_ALL_LAYERS_VERTICALLY = "mirrorAllNodesY" + PIN_TO_TIMELINE = "pin_to_timeline" + TOGGLE_LAYER_SOLOING = "toggle_layer_soloing" + SAVE_GROUP_LAYERS = "save_groups_as_images" + MOVE_LAYER_OR_MASK_UP = "move_layer_up" + MOVE_LAYER_OR_MASK_DOWN = "move_layer_down" + SELECT_ALL_LAYERS = "select_all_layers" + SELECT_VISIBLE_LAYERS = "select_visible_layers" + SELECT_LOCKED_LAYERS = "select_locked_layers" + SELECT_INVISIBLE_LAYERS = "select_invisible_layers" + SELECT_UNLOCKED_LAYERS = "select_unlocked_layers" + ACTIVATE_NEXT_LAYER = "activateNextLayer" + ACTIVATE_NEXT_SIBLING_LAYER = \ + "activateNextSiblingLayer" + ACTIVATE_PREVIOUS_LAYER = "activatePreviousLayer" + ACTIVATE_PREVIOUS_SIBLING_LAYER = \ + "activatePreviousSiblingLayer" + CONVERT_GROUP_TO_ANIMATED_LAYER = "convert_group_to_animated" + TRIM_TO_CURRENT_LAYER = "resizeimagetolayer" + TRIM_TO_IMAGE_SIZE = "trim_to_image" + LAYER_STYLE = "layer_style" + COPY_LAYER_STYLE = "copy_layer_style" + PASTE_LAYER_STYLE = "paste_layer_style" + ACTIVATE_PREVIOUSLY_SELECTED_LAYER = "switchToPreviouslyActiveNode" + SAVE_LAYER_MASK = "save_node_as_image" + SAVE_VECTOR_LAYER_AS_SVG = "save_vector_node_to_svg" + TO_FILE_LAYER = "convert_to_file_layer" + + _layer_creation = Group("Layer creation") + ADD_PAINT_LAYER = "add_new_paint_layer" + ADD_GROUP_LAYER = "add_new_group_layer" + ADD_CLONE_LAYER = "add_new_clone_layer" + ADD_VECTOR_LAYER = "add_new_shape_layer" + ADD_FILTER_LAYER = "add_new_adjustment_layer" + ADD_FILL_LAYER = "add_new_fill_layer" + ADD_FILE_LAYER = "add_new_file_layer" + ADD_TRANSPARENCY_MASK = "add_new_transparency_mask" + ADD_FILTER_MASK = "add_new_filter_mask" + ADD_COLOURISE_MASK = "add_new_colorize_mask" + ADD_TRANSFORM_MASK = "add_new_transform_mask" + ADD_LOCAL_SELECTION = "add_new_selection_mask" + CONVERT_TO_PAINT_LAYER = "convert_to_paint_layer" + CONVERT_TO_SELECTION_MASK = "convert_to_selection_mask" + CONVERT_TO_FILTER_MASK = "convert_to_filter_mask" + CONVERT_TO_TRANSPARENCY_MASK = "convert_to_transparency_mask" + CONVERT_TO_ANIMATED_LAYER = "convert_to_animated" + IMPORT_LAYER = "import_layer_from_file" + AS_PAINT_LAYER = "import_layer_as_paint_layer" + AS_TRANSPARENCY_MASK = "import_layer_as_transparency_mask" + AS_FILTER_MASK = "import_layer_as_filter_mask" + AS_SELECTION_MASK = "import_layer_as_selection_mask" + NEW_LAYER_FROM_VISIBLE = "new_from_visible" + + _layer_handling = Group("Layer handling") + ISOLATE_ACTIVE_GROUP = "isolate_active_group" + TOGGLE_LAYER_VISIBILITY = "toggle_layer_visibility" + TOGGLE_LAYER_LOCK = "toggle_layer_lock" + TOGGLE_LAYER_ALPHA_INHERITANCE = "toggle_layer_inherit_alpha" + TOGGLE_LAYER_ALPHA = "toggle_layer_alpha_lock" + ALPHA_INTO_MASK = "split_alpha_into_mask" + WRITE_AS_ALPHA = "split_alpha_write" + + _brush_property_editing = Group("Brush property editing") + NEXT_FAVOURITE_PRESET = "next_favorite_preset" + PREVIOUS_FAVOURITE_PRESET = "previous_favorite_preset" + SWITCH_TO_PREVIOUS_PRESET = "previous_preset" + RELOAD_ORIGINAL_PRESET = "reload_preset_action" + INCREASE_BRUSH_SIZE = "increase_brush_size" + DECREASE_BRUSH_SIZE = "decrease_brush_size" + MAKE_BRUSH_COLOUR_LIGHTER = "make_brush_color_lighter" + MAKE_BRUSH_COLOUR_DARKER = "make_brush_color_darker" + MAKE_BRUSH_COLOUR_MORE_SATURATED = "make_brush_color_saturated" + MAKE_BRUSH_COLOUR_MORE_DESATURATED = "make_brush_color_desaturated" + SHIFT_BRUSH_COLOUR_HUE_CLOCKWISE = "shift_brush_color_clockwise" + SHIFT_BRUSH_COLOUR_HUE_COUNTER_CLOCKWISE = \ + "shift_brush_color_counter_clockwise" + MAKE_BRUSH_COLOUR_MORE_RED = "make_brush_color_redder" + MAKE_BRUSH_COLOUR_MORE_GREEN = "make_brush_color_greener" + MAKE_BRUSH_COLOUR_MORE_BLUE = "make_brush_color_bluer" + MAKE_BRUSH_COLOUR_MORE_YELLOW = "make_brush_color_yellower" + INCREASE_OPACITY = "increase_opacity" + DECREASE_OPACITY = "decrease_opacity" + INCREASE_FLOW = "increase_flow" + DECREASE_FLOW = "decrease_flow" + INCREASE_FADE = "increase_fade" + DECREASE_FADE = "decrease_fade" + INCREASE_SCATTER = "increase_scatter" + DECREASE_SCATTER = "decrease_scatter" + BRUSH_SMOOTHING_DISABLED = "set_no_brush_smoothing" + BRUSH_SMOOTHING_BASIC = "set_simple_brush_smoothing" + BRUSH_SMOOTHING_WEIGHTED = "set_weighted_brush_smoothing" + BRUSH_SMOOTHING_STABILISER = "set_stabilizer_brush_smoothing" + + _opening_menus = Group("Opening menus") + CONFIGURE_KRITA = "options_configure" + SHOW_BRUSH_EDITOR = "show_brush_editor" + CONFIGURE_TOOLBARS = "options_configure_toolbars" + CONFIGURE_SHORTCUT_COMPOSER = "Configure Shortcut Composer" + MANAGE_RESOURCE_LIBRARIES = "manage_bundles" + MANAGE_RESOURCES = "manage_resources" + START_GMIC_QT = "QMic" + REAPPLY_THE_LAST_GMIC_FILTER = "QMicAgain" + THEMES = "theme_menu" + SHOW_DOCKERS = "view_toggledockers" + RESET_ALL_SETTINGS = "reset_configurations" + PATTERNS = "patterns" + GRADIENTS = "gradients" + CHOOSE_FOREGROUND_AND_BACKGROUND_COLOURS = "dual" + PROPERTIES = "image_properties" + SHOW_SNAP_OPTIONS_POPUP = "show_snap_options_popup" + SHOW_BRUSH_PRESETS = "show_brush_presets" + OPEN_RESOURCES_FOLDER = "open_resources_directory" + + _tool_bars = Group("Tool bars") + HIDE_MIRROR_X_LINE = "mirrorX-hideDecorations" + LOCK_X_LINE = "mirrorX-lock" + MOVE_TO_CANVAS_CENTRE_X = "mirrorX-moveToCenter" + HIDE_MIRROR_Y_LINE = "mirrorY-hideDecorations" + LOCK_Y_LINE = "mirrorY-lock" + MOVE_TO_CANVAS_CENTRE_Y = "mirrorY-moveToCenter" + HORIZONTAL_MIRROR_TOOL = "hmirror_action" + VERTICAL_MIRROR_TOOL = "vmirror_action" + NEXT_BLENDING_MODE = "Next Blending Mode" + PREVIOUS_BLENDING_MODE = "Previous Blending Mode" + BRUSH_COMPOSITE = "composite_actions" + BRUSH_OPTION_SLIDER_1 = "brushslider1" + BRUSH_OPTION_SLIDER_2 = "brushslider2" + BRUSH_OPTION_SLIDER_3 = "brushslider3" + BRUSH_OPTION_SLIDER_4 = "brushslider4" + SHOW_GLOBAL_SELECTION_MASK = "show-global-selection-mask" + WRAP_AROUND_MODE = "wrap_around_mode" + MIRROR = "mirror_actions" + WORKSPACES = "workspaces" + USE_PEN_PRESSURE = "disable_pressure" + OPEN_FOREGROUND_COLOUR_SELECTOR = "chooseForegroundColor" + OPEN_BACKGROUND_COLOUR_SELECTOR = "chooseBackgroundColor" + SWAP_FOREGROUND_AND_BACKGROUND_COLOURS = "toggle_fg_bg" + SET_FOREGROUND_AND_BACKGROUND_COLOURS_TO_BLACK_AND_WHITE = "reset_fg_bg" + + _image_operations = Group("Image operations") + SCALE_IMAGE_TO_NEW_SIZE = "imagesize" + RESIZE_CANVAS = "canvassize" + ROTATE_IMAGE = "rotateimage" + ROTATE_IMAGE_90_TO_THE_RIGHT = "rotateImageCW90" + ROTATE_IMAGE_180 = "rotateImage180" + ROTATE_IMAGE_90_TO_THE_LEFT = "rotateImageCCW90" + MIRROR_IMAGE_HORIZONTALLY = "mirrorImageHorizontal" + MIRROR_IMAGE_VERTICALLY = "mirrorImageVertical" + IMAGE_BACKGROUND_COLOUR_AND_TRANSPARENCY = "image_color" + SEPARATE_IMAGE = "separate" + SHEAR_IMAGE = "shearimage" + OFFSET_IMAGE = "offsetimage" + + _layer_operations = Group("Layer operations") + SCALE_LAYER_TO_NEW_SIZE = "layersize" + SCALE_ALL_LAYERS_TO_NEW_SIZE = "scaleAllLayers" + SHEAR_LAYER = "shearlayer" + SHEAR_ALL_LAYERS = "shearAllLayers" + SPLIT_LAYER = "layersplit" + ROTATE_LAYER = "rotatelayer" + ROTATE_LAYER_180 = "rotateLayer180" + ROTATE_LAYER_90_TO_THE_RIGHT = "rotateLayerCW90" + ROTATE_LAYER_90_TO_THE_LEFT = "rotateLayerCCW90" + ROTATE_ALL_LAYERS = "rotateAllLayers" + ROTATE_ALL_LAYERS_90_TO_THE_RIGHT = "rotateAllLayersCW90" + ROTATE_ALL_LAYERS_90_TO_THE_LEFT = "rotateAllLayersCCW90" + ROTATE_ALL_LAYERS_180 = "rotateAllLayers180" + OFFSET_LAYER = "offsetlayer" + + _tool_specific_actions = Group("Tool specific actions") + MOVE_UP = "movetool-move-up" + MOVE_DOWN = "movetool-move-down" + MOVE_LEFT = "movetool-move-left" + MOVE_RIGHT = "movetool-move-right" + MOVE_UP_MORE = "movetool-move-up-more" + MOVE_DOWN_MORE = "movetool-move-down-more" + MOVE_LEFT_MORE = "movetool-move-left-more" + MOVE_RIGHT_MORE = "movetool-move-right-more" + SHOW_COORDINATES = "movetool-show-coordinates" + SCALE = "selectionscale" + CALLIGRAPHY = "KarbonCalligraphyTool" + CALLIGRAPHY_INCREASE_WIDTH = "calligraphy_increase_width" + CALLIGRAPHY_DECREASE_WIDTH = "calligraphy_decrease_width" + CALLIGRAPHY_INCREASE_ANGLE = "calligraphy_increase_angle" + CALLIGRAPHY_DECREASE_ANGLE = "calligraphy_decrease_angle" + WAVELET_DECOMPOSE = "waveletdecompose" + UNDO_POLYGON_SELECTION_POINTS = "undo_polygon_selection" + CORNER_POINT = "pathpoint-corner" + SMOOTH_POINT = "pathpoint-smooth" + SYMMETRIC_POINT = "pathpoint-symmetric" + MAKE_CURVE_POINT = "pathpoint-curve" + MAKE_LINE_POINT = "pathpoint-line" + SEGMENT_TO_LINE = "pathsegment-line" + SEGMENT_TO_CURVE = "pathsegment-curve" + INSERT_POINT = "pathpoint-insert" + REMOVE_POINT = "pathpoint-remove" + BREAK_AT_POINT = "path-break-point" + BREAK_AT_SEGMENT = "path-break-segment" + JOIN_WITH_SEGMENT = "pathpoint-join" + MERGE_POINTS = "pathpoint-merge" + TO_PATH = "convert-to-path" + BRING_TO_FRONT = "object_order_front" + RAISE = "object_order_raise" + LOWER = "object_order_lower" + SEND_TO_BACK = "object_order_back" + ALIGN_LEFT = "object_align_horizontal_left" + HORIZONTALLY_CENTRE = "object_align_horizontal_center" + ALIGN_RIGHT = "object_align_horizontal_right" + ALIGN_TOP = "object_align_vertical_top" + VERTICALLY_CENTRE = "object_align_vertical_center" + ALIGN_BOTTOM = "object_align_vertical_bottom" + DISTRIBUTE_LEFT = "object_distribute_horizontal_left" + DISTRIBUTE_CENTRES_HORIZONTALLY = "object_distribute_horizontal_center" + DISTRIBUTE_RIGHT = "object_distribute_horizontal_right" + DISTRIBUTE_HORIZONTAL_GAP = "object_distribute_horizontal_gaps" + DISTRIBUTE_TOP = "object_distribute_vertical_top" + DISTRIBUTE_CENTRES_VERTICALLY = "object_distribute_vertical_center" + DISTRIBUTE_BOTTOM = "object_distribute_vertical_bottom" + DISTRIBUTE_VERTICAL_GAP = "object_distribute_vertical_gaps" + GROUP = "object_group" + UNGROUP = "object_ungroup" + ROTATE_90_C_W = "object_transform_rotate_90_cw" + ROTATE_90_A_C_W = "object_transform_rotate_90_ccw" + ROTATE_180 = "object_transform_rotate_180" + MIRROR_HORIZONTALLY = "object_transform_mirror_horizontally" + MIRROR_VERTICALLY = "object_transform_mirror_vertically" + RESET_TRANSFORMATIONS = "object_transform_reset" + UNITE = "object_unite" + INTERSECT = "object_intersect" + SUBTRACT = "object_subtract" + SPLIT = "object_split" + + _animation = Group("Animation") + CREATE_BLANK_FRAME = "add_blank_frame" + CREATE_DUPLICATE_FRAME = "add_duplicate_frame" + INSERT_KEYFRAME_LEFT = "insert_keyframe_left" + INSERT_KEYFRAME_RIGHT = "insert_keyframe_right" + INSERT_MULTIPLE_KEYFRAMES = "insert_multiple_keyframes" + REMOVE_FRAME_AND_PULL = "remove_frames_and_pull" + REMOVE_KEYFRAME = "remove_frames" + INSERT_HOLD_FRAME = "insert_hold_frame" + INSERT_MULTIPLE_HOLD_FRAMES = "insert_multiple_hold_frames" + REMOVE_HOLD_FRAME = "remove_hold_frame" + REMOVE_MULTIPLE_HOLD_FRAMES = "remove_multiple_hold_frames" + MIRROR_FRAMES = "mirror_frames" + COPY_KEYFRAMES = "copy_frames" + CLONE_KEYFRAMES = "copy_frames_as_clones" + MAKE_UNIQUE = "make_clones_unique" + CUT_KEYFRAMES = "cut_frames" + PASTE_KEYFRAMES = "paste_frames" + SET_START_TIME = "set_start_time" + SET_END_TIME = "set_end_time" + UPDATE_PLAYBACK_RANGE = "update_playback_range" + PLAY_PAUSE_ANIMATION = "toggle_playback" + STOP_ANIMATION = "stop_playback" + PREVIOUS_FRAME = "previous_frame" + NEXT_FRAME = "next_frame" + PREVIOUS_KEYFRAME = "previous_keyframe" + NEXT_KEYFRAME = "next_keyframe" + PREVIOUS_MATCHING_KEYFRAME = "previous_matching_keyframe" + NEXT_MATCHING_KEYFRAME = "next_matching_keyframe" + PREVIOUS_UNFILTERED_KEYFRAME = "previous_unfiltered_keyframe" + NEXT_UNFILTERED_KEYFRAME = "next_unfiltered_keyframe" + AUTO_FRAME_MODE = "auto_key" + ADD_SCALAR_KEYFRAMES = "add_scalar_keyframes" + REMOVE_SCALAR_KEYFRAME = "remove_scalar_keyframe" + HOLD_CONSTANT_VALUE_NO_INTERPOLATION = "interpolation_constant" + LINEAR_INTERPOLATION = "interpolation_linear" + BEZIER_CURVE_INTERPOLATION = "interpolation_bezier" + SHARP_INTERPOLATION_TANGENTS = "tangents_sharp" + SMOOTH_INTERPOLATION_TANGENTS = "tangents_smooth" + ZOOM_VIEW_TO_FIT_CHANNEL_RANGE = "zoom_to_fit_range" + ZOOM_VIEW_TO_FIT_CURVE = "zoom_to_fit_curve" + DROP_FRAMES = "drop_frames" + + _krita_window = Group("Krita window") + DETACH_CANVAS = "view_detached_canvas" + SHOW_DOCKER_TITLEBARS = "view_toggledockertitlebars" + DOCKERS = "settings_dockers_menu" + WINDOW = "window" + STYLES = "style_menu" + CASCADE = "windows_cascade" + TILE = "windows_tile" + NEXT = "windows_next" + PREVIOUS = "windows_previous" + NEW_WINDOW = "view_newwindow" + CLOSE = "file_close" + SESSIONS = "file_sessions" + SEARCH_ACTIONS = "command_bar_open" + + _python_scripts = Group("Python scripts") + IMPORT_PYTHON_PLUGIN_FROM_FILE = "plugin_importer_file" + IMPORT_PYTHON_PLUGIN_FROM_WEB = "plugin_importer_web" + SCRIPTER = "python_scripter" + TEN_SCRIPTS = "ten_scripts" + EXECUTE_SCRIPT_1 = "execute_script_1" + EXECUTE_SCRIPT_2 = "execute_script_2" + EXECUTE_SCRIPT_3 = "execute_script_3" + EXECUTE_SCRIPT_4 = "execute_script_4" + EXECUTE_SCRIPT_5 = "execute_script_5" + EXECUTE_SCRIPT_6 = "execute_script_6" + EXECUTE_SCRIPT_7 = "execute_script_7" + EXECUTE_SCRIPT_8 = "execute_script_8" + EXECUTE_SCRIPT_9 = "execute_script_9" + EXECUTE_SCRIPT_10 = "execute_script_10" + EXPORT_LAYERS = "export_layers" + RECORD_TIMELAPSE = "recorder_record_toggle" + EXPORT_TIMELAPSE = "recorder_export" + HIDE_FILE_TOOLBAR = "mainToolBar" + HIDE_BRUSHES_AND_STUFF_TOOLBAR = "BrushesAndStuff" + ZOOM = "view_zoom" + SHOW_COLOUR_SELECTOR = "show_color_selector" + SHOW_MY_PAINT_SHADE_SELECTOR = "show_mypaint_shade_selector" + SHOW_MINIMAL_SHADE_SELECTOR = "show_minimal_shade_selector" + SHOW_COLOUR_HISTORY = "show_color_history" + SHOW_COMMON_COLOURS = "show_common_colors" + UPDATE_COMPOSITION = "update_composition" + RENAME_COMPOSITION = "rename_composition" + RELOAD_SHORTCUT_COMPOSER = "Reload Shortcut Composer" + + _toggles = Group("Toggles") + ERASER = "erase_action" + PRESERVE_ALPHA = "preserve_alpha" + MIRROR_CANVAS = "mirror_canvas" + SOFT_PROOFING = "softProof" + ISOLATE_LAYER = "isolate_active_layer" + VIEW_REFERENCE_IMAGES = "view_toggle_reference_images" + VIEW_ASSISTANTS = "view_toggle_painting_assistants" + VIEW_ASSISTANTS_PREVIEWS = "view_toggle_assistant_previews" + VIEW_GRID = "view_grid" + VIEW_RULER = "view_ruler" + VIEW_ONION_SKIN = "toggle_onion_skin" + SNAP_ASSISTANT = "toggle_assistant" + SNAP_TO_GRID = "view_snap_to_grid" + + _other = Group("Other") + CONVERT_IMAGE_COLOUR_SPACE = "imagecolorspaceconversion" + CONVERT_LAYER_COLOUR_SPACE = "layercolorspaceconversion" + EXPLORE_RESOURCES_CACHE_DATABASE = "dbexplorer" + IMAGE_SPLIT = "imagesplit" + MOVE_INTO_PREVIOUS_GROUP = "LayerGroupSwitcher/previous" + MOVE_INTO_NEXT_GROUP = "LayerGroupSwitcher/next" + SET_COPY_FROM = "set-copy-from" + CREATE_SNAPSHOT = "create_snapshot" + SWITCH_TO_SELECTED_SNAPSHOT = "switchto_snapshot" + REMOVE_SELECTED_SNAPSHOT = "remove_snapshot" + INSERT_COLUMN_LEFT = "insert_column_left" + INSERT_COLUMN_RIGHT = "insert_column_right" + INSERT_MULTIPLE_COLUMNS = "insert_multiple_columns" + REMOVE_COLUMN_AND_PULL = "remove_columns_and_pull" + REMOVE_COLUMN = "remove_columns" + INSERT_HOLD_COLUMN = "insert_hold_column" + INSERT_MULTIPLE_HOLD_COLUMNS = "insert_multiple_hold_columns" + REMOVE_HOLD_COLUMN = "remove_hold_column" + REMOVE_MULTIPLE_HOLD_COLUMNS = "remove_multiple_hold_columns" + MIRROR_COLUMNS = "mirror_columns" + CLEAR_CACHE = "clear_animation_cache" + COPY_COLUMNS = "copy_columns_to_clipboard" + CUT_COLUMNS = "cut_columns_to_clipboard" + PASTE_COLUMNS = "paste_columns_from_clipboard" + + def activate(self): + """Activate the action.""" + try: + Api.instance().action(self.value).trigger() + except AttributeError: + print(self.value) + + @property + def icon(self) -> QIcon: + """Return the icon of this action.""" + try: + return Api.instance().action(self.value).icon() + except AttributeError: + return QIcon() + + @property + def pretty_name(self) -> str: + """Return the name of this action.""" + try: + return Api.instance().action(self.value).text().replace("&", "") + except AttributeError: + return "---" diff --git a/shortcut_composer/api_krita/enums/blending_mode.py b/shortcut_composer/api_krita/enums/blending_mode.py index 381c5cae..5283be47 100644 --- a/shortcut_composer/api_krita/enums/blending_mode.py +++ b/shortcut_composer/api_krita/enums/blending_mode.py @@ -1,149 +1,197 @@ # SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus # SPDX-License-Identifier: GPL-3.0-or-later -from enum import Enum +from typing import Optional +from .helpers import EnumGroup, Group -class BlendingMode(Enum): +class BlendingMode(EnumGroup): """ Contains all known blending modes in krita. Example usage: `BlendingMode.NORMAL` """ - NORMAL = "normal" - ADD = "add" - BURN = "burn" - COLOR = "color" - DODGE = "dodge" - DARKEN = "darken" + _arithmetic = Group("Arithmetic") + ADD = "add", "Addition" DIVIDE = "divide" - ERASE = "erase" - LIGHTEN = "lighten" - LUMINIZE = "luminize" - MULTIPLY = "multiply" - OVERLAY = "overlay" - SATURATION = "saturation" - SCREEN = "screen" - SOFT_LIGHT_SVG = "soft_light_svg" INVERSE_SUBTRACT = "inverse_subtract" + MULTIPLY = "multiply" SUBTRACT = "subtract" - AND = "and" - CONVERSE = "converse" - IMPLICATION = "implication" - NAND = "nand" - NOR = "nor" - NOT_CONVERSE = "not_converse" - NOT_IMPLICATION = "not_implication" - OR = "or" - XNOR = "xnor" - XOR = "xor" + + _binary = Group("Binary") + AND = "and", "AND" + CONVERSE = "converse", "CONVERSE" + IMPLICATION = "implication", "IMPLICATION" + NAND = "nand", "NAND" + NOR = "nor", "NOR" + NOT_CONVERSE = "not_converse", "NOT_CONVERSE" + NOT_IMPLICATION = "not_implication", "NOT_IMPLICATION" + OR = "or", "OR" + XNOR = "xnor", "XNOR" + XOR = "xor", "XOR" + + _darken = Group("Darken") + BURN = "burn" + DARKEN = "darken" DARKER_COLOR = "darker color" EASY_BURN = "easy burn" - FOG_DARKEN_IFS_ILLUSIONS = "fog_darken_ifs_illusions" + FOG_DARKEN_IFS_ILLUSIONS = ( + "fog_darken_ifs_illusions", + "Fog Darken (IFS Illusions)") GAMMA_DARK = "gamma_dark" - SHADE_IFS_ILLUSIONS = "shade_ifs_illusions" LINEAR_BURN = "linear_burn" - COLOR_HSI = "color_hsi" - DEC_INTENSITY = "dec_intensity" - DEC_SATURATION_HSI = "dec_saturation_hsi" - HUE_HSI = "hue_hsi" - INC_INTENSITY = "inc_intensity" - INC_SATURATION_HSI = "inc_saturation_hsi" + SHADE_IFS_ILLUSIONS = ( + "shade_ifs_illusions" + "Shade (IFS Illusions)") + + _hsi = Group("HSI") + COLOR_HSI = "color_hsi", "Color HSI" + DEC_INTENSITY = "dec_intensity", "Decrease Intensity" + DEC_SATURATION_HSI = "dec_saturation_hsi", "Decrease Saturation HSI" + HUE_HSI = "hue_hsi", "Hue HSI" + INC_INTENSITY = "inc_intensity", "Increase Intensity" + INC_SATURATION_HSI = "inc_saturation_hsi", "Increase Saturation HSI" INTENSITY = "intensity" - SATURATION_HSI = "saturation_hsi" - DEC_LIGHTNESS = "dec_lightness" - COLOR_HSL = "color_hsl" - DEC_SATURATION_HSL = "dec_saturation_hsl" - HUE_HSL = "hue_hsl" - INC_LIGHTNESS = "inc_lightness" - INC_SATURATION_HSL = "inc_saturation_hsl" + SATURATION_HSI = "saturation_hsi", "Saturation HSI" + + _hsl = Group("HSL") + COLOR_HSL = "color_hsl", "Color HSL" + DEC_LIGHTNESS = "dec_lightness", "Decrease Lightness" + DEC_SATURATION_HSL = "dec_saturation_hsl", "Decrease Saturation HSL" + HUE_HSL = "hue_hsl", "Hue HSL" + INC_LIGHTNESS = "inc_lightness", "Increase Lightness" + INC_SATURATION_HSL = "inc_saturation_hsl", "Increase Saturation HSL" LIGHTNESS = "lightness" - SATURATION_HSL = "saturation_hsl" - COLOR_HSV = "color_hsv" - DEC_SATURATION_HSV = "dec_saturation_hsv" - DEC_VALUE = "dec_value" - HUE_HSV = "hue_hsv" - INC_SATURATION_HSV = "inc_saturation_hsv" - INC_VALUE = "inc_value" - SATURATION_HSV = "saturation_hsv" + SATURATION_HSL = "saturation_hsl", "Saturation HSL" + + _hsv = Group("HSV") + COLOR_HSV = "color_hsv", "Color HSV" + DEC_SATURATION_HSV = "dec_saturation_hsv", "Decrease Saturation HSV" + DEC_VALUE = "dec_value", "Decrease Value" + HUE_HSV = "hue_hsv", "Hue HSV" + INC_SATURATION_HSV = "inc_saturation_hsv", "Increase Saturation HSV" + INC_VALUE = "inc_value", "Increase Value" + SATURATION_HSV = "saturation_hsv", "Saturation HSV" VALUE = "value" - DEC_SATURATION = "dec_saturation" - DEC_LUMINOSITY = "dec_luminosity" + + _hsy = Group("HSY") + COLOR = "color" + DEC_LUMINOSITY = "dec_luminosity", "Decrease Luminosity" + DEC_SATURATION = "dec_saturation", "Decrease Saturation" HUE = "hue" - INC_LUMINOSITY = "inc_luminosity" - INC_SATURATION = "inc_saturation" + INC_LUMINOSITY = "inc_luminosity", "Increase Luminosity" + INC_SATURATION = "inc_saturation", "Increase Saturation" + LUMINIZE = "luminize" + SATURATION = "saturation" + + _lighten = Group("Lighten") + DODGE = "dodge" EASY_DODGE = "easy dodge" FLAT_LIGHT = "flat_light" + FOG_LIGHTEN_IFS_ILLUSIONS = ( + "fog_lighten_ifs_illusions", + "Fog Lighten (IFS Illusions)") GAMMA_ILLUMINATION = "gamma_illumination" - FOG_LIGHTEN_IFS_ILLUSIONS = "fog_lighten_ifs_illusions" GAMMA_LIGHT = "gamma_light" HARD_LIGHT = "hard_light" + LIGHTEN = "lighten" LIGHTER_COLOR = "lighter color" LINEAR_DODGE = "linear_dodge" LINEAR_LIGHT = "linear light" LUMINOSITY_SAI = "luminosity_sai" - PNORM_A = "pnorm_a" - PNORM_B = "pnorm_b" + PNORM_A = "pnorm_a", "P-Norm A" + PNORM_B = "pnorm_b", "P-Norm B" PIN_LIGHT = "pin_light" - SOFT_LIGHT_IFS_ILLUSIONS = "soft_light_ifs_illusions" - SOFT_LIGHT_PEGTOP_DELPHI = "soft_light_pegtop_delphi" - SOFT_LIGHT = "soft_light" + SCREEN = "screen" + SOFT_LIGHT_IFS_ILLUSIONS = ( + "soft_light_ifs_illusions", + "Soft Light (IFS Illusions)") + SOFT_LIGHT_PEGTOP_DELPHI = ( + "soft_light_pegtop_delphi" + "Soft Light (Pegtio-Delphi)") + SOFT_LIGHT = "soft_light", "Soft Light (Photoshop)" + SOFT_LIGHT_SVG = "soft_light_svg", "Soft Light (SVG)" SUPER_LIGHT = "super_light" - TINT_IFS_ILLUSIONS = "tint_ifs_illusions" + TINT_IFS_ILLUSIONS = "tint_ifs_illusions", "Tint (IFS Illusions)" VIVID_LIGHT = "vivid_light" + + _misc = Group("Misc") BUMPMAP = "bumpmap" - COMBINE_NORMAL = "combine_normal" + COMBINE_NORMAL = "combine_normal", "Combine Normal Map" COPY = "copy" COPY_BLUE = "copy_blue" COPY_GREEN = "copy_green" COPY_RED = "copy_red" DISSOLVE = "dissolve" TANGENT_NORMALMAP = "tangent_normalmap" + + _mix = Group("Mix") ALLANON = "allanon" ALPHADARKEN = "alphadarken" BEHIND = "behind" DESTINATION_ATOP = "destination-atop" DESTINATION_IN = "destination-in" + ERASE = "erase" GEOMETRIC_MEAN = "geometric_mean" GRAIN_EXTRACT = "grain_extract" GRAIN_MERGE = "grain_merge" GREATER = "greater" HARD_MIX = "hard mix" - HARD_MIX_PHOTOSHOP = "hard_mix_photoshop" - HARD_MIX_SOFTER_PHOTOSHOP = "hard_mix_softer_photoshop" + HARD_MIX_PHOTOSHOP = "hard_mix_photoshop", "Hard Mix (Photoshop)" + HARD_MIX_SOFTER_PHOTOSHOP = ( + "hard_mix_softer_photoshop", + "Hard Mix Softer (Photoshop)") HARD_OVERLAY = "hard overlay" INTERPOLATION = "interpolation" - INTERPOLATION_2X = "interpolation 2x" + INTERPOLATION_2X = "interpolation 2x", "Interpolation - 2X" + NORMAL = "normal" + OVERLAY = "overlay" PARALLEL = "parallel" PENUMBRA_A = "penumbra a" PENUMBRA_B = "penumbra b" PENUMBRA_C = "penumbra c" PENUMBRA_D = "penumbra d" + + _modulo = Group("Modulo") DIVISIVE_MODULO = "divisive_modulo" - DIVISIVE_MODULO_CONTINUOUS = "divisive_modulo_continuous" - MODULO_CONTINUOUS = "modulo_continuous" + DIVISIVE_MODULO_CONTINUOUS = ( + "divisive_modulo_continuous", + "Divisive Modulo - Continuous") + MODULO_CONTINUOUS = "modulo_continuous", "Modulo - Continuous" MODULO_SHIFT = "modulo_shift" - MODULO_SHIFT_CONTINUOUS = "modulo_shift_continuous" + MODULO_SHIFT_CONTINUOUS = ( + "modulo_shift_continuous", + "Modulo Shift - Continuous") + + _negative = Group("Negative") ADDITIVE_SUBTRACTIVE = "additive_subtractive" ARC_TANGENT = "arc_tangent" DIFF = "diff" EQUIVALENCE = "equivalence" EXCLUSION = "exclusion" NEGATION = "negation" + + _quadratic = Group("Quadratic") FREEZE = "freeze" - FREEZE_REFLECT = "freeze_reflect" + FREEZE_REFLECT = "freeze_reflect", "Freeze-Reflect" GLOW = "glow" - GLOW_HEAT = "glow_heat" + GLOW_HEAT = "glow_heat", "Glow-Heat" HEAT = "heat" - HEAT_GLOW = "heat_glow" - HEAT_GLOW_FREEZE_REFLECT_HYBRID = "heat_glow_freeze_reflect_hybrid" + HEAT_GLOW = "heat_glow", "Heat-Glow" + HEAT_GLOW_FREEZE_REFLECT_HYBRID = ( + "heat_glow_freeze_reflect_hybrid" + "Heat-Glow & Freeze-Reflect Hybrid") REFLECT = "reflect" - REFLECT_FREEZE = "reflect_freeze" + REFLECT_FREEZE = "reflect_freeze", "Reflect-Freeze" + + def __init__(self, value: str, pretty_name: Optional[str] = None): + self._value_ = value + self._custom_pretty_name = pretty_name @property def pretty_name(self) -> str: - """Format blending mode name like: `Darker Color`.""" - parts = self.name.split("_") - parts = [f"{part[0]}{part[1:].lower()}" for part in parts] - return " ".join(parts) + """Format blending mode name as in Krita Blending Mode combobox.""" + if self._custom_pretty_name is not None: + return self._custom_pretty_name + return self.name.replace("_", " ").title() diff --git a/shortcut_composer/api_krita/enums/helpers/__init__.py b/shortcut_composer/api_krita/enums/helpers/__init__.py new file mode 100644 index 00000000..a4c76080 --- /dev/null +++ b/shortcut_composer/api_krita/enums/helpers/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Components used in Enum definitions.""" + +from .enum_group import EnumGroup, Group + +__all__ = ["EnumGroup", "Group"] diff --git a/shortcut_composer/api_krita/enums/helpers/enum_group.py b/shortcut_composer/api_krita/enums/helpers/enum_group.py new file mode 100644 index 00000000..c1470637 --- /dev/null +++ b/shortcut_composer/api_krita/enums/helpers/enum_group.py @@ -0,0 +1,103 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +from typing import Dict, List, Tuple, TypeVar, Optional +from enum import Enum, EnumMeta +T = TypeVar("T", bound=Enum) + + +class EnumGroupMeta(EnumMeta): + """Metaclass for creating enum groups. See EnumGroup documentation.""" + + _groups_: Dict[str, 'Group'] + """Maps enum groups to their pretty names.""" + + def __new__( + cls, + name: str, + bases: Tuple[type, ...], + attrs + ) -> 'EnumGroupMeta': + # Filter out class attributes provided by Python. + items: List[Tuple[str, Group]] + items = [i for i in attrs.items() if not i[0].startswith("__")] + + # Add keys (which will become enum members) to correct groups + current_group: Optional[Group] = None + for key, value in items: + if isinstance(value, Group): + current_group = value + + elif isinstance(value, (int, str)): + if current_group is None: + raise RuntimeError("Enum defined before first group") + current_group.keys.append(key) + + # Remove groups from attrs, so they won't become Enum members + group_var_names = [k for k, v in items if isinstance(v, Group)] + for group_variable_name in group_var_names: + attrs._member_names.remove(group_variable_name) + + # Create Enum class. attrs emtpies itself in a process + new_class = super().__new__(cls, name, bases, attrs) + # List of all groups + group_list = [v for _, v in items if isinstance(v, Group)] + + # Replace keys with their Enum objects, as they exist now + for group in group_list: + for key in group.keys: + group.append(new_class[key]) # type: ignore + + # Store dict mapping groups to their pretty names to use by user + new_class._groups_ = {group.name: group for group in group_list} + + # Store groups in their respective fields + for group in group_list: + setattr(new_class, group.name, group) + + return new_class + + +class Group(List[Enum]): + """List of enum members belonging to one Enum.""" + + def __init__(self, name: str) -> None: + self.name = name + self.keys = [] + + +class EnumGroup(Enum, metaclass=EnumGroupMeta): + """ + Base class for `Enums` with specified groups. + + Groups are defined by placing separators between the members. Groups + are lists that will be filled with with the members belonging to it: + + ```python + class Edible(EnumGroup): + _fruit = Group("Fruit") + APPLE = 0 + ORANGE = 1 + + _vegetable = Group("Vegetable") + TOMATO = 2 + POTATO = 3 + + def format_member(self): + return f"{self.name}_{self.value}" + ``` + + Groups can be obtained as attributes, or with `_groups_` dictionary: + + ```python + assert Edible.APPLE in Edible._fruit + assert Edible.TOMATO in Edible._groups_["Vegetable"] + ``` + + Groups are class attributes, but they are not Enum members - they + will not be part of `_member_map_`. + + Every `Enum` member belongs to exactly one group. Every subclass must + start with a group separator. Otherwise, exception will be raised + during class creation. + """ diff --git a/shortcut_composer/api_krita/enums/node_types.py b/shortcut_composer/api_krita/enums/node_types.py index 4d15091e..6e4c77ca 100644 --- a/shortcut_composer/api_krita/enums/node_types.py +++ b/shortcut_composer/api_krita/enums/node_types.py @@ -30,27 +30,9 @@ class NodeType(Enum): @property def icon(self) -> QIcon: """Return the icon of this node type.""" - icon_name = _ICON_NAME_MAP.get(self, "edit-delete") - return Api.instance().icon(icon_name) + return Api.instance().icon(self.value) @property def pretty_name(self) -> str: """Format node type name like: `Paint layer`.""" - return f"{self.name[0]}{self.name[1:].lower().replace('_', ' ')}" - - -_ICON_NAME_MAP = { - NodeType.PAINT_LAYER: "paintLayer", - NodeType.GROUP_LAYER: "groupLayer", - NodeType.FILE_LAYER: "fileLayer", - NodeType.FILTER_LAYER: "filterLayer", - NodeType.FILL_LAYER: "fillLayer", - NodeType.CLONE_LAYER: "cloneLayer", - NodeType.VECTOR_LAYER: "vectorLayer", - NodeType.TRANSPARENCY_MASK: "transparencyMask", - NodeType.FILTER_MASK: "filterMask", - NodeType.TRANSFORM_MASK: "transformMask", - NodeType.SELECTION_MASK: "selectionMask", - NodeType.COLORIZE_MASK: "colorizeMask" -} -"""Maps node types to names of their icons.""" + return f"{self.name.replace('_', ' ').capitalize()}" diff --git a/shortcut_composer/api_krita/enums/toggle.py b/shortcut_composer/api_krita/enums/toggle.py index 41bca388..1fdea91d 100644 --- a/shortcut_composer/api_krita/enums/toggle.py +++ b/shortcut_composer/api_krita/enums/toggle.py @@ -29,7 +29,7 @@ class Toggle(Enum): @property def pretty_name(self) -> str: """Format toggle name like: `Preserve alpha`.""" - return f"{self.name[0]}{self.name[1:].lower().replace('_', ' ')}" + return f"{self.name.replace('_', ' ').capitalize()}" @property def state(self) -> bool: diff --git a/shortcut_composer/api_krita/enums/tool.py b/shortcut_composer/api_krita/enums/tool.py index 38a631ae..c7f8a477 100644 --- a/shortcut_composer/api_krita/enums/tool.py +++ b/shortcut_composer/api_krita/enums/tool.py @@ -2,24 +2,21 @@ # SPDX-License-Identifier: GPL-3.0-or-later from krita import Krita as Api -from enum import Enum +from typing import Optional from PyQt5.QtGui import QIcon +from .helpers import EnumGroup, Group -class Tool(Enum): - """ - Contains all known tools from krita toolbox. +class Tool(EnumGroup): - Extended with modes of the transform tool. - - Example usage: `Tool.FREEHAND_BRUSH` - """ - - SHAPE_SELECT = "InteractionTool" + _vectors = Group("Vectors") + SHAPE_SELECT = "InteractionTool", "Select Shapes Tool" TEXT = "SvgTextTool" EDIT_SHAPES = "PathTool" - CALLIGRAPHY = "KarbonCalligraphyTool" + CALLIGRAPHY = "KarbonCalligraphyTool", "Calligraphy" + + _painting = Group("Painting") FREEHAND_BRUSH = "KritaShape/KisToolBrush" LINE = "KritaShape/KisToolLine" RECTANGLE = "KritaShape/KisToolRectangle" @@ -30,18 +27,24 @@ class Tool(Enum): FREEHAND_PATH = "KisToolPencil" DYNAMIC_BRUSH = "KritaShape/KisToolDyna" MULTI_BRUSH = "KritaShape/KisToolMultiBrush" + + _editing = Group("Editing") TRANSFORM = "KisToolTransform" MOVE = "KritaTransform/KisToolMove" CROP = "KisToolCrop" GRADIENT = "KritaFill/KisToolGradient" - COLOR_SAMPLER = "KritaSelected/KisToolColorSampler" + COLOR_SAMPLER = "KritaSelected/KisToolColorSampler", "Color Sampler" COLORIZE_MASK = "KritaShape/KisToolLazyBrush" SMART_PATCH = "KritaShape/KisToolSmartPatch" FILL = "KritaFill/KisToolFill" - ENCLOSE_AND_FILL = "KisToolEncloseAndFill" - ASSISTANTS = "KisAssistantTool" + ENCLOSE_AND_FILL = "KisToolEncloseAndFill", "Enclose and Fill Tool" + + _utility = Group("Utility") + ASSISTANTS = "KisAssistantTool", "Assistant Tool" MEASUREMENT = "KritaShape/KisToolMeasure" - REFERENCE = "ToolReferenceImages" + REFERENCE = "ToolReferenceImages", "Reference Images Tool" + + _selection = Group("Selection") RECTANGULAR_SELECTION = "KisToolSelectRectangular" ELIPTICAL_SELECTION = "KisToolSelectElliptical" POLYGONAL_SELECTION = "KisToolSelectPolygonal" @@ -50,35 +53,31 @@ class Tool(Enum): SIMILAR_COLOR_SELECTION = "KisToolSelectSimilar" BEZIER_SELECTION = "KisToolSelectPath" MAGNETIC_SELECTION = "KisToolSelectMagnetic" + + _canvas_navigation = Group("Canvas navigation") ZOOM = "ZoomTool" PAN = "PanTool" + def __init__(self, value: str, pretty_name: Optional[str] = None): + self._value_ = value + self._custom_pretty_name = pretty_name + + @property + def pretty_name(self) -> str: + """Format tool name as in Krita Blending Mode combobox.""" + if self._custom_pretty_name is not None: + return self._custom_pretty_name + return f"{self.name.replace('_', ' ').title()} Tool" + def activate(self): Api.instance().action(self.value).trigger() - @staticmethod - def is_paintable(tool: 'Tool') -> bool: + @classmethod + def is_paintable(cls, tool: 'Tool') -> bool: """Is the user able to paint when the given tool is activated.""" - return tool in _PAINTABLE + return tool in cls._painting # type: ignore @property def icon(self) -> QIcon: """Return the icon of this tool.""" return Api.instance().action(self.value).icon() - - @property - def pretty_name(self) -> str: - """Format tool name like: `Shape select tool`.""" - return f"{self.name[0]}{self.name[1:].lower().replace('_', ' ')} tool" - - -_PAINTABLE = { - Tool.FREEHAND_BRUSH, - Tool.LINE, - Tool.ELLIPSE, - Tool.DYNAMIC_BRUSH, - Tool.RECTANGLE, - Tool.MULTI_BRUSH, - Tool.POLYLINE, -} -"""Set of tools that are used to paint on the canvas.""" diff --git a/shortcut_composer/api_krita/pyqt/round_button.py b/shortcut_composer/api_krita/pyqt/round_button.py index 991a6176..63243560 100644 --- a/shortcut_composer/api_krita/pyqt/round_button.py +++ b/shortcut_composer/api_krita/pyqt/round_button.py @@ -26,6 +26,7 @@ def __init__( self.setCursor(Qt.ArrowCursor) self._icon_scale = icon_scale + self._radius = initial_radius self._background_color = background_color self._active_color = active_color @@ -38,21 +39,21 @@ def __init__( self.setAttribute(Qt.WA_TranslucentBackground) self.setStyleSheet("background: transparent;") - self.resize(initial_radius) + self.refresh() self.show() - def resize(self, radius: int) -> None: + def refresh(self) -> None: """Change the size and repaint the button.""" - self.setGeometry(0, 0, radius*2, radius*2) + self.setGeometry(0, 0, self._radius*2, self._radius*2) self.setStyleSheet(f""" QPushButton [ - border: {round(radius*0.06)}px + border: {round(self._radius*0.06)}px {self._color_to_str(self._border_color)}; - border-radius: {radius}px; + border-radius: {self._radius}px; border-style: outset; background: {self._color_to_str(self._background_color)}; - qproperty-iconSize:{round(radius*self._icon_scale)}px; + qproperty-iconSize:{round(self._radius*self._icon_scale)}px; ] QPushButton:hover [ background: {self._color_to_str(self._active_color)}; diff --git a/shortcut_composer/api_krita/pyqt/safe_confirm_button.py b/shortcut_composer/api_krita/pyqt/safe_confirm_button.py index ab26fa2a..b9e8dc7e 100644 --- a/shortcut_composer/api_krita/pyqt/safe_confirm_button.py +++ b/shortcut_composer/api_krita/pyqt/safe_confirm_button.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import Optional from PyQt5.QtGui import QIcon diff --git a/shortcut_composer/composer_utils/compatibility_fix.py b/shortcut_composer/composer_utils/compatibility_fix.py deleted file mode 100644 index ef22ddf2..00000000 --- a/shortcut_composer/composer_utils/compatibility_fix.py +++ /dev/null @@ -1,36 +0,0 @@ -# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus -# SPDX-License-Identifier: GPL-3.0-or-later - -from api_krita import Krita - - -def fix_config(): - """Rewrites config values from their position in 1.1.1 to 1.2.0.""" - def fix(group: str, old_name: str, new_name: str): - if Krita.read_setting(group, new_name, "not given") != "not given": - return - value = Krita.read_setting("ShortcutComposer", old_name, "not given") - if value != "not given": - Krita.write_setting(group, new_name, value) - - data = ( - ("Pick brush presets (red)", "Tag (red)", "Tag"), - ("Pick brush presets (green)", "Tag (green)", "Tag"), - ("Pick brush presets (blue)", "Tag (blue)", "Tag"), - - ("Pick brush presets (red)", "Tag (red) values", "Values"), - ("Pick brush presets (green)", "Tag (green) values", "Values"), - ("Pick brush presets (blue)", "Tag (blue) values", "Values"), - - ("Pick painting blending modes", "Blending modes values", "Values"), - ("Pick misc tools", "Misc tools values", "Values"), - ("Cycle selection tools", "Selection tools values", "Values"), - ("Pick transform tool modes", "Transform modes values", "Values"), - ( - "Create painting layer with blending mode", - "Create blending layer values", - "Values"), - ) - - for group, old_name, new_name in data: - fix(f"ShortcutComposer: {group}", old_name, new_name) diff --git a/shortcut_composer/composer_utils/global_config.py b/shortcut_composer/composer_utils/global_config.py index b169b8d0..33a17fe7 100644 --- a/shortcut_composer/composer_utils/global_config.py +++ b/shortcut_composer/composer_utils/global_config.py @@ -2,6 +2,8 @@ # SPDX-License-Identifier: GPL-3.0-or-later from config_system import FieldGroup +from PyQt5.QtGui import QColor +from api_krita import Krita class GlobalConfig(FieldGroup): @@ -13,32 +15,77 @@ class GlobalConfig(FieldGroup): - read current value from krita config file in correct type - write given value to krita config file - Class holds a staticmethod which resets all config files. - - VALUES configs are string representations of lists. They hold values - to use in given action with elements separated with tabulators. - These are needed to be further parsed using TagConfigValues or - EnumConfigValues. + Class inherits a method which resets all config files. """ def __init__(self, name: str) -> None: super().__init__(name) self.SHORT_VS_LONG_PRESS_TIME = self.field( - "Short vs long press time", 0.3) + name="Short vs long press time", + default=0.3) self.TRACKER_SENSITIVITY_SCALE = self.field( - "Tracker sensitivity scale", 1.0) - self.TRACKER_DEADZONE = self.field("Tracker deadzone", 0) - self.FPS_LIMIT = self.field("FPS limit", 60) - self.PIE_GLOBAL_SCALE = self.field("Pie global scale", 1.0) - self.PIE_ICON_GLOBAL_SCALE = self.field("Pie icon global scale", 1.0) + name="Tracker sensitivity scale", + default=1.0) + self.TRACKER_DEADZONE = self.field( + name="Tracker deadzone", + default=0) + self.FPS_LIMIT = self.field( + name="FPS limit", + default=60) + + self.PIE_GLOBAL_SCALE = self.field( + name="Pie global scale", + default=1.0) + self.PIE_ICON_GLOBAL_SCALE = self.field( + name="Pie icon global scale", + default=1.0) self.PIE_DEADZONE_GLOBAL_SCALE = self.field( - "Pie deadzone global scale", 1.0) - self.PIE_ANIMATION_TIME = self.field("Pie animation time", 0.2) + name="Pie deadzone global scale", + default=1.0) + self.PIE_ANIMATION_TIME = self.field( + name="Pie animation time", + default=0.2) + + self.OVERRIDE_BACKGROUND_THEME_COLOR = self.field( + name="Override background theme color", + default=False) + self.DEFAULT_BACKGROUND_COLOR = self.field( + name="Global background color", + default=Krita.get_main_color_from_theme()) + + self.OVERRIDE_ACTIVE_THEME_COLOR = self.field( + name="Override active theme color", + default=True) + self.DEFAULT_ACTIVE_COLOR = self.field( + name="Global active color", + default=QColor(100, 150, 230)) + + self.DEFAULT_PIE_OPACITY = self.field( + name="Global pie opacity", + default=75) def get_sleep_time(self) -> int: """Read sleep time from FPS_LIMIT config field.""" fps_limit = self.FPS_LIMIT.read() return round(1000/fps_limit) if fps_limit else 1 + @property + def default_background_color(self) -> QColor: + """Color of pies, when the pie does not specify a custom one.""" + if self.OVERRIDE_BACKGROUND_THEME_COLOR.read(): + bg_color = self.DEFAULT_BACKGROUND_COLOR.read() + else: + bg_color = Krita.get_main_color_from_theme() + opacity = self.DEFAULT_PIE_OPACITY.read() * 255 / 100 + bg_color.setAlpha(round(opacity)) + return bg_color + + @property + def default_active_color(self) -> QColor: + """Pie highlight color, when the pie does not specify a custom one.""" + if self.OVERRIDE_ACTIVE_THEME_COLOR.read(): + return self.DEFAULT_ACTIVE_COLOR.read() + return Krita.get_active_color_from_theme() + Config = GlobalConfig("ShortcutComposer") diff --git a/shortcut_composer/composer_utils/settings_dialog.py b/shortcut_composer/composer_utils/settings_dialog.py index 6ec5e36e..ff5f7a09 100644 --- a/shortcut_composer/composer_utils/settings_dialog.py +++ b/shortcut_composer/composer_utils/settings_dialog.py @@ -7,7 +7,7 @@ from INFO import __version__, __author__, __license__ from api_krita import Krita -from config_system.ui import ConfigFormWidget, ConfigSpinBox +from config_system.ui import ConfigFormWidget, SpinBox, ColorButton, Checkbox from .global_config import Config from .buttons_layout import ButtonsLayout @@ -25,24 +25,96 @@ def __init__(self) -> None: self._general_tab = ConfigFormWidget([ "Common settings", - ConfigSpinBox( - Config.SHORT_VS_LONG_PRESS_TIME, self, None, 0.05, 4), - ConfigSpinBox(Config.FPS_LIMIT, self, None, 5, 500), + SpinBox( + config_field=Config.SHORT_VS_LONG_PRESS_TIME, + parent=self, + pretty_name="Short vs long press time", + step=0.05, + max_value=4), + SpinBox( + config_field=Config.FPS_LIMIT, + parent=self, + pretty_name="FPS limit", + step=5, + max_value=50), + "Cursor trackers", - ConfigSpinBox( - Config.TRACKER_SENSITIVITY_SCALE, self, None, 0.05, 400), - ConfigSpinBox(Config.TRACKER_DEADZONE, self, None, 1, 200), - "Pie menus display", - ConfigSpinBox(Config.PIE_GLOBAL_SCALE, self, None, 0.05, 4), - ConfigSpinBox(Config.PIE_ICON_GLOBAL_SCALE, self, None, 0.05, 4), - ConfigSpinBox( - Config.PIE_DEADZONE_GLOBAL_SCALE, self, None, 0.05, 4), - ConfigSpinBox(Config.PIE_ANIMATION_TIME, self, None, 0.01, 1), + SpinBox( + Config.TRACKER_SENSITIVITY_SCALE, + parent=self, + pretty_name="Tracker sensitivity scale", + step=0.05, + max_value=400), + SpinBox( + Config.TRACKER_DEADZONE, + parent=self, + pretty_name="Tracker deadzone", + step=1, + max_value=20), + + "Pie menu size", + SpinBox( + Config.PIE_GLOBAL_SCALE, + parent=self, + pretty_name="Pie global scale", + step=0.05, + max_value=4), + SpinBox( + Config.PIE_ICON_GLOBAL_SCALE, + parent=self, + pretty_name="Pie icon global scale", + step=0.05, + max_value=4), + SpinBox( + Config.PIE_DEADZONE_GLOBAL_SCALE, + parent=self, + pretty_name="Pie deadzone global scale", + step=0.05, + max_value=4), + + "Pie menu style", + bg_checkbox := Checkbox( + config_field=Config.OVERRIDE_BACKGROUND_THEME_COLOR, + parent=self, + pretty_name="Override background theme color"), + bg_button := ColorButton( + config_field=Config.DEFAULT_BACKGROUND_COLOR, + parent=self, + pretty_name="Default background color"), + active_checkbox := Checkbox( + config_field=Config.OVERRIDE_ACTIVE_THEME_COLOR, + parent=self, + pretty_name="Override active theme color"), + active_button := ColorButton( + config_field=Config.DEFAULT_ACTIVE_COLOR, + parent=self, + pretty_name="Default active color"), + SpinBox( + config_field=Config.DEFAULT_PIE_OPACITY, + parent=self, + pretty_name="Default pie opacity", + step=1, + max_value=100), + SpinBox( + config_field=Config.PIE_ANIMATION_TIME, + parent=self, + pretty_name="Pie animation time", + step=0.01, + max_value=1), + f"Shortcut Composer v{__version__}\n" f"Maintainer: {__author__}\n" f"License: {__license__}", ]) + def update_theme_state(): + """Hide color buttons when not taken into consideration.""" + bg_button.widget.setVisible(bg_checkbox.widget.isChecked()) + active_button.widget.setVisible(active_checkbox.widget.isChecked()) + bg_checkbox.widget.stateChanged.connect(update_theme_state) + active_checkbox.widget.stateChanged.connect(update_theme_state) + update_theme_state() + full_layout = QVBoxLayout(self) full_layout.addWidget(self._general_tab) full_layout.addLayout(ButtonsLayout( diff --git a/shortcut_composer/config_system/common_utils/__init__.py b/shortcut_composer/config_system/common_utils/__init__.py new file mode 100644 index 00000000..ebb1d8c2 --- /dev/null +++ b/shortcut_composer/config_system/common_utils/__init__.py @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Components used by the core of the config system.""" + +from .api_krita import Krita +from .save_location import SaveLocation + +__all__ = ["Krita", "SaveLocation"] diff --git a/shortcut_composer/config_system/api_krita.py b/shortcut_composer/config_system/common_utils/api_krita.py similarity index 100% rename from shortcut_composer/config_system/api_krita.py rename to shortcut_composer/config_system/common_utils/api_krita.py diff --git a/shortcut_composer/config_system/save_location.py b/shortcut_composer/config_system/common_utils/save_location.py similarity index 95% rename from shortcut_composer/config_system/save_location.py rename to shortcut_composer/config_system/common_utils/save_location.py index 0762c1ab..45395d58 100644 --- a/shortcut_composer/config_system/save_location.py +++ b/shortcut_composer/config_system/common_utils/save_location.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import Any, Optional, Protocol from enum import Enum from .api_krita import Krita diff --git a/shortcut_composer/config_system/field.py b/shortcut_composer/config_system/field.py index efeed33e..1132be11 100644 --- a/shortcut_composer/config_system/field.py +++ b/shortcut_composer/config_system/field.py @@ -44,7 +44,7 @@ def __new__( parser_type: Optional[type] = None, local: bool = False, ) -> 'Field[T]': - from .field_implementations import ListField, NonListField + from .field_base_impl import ListField, NonListField cls.original = super().__new__ if not isinstance(default, list): diff --git a/shortcut_composer/config_system/field_base.py b/shortcut_composer/config_system/field_base.py index bb2af0f9..c0f88993 100644 --- a/shortcut_composer/config_system/field_base.py +++ b/shortcut_composer/config_system/field_base.py @@ -5,9 +5,8 @@ from abc import ABC, abstractmethod from enum import Enum -from .parsers import Parser, BoolParser, EnumParser, BasicParser +from .common_utils import SaveLocation from .field import Field -from .save_location import SaveLocation T = TypeVar('T') E = TypeVar('E', bound=Enum) @@ -82,16 +81,3 @@ def _is_write_redundant(self, value: T) -> bool: def reset_default(self) -> None: """Write a default value to kritarc file.""" self.write(self.default) - - @staticmethod - def _get_parser(parser_type: type) -> Parser[T]: - """Return field parser.""" - if issubclass(parser_type, Enum): - return EnumParser(parser_type) # type: ignore - - return { - int: BasicParser(int), - float: BasicParser(float), - str: BasicParser(str), - bool: BoolParser() - }[parser_type] # type: ignore diff --git a/shortcut_composer/config_system/fields/__init__.py b/shortcut_composer/config_system/field_base_impl/__init__.py similarity index 51% rename from shortcut_composer/config_system/fields/__init__.py rename to shortcut_composer/config_system/field_base_impl/__init__.py index 5099fa0b..53193d40 100644 --- a/shortcut_composer/config_system/fields/__init__.py +++ b/shortcut_composer/config_system/field_base_impl/__init__.py @@ -1,7 +1,16 @@ # SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus # SPDX-License-Identifier: GPL-3.0-or-later +"""Implementations of Field.""" + +from .non_list_field import NonListField +from .list_field import ListField from .dual_field import DualField from .field_with_editable_default import FieldWithEditableDefault -__all__ = ["DualField", "FieldWithEditableDefault"] +__all__ = [ + "NonListField", + "ListField", + "DualField", + "FieldWithEditableDefault" +] diff --git a/shortcut_composer/config_system/field_base_impl/common_utils/__init__.py b/shortcut_composer/config_system/field_base_impl/common_utils/__init__.py new file mode 100644 index 00000000..77e47c44 --- /dev/null +++ b/shortcut_composer/config_system/field_base_impl/common_utils/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Components used by the implementations of Field.""" + +from .parsers import dispatch_parser, Parser + +__all__ = ["dispatch_parser", "Parser"] diff --git a/shortcut_composer/config_system/parsers.py b/shortcut_composer/config_system/field_base_impl/common_utils/parsers.py similarity index 68% rename from shortcut_composer/config_system/parsers.py rename to shortcut_composer/config_system/field_base_impl/common_utils/parsers.py index 65ac1850..06e3cf9d 100644 --- a/shortcut_composer/config_system/parsers.py +++ b/shortcut_composer/config_system/field_base_impl/common_utils/parsers.py @@ -3,12 +3,27 @@ from typing import Generic, TypeVar, Type, Protocol from enum import Enum +from PyQt5.QtGui import QColor T = TypeVar("T") Basic = TypeVar("Basic", str, int, float) EnumT = TypeVar("EnumT", bound=Enum) +def dispatch_parser(parser_type: type) -> 'Parser': + """Return a proper field parser based on given type.""" + if issubclass(parser_type, Enum): + return EnumParser(parser_type) + + return { + int: BasicParser(int), + float: BasicParser(float), + str: BasicParser(str), + bool: BoolParser(), + QColor: ColorParser() + }[parser_type] + + class Parser(Generic[T], Protocol): """Parses from string to specific type and vice-versa.""" @@ -67,3 +82,18 @@ def parse_to(self, value: str) -> EnumT: def parse_from(self, value: EnumT) -> str: """Parse from enum to string.""" return str(value.name) + + +class ColorParser(Parser[QColor]): + """Parses from string to QColor and vice-versa.""" + + type = QColor + + def parse_to(self, value: str) -> QColor: + """Parses from string to QColor.""" + element_list = value.split(",") + return QColor(*map(int, element_list)) + + def parse_from(self, value: QColor) -> str: + """Parses from QColor to string.""" + return f"{value.red()},{value.green()},{value.blue()},{value.alpha()}" diff --git a/shortcut_composer/config_system/fields/dual_field.py b/shortcut_composer/config_system/field_base_impl/dual_field.py similarity index 99% rename from shortcut_composer/config_system/fields/dual_field.py rename to shortcut_composer/config_system/field_base_impl/dual_field.py index 7b4e23f1..e9fd8337 100644 --- a/shortcut_composer/config_system/fields/dual_field.py +++ b/shortcut_composer/config_system/field_base_impl/dual_field.py @@ -1,11 +1,10 @@ # SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus # SPDX-License-Identifier: GPL-3.0-or-later +from typing import Callable, Generic, Optional, TypeVar from ..field import Field from ..field_group import FieldGroup -from typing import Callable, Generic, Optional, TypeVar - T = TypeVar("T") F = TypeVar("F", bound=Field) @@ -13,9 +12,11 @@ class DualField(Field, Generic[T]): """ Field switching save location based on passed field. + Implementation uses two identical fields, but with different save location. Each time DualField is red or written, correct field is picked from the determiner field. + NOTE: Callbacks are always stored in the global field, as they wouldn't run in local one when switching between documents. """ diff --git a/shortcut_composer/config_system/fields/field_with_editable_default.py b/shortcut_composer/config_system/field_base_impl/field_with_editable_default.py similarity index 99% rename from shortcut_composer/config_system/fields/field_with_editable_default.py rename to shortcut_composer/config_system/field_base_impl/field_with_editable_default.py index 0ce89e73..9df04b31 100644 --- a/shortcut_composer/config_system/fields/field_with_editable_default.py +++ b/shortcut_composer/config_system/field_base_impl/field_with_editable_default.py @@ -1,9 +1,8 @@ # SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus # SPDX-License-Identifier: GPL-3.0-or-later -from ..field import Field - from typing import Callable, Generic, TypeVar +from ..field import Field T = TypeVar("T") F = TypeVar("F", bound=Field) diff --git a/shortcut_composer/config_system/field_implementations.py b/shortcut_composer/config_system/field_base_impl/list_field.py similarity index 65% rename from shortcut_composer/config_system/field_implementations.py rename to shortcut_composer/config_system/field_base_impl/list_field.py index 83e88988..0eb81f34 100644 --- a/shortcut_composer/config_system/field_implementations.py +++ b/shortcut_composer/config_system/field_base_impl/list_field.py @@ -2,39 +2,12 @@ # SPDX-License-Identifier: GPL-3.0-or-later from typing import TypeVar, Generic, Optional, List - -from .parsers import Parser -from .field_base import FieldBase +from ..field_base import FieldBase +from .common_utils import dispatch_parser T = TypeVar('T') -class NonListField(FieldBase, Generic[T]): - """Config field containing a basic, non-list value.""" - - def __init__( - self, - config_group: str, - name: str, - default: T, - parser_type: Optional[type] = None, - local: bool = False, - ) -> None: - super().__init__(config_group, name, default, parser_type, local) - self._parser: Parser[T] = self._get_parser(type(self.default)) - - def read(self) -> T: - """Return value from kritarc parsed to field type.""" - raw = self.location.read(self.config_group, self.name) - if raw is None: - return self.default - return self._parser.parse_to(raw) - - def _to_string(self, value: T) -> str: - """Parse the field value to string using parser.""" - return self._parser.parse_from(value) - - class ListField(FieldBase, Generic[T]): """Config field containing a list value.""" @@ -47,8 +20,7 @@ def __init__( local: bool = False, ) -> None: super().__init__(config_group, name, default, parser_type, local) - self._parser: Parser[T] = self._get_parser( - self._get_type(self.parser_type)) + self._parser = dispatch_parser(self._get_type(self.parser_type)) def write(self, value: List[T]) -> None: for element in value: diff --git a/shortcut_composer/config_system/field_base_impl/non_list_field.py b/shortcut_composer/config_system/field_base_impl/non_list_field.py new file mode 100644 index 00000000..614e89a2 --- /dev/null +++ b/shortcut_composer/config_system/field_base_impl/non_list_field.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +from typing import TypeVar, Generic, Optional +from ..field_base import FieldBase +from .common_utils import dispatch_parser + +T = TypeVar('T') + + +class NonListField(FieldBase, Generic[T]): + """Config field containing a basic, non-list value.""" + + def __init__( + self, + config_group: str, + name: str, + default: T, + parser_type: Optional[type] = None, + local: bool = False, + ) -> None: + super().__init__(config_group, name, default, parser_type, local) + self._parser = dispatch_parser(type(self.default)) + + def read(self) -> T: + """Return value from kritarc parsed to field type.""" + raw = self.location.read(self.config_group, self.name) + if raw is None: + return self.default + return self._parser.parse_to(raw) + + def _to_string(self, value: T) -> str: + """Parse the field value to string using parser.""" + return self._parser.parse_from(value) diff --git a/shortcut_composer/config_system/ui/__init__.py b/shortcut_composer/config_system/ui/__init__.py index 957f79c9..a732785e 100644 --- a/shortcut_composer/config_system/ui/__init__.py +++ b/shortcut_composer/config_system/ui/__init__.py @@ -18,11 +18,20 @@ from .config_based_widget import ConfigBasedWidget from .config_form_widget import ConfigFormWidget -from .widgets import ConfigComboBox, ConfigSpinBox +from .widgets import ( + StringComboBox, + EnumComboBox, + ColorButton, + Checkbox, + SpinBox, +) __all__ = [ "ConfigBasedWidget", "ConfigFormWidget", - "ConfigComboBox", - "ConfigSpinBox" + "StringComboBox", + "EnumComboBox", + "ColorButton", + "Checkbox", + "SpinBox", ] diff --git a/shortcut_composer/config_system/ui/config_form_widget.py b/shortcut_composer/config_system/ui/config_form_widget.py index 4679e56e..2636061d 100644 --- a/shortcut_composer/config_system/ui/config_form_widget.py +++ b/shortcut_composer/config_system/ui/config_form_widget.py @@ -36,7 +36,7 @@ def __init__(self, elements: List[Union[ConfigBasedWidget, str]]) -> None: Qt.AlignHCenter | Qt.AlignTop) # type: ignore self.setLayout(self._layout) - self._widgets: List[ConfigBasedWidget] = [] + self.widgets: List[ConfigBasedWidget] = [] for element in elements: if isinstance(element, str): self.add_title(element) @@ -47,7 +47,7 @@ def __init__(self, elements: List[Union[ConfigBasedWidget, str]]) -> None: def add_row(self, element: ConfigBasedWidget) -> None: """Add a ConfigBasedWidget along with a label.""" - self._widgets.append(element) + self.widgets.append(element) self._layout.addRow(f"{element.pretty_name}:", element.widget) def add_title(self, text: str) -> None: @@ -59,10 +59,10 @@ def add_title(self, text: str) -> None: def refresh(self) -> None: """Read values from krita config and apply them to stored boxes.""" - for element in self._widgets: + for element in self.widgets: element.reset() def apply(self) -> None: """Write values from stored spin boxes to krita config file.""" - for element in self._widgets: + for element in self.widgets: element.save() diff --git a/shortcut_composer/config_system/ui/widgets.py b/shortcut_composer/config_system/ui/widgets.py index 021aef0c..4ba3fffa 100644 --- a/shortcut_composer/config_system/ui/widgets.py +++ b/shortcut_composer/config_system/ui/widgets.py @@ -1,25 +1,36 @@ # SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus # SPDX-License-Identifier: GPL-3.0-or-later -from typing import Any, List, Final, Optional, TypeVar, Generic, Protocol -from PyQt5.QtWidgets import QDoubleSpinBox, QComboBox, QSpinBox, QWidget +from enum import Enum +from typing import List, Final, Optional, TypeVar, Generic, Protocol, Type +from PyQt5.QtWidgets import ( + QWidget, + QDoubleSpinBox, + QSpinBox, + QComboBox, + QCheckBox, + QPushButton, + QColorDialog) +from PyQt5.QtGui import QColor from ..field import Field from .config_based_widget import ConfigBasedWidget F = TypeVar("F", bound=float) +E = TypeVar("E", bound=Enum) -class SpinBox(Protocol, Generic[F]): +class SpinBoxInterface(Protocol, Generic[F]): """Representation of both Qt spinboxes as one generic class.""" def value(self) -> F: ... def setValue(self, val: F) -> None: ... + def setEnabled(self, a0: bool) -> None: ... -class ConfigSpinBox(ConfigBasedWidget[F]): +class SpinBox(ConfigBasedWidget[F]): """ - Wrapper of SpinBox linked to a configutation field. + Wrapper of SpinBox linked to a `float` configutation field. Based on QSpinBox or QDoubleSpinBox depending on the config type. Works only for fields of type: `int` or `float`. @@ -37,18 +48,18 @@ def __init__( self._step = step self._max_value = max_value self._spin_box = self._init_spin_box() - self.widget: Final[SpinBox[F]] = self._spin_box + self.widget: Final[SpinBoxInterface[F]] = self._spin_box self.reset() def read(self) -> F: """Return the current value of the spinbox widget.""" return self._spin_box.value() - def set(self, value: F): + def set(self, value: F) -> None: """Replace the value of the spinbox widget with passed one.""" self._spin_box.setValue(value) - def _init_spin_box(self) -> SpinBox: + def _init_spin_box(self) -> SpinBoxInterface: """Return the spinbox widget of type based on config field type.""" spin_box: QDoubleSpinBox = {int: QSpinBox, float: QDoubleSpinBox}[ type(self.config_field.default)]() @@ -61,19 +72,15 @@ def _init_spin_box(self) -> SpinBox: return spin_box -class ConfigComboBox(ConfigBasedWidget[str]): - """ - Wrapper of Combobox linked to a configutation field. - - Works only for fields of type: `str`. - """ +class StringComboBox(ConfigBasedWidget[str]): + """Wrapper of Combobox linked to a `str` configutation field.""" def __init__( self, config_field: Field[str], parent: Optional[QWidget] = None, pretty_name: Optional[str] = None, - allowed_values: List[Any] = [], + allowed_values: List[str] = [], ) -> None: super().__init__(config_field, parent, pretty_name) self._allowed_values = allowed_values @@ -91,12 +98,121 @@ def read(self) -> str: """Return the current value of the ComboBox.""" return self._combo_box.currentText() - def set(self, value: str): + def set(self, value: str) -> None: """Replace the value of the ComboBox with passed one.""" - return self._combo_box.setCurrentText(value) + self._combo_box.setCurrentText(value) def _init_combo_box(self) -> QComboBox: - """Return the spinbox widget.""" + """Return the combobox widget.""" combo_box = QComboBox() combo_box.setObjectName(self.config_field.name) return combo_box + + +class EnumComboBox(ConfigBasedWidget[E]): + """ + Wrapper of Combobox linked to a `Enum` configutation field. + + Allows to pick one of enum members. + """ + + def __init__( + self, + config_field: Field[E], + enum_type: Type[E], + parent: Optional[QWidget] = None, + pretty_name: Optional[str] = None, + ) -> None: + super().__init__(config_field, parent, pretty_name) + self._enum_type = enum_type + self._combo_box = self._init_combo_box() + self.widget: Final[QComboBox] = self._combo_box + + keys = list(self._enum_type._value2member_map_.keys()) + self._combo_box.addItems(keys) + + self.reset() + + def read(self) -> E: + """Return Enum member selected with the combobox.""" + text = self._combo_box.currentText() + return self._enum_type(text) + + def set(self, value: E) -> None: + """Set the combobox to given Enum member.""" + self._combo_box.setCurrentText(value.value) + + def _init_combo_box(self) -> QComboBox: + """Return the combobox widget.""" + combo_box = QComboBox() + combo_box.setObjectName(self.config_field.name) + return combo_box + + +class ColorButton(ConfigBasedWidget[QColor]): + """ + Wrapper of QPushButton linked to a `QColor` configutation field. + + Button displays currently selected color, and clicking activates a + color picker for changing it. + """ + + def __init__( + self, + config_field: Field[QColor], + parent: Optional[QWidget] = None, + pretty_name: Optional[str] = None, + ) -> None: + super().__init__(config_field, parent, pretty_name) + self._button = self._init_button() + self._color = self.config_field.read() + self.widget: Final[QPushButton] = self._button + + self.reset() + + def read(self) -> QColor: + """Return QColor displayed in the button.""" + return self._color + + def set(self, value: QColor) -> None: + """Remember given color, and replace current button color with it.""" + self._color = value + self._button.setStyleSheet(f"background-color: {self._color.name()}") + + def _init_button(self) -> QPushButton: + """Return the QPushButton widget.""" + def on_click(): + """Set the selected color, if the dialog was not cancelled.""" + fetched_color = QColorDialog.getColor(self._color) + if fetched_color.isValid(): + self.set(fetched_color) + + button = QPushButton("") + policy = button.sizePolicy() + policy.setRetainSizeWhenHidden(True) + button.setSizePolicy(policy) + button.clicked.connect(on_click) + return button + + +class Checkbox(ConfigBasedWidget[bool]): + """Wrapper of QCheckBox linked to a `bool` configutation field.""" + + def __init__( + self, + config_field: Field[bool], + parent: Optional[QWidget] = None, + pretty_name: Optional[str] = None, + ) -> None: + super().__init__(config_field, parent, pretty_name) + self._checkbox = QCheckBox() + self.widget: Final[QCheckBox] = self._checkbox + self.reset() + + def read(self) -> bool: + """Return checkbox state.""" + return self._checkbox.isChecked() + + def set(self, value: bool) -> None: + """Set checkbox state.""" + self._checkbox.setChecked(value) diff --git a/shortcut_composer/core_components/controllers/__init__.py b/shortcut_composer/core_components/controllers/__init__.py index 4736ebf7..47a59d0d 100644 --- a/shortcut_composer/core_components/controllers/__init__.py +++ b/shortcut_composer/core_components/controllers/__init__.py @@ -1,26 +1,7 @@ # SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus # SPDX-License-Identifier: GPL-3.0-or-later -""" -Components that allow to get and set a specific property of krita. - -Available controllers: - - `ToolController` - - `BrushSizeController` - - `BlendingModeController` - - `OpacityController` - - `FlowController` - - `PresetController` - - `TimeController` - - `ActiveLayerController` - - `LayerVisibilityController` - - `LayerBlendingModeController`, - - `LayerOpacityController`, - - `CanvasRotationController` - - `CanvasZoomController` - - `ToggleController` - - `CreateLayerWithBlendingController` -""" +"""Components that allow to get and set a specific property of krita.""" from .document_controllers import ( ActiveLayerController, @@ -46,6 +27,7 @@ from .core_controllers import ( TransformModeController, ToggleController, + ActionController, ToolController, UndoController, ) @@ -63,6 +45,7 @@ "OpacityController", "ToggleController", "PresetController", + "ActionController", "TimeController", "ToolController", "UndoController", diff --git a/shortcut_composer/core_components/controllers/core_controllers.py b/shortcut_composer/core_components/controllers/core_controllers.py index d82d7ca5..d0211083 100644 --- a/shortcut_composer/core_components/controllers/core_controllers.py +++ b/shortcut_composer/core_components/controllers/core_controllers.py @@ -1,13 +1,14 @@ # SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus # SPDX-License-Identifier: GPL-3.0-or-later -from typing import Optional +from typing import Optional, Union, NoReturn from dataclasses import dataclass from PyQt5.QtGui import QIcon from api_krita import Krita -from api_krita.enums import Tool, Toggle, TransformMode +from api_krita.pyqt import Text +from api_krita.enums import Action, Tool, Toggle, TransformMode from api_krita.actions import TransformModeFinder from ..controller_base import Controller @@ -42,6 +43,38 @@ def get_pretty_name(self, value: Tool) -> str: return value.pretty_name +class ActionController(Controller[Action]): + """ + Gives access to krita actions. + + - Operates on `Action` + - Does not have a default value. + """ + + TYPE = Action + + @staticmethod + def get_value() -> NoReturn: + """Get currently active tool.""" + raise NotImplementedError() + + @staticmethod + def set_value(value: Action) -> None: + """Set a passed tool.""" + value.activate() + + def get_label(self, value: Tool) -> Union[QIcon, Text]: + """Forward the tools' icon.""" + icon = value.icon + if not icon.isNull(): + return value.icon + return Text(value.name[:3]) + + def get_pretty_name(self, value: Action) -> str: + """Forward enums' pretty name.""" + return value.pretty_name + + class TransformModeController(Controller[TransformMode]): """ Gives access to tools from toolbox. diff --git a/shortcut_composer/core_components/instructions/__init__.py b/shortcut_composer/core_components/instructions/__init__.py index ff84e377..c8577066 100644 --- a/shortcut_composer/core_components/instructions/__init__.py +++ b/shortcut_composer/core_components/instructions/__init__.py @@ -6,17 +6,6 @@ Depending on the picked instruction, tasks can be performed on key press, release, or in a loop while the key is pressed. - -Available instructions: - - `SetBrush` - - `SetBrushOnNonPaintable` - - `ToggleLayerVisibility` - - `ToggleVisibilityAbove` - - `UndoOnPress` - - `EnsureOn` - - `EnsureOff` - - `TemporaryOn` - - `TemporaryOff` """ from .layer_hide import ToggleLayerVisibility, ToggleVisibilityAbove diff --git a/shortcut_composer/data_components/__init__.py b/shortcut_composer/data_components/__init__.py index db2012d5..7d660120 100644 --- a/shortcut_composer/data_components/__init__.py +++ b/shortcut_composer/data_components/__init__.py @@ -10,6 +10,7 @@ """ from .current_layer_stack import CurrentLayerStack +from .deadzone_strategy import DeadzoneStrategy from .pick_strategy import PickStrategy from .slider import Slider from .range import Range @@ -17,6 +18,7 @@ __all__ = [ "CurrentLayerStack", + "DeadzoneStrategy", "PickStrategy", "Slider", "Range", diff --git a/shortcut_composer/data_components/deadzone_strategy.py b/shortcut_composer/data_components/deadzone_strategy.py new file mode 100644 index 00000000..df3d1247 --- /dev/null +++ b/shortcut_composer/data_components/deadzone_strategy.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +from enum import Enum + + +class DeadzoneStrategy(Enum): + """ + Enumeration of actions that can be done on deadzone key release. + + Values are strings meant for being displayed in the UI. + """ + DO_NOTHING = "Do nothing" + PICK_TOP = "Pick top" + PICK_PREVIOUS = "Pick previous" diff --git a/shortcut_composer/input_adapter/action_manager.py b/shortcut_composer/input_adapter/action_manager.py index 98a5c97a..f7f7f3ee 100644 --- a/shortcut_composer/input_adapter/action_manager.py +++ b/shortcut_composer/input_adapter/action_manager.py @@ -11,10 +11,8 @@ from PyQt5.QtWidgets import QWidgetAction -from .api_krita import Krita +from .action_manager_utils import Krita, ReleaseKeyEventFilter, ShortcutAdapter from .complex_action_interface import ComplexActionInterface -from .event_filter import ReleaseKeyEventFilter -from .shortcut_adapter import ShortcutAdapter @dataclass @@ -74,8 +72,8 @@ def bind_action(self, action: ComplexActionInterface) -> None: krita_action=Krita.create_action( window=self._window, name=action.name), - shortcut=self._create_adapter(action) - ) + shortcut=self._create_adapter(action)) + self._stored_actions[action.name] = container def _create_adapter(self, action: ComplexActionInterface) \ @@ -87,6 +85,5 @@ def _create_adapter(self, action: ComplexActionInterface) \ """ shortcut_adapter = ShortcutAdapter(action) self._event_filter.register_release_callback( - shortcut_adapter.event_filter_callback # type: ignore - ) + shortcut_adapter.event_filter_callback) # type: ignore return shortcut_adapter diff --git a/shortcut_composer/input_adapter/action_manager_utils/__init__.py b/shortcut_composer/input_adapter/action_manager_utils/__init__.py new file mode 100644 index 00000000..04052a7f --- /dev/null +++ b/shortcut_composer/input_adapter/action_manager_utils/__init__.py @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Utils used by core ActionManager.""" + +from .api_krita import Krita +from .release_key_event_filter import ReleaseKeyEventFilter +from .shortcut_adapter import ShortcutAdapter + +__all__ = ["Krita", "ReleaseKeyEventFilter", "ShortcutAdapter"] diff --git a/shortcut_composer/input_adapter/api_krita.py b/shortcut_composer/input_adapter/action_manager_utils/api_krita.py similarity index 100% rename from shortcut_composer/input_adapter/api_krita.py rename to shortcut_composer/input_adapter/action_manager_utils/api_krita.py diff --git a/shortcut_composer/input_adapter/event_filter.py b/shortcut_composer/input_adapter/action_manager_utils/release_key_event_filter.py similarity index 100% rename from shortcut_composer/input_adapter/event_filter.py rename to shortcut_composer/input_adapter/action_manager_utils/release_key_event_filter.py diff --git a/shortcut_composer/input_adapter/action_manager_utils/shortcut_adapter.py b/shortcut_composer/input_adapter/action_manager_utils/shortcut_adapter.py new file mode 100644 index 00000000..2dae22f5 --- /dev/null +++ b/shortcut_composer/input_adapter/action_manager_utils/shortcut_adapter.py @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +from time import time + +from PyQt5.QtGui import QKeyEvent + +from ..complex_action_interface import ComplexActionInterface + + +class ShortcutAdapter: + """ + Adds additional key events based on krita's key press and release. + + Krita events: + - on_key_press (connected to trigger of krita action) + - on_key_release (intercepted with event filter) + + Custom action events: + - on_key_press + - on_short_key_release (release directly after the press) + - on_long_key_release (release long time after the press) + - on_every_key_release (called after short or long release callback) + + Only one instance of ShortcutAdapter can handle key at a time. All + others are blocked. + """ + + def __init__(self, action: ComplexActionInterface) -> None: + self.action = action + self.local_lock = False + self.last_press_time = time() + + def on_key_press(self) -> None: + """Run action's on_key_press() and remember the time of it.""" + self.local_lock = True + self.last_press_time = time() + self.action.on_key_press() + + def event_filter_callback(self, release_event: QKeyEvent) -> None: + """Handle key release if the event is related to the action.""" + if self.local_lock and not release_event.isAutoRepeat(): + self._on_key_release() + + def _on_key_release(self) -> None: + """Run proper key release methods based on time elapsed from press.""" + elapsed_time = time() - self.last_press_time + if elapsed_time < self.action.short_vs_long_press_time: + self.action.on_short_key_release() + else: + self.action.on_long_key_release() + self.action.on_every_key_release() + self.local_lock = False diff --git a/shortcut_composer/input_adapter/shortcut_adapter.py b/shortcut_composer/input_adapter/shortcut_adapter.py deleted file mode 100644 index 5dcc174d..00000000 --- a/shortcut_composer/input_adapter/shortcut_adapter.py +++ /dev/null @@ -1,79 +0,0 @@ -# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus -# SPDX-License-Identifier: GPL-3.0-or-later - -from time import time - -from PyQt5.QtGui import QKeyEvent, QKeySequence - -from .api_krita import Krita -from .complex_action_interface import ComplexActionInterface - - -class ShortcutAdapter: - """ - Adds additional key events based on krita's key press and release. - - Krita events: - - on_key_press (connected to trigger of krita action) - - on_key_release (intercepted with event filter) - - Custom action events: - - on_key_press - - on_short_key_release (release directly after the press) - - on_long_key_release (release long time after the press) - - on_every_key_release (called after short or long release callback) - """ - - def __init__(self, action: ComplexActionInterface) -> None: - self.action = action - self.key_released = True - self.last_press_time = time() - - def on_key_press(self) -> None: - """Run action's on_key_press() and remember the time of it.""" - self.key_released = False - self.last_press_time = time() - self.action.on_key_press() - - def _on_key_release(self) -> None: - """Run proper key release methods based on time elapsed from press.""" - self.key_released = True - if time() - self.last_press_time < self._short_vs_long_press_time: - self.action.on_short_key_release() - else: - self.action.on_long_key_release() - self.action.on_every_key_release() - - def _is_event_key_release(self, release_event: QKeyEvent) -> bool: - """Decide if the key release event is matches shortcut and is valid.""" - return (not release_event.isAutoRepeat() - and not self.key_released - and self._match_shortcuts( - self._key_sequence_from_event(release_event), - self.tool_shortcut)) - - def event_filter_callback(self, release_event: QKeyEvent) -> None: - """Handle key release if the event is related to the action.""" - if self._is_event_key_release(release_event): - self._on_key_release() - - @property - def _short_vs_long_press_time(self) -> float: - """Time in seconds distinguishing short key presses from long ones.""" - return self.action.short_vs_long_press_time - - @property - def tool_shortcut(self) -> QKeySequence: - """Return shortcut assigned to shortcut red from krita settings.""" - return Krita.get_action_shortcut(self.action.name) - - @staticmethod - def _key_sequence_from_event(event: QKeyEvent): - return QKeySequence(event.modifiers() | event.key()) # type: ignore - - @staticmethod - def _match_shortcuts(_a: QKeySequence, _b: QKeySequence, /) -> bool: - """Custom match pattern - one string is preset in another one.""" - parsed_a = _a.toString() - parsed_b = _b.toString() - return parsed_a in parsed_b or parsed_b in parsed_a diff --git a/shortcut_composer/manual.html b/shortcut_composer/manual.html index 79e8b864..5d579aeb 100644 --- a/shortcut_composer/manual.html +++ b/shortcut_composer/manual.html @@ -9,7 +9,7 @@ -

Shortcut composer v1.3.2

+

Shortcut composer v1.4.0


Extension for painting application Krita, which allows to create custom, complex keyboard shortcuts.

The plugin adds new shortcuts of the following types:

@@ -23,7 +23,7 @@

Shortcut composer v1.3.2

Requirements
  • Version of krita on plugin release: 5.1.5
  • -
  • Required version of krita: 5.1.0
  • +
  • Required version of krita: 5.1.0 or later

OS support state:

    diff --git a/shortcut_composer/templates/mouse_tracker_utils/axis_trackers.py b/shortcut_composer/templates/mouse_tracker_utils/axis_trackers.py index fb6a0f5d..c04fa56d 100644 --- a/shortcut_composer/templates/mouse_tracker_utils/axis_trackers.py +++ b/shortcut_composer/templates/mouse_tracker_utils/axis_trackers.py @@ -6,8 +6,8 @@ from api_krita import Krita from api_krita.pyqt import Timer from core_components import Instruction +from templates.raw_instructions import RawInstructions from .slider_handler import SliderHandler -from ..raw_instructions import RawInstructions class SingleAxisTracker(RawInstructions): diff --git a/shortcut_composer/templates/multiple_assignment.py b/shortcut_composer/templates/multiple_assignment.py index 97711f5a..8f5efc03 100644 --- a/shortcut_composer/templates/multiple_assignment.py +++ b/shortcut_composer/templates/multiple_assignment.py @@ -19,7 +19,7 @@ class MultipleAssignment(RawInstructions, Generic[T]): Action cycles the values in `values_to_cycle` list: - short key press moves to next value in list. - if current value does not belong to the list, start from beginning - - when the list is exhausted, start again + - when the list is exhausted, start from beginning - end of long press ensures `default value` ### Arguments: @@ -72,53 +72,56 @@ def __init__( super().__init__(name, instructions, short_vs_long_press_time) self._controller = controller - self._default_value = self._read_default_value(default_value) - self.config = Field( + self._config = Field( config_group=f"ShortcutComposer: {self.name}", name="Values", default=values) + self._config.register_callback(self._reset) self._settings = SettingsHandler( self.name, - self.config, + self._config, self._instructions) - self._values_to_cycle = self.config.read() - - def reset() -> None: - self._values_to_cycle = self.config.read() - self._reset_iterator() - - self.config.register_callback(reset) + self._default_value = self._read_default_value(default_value) + self._values_to_cycle = self._config.read() + self._iterator = self._reset_iterator() self._last_value: Optional[T] = None - self._iterator: Iterator[T] def on_key_press(self) -> None: - """Use key press event only for switching to first value.""" - self._controller.refresh() + """Switch to the next value when values are being cycled.""" super().on_key_press() - if self._controller.get_value() != self._last_value: - self._reset_iterator() # NOTE: When there are no values to cycle, iterator is invalid - if self._values_to_cycle: - self._set_value(next(self._iterator)) + if not self._values_to_cycle: + return + + self._controller.refresh() + if self._controller.get_value() != self._last_value: + self._iterator = self._reset_iterator() + + self._set_value(next(self._iterator)) def on_long_key_release(self) -> None: - """Long releases set default value.""" + """Set default value.""" super().on_long_key_release() self._set_value(self._default_value) - self._reset_iterator() + self._iterator = self._reset_iterator() def _set_value(self, value: T) -> None: """Set the value using the controller, and remember it.""" self._last_value = value self._controller.set_value(value) - def _reset_iterator(self) -> None: - """Replace the iterator with new cyclic iterator over cycled values.""" - self._iterator = cycle(self._values_to_cycle) + def _reset(self) -> None: + """Reload values from config and start cycling from beginning.""" + self._values_to_cycle = self._config.read() + self._iterator = self._reset_iterator() + + def _reset_iterator(self) -> Iterator[T]: + """Return a new cyclic iterator for values to cycle.""" + return cycle(self._values_to_cycle) def _read_default_value(self, value: Optional[T]) -> T: """Read value from controller if it was not given.""" diff --git a/shortcut_composer/templates/pie_menu.py b/shortcut_composer/templates/pie_menu.py index c2942a62..5298ae2a 100644 --- a/shortcut_composer/templates/pie_menu.py +++ b/shortcut_composer/templates/pie_menu.py @@ -1,29 +1,26 @@ # SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus # SPDX-License-Identifier: GPL-3.0-or-later -from typing import List, Type, TypeVar, Generic, Optional +from typing import List, 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 data_components import DeadzoneStrategy from core_components import Controller, Instruction -from .pie_menu_utils.settings_gui import ( - PieSettings, - NumericPieSettings, - PresetPieSettings, - EnumPieSettings) +from .pie_menu_utils.pie_settings_impl import dispatch_pie_settings +from .pie_menu_utils.pie_config_impl import dispatch_pie_config from .pie_menu_utils import ( - NonPresetPieConfig, - PresetPieConfig, + PieSettings, PieManager, - PieConfig, PieWidget, + PieButton, + Actuator, + EditMode, PieStyle, Label) -from .pie_menu_utils.widget_utils import EditMode, PieButton from .raw_instructions import RawInstructions T = TypeVar('T') @@ -34,10 +31,10 @@ 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 + - Moving mouse in a direction of a value activates it 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 + release and can be configured (see PieSettings) ### Arguments: @@ -51,6 +48,10 @@ class PieMenu(RawInstructions, Generic[T]): - `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 + - `pie opacity` -- (optional) default opacity of the pie + - `save local` -- (optional) default save location + - `deadzone strategy` -- (optional) default strategy what to do, + when mouse does not leave deadzone - `short_vs_long_press_time` -- (optional) time [s] that specifies if key press is short or long @@ -81,60 +82,60 @@ def __init__( 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), + active_color: Optional[QColor] = None, + pie_opacity: int = 75, save_local: bool = False, + deadzone_strategy: DeadzoneStrategy = DeadzoneStrategy.DO_NOTHING, 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 = dispatch_pie_config(self._controller)( + 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, + pie_opacity=pie_opacity, + deadzone_strategy=deadzone_strategy) 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) + self.actuator = Actuator( + controller=self._controller, + strategy_field=self._config.DEADZONE_STRATEGY, + labels=self._labels) @cached_property def pie_widget(self) -> PieWidget: - """Qwidget of the Pie for selecting values.""" + """Create Qwidget of the Pie for selecting values.""" return PieWidget( style=self._style, labels=self._labels, + edit_mode=self._edit_mode, 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}") + """Create QWidget with pie settings right for given type of labels.""" + return dispatch_pie_settings(self._controller)( + config=self._config, + style=self._style, + controller=self._controller) @cached_property def pie_manager(self) -> PieManager: - """Manager which shows, hides and moves Pie widget and its settings.""" + """Create Manager which shows, hides and moves the Pie.""" return PieManager(pie_widget=self.pie_widget) @cached_property def settings_button(self): - """Button with which user can enter the edit mode.""" + """Create button with which user can enter the edit mode.""" settings_button = PieButton( icon=Krita.get_icon("properties"), icon_scale=1.1, @@ -147,7 +148,7 @@ def settings_button(self): @cached_property def accept_button(self): - """Button displayed in edit mode, which allows to hide the pie.""" + """Create button displayed in edit mode, for hiding the pie.""" accept_button = PieButton( icon=Krita.get_icon("dialog-ok"), icon_scale=1.5, @@ -177,25 +178,10 @@ def on_key_press(self) -> None: self._reset_labels() self.pie_widget.label_holder.reset() # HACK: should be automatic self._move_buttons() + self.actuator.mark_selected_widget(self.pie_widget.widget_holder) 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: @@ -221,3 +207,21 @@ def _reset_labels(self) -> None: self.INVALID_VALUES.add(value) self._config.refresh_order() + + def on_every_key_release(self) -> None: + """ + Handle the key release event. + + In normal mode: + close pie, and set selected value if deadzone was reached + In edit mode: + ignore input + + """ + super().on_every_key_release() + + if self._edit_mode: + return + self.pie_manager.stop() + + self.actuator.activate(self.pie_widget.active) diff --git a/shortcut_composer/templates/pie_menu_utils/__init__.py b/shortcut_composer/templates/pie_menu_utils/__init__.py index 083268a9..5e988a8d 100644 --- a/shortcut_composer/templates/pie_menu_utils/__init__.py +++ b/shortcut_composer/templates/pie_menu_utils/__init__.py @@ -1,22 +1,28 @@ # SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus # SPDX-License-Identifier: GPL-3.0-or-later -"""Implementation of PieMenu main elements.""" +"""Components used by PieMenu action.""" -from .pie_config import PieConfig, PresetPieConfig, NonPresetPieConfig +from .pie_config import PieConfig +from .pie_settings import PieSettings from .label_widget import LabelWidget from .pie_manager import PieManager from .pie_widget import PieWidget +from .pie_button import PieButton from .pie_style import PieStyle +from .edit_mode import EditMode from .label import Label +from .actuator import Actuator __all__ = [ - "NonPresetPieConfig", - "PresetPieConfig", + "PieSettings", "LabelWidget", "PieConfig", "PieManager", "PieWidget", + "PieButton", "PieStyle", + "EditMode", "Label", + "Actuator", ] diff --git a/shortcut_composer/templates/pie_menu_utils/actuator.py b/shortcut_composer/templates/pie_menu_utils/actuator.py new file mode 100644 index 00000000..43bd8186 --- /dev/null +++ b/shortcut_composer/templates/pie_menu_utils/actuator.py @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +from typing import Optional, List +from core_components import Controller +from config_system import Field +from data_components import DeadzoneStrategy +from .label import Label +from .pie_widget_utils import WidgetHolder + + +class Actuator: + """ + Activates the correct labels from the Pie. + + When a valid label is given in `activate()` method, it us activated + and also remembered. + + When label is not given in `activate()` method, it means that user + closed the pie while still being in deadzone. + Then it is handled using the currently active strategy. + + Actuator tracks selected strategy using `strategy_field` passed on + initialization. It can be changed in runtime. + + Strategies: + DeadzoneStrategy.DO_NOTHING - no action is needed + DeadzoneStrategy.PICK_TOP - first label in list is activated + DeadzoneStrategy.PICK_PREVIOUS - remembered label is activated + """ + + def __init__( + self, + controller: Controller, + strategy_field: Field, + labels: List[Label] + ) -> None: + self._controller = controller + self._last_label: Optional[Label] = None + self._labels = labels + + def update_strategy(): + self._current_strategy = strategy_field.read() + self._current_strategy: DeadzoneStrategy + strategy_field.register_callback(update_strategy) + update_strategy() + + def activate(self, active: Optional[Label]) -> None: + """Activate the correct label""" + if active is not None: + # Out of deadzone, label picked + self._controller.set_value(active.value) + self._last_label = active + return + + # In deadzone + if self.selected_label is not None: + self._controller.set_value(self.selected_label.value) + + @property + def selected_label(self) -> Optional[Label]: + """Return label which should be picked on deadzone.""" + if self._current_strategy == DeadzoneStrategy.DO_NOTHING: + return None + elif self._current_strategy == DeadzoneStrategy.PICK_TOP: + if self._labels: + return self._labels[0] + return None + elif self._current_strategy == DeadzoneStrategy.PICK_PREVIOUS: + if self._last_label in self._labels: + return self._last_label + return None + + def mark_selected_widget(self, widget_holder: WidgetHolder): + """Force color of the label that is selected for being picked.""" + widget_holder.clear_forced_widgets() + + if self.selected_label is None: + return + + try: + widget = widget_holder.on_label(self.selected_label) + except ValueError: + return + widget.forced = True diff --git a/shortcut_composer/templates/pie_menu_utils/widget_utils/edit_mode.py b/shortcut_composer/templates/pie_menu_utils/edit_mode.py similarity index 93% rename from shortcut_composer/templates/pie_menu_utils/widget_utils/edit_mode.py rename to shortcut_composer/templates/pie_menu_utils/edit_mode.py index c2a0619f..f89a071b 100644 --- a/shortcut_composer/templates/pie_menu_utils/widget_utils/edit_mode.py +++ b/shortcut_composer/templates/pie_menu_utils/edit_mode.py @@ -5,7 +5,7 @@ from PyQt5.QtCore import QPoint if TYPE_CHECKING: - from ...pie_menu import PieMenu + from ..pie_menu import PieMenu class EditMode: @@ -39,7 +39,7 @@ def set_edit_mode_true(self): """Set the edit mode on.""" self._obj.pie_manager.stop(hide=False) self._obj.pie_widget.set_draggable(True) - self._obj.pie_widget.is_edit_mode = True + self._obj.pie_widget.widget_holder.clear_forced_widgets() self._obj.pie_widget.repaint() self._obj.pie_settings.show() self._obj.pie_settings.resize(self._obj.pie_settings.sizeHint()) @@ -61,7 +61,6 @@ def set_edit_mode_false(self): """Set the edit mode off.""" self._obj.pie_widget.hide() self._obj.pie_widget.set_draggable(False) - self._obj.pie_widget.is_edit_mode = False self._obj.pie_settings.hide() self._obj.accept_button.hide() self._obj.settings_button.show() @@ -69,3 +68,6 @@ def set_edit_mode_false(self): def swap_mode(self): """Change the edit mode to the other one.""" self.set(not self._edit_mode) + + def __bool__(self) -> bool: + return self.get() diff --git a/shortcut_composer/templates/pie_menu_utils/label_widget.py b/shortcut_composer/templates/pie_menu_utils/label_widget.py index 1d78ac26..34601198 100644 --- a/shortcut_composer/templates/pie_menu_utils/label_widget.py +++ b/shortcut_composer/templates/pie_menu_utils/label_widget.py @@ -5,9 +5,10 @@ from PyQt5.QtCore import Qt, QMimeData, QEvent from PyQt5.QtWidgets import QWidget -from PyQt5.QtGui import QDrag, QPixmap, QMouseEvent +from PyQt5.QtGui import QDrag, QPixmap, QMouseEvent, QPaintEvent -from api_krita.pyqt import PixmapTransform, BaseWidget +from api_krita import Krita +from api_krita.pyqt import Painter, PixmapTransform, BaseWidget from .pie_style import PieStyle from .label import Label @@ -43,6 +44,7 @@ def __init__( self._draggable = True self._enabled = True self._hovered = False + self._forced = False self._instructions: list[WidgetInstructions] = [] @@ -50,6 +52,52 @@ def add_instruction(self, instruction: WidgetInstructions): """Add additional logic to do on entering and leaving widget.""" self._instructions.append(instruction) + def paintEvent(self, event: QPaintEvent) -> None: + with Painter(self, event) as painter: + self.paint(painter) + + def paint(self, painter: Painter): + """ + Paint the entire widget using the Painter wrapper. + + Paint a background behind a label its border, and image itself. + """ + # label background + painter.paint_wheel( + center=self.center, + outer_radius=( + self.icon_radius + - self._active_indicator_thickness + - self._style.border_thickness//2), + color=Krita.get_main_color_from_theme()) + + # label thin border + painter.paint_wheel( + center=self.center, + outer_radius=self.icon_radius-self._active_indicator_thickness, + color=self._style.border_color, + thickness=self._style.border_thickness) + + # label thick border when label when disabled + if not self.enabled: + painter.paint_wheel( + center=self.center, + outer_radius=self.icon_radius, + color=self._style.active_color_dark, + thickness=self._active_indicator_thickness) + + # label thick border when hovered (or it is forced) + if self.forced or (self._hovered and self.draggable): + painter.paint_wheel( + center=self.center, + outer_radius=self.icon_radius, + color=self._border_active_color, + thickness=self._active_indicator_thickness) + + @property + def _active_indicator_thickness(self): + return self._style.border_thickness*2 + @property def draggable(self) -> bool: """Return whether the label can be dragged.""" @@ -80,6 +128,19 @@ def enabled(self, value: bool) -> None: self.draggable = False self.repaint() + @property + def forced(self): + """Return whether the widget has forced active color.""" + return self._forced + + @forced.setter + def forced(self, value: bool) -> None: + """Make the widget look as it is active even if it is not.""" + if self._forced == value: + return + self._forced = value + self.repaint() + def move_to_label(self) -> None: """Move the widget according to current center of label it holds.""" self.move_center(self.label.center) @@ -115,13 +176,10 @@ def leaveEvent(self, e: QEvent) -> None: self.repaint() @property - def _border_color(self): - """Return border color which differs when enabled or hovered.""" - if not self.enabled: - return self._style.active_color_dark - if self._hovered and self.draggable: + def _border_active_color(self): + if self.forced: return self._style.active_color - return self._style.border_color + return self._style.active_color @property def icon_radius(self): diff --git a/shortcut_composer/templates/pie_menu_utils/label_widget_utils/__init__.py b/shortcut_composer/templates/pie_menu_utils/label_widget_impl/__init__.py similarity index 65% rename from shortcut_composer/templates/pie_menu_utils/label_widget_utils/__init__.py rename to shortcut_composer/templates/pie_menu_utils/label_widget_impl/__init__.py index 6d3e69fd..bd14b4bb 100644 --- a/shortcut_composer/templates/pie_menu_utils/label_widget_utils/__init__.py +++ b/shortcut_composer/templates/pie_menu_utils/label_widget_impl/__init__.py @@ -3,6 +3,6 @@ """Implementation of different LabelWidget types.""" -from .create_label_widget import create_label_widget +from .dispatch_label_widget import dispatch_label_widget -__all__ = ["create_label_widget"] +__all__ = ["dispatch_label_widget"] diff --git a/shortcut_composer/templates/pie_menu_utils/label_widget_utils/create_label_widget.py b/shortcut_composer/templates/pie_menu_utils/label_widget_impl/dispatch_label_widget.py similarity index 64% rename from shortcut_composer/templates/pie_menu_utils/label_widget_utils/create_label_widget.py rename to shortcut_composer/templates/pie_menu_utils/label_widget_impl/dispatch_label_widget.py index 9fbc5117..da593fa9 100644 --- a/shortcut_composer/templates/pie_menu_utils/label_widget_utils/create_label_widget.py +++ b/shortcut_composer/templates/pie_menu_utils/label_widget_impl/dispatch_label_widget.py @@ -7,10 +7,7 @@ QPixmap, QIcon, ) -from PyQt5.QtWidgets import QWidget - from api_krita.pyqt import Text -from ..pie_style import PieStyle from ..label import Label from ..label_widget import LabelWidget from .icon_label_widget import IconLabelWidget @@ -18,20 +15,13 @@ from .image_label_widget import ImageLabelWidget -def create_label_widget( - label: Label, - style: PieStyle, - parent: QWidget, - is_unscaled: bool = False, -) -> LabelWidget: - """Return LabelWidget which can display this label.""" +def dispatch_label_widget(label: Label) -> Type[LabelWidget]: + """Return type of LabelWidget proper for given label.""" if label.display_value is None: raise ValueError(f"Label {label} is not valid") - painter_type: Type[LabelWidget] = { + return { QPixmap: ImageLabelWidget, Text: TextLabelWidget, QIcon: IconLabelWidget, }[type(label.display_value)] - - return painter_type(label, style, parent, is_unscaled) diff --git a/shortcut_composer/templates/pie_menu_utils/label_widget_utils/icon_label_widget.py b/shortcut_composer/templates/pie_menu_utils/label_widget_impl/icon_label_widget.py similarity index 100% rename from shortcut_composer/templates/pie_menu_utils/label_widget_utils/icon_label_widget.py rename to shortcut_composer/templates/pie_menu_utils/label_widget_impl/icon_label_widget.py diff --git a/shortcut_composer/templates/pie_menu_utils/label_widget_utils/image_label_widget.py b/shortcut_composer/templates/pie_menu_utils/label_widget_impl/image_label_widget.py similarity index 55% rename from shortcut_composer/templates/pie_menu_utils/label_widget_utils/image_label_widget.py rename to shortcut_composer/templates/pie_menu_utils/label_widget_impl/image_label_widget.py index 7ac52b93..8247c6db 100644 --- a/shortcut_composer/templates/pie_menu_utils/label_widget_utils/image_label_widget.py +++ b/shortcut_composer/templates/pie_menu_utils/label_widget_impl/image_label_widget.py @@ -1,10 +1,7 @@ # SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus # SPDX-License-Identifier: GPL-3.0-or-later -from PyQt5.QtGui import ( - QPixmap, - QPaintEvent, -) +from PyQt5.QtGui import QPixmap from PyQt5.QtWidgets import QWidget from api_krita.pyqt import Painter, PixmapTransform @@ -26,25 +23,9 @@ def __init__( super().__init__(label, style, parent, is_unscaled) self.ready_image = self._prepare_image() - def paintEvent(self, event: QPaintEvent) -> None: - """ - Paint the entire widget using the Painter wrapper. - - Paint a background behind a label its border, and image itself. - """ - with Painter(self, event) as painter: - painter.paint_wheel( - center=self.center, - outer_radius=self.icon_radius, - color=self._style.icon_color) - - painter.paint_wheel( - center=self.center, - outer_radius=( - self.icon_radius-self._style.border_thickness//2), - color=self._border_color, - thickness=self._style.border_thickness) - painter.paint_pixmap(self.center, self.ready_image) + def paint(self, painter: Painter): + super().paint(painter) + painter.paint_pixmap(self.center, self.ready_image) def _prepare_image(self) -> QPixmap: """Return image after scaling and reshaping it to circle.""" @@ -56,4 +37,7 @@ def _prepare_image(self) -> QPixmap: rounded_image = PixmapTransform.make_pixmap_round(to_display) return PixmapTransform.scale_pixmap( pixmap=rounded_image, - size_px=round(self.icon_radius*1.8)) + size_px=round(( + self.icon_radius + - self._style.border_thickness + - self._active_indicator_thickness)*2)) diff --git a/shortcut_composer/templates/pie_menu_utils/label_widget_utils/text_label_widget.py b/shortcut_composer/templates/pie_menu_utils/label_widget_impl/text_label_widget.py similarity index 68% rename from shortcut_composer/templates/pie_menu_utils/label_widget_utils/text_label_widget.py rename to shortcut_composer/templates/pie_menu_utils/label_widget_impl/text_label_widget.py index 01431c14..54063339 100644 --- a/shortcut_composer/templates/pie_menu_utils/label_widget_utils/text_label_widget.py +++ b/shortcut_composer/templates/pie_menu_utils/label_widget_impl/text_label_widget.py @@ -2,15 +2,11 @@ # SPDX-License-Identifier: GPL-3.0-or-later from PyQt5.QtCore import Qt -from PyQt5.QtGui import ( - QFont, - QColor, - QFontDatabase, - QPaintEvent, -) +from PyQt5.QtGui import QFont, QColor, QFontDatabase from PyQt5.QtWidgets import QLabel, QWidget -from api_krita.pyqt import Painter, Text +from api_krita import Krita +from api_krita.pyqt import Text from ..pie_style import PieStyle from ..label import Label from ..label_widget import LabelWidget @@ -29,23 +25,6 @@ def __init__( super().__init__(label, style, parent, is_unscaled) self._pyqt_label = self._create_pyqt_label() - def paintEvent(self, event: QPaintEvent) -> None: - """ - Paint the entire widget using the Painter wrapper. - - Paint a background behind a label and its border. - """ - with Painter(self, event) as painter: - painter.paint_wheel( - center=self.center, - outer_radius=self.icon_radius, - color=self._style.icon_color) - painter.paint_wheel( - center=self.center, - outer_radius=self.icon_radius, - color=self._border_color, - thickness=self._style.border_thickness) - def _create_pyqt_label(self) -> QLabel: """Create and show a new Qt5 label. Does not need redrawing.""" to_display = self.label.display_value @@ -63,7 +42,8 @@ def _create_pyqt_label(self) -> QLabel: label.move(self.center.x()-heigth, self.center.y()-heigth//2) label.setStyleSheet(f''' - background-color:rgba({self._color_to_str(self._style.icon_color)}); + background-color:rgba({self._color_to_str( + Krita.get_main_color_from_theme())}); color:rgba({self._color_to_str(to_display.color)}); ''') diff --git a/shortcut_composer/templates/pie_menu_utils/widget_utils/pie_button.py b/shortcut_composer/templates/pie_menu_utils/pie_button.py similarity index 71% rename from shortcut_composer/templates/pie_menu_utils/widget_utils/pie_button.py rename to shortcut_composer/templates/pie_menu_utils/pie_button.py index e41af8a4..99cb3c41 100644 --- a/shortcut_composer/templates/pie_menu_utils/widget_utils/pie_button.py +++ b/shortcut_composer/templates/pie_menu_utils/pie_button.py @@ -7,8 +7,8 @@ from PyQt5.QtGui import QIcon from api_krita.pyqt import RoundButton -from ..pie_style import PieStyle -from ..pie_config import PieConfig +from .pie_style import PieStyle +from .pie_config import PieConfig class PieButton(RoundButton): @@ -28,6 +28,10 @@ def __init__( config: PieConfig, parent: Optional[QWidget] = None, ) -> None: + self._radius_callback = radius_callback + self._style = style + config.register_callback(self.refresh) + super().__init__( icon=icon, icon_scale=icon_scale, @@ -36,4 +40,8 @@ def __init__( active_color=style.active_color, parent=parent) - config.register_callback(lambda: self.resize(radius_callback())) + def refresh(self) -> None: + self._radius = self._radius_callback() + self._background_color = self._style.background_color + self._active_color = self._style.active_color + super().refresh() diff --git a/shortcut_composer/templates/pie_menu_utils/pie_config.py b/shortcut_composer/templates/pie_menu_utils/pie_config.py index 185f838e..6f8d9f38 100644 --- a/shortcut_composer/templates/pie_menu_utils/pie_config.py +++ b/shortcut_composer/templates/pie_menu_utils/pie_config.py @@ -2,11 +2,12 @@ # SPDX-License-Identifier: GPL-3.0-or-later from abc import ABC, abstractmethod -from typing import List, Callable, Generic, TypeVar, Optional, Union +from typing import List, Callable, Generic, TypeVar, Optional from PyQt5.QtGui import QColor -from config_system import Field, FieldGroup -from config_system.fields import DualField, FieldWithEditableDefault -from data_components import Tag +from api_krita import Krita +from config_system import FieldGroup +from config_system.field_base_impl import DualField, FieldWithEditableDefault +from data_components import DeadzoneStrategy T = TypeVar("T") U = TypeVar("U") @@ -15,18 +16,60 @@ class PieConfig(FieldGroup, Generic[T], ABC): """Abstract FieldGroup representing config of PieMenu.""" + def __init__( + self, + name: str, + values: List[T], + pie_radius_scale: float, + icon_radius_scale: float, + save_local: bool, + background_color: Optional[QColor], + active_color: Optional[QColor], + pie_opacity: int, + deadzone_strategy: DeadzoneStrategy + ) -> None: + super().__init__(name) + self._values = values + + self.PIE_RADIUS_SCALE = self.field( + name="Pie scale", + default=pie_radius_scale) + self.ICON_RADIUS_SCALE = self.field( + name="Icon scale", + default=icon_radius_scale) + + self.SAVE_LOCAL = self.field( + name="Save local", + default=save_local) + self.DEADZONE_STRATEGY = self.field( + name="deadzone", + default=deadzone_strategy) + + override_default = bool(active_color or background_color) + if background_color is None: + background_color = Krita.get_main_color_from_theme() + if active_color is None: + active_color = Krita.get_active_color_from_theme() + + self.OVERRIDE_DEFAULT_THEME = self.field( + name="Override default theme", + default=override_default) + self.BACKGROUND_COLOR = self.field( + name="Background color", + default=background_color) + self.ACTIVE_COLOR = self.field( + name="Active color", + default=active_color) + self.PIE_OPACITY = self.field( + name="Pie opacity", + default=pie_opacity) + allow_value_edit: bool """Is it allowed to remove elements in runtime. """ - name: str """Name of field group.""" - background_color: Optional[QColor] - active_color: QColor - - SAVE_LOCAL: Field[bool] ORDER: FieldWithEditableDefault[List[T], DualField[List[T]]] - PIE_RADIUS_SCALE: Field[float] - ICON_RADIUS_SCALE: Field[float] + """Values displayed in the pie. Used to synchronize pie elements.""" @abstractmethod def values(self) -> List[T]: @@ -77,154 +120,3 @@ def _create_editable_dual_field( return FieldWithEditableDefault( DualField(self, self.SAVE_LOCAL, field_name, default, parser_type), self.field(f"{field_name} default", default, parser_type)) - - -class PresetPieConfig(PieConfig[str]): - """ - FieldGroup representing config of PieMenu of presets. - - Values are calculated according to presets belonging to handled tag - and the custom order saved by the user in kritarc. - """ - - def __init__( - self, - name: str, - values: Union[Tag, List[str]], - pie_radius_scale: float, - icon_radius_scale: float, - save_local: bool, - background_color: Optional[QColor], - active_color: QColor, - ) -> None: - super().__init__(name) - - self.PIE_RADIUS_SCALE = self.field("Pie scale", pie_radius_scale) - self.ICON_RADIUS_SCALE = self.field("Icon scale", icon_radius_scale) - - self.SAVE_LOCAL = self.field("Save local", save_local) - - tag_mode = isinstance(values, Tag) - tag_name = values.tag_name if isinstance(values, Tag) else "" - self.TAG_MODE = self._create_editable_dual_field("Tag mode", tag_mode) - self.TAG_NAME = self._create_editable_dual_field("Tag", tag_name) - self.ORDER = self._create_editable_dual_field("Values", [], str) - - self.background_color = background_color - self.active_color = active_color - - @property - def allow_value_edit(self) -> bool: - """Return whether user can add and remove items from the pie.""" - return not self.TAG_MODE.read() - - def values(self) -> List[str]: - """Return all presets based on mode and stored order.""" - if not self.TAG_MODE.read(): - return self.ORDER.read() - return Tag(self.TAG_NAME.read()) - - def set_values(self, values: List[str]) -> None: - """When in tag mode, remember the tag order. Then write normally.""" - if self.TAG_MODE.read(): - group = "ShortcutComposer: Tag order" - field = Field(group, self.TAG_NAME.read(), [], str) - field.write(values) - - self.ORDER.write(values) - - def refresh_order(self) -> None: - """Refresh the values in case the active document changed.""" - self.TAG_MODE.field.refresh() - self.TAG_NAME.field.refresh() - self.ORDER.write(self.values()) - - def set_current_as_default(self): - """Set current pie values as a new default list of values.""" - self.TAG_MODE.default = self.TAG_MODE.read() - self.TAG_NAME.default = self.TAG_NAME.read() - self.ORDER.default = self.ORDER.read() - - def reset_the_default(self) -> None: - """Set empty pie as a new default list of values.""" - self.TAG_MODE.default = False - self.TAG_NAME.default = "" - self.ORDER.default = [] - - def reset_to_default(self) -> None: - """Replace current list of values in pie with the default list.""" - self.TAG_MODE.reset_default() - self.TAG_NAME.reset_default() - self.ORDER.reset_default() - self.refresh_order() - - def is_order_default(self) -> bool: - """Return whether order is the same as default one.""" - return ( - self.TAG_MODE.read() == self.TAG_MODE.default - and self.TAG_NAME.read() == self.TAG_NAME.default - and self.ORDER.read() == self.ORDER.default) - - def register_to_order_related(self, callback: Callable[[], None]) -> None: - """Register callback to all fields related to value order.""" - self.TAG_MODE.register_callback(callback) - self.TAG_NAME.register_callback(callback) - self.ORDER.register_callback(callback) - - -class NonPresetPieConfig(PieConfig[T], Generic[T]): - """FieldGroup representing config of PieMenu of non-preset values.""" - - def __init__( - self, - name: str, - values: List[T], - pie_radius_scale: float, - icon_radius_scale: float, - save_local: bool, - background_color: Optional[QColor], - active_color: QColor, - ) -> None: - super().__init__(name) - - self.PIE_RADIUS_SCALE = self.field("Pie scale", pie_radius_scale) - self.ICON_RADIUS_SCALE = self.field("Icon scale", icon_radius_scale) - - self.SAVE_LOCAL = self.field("Save local", save_local) - self.ORDER = self._create_editable_dual_field("Values", values) - - self.background_color = background_color - self.active_color = active_color - self.allow_value_edit = True - - def values(self) -> List[T]: - """Return values defined be the user to display as icons.""" - return self.ORDER.read() - - def set_values(self, values: List[T]) -> None: - """Change current values to new ones.""" - self.ORDER.write(values) - - def refresh_order(self) -> None: - """Refresh the values in case the active document changed.""" - self.ORDER.write(self.values()) - - def set_current_as_default(self): - """Set current pie values as a new default list of values.""" - self.ORDER.default = self.ORDER.read() - - def reset_the_default(self) -> None: - """Set empty pie as a new default list of values.""" - self.ORDER.default = [] - - def reset_to_default(self) -> None: - self.ORDER.reset_default() - self.refresh_order() - - def is_order_default(self) -> bool: - """Return whether order is the same as default one.""" - return self.ORDER.read() == self.ORDER.default - - def register_to_order_related(self, callback: Callable[[], None]) -> None: - """Register callback to all fields related to value order.""" - self.ORDER.register_callback(callback) diff --git a/shortcut_composer/templates/pie_menu_utils/pie_config_impl/__init__.py b/shortcut_composer/templates/pie_menu_utils/pie_config_impl/__init__.py new file mode 100644 index 00000000..f33633c0 --- /dev/null +++ b/shortcut_composer/templates/pie_menu_utils/pie_config_impl/__init__.py @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Implementations of PieConfig.""" + +from .dispatch_pie_config import dispatch_pie_config +from .preset_pie_config import PresetPieConfig +from .non_preset_pie_config import NonPresetPieConfig + +__all__ = ["dispatch_pie_config", "PresetPieConfig", "NonPresetPieConfig"] diff --git a/shortcut_composer/templates/pie_menu_utils/pie_config_impl/dispatch_pie_config.py b/shortcut_composer/templates/pie_menu_utils/pie_config_impl/dispatch_pie_config.py new file mode 100644 index 00000000..4354f1e0 --- /dev/null +++ b/shortcut_composer/templates/pie_menu_utils/pie_config_impl/dispatch_pie_config.py @@ -0,0 +1,14 @@ +from typing import Type, TypeVar +from core_components import Controller +from ..pie_config import PieConfig +from .preset_pie_config import PresetPieConfig +from .non_preset_pie_config import NonPresetPieConfig + +T = TypeVar('T') + + +def dispatch_pie_config(controller: Controller[T]) -> Type[PieConfig[T]]: + """Return type of PieConfig specialisation based on controller type.""" + if issubclass(controller.TYPE, str): + return PresetPieConfig # type: ignore + return NonPresetPieConfig diff --git a/shortcut_composer/templates/pie_menu_utils/pie_config_impl/non_preset_pie_config.py b/shortcut_composer/templates/pie_menu_utils/pie_config_impl/non_preset_pie_config.py new file mode 100644 index 00000000..15a289bf --- /dev/null +++ b/shortcut_composer/templates/pie_menu_utils/pie_config_impl/non_preset_pie_config.py @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +from typing import List, Callable, Generic, TypeVar, Optional +from PyQt5.QtGui import QColor + +from data_components import DeadzoneStrategy +from ..pie_config import PieConfig + +T = TypeVar("T") + + +class NonPresetPieConfig(PieConfig[T], Generic[T]): + """FieldGroup representing config of PieMenu of non-preset values.""" + + def __init__( + self, + name: str, + values: List[T], + pie_radius_scale: float, + icon_radius_scale: float, + save_local: bool, + background_color: Optional[QColor], + active_color: Optional[QColor], + pie_opacity: int, + deadzone_strategy: DeadzoneStrategy + ) -> None: + super().__init__( + name=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, + pie_opacity=pie_opacity, + deadzone_strategy=deadzone_strategy) + + self.ORDER = self._create_editable_dual_field( + field_name="Values", + default=self._values) + self.allow_value_edit = True + + def values(self) -> List[T]: + """Return values defined be the user to display as icons.""" + return self.ORDER.read() + + def set_values(self, values: List[T]) -> None: + """Change current values to new ones.""" + self.ORDER.write(values) + + def refresh_order(self) -> None: + """Refresh the values in case the active document changed.""" + self.ORDER.write(self.values()) + + def set_current_as_default(self): + """Set current pie values as a new default list of values.""" + self.ORDER.default = self.ORDER.read() + + def reset_the_default(self) -> None: + """Set empty pie as a new default list of values.""" + self.ORDER.default = [] + + def reset_to_default(self) -> None: + self.ORDER.reset_default() + self.refresh_order() + + def is_order_default(self) -> bool: + """Return whether order is the same as default one.""" + return self.ORDER.read() == self.ORDER.default + + def register_to_order_related(self, callback: Callable[[], None]) -> None: + """Register callback to all fields related to value order.""" + self.ORDER.register_callback(callback) diff --git a/shortcut_composer/templates/pie_menu_utils/pie_config_impl/preset_pie_config.py b/shortcut_composer/templates/pie_menu_utils/pie_config_impl/preset_pie_config.py new file mode 100644 index 00000000..e8b99e26 --- /dev/null +++ b/shortcut_composer/templates/pie_menu_utils/pie_config_impl/preset_pie_config.py @@ -0,0 +1,112 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +from typing import List, Callable, Union, Optional +from PyQt5.QtGui import QColor + +from config_system import Field +from data_components import Tag, DeadzoneStrategy +from ..pie_config import PieConfig + + +class PresetPieConfig(PieConfig[str]): + """ + FieldGroup representing config of PieMenu of presets. + + Values are calculated according to presets belonging to handled tag + and the custom order saved by the user in kritarc. + """ + + def __init__( + self, + name: str, + values: Union[Tag, List[str]], + pie_radius_scale: float, + icon_radius_scale: float, + save_local: bool, + background_color: Optional[QColor], + active_color: Optional[QColor], + pie_opacity: int, + deadzone_strategy: DeadzoneStrategy + ) -> None: + super().__init__( + name=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, + pie_opacity=pie_opacity, + deadzone_strategy=deadzone_strategy) + + tag_mode = isinstance(values, Tag) + tag_name = values.tag_name if isinstance(values, Tag) else "" + self.TAG_MODE = self._create_editable_dual_field( + field_name="Tag mode", + default=tag_mode) + self.TAG_NAME = self._create_editable_dual_field( + field_name="Tag", + default=tag_name) + self.ORDER = self._create_editable_dual_field( + field_name="Values", + default=[], + parser_type=str) + + @property + def allow_value_edit(self) -> bool: + """Return whether user can add and remove items from the pie.""" + return not self.TAG_MODE.read() + + def values(self) -> List[str]: + """Return all presets based on mode and stored order.""" + if not self.TAG_MODE.read(): + return self.ORDER.read() + return Tag(self.TAG_NAME.read()) + + def set_values(self, values: List[str]) -> None: + """When in tag mode, remember the tag order. Then write normally.""" + if self.TAG_MODE.read(): + group = "ShortcutComposer: Tag order" + field = Field(group, self.TAG_NAME.read(), [], str) + field.write(values) + + self.ORDER.write(values) + + def refresh_order(self) -> None: + """Refresh the values in case the active document changed.""" + self.TAG_MODE.field.refresh() + self.TAG_NAME.field.refresh() + self.ORDER.write(self.values()) + + def set_current_as_default(self): + """Set current pie values as a new default list of values.""" + self.TAG_MODE.default = self.TAG_MODE.read() + self.TAG_NAME.default = self.TAG_NAME.read() + self.ORDER.default = self.ORDER.read() + + def reset_the_default(self) -> None: + """Set empty pie as a new default list of values.""" + self.TAG_MODE.default = False + self.TAG_NAME.default = "" + self.ORDER.default = [] + + def reset_to_default(self) -> None: + """Replace current list of values in pie with the default list.""" + self.TAG_MODE.reset_default() + self.TAG_NAME.reset_default() + self.ORDER.reset_default() + self.refresh_order() + + def is_order_default(self) -> bool: + """Return whether order is the same as default one.""" + return ( + self.TAG_MODE.read() == self.TAG_MODE.default + and self.TAG_NAME.read() == self.TAG_NAME.default + and self.ORDER.read() == self.ORDER.default) + + def register_to_order_related(self, callback: Callable[[], None]) -> None: + """Register callback to all fields related to value order.""" + self.TAG_MODE.register_callback(callback) + self.TAG_NAME.register_callback(callback) + self.ORDER.register_callback(callback) diff --git a/shortcut_composer/templates/pie_menu_utils/pie_manager.py b/shortcut_composer/templates/pie_menu_utils/pie_manager.py index 0c0c9bf6..04fbfed4 100644 --- a/shortcut_composer/templates/pie_menu_utils/pie_manager.py +++ b/shortcut_composer/templates/pie_menu_utils/pie_manager.py @@ -8,8 +8,8 @@ from api_krita.pyqt import Timer from composer_utils import Config from .pie_widget import PieWidget +from .pie_widget_utils import CirclePoints from .label import Label -from .widget_utils import CirclePoints class PieManager: diff --git a/shortcut_composer/templates/pie_menu_utils/settings_gui/pie_settings.py b/shortcut_composer/templates/pie_menu_utils/pie_settings.py similarity index 73% rename from shortcut_composer/templates/pie_menu_utils/settings_gui/pie_settings.py rename to shortcut_composer/templates/pie_menu_utils/pie_settings.py index b210a698..350193fa 100644 --- a/shortcut_composer/templates/pie_menu_utils/settings_gui/pie_settings.py +++ b/shortcut_composer/templates/pie_menu_utils/pie_settings.py @@ -14,10 +14,16 @@ from api_krita import Krita from api_krita.pyqt import AnimatedWidget, BaseWidget, SafeConfirmButton -from config_system.ui import ConfigFormWidget, ConfigSpinBox +from config_system.ui import ( + ConfigFormWidget, + SpinBox, + EnumComboBox, + ColorButton, + Checkbox) from composer_utils import Config -from ..pie_style import PieStyle -from ..pie_config import PieConfig +from data_components import DeadzoneStrategy +from .pie_style import PieStyle +from .pie_config import PieConfig class PieSettings(AnimatedWidget, BaseWidget): @@ -38,6 +44,7 @@ def __init__( self, config: PieConfig, style: PieStyle, + *args, **kwargs ) -> None: AnimatedWidget.__init__(self, None, Config.PIE_ANIMATION_TIME.read()) self.setMinimumHeight(round(style.widget_radius*2)) @@ -55,17 +62,79 @@ def __init__( self._tab_holder = QTabWidget() self._local_settings = ConfigFormWidget([ - ConfigSpinBox(config.PIE_RADIUS_SCALE, self, "Pie scale", 0.05, 4), - ConfigSpinBox(config.ICON_RADIUS_SCALE, self, "Icon max scale", - 0.05, 4), + "Behaviour", + EnumComboBox( + config_field=config.DEADZONE_STRATEGY, + parent=self, + pretty_name="On deadzone", + enum_type=DeadzoneStrategy), + + "Size", + SpinBox( + config_field=config.PIE_RADIUS_SCALE, + parent=self, + pretty_name="Pie scale", + step=0.05, + max_value=4), + SpinBox( + config_field=config.ICON_RADIUS_SCALE, + parent=self, + pretty_name="Icon max scale", + step=0.05, + max_value=4), + + "Style", + theme_checkbox := Checkbox( + config_field=config.OVERRIDE_DEFAULT_THEME, + parent=self, + pretty_name="Override default theme"), + bg_button := ColorButton( + config_field=config.BACKGROUND_COLOR, + parent=self, + pretty_name="Background color"), + active_button := ColorButton( + config_field=config.ACTIVE_COLOR, + parent=self, + pretty_name="Active color"), + opacity_picker := SpinBox( + config_field=config.PIE_OPACITY, + parent=self, + pretty_name="Pie opacity", + step=1, + max_value=100), ]) - self._tab_holder.addTab(self._local_settings, "Preferences") + + def update_theme_state(): + """Hide color buttons when not taken into consideration.""" + enable_state = theme_checkbox.widget.isChecked() + bg_button.widget.setVisible(enable_state) + active_button.widget.setVisible(enable_state) + opacity_picker.widget.setEnabled(enable_state) + theme_checkbox.widget.stateChanged.connect(update_theme_state) + update_theme_state() + + preferences_widget = QWidget() + preferences = QVBoxLayout(self) + preferences.addWidget(self._local_settings) + preferences.addStretch() + preferences.addWidget(self._init_full_reset_button()) + preferences_widget.setLayout(preferences) + + self._tab_holder.addTab(preferences_widget, "Preferences") self._tab_holder.addTab(LocationTab(self._config), "Save location") layout = QVBoxLayout(self) layout.addWidget(self._tab_holder) self.setLayout(layout) + def _init_full_reset_button(self) -> SafeConfirmButton: + button = SafeConfirmButton( + text="Reset pie preferences", + icon=Krita.get_icon("edit-delete")) + button.clicked.connect(self._reset_config_to_default) + button.setFixedHeight(button.sizeHint().height()*2) + return button + def show(self) -> None: """Show the window after its settings are refreshed.""" self._local_settings.refresh() @@ -80,6 +149,15 @@ def _reset(self) -> None: """React to change in pie size.""" self.setMinimumHeight(self._style.widget_radius*2) + def _reset_config_to_default(self): + """ + Reset widgets from preferences layout to default values. + + Does not write to config yet, to prevent artifacts on pie. + """ + for widget in self._local_settings.widgets: + widget.set(widget.config_field.default) + class LocationTab(QWidget): """PieSettings tab for changing location in which values are saved.""" diff --git a/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/__init__.py b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/__init__.py new file mode 100644 index 00000000..31eb9eb6 --- /dev/null +++ b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Implementation of different PieSettings.""" + +from .dispatch_pie_settings import dispatch_pie_settings + +__all__ = ["dispatch_pie_settings"] diff --git a/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/common_utils/__init__.py b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/common_utils/__init__.py new file mode 100644 index 00000000..d22c772b --- /dev/null +++ b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/common_utils/__init__.py @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Components used by implementations of PieSettins.""" + +from .group_combo_box import GroupComboBox +from .group_manager import GroupManager +from .group_scroll_area import GroupScrollArea +from .scroll_area import ScrollArea + +__all__ = [ + "GroupComboBox", + "GroupManager", + "GroupScrollArea", + "ScrollArea", +] diff --git a/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/common_utils/group_combo_box.py b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/common_utils/group_combo_box.py new file mode 100644 index 00000000..456e31d5 --- /dev/null +++ b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/common_utils/group_combo_box.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +from typing import List, Optional + +from PyQt5.QtWidgets import QWidget + +from config_system import Field +from config_system.ui import StringComboBox +from .group_manager import GroupManager + + +class GroupComboBox(StringComboBox): + + def __init__( + self, + config_field: Field[str], + group_fetcher: GroupManager, + parent: Optional[QWidget] = None, + pretty_name: Optional[str] = None, + additional_fields: List[str] = [], + ) -> None: + self._additional_fields = additional_fields + self._group_fetcher = group_fetcher + super().__init__(config_field, parent, pretty_name) + self.config_field.register_callback( + lambda: self.set(self.config_field.read())) + + def reset(self) -> None: + """Replace list of available tags with those red from database.""" + self._combo_box.clear() + self._combo_box.addItems(self._additional_fields) + self._combo_box.addItems(self._group_fetcher.fetch_groups()) + self.set(self.config_field.read()) diff --git a/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/common_utils/group_manager.py b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/common_utils/group_manager.py new file mode 100644 index 00000000..ae0c5a26 --- /dev/null +++ b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/common_utils/group_manager.py @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +from typing import List, Protocol +from enum import Enum + +from templates.pie_menu_utils import Label + + +class GroupManager(Protocol): + def fetch_groups(self) -> list: ... + def get_values(self, group: str) -> list: ... + def create_labels(self, values: List[Enum]) -> List[Label]: ... diff --git a/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/common_utils/group_scroll_area.py b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/common_utils/group_scroll_area.py new file mode 100644 index 00000000..753f4bed --- /dev/null +++ b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/common_utils/group_scroll_area.py @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +from typing import List + +from config_system import Field +from templates.pie_menu_utils import PieStyle +from templates.pie_menu_utils.pie_settings_impl.common_utils import ( + GroupComboBox, + GroupManager) +from .scroll_area import ScrollArea + + +class GroupScrollArea(ScrollArea): + def __init__( + self, + fetcher: GroupManager, + style: PieStyle, + columns: int, + field: Field, + additional_fields: List[str] = [], + parent=None + ) -> None: + super().__init__(style, columns, parent) + self._field = field + self._fetcher = fetcher + self._chooser = GroupComboBox( + config_field=self._field, + group_fetcher=self._fetcher, + additional_fields=additional_fields) + self._chooser.widget.currentTextChanged.connect(self._display_group) + self._layout.insertWidget(0, self._chooser.widget) + self._display_group() + + def _display_group(self) -> None: + """Update preset widgets according to tag selected in combobox.""" + picked_group = self._chooser.widget.currentText() + values = self._fetcher.get_values(picked_group) + self.replace_handled_labels(self._fetcher.create_labels(values)) + self._apply_search_bar_filter() + self._chooser.save() diff --git a/shortcut_composer/templates/pie_menu_utils/settings_gui/offset_grid_layout.py b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/common_utils/offset_grid_layout.py similarity index 96% rename from shortcut_composer/templates/pie_menu_utils/settings_gui/offset_grid_layout.py rename to shortcut_composer/templates/pie_menu_utils/pie_settings_impl/common_utils/offset_grid_layout.py index 9680dc90..1693521c 100644 --- a/shortcut_composer/templates/pie_menu_utils/settings_gui/offset_grid_layout.py +++ b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/common_utils/offset_grid_layout.py @@ -6,7 +6,7 @@ from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QWidget, QGridLayout -from ..label_widget import LabelWidget +from templates.pie_menu_utils import LabelWidget class GridPosition(NamedTuple): @@ -38,7 +38,7 @@ def __init__(self, max_columns: int, owner: QWidget) -> None: self._max_columns = max_columns self._items_in_group = 2*max_columns - 1 self._owner = owner - self.setAlignment(Qt.AlignTop | Qt.AlignLeft) # type: ignore + self.setAlignment(Qt.AlignTop) # type: ignore self.setVerticalSpacing(5) self.setHorizontalSpacing(5) diff --git a/shortcut_composer/templates/pie_menu_utils/settings_gui/scroll_area.py b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/common_utils/scroll_area.py similarity index 90% rename from shortcut_composer/templates/pie_menu_utils/settings_gui/scroll_area.py rename to shortcut_composer/templates/pie_menu_utils/pie_settings_impl/common_utils/scroll_area.py index 3af534ed..3fffa436 100644 --- a/shortcut_composer/templates/pie_menu_utils/settings_gui/scroll_area.py +++ b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/common_utils/scroll_area.py @@ -12,30 +12,14 @@ QLabel, QLineEdit, QVBoxLayout, - QHBoxLayout) + QHBoxLayout, + QSizePolicy) -from ..label import Label -from ..label_widget import LabelWidget -from ..label_widget_utils import create_label_widget -from ..pie_style import PieStyle +from templates.pie_menu_utils import Label, LabelWidget, PieStyle +from templates.pie_menu_utils.label_widget_impl import dispatch_label_widget from .offset_grid_layout import OffsetGridLayout -class ChildInstruction: - """Logic of displaying widget text in passed QLabel.""" - - def __init__(self, display_label: QLabel) -> None: - self._display_label = display_label - - def on_enter(self, label: Label) -> None: - """Set text of label which was entered with mouse.""" - self._display_label.setText(str(label.pretty_name)) - - def on_leave(self, label: Label) -> None: - """Reset text after mouse leaves the widget.""" - self._display_label.setText("") - - class EmptySignal(Protocol): """Protocol fixing the wrong PyQt typing.""" @@ -82,7 +66,7 @@ def __init__( self._children_list: List[LabelWidget] = [] self._grid = OffsetGridLayout(self._columns, self) - self._active_label_display = QLabel(self) + self._active_label_display = self._init_active_label_display() self._search_bar = self._init_search_bar() self._layout = self._init_layout() @@ -106,6 +90,16 @@ def _init_layout(self) -> QVBoxLayout: layout.addLayout(footer) return layout + def _init_active_label_display(self): + """Return a label displaying hovered label.""" + label = QLabel(self) + label.setSizePolicy( + QSizePolicy.Ignored, + QSizePolicy.Expanding) + label.setMaximumHeight(label.sizeHint().height()*2) + label.setWordWrap(True) + return label + def _init_scroll_area(self) -> QScrollArea: """Create a widget, which scrolls internal widget with grid layout.""" internal = QWidget() @@ -146,7 +140,7 @@ def _apply_search_bar_filter(self) -> None: def _create_child(self, label: Label) -> LabelWidget: """Create LabelWidget that represent the label.""" - child = create_label_widget( + child = dispatch_label_widget(label)( label=label, style=self._style, parent=self, @@ -182,3 +176,18 @@ def mark_used_values(self, used_values: list) -> None: else: widget.enabled = True widget.draggable = True + + +class ChildInstruction: + """Logic of displaying widget text in passed QLabel.""" + + def __init__(self, display_label: QLabel) -> None: + self._display_label = display_label + + def on_enter(self, label: Label) -> None: + """Set text of label which was entered with mouse.""" + self._display_label.setText(str(label.pretty_name)) + + def on_leave(self, label: Label) -> None: + """Reset text after mouse leaves the widget.""" + self._display_label.setText("") diff --git a/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/dispatch_pie_settings.py b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/dispatch_pie_settings.py new file mode 100644 index 00000000..3844f724 --- /dev/null +++ b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/dispatch_pie_settings.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +from typing import Type +from enum import Enum + +from api_krita.enums.helpers import EnumGroup +from core_components import Controller +from ..pie_settings import PieSettings +from .enum_group_pie_settings import EnumGroupPieSettings +from .numeric_pie_settings import NumericPieSettings +from .preset_pie_settings import PresetPieSettings +from .enum_pie_settings import EnumPieSettings + + +def dispatch_pie_settings(controller: Controller) -> Type[PieSettings]: + """Return the right settings type based on value type.""" + if issubclass(controller.TYPE, str): + return PresetPieSettings + elif issubclass(controller.TYPE, float): + return NumericPieSettings + elif issubclass(controller.TYPE, EnumGroup): + return EnumGroupPieSettings + elif issubclass(controller.TYPE, Enum): + return EnumPieSettings + raise ValueError(f"No known pie settings for type of `{controller.TYPE}`") diff --git a/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/enum_group_pie_settings.py b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/enum_group_pie_settings.py new file mode 100644 index 00000000..ae23a9c5 --- /dev/null +++ b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/enum_group_pie_settings.py @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus +# SPDX-License-Identifier: GPL-3.0-or-later + +from typing import List +from enum import Enum + +from api_krita.enums.helpers import EnumGroup +from core_components import Controller +from templates.pie_menu_utils.pie_config_impl import NonPresetPieConfig +from templates.pie_menu_utils import Label, PieStyle, PieSettings +from .common_utils import GroupScrollArea, GroupManager + + +class EnumGroupPieSettings(PieSettings): + def __init__( + self, + controller: Controller[EnumGroup], + config: NonPresetPieConfig, + style: PieStyle, + *args, **kwargs + ) -> None: + super().__init__(config, style) + + self._action_values = GroupScrollArea( + fetcher=EnumGroupManager(controller), + style=self._style, + columns=3, + field=self._config.field("Last tag selected", "All"), + additional_fields=["All"]) + self._tab_holder.insertTab(1, self._action_values, "Values") + self._tab_holder.setCurrentIndex(1) + + self._action_values.widgets_changed.connect(self._refresh_draggable) + self._config.ORDER.register_callback(self._refresh_draggable) + self._refresh_draggable() + + def _refresh_draggable(self) -> None: + """Make all values currently used in pie undraggable and disabled.""" + self._action_values.mark_used_values(self._config.values()) + + +class EnumGroupManager(GroupManager): + def __init__(self, controller: Controller) -> None: + self._controller = controller + self._enum_type = self._controller.TYPE + + def fetch_groups(self) -> List[str]: + return list(self._enum_type._groups_.keys()) + + def get_values(self, group: str) -> List[Enum]: + if group == "All": + return list(self._enum_type._member_map_.values()) + return self._enum_type._groups_[group] + + def create_labels(self, values: List[Enum]) -> List[Label[Enum]]: + """Create labels from list of preset names.""" + labels = [Label.from_value(v, self._controller) for v in values] + return [label for label in labels if label is not None] diff --git a/shortcut_composer/templates/pie_menu_utils/settings_gui/enum_pie_settings.py b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/enum_pie_settings.py similarity index 87% rename from shortcut_composer/templates/pie_menu_utils/settings_gui/enum_pie_settings.py rename to shortcut_composer/templates/pie_menu_utils/pie_settings_impl/enum_pie_settings.py index 89c0891c..d5383693 100644 --- a/shortcut_composer/templates/pie_menu_utils/settings_gui/enum_pie_settings.py +++ b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/enum_pie_settings.py @@ -4,11 +4,9 @@ from enum import Enum from core_components import Controller -from ..label import Label -from ..pie_style import PieStyle -from ..pie_config import NonPresetPieConfig -from .pie_settings import PieSettings -from .scroll_area import ScrollArea +from templates.pie_menu_utils.pie_config_impl import NonPresetPieConfig +from templates.pie_menu_utils import Label, PieStyle, PieSettings +from .common_utils import ScrollArea class EnumPieSettings(PieSettings): @@ -25,6 +23,7 @@ def __init__( controller: Controller[Enum], config: NonPresetPieConfig, style: PieStyle, + *args, **kwargs ) -> None: super().__init__(config, style) diff --git a/shortcut_composer/templates/pie_menu_utils/settings_gui/numeric_pie_settings.py b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/numeric_pie_settings.py similarity index 85% rename from shortcut_composer/templates/pie_menu_utils/settings_gui/numeric_pie_settings.py rename to shortcut_composer/templates/pie_menu_utils/pie_settings_impl/numeric_pie_settings.py index 21be21d3..7c496ecd 100644 --- a/shortcut_composer/templates/pie_menu_utils/settings_gui/numeric_pie_settings.py +++ b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/numeric_pie_settings.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus # SPDX-License-Identifier: GPL-3.0-or-later -from .pie_settings import PieSettings +from templates.pie_menu_utils import PieSettings class NumericPieSettings(PieSettings): diff --git a/shortcut_composer/templates/pie_menu_utils/settings_gui/preset_pie_settings.py b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/preset_pie_settings.py similarity index 66% rename from shortcut_composer/templates/pie_menu_utils/settings_gui/preset_pie_settings.py rename to shortcut_composer/templates/pie_menu_utils/pie_settings_impl/preset_pie_settings.py index 8f91e768..40713b4b 100644 --- a/shortcut_composer/templates/pie_menu_utils/settings_gui/preset_pie_settings.py +++ b/shortcut_composer/templates/pie_menu_utils/pie_settings_impl/preset_pie_settings.py @@ -1,118 +1,34 @@ # SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus # SPDX-License-Identifier: GPL-3.0-or-later -from typing import List, Dict, Union, Optional, Iterable +from typing import List, Dict, Union, Iterable, Optional from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout -from config_system import Field -from config_system.ui import ConfigComboBox -from core_components.controllers import PresetController -from data_components import Tag from api_krita import Krita from api_krita.wrappers import Database from api_krita.pyqt import SafeConfirmButton -from ..label import Label -from ..pie_style import PieStyle -from ..pie_config import PresetPieConfig -from .pie_settings import PieSettings -from .scroll_area import ScrollArea - - -class TagComboBox(ConfigComboBox): - """ - Combobox for picking preset tags, which can be saved in config. - - When `allow_all` flag is True, the combobox will contain "All" item - will be added above the actual tags. - """ - - def __init__( - self, - config_field: Field[str], - parent: Optional[QWidget] = None, - pretty_name: Optional[str] = None, - additional_fields: List[str] = [], - ) -> None: - self._additional_fields = additional_fields - super().__init__(config_field, parent, pretty_name) - self.config_field.register_callback( - lambda: self.set(self.config_field.read())) - - def reset(self) -> None: - """Replace list of available tags with those red from database.""" - self._combo_box.clear() - self._combo_box.addItems(self._additional_fields) - with Database() as database: - self._combo_box.addItems(database.get_brush_tags()) - self.set(self.config_field.read()) +from core_components.controllers import PresetController +from data_components import Tag +from templates.pie_menu_utils.pie_config_impl import PresetPieConfig +from templates.pie_menu_utils import Label, PieStyle, PieSettings +from .common_utils import GroupComboBox, GroupScrollArea, GroupManager -class PresetScrollArea(ScrollArea): +class PresetPieSettings(PieSettings): """ - Scroll area for holding preset pies. - - Extends usual scroll area with the combobox over the area for - picking displayed tag. The picked tag is saved to given field. + Pie setting window for pie values being brush presets. - Operates in two modes: + Its `Values` tab operates in two modes: - Tag mode - the presets are determined by tracking krita tag - Manual mode - the presets are manually picked by the user """ - known_labels: Dict[str, Union[Label, None]] = {} - - def __init__( - self, - style: PieStyle, - columns: int, - field: Field, - parent=None - ) -> None: - super().__init__(style, columns, parent) - self._field = field - self.tag_chooser = TagComboBox( - self._field, - additional_fields=["---Select tag---", "All"]) - self.tag_chooser.widget.currentTextChanged.connect(self._display_tag) - self._layout.insertWidget(0, self.tag_chooser.widget) - self._display_tag() - - def _display_tag(self) -> None: - """Update preset widgets according to tag selected in combobox.""" - picked_tag = self.tag_chooser.widget.currentText() - if picked_tag == "All": - presets = Krita.get_presets().keys() - else: - presets = Tag(picked_tag) - - self.replace_handled_labels(self._create_labels(presets)) - self._apply_search_bar_filter() - self.tag_chooser.save() - - def _create_labels(self, values: Iterable[str]) -> List[Label[str]]: - """Create labels from list of preset names.""" - controller = PresetController() - labels: list[Optional[Label]] = [] - - for preset in values: - if preset in self.known_labels: - label = self.known_labels[preset] - else: - label = Label.from_value(preset, controller) - self.known_labels[preset] = label - labels.append(label) - - return [label for label in labels if label is not None] - - -class PresetPieSettings(PieSettings): - """Pie setting window for pie values being brush presets.""" - def __init__( self, config: PresetPieConfig, style: PieStyle, + *args, **kwargs ) -> None: super().__init__(config, style) self._config: PresetPieConfig @@ -120,19 +36,21 @@ def __init__( self._preset_scroll_area = self._init_preset_scroll_area() self._mode_button = self._init_mode_button() self._auto_combobox = self._init_auto_combobox() - self._manual_combobox = self._preset_scroll_area.tag_chooser + self._manual_combobox = self._preset_scroll_area._chooser self.set_tag_mode(self._config.TAG_MODE.read()) action_values = self._init_action_values() self._tab_holder.insertTab(1, action_values, "Values") self._tab_holder.setCurrentIndex(1) - def _init_preset_scroll_area(self) -> PresetScrollArea: + def _init_preset_scroll_area(self) -> GroupScrollArea: """Create preset scroll area which tracks which ones are used.""" - preset_scroll_area = PresetScrollArea( + preset_scroll_area = GroupScrollArea( + fetcher=PresetGroupManager(), style=self._style, columns=3, - field=self._config.field("Last tag selected", "---Select tag---")) + field=self._config.field("Last tag selected", "---Select tag---"), + additional_fields=["---Select tag---", "All"]) policy = preset_scroll_area.sizePolicy() policy.setRetainSizeWhenHidden(True) preset_scroll_area.setSizePolicy(policy) @@ -170,14 +88,18 @@ def switch_mode(): lambda: self.set_tag_mode(self._config.TAG_MODE.read(), False)) return mode_button - def _init_auto_combobox(self) -> TagComboBox: + def _init_auto_combobox(self) -> GroupComboBox: """Create tag modecombobox, which sets tag presets to the pie.""" def handle_picked_tag(): """Save used tag in config and report the values changed.""" auto_combobox.save() self._config.refresh_order() - auto_combobox = TagComboBox(self._config.TAG_NAME, self, "Tag name") + auto_combobox = GroupComboBox( + config_field=self._config.TAG_NAME, + group_fetcher=PresetGroupManager(), + pretty_name="Tag name") + auto_combobox.widget.currentTextChanged.connect(handle_picked_tag) return auto_combobox @@ -228,3 +150,34 @@ def set_tag_mode(self, value: bool, notify: bool = True) -> None: self._preset_scroll_area.show() self._manual_combobox.widget.show() self._auto_combobox.widget.hide() + + +class PresetGroupManager(GroupManager): + + known_labels: Dict[str, Union[Label, None]] = {} + + def __init__(self) -> None: + self._controller = PresetController() + + def fetch_groups(self) -> List[str]: + with Database() as database: + return database.get_brush_tags() + + def get_values(self, group: str) -> List[str]: + if group == "All": + return list(Krita.get_presets().keys()) + return Tag(group) + + def create_labels(self, values: Iterable[str]) -> List[Label[str]]: + """Create labels from list of preset names.""" + labels: list[Optional[Label]] = [] + + for preset in values: + if preset in self.known_labels: + label = self.known_labels[preset] + else: + label = Label.from_value(preset, self._controller) + self.known_labels[preset] = label + labels.append(label) + + return [label for label in labels if label is not None] diff --git a/shortcut_composer/templates/pie_menu_utils/pie_style.py b/shortcut_composer/templates/pie_menu_utils/pie_style.py index 4be4d10e..e6e38797 100644 --- a/shortcut_composer/templates/pie_menu_utils/pie_style.py +++ b/shortcut_composer/templates/pie_menu_utils/pie_style.py @@ -4,7 +4,6 @@ import math import platform from typing import TYPE_CHECKING -from copy import copy from PyQt5.QtGui import QColor @@ -94,8 +93,13 @@ def widget_radius(self) -> int: @property def border_thickness(self): - """Thickness of border around icons.""" - return round(self.unscaled_icon_radius*0.06) + """Thickness of border of the pie and icons.""" + return round(self.unscaled_icon_radius*0.05) + + @property + def decorator_thickness(self): + """Thickness of decorators near edges.""" + return self.border_thickness*4 @property def area_thickness(self): @@ -107,11 +111,6 @@ def inner_edge_radius(self): """Radius at which the base area starts.""" return self.pie_radius - self.area_thickness - @property - def no_border_radius(self): - """Radius at which pie decoration border starts.""" - return self.pie_radius - self.border_thickness//2 - @property def setting_button_radius(self) -> int: """Radius of the button which activates edit mode.""" @@ -125,18 +124,32 @@ def accept_button_radius(self) -> int: * Config.PIE_DEADZONE_GLOBAL_SCALE.read()) @property - def active_color(self): - """Color of active element.""" - return self._pie_config.active_color + def active_color(self) -> QColor: + """ + Color of highlight, when label is active. + + If custom one is not specified, use the default one. + """ + if self._pie_config.OVERRIDE_DEFAULT_THEME.read(): + return self._pie_config.ACTIVE_COLOR.read() + else: + return Config.default_active_color @property def background_color(self) -> QColor: - """Color of base area. Depends on the app theme lightness""" - if self._pie_config.background_color is not None: - return self._pie_config.background_color - if Krita.is_light_theme_active: - return QColor(210, 210, 210, 190) - return QColor(75, 75, 75, 190) + """ + Color of pie background area. + + If custom one is not specified, use the default one. + Opacity is stored in a separate field in <0-100> range + """ + if not self._pie_config.OVERRIDE_DEFAULT_THEME.read(): + return Config.default_background_color + + background_color = self._pie_config.BACKGROUND_COLOR.read() + opacity = self._pie_config.PIE_OPACITY.read() * 255 / 100 + background_color.setAlpha(round(opacity)) + return background_color @property def active_color_dark(self): @@ -146,22 +159,29 @@ def active_color_dark(self): round(self.active_color.green()*0.8), round(self.active_color.blue()*0.8)) - @property - def icon_color(self): - """Color of icon background.""" - color = copy(self.background_color) - color.setAlpha(255) - return color - @property def border_color(self): """Color of icon borders.""" return QColor( - min(self.icon_color.red()+15, 255), - min(self.icon_color.green()+15, 255), - min(self.icon_color.blue()+15, 255), + min(self.background_color.red()+15, 255), + min(self.background_color.green()+15, 255), + min(self.background_color.blue()+15, 255), 255) + @property + def background_decorator_color(self): + """Color of decorator near inner edge.""" + color = self.background_color + color = QColor(color.red()-5, color.green()-5, color.blue()-5, 60) + return color + + @property + def pie_decorator_color(self): + """Color of pie decorator near outer pie edge.""" + color = self.active_color_dark + color = QColor(color.red()-5, color.green()-5, color.blue()-5, 60) + return color + @property def font_multiplier(self): """Multiplier to apply to the font depending on the used OS.""" @@ -172,4 +192,4 @@ def font_multiplier(self): "Windows": 0.11, "Darwin": 0.265, "": 0.125} - """Scale to fix different font sizes each OS..""" + """Scale to fix different font sizes each OS.""" diff --git a/shortcut_composer/templates/pie_menu_utils/pie_widget.py b/shortcut_composer/templates/pie_menu_utils/pie_widget.py index 186b1379..a8735c14 100644 --- a/shortcut_composer/templates/pie_menu_utils/pie_widget.py +++ b/shortcut_composer/templates/pie_menu_utils/pie_widget.py @@ -12,11 +12,12 @@ from api_krita.pyqt import Painter, AnimatedWidget, BaseWidget from composer_utils import Config +from .edit_mode import EditMode from .pie_style import PieStyle from .label import Label from .label_widget import LabelWidget from .pie_config import PieConfig -from .widget_utils import ( +from .pie_widget_utils import ( WidgetHolder, CirclePoints, LabelHolder, @@ -29,14 +30,9 @@ class PieWidget(AnimatedWidget, BaseWidget, Generic[T]): """ PyQt5 widget with icons on ring that can be selected by hovering. - Methods inherits from QWidget used by other components: - - show() - displays the widget - - hide() - hides the widget - - repaint() - updates widget display after its data was changed - - It uses LabelHolder to store children widgets representing the - values user can pick. When the pie enters the edit mode, its - children become draggable. + Uses LabelHolder to store children widgets representing available + values. When the pie enters the edit mode, its children become + draggable. By dragging children, user can change their order or remove them by moving them out of the widget. New children can be added by @@ -47,9 +43,10 @@ def __init__( self, style: PieStyle, labels: List[Label[T]], + edit_mode: EditMode, config: PieConfig, parent=None - ): + ) -> None: AnimatedWidget.__init__(self, parent, Config.PIE_ANIMATION_TIME.read()) self.setGeometry(0, 0, style.widget_radius*2, style.widget_radius*2) @@ -66,11 +63,11 @@ def __init__( self._style = style self._labels = labels + self._edit_mode = edit_mode self.config = config self.config.register_callback(self._reset) self.active: Optional[Label] = None - self.is_edit_mode = False self._last_widget = None self.label_holder = LabelHolder( @@ -95,11 +92,11 @@ def deadzone(self) -> float: def paintEvent(self, event: QPaintEvent) -> None: """Paint the entire widget using the Painter wrapper.""" with Painter(self, event) as painter: - PiePainter(painter, self._labels, self._style, self.is_edit_mode) + PiePainter(painter, self._labels, self._style) def dragEnterEvent(self, e: QDragEnterEvent) -> None: """Allow dragging the widgets while in edit mode.""" - if self.is_edit_mode: + if self._edit_mode: return e.accept() e.ignore() @@ -107,46 +104,49 @@ def dragMoveEvent(self, e: QDragMoveEvent) -> None: """Handle all children actions - order change, add and remove.""" e.accept() source_widget = e.source() - pos = e.pos() + label = source_widget.label circle_points = CirclePoints( center=self.center, radius=self._style.pie_radius) - distance = circle_points.distance(pos) + distance = circle_points.distance(e.pos()) if not isinstance(source_widget, LabelWidget): # Drag incoming from outside the PieWidget ecosystem return - if self.type and not isinstance(source_widget.label.value, self.type): + if self._type and not isinstance(label.value, self._type): # Label type does not match the type of pie menu return self._last_widget = source_widget if distance > self._style.widget_radius: # Dragged out of the PieWidget - return self.label_holder.remove(source_widget.label) + return self.label_holder.remove(label) if not self._labels: # First label dragged to empty pie - return self.label_holder.insert(0, source_widget.label) + return self.label_holder.insert(0, label) if distance < self._style.deadzone_radius: # Do nothing in deadzone return - angle = circle_points.angle_from_point(pos) - _a = self._widget_holder.on_angle(angle) + angle = circle_points.angle_from_point(e.pos()) + _a = self.widget_holder.on_angle(angle) - if source_widget.label not in self.label_holder or not self._labels: + if label not in self.label_holder or not self._labels: # Dragged with unknown label index = self.label_holder.index(_a.label) - return self.label_holder.insert(index, source_widget.label) + return self.label_holder.insert(index, label) + + _b = self.widget_holder.on_label(label) + if _a == _b: + # Dragged over the same widget + return - _b = self._widget_holder.on_label(source_widget.label) - if _a != _b: - # Dragged existing label to a new location - self.label_holder.swap(_a.label, _b.label) - self.repaint() + # Dragged existing label to a new location + self.label_holder.swap(_a.label, _b.label) + self.repaint() def dragLeaveEvent(self, e: QDragLeaveEvent) -> None: """Remove the label when its widget is dragged out.""" @@ -160,12 +160,12 @@ def set_draggable(self, draggable: bool): widget.draggable = draggable @property - def _widget_holder(self) -> WidgetHolder: + def widget_holder(self) -> WidgetHolder: """Return the holder with child widgets.""" return self.label_holder.widget_holder @property - def type(self) -> Optional[type]: + def _type(self) -> Optional[type]: """Return type of values stored in labels. None if no labels.""" if not self._labels: return None diff --git a/shortcut_composer/templates/pie_menu_utils/widget_utils/__init__.py b/shortcut_composer/templates/pie_menu_utils/pie_widget_utils/__init__.py similarity index 70% rename from shortcut_composer/templates/pie_menu_utils/widget_utils/__init__.py rename to shortcut_composer/templates/pie_menu_utils/pie_widget_utils/__init__.py index bfd3d700..55fa7ead 100644 --- a/shortcut_composer/templates/pie_menu_utils/widget_utils/__init__.py +++ b/shortcut_composer/templates/pie_menu_utils/pie_widget_utils/__init__.py @@ -1,20 +1,16 @@ # SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus # SPDX-License-Identifier: GPL-3.0-or-later -"""Additional classes used by pie menu components.""" +"""Additional classes used by pie widget components.""" from .circle_points import CirclePoints from .widget_holder import WidgetHolder from .label_holder import LabelHolder from .pie_painter import PiePainter -from .pie_button import PieButton -from .edit_mode import EditMode __all__ = [ "CirclePoints", "WidgetHolder", "LabelHolder", "PiePainter", - "PieButton", - "EditMode", ] diff --git a/shortcut_composer/templates/pie_menu_utils/widget_utils/circle_points.py b/shortcut_composer/templates/pie_menu_utils/pie_widget_utils/circle_points.py similarity index 100% rename from shortcut_composer/templates/pie_menu_utils/widget_utils/circle_points.py rename to shortcut_composer/templates/pie_menu_utils/pie_widget_utils/circle_points.py diff --git a/shortcut_composer/templates/pie_menu_utils/widget_utils/label_holder.py b/shortcut_composer/templates/pie_menu_utils/pie_widget_utils/label_holder.py similarity index 96% rename from shortcut_composer/templates/pie_menu_utils/widget_utils/label_holder.py rename to shortcut_composer/templates/pie_menu_utils/pie_widget_utils/label_holder.py index acdf02fa..34e7d685 100644 --- a/shortcut_composer/templates/pie_menu_utils/widget_utils/label_holder.py +++ b/shortcut_composer/templates/pie_menu_utils/pie_widget_utils/label_holder.py @@ -8,7 +8,7 @@ from ..pie_style import PieStyle from ..label import Label from ..label_widget import LabelWidget -from ..label_widget_utils import create_label_widget +from ..label_widget_impl import dispatch_label_widget from ..pie_config import PieConfig from .widget_holder import WidgetHolder from .circle_points import CirclePoints @@ -114,8 +114,8 @@ def reset(self, notify: bool = True) -> None: children_widgets: List[LabelWidget] = [] for label in self._labels: - children_widgets.append( - create_label_widget(label, self._style, self._owner)) + children_widgets.append(dispatch_label_widget(label)( + label, self._style, self._owner)) circle_points = CirclePoints( center=self._owner.center, diff --git a/shortcut_composer/templates/pie_menu_utils/widget_utils/pie_painter.py b/shortcut_composer/templates/pie_menu_utils/pie_widget_utils/pie_painter.py similarity index 72% rename from shortcut_composer/templates/pie_menu_utils/widget_utils/pie_painter.py rename to shortcut_composer/templates/pie_menu_utils/pie_widget_utils/pie_painter.py index fe7be481..ce72bc87 100644 --- a/shortcut_composer/templates/pie_menu_utils/widget_utils/pie_painter.py +++ b/shortcut_composer/templates/pie_menu_utils/pie_widget_utils/pie_painter.py @@ -19,14 +19,12 @@ class PiePainter: painter: Painter labels: List[Label] style: PieStyle - edit_mode: bool def __post_init__(self): """Paint the widget which created the passed painter.""" self._paint_deadzone_indicator() self._paint_base_wheel() self._paint_active_pie() - self._paint_base_border() @property def _center(self) -> QPoint: @@ -59,20 +57,29 @@ def _paint_base_wheel(self) -> None: outer_radius=self.style.widget_radius, color=QColor(128, 128, 128, 1)) + # base wheel self.painter.paint_wheel( center=self._center, - outer_radius=self.style.no_border_radius, + outer_radius=self.style.pie_radius, color=self.style.background_color, - thickness=self.style.area_thickness) + thickness=self.style.area_thickness+self.style.border_thickness//2) - def _paint_base_border(self) -> None: - """Paint a border on the inner edge of base circle.""" + # base wheel border self.painter.paint_wheel( center=self._center, outer_radius=self.style.inner_edge_radius, color=self.style.border_color, thickness=self.style.border_thickness) + # base wheel decorator + self.painter.paint_wheel( + center=self._center, + outer_radius=( + self.style.inner_edge_radius + + self.style.decorator_thickness), + color=self.style.background_decorator_color, + thickness=self.style.decorator_thickness) + def _paint_active_pie(self) -> None: """Paint a pie behind a label which is active or during animation.""" for label in self.labels: @@ -83,14 +90,34 @@ def _paint_active_pie(self) -> None: 0.15 * label.activation_progress.value * self.style.area_thickness) + # active pie self.painter.paint_pie( center=self._center, - outer_radius=self.style.no_border_radius + thickness_addition, + outer_radius=self.style.pie_radius + thickness_addition, angle=label.angle, span=360//len(self.labels), color=self._pick_pie_color(label), thickness=self.style.area_thickness + thickness_addition) + # pie decorator + self.painter.paint_pie( + center=self._center, + outer_radius=self.style.pie_radius + thickness_addition, + angle=label.angle, + span=360//len(self.labels), + color=self.style.pie_decorator_color, + thickness=self.style.border_thickness*4) + + # active pie border + self.painter.paint_pie( + center=self._center, + outer_radius=self.style.pie_radius + + thickness_addition + self.style.border_thickness//2, + angle=label.angle, + span=360//len(self.labels), + color=self.style.active_color_dark, + thickness=self.style.border_thickness) + def _pick_pie_color(self, label: Label) -> QColor: """Pick color of pie based on widget mode and animation progress.""" return self._overlay_colors( diff --git a/shortcut_composer/templates/pie_menu_utils/widget_utils/widget_holder.py b/shortcut_composer/templates/pie_menu_utils/pie_widget_utils/widget_holder.py similarity index 92% rename from shortcut_composer/templates/pie_menu_utils/widget_utils/widget_holder.py rename to shortcut_composer/templates/pie_menu_utils/pie_widget_utils/widget_holder.py index 6bf779b8..1d3bda7a 100644 --- a/shortcut_composer/templates/pie_menu_utils/widget_utils/widget_holder.py +++ b/shortcut_composer/templates/pie_menu_utils/pie_widget_utils/widget_holder.py @@ -69,3 +69,8 @@ def __iter__(self) -> Iterator[LabelWidget]: def __len__(self) -> int: """Return amount of held LabelWidgets.""" return len(self._widgets) + + def clear_forced_widgets(self): + """Clear the forced colors of all held widgets. Helper method.""" + for widget in self._widgets.values(): + widget.forced = False diff --git a/shortcut_composer/templates/pie_menu_utils/settings_gui/__init__.py b/shortcut_composer/templates/pie_menu_utils/settings_gui/__init__.py deleted file mode 100644 index d6677014..00000000 --- a/shortcut_composer/templates/pie_menu_utils/settings_gui/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# SPDX-FileCopyrightText: © 2022-2023 Wojciech Trybus -# SPDX-License-Identifier: GPL-3.0-or-later - -from .pie_settings import PieSettings -from .enum_pie_settings import EnumPieSettings -from .preset_pie_settings import PresetPieSettings -from .numeric_pie_settings import NumericPieSettings - -__all__ = [ - "PieSettings", - "EnumPieSettings", - "PresetPieSettings", - "NumericPieSettings" -] diff --git a/shortcut_composer/templates/temporary_key.py b/shortcut_composer/templates/temporary_key.py index 3ab2e505..c0cdcca3 100644 --- a/shortcut_composer/templates/temporary_key.py +++ b/shortcut_composer/templates/temporary_key.py @@ -72,11 +72,11 @@ def __init__( self._was_high_before_press = False def _set_low(self) -> None: - """Defines how to switch to low state.""" + """Switch to low state.""" self._controller.set_value(self._low_value) def _set_high(self) -> None: - """Defines how to switch to high state.""" + """Switch to high state.""" self._controller.set_value(self._high_value) def _is_high_state(self) -> bool: