From 8775d89fc4acce367c11cfdecdfeb3ebc6749dcb Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Tue, 4 Jul 2023 22:25:13 -0400 Subject: [PATCH] first pass implementation --- porcupine/plugins/python_tools.py | 12 +++- porcupine/plugins/toolbar.py | 110 ++++++++++++++++++++++++++++++ tests/test_toolbar_plugin.py | 39 +++++++++++ 3 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 porcupine/plugins/toolbar.py create mode 100644 tests/test_toolbar_plugin.py diff --git a/porcupine/plugins/python_tools.py b/porcupine/plugins/python_tools.py index 3c62bd8b8..5d292a9c8 100644 --- a/porcupine/plugins/python_tools.py +++ b/porcupine/plugins/python_tools.py @@ -3,7 +3,6 @@ Available in Tools/Python/Black and Tools/Python/Isort. """ - from __future__ import annotations import logging @@ -14,7 +13,7 @@ from tkinter import messagebox from porcupine import menubar, tabs, textutils, utils -from porcupine.plugins import python_venv +from porcupine.plugins import python_venv, toolbar log = logging.getLogger(__name__) @@ -66,3 +65,12 @@ def format_code_in_textwidget(tool: str, tab: tabs.FileTab) -> None: 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")) + + buttons = [] + buttons.append( + toolbar.Button(text="Black", command=partial(format_code_in_textwidget, "black")) + ) + buttons.append( + toolbar.Button(text="isort", command=partial(format_code_in_textwidget, "isort")) + ) + toolbar.add_button_group(filetype_name="Python", name="Python Tools", buttons=buttons) diff --git a/porcupine/plugins/toolbar.py b/porcupine/plugins/toolbar.py new file mode 100644 index 000000000..8445943e2 --- /dev/null +++ b/porcupine/plugins/toolbar.py @@ -0,0 +1,110 @@ +"""Display a toolbar in each file tab.""" +from __future__ import annotations + +import dataclasses +import logging +import tkinter +from functools import partial +from tkinter import ttk +from typing import Any, Callable, Iterable + +from porcupine import get_tab_manager, tabs + +log = logging.getLogger(__name__) + +setup_after = ["filetypes"] + + +# TODO: add icon (make text optional?) +@dataclasses.dataclass(kw_only=True) +class Button: + text: str + description: str | None = None + command: Callable + + +@dataclasses.dataclass(kw_only=True) +class ButtonGroup: + name: str + priority: int # 0 is highest + buttons: list[Button] + separator: bool = True + + +class SortedButtonGroupList(list[ButtonGroup]): + """A of button groups that sorts itself automatically""" + + # no this wasn't necessary, but I am in too deep to stop now + # but seriously why isn't there a sorted list type in the stdlib? + @classmethod + def _key_func(cls, group: ButtonGroup) -> int: + return group.priority + + def __init__(self, *args: Iterable[ButtonGroup], **kwargs: ButtonGroup) -> None: + super().__init__(*args, **kwargs) + self.sort(key=self._key_func) + + def append(self, __item: ButtonGroup) -> None: + super().append(__item) + self.sort(key=self._key_func) + + def extend(self, __iterable: Iterable[ButtonGroup]) -> None: + super().extend(__iterable) + self.sort(key=self._key_func) + + +filetype_button_groups_mapping: dict[str, SortedButtonGroupList] = {} + + +def add_button_group( + *, filetype_name: str, name: str, buttons: list[Button], priority: int = 0, separator=True +) -> None: + button_group = ButtonGroup(name=name, priority=priority, buttons=buttons, separator=separator) + if filetype_button_groups_mapping.get(filetype_name): + filetype_button_groups_mapping[filetype_name].append(button_group) + else: + filetype_button_groups_mapping[filetype_name] = SortedButtonGroupList([button_group]) + + +class ToolBar(ttk.Frame): + def __init__(self, tab: tabs.FileTab): + super().__init__(tab.top_frame, name="toolbar", border=1, relief="raised") + self._tab = tab + + def update_buttons(self, tab: tabs.FileTab, junk: object = None) -> None: + """Different filetypes have different buttons associated with them.""" + filetype_name = tab.settings.get("filetype_name", object) + button_groups = filetype_button_groups_mapping.get(filetype_name) + if not button_groups: + return + + for button_group in button_groups: + for button in button_group.buttons: + ttk.Button( + self, + command=partial(button.command, tab), + style="Statusbar.TButton", + text=button.text, + ).pack(side="left", padx=10, pady=5) + + +def on_new_filetab(tab: tabs.FileTab) -> None: + toolbar = ToolBar(tab) + toolbar.pack(side="bottom", fill="x") + + tab.bind("<>", partial(toolbar.update_buttons, tab), add=True) + toolbar.update_buttons(tab) + + +def update_button_style(junk_event: object = None) -> None: + # https://tkdocs.com/tutorial/styles.html + # tkinter's style stuff sucks + get_tab_manager().tk.eval( + "ttk::style configure Statusbar.TButton -padding {10 0} -anchor center" + ) + + +def setup() -> None: + get_tab_manager().add_filetab_callback(on_new_filetab) + get_tab_manager().bind("<>", update_button_style, add=True) + update_button_style() diff --git a/tests/test_toolbar_plugin.py b/tests/test_toolbar_plugin.py new file mode 100644 index 000000000..2d7108e11 --- /dev/null +++ b/tests/test_toolbar_plugin.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from porcupine.plugins import toolbar + + +def _gen_button_group(priority: int, name: str | None = None) -> toolbar.ButtonGroup: + return toolbar.ButtonGroup( + name=f"priority = {priority}" if not name else name, priority=priority, buttons=[] + ) + + +def test_manual_sorted_button_group_list_with_append_and_extend(): + # reverse order + sorted_button_group_list = toolbar.SortedButtonGroupList( + [_gen_button_group(i) for i in [100, 0, 50, 25, 2, 1, 99]] + ) + + sorted_button_group_list.append(_gen_button_group(33)) + sorted_button_group_list.append(_gen_button_group(5)) + + sorted_button_group_list.extend( + [_gen_button_group(9), _gen_button_group(8), _gen_button_group(7)] + ) + + for i, button_group in zip( + [0, 1, 2, 5, 7, 8, 9, 25, 33, 50, 99, 100], sorted_button_group_list + ): + assert i == button_group.priority + + +def test_big_reversed_sorted_button_group_list(): + qty = 100 + # reverse order + sorted_button_group_list = toolbar.SortedButtonGroupList( + [_gen_button_group(i) for i in reversed(range(qty))] + ) + + for i, button_group in zip(range(qty), sorted_button_group_list): + assert i == button_group.priority