tkinter-tooltip
Advanced tools
| { | ||
| "python.analysis.typeCheckingMode": "basic", | ||
| "python.testing.pytestArgs": [ | ||
| "test" | ||
| ], | ||
| "python.testing.unittestEnabled": false, | ||
| "python.testing.pytestEnabled": true | ||
| } |
| import time | ||
| import tkinter as tk | ||
| import tkinter.ttk as ttk | ||
| from itertools import product | ||
| from tkinter import font as tkFont | ||
| from tktooltip import ToolTip | ||
| def custom_font(frame, **kwargs): | ||
| return tkFont.Font(frame, **kwargs) | ||
| def main(): | ||
| root = tk.Tk() | ||
| s = ttk.Style() | ||
| s.configure("custom.TButton", foreground="#208020", background="#fafafa") | ||
| # root.tk.call("source", "themes/sun-valley/sun-valley.tcl") | ||
| # root.tk.call("set_theme", "dark") | ||
| btn_list = [] | ||
| for i, j in list(product(range(2), range(4))): | ||
| text = f"delay={i}s\n" | ||
| delay = i | ||
| if j >= 2: | ||
| follow = True | ||
| text += "follow tooltip: \u2714\n" # yes | ||
| else: | ||
| follow = False | ||
| text += "follow tooltip: \u274C\n" # no | ||
| if j % 2 == 0: | ||
| msg = time.asctime | ||
| text += "tooltip: Function" | ||
| else: | ||
| msg = f"Button at {str((i, j))}" | ||
| text += "tooltip: String" | ||
| btn_list.append(ttk.Button(root, text=text, style="custom.TButton")) | ||
| ToolTip( | ||
| btn_list[-1], | ||
| msg=msg, | ||
| follow=follow, | ||
| delay=delay, | ||
| # Parent frame arguments | ||
| parent_kwargs={"bg": "black", "padx": 5, "pady": 5}, | ||
| # These are arguments to the tk.Message | ||
| fg="#202020", | ||
| bg="#fafafa", | ||
| padx=10, | ||
| pady=10, | ||
| font=custom_font(root, size=8, weight=tkFont.BOLD), | ||
| ) | ||
| btn_list[-1].grid(row=i, column=j, sticky="nsew", ipadx=20, ipady=20) | ||
| root.mainloop() | ||
| if __name__ == "__main__": | ||
| main() |
| import tkinter as tk | ||
| from tktooltip import ToolTip # Assuming tooltip.py is in the same directory | ||
| class Application(tk.Tk): | ||
| def __init__(self): | ||
| super().__init__() | ||
| self.iter = 0 | ||
| # Create a label with a tooltip | ||
| self.label = tk.Label(self, text="Hover over me!") | ||
| self.label.pack(padx=10, pady=10) | ||
| self.tooltip = ToolTip(self.label, msg=f"Hello {self.iter}") | ||
| # Create a button that destroys the tooltip | ||
| self.destroy_b = tk.Button( | ||
| self, text="Destroy Tooltip", command=self.destroy_tooltip | ||
| ) | ||
| self.destroy_b.pack(padx=10, pady=10) | ||
| self.create_b = tk.Button( | ||
| self, text="Create Tooltip", command=self.create_tooltip | ||
| ) | ||
| self.create_b.pack(padx=10, pady=10) | ||
| def destroy_tooltip(self): | ||
| # Destroy the tooltip | ||
| if self.tooltip is not None: | ||
| self.tooltip.destroy() | ||
| self.tooltip = None | ||
| def create_tooltip(self): | ||
| # Create the tooltip | ||
| print(type(self.tooltip)) | ||
| print(self.tooltip) | ||
| print("===========") | ||
| for c in list(self.children.values()): | ||
| print(c) | ||
| print("***********") | ||
| if self.tooltip is None: | ||
| self.iter += 1 | ||
| self.tooltip = ToolTip(self.label, msg=f"Hello {self.iter}") | ||
| if __name__ == "__main__": | ||
| app = Application() | ||
| app.mainloop() |
@@ -45,3 +45,3 @@ # For most projects, this workflow file will not need changing; you simply need | ||
| - name: Initialize CodeQL | ||
| uses: github/codeql-action/init@v2 | ||
| uses: github/codeql-action/init@v3 | ||
| with: | ||
@@ -57,3 +57,3 @@ languages: ${{ matrix.language }} | ||
| - name: Autobuild | ||
| uses: github/codeql-action/autobuild@v2 | ||
| uses: github/codeql-action/autobuild@v3 | ||
@@ -72,2 +72,2 @@ # ℹ️ Command-line programs to run using the OS shell. | ||
| - name: Perform CodeQL Analysis | ||
| uses: github/codeql-action/analyze@v2 | ||
| uses: github/codeql-action/analyze@v3 |
@@ -13,3 +13,3 @@ # See https://pre-commit.com for more information | ||
| - repo: https://github.com/PyCQA/flake8 | ||
| rev: 6.1.0 | ||
| rev: 7.0.0 | ||
| hooks: | ||
@@ -22,3 +22,3 @@ - id: flake8 | ||
| - repo: https://github.com/pycqa/isort | ||
| rev: 5.13.0 | ||
| rev: 5.13.2 | ||
| hooks: | ||
@@ -28,4 +28,4 @@ - id: isort | ||
| - repo: https://github.com/psf/black | ||
| rev: 23.11.0 | ||
| rev: 24.1.1 | ||
| hooks: | ||
| - id: black |
| import re | ||
| import tkinter as tk | ||
| from pathlib import Path | ||
| from tkinter import ttk | ||
@@ -23,3 +24,3 @@ | ||
| self.color = "0,0,0" # Default color to replace (it's black) | ||
| img = "tooltip_logo.png" # image name to loads | ||
| img = Path(__file__).parent / "tooltip_logo.png" # image name to loads | ||
| self.img = Image.open(img).resize((100, 100)) # Load and scale image | ||
@@ -26,0 +27,0 @@ self.img_tk = ImageTk.PhotoImage(self.img) # Store to stop garbage collection |
+9
-1
| Metadata-Version: 2.1 | ||
| Name: tkinter-tooltip | ||
| Version: 2.2.0 | ||
| Version: 3.0.0 | ||
| Summary: An easy and customisable ToolTip implementation for Tkinter | ||
@@ -63,2 +63,3 @@ Home-page: https://github.com/gnikit/tkinter-tooltip | ||
| [](https://github.com/psf/black) | ||
| <!-- [](https://www.codefactor.io/repository/github/gnikit/tkinter-tooltip) --> | ||
@@ -207,2 +208,9 @@ | ||
| ## Notes | ||
| - Certain options do not match great with each other, a good example is `follow` | ||
| and `delay` using small x/y offsets. This can cause the tooltip to appear | ||
| inside the widget. Hovering over the tooltip will cause it to disappear and | ||
| reappear, in a new position, potentially again inside the widget. | ||
| ## Contributing | ||
@@ -209,0 +217,0 @@ |
+8
-0
@@ -10,2 +10,3 @@ <div align="center"> | ||
| [](https://github.com/psf/black) | ||
| <!-- [](https://www.codefactor.io/repository/github/gnikit/tkinter-tooltip) --> | ||
@@ -154,2 +155,9 @@ | ||
| ## Notes | ||
| - Certain options do not match great with each other, a good example is `follow` | ||
| and `delay` using small x/y offsets. This can cause the tooltip to appear | ||
| inside the widget. Hovering over the tooltip will cause it to disappear and | ||
| reappear, in a new position, potentially again inside the widget. | ||
| ## Contributing | ||
@@ -156,0 +164,0 @@ |
@@ -6,3 +6,3 @@ import tkinter as tk | ||
| from tktooltip import ToolTip | ||
| from tktooltip import ToolTip, ToolTipStatus | ||
@@ -67,2 +67,3 @@ | ||
| (lambda: "Callable Tooltip"), | ||
| (lambda: ["Callable Tooltip 1", "Callable Tooltip 2"]), | ||
| "text", | ||
@@ -77,8 +78,19 @@ (["text 1", "text 2"]), | ||
| tooltip = ToolTip(widget, msg=msg) | ||
| assert tooltip.status == "outside" | ||
| assert tooltip.status == ToolTipStatus.OUTSIDE | ||
| widget.event_generate("<Enter>") | ||
| assert tooltip.status == "inside" | ||
| assert tooltip.status == ToolTipStatus.INSIDE | ||
| tooltip._show() | ||
| assert tooltip.status == "visible" | ||
| assert tooltip.status == ToolTipStatus.VISIBLE | ||
| widget.event_generate("<Leave>") | ||
| assert tooltip.status == "outside" | ||
| assert tooltip.status == ToolTipStatus.OUTSIDE | ||
| def test_tooltip_destroy(widget: tk.Widget): | ||
| tooltip = ToolTip(widget, msg="Test") | ||
| widget.event_generate("<Enter>") | ||
| assert tooltip.status == ToolTipStatus.INSIDE | ||
| tooltip._show() | ||
| assert tooltip.status == ToolTipStatus.VISIBLE | ||
| tooltip.destroy() | ||
| print(tooltip.bindigs) | ||
| assert tooltip.bindigs == [] |
| Metadata-Version: 2.1 | ||
| Name: tkinter-tooltip | ||
| Version: 2.2.0 | ||
| Version: 3.0.0 | ||
| Summary: An easy and customisable ToolTip implementation for Tkinter | ||
@@ -63,2 +63,3 @@ Home-page: https://github.com/gnikit/tkinter-tooltip | ||
| [](https://github.com/psf/black) | ||
| <!-- [](https://www.codefactor.io/repository/github/gnikit/tkinter-tooltip) --> | ||
@@ -207,2 +208,9 @@ | ||
| ## Notes | ||
| - Certain options do not match great with each other, a good example is `follow` | ||
| and `delay` using small x/y offsets. This can cause the tooltip to appear | ||
| inside the widget. Hovering over the tooltip will cause it to disappear and | ||
| reappear, in a new position, potentially again inside the widget. | ||
| ## Contributing | ||
@@ -209,0 +217,0 @@ |
@@ -17,2 +17,3 @@ .coveragerc | ||
| .github/workflows/python-publish.yml | ||
| .vscode/settings.json | ||
| assets/animations/color-changer.gif | ||
@@ -41,2 +42,4 @@ assets/animations/color-changer.mp4 | ||
| examples/color_changer.py | ||
| examples/matrix.py | ||
| examples/tooltip_destroy.py | ||
| examples/tooltip_logo.png | ||
@@ -51,4 +54,3 @@ test/test_tktooltip.py | ||
| tktooltip/_version.py | ||
| tktooltip/demo.py | ||
| tktooltip/tooltip.py | ||
| tktooltip/version.py |
@@ -16,3 +16,3 @@ """ | ||
| from .tooltip import ToolTip | ||
| from .tooltip import ToolTip, ToolTipStatus | ||
| from .version import __version__ | ||
@@ -22,3 +22,4 @@ | ||
| "ToolTip", | ||
| "ToolTipStatus", | ||
| "__version__", | ||
| ] |
@@ -15,3 +15,3 @@ # file generated by setuptools_scm | ||
| __version__ = version = '2.2.0' | ||
| __version_tuple__ = version_tuple = (2, 2, 0) | ||
| __version__ = version = '3.0.0' | ||
| __version_tuple__ = version_tuple = (3, 0, 0) |
+94
-52
| """ | ||
| Module defining the ToolTip widget | ||
| """ | ||
| from __future__ import annotations | ||
@@ -8,2 +9,4 @@ | ||
| import tkinter as tk | ||
| from contextlib import suppress | ||
| from enum import Enum, auto | ||
| from typing import Any, Callable | ||
@@ -15,2 +18,18 @@ | ||
| class ToolTipStatus(Enum): | ||
| OUTSIDE = auto() | ||
| INSIDE = auto() | ||
| VISIBLE = auto() | ||
| class Binding: | ||
| def __init__(self, widget: tk.Widget, binding_name: str, functor: Callable) -> None: | ||
| self._widget = widget | ||
| self._name: str = binding_name | ||
| self._id: str = self._widget.bind(binding_name, functor, add="+") | ||
| def unbind(self) -> None: | ||
| self._widget.unbind(self._name, self._id) | ||
| class ToolTip(tk.Toplevel): | ||
@@ -21,2 +40,6 @@ """ | ||
| DEFAULT_PARENT_KWARGS = {"bg": "black", "padx": 1, "pady": 1} | ||
| DEFAULT_MESSAGE_KWARGS = {"aspect": 1000} | ||
| S_TO_MS = 1000 | ||
| def __init__( | ||
@@ -31,3 +54,3 @@ self, | ||
| y_offset: int = +10, | ||
| parent_kwargs: dict[Any, Any] = {"bg": "black", "padx": 1, "pady": 1}, | ||
| parent_kwargs: dict | None = None, | ||
| **message_kwargs: Any, | ||
@@ -65,3 +88,3 @@ ): | ||
| # otherwise in the `parent_kwargs` | ||
| tk.Toplevel.__init__(self, **parent_kwargs) | ||
| tk.Toplevel.__init__(self, **(parent_kwargs or self.DEFAULT_PARENT_KWARGS)) | ||
| self.withdraw() # Hide initially in case there is a delay | ||
@@ -72,14 +95,5 @@ # Disable ToolTip's title bar | ||
| # StringVar instance for msg string|function | ||
| self.msgVar = tk.StringVar() | ||
| # This can be a string or a function | ||
| if not ( | ||
| callable(msg) | ||
| or (isinstance(msg, str)) | ||
| or (isinstance(msg, list) and all(isinstance(m, str) for m in msg)) | ||
| ): | ||
| raise TypeError( | ||
| "Error: ToolTip `msg` must be a string, list of strings or string " | ||
| + f"returning function instead `msg` of type {type(msg)} was input" | ||
| ) | ||
| self.msg_var = tk.StringVar() | ||
| self.msg = msg | ||
| self._update_message() | ||
| self.delay = delay | ||
@@ -91,13 +105,36 @@ self.follow = follow | ||
| # visibility status of the ToolTip inside|outside|visible | ||
| self.status = "outside" | ||
| self.status = ToolTipStatus.OUTSIDE | ||
| self.last_moved = 0 | ||
| # use Message widget to host ToolTip | ||
| tk.Message(self, textvariable=self.msgVar, aspect=1000, **message_kwargs).grid() | ||
| # Add bindings to the widget without overriding the existing ones | ||
| self.widget.bind("<Enter>", self.on_enter, add="+") | ||
| self.widget.bind("<Leave>", self.on_leave, add="+") | ||
| self.widget.bind("<Motion>", self.on_enter, add="+") | ||
| self.widget.bind("<ButtonPress>", self.on_leave, add="+") | ||
| self.message_kwargs: dict = message_kwargs or self.DEFAULT_MESSAGE_KWARGS | ||
| self.message_widget = tk.Message( | ||
| self, | ||
| textvariable=self.msg_var, | ||
| **self.message_kwargs, | ||
| ) | ||
| self.message_widget.grid() | ||
| self.bindigs = self._init_bindings() | ||
| def on_enter(self, event) -> None: | ||
| def _init_bindings(self) -> list[Binding]: | ||
| """Initialize the bindings.""" | ||
| bindings = [ | ||
| Binding(self.widget, "<Enter>", self.on_enter), | ||
| Binding(self.widget, "<Leave>", self.on_leave), | ||
| Binding(self.widget, "<ButtonPress>", self.on_leave), | ||
| ] | ||
| if self.follow: | ||
| bindings.append( | ||
| Binding(self.widget, "<Motion>", self._update_tooltip_coords) | ||
| ) | ||
| return bindings | ||
| def destroy(self) -> None: | ||
| """Destroy the ToolTip and unbind all the bindings.""" | ||
| with suppress(tk.TclError): | ||
| for b in self.bindigs: | ||
| b.unbind() | ||
| self.bindigs.clear() | ||
| super().destroy() | ||
| def on_enter(self, event: tk.Event) -> None: | ||
| """ | ||
@@ -107,26 +144,36 @@ Processes motion within the widget including entering and moving. | ||
| self.last_moved = time.time() | ||
| self.status = ToolTipStatus.INSIDE | ||
| self._update_tooltip_coords(event) | ||
| self.after(int(self.delay * self.S_TO_MS), self._show) | ||
| # Set the status as inside for the very first time | ||
| if self.status == "outside": | ||
| self.status = "inside" | ||
| # If the follow flag is not set, motion within the widget will | ||
| # make the ToolTip dissapear | ||
| if not self.follow: | ||
| self.status = "inside" | ||
| self.withdraw() | ||
| # Offsets the ToolTip using the coordinates od an event as an origin | ||
| self.geometry(f"+{event.x_root + self.x_offset}+{event.y_root + self.y_offset}") | ||
| # Time is integer and in milliseconds | ||
| self.after(int(self.delay * 1000), self._show) | ||
| def on_leave(self, event=None) -> None: | ||
| def on_leave(self, event: tk.Event | None = None) -> None: | ||
| """ | ||
| Hides the ToolTip. | ||
| """ | ||
| self.status = "outside" | ||
| self.status = ToolTipStatus.OUTSIDE | ||
| self.withdraw() | ||
| def _update_tooltip_coords(self, event: tk.Event) -> None: | ||
| """ | ||
| Updates the ToolTip's position. | ||
| """ | ||
| self.geometry(f"+{event.x_root + self.x_offset}+{event.y_root + self.y_offset}") | ||
| def _update_message(self) -> None: | ||
| """Update the message displayed in the tooltip.""" | ||
| if callable(self.msg): | ||
| msg = self.msg() | ||
| if isinstance(msg, list): | ||
| msg = "\n".join(msg) | ||
| elif isinstance(self.msg, str): | ||
| msg = self.msg | ||
| elif isinstance(self.msg, list): | ||
| msg = "\n".join(self.msg) | ||
| else: | ||
| raise TypeError( | ||
| f"ToolTip `msg` must be a string, list of strings, or a " | ||
| f"callable returning them, not {type(self.msg)}." | ||
| ) | ||
| self.msg_var.set(msg) | ||
| def _show(self) -> None: | ||
@@ -138,15 +185,10 @@ """ | ||
| """ | ||
| if self.status == "inside" and time.time() - self.last_moved > self.delay: | ||
| self.status = "visible" | ||
| if ( | ||
| self.status == ToolTipStatus.INSIDE | ||
| and time.time() - self.last_moved > self.delay | ||
| ): | ||
| self.status = ToolTipStatus.VISIBLE | ||
| if self.status == "visible": | ||
| # Update the string with the latest function call | ||
| if callable(self.msg): | ||
| self.msgVar.set(self.msg()) | ||
| # Update the string with the latest string | ||
| elif isinstance(self.msg, str): | ||
| self.msgVar.set(self.msg) | ||
| # Update the string with the latest list | ||
| elif isinstance(self.msg, list): | ||
| self.msgVar.set("\n".join(self.msg)) | ||
| if self.status == ToolTipStatus.VISIBLE: | ||
| self._update_message() | ||
| self.deiconify() | ||
@@ -157,2 +199,2 @@ | ||
| # that in turn changes the `status` to outside | ||
| self.after(int(self.refresh * 1000), self._show) | ||
| self.after(int(self.refresh * self.S_TO_MS), self._show) |
| import time | ||
| import tkinter as tk | ||
| import tkinter.ttk as ttk | ||
| from itertools import product | ||
| from tkinter import font as tkFont | ||
| from tooltip import ToolTip | ||
| def custom_font(frame, **kwargs): | ||
| return tkFont.Font(frame, **kwargs) | ||
| def main(): | ||
| root = tk.Tk() | ||
| s = ttk.Style() | ||
| s.configure("custom.TButton", foreground="#208020", background="#fafafa") | ||
| # root.tk.call("source", "themes/sun-valley/sun-valley.tcl") | ||
| # root.tk.call("set_theme", "dark") | ||
| btn_list = [] | ||
| for i, j in list(product(range(2), range(4))): | ||
| text = f"delay={i}s\n" | ||
| delay = i | ||
| if j >= 2: | ||
| follow = True | ||
| text += "follow tooltip: \u2714\n" # yes | ||
| else: | ||
| follow = False | ||
| text += "follow tooltip: \u274C\n" # no | ||
| if j % 2 == 0: | ||
| msg = time.asctime | ||
| text += "tooltip: Function" | ||
| else: | ||
| msg = f"Button at {str((i, j))}" | ||
| text += "tooltip: String" | ||
| btn_list.append(ttk.Button(root, text=text, style="custom.TButton")) | ||
| ToolTip( | ||
| btn_list[-1], | ||
| msg=msg, | ||
| follow=follow, | ||
| delay=delay, | ||
| # Parent frame arguments | ||
| parent_kwargs={"bg": "black", "padx": 5, "pady": 5}, | ||
| # These are arguments to the tk.Message | ||
| fg="#202020", | ||
| bg="#fafafa", | ||
| padx=10, | ||
| pady=10, | ||
| font=custom_font(root, size=8, weight=tkFont.BOLD), | ||
| ) | ||
| btn_list[-1].grid(row=i, column=j, sticky="nsew", ipadx=20, ipady=20) | ||
| root.mainloop() | ||
| if __name__ == "__main__": | ||
| main() |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
993379
0.41%54
3.85%550
20.35%