Skip to content

Commit

Permalink
Improve Balloon widget for #39
Browse files Browse the repository at this point in the history
  • Loading branch information
RedFantom committed Dec 7, 2019
1 parent 8eca0f7 commit acb372d
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 17 deletions.
35 changes: 31 additions & 4 deletions tests/test_balloon.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Copyright (c) RedFantom 2017
# For license see LICENSE
"""
Author: RedFantom
License: GNU GPLv3
Source: This repository
"""
from ttkwidgets.frames import Balloon
from ttkwidgets.utilities import parse_geometry_string
from tests import BaseWidgetTest
try:
import Tkinter as tk
Expand All @@ -16,7 +20,7 @@ def test_balloon_init(self):

def test_balloon_kwargs(self):
balloon = Balloon(self.window, headertext="Help", text="This is a test for the Balloon widget.", width=300,
timeout=2, background="white")
timeout=2, background="white", showheader=True, offset=(20, 20), static=True)
self.assertEqual(balloon.cget("headertext"), "Help")
self.assertEqual(balloon.cget("text"), "This is a test for the Balloon widget.")
self.assertEqual(balloon.cget("width"), 300)
Expand All @@ -30,6 +34,9 @@ def test_balloon_kwargs(self):
self.assertEqual(balloon["width"], 400)
self.assertEqual(balloon["timeout"], 3)
self.assertEqual(balloon["background"], "black")
self.assertEqual(balloon["showheader"], True)
self.assertEqual(balloon["offset"], (20, 20))
self.assertEqual(balloon["static"], True)

# Keys for the Frame widget
balloon.configure(height=40)
Expand All @@ -41,6 +48,27 @@ def test_balloon_kwargs(self):
for key in ["headertext", "text", "width", "timeout", "background"]:
self.assertIn(key, balloon.keys())

balloon.config(showheader=False)
balloon.show()
self.assertFalse(balloon.header_label.winfo_viewable() == 1)

balloon.config(offset=(0, 0))
balloon.show()
self.window.update()
x1, y1, _, _ = parse_geometry_string(balloon._toplevel.winfo_geometry())
balloon.config(offset=(20, 20))
balloon._on_leave(None)
balloon.show()
self.window.update()
x2, y2, _, _ = parse_geometry_string(balloon._toplevel.winfo_geometry())
self.assertTrue(x2 - x1 == 20 and y2 - y1 == 20)

balloon.config(static=False)
balloon.show()
self.window.update()
x3, y3, _, _ = parse_geometry_string(balloon._toplevel.winfo_geometry())
self.assertFalse(x2 == x3 or y2 == y3)

def test_balloon_show(self):
balloon = Balloon(self.window)
self.window.update()
Expand All @@ -67,4 +95,3 @@ def test_balloon_events_noshow(self):
balloon = Balloon(self.window)
balloon._on_enter(None)
balloon._on_leave(None)

57 changes: 45 additions & 12 deletions ttkwidgets/frames/balloon.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ class Balloon(ttk.Frame):
"""Simple help hover balloon."""

def __init__(self, master=None, headertext="Help", text="Some great help is displayed here.", width=200, timeout=1,
background="#fef9cd", **kwargs):
background="#fef9cd", offset=(2, 2), showheader=True, static=False, **kwargs):
"""
Create a Balloon.
Create a Balloon
:param master: widget to bind the Balloon to
:type master: widget
Expand All @@ -34,6 +34,15 @@ def __init__(self, master=None, headertext="Help", text="Some great help is disp
:type timeout: float
:param background: background color of the Balloon
:type background: str
:param offset: The offset from the mouse position the Ballon shows up
:type offset: Tuple[int, int]
:param showheader: Whether to display the header with image
:type showheader: bool
:param static: Whether to display the tooltip with static
position. When the position is set to static, the balloon
will always appear an offset from the bottom right corner of
the widget.
:type static: bool
:param kwargs: keyword arguments passed on to the :class:`ttk.Frame` initializer
"""
ttk.Frame.__init__(self, master, **kwargs)
Expand All @@ -51,11 +60,16 @@ def __init__(self, master=None, headertext="Help", text="Some great help is disp
self.__headertext = headertext
self.__text = text
self.__width = width
self.__offset = offset
self.__showheader = showheader
self.__static = static

self.master = master
self._id = None
self._timeout = timeout
self.master.bind("<Enter>", self._on_enter)
self.master.bind("<Leave>", self._on_leave)
self.master.bind("<ButtonPress>", self._on_leave)

def __getitem__(self, key):
return self.cget(key)
Expand All @@ -66,7 +80,8 @@ def __setitem__(self, key, value):
def _grid_widgets(self):
"""Place the widgets in the Toplevel."""
self._canvas.grid(sticky="nswe")
self.header_label.grid(row=1, column=1, sticky="nswe", pady=5, padx=5)
if self.__showheader is True:
self.header_label.grid(row=1, column=1, sticky="nswe", pady=5, padx=5)
self.text_label.grid(row=3, column=1, sticky="nswe", pady=6, padx=5)

def _on_enter(self, event):
Expand All @@ -84,9 +99,10 @@ def _on_leave(self, event):

def show(self):
"""
Create the Toplevel widget and its child widgets to show in the spot of the cursor.
Create the Toplevel and its children to show near the cursor
This is the callback for the delayed :obj:`<Enter>` event (see :meth:`~Balloon._on_enter`).
This is the callback for the delayed :obj:`<Enter>` event
(see :meth:`~Balloon._on_enter`).
"""
self._toplevel = tk.Toplevel(self.master)
self._canvas = tk.Canvas(self._toplevel, background=self.__background)
Expand All @@ -97,11 +113,17 @@ def show(self):
self._toplevel.attributes("-topmost", True)
self._toplevel.overrideredirect(True)
self._grid_widgets()
x, y = self.master.winfo_pointerxy()
if self.__static is True:
x, y = self.master.winfo_rootx(), self.master.winfo_rooty()
w, h = self.master.winfo_width(), self.master.winfo_height()
x, y = x + w, y + h
else:
x, y = self.master.winfo_pointerxy()
self._canvas.update()
# Update the Geometry of the Toplevel to update its position and size
self._toplevel.geometry("{0}x{1}+{2}+{3}".format(self._canvas.winfo_width(), self._canvas.winfo_height(),
x + 2, y + 2))
self._toplevel.geometry("{0}x{1}+{2}+{3}".format(
self._canvas.winfo_width(), self._canvas.winfo_height(),
x + self.__offset[0], y + self.__offset[1]))

def cget(self, key):
"""
Expand All @@ -111,7 +133,8 @@ def cget(self, key):
:type key: str
:return: value of the option
To get the list of options for this widget, call the method :meth:`~Balloon.keys`.
To get the list of options for this widget, call the method
:meth:`~Balloon.keys`.
"""
if key == "headertext":
return self.__headertext
Expand All @@ -123,21 +146,31 @@ def cget(self, key):
return self._timeout
elif key == "background":
return self.__background
elif key == "offset":
return self.__offset
elif key == "showheader":
return self.__showheader
elif key == "static":
return self.__static
else:
return ttk.Frame.cget(self, key)

def config(self, **kwargs):
"""
Configure resources of the widget.
To get the list of options for this widget, call the method :meth:`~Balloon.keys`.
See :meth:`~Balloon.__init__` for a description of the widget specific option.
To get the list of options for this widget, call the method
:meth:`~Balloon.keys`. See :meth:`~Balloon.__init__` for a
description of the widget specific option.
"""
self.__headertext = kwargs.pop("headertext", self.__headertext)
self.__text = kwargs.pop("text", self.__text)
self.__width = kwargs.pop("width", self.__width)
self._timeout = kwargs.pop("timeout", self._timeout)
self.__background = kwargs.pop("background", self.__background)
self.__offset = kwargs.pop("offset", self.__offset)
self.__showheader = kwargs.pop("showheader", self.__showheader)
self.__static = kwargs.pop("static", self.__static)
if self._toplevel:
self._on_leave(None)
self.show()
Expand All @@ -147,5 +180,5 @@ def config(self, **kwargs):

def keys(self):
keys = ttk.Frame.keys(self)
keys.extend(["headertext", "text", "width", "timeout", "background"])
keys.extend(["headertext", "text", "width", "timeout", "background", "offset", "showheader", "static"])
return keys
15 changes: 14 additions & 1 deletion ttkwidgets/utilities.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# Copyright (c) RedFantom 2017
"""
Author: The ttkwidgets authors
License: GNU GPLv3
Source: The ttkwidgets repository
"""
import os
from PIL import Image, ImageTk

Expand All @@ -9,3 +13,12 @@ def get_assets_directory():

def open_icon(icon_name):
return ImageTk.PhotoImage(Image.open(os.path.join(get_assets_directory(), icon_name)))


def parse_geometry_string(string):
"""Parse a Tkinter geometry string ('XxY+W+H') into a box tuple"""
e = string.split("x")
w = int(e[0])
e = e[1].split("+")
h, x, y = map(int, e)
return x, y, w, h

0 comments on commit acb372d

Please sign in to comment.