From 0dfdeceab293170e4d2657420bcabef3b1c47763 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Thu, 6 Jul 2023 23:18:30 -0400 Subject: [PATCH 01/39] we out here --- porcupine/actions.py | 140 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 porcupine/actions.py diff --git a/porcupine/actions.py b/porcupine/actions.py new file mode 100644 index 000000000..f1b246a05 --- /dev/null +++ b/porcupine/actions.py @@ -0,0 +1,140 @@ +from dataclasses import dataclass +from functools import partial +from pathlib import Path +from typing import Callable + +from porcupine.tabs import FileTab + +action_availability_callback = Callable[..., bool] + + +@dataclass(kw_only=True, slots=True, frozen=True) +class Action: + """Action that requires no context in the callback""" + + name: str + description: str + callback: Callable[..., None] + availability_callbacks: list[action_availability_callback] | None = None + + +filetab_action_availability_callback = Callable[[FileTab], bool] + + +@dataclass(kw_only=True, slots=True, frozen=True) +class FileTabAction: + """Action that requires a FileTab to be provided to the callback""" + + name: str + description: str + callback: Callable[[FileTab], None] + availability_callbacks: list[filetab_action_availability_callback] | None = None + + +path_action_availability_callback = Callable[[Path], bool] + + +@dataclass(kw_only=True, slots=True, frozen=True) +class PathAction: + """Action that requires a Path to be provided to the callback""" + + name: str + description: str + callback: Callable[[Path], None] + file_compatible: bool = True + directory_compatible: bool = False + availability_callbacks: list[path_action_availability_callback] | None = None + + +ActionTypes = Action | FileTabAction | PathAction + +_actions: dict[str, ActionTypes] = {} + + +def register_action( + *, + name: str, + description: str, + callback: Callable[..., None], + availability_callbacks: list[action_availability_callback] | None = None, +) -> Action: + if name in _actions: + raise ValueError(f"Action with the name '{name}' already exists") + action = Action( + name=name, + description=description, + callback=callback, + availability_callbacks=availability_callbacks, + ) + _actions[name] = action + return action + + +def register_filetab_action( + *, + name: str, + description: str, + callback: Callable[[FileTab], None], + availability_callbacks: list[filetab_action_availability_callback] | None, +) -> FileTabAction: + if name in _actions: + raise ValueError(f"Action with the name '{name}' already exists") + action = FileTabAction( + name=name, + description=description, + callback=callback, + availability_callbacks=availability_callbacks, + ) + _actions[name] = action + return action + + +def register_path_action( + *, + name: str, + description: str, + callback: Callable[[Path], None], + file_compatible: bool = True, + directory_compatible: bool = False, + availability_callbacks: list[path_action_availability_callback] | None, +) -> PathAction: + if name in _actions: + raise ValueError(f"Action with the name '{name}' already exists") + action = PathAction( + name=name, + description=description, + callback=callback, + file_compatible=file_compatible, + directory_compatible=directory_compatible, + availability_callbacks=availability_callbacks, + ) + _actions[name] = action + return action + + +def filetype_availability(filetypes: list[str]) -> Callable[[FileTab | Path], bool]: + def _filetype_availability(filetypes: list[str], context: FileTab | Path) -> bool: + if isinstance(context, FileTab): + tab = context + if tab.settings.get("filetype_name", object) in filetypes: + return True + return False + + if isinstance(context, Path): + path = context + + if not path.exists(): + raise RuntimeError(f"{path} does not exist.") + if path.is_dir(): + raise RuntimeError( + f"{path} is a directory - an action consumer registered this action incorrectly" + ) + if not path.is_file(): + raise RuntimeError(f"{path} is not a file") + + # return True if get_filetype_from_path(path) in filetypes else False + raise NotImplementedError # TODO: there is a way to do this already right? + + raise RuntimeError("wrong context passed") + + return partial(_filetype_availability, filetypes) From 77214b19410e456f843070d84e2307f1f553f18d Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Fri, 7 Jul 2023 00:36:04 -0400 Subject: [PATCH 02/39] I spent literally 3 hours trying to figure this out ChatGPT sent me on so many wild goose chases, I tried: - Protocol - singledispatch - overloads and none of them worked. ChatGPT spun tales of how Mypy is incapable of detecting callables through partials, how it can't track duck typed functions, etc etc literally for a 2 character change fml --- porcupine/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/porcupine/actions.py b/porcupine/actions.py index f1b246a05..2a2413b88 100644 --- a/porcupine/actions.py +++ b/porcupine/actions.py @@ -5,7 +5,7 @@ from porcupine.tabs import FileTab -action_availability_callback = Callable[..., bool] +action_availability_callback = Callable[[], bool] @dataclass(kw_only=True, slots=True, frozen=True) From d9b209d41a136670001f77147fc581d42848d5df Mon Sep 17 00:00:00 2001 From: Akuli Date: Fri, 7 Jul 2023 10:59:10 +0300 Subject: [PATCH 03/39] Trying to make CI happy: delete kw_only=True (new in python 3.10) --- porcupine/actions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/porcupine/actions.py b/porcupine/actions.py index 2a2413b88..facae13aa 100644 --- a/porcupine/actions.py +++ b/porcupine/actions.py @@ -8,7 +8,7 @@ action_availability_callback = Callable[[], bool] -@dataclass(kw_only=True, slots=True, frozen=True) +@dataclass(slots=True, frozen=True) class Action: """Action that requires no context in the callback""" @@ -21,7 +21,7 @@ class Action: filetab_action_availability_callback = Callable[[FileTab], bool] -@dataclass(kw_only=True, slots=True, frozen=True) +@dataclass(slots=True, frozen=True) class FileTabAction: """Action that requires a FileTab to be provided to the callback""" @@ -34,7 +34,7 @@ class FileTabAction: path_action_availability_callback = Callable[[Path], bool] -@dataclass(kw_only=True, slots=True, frozen=True) +@dataclass(slots=True, frozen=True) class PathAction: """Action that requires a Path to be provided to the callback""" From 2d80aba8c9c8138a371795944a10663bd758001a Mon Sep 17 00:00:00 2001 From: Akuli Date: Fri, 7 Jul 2023 11:00:39 +0300 Subject: [PATCH 04/39] slots=True too --- porcupine/actions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/porcupine/actions.py b/porcupine/actions.py index facae13aa..328572817 100644 --- a/porcupine/actions.py +++ b/porcupine/actions.py @@ -8,7 +8,7 @@ action_availability_callback = Callable[[], bool] -@dataclass(slots=True, frozen=True) +@dataclass(frozen=True) class Action: """Action that requires no context in the callback""" @@ -21,7 +21,7 @@ class Action: filetab_action_availability_callback = Callable[[FileTab], bool] -@dataclass(slots=True, frozen=True) +@dataclass(frozen=True) class FileTabAction: """Action that requires a FileTab to be provided to the callback""" @@ -34,7 +34,7 @@ class FileTabAction: path_action_availability_callback = Callable[[Path], bool] -@dataclass(slots=True, frozen=True) +@dataclass(frozen=True) class PathAction: """Action that requires a Path to be provided to the callback""" From e22cbef5fd245f513766ba363ca3e4798e2ecd1e Mon Sep 17 00:00:00 2001 From: Akuli Date: Fri, 7 Jul 2023 11:03:07 +0300 Subject: [PATCH 05/39] Fix typing syntax problems --- porcupine/actions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/porcupine/actions.py b/porcupine/actions.py index 328572817..f52beaab4 100644 --- a/porcupine/actions.py +++ b/porcupine/actions.py @@ -1,7 +1,9 @@ +from __future__ import annotations + from dataclasses import dataclass from functools import partial from pathlib import Path -from typing import Callable +from typing import Callable, Union from porcupine.tabs import FileTab @@ -46,7 +48,7 @@ class PathAction: availability_callbacks: list[path_action_availability_callback] | None = None -ActionTypes = Action | FileTabAction | PathAction +ActionTypes = Union[Action, FileTabAction, PathAction] _actions: dict[str, ActionTypes] = {} From 63d7a9866af51ec3f4931f1dbc833e49250bf5ba Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Fri, 7 Jul 2023 10:36:18 -0400 Subject: [PATCH 06/39] fix callback type Co-authored-by: Akuli --- porcupine/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/porcupine/actions.py b/porcupine/actions.py index f52beaab4..6d66b12b1 100644 --- a/porcupine/actions.py +++ b/porcupine/actions.py @@ -16,7 +16,7 @@ class Action: name: str description: str - callback: Callable[..., None] + callback: Callable[[], None] availability_callbacks: list[action_availability_callback] | None = None From 9c330d3e4b4a2403493b08ef09345ce371d50887 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Fri, 7 Jul 2023 17:22:07 -0400 Subject: [PATCH 07/39] incorporating feedback --- porcupine/actions.py | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/porcupine/actions.py b/porcupine/actions.py index 6d66b12b1..039cf6ebb 100644 --- a/porcupine/actions.py +++ b/porcupine/actions.py @@ -11,13 +11,13 @@ @dataclass(frozen=True) -class Action: +class BareAction: """Action that requires no context in the callback""" name: str description: str callback: Callable[[], None] - availability_callbacks: list[action_availability_callback] | None = None + availability_callback: action_availability_callback filetab_action_availability_callback = Callable[[FileTab], bool] @@ -30,7 +30,7 @@ class FileTabAction: name: str description: str callback: Callable[[FileTab], None] - availability_callbacks: list[filetab_action_availability_callback] | None = None + availability_callback: filetab_action_availability_callback path_action_availability_callback = Callable[[Path], bool] @@ -43,14 +43,12 @@ class PathAction: name: str description: str callback: Callable[[Path], None] - file_compatible: bool = True - directory_compatible: bool = False - availability_callbacks: list[path_action_availability_callback] | None = None + availability_callback: path_action_availability_callback -ActionTypes = Union[Action, FileTabAction, PathAction] +Action = Union[BareAction, FileTabAction, PathAction] -_actions: dict[str, ActionTypes] = {} +_actions: dict[str, Action] = {} def register_action( @@ -58,15 +56,15 @@ def register_action( name: str, description: str, callback: Callable[..., None], - availability_callbacks: list[action_availability_callback] | None = None, -) -> Action: + availability_callback: action_availability_callback = lambda: True, +) -> BareAction: if name in _actions: raise ValueError(f"Action with the name '{name}' already exists") - action = Action( + action = BareAction( name=name, description=description, callback=callback, - availability_callbacks=availability_callbacks, + availability_callback=availability_callback, ) _actions[name] = action return action @@ -77,7 +75,7 @@ def register_filetab_action( name: str, description: str, callback: Callable[[FileTab], None], - availability_callbacks: list[filetab_action_availability_callback] | None, + availability_callback: filetab_action_availability_callback = lambda tab: True, ) -> FileTabAction: if name in _actions: raise ValueError(f"Action with the name '{name}' already exists") @@ -85,7 +83,7 @@ def register_filetab_action( name=name, description=description, callback=callback, - availability_callbacks=availability_callbacks, + availability_callback=availability_callback, ) _actions[name] = action return action @@ -96,9 +94,7 @@ def register_path_action( name: str, description: str, callback: Callable[[Path], None], - file_compatible: bool = True, - directory_compatible: bool = False, - availability_callbacks: list[path_action_availability_callback] | None, + availability_callback: path_action_availability_callback = lambda path: True, ) -> PathAction: if name in _actions: raise ValueError(f"Action with the name '{name}' already exists") @@ -106,9 +102,7 @@ def register_path_action( name=name, description=description, callback=callback, - file_compatible=file_compatible, - directory_compatible=directory_compatible, - availability_callbacks=availability_callbacks, + availability_callback=availability_callback, ) _actions[name] = action return action From 499e20cf5b2063bd1617e84c095b2824abf8dc40 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Fri, 7 Jul 2023 17:31:54 -0400 Subject: [PATCH 08/39] finishing touches --- porcupine/actions.py | 32 ++++++-------------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/porcupine/actions.py b/porcupine/actions.py index 039cf6ebb..9c2f87073 100644 --- a/porcupine/actions.py +++ b/porcupine/actions.py @@ -108,29 +108,9 @@ def register_path_action( return action -def filetype_availability(filetypes: list[str]) -> Callable[[FileTab | Path], bool]: - def _filetype_availability(filetypes: list[str], context: FileTab | Path) -> bool: - if isinstance(context, FileTab): - tab = context - if tab.settings.get("filetype_name", object) in filetypes: - return True - return False - - if isinstance(context, Path): - path = context - - if not path.exists(): - raise RuntimeError(f"{path} does not exist.") - if path.is_dir(): - raise RuntimeError( - f"{path} is a directory - an action consumer registered this action incorrectly" - ) - if not path.is_file(): - raise RuntimeError(f"{path} is not a file") - - # return True if get_filetype_from_path(path) in filetypes else False - raise NotImplementedError # TODO: there is a way to do this already right? - - raise RuntimeError("wrong context passed") - - return partial(_filetype_availability, filetypes) +def query_actions(name: str) -> Action | None: + return _actions.get(name) + + +def get_all_actions() -> dict[str, Action]: + return _actions.copy() From 1173674de26f6f92f3ff6d0e96cd482ccd05ef78 Mon Sep 17 00:00:00 2001 From: benjamin-kirkbride Date: Fri, 7 Jul 2023 21:32:39 +0000 Subject: [PATCH 09/39] Run pycln, black and isort --- porcupine/actions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/porcupine/actions.py b/porcupine/actions.py index 9c2f87073..229eef053 100644 --- a/porcupine/actions.py +++ b/porcupine/actions.py @@ -1,7 +1,6 @@ from __future__ import annotations from dataclasses import dataclass -from functools import partial from pathlib import Path from typing import Callable, Union From 543cdb5f8fa46ea13e5c4324729cad51fb6b26fd Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Fri, 7 Jul 2023 17:51:13 -0400 Subject: [PATCH 10/39] Update porcupine/actions.py Co-authored-by: Akuli --- porcupine/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/porcupine/actions.py b/porcupine/actions.py index 229eef053..a8d7af939 100644 --- a/porcupine/actions.py +++ b/porcupine/actions.py @@ -50,7 +50,7 @@ class PathAction: _actions: dict[str, Action] = {} -def register_action( +def register_bare_action( *, name: str, description: str, From 0ffe9cf8e57c1e346792b6ba5987627bb9d7fa37 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Fri, 7 Jul 2023 18:11:04 -0400 Subject: [PATCH 11/39] extremely useful test --- tests/test_actions.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/test_actions.py diff --git a/tests/test_actions.py b/tests/test_actions.py new file mode 100644 index 000000000..0cf718537 --- /dev/null +++ b/tests/test_actions.py @@ -0,0 +1,24 @@ +from porcupine import actions + + +def test_action_registry(): + bare_action = actions.register_bare_action( + name="bare action", description="", callback=lambda: None + ) + filetab_action = actions.register_filetab_action( + name="filetab action", description="", callback=lambda tab: None + ) + path_action = actions.register_path_action( + name="path action", description="", callback=lambda path: None + ) + + assert isinstance(bare_action, actions.BareAction) + assert isinstance(filetab_action, actions.FileTabAction) + assert isinstance(path_action, actions.PathAction) + + all_actions = actions.get_all_actions() + for action in [bare_action, filetab_action, path_action]: + assert actions.query_actions(action.name) == action + assert action in all_actions.values() + + assert actions.query_actions("nonexistent action") is None From 824ec0c95cb39d224df8806d1f5325dbfacc652a Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Fri, 7 Jul 2023 18:13:41 -0400 Subject: [PATCH 12/39] moderate improvement to useful test --- tests/test_actions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_actions.py b/tests/test_actions.py index 0cf718537..711e8440d 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -22,3 +22,8 @@ def test_action_registry(): assert action in all_actions.values() assert actions.query_actions("nonexistent action") is None + + all_actions["garbage"] = "mean lean fighting machine" # type: ignore + assert ( + actions.query_actions("garbage") is None + ), "`all_actions` should be a copy, changes to it should not effect `_actions`" From 253fdcaf2660632cbf220b3c608eb527464f4f2e Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Fri, 7 Jul 2023 18:15:33 -0400 Subject: [PATCH 13/39] remove availability_callback types --- porcupine/actions.py | 20 ++++++-------------- pyproject.toml | 3 +++ 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/porcupine/actions.py b/porcupine/actions.py index a8d7af939..4982a6378 100644 --- a/porcupine/actions.py +++ b/porcupine/actions.py @@ -6,8 +6,6 @@ from porcupine.tabs import FileTab -action_availability_callback = Callable[[], bool] - @dataclass(frozen=True) class BareAction: @@ -16,10 +14,7 @@ class BareAction: name: str description: str callback: Callable[[], None] - availability_callback: action_availability_callback - - -filetab_action_availability_callback = Callable[[FileTab], bool] + availability_callback: Callable[[], bool] @dataclass(frozen=True) @@ -29,10 +24,7 @@ class FileTabAction: name: str description: str callback: Callable[[FileTab], None] - availability_callback: filetab_action_availability_callback - - -path_action_availability_callback = Callable[[Path], bool] + availability_callback: Callable[[FileTab], bool] @dataclass(frozen=True) @@ -42,7 +34,7 @@ class PathAction: name: str description: str callback: Callable[[Path], None] - availability_callback: path_action_availability_callback + availability_callback: Callable[[Path], bool] Action = Union[BareAction, FileTabAction, PathAction] @@ -55,7 +47,7 @@ def register_bare_action( name: str, description: str, callback: Callable[..., None], - availability_callback: action_availability_callback = lambda: True, + availability_callback: Callable[[], bool] = lambda: True, ) -> BareAction: if name in _actions: raise ValueError(f"Action with the name '{name}' already exists") @@ -74,7 +66,7 @@ def register_filetab_action( name: str, description: str, callback: Callable[[FileTab], None], - availability_callback: filetab_action_availability_callback = lambda tab: True, + availability_callback: Callable[[FileTab], bool] = lambda tab: True, ) -> FileTabAction: if name in _actions: raise ValueError(f"Action with the name '{name}' already exists") @@ -93,7 +85,7 @@ def register_path_action( name: str, description: str, callback: Callable[[Path], None], - availability_callback: path_action_availability_callback = lambda path: True, + availability_callback: Callable[[Path], bool] = lambda path: True, ) -> PathAction: if name in _actions: raise ValueError(f"Action with the name '{name}' already exists") diff --git a/pyproject.toml b/pyproject.toml index b84a52875..08760eaa0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,6 @@ +[tool.ruff] +ignore = ["E501"] + [tool.black] line-length = 100 skip-magic-trailing-comma = true From 6c55c801fc84c176fbd50da27f181ad150da412a Mon Sep 17 00:00:00 2001 From: Akuli Date: Sat, 8 Jul 2023 01:51:58 +0300 Subject: [PATCH 14/39] Delete duplicated ruff config --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 473555a3b..08760eaa0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,9 +10,6 @@ line_length = 100 profile = "black" multi_line_output = 3 -[tool.ruff] -ignore = ["E501"] - # Flit configuration ([build-system] and [project]) are used when pip installing with github url. # See commands in README. [build-system] From db47f32ecf278a034a7a942d6ede8f6a534af762 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Fri, 7 Jul 2023 18:49:33 -0400 Subject: [PATCH 15/39] convert python_tools to actions --- porcupine/menubar.py | 27 ++++++++++++++++++++++++++- porcupine/plugins/python_tools.py | 21 ++++++++++++++++++--- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/porcupine/menubar.py b/porcupine/menubar.py index e876caaa8..78b4e97a2 100644 --- a/porcupine/menubar.py +++ b/porcupine/menubar.py @@ -1,5 +1,5 @@ from __future__ import annotations - +from porcupine import actions import logging import re import sys @@ -328,6 +328,31 @@ def setup() -> None: set_enabled_based_on_tab(path, (lambda tab: isinstance(tab, tabs.FileTab))) +def add_filetab_action(path: str, action: actions.FileTabAction, **kwargs: Any) -> None: + """ + This is a convenience function that does several things: + + * Create a menu item at the given path. + * Ensure the menu item is enabled only when the selected tab is a + :class:`~porcupine.tabs.FileTab` AND when + :class:`~porcupine.actions.FileTabAction.availability_callback` + evaluates to True. + * Run :class:`~porcupine.actions.FileTabAction.callback` when the + menu item is clicked. + + The ``callback`` is called with the selected tab as the only + argument when the menu item is clicked. + + You usually don't need to provide any keyword arguments in ``**kwargs``, + but if you do, they are passed to :meth:`tkinter.Menu.add_command`. + """ + menu_path, item_text = _split_parent(path) + get_menu(menu_path).add_command( + label=item_text, command=lambda: action.callback(get_filetab()), **kwargs + ) + set_enabled_based_on_tab(path, action.availability_callback) + + # TODO: pluginify? def _fill_menus_with_default_stuff() -> None: # Make sure to get the order of menus right: diff --git a/porcupine/plugins/python_tools.py b/porcupine/plugins/python_tools.py index 3c62bd8b8..ab124f12d 100644 --- a/porcupine/plugins/python_tools.py +++ b/porcupine/plugins/python_tools.py @@ -13,7 +13,7 @@ from pathlib import Path from tkinter import messagebox -from porcupine import menubar, tabs, textutils, utils +from porcupine import menubar, tabs, textutils, utils, actions from porcupine.plugins import python_venv log = logging.getLogger(__name__) @@ -63,6 +63,21 @@ def format_code_in_textwidget(tool: str, tab: tabs.FileTab) -> None: tab.textwidget.replace("1.0", "end - 1 char", after) +black_format_tab = actions.register_filetab_action( + name="Black Format Tab", + description="Autoformat open tab using Black", + callback=partial(format_code_in_textwidget, "black"), + availability_callback=lambda tab: isinstance(tab, tabs.FileTab), +) + +isort_format_tab = actions.register_filetab_action( + name="isort Format Tab", + description="Sort Imports of open tab with isort", + callback=partial(format_code_in_textwidget, "isort"), + availability_callback=lambda tab: isinstance(tab, tabs.FileTab), +) + + def setup() -> None: - menubar.add_filetab_command("Tools/Python/Black", partial(format_code_in_textwidget, "black")) - menubar.add_filetab_command("Tools/Python/Isort", partial(format_code_in_textwidget, "isort")) + menubar.add_filetab_action("Tools/Python/Black", black_format_tab) + menubar.add_filetab_action("Tools/Python/Isort", isort_format_tab) From 199ab5f7395dd155226f209982e26bdff08dc4f3 Mon Sep 17 00:00:00 2001 From: Akuli Date: Fri, 7 Jul 2023 22:50:13 +0000 Subject: [PATCH 16/39] Run pycln, black and isort --- porcupine/menubar.py | 4 +++- porcupine/plugins/python_tools.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/porcupine/menubar.py b/porcupine/menubar.py index 78b4e97a2..019d4132f 100644 --- a/porcupine/menubar.py +++ b/porcupine/menubar.py @@ -1,5 +1,5 @@ from __future__ import annotations -from porcupine import actions + import logging import re import sys @@ -11,6 +11,8 @@ from tkinter import filedialog from typing import Any, Callable, Iterator +from porcupine import actions + if sys.version_info >= (3, 8): from typing import Literal else: diff --git a/porcupine/plugins/python_tools.py b/porcupine/plugins/python_tools.py index ab124f12d..56f702a22 100644 --- a/porcupine/plugins/python_tools.py +++ b/porcupine/plugins/python_tools.py @@ -13,7 +13,7 @@ from pathlib import Path from tkinter import messagebox -from porcupine import menubar, tabs, textutils, utils, actions +from porcupine import actions, menubar, tabs, textutils, utils from porcupine.plugins import python_venv log = logging.getLogger(__name__) From 6cd954a702944247afd727eae822cfbc74ececc1 Mon Sep 17 00:00:00 2001 From: Akuli Date: Sat, 8 Jul 2023 01:53:05 +0300 Subject: [PATCH 17/39] moving the ruff around --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 08760eaa0..330b2e91f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,3 @@ -[tool.ruff] -ignore = ["E501"] - [tool.black] line-length = 100 skip-magic-trailing-comma = true @@ -10,6 +7,9 @@ line_length = 100 profile = "black" multi_line_output = 3 +[tool.ruff] +ignore = ["E501"] + # Flit configuration ([build-system] and [project]) are used when pip installing with github url. # See commands in README. [build-system] From 0e77816bb82f226266ddcb3ca6211ebb40d3f7d5 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Fri, 7 Jul 2023 19:22:09 -0400 Subject: [PATCH 18/39] optionally get labels from action.name --- porcupine/menubar.py | 15 +++++++++++++-- porcupine/plugins/python_tools.py | 4 ++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/porcupine/menubar.py b/porcupine/menubar.py index 019d4132f..1daaf0932 100644 --- a/porcupine/menubar.py +++ b/porcupine/menubar.py @@ -334,7 +334,8 @@ def add_filetab_action(path: str, action: actions.FileTabAction, **kwargs: Any) """ This is a convenience function that does several things: - * Create a menu item at the given path. + * Create a menu item at the given path. If the path has a trailing + / the menu item is obtained from the action object. * Ensure the menu item is enabled only when the selected tab is a :class:`~porcupine.tabs.FileTab` AND when :class:`~porcupine.actions.FileTabAction.availability_callback` @@ -348,7 +349,17 @@ def add_filetab_action(path: str, action: actions.FileTabAction, **kwargs: Any) You usually don't need to provide any keyword arguments in ``**kwargs``, but if you do, they are passed to :meth:`tkinter.Menu.add_command`. """ - menu_path, item_text = _split_parent(path) + if path[-1] == "/": + # we get the label from the action.name + menu_path = path[:-1] + item_text = action.name + + path = f"{menu_path}/{item_text}" + + else: + # a full path has been passed, including the label + menu_path, item_text = _split_parent(path) + get_menu(menu_path).add_command( label=item_text, command=lambda: action.callback(get_filetab()), **kwargs ) diff --git a/porcupine/plugins/python_tools.py b/porcupine/plugins/python_tools.py index 56f702a22..24adb04bd 100644 --- a/porcupine/plugins/python_tools.py +++ b/porcupine/plugins/python_tools.py @@ -79,5 +79,5 @@ def format_code_in_textwidget(tool: str, tab: tabs.FileTab) -> None: def setup() -> None: - menubar.add_filetab_action("Tools/Python/Black", black_format_tab) - menubar.add_filetab_action("Tools/Python/Isort", isort_format_tab) + menubar.add_filetab_action("Tools/Python/", black_format_tab) + menubar.add_filetab_action("Tools/Python/", isort_format_tab) From 4f867b1d64a335296166468885012086b6db4a7a Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Fri, 7 Jul 2023 19:29:16 -0400 Subject: [PATCH 19/39] make update_enabledness always provide a true tab --- porcupine/menubar.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/porcupine/menubar.py b/porcupine/menubar.py index 1daaf0932..fbe70cc47 100644 --- a/porcupine/menubar.py +++ b/porcupine/menubar.py @@ -249,7 +249,7 @@ def update_keyboard_shortcuts() -> None: def set_enabled_based_on_tab( - path: str, callback: Callable[[tabs.Tab | None], bool] + path: str, callback: Callable[[tabs.Tab], bool] ) -> Callable[..., None]: """Use this for disabling menu items depending on the currently selected tab. @@ -284,11 +284,15 @@ def setup(): def update_enabledness(*junk: object) -> None: tab = get_tab_manager().select() + parent, child = _split_parent(path) menu = get_menu(parent) index = _find_item(menu, child) if index is None: raise LookupError(f"menu item {path!r} not found") + if not tab: + menu.entryconfig(index, state="disabled") + return menu.entryconfig(index, state=("normal" if callback(tab) else "disabled")) update_enabledness() From f213ed33ed73c790736b2bf1f40b25a7e2fac9b9 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Fri, 7 Jul 2023 20:45:41 -0400 Subject: [PATCH 20/39] last nit --- porcupine/menubar.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/porcupine/menubar.py b/porcupine/menubar.py index fbe70cc47..6cdd99f8e 100644 --- a/porcupine/menubar.py +++ b/porcupine/menubar.py @@ -353,19 +353,9 @@ def add_filetab_action(path: str, action: actions.FileTabAction, **kwargs: Any) You usually don't need to provide any keyword arguments in ``**kwargs``, but if you do, they are passed to :meth:`tkinter.Menu.add_command`. """ - if path[-1] == "/": - # we get the label from the action.name - menu_path = path[:-1] - item_text = action.name - path = f"{menu_path}/{item_text}" - - else: - # a full path has been passed, including the label - menu_path, item_text = _split_parent(path) - - get_menu(menu_path).add_command( - label=item_text, command=lambda: action.callback(get_filetab()), **kwargs + get_menu(path).add_command( + label=action.name, command=lambda: action.callback(get_filetab()), **kwargs ) set_enabled_based_on_tab(path, action.availability_callback) From 23f3ec9ae17b7db6117f7e9464a11db9f0160065 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Fri, 7 Jul 2023 20:46:17 -0400 Subject: [PATCH 21/39] disable on non-filetab tab --- porcupine/menubar.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/porcupine/menubar.py b/porcupine/menubar.py index 6cdd99f8e..087f6157c 100644 --- a/porcupine/menubar.py +++ b/porcupine/menubar.py @@ -249,7 +249,7 @@ def update_keyboard_shortcuts() -> None: def set_enabled_based_on_tab( - path: str, callback: Callable[[tabs.Tab], bool] + path: str, callback: Callable[[tabs.FileTab], bool] ) -> Callable[..., None]: """Use this for disabling menu items depending on the currently selected tab. @@ -293,6 +293,11 @@ def update_enabledness(*junk: object) -> None: if not tab: menu.entryconfig(index, state="disabled") return + + if not isinstance(tab, tabs.FileTab): + menu.entryconfig(index, state="disabled") + return + menu.entryconfig(index, state=("normal" if callback(tab) else "disabled")) update_enabledness() From 123b0dc6ceb426acc3e09f4fe886fb1e9e4721ee Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Fri, 7 Jul 2023 21:13:14 -0400 Subject: [PATCH 22/39] rework non-filetab checking also fixed some docs --- porcupine/menubar.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/porcupine/menubar.py b/porcupine/menubar.py index 087f6157c..493804a78 100644 --- a/porcupine/menubar.py +++ b/porcupine/menubar.py @@ -249,7 +249,7 @@ def update_keyboard_shortcuts() -> None: def set_enabled_based_on_tab( - path: str, callback: Callable[[tabs.FileTab], bool] + path: str, callback: Callable[[tabs.FileTab], bool], filetab_only: bool = False ) -> Callable[..., None]: """Use this for disabling menu items depending on the currently selected tab. @@ -294,10 +294,6 @@ def update_enabledness(*junk: object) -> None: menu.entryconfig(index, state="disabled") return - if not isinstance(tab, tabs.FileTab): - menu.entryconfig(index, state="disabled") - return - menu.entryconfig(index, state=("normal" if callback(tab) else "disabled")) update_enabledness() @@ -343,8 +339,7 @@ def add_filetab_action(path: str, action: actions.FileTabAction, **kwargs: Any) """ This is a convenience function that does several things: - * Create a menu item at the given path. If the path has a trailing - / the menu item is obtained from the action object. + * Create a menu item at the given path with action.name as label * Ensure the menu item is enabled only when the selected tab is a :class:`~porcupine.tabs.FileTab` AND when :class:`~porcupine.actions.FileTabAction.availability_callback` @@ -362,7 +357,11 @@ def add_filetab_action(path: str, action: actions.FileTabAction, **kwargs: Any) get_menu(path).add_command( label=action.name, command=lambda: action.callback(get_filetab()), **kwargs ) - set_enabled_based_on_tab(path, action.availability_callback) + set_enabled_based_on_tab( + path, + callback=lambda tab: (lambda tab: isinstance(tab, tabs.FileTab))(tab) + and action.availability_callback(tab), + ) # TODO: pluginify? From 6c7ca1456c8943079d1f04bb13a36b73937ae4b5 Mon Sep 17 00:00:00 2001 From: Akuli Date: Sat, 8 Jul 2023 11:39:33 +0300 Subject: [PATCH 23/39] Uncurse porcupine/menubar.py --- porcupine/menubar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/porcupine/menubar.py b/porcupine/menubar.py index 493804a78..2b5b0c5a2 100644 --- a/porcupine/menubar.py +++ b/porcupine/menubar.py @@ -359,7 +359,7 @@ def add_filetab_action(path: str, action: actions.FileTabAction, **kwargs: Any) ) set_enabled_based_on_tab( path, - callback=lambda tab: (lambda tab: isinstance(tab, tabs.FileTab))(tab) + callback=lambda tab: isinstance(tab, tabs.FileTab) and action.availability_callback(tab), ) From 9319b6bc7dbd2fc1802916044d3fde6b50b3972a Mon Sep 17 00:00:00 2001 From: Akuli Date: Sat, 8 Jul 2023 08:40:14 +0000 Subject: [PATCH 24/39] Run pycln, black and isort --- porcupine/menubar.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/porcupine/menubar.py b/porcupine/menubar.py index 2b5b0c5a2..4c3a6bb01 100644 --- a/porcupine/menubar.py +++ b/porcupine/menubar.py @@ -359,8 +359,7 @@ def add_filetab_action(path: str, action: actions.FileTabAction, **kwargs: Any) ) set_enabled_based_on_tab( path, - callback=lambda tab: isinstance(tab, tabs.FileTab) - and action.availability_callback(tab), + callback=lambda tab: isinstance(tab, tabs.FileTab) and action.availability_callback(tab), ) From 63afbdd777c5be3be3dea646ae79fd92ebae7aa4 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 10 Jul 2023 17:58:45 -0400 Subject: [PATCH 25/39] s/evaluates to/returns Co-authored-by: Akuli --- porcupine/menubar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/porcupine/menubar.py b/porcupine/menubar.py index 4c3a6bb01..1a788e9b8 100644 --- a/porcupine/menubar.py +++ b/porcupine/menubar.py @@ -343,7 +343,7 @@ def add_filetab_action(path: str, action: actions.FileTabAction, **kwargs: Any) * Ensure the menu item is enabled only when the selected tab is a :class:`~porcupine.tabs.FileTab` AND when :class:`~porcupine.actions.FileTabAction.availability_callback` - evaluates to True. + returns True. * Run :class:`~porcupine.actions.FileTabAction.callback` when the menu item is clicked. From 72ee7dd61027ef32deb1e49abd04ab6ac2dee857 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 10 Jul 2023 17:59:11 -0400 Subject: [PATCH 26/39] Update porcupine/plugins/python_tools.py Co-authored-by: Akuli --- porcupine/plugins/python_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/porcupine/plugins/python_tools.py b/porcupine/plugins/python_tools.py index 24adb04bd..04ecfd2d6 100644 --- a/porcupine/plugins/python_tools.py +++ b/porcupine/plugins/python_tools.py @@ -67,7 +67,7 @@ def format_code_in_textwidget(tool: str, tab: tabs.FileTab) -> None: name="Black Format Tab", description="Autoformat open tab using Black", callback=partial(format_code_in_textwidget, "black"), - availability_callback=lambda tab: isinstance(tab, tabs.FileTab), + availability_callback=lambda tab: True, ) isort_format_tab = actions.register_filetab_action( From 1edd389626939c72a30a3b190914e50321d8a908 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 10 Jul 2023 18:00:49 -0400 Subject: [PATCH 27/39] Update porcupine/plugins/python_tools.py Co-authored-by: Akuli --- porcupine/plugins/python_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/porcupine/plugins/python_tools.py b/porcupine/plugins/python_tools.py index 04ecfd2d6..71faee74b 100644 --- a/porcupine/plugins/python_tools.py +++ b/porcupine/plugins/python_tools.py @@ -74,7 +74,7 @@ def format_code_in_textwidget(tool: str, tab: tabs.FileTab) -> None: name="isort Format Tab", description="Sort Imports of open tab with isort", callback=partial(format_code_in_textwidget, "isort"), - availability_callback=lambda tab: isinstance(tab, tabs.FileTab), + availability_callback=lambda tab: True, ) From 0946690b8fae5a6caf2b66fbe42bba507b7ed4bb Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 10 Jul 2023 18:09:51 -0400 Subject: [PATCH 28/39] move action definitions to setup --- porcupine/plugins/python_tools.py | 33 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/porcupine/plugins/python_tools.py b/porcupine/plugins/python_tools.py index 71faee74b..89c777c37 100644 --- a/porcupine/plugins/python_tools.py +++ b/porcupine/plugins/python_tools.py @@ -63,21 +63,20 @@ def format_code_in_textwidget(tool: str, tab: tabs.FileTab) -> None: tab.textwidget.replace("1.0", "end - 1 char", after) -black_format_tab = actions.register_filetab_action( - name="Black Format Tab", - description="Autoformat open tab using Black", - callback=partial(format_code_in_textwidget, "black"), - availability_callback=lambda tab: True, -) - -isort_format_tab = actions.register_filetab_action( - name="isort Format Tab", - description="Sort Imports of open tab with isort", - callback=partial(format_code_in_textwidget, "isort"), - availability_callback=lambda tab: True, -) - - def setup() -> None: - menubar.add_filetab_action("Tools/Python/", black_format_tab) - menubar.add_filetab_action("Tools/Python/", isort_format_tab) + black_format_tab_action = actions.register_filetab_action( + name="Black Format Tab", + description="Autoformat open tab using Black", + callback=partial(format_code_in_textwidget, "black"), + availability_callback=lambda tab: True, + ) + + isort_format_tab_action = actions.register_filetab_action( + name="isort Format Tab", + description="Sort Imports of open tab with isort", + callback=partial(format_code_in_textwidget, "isort"), + availability_callback=lambda tab: True, + ) + + menubar.add_filetab_action("Tools/Python/", black_format_tab_action) + menubar.add_filetab_action("Tools/Python/", isort_format_tab_action) From 82532ab5b2ba10d50d8336e615c9d247d7c03def Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 10 Jul 2023 18:26:25 -0400 Subject: [PATCH 29/39] fix annotation --- porcupine/menubar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/porcupine/menubar.py b/porcupine/menubar.py index 1a788e9b8..63de26baa 100644 --- a/porcupine/menubar.py +++ b/porcupine/menubar.py @@ -249,7 +249,7 @@ def update_keyboard_shortcuts() -> None: def set_enabled_based_on_tab( - path: str, callback: Callable[[tabs.FileTab], bool], filetab_only: bool = False + path: str, callback: Callable[[tabs.Tab], bool], filetab_only: bool = False ) -> Callable[..., None]: """Use this for disabling menu items depending on the currently selected tab. From 157a1f35cab97ca1f488a7bbc04653dd2dbf6885 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 10 Jul 2023 22:42:03 -0400 Subject: [PATCH 30/39] add `filetype_is` availability helper part of #1357 --- porcupine/actions.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/porcupine/actions.py b/porcupine/actions.py index 4982a6378..156c5c4b4 100644 --- a/porcupine/actions.py +++ b/porcupine/actions.py @@ -1,6 +1,7 @@ from __future__ import annotations from dataclasses import dataclass +from functools import partial from pathlib import Path from typing import Callable, Union @@ -105,3 +106,22 @@ def query_actions(name: str) -> Action | None: def get_all_actions() -> dict[str, Action]: return _actions.copy() + + +# Availability Helpers + + +def filetype_is(filetypes: Union[list[str], str]) -> Callable[[FileTab], bool]: + def _filetype_is(filetypes: list[str], tab: FileTab) -> bool: + try: + filetype = tab.settings.get("filetype_name", object) + except KeyError: + # don't ask me why a `get` method can raise a KeyError :p + return False + + return filetype in filetypes + + if isinstance(filetypes, str): + filetypes = [filetypes] + + return partial(_filetype_is, filetypes) From 0b5f0a9a17767aa9a8c97ca5984466666472633c Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 10 Jul 2023 22:42:50 -0400 Subject: [PATCH 31/39] fix #1363 --- porcupine/menubar.py | 2 ++ porcupine/plugins/filetypes.py | 2 ++ porcupine/plugins/python_tools.py | 8 +++--- tests/test_menubar.py | 47 ++++++++++++++++++++++++++++++- 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/porcupine/menubar.py b/porcupine/menubar.py index 63de26baa..24f3e70c3 100644 --- a/porcupine/menubar.py +++ b/porcupine/menubar.py @@ -297,7 +297,9 @@ def update_enabledness(*junk: object) -> None: menu.entryconfig(index, state=("normal" if callback(tab) else "disabled")) update_enabledness() + get_tab_manager().bind("<>", update_enabledness, add=True) get_tab_manager().bind("<>", update_enabledness, add=True) + return update_enabledness diff --git a/porcupine/plugins/filetypes.py b/porcupine/plugins/filetypes.py index 8496b74d7..d928cd895 100644 --- a/porcupine/plugins/filetypes.py +++ b/porcupine/plugins/filetypes.py @@ -224,6 +224,8 @@ def apply_filetype_to_tab(filetype: FileType, tab: tabs.FileTab) -> None: if name not in {"filename_patterns", "shebang_regex"}: tab.settings.set(name, value, from_config=True, tag="from_filetype") + get_tab_manager().event_generate("<>") + def on_path_changed(tab: tabs.FileTab, junk: object = None) -> None: log.info(f"file path changed: {tab.path}") diff --git a/porcupine/plugins/python_tools.py b/porcupine/plugins/python_tools.py index 89c777c37..481aa6326 100644 --- a/porcupine/plugins/python_tools.py +++ b/porcupine/plugins/python_tools.py @@ -68,15 +68,15 @@ def setup() -> None: name="Black Format Tab", description="Autoformat open tab using Black", callback=partial(format_code_in_textwidget, "black"), - availability_callback=lambda tab: True, + availability_callback=actions.filetype_is("Python"), ) isort_format_tab_action = actions.register_filetab_action( name="isort Format Tab", description="Sort Imports of open tab with isort", callback=partial(format_code_in_textwidget, "isort"), - availability_callback=lambda tab: True, + availability_callback=actions.filetype_is("Python"), ) - menubar.add_filetab_action("Tools/Python/", black_format_tab_action) - menubar.add_filetab_action("Tools/Python/", isort_format_tab_action) + menubar.add_filetab_action("Tools/Python", black_format_tab_action) + menubar.add_filetab_action("Tools/Python", isort_format_tab_action) diff --git a/tests/test_menubar.py b/tests/test_menubar.py index 0886c3a1f..9b548572a 100644 --- a/tests/test_menubar.py +++ b/tests/test_menubar.py @@ -3,7 +3,7 @@ import pytest -from porcupine import get_main_window, menubar, tabs +from porcupine import actions, get_main_window, menubar, tabs def test_virtual_events_calling_menu_callbacks(): @@ -89,3 +89,48 @@ def test_alt_f4_bug_without_filetab(mocker): mock_quit = mocker.patch("porcupine.menubar.quit") get_main_window().event_generate("") mock_quit.assert_called_once_with() + + +def test_add_filetab_action(filetab, tmp_path): + def _callback(tab): + filetab.save_as(tmp_path / "asdf.md") + tab.update() + + # TODO: https://github.com/Akuli/porcupine/issues/1364 + assert filetab.settings.get("filetype_name", object) == "Python" + + # create action + action = actions.register_filetab_action( + name="python", + description="test python action", + callback=_callback, + availability_callback=actions.filetype_is("Python"), + ) + + path = "testy_test/python" + + # check that no item exists at path + menu_item = menubar._find_item( + menubar.get_menu(menubar._split_parent(path)[0]), menubar._split_parent(path)[1] + ) + assert menu_item is None + + # register action to path + menubar.add_filetab_action(path=path, action=action) + + # check path item exists + menu = menubar.get_menu(menubar._split_parent(path)[0]) + menu_item = menubar._find_item(menu, menubar._split_parent(path)[1]) + assert menu_item is not None + + # check path item available + assert menu.entrycget(index=menu_item, option="state") == "normal" + + # activate item + action.callback(filetab) + + # verify something happened + assert filetab.settings.get("filetype_name", object) == "Markdown" + + # check unavailable (because Markdown != Python) + assert menu.entrycget(index=menu_item, option="state") == "disabled" From 8128316264d6053ec3374b5cae90562fd92945ca Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Tue, 11 Jul 2023 23:29:43 -0400 Subject: [PATCH 32/39] menubar has no idea about filetypes by job he's done it --- porcupine/menubar.py | 26 ++++++++++++++++++++------ porcupine/plugins/filetypes.py | 2 ++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/porcupine/menubar.py b/porcupine/menubar.py index 223056225..812ff5c55 100644 --- a/porcupine/menubar.py +++ b/porcupine/menubar.py @@ -248,9 +248,23 @@ def update_keyboard_shortcuts() -> None: _update_shortcuts_for_opening_submenus() +_menu_item_enabledness_callbacks: list[Callable[..., None]] = [] + + +def _refresh_menu_item_enabledness(*junk: object) -> None: + for callback in _menu_item_enabledness_callbacks: + callback(*junk) + + +# TODO: create type for events +def register_enabledness_check_event(event: str) -> None: + """Register an event which will cause all menu items to check if they are available""" + get_tab_manager().bind(event, _refresh_menu_item_enabledness, add=True) + + def set_enabled_based_on_tab( path: str, callback: Callable[[tabs.Tab], bool], filetab_only: bool = False -) -> Callable[..., None]: +) -> None: """Use this for disabling menu items depending on the currently selected tab. When the selected :class:`~porcupine.tabs.Tab` changes, ``callback`` will @@ -282,7 +296,7 @@ def setup(): easier. """ - def update_enabledness(*junk: object) -> None: + def update_enabledness(*junk: object, path: str) -> None: tab = get_tab_manager().select() parent, child = _split_parent(path) @@ -296,11 +310,9 @@ def update_enabledness(*junk: object) -> None: menu.entryconfig(index, state=("normal" if callback(tab) else "disabled")) - update_enabledness() - get_tab_manager().bind("<>", update_enabledness, add=True) - get_tab_manager().bind("<>", update_enabledness, add=True) + update_enabledness(path=path) - return update_enabledness + _menu_item_enabledness_callbacks.append(partial(update_enabledness, path=path)) def get_filetab() -> tabs.FileTab: @@ -367,6 +379,8 @@ def add_filetab_action(path: str, action: actions.FileTabAction, **kwargs: Any) # TODO: pluginify? def _fill_menus_with_default_stuff() -> None: + register_enabledness_check_event("<>") + # Make sure to get the order of menus right: # File, Edit, , Help get_menu("Help") # handled specially in get_menu diff --git a/porcupine/plugins/filetypes.py b/porcupine/plugins/filetypes.py index 788222708..55c5a6320 100644 --- a/porcupine/plugins/filetypes.py +++ b/porcupine/plugins/filetypes.py @@ -252,6 +252,8 @@ def _add_filetype_menuitem(name: str, tk_var: tkinter.StringVar) -> None: def setup() -> None: + menubar.register_enabledness_check_event("<>") + global_settings.add_option("default_filetype", "Python") # load_filetypes() got already called in setup_argument_parser() From 2efc3eef7b183e3ace80e92fe167b9d4b9f4906e Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 17 Jul 2023 15:16:39 -0400 Subject: [PATCH 33/39] Update porcupine/menubar.py Co-authored-by: Akuli --- porcupine/menubar.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/porcupine/menubar.py b/porcupine/menubar.py index 812ff5c55..20e52ca4a 100644 --- a/porcupine/menubar.py +++ b/porcupine/menubar.py @@ -304,11 +304,10 @@ def update_enabledness(*junk: object, path: str) -> None: index = _find_item(menu, child) if index is None: raise LookupError(f"menu item {path!r} not found") - if not tab: + if tab is not None and callback(tab): + menu.entryconfig(index, state="normal") + else: menu.entryconfig(index, state="disabled") - return - - menu.entryconfig(index, state=("normal" if callback(tab) else "disabled")) update_enabledness(path=path) From 809b900a2367b5b402d024b4dc91fb68f5106e99 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 17 Jul 2023 16:01:02 -0400 Subject: [PATCH 34/39] Update porcupine/actions.py Co-authored-by: rdbende --- porcupine/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/porcupine/actions.py b/porcupine/actions.py index 156c5c4b4..7fa823b60 100644 --- a/porcupine/actions.py +++ b/porcupine/actions.py @@ -111,7 +111,7 @@ def get_all_actions() -> dict[str, Action]: # Availability Helpers -def filetype_is(filetypes: Union[list[str], str]) -> Callable[[FileTab], bool]: +def filetype_is(filetypes: str | list[str]) -> Callable[[FileTab], bool]: def _filetype_is(filetypes: list[str], tab: FileTab) -> bool: try: filetype = tab.settings.get("filetype_name", object) From 3f528fc8d8b472fccc3c66a79d156689f7186ae4 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 17 Jul 2023 16:01:43 -0400 Subject: [PATCH 35/39] Update porcupine/actions.py Co-authored-by: rdbende --- porcupine/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/porcupine/actions.py b/porcupine/actions.py index 7fa823b60..f7bb5ddc7 100644 --- a/porcupine/actions.py +++ b/porcupine/actions.py @@ -51,7 +51,7 @@ def register_bare_action( availability_callback: Callable[[], bool] = lambda: True, ) -> BareAction: if name in _actions: - raise ValueError(f"Action with the name '{name}' already exists") + raise ValueError(f"Action with the name {name!r} already exists") action = BareAction( name=name, description=description, From 034a9725e85e399008c595f12f97406c82c76de4 Mon Sep 17 00:00:00 2001 From: rdbende Date: Mon, 17 Jul 2023 22:07:24 +0200 Subject: [PATCH 36/39] Update actions.py --- porcupine/actions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/porcupine/actions.py b/porcupine/actions.py index f7bb5ddc7..38ef61300 100644 --- a/porcupine/actions.py +++ b/porcupine/actions.py @@ -70,7 +70,7 @@ def register_filetab_action( availability_callback: Callable[[FileTab], bool] = lambda tab: True, ) -> FileTabAction: if name in _actions: - raise ValueError(f"Action with the name '{name}' already exists") + raise ValueError(f"Action with the name {name!r} already exists") action = FileTabAction( name=name, description=description, @@ -89,7 +89,7 @@ def register_path_action( availability_callback: Callable[[Path], bool] = lambda path: True, ) -> PathAction: if name in _actions: - raise ValueError(f"Action with the name '{name}' already exists") + raise ValueError(f"Action with the name {name!r} already exists") action = PathAction( name=name, description=description, From 68674463b666d67d683ca25530209d9a409ab934 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 31 Jul 2023 21:44:35 -0400 Subject: [PATCH 37/39] remove filetab_only switch --- porcupine/menubar.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/porcupine/menubar.py b/porcupine/menubar.py index bc00222da..7f0cee30b 100644 --- a/porcupine/menubar.py +++ b/porcupine/menubar.py @@ -255,9 +255,7 @@ def register_enabledness_check_event(event: str) -> None: get_tab_manager().bind(event, _refresh_menu_item_enabledness, add=True) -def set_enabled_based_on_tab( - path: str, callback: Callable[[tabs.Tab], bool], filetab_only: bool = False -) -> None: +def set_enabled_based_on_tab(path: str, callback: Callable[[tabs.Tab], bool]) -> None: """Use this for disabling menu items depending on the currently selected tab. When the selected :class:`~porcupine.tabs.Tab` changes, ``callback`` will From d861f09bd24490532cc87ad6ad109c1a8c788eb1 Mon Sep 17 00:00:00 2001 From: rdbende Date: Tue, 1 Aug 2023 09:09:37 +0200 Subject: [PATCH 38/39] Rename function --- porcupine/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/porcupine/actions.py b/porcupine/actions.py index 38ef61300..a6bdddc77 100644 --- a/porcupine/actions.py +++ b/porcupine/actions.py @@ -100,7 +100,7 @@ def register_path_action( return action -def query_actions(name: str) -> Action | None: +def get_action(name: str) -> Action | None: return _actions.get(name) From 3813ccd8d84f0406b4d2e5d2651b969e5b1a4f58 Mon Sep 17 00:00:00 2001 From: rdbende Date: Tue, 1 Aug 2023 09:19:18 +0200 Subject: [PATCH 39/39] There are test too. Heck yeah XD --- tests/test_actions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_actions.py b/tests/test_actions.py index 711e8440d..9ffe04d7c 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -18,12 +18,12 @@ def test_action_registry(): all_actions = actions.get_all_actions() for action in [bare_action, filetab_action, path_action]: - assert actions.query_actions(action.name) == action + assert actions.get_action(action.name) is action assert action in all_actions.values() - assert actions.query_actions("nonexistent action") is None + assert actions.get_action("nonexistent action") is None all_actions["garbage"] = "mean lean fighting machine" # type: ignore assert ( - actions.query_actions("garbage") is None + actions.get_action("garbage") is None ), "`all_actions` should be a copy, changes to it should not effect `_actions`"