From 920de93cce86145e1b03dcba7aae6264e0e18d74 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Tue, 13 Feb 2018 17:36:52 +0100 Subject: [PATCH 01/13] Create VNotebook widget --- ttkwidgets/frames/__init__.py | 1 + ttkwidgets/frames/vnotebook.py | 321 +++++++++++++++++++++++++++++++++ 2 files changed, 322 insertions(+) create mode 100644 ttkwidgets/frames/vnotebook.py diff --git a/ttkwidgets/frames/__init__.py b/ttkwidgets/frames/__init__.py index befc0fd6..1ad33fcd 100644 --- a/ttkwidgets/frames/__init__.py +++ b/ttkwidgets/frames/__init__.py @@ -3,3 +3,4 @@ from .scrolledframe import ScrolledFrame from .toggledframe import ToggledFrame from .balloon import Balloon +from .vnotebook import VNotebook diff --git a/ttkwidgets/frames/vnotebook.py b/ttkwidgets/frames/vnotebook.py new file mode 100644 index 00000000..77fec359 --- /dev/null +++ b/ttkwidgets/frames/vnotebook.py @@ -0,0 +1,321 @@ +""" +Author: RedFantom +License: GNU GPLv3, as in LICENSE.md +Copyright (C) 2018 RedFantom +""" +# Basic UI imports +import tkinter as tk +from tkinter import ttk +from tkinter.font import Font + + +class VNotebook(ttk.Frame): + """ + Notebook with Vertical tabs. Does not actually use the Notebook + widget, but a set of Toolbutton-styles Radiobuttons to select a + Frame of a set. Provides an interface that functions the same as a + normal Notebook widget. + """ + + options = [ + # Notebook options + "cursor", + "padding", + "style", + "takefocus", + # VNotebook options + "compound", + "callback", + ] + + tab_options = [ + "compound", + "padding", + "sticky", + "image", + "text", + "underline", + "font", + "id", + ] + + def __init__(self, master, **kwargs): + """ + :param cursor: Cursor set upon hovering Buttons + :param padding: Amount of pixels between the Buttons + :param compound: Location of the Buttons + :param callback: Callback called upon change of Frame + :param kwargs: Passed on to ttk.Frame initializer + """ + # Argument processing + self._cursor = kwargs.pop("cursor", "default") + self._height = kwargs.pop("cursor", 400) + self._width = kwargs.pop("width", 400) + self._padding = kwargs.pop("padding", 0) + self._style = kwargs.pop("style", "Toolbutton") + self._compound = kwargs.pop("compound", tk.LEFT) + + kwargs["width"] = self._width + kwargs["height"] = self._height + + # Initialize + ttk.Frame.__init__(self, master, **kwargs) + self.grid_propagate(False) + + # Attributes + self._tab_buttons = dict() + self._tab_frames = dict() + self._tab_ids = list() + self._frame_padding = dict() + self._hidden = dict() + self._variable = tk.StringVar() + self.__current_tab = None + self._buttons_frame = None + self._separator = None + + # Initialize widgets + self.init_widgets() + self.grid_widgets() + + def init_widgets(self): + """Initialize child widgets""" + self._buttons_frame = ttk.Frame(self) + self._separator = ttk.Separator(self) + + def grid_widgets(self): + """Put child widgets in place""" + horizontal = self._compound in (tk.BOTTOM, tk.TOP) + if horizontal is True: + sticky = "sw" if self._compound == tk.BOTTOM else "nw" + self.columnconfigure(2, weight=1) + else: + sticky = "nw" if self._compound == tk.RIGHT else "ne" + self.rowconfigure(2, weight=1) + + self._buttons_frame.grid(row=2, column=2, sticky=sticky) + self._separator.config( + orient=tk.HORIZONTAL if horizontal is True else tk.VERTICAL) + if self.active is None: + return + + # Grid the position dependent widgets + pad, sticky = self._frame_padding[self.active] + padding = {"padx": pad, "pady": pad} + if self._compound == tk.BOTTOM: + self._separator.grid(row=3, column=2, pady=4, sticky="swe") + self.__current_tab.grid(row=4, column=2, sticky=sticky, **padding) + elif self._compound == tk.TOP: + self._separator.grid(row=1, column=2, pady=4, sticky="nwe") + self.__current_tab.grid(row=0, column=2, sticky=sticky, **padding) + elif self._compound == tk.RIGHT: + self._separator.grid(row=2, column=3, padx=4, sticky="nsw") + self.__current_tab.grid(row=2, column=4, sticky=sticky, **padding) + elif self._compound == tk.LEFT: + self._separator.grid(row=2, column=1, padx=4, sticky="nse") + self.__current_tab.grid(row=2, column=0, sticky=sticky, **padding) + else: + raise ValueError("Invalid compound value: {}".format(self._compound)) + self._grid_tabs() + + def grid_forget_widgets(self): + """Remove child widgets from grid""" + self._buttons_frame.grid_forget() + if self.__current_tab is not None: + self.__current_tab.grid_forget() + for button in self._tab_buttons.values(): + button.grid_forget() + return + + def _grid_tabs(self): + """Organize tab buttons""" + for button in self._tab_buttons.values(): + button.grid_forget() + for index, tab_id in enumerate(self._tab_ids): + horizontal = self._compound in (tk.BOTTOM, tk.TOP) + row = index if horizontal is False else 0 + column = index if horizontal is True else 0 + self._tab_buttons[tab_id].grid( + row=row, column=column, pady=self._padding, padx=self._padding, sticky="nswe") + if self.active is None and len(self._tab_ids) != 0: + self.activate(self._tab_ids[0]) + return + + def add(self, child, **kwargs): + """ + Create new tab in the notebook and append it to the end. + :param child: Child widget, such as a ttk.Frame + :param kwargs: Keyword arguments to create tab with. Supports + all arguments supported by tab() function, and in addition + supports: + :param id: ID for the newly added Tab. If the ID is not + given, one is generated automatically. + :param where: Position of the new Notebook + :return: ID for the new Tab + """ + tab_id = kwargs.pop("id", hash(child)) + self._tab_buttons[tab_id] = ttk.Radiobutton( + self._buttons_frame, variable=self._variable, value=tab_id) + self._tab_frames[tab_id] = child + # Process where argument + where = kwargs.pop("where", tk.END) + if where == tk.END: + self._tab_ids.append(tab_id) + else: + self._tab_ids.insert(where, tab_id) + self.tab(tab_id, **kwargs) + + def insert(self, where, child, **kwargs): + """add() alias with non-optional where argument""" + kwargs.update({"where": where}) + self.add(child, **kwargs) + + def enable_traversal(self, enable=True): + """Setup keybinds for CTRL-TAB to switch tabs""" + if enable is True: + func = "bind" + args = ("", self.__switch_tab,) + else: + func = "unbind" + args = ("",) + for widget in (self, self._buttons_frame, self._separator) + tuple(self._tab_frames.values()): + getattr(widget, func)(*args) + return enable + + def forget(self, child): + """Remove a child by widget or tab_id""" + tab_id = self.get_id_for_tab(child) + self._tab_buttons[tab_id].destroy() + del self._tab_buttons[tab_id] + del self._tab_frames[tab_id] + self._tab_ids.remove(tab_id) + self._grid_tabs() + + def hide(self, child, hide=True): + """Hide or unhide a Tab""" + pass + + def index(self, child): + """Return zero-indexed index value of a child OR tab_id""" + return self._tab_ids.index(self.get_id_for_tab(child)) + + def tab(self, tab_id, option=None, **kwargs): + """ + Configure a tab with options given in kwargs. + + :param tab_id: Non-optional tab ID of the tab to be configured + :param option: If not None, function returns value for option + key given in this argument + + :param compound: Determines position of image in the Tab Button + if an image is given + :param padding: Amount of space around the frame + :param sticky: Passed on to grid function called upon the + notebook tab frame + :param image: Image that is shown on the tab Button + :param text: Text that is shown on the tab Button + :param underline: Whether the text for the Button is underlined. + This argument should be a bool value, which does not match + the ttk.Notebook behaviour! + + :param font: Font tuple or instance passed on to Button. If + font is given, underline is ignored. + """ + if option is not None: + return self._tab_buttons[tab_id].cget(option) + # Argument processing + font = kwargs.pop("font", None) + underline = kwargs.pop("underline", False) + self._frame_padding[tab_id] = \ + (kwargs.pop("padding", 0), kwargs.pop("sticky", tk.N)) + kwargs["command"] = lambda tab_id=tab_id: self.activate(tab_id) + kwargs["style"] = "Toolbutton" + # Process underline if font argument is Font type + if isinstance(font, Font): + if underline != font.cget("underline"): + font.configure(underline=underline) + kwargs["font"] = font + # Configure Buttons + self._tab_buttons[tab_id].configure(**kwargs) + self._grid_tabs() + + def tab_configure(self, tab_id, **kwargs): + """configure alias for self.tab""" + self.tab(tab_id, **kwargs) + + def tab_cget(self, tab_id, key): + """cget alias for self.tab""" + self.tab(tab_id, option=key) + + def tabs(self): + """Return list of tab IDs""" + return self._tab_ids + + def config(self, **kwargs): + """Alias for self.configure""" + return self.configure(**kwargs) + + def configure(self, **kwargs): + """Change settings for the widget""" + for option in self.options: + attr = "_{}".format(option) + setattr(self, attr, kwargs.pop(option, getattr(self, attr))) + return ttk.Frame.configure(**kwargs) + + def cget(self, key): + """Return current value for a setting""" + if key in self.options: + return getattr(self, "_{}".format(key)) + return ttk.Frame.cget(self, key) + + def __getitem__(self, item): + return self.cget(item) + + def __setitem__(self, key, value): + return self.configure(**{key: value}) + + def activate(self, tab_id): + """Activate a new Tab in the Notebook""" + if self.active is not None: + self.__current_tab.grid_forget() + self.__current_tab = self._tab_frames[tab_id] + self.grid_widgets() + self._variable.set(tab_id) + + def activate_index(self, index): + """Activate Tab by zero-indexed value""" + return self.activate(self._tab_ids[index]) + + @property + def active(self): + """Return tab_id for currently active Tab""" + if self.__current_tab is None: + return None + return self.get_id_for_tab(self.__current_tab) + + def get_id_for_tab(self, child): + """Return tab_id for child, which can be tab_id or Widget""" + if child in self._tab_ids: + return child + return {widget: tab_id for tab_id, widget in self._tab_frames.items()}[child] + + def __switch_tab(self, event): + """Callback for CTRL-TAB""" + if self.active is None: + self.activate(self._tab_ids[0]) + return + to_activate = self._tab_ids.index(self.active) + 1 + if to_activate == len(self._tab_ids): + to_activate = 0 + self.activate(self._tab_ids[to_activate]) + + +if __name__ == '__main__': + from ttkthemes import ThemedTk + root = ThemedTk() + root.set_theme("radiance") + notebook = VNotebook(root, compound=tk.RIGHT) + notebook.add(ttk.Scale(notebook), text="Scale") + notebook.add(ttk.Button(notebook, text="Destroy", command=root.destroy), text="Button") + notebook.enable_traversal() + notebook.grid(row=1) + root.mainloop() From 21519faf8845d88264c3290dd40414f119844105 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Tue, 13 Feb 2018 17:41:32 +0100 Subject: [PATCH 02/13] Finish VNotebook functionality --- ttkwidgets/frames/vnotebook.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ttkwidgets/frames/vnotebook.py b/ttkwidgets/frames/vnotebook.py index 77fec359..c779eec2 100644 --- a/ttkwidgets/frames/vnotebook.py +++ b/ttkwidgets/frames/vnotebook.py @@ -131,6 +131,8 @@ def _grid_tabs(self): for button in self._tab_buttons.values(): button.grid_forget() for index, tab_id in enumerate(self._tab_ids): + if tab_id in self._hidden and self._hidden[tab_id] is True: + continue horizontal = self._compound in (tk.BOTTOM, tk.TOP) row = index if horizontal is False else 0 column = index if horizontal is True else 0 @@ -163,11 +165,12 @@ def add(self, child, **kwargs): else: self._tab_ids.insert(where, tab_id) self.tab(tab_id, **kwargs) + return tab_id def insert(self, where, child, **kwargs): """add() alias with non-optional where argument""" kwargs.update({"where": where}) - self.add(child, **kwargs) + return self.add(child, **kwargs) def enable_traversal(self, enable=True): """Setup keybinds for CTRL-TAB to switch tabs""" @@ -192,7 +195,9 @@ def forget(self, child): def hide(self, child, hide=True): """Hide or unhide a Tab""" - pass + tab_id = self.get_id_for_tab(child) + self._hidden[tab_id] = hide + self.grid_widgets() def index(self, child): """Return zero-indexed index value of a child OR tab_id""" @@ -316,6 +321,8 @@ def __switch_tab(self, event): notebook = VNotebook(root, compound=tk.RIGHT) notebook.add(ttk.Scale(notebook), text="Scale") notebook.add(ttk.Button(notebook, text="Destroy", command=root.destroy), text="Button") + id = notebook.add(ttk.Frame(notebook), text="Hidden") + notebook.hide(id) notebook.enable_traversal() notebook.grid(row=1) root.mainloop() From 906baf068e16e5ffb2de90bdd6659eae9cd11c03 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Wed, 1 Aug 2018 15:05:44 +0200 Subject: [PATCH 03/13] Fix issues with VNotebook and build tests --- tests/test_vnotebook.py | 71 ++++++++++++++++++++++++++++++++++ ttkwidgets/frames/vnotebook.py | 60 +++++++++++++--------------- 2 files changed, 97 insertions(+), 34 deletions(-) create mode 100644 tests/test_vnotebook.py diff --git a/tests/test_vnotebook.py b/tests/test_vnotebook.py new file mode 100644 index 00000000..18435134 --- /dev/null +++ b/tests/test_vnotebook.py @@ -0,0 +1,71 @@ +""" +Author: RedFantom +License: GNU GPLv3, as in LICENSE.md +Copyright (C) 2018 RedFantom +""" +from unittest import TestCase +# Basic UI imports +import tkinter as tk +from tkinter import ttk +# Module to test +from ttkwidgets.frames import VNotebook + + +class TestVNotebook(TestCase): + def setUp(self): + self.window = tk.Tk() + + def tearDown(self): + self.window.destroy() + + def test_init(self): + VNotebook(self.window).grid() + self.window.update() + + def add_test_frame(self, notebook, **kwargs): + notebook.grid() + frame = ttk.Frame(notebook) + ttk.Scale(frame).grid() + frame.grid() + return notebook.add(frame, text="Test", **kwargs) + + def test_add(self): + notebook = VNotebook(self.window) + self.add_test_frame(notebook) + self.window.update() + + def test_compound(self): + for compound in (tk.BOTTOM, tk.TOP, tk.RIGHT, tk.LEFT): + VNotebook(self.window, compound=compound).grid() + self.window.update() + + def test_index(self): + notebook = VNotebook(self.window) + self.add_test_frame(notebook) + frame = ttk.Frame(notebook) + notebook.insert(0, frame) + self.assertEqual(notebook.tabs[0], notebook.get_id_for_tab(frame)) + self.assertEqual(0, notebook.index(frame)) + + def test_enable_traversal(self): + notebook = VNotebook(self.window) + self.add_test_frame(notebook) + self.add_test_frame(notebook) + notebook.enable_traversal() + active = notebook.active + notebook._switch_tab(None) + self.assertNotEqual(active, notebook.active) + + def test_tab_config(self): + notebook = VNotebook(self.window) + id = self.add_test_frame(notebook) + notebook.tab_configure(id, text="Hello") + self.assertEqual(notebook.tab_cget(id, "text"), "Hello") + + def test_activate(self): + notebook = VNotebook(self.window) + self.add_test_frame(notebook) + self.add_test_frame(notebook) + self.assertEqual(notebook.tabs[0], notebook.active) + notebook.activate_index(1) + self.assertEqual(notebook.tabs[1], notebook.active) diff --git a/ttkwidgets/frames/vnotebook.py b/ttkwidgets/frames/vnotebook.py index c779eec2..6400c0af 100644 --- a/ttkwidgets/frames/vnotebook.py +++ b/ttkwidgets/frames/vnotebook.py @@ -124,7 +124,6 @@ def grid_forget_widgets(self): self.__current_tab.grid_forget() for button in self._tab_buttons.values(): button.grid_forget() - return def _grid_tabs(self): """Organize tab buttons""" @@ -151,7 +150,7 @@ def add(self, child, **kwargs): supports: :param id: ID for the newly added Tab. If the ID is not given, one is generated automatically. - :param where: Position of the new Notebook + :param index: Position of the new Notebook :return: ID for the new Tab """ tab_id = kwargs.pop("id", hash(child)) @@ -159,7 +158,7 @@ def add(self, child, **kwargs): self._buttons_frame, variable=self._variable, value=tab_id) self._tab_frames[tab_id] = child # Process where argument - where = kwargs.pop("where", tk.END) + where = kwargs.pop("index", tk.END) if where == tk.END: self._tab_ids.append(tab_id) else: @@ -167,16 +166,16 @@ def add(self, child, **kwargs): self.tab(tab_id, **kwargs) return tab_id - def insert(self, where, child, **kwargs): + def insert(self, index, child, **kwargs): """add() alias with non-optional where argument""" - kwargs.update({"where": where}) + kwargs.update({"index": index}) return self.add(child, **kwargs) def enable_traversal(self, enable=True): """Setup keybinds for CTRL-TAB to switch tabs""" if enable is True: func = "bind" - args = ("", self.__switch_tab,) + args = ("", self._switch_tab,) else: func = "unbind" args = ("",) @@ -184,6 +183,10 @@ def enable_traversal(self, enable=True): getattr(widget, func)(*args) return enable + def disable_traversal(self): + """Alias of self.enable_traversal(enable=False)""" + return self.enable_traversal(enable=False) + def forget(self, child): """Remove a child by widget or tab_id""" tab_id = self.get_id_for_tab(child) @@ -197,8 +200,14 @@ def hide(self, child, hide=True): """Hide or unhide a Tab""" tab_id = self.get_id_for_tab(child) self._hidden[tab_id] = hide + if tab_id == self.active and len(self._tab_ids) != 1: + self.activate(self._tab_ids[0]) self.grid_widgets() + def show(self, child): + """Alias for hide(hide=False)""" + return self.hide(child, hide=False) + def index(self, child): """Return zero-indexed index value of a child OR tab_id""" return self._tab_ids.index(self.get_id_for_tab(child)) @@ -210,50 +219,30 @@ def tab(self, tab_id, option=None, **kwargs): :param tab_id: Non-optional tab ID of the tab to be configured :param option: If not None, function returns value for option key given in this argument - - :param compound: Determines position of image in the Tab Button - if an image is given - :param padding: Amount of space around the frame - :param sticky: Passed on to grid function called upon the - notebook tab frame - :param image: Image that is shown on the tab Button - :param text: Text that is shown on the tab Button - :param underline: Whether the text for the Button is underlined. - This argument should be a bool value, which does not match - the ttk.Notebook behaviour! - - :param font: Font tuple or instance passed on to Button. If - font is given, underline is ignored. """ if option is not None: return self._tab_buttons[tab_id].cget(option) # Argument processing - font = kwargs.pop("font", None) - underline = kwargs.pop("underline", False) self._frame_padding[tab_id] = \ - (kwargs.pop("padding", 0), kwargs.pop("sticky", tk.N)) + (kwargs.pop("padding", self._padding), kwargs.pop("sticky", tk.N)) kwargs["command"] = lambda tab_id=tab_id: self.activate(tab_id) kwargs["style"] = "Toolbutton" - # Process underline if font argument is Font type - if isinstance(font, Font): - if underline != font.cget("underline"): - font.configure(underline=underline) - kwargs["font"] = font # Configure Buttons self._tab_buttons[tab_id].configure(**kwargs) self._grid_tabs() def tab_configure(self, tab_id, **kwargs): """configure alias for self.tab""" - self.tab(tab_id, **kwargs) + return self.tab(tab_id, **kwargs) def tab_cget(self, tab_id, key): """cget alias for self.tab""" - self.tab(tab_id, option=key) + return self.tab(tab_id, option=key) + @property def tabs(self): """Return list of tab IDs""" - return self._tab_ids + return self._tab_ids.copy() def config(self, **kwargs): """Alias for self.configure""" @@ -303,7 +292,7 @@ def get_id_for_tab(self, child): return child return {widget: tab_id for tab_id, widget in self._tab_frames.items()}[child] - def __switch_tab(self, event): + def _switch_tab(self, event): """Callback for CTRL-TAB""" if self.active is None: self.activate(self._tab_ids[0]) @@ -321,8 +310,11 @@ def __switch_tab(self, event): notebook = VNotebook(root, compound=tk.RIGHT) notebook.add(ttk.Scale(notebook), text="Scale") notebook.add(ttk.Button(notebook, text="Destroy", command=root.destroy), text="Button") - id = notebook.add(ttk.Frame(notebook), text="Hidden") - notebook.hide(id) + frame = ttk.Frame(notebook) + id = notebook.add(frame, text="Hidden") + def callback(): + notebook.hide(id) + ttk.Button(frame, command=callback, text="Hide").grid() notebook.enable_traversal() notebook.grid(row=1) root.mainloop() From 079b1575f7c7552153370111217631a2fb4215db Mon Sep 17 00:00:00 2001 From: Dogeek Date: Sun, 8 Dec 2019 22:30:35 +0100 Subject: [PATCH 04/13] example, entry in authors.md, setup --- AUTHORS.md | 1 + examples/example_vnotebook.py | 21 +++++++++++++++++++++ setup.py | 2 +- ttkwidgets/__init__.py | 2 ++ ttkwidgets/frames/vnotebook.py | 17 ----------------- 5 files changed, 25 insertions(+), 18 deletions(-) create mode 100644 examples/example_vnotebook.py diff --git a/AUTHORS.md b/AUTHORS.md index 7c884513..3fd7c77a 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -11,6 +11,7 @@ This file contains a list of all the authors of widgets in this repository. Plea * `Balloon` * `ItemsCanvas` * `TimeLine` + * `VNotebook` - The Python Team * `Calendar`, found [here](http://svn.python.org/projects/sandbox/trunk/ttk-gsoc/samples/ttkcalendar.py) - Mitja Martini diff --git a/examples/example_vnotebook.py b/examples/example_vnotebook.py new file mode 100644 index 00000000..171acc35 --- /dev/null +++ b/examples/example_vnotebook.py @@ -0,0 +1,21 @@ +from ttkthemes import ThemedTk +from ttkwidgets import VNotebook +import tkinter.ttk as ttk +import tkinter as tk + + +def callback(): + notebook.hide(id_) + + +root = ThemedTk() +root.set_theme("radiance") +notebook = VNotebook(root, compound=tk.RIGHT) +notebook.add(ttk.Scale(notebook), text="Scale") +notebook.add(ttk.Button(notebook, text="Destroy", command=root.destroy), text="Button") +frame = ttk.Frame(notebook) +id_ = notebook.add(frame, text="Hidden") +ttk.Button(frame, command=callback, text="Hide").grid() +notebook.enable_traversal() +notebook.grid(row=1) +root.mainloop() \ No newline at end of file diff --git a/setup.py b/setup.py index 5efbf23e..ce23f40c 100644 --- a/setup.py +++ b/setup.py @@ -16,5 +16,5 @@ classifiers=["Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)"], - install_requires=["pillow"] + install_requires=["pillow", "ttkthemes"] ) diff --git a/ttkwidgets/__init__.py b/ttkwidgets/__init__.py index d021bd93..ccadb5cc 100644 --- a/ttkwidgets/__init__.py +++ b/ttkwidgets/__init__.py @@ -10,3 +10,5 @@ from ttkwidgets.scaleentry import ScaleEntry from ttkwidgets.timeline import TimeLine from ttkwidgets.tickscale import TickScale + +from ttkwidgets.frames import VNotebook diff --git a/ttkwidgets/frames/vnotebook.py b/ttkwidgets/frames/vnotebook.py index 6400c0af..ed62614f 100644 --- a/ttkwidgets/frames/vnotebook.py +++ b/ttkwidgets/frames/vnotebook.py @@ -301,20 +301,3 @@ def _switch_tab(self, event): if to_activate == len(self._tab_ids): to_activate = 0 self.activate(self._tab_ids[to_activate]) - - -if __name__ == '__main__': - from ttkthemes import ThemedTk - root = ThemedTk() - root.set_theme("radiance") - notebook = VNotebook(root, compound=tk.RIGHT) - notebook.add(ttk.Scale(notebook), text="Scale") - notebook.add(ttk.Button(notebook, text="Destroy", command=root.destroy), text="Button") - frame = ttk.Frame(notebook) - id = notebook.add(frame, text="Hidden") - def callback(): - notebook.hide(id) - ttk.Button(frame, command=callback, text="Hide").grid() - notebook.enable_traversal() - notebook.grid(row=1) - root.mainloop() From 7aa97756952074a9bece63a095cb970ece0f2883 Mon Sep 17 00:00:00 2001 From: Juliette Monsel Date: Wed, 18 Dec 2019 14:36:33 +0100 Subject: [PATCH 05/13] Fix VNotebook tests Rename TestVNotebook.add_test_frame() to prevent this method to be executed as part of the tests with no arguments which lead to the previous error. --- tests/test_vnotebook.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_vnotebook.py b/tests/test_vnotebook.py index 18435134..ce4af5be 100644 --- a/tests/test_vnotebook.py +++ b/tests/test_vnotebook.py @@ -22,7 +22,7 @@ def test_init(self): VNotebook(self.window).grid() self.window.update() - def add_test_frame(self, notebook, **kwargs): + def _add_test_frame(self, notebook, **kwargs): notebook.grid() frame = ttk.Frame(notebook) ttk.Scale(frame).grid() @@ -31,7 +31,7 @@ def add_test_frame(self, notebook, **kwargs): def test_add(self): notebook = VNotebook(self.window) - self.add_test_frame(notebook) + self._add_test_frame(notebook) self.window.update() def test_compound(self): @@ -41,7 +41,7 @@ def test_compound(self): def test_index(self): notebook = VNotebook(self.window) - self.add_test_frame(notebook) + self._add_test_frame(notebook) frame = ttk.Frame(notebook) notebook.insert(0, frame) self.assertEqual(notebook.tabs[0], notebook.get_id_for_tab(frame)) @@ -49,8 +49,8 @@ def test_index(self): def test_enable_traversal(self): notebook = VNotebook(self.window) - self.add_test_frame(notebook) - self.add_test_frame(notebook) + self._add_test_frame(notebook) + self._add_test_frame(notebook) notebook.enable_traversal() active = notebook.active notebook._switch_tab(None) @@ -58,14 +58,14 @@ def test_enable_traversal(self): def test_tab_config(self): notebook = VNotebook(self.window) - id = self.add_test_frame(notebook) + id = self._add_test_frame(notebook) notebook.tab_configure(id, text="Hello") self.assertEqual(notebook.tab_cget(id, "text"), "Hello") def test_activate(self): notebook = VNotebook(self.window) - self.add_test_frame(notebook) - self.add_test_frame(notebook) + self._add_test_frame(notebook) + self._add_test_frame(notebook) self.assertEqual(notebook.tabs[0], notebook.active) notebook.activate_index(1) self.assertEqual(notebook.tabs[1], notebook.active) From 76fb8d5e86bf8947d11d0922aef5fd6089c5a4c7 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Sat, 7 Dec 2019 21:37:40 +0100 Subject: [PATCH 06/13] Disable Python 2 CI and enable 3.7 and 3.8 --- .appveyor.yml | 3 ++- .travis.yml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index b5ff66af..bf5fb19c 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,9 +1,10 @@ environment: matrix: - - PYTHON: "C:\\PYTHON27" - PYTHON: "C:\\PYTHON34" - PYTHON: "C:\\PYTHON35" - PYTHON: "C:\\PYTHON36" + - PYTHON: "C:\\PYTHON37" + - PYTHON: "C:\\PYTHON38" install: - "%PYTHON%\\python.exe -m pip install codecov coverage nose mock pynput" build: off diff --git a/.travis.yml b/.travis.yml index 9022e37c..fd4429ec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ language: python required: sudo python: - - "2.7" - "3.5" - "3.6" - "3.7" + - "3.8" before_install: - "export DISPLAY=:99.0" - sudo systemctl start xvfb From 8b8085a7e504261ea68bbf5e7c7561addf6904f7 Mon Sep 17 00:00:00 2001 From: Dogeek Date: Wed, 18 Dec 2019 15:23:17 +0100 Subject: [PATCH 07/13] fixed syntax warnings, and removed ttkthemes dependancy --- examples/example_vnotebook.py | 4 +--- setup.py | 10 +++++----- ttkwidgets/debugwindow.py | 8 ++++---- ttkwidgets/itemscanvas.py | 26 +++++++++++++------------- ttkwidgets/linklabel.py | 11 +++++------ 5 files changed, 28 insertions(+), 31 deletions(-) diff --git a/examples/example_vnotebook.py b/examples/example_vnotebook.py index 171acc35..378579f1 100644 --- a/examples/example_vnotebook.py +++ b/examples/example_vnotebook.py @@ -1,4 +1,3 @@ -from ttkthemes import ThemedTk from ttkwidgets import VNotebook import tkinter.ttk as ttk import tkinter as tk @@ -8,8 +7,7 @@ def callback(): notebook.hide(id_) -root = ThemedTk() -root.set_theme("radiance") +root = tk.Tk() notebook = VNotebook(root, compound=tk.RIGHT) notebook.add(ttk.Scale(notebook), text="Scale") notebook.add(ttk.Button(notebook, text="Destroy", command=root.destroy), text="Button") diff --git a/setup.py b/setup.py index 54891064..9befcd9e 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ License ------- -ttkwidgets: A collection of widgets for Tkinter's ttk extensions by various authors +ttkwidgets: A collection of widgets for Tkinter's ttk extensions by various authors Copyright (C) RedFantom 2017 Copyright (C) The Python Team Copyright (C) Mitja Martini 2008 @@ -47,15 +47,15 @@ .. |Codecov| image:: https://codecov.io/gh/RedFantom/ttkwidgets/branch/master/graph/badge.svg :alt: Code Coverage :target: https://codecov.io/gh/RedFantom/ttkwidgets - + .. |Pypi| image:: https://badge.fury.io/py/ttkwidgets.svg :alt: PyPI version :target: https://badge.fury.io/py/ttkwidgets - + .. |License| image:: https://img.shields.io/badge/License-GPL%20v3-blue.svg :alt: License: GPL v3 :target: http://www.gnu.org/licenses/gpl-3.0 - + """ setup( @@ -73,5 +73,5 @@ classifiers=["Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)"], - install_requires=["pillow", "ttkthemes"] + install_requires=["pillow"] ) diff --git a/ttkwidgets/debugwindow.py b/ttkwidgets/debugwindow.py index 31996ceb..b371df1b 100644 --- a/ttkwidgets/debugwindow.py +++ b/ttkwidgets/debugwindow.py @@ -19,11 +19,11 @@ class DebugWindow(tk.Toplevel): """ A Toplevel that shows sys.stdout and sys.stderr for Tkinter applications """ - def __init__(self, master=None, title="Debug window", stdout=True, + def __init__(self, master=None, title="Debug window", stdout=True, stderr=False, width=70, autohidescrollbar=True, **kwargs): """ Create a Debug window. - + :param master: master widget :type master: widget :param stdout: whether to redirect stdout to the widget @@ -66,7 +66,7 @@ def __init__(self, master=None, title="Debug window", stdout=True, def save(self): """Save widget content.""" file_name = fd.asksaveasfilename() - if file_name is "" or file_name is None: + if file_name == "" or file_name is None: return with open(file_name, "w") as f: f.write(self.text.get("1.0", tk.END)) @@ -78,7 +78,7 @@ def _grid_widgets(self): def write(self, line): """ Write line at the end of the widget. - + :param line: text to insert in the widget :type line: str """ diff --git a/ttkwidgets/itemscanvas.py b/ttkwidgets/itemscanvas.py index 33adf0ba..79f9cafc 100644 --- a/ttkwidgets/itemscanvas.py +++ b/ttkwidgets/itemscanvas.py @@ -15,15 +15,15 @@ class ItemsCanvas(ttk.Frame): """ - A :class:`ttk.Frame` containing a Canvas upon which text items can be placed with a coloured background. - + A :class:`ttk.Frame` containing a Canvas upon which text items can be placed with a coloured background. + The items can be moved around and deleted. A background can also be set. """ def __init__(self, *args, **kwargs): """ Create an ItemsCanvas. - + :param canvaswidth: width of the canvas in pixels :type canvaswidth: int :param canvasheight: height of the canvas in pixels @@ -77,7 +77,7 @@ def left_press(self, event): Callback for the press of the left mouse button. Selects a new item and sets its highlightcolor. - + :param event: Tkinter event """ self.current_coords = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y) @@ -132,7 +132,7 @@ def left_motion(self, event): def right_press(self, event): """ Callback for the right mouse button event to pop up the correct menu. - + :param event: Tkinter event """ self.set_current() @@ -153,7 +153,7 @@ def add_item(self, text, font=("default", 12, "bold"), backgroundcolor="yellow", highlightcolor="blue"): """ Add a new item on the Canvas. - + :param text: text to display :type text: str :param font: font of the text @@ -189,7 +189,7 @@ def _new_item(self): def set_background(self, image=None, path=None, resize=True): """ Set the background image of the Canvas. - + :param image: background image :type image: PhotoImage :param path: background image path @@ -227,17 +227,17 @@ def cget(self, key): To get the list of options for this widget, call the method :meth:`~ItemsCanvas.keys`. """ - if key is "canvaswidth": + if key == "canvaswidth": return self._canvaswidth - elif key is "canvasheight": + elif key == "canvasheight": return self._canvasheight - elif key is "function_new": + elif key == "function_new": return self._function_new - elif key is "callback_add": + elif key == "callback_add": return self._callback_add - elif key is "callback_del": + elif key == "callback_del": return self._callback_del - elif key is "callback_move": + elif key == "callback_move": return self._callback_move else: ttk.Frame.cget(self, key) diff --git a/ttkwidgets/linklabel.py b/ttkwidgets/linklabel.py index 902d7d87..56299fde 100644 --- a/ttkwidgets/linklabel.py +++ b/ttkwidgets/linklabel.py @@ -21,7 +21,7 @@ class LinkLabel(ttk.Label): def __init__(self, master=None, **kwargs): """ Create a LinkLabel. - + :param master: master widget :param link: link to be opened :type link: str @@ -85,13 +85,13 @@ def cget(self, key): To get the list of options for this widget, call the method :meth:`~LinkLabel.keys`. """ - if key is "link": + if key == "link": return self._link - elif key is "hover_color": + elif key == "hover_color": return self._hover_color - elif key is "normal_color": + elif key == "normal_color": return self._normal_color - elif key is "clicked_color": + elif key == "clicked_color": return self._clicked_color else: return ttk.Label.cget(self, key) @@ -115,4 +115,3 @@ def keys(self): keys = ttk.Label.keys(self) keys.extend(["link", "normal_color", "hover_color", "clicked_color"]) return keys - From 0cc8a1e8902447f009fd70eec70e263f11783fe8 Mon Sep 17 00:00:00 2001 From: Juliette Monsel Date: Wed, 18 Dec 2019 15:34:29 +0100 Subject: [PATCH 08/13] Add VNotebook in sphinx doc --- docs/source/authors.rst | 1 + docs/source/ttkwidgets/ttkwidgets.frames.rst | 1 + .../ttkwidgets.frames/ttkwidgets.frames.VNotebook.rst | 10 ++++++++++ 3 files changed, 12 insertions(+) create mode 100644 docs/source/ttkwidgets/ttkwidgets.frames/ttkwidgets.frames.VNotebook.rst diff --git a/docs/source/authors.rst b/docs/source/authors.rst index 3e53e915..1ea86b04 100644 --- a/docs/source/authors.rst +++ b/docs/source/authors.rst @@ -14,6 +14,7 @@ List of all the authors of widgets in this repository. Please note that this lis * :class:`~ttkwidgets.frames.Balloon` * :class:`~ttkwidgets.ItemsCanvas` * :class:`~ttkwidgets.TimeLine` + * :class:`~ttkwidgets.VNotebook` - The Python Team diff --git a/docs/source/ttkwidgets/ttkwidgets.frames.rst b/docs/source/ttkwidgets/ttkwidgets.frames.rst index af677973..31d348d9 100644 --- a/docs/source/ttkwidgets/ttkwidgets.frames.rst +++ b/docs/source/ttkwidgets/ttkwidgets.frames.rst @@ -14,3 +14,4 @@ ttkwidgets.frames Balloon ScrolledFrame ToggledFrame + VNotebook diff --git a/docs/source/ttkwidgets/ttkwidgets.frames/ttkwidgets.frames.VNotebook.rst b/docs/source/ttkwidgets/ttkwidgets.frames/ttkwidgets.frames.VNotebook.rst new file mode 100644 index 00000000..170532e8 --- /dev/null +++ b/docs/source/ttkwidgets/ttkwidgets.frames/ttkwidgets.frames.VNotebook.rst @@ -0,0 +1,10 @@ +VNotebook +========= + +.. currentmodule:: ttkwidgets.frames + +.. autoclass:: VNotebook + :show-inheritance: + :members: + + .. automethod:: __init__ \ No newline at end of file From 4c8a4e22ac7134c23d2609ce2ba62a815d81825a Mon Sep 17 00:00:00 2001 From: Juliette Monsel Date: Wed, 18 Dec 2019 16:11:43 +0100 Subject: [PATCH 09/13] Improve VNotebook docstrings formatting --- ttkwidgets/frames/vnotebook.py | 77 +++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/ttkwidgets/frames/vnotebook.py b/ttkwidgets/frames/vnotebook.py index ed62614f..e6059691 100644 --- a/ttkwidgets/frames/vnotebook.py +++ b/ttkwidgets/frames/vnotebook.py @@ -11,10 +11,10 @@ class VNotebook(ttk.Frame): """ - Notebook with Vertical tabs. Does not actually use the Notebook - widget, but a set of Toolbutton-styles Radiobuttons to select a - Frame of a set. Provides an interface that functions the same as a - normal Notebook widget. + Notebook with vertical tabs. Does not actually use the + :class:`ttk.Notebook` widget, but a set of Toolbutton-styles + Radiobuttons to select a Frame of a set. Provides an interface that + behaves like a normal :class:`ttk.Notebook` widget. """ options = [ @@ -41,11 +41,17 @@ class VNotebook(ttk.Frame): def __init__(self, master, **kwargs): """ + Create a VNotebook. + :param cursor: Cursor set upon hovering Buttons + :type cursor: str :param padding: Amount of pixels between the Buttons + :type padding: int :param compound: Location of the Buttons + :type compound: str :param callback: Callback called upon change of Frame - :param kwargs: Passed on to ttk.Frame initializer + :type callback: callable + :param kwargs: Passed on to :class:`ttk.Frame` initializer """ # Argument processing self._cursor = kwargs.pop("cursor", "default") @@ -78,12 +84,12 @@ def __init__(self, master, **kwargs): self.grid_widgets() def init_widgets(self): - """Initialize child widgets""" + """Initialize child widgets.""" self._buttons_frame = ttk.Frame(self) self._separator = ttk.Separator(self) def grid_widgets(self): - """Put child widgets in place""" + """Put child widgets in place.""" horizontal = self._compound in (tk.BOTTOM, tk.TOP) if horizontal is True: sticky = "sw" if self._compound == tk.BOTTOM else "nw" @@ -118,7 +124,7 @@ def grid_widgets(self): self._grid_tabs() def grid_forget_widgets(self): - """Remove child widgets from grid""" + """Remove child widgets from grid.""" self._buttons_frame.grid_forget() if self.__current_tab is not None: self.__current_tab.grid_forget() @@ -126,7 +132,7 @@ def grid_forget_widgets(self): button.grid_forget() def _grid_tabs(self): - """Organize tab buttons""" + """Organize tab buttons.""" for button in self._tab_buttons.values(): button.grid_forget() for index, tab_id in enumerate(self._tab_ids): @@ -144,14 +150,17 @@ def _grid_tabs(self): def add(self, child, **kwargs): """ Create new tab in the notebook and append it to the end. - :param child: Child widget, such as a ttk.Frame - :param kwargs: Keyword arguments to create tab with. Supports - all arguments supported by tab() function, and in addition - supports: - :param id: ID for the newly added Tab. If the ID is not - given, one is generated automatically. - :param index: Position of the new Notebook + + :param child: Child widget, such as a :class:`ttk.Frame` + :param kwargs: Keyword arguments to create tab with. + Supports all arguments supported by :meth:`VNotebook.tab` + function, and in addition supports: + + :param id: ID for the newly added Tab. If the ID is not given, one is generated automatically. + :param index: Position of the new Tab. + :return: ID for the new Tab + :rtype: int """ tab_id = kwargs.pop("id", hash(child)) self._tab_buttons[tab_id] = ttk.Radiobutton( @@ -167,12 +176,12 @@ def add(self, child, **kwargs): return tab_id def insert(self, index, child, **kwargs): - """add() alias with non-optional where argument""" + """:meth:`VNotebook.add` alias with non-optional index argument.""" kwargs.update({"index": index}) return self.add(child, **kwargs) def enable_traversal(self, enable=True): - """Setup keybinds for CTRL-TAB to switch tabs""" + """Setup keybinds for CTRL-TAB to switch tabs.""" if enable is True: func = "bind" args = ("", self._switch_tab,) @@ -184,11 +193,11 @@ def enable_traversal(self, enable=True): return enable def disable_traversal(self): - """Alias of self.enable_traversal(enable=False)""" + """Alias of :obj:`VNotebook.enable_traversal(enable=False)`.""" return self.enable_traversal(enable=False) def forget(self, child): - """Remove a child by widget or tab_id""" + """Remove a child by widget or tab_id.""" tab_id = self.get_id_for_tab(child) self._tab_buttons[tab_id].destroy() del self._tab_buttons[tab_id] @@ -197,7 +206,7 @@ def forget(self, child): self._grid_tabs() def hide(self, child, hide=True): - """Hide or unhide a Tab""" + """Hide or unhide a Tab.""" tab_id = self.get_id_for_tab(child) self._hidden[tab_id] = hide if tab_id == self.active and len(self._tab_ids) != 1: @@ -205,11 +214,11 @@ def hide(self, child, hide=True): self.grid_widgets() def show(self, child): - """Alias for hide(hide=False)""" + """Alias for :obj:`VNotebook.hide(hide=False)`""" return self.hide(child, hide=False) def index(self, child): - """Return zero-indexed index value of a child OR tab_id""" + """Return zero-indexed index value of a child or tab_id.""" return self._tab_ids.index(self.get_id_for_tab(child)) def tab(self, tab_id, option=None, **kwargs): @@ -232,31 +241,31 @@ def tab(self, tab_id, option=None, **kwargs): self._grid_tabs() def tab_configure(self, tab_id, **kwargs): - """configure alias for self.tab""" + """Configure alias for :meth:`VNotebook.tab`""" return self.tab(tab_id, **kwargs) def tab_cget(self, tab_id, key): - """cget alias for self.tab""" + """cget alias for :meth:`VNotebook.tab`""" return self.tab(tab_id, option=key) @property def tabs(self): - """Return list of tab IDs""" + """Return list of tab IDs.""" return self._tab_ids.copy() def config(self, **kwargs): - """Alias for self.configure""" + """Alias for :meth:`VNotebook.configure`""" return self.configure(**kwargs) def configure(self, **kwargs): - """Change settings for the widget""" + """Change settings for the widget.""" for option in self.options: attr = "_{}".format(option) setattr(self, attr, kwargs.pop(option, getattr(self, attr))) return ttk.Frame.configure(**kwargs) def cget(self, key): - """Return current value for a setting""" + """Return current value for a setting.""" if key in self.options: return getattr(self, "_{}".format(key)) return ttk.Frame.cget(self, key) @@ -268,7 +277,7 @@ def __setitem__(self, key, value): return self.configure(**{key: value}) def activate(self, tab_id): - """Activate a new Tab in the Notebook""" + """Activate a new Tab in the notebook.""" if self.active is not None: self.__current_tab.grid_forget() self.__current_tab = self._tab_frames[tab_id] @@ -276,24 +285,24 @@ def activate(self, tab_id): self._variable.set(tab_id) def activate_index(self, index): - """Activate Tab by zero-indexed value""" + """Activate Tab by zero-indexed value.""" return self.activate(self._tab_ids[index]) @property def active(self): - """Return tab_id for currently active Tab""" + """Return tab_id for currently active Tab.""" if self.__current_tab is None: return None return self.get_id_for_tab(self.__current_tab) def get_id_for_tab(self, child): - """Return tab_id for child, which can be tab_id or Widget""" + """Return tab_id for child, which can be tab_id or widget.""" if child in self._tab_ids: return child return {widget: tab_id for tab_id, widget in self._tab_frames.items()}[child] def _switch_tab(self, event): - """Callback for CTRL-TAB""" + """Callback for CTRL-TAB.""" if self.active is None: self.activate(self._tab_ids[0]) return From 79d59c3154ff8273d5f8dc249ad7e026c74518ff Mon Sep 17 00:00:00 2001 From: Dogeek Date: Thu, 19 Dec 2019 02:23:51 +0100 Subject: [PATCH 10/13] fixed a test --- tests/test_tickscale.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_tickscale.py b/tests/test_tickscale.py index 29f1970e..6700609f 100644 --- a/tests/test_tickscale.py +++ b/tests/test_tickscale.py @@ -53,7 +53,9 @@ def test_tickscale_methods(self): 'class', 'tickinterval', 'showvalue', - 'digits'] + 'digits', + 'state'] + self.assertEqual(sorted(scale.keys()), sorted(keys)) scale.config(from_=-1) From f6dbfebea14bb90c8037fc27f3a6494d348fee73 Mon Sep 17 00:00:00 2001 From: Dogeek Date: Thu, 19 Dec 2019 02:32:55 +0100 Subject: [PATCH 11/13] test fix (again)? --- tests/test_tickscale.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/tests/test_tickscale.py b/tests/test_tickscale.py index 6700609f..a1a745af 100644 --- a/tests/test_tickscale.py +++ b/tests/test_tickscale.py @@ -40,21 +40,7 @@ def test_tickscale_methods(self): self.assertTrue(scale.cget('showvalue')) self.assertEqual(scale['from'], 0) self.assertEqual(scale.cget('to'), 10) - keys = ['command', - 'variable', - 'orient', - 'from', - 'to', - 'value', - 'length', - 'takefocus', - 'cursor', - 'style', - 'class', - 'tickinterval', - 'showvalue', - 'digits', - 'state'] + keys = ttk.Scale(self.window).keys() + ['showvalue', 'tickinterval', 'digits'] self.assertEqual(sorted(scale.keys()), sorted(keys)) From 1aa18c8bf1b069a1ce60780e9e6cb33c324ef362 Mon Sep 17 00:00:00 2001 From: Juliette Monsel Date: Fri, 20 Dec 2019 17:32:26 +0100 Subject: [PATCH 12/13] Remove unused Font import in vnotebook.py --- ttkwidgets/frames/vnotebook.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/ttkwidgets/frames/vnotebook.py b/ttkwidgets/frames/vnotebook.py index e6059691..eba9a982 100644 --- a/ttkwidgets/frames/vnotebook.py +++ b/ttkwidgets/frames/vnotebook.py @@ -6,14 +6,13 @@ # Basic UI imports import tkinter as tk from tkinter import ttk -from tkinter.font import Font class VNotebook(ttk.Frame): """ - Notebook with vertical tabs. Does not actually use the - :class:`ttk.Notebook` widget, but a set of Toolbutton-styles - Radiobuttons to select a Frame of a set. Provides an interface that + Notebook with vertical tabs. Does not actually use the + :class:`ttk.Notebook` widget, but a set of Toolbutton-styles + Radiobuttons to select a Frame of a set. Provides an interface that behaves like a normal :class:`ttk.Notebook` widget. """ @@ -42,7 +41,7 @@ class VNotebook(ttk.Frame): def __init__(self, master, **kwargs): """ Create a VNotebook. - + :param cursor: Cursor set upon hovering Buttons :type cursor: str :param padding: Amount of pixels between the Buttons @@ -150,15 +149,15 @@ def _grid_tabs(self): def add(self, child, **kwargs): """ Create new tab in the notebook and append it to the end. - + :param child: Child widget, such as a :class:`ttk.Frame` - :param kwargs: Keyword arguments to create tab with. - Supports all arguments supported by :meth:`VNotebook.tab` + :param kwargs: Keyword arguments to create tab with. + Supports all arguments supported by :meth:`VNotebook.tab` function, and in addition supports: - + :param id: ID for the newly added Tab. If the ID is not given, one is generated automatically. :param index: Position of the new Tab. - + :return: ID for the new Tab :rtype: int """ From 77542057ce2857ddd1dc9a457e35991a4f16ae4e Mon Sep 17 00:00:00 2001 From: Dogeek Date: Sun, 29 Dec 2019 15:32:19 +0100 Subject: [PATCH 13/13] fixed odd `add` behavior. Replaced ttk.Frame.configure with super() call. --- examples/example_vnotebook.py | 2 +- ttkwidgets/frames/vnotebook.py | 30 ++++++++++++++++++++---------- ttkwidgets/utilities.py | 15 +++++++++++++++ 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/examples/example_vnotebook.py b/examples/example_vnotebook.py index 378579f1..92462192 100644 --- a/examples/example_vnotebook.py +++ b/examples/example_vnotebook.py @@ -16,4 +16,4 @@ def callback(): ttk.Button(frame, command=callback, text="Hide").grid() notebook.enable_traversal() notebook.grid(row=1) -root.mainloop() \ No newline at end of file +root.mainloop() diff --git a/ttkwidgets/frames/vnotebook.py b/ttkwidgets/frames/vnotebook.py index eba9a982..e8527a98 100644 --- a/ttkwidgets/frames/vnotebook.py +++ b/ttkwidgets/frames/vnotebook.py @@ -6,6 +6,7 @@ # Basic UI imports import tkinter as tk from tkinter import ttk +from ttkwidgets.utilities import get_widget_options class VNotebook(ttk.Frame): @@ -48,8 +49,6 @@ def __init__(self, master, **kwargs): :type padding: int :param compound: Location of the Buttons :type compound: str - :param callback: Callback called upon change of Frame - :type callback: callable :param kwargs: Passed on to :class:`ttk.Frame` initializer """ # Argument processing @@ -149,6 +148,7 @@ def _grid_tabs(self): def add(self, child, **kwargs): """ Create new tab in the notebook and append it to the end. + If the child is already managed by the VNotebook widget, then update the child with its settings. :param child: Child widget, such as a :class:`ttk.Frame` :param kwargs: Keyword arguments to create tab with. @@ -162,15 +162,25 @@ def add(self, child, **kwargs): :rtype: int """ tab_id = kwargs.pop("id", hash(child)) - self._tab_buttons[tab_id] = ttk.Radiobutton( - self._buttons_frame, variable=self._variable, value=tab_id) - self._tab_frames[tab_id] = child - # Process where argument - where = kwargs.pop("index", tk.END) - if where == tk.END: - self._tab_ids.append(tab_id) + updating = child in self._tab_frames.values() + if not updating: + self._tab_buttons[tab_id] = ttk.Radiobutton( + self._buttons_frame, variable=self._variable, value=tab_id) + self._tab_frames[tab_id] = child else: + self._tab_frames[tab_id].config(**get_widget_options(child)) + + # Process where argument + where = kwargs.get("index", tk.END) + if updating and 'index' in kwargs: + self._tab_ids.pop(where) self._tab_ids.insert(where, tab_id) + else: + if where == tk.END: + self._tab_ids.append(tab_id) + else: + self._tab_ids.insert(where, tab_id) + kwargs.pop('index', None) self.tab(tab_id, **kwargs) return tab_id @@ -261,7 +271,7 @@ def configure(self, **kwargs): for option in self.options: attr = "_{}".format(option) setattr(self, attr, kwargs.pop(option, getattr(self, attr))) - return ttk.Frame.configure(**kwargs) + return super().configure(**kwargs) def cget(self, key): """Return current value for a setting.""" diff --git a/ttkwidgets/utilities.py b/ttkwidgets/utilities.py index ad32dbe3..f6b8f4b4 100644 --- a/ttkwidgets/utilities.py +++ b/ttkwidgets/utilities.py @@ -9,3 +9,18 @@ def get_assets_directory(): def open_icon(icon_name): return ImageTk.PhotoImage(Image.open(os.path.join(get_assets_directory(), icon_name))) + + +def get_widget_options(widget): + """ + Gets the options from a widget + + :param widget: tkinter.Widget instance to get the config options from + :return: dict of options that you can pass on to widget.config() + """ + options = {} + for key in widget.keys(): + value = widget.cget(key) + if value not in ("", None): + options[key] = value + return options