From 4f0ef7972d318e1b5db10188873ee892ba2c083c Mon Sep 17 00:00:00 2001 From: Dogeek Date: Sun, 1 Dec 2019 05:41:09 +0100 Subject: [PATCH 01/31] Add Notebook widget --- examples/example_notebook.py | 22 + ttkwidgets/__init__.py | 1 + ttkwidgets/notebook.py | 935 +++++++++++++++++++++++++++++++++++ ttkwidgets/utilities.py | 117 +++++ 4 files changed, 1075 insertions(+) create mode 100644 examples/example_notebook.py create mode 100644 ttkwidgets/notebook.py diff --git a/examples/example_notebook.py b/examples/example_notebook.py new file mode 100644 index 00000000..aa9461a5 --- /dev/null +++ b/examples/example_notebook.py @@ -0,0 +1,22 @@ +import tkinter as tk +import tkinter.ttk as ttk +from ttkwidgets import Notebook + + +class MainWindow(ttk.Frame): + def __init__(self, master=None): + super().__init__(master) + self.nb = Notebook(self) + self.frames = [tk.Frame(self) for i in range(10)] + for i, w in enumerate(self.frames): + tk.Canvas(w, width=300, height=300).grid(sticky="nswe") + self.nb.add(w, text="Frame " + str(i)) + w.grid() + self.nb.grid() + + +root = tk.Tk() +root.title("Notebook Example") +gui = MainWindow(root) +gui.grid() +root.mainloop() \ No newline at end of file diff --git a/ttkwidgets/__init__.py b/ttkwidgets/__init__.py index 9788fb40..24e99be8 100644 --- a/ttkwidgets/__init__.py +++ b/ttkwidgets/__init__.py @@ -11,3 +11,4 @@ from ttkwidgets.timeline import TimeLine from ttkwidgets.tickscale import TickScale from ttkwidgets.table import Table +from ttkwidgets.notebook import Notebook diff --git a/ttkwidgets/notebook.py b/ttkwidgets/notebook.py new file mode 100644 index 00000000..8049728d --- /dev/null +++ b/ttkwidgets/notebook.py @@ -0,0 +1,935 @@ +#! /usr/bin/python3 +# -*- coding: utf-8 -*- + +""" +Copyright 2018-2019 Juliette Monsel +Copyright 2019 Dogeek + +Adapted from PyTkEditor - Python IDE's Notebook widget by +Juliette Monsel. Adapted by Dogeek + +PyTkEditor is distributed with the GNU GPL license. + +Notebook with draggable / scrollable tabs +""" + +from tkinter import ttk +import tkinter as tk +from ttkwidgets.utilities import move_widget, parse_geometry, coordinates_in_box + + +class Tab(ttk.Frame): + """Notebook tab.""" + def __init__(self, master=None, tab_nb=0, **kwargs): + """ + :param master: parent widget + :param tab_nb: tab index + :param **kwargs: keyword arguments for ttk::Frame widgets + """ + ttk.Frame.__init__(self, master, class_='Notebook.Tab', + style='Notebook.Tab', padding=1) + self._state = kwargs.pop('state', 'normal') + self.tab_nb = tab_nb + self.hovering_tab = False + self._closebutton = kwargs.pop('closebutton', True) + self._closecommand = kwargs.pop('closecommand', None) + self.frame = ttk.Frame(self, style='Notebook.Tab.Frame') + self.label = ttk.Label(self.frame, style='Notebook.Tab.Label', anchor='center', takefocus=False, **kwargs) + self.closebtn = ttk.Button( + self.frame, style='Notebook.Tab.Close', command=self.closecommand, + class_='Notebook.Tab.Close', takefocus=False) + self.label.pack(side='left', padx=(6, 0)) + if self._closebutton: + self.closebtn.pack(side='right', padx=(0, 6)) + self.update_idletasks() + self.configure(width=self.frame.winfo_reqwidth() + 6, + height=self.frame.winfo_reqheight() + 6) + self.frame.place(bordermode='inside', anchor='nw', x=0, y=0, + relwidth=1, relheight=1) + self.label.bind('', self._resize) + if self._state == 'disabled': + self.state(['disabled']) + elif self._state != 'normal': + raise ValueError("state option should be 'normal' or 'disabled'") + + self.bind('', self._b2_press) + self.bind('', self._on_enter_tab) + self.bind('', self._on_leave_tab) + self.bind('', self._on_mousewheel) + + def _on_mousewheel(self, event): + if self.hovering_tab: + if event.delta > 0: + self.master.master.select_prev(True) + else: + self.master.master.select_next(True) + + def _on_enter_tab(self, event): + self.hovering_tab = True + + def _on_leave_tab(self, event): + self.hovering_tab = False + + def _b2_press(self, event): + if self.identify(event.x, event.y): + self.closecommand() + + def _resize(self, event): + self.configure(width=self.frame.winfo_reqwidth() + 6, + height=self.frame.winfo_reqheight() + 6) + + def closecommand(self): + """ + Calls the closecommand callback with the tab index as an argument + """ + self._closecommand(self.tab_nb) + + def state(self, *args): + res = ttk.Frame.state(self, *args) + self.label.state(*args) + self.frame.state(*args) + self.closebtn.state(*args) + if args and 'selected' in self.state(): + self.configure(width=self.frame.winfo_reqwidth() + 6, + height=self.frame.winfo_reqheight() + 6) + self.frame.place_configure(relheight=1.1) + else: + self.frame.place_configure(relheight=1) + self.configure(width=self.frame.winfo_reqwidth() + 6, + height=self.frame.winfo_reqheight() + 6) + return res + + def bind(self, sequence=None, func=None, add=None): + return self.frame.bind(sequence, func, add), self.label.bind(sequence, func, add) + + def unbind(self, sequence, funcids=(None, None)): + self.label.unbind(sequence, funcids[1]) + self.frame.unbind(sequence, funcids[0]) + + def tab_configure(self, **kwargs): + if 'closecommand' in kwargs: + self._closecommand = kwargs.pop('closecommand') + if 'closebutton' in kwargs: + self._closebutton = kwargs.pop('closebutton') + if self._closebutton: + self.closebtn.pack(side='right', padx=(0, 6)) + else: + self.closebtn.pack_forget() + self.update_idletasks() + self.configure(width=self.frame.winfo_reqwidth() + 6, + height=self.frame.winfo_reqheight() + 6) + if 'state' in kwargs: + state = kwargs.pop('state') + if state == 'normal': + self.state(['!disabled']) + elif state == 'disabled': + self.state(['disabled']) + else: + raise ValueError("state option should be 'normal' or 'disabled'") + self._state = state + if not kwargs: + return + self.label.configure(**kwargs) + + def tab_cget(self, option): + if option == 'closecommand': + return self._closecommand + elif option == 'closebutton': + return self._closebutton + elif option == 'state': + return self._state + else: + return self.label.cget(option) + + +class Notebook(ttk.Frame): + """ + Notebook widget. + + Unlike the ttk.Notebook, the tab width is constant and determine by the tab + label. When there are too many tabs to fit in the widget, buttons appear on + the left and the right of the Notebook to navigate through the tabs. + + The tab have an optional close button and the notebook has an optional tab + menu. Tabs can be optionnaly dragged. + """ + + _initialized = False + + def __init__(self, master=None, **kwargs): + """ + Create a Notebook widget with parent master. + + STANDARD OPIONS + + class, cursor, style, takefocus + + WIDGET-SPECIFIC OPTIONS + + closebutton: boolean (default True) + whether to display a close button on the tabs + + closecommand: function or None (default Notebook.forget) + command executed when the close button of a tab is pressed, + the tab index is passed in argument. + + tabdrag: boolean (default True) + whether to enable dragging of tab labels + + drag_to_toplevel : boolean (default tabdrag) + whether to enable dragging tabs to Toplevel windows + + tabmenu: boolean (default True) + whether to display a menu showing the tab labels in alphabetical order + + TAB OPTIONS + + state, sticky, padding, text, image, compound + + TAB IDENTIFIERS (tab_id) + + The tab_id argument found in several methods may take any of + the following forms: + + * An integer between zero and the number of tabs + * The name of a child window + * The string "current", which identifies the + currently-selected tab + * The string "end", which returns the number of tabs (only + valid for method index) + + """ + self._init_kwargs = kwargs.copy() + + self._closebutton = bool(kwargs.pop('closebutton', True)) + self._closecommand = kwargs.pop('closecommand', self.forget) + self._tabdrag = bool(kwargs.pop('tabdrag', True)) + self._drag_to_toplevel = bool(kwargs.pop('drag_to_toplevel', self._tabdrag)) + self._tabmenu = bool(kwargs.pop('tabmenu', True)) + + ttk.Frame.__init__(self, master, class_='Notebook', padding=(0, 0, 0, 1), + **kwargs) + if not Notebook._initialized: + self.setup_style() + + self.rowconfigure(1, weight=1) + self.columnconfigure(2, weight=1) + + self._tab_var = tk.IntVar(self, -1) + + self._visible_tabs = [] + self._active_tabs = [] # not disabled + self._hidden_tabs = [] + self._tab_labels = {} + self._tab_menu_entries = {} + self._tabs = {} + self._tab_options = {} + self._indexes = {} + self._nb_tab = 0 + self.current_tab = -1 + self._dragged_tab = None + self._toplevels = [] + + style = ttk.Style(self) + bg = style.lookup('TFrame', 'background') + + # --- widgets + # to display current tab content + self._body = ttk.Frame(self, padding=1, style='Notebook', + relief='flat') + self._body.rowconfigure(0, weight=1) + self._body.columnconfigure(0, weight=1) + self._body.grid_propagate(False) + # tab labels + # canvas to scroll through tab labels + self._canvas = tk.Canvas(self, bg=bg, highlightthickness=0, + borderwidth=0, takefocus=False) + self._tab_frame2 = ttk.Frame(self, height=26, style='Notebook', + relief='flat') + # self._tab_frame2 is a trick to be able to drag a tab on the full + # canvas width even if self._tab_frame is smaller. + self._tab_frame = ttk.Frame(self._tab_frame2, style='Notebook', + relief='flat', height=26) # to display tab labels + self._sep = ttk.Separator(self._tab_frame2, orient='horizontal') + self._sep.place(bordermode='outside', anchor='sw', x=0, rely=1, + relwidth=1, height=1) + self._tab_frame.pack(side='left') + + self._canvas.create_window(0, 0, anchor='nw', window=self._tab_frame2, + tags='window') + self._canvas.configure(height=self._tab_frame.winfo_reqheight()) + # empty frame to show the spot formerly occupied by the tab + self._dummy_frame = ttk.Frame(self._tab_frame, style='Notebook', relief='flat') + self._dummy_sep = ttk.Separator(self._tab_frame, orient='horizontal') + self._dummy_sep.place(in_=self._dummy_frame, x=0, relwidth=1, height=1, + y=0, anchor='sw', bordermode='outside') + # tab navigation + self._tab_menu = tk.Menu(self, tearoff=False, relief='sunken', + bg=style.lookup('TEntry', 'fieldbackground', + default='white'), + activebackground=style.lookup('TEntry', + 'selectbackground', + ['focus'], 'gray70'), + activeforeground=style.lookup('TEntry', + 'selectforeground', + ['focus'], 'gray70')) + self._tab_list = ttk.Menubutton(self, width=1, menu=self._tab_menu, + style='Notebook.TMenubutton', + padding=0) + self._tab_list.state(['disabled']) + self._btn_left = ttk.Button(self, style='Left.Notebook.TButton', + command=self.select_prev, takefocus=False) + self._btn_right = ttk.Button(self, style='Right.Notebook.TButton', + command=self.select_next, takefocus=False) + + # --- grid + self._tab_list.grid(row=0, column=0, sticky='ns', pady=(0, 1)) + if not self._tabmenu: + self._tab_list.grid_remove() + self._btn_left.grid(row=0, column=1, sticky='ns', pady=(0, 1)) + self._canvas.grid(row=0, column=2, sticky='ew') + self._btn_right.grid(row=0, column=3, sticky='ns', pady=(0, 1)) + self._body.grid(row=1, columnspan=4, sticky='ewns', padx=1, pady=1) + + ttk.Frame(self, height=1, + style='separator.TFrame').place(x=1, anchor='nw', + rely=1, height=1, + relwidth=1) + + self._border_left = ttk.Frame(self, width=1, style='separator.TFrame') + self._border_right = ttk.Frame(self, width=1, style='separator.TFrame') + self._border_left.place(bordermode='outside', in_=self._body, x=-1, y=-2, + width=1, height=self._body.winfo_reqheight() + 2, relheight=1) + self._border_right.place(bordermode='outside', in_=self._body, relx=1, y=-2, + width=1, height=self._body.winfo_reqheight() + 2, relheight=1) + + # --- bindings + self._tab_frame.bind('', self._on_configure) + self._canvas.bind('', self._on_configure) + self.bind_all('', self._on_click) + + self.config = self.configure + Notebook._initialized = True + + def __getitem__(self, key): + return self.cget(key) + + def __setitem__(self, key, value): + self.configure(**{key: value}) + + def setup_style(self, bg="#dddddd", activebg="#efefef", pressedbg="#c1c1c1", + fg="black", fieldbg="white", lightcolor="#ededed", darkcolor="##cfcdc8", + bordercolor="#888888", focusbordercolor="#5e5e5e", selectbg="#c1c1c1", + selectfg="black", unselectfg="#999999", disabledfg='#999999', disabledbg="#dddddd"): + """ + Setups the style for the notebook. + :param bg: + :param activebg: + :param pressedbg: + :param fg: + :param fieldbg: + :param lightcolor: + :param darkcolor: + :param bordercolor: + :param focusbordercolor: + :param selectbg: + :param selectfb: + :param unselectfg: + :param disabledfg: + :param disabledbg: + """ + theme = {'bg': bg, + 'activebg': activebg, + 'pressedbg': pressedbg, + 'fg': fg, + 'fieldbg': fieldbg, + 'lightcolor': lightcolor, + 'darkcolor': darkcolor, + 'bordercolor': bordercolor, + 'focusbordercolor': focusbordercolor, + 'selectbg': selectbg, + 'selectfg': selectfg, + 'unselectedfg': unselectfg, + 'disabledfg': disabledfg, + 'disabledbg': disabledbg} + + self.images = ( + tk.PhotoImage("img_close", data=''' + R0lGODlhCAAIAMIBAAAAADs7O4+Pj9nZ2Ts7Ozs7Ozs7Ozs7OyH+EUNyZWF0ZWQg + d2l0aCBHSU1QACH5BAEKAAQALAAAAAAIAAgAAAMVGDBEA0qNJyGw7AmxmuaZhWEU + 5kEJADs= + ''', master=self), + tk.PhotoImage("img_closeactive", data=''' + R0lGODlhCAAIAMIEAAAAAP/SAP/bNNnZ2cbGxsbGxsbGxsbGxiH5BAEKAAQALAAA + AAAIAAgAAAMVGDBEA0qNJyGw7AmxmuaZhWEU5kEJADs= + ''', master=self), + tk.PhotoImage("img_closepressed", data=''' + R0lGODlhCAAIAMIEAAAAAOUqKv9mZtnZ2Ts7Ozs7Ozs7Ozs7OyH+EUNyZWF0ZWQg + d2l0aCBHSU1QACH5BAEKAAQALAAAAAAIAAgAAAMVGDBEA0qNJyGw7AmxmuaZhWEU + 5kEJADs= + ''', master=self) + ) + + for seq in self.bind_class('TButton'): + self.bind_class('Notebook.Tab.Close', seq, self.bind_class('TButton', seq), True) + + style_config = {'bordercolor': theme['bordercolor'], + 'background': theme['bg'], + 'foreground': theme['fg'], + 'arrowcolor': theme['fg'], + 'gripcount': 0, + 'lightcolor': theme['lightcolor'], + 'darkcolor': theme['darkcolor'], + 'troughcolor': theme['pressedbg']} + + style = ttk.Style(self) + style.element_create('close', 'image', "img_close", + ("active", "pressed", "!disabled", "img_closepressed"), + ("active", "!disabled", "img_closeactive"), + sticky='') + style.layout('Notebook', style.layout('TFrame')) + style.layout('Notebook.TMenubutton', + [('Menubutton.border', + {'sticky': 'nswe', + 'children': [('Menubutton.focus', + {'sticky': 'nswe', + 'children': [('Menubutton.indicator', + {'side': 'right', 'sticky': ''}), + ('Menubutton.padding', + {'expand': '1', + 'sticky': 'we'})]})]})]) + style.layout('Notebook.Tab', style.layout('TFrame')) + style.layout('Notebook.Tab.Frame', style.layout('TFrame')) + style.layout('Notebook.Tab.Label', style.layout('TLabel')) + style.layout('Notebook.Tab.Close', + [('Close.padding', + {'sticky': 'nswe', + 'children': [('Close.border', + {'border': '1', + 'sticky': 'nsew', + 'children': [('Close.close', + {'sticky': 'ewsn'})]})]})]) + style.layout('Left.Notebook.TButton', + [('Button.padding', + {'sticky': 'nswe', + 'children': [('Button.leftarrow', {'sticky': 'nswe'})]})]) + style.layout('Right.Notebook.TButton', + [('Button.padding', + {'sticky': 'nswe', + 'children': [('Button.rightarrow', {'sticky': 'nswe'})]})]) + style.configure('Notebook', **style_config) + style.configure('Notebook.Tab', relief='raised', borderwidth=1, + **style_config) + style.configure('Notebook.Tab.Frame', relief='flat', borderwidth=0, + **style_config) + style.configure('Notebook.Tab.Label', relief='flat', borderwidth=1, + padding=0, **style_config) + style.configure('Notebook.Tab.Label', foreground=theme['unselectedfg']) + style.configure('Notebook.Tab.Close', relief='flat', borderwidth=1, + padding=0, **style_config) + style.configure('Notebook.Tab.Frame', background=theme['bg']) + style.configure('Notebook.Tab.Label', background=theme['bg']) + style.configure('Notebook.Tab.Close', background=theme['bg']) + + style.map('Notebook.Tab.Frame', + **{'background': [('selected', '!disabled', theme['activebg'])]}) + style.map('Notebook.Tab.Label', + **{'background': [('selected', '!disabled', theme['activebg'])], + 'foreground': [('selected', '!disabled', theme['fg'])]}) + style.map('Notebook.Tab.Close', + **{'background': [('selected', theme['activebg']), + ('pressed', theme['darkcolor']), + ('active', theme['activebg'])], + 'relief': [('hover', '!disabled', 'raised'), + ('active', '!disabled', 'raised'), + ('pressed', '!disabled', 'sunken')], + 'lightcolor': [('pressed', theme['darkcolor'])], + 'darkcolor': [('pressed', theme['lightcolor'])]}) + style.map('Notebook.Tab', + **{'background': [('selected', '!disabled', theme['activebg'])]}) + + style.configure('TNotebook.Tab', background=theme['bg'], + foreground=theme['unselectedfg']) + style.map('TNotebook.Tab', + **{'background': [('selected', '!disabled', theme['activebg'])], + 'foreground': [('selected', '!disabled', theme['fg'])]}) + + def _on_configure(self, event=None): + self.update_idletasks() + # ensure that canvas has the same height as the tabs + h = self._tab_frame.winfo_reqheight() + self._canvas.configure(height=h) + # ensure that _tab_frame2 fills the canvas if _tab_frame is smaller + self._canvas.itemconfigure('window', width=max(self._canvas.winfo_width(), self._tab_frame.winfo_reqwidth())) + # update canvas scrollregion + self._canvas.configure(scrollregion=self._canvas.bbox('all')) + # ensure visibility of current tab + self.see(self.current_tab) + # check wheter next/prev buttons needs to be displayed + if self._tab_frame.winfo_reqwidth() < self._canvas.winfo_width(): + self._btn_left.grid_remove() + self._btn_right.grid_remove() + elif len(self._visible_tabs) > 1: + self._btn_left.grid() + self._btn_right.grid() + + def _on_press(self, event, tab): + # show clicked tab content + self._show(tab) + + if not self._tabdrag or self.tab(tab, 'state') == 'disabled': + return + + # prepare dragging + widget = self._tab_labels[tab] + x = widget.winfo_x() + y = widget.winfo_y() + # replace tab by blank space (dummy) + self._dummy_frame.configure(width=widget.winfo_reqwidth(), + height=widget.winfo_reqheight()) + self._dummy_frame.grid(**widget.grid_info()) + self.update_idletasks() + self._dummy_sep.place_configure(in_=self._dummy_frame, y=self._dummy_frame.winfo_height()) + widget.grid_remove() + # place tab above the rest to drag it + widget.place(bordermode='outside', x=x, y=y) + widget.lift() + self._dragged_tab = widget + self._dx = - event.x_root # - current mouse x position on screen + self._y = event.y_root # current y mouse position on screen + self._distance_to_dragged_border = widget.winfo_rootx() - event.x_root + widget.bind_all('', self._on_drag) + + def _on_drag(self, event): + self._dragged_tab.place_configure(x=self._dragged_tab.winfo_x() + event.x_root + self._dx) + x_border = event.x_root + self._distance_to_dragged_border + # get tab below dragged_tab + if event.x_root > - self._dx: + # move towards right + w = self._dragged_tab.winfo_width() + tab_below = self._tab_frame.winfo_containing(x_border + w + 2, self._y) + else: + # move towards left + tab_below = self._tab_frame.winfo_containing(x_border - 2, self._y) + if tab_below and tab_below.master in self._tab_labels.values(): + tab_below = tab_below.master + elif tab_below not in self._tab_labels: + tab_below = None + + if tab_below and abs(x_border - tab_below.winfo_rootx()) < tab_below.winfo_width() / 2: + # swap + self._swap(tab_below) + + self._dx = - event.x_root + + def _swap(self, tab): + """Swap dragged_tab with tab.""" + g1, g2 = self._dummy_frame.grid_info(), tab.grid_info() + self._dummy_frame.grid(**g2) + tab.grid(**g1) + i1 = self._visible_tabs.index(self._dragged_tab.tab_nb) + i2 = self._visible_tabs.index(tab.tab_nb) + self._visible_tabs[i1] = tab.tab_nb + self._visible_tabs[i2] = self._dragged_tab.tab_nb + self.see(self._dragged_tab.tab_nb) + + def _on_click(self, event): + """Stop dragging.""" + if self._dragged_tab: + self._dragged_tab.unbind_all('') + self._dragged_tab.grid(**self._dummy_frame.grid_info()) + self._dummy_frame.grid_forget() + + if self._drag_to_toplevel: + end_pos_in_widget = coordinates_in_box((event.x_root, event.y_root), + parse_geometry(self.winfo_toplevel().winfo_geometry())) + if not end_pos_in_widget: + self.move_to_toplevel(self._dragged_tab) + self._dragged_tab = None + + def _menu_insert(self, tab, text): + menu = [] + for t in self._tabs.keys(): + menu.append((self.tab(t, 'text'), t)) + menu.sort() + ind = menu.index((text, tab)) + self._tab_menu.insert_radiobutton(ind, label=text, + variable=self._tab_var, value=tab, + command=lambda t=tab: self._show(t)) + for i, (text, tab) in enumerate(menu): + self._tab_menu_entries[tab] = i + + def _resize(self): + """Resize the notebook so that all widgets can be displayed fully.""" + w, h = 0, 0 + for tab in self._visible_tabs: + widget = self._tabs[tab] + w = max(w, widget.winfo_reqwidth()) + h = max(h, widget.winfo_reqheight()) + w = max(w, self._tab_frame.winfo_reqwidth()) + self._canvas.configure(width=w) + self._body.configure(width=w, height=h) + self._on_configure() + + def _show(self, tab_id, new=False, update=False): + if self.tab(tab_id, 'state') == 'disabled': + if tab_id in self._active_tabs: + self._active_tabs.remove(tab_id) + return + # hide current tab body + if self._current_tab >= 0: + self._tabs[self.current_tab].grid_remove() + self._tab_labels[self.current_tab].state(['!selected']) + + # restore tab if hidden + if tab_id in self._hidden_tabs: + self._tab_labels[tab_id].grid(in_=self._tab_frame) + self._visible_tabs.insert(self._tab_labels[tab_id].grid_info()['column'], tab_id) + self._active_tabs = [t for t in self._visible_tabs + if self._tab_options[t]['state'] == 'normal'] + self._hidden_tabs.remove(tab_id) + + # update current tab + self.current_tab = tab_id + self._tab_var.set(tab_id) + self._tab_labels[tab_id].state(['selected']) + + if new: + # add new tab + c, r = self._tab_frame.grid_size() + self._tab_labels[tab_id].grid(in_=self._tab_frame, row=0, column=c, sticky='s') + self._visible_tabs.append(tab_id) + + self.update_idletasks() + self._on_configure() + # ensure tab visibility + self.see(tab_id) + # display body + if update: + sticky = self._tab_options[tab_id]['sticky'] + pad = self._tab_options[tab_id]['padding'] + self._tabs[tab_id].grid(in_=self._body, sticky=sticky, padx=pad, pady=pad) + else: + self._tabs[tab_id].grid(in_=self._body) + self.update_idletasks() + self.event_generate('<>') + + def _popup_menu(self, event, tab): + self._show(tab) + if self.menu is not None: + self.menu.tk_popup(event.x_root, event.y_root) + + @property + def current_tab(self): + """ Gets the current tab """ + return self._current_tab + + @current_tab.setter + def current_tab(self, tab_nb): + self._current_tab = tab_nb + self._tab_var.set(tab_nb) + + def cget(self, key): + if key == 'closebutton': + return self._closebutton + elif key == 'closecommand': + return self._closecommand + elif key == 'tabmenu': + return self._tabmenu + elif key == 'tabdrag': + return self._tabdrag + elif key == 'drag_to_toplevel': + return self._drag_to_toplevel + else: + return ttk.Frame.cget(self, key) + + def configure(self, cnf=None, **kw): + """ + Configures this Notebook widget. + + :param closebutton: If a close button should show on the tabs + :type closebutton: bool + :param closecommand: A callable to call when the tab is closed, takes one argument, the tab_id + :type closecommand: callable + :param tabdrag: Enable/disable tab dragging and reordering + :type tabdrag: bool + :param drag_to_toplevel: Enable/disable tab dragging to toplevel windows + :type drag_to_toplevel: bool + :param **kw: Other keyword arguments as expected by ttk.Notebook + """ + if cnf: + kwargs = cnf.copy() + kwargs.update(kw) + else: + kwargs = kw.copy() + tab_kw = {} + if 'closebutton' in kwargs: + self._closebutton = bool(kwargs.pop('closebutton')) + tab_kw['closebutton'] = self._closebutton + if 'closecommand' in kwargs: + self._closecommand = kwargs.pop('closecommand') + tab_kw['closecommand'] = self._closecommand + if 'tabdrag' in kwargs: + self._tabdrag = bool(kwargs.pop('tabdrag')) + if 'drag_to_toplevel' in kwargs: + self._drag_to_toplevel = bool(kwargs.pop('drag_to_toplevel')) + if 'tabmenu' in kwargs: + self._tabmenu = bool(kwargs.pop('tabmenu')) + if self._tabmenu: + self._tab_list.grid() + else: + self._tab_list.grid_remove() + self.update_idletasks() + self._on_configure() + if tab_kw: + for tab, label in self._tab_labels.items(): + label.tab_configure(**tab_kw) + self.update_idletasks() + ttk.Frame.configure(self, **kwargs) + + def keys(self): + keys = ttk.Frame.keys(self) + return keys + ['closebutton', 'closecommand', 'tabmenu'] + + def add(self, widget, **kwargs): + """ + Add widget (or redisplay it if it was hidden) in the notebook and return + the tab index. + + :param text: tab label + :param image: tab image + :param compound: how the tab label and image are organized + :param sticky: for the widget inside the notebook + :param padding: padding (int) around the widget in the notebook + :param state: state ('normal' or 'disabled') of the tab + """ + # Todo: underline + name = str(widget) + if name in self._indexes: + ind = self._indexes[name] + self.tab(ind, **kwargs) + self._show(ind) + self.update_idletasks() + else: + sticky = kwargs.pop('sticky', 'ewns') + padding = kwargs.pop('padding', 0) + self._tabs[self._nb_tab] = widget + ind = self._nb_tab + self._indexes[name] = ind + self._tab_labels[ind] = Tab(self._tab_frame2, tab_nb=ind, + closecommand=self._closecommand, + closebutton=self._closebutton, + **kwargs) + self._tab_labels[ind].bind('', self._on_click) + self._tab_labels[ind].bind('', lambda e: self._popup_menu(e, ind)) + self._tab_labels[ind].bind('', lambda e: self._on_press(e, ind)) + self._body.configure(height=max(self._body.winfo_height(), widget.winfo_reqheight()), + width=max(self._body.winfo_width(), widget.winfo_reqwidth())) + + self._tab_options[ind] = dict(text='', image='', compound='none', state='normal') + self._tab_options[ind].update(kwargs) + self._tab_options[ind].update(dict(padding=padding, sticky=sticky)) + self._tab_menu_entries[ind] = self._tab_menu.index('end') + self._tab_list.state(['!disabled']) + self._active_tabs.append(ind) + self._show(self._nb_tab, new=True, update=True) + + self._nb_tab += 1 + self._menu_insert(ind, kwargs.get('text', '')) + return ind + + def insert(self, where, widget, **kwargs): + """ + Insert WIDEGT at the position given by WHERE in the notebook. + + For keyword options, see add method. + """ + existing = str(widget) in self._indexes + index = self.add(widget, **kwargs) + if where == 'end': + if not existing: + return + where = self.index(where) + self._visible_tabs.remove(index) + self._visible_tabs.insert(where, index) + for i in range(where, len(self._visible_tabs)): + ind = self._visible_tabs[i] + self._tab_labels[ind].grid_configure(column=i) + self.update_idletasks() + self._on_configure() + + def enable_traversal(self): + self.bind('', lambda e: self.select_next(True)) + self.bind('', lambda e: self.select_prev(True)) + + def index(self, tab_id): + """Return the tab index of TAB_ID.""" + if tab_id == tk.END: + return len(self._tabs) + elif tab_id == tk.CURRENT: + return self.current_tab + elif tab_id in self._tabs: + return tab_id + else: + try: + return self._indexes[str(tab_id)] + except KeyError: + raise ValueError('No such tab in the Notebook: %s' % tab_id) + + def select_next(self, rotate=False): + """Go to next tab.""" + if self.current_tab >= 0: + index = self._visible_tabs.index(self.current_tab) + index += 1 + if index < len(self._visible_tabs): + self._show(self._visible_tabs[index]) + elif rotate: + self._show(self._visible_tabs[0]) + + def select_prev(self, rotate=False): + """Go to prev tab.""" + if self.current_tab >= 0: + index = self._visible_tabs.index(self.current_tab) + index -= 1 + if index >= 0: + self._show(self._visible_tabs[index]) + elif rotate: + self._show(self._visible_tabs[-1]) + + def see(self, tab_id): + """Make label of tab TAB_ID visible.""" + if tab_id < 0: + return + tab = self.index(tab_id) + w = self._tab_frame.winfo_reqwidth() + label = self._tab_labels[tab] + x1 = label.winfo_x() / w + x2 = x1 + label.winfo_reqwidth() / w + xc1, xc2 = self._canvas.xview() + if x1 < xc1: + self._canvas.xview_moveto(x1) + elif x2 > xc2: + self._canvas.xview_moveto(xc1 + x2 - xc2) + i = self._visible_tabs.index(tab) + if i == 0: + self._btn_left.state(['disabled']) + if len(self._visible_tabs) > 1: + self._btn_right.state(['!disabled']) + elif i == len(self._visible_tabs) - 1: + self._btn_right.state(['disabled']) + self._btn_left.state(['!disabled']) + else: + self._btn_right.state(['!disabled']) + self._btn_left.state(['!disabled']) + + def hide(self, tab_id): + """Hide tab TAB_ID.""" + tab = self.index(tab_id) + if tab in self._visible_tabs: + self._visible_tabs.remove(tab) + if tab in self._active_tabs: + self._active_tabs.remove(tab) + self._hidden_tabs.append(tab) + self._tab_labels[tab].grid_remove() + if self.current_tab == tab: + if self._active_tabs: + self._show(self._active_tabs[0]) + else: + self.current_tab = -1 + self._tabs[tab].grid_remove() + self.update_idletasks() + self._on_configure() + self._resize() + + def forget(self, tab_id): + """Remove tab TAB_ID from notebook.""" + tab = self.index(tab_id) + if tab in self._hidden_tabs: + self._hidden_tabs.remove(tab) + elif tab in self._visible_tabs: + if tab in self._active_tabs: + self._active_tabs.remove(tab) + self._visible_tabs.remove(tab) + self._tab_labels[tab].grid_forget() + if self.current_tab == tab: + if self._active_tabs: + self._show(self._active_tabs[0]) + else: + self.current_tab = -1 + if not self._visible_tabs and not self._hidden_tabs: + self._tab_list.state(['disabled']) + self._tabs[tab].grid_forget() + del self._tab_labels[tab] + del self._indexes[str(self._tabs[tab])] + del self._tabs[tab] + self.update_idletasks() + self._on_configure() + i = self._tab_menu_entries[tab] + for t, ind in self._tab_menu_entries.items(): + if ind > i: + self._tab_menu_entries[t] -= 1 + self._tab_menu.delete(self._tab_menu_entries[tab]) + del self._tab_menu_entries[tab] + self._resize() + + def move_to_toplevel(self, tab): + tl = tk.Toplevel(self) + nb = Notebook(tl, dont_setup_style=True, **self._init_kwargs) + move_widget(tab, nb) + nb.add(tab) + nb.grid() + self._toplevels.append(tl) + tl.mainloop() + + def select(self, tab_id=None): + """Select tab TAB_ID. If TAB_ID is None, return currently selected tab.""" + if tab_id is None: + return self.current_tab + self._show(self.index(tab_id)) + + def tab(self, tab_id, option=None, **kw): + """ + Query or modify TAB_ID options. + + The widget corresponding to tab_id can be obtained by passing the option + 'widget' but cannot be modified. + """ + tab = self.index(tab_id) + if option == 'widget': + return self._tabs[tab] + elif option: + return self._tab_options[tab][option] + else: + self._tab_options[tab].update(kw) + sticky = kw.pop('padding', None) + padding = kw.pop('sticky', None) + self._tab_labels[tab].tab_configure(**kw) + if sticky is not None or padding is not None and self.current_tab == tab: + self._show(tab, update=True) + if 'text' in kw: + self._tab_menu.delete(self._tab_menu_entries[tab]) + self._menu_insert(tab, kw['text']) + if 'state' in kw: + self._tab_menu.entryconfigure(self._tab_menu_entries[tab], + state=kw['state']) + if kw['state'] == 'disabled': + if tab in self._active_tabs: + self._active_tabs.remove(tab) + if tab == self.current_tab: + tabs = self._visible_tabs.copy() + if tab in tabs: + tabs.remove(tab) + if tabs: + self._show(tabs[0]) + else: + self._tabs[tab].grid_remove() + self.current_tab = -1 + else: + self._active_tabs = [t for t in self._visible_tabs + if self._tab_options[t]['state'] == 'normal'] + if self.current_tab == -1: + self._show(tab) + + def tabs(self): + """Return the tuple of visible tab ids in the order of display.""" + return tuple(self._visible_tabs) diff --git a/ttkwidgets/utilities.py b/ttkwidgets/utilities.py index 22bf5134..6cf34caf 100644 --- a/ttkwidgets/utilities.py +++ b/ttkwidgets/utilities.py @@ -5,6 +5,7 @@ """ import os from PIL import Image, ImageTk +import re def get_assets_directory(): @@ -22,3 +23,119 @@ def parse_geometry_string(string): e = e[1].split("+") h, x, y = map(int, e) return x, y, w, h + + +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() + """ + return {key: widget.cget(key) for key in widget.keys()} + + +def copy_widget(widget, new_parent, level=0): + """ + Recursive function that copies a widget to a new parent. + + Ported to python from this tcl code : + https://stackoverflow.com/questions/6285648/can-you-change-a-widgets-parent-in-python-tkinter + + :param widget: widget to copy (tkinter.Widget instance) + :param new_parent: new widget to parent to. + :param level: (default: 0) current level of the recursive algorithm + + :return: tkinter.Widget instance, the copied widget. + """ + rv = widget.__class__(master=new_parent, **get_widget_options(widget)) + for b in widget.bind(): + script = widget.bind(b) + # TODO: bind the script to the new widget (rv) + # set type [ getWidgetType $w ] + # set name [ string trimright $newparent.[lindex [split $w "." ] end ] "." ] + # set retval [ $type $name {*}[ getConfigOptions $w ] ] + # foreach b [ bind $w ] { + # puts "bind $retval $b [subst { [bind $w $b ] } ] " + # bind $retval $b [subst { [bind $w $b ] } ] + # } + + if level > 0: + if widget.grid_info(): # if geometry manager is grid + temp = widget.grid_info() + del temp['in'] + rv.grid(**temp) + elif widget.place_info(): # if geometry manager is place + temp = widget.place_info() + del temp['in'] + rv.place(**temp) + else: # if geometry manager is pack + temp = widget.pack_info() + del temp['in'] + rv.pack(**temp) + level += 1 + if widget.pack_slaves(): # subwidgets are using the pack() geometry manager + for child in widget.pack_slaves(): + copy_widget(child, rv, level) + else: + for child in widget.winfo_children(): + copy_widget(child, rv, level) + return rv + + +def move_widget(widget, new_parent): + """ + Moves widget to new_parent + + :param widget: widget to move + :param new_parent: new parent for the widget + + :return: moved widget reference + """ + rv = copy_widget(widget, new_parent) + widget.destroy() + return rv + + +def parse_geometry(geometry): + """ + Parses a tkinter geometry string into a 4-tuple (x, y, width, height) + + :param geometry: a tkinter geometry string in the format (wxh+x+y) + :type geometry: str + :returns: 4-tuple (x, y, width, height) + :rtype: tuple + """ + match = re.search(r'(\d+)x(\d+)\+(\d+)\+(\d+)', geometry) + return ( int(match.group(3)), int(match.group(4)), + int(match.group(1)), int(match.group(2))) + + +def coordinates_in_box(coords, bbox, include_edges=True): + """ + Checks whether coords are inside bbox + + :param coords: 2-tuple of coordinates x, y + :type coords: tuple + :param bbox: 4-tuple (x, y, width, height) of a bounding box + :type bbox: tuple + :param include_edges: default True whether to include the edges + :type include_edges: bool + :returns: whether coords is inside bbox + :rtype: bool + :raises: ValueError if length of bbox or coords do not match the specifications + """ + if len(coords) != 2: + raise ValueError("Coords argument is supposed to be of length 2") + if len(bbox) != 4: + raise ValueError("Bbox argument is supposed to be of length 4") + + x, y = coords + xmin, ymin, width, height = bbox + xmax, ymax = xmin + width, ymin + height + if include_edges: + xmin = max(xmin - 1, 0) + xmax += 1 + ymin = max(ymin - 1, 0) + ymax += 1 + return xmin < x < xmax and ymin < y < ymax From ccbcba589ca40565ff74c9dfa2ead1eb368e6d7e Mon Sep 17 00:00:00 2001 From: Dogeek Date: Mon, 2 Dec 2019 17:11:52 +0100 Subject: [PATCH 02/31] Improve Notebook example with colour --- examples/example_notebook.py | 16 ++++++++++------ ttkwidgets/notebook.py | 6 +++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/examples/example_notebook.py b/examples/example_notebook.py index aa9461a5..e52728d7 100644 --- a/examples/example_notebook.py +++ b/examples/example_notebook.py @@ -4,19 +4,23 @@ class MainWindow(ttk.Frame): - def __init__(self, master=None): - super().__init__(master) - self.nb = Notebook(self) - self.frames = [tk.Frame(self) for i in range(10)] + def __init__(self, master): + ttk.Frame.__init__(self, master) + colors = ['red', 'blue', 'green', 'yellow', 'cyan', 'magenta', 'black', 'white', 'purple', 'brown'] + self.nb = Notebook(self, tabdrag=True, tabmenu=True, closebutton=True, closecommand=self.closecmd) + self.frames = [tk.Frame(self, width=300, height=300, bg=color) for i, color in enumerate(colors)] for i, w in enumerate(self.frames): - tk.Canvas(w, width=300, height=300).grid(sticky="nswe") self.nb.add(w, text="Frame " + str(i)) w.grid() self.nb.grid() + + def closecmd(self, tab_id): + print("Close tab " + str(tab_id)) + self.nb.forget(tab_id) root = tk.Tk() root.title("Notebook Example") gui = MainWindow(root) gui.grid() -root.mainloop() \ No newline at end of file +root.mainloop() diff --git a/ttkwidgets/notebook.py b/ttkwidgets/notebook.py index 8049728d..34b235c1 100644 --- a/ttkwidgets/notebook.py +++ b/ttkwidgets/notebook.py @@ -207,8 +207,8 @@ def __init__(self, master=None, **kwargs): self._drag_to_toplevel = bool(kwargs.pop('drag_to_toplevel', self._tabdrag)) self._tabmenu = bool(kwargs.pop('tabmenu', True)) - ttk.Frame.__init__(self, master, class_='Notebook', padding=(0, 0, 0, 1), - **kwargs) + ttk.Frame.__init__(self, master, class_='Notebook', padding=(0, 0, 0, 1), **kwargs) + if not Notebook._initialized: self.setup_style() @@ -874,7 +874,7 @@ def forget(self, tab_id): def move_to_toplevel(self, tab): tl = tk.Toplevel(self) - nb = Notebook(tl, dont_setup_style=True, **self._init_kwargs) + nb = Notebook(tl, **self._init_kwargs) move_widget(tab, nb) nb.add(tab) nb.grid() From 8a46c0379ff4d09befce121366e0a6532d3c92ac Mon Sep 17 00:00:00 2001 From: Dogeek Date: Fri, 6 Dec 2019 01:20:28 +0100 Subject: [PATCH 03/31] Update Notebook style options --- ttkwidgets/notebook.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ttkwidgets/notebook.py b/ttkwidgets/notebook.py index 34b235c1..e47ed7a0 100644 --- a/ttkwidgets/notebook.py +++ b/ttkwidgets/notebook.py @@ -448,11 +448,8 @@ def setup_style(self, bg="#dddddd", activebg="#efefef", pressedbg="#c1c1c1", style.map('Notebook.Tab', **{'background': [('selected', '!disabled', theme['activebg'])]}) - style.configure('TNotebook.Tab', background=theme['bg'], - foreground=theme['unselectedfg']) - style.map('TNotebook.Tab', - **{'background': [('selected', '!disabled', theme['activebg'])], - 'foreground': [('selected', '!disabled', theme['fg'])]}) + style.configure('Left.Notebook.TButton', padding=0) + style.configure('Right.Notebook.TButton', padding=0) def _on_configure(self, event=None): self.update_idletasks() From a10ee66c4e67ba0b44023c91f77aa6c97784bd27 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Sat, 7 Dec 2019 13:19:17 +0100 Subject: [PATCH 04/31] Create basic test for Notebook widget --- examples/example_notebook.py | 8 ++++++-- tests/test_notebook.py | 14 ++++++++++++++ ttkwidgets/notebook.py | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 tests/test_notebook.py diff --git a/examples/example_notebook.py b/examples/example_notebook.py index e52728d7..e06eb114 100644 --- a/examples/example_notebook.py +++ b/examples/example_notebook.py @@ -1,5 +1,9 @@ -import tkinter as tk -import tkinter.ttk as ttk +try: + import tkinter as tk + from tkinter import ttk +except ImportError: + import Tkinter as tk + import ttk from ttkwidgets import Notebook diff --git a/tests/test_notebook.py b/tests/test_notebook.py new file mode 100644 index 00000000..aee4949b --- /dev/null +++ b/tests/test_notebook.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" +Author: Dogeek +Copyright (c) 2019 Dogeek +""" +from ttkwidgets import Notebook +from tests import BaseWidgetTest + + +class TestNotebook(BaseWidgetTest): + def test_notebook_init(self): + nb = Notebook(self.window) + nb.grid() + self.window.update() diff --git a/ttkwidgets/notebook.py b/ttkwidgets/notebook.py index e47ed7a0..0876e07f 100644 --- a/ttkwidgets/notebook.py +++ b/ttkwidgets/notebook.py @@ -13,8 +13,8 @@ Notebook with draggable / scrollable tabs """ -from tkinter import ttk import tkinter as tk +from tkinter import ttk from ttkwidgets.utilities import move_widget, parse_geometry, coordinates_in_box From 2772be2ed855af557b7f6620209afcc5b0a5981f Mon Sep 17 00:00:00 2001 From: RedFantom Date: Sat, 7 Dec 2019 14:06:04 +0100 Subject: [PATCH 05/31] Create entry in AUTHORS.md for Notebook widget --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index 5e60a2d5..a12101a7 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -24,5 +24,6 @@ This file contains a list of all the authors of widgets in this repository. Plea * `AutoHideScrollbar` based on an idea by [Fredrik Lundh](effbot.org/zone/tkinter-autoscrollbar.htm) * All color widgets: `askcolor`, `ColorPicker`, `GradientBar` and `ColorSquare`, `LimitVar`, `Spinbox`, `AlphaBar` and supporting functions in `functions.py`. * `AutocompleteEntryListbox` + * [`Notebook`](https://github.com/j4321/PyTkEditor/blob/master/pytkeditorlib/notebook.py), modified by [Dogeek](https://github.com/Dogeek) - Multiple authors: * `ScaleEntry` (RedFantom and Juliette Monsel) From d470cb6496f840ede8c535cde2ce9a6f77c32cf8 Mon Sep 17 00:00:00 2001 From: Dogeek Date: Sat, 7 Dec 2019 18:01:52 +0100 Subject: [PATCH 06/31] Add tests for newly added utilities --- tests/test_utilities.py | 57 +++++++++++++++++++++++++++++++++++++++++ ttkwidgets/utilities.py | 6 +++-- 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 tests/test_utilities.py diff --git a/tests/test_utilities.py b/tests/test_utilities.py new file mode 100644 index 00000000..887ecfce --- /dev/null +++ b/tests/test_utilities.py @@ -0,0 +1,57 @@ +# Copyright (c) Dogeek 2019 +# For license see LICENSE +from ttkwidgets.utilities import move_widget, parse_geometry +from tests import BaseWidgetTest +try: + import Tkinter as tk + import ttk +except ImportError: + import tkinter as tk + from tkinter import ttk + + +class TestUtilities(BaseWidgetTest): + def test_move_widget(self): + label = ttk.Label(self.window) + tl = tk.Toplevel(self.window) + label = move_widget(label, tl) + self.assertTrue(label.winfo_parent() == '.' + tl.winfo_name()) + + def test_move_widget_pack(self): + label = ttk.Label(self.window) + label.pack() + tl = tk.Toplevel(self.window) + label = move_widget(label, tl) + label.pack() + self.assertTrue(label.winfo_parent() == '.' + tl.winfo_name()) + self.assertIn(label, tl.pack_slaves()) + + def test_move_widget_grid(self): + label = ttk.Label(self.window) + label.grid() + tl = tk.Toplevel(self.window) + label = move_widget(label, tl) + label.grid() + self.assertTrue(label.winfo_parent() == '.' + tl.winfo_name()) + self.assertIn(label, tl.grid_slaves()) + + def test_move_widget_place(self): + label = ttk.Label(self.window) + label.place() + tl = tk.Toplevel(self.window) + label = move_widget(label, tl) + label.place() + self.assertTrue(label.winfo_parent() == '.' + tl.winfo_name()) + self.assertIn(label, tl.place_slaves()) + + def test_move_widget_with_binding(self): + raise NotImplementedError + + def test_move_widget_with_binding_on_parent(self): + raise NotImplementedError + + def test_parse_geometry(self): + g = parse_geometry('1x1+1+1') + self.assertEqual(g, (1, 1, 1, 1)) + g = parse_geometry('1x1-1-1') + self.assertEqual(g, (1, 1, -1, -1)) diff --git a/ttkwidgets/utilities.py b/ttkwidgets/utilities.py index 6cf34caf..a2f90210 100644 --- a/ttkwidgets/utilities.py +++ b/ttkwidgets/utilities.py @@ -106,8 +106,10 @@ def parse_geometry(geometry): :returns: 4-tuple (x, y, width, height) :rtype: tuple """ - match = re.search(r'(\d+)x(\d+)\+(\d+)\+(\d+)', geometry) - return ( int(match.group(3)), int(match.group(4)), + match = re.search(r'(\d+)x(\d+)(\+|-)(\d+)(\+|-)(\d+)', geometry) + xmod = -1 if match.group(3) == '-' else 1 + ymod = -1 if match.group(5) == '-' else 1 + return ( xmod * int(match.group(4)), ymod * int(match.group(6)), int(match.group(1)), int(match.group(2))) From f2d03e3f78224b29624d25eb3c86c02adc050bd2 Mon Sep 17 00:00:00 2001 From: Dogeek Date: Sat, 7 Dec 2019 18:03:38 +0100 Subject: [PATCH 07/31] Implement some of requested Notebook changes - Removed shebang and encoding - Removed Notebook widget from Toplevel in move_to_toplevel --- ttkwidgets/notebook.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/ttkwidgets/notebook.py b/ttkwidgets/notebook.py index 0876e07f..4550d579 100644 --- a/ttkwidgets/notebook.py +++ b/ttkwidgets/notebook.py @@ -1,6 +1,3 @@ -#! /usr/bin/python3 -# -*- coding: utf-8 -*- - """ Copyright 2018-2019 Juliette Monsel Copyright 2019 Dogeek @@ -871,12 +868,9 @@ def forget(self, tab_id): def move_to_toplevel(self, tab): tl = tk.Toplevel(self) - nb = Notebook(tl, **self._init_kwargs) - move_widget(tab, nb) - nb.add(tab) - nb.grid() + move_widget(tab, tl) + tab.grid() self._toplevels.append(tl) - tl.mainloop() def select(self, tab_id=None): """Select tab TAB_ID. If TAB_ID is None, return currently selected tab.""" From c652c04dd9d750bdf1c31aeacca27c59a07bf706 Mon Sep 17 00:00:00 2001 From: Dogeek Date: Sat, 7 Dec 2019 18:10:33 +0100 Subject: [PATCH 08/31] Add tests for coords_in_box function --- tests/test_utilities.py | 21 ++++++++++++++------- ttkwidgets/utilities.py | 1 + 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 887ecfce..3e502bca 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -1,13 +1,9 @@ # Copyright (c) Dogeek 2019 # For license see LICENSE -from ttkwidgets.utilities import move_widget, parse_geometry +from ttkwidgets.utilities import move_widget, parse_geometry, coordinates_in_box from tests import BaseWidgetTest -try: - import Tkinter as tk - import ttk -except ImportError: - import tkinter as tk - from tkinter import ttk +import tkinter as tk +from tkinter import ttk class TestUtilities(BaseWidgetTest): @@ -55,3 +51,14 @@ def test_parse_geometry(self): self.assertEqual(g, (1, 1, 1, 1)) g = parse_geometry('1x1-1-1') self.assertEqual(g, (1, 1, -1, -1)) + + def test_coordinates_in_box(self): + with self.assertRaises(ValueError): + coordinates_in_box((1, ), (1, 1, 3, 3)) + + with self.assertRaises(ValueError): + coordinates_in_box((1, 1), (1, 1, 3, 3, 4)) + + self.assertTrue(coordinates_in_box((1, 1), (0, 0, 2, 2))) + self.assertFalse(coordinates_in_box((1, 1), (1, 1, 2, 2), include_edges=False)) + self.assertTrue(coordinates_in_box((0, 0), (-1, -1, 1, 1))) diff --git a/ttkwidgets/utilities.py b/ttkwidgets/utilities.py index a2f90210..3bbf62fe 100644 --- a/ttkwidgets/utilities.py +++ b/ttkwidgets/utilities.py @@ -51,6 +51,7 @@ def copy_widget(widget, new_parent, level=0): rv = widget.__class__(master=new_parent, **get_widget_options(widget)) for b in widget.bind(): script = widget.bind(b) + rv.bind(b, script) # Not sure it will work tho # TODO: bind the script to the new widget (rv) # set type [ getWidgetType $w ] # set name [ string trimright $newparent.[lindex [split $w "." ] end ] "." ] From 17a7e57148b1e4a7f569ed0e8c9410160506e652 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Sat, 7 Dec 2019 18:24:25 +0100 Subject: [PATCH 09/31] Refactor coordinates_in_box to coords_in_box --- tests/test_utilities.py | 12 ++++++------ ttkwidgets/notebook.py | 7 +++---- ttkwidgets/utilities.py | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 3e502bca..0e2b0b02 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -1,6 +1,6 @@ # Copyright (c) Dogeek 2019 # For license see LICENSE -from ttkwidgets.utilities import move_widget, parse_geometry, coordinates_in_box +from ttkwidgets.utilities import move_widget, parse_geometry, coords_in_box from tests import BaseWidgetTest import tkinter as tk from tkinter import ttk @@ -54,11 +54,11 @@ def test_parse_geometry(self): def test_coordinates_in_box(self): with self.assertRaises(ValueError): - coordinates_in_box((1, ), (1, 1, 3, 3)) + coords_in_box((1,), (1, 1, 3, 3)) with self.assertRaises(ValueError): - coordinates_in_box((1, 1), (1, 1, 3, 3, 4)) + coords_in_box((1, 1), (1, 1, 3, 3, 4)) - self.assertTrue(coordinates_in_box((1, 1), (0, 0, 2, 2))) - self.assertFalse(coordinates_in_box((1, 1), (1, 1, 2, 2), include_edges=False)) - self.assertTrue(coordinates_in_box((0, 0), (-1, -1, 1, 1))) + self.assertTrue(coords_in_box((1, 1), (0, 0, 2, 2))) + self.assertFalse(coords_in_box((1, 1), (1, 1, 2, 2), include_edges=False)) + self.assertTrue(coords_in_box((0, 0), (-1, -1, 1, 1))) diff --git a/ttkwidgets/notebook.py b/ttkwidgets/notebook.py index 4550d579..3547ff99 100644 --- a/ttkwidgets/notebook.py +++ b/ttkwidgets/notebook.py @@ -9,10 +9,9 @@ Notebook with draggable / scrollable tabs """ - import tkinter as tk from tkinter import ttk -from ttkwidgets.utilities import move_widget, parse_geometry, coordinates_in_box +from ttkwidgets.utilities import move_widget, parse_geometry, coords_in_box class Tab(ttk.Frame): @@ -535,8 +534,8 @@ def _on_click(self, event): self._dummy_frame.grid_forget() if self._drag_to_toplevel: - end_pos_in_widget = coordinates_in_box((event.x_root, event.y_root), - parse_geometry(self.winfo_toplevel().winfo_geometry())) + end_pos_in_widget = coords_in_box((event.x_root, event.y_root), + parse_geometry(self.winfo_toplevel().winfo_geometry())) if not end_pos_in_widget: self.move_to_toplevel(self._dragged_tab) self._dragged_tab = None diff --git a/ttkwidgets/utilities.py b/ttkwidgets/utilities.py index 3bbf62fe..b95c4d18 100644 --- a/ttkwidgets/utilities.py +++ b/ttkwidgets/utilities.py @@ -114,7 +114,7 @@ def parse_geometry(geometry): int(match.group(1)), int(match.group(2))) -def coordinates_in_box(coords, bbox, include_edges=True): +def coords_in_box(coords, bbox, include_edges=True): """ Checks whether coords are inside bbox From 9d5be6c5d3eb98be7f01eabad899d091bc4ca853 Mon Sep 17 00:00:00 2001 From: Dogeek Date: Sat, 7 Dec 2019 18:28:20 +0100 Subject: [PATCH 10/31] Extend newly added utilities unit tests - Move widget with binding on parent - Coordinates in box fixed - Move widget with binding --- tests/test_utilities.py | 29 +++++++++++++++++++++++------ ttkwidgets/utilities.py | 21 +++++++++++++-------- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 0e2b0b02..54406e6e 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -7,6 +7,9 @@ class TestUtilities(BaseWidgetTest): + def _dummy_bind(self, event): + pass + def test_move_widget(self): label = ttk.Label(self.window) tl = tk.Toplevel(self.window) @@ -41,16 +44,30 @@ def test_move_widget_place(self): self.assertIn(label, tl.place_slaves()) def test_move_widget_with_binding(self): - raise NotImplementedError + label = ttk.Label(self.window) + label.bind('', self._dummy_bind) + label.pack() + tl = tk.Toplevel(self.window) + label = move_widget(label, tl) + label.pack() + self.assertTrue(label.winfo_parent() == '.' + tl.winfo_name()) + self.assertIn('', label.bind()) def test_move_widget_with_binding_on_parent(self): - raise NotImplementedError - + self.window.bind('', self._dummy_bind) + label = ttk.Label(self.window) + label.pack() + tl = tk.Toplevel(self.window) + label = move_widget(label, tl) + label.pack() + self.assertTrue(label.winfo_parent() == '.' + tl.winfo_name()) + self.assertIn('', tl.bind()) + def test_parse_geometry(self): g = parse_geometry('1x1+1+1') self.assertEqual(g, (1, 1, 1, 1)) g = parse_geometry('1x1-1-1') - self.assertEqual(g, (1, 1, -1, -1)) + self.assertEqual(g, (-1, -1, 1, 1)) def test_coordinates_in_box(self): with self.assertRaises(ValueError): @@ -58,7 +75,7 @@ def test_coordinates_in_box(self): with self.assertRaises(ValueError): coords_in_box((1, 1), (1, 1, 3, 3, 4)) - + self.assertTrue(coords_in_box((1, 1), (0, 0, 2, 2))) self.assertFalse(coords_in_box((1, 1), (1, 1, 2, 2), include_edges=False)) - self.assertTrue(coords_in_box((0, 0), (-1, -1, 1, 1))) + self.assertTrue(coords_in_box((0, 0), (-1, -1, 1, 1), bbox_is_x1y1x2y2=True)) diff --git a/ttkwidgets/utilities.py b/ttkwidgets/utilities.py index b95c4d18..46142832 100644 --- a/ttkwidgets/utilities.py +++ b/ttkwidgets/utilities.py @@ -114,7 +114,7 @@ def parse_geometry(geometry): int(match.group(1)), int(match.group(2))) -def coords_in_box(coords, bbox, include_edges=True): +def coords_in_box(coords, bbox, include_edges=True, bbox_is_x1y1x2y2=False): """ Checks whether coords are inside bbox @@ -124,6 +124,9 @@ def coords_in_box(coords, bbox, include_edges=True): :type bbox: tuple :param include_edges: default True whether to include the edges :type include_edges: bool + :param bbox_is_x1y1x2y2: default False whether the bbox is in + (x, y, width, height) or (x1, y1, x2, y2) format + :type bbox_is_x1y1x2y2: bool :returns: whether coords is inside bbox :rtype: bool :raises: ValueError if length of bbox or coords do not match the specifications @@ -134,11 +137,13 @@ def coords_in_box(coords, bbox, include_edges=True): raise ValueError("Bbox argument is supposed to be of length 4") x, y = coords - xmin, ymin, width, height = bbox - xmax, ymax = xmin + width, ymin + height + if bbox_is_x1y1x2y2: + xmin, ymin, xmax, ymax = bbox + else: + xmin, ymin, width, height = bbox + xmax, ymax = xmin + width, ymin + height + if include_edges: - xmin = max(xmin - 1, 0) - xmax += 1 - ymin = max(ymin - 1, 0) - ymax += 1 - return xmin < x < xmax and ymin < y < ymax + return xmin <= x <= xmax and ymin <= y <= ymax + else: + return xmin < x < xmax and ymin < y < ymax From a3e19ea172fa4f50e5845509ab1c28204dc4c0e9 Mon Sep 17 00:00:00 2001 From: Dogeek Date: Sat, 7 Dec 2019 18:32:59 +0100 Subject: [PATCH 11/31] Addressed more requested changes to Notebook - Typos in docstrings - Docstring format in test_notebook.py --- tests/test_notebook.py | 7 ++----- ttkwidgets/notebook.py | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/test_notebook.py b/tests/test_notebook.py index aee4949b..e030bc5d 100644 --- a/tests/test_notebook.py +++ b/tests/test_notebook.py @@ -1,8 +1,5 @@ -# -*- coding: utf-8 -*- -""" -Author: Dogeek -Copyright (c) 2019 Dogeek -""" +# Copyright (c) Dogeek 2019 +# For license see LICENSE from ttkwidgets import Notebook from tests import BaseWidgetTest diff --git a/ttkwidgets/notebook.py b/ttkwidgets/notebook.py index 3547ff99..710d0734 100644 --- a/ttkwidgets/notebook.py +++ b/ttkwidgets/notebook.py @@ -20,7 +20,7 @@ def __init__(self, master=None, tab_nb=0, **kwargs): """ :param master: parent widget :param tab_nb: tab index - :param **kwargs: keyword arguments for ttk::Frame widgets + :param **kwargs: keyword arguments for ttk::Label widgets """ ttk.Frame.__init__(self, master, class_='Notebook.Tab', style='Notebook.Tab', padding=1) @@ -733,7 +733,7 @@ def add(self, widget, **kwargs): def insert(self, where, widget, **kwargs): """ - Insert WIDEGT at the position given by WHERE in the notebook. + Insert WIDGET at the position given by WHERE in the notebook. For keyword options, see add method. """ From ec87fd0a587273cf0cb5d47205d6b5bf02f61eb8 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Sat, 7 Dec 2019 22:15:29 +0100 Subject: [PATCH 12/31] Fix get_widget_options returning unused keys --- ttkwidgets/utilities.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ttkwidgets/utilities.py b/ttkwidgets/utilities.py index 46142832..ff462f30 100644 --- a/ttkwidgets/utilities.py +++ b/ttkwidgets/utilities.py @@ -32,7 +32,12 @@ def get_widget_options(widget): :param widget: tkinter.Widget instance to get the config options from :return: dict of options that you can pass on to widget.config() """ - return {key: widget.cget(key) for key in widget.keys()} + options = {} + for key in widget.keys(): + value = widget.cget(key) + if value not in ("", None): + options[key] = value + return options def copy_widget(widget, new_parent, level=0): From a6137eb6f3462352e2ba5a1d95bd4eb8d389b634 Mon Sep 17 00:00:00 2001 From: Dogeek Date: Sun, 8 Dec 2019 01:51:57 +0100 Subject: [PATCH 13/31] Fix parent reference in move_widget tests --- tests/test_utilities.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 54406e6e..81fee8e7 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -14,7 +14,7 @@ def test_move_widget(self): label = ttk.Label(self.window) tl = tk.Toplevel(self.window) label = move_widget(label, tl) - self.assertTrue(label.winfo_parent() == '.' + tl.winfo_name()) + self.assertTrue(label.winfo_parent() == tl.winfo_parent() + '.' + tl.winfo_name()) def test_move_widget_pack(self): label = ttk.Label(self.window) @@ -22,7 +22,7 @@ def test_move_widget_pack(self): tl = tk.Toplevel(self.window) label = move_widget(label, tl) label.pack() - self.assertTrue(label.winfo_parent() == '.' + tl.winfo_name()) + self.assertTrue(label.winfo_parent() == tl.winfo_parent() + '.' + tl.winfo_name()) self.assertIn(label, tl.pack_slaves()) def test_move_widget_grid(self): @@ -31,7 +31,7 @@ def test_move_widget_grid(self): tl = tk.Toplevel(self.window) label = move_widget(label, tl) label.grid() - self.assertTrue(label.winfo_parent() == '.' + tl.winfo_name()) + self.assertTrue(label.winfo_parent() == tl.winfo_parent() + '.' + tl.winfo_name()) self.assertIn(label, tl.grid_slaves()) def test_move_widget_place(self): @@ -40,7 +40,7 @@ def test_move_widget_place(self): tl = tk.Toplevel(self.window) label = move_widget(label, tl) label.place() - self.assertTrue(label.winfo_parent() == '.' + tl.winfo_name()) + self.assertTrue(label.winfo_parent() == tl.winfo_parent() + '.' + tl.winfo_name()) self.assertIn(label, tl.place_slaves()) def test_move_widget_with_binding(self): @@ -50,7 +50,7 @@ def test_move_widget_with_binding(self): tl = tk.Toplevel(self.window) label = move_widget(label, tl) label.pack() - self.assertTrue(label.winfo_parent() == '.' + tl.winfo_name()) + self.assertTrue(label.winfo_parent() == tl.winfo_parent() + '.' + tl.winfo_name()) self.assertIn('', label.bind()) def test_move_widget_with_binding_on_parent(self): @@ -60,7 +60,7 @@ def test_move_widget_with_binding_on_parent(self): tl = tk.Toplevel(self.window) label = move_widget(label, tl) label.pack() - self.assertTrue(label.winfo_parent() == '.' + tl.winfo_name()) + self.assertTrue(label.winfo_parent() == tl.winfo_parent() + '.' + tl.winfo_name()) self.assertIn('', tl.bind()) def test_parse_geometry(self): From afdeb3b480b81b47e67d92b3b33dd6ceee516d77 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Sat, 14 Dec 2019 12:22:23 +0100 Subject: [PATCH 14/31] Reduce code duplication in TestUtilities --- tests/test_utilities.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 81fee8e7..41182a19 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -7,6 +7,13 @@ class TestUtilities(BaseWidgetTest): + def assertIsChild(self, child, parent): + self.assertIn(child, parent.children.values()) + parent_of_parent = parent.winfo_parent() + if not parent_of_parent.endswith("."): + parent_of_parent += "." + self.assertEquals(child.winfo_parent(), parent_of_parent + parent.winfo_name()) + def _dummy_bind(self, event): pass @@ -14,7 +21,7 @@ def test_move_widget(self): label = ttk.Label(self.window) tl = tk.Toplevel(self.window) label = move_widget(label, tl) - self.assertTrue(label.winfo_parent() == tl.winfo_parent() + '.' + tl.winfo_name()) + self.assertIsChild(label, tl) def test_move_widget_pack(self): label = ttk.Label(self.window) @@ -22,7 +29,7 @@ def test_move_widget_pack(self): tl = tk.Toplevel(self.window) label = move_widget(label, tl) label.pack() - self.assertTrue(label.winfo_parent() == tl.winfo_parent() + '.' + tl.winfo_name()) + self.assertIsChild(label, tl) self.assertIn(label, tl.pack_slaves()) def test_move_widget_grid(self): @@ -31,7 +38,7 @@ def test_move_widget_grid(self): tl = tk.Toplevel(self.window) label = move_widget(label, tl) label.grid() - self.assertTrue(label.winfo_parent() == tl.winfo_parent() + '.' + tl.winfo_name()) + self.assertIsChild(label, tl) self.assertIn(label, tl.grid_slaves()) def test_move_widget_place(self): @@ -40,7 +47,7 @@ def test_move_widget_place(self): tl = tk.Toplevel(self.window) label = move_widget(label, tl) label.place() - self.assertTrue(label.winfo_parent() == tl.winfo_parent() + '.' + tl.winfo_name()) + self.assertIsChild(label, tl) self.assertIn(label, tl.place_slaves()) def test_move_widget_with_binding(self): @@ -50,7 +57,7 @@ def test_move_widget_with_binding(self): tl = tk.Toplevel(self.window) label = move_widget(label, tl) label.pack() - self.assertTrue(label.winfo_parent() == tl.winfo_parent() + '.' + tl.winfo_name()) + self.assertIsChild(label, tl) self.assertIn('', label.bind()) def test_move_widget_with_binding_on_parent(self): From cbea5d982d7b7982a17c4581c333cd0822622304 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Sat, 14 Dec 2019 12:33:57 +0100 Subject: [PATCH 15/31] Extend tests for move_widget for widgets with bindings --- tests/test_utilities.py | 71 ++++++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 11 deletions(-) diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 41182a19..e4f845ad 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -7,15 +7,23 @@ class TestUtilities(BaseWidgetTest): + def setUp(self): + BaseWidgetTest.setUp(self) + self._dummy_flag = False + def assertIsChild(self, child, parent): self.assertIn(child, parent.children.values()) parent_of_parent = parent.winfo_parent() if not parent_of_parent.endswith("."): parent_of_parent += "." self.assertEquals(child.winfo_parent(), parent_of_parent + parent.winfo_name()) + + def assertHasBeenInvoked(self): + self.assertTrue(self._dummy_flag) + self._dummy_flag = False - def _dummy_bind(self, event): - pass + def _dummy_bind(self, _=None): + self._dummy_flag = True def test_move_widget(self): label = ttk.Label(self.window) @@ -60,16 +68,57 @@ def test_move_widget_with_binding(self): self.assertIsChild(label, tl) self.assertIn('', label.bind()) + def test_move_widget_with_command(self): + widget = ttk.Button(self.window, command=self._dummy_bind) + self.assertIsChild(widget, self.window) + widget.invoke() + self.assertHasBeenInvoked() + + parent = tk.Toplevel() + child = move_widget(widget, parent) + self.assertIsChild(child, parent) + widget.invoke() + self.assertHasBeenInvoked() + + def test_move_widget_with_bound_method_on_parent(self): + tl1 = tk.Toplevel(self.window) + tl2 = tk.Toplevel(self.window) + tl1._dummy_bind = self._dummy_bind + + button = ttk.Button(tl1) + button.bind("", tl1._dummy_bind) + button.event_generate("") + self.assertHasBeenInvoked() + + button = move_widget(button, tl2) + button.event_generate("") + self.assertHasBeenInvoked() + + def test_move_widget_with_command_method_on_parent(self): + tl1 = tk.Toplevel(self.window) + tl2 = tk.Toplevel(self.window) + tl1._dummy_bind = self._dummy_bind + + button = ttk.Button(tl1, command=tl1._dummy_bind) + button.invoke() + self.assertHasBeenInvoked() + + button = move_widget(button, tl2) + button.invoke() + self.assertHasBeenInvoked() + def test_move_widget_with_binding_on_parent(self): - self.window.bind('', self._dummy_bind) - label = ttk.Label(self.window) - label.pack() - tl = tk.Toplevel(self.window) - label = move_widget(label, tl) - label.pack() - self.assertTrue(label.winfo_parent() == tl.winfo_parent() + '.' + tl.winfo_name()) - self.assertIn('', tl.bind()) - + widget = ttk.Label(self.window) + widget._dummy_bind = self._dummy_bind + self.window.bind("", widget._dummy_bind) + + self.window.event_generate("") + self.assertHasBeenInvoked() + + move_widget(widget, tk.Toplevel()) + self.window.event_generate("") + self.assertHasBeenInvoked() + def test_parse_geometry(self): g = parse_geometry('1x1+1+1') self.assertEqual(g, (1, 1, 1, 1)) From dea90d0436d9935df48d7548ee926335bd8ebe5c Mon Sep 17 00:00:00 2001 From: RedFantom Date: Sun, 15 Dec 2019 15:07:37 +0100 Subject: [PATCH 16/31] Preserve created Tcl commands for bindings --- ttkwidgets/utilities.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/ttkwidgets/utilities.py b/ttkwidgets/utilities.py index ff462f30..036cf83d 100644 --- a/ttkwidgets/utilities.py +++ b/ttkwidgets/utilities.py @@ -1,7 +1,9 @@ """ Author: The ttkwidgets authors -License: GNU GPLv3 -Source: The ttkwidgets repository +License: GNU GPLv3, as in LICENSE.md +Copyright (c) 2016-2019 The ttkwidgets authors + +For author details, see AUTHORS.md """ import os from PIL import Image, ImageTk @@ -55,16 +57,9 @@ def copy_widget(widget, new_parent, level=0): """ rv = widget.__class__(master=new_parent, **get_widget_options(widget)) for b in widget.bind(): + widget._tclCommands = None # Preserve bound functions script = widget.bind(b) - rv.bind(b, script) # Not sure it will work tho - # TODO: bind the script to the new widget (rv) - # set type [ getWidgetType $w ] - # set name [ string trimright $newparent.[lindex [split $w "." ] end ] "." ] - # set retval [ $type $name {*}[ getConfigOptions $w ] ] - # foreach b [ bind $w ] { - # puts "bind $retval $b [subst { [bind $w $b ] } ] " - # bind $retval $b [subst { [bind $w $b ] } ] - # } + rv.bind(b, script) if level > 0: if widget.grid_info(): # if geometry manager is grid From a417614b642d4c3e9b873a8f89fb7ff4000ebf63 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Mon, 16 Dec 2019 10:40:43 +0100 Subject: [PATCH 17/31] Extend move_widget tests, add preserve_geometry kwarg The 'preserve_geometry' kwarg to 'move_widget' is passed to 'copy_widget' and allows a user to force the function to automatically reconfigure the widget in the new parent using the old geometry manager information (as it does for the children, by setting the level to 1 at the start of the copy_widget recursive function) --- tests/test_utilities.py | 85 ++++++++++++++++++++++++++++++++++++----- ttkwidgets/utilities.py | 11 ++++-- 2 files changed, 83 insertions(+), 13 deletions(-) diff --git a/tests/test_utilities.py b/tests/test_utilities.py index e4f845ad..f064063f 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -7,6 +7,11 @@ class TestUtilities(BaseWidgetTest): + def assertGeometryInfoEquals(self, info1, info2): + info1.pop("in", None) + info2.pop("in", None) + self.assertEquals(info1, info2) + def setUp(self): BaseWidgetTest.setUp(self) self._dummy_flag = False @@ -33,30 +38,36 @@ def test_move_widget(self): def test_move_widget_pack(self): label = ttk.Label(self.window) - label.pack() + label.pack(side=tk.LEFT) + info = label.pack_info() tl = tk.Toplevel(self.window) - label = move_widget(label, tl) - label.pack() + label = move_widget(label, tl, preserve_geometry=True) self.assertIsChild(label, tl) self.assertIn(label, tl.pack_slaves()) + self.assertNotIn(label, self.window.pack_slaves()) + self.assertGeometryInfoEquals(info, label.pack_info()) def test_move_widget_grid(self): label = ttk.Label(self.window) - label.grid() + label.grid(row=1, column=1) + info = label.grid_info() tl = tk.Toplevel(self.window) - label = move_widget(label, tl) - label.grid() + label = move_widget(label, tl, preserve_geometry=True) self.assertIsChild(label, tl) - self.assertIn(label, tl.grid_slaves()) + self.assertIn(label, tl.grid_slaves(row=1, column=1)) + self.assertNotIn(label, self.window.grid_slaves(row=1, column=1)) + self.assertGeometryInfoEquals(info, label.grid_info()) def test_move_widget_place(self): label = ttk.Label(self.window) - label.place() + label.place(x=0, y=10) + info = label.place_info() tl = tk.Toplevel(self.window) - label = move_widget(label, tl) - label.place() + label = move_widget(label, tl, preserve_geometry=True) self.assertIsChild(label, tl) self.assertIn(label, tl.place_slaves()) + self.assertNotIn(label, self.window.place_slaves()) + self.assertGeometryInfoEquals(info, label.place_info()) def test_move_widget_with_binding(self): label = ttk.Label(self.window) @@ -119,6 +130,60 @@ def test_move_widget_with_binding_on_parent(self): self.window.event_generate("") self.assertHasBeenInvoked() + def test_move_widget_with_children_pack(self): + frame = ttk.Frame(self.window) + label = ttk.Label(frame) + parent = tk.Toplevel() + label.pack(side=tk.BOTTOM) + info = label.pack_info() + frame.pack(expand=True) + + frame = move_widget(frame, parent) + self.assertTrue(len(frame.pack_slaves()) == 1) + label2 = frame.nametowidget(frame.pack_slaves()[0]) + self.assertTrue(label is not label2) + + self.assertGeometryInfoEquals(info, label2.pack_info()) + self.assertRaises(tk.TclError, label.pack_info) + self.assertRaises(tk.TclError, frame.pack_info) # Frame is not packed + self.assertIn(label2, frame.pack_slaves()) + + def test_move_widget_with_children_grid(self): + frame = ttk.Frame(self.window) + label = ttk.Label(frame) + parent = tk.Toplevel() + label.grid(row=1, column=1) + info = label.grid_info() + frame.grid(row=1, column=1) + + frame = move_widget(frame, parent) + self.assertTrue(len(frame.grid_slaves()) == 1) + label2 = frame.nametowidget(frame.grid_slaves()[0]) + self.assertTrue(label is not label2) + + self.assertGeometryInfoEquals(info, label2.grid_info()) + self.assertRaises(tk.TclError, label.grid_info) + self.assertTrue(len(frame.grid_info()) == 0) # Frame is not in grid + self.assertIn(label2, frame.grid_slaves()) + + def test_move_widget_with_children_place(self): + frame = ttk.Frame(self.window) + label = ttk.Label(frame) + parent = tk.Toplevel() + label.place(x=53, y=13) + info = label.place_info() + frame.place(x=100, y=10) + + frame = move_widget(frame, parent) + self.assertTrue(len(frame.place_slaves()) == 1) + label2 = frame.nametowidget(frame.place_slaves()[0]) + self.assertTrue(label is not label2) + + self.assertGeometryInfoEquals(info, label2.place_info()) + self.assertRaises(tk.TclError, label.place_info) + self.assertTrue(len(frame.place_info()) == 0) + self.assertIn(label2, frame.place_slaves()) + def test_parse_geometry(self): g = parse_geometry('1x1+1+1') self.assertEqual(g, (1, 1, 1, 1)) diff --git a/ttkwidgets/utilities.py b/ttkwidgets/utilities.py index 036cf83d..5ab2183a 100644 --- a/ttkwidgets/utilities.py +++ b/ttkwidgets/utilities.py @@ -60,7 +60,6 @@ def copy_widget(widget, new_parent, level=0): widget._tclCommands = None # Preserve bound functions script = widget.bind(b) rv.bind(b, script) - if level > 0: if widget.grid_info(): # if geometry manager is grid temp = widget.grid_info() @@ -75,6 +74,7 @@ def copy_widget(widget, new_parent, level=0): del temp['in'] rv.pack(**temp) level += 1 + if widget.pack_slaves(): # subwidgets are using the pack() geometry manager for child in widget.pack_slaves(): copy_widget(child, rv, level) @@ -84,16 +84,21 @@ def copy_widget(widget, new_parent, level=0): return rv -def move_widget(widget, new_parent): +def move_widget(widget, new_parent, preserve_geometry=False): """ Moves widget to new_parent :param widget: widget to move + :type widget: tk.Widget :param new_parent: new parent for the widget + :type new_parent: tk.Widget + :param preserve_geometry: Whether to preserve the geometry of the + widget in the old parent into the new parent + :type preserve_geometry: bool :return: moved widget reference """ - rv = copy_widget(widget, new_parent) + rv = copy_widget(widget, new_parent, level=preserve_geometry) widget.destroy() return rv From 1a77fb7f07fa29728dd30e5a60aa1a1d9463734d Mon Sep 17 00:00:00 2001 From: RedFantom Date: Mon, 16 Dec 2019 10:50:34 +0100 Subject: [PATCH 18/31] Modify copy_widget to allow non-visible widgets --- tests/test_utilities.py | 11 +++++++++++ ttkwidgets/utilities.py | 12 +++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/test_utilities.py b/tests/test_utilities.py index f064063f..b3a573fe 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -69,6 +69,17 @@ def test_move_widget_place(self): self.assertNotIn(label, self.window.place_slaves()) self.assertGeometryInfoEquals(info, label.place_info()) + def test_move_widget_none(self): + label = ttk.Label(self.window) + self.assertFalse(label.place_info() is True) + self.assertFalse(label.grid_info() is True) + self.assertRaises(tk.TclError, label.pack_info) + tl = tk.Toplevel(self.window) + label = move_widget(label, tl, preserve_geometry=True) + self.assertFalse(label.place_info() is True) + self.assertFalse(label.grid_info() is True) + self.assertRaises(tk.TclError, label.pack_info) + def test_move_widget_with_binding(self): label = ttk.Label(self.window) label.bind('', self._dummy_bind) diff --git a/ttkwidgets/utilities.py b/ttkwidgets/utilities.py index 5ab2183a..dc517a1f 100644 --- a/ttkwidgets/utilities.py +++ b/ttkwidgets/utilities.py @@ -8,6 +8,10 @@ import os from PIL import Image, ImageTk import re +try: + import Tkinter as tk +except ImportError: + import tkinter as tk def get_assets_directory(): @@ -61,6 +65,10 @@ def copy_widget(widget, new_parent, level=0): script = widget.bind(b) rv.bind(b, script) if level > 0: + try: + pack_info = widget.pack_info() + except tk.TclError: + pack_info = {} if widget.grid_info(): # if geometry manager is grid temp = widget.grid_info() del temp['in'] @@ -69,10 +77,12 @@ def copy_widget(widget, new_parent, level=0): temp = widget.place_info() del temp['in'] rv.place(**temp) - else: # if geometry manager is pack + elif pack_info: # if geometry manager is pack temp = widget.pack_info() del temp['in'] rv.pack(**temp) + else: # No geometry manager configured + pass level += 1 if widget.pack_slaves(): # subwidgets are using the pack() geometry manager From d9b50bb60bcec432b38e49e02081c80c5bde5221 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Mon, 16 Dec 2019 10:57:27 +0100 Subject: [PATCH 19/31] Add test and error raise for moving widget to new Tk instance --- tests/test_utilities.py | 7 ++++++- ttkwidgets/utilities.py | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_utilities.py b/tests/test_utilities.py index b3a573fe..57980f1d 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -195,6 +195,11 @@ def test_move_widget_with_children_place(self): self.assertTrue(len(frame.place_info()) == 0) self.assertIn(label2, frame.place_slaves()) + def test_move_widget_to_new_tk(self): + label = tk.Label(self.window) + window = tk.Tk() + self.assertRaises(RuntimeError, move_widget, label, window) + def test_parse_geometry(self): g = parse_geometry('1x1+1+1') self.assertEqual(g, (1, 1, 1, 1)) @@ -210,4 +215,4 @@ def test_coordinates_in_box(self): self.assertTrue(coords_in_box((1, 1), (0, 0, 2, 2))) self.assertFalse(coords_in_box((1, 1), (1, 1, 2, 2), include_edges=False)) - self.assertTrue(coords_in_box((0, 0), (-1, -1, 1, 1), bbox_is_x1y1x2y2=True)) + self.assertTrue(coords_in_box((0, 0), (-1, -1, 1, 1), bbox_is_x1y1x02y2=True)) diff --git a/ttkwidgets/utilities.py b/ttkwidgets/utilities.py index dc517a1f..078675f0 100644 --- a/ttkwidgets/utilities.py +++ b/ttkwidgets/utilities.py @@ -59,6 +59,8 @@ def copy_widget(widget, new_parent, level=0): :return: tkinter.Widget instance, the copied widget. """ + if widget.tk is not new_parent.tk: + raise RuntimeError("Widget may not be copied into new Tk instance") rv = widget.__class__(master=new_parent, **get_widget_options(widget)) for b in widget.bind(): widget._tclCommands = None # Preserve bound functions From 695c8830c81f8b6ad398f104ceebca835c35f153 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Wed, 18 Dec 2019 12:37:06 +0100 Subject: [PATCH 20/31] Fix last issues with move_widget function --- tests/test_utilities.py | 4 ++-- ttkwidgets/utilities.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 57980f1d..229cd584 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -99,7 +99,7 @@ def test_move_widget_with_command(self): parent = tk.Toplevel() child = move_widget(widget, parent) self.assertIsChild(child, parent) - widget.invoke() + child.invoke() self.assertHasBeenInvoked() def test_move_widget_with_bound_method_on_parent(self): @@ -215,4 +215,4 @@ def test_coordinates_in_box(self): self.assertTrue(coords_in_box((1, 1), (0, 0, 2, 2))) self.assertFalse(coords_in_box((1, 1), (1, 1, 2, 2), include_edges=False)) - self.assertTrue(coords_in_box((0, 0), (-1, -1, 1, 1), bbox_is_x1y1x02y2=True)) + self.assertTrue(coords_in_box((0, 0), (-1, -1, 1, 1), bbox_is_x1y1x2y2=True)) diff --git a/ttkwidgets/utilities.py b/ttkwidgets/utilities.py index 078675f0..184430a4 100644 --- a/ttkwidgets/utilities.py +++ b/ttkwidgets/utilities.py @@ -61,11 +61,13 @@ def copy_widget(widget, new_parent, level=0): """ if widget.tk is not new_parent.tk: raise RuntimeError("Widget may not be copied into new Tk instance") + widget._tclCommands = None # Preserve bound functions rv = widget.__class__(master=new_parent, **get_widget_options(widget)) + for b in widget.bind(): - widget._tclCommands = None # Preserve bound functions script = widget.bind(b) rv.bind(b, script) + if level > 0: try: pack_info = widget.pack_info() @@ -93,6 +95,8 @@ def copy_widget(widget, new_parent, level=0): else: for child in widget.winfo_children(): copy_widget(child, rv, level) + + widget.destroy() return rv From 45178bce9fb35e8956d44f7f22bbd9bdf601df07 Mon Sep 17 00:00:00 2001 From: Dogeek Date: Sun, 22 Dec 2019 20:08:18 +0100 Subject: [PATCH 21/31] Create tests for Notebook --- tests/test_notebook.py | 78 ++++++++++++++++++++++++++++++++++++++++++ ttkwidgets/notebook.py | 23 +++++++------ 2 files changed, 90 insertions(+), 11 deletions(-) diff --git a/tests/test_notebook.py b/tests/test_notebook.py index e030bc5d..31ec138b 100644 --- a/tests/test_notebook.py +++ b/tests/test_notebook.py @@ -2,6 +2,8 @@ # For license see LICENSE from ttkwidgets import Notebook from tests import BaseWidgetTest +from tkinter import ttk +import tkinter as tk class TestNotebook(BaseWidgetTest): @@ -9,3 +11,79 @@ def test_notebook_init(self): nb = Notebook(self.window) nb.grid() self.window.update() + + def test_notebook_add_tab(self): + nb = Notebook(self.window) + frame = ttk.Frame(self.window, width=200, height=200) + nb.add(frame, text="Frame") + nb.grid() + self.window.update() + + def test_notebook_select_tab(self): + nb = Notebook(self.window) + frame = ttk.Frame(self.window, width=200, height=200) + frame2 = ttk.Frame(self.window, width=200, height=200) + nb.add(frame, text="Frame") + nb.add(frame2, text="Frame2") + nb.grid() + nb.select_next() + nb.select_next() + nb.select_prev() + self.window.update() + + def test_notebook_move_tab(self): + nb = Notebook(self.window, drag_to_toplevel=False) + for i in range(3): + frame = ttk.Frame(self.window, width=200, height=200) + nb.add(frame, text="Frame" + str(i)) + nb._dragged_tab = nb.tabs()[0] + nb.swap(nb.tabs()[1]) + nb._on_click(None) + self.assertEqual(nb._visible_tabs, [1, 0, 2]) + + def test_notebook_insert(self): + nb = Notebook(self.window, drag_to_toplevel=False) + for i in range(3): + frame = ttk.Frame(self.window, width=200, height=200) + nb.add(frame, text="Frame" + str(i)) + nb.insert('.!toplevel.!frame2', + ttk.Frame(self.window, width=200, height=200), + text="Added") + + self.assertEqual(nb._visible_tabs, [3, 0, 1, 2]) # not sure + + def test_notebook_index(self): + nb = Notebook(self.window) + for i in range(10): + frame = ttk.Frame(self.window, width=200, height=200) + nb.add(frame, text="Frame" + str(i)) + + with self.assertRaises(ValueError): + nb.index('.!toplevel.!frame11') + + self.assertEqual(nb.index('.!toplevel.!frame'), 0) + self.assertEqual(nb.index(tk.END), 9) + nb.current_tab = 0 + self.assertEqual(nb.index(tk.CURRENT), 0) + + self.window.update() + + def test_notebook_forget_tab(self): + nb = Notebook(self.window) + for i in range(3): + frame = ttk.Frame(self.window, width=200, height=200) + nb.add(frame, text="Frame" + str(i)) + + nb.forget('.!toplevel.!frame') + + def test_notebook_config_tab(self): + nb = Notebook(self.window) + for i in range(10): + frame = ttk.Frame(self.window, width=200, height=200) + nb.add(frame, text="Frame" + str(i)) + + with self.assertRaises(ValueError): + nb.tab(tk.CURRENT, state='random') + + nb.tab(tk.CURRENT, text="Changed") + self.window.update() diff --git a/ttkwidgets/notebook.py b/ttkwidgets/notebook.py index 710d0734..8665e4dc 100644 --- a/ttkwidgets/notebook.py +++ b/ttkwidgets/notebook.py @@ -52,7 +52,7 @@ def __init__(self, master=None, tab_nb=0, **kwargs): self.bind('', self._on_enter_tab) self.bind('', self._on_leave_tab) self.bind('', self._on_mousewheel) - + def _on_mousewheel(self, event): if self.hovering_tab: if event.delta > 0: @@ -62,7 +62,7 @@ def _on_mousewheel(self, event): def _on_enter_tab(self, event): self.hovering_tab = True - + def _on_leave_tab(self, event): self.hovering_tab = False @@ -171,7 +171,7 @@ def __init__(self, master=None, **kwargs): tabdrag: boolean (default True) whether to enable dragging of tab labels - + drag_to_toplevel : boolean (default tabdrag) whether to enable dragging tabs to Toplevel windows @@ -312,16 +312,16 @@ def __getitem__(self, key): def __setitem__(self, key, value): self.configure(**{key: value}) - + def setup_style(self, bg="#dddddd", activebg="#efefef", pressedbg="#c1c1c1", fg="black", fieldbg="white", lightcolor="#ededed", darkcolor="##cfcdc8", bordercolor="#888888", focusbordercolor="#5e5e5e", selectbg="#c1c1c1", selectfg="black", unselectfg="#999999", disabledfg='#999999', disabledbg="#dddddd"): """ Setups the style for the notebook. - :param bg: - :param activebg: - :param pressedbg: + :param bg: + :param activebg: + :param pressedbg: :param fg: :param fieldbg: :param lightcolor: @@ -365,10 +365,10 @@ def setup_style(self, bg="#dddddd", activebg="#efefef", pressedbg="#c1c1c1", 5kEJADs= ''', master=self) ) - + for seq in self.bind_class('TButton'): self.bind_class('Notebook.Tab.Close', seq, self.bind_class('TButton', seq), True) - + style_config = {'bordercolor': theme['bordercolor'], 'background': theme['bg'], 'foreground': theme['fg'], @@ -377,7 +377,7 @@ def setup_style(self, bg="#dddddd", activebg="#efefef", pressedbg="#c1c1c1", 'lightcolor': theme['lightcolor'], 'darkcolor': theme['darkcolor'], 'troughcolor': theme['pressedbg']} - + style = ttk.Style(self) style.element_create('close', 'image', "img_close", ("active", "pressed", "!disabled", "img_closepressed"), @@ -539,6 +539,7 @@ def _on_click(self, event): if not end_pos_in_widget: self.move_to_toplevel(self._dragged_tab) self._dragged_tab = None + print(self._visible_tabs) def _menu_insert(self, tab, text): menu = [] @@ -639,7 +640,7 @@ def cget(self, key): def configure(self, cnf=None, **kw): """ Configures this Notebook widget. - + :param closebutton: If a close button should show on the tabs :type closebutton: bool :param closecommand: A callable to call when the tab is closed, takes one argument, the tab_id From 06d8abfbba0f95ad8e5208ba344e9ebb04f8c8f8 Mon Sep 17 00:00:00 2001 From: Dogeek Date: Sun, 22 Dec 2019 20:23:05 +0100 Subject: [PATCH 22/31] Partially fix Notebook tests --- tests/test_notebook.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/test_notebook.py b/tests/test_notebook.py index 31ec138b..435a18be 100644 --- a/tests/test_notebook.py +++ b/tests/test_notebook.py @@ -33,11 +33,13 @@ def test_notebook_select_tab(self): def test_notebook_move_tab(self): nb = Notebook(self.window, drag_to_toplevel=False) + frames = [] for i in range(3): frame = ttk.Frame(self.window, width=200, height=200) + frames.append(frame) nb.add(frame, text="Frame" + str(i)) - nb._dragged_tab = nb.tabs()[0] - nb.swap(nb.tabs()[1]) + nb._dragged_tab = nb._tab_labels[0] + nb._swap(nb._tab_labels[1]) nb._on_click(None) self.assertEqual(nb._visible_tabs, [1, 0, 2]) @@ -46,11 +48,11 @@ def test_notebook_insert(self): for i in range(3): frame = ttk.Frame(self.window, width=200, height=200) nb.add(frame, text="Frame" + str(i)) - nb.insert('.!toplevel.!frame2', + nb.insert(str(self.window) + '.!frame2', ttk.Frame(self.window, width=200, height=200), text="Added") - self.assertEqual(nb._visible_tabs, [3, 0, 1, 2]) # not sure + self.assertEqual(nb._visible_tabs, [0, 3, 1, 2]) def test_notebook_index(self): nb = Notebook(self.window) @@ -59,10 +61,10 @@ def test_notebook_index(self): nb.add(frame, text="Frame" + str(i)) with self.assertRaises(ValueError): - nb.index('.!toplevel.!frame11') + nb.index(str(self.window) + '.!frame11') - self.assertEqual(nb.index('.!toplevel.!frame'), 0) - self.assertEqual(nb.index(tk.END), 9) + self.assertEqual(nb.index(str(self.window) + '.!frame'), 0) + self.assertEqual(nb.index(tk.END), 10) nb.current_tab = 0 self.assertEqual(nb.index(tk.CURRENT), 0) @@ -74,7 +76,7 @@ def test_notebook_forget_tab(self): frame = ttk.Frame(self.window, width=200, height=200) nb.add(frame, text="Frame" + str(i)) - nb.forget('.!toplevel.!frame') + nb.forget(str(self.window) + '.!frame') def test_notebook_config_tab(self): nb = Notebook(self.window) From adf0ad1a9405bf24734268910f4b1a6dc02535e9 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Thu, 9 Jan 2020 10:58:27 +0100 Subject: [PATCH 23/31] Fix test_notebook_index and test_notebook_forget_tab --- tests/test_notebook.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/test_notebook.py b/tests/test_notebook.py index 435a18be..1e957756 100644 --- a/tests/test_notebook.py +++ b/tests/test_notebook.py @@ -56,15 +56,17 @@ def test_notebook_insert(self): def test_notebook_index(self): nb = Notebook(self.window) - for i in range(10): + ids = list() + n = 10 + for i in range(n): frame = ttk.Frame(self.window, width=200, height=200) - nb.add(frame, text="Frame" + str(i)) + ids.append(nb.add(frame, text="Frame" + str(i))) with self.assertRaises(ValueError): nb.index(str(self.window) + '.!frame11') - self.assertEqual(nb.index(str(self.window) + '.!frame'), 0) - self.assertEqual(nb.index(tk.END), 10) + self.assertTrue(all(ids.index(id) == nb.index(id) for id in ids)) + self.assertEqual(nb.index(tk.END), n) nb.current_tab = 0 self.assertEqual(nb.index(tk.CURRENT), 0) @@ -72,11 +74,19 @@ def test_notebook_index(self): def test_notebook_forget_tab(self): nb = Notebook(self.window) - for i in range(3): + ids = list() + n = 3 + for i in range(n): frame = ttk.Frame(self.window, width=200, height=200) - nb.add(frame, text="Frame" + str(i)) - - nb.forget(str(self.window) + '.!frame') + id = nb.add(frame, text="Frame" + str(i)) + ids.append(id) + + tabs = nb.tabs() + self.assertIn(id, tabs) + nb.forget(id) # Test forgetting of the last created tab + tabs = nb.tabs() + self.assertEquals(len(tabs), n-1) + self.assertNotIn(id, tabs) def test_notebook_config_tab(self): nb = Notebook(self.window) From 43393bca70c6a8af0ef8d7ecb37d087888c58910 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Thu, 9 Jan 2020 11:03:37 +0100 Subject: [PATCH 24/31] Add missin return to Notebook.insert Both the insert and add functions should return a valid TAB_ID --- ttkwidgets/notebook.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ttkwidgets/notebook.py b/ttkwidgets/notebook.py index 8665e4dc..970d943b 100644 --- a/ttkwidgets/notebook.py +++ b/ttkwidgets/notebook.py @@ -751,6 +751,7 @@ def insert(self, where, widget, **kwargs): self._tab_labels[ind].grid_configure(column=i) self.update_idletasks() self._on_configure() + return index def enable_traversal(self): self.bind('', lambda e: self.select_next(True)) From 7a3aa528d5eb053f1c1faa01dc06e47618ba4b02 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Thu, 9 Jan 2020 11:06:49 +0100 Subject: [PATCH 25/31] Extend test_notebook_index with getting widget indices --- tests/test_notebook.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_notebook.py b/tests/test_notebook.py index 1e957756..77eb73f5 100644 --- a/tests/test_notebook.py +++ b/tests/test_notebook.py @@ -57,15 +57,19 @@ def test_notebook_insert(self): def test_notebook_index(self): nb = Notebook(self.window) ids = list() + frames = list() n = 10 for i in range(n): frame = ttk.Frame(self.window, width=200, height=200) + frames.append(frame) ids.append(nb.add(frame, text="Frame" + str(i))) with self.assertRaises(ValueError): nb.index(str(self.window) + '.!frame11') self.assertTrue(all(ids.index(id) == nb.index(id) for id in ids)) + self.assertTrue(all(nb.index(id) == nb.index(frame) for id, frame in zip(ids, frames))) + self.assertEqual(nb.index(tk.END), n) nb.current_tab = 0 self.assertEqual(nb.index(tk.CURRENT), 0) From cb79ec3563646cb7e2a78cde814cb65a0bdfb497 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Thu, 9 Jan 2020 11:23:53 +0100 Subject: [PATCH 26/31] Change error raised in Notebook.index --- tests/test_notebook.py | 4 ++-- ttkwidgets/notebook.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_notebook.py b/tests/test_notebook.py index 77eb73f5..a9d3ad4c 100644 --- a/tests/test_notebook.py +++ b/tests/test_notebook.py @@ -64,12 +64,12 @@ def test_notebook_index(self): frames.append(frame) ids.append(nb.add(frame, text="Frame" + str(i))) - with self.assertRaises(ValueError): + with self.assertRaises(KeyError): nb.index(str(self.window) + '.!frame11') self.assertTrue(all(ids.index(id) == nb.index(id) for id in ids)) self.assertTrue(all(nb.index(id) == nb.index(frame) for id, frame in zip(ids, frames))) - + self.assertEqual(nb.index(tk.END), n) nb.current_tab = 0 self.assertEqual(nb.index(tk.CURRENT), 0) diff --git a/ttkwidgets/notebook.py b/ttkwidgets/notebook.py index 970d943b..f69a86c7 100644 --- a/ttkwidgets/notebook.py +++ b/ttkwidgets/notebook.py @@ -767,9 +767,10 @@ def index(self, tab_id): return tab_id else: try: - return self._indexes[str(tab_id)] - except KeyError: - raise ValueError('No such tab in the Notebook: %s' % tab_id) + return self._visible_tabs[self._indexes[str(tab_id)]] + except KeyError as e: + e.message = "No such tab in the Notebook: {}".format(tab_id) + raise def select_next(self, rotate=False): """Go to next tab.""" From 91f21825955e626a14612043610b3d32af2d109f Mon Sep 17 00:00:00 2001 From: RedFantom Date: Thu, 9 Jan 2020 11:26:31 +0100 Subject: [PATCH 27/31] Change test_notebook_insert to test for ttk.Notebook.insert compliance --- tests/test_notebook.py | 16 ++++++++++------ ttkwidgets/notebook.py | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/test_notebook.py b/tests/test_notebook.py index a9d3ad4c..379e7e04 100644 --- a/tests/test_notebook.py +++ b/tests/test_notebook.py @@ -45,13 +45,17 @@ def test_notebook_move_tab(self): def test_notebook_insert(self): nb = Notebook(self.window, drag_to_toplevel=False) - for i in range(3): + ids = list() + n = 3 + for i in range(n): frame = ttk.Frame(self.window, width=200, height=200) - nb.add(frame, text="Frame" + str(i)) - nb.insert(str(self.window) + '.!frame2', - ttk.Frame(self.window, width=200, height=200), - text="Added") - + ids.append(nb.add(frame, text="Frame" + str(i))) + print(ids) + id = nb.insert(n-2, ttk.Frame(self.window, width=200, height=200), text="Added") + tabs = nb.tabs() + self.assertIn(id, tabs) + self.assertEquals(tabs.index(id), n-2) + self.assertEquals(nb.index(id), n-2) self.assertEqual(nb._visible_tabs, [0, 3, 1, 2]) def test_notebook_index(self): diff --git a/ttkwidgets/notebook.py b/ttkwidgets/notebook.py index f69a86c7..073490d7 100644 --- a/ttkwidgets/notebook.py +++ b/ttkwidgets/notebook.py @@ -765,7 +765,7 @@ def index(self, tab_id): return self.current_tab elif tab_id in self._tabs: return tab_id - else: + else: # tab_id is str or tk.Widget try: return self._visible_tabs[self._indexes[str(tab_id)]] except KeyError as e: From 9e5c6c3a17ed0649415ca8b441ed3f5e006b77d9 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Thu, 9 Jan 2020 11:40:19 +0100 Subject: [PATCH 28/31] Create sphinx documentation Notebook entry --- docs/source/ttkwidgets/ttkwidgets.rst | 2 +- .../ttkwidgets/ttkwidgets/ttkwidgets.Notebook.rst | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 docs/source/ttkwidgets/ttkwidgets/ttkwidgets.Notebook.rst diff --git a/docs/source/ttkwidgets/ttkwidgets.rst b/docs/source/ttkwidgets/ttkwidgets.rst index eb2f17f4..26604c7a 100644 --- a/docs/source/ttkwidgets/ttkwidgets.rst +++ b/docs/source/ttkwidgets/ttkwidgets.rst @@ -17,9 +17,9 @@ ttkwidgets DebugWindow ItemsCanvas LinkLabel + Notebook ScaleEntry ScrolledListbox Table TickScale TimeLine - diff --git a/docs/source/ttkwidgets/ttkwidgets/ttkwidgets.Notebook.rst b/docs/source/ttkwidgets/ttkwidgets/ttkwidgets.Notebook.rst new file mode 100644 index 00000000..631c80b4 --- /dev/null +++ b/docs/source/ttkwidgets/ttkwidgets/ttkwidgets.Notebook.rst @@ -0,0 +1,10 @@ +Notebook +======== + +.. currentmodule:: ttkwidgets + +.. autoclass:: Notebook + :show-inheritance: + :members: + + .. automethod:: __init__ From e56757f9fb263177dd5dfadbba848eeb0d5a7fd8 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Fri, 24 Apr 2020 12:09:36 +0200 Subject: [PATCH 29/31] Update notebook.py to be consistent with quotes Personally, I prefer double quotes everywhere and as the file contained a mix of double and single quotes, I replaced all single quotes with double ones. --- ttkwidgets/notebook.py | 481 +++++++++++++++++++++-------------------- 1 file changed, 241 insertions(+), 240 deletions(-) diff --git a/ttkwidgets/notebook.py b/ttkwidgets/notebook.py index 073490d7..d5bec37d 100644 --- a/ttkwidgets/notebook.py +++ b/ttkwidgets/notebook.py @@ -22,36 +22,36 @@ def __init__(self, master=None, tab_nb=0, **kwargs): :param tab_nb: tab index :param **kwargs: keyword arguments for ttk::Label widgets """ - ttk.Frame.__init__(self, master, class_='Notebook.Tab', - style='Notebook.Tab', padding=1) - self._state = kwargs.pop('state', 'normal') + ttk.Frame.__init__(self, master, class_="Notebook.Tab", + style="Notebook.Tab", padding=1) + self._state = kwargs.pop("state", "normal") self.tab_nb = tab_nb self.hovering_tab = False - self._closebutton = kwargs.pop('closebutton', True) - self._closecommand = kwargs.pop('closecommand', None) - self.frame = ttk.Frame(self, style='Notebook.Tab.Frame') - self.label = ttk.Label(self.frame, style='Notebook.Tab.Label', anchor='center', takefocus=False, **kwargs) + self._closebutton = kwargs.pop("closebutton", True) + self._closecommand = kwargs.pop("closecommand", None) + self.frame = ttk.Frame(self, style="Notebook.Tab.Frame") + self.label = ttk.Label(self.frame, style="Notebook.Tab.Label", anchor="center", takefocus=False, **kwargs) self.closebtn = ttk.Button( - self.frame, style='Notebook.Tab.Close', command=self.closecommand, - class_='Notebook.Tab.Close', takefocus=False) - self.label.pack(side='left', padx=(6, 0)) + self.frame, style="Notebook.Tab.Close", command=self.closecommand, + class_="Notebook.Tab.Close", takefocus=False) + self.label.pack(side="left", padx=(6, 0)) if self._closebutton: - self.closebtn.pack(side='right', padx=(0, 6)) + self.closebtn.pack(side="right", padx=(0, 6)) self.update_idletasks() self.configure(width=self.frame.winfo_reqwidth() + 6, height=self.frame.winfo_reqheight() + 6) - self.frame.place(bordermode='inside', anchor='nw', x=0, y=0, + self.frame.place(bordermode="inside", anchor="nw", x=0, y=0, relwidth=1, relheight=1) - self.label.bind('', self._resize) - if self._state == 'disabled': - self.state(['disabled']) - elif self._state != 'normal': + self.label.bind("", self._resize) + if self._state == "disabled": + self.state(["disabled"]) + elif self._state != "normal": raise ValueError("state option should be 'normal' or 'disabled'") - self.bind('', self._b2_press) - self.bind('', self._on_enter_tab) - self.bind('', self._on_leave_tab) - self.bind('', self._on_mousewheel) + self.bind("", self._b2_press) + self.bind("", self._on_enter_tab) + self.bind("", self._on_leave_tab) + self.bind("", self._on_mousewheel) def _on_mousewheel(self, event): if self.hovering_tab: @@ -85,7 +85,7 @@ def state(self, *args): self.label.state(*args) self.frame.state(*args) self.closebtn.state(*args) - if args and 'selected' in self.state(): + if args and "selected" in self.state(): self.configure(width=self.frame.winfo_reqwidth() + 6, height=self.frame.winfo_reqheight() + 6) self.frame.place_configure(relheight=1.1) @@ -103,23 +103,23 @@ def unbind(self, sequence, funcids=(None, None)): self.frame.unbind(sequence, funcids[0]) def tab_configure(self, **kwargs): - if 'closecommand' in kwargs: - self._closecommand = kwargs.pop('closecommand') - if 'closebutton' in kwargs: - self._closebutton = kwargs.pop('closebutton') + if "closecommand" in kwargs: + self._closecommand = kwargs.pop("closecommand") + if "closebutton" in kwargs: + self._closebutton = kwargs.pop("closebutton") if self._closebutton: - self.closebtn.pack(side='right', padx=(0, 6)) + self.closebtn.pack(side="right", padx=(0, 6)) else: self.closebtn.pack_forget() self.update_idletasks() self.configure(width=self.frame.winfo_reqwidth() + 6, height=self.frame.winfo_reqheight() + 6) - if 'state' in kwargs: - state = kwargs.pop('state') - if state == 'normal': - self.state(['!disabled']) - elif state == 'disabled': - self.state(['disabled']) + if "state" in kwargs: + state = kwargs.pop("state") + if state == "normal": + self.state(["!disabled"]) + elif state == "disabled": + self.state(["disabled"]) else: raise ValueError("state option should be 'normal' or 'disabled'") self._state = state @@ -128,11 +128,11 @@ def tab_configure(self, **kwargs): self.label.configure(**kwargs) def tab_cget(self, option): - if option == 'closecommand': + if option == "closecommand": return self._closecommand - elif option == 'closebutton': + elif option == "closebutton": return self._closebutton - elif option == 'state': + elif option == "state": return self._state else: return self.label.cget(option) @@ -197,13 +197,13 @@ def __init__(self, master=None, **kwargs): """ self._init_kwargs = kwargs.copy() - self._closebutton = bool(kwargs.pop('closebutton', True)) - self._closecommand = kwargs.pop('closecommand', self.forget) - self._tabdrag = bool(kwargs.pop('tabdrag', True)) - self._drag_to_toplevel = bool(kwargs.pop('drag_to_toplevel', self._tabdrag)) - self._tabmenu = bool(kwargs.pop('tabmenu', True)) + self._closebutton = bool(kwargs.pop("closebutton", True)) + self._closecommand = kwargs.pop("closecommand", self.forget) + self._tabdrag = bool(kwargs.pop("tabdrag", True)) + self._drag_to_toplevel = bool(kwargs.pop("drag_to_toplevel", self._tabdrag)) + self._tabmenu = bool(kwargs.pop("tabmenu", True)) - ttk.Frame.__init__(self, master, class_='Notebook', padding=(0, 0, 0, 1), **kwargs) + ttk.Frame.__init__(self, master, class_="Notebook", padding=(0, 0, 0, 1), **kwargs) if not Notebook._initialized: self.setup_style() @@ -227,12 +227,12 @@ def __init__(self, master=None, **kwargs): self._toplevels = [] style = ttk.Style(self) - bg = style.lookup('TFrame', 'background') + bg = style.lookup("TFrame", "background") # --- widgets # to display current tab content - self._body = ttk.Frame(self, padding=1, style='Notebook', - relief='flat') + self._body = ttk.Frame(self, padding=1, style="Notebook", + relief="flat") self._body.rowconfigure(0, weight=1) self._body.columnconfigure(0, weight=1) self._body.grid_propagate(False) @@ -240,69 +240,70 @@ def __init__(self, master=None, **kwargs): # canvas to scroll through tab labels self._canvas = tk.Canvas(self, bg=bg, highlightthickness=0, borderwidth=0, takefocus=False) - self._tab_frame2 = ttk.Frame(self, height=26, style='Notebook', - relief='flat') + self._tab_frame2 = ttk.Frame(self, height=26, style="Notebook", + relief="flat") # self._tab_frame2 is a trick to be able to drag a tab on the full # canvas width even if self._tab_frame is smaller. - self._tab_frame = ttk.Frame(self._tab_frame2, style='Notebook', - relief='flat', height=26) # to display tab labels - self._sep = ttk.Separator(self._tab_frame2, orient='horizontal') - self._sep.place(bordermode='outside', anchor='sw', x=0, rely=1, + self._tab_frame = ttk.Frame(self._tab_frame2, style="Notebook", + relief="flat", height=26) # to display tab labels + self._sep = ttk.Separator(self._tab_frame2, orient="horizontal") + self._sep.place(bordermode="outside", anchor="sw", x=0, rely=1, relwidth=1, height=1) - self._tab_frame.pack(side='left') + self._tab_frame.pack(side="left") - self._canvas.create_window(0, 0, anchor='nw', window=self._tab_frame2, - tags='window') + self._canvas.create_window(0, 0, anchor="nw", window=self._tab_frame2, + tags="window") self._canvas.configure(height=self._tab_frame.winfo_reqheight()) # empty frame to show the spot formerly occupied by the tab - self._dummy_frame = ttk.Frame(self._tab_frame, style='Notebook', relief='flat') - self._dummy_sep = ttk.Separator(self._tab_frame, orient='horizontal') + self._dummy_frame = ttk.Frame(self._tab_frame, style="Notebook", relief="flat") + self._dummy_sep = ttk.Separator(self._tab_frame, orient="horizontal") self._dummy_sep.place(in_=self._dummy_frame, x=0, relwidth=1, height=1, - y=0, anchor='sw', bordermode='outside') + y=0, anchor="sw", bordermode="outside") # tab navigation - self._tab_menu = tk.Menu(self, tearoff=False, relief='sunken', - bg=style.lookup('TEntry', 'fieldbackground', - default='white'), - activebackground=style.lookup('TEntry', - 'selectbackground', - ['focus'], 'gray70'), - activeforeground=style.lookup('TEntry', - 'selectforeground', - ['focus'], 'gray70')) + self._tab_menu = tk.Menu(self, tearoff=False, relief="sunken", + bg=style.lookup("TEntry", "fieldbackground", + default="white"), + activebackground=style.lookup("TEntry", + "selectbackground", + ["focus"], "gray70"), + activeforeground=style.lookup("TEntry", + "selectforeground", + ["focus"], "gray70")) self._tab_list = ttk.Menubutton(self, width=1, menu=self._tab_menu, - style='Notebook.TMenubutton', + style="Notebook.TMenubutton", padding=0) - self._tab_list.state(['disabled']) - self._btn_left = ttk.Button(self, style='Left.Notebook.TButton', + self._tab_list.state(["disabled"]) + self._btn_left = ttk.Button(self, style="Left.Notebook.TButton", command=self.select_prev, takefocus=False) - self._btn_right = ttk.Button(self, style='Right.Notebook.TButton', + self._btn_right = ttk.Button(self, style="Right.Notebook.TButton", command=self.select_next, takefocus=False) # --- grid - self._tab_list.grid(row=0, column=0, sticky='ns', pady=(0, 1)) + self._tab_list.grid(row=0, column=0, sticky="ns", pady=(0, 1)) if not self._tabmenu: self._tab_list.grid_remove() - self._btn_left.grid(row=0, column=1, sticky='ns', pady=(0, 1)) - self._canvas.grid(row=0, column=2, sticky='ew') - self._btn_right.grid(row=0, column=3, sticky='ns', pady=(0, 1)) - self._body.grid(row=1, columnspan=4, sticky='ewns', padx=1, pady=1) + self._btn_left.grid(row=0, column=1, sticky="ns", pady=(0, 1)) + self._canvas.grid(row=0, column=2, sticky="ew") + self._btn_right.grid(row=0, column=3, sticky="ns", pady=(0, 1)) + self._body.grid(row=1, columnspan=4, sticky="ewns", padx=1, pady=1) ttk.Frame(self, height=1, - style='separator.TFrame').place(x=1, anchor='nw', + style="separator.TFrame").place(x=1, anchor="nw", rely=1, height=1, relwidth=1) - self._border_left = ttk.Frame(self, width=1, style='separator.TFrame') - self._border_right = ttk.Frame(self, width=1, style='separator.TFrame') - self._border_left.place(bordermode='outside', in_=self._body, x=-1, y=-2, + self._border_left = ttk.Frame(self, width=1, style="separator.TFrame") + self._border_right = ttk.Frame(self, width=1, style="separator.TFrame") + self._border_left.place(bordermode="outside", in_=self._body, x=-1, y=-2, width=1, height=self._body.winfo_reqheight() + 2, relheight=1) - self._border_right.place(bordermode='outside', in_=self._body, relx=1, y=-2, + self._border_right.place(bordermode="outside", in_=self._body, relx=1, y=-2, width=1, height=self._body.winfo_reqheight() + 2, relheight=1) # --- bindings - self._tab_frame.bind('', self._on_configure) - self._canvas.bind('', self._on_configure) - self.bind_all('', self._on_click) + self._tab_frame.bind("", self._on_configure) + self._canvas.bind("", self._on_configure) + self.bind_all("", self._on_click) + self.bind_all() self.config = self.configure Notebook._initialized = True @@ -316,7 +317,7 @@ def __setitem__(self, key, value): def setup_style(self, bg="#dddddd", activebg="#efefef", pressedbg="#c1c1c1", fg="black", fieldbg="white", lightcolor="#ededed", darkcolor="##cfcdc8", bordercolor="#888888", focusbordercolor="#5e5e5e", selectbg="#c1c1c1", - selectfg="black", unselectfg="#999999", disabledfg='#999999', disabledbg="#dddddd"): + selectfg="black", unselectfg="#999999", disabledfg="#999999", disabledbg="#dddddd"): """ Setups the style for the notebook. :param bg: @@ -334,118 +335,118 @@ def setup_style(self, bg="#dddddd", activebg="#efefef", pressedbg="#c1c1c1", :param disabledfg: :param disabledbg: """ - theme = {'bg': bg, - 'activebg': activebg, - 'pressedbg': pressedbg, - 'fg': fg, - 'fieldbg': fieldbg, - 'lightcolor': lightcolor, - 'darkcolor': darkcolor, - 'bordercolor': bordercolor, - 'focusbordercolor': focusbordercolor, - 'selectbg': selectbg, - 'selectfg': selectfg, - 'unselectedfg': unselectfg, - 'disabledfg': disabledfg, - 'disabledbg': disabledbg} + theme = {"bg": bg, + "activebg": activebg, + "pressedbg": pressedbg, + "fg": fg, + "fieldbg": fieldbg, + "lightcolor": lightcolor, + "darkcolor": darkcolor, + "bordercolor": bordercolor, + "focusbordercolor": focusbordercolor, + "selectbg": selectbg, + "selectfg": selectfg, + "unselectedfg": unselectfg, + "disabledfg": disabledfg, + "disabledbg": disabledbg} self.images = ( - tk.PhotoImage("img_close", data=''' + tk.PhotoImage("img_close", data=""" R0lGODlhCAAIAMIBAAAAADs7O4+Pj9nZ2Ts7Ozs7Ozs7Ozs7OyH+EUNyZWF0ZWQg d2l0aCBHSU1QACH5BAEKAAQALAAAAAAIAAgAAAMVGDBEA0qNJyGw7AmxmuaZhWEU 5kEJADs= - ''', master=self), - tk.PhotoImage("img_closeactive", data=''' + """, master=self), + tk.PhotoImage("img_closeactive", data=""" R0lGODlhCAAIAMIEAAAAAP/SAP/bNNnZ2cbGxsbGxsbGxsbGxiH5BAEKAAQALAAA AAAIAAgAAAMVGDBEA0qNJyGw7AmxmuaZhWEU5kEJADs= - ''', master=self), - tk.PhotoImage("img_closepressed", data=''' + """, master=self), + tk.PhotoImage("img_closepressed", data=""" R0lGODlhCAAIAMIEAAAAAOUqKv9mZtnZ2Ts7Ozs7Ozs7Ozs7OyH+EUNyZWF0ZWQg d2l0aCBHSU1QACH5BAEKAAQALAAAAAAIAAgAAAMVGDBEA0qNJyGw7AmxmuaZhWEU 5kEJADs= - ''', master=self) + """, master=self) ) - for seq in self.bind_class('TButton'): - self.bind_class('Notebook.Tab.Close', seq, self.bind_class('TButton', seq), True) + for seq in self.bind_class("TButton"): + self.bind_class("Notebook.Tab.Close", seq, self.bind_class("TButton", seq), True) - style_config = {'bordercolor': theme['bordercolor'], - 'background': theme['bg'], - 'foreground': theme['fg'], - 'arrowcolor': theme['fg'], - 'gripcount': 0, - 'lightcolor': theme['lightcolor'], - 'darkcolor': theme['darkcolor'], - 'troughcolor': theme['pressedbg']} + style_config = {"bordercolor": theme["bordercolor"], + "background": theme["bg"], + "foreground": theme["fg"], + "arrowcolor": theme["fg"], + "gripcount": 0, + "lightcolor": theme["lightcolor"], + "darkcolor": theme["darkcolor"], + "troughcolor": theme["pressedbg"]} style = ttk.Style(self) - style.element_create('close', 'image', "img_close", + style.element_create("close", "image", "img_close", ("active", "pressed", "!disabled", "img_closepressed"), ("active", "!disabled", "img_closeactive"), - sticky='') - style.layout('Notebook', style.layout('TFrame')) - style.layout('Notebook.TMenubutton', - [('Menubutton.border', - {'sticky': 'nswe', - 'children': [('Menubutton.focus', - {'sticky': 'nswe', - 'children': [('Menubutton.indicator', - {'side': 'right', 'sticky': ''}), - ('Menubutton.padding', - {'expand': '1', - 'sticky': 'we'})]})]})]) - style.layout('Notebook.Tab', style.layout('TFrame')) - style.layout('Notebook.Tab.Frame', style.layout('TFrame')) - style.layout('Notebook.Tab.Label', style.layout('TLabel')) - style.layout('Notebook.Tab.Close', - [('Close.padding', - {'sticky': 'nswe', - 'children': [('Close.border', - {'border': '1', - 'sticky': 'nsew', - 'children': [('Close.close', - {'sticky': 'ewsn'})]})]})]) - style.layout('Left.Notebook.TButton', - [('Button.padding', - {'sticky': 'nswe', - 'children': [('Button.leftarrow', {'sticky': 'nswe'})]})]) - style.layout('Right.Notebook.TButton', - [('Button.padding', - {'sticky': 'nswe', - 'children': [('Button.rightarrow', {'sticky': 'nswe'})]})]) - style.configure('Notebook', **style_config) - style.configure('Notebook.Tab', relief='raised', borderwidth=1, + sticky="") + style.layout("Notebook", style.layout("TFrame")) + style.layout("Notebook.TMenubutton", + [("Menubutton.border", + {"sticky": "nswe", + "children": [("Menubutton.focus", + {"sticky": "nswe", + "children": [("Menubutton.indicator", + {"side": "right", "sticky": ""}), + ("Menubutton.padding", + {"expand": "1", + "sticky": "we"})]})]})]) + style.layout("Notebook.Tab", style.layout("TFrame")) + style.layout("Notebook.Tab.Frame", style.layout("TFrame")) + style.layout("Notebook.Tab.Label", style.layout("TLabel")) + style.layout("Notebook.Tab.Close", + [("Close.padding", + {"sticky": "nswe", + "children": [("Close.border", + {"border": "1", + "sticky": "nsew", + "children": [("Close.close", + {"sticky": "ewsn"})]})]})]) + style.layout("Left.Notebook.TButton", + [("Button.padding", + {"sticky": "nswe", + "children": [("Button.leftarrow", {"sticky": "nswe"})]})]) + style.layout("Right.Notebook.TButton", + [("Button.padding", + {"sticky": "nswe", + "children": [("Button.rightarrow", {"sticky": "nswe"})]})]) + style.configure("Notebook", **style_config) + style.configure("Notebook.Tab", relief="raised", borderwidth=1, **style_config) - style.configure('Notebook.Tab.Frame', relief='flat', borderwidth=0, + style.configure("Notebook.Tab.Frame", relief="flat", borderwidth=0, **style_config) - style.configure('Notebook.Tab.Label', relief='flat', borderwidth=1, + style.configure("Notebook.Tab.Label", relief="flat", borderwidth=1, padding=0, **style_config) - style.configure('Notebook.Tab.Label', foreground=theme['unselectedfg']) - style.configure('Notebook.Tab.Close', relief='flat', borderwidth=1, + style.configure("Notebook.Tab.Label", foreground=theme["unselectedfg"]) + style.configure("Notebook.Tab.Close", relief="flat", borderwidth=1, padding=0, **style_config) - style.configure('Notebook.Tab.Frame', background=theme['bg']) - style.configure('Notebook.Tab.Label', background=theme['bg']) - style.configure('Notebook.Tab.Close', background=theme['bg']) - - style.map('Notebook.Tab.Frame', - **{'background': [('selected', '!disabled', theme['activebg'])]}) - style.map('Notebook.Tab.Label', - **{'background': [('selected', '!disabled', theme['activebg'])], - 'foreground': [('selected', '!disabled', theme['fg'])]}) - style.map('Notebook.Tab.Close', - **{'background': [('selected', theme['activebg']), - ('pressed', theme['darkcolor']), - ('active', theme['activebg'])], - 'relief': [('hover', '!disabled', 'raised'), - ('active', '!disabled', 'raised'), - ('pressed', '!disabled', 'sunken')], - 'lightcolor': [('pressed', theme['darkcolor'])], - 'darkcolor': [('pressed', theme['lightcolor'])]}) - style.map('Notebook.Tab', - **{'background': [('selected', '!disabled', theme['activebg'])]}) - - style.configure('Left.Notebook.TButton', padding=0) - style.configure('Right.Notebook.TButton', padding=0) + style.configure("Notebook.Tab.Frame", background=theme["bg"]) + style.configure("Notebook.Tab.Label", background=theme["bg"]) + style.configure("Notebook.Tab.Close", background=theme["bg"]) + + style.map("Notebook.Tab.Frame", + **{"background": [("selected", "!disabled", theme["activebg"])]}) + style.map("Notebook.Tab.Label", + **{"background": [("selected", "!disabled", theme["activebg"])], + "foreground": [("selected", "!disabled", theme["fg"])]}) + style.map("Notebook.Tab.Close", + **{"background": [("selected", theme["activebg"]), + ("pressed", theme["darkcolor"]), + ("active", theme["activebg"])], + "relief": [("hover", "!disabled", "raised"), + ("active", "!disabled", "raised"), + ("pressed", "!disabled", "sunken")], + "lightcolor": [("pressed", theme["darkcolor"])], + "darkcolor": [("pressed", theme["lightcolor"])]}) + style.map("Notebook.Tab", + **{"background": [("selected", "!disabled", theme["activebg"])]}) + + style.configure("Left.Notebook.TButton", padding=0) + style.configure("Right.Notebook.TButton", padding=0) def _on_configure(self, event=None): self.update_idletasks() @@ -453,12 +454,12 @@ def _on_configure(self, event=None): h = self._tab_frame.winfo_reqheight() self._canvas.configure(height=h) # ensure that _tab_frame2 fills the canvas if _tab_frame is smaller - self._canvas.itemconfigure('window', width=max(self._canvas.winfo_width(), self._tab_frame.winfo_reqwidth())) + self._canvas.itemconfigure("window", width=max(self._canvas.winfo_width(), self._tab_frame.winfo_reqwidth())) # update canvas scrollregion - self._canvas.configure(scrollregion=self._canvas.bbox('all')) + self._canvas.configure(scrollregion=self._canvas.bbox("all")) # ensure visibility of current tab self.see(self.current_tab) - # check wheter next/prev buttons needs to be displayed + # check whether next/prev buttons needs to be displayed if self._tab_frame.winfo_reqwidth() < self._canvas.winfo_width(): self._btn_left.grid_remove() self._btn_right.grid_remove() @@ -470,7 +471,7 @@ def _on_press(self, event, tab): # show clicked tab content self._show(tab) - if not self._tabdrag or self.tab(tab, 'state') == 'disabled': + if not self._tabdrag or self.tab(tab, "state") == "disabled": return # prepare dragging @@ -485,13 +486,13 @@ def _on_press(self, event, tab): self._dummy_sep.place_configure(in_=self._dummy_frame, y=self._dummy_frame.winfo_height()) widget.grid_remove() # place tab above the rest to drag it - widget.place(bordermode='outside', x=x, y=y) + widget.place(bordermode="outside", x=x, y=y) widget.lift() self._dragged_tab = widget self._dx = - event.x_root # - current mouse x position on screen self._y = event.y_root # current y mouse position on screen self._distance_to_dragged_border = widget.winfo_rootx() - event.x_root - widget.bind_all('', self._on_drag) + widget.bind_all("", self._on_drag) def _on_drag(self, event): self._dragged_tab.place_configure(x=self._dragged_tab.winfo_x() + event.x_root + self._dx) @@ -529,7 +530,7 @@ def _swap(self, tab): def _on_click(self, event): """Stop dragging.""" if self._dragged_tab: - self._dragged_tab.unbind_all('') + self._dragged_tab.unbind_all("") self._dragged_tab.grid(**self._dummy_frame.grid_info()) self._dummy_frame.grid_forget() @@ -544,7 +545,7 @@ def _on_click(self, event): def _menu_insert(self, tab, text): menu = [] for t in self._tabs.keys(): - menu.append((self.tab(t, 'text'), t)) + menu.append((self.tab(t, "text"), t)) menu.sort() ind = menu.index((text, tab)) self._tab_menu.insert_radiobutton(ind, label=text, @@ -566,32 +567,32 @@ def _resize(self): self._on_configure() def _show(self, tab_id, new=False, update=False): - if self.tab(tab_id, 'state') == 'disabled': + if self.tab(tab_id, "state") == "disabled": if tab_id in self._active_tabs: self._active_tabs.remove(tab_id) return # hide current tab body if self._current_tab >= 0: self._tabs[self.current_tab].grid_remove() - self._tab_labels[self.current_tab].state(['!selected']) + self._tab_labels[self.current_tab].state(["!selected"]) # restore tab if hidden if tab_id in self._hidden_tabs: self._tab_labels[tab_id].grid(in_=self._tab_frame) - self._visible_tabs.insert(self._tab_labels[tab_id].grid_info()['column'], tab_id) + self._visible_tabs.insert(self._tab_labels[tab_id].grid_info()["column"], tab_id) self._active_tabs = [t for t in self._visible_tabs - if self._tab_options[t]['state'] == 'normal'] + if self._tab_options[t]["state"] == "normal"] self._hidden_tabs.remove(tab_id) # update current tab self.current_tab = tab_id self._tab_var.set(tab_id) - self._tab_labels[tab_id].state(['selected']) + self._tab_labels[tab_id].state(["selected"]) if new: # add new tab c, r = self._tab_frame.grid_size() - self._tab_labels[tab_id].grid(in_=self._tab_frame, row=0, column=c, sticky='s') + self._tab_labels[tab_id].grid(in_=self._tab_frame, row=0, column=c, sticky="s") self._visible_tabs.append(tab_id) self.update_idletasks() @@ -600,17 +601,17 @@ def _show(self, tab_id, new=False, update=False): self.see(tab_id) # display body if update: - sticky = self._tab_options[tab_id]['sticky'] - pad = self._tab_options[tab_id]['padding'] + sticky = self._tab_options[tab_id]["sticky"] + pad = self._tab_options[tab_id]["padding"] self._tabs[tab_id].grid(in_=self._body, sticky=sticky, padx=pad, pady=pad) else: self._tabs[tab_id].grid(in_=self._body) self.update_idletasks() - self.event_generate('<>') + self.event_generate("<>") def _popup_menu(self, event, tab): self._show(tab) - if self.menu is not None: + if hasattr(self, "menu") and self.menu is not None: self.menu.tk_popup(event.x_root, event.y_root) @property @@ -624,15 +625,15 @@ def current_tab(self, tab_nb): self._tab_var.set(tab_nb) def cget(self, key): - if key == 'closebutton': + if key == "closebutton": return self._closebutton - elif key == 'closecommand': + elif key == "closecommand": return self._closecommand - elif key == 'tabmenu': + elif key == "tabmenu": return self._tabmenu - elif key == 'tabdrag': + elif key == "tabdrag": return self._tabdrag - elif key == 'drag_to_toplevel': + elif key == "drag_to_toplevel": return self._drag_to_toplevel else: return ttk.Frame.cget(self, key) @@ -657,18 +658,18 @@ def configure(self, cnf=None, **kw): else: kwargs = kw.copy() tab_kw = {} - if 'closebutton' in kwargs: - self._closebutton = bool(kwargs.pop('closebutton')) - tab_kw['closebutton'] = self._closebutton - if 'closecommand' in kwargs: - self._closecommand = kwargs.pop('closecommand') - tab_kw['closecommand'] = self._closecommand - if 'tabdrag' in kwargs: - self._tabdrag = bool(kwargs.pop('tabdrag')) - if 'drag_to_toplevel' in kwargs: - self._drag_to_toplevel = bool(kwargs.pop('drag_to_toplevel')) - if 'tabmenu' in kwargs: - self._tabmenu = bool(kwargs.pop('tabmenu')) + if "closebutton" in kwargs: + self._closebutton = bool(kwargs.pop("closebutton")) + tab_kw["closebutton"] = self._closebutton + if "closecommand" in kwargs: + self._closecommand = kwargs.pop("closecommand") + tab_kw["closecommand"] = self._closecommand + if "tabdrag" in kwargs: + self._tabdrag = bool(kwargs.pop("tabdrag")) + if "drag_to_toplevel" in kwargs: + self._drag_to_toplevel = bool(kwargs.pop("drag_to_toplevel")) + if "tabmenu" in kwargs: + self._tabmenu = bool(kwargs.pop("tabmenu")) if self._tabmenu: self._tab_list.grid() else: @@ -683,7 +684,7 @@ def configure(self, cnf=None, **kw): def keys(self): keys = ttk.Frame.keys(self) - return keys + ['closebutton', 'closecommand', 'tabmenu'] + return keys + ["closebutton", "closecommand", "tabmenu"] def add(self, widget, **kwargs): """ @@ -695,7 +696,7 @@ def add(self, widget, **kwargs): :param compound: how the tab label and image are organized :param sticky: for the widget inside the notebook :param padding: padding (int) around the widget in the notebook - :param state: state ('normal' or 'disabled') of the tab + :param state: state ("normal" or "disabled") of the tab """ # Todo: underline name = str(widget) @@ -705,8 +706,8 @@ def add(self, widget, **kwargs): self._show(ind) self.update_idletasks() else: - sticky = kwargs.pop('sticky', 'ewns') - padding = kwargs.pop('padding', 0) + sticky = kwargs.pop("sticky", "ewns") + padding = kwargs.pop("padding", 0) self._tabs[self._nb_tab] = widget ind = self._nb_tab self._indexes[name] = ind @@ -714,22 +715,22 @@ def add(self, widget, **kwargs): closecommand=self._closecommand, closebutton=self._closebutton, **kwargs) - self._tab_labels[ind].bind('', self._on_click) - self._tab_labels[ind].bind('', lambda e: self._popup_menu(e, ind)) - self._tab_labels[ind].bind('', lambda e: self._on_press(e, ind)) + self._tab_labels[ind].bind("", self._on_click) + self._tab_labels[ind].bind("", lambda e: self._popup_menu(e, ind)) + self._tab_labels[ind].bind("", lambda e: self._on_press(e, ind)) self._body.configure(height=max(self._body.winfo_height(), widget.winfo_reqheight()), width=max(self._body.winfo_width(), widget.winfo_reqwidth())) - self._tab_options[ind] = dict(text='', image='', compound='none', state='normal') + self._tab_options[ind] = dict(text="", image="", compound="none", state="normal") self._tab_options[ind].update(kwargs) self._tab_options[ind].update(dict(padding=padding, sticky=sticky)) - self._tab_menu_entries[ind] = self._tab_menu.index('end') - self._tab_list.state(['!disabled']) + self._tab_menu_entries[ind] = self._tab_menu.index("end") + self._tab_list.state(["!disabled"]) self._active_tabs.append(ind) self._show(self._nb_tab, new=True, update=True) self._nb_tab += 1 - self._menu_insert(ind, kwargs.get('text', '')) + self._menu_insert(ind, kwargs.get("text", "")) return ind def insert(self, where, widget, **kwargs): @@ -740,7 +741,7 @@ def insert(self, where, widget, **kwargs): """ existing = str(widget) in self._indexes index = self.add(widget, **kwargs) - if where == 'end': + if where == "end": if not existing: return where = self.index(where) @@ -754,8 +755,8 @@ def insert(self, where, widget, **kwargs): return index def enable_traversal(self): - self.bind('', lambda e: self.select_next(True)) - self.bind('', lambda e: self.select_prev(True)) + self.bind("", lambda e: self.select_next(True)) + self.bind("", lambda e: self.select_prev(True)) def index(self, tab_id): """Return the tab index of TAB_ID.""" @@ -808,15 +809,15 @@ def see(self, tab_id): self._canvas.xview_moveto(xc1 + x2 - xc2) i = self._visible_tabs.index(tab) if i == 0: - self._btn_left.state(['disabled']) + self._btn_left.state(["disabled"]) if len(self._visible_tabs) > 1: - self._btn_right.state(['!disabled']) + self._btn_right.state(["!disabled"]) elif i == len(self._visible_tabs) - 1: - self._btn_right.state(['disabled']) - self._btn_left.state(['!disabled']) + self._btn_right.state(["disabled"]) + self._btn_left.state(["!disabled"]) else: - self._btn_right.state(['!disabled']) - self._btn_left.state(['!disabled']) + self._btn_right.state(["!disabled"]) + self._btn_left.state(["!disabled"]) def hide(self, tab_id): """Hide tab TAB_ID.""" @@ -853,7 +854,7 @@ def forget(self, tab_id): else: self.current_tab = -1 if not self._visible_tabs and not self._hidden_tabs: - self._tab_list.state(['disabled']) + self._tab_list.state(["disabled"]) self._tabs[tab].grid_forget() del self._tab_labels[tab] del self._indexes[str(self._tabs[tab])] @@ -885,27 +886,27 @@ def tab(self, tab_id, option=None, **kw): Query or modify TAB_ID options. The widget corresponding to tab_id can be obtained by passing the option - 'widget' but cannot be modified. + "widget" but cannot be modified. """ tab = self.index(tab_id) - if option == 'widget': + if option == "widget": return self._tabs[tab] elif option: return self._tab_options[tab][option] else: self._tab_options[tab].update(kw) - sticky = kw.pop('padding', None) - padding = kw.pop('sticky', None) + sticky = kw.pop("padding", None) + padding = kw.pop("sticky", None) self._tab_labels[tab].tab_configure(**kw) if sticky is not None or padding is not None and self.current_tab == tab: self._show(tab, update=True) - if 'text' in kw: + if "text" in kw: self._tab_menu.delete(self._tab_menu_entries[tab]) - self._menu_insert(tab, kw['text']) - if 'state' in kw: + self._menu_insert(tab, kw["text"]) + if "state" in kw: self._tab_menu.entryconfigure(self._tab_menu_entries[tab], - state=kw['state']) - if kw['state'] == 'disabled': + state=kw["state"]) + if kw["state"] == "disabled": if tab in self._active_tabs: self._active_tabs.remove(tab) if tab == self.current_tab: @@ -919,7 +920,7 @@ def tab(self, tab_id, option=None, **kw): self.current_tab = -1 else: self._active_tabs = [t for t in self._visible_tabs - if self._tab_options[t]['state'] == 'normal'] + if self._tab_options[t]["state"] == "normal"] if self.current_tab == -1: self._show(tab) From 78f0810e3227571e4f22ea704ec8fb3512896790 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Fri, 24 Apr 2020 12:18:44 +0200 Subject: [PATCH 30/31] Fix scrolling through tabs on Linux --- ttkwidgets/notebook.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ttkwidgets/notebook.py b/ttkwidgets/notebook.py index d5bec37d..2b45068c 100644 --- a/ttkwidgets/notebook.py +++ b/ttkwidgets/notebook.py @@ -1,6 +1,7 @@ """ Copyright 2018-2019 Juliette Monsel Copyright 2019 Dogeek +Copyright 2020 RedFantom Adapted from PyTkEditor - Python IDE's Notebook widget by Juliette Monsel. Adapted by Dogeek @@ -9,6 +10,7 @@ Notebook with draggable / scrollable tabs """ +from functools import partial import tkinter as tk from tkinter import ttk from ttkwidgets.utilities import move_widget, parse_geometry, coords_in_box @@ -51,11 +53,13 @@ def __init__(self, master=None, tab_nb=0, **kwargs): self.bind("", self._b2_press) self.bind("", self._on_enter_tab) self.bind("", self._on_leave_tab) - self.bind("", self._on_mousewheel) + self.bind("", partial(self._on_mousewheel, None)) + self.bind("", partial(self._on_mousewheel, True)) # Linux mousewheel bind + self.bind("", partial(self._on_mousewheel, False)) - def _on_mousewheel(self, event): + def _on_mousewheel(self, updown, event): if self.hovering_tab: - if event.delta > 0: + if (updown is None and event.delta > 0) or updown is True: self.master.master.select_prev(True) else: self.master.master.select_next(True) @@ -330,7 +334,7 @@ def setup_style(self, bg="#dddddd", activebg="#efefef", pressedbg="#c1c1c1", :param bordercolor: :param focusbordercolor: :param selectbg: - :param selectfb: + :param selectfg: :param unselectfg: :param disabledfg: :param disabledbg: From 02806f6de9ebfc185b3a4995252758f308983311 Mon Sep 17 00:00:00 2001 From: RedFantom Date: Fri, 24 Apr 2020 12:38:18 +0200 Subject: [PATCH 31/31] Improve styling of Notebook widget with theme --- ttkwidgets/notebook.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/ttkwidgets/notebook.py b/ttkwidgets/notebook.py index 2b45068c..d6f29f17 100644 --- a/ttkwidgets/notebook.py +++ b/ttkwidgets/notebook.py @@ -318,10 +318,9 @@ def __getitem__(self, key): def __setitem__(self, key, value): self.configure(**{key: value}) - def setup_style(self, bg="#dddddd", activebg="#efefef", pressedbg="#c1c1c1", - fg="black", fieldbg="white", lightcolor="#ededed", darkcolor="##cfcdc8", - bordercolor="#888888", focusbordercolor="#5e5e5e", selectbg="#c1c1c1", - selectfg="black", unselectfg="#999999", disabledfg="#999999", disabledbg="#dddddd"): + def setup_style(self, bg=None, activebg=None, pressedbg=None, fg=None, fieldbg=None, lightcolor="#ededed", + darkcolor="#cfcdc8", bordercolor="#888888", focusbordercolor="#5e5e5e", selectbg=None, + selectfg=None, unselectfg="#999999", disabledfg=None, disabledbg=None): """ Setups the style for the notebook. :param bg: @@ -339,22 +338,24 @@ def setup_style(self, bg="#dddddd", activebg="#efefef", pressedbg="#c1c1c1", :param disabledfg: :param disabledbg: """ - theme = {"bg": bg, - "activebg": activebg, - "pressedbg": pressedbg, - "fg": fg, - "fieldbg": fieldbg, - "lightcolor": lightcolor, - "darkcolor": darkcolor, + style = ttk.Style(self) + + theme = {"bg": bg or style.lookup(".", "background", default="#dddddd"), + "activebg": activebg or style.lookup(".", "background", ("active",), default="#efefef"), + "pressedbg": pressedbg or style.lookup(".", "selectbackground", default="#c1c1c1"), + "fg": fg or style.lookup(".", "foreground", default="black"), + "fieldbg": fieldbg or style.lookup(".", "fieldbackground", default="white"), + "lightcolor": lightcolor or style.lookup(".", "focuscolor", default="#ededed"), + "darkcolor": darkcolor or style.lookup(".", "throughcolor", default="#cfcdc8"), "bordercolor": bordercolor, "focusbordercolor": focusbordercolor, - "selectbg": selectbg, - "selectfg": selectfg, + "selectbg": selectbg or style.lookup(".", "selectbackground", default="#c1c1c1"), + "selectfg": selectfg or style.lookup(".", "selectforeground", default="black"), "unselectedfg": unselectfg, - "disabledfg": disabledfg, - "disabledbg": disabledbg} + "disabledfg": disabledfg or style.lookup(".", "foreground", ("disabled",), default="#999999"), + "disabledbg": disabledbg or style.lookup(".", "background", ("disabled",), default="#dddddd")} - self.images = ( + self.images = ( # Must be on self to keep reference tk.PhotoImage("img_close", data=""" R0lGODlhCAAIAMIBAAAAADs7O4+Pj9nZ2Ts7Ozs7Ozs7Ozs7OyH+EUNyZWF0ZWQg d2l0aCBHSU1QACH5BAEKAAQALAAAAAAIAAgAAAMVGDBEA0qNJyGw7AmxmuaZhWEU @@ -383,7 +384,6 @@ def setup_style(self, bg="#dddddd", activebg="#efefef", pressedbg="#c1c1c1", "darkcolor": theme["darkcolor"], "troughcolor": theme["pressedbg"]} - style = ttk.Style(self) style.element_create("close", "image", "img_close", ("active", "pressed", "!disabled", "img_closepressed"), ("active", "!disabled", "img_closeactive"),