Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Toolbar #1334

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
28e440d
python 3.11 testing
benjamin-kirkbride Jul 3, 2023
6fe65eb
improve headless testing
benjamin-kirkbride Jul 3, 2023
1c41dbe
fix headless detection
benjamin-kirkbride Jul 3, 2023
e844415
add .venv to .gitignore
benjamin-kirkbride Jun 30, 2023
16bf54e
markdown plugin
benjamin-kirkbride Jun 30, 2023
b5de9eb
Run pycln, black and isort
benjamin-kirkbride Jun 30, 2023
5c81b97
add explicit return
benjamin-kirkbride Jul 1, 2023
0a14f13
Run pycln, black and isort
benjamin-kirkbride Jul 1, 2023
333688a
improve coverage and regex
benjamin-kirkbride Jul 1, 2023
c876442
Run pycln, black and isort
benjamin-kirkbride Jul 1, 2023
c525b06
remove autoindent regexes for markdown
benjamin-kirkbride Jul 1, 2023
eae3e71
remove extranious sleep
benjamin-kirkbride Jul 1, 2023
d926695
update module docstring
benjamin-kirkbride Jul 1, 2023
6c75144
fix autocomplete
benjamin-kirkbride Jul 1, 2023
c86cd39
fix assert
benjamin-kirkbride Jul 2, 2023
dd54e3f
fix flaking (?) tests
benjamin-kirkbride Jul 2, 2023
d086ad9
add list continuation
benjamin-kirkbride Jul 3, 2023
17d5f20
add update() before all events
benjamin-kirkbride Jul 4, 2023
dd4a0aa
remove print
benjamin-kirkbride Jul 3, 2023
c6caf77
remove bad test
benjamin-kirkbride Jul 3, 2023
68e7fcb
fix crash in case no filetype
benjamin-kirkbride Jul 3, 2023
2c24d1b
fix list continuation tests
benjamin-kirkbride Jul 3, 2023
7809f5b
actually fix the list continuation test
benjamin-kirkbride Jul 4, 2023
8065b37
clear empty list item on return
benjamin-kirkbride Jul 4, 2023
8775d89
first pass implementation
benjamin-kirkbride Jul 5, 2023
44d609b
Merge branch 'Akuli:main' into feat_control_tabs
benjamin-kirkbride Jul 5, 2023
544723a
Run pycln, black and isort
benjamin-kirkbride Jul 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/autofix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
path: ./pr
- uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.11"
cache: pip
- run: pip install wheel
- run: pip install -r requirements-dev.txt
Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.11"
cache: pip
- run: pip install wheel
- run: pip install -r requirements.txt -r requirements-dev.txt
Expand All @@ -26,13 +26,14 @@ jobs:
time mypy --platform darwin --python-version 3.8 porcupine docs/extensions.py
time mypy --platform darwin --python-version 3.9 porcupine docs/extensions.py
time mypy --platform darwin --python-version 3.10 porcupine docs/extensions.py
time mypy --platform darwin --python-version 3.11 porcupine docs/extensions.py

pytest:
timeout-minutes: 10
strategy:
matrix:
os: ["ubuntu-latest", "windows-latest"]
python-version: ["3.8", "3.9", "3.10"]
python-version: ["3.8", "3.9", "3.10", "3.11"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
Expand Down Expand Up @@ -60,7 +61,7 @@ jobs:
runs-on: macos-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10"]
python-version: ["3.8", "3.9", "3.10", "3.11"]
env:
# TODO: how to install tkdnd on mac? add instructions to README or make mac app that bundles it
TCLLIBPATH: ./lib
Expand All @@ -82,7 +83,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.11"
cache: pip
# TODO: adding these to requirements-dev.txt breaks pip install
- run: pip install flake8==5.0.4 flake8-tkinter==0.5.0
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.11"
cache: pip
- run: pip install wheel
- run: pip install -r requirements.txt -r requirements-dev.txt
Expand Down
1 change: 0 additions & 1 deletion porcupine/default_filetypes.toml
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,6 @@ filename_patterns = ["*.md", "*.markdown"]
pygments_lexer = "pygments.lexers.MarkdownLexer"
syntax_highlighter = "tree_sitter"
tree_sitter_language_name = "markdown"
autoindent_regexes = {dedent = '.*\.', indent = '^([0-9]+\.|-) .*'}

[YAML]
filename_patterns = ["*.yml", "*.yaml"]
Expand Down
2 changes: 2 additions & 0 deletions porcupine/plugins/autoindent.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ def after_enter(tab: tabs.FileTab, alt_pressed: bool) -> None:
if dedent_prev_line:
tab.textwidget.dedent("insert - 1 line")

tab.textwidget.event_generate("<<post-autoindent>>")


def on_enter_press(
tab: tabs.FileTab, alt_pressed: bool, event: tkinter.Event[tkinter.Text]
Expand Down
109 changes: 109 additions & 0 deletions porcupine/plugins/markdown.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Features for working with Markdown Files.

- Indenting and dedenting lists
"""

from __future__ import annotations

import logging
import re
import tkinter
from functools import partial

from porcupine import get_tab_manager, tabs, textutils, utils

log = logging.getLogger(__name__)


setup_before = ["tabs2spaces", "autoindent"]


def _list_item(line: str) -> re.Match[str] | None:
"""Regex for markdown list item

1st group is the whitespace (if any) preceding the item
2nd group is the list item prefix (ex `-`, `+`, `6.`, `#.`)
3rd group is the item text

According to:
- https://spec.commonmark.org/0.30/#lists
- https://pandoc.org/MANUAL.html#lists
Technically `#)` is not in either spec, but I won't tell if you won't
"""
assert isinstance(line, str)
if not line:
# empty string
return None

assert len(line.splitlines()) == 1

list_item_regex = re.compile(r"(^[\t ]*)(\d{1,9}[.)]|[-+*]|#\)|#\.) (.*)")
match = list_item_regex.search(line)
return match if match else None


def on_tab_key(
tab: tabs.FileTab, event: tkinter.Event[textutils.MainText], shift_pressed: bool
) -> str | None:
"""Indenting and dedenting list items"""
if tab.settings.get("filetype_name", object) == "Markdown":
line = event.widget.get("insert linestart", "insert lineend")
list_item_status = _list_item(line)

# shift-tab
if shift_pressed and list_item_status:
event.widget.dedent("insert linestart")
return "break"

# if it isn't, we want tab to trigger autocomplete instead
char_before_cursor_is_space = tab.textwidget.get("insert - 1 char", "insert") == " "

# tab
if list_item_status and char_before_cursor_is_space:
event.widget.indent("insert linestart")
return "break"

return None


def continue_list(tab: tabs.FileTab, event: tkinter.Event[tkinter.Text]) -> str | None:
"""Automatically continue lists

This happens after the `autoindent` plugin automatically handles indentation
"""
if tab.settings.get("filetype_name", object) == "Markdown":
current_line = event.widget.get("insert - 1l linestart", "insert -1l lineend")
list_item_match = _list_item(current_line)
if list_item_match:
indentation, prefix, item_text = list_item_match.groups()

tab.textwidget.insert("insert", prefix + " ")
tab.update()

return None


def on_enter_press(tab: tabs.FileTab, event: tkinter.Event[tkinter.Text]) -> str | None:
if tab.settings.get("filetype_name", object) == "Markdown":
current_line = event.widget.get("insert linestart", "insert lineend")
list_item_match = _list_item(current_line)
if list_item_match:
indentation, prefix, item_text = list_item_match.groups()
if item_text:
# there is item text, so we are done here
return None

event.widget.delete("insert linestart", "insert lineend")
return "break"

return None


def on_new_filetab(tab: tabs.FileTab) -> None:
utils.bind_tab_key(tab.textwidget, partial(on_tab_key, tab), add=True)
tab.textwidget.bind("<<post-autoindent>>", partial(continue_list, tab), add=True)
tab.textwidget.bind("<Return>", partial(on_enter_press, tab), add=True)


def setup() -> None:
get_tab_manager().add_filetab_callback(on_new_filetab)
12 changes: 10 additions & 2 deletions porcupine/plugins/python_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

Available in Tools/Python/Black and Tools/Python/Isort.
"""

from __future__ import annotations

import logging
Expand All @@ -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__)

Expand Down Expand Up @@ -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)
109 changes: 109 additions & 0 deletions porcupine/plugins/toolbar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Display a toolbar in each file tab."""
from __future__ import annotations

import dataclasses
import logging
from functools import partial
from tkinter import ttk
from typing import 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("<<TabSettingChanged:filetype_name>>", 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("<<ThemeChanged>>", update_button_style, add=True)
update_button_style()
15 changes: 13 additions & 2 deletions tests/test_fullscreen_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,20 @@
from porcupine import get_main_window
from porcupine.menubar import get_menu

github_actions = os.getenv("GITHUB_ACTIONS")
xauthority = os.getenv("XAUTHORITY")

headless = False

if github_actions is not None:
headless = github_actions == "true"
elif xauthority is not None:
headless = "xvfb" in xauthority


pytestmark = pytest.mark.skipif(headless, reason="Does not work in headless environments")


@pytest.mark.skipif(os.getenv("GITHUB_ACTIONS") == "true", reason="fails CI on all platforms")
def test_basic(wait_until):
assert not get_main_window().attributes("-fullscreen")

Expand All @@ -18,7 +30,6 @@ def test_basic(wait_until):


# Window managers can toggle full-screen-ness without going through our menubar
@pytest.mark.skipif(os.getenv("GITHUB_ACTIONS") == "true", reason="fails CI on all platforms")
def test_toggled_without_menu_bar(wait_until):
get_main_window().attributes("-fullscreen", 1)
wait_until(lambda: bool(get_main_window().attributes("-fullscreen")))
Expand Down
22 changes: 0 additions & 22 deletions tests/test_indent_dedent.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,28 +198,6 @@ def check(filename, input_commands, output):
return check


def test_markdown_autoindent(check_autoindents):
check_autoindents(
"hello.md",
"""
1. Lol and
wat.
- Foo and
bar and
baz.
End of list
""",
"""
1. Lol and
wat.
- Foo and
bar and
baz.
End of list
""",
)


def test_shell_autoindent(check_autoindents):
check_autoindents(
"loll.sh",
Expand Down
Loading