Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
551a19c
Create NumberEntry widget
rdbende Mar 1, 2021
a6db0b3
Change the default hover-cursor to hand2
rdbende Mar 1, 2021
7611f2a
Change the movement cursor to fleur
rdbende Mar 1, 2021
61a46c5
Add NumberEntry to __init__.py
rdbende Mar 1, 2021
8941ab3
Fixed portability issues
rdbende Mar 2, 2021
8488995
Restore cursor
rdbende Mar 9, 2021
8b614d2
Restore linklabel
rdbende Mar 9, 2021
9584d3f
Add __getitem__, __setitem__ and configure
rdbende Mar 9, 2021
af42360
Update AUTHORS.md
rdbende Mar 9, 2021
a4af566
Create example for NumberEntry
rdbende Mar 9, 2021
439323e
Create unittest for NumberEntry
rdbende Mar 9, 2021
19f361e
Add NumberEntry to sphinx documentation
rdbende Mar 9, 2021
acdbec5
Update AUTHORS.md
rdbende Mar 9, 2021
fb2dd12
Fixed fatal config bug
rdbende Mar 11, 2021
e5d60f3
Update __init__.py
rdbende Mar 22, 2021
de2d5c2
Update __init__.py
rdbende Mar 22, 2021
d2fa5e6
Update ttkwidgets.rst
rdbende Mar 24, 2021
28d339c
Update __init__.py
rdbende Mar 24, 2021
d7891cb
Update AUTHORS.md
rdbende Mar 24, 2021
e98f416
Delete test_numberentry.py
rdbende Mar 24, 2021
a386adf
Delete example_numberentry.py
rdbende Mar 24, 2021
7806fee
Delete numberentry.py
rdbende Mar 24, 2021
c6e7ab8
native cursors, virtual event, colors
rdbende Mar 25, 2021
32ed8a4
Change exchange cursor to fleur
rdbende Mar 25, 2021
e80986d
ToggledFrame improved
rdbende Mar 26, 2021
e8c3e0d
Merge branch 'master' into some-improvements
rdbende Oct 13, 2022
6e8cf44
Update LinkLabel
rdbende Dec 13, 2022
364021d
Update LinkLabel tests
rdbende Dec 13, 2022
040b13e
State changes as required by this GPL license
rdbende Dec 13, 2022
3477879
Remove unnecessary variable
rdbende Dec 13, 2022
d3a67db
Fix LinkLabel test
rdbende Dec 13, 2022
12560cf
Update ToggledFrame stuff
rdbende Dec 13, 2022
3f8ac80
Use keyword arguments with default instead of kwargs.pop
rdbende Dec 13, 2022
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
15 changes: 10 additions & 5 deletions tests/test_linklabel.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
# Copyright (c) RedFantom 2017
# For license see LICENSE
from ttkwidgets import LinkLabel
from tests import BaseWidgetTest
import tkinter as tk

from tests import BaseWidgetTest
from ttkwidgets import LinkLabel


class TestLinkLabel(BaseWidgetTest):
def test_linklabel_init(self):
label = LinkLabel(self.window, link="www.google.com", text="Visit Google")
label.pack()
self.window.update()

def test_linklabel_events(self):
label = LinkLabel(self.window, link="www.google.com", text="Visit Google")
label.pack()

self.window.update()
label._on_enter()
self.window.update()
label._on_leave()
self.window.update()
label.open_link()
self.window.update()

def test_linklabel_config(self):
label = LinkLabel(self.window, link="www.google.com", text="Visit Google")
label.pack()

self.window.update()
label.keys()
self.window.update()
Expand All @@ -36,12 +37,16 @@ def test_linklabel_config(self):
self.window.update()
label["clicked_color"] = "purple"
self.window.update()
label.config(cursor="hand1")
self.window.update()
assert str(label.cget("cursor")) == "hand1"

def test_linklabel_cget(self):
label = LinkLabel(self.window, link="www.google.com", text="Visit Google")
label.pack()
assert label.cget("hover_color") == label._hover_color

assert label.cget("link") == label._link
assert label.cget("normal_color") == label._normal_color
assert label.cget("hover_color") == label._hover_color
assert label.cget("clicked_color") == label._clicked_color
assert label.cget("text") == "Visit Google"
24 changes: 17 additions & 7 deletions tests/test_toggledframe.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
# Copyright (c) RedFantom 2017
# For license see LICENSE
from ttkwidgets.frames import ToggledFrame
from tests import BaseWidgetTest
import tkinter as tk

from tests import BaseWidgetTest
from ttkwidgets.frames import ToggledFrame


class TestToggledFrame(BaseWidgetTest):
def test_toggledframe_init(self):
frame = ToggledFrame(self.window)
frame.pack()
self.window.update()

def test_toggledframe_open(self):
frame = ToggledFrame(self.window)
frame.pack()
self.window.update()
frame.toggle()
self.assertTrue(frame._open)
assert frame.opened

def test_toggledframe_open_close(self):
frame = ToggledFrame(self.window)
frame.pack()
self.window.update()
frame.toggle()
self.assertTrue(frame._open)
frame.toggle()
self.assertFalse(frame._open)
self.window.update()
assert frame.opened
frame.close()
self.window.update()
assert not frame.opened
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are good reasons to use assertTrue, assertFalse, assertEquals, and so on, so it's important that they are not changed. See also here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use Pytest as test runner, which prefers the assert keyword, and logs usable output with it, but I see we're using Nose here, so I'll rewrite it.

frame.open()
self.window.update()
assert frame.opened
frame._button.invoke()
self.window.update()
assert not frame.opened
117 changes: 88 additions & 29 deletions ttkwidgets/frames/toggledframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@
Author: RedFantom
License: GNU GPLv3
Source: This repository

Improved by rdbende
"""

import tkinter as tk
from pathlib import Path
from tkinter import ttk
import os
from PIL import Image, ImageTk

from ttkwidgets.utilities import get_assets_directory

assets_dir = Path(get_assets_directory())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the use of pathlib, but why does it have to be a global variable?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I thought I might need it elsewhere, but it turns out I didn't.



class ToggledFrame(ttk.Frame):
"""
Expand All @@ -17,44 +22,98 @@ class ToggledFrame(ttk.Frame):
:ivar interior: :class:`ttk.Frame` in which to put the widgets to be toggled with any geometry manager.
"""

def __init__(self, master=None, text="", width=20, compound=tk.LEFT, **kwargs):
def __init__(self, master=None, *, text=None, cursor="arrow", width=20, **kwargs):
"""
Create a ToggledFrame.

:param master: master widget
:type master: widget
:param text: text to display next to the toggle arrow
:param text: text to in the header of the ToggledFrame
:type text: str
:param width: width of the closed ToggledFrame (in characters)
:type width: int
:param compound: "center", "none", "top", "bottom", "right" or "left":
position of the toggle arrow compared to the text
:type compound: str
:param cursor: cursor that appears on the ToggledFrame's button
:type cursor: str
:param kwargs: keyword arguments passed on to the :class:`ttk.Frame` initializer
"""
self._open = tk.BooleanVar(value=False)

ttk.Frame.__init__(self, master, **kwargs)
self._open = False
self.__checkbutton_var = tk.BooleanVar()
self._open_image = ImageTk.PhotoImage(Image.open(os.path.join(get_assets_directory(), "open.png")))
self._closed_image = ImageTk.PhotoImage(Image.open(os.path.join(get_assets_directory(), "closed.png")))
self._checkbutton = ttk.Checkbutton(self, style="Toolbutton", command=self.toggle,
variable=self.__checkbutton_var, text=text, compound=compound,
image=self._closed_image, width=width)
self.interior = ttk.Frame(self, relief=tk.SUNKEN)
self._grid_widgets()

def _grid_widgets(self):
self._checkbutton.grid(row=0, column=0, sticky="we")

def toggle(self):
"""Toggle :obj:`ToggledFrame.interior` opened or closed."""
if self._open:
self._open = False
self.__checkbutton_var.set(False)
self.interior = ttk.Frame(self)

self._open_image = tk.PhotoImage(file=assets_dir / "open.png")
self._closed_image = tk.PhotoImage(file=assets_dir / "closed.png")

self._button = ttk.Checkbutton(
self,
style="Toolbutton",
compound="right",
cursor=cursor,
image=self._closed_image,
text=text,
variable=self._open,
command=self._toggle_when_clicked,
width=width,
)
self._button.grid(row=0, column=0, sticky="ew")

def __getitem__(self, key):
return self.cget(key)

def __setitem__(self, key, value):
self.configure(**{key: value})

def _toggle_when_clicked(self):
# when clicking the checkbutton it inverts its variable, so we can't simply use self.toggle
if self._open.get():
self.interior.grid(row=1, column=0, sticky="nswe")
self._button.config(image=self._open_image)
self.event_generate("<<ToggledFrameOpened>>")
else:
self.interior.grid_forget()
self._checkbutton.config(image=self._closed_image)
self._button.config(image=self._closed_image)
self.event_generate("<<ToggledFrameClosed>>")

def open(self):
self.interior.grid(row=1, column=0, sticky="nswe")
self._open.set(True)
self._button.config(image=self._open_image)
self.event_generate("<<ToggledFrameOpened>>")

def close(self):
self.interior.grid_forget()
self._open.set(False)
self._button.config(image=self._closed_image)
self.event_generate("<<ToggledFrameClosed>>")

def toggle(self):
if self._open.get():
self.close()
else:
self._open = True
self.__checkbutton_var.set(True)
self.interior.grid(row=1, column=0, sticky="nswe")
self._checkbutton.config(image=self._open_image)
self.open()

@property
def opened(self):
return self._open.get()

def configure(self, **kwargs):
"""Configure resources of the widget"""
button_options = {
key: kwargs.pop(key) for key in ("cursor", "text", "width") if key in kwargs
}
self._button.configure(**button_options)
ttk.Frame.configure(self, **kwargs)

config = configure

def cget(self, key):
"""Return the resource value for a KEY given as string"""
if key in {"cursor", "text", "width"}:
return self._button.cget(key)
else:
return ttk.Frame.cget(self, key)

def keys(self):
"""Return a list of all resource names of this widget"""
return sorted(ttk.Frame.keys(self) + ["text"])
2 changes: 1 addition & 1 deletion ttkwidgets/itemscanvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def left_motion(self, event):

item = results[0]
rectangle = self.items[item]
self.config(cursor="exchange")
self.config(cursor="fleur")
self.canvas.itemconfigure(item, fill="blue")
xc, yc = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
dx, dy = xc - self.current_coords[0], yc - self.current_coords[1]
Expand Down
86 changes: 51 additions & 35 deletions ttkwidgets/linklabel.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,51 @@
Author: RedFantom
License: GNU GPLv3
Source: This repository

Edited by rdbende: change default widget colors, use native cursors by default, add virtual event
"""
# Based on an idea by Nelson Brochado (https://www.github.com/nbrol/tkinter-kit)
# Available from fork: https://www.github.com/RedFantom/tkinter-kit
import tkinter as tk
from tkinter import ttk
import webbrowser
from tkinter import ttk


class LinkLabel(ttk.Label):
"""
A :class:`ttk.Label` that can be clicked to open a link with a default blue color, a purple color when clicked and a bright
blue color when hovering over the Label.
A :class:`ttk.Label` that can be clicked to open a link with a default blue color,
a purple when clicked and dark blue when hovering over the Label.
"""

def __init__(self, master=None, **kwargs):
"""
Create a LinkLabel.

:param master: master widget
:param link: link to be opened
:type link: str
:param normal_color: text color when widget is created
:param normal_color: text color when the widget is in neutral state
:type normal_color: str
:param hover_color: text color when hovering over the widget
:type hover_color: str
:param clicked_color: text color when link is clicked
:param clicked_color: text color when the widget has been clicked
:type clicked_color: str
:param kwargs: options to be passed on to the :class:`ttk.Label` initializer
"""
self._cursor = kwargs.pop("cursor", "hand1")
self._link = kwargs.pop("link", "")
self._normal_color = kwargs.pop("normal_color", "#0563c1")
self._hover_color = kwargs.pop("hover_color", "#057bc1")
self._clicked_color = kwargs.pop("clicked_color", "#954f72")
self._normal_color = kwargs.pop("normal_color", "#005fff")
self._hover_color = kwargs.pop("hover_color", "#000fff")
self._clicked_color = kwargs.pop("clicked_color", "#6600a6")

is_mac = (master or tk._default_root).tk.call("tk", "windowingsystem") == "aqua"
kwargs.setdefault("cursor", "pointinghand" if is_mac else "hand2")

ttk.Label.__init__(self, master, **kwargs)
self.config(foreground=self._normal_color)
self.__clicked = False

if "disabled" not in self.state():
self.configure(foreground=self._normal_color)

self._clicked = False
self.bind("<Button-1>", self.open_link)
self.bind("<Enter>", self._on_enter)
self.bind("<Leave>", self._on_leave)
Expand All @@ -49,27 +59,47 @@ def __setitem__(self, key, value):

def _on_enter(self, *args):
"""Set the text color to the hover color."""
self.config(foreground=self._hover_color, cursor=self._cursor)
if self._clicked:
self.config(foreground=self._clicked_color)
else:
self.config(foreground=self._hover_color)

def _on_leave(self, *args):
"""Set the text color to either the normal color when not clicked or the clicked color when clicked."""
if self.__clicked:
if self._clicked:
self.config(foreground=self._clicked_color)
else:
self.config(foreground=self._normal_color)
self.config(cursor="")

def reset(self):
"""Reset Label to unclicked status if previously clicked."""
self.__clicked = False
self._clicked = False
self._on_leave()

def open_link(self, *args):
"""Open the link in the web browser."""
if "disabled" not in self.state():
webbrowser.open(self._link)
self.__clicked = True
self._on_leave()
if "disabled" in self.state():
return

webbrowser.open(self._link)
self._clicked = True
self._on_leave()
self.event_generate("<<LinkOpened>>")

def configure(self, **kwargs):
"""
Configure resources of the widget.

To get the list of options for this widget, call the method :meth:`~LinkLabel.keys`.
See :meth:`~LinkLabel.__init__` for a description of the widget specific option.
"""
self._link = kwargs.pop("link", self._link)
self._normal_color = kwargs.pop("normal_color", self._normal_color)
self._hover_color = kwargs.pop("hover_color", self._hover_color)
self._clicked_color = kwargs.pop("clicked_color", self._clicked_color)
ttk.Label.configure(self, **kwargs)

config = configure

def cget(self, key):
"""
Expand All @@ -92,23 +122,9 @@ def cget(self, key):
else:
return ttk.Label.cget(self, key)

def configure(self, **kwargs):
"""
Configure resources of the widget.

To get the list of options for this widget, call the method :meth:`~LinkLabel.keys`.
See :meth:`~LinkLabel.__init__` for a description of the widget specific option.
"""
self._link = kwargs.pop("link", self._link)
self._hover_color = kwargs.pop("hover_color", self._hover_color)
self._normal_color = kwargs.pop("normal_color", self._normal_color)
self._clicked_color = kwargs.pop("clicked_color", self._clicked_color)
ttk.Label.configure(self, **kwargs)
self._on_leave()

def keys(self):
"""Return a list of all resource names of this widget."""
keys = ttk.Label.keys(self)
keys.extend(["link", "normal_color", "hover_color", "clicked_color"])
keys.sort()
return keys