Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into wm-cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
rdbende committed May 1, 2023
2 parents 1b60b47 + 0d4c507 commit 45d7587
Show file tree
Hide file tree
Showing 34 changed files with 355 additions and 74 deletions.
18 changes: 10 additions & 8 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,31 @@ jobs:
black:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v1
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.11"
cache: pip
- run: python3 -m pip install black
- run: python3 -m black --check tukaan/*.py tukaan/*/*.py
isort:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v1
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.11"
cache: pip
- run: python3 -m pip install isort
- run: python3 -m isort --check tukaan/*.py tukaan/*/*.py
pytest:
strategy:
matrix:
os: ["ubuntu-latest", "windows-latest"]
os: ["ubuntu-latest", "windows-latest", "macos-latest"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
uses: actions/setup-python@v4
with:
python-version: "3.9"
python-version: "3.10"
cache: pip
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ __pycache__/
*$py.class

# C extensions
# *.so
*.so

# Distribution / packaging
.Python
Expand Down
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pillow>=9.3.0
screeninfo>=0.8
libtukaan-win==0.1.3; sys_platform == "win32"
libtukaan-mac==0.1.3; sys_platform == "darwin"
libtukaan-unix==0.1.3; sys_platform == "linux"
libtukaan-win==0.1.4; sys_platform == "win32"
libtukaan-mac==0.1.4; sys_platform == "darwin"
libtukaan-unix==0.1.4; sys_platform == "linux"
typing_extensions>=4.3.0; python_version < "3.10"
4 changes: 4 additions & 0 deletions tests/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import tukaan
from tukaan._tcl import Tcl

from pathlib import Path

TESTS_DIRECTORY = Path(__file__).parent

app = None
window = None

Expand Down
Binary file added tests/move_cursor
Binary file not shown.
Binary file added tests/move_cursor.cur
Binary file not shown.
1 change: 1 addition & 0 deletions tests/sources.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`move_cursor`, `watch_cursor`, `move_cursor.cur` and `watch_cursor.ani`: The Suru icon theme
73 changes: 73 additions & 0 deletions tests/test_cursors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import sys
from pathlib import Path

import pytest

import tukaan
from tests.base import TESTS_DIRECTORY, with_app_context
from tukaan import CursorFile
from tukaan.enums import Cursor, LegacyX11Cursor
from libtukaan import Xcursor


@with_app_context
def test_cursor_convert(app, window):
label = tukaan.Label(window, cursor=Cursor.Blank)
assert label.cursor is Cursor.Blank

for cursor in Cursor:
label.cursor = cursor
assert label.cursor == cursor

for cursor in LegacyX11Cursor:
label.cursor = cursor
assert label.cursor == cursor


def test_legacy_x11_cursor_naming():
for cursor in LegacyX11Cursor:
if cursor.name == "X":
continue
assert cursor.name.lower() == cursor.value.replace("_", "")


@pytest.mark.skipif(sys.platform != "win32", reason="Windows only cursor loading thingy")
@with_app_context
def test_windows_cursor_file(app, window):
label = tukaan.Label(window, cursor=CursorFile(TESTS_DIRECTORY / "move_cursor.cur"))
assert label.cursor == CursorFile(TESTS_DIRECTORY / "move_cursor.cur")
assert label.cursor._name.startswith("@")
assert label.cursor._name.endswith("move_cursor.cur")

label_2 = tukaan.Label(window, cursor=CursorFile(TESTS_DIRECTORY / "watch_cursor.ani"))
assert label_2.cursor == CursorFile(TESTS_DIRECTORY / "watch_cursor.ani")
assert label_2.cursor._name.startswith("@")
assert label_2.cursor._name.endswith("watch_cursor.ani")

with pytest.raises(ValueError):
CursorFile(Path("./foo.png"))


@pytest.mark.skipif(sys.platform != "linux", reason="Xcursor is Linux only")
@with_app_context
def test_xcursor(app, window):
frame = tukaan.Frame(window, cursor=CursorFile(TESTS_DIRECTORY / "move_cursor"))
label = tukaan.Label(frame, cursor=CursorFile(TESTS_DIRECTORY / "watch_cursor"))
label_2 = tukaan.Label(frame, cursor=CursorFile(TESTS_DIRECTORY / "watch_cursor"))
assert len(Xcursor._defined_cursors) == 3
assert frame.cursor == CursorFile(TESTS_DIRECTORY / "move_cursor")
assert label.cursor == CursorFile(TESTS_DIRECTORY / "watch_cursor")
assert label_2.cursor == CursorFile(TESTS_DIRECTORY / "watch_cursor")
label_2.destroy()
assert len(Xcursor._defined_cursors) == 2
frame.destroy()
assert len(Xcursor._defined_cursors) == 0


@with_app_context
def test_default_cursors(app, window):
button = tukaan.Button(window)
assert button.cursor is Cursor.Arrow

textbox = tukaan.TextBox(window)
assert textbox.cursor is Cursor.Text
Binary file added tests/watch_cursor
Binary file not shown.
Binary file added tests/watch_cursor.ani
Binary file not shown.
1 change: 1 addition & 0 deletions tukaan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from ._events import KeySeq
from ._images import Icon, IconFactory, Image
from ._misc import CursorFile
from ._system import Platform
from ._variables import BoolVar, FloatVar, IntVar, StringVar
from .a11y.a11y import Accessibility
Expand Down
42 changes: 41 additions & 1 deletion tukaan/_base.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
from __future__ import annotations

import collections
import contextlib
from typing import Any, Callable

from libtukaan import Xcursor

from tukaan._collect import commands, widgets
from tukaan._events import BindingsMixin
from tukaan._layout import ContainerGrid, Geometry, Grid, Position, ToplevelGrid
from tukaan._misc import CursorFile
from tukaan._mixins import GeometryMixin, VisibilityMixin, WidgetMixin
from tukaan._props import cget, config
from tukaan._tcl import Tcl
from tukaan._utils import count
from tukaan.enums import Cursor, LegacyX11Cursor
from tukaan.widgets.tooltip import ToolTipProvider


Expand Down Expand Up @@ -80,7 +85,13 @@ def __init__(self) -> None:


class WidgetBase(TkWidget, GeometryMixin):
def __init__(self, parent: TkWidget, tooltip: str | None = None, **kwargs: Any) -> None:
def __init__(
self,
parent: TkWidget,
cursor: Cursor_T | None = None,
tooltip: str | None = None,
**kwargs: Any,
) -> None:
assert isinstance(parent, Container), "parent must be a container"

self._name = self._lm_path = generate_pathname(self, parent)
Expand All @@ -95,16 +106,45 @@ def __init__(self, parent: TkWidget, tooltip: str | None = None, **kwargs: Any)

Tcl.call(None, self._tcl_class, self._name, *Tcl.to_tcl_args(**kwargs))

self._xcursor = None
if cursor:
self.cursor = cursor
if tooltip:
ToolTipProvider.add(self, tooltip)

def destroy(self) -> None:
"""Destroy this widget, and remove it from the screen."""
Xcursor.undefine_cursors(Tcl.eval({str}, f"winfo children {self._lm_path}"))
Xcursor.undefine_cursors({self._lm_path})
Tcl.call(None, "destroy", self._name)

del self.parent._children[self._name]
del widgets[self._name]

@property
def cursor(self) -> Cursor | LegacyX11Cursor | CursorFile:
if self._xcursor is not None:
# This must be checked first,
# since it's independent of the widget's actual Tk cursor
return CursorFile.__from_tcl__(self._xcursor)

tk_cursor = cget(self, str, "-cursor")
with contextlib.suppress(ValueError):
return Cursor(tk_cursor)

with contextlib.suppress(ValueError):
return LegacyX11Cursor(tk_cursor)

return CursorFile.__from_tcl__(tk_cursor)

@cursor.setter
def cursor(self, value: Cursor | LegacyX11Cursor | CursorFile) -> None:
if isinstance(value, CursorFile) and Tcl.windowing_system == "x11":
self._xcursor = value._name
return Xcursor.set_cursor(self._lm_path, value._name)
self._xcursor = None
return config(self, cursor=value)

@property
def tooltip(self) -> str | None:
return ToolTipProvider.get(self)
Expand Down
41 changes: 41 additions & 0 deletions tukaan/_misc.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
from __future__ import annotations

from pathlib import Path
from typing import NamedTuple

from libtukaan import Xcursor

from tukaan._system import Platform
from tukaan._tcl import Tcl
from tukaan.exceptions import PlatformSpecificError


class Bbox(NamedTuple):
x: int
Expand All @@ -18,3 +25,37 @@ class Position(NamedTuple):
class Size(NamedTuple):
width: int
height: int


class CursorFile:
def __init__(self, source: Path) -> None:
source = source.resolve().absolute()

if Platform.os == "Windows":
if source.suffix not in (".cur", ".ani"):
raise ValueError(
f'bad cursor file type: "{source.suffix}". Should be ".cur" or ".ani"'
)
self._name = f"@{source.as_posix()!s}" # Windows needs .as_posix() for some reason
elif Tcl.windowing_system == "x11":
self._name = Xcursor.load_cursor(source)
else:
raise PlatformSpecificError(f"can't load cursor from file on {Platform.os}")

@classmethod
def __from_tcl__(cls, value: str) -> CursorFile:
cursor = cls.__new__(cls)
cursor._name = value
return cursor

def __repr__(self) -> str:
if "@" in self._name:
path = self._name.lstrip("@")
else:
path = Xcursor.get_path_for_cursor(self._name)
return f"CursorFile(Path({path!r}))"

def __eq__(self, other: object) -> bool:
if not isinstance(other, CursorFile):
return NotImplemented
return self._name == other._name
2 changes: 2 additions & 0 deletions tukaan/_props.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from typing import Callable
from typing_extensions import Protocol

from pathlib import Path

from tukaan._collect import commands
from tukaan._tcl import Tcl
from tukaan._typing import P, T, T_co, T_contra
Expand Down
14 changes: 7 additions & 7 deletions tukaan/_tcl.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,17 +207,17 @@ def from_(return_type: Any, value: TclValue) -> T: # noqa: CCR001
else:
return return_type.__from_tcl__(value)

if isinstance(return_type, (list, tuple, dict)):
if isinstance(return_type, (set, list, tuple, dict)):
sequence = Tcl._interp.splitlist(value)

if isinstance(return_type, list):
type_ = return_type[0]
return [Tcl.from_(type_, item) for item in sequence]
if isinstance(return_type, (set, list)):
[items_type] = return_type
return type(return_type)((Tcl.from_(items_type, item) for item in sequence))

if isinstance(return_type, tuple):
items_len = len(sequence)
if len(return_type) != items_len:
return_type *= items_len
diff = len(sequence) - len(return_type)
if diff:
return_type = return_type + (return_type[-1],) * diff
return tuple(map(Tcl.from_, return_type, sequence))

if isinstance(return_type, dict):
Expand Down
9 changes: 7 additions & 2 deletions tukaan/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
except ImportError as e:
raise ImportError("Tukaan needs PIL and PIL._imagingtk to work with images.") from e

from libtukaan import Serif
from libtukaan import Serif, Xcursor

from tukaan._tcl import Tcl
from tukaan.theming import LookAndFeel, NativeTheme, Theme
Expand Down Expand Up @@ -36,6 +36,8 @@ def __init__(
App._exists = True

Serif.init()
if Tcl.windowing_system == "x11":
Xcursor.init()
ImagingTk.tkinit(Tcl.interp_address)

NativeTheme.use()
Expand Down Expand Up @@ -85,8 +87,11 @@ def theme(self, theme: Theme) -> None:

@classmethod
def quit(cls) -> None:
"""Quit the entire Tcl interpreter."""
"""Destroy all widgets and quit the Tcl interpreter."""
Serif.cleanup()
Xcursor.cleanup_cursors()
Tcl.call(None, "destroy", ".app")
Tcl.call(None, "destroy", ".")
Tcl.quit()

@classmethod
Expand Down
Loading

0 comments on commit 45d7587

Please sign in to comment.