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

Merge tooltip into master to replace Balloon with Tooltip #51

Merged
merged 5 commits into from
Apr 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This file contains a list of all the authors of widgets in this repository. Plea
* `ScrolledListbox`
* `FontChooser`, based on an idea by [Nelson Brochado](https://www.github.com/nbro)
* `FontSelectFrame`
* `Balloon`
* `Tooltip`
* `ItemsCanvas`
* `TimeLine`
- The Python Team
Expand Down
2 changes: 1 addition & 1 deletion docs/source/authors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ List of all the authors of widgets in this repository. Please note that this lis
* :class:`~ttkwidgets.ScrolledListbox`
* :class:`~ttkwidgets.font.FontChooser`, based on an idea by `Nelson Brochado <https://www.github.com/nbro>`_
* :class:`~ttkwidgets.font.FontSelectFrame`
* :class:`~ttkwidgets.frames.Balloon`
* :class:`~ttkwidgets.frames.Tooltip`
* :class:`~ttkwidgets.ItemsCanvas`
* :class:`~ttkwidgets.TimeLine`

Expand Down
2 changes: 1 addition & 1 deletion docs/source/ttkwidgets/ttkwidgets.frames.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ ttkwidgets.frames
:nosignatures:
:toctree: ttkwidgets.frames

Balloon
Tooltip
ScrolledFrame
ToggledFrame
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
Balloon
Tooltip
=======

.. currentmodule:: ttkwidgets.frames

.. autoclass:: Balloon
.. autoclass:: Tooltip
:show-inheritance:
:members:

.. automethod:: __init__
.. automethod:: __init__
5 changes: 2 additions & 3 deletions examples/example_balloon.py → examples/example_tooltip.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@

# Copyright (c) RedFantom 2017
# For license see LICENSE
from ttkwidgets.frames import Balloon
from ttkwidgets.frames import Tooltip
import tkinter as tk
from tkinter import ttk


window = tk.Tk()
button = tk.Button(window, text="Button", command=window.destroy)
button.pack()
balloon = Balloon(button)
balloon = Tooltip(button)
window.mainloop()
55 changes: 41 additions & 14 deletions tests/test_balloon.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,39 @@
# Copyright (c) RedFantom 2017
# For license see LICENSE
from ttkwidgets.frames import Balloon
"""
Author: RedFantom
License: GNU GPLv3
Source: This repository
"""
from ttkwidgets.frames import Tooltip
from ttkwidgets.utilities import parse_geometry_string
from tests import BaseWidgetTest
import tkinter as tk
from time import sleep


class TestBalloon(BaseWidgetTest):
class TestTooltip(BaseWidgetTest):
def test_balloon_init(self):
balloon = Balloon(self.window)
balloon = Tooltip(self.window)
self.window.update()

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")
balloon = Tooltip(self.window, headertext="Help", text="This is a test for the Tooltip widget.", width=300,
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("text"), "This is a test for the Tooltip widget.")
self.assertEqual(balloon.cget("width"), 300)
self.assertEqual(balloon.cget("timeout"), 2)
self.assertEqual(balloon.cget("background"), "white")

balloon.config(headertext="New Help", text="This is another test for the Balloon widget.", width=400,
balloon.config(headertext="New Help", text="This is another test for the Tooltip widget.", width=400,
timeout=3, background="black")
self.assertEqual(balloon["headertext"], "New Help")
self.assertEqual(balloon["text"], "This is another test for the Balloon widget.")
self.assertEqual(balloon["text"], "This is another test for the Tooltip widget.")
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 @@ -38,8 +45,29 @@ 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)
balloon = Tooltip(self.window)
self.window.update()
balloon.show()
self.window.update()
Expand All @@ -51,7 +79,7 @@ def test_balloon_show(self):
self.assertIs(balloon._toplevel, None)

def test_balloon_events(self):
balloon = Balloon(self.window, timeout=0.2)
balloon = Tooltip(self.window, timeout=0.2)
balloon._on_enter(None)
self.window.update()
sleep(0.3)
Expand All @@ -61,7 +89,6 @@ def test_balloon_events(self):
self.assertIs(balloon._toplevel, None)

def test_balloon_events_noshow(self):
balloon = Balloon(self.window)
balloon = Tooltip(self.window)
balloon._on_enter(None)
balloon._on_leave(None)

2 changes: 1 addition & 1 deletion ttkwidgets/frames/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
# Available under the license found in LICENSE
from .scrolledframe import ScrolledFrame
from .toggledframe import ToggledFrame
from .balloon import Balloon
from .tooltip import Tooltip
74 changes: 56 additions & 18 deletions ttkwidgets/frames/balloon.py → ttkwidgets/frames/tooltip.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,35 @@
from ttkwidgets.utilities import get_assets_directory


class Balloon(ttk.Frame):
class Tooltip(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 Tooltip

:param master: widget to bind the Balloon to
:param master: widget to bind the Tooltip to
:type master: widget
:param headertext: text to show in window header
:type headertext: str
:param text: text to show as help text
:type text: str
:param width: width of the window
:type width: int
:param timeout: timeout in seconds to wait until the Balloon is shown
:param timeout: timeout in seconds to wait until the Tooltip is shown
:type timeout: float
:param background: background color of the Balloon
:param background: background color of the Tooltip
: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 @@ -47,11 +56,21 @@ 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._bind_to_master()

def _bind_to_master(self):
"""Bind the Balloon widget to the master widget's events"""
self.master.bind("<Enter>", self._on_enter, "add")
self.master.bind("<Leave>", self._on_leave, "add")
self.master.bind("<ButtonPress>", self._on_leave, "add")

def __getitem__(self, key):
return self.cget(key)
Expand All @@ -62,7 +81,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 @@ -80,9 +100,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:`~Tooltip._on_enter`).
"""
self._toplevel = tk.Toplevel(self.master)
self._canvas = tk.Canvas(self._toplevel, background=self.__background)
Expand All @@ -93,11 +114,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 @@ -107,7 +134,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:`~Tooltip.keys`.
"""
if key == "headertext":
return self.__headertext
Expand All @@ -119,21 +147,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:`~Tooltip.keys`. See :meth:`~Tooltip.__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 @@ -143,5 +181,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