-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merge notebook into master for improved Notebook widget #48
base: master
Are you sure you want to change the base?
Conversation
Codecov Report
@@ Coverage Diff @@
## master #48 +/- ##
==========================================
- Coverage 89.76% 85.82% -3.95%
==========================================
Files 36 59 +23
Lines 3751 6312 +2561
==========================================
+ Hits 3367 5417 +2050
- Misses 384 895 +511
Continue to review full report at Codecov.
|
TODO List:
|
769f72a
to
a072156
Compare
tests/test_notebook.py
Outdated
@@ -0,0 +1,14 @@ | |||
# -*- coding: utf-8 -*- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tests should be extended to be much more extensive and cover more of the notebook.py
file. Just testing initialization is not sufficient.
Thank you for these commits, @dogeek . I have cherry-picked them, which yields an easier and cleaner solution than merging into this branch. In the future, just mention the branch your commits are on, then I can continue cherry-picking them. The CI is now failing because of issues with copying the widgets. If no value is given for an option, the default option (an empty value) appears to not always be valid. This will require some investigation. Also, I would like to test the bindings in more detail, verifying that the actual functions are still bound. I will work on it when I can. |
@RedFantom I've fixed the copy_widget and move_widget functions in the |
Thank you for taking a look at it! When I cherry-picked your commit and tested the solution against the unit tests, the issue still occurred however. I believe this is because the instantiation of an empty widget still leaves the options unspecified and rather than returning the defaults, These empty values must be filtered out. This does mean that if a widget is instantiated using an option that is an empty string, this will fail. However, I do not see any other option to do this properly. The new issue is unrelated to the previous one. This one has to do with the name of the widget. As the widget is newly created, the names do not necessarily match. The equalness of the widgets must be verified differently. |
@RedFantom Added the fix with runtime error (same branch as before) and some more tests for utilities functions cause why not |
forgot to update the docstring for copy_widget (to signal possible raise of RuntimeError), docstring of move_widget could be updated as well |
I checked your commit for the get_widget_options thing, and my implementation is way better (dict comprehensions are about 10X faster than dict mutations, and I actually check if the option is at its default value instead of if the option is '' or None) |
While a comprehension may be faster, the solution you provide actually does not work, as the default values of the widget options are also empty. I checked by cherry-picking your commit and running the tests against it. My implementation might still be rewritten as a comprehension though. |
@RedFantom I've tested it myself, and it does work. it returns a dictionary of all the diverging keys from the default values. That means that if you do this : import tkinter as tk
root = tk.Tk()
lbl = tk.Label(root, takefocus=True)
print(get_widget_options(lbl))
# {'takefocus': 1} you can see it works. If all the values are the default, it will return an empty dict, which is also fine. |
pushed a bunch of fixes to |
My apologies, I must have cherry-picked and then merged the changes wrongly, as you are correct: Your implementation does indeed work. However, I decided to profile the function performance, and the version with dictionary modifications is significantly faster than the one with the dictionary comprehension. Even after taking out duplicate function calls and changing it so that the base widget object is only created once, the function with the 'normal' for loop yields a running time of I ran the test multiple times with multiple variations, but it was always slower. This is an interesting result that I had actually not expected. Perhaps the slow down is due to the added if-statement in the comprehension, I'm not sure. Regardless, your implementation does work, but the one on the branch right now is a tad bit faster. |
Interesting. Though speed is not the only thing that matters in this case. I implemented it that way to take full advantage of Python's capabilities. My implementation is better in that regard, if the tkinter API ever changes, with the defaults changing, it's much better to do it my way. Regardless, dict comprehensions are actually faster anyways, the only reason my implementation is slower is because there's 2 comprehensions, filtering 2 times. A single comprehension like : return {key: widget.cget(key) for key in widget.keys() if widget.cget(key)} is surely faster than dict mutating. Also did you test with a widget with a bunch of options changed ? The more the options, the greater the gains of using a comprehension over mutating the dict |
The tests added in commit Of course, the widget can be made to not have the functionality of moving tabs to a new parent and in this way be merged. The problem is illustrated by following code snippet: >>> import tkinter as tk
>>> window = tk.Tk()
>>> button = tk.Button(window, command=window.destroy
>>> button.cget("command")
'140439709217672destroy' I cannot cherry-pick all of your changes, @dogeek . The branches have diverged too far and it would basically mean using copy-paste on all the changed lines would be faster than using |
The problem is that I don't know how to grab the reference to the functions. The only solution I can see right now would be to hook into the ttk::Widget and tkinter::Widget classes to append the function references to a list. It should be that 'tk::Widget().bind(key)' returns the contents of the script but it doesn't. @j4321 do you have any insight on this ? |
I thought I'd take a look at the Tkinter source code to figure out how the functions are called from the
This is where it gets... Complicated with the
ConclusionMy conclusion is that once a binding is created, it can no longer be retrieved as a Python function, as it only still exists in the memory of the Python interpreter but there are no direct references to it from the Python. The Python interpreter simply keeps the memory as there is still a non-zero reference count to the created objects and the Tcl-interpreter manages its own memory, in which the binding is stored. There are two ways I can see to change this behaviour:
Note that I have not yet researched how the |
I found out that binding through that return value actually works for some reason. Like : import tkinter as tk
def callback(ev):
print('Entered ' + str(ev.widget))
root = tk.Tk()
root.title('root')
frame = tk.Frame(root, width=300, height=300)
frame.bind('<Enter>', callback)
frame.pack()
print(frame.bind()) # ('<Enter>', )
print(frame.bind('<Enter>')) # 'if {"[64351688callback %# %b %f %h %k %s %t %w %x %y %A %E %K %N %W %T %X %Y %D]" == "break"} break\n'
tl = tk.Toplevel(root)
tl.title('tl')
other = tk.Frame(tl, width=300, height=300)
other.bind('<Enter>', frame.bind('<Enter>'))
other.pack()
root.mainloop() The callback is still properly called, even though a string is passed as the callback argument. So I don't know why there is an error here |
Ah, of course. As you are invoking the So then the idea is to setup the bindings of the new widget to the string representations of the old widget's bindings. I'll try building an implementation. |
The solution for the bindings is actually simpler still. As the commands and their wrappers as far as I know do not specifically depend on the widgets that they are created for, they may be preserved by not destroying them when the widget is destroyed. The easiest way to do this is to make sure that the Python Next up: Commands and the |
The place geometry manager is actually handled already. The widgets are copied as is, with all their options. The command is actually behaving in the same way as the bind callbacks. I suppose every tkinter callback works that way (like after, bind, bind_all, command etc). >>> import tkinter as tk
>>> def cb():
... print('callback')
...
>>> but = tk.Button(command=cb, text='ok')
>>> but.pack()
>>> callback
>>> but['command']
<string object: '44558152cb'>
>>> dir(but['command'])
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
>>> but2 = tk.Button(command=but['command'], text='2')
>>> but2.pack()
>>> callback
>>> but2['command']
<string object: '44558152cb'>
>>> |
- Removed shebang and encoding - Removed Notebook widget from Toplevel in move_to_toplevel
- Move widget with binding on parent - Coordinates in box fixed - Move widget with binding
- Typos in docstrings - Docstring format in test_notebook.py
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)
Both the insert and add functions should return a valid TAB_ID
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.
PR Details:
Notebook
Description
A
Notebook
widget implementation with functionality to reorder, close and detach tabs. In addition, when there are too many tabs, buttons appear to navigate through them. AMenu
is also available to allow quick jumping to a specific tab.Checklist
/examples
/tests
AUTHORS.md
This PR contains much the same changes as #43 , but rebased onto
master
, changing commit messages and squashing commits into larger ones. This branch contains only changes required for the widget and no other commits.