├── .flake8 ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── TODO.txt ├── docs ├── bind.rst ├── canvas.rst ├── concurrency.rst ├── conf.py ├── dialog.rst ├── eventloop.rst ├── extensions.py ├── extras.rst ├── geometry-managers.rst ├── index.rst ├── menu.rst ├── misc-objs.rst ├── notebook.rst ├── platform-info.rst ├── tcl-calls.rst ├── textwidget.rst ├── tkinter.rst ├── tutorial.rst └── widgets.rst ├── examples ├── README.md ├── __init__.py ├── __main__.py ├── button.py ├── checkbutton.py ├── hello_world.py ├── image.py ├── links.py ├── paint.py ├── separator.py ├── soup.py ├── text.py ├── timeout.py └── tooltip.py ├── pyproject.toml ├── pytest.ini ├── readthedocs.yml ├── teek ├── __init__.py ├── _font.py ├── _platform_info.py ├── _structures.py ├── _tcl_calls.py ├── _timeouts.py ├── _widgets │ ├── __init__.py │ ├── base.py │ ├── canvas.py │ ├── menu.py │ ├── misc.py │ ├── notebook.py │ ├── text.py │ └── windows.py ├── dialog.py └── extras │ ├── __init__.py │ ├── cross_platform.py │ ├── image_loader.py │ ├── image_loader_dummy.py │ ├── links.py │ ├── more_dialogs.py │ ├── soup.py │ └── tooltips.py ├── tests ├── conftest.py ├── data │ ├── firefox.svg │ ├── rectangle.svg │ ├── smiley.gif │ ├── smiley.jpg │ ├── sources.txt │ └── subclasser.py ├── extras │ ├── __init__.py │ ├── test_cross_platform.py │ ├── test_image_loader.py │ ├── test_links.py │ ├── test_more_dialogs.py │ ├── test_soup.py │ └── test_tooltips.py ├── test_dialog.py ├── test_docs_stuff.py ├── test_examples.py ├── test_font.py ├── test_geometry_managers.py ├── test_images.py ├── test_platform_info.py ├── test_structures.py ├── test_tcl_calls.py ├── test_threads.py ├── test_timeouts.py ├── test_vars.py └── widgets │ ├── __init__.py │ ├── test_canvas.py │ ├── test_common_stuff.py │ ├── test_menu.py │ ├── test_misc_widgets.py │ ├── test_notebook.py │ ├── test_text_widget.py │ └── test_window_widgets.py └── tk-ttk.png /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | 3 | # E265 block comment should start with '# ' 4 | # this is ignored because '#print("toot")' is a handy way to comment 5 | # things out, and easier to work with than '# print("toot")' which is 6 | # best suited for human-readable comments 7 | ignore = E265, E701, W504 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | .pytest_cache/ 92 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.5" 4 | - "3.6" 5 | - "3.7" 6 | - "3.8" 7 | - nightly 8 | - "pypy3.5" 9 | install: 10 | - pip install 'pytest>=4.0' pytest-cov python-coveralls flake8 sphinx flit 11 | - flit install --symlink --extras=image_loader,soup_viewer 12 | script: 13 | - flake8 teek/ tests/ examples/ *.py 14 | 15 | # https://docs.travis-ci.com/user/gui-and-headless-browsers/#using-xvfb-to-run-tests-that-require-a-gui 16 | - xvfb-run python -m pytest --cov=teek 17 | after_success: 18 | - coveralls 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2019 Akuli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | === Text === 2 | tag lower *2 3 | sel.first, sel.last could be used everywhere if they existed 4 | edit separator 5 | edit reset 6 | Text.search 7 | text index with @ 8 | 9 | === winfo and wm === 10 | winfo reqwidth *2 11 | winfo reqheight *2 12 | winfo x 13 | winfo y 14 | wm attributes 15 | wm min,maxsize 16 | wm resizable 17 | wm iconphoto 18 | wm overrideredirect 19 | 20 | === Entry === 21 | selection range 22 | 23 | === Notebook === 24 | lookup tab by xy 25 | 26 | === misc stuffs === 27 | clipboard 28 | identify 29 | Style api 30 | 31 | === new extras === 32 | simple dialogs would allow removing a lot of code from porcu 33 | ttkthemes needs Style api 34 | 35 | 36 | 37 | make threading stuff faster, this is slow: 38 | 39 | import threading 40 | import teek 41 | 42 | teek.init_threads() 43 | text = teek.Text(teek.Window()) 44 | text.pack() 45 | 46 | def inserter(): 47 | i = 0 48 | while True: 49 | i += 1 50 | text.insert(text.end, 'hello %d\n' % i) 51 | #teek.tcl_call(None, text, 'insert', 'end - 1 char', 'hello %d\n' % i) 52 | 53 | threading.Thread(target=inserter).start() 54 | teek.run() 55 | 56 | because text.insert and text.end end up in the queue as separate items, which 57 | is a problem because this is a particularly common way to use things in a 58 | thread... maybe add an insert_to_end method? 59 | -------------------------------------------------------------------------------- /docs/canvas.rst: -------------------------------------------------------------------------------- 1 | .. _canvaswidget: 2 | 3 | Canvas Widget 4 | ============= 5 | 6 | **Manual page:** :man:`canvas(3tk)` 7 | 8 | This widget is useful for displaying drawings and other things. See 9 | :source:`examples/paint.py` for an example. 10 | 11 | .. note:: 12 | This widget is **not** a Ttk widget because there is no Ttk canvas widget. 13 | However, that's not a problem because usually canvas widgets contain 14 | things that shouldn't be colored based on the current Ttk theme anyway. 15 | 16 | 17 | Hello World! 18 | ------------ 19 | 20 | This program displays some simple things on a canvas. 21 | 22 | :: 23 | 24 | import teek 25 | 26 | window = teek.Window() 27 | canvas = teek.Canvas(window) 28 | canvas.pack() 29 | 30 | canvas.create_line(100, 100, 120, 150) 31 | canvas.create_oval(200, 200, 250, 250) 32 | canvas.create_rectangle(200, 100, 230, 130) 33 | 34 | window.on_delete_window.connect(teek.quit) 35 | teek.run() 36 | 37 | The background color of the canvas depends on the system that this 38 | code is ran on. If you don't want that, you can create the canvas like 39 | ``canvas = teek.Canvas(window, bg='white')``, for instance. 40 | 41 | The numbers passed to :func:`~teek.Canvas.create_line` and similar methods are 42 | **coordinates**. They work so that, for example, ``(200, 100)`` means 200 pixels 43 | right from the left side of the canvas, and 100 pixels down from the top. The 44 | "how much right" coordinate is called the **x coordinate**, and the "how much 45 | down" coordinate is called the "y coordinate". 46 | 47 | .. note:: 48 | The y coordinates work differently than they usually work in mathematics; 49 | that is, more y means down in Tk and teek. This also means that rotations 50 | go the other way, and positive angles mean clockwise rotations in teek even 51 | though they mean counter-clockwise rotations in mathematics. 52 | 53 | The ``create_something()`` methods take four numbers each, because they are two 54 | pairs of ``(x, y)`` coordinates. For example, 55 | ``canvas.create_rectangle(200, 100, 230, 130)`` means that one corner of the 56 | created rectangle is at ``(200, 100)`` and another corner is at ``(230, 130)``. 57 | (It is actually a square, because it is 30 pixels high and 30 pixels wide). 58 | 59 | 60 | .. _canvas-items: 61 | 62 | Item Objects 63 | ------------ 64 | 65 | The ``create_something()`` methods return item objects that you can keep around 66 | and do stuff with. For example, you can do this:: 67 | 68 | >>> canvas = teek.Canvas(teek.Window()) 69 | >>> square = canvas.create_rectangle(200, 100, 230, 130) 70 | >>> square 71 | 72 | >>> square.config['fill'] = 'red' # this makes the square red 73 | 74 | Or you can pass some config options to ``create_rectangle()`` directly, as 75 | usual:: 76 | 77 | >>> square = canvas.create_rectangle(200, 100, 230, 130, fill='red') 78 | >>> square.config['fill'] 79 | 80 | 81 | Here ``square`` is a canvas item object. 82 | 83 | All canvas item objects have these attributes and methods: 84 | 85 | .. attribute:: some_canvas_item.config 86 | 87 | This dictionary-like object is similar to the ``config`` attribute of 88 | widgets, which is explained :ref:`here `. 89 | 90 | .. attribute:: some_canvas_item.coords 91 | 92 | A tuple of coordinates of the canvas. This can be set to move an existing 93 | canvas item without having to create a new item. 94 | 95 | .. attribute:: some_canvas_item.tags 96 | 97 | This is a set-like object of the canvas item's :ref:`tags `, 98 | as strings. You can ``.add()`` and ``.remove()`` the tags, for example. 99 | 100 | .. attribute:: some_canvas_item.item_type_name 101 | 102 | The kind of the canvas item as a string, e.g. ``'oval'`` or ``'rectangle'``. 103 | 104 | .. method:: some_canvas_item.delete() 105 | 106 | This deletes the canvas item. Trying to do something with the canvas item 107 | will raise an error. 108 | :: 109 | 110 | >>> circle = canvas.create_oval(200, 200, 250, 250) 111 | >>> circle 112 | 113 | >>> circle.delete() 114 | >>> circle 115 | 116 | 117 | .. method:: some_canvas_item.find_above() 118 | some_canvas_item.find_below() 119 | 120 | These return another canvas item object. See ``pathName find`` and the 121 | ``above`` and ``below`` parts of ``pathName addtag`` in :man:`canvas(3tk)` 122 | for more information. 123 | 124 | The type of the canvas items is accessible as ``the_canvas_widget.Item``. It's 125 | useful for :func:`isinstance` checks, but not much else:: 126 | 127 | >>> isinstance(circle, canvas.Item) 128 | True 129 | >>> isinstance('lolwat', canvas.Item) 130 | False 131 | 132 | Note that this is different for each canvas; the items of two different 133 | canvases are not of the same type. 134 | 135 | 136 | .. _canvas-tags: 137 | 138 | Tags 139 | ---- 140 | 141 | If you have lots of related canvas items, you can just keep a list of them and 142 | do something to each item in that list. Alternatively, you can use tags to mark 143 | the canvas items, and then do something to all items tagged with a specific 144 | tag. Like this:: 145 | 146 | >>> canvas = teek.Canvas(teek.Window()) 147 | >>> line = canvas.create_line(100, 100, 120, 150) 148 | >>> circle = canvas.create_oval(200, 200, 250, 250) 149 | >>> square = canvas.create_rectangle(200, 100, 230, 130) 150 | >>> canvas.find_withtag('lol') 151 | [] 152 | >>> circle.tags.add('lol') 153 | >>> canvas.find_withtag('lol') # returns a list that contains the circle 154 | [] 155 | 156 | 157 | Canvas Widget Methods and Attributes 158 | ------------------------------------ 159 | 160 | .. autoclass:: teek.Canvas 161 | :members: 162 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | sys.path.insert(0, os.path.abspath('.')) # for extensions.py 18 | sys.path.insert(0, os.path.abspath('..')) # for teek 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'Teek' 24 | copyright = '2018, Akuli' 25 | author = 'Akuli' 26 | 27 | # The short X.Y version 28 | version = '' 29 | # The full version, including alpha/beta/rc tags 30 | release = '' 31 | 32 | # to get warnings when links to other parts of the docs don't work 33 | nitpicky = True 34 | 35 | 36 | # -- General configuration --------------------------------------------------- 37 | 38 | # If your documentation needs a minimal Sphinx version, state it here. 39 | # 40 | # needs_sphinx = '1.0' 41 | 42 | # Add any Sphinx extension module names here, as strings. They can be 43 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 44 | # ones. 45 | extensions = [ 46 | 'sphinx.ext.autodoc', 47 | 'sphinx.ext.intersphinx', 48 | 'sphinx.ext.viewcode', 49 | 'extensions', 50 | ] 51 | 52 | # Add any paths that contain templates here, relative to this directory. 53 | templates_path = ['_templates'] 54 | 55 | # The suffix(es) of source filenames. 56 | # You can specify multiple suffix as a list of string: 57 | # 58 | # source_suffix = ['.rst', '.md'] 59 | source_suffix = '.rst' 60 | 61 | # The master toctree document. 62 | master_doc = 'index' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | # This pattern also affects html_static_path and html_extra_path . 74 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 75 | 76 | # The name of the Pygments (syntax highlighting) style to use. 77 | pygments_style = 'sphinx' 78 | 79 | 80 | # -- Options for HTML output ------------------------------------------------- 81 | 82 | # The theme to use for HTML and HTML Help pages. See the documentation for 83 | # a list of builtin themes. 84 | # 85 | html_theme = 'sphinx_rtd_theme' 86 | 87 | # Theme options are theme-specific and customize the look and feel of a theme 88 | # further. For a list of options available for each theme, see the 89 | # documentation. 90 | # 91 | # html_theme_options = {} 92 | 93 | # Add any paths that contain custom static files (such as style sheets) here, 94 | # relative to this directory. They are copied after the builtin static files, 95 | # so a file named "default.css" will overwrite the builtin "default.css". 96 | html_static_path = ['_static'] 97 | 98 | # Custom sidebar templates, must be a dictionary that maps document names 99 | # to template names. 100 | # 101 | # The default sidebars (for documents that don't match any pattern) are 102 | # defined by theme itself. Builtin themes are using these templates by 103 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 104 | # 'searchbox.html']``. 105 | # 106 | # html_sidebars = {} 107 | 108 | 109 | # -- Options for HTMLHelp output --------------------------------------------- 110 | 111 | # Output file base name for HTML help builder. 112 | htmlhelp_basename = 'Teekdoc' 113 | 114 | 115 | # -- Options for LaTeX output ------------------------------------------------ 116 | 117 | latex_elements = { 118 | # The paper size ('letterpaper' or 'a4paper'). 119 | # 120 | # 'papersize': 'letterpaper', 121 | 122 | # The font size ('10pt', '11pt' or '12pt'). 123 | # 124 | # 'pointsize': '10pt', 125 | 126 | # Additional stuff for the LaTeX preamble. 127 | # 128 | # 'preamble': '', 129 | 130 | # Latex figure (float) alignment 131 | # 132 | # 'figure_align': 'htbp', 133 | } 134 | 135 | # Grouping the document tree into LaTeX files. List of tuples 136 | # (source start file, target name, title, 137 | # author, documentclass [howto, manual, or own class]). 138 | latex_documents = [ 139 | (master_doc, 'Teek.tex', 'Teek Documentation', 140 | 'Akuli', 'manual'), 141 | ] 142 | 143 | 144 | # -- Options for manual page output ------------------------------------------ 145 | 146 | # One entry per manual page. List of tuples 147 | # (source start file, name, description, authors, manual section). 148 | man_pages = [ 149 | (master_doc, 'teek', 'Teek Documentation', 150 | [author], 1) 151 | ] 152 | 153 | 154 | # -- Options for Texinfo output ---------------------------------------------- 155 | 156 | # Grouping the document tree into Texinfo files. List of tuples 157 | # (source start file, target name, title, author, 158 | # dir menu entry, description, category) 159 | texinfo_documents = [ 160 | (master_doc, 'Teek', 'Teek Documentation', 161 | author, 'Teek', 'One line description of project.', 162 | 'Miscellaneous'), 163 | ] 164 | 165 | 166 | # -- Extension configuration ------------------------------------------------- 167 | 168 | # -- Options for intersphinx extension --------------------------------------- 169 | 170 | # Example configuration for intersphinx: refer to the Python standard library. 171 | intersphinx_mapping = {'https://docs.python.org/': None} 172 | -------------------------------------------------------------------------------- /docs/dialog.rst: -------------------------------------------------------------------------------- 1 | Dialogs 2 | ======= 3 | 4 | .. module:: teek.dialog 5 | 6 | This page contains things for asking the user things like file names and 7 | colors. If you want to display a custom dialog, create a :class:`.Window`, add 8 | some stuff to it and use :meth:`~.Toplevel.wait_window`. 9 | 10 | .. note:: 11 | All functions documented on this page take a ``parent`` keyword argument. 12 | Use that whenever you are calling the functions from a program that has 13 | another window. This way the dialog will look like it belongs to that 14 | parent window. 15 | 16 | 17 | Message Boxes 18 | ------------- 19 | 20 | These functions call :man:`tk_messageBox(3tk)`. Options are passed to 21 | :man:`tk_messageBox(3tk)` so that this code... 22 | :: 23 | 24 | teek.dialog.ok_cancel("Question", "Do you want that?") 25 | 26 | ...does a Tcl call like this: 27 | 28 | .. code-block:: tcl 29 | 30 | tk_messageBox -type okcancel -title "Question" -message "Do you want that?" -icon question 31 | 32 | .. function:: info(title, message, detail=None, **kwargs) 33 | warning(title, message, detail=None, **kwargs) 34 | error(title, message, detail=None, **kwargs) 35 | 36 | Each of these functions shows a message box that has an "ok" button. The 37 | icon option is ``'info'``, ``'warning'`` or ``'error'`` respectively. These 38 | functions always return ``None``. 39 | 40 | .. function:: ok_cancel(title, message, detail=None, **kwargs) 41 | 42 | Shows a message box with "ok" and "cancel" buttons. The icon is 43 | ``'question'`` by default, but you can change it by passing a keyword 44 | argument, e.g. ``icon='warning'``. Returns ``True`` if "ok" is clicked, and 45 | ``False`` if "cancel" is clicked. 46 | 47 | .. function:: retry_cancel(title, message, detail=None, **kwargs) 48 | 49 | Like :func:`ok_cancel`, but with a "retry" button instead of an "ok" button 50 | and ``'warning'`` as the default icon. 51 | 52 | .. function:: yes_no(title, message, detail=None, **kwargs) 53 | 54 | Shows a message box with "yes" and "no" buttons. The icon is ``'question'`` 55 | by default. Returns ``True`` for "yes" and ``False`` for "no". 56 | 57 | .. function:: yes_no_cancel(title, message, detail=None, **kwargs) 58 | 59 | Shows a message box with "yes", "no" and "cancel" buttons. The icon is 60 | ``'question'`` by default. Returns one of the strings ``'yes'``, ``'no'`` 61 | or ``'cancel'``. 62 | 63 | .. function:: abort_retry_ignore(title, message, detail=None, **kwargs) 64 | 65 | Like :func:`yes_no_cancel`, but with different buttons and return value 66 | strings. The icon is ``'error'`` by default. 67 | 68 | 69 | File and Directory Dialogs 70 | -------------------------- 71 | 72 | Keyword arguments work as usual. Note that paths are returned as strings of 73 | absolute paths, not e.g. :class:`pathlib.Path` objects. 74 | 75 | .. autofunction:: open_file 76 | .. autofunction:: open_multiple_files 77 | .. autofunction:: save_file 78 | .. autofunction:: directory 79 | 80 | 81 | Other Dialogs 82 | ------------- 83 | 84 | .. autofunction:: color 85 | -------------------------------------------------------------------------------- /docs/eventloop.rst: -------------------------------------------------------------------------------- 1 | .. _eventloop: 2 | 3 | Event Loop 4 | ========== 5 | 6 | Tk is event-based. When you click a :class:`~teek.Button`, a click event is 7 | generated, and Tk processes it. Usually that involves making the button look 8 | like it's pressed down, and maybe calling a callback function that you have 9 | told the button to run. 10 | 11 | The **event loop** works essentially like this pseudo code:: 12 | 13 | while True: 14 | handle_an_event() 15 | if there_are_no_more_events_because_we_handled_all_of_them: 16 | wait_for_more_events() 17 | 18 | These functions can be used for working with the event loop: 19 | 20 | .. autofunction:: teek.run 21 | .. autofunction:: teek.quit 22 | 23 | .. data:: teek.before_quit 24 | 25 | :func:`.quit` runs this callback with no arguments before it does anything 26 | else. This means that when this callback runs, widgets have not been 27 | destroyed yet, but they will be destroyed soon. 28 | 29 | .. data:: teek.after_quit 30 | 31 | :func:`.quit` runs this callback when it has done everything else 32 | successfully. 33 | 34 | .. autofunction:: teek.update 35 | -------------------------------------------------------------------------------- /docs/extensions.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import os 3 | import re 4 | import urllib.request # because requests might not be installed 5 | 6 | import docutils.nodes 7 | import docutils.utils 8 | from sphinx.util.nodes import split_explicit_title 9 | 10 | 11 | @functools.lru_cache() 12 | def check_url(url): 13 | # this is kind of slow on my system, so do nothing unless running in 14 | # readthedocs 15 | if os.environ.get('READTHEDOCS', None) != 'True': 16 | return 17 | 18 | print("\ndocs/extensions.py: checking if url exists: %s" % url) 19 | 20 | # this doesn't work with urllib's default User-Agent for some reason 21 | request = urllib.request.Request(url, headers={'User-Agent': 'asd'}) 22 | response = urllib.request.urlopen(request) 23 | assert response.status == 200 24 | assert (b'The page you are looking for cannot be found.' 25 | not in response.read()), url 26 | 27 | 28 | # there are urls like .../man/tcl8.6/... and .../man/tcl/... 29 | # the non-8.6 ones always point to latest version, which is good because no 30 | # need to hard-code version number 31 | MAN_URL_TEMPLATE = 'https://www.tcl.tk/man/tcl/%s%s/%s.htm' 32 | 33 | # because multiple functions are documented in the same man page 34 | # for example, 'man Tk_GetBoolean' and 'man Tk_GetInt' open the same manual 35 | # page on my system 36 | MANPAGE_REDIRECTS = { 37 | 'Tcl_GetBoolean': 'Tcl_GetInt', 38 | 'tk_getSaveFile': 'tk_getOpenFile', 39 | } 40 | 41 | SOURCE_URI_PREFIX = 'https://github.com/Akuli/teek/blob/master/' 42 | 43 | 44 | def get_manpage_url(manpage_name, tcl_or_tk): 45 | manpage_name = MANPAGE_REDIRECTS.get(manpage_name, manpage_name) 46 | 47 | # c functions are named like Tk_GetColor, and the URLs only contain the 48 | # GetColor part for some reason 49 | is_c_function = manpage_name.startswith(tcl_or_tk.capitalize() + '_') 50 | 51 | # ik, this is weird 52 | if manpage_name.startswith('ttk_'): 53 | # ttk_separator --> ttk_separator 54 | name_part = manpage_name 55 | else: 56 | # tk_chooseColor --> chooseColor 57 | # Tk_GetColor --> GetColor 58 | name_part = manpage_name.split('_')[-1] 59 | 60 | return MAN_URL_TEMPLATE % (tcl_or_tk.capitalize(), 61 | 'Lib' if is_c_function else 'Cmd', 62 | name_part) 63 | 64 | 65 | # i don't know how to use sphinx, i copy/pasted things from here: 66 | # https://doughellmann.com/blog/2010/05/09/defining-custom-roles-in-sphinx/ 67 | def man_role(name, rawtext, text, lineno, inliner, options={}, content=[]): 68 | match = re.fullmatch(r'(\w+)\(3(tcl|tk)\)', text) 69 | assert match is not None, "invalid man page %r" % text 70 | manpage_name, tcl_or_tk = match.groups() 71 | 72 | url = get_manpage_url(manpage_name, tcl_or_tk) 73 | check_url(url) 74 | 75 | # this is the copy/pasta part 76 | node = docutils.nodes.reference(rawtext, text, refuri=url, **options) 77 | return [node], [] 78 | 79 | 80 | # this is mostly copy/pasted from cpython's Doc/tools/extensions/pyspecific.py 81 | def source_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): 82 | has_t, title, target = split_explicit_title(text) 83 | title = docutils.utils.unescape(title) 84 | target = docutils.utils.unescape(target) 85 | 86 | url = SOURCE_URI_PREFIX + target 87 | check_url(url) 88 | 89 | refnode = docutils.nodes.reference(title, title, refuri=url) 90 | return [refnode], [] 91 | 92 | 93 | def setup(app): 94 | app.add_role('source', source_role) 95 | app.add_role('man', man_role) 96 | -------------------------------------------------------------------------------- /docs/extras.rst: -------------------------------------------------------------------------------- 1 | Extras 2 | ====== 3 | 4 | .. module:: teek.extras 5 | 6 | Big tkinter projects often have their own implementations of some commonly 7 | needed things that tkinter doesn't come with. The ``teek.extras`` module 8 | contains a collection of them. 9 | 10 | To use the extras, ``import teek`` is not enough; that doesn't import 11 | any extras. This is good because most teek programs don't need the extras, 12 | and for them, ``import teek`` may run a little bit faster if it 13 | doesn't import the extras. Instead, you typically need to do something like 14 | this to use the extras:: 15 | 16 | import teek 17 | from teek.extras import tooltips 18 | 19 | # now some code that uses tooltips.set_tooltip 20 | 21 | 22 | .. module:: teek.extras.tooltips 23 | 24 | tooltips 25 | -------- 26 | 27 | This module contains a simple tooltip implementation with teek. 28 | There is example code in :source:`examples/tooltip.py`. 29 | 30 | If you have read some of IDLE's source code (if you haven't, that's 31 | good; IDLE's source code is ugly), you might be wondering what this 32 | thing has to do with ``idlelib/tooltip.py``. Don't worry, I didn't 33 | copy/paste any code from idlelib and I didn't read idlelib while I 34 | wrote the tooltip code! Idlelib is awful and I don't want to use 35 | anything from it in my projects. 36 | 37 | .. autofunction:: set_tooltip 38 | 39 | 40 | .. module:: teek.extras.cross_platform 41 | 42 | cross_platform 43 | -------------- 44 | 45 | Most teek things work the same on most platforms, but not everything does. 46 | For example, binding ```` works everywhere, but binding ```` 47 | doesn't work on Linux and you need a different binding instead. This extra 48 | module contains utilities for dealing with things like that. 49 | 50 | .. autofunction:: bind_tab_key 51 | 52 | 53 | .. module:: teek.extras.more_dialogs 54 | 55 | more_dialogs 56 | ------------ 57 | 58 | This is useful when :mod:`teek.dialog` is not enough. 59 | 60 | All of the functions take these arguments: 61 | 62 | * ``title`` will be the title of the dialog. 63 | * ``text`` will be displayed in a label above the text entry or spinbox. 64 | * ``initial_value`` will be added to the entry or spinbox before the user 65 | changes it. 66 | * ``parent`` is a window widget that the dialog will be displayed on top of. 67 | 68 | .. autofunction:: ask_string 69 | .. autofunction:: ask_integer 70 | 71 | 72 | .. module:: teek.extras.links 73 | 74 | links 75 | ----- 76 | 77 | With this extra, you can insert web browser style links to 78 | :class:`~teek.Text` widgets. This is based on 79 | `this tutorial `_ that is 80 | almost as old as I am, but it's still usable. 81 | 82 | See :source:`examples/links.py` for example code. 83 | 84 | .. autofunction:: add_url_link 85 | .. autofunction:: add_function_link 86 | 87 | 88 | .. module:: teek.extras.image_loader 89 | 90 | image_loader 91 | ------------ 92 | 93 | .. note:: 94 | This extra has dependencies that don't come with teek when you install it 95 | with ``pip install teek`` or similar, because I want teek to be 96 | light-weight and I don't want to bring in lots of dependencies with it. Run 97 | ``pip install teek[image_loader]`` to install the things you need for using 98 | this extra. 99 | 100 | There's also ``image_loader_dummy``, which is like ``image_loader`` except 101 | that it just creates :meth:`teek.Image` objects, and doesn't support 102 | anything that plain :meth:`teek.Image` doesn't support. It's meant to be 103 | used like this:: 104 | 105 | try: 106 | from teek.extras import image_loader 107 | except ImportError: 108 | from teek.extras import image_loader_dummy as image_loader 109 | 110 | This extra lets you create :class:`teek.Image` objects of images that Tk itself 111 | doesn't support. It uses other Python libraries like 112 | `PIL `_ and 113 | `svglib `_ to do that, and you can just 114 | tell it to load an image and let it use whatever libraries are needed. 115 | 116 | .. autofunction:: from_file 117 | .. autofunction:: from_bytes 118 | .. autofunction:: from_pil 119 | 120 | 121 | .. module:: teek.extras.soup 122 | 123 | soup 124 | ---- 125 | 126 | This extra contains code that views HTML to text widgets. The HTML is taken as 127 | `BeautifulSoup `_ 128 | elements, so you need to have it installed to use this module. Don't get too 129 | excited though -- this is not something that's intended to be used for creating 130 | a web browser or something, because this thing doesn't even support JavaScript 131 | or CSS! It's meant to be used e.g. when you want to display some markup to the 132 | user, and you know a library that can convert it to simple HTML. 133 | 134 | If the HTML contains images that are not GIF images, make sure to install 135 | :mod:`image_loader `. The ``soup`` extra will use it 136 | if it's installed. 137 | 138 | Only these HTML elements are supported by default (but you can subclass 139 | :class:`SoupViewer` and add support for more elements, see 140 | :meth:`SoupViewer.add_soup`): 141 | 142 | * ``

``, ``

``, ``

``, ``

``, ``

`` and ``
`` 143 | * ``
`` and ````
144 | * ``
`` 145 | * ````, ````, ```` and ```` 146 | * ``

`` 147 | * ``

    ``, ``
      `` and ``
    1. `` 148 | * ```` 149 | * ```` 150 | 151 | .. autoclass:: SoupViewer 152 | :members: 153 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Teek documentation master file, created by 2 | sphinx-quickstart on Wed Aug 15 22:15:40 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Teek's documentation! 7 | =================================== 8 | 9 | Teek is a pythonic way to write Tk GUIs in Python. See 10 | `the README `_ for an introduction 11 | to teek, and then click the tutorial link below to get started. 12 | 13 | 14 | Tutorials and Guides 15 | -------------------- 16 | 17 | .. toctree:: 18 | :maxdepth: 1 19 | 20 | Beginner-friendly tutorial (start here) 21 | tkinter 22 | concurrency 23 | 24 | 25 | Reference 26 | --------- 27 | 28 | Use this section when you want to know something about things that were not 29 | covered in the tutorial. 30 | 31 | .. toctree:: 32 | :maxdepth: 1 33 | 34 | geometry-managers 35 | widgets 36 | bind 37 | dialog 38 | misc-objs 39 | platform-info 40 | extras 41 | 42 | 43 | Understanding teek 44 | --------------------- 45 | 46 | Things documented here are useful if you want to know how stuff works or you 47 | want to do some advanced tricks with teek. I recommend reading these things 48 | if you want to help me with developing teek. 49 | 50 | .. toctree:: 51 | :maxdepth: 1 52 | 53 | eventloop 54 | tcl-calls 55 | 56 | .. 57 | Indices and tables 58 | ================== 59 | 60 | * :ref:`genindex` 61 | * :ref:`modindex` 62 | * :ref:`search` 63 | -------------------------------------------------------------------------------- /docs/menu.rst: -------------------------------------------------------------------------------- 1 | .. _menu: 2 | 3 | Menu Widget 4 | =========== 5 | 6 | **Manual page:** :man:`menu(3tk)` 7 | 8 | You can use menu widges for a few different things: 9 | 10 | * **Menu bars** are menus that are typically displayed at the top of a window, 11 | or top of the screen if you are using e.g. Mac OSX. 12 | * **Pop-up menus** open when the user e.g. right-clicks something. 13 | 14 | Here is an example of a menu bar:: 15 | 16 | import teek 17 | 18 | window = teek.Window() 19 | 20 | def hello(): 21 | print("hello") 22 | 23 | window.config['menu'] = teek.Menu([ 24 | teek.MenuItem("File", [ 25 | teek.MenuItem("New", hello), 26 | teek.MenuItem("Open", hello), 27 | teek.MenuItem("Save", hello), 28 | teek.MenuItem("Quit", hello), 29 | ]), 30 | teek.MenuItem("Edit", [ 31 | teek.MenuItem("Cut", hello), 32 | teek.MenuItem("Copy", hello), 33 | teek.MenuItem("Paste", hello), 34 | ]), 35 | ]) 36 | 37 | window.geometry(300, 200) 38 | window.on_delete_window.connect(teek.quit) 39 | teek.run() 40 | 41 | As you can see, :class:`.Menu` takes one argument, which is a list of 42 | :class:`.MenuItem` objects. This example uses two kinds of menu items; some 43 | menu items just call the ``hello()`` function when they are clicked, while the 44 | "File" and "Edit" items display submenus. There are more details about 45 | different kinds of items :ref:`below `. 46 | 47 | Here is a pop-up menu example. See :ref:`bind documentation ` for more 48 | details about the binding stuff. 49 | :: 50 | 51 | import teek 52 | 53 | window = teek.Window() 54 | 55 | def hello(): 56 | print("hello") 57 | 58 | menu = teek.Menu([ 59 | teek.MenuItem("Cut", hello), 60 | teek.MenuItem("Copy", hello), 61 | teek.MenuItem("Paste", hello), 62 | ]) 63 | 64 | def on_right_click(event): 65 | menu.popup(event.rootx, event.rooty) 66 | 67 | if teek.windowingsystem() == 'aqua': 68 | # running on Mac OSX, there's no right-click so this must be done a bit 69 | # differently 70 | window.bind('', on_right_click, event=True) 71 | window.bind('', on_right_click, event=True) 72 | else: 73 | window.bind('', on_right_click, event=True) 74 | 75 | window.geometry(300, 200) 76 | window.on_delete_window.connect(teek.quit) 77 | teek.run() 78 | 79 | I found the Mac OSX specific code from here_. 80 | 81 | .. _here: https://books.google.fi/books?id=BWf6mdwHjDMC&pg=PT392&lpg=PT392&dq=tk_popup+vs+post&source=bl&ots=n32EYTF27b&sig=V0xXtCqHmKKa37BRNCdPk6unhr4&hl=fi&sa=X&ved=2ahUKEwikwa7A0pzdAhVKCCwKHSeNCa4Q6AEwBHoECAYQAQ#v=onepage&q=tk_popup%20vs%20post&f=false 82 | 83 | Menu widgets are *not* Ttk widgets. If you don't know what that means, you 84 | should go :ref:`here ` and learn. The only practical 85 | thing I can think of right now is that menus don't have a 86 | :attr:`~.Widget.state` attribute. 87 | 88 | 89 | .. _creating-menu-items: 90 | 91 | Creating Menu Items 92 | ~~~~~~~~~~~~~~~~~~~ 93 | 94 | There are a few different ways to create instances of :class:`.MenuItem`. Here 95 | ``label`` must be a string. 96 | 97 | .. TODO: create a Radiobutton widget 98 | 99 | * ``MenuItem()`` creates a separator. 100 | * ``MenuItem(label, function)`` creates a ``command`` item that runs 101 | ``function()`` when it's clicked. See also :class:`.Button`. 102 | * ``MenuItem(label, checked_var)`` creates a ``checkbutton`` menu item. The 103 | ``checked_var`` must be a :class:`.BooleanVar`. See also 104 | :class:`.Checkbutton`. 105 | * ``MenuItem(label, string_var, value)`` creates a ``radiobutton`` menu item. 106 | Use this for letting the user choose one of multiple options. Clicking the 107 | item sets ``value`` to ``string_var``. The ``string_var`` must be a 108 | :class:`.StringVar` object, and ``value`` must be a string. 109 | * ``MenuItem(label, menu)`` creates a ``cascade`` menu item; that is, it 110 | displays a submenu with the items of ``menu`` in it. The ``menu`` must be a 111 | :class:`.Menu` widget. 112 | * ``MenuItem(label, item_list)`` is a handy way to create a new :class:`.Menu` 113 | and add it as a ``cascade`` item as explained above. 114 | 115 | You can also pass options as keyword arguments in any of the above forms. The 116 | available options are documented as ``MENU ENTRY OPTIONS`` in :man:`menu(3tk)`. 117 | For example, instead of this... 118 | :: 119 | 120 | MenuItem("Copy", do_the_copy) 121 | 122 | ...you probably want to do something like this:: 123 | 124 | MenuItem("Copy", do_the_copy, accelerator='Ctrl+C') 125 | 126 | Note that this does not :ref:`bind ` anything automatically, so you 127 | need to do that yourself if want that Ctrl+C actually does something. 128 | 129 | Here is an example that demonstrates most things. See :class:`.StringVar` and 130 | :class:`.BooleanVar` documentation for more info about them. 131 | :: 132 | 133 | import teek 134 | 135 | 136 | def on_click(): 137 | print("clicked") 138 | 139 | def on_check(is_checked): 140 | print("is it checked now?", is_checked) 141 | 142 | def on_choice(choice): 143 | print("chose", repr(choice)) 144 | 145 | 146 | window = teek.Window() 147 | 148 | submenu = teek.Menu([ 149 | teek.MenuItem("Asd", on_click), 150 | teek.MenuItem("Toot", on_click), 151 | ]) 152 | 153 | check_var = teek.BooleanVar() 154 | check_var.write_trace.connect(on_check) 155 | choice_var = teek.StringVar() 156 | choice_var.write_trace.connect(on_choice) 157 | 158 | window.config['menu'] = teek.Menu([ 159 | teek.MenuItem("Stuff", [ 160 | teek.MenuItem("Click me", on_click), 161 | teek.MenuItem("Check me", check_var), 162 | teek.MenuItem("More stuff", submenu), 163 | teek.MenuItem(), # separator 164 | teek.MenuItem("Choice 1", choice_var, "one"), 165 | teek.MenuItem("Choice 2", choice_var, "two"), 166 | teek.MenuItem("Choice 3", choice_var, "three"), 167 | ]), 168 | ]) 169 | 170 | window.geometry(300, 200) 171 | window.on_delete_window.connect(teek.quit) 172 | teek.run() 173 | 174 | 175 | Reference 176 | ~~~~~~~~~ 177 | 178 | .. explicit member list for same reason as with Notebook 179 | 180 | .. autoclass:: teek.Menu 181 | :members: popup 182 | 183 | .. autoclass:: teek.MenuItem 184 | :members: 185 | -------------------------------------------------------------------------------- /docs/misc-objs.rst: -------------------------------------------------------------------------------- 1 | Miscellaneous Classes 2 | ===================== 3 | 4 | This page contains documentation of classes that represent different kinds of 5 | things. 6 | 7 | 8 | Colors 9 | ------ 10 | 11 | .. autoclass:: teek.Color 12 | :members: 13 | 14 | 15 | Callbacks 16 | --------- 17 | 18 | .. autoclass:: teek.Callback 19 | :members: 20 | 21 | 22 | .. _font-objs: 23 | 24 | Fonts 25 | ----- 26 | 27 | There are different kinds of fonts in Tk: 28 | 29 | * **Named fonts** are mutable; if you set the font of a widget to a named font 30 | and you then change that named font (e.g. make it bigger), the widget's font 31 | will change as well. 32 | * **Anonymous fonts** don't work like that, but they are handy if you don't 33 | want to create a font object just to set the font of a widget. 34 | 35 | For example, if you have :class:`.Label`... 36 | 37 | >>> window = teek.Window() 38 | >>> label = teek.Label(window, "Hello World") 39 | >>> label.pack() 40 | 41 | ...and you want to make its text bigger, you can do this:: 42 | 43 | >>> label.config['font'] = ('Helvetica', 20) 44 | 45 | This form is the teek equivalent of the alternative ``[3]`` in the 46 | ``FONT DESCRIPTIONS`` part of :man:`font(3tk)`. All of the other font 47 | descriptions work as well. 48 | 49 | If you then get the font of the label, you get a :class:`teek.Font` object: 50 | 51 | >>> label.config['font'] 52 | Font('Helvetica 20') 53 | 54 | With a named font, the code looks like this: 55 | 56 | >>> named_font = teek.NamedFont(family='Helvetica', size=20) 57 | >>> label.config['font'] = named_font 58 | >>> named_font.size = 50 # even bigger! label will use this new size automatically 59 | 60 | Of course, :class:`.Font` and :class:`.NamedFont` objects can also be set to 61 | ``label.config['font']``. 62 | 63 | .. autoclass:: teek.Font 64 | :members: 65 | 66 | .. autoclass:: teek.NamedFont 67 | :members: 68 | 69 | 70 | Tcl Variable Objects 71 | -------------------- 72 | 73 | .. autoclass:: teek.TclVariable 74 | :members: 75 | 76 | .. class:: teek.StringVar 77 | teek.IntVar 78 | teek.FloatVar 79 | teek.BooleanVar 80 | 81 | Handy :class:`.TclVariable` subclasses for variables with :class:`str`, 82 | :class:`int`, :class:`float` and :class:`bool` values, respectively. 83 | 84 | 85 | Images 86 | ------ 87 | 88 | .. autoclass:: teek.Image 89 | :members: 90 | 91 | 92 | Screen Distance Objects 93 | ----------------------- 94 | 95 | .. autoclass:: teek.ScreenDistance 96 | :members: 97 | -------------------------------------------------------------------------------- /docs/notebook.rst: -------------------------------------------------------------------------------- 1 | .. _notebook: 2 | 3 | Notebook Widget 4 | =============== 5 | 6 | **Manual page:** :man:`ttk_notebook(3tk)` 7 | 8 | This widget is useful for creating tabs as in the tabs of your web browser, not 9 | ``\t`` characters. Let's look at an example. 10 | 11 | :: 12 | 13 | import teek 14 | 15 | window = teek.Window("Notebook Example") 16 | notebook = teek.Notebook(window) 17 | notebook.pack(fill='both', expand=True) 18 | 19 | for number in [1, 2, 3]: 20 | label = teek.Label(notebook, "Hello {}!".format(number)) 21 | tab = teek.NotebookTab(label, text="Tab {}".format(number)) 22 | notebook.append(tab) 23 | 24 | window.geometry(300, 200) 25 | window.on_delete_window.connect(teek.quit) 26 | teek.run() 27 | 28 | This program displays a notebook widget with 3 tabs. Let's go trough some of 29 | the code. 30 | 31 | :: 32 | 33 | label = teek.Label(notebook, "Hello {}!".format(number)) 34 | 35 | The label is created as a child widget of the notebook, because it will be 36 | added to the notebook eventually. However, we *don't* use a 37 | :ref:`geometry manager ` because the notebook itself handles 38 | showing the widget. 39 | 40 | :: 41 | 42 | tab = teek.NotebookTab(label, text="Tab {}".format(number)) 43 | 44 | We need to create a :class:`.NotebookTab` object in order to add a tab to the 45 | notebook. The :class:`.NotebookTab` objects themselves are **not** widgets. A 46 | :class:`.NotebookTab` represents a widget and its tab options like ``text``. 47 | 48 | :: 49 | 50 | notebook.append(tab) 51 | 52 | Lists also have an append method, and this is no coincidence. 53 | :class:`.Notebook` widgets behave like lists, and if you can do something to a 54 | list, you can probably do it to a notebook as well. However, there are a few 55 | things you can't do to notebook widgets, but you can do to lists: 56 | 57 | * You can't put any objects you want into a :class:`.Notebook`. All the objects 58 | must be :class:`NotebookTabs <.NotebookTab>`. 59 | * You can't slice notebooks like ``notebook[1:]``. However, you can get a list 60 | of all tabs in the notebook with ``list(notebook)`` and then slice that. 61 | * You can't sort notebooks like ``notebook.sort()``. However, you can do 62 | ``sorted(notebook)`` to get a sorted list of 63 | :class:`NotebookTabs <.NotebookTab>`, except that it doesn't quite work 64 | because tab objects can't be compared with each other. Something like 65 | ``sorted(notebook, key=(lambda tab: tab.config['text']))`` might be useful 66 | though. 67 | * You can't add the same :class:`.NotebookTab` to the notebook twice, and in 68 | fact, you can't create two :class:`NotebookTabs <.NotebookTab>` that 69 | represent the same widget, so you can't add the same widget to the notebook 70 | as two different tabs. 71 | 72 | Note that instead of this... 73 | 74 | :: 75 | 76 | label = teek.Label(notebook, "Hello {}!".format(number)) 77 | tab = teek.NotebookTab(label, text="Tab {}".format(number)) 78 | notebook.append(tab) 79 | 80 | ...you can also do this:: 81 | 82 | notebook.append( 83 | teek.NotebookTab( 84 | teek.Label(notebook, "Hello {}!".format(number)), 85 | text="Tab {}".format(number) 86 | ) 87 | ) 88 | 89 | I recommend using common sense here. The first alternative is actually only 90 | half as many lines of code as the second one, even though it uses more 91 | variables. Try to keep the code readable, as usual. 92 | 93 | Here is some reference: 94 | 95 | .. autoclass:: teek.Notebook 96 | :members: 97 | .. autoclass:: teek.NotebookTab 98 | :members: 99 | -------------------------------------------------------------------------------- /docs/platform-info.rst: -------------------------------------------------------------------------------- 1 | Platform Information 2 | ==================== 3 | 4 | This page documents things that can tell you which platform your program is 5 | running on. 6 | 7 | .. data:: teek.TCL_VERSION 8 | teek.TK_VERSION 9 | 10 | These can be used for checking the versions of the Tcl interpreter and its 11 | Tk library that teek is using. These are two-tuples of integers, and you 12 | can compare integer tuples nicely, so you can do e.g. this:: 13 | 14 | if teek.TK_VERSION >= (8, 6): 15 | # use a feature new in Tk 8.6 16 | else: 17 | # show an error message or do things without the new feature 18 | 19 | Teek refuses to run if Tcl or Tk is older than 8.5, so you can use all 20 | features new in Tcl/Tk 8.5 freely. 21 | 22 | .. note:: 23 | The manual page links in this tutorial like :man:`label(3tk)` always 24 | point to the latest manual pages, which are for Tcl/Tk 8.6 at the time 25 | of writing this. 26 | 27 | .. autofunction:: teek.windowingsystem 28 | 29 | This function returns ``'win32'``, ``'aqua'`` or ``'x11'``. Use it instead 30 | of :func:`platform.system` when you have platform-specific teek code. 31 | For example, it's possible to run X11 on a Mac, in which case 32 | :func:`platform.system` returns ``'Darwin'`` and this function returns 33 | ``'x11'``. If you have code that should even then behave like it would 34 | normally behave on a Mac, use :func:`platform.system`. 35 | 36 | The Tk documentation for this function is ``tk windowingsystem`` in 37 | :man:`tk(3tk)`. 38 | -------------------------------------------------------------------------------- /docs/tcl-calls.rst: -------------------------------------------------------------------------------- 1 | .. _tcl-calls: 2 | 3 | Tcl Calls 4 | ========= 5 | 6 | .. note:: 7 | This section assumes that you know Tcl. You may have learned some of it 8 | while using teek, but something like 9 | `Learn Tcl in Y Minutes `_ might be 10 | useful for you. 11 | 12 | Teek does most things by calling commands in Tcl. You can also call Tcl 13 | commands yourself, which is useful if you want to do something that can be done 14 | in Tcl, but there is no other way to do it in teek. 15 | 16 | There are two functions for doing this: 17 | 18 | .. autofunction:: teek.tcl_eval 19 | .. autofunction:: teek.tcl_call 20 | 21 | Both of these functions are ran so that they have access to Tcl's global 22 | variables, and if they create more variables, they will also be global. 23 | 24 | The ``None`` means that the return value is ignored, and ``int`` means that 25 | it's converted to a Python integer. There are more details about these 26 | conversions below. 27 | 28 | Tcl errors always raise the same Python exception: 29 | 30 | .. autoexception:: teek.TclError 31 | 32 | 33 | Data Types 34 | ---------- 35 | 36 | Everything is a string in Tcl. Teek converts Python objects to strings and 37 | strings to Python objects for you, but you need to tell teek what types of 38 | values you want to get. This section describes how. 39 | 40 | 41 | .. _to-tcl: 42 | 43 | Python to Tcl conversion 44 | ~~~~~~~~~~~~~~~~~~~~~~~~ 45 | 46 | Arguments passed to :func:`.tcl_call` are handled like this: 47 | 48 | * Strings are passed to Tcl as is. 49 | * If the argument is None, an empty string is passed to Tcl because Tcl uses an 50 | empty string in many places where Python uses None. 51 | * If the argument is a dictionary-like object (more precisely, 52 | :class:`collections.abc.Mapping`), it is turned into a list of pairs. This is 53 | because ``{'a': 'b', 'c': 'd'}`` and ``['a', 'b', 'c', 'd']`` are represented 54 | the same way in Tcl. 55 | * ``True`` and ``False`` are converted to ``1`` and ``0``, respectively. 56 | * Integers, floats and other real numbers (:class:`numbers.Real`) are converted 57 | to strings with ``str()``. 58 | * If the value has a ``to_tcl()`` method, it's called with no arguments. It 59 | should return a string that will be passed to Tcl. 60 | * Anything else is treated as an iterable. Every element of the iterable is 61 | converted as described here, and the result is a Tcl list. 62 | 63 | Conversions should raise :class:`ValueError` or :class:`.TclError` when they 64 | fail. 65 | 66 | 67 | .. _type-spec: 68 | 69 | Type Specifications 70 | ~~~~~~~~~~~~~~~~~~~ 71 | 72 | Teek also has **type specifications** for converting from a Tcl object to a 73 | Python object. Here is a list of valid type specifications: 74 | 75 | * ``str`` (that is, literally ``str``, not e.g. ``'hello'``) means that a 76 | string is returned. 77 | * ``None`` means that the value will be ignored entirely, and the Python value 78 | is always None. 79 | * ``bool`` means that the value is treated as a Tcl boolean. All valid Tcl 80 | booleans specified in :man:`Tcl_GetBoolean(3tcl)` are supported. 81 | * ``int``, ``float`` or any other subclass of :class:`numbers.Real` means that 82 | the value will be converted to that class by first converting to string as if 83 | ``str`` was used, and then calling the class with the stringed value as an 84 | argument. However, if the stringed value is the empty string, None is 85 | returned and the class isn't called. 86 | * If the type specification is a class with a ``from_tcl()`` classmethod, that 87 | will be called with one argument, the value converted to a string. If the 88 | stringed value is an empty string, None is returned and ``from_tcl()`` is not 89 | called. 90 | 91 | The type specifications can be also combined in the following ways. These 92 | examples use ``str``, ``int`` and ``float``, but all other valid specifications 93 | work as well. The return types can be nested arbitrarily; for example, 94 | ``[(int, float)]`` means a value like ``[(12, 3.4), (56, 7.8)]``. 95 | 96 | * ``[str]`` means a list of strings, of any length. 97 | * ``(str, int)`` means a tuple of a string followed by an integer. This allows 98 | you to create a sequence with different kinds of items in it. For 99 | example, ``(str, str, str)`` is like ``[str]`` except that it also 100 | makes sure that the length of the result is 3, and returns a tuple instead of 101 | a list. 102 | * ``{'a': int, 'b': float}`` means a dictionary with string keys. If the Tcl 103 | dictionary happens to have a key named ``a`` or ``b``, it is converted to 104 | ``int`` or ``float`` respectively; other keys will be strings. This means 105 | that ``{}`` is a dictionary with all keys as strings and values as integers. 106 | There is no way to work with dictionaries that have non-string keys. 107 | 108 | Examples: 109 | 110 | >>> teek.tcl_call([str], 'list', 'a', 'b', 'c') 111 | ['a', 'b', 'c'] 112 | >>> teek.tcl_call((str, int, float), 'list', 'hello', '3', '3.14') 113 | ('hello', 3, 3.14) 114 | >>> teek.tcl_call([bool], 'list', 'yes', 'ye', 'true', 't', 'on', '1') 115 | [True, True, True, True, True, True] 116 | >>> teek.tcl_call({}, 'dict', 'create', 'a', 1, 'b', 2) # doctest: +SKIP 117 | {'a': '1', 'b': '2'} 118 | >>> teek.tcl_call([str], 'list', 123, 3.14, None, 'hello') 119 | ['123', '3.14', '', 'hello'] 120 | 121 | 122 | Creating Tcl Commands 123 | ~~~~~~~~~~~~~~~~~~~~~ 124 | 125 | It's possible to create Tcl commands that Tcl code can call. For example, when 126 | a button is clicked, Tcl invokes a command that the :class:`.Button` class 127 | created with :func:`.create_command`. 128 | 129 | .. autofunction:: teek.create_command 130 | .. autofunction:: teek.delete_command 131 | -------------------------------------------------------------------------------- /docs/widgets.rst: -------------------------------------------------------------------------------- 1 | .. _widgets: 2 | 3 | Widget Reference 4 | ================ 5 | 6 | This is a big list of widgets and explanations of what they do. As usual, don't 7 | try to read all of it, unless you're very bored; it's best to find what you're 8 | looking for, and ignore rest of this. 9 | 10 | .. toctree:: 11 | :hidden: 12 | 13 | canvas 14 | menu 15 | notebook 16 | textwidget 17 | 18 | .. autoclass:: teek.Widget 19 | :members: from_tcl, to_tcl, destroy, focus, winfo_exists, winfo_children, winfo_toplevel, winfo_width, winfo_height, winfo_reqwidth, winfo_reqheight, winfo_x, winfo_y, winfo_rootx, winfo_rooty, winfo_id 20 | 21 | .. autoclass:: teek.Button 22 | :members: 23 | 24 | .. class:: teek.Canvas 25 | :noindex: 26 | 27 | See :ref:`canvaswidget`. 28 | 29 | .. autoclass:: teek.Checkbutton 30 | :members: 31 | .. autoclass:: teek.Combobox 32 | :members: 33 | .. autoclass:: teek.Entry 34 | :members: 35 | .. autoclass:: teek.Frame 36 | :members: 37 | .. autoclass:: teek.Label 38 | :members: 39 | .. autoclass:: teek.LabelFrame 40 | :members: 41 | 42 | .. class:: teek.Menu 43 | :noindex: 44 | 45 | See :ref:`menu`. 46 | 47 | .. class:: teek.Notebook 48 | :noindex: 49 | 50 | See :ref:`notebook`. 51 | 52 | .. autoclass:: teek.Progressbar 53 | :members: 54 | .. autoclass:: teek.Scrollbar 55 | :members: 56 | .. autoclass:: teek.Separator 57 | :members: 58 | .. autoclass:: teek.Spinbox 59 | :members: 60 | 61 | .. class:: teek.Text 62 | :noindex: 63 | 64 | See :ref:`textwidget`. 65 | 66 | .. autoclass:: teek.Toplevel 67 | :members: 68 | .. autoclass:: teek.Window 69 | :members: 70 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Teek Examples 2 | 3 | This directory contains small example programs that use teek. 4 | 5 | If you haven't installed teek, you need to run these programs with 6 | Python's `-m` option: 7 | 8 | ``` 9 | bla/bla/teek/examples$ python3 hello_world.py 10 | Traceback (most recent call last): 11 | ... 12 | ImportError: No module named 'teek' 13 | bla/bla/teek/examples$ cd .. 14 | bla/bla/teek$ python3 -m examples.hello_world # this works 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akuli/teek/c360fbfe086ca09cdd856a8636de05b24e1b7093/examples/__init__.py -------------------------------------------------------------------------------- /examples/__main__.py: -------------------------------------------------------------------------------- 1 | import pkgutil 2 | import subprocess 3 | import sys 4 | 5 | import examples 6 | 7 | 8 | for junk, name, junk2 in pkgutil.iter_modules(examples.__path__, 'examples.'): 9 | if name != 'examples.__main__': 10 | if subprocess.call([sys.executable, '-m', name]) != 0: 11 | break 12 | -------------------------------------------------------------------------------- /examples/button.py: -------------------------------------------------------------------------------- 1 | import teek 2 | 3 | 4 | def on_click(): 5 | print("You clicked me!") 6 | 7 | 8 | window = teek.Window() 9 | button = teek.Button(window, "Click me", command=on_click) 10 | button.pack() 11 | window.on_delete_window.connect(teek.quit) 12 | teek.run() 13 | -------------------------------------------------------------------------------- /examples/checkbutton.py: -------------------------------------------------------------------------------- 1 | import teek 2 | 3 | 4 | def on_check_or_uncheck(checked): 5 | if checked: 6 | print("Checked") 7 | else: 8 | print("Unchecked") 9 | 10 | 11 | window = teek.Window("Checkbutton Example") 12 | teek.Checkbutton(window, "Check me", on_check_or_uncheck).pack() 13 | window.on_delete_window.connect(teek.quit) 14 | teek.run() 15 | -------------------------------------------------------------------------------- /examples/hello_world.py: -------------------------------------------------------------------------------- 1 | import teek 2 | 3 | 4 | window = teek.Window("Hello") 5 | label = teek.Label(window, "Hello World!") 6 | label.pack() 7 | window.on_delete_window.connect(teek.quit) 8 | teek.run() 9 | -------------------------------------------------------------------------------- /examples/image.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import teek 4 | 5 | 6 | IMAGE_PATH = os.path.join( 7 | os.path.dirname(__file__), '..', 'tests', 'data', 'smiley.gif') 8 | 9 | window = teek.Window() 10 | label = teek.Label(window, image=teek.Image(file=IMAGE_PATH), 11 | text="Here's a smiley:", compound="bottom") 12 | label.pack() 13 | window.on_delete_window.connect(teek.quit) 14 | teek.run() 15 | -------------------------------------------------------------------------------- /examples/links.py: -------------------------------------------------------------------------------- 1 | import teek 2 | from teek.extras import links 3 | 4 | 5 | def lol(): 6 | teek.dialog.info("Link Example Dialog", "Lol") 7 | 8 | 9 | window = teek.Window("Link Example") 10 | text = teek.Text(window) 11 | text.pack() 12 | 13 | old_end = text.end 14 | text.insert(text.end, "Docs") 15 | links.add_url_link(text, 'https://teek.rtfd.io/', old_end, text.end) 16 | text.insert(text.end, '\n\n') 17 | 18 | old_end = text.end 19 | text.insert(text.end, "GitHub") 20 | links.add_url_link(text, 'https://github.com/Akuli/teek', old_end, text.end) 21 | text.insert(text.end, '\n\n') 22 | 23 | old_end = text.end 24 | text.insert(text.end, "Lol") 25 | links.add_function_link(text, lol, old_end, text.end) 26 | text.insert(text.end, '\n\n') 27 | 28 | window.on_delete_window.connect(teek.quit) 29 | teek.run() 30 | -------------------------------------------------------------------------------- /examples/paint.py: -------------------------------------------------------------------------------- 1 | import teek 2 | 3 | 4 | class Paint: 5 | 6 | def __init__(self, window): 7 | self.canvas = teek.Canvas(window, bg='white') 8 | self.canvas.bind('', self.begin_draw, event=True) 9 | self.canvas.bind('', self.do_draw, event=True) 10 | self.canvas.bind('', self.end_draw) 11 | self._previous_mouse_xy = None 12 | 13 | def begin_draw(self, event): 14 | self._previous_mouse_xy = (event.x, event.y) 15 | 16 | def do_draw(self, event): 17 | self.canvas.create_line( 18 | self._previous_mouse_xy[0], self._previous_mouse_xy[1], 19 | event.x, event.y) 20 | self._previous_mouse_xy = (event.x, event.y) 21 | 22 | def end_draw(self): 23 | self._previous_mouse_xy = None 24 | 25 | 26 | window = teek.Window() 27 | 28 | paint = Paint(window) 29 | paint.canvas.pack() 30 | 31 | window.on_delete_window.connect(teek.quit) 32 | teek.run() 33 | -------------------------------------------------------------------------------- /examples/separator.py: -------------------------------------------------------------------------------- 1 | import teek 2 | 3 | 4 | window = teek.Window() 5 | 6 | teek.Label(window, "asd asd").pack() 7 | teek.Separator(window).pack(fill='x') 8 | teek.Label(window, "moar asd").pack() 9 | 10 | window.on_delete_window.connect(teek.quit) 11 | teek.run() 12 | -------------------------------------------------------------------------------- /examples/soup.py: -------------------------------------------------------------------------------- 1 | import bs4 2 | 3 | import teek 4 | from teek.extras.soup import SoupViewer 5 | 6 | 7 | html_string = """ 8 |

      Hello World!

      9 | 10 |

      11 | This is some HTML text. 12 | Bold, italics and stuff are supported. 13 | This is a link. 14 |

      15 | 16 |
      print("this is some code")
      17 | 18 |

      Text in <code> tags displays with monospace font.
      19 | It's easy to see the difference with the letter i:

      20 | 21 |

      Without a code tag: iiiiiiiiii
      22 | With a code tag: iiiiiiiiii 23 |

      24 | 25 |

      This is teek's travis badge: 26 | 27 | Build Status 29 |

      30 | 31 |

      iibb

      32 | """ 33 | 34 | teek.init_threads() # image loading uses threads 35 | 36 | window = teek.Window() 37 | text = teek.Text(window, font=('', 11, '')) 38 | text.pack(fill='both', expand=True) 39 | 40 | viewer = SoupViewer(text) 41 | viewer.create_tags() 42 | for element in bs4.BeautifulSoup(html_string, 'lxml').body: 43 | viewer.add_soup(element) 44 | 45 | window.on_delete_window.connect(teek.quit) 46 | teek.run() 47 | 48 | viewer.stop_loading(cleanup=True) 49 | -------------------------------------------------------------------------------- /examples/text.py: -------------------------------------------------------------------------------- 1 | import teek 2 | 3 | 4 | window = teek.Window("Text Widget Demo") 5 | 6 | text = teek.Text(window) 7 | text.pack(fill='both', expand=True) 8 | text.insert(text.start, "hello world") 9 | 10 | hello_tag = text.get_tag('hello_tag') 11 | hello_tag['foreground'] = teek.Color('red') 12 | hello_tag.add(text.start, text.start.forward(chars=5)) 13 | 14 | world_tag = text.get_tag('world_tag') 15 | world_tag['foreground'] = teek.Color('green') 16 | world_tag.add(text.end.back(chars=5), text.end) 17 | 18 | # move cursor after hello 19 | text.marks['insert'] = text.start.forward(chars=5) 20 | 21 | window.on_delete_window.connect(teek.quit) 22 | teek.run() 23 | -------------------------------------------------------------------------------- /examples/timeout.py: -------------------------------------------------------------------------------- 1 | import teek 2 | 3 | 4 | class TimeoutDemo(teek.Frame): 5 | 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | self.timeout = None 9 | 10 | startbutton = teek.Button(self, "Start", command=self.start) 11 | startbutton.pack() 12 | cancelbutton = teek.Button(self, "Cancel", command=self.cancel) 13 | cancelbutton.pack() 14 | 15 | def start(self): 16 | if self.timeout is None: 17 | self.timeout = teek.after(3000, self.callback) 18 | print("running callback after 3 seconds") 19 | else: 20 | print("already started") 21 | 22 | def cancel(self): 23 | if self.timeout is None: 24 | print("already cancelled") 25 | else: 26 | self.timeout.cancel() 27 | self.timeout = None 28 | print("callback won't be ran") 29 | 30 | def callback(self): 31 | print("*** running the callback ***") 32 | self.timeout = None 33 | 34 | 35 | window = teek.Window() 36 | TimeoutDemo(window).pack() 37 | window.on_delete_window.connect(teek.quit) 38 | teek.run() 39 | -------------------------------------------------------------------------------- /examples/tooltip.py: -------------------------------------------------------------------------------- 1 | import teek 2 | from teek.extras import tooltips 3 | 4 | window = teek.Window("Tooltip Example") 5 | label = teek.Label(window, "I have a tooltip") 6 | label.pack() 7 | tooltips.set_tooltip(label, "This is the tooltip") 8 | 9 | window.on_delete_window.connect(teek.quit) 10 | teek.run() 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit"] 3 | build-backend = "flip.buildapi" 4 | 5 | [tool.flit.metadata] 6 | module = "teek" 7 | author = "Akuli" 8 | author-email = "akuviljanen17@gmail.com" 9 | home-page = "https://github.com/Akuli/teek" 10 | description-file = "README.md" 11 | requires-python = ">=3.5" 12 | keywords = "pythonic tk tcl tkinter gui beginner" 13 | 14 | [tool.flit.metadata.requires-extra] 15 | # lxml is in soup_viewer because examples/soup.py does BeautifulSoup(string, 'lxml') 16 | # exact svglib version because: https://github.com/Akuli/teek/issues/13 17 | image_loader = ["pillow", "reportlab", "svglib==0.9.0", "lxml"] 18 | soup_viewer = ["beautifulsoup4", "lxml"] 19 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --doctest-modules --doctest-glob='*.rst' --capture=no 3 | testpaths = tests/ teek/ docs/ 4 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | build: 2 | image: latest 3 | 4 | python: 5 | version: 3.6 6 | -------------------------------------------------------------------------------- /teek/__init__.py: -------------------------------------------------------------------------------- 1 | """Teek is a pythonic alternative to tkinter.""" 2 | # flake8: noqa 3 | 4 | import os as _os 5 | import sys as _sys 6 | 7 | __version__ = '0.6' 8 | 9 | if _os.environ.get('READTHEDOCS', None) == 'True': # pragma: no cover 10 | # readthedocs must be able to import everything without _tkinter 11 | import types 12 | _sys.modules['_tkinter'] = types.SimpleNamespace( 13 | TclError=None, 14 | TCL_VERSION='8.6', 15 | TK_VERSION='8.6', 16 | ) 17 | else: # pragma: no cover 18 | # python 3.4's tkinter does this BEFORE importing _tkinter 19 | if _sys.platform.startswith("win32") and _sys.version_info < (3, 5): 20 | import tkinter._fix 21 | 22 | 23 | # not to be confused with _tkinter's TclError, this is defined here because 24 | # this way error messages say teek.TclError instead of 25 | # teek._something.TclError, or worse yet, _tkinter.TclError 26 | class TclError(Exception): 27 | """This is raised when a Tcl command fails.""" 28 | 29 | 30 | # _platform_info does a version check, it must be first 31 | from teek._platform_info import TCL_VERSION, TK_VERSION, windowingsystem 32 | from teek._font import Font, NamedFont 33 | from teek._structures import ( 34 | Callback, Color, Image, ScreenDistance, TclVariable, StringVar, IntVar, 35 | FloatVar, BooleanVar, before_quit, after_quit) 36 | from teek._tcl_calls import ( 37 | tcl_call, tcl_eval, create_command, delete_command, run, quit, update, 38 | init_threads, make_thread_safe) 39 | from teek._timeouts import after, after_idle 40 | from teek._widgets.base import Widget 41 | from teek._widgets.canvas import Canvas 42 | from teek._widgets.menu import Menu, MenuItem 43 | from teek._widgets.misc import ( 44 | Button, Checkbutton, Combobox, Entry, Frame, Label, LabelFrame, 45 | Progressbar, Scrollbar, Separator, Spinbox) 46 | from teek._widgets.notebook import Notebook, NotebookTab 47 | from teek._widgets.text import Text 48 | from teek._widgets.windows import Window, Toplevel 49 | from teek import dialog, extras 50 | -------------------------------------------------------------------------------- /teek/_font.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | import teek 4 | from teek._tcl_calls import to_tcl 5 | 6 | flatten = itertools.chain.from_iterable 7 | 8 | 9 | def _font_property(type_spec, option): 10 | 11 | def getter(self): 12 | return teek.tcl_call(type_spec, "font", "actual", self, "-" + option) 13 | 14 | def setter(self, value): 15 | if not isinstance(self, NamedFont): 16 | raise AttributeError( 17 | "cannot change options of non-named fonts, but you can use " 18 | "the_font.to_named_font() to create a mutable font object") 19 | teek.tcl_call(None, "font", "configure", self, "-" + option, value) 20 | 21 | return property(getter, setter) 22 | 23 | 24 | def _anonymous_font_new_helper(font_description): 25 | # magic: Font(a_font_name) returns a NamedFont 26 | # is the font description a font name? configure works only with 27 | # font names, not other kinds of font descriptions 28 | try: 29 | teek.tcl_call(None, 'font', 'configure', font_description) 30 | except teek.TclError: 31 | return None 32 | 33 | # it is a font name 34 | return NamedFont(font_description) 35 | 36 | 37 | class Font: 38 | """Represents an anonymous font. 39 | 40 | Creating a :class:`.Font` object with a valid font name as an argument 41 | returns a :class:`.NamedFont` object. For example: 42 | 43 | >>> teek.Font('Helvetica 12') # not a font name 44 | Font('Helvetica 12') 45 | >>> teek.Font('TkFixedFont') # special font name for default monospace \ 46 | font 47 | NamedFont('TkFixedFont') 48 | >>> teek.NamedFont('TkFixedFont') # does the same thing 49 | NamedFont('TkFixedFont') 50 | 51 | .. attribute:: family 52 | size 53 | weight 54 | slant 55 | underline 56 | overstrike 57 | 58 | See :man:`font(3tk)` for a description of each attribute. ``size`` is 59 | an integer, ``underline`` and ``overstrike`` are bools, and other 60 | attributes are strings. You can set values to these attributes only 61 | with :class:`.NamedFont`. 62 | 63 | The values of these attributes are looked up with ``font actual`` in 64 | :man:`font(3tk)`, so they might differ from the values passed to 65 | ``Font()``. For example, the ``'Helvetica'`` family can meany any 66 | Helvetica-like font, so this line of code gives different values 67 | platform-specifically: 68 | 69 | >>> teek.Font('Helvetica 12').family # doctest: +SKIP 70 | 'Nimbus Sans L' 71 | """ 72 | 73 | def __new__(cls, *args, **kwargs): 74 | if not issubclass(cls, NamedFont): # Font, but not NamedFont 75 | named_font = _anonymous_font_new_helper(*args, **kwargs) 76 | if named_font is not None: 77 | return named_font 78 | 79 | return super(Font, cls).__new__(cls) 80 | 81 | def __init__(self, font_description): 82 | # the _font_description of NamedFont is the font name 83 | self._font_description = font_description 84 | 85 | family = _font_property(str, 'family') 86 | size = _font_property(int, 'size') 87 | weight = _font_property(str, 'weight') 88 | slant = _font_property(str, 'slant') 89 | underline = _font_property(bool, 'underline') 90 | overstrike = _font_property(bool, 'overstrike') 91 | 92 | def __repr__(self): 93 | return '%s(%r)' % (type(self).__name__, self._font_description) 94 | 95 | def __eq__(self, other): 96 | if not isinstance(other, Font): 97 | return False 98 | return self._font_description == other._font_description 99 | 100 | def __hash__(self): 101 | return hash(self._font_description) 102 | 103 | @classmethod 104 | def from_tcl(cls, font_description): 105 | """ 106 | ``Font.from_tcl(font_description)`` returns ``Font(font_description)``. 107 | 108 | This is just for compatibility with 109 | :ref:`type specifications `. 110 | """ 111 | return cls(font_description) 112 | 113 | def to_tcl(self): 114 | """ 115 | Returns the font description passed to ``Font(font_description)``. 116 | """ 117 | return to_tcl(self._font_description) 118 | 119 | def measure(self, text): 120 | """ 121 | Calls ``font measure`` documented in :man:`font(3tk)`, and returns an 122 | integer. 123 | """ 124 | return teek.tcl_call(int, "font", "measure", self, text) 125 | 126 | def metrics(self): 127 | """ 128 | Calls ``font metrics`` documented in :man:`font(3tk)`, and returns 129 | a dictionary that has at least the following keys: 130 | 131 | * The values of ``'ascent'``, ``'descent'`` and ``'linespace'`` are 132 | integers. 133 | * The value of ``'fixed'`` is True or False. 134 | """ 135 | result = teek.tcl_call( 136 | {"-ascent": int, "-descent": int, 137 | "-linespace": int, "-fixed": bool}, 138 | "font", "metrics", self) 139 | return {name.lstrip('-'): value for name, value in result.items()} 140 | 141 | def to_named_font(self): 142 | """Returns a :class:`.NamedFont` object created from this font. 143 | 144 | If this font is already a :class:`.NamedFont`, a copy of it is created 145 | and returned. 146 | """ 147 | options = teek.tcl_call({}, 'font', 'actual', self) 148 | kwargs = {name.lstrip('-'): value for name, value in options.items()} 149 | return NamedFont(**kwargs) 150 | 151 | @classmethod 152 | def families(self, *, allow_at_prefix=False): 153 | """Returns a list of font families as strings. 154 | 155 | On Windows, some font families start with ``'@'``. I don't know what 156 | those families are and how they might be useful, but most of the time 157 | tkinter users (including me) ignore those, so this method ignores them 158 | by default. Pass ``allow_at_prefix=True`` to get a list that includes 159 | the ``'@'`` fonts. 160 | """ 161 | result = teek.tcl_call([str], "font", "families") 162 | if allow_at_prefix: 163 | return result 164 | return [family for family in result if not family.startswith('@')] 165 | 166 | 167 | class NamedFont(Font): 168 | """A font that has a name in Tcl. 169 | 170 | :class:`.NamedFont` is a subclass of :class:`.Font`; that is, all 171 | NamedFonts are Fonts, but not all Fonts are NamedFonts: 172 | 173 | >>> isinstance(teek.NamedFont('toot'), teek.Font) 174 | True 175 | >>> isinstance(teek.Font('Helvetica 12'), teek.NamedFont) 176 | False 177 | 178 | If ``name`` is not given, Tk will choose a font name that is not in use 179 | yet. If ``name`` is given, it can be a name of an existing font, but if a 180 | font with the given name doesn't exist, it'll be created instead. 181 | 182 | The ``kwargs`` are values for ``family``, ``size``, ``weight``, ``slant``, 183 | ``underline`` and ``overstrike`` attributes. For example, this... 184 | :: 185 | 186 | shouting_font = teek.NamedFont(size=30, weight='bold') 187 | 188 | ...does the same thing as this:: 189 | 190 | shouting_font = teek.NamedFont() 191 | shouting_font.size = 30 192 | shouting_font.weight = 'bold' 193 | """ 194 | 195 | def __init__(self, name=None, **kwargs): 196 | options_with_dashes = [] 197 | for option_name, value in kwargs.items(): 198 | options_with_dashes.extend(['-' + option_name, value]) 199 | 200 | if name is None: 201 | # let tk choose a name that's not used yet 202 | name = teek.tcl_call(str, 'font', 'create', *options_with_dashes) 203 | else: 204 | # do we need to create a font with the given name? 205 | try: 206 | teek.tcl_call(None, 'font', 'create', name, 207 | *options_with_dashes) 208 | except teek.TclError: 209 | # no, it exists already, but we must do something with the 210 | # options 211 | teek.tcl_call(None, 'font', 'configure', name, 212 | *options_with_dashes) 213 | 214 | super().__init__(name) 215 | 216 | # __repr__, __eq__, __hash__, and event to_named_font, {from,to}_tcl are 217 | # fine, to_named_font creates a copy of this font 218 | 219 | # TODO: rename this less verbosely if it's possible while keeping the 220 | # meaning obvious 221 | @classmethod 222 | def get_all_named_fonts(cls): 223 | """Returns a list of all :class:`.NamedFont` objects.""" 224 | return list(map(cls, teek.tcl_call([str], 'font', 'names'))) 225 | 226 | def delete(self): 227 | """Calls ``font delete``. 228 | 229 | The font object is useless after this, and most things will raise 230 | :exc:`.TclError`. 231 | """ 232 | teek.tcl_call(None, "font", "delete", self) 233 | -------------------------------------------------------------------------------- /teek/_platform_info.py: -------------------------------------------------------------------------------- 1 | import _tkinter 2 | 3 | import teek 4 | 5 | 6 | # i'm not sure if these can be different, but why not allow that i guess... lol 7 | TCL_VERSION = tuple(map(int, _tkinter.TCL_VERSION.split('.'))) 8 | TK_VERSION = tuple(map(int, _tkinter.TK_VERSION.split('.'))) 9 | 10 | 11 | # this is a function to make this testable 12 | def _version_check(): 13 | if TK_VERSION < (8, 5) or TCL_VERSION < (8, 5): 14 | raise RuntimeError( 15 | "sorry, your Tcl/Tk installation is too old " 16 | "(expected 8.5 or newer, found Tcl %d.%d and Tk %d.%d)" 17 | % (TCL_VERSION + TK_VERSION)) 18 | 19 | 20 | _version_check() 21 | 22 | 23 | def windowingsystem(): 24 | return teek.tcl_call(str, 'tk', 'windowingsystem') 25 | -------------------------------------------------------------------------------- /teek/_timeouts.py: -------------------------------------------------------------------------------- 1 | import teek 2 | from teek._tcl_calls import make_thread_safe 3 | 4 | 5 | # there's no after_info because i don't see how it would be useful in 6 | # teek 7 | 8 | class _Timeout: 9 | 10 | def __init__(self, after_what, callback, args, kwargs): 11 | if kwargs is None: 12 | kwargs = {} 13 | 14 | self._callback = callback 15 | self._args = args 16 | self._kwargs = kwargs 17 | 18 | self._state = 'pending' # just for __repr__ and error messages 19 | self._tcl_command = teek.create_command(self._run) 20 | self._id = teek.tcl_call(str, 'after', after_what, self._tcl_command) 21 | 22 | def __repr__(self): 23 | name = getattr(self._callback, '__name__', self._callback) 24 | return '<%s %r timeout %r>' % (self._state, name, self._id) 25 | 26 | def _run(self): 27 | needs_cleanup = True 28 | 29 | # this is important, thread tests freeze without this special 30 | # case for some reason 31 | def quit_callback(): 32 | nonlocal needs_cleanup 33 | needs_cleanup = False 34 | 35 | teek.before_quit.connect(quit_callback) 36 | 37 | try: 38 | self._callback(*self._args, **self._kwargs) 39 | self._state = 'successfully completed' 40 | except Exception as e: 41 | self._state = 'failed' 42 | raise e 43 | finally: 44 | teek.before_quit.disconnect(quit_callback) 45 | if needs_cleanup: 46 | teek.delete_command(self._tcl_command) 47 | 48 | @make_thread_safe 49 | def cancel(self): 50 | """Prevent this timeout from running as scheduled. 51 | 52 | :exc:`RuntimeError` is raised if the timeout has already ran or 53 | it has been cancelled. 54 | 55 | There is example code in :source:`examples/timeout.py`. 56 | """ 57 | if self._state != 'pending': 58 | raise RuntimeError("cannot cancel a %s timeout" % self._state) 59 | teek.tcl_call(None, 'after', 'cancel', self._id) 60 | self._state = 'cancelled' 61 | teek.delete_command(self._tcl_command) 62 | 63 | 64 | @make_thread_safe 65 | def after(ms, callback, args=(), kwargs=None): 66 | """Run ``callback(*args, **kwargs)`` after waiting for the given time. 67 | 68 | The *ms* argument should be a waiting time in milliseconds, and 69 | *kwargs* defaults to ``{}``. This returns a timeout object with a 70 | ``cancel()`` method that takes no arguments; you can use that to 71 | cancel the timeout before it runs. 72 | """ 73 | return _Timeout(ms, callback, args, kwargs) 74 | 75 | 76 | @make_thread_safe 77 | def after_idle(callback, args=(), kwargs=None): 78 | """Like :func:`after`, but runs the timeout as soon as possible.""" 79 | return _Timeout('idle', callback, args, kwargs) 80 | -------------------------------------------------------------------------------- /teek/_widgets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akuli/teek/c360fbfe086ca09cdd856a8636de05b24e1b7093/teek/_widgets/__init__.py -------------------------------------------------------------------------------- /teek/_widgets/canvas.py: -------------------------------------------------------------------------------- 1 | import collections.abc 2 | import functools 3 | 4 | import teek 5 | from teek._widgets.base import Widget, ChildMixin 6 | from teek._structures import CgetConfigureConfigDict 7 | 8 | 9 | # you can also make a list of canvas items instead of tagging, but there are 10 | # things that are much easier with good support for tags, like bbox of multiple 11 | # canvas items 12 | # 13 | # TODO: how are these different from item.config['tags']? 14 | # which is better? or are they equivalent in all the possible ways? 15 | class Tags(collections.abc.MutableSet): 16 | 17 | def __init__(self, item): 18 | self._item = item 19 | 20 | def _gettags(self): 21 | return self._item.canvas._call( 22 | [str], self._item.canvas, 'gettags', self._item) 23 | 24 | def __iter__(self): 25 | return iter(self._gettags()) 26 | 27 | def __contains__(self, tag): 28 | return tag in self._gettags() 29 | 30 | def __len__(self): 31 | return len(self._gettags()) 32 | 33 | def add(self, tag): 34 | self._item.canvas._call( 35 | None, self._item.canvas, 'addtag', tag, 'withtag', self._item) 36 | 37 | def discard(self, tag): 38 | self._item._call(None, 'dtag', self._item, tag) 39 | 40 | 41 | class CanvasItem: 42 | 43 | # a 'canvas' attribute is added in subclasses 44 | 45 | # __new__ magic ftw 46 | def __init__(self, *args, **kwargs): 47 | raise TypeError( 48 | "don't create canvas.Item objects yourself, use methods like " 49 | "create_line(), create_rectangle() etc instead") 50 | 51 | def _create(self, type_string, *coords, **kwargs): 52 | id_ = self.canvas._call( 53 | str, self.canvas, 'create', type_string, *coords) 54 | self._setup(type_string, id_) 55 | self.config.update(kwargs) 56 | 57 | def __repr__(self): 58 | try: 59 | coords = self.coords 60 | except RuntimeError: 61 | return '' % self.type_string 62 | return '<%s canvas item at %r>' % (self.type_string, coords) 63 | 64 | def _call(self, returntype, subcommand, *args): 65 | return self.canvas._call( 66 | returntype, self.canvas, subcommand, *args) 67 | 68 | def _config_caller(self, returntype, cget_or_configure, *args): 69 | return self._call(returntype, 'item' + cget_or_configure, self, *args) 70 | 71 | def _setup(self, type_string, id_): 72 | self.type_string = type_string 73 | self._id = id_ 74 | 75 | self.tags = Tags(self) 76 | self.config = CgetConfigureConfigDict(self._config_caller) 77 | 78 | prefixed = { 79 | #'stipple': ???, 80 | #'outlinestipple': ???, 81 | 'fill': teek.Color, 82 | 'outline': teek.Color, 83 | 'dash': str, 84 | # TODO: support non-float coordinates? see COORDINATES in man page 85 | 'width': float, 86 | } 87 | for prefix in ['', 'active', 'disabled']: 88 | self.config._types.update({ 89 | prefix + key: value for key, value in prefixed.items()}) 90 | 91 | self.config._types.update({ 92 | 'offset': str, 93 | 'outlineoffset': str, 94 | 'joinstyle': str, 95 | 'splinesteps': int, 96 | 'smooth': str, 97 | 'state': str, 98 | 'tags': [str], 99 | 'capstyle': str, 100 | 'arrow': str, 101 | # see comment about floats in prefixed 102 | 'dashoffset': float, 103 | 'arrowshape': (float, float, float), 104 | }) 105 | 106 | @classmethod 107 | def from_tcl(cls, id_): 108 | type_ = cls.canvas._call(str, cls.canvas, 'type', id_) 109 | item = cls.__new__(cls) # create instance without calling __init__ 110 | item._setup(type_, id_) 111 | return item 112 | 113 | def to_tcl(self): 114 | return self._id 115 | 116 | def __eq__(self, other): 117 | if isinstance(other, CanvasItem): 118 | return (self.canvas is other.canvas and 119 | self._id == other._id) 120 | return NotImplemented 121 | 122 | def __hash__(self): 123 | return hash((self.canvas, self._id)) 124 | 125 | @property 126 | def coords(self): 127 | result = self._call([float], 'coords', self) 128 | if not result: 129 | raise RuntimeError("the canvas item has been deleted") 130 | return tuple(result) 131 | 132 | @coords.setter 133 | def coords(self, coords): 134 | self._call(None, 'coords', self, *coords) 135 | 136 | def find_above(self): 137 | return self._call(self.canvas.Item, 'find', 'above', self) 138 | 139 | def find_below(self): 140 | return self._call(self.canvas.Item, 'find', 'below', self) 141 | 142 | # TODO: bind, dchars 143 | 144 | def delete(self): 145 | return self._call(None, 'delete', self) 146 | 147 | 148 | # TODO: arc, bitmap, image, line, polygon, text, window 149 | 150 | 151 | class Canvas(ChildMixin, Widget): 152 | """This is the canvas widget. 153 | 154 | Manual page: :man:`canvas(3tk)` 155 | 156 | .. method:: create_line(*x_and_y_coords, **kwargs) 157 | create_oval(x1, y1, x2, y2, **kwargs) 158 | create_rectangle(x1, y1, x2, y2, **kwargs) 159 | 160 | These create and return new :ref:`canvas items `. See 161 | the appropriate sections of :man:`canvas(3tk)` for details, e.g. 162 | ``RECTANGLE ITEMS`` for ``create_rectangle()``. 163 | """ 164 | 165 | _widget_name = 'canvas' 166 | tk_class_name = 'Canvas' 167 | 168 | def __init__(self, *args, **kwargs): 169 | super().__init__(*args, **kwargs) 170 | self.Item = type('Item', (CanvasItem,), {'canvas': self}) 171 | 172 | def _init_config(self): 173 | super()._init_config() 174 | self.config._types.update({ 175 | 'xscrollincrement': teek.ScreenDistance, 176 | 'yscrollincrement': teek.ScreenDistance, 177 | 'offset': str, # couldn't be bothered to make it different 178 | 'closeenough': float, 179 | 'confine': bool, 180 | # TODO: support non-float coordinates? see COORDINATES in man page 181 | 'width': float, 182 | 'height': float, 183 | 'scrollregion': [float], 184 | }) 185 | 186 | def _create(self, type_string, *coords, **kwargs): 187 | item = self.Item.__new__(self.Item) 188 | item._create(type_string, *coords, **kwargs) 189 | return item 190 | 191 | create_rectangle = functools.partialmethod(_create, 'rectangle') 192 | create_oval = functools.partialmethod(_create, 'oval') 193 | create_line = functools.partialmethod(_create, 'line') 194 | 195 | def find_all(self): 196 | """Returns a list of all items on the canvas.""" 197 | return self._call([self.Item], self, 'find', 'all') 198 | 199 | def find_closest(self, x, y, *args): 200 | """Returns the canvas item that is closest to the given coordinates. 201 | 202 | See the ``closest`` documentation of ``pathName addtag`` in 203 | :man:`canvas(3tk)` for details. 204 | """ 205 | return self._call(self.Item, self, 'find', 'closest', x, y, *args) 206 | 207 | def find_enclosed(self, x1, y1, x2, y2): 208 | """Returns a list of canvas items. 209 | 210 | See the ``enclosed`` documentation of ``pathName addtag`` in 211 | :man:`canvas(3tk)` for details. 212 | """ 213 | return self._call( 214 | [self.Item], self, 'find', 'enclosed', x1, y1, x2, y2) 215 | 216 | def find_overlapping(self, x1, y1, x2, y2): 217 | """Returns a list of canvas items. 218 | 219 | See the ``enclosed`` documentation of ``pathName addtag`` in 220 | :man:`canvas(3tk)` for details. 221 | """ 222 | return self._call( 223 | [self.Item], self, 'find', 'overlapping', x1, y1, x2, y2) 224 | 225 | def find_withtag(self, tag_name): 226 | """Returns a list of canvas items that have the given \ 227 | :ref:`tag `. 228 | 229 | The tag name is given as a string. See the ``enclosed`` documentation 230 | of ``pathName addtag`` in :man:`canvas(3tk)` for details. 231 | """ 232 | return self._call([self.Item], self, 'find', 'withtag', tag_name) 233 | 234 | # TODO: canvasx canvasy 235 | -------------------------------------------------------------------------------- /teek/dialog.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | import os 3 | 4 | import teek 5 | 6 | 7 | def _options(kwargs): 8 | if 'parent' in kwargs and isinstance(kwargs['parent'], teek.Window): 9 | kwargs['parent'] = kwargs['parent'].toplevel 10 | 11 | for name, value in kwargs.items(): 12 | yield '-' + name 13 | yield value 14 | 15 | 16 | def color(**kwargs): 17 | """Calls :man:`tk_chooseColor(3tk)`. 18 | 19 | The color selected by the user is returned, or ``None`` if the user 20 | cancelled the dialog. 21 | """ 22 | return teek.tcl_call(teek.Color, 'tk_chooseColor', *_options(kwargs)) 23 | 24 | 25 | def _messagebox(type, title, message, detail=None, **kwargs): 26 | kwargs['type'] = type 27 | kwargs['title'] = title 28 | kwargs['message'] = message 29 | if detail is not None: 30 | kwargs['detail'] = detail 31 | 32 | if type == 'ok': 33 | teek.tcl_call(None, 'tk_messageBox', *_options(kwargs)) 34 | return None 35 | if type == 'okcancel': 36 | return teek.tcl_call(str, 'tk_messageBox', *_options(kwargs)) == 'ok' 37 | if type == 'retrycancel': 38 | return ( 39 | teek.tcl_call(str, 'tk_messageBox', *_options(kwargs)) == 'retry') 40 | if type == 'yesno': 41 | return teek.tcl_call(str, 'tk_messageBox', *_options(kwargs)) == 'yes' 42 | 43 | # for anything else, return a string 44 | return teek.tcl_call(str, 'tk_messageBox', *_options(kwargs)) 45 | 46 | 47 | info = partial(_messagebox, 'ok', icon='info') 48 | warning = partial(_messagebox, 'ok', icon='warning') 49 | error = partial(_messagebox, 'ok', icon='error') 50 | ok_cancel = partial(_messagebox, 'okcancel', icon='question') 51 | retry_cancel = partial(_messagebox, 'retrycancel', icon='warning') 52 | yes_no = partial(_messagebox, 'yesno', icon='question') 53 | yes_no_cancel = partial(_messagebox, 'yesnocancel', icon='question') 54 | abort_retry_ignore = partial(_messagebox, 'abortretryignore', icon='error') 55 | 56 | 57 | def _check_multiple(kwargs): 58 | if 'multiple' in kwargs: 59 | raise TypeError( 60 | "the 'multiple' option is not supported, use open_file() or " 61 | "open_multiple_files() depending on whether you want to support " 62 | "selecting multiple files at once") 63 | 64 | 65 | def open_file(**kwargs): 66 | """ 67 | Ask the user to choose an existing file. Returns the path. 68 | 69 | This calls :man:`tk_getOpenFile(3tk)` without ``-multiple``. ``None`` is 70 | returned if the user cancels. 71 | """ 72 | _check_multiple(kwargs) 73 | result = teek.tcl_call(str, 'tk_getOpenFile', *_options(kwargs)) 74 | if not result: 75 | return None 76 | return os.path.abspath(result) 77 | 78 | 79 | def open_multiple_files(**kwargs): 80 | """ 81 | Ask the user to choose one or more existing files. Returns a list of paths. 82 | 83 | This calls :man:`tk_getOpenFile(3tk)` with ``-multiple`` set to true. An 84 | empty list is returned if the user cancels. 85 | """ 86 | _check_multiple(kwargs) 87 | result = teek.tcl_call( 88 | [str], 'tk_getOpenFile', '-multiple', True, *_options(kwargs)) 89 | return list(map(os.path.abspath, result)) 90 | 91 | 92 | def save_file(**kwargs): 93 | """Ask the user to choose a path for a new file. Return the path. 94 | 95 | This calls :man:`tk_getSaveFile(3tk)`, and returns ``None`` if the user 96 | cancels. 97 | """ 98 | result = teek.tcl_call(str, 'tk_getSaveFile', *_options(kwargs)) 99 | if not result: 100 | return None 101 | return os.path.abspath(result) 102 | 103 | 104 | def directory(**kwargs): 105 | """Asks the user to choose a directory, and return a path to it. 106 | 107 | This calls :man:`tk_chooseDirectory(3tk)`, and returns ``None`` if the user 108 | cancels. 109 | 110 | .. note:: 111 | By default, the user can choose a directory that doesn't exist yet. 112 | This behaviour is documented in :man:`tk_chooseDirectory(3tk)`. If you 113 | want the user to choose an existing directory, use ``mustexist=True``. 114 | """ 115 | result = teek.tcl_call(str, 'tk_chooseDirectory', *_options(kwargs)) 116 | if not result: 117 | return None 118 | return os.path.abspath(result) 119 | -------------------------------------------------------------------------------- /teek/extras/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akuli/teek/c360fbfe086ca09cdd856a8636de05b24e1b7093/teek/extras/__init__.py -------------------------------------------------------------------------------- /teek/extras/cross_platform.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | import teek 4 | 5 | 6 | # this is not called bind_tab to avoid confusing with: 7 | # * \t characters 8 | # * web browser tabs as in teek.Notebook 9 | def bind_tab_key(widget, callback, **bind_kwargs): 10 | """A cross-platform way to bind Tab and Shift+Tab. 11 | 12 | Use this function like this:: 13 | 14 | from teek.extras import cross_platform 15 | 16 | def on_tab(shifted): 17 | if shifted: 18 | print("Shift+Tab was pressed") 19 | else: 20 | print("Tab was pressed") 21 | 22 | cross_platform.bind_tab_key(some_widget, on_tab) 23 | 24 | Binding ``''`` works on all systems I've tried it on, but if you also 25 | want to bind tab presses where the shift key is held down, use this 26 | function instead. 27 | 28 | This function can also take any of the keyword arguments that 29 | :meth:`teek.Widget.bind` takes. If you pass ``event=True``, the callback 30 | will be called like ``callback(shifted, event)``; that is, the ``shifted`` 31 | bool is the first argument, and the event object is the second. 32 | """ 33 | if teek.windowingsystem() == 'x11': 34 | # even though the event keysym says Left, holding down the right 35 | # shift and pressing tab also works :D 36 | shift_tab = '' 37 | else: 38 | shift_tab = '' # pragma: no cover 39 | 40 | widget.bind('', functools.partial(callback, False), **bind_kwargs) 41 | widget.bind(shift_tab, functools.partial(callback, True), **bind_kwargs) 42 | -------------------------------------------------------------------------------- /teek/extras/image_loader.py: -------------------------------------------------------------------------------- 1 | # if you change this file, consider also changing image_loader_dummy.py 2 | 3 | import io 4 | 5 | # see pyproject.toml 6 | try: 7 | import lxml.etree 8 | import PIL.Image 9 | import reportlab.graphics.renderPM 10 | from svglib import svglib 11 | except ImportError as e: 12 | raise ImportError(str(e) + ". Maybe try 'pip install teek[image_loader]' " 13 | "to fix this?").with_traceback(e.__traceback__) from None 14 | 15 | import teek 16 | 17 | # both of these files have the same from_pil implementation, because it doesn't 18 | # actually need to import PIL 19 | from teek.extras.image_loader_dummy import from_pil 20 | 21 | 22 | def from_file(file): 23 | """Creates a :class:`teek.Image` from a file object. 24 | 25 | The file object must be readable, and it must be in bytes mode. It must 26 | have ``read()``, ``seek()`` and ``tell()`` methods. For example, files from 27 | ``open(some_path, 'rb')`` and ``io.BytesIO(some_data)`` work. 28 | 29 | This supports all file formats that PIL supports and SVG. 30 | 31 | Example:: 32 | 33 | from teek.extras import image_loader 34 | 35 | with open(the_image_path, 'rb') as file: 36 | image = image_loader.from_file(file) 37 | """ 38 | first_3_bytes = file.read(3) 39 | file.seek(0) 40 | if first_3_bytes == b'GIF': 41 | return teek.Image(data=file.read()) 42 | 43 | # https://stackoverflow.com/a/15136684 44 | try: 45 | event, element = next(lxml.etree.iterparse(file, ('start',))) 46 | is_svg = (element.tag == '{http://www.w3.org/2000/svg}svg') 47 | except (lxml.etree.ParseError, StopIteration): 48 | is_svg = False 49 | file.seek(0) 50 | 51 | if is_svg: 52 | # svglib takes open file objects, even though it doesn't look like it 53 | # https://github.com/deeplook/svglib/issues/173 54 | rlg = svglib.svg2rlg(io.TextIOWrapper(file, encoding='utf-8')) 55 | 56 | with reportlab.graphics.renderPM.drawToPIL(rlg) as pil_image: 57 | return from_pil(pil_image) 58 | 59 | with PIL.Image.open(file) as pil_image: 60 | return from_pil(pil_image) 61 | 62 | 63 | def from_bytes(bytes_): 64 | """ 65 | Creates a :class:`teek.Image` from bytes that would normally be in an image 66 | file. 67 | 68 | Example:: 69 | 70 | # if you have a file, it's recommended to use 71 | # from_file() instead, but this example works anyway 72 | with open(the_image_path, 'rb') as file: 73 | data = file.read() 74 | 75 | image = image_loader.from_bytes(data) 76 | """ 77 | with io.BytesIO(bytes_) as fake_file: 78 | return from_file(fake_file) 79 | -------------------------------------------------------------------------------- /teek/extras/image_loader_dummy.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | import teek 4 | 5 | 6 | def from_pil(pil_image, **kwargs): 7 | """Converts a PIL_ ``Image`` object to a :class:`teek.Image`. 8 | 9 | All keyword arguments are passed to PIL's save_ method. 10 | 11 | .. _PIL: https://pillow.readthedocs.io/en/stable/ 12 | .. _save: https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL\ 13 | .Image.Image.save 14 | """ 15 | # TODO: borrow some magic code from PIL.ImageTk to make this faster? 16 | # or maybe optimize teek.Image? currently it base64 encodes the data 17 | gif = io.BytesIO() 18 | pil_image.save(gif, 'gif', **kwargs) 19 | return teek.Image(data=gif.getvalue()) 20 | 21 | 22 | def from_file(file): 23 | return teek.Image(data=file.read()) 24 | 25 | 26 | def from_bytes(bytes_): 27 | return teek.Image(data=bytes_) 28 | -------------------------------------------------------------------------------- /teek/extras/links.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import webbrowser 3 | 4 | _TAG_PREFIX = 'teek-extras-link-' 5 | 6 | 7 | def _init_links(widget): 8 | if _TAG_PREFIX + 'common' in (tag.name for tag in widget.get_all_tags()): 9 | return widget.get_tag(_TAG_PREFIX + 'common') 10 | 11 | old_cursor = widget.config['cursor'] 12 | 13 | def enter(): 14 | nonlocal old_cursor 15 | old_cursor = widget.config['cursor'] 16 | widget.config['cursor'] = 'hand2' 17 | 18 | def leave(): 19 | widget.config['cursor'] = old_cursor 20 | 21 | tag = widget.get_tag(_TAG_PREFIX + 'common') 22 | tag['foreground'] = 'blue' 23 | tag['underline'] = True 24 | tag.bind('', enter) 25 | tag.bind('', leave) 26 | return tag 27 | 28 | 29 | def add_function_link(textwidget, function, start, end): 30 | """ 31 | Like :func:`add_url_link`, but calls a function instead of opening a URL 32 | in a web browser. 33 | """ 34 | common_tag = _init_links(textwidget) 35 | 36 | names = {tag.name for tag in textwidget.get_all_tags()} 37 | i = 1 38 | while _TAG_PREFIX + str(i) in names: 39 | i += 1 40 | specific_tag = textwidget.get_tag(_TAG_PREFIX + str(i)) 41 | 42 | # bind callbacks must return None or 'break', but this ignores the 43 | # function's return value 44 | def none_return_function(): 45 | function() 46 | 47 | specific_tag.bind('', none_return_function) 48 | for tag in [common_tag, specific_tag]: 49 | tag.add(start, end) 50 | 51 | 52 | def add_url_link(textwidget, url, start, end): 53 | """ 54 | Make some of the text in the textwidget to be clickable so that clicking 55 | it will open ``url``. 56 | 57 | The text between the :ref:`text indexes ` ``start`` and 58 | ``end`` becomes clickable, and rest of the text is not touched. 59 | 60 | Do this if you want to insert some text and make it a link immediately:: 61 | 62 | from teek.extras import links 63 | 64 | ... 65 | 66 | old_end = textwidget.end # adding text to end changes textwidget.end 67 | textwidget.insert(textwidget.end, 'Click me') 68 | links.add_url_link(textwidget, 'https://example.com/', old_end, textwi\ 69 | dget.end) 70 | 71 | This function uses :func:`webbrowser.open` for opening ``url``. 72 | """ 73 | add_function_link(textwidget, functools.partial(webbrowser.open, url), 74 | start, end) 75 | -------------------------------------------------------------------------------- /teek/extras/more_dialogs.py: -------------------------------------------------------------------------------- 1 | import teek 2 | 3 | 4 | class _EntryDialog: 5 | 6 | def __init__(self, title, text, entry_creator, validator, 7 | initial_value, parent): 8 | self.validator = validator 9 | 10 | self.window = teek.Window(title) 11 | self.window.on_delete_window.connect(self.on_cancel) 12 | if parent is not None: 13 | self.window.transient = parent 14 | 15 | self.var = teek.StringVar() 16 | 17 | teek.Label(self.window, text).grid(row=0, column=0, columnspan=2) 18 | entry = entry_creator(self.window) 19 | entry.config['textvariable'] = self.var 20 | entry.grid(row=1, column=0, columnspan=2) 21 | entry.bind('', self.on_ok) 22 | entry.bind('', self.on_cancel) 23 | 24 | self.ok_button = teek.Button(self.window, "OK", self.on_ok) 25 | self.ok_button.grid(row=3, column=0) 26 | teek.Button(self.window, "Cancel", self.on_cancel).grid( 27 | row=3, column=1) 28 | 29 | self.window.grid_rows[0].config['weight'] = 1 30 | self.window.grid_rows[2].config['weight'] = 1 31 | for column in self.window.grid_columns: 32 | column.config['weight'] = 1 33 | 34 | self.result = None 35 | self.var.write_trace.connect(self.on_var_changed) 36 | self.var.set(initial_value) 37 | self.on_var_changed(self.var) # TODO: is this needed? 38 | 39 | # TODO: add a way to select stuff to teek 40 | self.window.geometry(300, 150) 41 | entry.focus() 42 | teek.tcl_call(None, entry, 'selection', 'range', '0', 'end') 43 | 44 | def on_var_changed(self, var): 45 | result = self.var.get() 46 | try: 47 | self.result = self.validator(result) 48 | self.ok_button.config['state'] = 'normal' 49 | except ValueError: 50 | self.result = None 51 | self.ok_button.config['state'] = 'disabled' 52 | 53 | def on_ok(self): 54 | # this state check is needed because is bound to this, and 55 | # that binding can run even if the button is disabled 56 | if self.ok_button.config['state'] == 'normal': 57 | self.window.destroy() 58 | 59 | def on_cancel(self): 60 | self.result = None 61 | self.window.destroy() 62 | 63 | def run(self): 64 | self.window.wait_window() 65 | return self.result 66 | 67 | 68 | def ask_string(title, text, *, validator=str, initial_value='', parent=None): 69 | """Displays a dialog that contains a :class:`teek.Entry` widget. 70 | 71 | The ``validator`` should be a function that takes a string as an argument, 72 | and returns something useful (see below). By default, it returns the string 73 | unchanged, which is useful for asking a string from the user. If the 74 | validator raises :exc:`ValueError`, the OK button of the dialog is disabled 75 | to tell the user that the value they entered is invalid. Then the user 76 | needs to enter a valid value or cancel the dialog. 77 | 78 | This returns whatever ``validator`` returned, or ``None`` if the dialog was 79 | canceled. 80 | """ 81 | return _EntryDialog(title, text, teek.Entry, validator, initial_value, 82 | parent).run() 83 | 84 | 85 | def ask_integer(title, text, allowed_values, *, initial_value=None, 86 | parent=None): 87 | """Displays a dialog that contains a :class:`teek.Spinbox` widget. 88 | 89 | ``allowed_values`` can be a sequence of acceptable integers or a 90 | :class:`range`. If ``initial_value`` is given, it must be in 91 | ``allowed_values``. If it's not, ``allowed_values[0]`` is used. 92 | 93 | This returns an integer in ``allowed_values``, or ``None`` if the user 94 | cancels. 95 | """ 96 | def creator(spinbox_parent): 97 | if isinstance(allowed_values, range): 98 | # range(blah, blah, 0) raises an error, so the step can't be zero 99 | if allowed_values.step < 0: 100 | raise ValueError( 101 | "ranges with negative steps are not supported") 102 | 103 | # allowed_values.stop is not same as allowed_values[-1], the -1 one 104 | # is inclusive 105 | return teek.Spinbox( 106 | spinbox_parent, from_=allowed_values[0], to=allowed_values[-1], 107 | increment=allowed_values.step) 108 | 109 | return teek.Spinbox(spinbox_parent, values=allowed_values) 110 | 111 | def validator(value): 112 | int_value = int(value) 113 | if int_value not in allowed_values: 114 | raise ValueError 115 | return int_value 116 | 117 | if initial_value is None: 118 | initial_value = allowed_values[0] 119 | elif initial_value not in allowed_values: 120 | raise ValueError("initial value %r not in %r" 121 | % (initial_value, allowed_values)) 122 | 123 | return _EntryDialog(title, text, creator, validator, initial_value, 124 | parent).run() 125 | -------------------------------------------------------------------------------- /teek/extras/tooltips.py: -------------------------------------------------------------------------------- 1 | import teek 2 | 3 | 4 | class _TooltipManager: 5 | 6 | # This needs to be shared by all instances because there's only one 7 | # mouse pointer. 8 | tipwindow = None 9 | 10 | def __init__(self, widget: teek.Widget): 11 | widget.bind('', self.enter, event=True) 12 | widget.bind('', self.leave, event=True) 13 | widget.bind('', self.motion, event=True) 14 | self.widget = widget 15 | self.got_mouse = False 16 | self.text = None 17 | 18 | @classmethod 19 | def destroy_tipwindow(cls): 20 | if cls.tipwindow is not None: 21 | cls.tipwindow.destroy() 22 | cls.tipwindow = None 23 | 24 | def enter(self, event): 25 | # For some reason, toplevels get also notified of their 26 | # childrens' events. 27 | if event.widget is self.widget: 28 | self.destroy_tipwindow() 29 | self.got_mouse = True 30 | teek.after(1000, self.show) 31 | 32 | # these are important, it's possible to enter without mouse move 33 | self.mousex = event.rootx 34 | self.mousey = event.rooty 35 | 36 | def leave(self, event): 37 | if event.widget is self.widget: 38 | self.destroy_tipwindow() 39 | self.got_mouse = False 40 | 41 | def motion(self, event): 42 | self.mousex = event.rootx 43 | self.mousey = event.rooty 44 | 45 | def show(self): 46 | if not self.got_mouse: 47 | return 48 | 49 | self.destroy_tipwindow() 50 | if self.text is not None: 51 | # the label and the tipwindow are not ttk widgets because that way 52 | # they can look different than other widgets, which is what 53 | # tooltips are usually like 54 | tipwindow = type(self).tipwindow = teek.Toplevel() 55 | tipwindow.geometry(x=(self.mousex + 10), y=(self.mousey - 10)) 56 | tipwindow.bind('', self.destroy_tipwindow) 57 | 58 | # TODO: add overrideredirect to teek 59 | teek.tcl_call(None, 'wm', 'overrideredirect', tipwindow, 1) 60 | 61 | # i don't think there's a need to add better support for things 62 | # like non-ttk labels because they are not needed very often 63 | # 64 | # refactoring note: if you change this, make sure that either BOTH 65 | # of fg and bg are set, or NEITHER is set, because e.g. bg='white' 66 | # with no fg gives white text on white background on systems with 67 | # white default foreground, but works fine on systems with black 68 | # default foreground 69 | label = teek.tcl_call(str, 'label', tipwindow.to_tcl() + '.label', 70 | '-text', self.text, '-bd', 1, '-relief', 71 | 'solid', '-fg', 'black', '-bg', '#ffc', 72 | '-padx', 2, '-pady', 2) 73 | teek.tcl_call(None, 'pack', label) 74 | 75 | 76 | def set_tooltip(widget, text): 77 | """Create tooltips for a widget. 78 | 79 | After calling ``set_tooltip(some_widget, "hello")``, "hello" will be 80 | displayed in a small window when the user moves the mouse over the 81 | widget and waits for a small period of time. Do 82 | ``set_tooltip(some_widget, None)`` to get rid of a tooltip. 83 | """ 84 | if text is None: 85 | if hasattr(widget, '_tooltip_manager'): 86 | widget._tooltip_manager.text = None 87 | else: 88 | if not hasattr(widget, '_tooltip_manager'): 89 | widget._tooltip_manager = _TooltipManager(widget) 90 | widget._tooltip_manager.text = text 91 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import os 3 | 4 | import pytest 5 | 6 | import teek 7 | 8 | 9 | # https://docs.pytest.org/en/latest/doctest.html#the-doctest-namespace-fixture 10 | @pytest.fixture(autouse=True) 11 | def add_teek(doctest_namespace): 12 | doctest_namespace['teek'] = teek 13 | 14 | 15 | # the following url is on 2 lines because pep8 line length 16 | # 17 | # https://docs.pytest.org/en/latest/example/simple.html#control-skipping-of-tes 18 | # ts-according-to-command-line-option 19 | def pytest_addoption(parser): 20 | parser.addoption( 21 | "--skipslow", action="store_true", default=False, help="run slow tests" 22 | ) 23 | 24 | 25 | def pytest_cmdline_preparse(config, args): 26 | try: 27 | import teek.extras.image_loader 28 | except ImportError: 29 | import teek.extras 30 | path = os.path.join(teek.extras.__path__[0], 'image_loader.py') 31 | args.append('--ignore=' + path) 32 | 33 | 34 | def pytest_collection_modifyitems(config, items): 35 | if config.getoption("--skipslow"): 36 | skip_slow = pytest.mark.skip(reason="--skipslow was used") 37 | for item in items: 38 | if "slow" in item.keywords: 39 | item.add_marker(skip_slow) 40 | 41 | 42 | @pytest.fixture 43 | def deinit_threads(): 44 | """Make sure that init_threads() has not been called when test completes. 45 | 46 | If you have a test like this... 47 | 48 | def test_tootie(): 49 | teek.init_threads() 50 | 51 | ...the test will cause problems for any other tests that also call 52 | init_threads(), because it can't be called twice. Using this fixture in 53 | test_tootie() would fix that problem. 54 | """ 55 | yield 56 | teek.after_idle(teek.quit) 57 | teek.run() 58 | 59 | 60 | @pytest.fixture 61 | def handy_callback(): 62 | def handy_callback_decorator(function): 63 | def result(*args, **kwargs): 64 | return_value = function(*args, **kwargs) 65 | result.ran += 1 66 | return return_value 67 | 68 | result.ran = 0 69 | result.ran_once = (lambda: result.ran == 1) 70 | return result 71 | 72 | return handy_callback_decorator 73 | 74 | 75 | @pytest.fixture 76 | def check_config_types(): 77 | def checker(config, debug_info, *, ignore_list=()): 78 | # this converts all values to their types, and this probably fails if 79 | # the types are wrong 80 | dict(config) 81 | 82 | complained = set() 83 | 84 | # were there keys that defaulted to str? 85 | known_keys = config._types.keys() | config._special.keys() 86 | for key in (config.keys() - known_keys - set(ignore_list)): 87 | print('\ncheck_config_types', debug_info, 'warning: type of', 88 | key, 'was guessed to be str') 89 | complained.add(key) 90 | 91 | return complained 92 | 93 | return checker 94 | 95 | 96 | # this is tested in test_widgets.py 97 | @pytest.fixture 98 | def all_widgets(): 99 | window = teek.Window() 100 | return [ 101 | teek.Button(window), 102 | teek.Canvas(window), 103 | teek.Checkbutton(window), 104 | teek.Combobox(window), 105 | teek.Entry(window), 106 | teek.Frame(window), 107 | teek.Label(window), 108 | teek.LabelFrame(window), 109 | teek.Notebook(window), 110 | teek.Menu(), 111 | teek.Progressbar(window), 112 | teek.Scrollbar(window), 113 | teek.Separator(window), 114 | teek.Spinbox(window), 115 | teek.Text(window), 116 | teek.Toplevel(), 117 | window, 118 | ] 119 | 120 | 121 | @pytest.fixture 122 | def fake_command(): 123 | @contextlib.contextmanager 124 | def faker(name, return_value=None): 125 | called = [] 126 | 127 | def command_func(*args): 128 | called.append(list(args)) 129 | return return_value 130 | 131 | fake = teek.create_command(command_func, [], extra_args_type=str) 132 | 133 | teek.tcl_call(None, 'rename', name, name + '_real') 134 | try: 135 | teek.tcl_call(None, 'rename', fake, name) 136 | yield called 137 | finally: 138 | try: 139 | teek.delete_command(name) 140 | except teek.TclError: 141 | pass 142 | teek.tcl_call(None, 'rename', name + '_real', name) 143 | 144 | return faker 145 | -------------------------------------------------------------------------------- /tests/data/rectangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/data/smiley.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akuli/teek/c360fbfe086ca09cdd856a8636de05b24e1b7093/tests/data/smiley.gif -------------------------------------------------------------------------------- /tests/data/smiley.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akuli/teek/c360fbfe086ca09cdd856a8636de05b24e1b7093/tests/data/smiley.jpg -------------------------------------------------------------------------------- /tests/data/sources.txt: -------------------------------------------------------------------------------- 1 | firefox.svg: https://commons.wikimedia.org/wiki/File:Firefox_Logo,_2017.svg 2 | rectangle.svg: https://commons.wikimedia.org/wiki/SVG_examples 3 | -------------------------------------------------------------------------------- /tests/data/subclasser.py: -------------------------------------------------------------------------------- 1 | import teek 2 | 3 | 4 | class LolLabel(teek.Label): 5 | pass 6 | -------------------------------------------------------------------------------- /tests/extras/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akuli/teek/c360fbfe086ca09cdd856a8636de05b24e1b7093/tests/extras/__init__.py -------------------------------------------------------------------------------- /tests/extras/test_cross_platform.py: -------------------------------------------------------------------------------- 1 | import teek 2 | from teek.extras import cross_platform 3 | 4 | 5 | def test_bind_tab_key(): 6 | what_happened = [] 7 | 8 | def callback1(shifted): 9 | what_happened.append((1, shifted)) 10 | 11 | def callback2(shifted, event): 12 | assert event == 'fake event' 13 | what_happened.append((2, shifted)) 14 | 15 | widget = teek.Window() 16 | cross_platform.bind_tab_key(widget, callback1) 17 | cross_platform.bind_tab_key(widget, callback2, event=True) 18 | 19 | # might be nice to trigger a warning when attempting to use 20 | # on x11 21 | widget.bindings[''].run('fake event') 22 | if teek.windowingsystem() == 'x11': 23 | widget.bindings[''].run('fake event') 24 | else: 25 | widget.bindings[''].run('fake event') 26 | 27 | assert what_happened == [ 28 | (1, False), (2, False), 29 | (1, True), (2, True), 30 | ] 31 | -------------------------------------------------------------------------------- /tests/extras/test_image_loader.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import sys 4 | 5 | import pytest 6 | 7 | import teek 8 | from teek.extras import image_loader_dummy 9 | try: 10 | from teek.extras import image_loader 11 | loader_libs = [image_loader, image_loader_dummy] 12 | except ImportError as e: 13 | assert 'pip install teek[image_loader]' in str(e) 14 | image_loader = None 15 | loader_libs = [image_loader_dummy] 16 | else: 17 | import PIL.Image 18 | 19 | 20 | ignore_svglib_warnings = pytest.mark.filterwarnings( 21 | "ignore:The 'warn' method is deprecated") 22 | needs_image_loader = pytest.mark.skipif( 23 | image_loader is None, 24 | reason="teek.extras.image_loader dependencies are not installed") 25 | 26 | DATA_DIR = os.path.join( 27 | os.path.dirname(os.path.abspath(__file__)), '..', 'data') 28 | 29 | 30 | def test_nice_import_error(monkeypatch, tmp_path): 31 | old_modules = sys.modules.copy() 32 | try: 33 | for module in ['PIL', 'PIL.Image', 'teek.extras.image_loader']: 34 | if module in sys.modules: 35 | del sys.modules[module] 36 | 37 | with open(os.path.join(str(tmp_path), 'PIL.py'), 'w') as bad_pil: 38 | bad_pil.write('raise ImportError("oh no")') 39 | 40 | monkeypatch.syspath_prepend(str(tmp_path)) 41 | with pytest.raises(ImportError) as error: 42 | import teek.extras.image_loader # noqa 43 | 44 | assert str(error.value) == ( 45 | "oh no. Maybe try 'pip install teek[image_loader]' to fix this?") 46 | finally: 47 | sys.modules.update(old_modules) 48 | 49 | 50 | def images_equal(image1, image2): 51 | if image1.width != image2.width or image1.height != image2.height: 52 | return False 53 | return image1.get_bytes('gif') == image2.get_bytes('gif') 54 | 55 | 56 | @needs_image_loader 57 | @pytest.mark.filterwarnings("ignore:unclosed file") # because PIL 58 | def test_from_pil(): 59 | for lib in loader_libs: 60 | smiley1 = teek.Image(file=os.path.join(DATA_DIR, 'smiley.gif')) 61 | with PIL.Image.open(os.path.join(DATA_DIR, 'smiley.gif')) as pillu: 62 | smiley2 = lib.from_pil(pillu) 63 | assert images_equal(smiley1, smiley2) 64 | 65 | 66 | # yields different kinds of file objects that contain the data from the file 67 | def open_file(path): 68 | with open(path, 'rb') as file: 69 | yield io.BytesIO(file.read()) 70 | file.seek(0) 71 | yield file 72 | 73 | 74 | def test_from_file_gif(): 75 | for lib in loader_libs: 76 | smiley1 = teek.Image(file=os.path.join(DATA_DIR, 'smiley.gif')) 77 | for file in open_file(os.path.join(DATA_DIR, 'smiley.gif')): 78 | smiley2 = lib.from_file(file) 79 | assert images_equal(smiley1, smiley2) 80 | 81 | 82 | @needs_image_loader 83 | def test_from_file_pil(): 84 | smileys = [] 85 | for file in open_file(os.path.join(DATA_DIR, 'smiley.jpg')): 86 | smileys.append(image_loader.from_file(file)) 87 | 88 | for smiley in smileys: 89 | assert smiley.width == 32 90 | assert smiley.height == 32 91 | assert images_equal(smiley, smileys[0]) 92 | 93 | 94 | @needs_image_loader 95 | @ignore_svglib_warnings 96 | @pytest.mark.slow 97 | def test_from_file_svg(): 98 | for filename in ['firefox.svg', 'rectangle.svg']: 99 | images = [] 100 | for file in open_file(os.path.join(DATA_DIR, filename)): 101 | images.append(image_loader.from_file(file)) 102 | 103 | for image in images: 104 | assert images_equal(image, images[0]) 105 | 106 | 107 | def test_from_bytes(): 108 | for lib in loader_libs: 109 | with open(os.path.join(DATA_DIR, 'smiley.gif'), 'rb') as file1: 110 | smiley1 = lib.from_file(file1) 111 | with open(os.path.join(DATA_DIR, 'smiley.gif'), 'rb') as file2: 112 | smiley2 = lib.from_bytes(file2.read()) 113 | assert images_equal(smiley1, smiley2) 114 | -------------------------------------------------------------------------------- /tests/extras/test_links.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import webbrowser 3 | 4 | import teek 5 | from teek.extras import links 6 | 7 | 8 | def test_links_clicking(): 9 | text = teek.Text(teek.Window()) 10 | text.insert(text.end, 'Blah') 11 | 12 | stuff = [] 13 | links.add_function_link( 14 | text, functools.partial(stuff.append, 1), (1, 0), (1, 2)) 15 | links.add_function_link( 16 | text, functools.partial(stuff.append, 2), (1, 2), (1, 4)) 17 | 18 | all_tag_names = (tag.name for tag in text.get_all_tags()) 19 | assert {'teek-extras-link-1', 20 | 'teek-extras-link-2', 21 | 'teek-extras-link-common'}.issubset(all_tag_names) 22 | 23 | assert text.get_tag('teek-extras-link-1').ranges() == [((1, 0), (1, 2))] 24 | assert text.get_tag('teek-extras-link-2').ranges() == [((1, 2), (1, 4))] 25 | 26 | text.get_tag('teek-extras-link-1').bindings['<1>'].run(None) 27 | text.get_tag('teek-extras-link-2').bindings['<1>'].run(None) 28 | assert stuff == [1, 2] 29 | 30 | 31 | def test_links_cursor_changes(): 32 | text = teek.Text(teek.Window()) 33 | text.config['cursor'] = 'clock' 34 | text.insert(text.end, 'abc') 35 | 36 | links.add_function_link(text, print, text.start, text.end) 37 | assert text.config['cursor'] == 'clock' 38 | 39 | for binding, cursor in [('', 'clock'), 40 | ('', 'hand2'), 41 | ('', 'clock')]: 42 | text.get_tag('teek-extras-link-common').bindings[binding].run(None) 43 | assert text.config['cursor'] == cursor 44 | 45 | 46 | URL = 'https://github.com/Akuli/teek' 47 | 48 | 49 | def test_add_url_link(monkeypatch, handy_callback): 50 | stuff = [] 51 | monkeypatch.setattr(webbrowser, 'open', stuff.append) 52 | 53 | text = teek.Text(teek.Window()) 54 | text.insert(text.end, 'teek') 55 | links.add_url_link(text, URL, text.start, text.end) 56 | text.get_tag('teek-extras-link-1').bindings['<1>'].run(None) 57 | assert stuff == [URL] 58 | -------------------------------------------------------------------------------- /tests/extras/test_more_dialogs.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import teek 4 | from teek._widgets.windows import WmMixin 5 | from teek.extras import more_dialogs 6 | 7 | 8 | def test_ask_string(handy_callback, monkeypatch): 9 | def validator(string): 10 | if string not in ('asd', 'wat'): 11 | raise ValueError 12 | return string.upper() 13 | 14 | real_run = more_dialogs._EntryDialog.run 15 | 16 | @handy_callback 17 | def fake_run(entrydialog): 18 | 19 | @handy_callback 20 | def fake_wait_window(): 21 | [label] = [widget for widget in entrydialog.window.winfo_children() 22 | if isinstance(widget, teek.Label)] 23 | [entry] = [widget for widget in entrydialog.window.winfo_children() 24 | if isinstance(widget, teek.Entry)] 25 | 26 | assert entrydialog.window.toplevel.title == 'A' 27 | assert label.config['text'] == 'B' 28 | 29 | def get_stuff(): 30 | assert entry.text == entrydialog.var.get() 31 | return (entry.text, entrydialog.ok_button.config['state'], 32 | entrydialog.result) 33 | 34 | assert get_stuff() == ('boo', 'disabled', None) 35 | entry.text = 'a' 36 | assert get_stuff() == ('a', 'disabled', None) 37 | entry.text = 'asd' 38 | assert get_stuff() == ('asd', 'normal', 'ASD') 39 | entry.text = 'b' 40 | assert get_stuff() == ('b', 'disabled', None) 41 | entry.text = 'wat' 42 | assert get_stuff() == ('wat', 'normal', 'WAT') 43 | entry.text = 'c' 44 | assert get_stuff() == ('c', 'disabled', None) 45 | 46 | # the button is disabled now, so on_ok must do nothing 47 | entrydialog.on_ok() 48 | assert get_stuff() == ('c', 'disabled', None) 49 | assert entrydialog.window.winfo_exists() 50 | 51 | entry.text = 'wat' 52 | assert get_stuff() == ('wat', 'normal', 'WAT') 53 | entrydialog.on_ok() 54 | assert not entrydialog.window.winfo_exists() 55 | 56 | entrydialog.window.wait_window = fake_wait_window 57 | result = real_run(entrydialog) 58 | assert fake_wait_window.ran_once() 59 | return result 60 | 61 | monkeypatch.setattr(more_dialogs._EntryDialog, 'run', fake_run) 62 | assert more_dialogs.ask_string( 63 | 'A', 'B', validator=validator, initial_value='boo', 64 | parent=teek.Window()) == 'WAT' 65 | assert fake_run.ran_once() 66 | 67 | 68 | def test_ask_string_canceling(handy_callback, monkeypatch): 69 | @handy_callback 70 | def fake_run(entrydialog): 71 | [entry] = [widget for widget in entrydialog.window.winfo_children() 72 | if isinstance(widget, teek.Entry)] 73 | entry.text = 'a' 74 | 75 | assert entrydialog.window.winfo_exists() 76 | assert entrydialog.result == 'a' 77 | entrydialog.on_cancel() 78 | assert not entrydialog.window.winfo_exists() 79 | assert entrydialog.result is None 80 | 81 | monkeypatch.setattr(more_dialogs._EntryDialog, 'run', fake_run) 82 | more_dialogs.ask_string('A', 'B') 83 | assert fake_run.ran_once() 84 | 85 | 86 | def test_ask_integer(handy_callback, monkeypatch): 87 | with pytest.raises(ValueError): 88 | more_dialogs.ask_integer('a', 'b', range(10, 1, -1)) 89 | 90 | real_entrydialog = more_dialogs._EntryDialog 91 | 92 | @handy_callback 93 | def fake_entrydialog(*args): 94 | a, b, creator, validator, initial_value, parent = args 95 | assert a == 'a' 96 | assert b == 'b' 97 | assert callable(creator) 98 | assert callable(validator) 99 | assert initial_value == initial 100 | assert parent is None or isinstance(parent, WmMixin) 101 | 102 | entrydialog = real_entrydialog(*args) 103 | entrydialog.run = lambda: 123 104 | 105 | [spinbox] = [widget for widget in entrydialog.window.winfo_children() 106 | if isinstance(widget, teek.Spinbox)] 107 | 108 | assert spinbox.text == str(initial) 109 | assert entrydialog.result == initial 110 | spinbox.text = 'asd' 111 | assert entrydialog.result is None 112 | spinbox.text = '12345678' 113 | assert entrydialog.result is None 114 | spinbox.text = str(initial) 115 | assert entrydialog.result == initial 116 | 117 | for item in spinbox_config.items(): 118 | assert item in list(spinbox.config.items()) 119 | 120 | return entrydialog 121 | 122 | monkeypatch.setattr(more_dialogs, '_EntryDialog', fake_entrydialog) 123 | assert fake_entrydialog.ran == 0 124 | 125 | spinbox_config = {'from': 10, 'to': 30, 'increment': 5} 126 | initial = 10 127 | assert more_dialogs.ask_integer('a', 'b', range(10, 33, 5)) == 123 128 | assert fake_entrydialog.ran == 1 129 | 130 | # the spinbox's config contains strings because spinboxes can be used for 131 | # non-integer things too 132 | spinbox_config = {'values': ['1', '4', '3']} 133 | initial = 1 134 | assert more_dialogs.ask_integer('a', 'b', [1, 4, 3]) == 123 135 | assert fake_entrydialog.ran == 2 136 | 137 | initial = 4 138 | assert more_dialogs.ask_integer('a', 'b', [1, 4, 3], initial_value=4, 139 | parent=teek.Window()) == 123 140 | assert fake_entrydialog.ran == 3 141 | 142 | with pytest.raises(ValueError): 143 | more_dialogs.ask_integer('a', 'b', [1, 4, 3], initial_value=666) 144 | -------------------------------------------------------------------------------- /tests/extras/test_soup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import urllib.request 4 | 5 | import pytest 6 | bs4 = pytest.importorskip('bs4') # noqa 7 | pytest.importorskip('lxml') # noqa 8 | 9 | import teek 10 | from teek.extras.soup import SoupViewer 11 | pytest.importorskip('teek.extras.image_loader') 12 | 13 | 14 | DATA_DIR = os.path.join( 15 | os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data') 16 | FIREFOX_SVG_URL = 'file://' + urllib.request.pathname2url( 17 | os.path.join(DATA_DIR, 'firefox.svg')) 18 | 19 | # from test_image_loader.py 20 | ignore_svglib_warnings = pytest.mark.filterwarnings( 21 | "ignore:The 'warn' method is deprecated") 22 | 23 | big_html = ''' 24 |

      Header 1

      25 |

      Header 2

      26 |

      Header 3

      27 |

      Header 4

      28 |
      Header 5
      29 |
      Header 6
      30 | 31 |

      The below code uses the print function.

      32 | 33 |
      print("Hello World")
      34 | 35 |

      Line
      break

      36 | 37 | Italics 1Italics 2 38 | Bold 1Bold 2 39 | 40 | 42 |

      This contains 43 | 44 | many spaces

      45 | 46 |
        47 |
      • One
      • 48 |
      • Two
      • 49 |
      • Three
      • 50 |
      51 | 52 |
        53 |
      1. One
      2. 54 |
      3. Two
      4. 55 |
      5. Three
      6. 56 |
      57 | 58 |

      Link

      59 | 60 | firefox pic alt 61 | '''.format(FIREFOX_SVG_URL=FIREFOX_SVG_URL) 62 | 63 | 64 | def get_tag_names(widget, string): 65 | # TODO: add search to teek 66 | start = teek.tcl_call(widget.TextIndex, widget, 'search', string, '1.0') 67 | end = start.forward(chars=len(string)) 68 | tags = set(widget.get_all_tags(start)) 69 | 70 | # must be same tags in the whole text 71 | index = start 72 | while index < end: 73 | assert set(widget.get_all_tags(index)) == tags 74 | index = index.forward(chars=1) 75 | 76 | return {tag.name[len('soup-'):] for tag in tags 77 | if tag.name.startswith('soup-')} 78 | 79 | 80 | def create_souped_widget(html, **kwargs): 81 | widget = teek.Text(teek.Window()) 82 | souper = SoupViewer(widget, **kwargs) 83 | souper.create_tags() 84 | for element in bs4.BeautifulSoup(html, 'lxml').body: 85 | souper.add_soup(element) 86 | 87 | return (souper, widget) 88 | 89 | 90 | def test_tagging(): 91 | souper, widget = create_souped_widget(big_html, threads=False) 92 | 93 | for h in '123456': 94 | assert get_tag_names(widget, 'Header ' + h) == {'h' + h} 95 | assert get_tag_names(widget, 'print') == {'p', 'code'} 96 | assert get_tag_names(widget, 'print(') == {'pre'} 97 | assert 'Line\nbreak' in widget.get() 98 | assert get_tag_names(widget, 'Italics 1') == {'i'} 99 | assert get_tag_names(widget, 'Italics 2') == {'em'} 100 | assert get_tag_names(widget, 'Bold 1') == {'b'} 101 | assert get_tag_names(widget, 'Bold 2') == {'strong'} 102 | assert 'This contains many spaces' in widget.get() 103 | assert get_tag_names(widget, '\N{bullet} One') == {'li', 'ul'} 104 | assert get_tag_names(widget, '1. One') == {'li', 'ol'} 105 | assert get_tag_names(widget, 'Link') == {'p', 'a'} 106 | assert get_tag_names(widget, 'firefox pic alt') == {'img'} 107 | 108 | 109 | @pytest.mark.slow 110 | def test_image_doesnt_load_without_threads(): 111 | souper, widget = create_souped_widget(big_html, threads=False) 112 | assert 'firefox pic alt' in widget.get() 113 | 114 | end = time.time() + 0.5 115 | while time.time() < end: 116 | teek.update() 117 | assert 'firefox pic alt' in widget.get() 118 | 119 | 120 | @ignore_svglib_warnings 121 | @pytest.mark.slow 122 | def test_image_loads_with_threads(deinit_threads): 123 | teek.init_threads() 124 | souper, widget = create_souped_widget(big_html) # threads=True is default 125 | 126 | assert 'firefox pic alt' in widget.get() 127 | time.sleep(1) # this used to be 0.1 and it made tests fail randomly 128 | teek.update() 129 | assert 'firefox pic alt' not in widget.get() 130 | 131 | 132 | def test_unknown_tag(): 133 | with pytest.warns(RuntimeWarning, 134 | match=(r'contains a ', threads=False) 138 | assert widget.get() == 'console.log("hey")' 139 | -------------------------------------------------------------------------------- /tests/extras/test_tooltips.py: -------------------------------------------------------------------------------- 1 | import time 2 | import types 3 | import pytest 4 | 5 | import teek 6 | from teek.extras import tooltips 7 | 8 | 9 | def run_event_loop(for_how_long): 10 | # this is dumb 11 | start = time.time() 12 | while time.time() < start + for_how_long: 13 | teek.update() 14 | 15 | 16 | @pytest.mark.slow 17 | def test_set_tooltip(): 18 | window = teek.Window() 19 | assert not hasattr(window, '_tooltip_manager') 20 | 21 | tooltips.set_tooltip(window, None) 22 | assert not hasattr(window, '_tooltip_manager') 23 | 24 | tooltips.set_tooltip(window, 'Boo') 25 | assert window._tooltip_manager.text == 'Boo' 26 | 27 | tooltips.set_tooltip(window, None) 28 | assert window._tooltip_manager.text is None 29 | 30 | tooltips.set_tooltip(window, 'lol') 31 | assert window._tooltip_manager.text == 'lol' 32 | 33 | N = types.SimpleNamespace # because pep8 line length 34 | 35 | assert not window._tooltip_manager.got_mouse 36 | window._tooltip_manager.enter(N(widget=window, rootx=123, rooty=456)) 37 | assert window._tooltip_manager.got_mouse 38 | assert window._tooltip_manager.mousex == 123 39 | assert window._tooltip_manager.mousey == 456 40 | 41 | window._tooltip_manager.motion(N(rootx=789, rooty=101112)) 42 | assert window._tooltip_manager.got_mouse 43 | assert window._tooltip_manager.mousex == 789 44 | assert window._tooltip_manager.mousey == 101112 45 | 46 | run_event_loop(1.1) 47 | assert window._tooltip_manager.tipwindow is not None 48 | assert window._tooltip_manager.got_mouse 49 | window._tooltip_manager.leave(N(widget=window)) 50 | assert not window._tooltip_manager.got_mouse 51 | assert window._tooltip_manager.tipwindow is None 52 | 53 | # what happens if the window gets destroyed before it's supposed to show? 54 | window._tooltip_manager.enter(N(widget=window, rootx=1, rooty=2)) 55 | window._tooltip_manager.leave(N(widget=window)) 56 | assert window._tooltip_manager.tipwindow is None 57 | run_event_loop(1.1) 58 | assert window._tooltip_manager.tipwindow is None 59 | -------------------------------------------------------------------------------- /tests/test_dialog.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import os 3 | 4 | import pytest 5 | 6 | import teek 7 | 8 | 9 | @pytest.fixture 10 | def fake_dialog_command(fake_command): 11 | @contextlib.contextmanager 12 | def faker(name, options, return_value): 13 | with fake_command(name, return_value) as called: 14 | yield 15 | [args] = called 16 | assert len(args) % 2 == 0 17 | assert dict(zip(args[0::2], args[1::2])) == options 18 | 19 | return faker 20 | 21 | 22 | def test_message_boxes(fake_dialog_command): 23 | with fake_dialog_command('tk_messageBox', { 24 | '-type': 'ok', 25 | '-icon': 'info', 26 | '-title': 'a', 27 | '-message': 'b', 28 | '-detail': 'c'}, 'ok'): 29 | assert teek.dialog.info('a', 'b', 'c') is None 30 | 31 | for icon in ['info', 'warning', 'error']: 32 | with fake_dialog_command('tk_messageBox', { 33 | '-type': 'ok', 34 | '-icon': icon, 35 | '-title': 'a', 36 | '-message': 'b'}, 'ok'): 37 | assert getattr(teek.dialog, icon)('a', 'b') is None 38 | 39 | for func, ok, icon in [(teek.dialog.ok_cancel, 'ok', 'question'), 40 | (teek.dialog.retry_cancel, 'retry', 'warning')]: 41 | with fake_dialog_command('tk_messageBox', { 42 | '-type': ok + 'cancel', 43 | '-icon': icon, 44 | '-title': 'a', 45 | '-message': 'b'}, ok): 46 | assert func('a', 'b') is True 47 | 48 | with fake_dialog_command('tk_messageBox', { 49 | '-type': ok + 'cancel', 50 | '-icon': icon, 51 | '-title': 'a', 52 | '-message': 'b'}, 'cancel'): 53 | assert func('a', 'b') is False 54 | 55 | for string, boolean in [('yes', True), ('no', False)]: 56 | with fake_dialog_command('tk_messageBox', { 57 | '-type': 'yesno', 58 | '-icon': 'question', 59 | '-title': 'a', 60 | '-message': 'b'}, string): 61 | assert teek.dialog.yes_no('a', 'b') is boolean 62 | 63 | for function_name, icon in [('yes_no_cancel', 'question'), 64 | ('abort_retry_ignore', 'error')]: 65 | for answer in function_name.split('_'): 66 | with fake_dialog_command('tk_messageBox', { 67 | '-type': function_name.replace('_', ''), 68 | '-icon': icon, 69 | '-title': 'a', 70 | '-message': 'b'}, answer): 71 | assert getattr(teek.dialog, function_name)('a', 'b') == answer 72 | 73 | 74 | def test_color(fake_dialog_command): 75 | with fake_dialog_command('tk_chooseColor', {}, '#ffffff'): 76 | assert teek.dialog.color() == teek.Color('white') 77 | 78 | window = teek.Window() 79 | with fake_dialog_command('tk_chooseColor', { 80 | '-title': 'toot', 81 | '-initialcolor': teek.Color('maroon').to_tcl(), 82 | '-parent': window.toplevel.to_tcl()}, '#ffffff'): 83 | assert teek.dialog.color( 84 | initialcolor=teek.Color('maroon'), 85 | parent=window, 86 | title='toot', 87 | ) == teek.Color('white') 88 | 89 | with fake_dialog_command('tk_chooseColor', {}, ''): 90 | assert teek.dialog.color() is None 91 | 92 | 93 | def test_open_file(fake_dialog_command): 94 | window = teek.Window() 95 | 96 | def check(func, python_return, tcl_command, tcl_return, tcl_options=None): 97 | tcl_options = {} if tcl_options is None else tcl_options.copy() 98 | 99 | with fake_dialog_command(tcl_command, tcl_options, tcl_return): 100 | assert func() == python_return 101 | 102 | tcl_options['-parent'] = window.toplevel.to_tcl() 103 | with fake_dialog_command(tcl_command, tcl_options, tcl_return): 104 | assert func(parent=window) == python_return 105 | 106 | # aa = absolute a 107 | aa = os.path.abspath('a') 108 | ab = os.path.abspath('b') 109 | 110 | check(teek.dialog.open_file, None, 'tk_getOpenFile', '') 111 | check(teek.dialog.open_file, aa, 'tk_getOpenFile', 'a') 112 | check(teek.dialog.open_multiple_files, [], 'tk_getOpenFile', '', 113 | {'-multiple': '1'}) 114 | check(teek.dialog.open_multiple_files, [aa, ab], 'tk_getOpenFile', 115 | ['a', 'b'], {'-multiple': '1'}) 116 | check(teek.dialog.save_file, None, 'tk_getSaveFile', '') 117 | check(teek.dialog.save_file, aa, 'tk_getSaveFile', 'a') 118 | check(teek.dialog.directory, None, 'tk_chooseDirectory', '') 119 | check(teek.dialog.directory, aa, 'tk_chooseDirectory', 'a') 120 | 121 | with pytest.raises(TypeError) as error: 122 | teek.dialog.open_file(multiple=True) 123 | assert 'open_multiple_files()' in str(error.value) 124 | -------------------------------------------------------------------------------- /tests/test_docs_stuff.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | 6 | DOCS_DIR = os.path.join( 7 | os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'docs') 8 | 9 | 10 | @pytest.mark.slow 11 | def test_manpage_url_checker(monkeypatch, capsys): 12 | monkeypatch.syspath_prepend(DOCS_DIR) 13 | import extensions 14 | 15 | asd_url = extensions.get_manpage_url('asd', 'tk') 16 | assert isinstance(asd_url, str) 17 | 18 | monkeypatch.setitem(os.environ, 'READTHEDOCS', 'True') 19 | assert capsys.readouterr() == ('', '') 20 | with pytest.raises(Exception): 21 | extensions.check_url(asd_url) 22 | 23 | output, errors = capsys.readouterr() 24 | assert 'extensions.py: checking if url exists' in output 25 | assert not errors 26 | -------------------------------------------------------------------------------- /tests/test_examples.py: -------------------------------------------------------------------------------- 1 | import os 2 | import teek 3 | 4 | try: 5 | # examples/soup.py does bs4.BeautifulSoup(html_string, 'lxml') 6 | import bs4 # noqa 7 | import lxml # noqa 8 | soup_py_can_run = True 9 | except ImportError: 10 | soup_py_can_run = False 11 | 12 | 13 | EXAMPLES_DIR = os.path.join( 14 | os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 15 | 'examples') 16 | 17 | 18 | # magic ftw 19 | # TODO: this doesn't work with pytest-xdist and pythons that don't have 20 | # ordered dict, i have no idea why and i don't know how to fix it 21 | def _create_test_function(filename): 22 | if filename == 'soup.py' and not soup_py_can_run: 23 | return 24 | 25 | with open(os.path.join(EXAMPLES_DIR, filename), 'r') as file: 26 | code = file.read() 27 | 28 | def func(monkeypatch, handy_callback): 29 | @handy_callback 30 | def fake_run(): 31 | pass 32 | 33 | with monkeypatch.context() as monkey: 34 | monkey.setattr(teek, 'run', fake_run) 35 | exec(code, {'__file__': os.path.join(EXAMPLES_DIR, filename)}) 36 | 37 | assert fake_run.ran_once() 38 | 39 | # make sure that nothing breaks if the real .run() is called 40 | teek.update() 41 | teek.after_idle(teek.quit) 42 | teek.run() 43 | 44 | func.__name__ = func.__qualname__ = 'test_' + filename.replace('.', '_') 45 | globals()[func.__name__] = func 46 | 47 | 48 | for filename in sorted(os.listdir(EXAMPLES_DIR)): 49 | if filename.endswith('.py') and not filename.startswith('_'): 50 | _create_test_function(filename) 51 | -------------------------------------------------------------------------------- /tests/test_font.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import teek 4 | from teek._tcl_calls import to_tcl 5 | 6 | 7 | def get_test_family(): 8 | """Get font family with actual size equal to given size 9 | """ 10 | for family in teek.Font.families(): 11 | font = teek.Font((family, 42)) 12 | 13 | if font.size == 42: 14 | return family 15 | 16 | return teek.Font.families()[0] 17 | 18 | 19 | def test_font_magic_new_method(): 20 | font = teek.Font('a_font_with_this_name_does_not_exist') 21 | assert isinstance(font, teek.Font) 22 | assert not isinstance(font, teek.NamedFont) 23 | 24 | teek.tcl_eval(None, 'font create test_font_name') 25 | named_font = teek.Font('test_font_name') 26 | assert isinstance(named_font, teek.NamedFont) 27 | named_font.delete() 28 | 29 | 30 | def test_repr_eq_hash(): 31 | font_family = get_test_family() 32 | font = teek.Font((font_family, 12)) 33 | named_font = teek.NamedFont('asda') 34 | assert repr(font) == "Font(('%s', 12))" % font_family 35 | assert repr(named_font) == "NamedFont('asda')" 36 | 37 | another_named_font = teek.NamedFont('asda') 38 | assert named_font == another_named_font 39 | assert {named_font: 'toot'}[another_named_font] == 'toot' 40 | 41 | def all_names(): 42 | return (font.to_tcl() 43 | for font in teek.NamedFont.get_all_named_fonts()) 44 | 45 | assert 'asda' in all_names() 46 | named_font.delete() 47 | assert 'asda' not in all_names() 48 | 49 | assert font != 'toot' 50 | assert named_font != 'toot' 51 | 52 | 53 | def test_from_and_to_tcl(): 54 | description = [get_test_family(), 42, 'bold'] 55 | descriptiony_font = teek.Font(description) 56 | assert descriptiony_font.to_tcl() == to_tcl(description) 57 | assert teek.Font.from_tcl(description) == descriptiony_font 58 | 59 | teek.tcl_eval(None, 'font create test_font_name') 60 | named_font = teek.NamedFont.from_tcl('test_font_name') 61 | assert isinstance(named_font, teek.NamedFont) 62 | assert named_font.to_tcl() == 'test_font_name' 63 | named_font.delete() 64 | 65 | 66 | def test_properties(): 67 | font_family = get_test_family() 68 | anonymous_font = teek.Font((font_family, 42, 'bold', 'underline')) 69 | named_font = teek.NamedFont( 70 | family=font_family, size=42, weight='bold', underline=True) 71 | 72 | # just to make debugging easier because these facts are needed below 73 | assert not isinstance(anonymous_font, teek.NamedFont) 74 | assert isinstance(named_font, teek.NamedFont) 75 | 76 | for font in [anonymous_font, named_font]: 77 | assert font.size == 42 78 | assert font.weight == 'bold' 79 | assert font.slant == 'roman' 80 | assert font.underline is True 81 | assert font.overstrike is False 82 | 83 | # the actual properties might differ from the font specification, e.g. 84 | # "Helvetica" is "Nimbus Sans L" on my system 85 | assert isinstance(font.family, str) 86 | 87 | # test setting error 88 | with pytest.raises(AttributeError) as error: 89 | anonymous_font.weight = 'normal' 90 | assert '.to_named_font()' in str(error.value) 91 | 92 | # test successful setting 93 | assert named_font.underline is True 94 | named_font.underline = False 95 | assert named_font.underline is False 96 | 97 | assert not hasattr(anonymous_font, 'delete') 98 | named_font.delete() 99 | 100 | 101 | def test_measure(): 102 | assert teek.Font((get_test_family(), 42, 'bold')).measure('') == 0 103 | 104 | 105 | def test_metrics(): 106 | metrics = teek.Font((get_test_family(), 42, 'bold')).metrics() 107 | assert isinstance(metrics['ascent'], int) 108 | assert isinstance(metrics['descent'], int) 109 | assert isinstance(metrics['linespace'], int) 110 | assert isinstance(metrics['fixed'], bool) 111 | 112 | 113 | def test_families(): 114 | for at in [True, False]: 115 | families = teek.Font.families(allow_at_prefix=at) 116 | assert isinstance(families, list) 117 | assert families 118 | for family in families: 119 | assert isinstance(family, str) 120 | if not at: 121 | assert not family.startswith('@') 122 | 123 | # default should be False 124 | if not at: 125 | assert set(teek.Font.families()) == set(families) 126 | 127 | 128 | def fonts_are_similar(font1, font2): 129 | return (font1.family == font2.family and 130 | font1.size == font2.size and 131 | font1.weight == font2.weight and 132 | font1.slant == font2.slant and 133 | font1.underline is font2.underline and 134 | font1.overstrike is font2.overstrike) 135 | 136 | 137 | def test_to_named_font(): 138 | anonymous = teek.Font((get_test_family(), 42)) 139 | named = anonymous.to_named_font() 140 | assert isinstance(named, teek.NamedFont) 141 | assert fonts_are_similar(anonymous, named) 142 | 143 | named2 = named.to_named_font() # creates a copy 144 | assert isinstance(named2, teek.NamedFont) 145 | assert named != named2 # it is a copy 146 | assert fonts_are_similar(named, named2) 147 | 148 | named2.size = 18 149 | assert named.size != named2.size 150 | named2.size = 42 151 | assert fonts_are_similar(named, named2) 152 | 153 | named.delete() 154 | named2.delete() 155 | 156 | 157 | def test_special_font_names(): 158 | assert isinstance(teek.Font('TkFixedFont'), teek.NamedFont) 159 | assert isinstance(teek.Font.from_tcl('TkFixedFont'), teek.NamedFont) 160 | -------------------------------------------------------------------------------- /tests/test_geometry_managers.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import teek 4 | 5 | 6 | def test_pack(): 7 | window = teek.Window() 8 | button = teek.Button(window) 9 | button.pack(fill='both', expand=True) 10 | 11 | pack_info = button.pack_info() 12 | assert pack_info['in'] is window 13 | assert pack_info['side'] == 'top' 14 | assert pack_info['fill'] == 'both' 15 | assert pack_info['expand'] is True 16 | assert pack_info['anchor'] == 'center' 17 | 18 | for option in ['padx', 'pady']: 19 | assert isinstance(pack_info[option], list) 20 | assert len(pack_info[option]) in {1, 2} 21 | for item in pack_info[option]: 22 | assert isinstance(item, teek.ScreenDistance) 23 | for option in ['ipadx', 'ipady']: 24 | assert isinstance(pack_info[option], teek.ScreenDistance) 25 | 26 | button.pack_forget() 27 | with pytest.raises(teek.TclError): 28 | button.pack_info() 29 | 30 | button.pack(**pack_info) 31 | assert button.pack_info() == pack_info 32 | button.pack_forget() 33 | 34 | assert window.pack_slaves() == [] 35 | label1 = teek.Label(window, 'label one') 36 | label1.pack() 37 | label2 = teek.Label(window, 'label two') 38 | label2.pack() 39 | assert window.pack_slaves() == [label1, label2] 40 | 41 | frame = teek.Frame(window) 42 | label2.pack(in_=frame) 43 | assert window.pack_slaves() == [label1] 44 | assert frame.pack_slaves() == [label2] 45 | 46 | 47 | def test_grid(): 48 | # grid shares a lot of code with pack, so no need to test everything 49 | # separately 50 | window = teek.Window() 51 | button = teek.Button(window) 52 | button.grid(column=1, rowspan=2, sticky='nswe') 53 | 54 | grid_info = button.grid_info() 55 | assert grid_info['column'] == 1 56 | assert grid_info['columnspan'] == 1 57 | assert grid_info['row'] == 0 58 | assert grid_info['rowspan'] == 2 59 | assert grid_info['in'] is button.parent 60 | assert grid_info['padx'] == grid_info['pady'] == [teek.ScreenDistance(0)] 61 | assert grid_info['ipadx'] == grid_info['ipady'] == teek.ScreenDistance(0) 62 | assert grid_info['sticky'] == 'nesw' # not 'nswe' for some reason 63 | 64 | assert window.grid_slaves() == [button] 65 | 66 | 67 | def test_grid_row_and_column_objects(check_config_types): 68 | window = teek.Window() 69 | assert window.grid_rows == [] 70 | assert window.grid_columns == [] 71 | 72 | # a new list is created every time 73 | assert window.grid_rows is not window.grid_rows 74 | assert window.grid_rows == window.grid_rows 75 | 76 | label = teek.Label(window) 77 | label.grid() 78 | 79 | for rows_columns in [window.grid_rows, window.grid_columns]: 80 | assert isinstance(rows_columns, list) 81 | assert len(rows_columns) == 1 82 | row_column = rows_columns[0] 83 | 84 | assert row_column.get_slaves() == [label] 85 | check_config_types(row_column.config, 'grid row or column object') 86 | 87 | row_column.config['weight'] = 4 88 | assert isinstance(row_column.config['weight'], float) 89 | assert row_column.config['weight'] == 4.0 90 | 91 | assert row_column == row_column 92 | assert row_column != 'toot' 93 | assert {row_column: 'woot'}[row_column] == 'woot' 94 | 95 | 96 | def test_place(): 97 | window = teek.Window() 98 | button = teek.Button(window) 99 | button.place(x=123, rely=0.5) 100 | 101 | place_info = button.place_info() 102 | assert place_info['anchor'] == 'nw' 103 | assert place_info['bordermode'] == 'inside' 104 | assert place_info['in'] is window 105 | assert place_info['x'] == teek.ScreenDistance(123) 106 | assert place_info['rely'] == 0.5 107 | 108 | assert isinstance(place_info['relx'], float) 109 | assert isinstance(place_info['rely'], float) 110 | assert isinstance(place_info['x'], teek.ScreenDistance) 111 | assert isinstance(place_info['y'], teek.ScreenDistance) 112 | 113 | assert place_info['width'] is None 114 | assert place_info['height'] is None 115 | assert place_info['relwidth'] is None 116 | assert place_info['relheight'] is None 117 | 118 | button.place_forget() 119 | assert button.place_info() == {} 120 | 121 | button.place(**place_info) 122 | assert button.place_info() == place_info 123 | button.place_forget() 124 | 125 | assert window.place_slaves() == [] 126 | label1 = teek.Label(window, 'label one') 127 | label1.place(x=1) 128 | label2 = teek.Label(window, 'label two') 129 | label2.place(x=2) 130 | assert set(window.place_slaves()) == {label1, label2} # allow any order 131 | 132 | frame = teek.Frame(window) 133 | label2.place(in_=frame) 134 | assert window.place_slaves() == [label1] 135 | assert frame.place_slaves() == [label2] 136 | 137 | 138 | def test_place_special_error(): 139 | label = teek.Label(teek.Window()) 140 | with pytest.raises(TypeError) as error: 141 | label.place() 142 | 143 | assert str(error.value).startswith( 144 | "cannot call widget.place() without any arguments, do e.g. ") 145 | -------------------------------------------------------------------------------- /tests/test_images.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import tempfile 4 | 5 | import pytest 6 | 7 | import teek 8 | 9 | 10 | SMILEY_PATH = os.path.join( 11 | os.path.dirname(os.path.abspath(__file__)), 'data', 'smiley.gif') 12 | 13 | 14 | def test_width_and_height(): 15 | image = teek.Image(file=SMILEY_PATH) 16 | assert image.width == 32 17 | assert image.height == 32 18 | 19 | 20 | # tests that use this so that it returns True (for loops run) are marked slow 21 | def slow_content_eq_check(image1, image2): 22 | # magic ftw, the double for loop is slow with big images which is why the 23 | # test images are small 24 | return ( 25 | image1.width == image2.width and 26 | image1.height == image2.height and 27 | all( 28 | image1.get(x, y) == image2.get(x, y) 29 | for x in range(image1.width) 30 | for y in range(image1.height) 31 | ) 32 | ) 33 | 34 | 35 | @pytest.mark.slow 36 | def test_images_contain_same_data_util(): 37 | image1 = teek.Image(file=SMILEY_PATH) 38 | image2 = teek.Image(file=SMILEY_PATH) 39 | assert slow_content_eq_check(image1, image1) 40 | assert slow_content_eq_check(image1, image2) 41 | 42 | image3 = teek.Image(width=1, height=2) 43 | assert not slow_content_eq_check(image1, image3) 44 | 45 | 46 | def test_config(check_config_types): 47 | check_config_types(teek.Image(file=SMILEY_PATH).config, 'Image') 48 | 49 | 50 | @pytest.mark.slow 51 | def test_data_base64(): 52 | with open(SMILEY_PATH, 'rb') as file: 53 | binary_data = file.read() 54 | 55 | assert slow_content_eq_check(teek.Image(file=SMILEY_PATH), 56 | teek.Image(data=binary_data)) 57 | 58 | 59 | @pytest.mark.slow 60 | def test_from_to_tcl(): 61 | image1 = teek.Image(file=SMILEY_PATH) 62 | teek.tcl_eval(None, 'proc returnArg {arg} {return $arg}') 63 | image2 = teek.tcl_call(teek.Image, 'returnArg', image1) 64 | assert image1.to_tcl() == image2.to_tcl() # no implicit copying 65 | assert image1 is not image2 # no implicit cache 66 | assert slow_content_eq_check(image1, image2) 67 | 68 | 69 | def test_eq_hash(): 70 | image1 = teek.Image(file=SMILEY_PATH) 71 | image2 = teek.Image.from_tcl(image1.to_tcl()) 72 | image3 = teek.Image(file=SMILEY_PATH) 73 | 74 | assert image1 == image2 75 | assert {image1: 'woot'}[image2] == 'woot' 76 | assert image1 != image3 77 | assert image2 != image3 78 | assert image2 != 'woot' 79 | 80 | 81 | def test_delete_and_all_images(): 82 | old_images = set(teek.Image.get_all_images()) 83 | new_image = teek.Image() 84 | assert set(teek.Image.get_all_images()) == old_images | {new_image} 85 | new_image.delete() 86 | assert set(teek.Image.get_all_images()) == old_images 87 | 88 | 89 | def test_repr(): 90 | image = teek.Image(file=SMILEY_PATH) 91 | assert repr(image) == '' % repr(SMILEY_PATH) 92 | image.delete() 93 | assert repr(image) == '' % repr(SMILEY_PATH) 94 | 95 | image2 = teek.Image(width=123, height=456) 96 | assert repr(image2) == '' 97 | image2.delete() 98 | assert repr(image2) == '' 99 | 100 | 101 | def test_in_use(): 102 | image = teek.Image(file=SMILEY_PATH) 103 | assert image.in_use() is False 104 | window = teek.Window() 105 | assert image.in_use() is False 106 | 107 | widget = teek.Label(window, image=image) 108 | assert image.in_use() is True 109 | widget2 = teek.Label(window, image=image) 110 | assert image.in_use() is True 111 | widget.destroy() 112 | assert image.in_use() is True 113 | widget2.destroy() 114 | assert image.in_use() is False 115 | 116 | 117 | def test_blank(): 118 | image1 = teek.Image(file=SMILEY_PATH) 119 | image2 = image1.copy() 120 | image2.blank() 121 | assert not slow_content_eq_check(image1, image2) # not slow 122 | 123 | 124 | @pytest.mark.slow 125 | def test_read(): 126 | image1 = teek.Image(file=SMILEY_PATH) 127 | assert image1.width > 0 and image1.height > 0 128 | 129 | image2 = teek.Image() 130 | assert image2.width == image2.height == 0 131 | image2.read(SMILEY_PATH) 132 | assert (image2.width, image2.height) == (image1.width, image1.height) 133 | assert slow_content_eq_check(image1, image2) 134 | 135 | 136 | def test_redither(): 137 | # no idea what this should do, so let's test that it does something... 138 | # except that it doesn't seem to actually do anything to the smiley, so 139 | # just test that it calls the Tcl redither command 140 | called = [] 141 | fake_image = teek.create_command(called.append, [str]) 142 | teek.Image.from_tcl(fake_image).redither() 143 | assert called == ['redither'] 144 | 145 | 146 | def test_transparency(): 147 | image = teek.Image(file=SMILEY_PATH) 148 | x = random.randint(0, image.width - 1) 149 | y = random.randint(0, image.height - 1) 150 | assert image.transparency_get(x, y) is False 151 | image.transparency_set(x, y, True) 152 | assert image.transparency_get(x, y) is True 153 | image.transparency_set(x, y, False) 154 | assert image.transparency_get(x, y) is False 155 | 156 | 157 | @pytest.mark.slow 158 | def test_write(): 159 | image1 = teek.Image(file=SMILEY_PATH) 160 | with tempfile.TemporaryDirectory() as tmpdir: 161 | asd = os.path.join(tmpdir, 'asd.gif') 162 | image1.write(asd, format='gif') 163 | image2 = teek.Image(file=asd) 164 | assert slow_content_eq_check(image1, image2) 165 | 166 | 167 | def test_get_bytes(): 168 | image = teek.Image(file=SMILEY_PATH) 169 | 170 | with tempfile.TemporaryDirectory() as tmpdir: 171 | asd = os.path.join(tmpdir, 'asd.gif') 172 | image.write(asd, format='gif') 173 | with open(asd, 'rb') as file: 174 | actual_gif_bytes = file.read() 175 | 176 | assert image.get_bytes('gif') == actual_gif_bytes 177 | -------------------------------------------------------------------------------- /tests/test_platform_info.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import teek 4 | 5 | 6 | def test_versions(): 7 | for version in [teek.TCL_VERSION, teek.TK_VERSION]: 8 | assert isinstance(version, tuple) 9 | major, minor = version 10 | assert isinstance(major, int) 11 | assert isinstance(minor, int) 12 | 13 | 14 | def test_version_check(monkeypatch): 15 | with pytest.raises(AttributeError): 16 | teek.version_check 17 | 18 | from teek import _platform_info 19 | monkeypatch.setattr(_platform_info, 'TCL_VERSION', (1, 2)) 20 | monkeypatch.setattr(_platform_info, 'TK_VERSION', (3, 4)) 21 | 22 | with pytest.raises(RuntimeError) as error: 23 | _platform_info._version_check() 24 | assert str(error.value) == ( 25 | "sorry, your Tcl/Tk installation is too old " 26 | "(expected 8.5 or newer, found Tcl 1.2 and Tk 3.4)") 27 | 28 | 29 | def test_windowingsystem(): 30 | assert teek.windowingsystem() in {'x11', 'aqua', 'win32'} 31 | -------------------------------------------------------------------------------- /tests/test_structures.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | import teek 4 | 5 | import pytest 6 | 7 | 8 | def test_callbacks(capsys): 9 | result1 = [] 10 | result2 = [] 11 | 12 | cb = teek.Callback() 13 | cb.connect(result1.append) 14 | cb.connect(result1.append) # repeated intentionally 15 | cb.connect(result2.append) 16 | 17 | assert cb.run('lol') is None 18 | assert result1 == ['lol', 'lol'] 19 | assert result2 == ['lol'] 20 | result1.clear() 21 | result2.clear() 22 | 23 | if platform.python_implementation() == 'PyPy': 24 | # in pypy, [].append == [].append 25 | result1.append('woot') 26 | cb.disconnect(result1.append) 27 | result1.clear() 28 | else: 29 | cb.disconnect(result1.append) 30 | 31 | assert cb.run('wut') is None 32 | assert result1 == result2 == ['wut'] 33 | result1.clear() 34 | result2.clear() 35 | 36 | cb.disconnect(result1.append) 37 | cb.disconnect(result2.append) 38 | assert cb.run('wat wat') is None 39 | assert result1 == result2 == [] 40 | 41 | with pytest.raises(ValueError): 42 | cb.disconnect(result1) 43 | 44 | assert capsys.readouterr() == ('', '') 45 | 46 | def broken_callback(whatever): 47 | 1 / 0 48 | 49 | stuff = [] 50 | cb.connect(broken_callback) 51 | cb.connect(stuff.append) 52 | assert cb.run('wat') is None # doesn't raise an error 53 | assert not stuff # running callbacks stopped because error 54 | output, errors = capsys.readouterr() 55 | assert not output 56 | assert '\n cb.connect(broken_callback)\n' in errors 57 | 58 | 59 | def test_callback_break(capsys): 60 | stuff = [] 61 | 62 | def non_breaking(): 63 | stuff.append('no break') 64 | 65 | def breaking(): 66 | stuff.append('break') 67 | return 'break' 68 | 69 | def wat(): 70 | stuff.append('wat') 71 | return 'wat' 72 | 73 | cb = teek.Callback() 74 | cb.connect(non_breaking) 75 | cb.connect(breaking) 76 | cb.connect(non_breaking) 77 | assert cb.run() == 'break' 78 | assert stuff == ['no break', 'break'] 79 | stuff.clear() 80 | 81 | cb2 = teek.Callback() 82 | cb2.connect(wat) 83 | cb2.connect(non_breaking) 84 | assert cb2.run() is None 85 | assert stuff == ['wat'] 86 | 87 | output, errors = capsys.readouterr() 88 | assert not output 89 | assert '\n cb2.connect(wat)\n' in errors 90 | assert errors.endswith( 91 | "\nValueError: expected None or 'break', got 'wat'\n") 92 | 93 | 94 | # most things are tested with doctests, but this is for testing corner cases 95 | def test_colors(): 96 | # these must work 97 | teek.Color(1, 2, 255) 98 | teek.Color(1, 2, 0) 99 | 100 | with pytest.raises(ValueError): 101 | teek.Color(1, 2, 256) 102 | with pytest.raises(ValueError): 103 | teek.Color(1, 2, -1) 104 | with pytest.raises(TypeError): 105 | teek.Color(1, 2, 3, 4) 106 | with pytest.raises(TypeError): 107 | teek.Color() 108 | 109 | blue1 = teek.Color(0, 0, 255) 110 | blue2 = teek.Color('blue') 111 | white = teek.Color('white') 112 | 113 | assert repr(blue1).startswith(" centimeter 151 | 152 | assert inch != 'asd' 153 | assert inch != '1i' 154 | with pytest.raises(TypeError): 155 | inch < '1i' 156 | 157 | teek.tcl_eval(None, 'proc returnArg {arg} {return $arg}') 158 | try: 159 | assert teek.tcl_eval(teek.ScreenDistance, 'returnArg 1i') == inch 160 | assert teek.tcl_eval(teek.ScreenDistance, 'returnArg 1c') == centimeter 161 | assert teek.tcl_eval(teek.ScreenDistance, 'returnArg 1') == pixel 162 | finally: 163 | teek.delete_command('returnArg') 164 | 165 | with pytest.raises(teek.TclError): 166 | teek.ScreenDistance('asdf asdf') 167 | -------------------------------------------------------------------------------- /tests/test_threads.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import re 3 | import threading 4 | import traceback 5 | 6 | import pytest 7 | 8 | import teek 9 | 10 | 11 | @pytest.mark.slow 12 | def test_make_thread_safe(handy_callback, deinit_threads): 13 | @teek.make_thread_safe 14 | @handy_callback 15 | def thread_target(): 16 | assert threading.current_thread() is threading.main_thread() 17 | 18 | teek.init_threads() 19 | thread = threading.Thread(target=thread_target) 20 | thread.start() 21 | 22 | # make_thread_safe needs teek.run to work 23 | teek.after(500, teek.quit) 24 | teek.run() 25 | 26 | assert not thread.is_alive() 27 | assert thread_target.ran_once() 28 | 29 | 30 | @pytest.mark.slow 31 | def test_basic_stuff(deinit_threads, handy_callback): 32 | teek.init_threads() 33 | text = teek.Text(teek.Window()) 34 | 35 | def thread_target(): 36 | for i in (1, 2, 3): 37 | text.insert(text.end, 'hello %d\n' % i) 38 | 39 | thread = threading.Thread(target=thread_target) 40 | thread.start() 41 | 42 | @handy_callback 43 | def done_callback(): 44 | assert text.get(text.start, text.end) == 'hello 1\nhello 2\nhello 3\n' 45 | teek.quit() 46 | 47 | # i experimented with different values: 500 was enough and 450 wasn't, so 48 | # this should be plenty 49 | teek.after(1000, done_callback) 50 | teek.run() 51 | thread.join() 52 | assert done_callback.ran_once() 53 | 54 | 55 | def test_init_threads_errors(deinit_threads, handy_callback): 56 | @handy_callback 57 | def thread1_target(): 58 | # the Tcl interpreter isn't started yet, so this runs an error that is 59 | # not covered by the code below 60 | with pytest.raises(RuntimeError) as error: 61 | teek.tcl_eval(None, '') 62 | assert str(error.value) == "init_threads() wasn't called" 63 | 64 | thread1 = threading.Thread(target=thread1_target) 65 | thread1.start() 66 | thread1.join() 67 | assert thread1_target.ran_once() 68 | 69 | # this starts the Tcl interpreter 70 | teek.tcl_eval(None, '') 71 | 72 | @handy_callback 73 | def thread2_target(): 74 | with pytest.raises(RuntimeError) as error: 75 | teek.init_threads() 76 | assert (str(error.value) == 77 | "init_threads() must be called from main thread") 78 | 79 | for cb in [functools.partial(teek.tcl_call, None, 'puts', 'hello'), 80 | functools.partial(teek.tcl_eval, None, 'puts hello')]: 81 | with pytest.raises(RuntimeError) as error: 82 | cb() 83 | assert str(error.value) == "init_threads() wasn't called" 84 | 85 | thread2 = threading.Thread(target=thread2_target) 86 | thread2.start() 87 | thread2.join() 88 | assert thread2_target.ran_once() 89 | 90 | teek.init_threads() 91 | with pytest.raises(RuntimeError) as error: 92 | teek.init_threads() 93 | assert str(error.value) == "init_threads() was called twice" 94 | 95 | teek.after_idle(teek.quit) 96 | teek.run() 97 | 98 | 99 | def test_run_called_from_wrong_thread(handy_callback): 100 | # this starts the Tcl interpreter, we get different errors without this 101 | teek.tcl_eval(None, '') 102 | 103 | @handy_callback 104 | def thread_target(): 105 | with pytest.raises(RuntimeError) as error: 106 | teek.run() 107 | assert str(error.value) == "run() must be called from main thread" 108 | 109 | thread = threading.Thread(target=thread_target) 110 | thread.start() 111 | thread.join() 112 | assert thread_target.ran_once() 113 | 114 | 115 | def test_error_in_thread_call(deinit_threads, handy_callback): 116 | teek.init_threads() 117 | 118 | @handy_callback 119 | def thread_target(): 120 | with pytest.raises(teek.TclError) as error: 121 | teek.tcl_eval(None, "expr {1/0}") 122 | 123 | exc = error.value 124 | assert isinstance(exc, teek.TclError) 125 | assert exc.__traceback__ is not None 126 | 127 | # error_message is the traceback that python would display if this 128 | # error wasn't caught 129 | error_message = ''.join(traceback.format_exception( 130 | type(exc), exc, exc.__traceback__)) 131 | assert error_message.startswith("Traceback (most recent call last):\n") 132 | 133 | regex = (r'\n' 134 | r' File ".*test_threads\.py", line \d+, in thread_target\n' 135 | r' teek\.tcl_eval\(None, "expr {1/0}"\)\n') 136 | assert re.search(regex, error_message) is not None 137 | 138 | thread = threading.Thread(target=thread_target) 139 | thread.start() 140 | teek.after(100, teek.quit) 141 | teek.run() 142 | thread.join() 143 | assert thread_target.ran_once() 144 | 145 | 146 | def test_quitting_from_another_thread(handy_callback): 147 | teek.init_threads() 148 | 149 | @handy_callback 150 | def thread_target(): 151 | with pytest.raises(RuntimeError) as error: 152 | teek.quit() 153 | assert str(error.value) == "can only quit from main thread" 154 | 155 | thread = threading.Thread(target=thread_target) 156 | thread.start() 157 | thread.join() 158 | assert thread_target.ran_once() 159 | -------------------------------------------------------------------------------- /tests/test_timeouts.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | import pytest 5 | 6 | import teek 7 | 8 | 9 | @pytest.mark.slow 10 | @pytest.mark.skipif('CI' in os.environ, 11 | reason="this fails randomly in travis, no idea why") 12 | def test_after(): 13 | start = time.time() 14 | timeout = teek.after(200, teek.quit) 15 | assert repr(timeout).startswith("" 70 | var.set('asd') 71 | assert repr(var) == "" 72 | 73 | 74 | @pytest.mark.slow 75 | def test_wait(): 76 | var = teek.StringVar() 77 | start = time.time() 78 | teek.after(500, functools.partial(var.set, "boo")) 79 | var.wait() # should run the event loop ==> after callback works 80 | end = time.time() 81 | assert (end - start) > 0.5 82 | 83 | 84 | def test_not_subclassed(): 85 | with pytest.raises(TypeError) as error: 86 | teek.TclVariable() 87 | assert "cannot create instances of TclVariable" in str(error.value) 88 | assert "subclass TclVariable" in str(error.value) 89 | assert "'type_spec' class attribute" in str(error.value) 90 | -------------------------------------------------------------------------------- /tests/widgets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akuli/teek/c360fbfe086ca09cdd856a8636de05b24e1b7093/tests/widgets/__init__.py -------------------------------------------------------------------------------- /tests/widgets/test_canvas.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import teek 4 | 5 | 6 | def test_item_eq_hash_repr_fromtcl_totcl(): 7 | canvas = teek.Canvas(teek.Window()) 8 | rect = canvas.create_rectangle(100, 100, 200, 200) 9 | oval = canvas.create_oval(150, 150, 250, 250) 10 | 11 | assert repr(rect) == ( 12 | '') 13 | assert repr(oval) == '' 14 | 15 | assert oval == oval 16 | assert rect != oval 17 | assert rect != 'lol wat' 18 | assert len({rect, oval}) == 2 19 | 20 | assert oval == canvas.Item.from_tcl(oval.to_tcl()) 21 | assert hash(oval) == hash(canvas.Item.from_tcl(oval.to_tcl())) 22 | 23 | oval.delete() 24 | assert repr(oval) == '' 25 | 26 | 27 | def test_trying_2_create_item_directly(): 28 | canvas = teek.Canvas(teek.Window()) 29 | with pytest.raises(TypeError) as error: 30 | canvas.Item() 31 | assert str(error.value).startswith("don't create canvas.Item objects") 32 | 33 | 34 | def test_item_config_usage(): 35 | canvas = teek.Canvas(teek.Window()) 36 | rect = canvas.create_rectangle(100, 100, 200, 200, dash='-') 37 | 38 | assert rect.config['dash'] == '-' 39 | assert rect.config['fill'] is None 40 | rect.config['fill'] = 'blue' 41 | assert rect.config['fill'] == teek.Color('blue') 42 | 43 | 44 | def create_different_items(canvas): 45 | return [ 46 | canvas.create_rectangle(100, 200, 300, 400), 47 | canvas.create_oval(100, 200, 300, 400), 48 | canvas.create_line(100, 200, 300, 400, 500, 600), 49 | ] 50 | 51 | 52 | def test_create_different_items_util_function(): 53 | canvas = teek.Canvas(teek.Window()) 54 | from_method_names = {name.split('_')[1] for name in dir(canvas) 55 | if name.startswith('create_')} 56 | from_util_func = {item.type_string 57 | for item in create_different_items(canvas)} 58 | assert from_method_names == from_util_func 59 | 60 | 61 | def test_config_types(check_config_types): 62 | canvas = teek.Canvas(teek.Window()) 63 | check_config_types(canvas.config, 'Canvas') 64 | 65 | # it would be repeatitive to see the same warnings over and over again 66 | already_heard = set() 67 | 68 | for item in create_different_items(canvas): 69 | already_heard |= check_config_types( 70 | item.config, 'Canvas %s item' % item.type_string, 71 | ignore_list=already_heard) 72 | 73 | 74 | def test_coords(): 75 | canvas = teek.Canvas(teek.Window()) 76 | oval = canvas.create_oval(150, 150, 250, 250) 77 | 78 | assert oval.coords == (150, 150, 250, 250) 79 | oval.coords = (50, 50, 100, 100.123) 80 | assert oval.coords == (50, 50, 100, 100.123) 81 | assert repr(oval) == '' 82 | 83 | 84 | def test_tags(): 85 | canvas = teek.Canvas(teek.Window()) 86 | rect = canvas.create_rectangle(100, 100, 200, 200) 87 | oval = canvas.create_oval(150, 150, 250, 250) 88 | 89 | assert list(rect.tags) == [] 90 | assert list(oval.tags) == [] 91 | rect.tags.add('a') 92 | assert list(rect.tags) == ['a'] 93 | assert list(oval.tags) == [] 94 | rect.tags.add('a') 95 | assert list(rect.tags) == ['a'] 96 | assert list(oval.tags) == [] 97 | rect.tags.add('b') 98 | assert list(rect.tags) == ['a', 'b'] 99 | assert list(oval.tags) == [] 100 | rect.tags.discard('b') 101 | assert list(rect.tags) == ['a'] 102 | assert list(oval.tags) == [] 103 | rect.tags.discard('b') 104 | assert list(rect.tags) == ['a'] 105 | assert list(oval.tags) == [] 106 | 107 | assert 'a' in rect.tags 108 | assert 'b' not in rect.tags 109 | 110 | 111 | def test_find(): 112 | canvas = teek.Canvas(teek.Window()) 113 | rect = canvas.create_rectangle(150, 150, 200, 200) 114 | oval = canvas.create_oval(50, 50, 100, 100) 115 | 116 | assert canvas.find_closest(70, 70) == oval 117 | assert canvas.find_enclosed(40, 40, 110, 110) == [oval] 118 | assert canvas.find_overlapping(90, 90, 160, 160) == [rect, oval] 119 | 120 | assert rect.find_above() == oval 121 | assert oval.find_above() is None 122 | assert oval.find_below() == rect 123 | assert rect.find_below() is None 124 | assert canvas.find_all() == [rect, oval] 125 | 126 | rect.tags.add('asdf') 127 | assert canvas.find_withtag('asdf') == [rect] 128 | oval.tags.add('asdf') 129 | assert canvas.find_withtag('asdf') == [rect, oval] 130 | -------------------------------------------------------------------------------- /tests/widgets/test_menu.py: -------------------------------------------------------------------------------- 1 | import teek 2 | 3 | import pytest 4 | 5 | 6 | def test_item_types(): 7 | menu = teek.Menu([ 8 | teek.MenuItem("Click me", print), 9 | teek.MenuItem(), # separator 10 | teek.MenuItem("Check me", teek.BooleanVar()), 11 | teek.MenuItem("Choose me or a friend of mine", teek.StringVar(), "me"), 12 | teek.MenuItem("Submenu", [ 13 | teek.MenuItem("Click here", print) 14 | ]), 15 | teek.MenuItem("Different kind of submenu", teek.Menu([ 16 | teek.MenuItem("Click me", print), 17 | ])), 18 | ]) 19 | assert [item.type for item in menu] == [ 20 | 'command', 'separator', 'checkbutton', 'radiobutton', 'cascade', 21 | 'cascade'] 22 | 23 | for submenu_item in (menu[-2], menu[-1]): 24 | assert submenu_item.config['menu'][0].type == 'command' 25 | 26 | with pytest.raises(TypeError, match=r'^expected .* arguments.*$'): 27 | teek.MenuItem("adding only 1 argument is wrong") 28 | 29 | 30 | def test_menuitem_objects(): 31 | item = teek.MenuItem("Click me", print, 32 | activeforeground=teek.Color(1, 2, 3)) 33 | assert repr(item) == ( 34 | "): " 35 | "type='command', not added to a menu yet>") 36 | 37 | menu = teek.Menu([item]) 38 | assert repr(item) == ( 39 | "): " 40 | "type='command', added to a menu>") 41 | 42 | # the menu should cache the item 43 | assert menu[0] is item 44 | assert menu[0] is menu[0] 45 | 46 | 47 | def test_repr(): 48 | menu = teek.Menu([ 49 | teek.MenuItem("Click me", print), 50 | teek.MenuItem("Check me", teek.BooleanVar()), 51 | ]) 52 | assert repr(menu) == '' 53 | 54 | 55 | def test_must_be_menuitem_object(): 56 | with pytest.raises(TypeError) as error: 57 | teek.Menu().append(("Click me", print)) 58 | assert str(error.value) == ( 59 | "expected a MenuItem, got ('Click me', )") 60 | 61 | 62 | def test_not_added_to_menu_yet(): 63 | menu = teek.Menu() 64 | item = teek.MenuItem("Click me", print) 65 | 66 | check = pytest.raises(RuntimeError, 67 | match=r".*hasn't been added to a Menu.*") 68 | with check: 69 | item.config['label'] 70 | menu.append(item) 71 | assert item.config['label'] == "Click me" 72 | assert menu.pop() is item 73 | with check: 74 | item.config['label'] 75 | 76 | 77 | def test_added_to_2_menus_at_the_same_time(): 78 | item = teek.MenuItem("Click me", print) 79 | menu = teek.Menu() 80 | menu.append(item) 81 | 82 | check = pytest.raises(RuntimeError, match=( 83 | r'^cannot add a MenuItem to two different menus ' 84 | r'or twice to the same menu$')) 85 | with check: 86 | menu.append(item) 87 | with check: 88 | teek.Menu().append(item) 89 | 90 | 91 | def test_destroying_deletes_commands(handy_callback): 92 | @handy_callback 93 | def callback_func(): 94 | pass 95 | 96 | menu = teek.Menu([ 97 | teek.MenuItem("Click me", callback_func), 98 | ]) 99 | command_string = teek.tcl_call(str, menu, 'entrycget', 0, '-command') 100 | teek.tcl_call(None, command_string) 101 | assert callback_func.ran_once() 102 | 103 | menu.destroy() 104 | with pytest.raises(teek.TclError): 105 | teek.tcl_call(None, command_string) 106 | 107 | 108 | def test_config(check_config_types): 109 | boolean_var = teek.BooleanVar() 110 | string_var = teek.StringVar() 111 | menu = teek.Menu([ 112 | teek.MenuItem("Click me", print), 113 | teek.MenuItem("Check me", boolean_var), 114 | teek.MenuItem("Choice A", string_var, "a"), 115 | teek.MenuItem("Choice B", string_var, "b"), 116 | teek.MenuItem("Choice C", string_var, "c"), 117 | teek.MenuItem(), 118 | teek.MenuItem("Submenu", []), 119 | ]) 120 | 121 | for item in menu: 122 | check_config_types(item.config, 'menu %s item' % item.type) 123 | 124 | assert isinstance(menu[0].config['command'], teek.Callback) 125 | menu[0].config['command'].disconnect(print) # fails if not connected 126 | 127 | assert isinstance(menu[1].config['variable'], teek.BooleanVar) 128 | assert menu[1].config['variable'] == boolean_var 129 | 130 | for index in (2, 3, 4): 131 | assert isinstance(menu[index].config['variable'], teek.StringVar) 132 | assert menu[index].config['variable'] == string_var 133 | 134 | 135 | def test_list_like_behaviour(): 136 | menu = teek.Menu() 137 | menu.append(teek.MenuItem("Click me", print)) 138 | assert len(menu) == 1 139 | assert menu[0].config['label'] == "Click me" 140 | 141 | menu[-1] = teek.MenuItem("No, click me instead", print) 142 | assert len(menu) == 1 143 | assert menu[0].config['label'] == "No, click me instead" 144 | 145 | menu.insert(123, teek.MenuItem("Last item", print)) 146 | assert menu[-1].config['label'] == 'Last item' 147 | 148 | 149 | def test_slicing_not_supported_errors(): 150 | check = pytest.raises(TypeError, 151 | match=r'^slicing a Menu widget is not supported$') 152 | 153 | menu = teek.Menu() 154 | with check: 155 | menu[:] 156 | with check: 157 | menu[:] = [] 158 | with check: 159 | del menu[:] 160 | 161 | 162 | def test_readding_to_menus(): 163 | menu1 = teek.Menu() 164 | menu2 = teek.Menu() 165 | 166 | def lengths(): 167 | return (len(menu1), len(menu2)) 168 | 169 | item = teek.MenuItem() 170 | assert lengths() == (0, 0) 171 | 172 | menu1.append(item) 173 | assert lengths() == (1, 0) 174 | menu1.remove(item) 175 | assert lengths() == (0, 0) 176 | 177 | menu1.append(item) 178 | assert lengths() == (1, 0) 179 | menu1.remove(item) 180 | assert lengths() == (0, 0) 181 | 182 | menu2.append(item) 183 | assert lengths() == (0, 1) 184 | menu2.remove(item) 185 | assert lengths() == (0, 0) 186 | 187 | menu1.append(item) 188 | assert lengths() == (1, 0) 189 | menu1.remove(item) 190 | assert lengths() == (0, 0) 191 | 192 | 193 | def test_indexes_dont_mess_up_ever_like_srsly_not_ever_never(): 194 | menu = teek.Menu() 195 | 196 | def check(): 197 | assert [item._index for item in menu] == list(range(len(menu))) 198 | 199 | menu.append(teek.MenuItem()) 200 | check() 201 | menu.append(teek.MenuItem()) 202 | check() 203 | menu.extend([teek.MenuItem(), teek.MenuItem(), 204 | teek.MenuItem(), teek.MenuItem()]) 205 | check() 206 | menu.pop() 207 | check() 208 | menu.pop(-2) 209 | check() 210 | del menu[0] 211 | check() 212 | menu.insert(1, teek.MenuItem()) 213 | check() 214 | menu[1] = teek.MenuItem() 215 | check() 216 | menu.clear() 217 | check() 218 | 219 | 220 | def test_popup(): 221 | menu = teek.Menu() 222 | assert not menu.winfo_ismapped() 223 | menu.popup(123, 456) 224 | assert menu.winfo_ismapped() 225 | 226 | menu2 = teek.Menu([teek.MenuItem("Click me", print)]) 227 | menu2.popup(123, 456, menu2[0]) 228 | assert menu2.winfo_ismapped() 229 | -------------------------------------------------------------------------------- /tests/widgets/test_misc_widgets.py: -------------------------------------------------------------------------------- 1 | import teek 2 | 3 | import pytest 4 | 5 | 6 | def test_button(capsys): 7 | window = teek.Window() 8 | stuff = [] 9 | 10 | button1 = teek.Button(window) 11 | button2 = teek.Button(window, 'click me') 12 | button3 = teek.Button(window, 'click me', (lambda: stuff.append(3))) 13 | 14 | assert "text=''" in repr(button1) 15 | assert "text='click me'" in repr(button2) 16 | assert "text='click me'" in repr(button3) 17 | 18 | assert button1.config['text'] == '' 19 | assert button2.config['text'] == button3.config['text'] == 'click me' 20 | 21 | for button in [button1, button2, button3]: 22 | assert isinstance(button.config['command'], teek.Callback) 23 | with pytest.raises(ValueError) as error: 24 | button.config['command'] = print 25 | assert str(error.value) == ( 26 | "cannot set the value of 'command', " 27 | "maybe use widget.config['command'].connect() instead?") 28 | 29 | # ideally there would be some way to click the button virtually and 30 | # let tk handle it, but i haven't gotten anything like that to work 31 | button1.config['command'].run() 32 | button2.config['command'].run() 33 | assert stuff == [] 34 | button3.config['command'].run() 35 | assert stuff == [3] 36 | 37 | button1.config['command'].connect(stuff.append, args=[1]) 38 | button2.config['command'].connect(stuff.append, args=[2]) 39 | button3.config['command'].connect(stuff.append, args=['wolo']) 40 | button3.config['command'].connect(stuff.append, args=['wolo']) 41 | 42 | stuff.clear() 43 | for button in [button1, button2, button3]: 44 | button.config['command'].run() 45 | assert stuff == [1, 2, 3, 'wolo', 'wolo'] 46 | 47 | def oops(): 48 | raise ValueError("shit") 49 | 50 | assert capsys.readouterr() == ('', '') 51 | button1.config['command'].connect(oops) 52 | button1.config['command'].run() 53 | output, errors = capsys.readouterr() 54 | assert not output 55 | assert "button1.config['command'].connect(oops)" in errors 56 | 57 | 58 | def test_button_invoke(): 59 | button = teek.Button(teek.Window()) 60 | stuff = [] 61 | button.config['command'].connect(stuff.append, args=[1]) 62 | button.config['command'].connect(stuff.append, args=[2]) 63 | button.invoke() 64 | assert stuff == [1, 2] 65 | 66 | 67 | def test_checkbutton(): 68 | assert teek.Checkbutton(teek.Window()).config['text'] == '' 69 | assert teek.Checkbutton(teek.Window(), 'asd').config['text'] == 'asd' 70 | 71 | asd = [] 72 | 73 | checkbutton = teek.Checkbutton(teek.Window(), 'asd', asd.append) 74 | checkbutton.config['command'].connect(asd.append) 75 | checkbutton.invoke() 76 | assert checkbutton.config['variable'].get() is True 77 | checkbutton.invoke() 78 | assert checkbutton.config['variable'].get() is False 79 | assert asd == [True, True, False, False] 80 | asd.clear() 81 | 82 | checkbutton = teek.Checkbutton(teek.Window(), 'asd', asd.append, 83 | onvalue=False, offvalue=True) 84 | checkbutton.config['command'].connect(asd.append) 85 | checkbutton.invoke() 86 | assert checkbutton.config['variable'].get() is False 87 | checkbutton.invoke() 88 | assert checkbutton.config['variable'].get() is True 89 | assert asd == [False, False, True, True] 90 | asd.clear() 91 | 92 | 93 | def test_entry(): 94 | entry = teek.Entry(teek.Window(), "some text") 95 | assert "text='some text'" in repr(entry) 96 | 97 | assert entry.text == 'some text' 98 | entry.text = 'new text' 99 | assert entry.text == 'new text' 100 | assert "text='new text'" in repr(entry) 101 | 102 | assert entry.cursor_pos == len(entry.text) 103 | entry.cursor_pos = 0 104 | assert entry.cursor_pos == 0 105 | 106 | assert entry.config['exportselection'] is True 107 | assert isinstance(entry.config['width'], int) 108 | 109 | 110 | def test_label(): 111 | window = teek.Window() 112 | 113 | label = teek.Label(window) 114 | assert "text=''" in repr(label) 115 | assert label.config['text'] == '' 116 | 117 | label.config.update({'text': 'new text'}) 118 | assert label.config['text'] == 'new text' 119 | assert "text='new text'" in repr(label) 120 | 121 | label2 = teek.Label(window, 'new text') 122 | assert label.config == label2.config 123 | 124 | 125 | def test_labelframe(): 126 | assert teek.LabelFrame(teek.Window()).config['text'] == '' 127 | assert teek.LabelFrame(teek.Window(), 'hello').config['text'] == 'hello' 128 | 129 | labelframe = teek.LabelFrame(teek.Window(), 'hello') 130 | assert repr(labelframe) == "" 131 | 132 | 133 | def test_progressbar(): 134 | progress_bar = teek.Progressbar(teek.Window()) 135 | assert progress_bar.config['value'] == 0 136 | assert repr(progress_bar) == ( 137 | "") 139 | 140 | progress_bar.config['mode'] = 'indeterminate' 141 | assert repr(progress_bar) == ( 142 | "") 143 | 144 | # test that bouncy methods don't raise errors, they are tested better but 145 | # more slowly below 146 | progress_bar.start() 147 | progress_bar.stop() 148 | 149 | 150 | @pytest.mark.slow 151 | def test_progressbar_bouncing(): 152 | progress_bar = teek.Progressbar(teek.Window(), mode='indeterminate') 153 | assert progress_bar.config['value'] == 0 154 | progress_bar.start() 155 | 156 | def done_callback(): 157 | try: 158 | # sometimes the value gets set to 2.0 on this vm, so this works 159 | assert progress_bar.config['value'] > 1 160 | progress_bar.stop() # prevents funny tk errors 161 | finally: 162 | # if this doesn't run, the test freezes 163 | teek.quit() 164 | 165 | teek.after(500, done_callback) 166 | teek.run() 167 | 168 | 169 | def test_scrollbar(fake_command, handy_callback): 170 | scrollbar = teek.Scrollbar(teek.Window()) 171 | assert scrollbar.get() == (0.0, 1.0) 172 | 173 | # testing the set method isn't as easy as you might think because get() 174 | # doesn't return the newly set arguments after calling set() 175 | with fake_command(scrollbar.to_tcl()) as called: 176 | scrollbar.set(1.2, 3.4) 177 | assert called == [['set', '1.2', '3.4']] 178 | 179 | # this tests the code that runs when the user scrolls the scrollbar 180 | log = [] 181 | scrollbar.config['command'].connect(lambda *args: log.append(args)) 182 | 183 | teek.tcl_eval(None, ''' 184 | set command [%s cget -command] 185 | $command moveto 1.2 186 | $command scroll 1 units 187 | $command scroll 2 pages 188 | ''' % scrollbar.to_tcl()) 189 | assert log == [ 190 | ('moveto', 1.2), 191 | ('scroll', 1, 'units'), 192 | ('scroll', 2, 'pages') 193 | ] 194 | 195 | 196 | def test_separator(): 197 | hsep = teek.Separator(teek.Window()) 198 | vsep = teek.Separator(teek.Window(), orient='vertical') 199 | assert hsep.config['orient'] == 'horizontal' 200 | assert vsep.config['orient'] == 'vertical' 201 | assert repr(hsep) == "" 202 | assert repr(vsep) == "" 203 | 204 | 205 | def test_spinbox(): 206 | asd = [] 207 | spinbox = teek.Spinbox(teek.Window(), from_=0, to=10, 208 | command=(lambda: asd.append('boo'))) 209 | assert asd == [] 210 | teek.tcl_eval(None, ''' 211 | set command [%s cget -command] 212 | $command 213 | $command 214 | ''' % spinbox.to_tcl()) 215 | assert asd == ['boo', 'boo'] 216 | 217 | 218 | def test_combobox(): 219 | combobox = teek.Combobox(teek.Window(), values=['a', 'b', 'c and d']) 220 | assert combobox.config['values'] == ['a', 'b', 'c and d'] 221 | combobox.text = 'c and d' 222 | assert combobox.text in combobox.config['values'] 223 | -------------------------------------------------------------------------------- /tests/widgets/test_notebook.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import teek 4 | 5 | 6 | class LolTab(teek.NotebookTab): 7 | pass 8 | 9 | 10 | def test_reprs(): 11 | notebook = teek.Notebook(teek.Window()) 12 | 13 | label = teek.Label(notebook, "asd") 14 | label2 = teek.Label(notebook, "asdasd") 15 | tab = teek.NotebookTab(label, text='toot') 16 | tab2 = LolTab(label2, text='toot toot') 17 | assert repr(tab) == "NotebookTab(" + repr(label) + ", text='toot')" 18 | assert repr(tab2) == "LolTab(" + repr(label2) + ", text='toot toot')" 19 | 20 | assert repr(notebook) == '' 21 | notebook.append(tab) 22 | assert repr(notebook) == '' 23 | notebook.remove(tab) 24 | assert repr(notebook) == '' 25 | 26 | 27 | def test_config_types(check_config_types): 28 | notebook = teek.Notebook(teek.Window()) 29 | check_config_types(notebook.config, 'Notebook') 30 | 31 | tab = teek.NotebookTab(teek.Label(notebook, "asd")) 32 | notebook.append(tab) 33 | check_config_types(tab.config, 'NotebookTab') 34 | 35 | 36 | def test_tab_object_caching(): 37 | notebook = teek.Notebook(teek.Window()) 38 | tab1 = teek.NotebookTab(teek.Label(notebook, "asd")) 39 | notebook.append(tab1) 40 | assert notebook[0] is tab1 41 | assert notebook.get_tab_by_widget(tab1.widget) is tab1 42 | 43 | 44 | def test_initial_options(): 45 | notebook = teek.Notebook(teek.Window()) 46 | tab = teek.NotebookTab(teek.Label(notebook)) 47 | 48 | with pytest.raises(RuntimeError): 49 | tab.config['text'] = 'lol' 50 | with pytest.raises(RuntimeError): 51 | tab.config['text'] 52 | 53 | assert tab.initial_options == {} 54 | tab.initial_options['text'] = 'lol' 55 | notebook.append(tab) 56 | assert tab.config['text'] == 'lol' 57 | 58 | 59 | def test_get_tab_by_widget_error(): 60 | notebook = teek.Notebook(teek.Window()) 61 | with pytest.raises(ValueError) as error: 62 | notebook.get_tab_by_widget(teek.Label(teek.Window(), text='lol')) 63 | 64 | assert str(error.value) == ( 65 | "expected a widget with the notebook as its parent, " 66 | "got ") 67 | 68 | 69 | def test_insert_with_different_indexes(): 70 | notebook = teek.Notebook(teek.Window()) 71 | 72 | notebook.insert(0, teek.NotebookTab(teek.Label(notebook, "1"))) 73 | notebook.insert(1, teek.NotebookTab(teek.Label(notebook, "2"))) 74 | notebook.insert(10, teek.NotebookTab(teek.Label(notebook, "3"))) 75 | notebook.insert(-10, teek.NotebookTab(teek.Label(notebook, "0"))) 76 | assert [tab.widget.config['text'] for tab in notebook] == list('0123') 77 | 78 | 79 | def test_list_like_behaviour(): 80 | notebook = teek.Notebook(teek.Window()) 81 | tab1 = teek.NotebookTab(teek.Label(notebook, "1")) 82 | tab2 = teek.NotebookTab(teek.Label(notebook, "2")) 83 | tab3 = teek.NotebookTab(teek.Label(notebook, "3")) 84 | tab4 = teek.NotebookTab(teek.Label(notebook, "4")) 85 | tab5 = teek.NotebookTab(teek.Label(notebook, "5")) 86 | 87 | notebook.append(tab3) 88 | notebook.extend([tab4, tab5]) 89 | notebook.insert(0, tab1) 90 | notebook.insert(1, tab2) 91 | assert list(notebook) == [tab1, tab2, tab3, tab4, tab5] 92 | 93 | assert notebook.pop() == tab5 94 | assert list(notebook) == [tab1, tab2, tab3, tab4] 95 | 96 | notebook[0] = tab5 97 | assert list(notebook) == [tab5, tab2, tab3, tab4] 98 | 99 | 100 | def test_moves_only(): 101 | notebook = teek.Notebook(teek.Window()) 102 | tab1 = teek.NotebookTab(teek.Label(notebook, text="1"), text="One") 103 | tab2 = teek.NotebookTab(teek.Label(notebook, text="2"), text="Two") 104 | notebook.extend([tab1, tab2]) 105 | tab1.config['text'] = 'wut1' 106 | tab2.config['text'] = 'wut2' 107 | 108 | notebook.insert(1, notebook[0]) 109 | assert tab1.config['text'] == 'wut1' 110 | assert tab2.config['text'] == 'wut2' 111 | assert list(notebook) != [tab1, tab2] 112 | assert list(notebook) == [tab2, tab1] 113 | 114 | 115 | def test_slicing_not_supported_error(): 116 | notebook = teek.Notebook(teek.Window()) 117 | catcher = pytest.raises(TypeError, 118 | match=r'^slicing a Notebook is not supported$') 119 | with catcher: 120 | notebook[::-1] 121 | with catcher: 122 | notebook[:3] = 'lol wat' 123 | with catcher: 124 | del notebook[:3] 125 | 126 | 127 | def test_hide_unhide_preserve_order(): 128 | notebook = teek.Notebook(teek.Window()) 129 | tabs = [teek.NotebookTab(teek.Label(notebook, str(n))) for n in [1, 2, 3]] 130 | notebook.extend(tabs) 131 | 132 | assert list(notebook) == tabs 133 | tabs[1].hide() 134 | assert list(notebook) == tabs 135 | tabs[1].unhide() 136 | assert list(notebook) == tabs 137 | 138 | 139 | def test_move(): 140 | notebook = teek.Notebook(teek.Window()) 141 | tab1 = teek.NotebookTab(teek.Label(notebook, text="one")) 142 | tab2 = teek.NotebookTab(teek.Label(notebook, text="two")) 143 | notebook.extend([tab1, tab2]) 144 | 145 | notebook.move(tab2, 0) 146 | assert list(notebook) == [tab2, tab1] 147 | notebook.move(tab2, 0) 148 | assert list(notebook) == [tab2, tab1] 149 | notebook.move(tab1, 0) 150 | assert list(notebook) == [tab1, tab2] 151 | notebook.move(tab1, 1) 152 | assert list(notebook) == [tab2, tab1] 153 | notebook.move(tab1, -1) # some_list[-1] is last item 154 | assert list(notebook) == [tab2, tab1] 155 | notebook.move(tab1, -2) 156 | assert list(notebook) == [tab1, tab2] 157 | 158 | with pytest.raises(IndexError): 159 | notebook.move(tab1, 2) 160 | with pytest.raises(IndexError): 161 | notebook.move(tab1, -3) 162 | 163 | tab3 = teek.NotebookTab(teek.Label(notebook, text="three")) 164 | with pytest.raises(ValueError): 165 | notebook.move(tab3, 0) 166 | 167 | 168 | def test_selected_tab(): 169 | notebook = teek.Notebook(teek.Window()) 170 | tab1 = teek.NotebookTab(teek.Label(notebook, text="one")) 171 | tab2 = teek.NotebookTab(teek.Label(notebook, text="two")) 172 | notebook.extend([tab1, tab2]) 173 | assert notebook.selected_tab is tab1 174 | 175 | notebook.selected_tab = tab2 176 | assert notebook.selected_tab is tab2 177 | notebook.selected_tab = tab2 # intentionally repeated 178 | assert notebook.selected_tab is tab2 179 | 180 | notebook.clear() 181 | assert notebook.selected_tab is None 182 | 183 | 184 | def test_insert_errors(): 185 | window = teek.Window() 186 | notebook1 = teek.Notebook(window) 187 | label1 = teek.Label(notebook1, text="one") 188 | notebook2 = teek.Notebook(window) 189 | tab2 = teek.NotebookTab(teek.Label(notebook2, text="two")) 190 | 191 | with pytest.raises(ValueError) as error: 192 | notebook1.append(tab2) 193 | assert (repr(notebook2) + "'s tab") in str(error.value) 194 | assert str(error.value).endswith('to ' + repr(notebook1)) 195 | 196 | # i imagine this will be a common mistake, so better be prepared to it 197 | with pytest.raises(TypeError) as error: 198 | notebook1.append(label1) 199 | assert str(error.value).startswith('expected a NotebookTab object') 200 | 201 | 202 | def test_check_in_notebook(): 203 | tab = teek.NotebookTab(teek.Label(teek.Notebook(teek.Window()))) 204 | with pytest.raises(RuntimeError) as error: 205 | tab.hide() 206 | assert 'not in the notebook' in str(error.value) 207 | 208 | 209 | def test_notebooktab_init_errors(): 210 | notebook = teek.Notebook(teek.Window()) 211 | label = teek.Label(notebook) 212 | 213 | lel_widget = teek.Window() 214 | with pytest.raises(ValueError) as error: 215 | teek.NotebookTab(lel_widget) 216 | assert ('widgets of NotebookTabs must be child widgets of a Notebook' 217 | in str(error.value)) 218 | 219 | teek.NotebookTab(label) 220 | with pytest.raises(RuntimeError) as error: 221 | teek.NotebookTab(label) 222 | assert 'there is already a NotebookTab' in str(error.value) 223 | 224 | 225 | def test_tab_added_with_tcl_call_so_notebooktab_object_is_created_automagic(): 226 | notebook = teek.Notebook(teek.Window()) 227 | label = teek.Label(notebook) 228 | teek.tcl_call(None, notebook, 'add', label) 229 | 230 | # looking up notebook[0] should create a new NotebookTab object 231 | assert isinstance(notebook[0], teek.NotebookTab) 232 | assert notebook[0] is notebook[0] # and it should be "cached" now 233 | -------------------------------------------------------------------------------- /tests/widgets/test_text_widget.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import os 3 | 4 | import pytest 5 | 6 | import teek 7 | 8 | 9 | def test_between_start_end(): 10 | text = teek.Text(teek.Window()) 11 | text.insert(text.start, 'lol') 12 | assert tuple(text.TextIndex(-10, -10)) == (-10, -10) 13 | assert tuple(text.TextIndex(10, 10)) == (10, 10) 14 | assert tuple(text.TextIndex(-10, -10).between_start_end()) == (1, 0) 15 | assert tuple(text.TextIndex(10, 10).between_start_end()) == (1, 3) 16 | 17 | # from_tcl must NOT call between_start_end() whenever that's avoidable, but 18 | # sometimes it isn't 19 | assert text.TextIndex.from_tcl('100.100') == (100, 100) 20 | assert text.TextIndex.from_tcl('100.100 + 1 char') == text.end 21 | 22 | 23 | def test_basic_stuff(): 24 | text = teek.Text(teek.Window()) 25 | 26 | # start and end should be namedtuples 27 | assert isinstance(text.start, tuple) 28 | assert type(text.start) is not tuple 29 | 30 | # there is nothing in the text widget yet 31 | assert text.start == text.end == (1, 0) 32 | 33 | text.insert(text.end, 'this is some text\nbla bla bla') 34 | assert text.get(text.start, text.end) == 'this is some text\nbla bla bla' 35 | assert text.start == (1, 0) 36 | assert text.end == (2, 11) 37 | assert 'contains 2 lines of text' in repr(text) 38 | 39 | text.replace((1, 0), (1, 4), 'lol') 40 | assert text.get(text.start, text.end) == 'lol is some text\nbla bla bla' 41 | 42 | assert text.get((1, 0), (1, 6)) == 'lol is' 43 | assert text.get((1, 12), (2, 3)) == 'text\nbla' 44 | 45 | assert text.get() == text.get(text.start, text.end) 46 | assert text.get(text.start) == text.get(text.start, text.end) 47 | 48 | assert text.start.forward(chars=2, lines=1) == (2, 2) 49 | assert (text.start.forward(chars=2, lines=1).back(chars=2, lines=1) == 50 | text.start) 51 | assert text.start.forward(chars=100) == text.end 52 | assert text.TextIndex(1000, 1000) > text.end 53 | 54 | assert text.start.wordend() == (1, 3) # after 'lol' 55 | assert text.start.wordend().linestart() == text.start 56 | assert (text.start.wordend().lineend() == 57 | text.start.forward(lines=1).back(chars=1)) 58 | 59 | # Tk's wordstart() seems to be funny, so this is the best test i came 60 | # up with 61 | assert text.start.wordstart() == text.start 62 | 63 | # indexes compare nicelys 64 | assert (text.start < 65 | text.start.forward(chars=1) < 66 | text.start.forward(lines=1) < 67 | text.start.forward(chars=1, lines=1)) 68 | 69 | 70 | def test_tkinter_index_string_error(): 71 | text = teek.Text(teek.Window()) 72 | with pytest.raises(TypeError) as error: 73 | text.get('1.0', 'end') 74 | assert "use (line, column) int tuples or TextIndex objects" in str( 75 | error.value) # lol pep8 line length 76 | 77 | 78 | def test_delete(): 79 | text = teek.Text(teek.Window()) 80 | text.insert(text.end, 'wat batman') 81 | text.delete((1, 1), text.end.back(chars=5)) 82 | assert text.get(text.start, text.end) == 'watman' 83 | 84 | 85 | def test_see(): 86 | text = teek.Text(teek.Window()) 87 | for i in range(1, 1000): 88 | text.insert(text.end, 'toot %d\n' % i) 89 | 90 | teek.update() 91 | yview1 = text.yview() 92 | text.see(text.end) 93 | teek.update() 94 | yview2 = text.yview() 95 | assert max(yview1) < 0.5 and min(yview2) > 0.5 96 | 97 | 98 | # see text(3tk) with different tk versions 99 | def test_config_types(check_config_types): 100 | text = teek.Text(teek.Window()) 101 | check_config_types(text.config, 'Text') 102 | check_config_types(text.get_tag('asdfasdf'), 'Text tag') 103 | 104 | 105 | def test_tags(): 106 | text = teek.Text(teek.Window()) 107 | text.insert(text.start, "asd toot boo") 108 | 109 | assert {tag.name for tag in text.get_all_tags()} == {'sel'} 110 | assert text.get_tag('asd').name == 'asd' 111 | 112 | # do any tag Tcl call that ensures the asd tag exists 113 | text.get_tag('asd')['foreground'] 114 | 115 | assert {tag.name for tag in text.get_all_tags()} == {'sel', 'asd'} 116 | 117 | for tag in [text.get_tag('asd'), text.get_tag('sel')]: 118 | assert tag is text.get_tag(tag.name) # returns same tag obj every time 119 | 120 | tag.add((1, 4), (1, 8)) 121 | assert tag.ranges() == [((1, 4), (1, 8))] 122 | flatten = itertools.chain.from_iterable 123 | assert all(isinstance(index, type(text.start)) 124 | for index in flatten(tag.ranges())) 125 | 126 | assert tag.nextrange((1, 0)) == ((1, 4), (1, 8)) 127 | assert tag.nextrange((1, 0), (1, 4)) is None 128 | for index in tag.nextrange((1, 0)): 129 | assert isinstance(index, type(text.start)) 130 | 131 | tag.remove() 132 | assert tag.ranges() == [] 133 | 134 | tag.add((1, 4), (1, 8)) 135 | tag.remove((0, 0), (100, 200)) 136 | assert tag.ranges() == [] 137 | 138 | # all tags must have the same options 139 | option_frozensets = set() 140 | for tag in text.get_all_tags(): 141 | option_frozensets.add(frozenset(tag.keys())) 142 | assert len(option_frozensets) == 1 # they are unique 143 | 144 | # because nothing else covers this 145 | assert len(text.get_tag('asd')) == len(list(option_frozensets)[0]) 146 | 147 | toot = text.get_tag('toot') 148 | toot.add((1, 4), text.end) 149 | assert toot.ranges() != [] 150 | toot.delete() 151 | assert toot not in text.get_all_tags() 152 | assert toot.ranges() == [] 153 | assert toot not in text.get_all_tags() 154 | 155 | # if it's set to a string, it must still be a Color object when getting 156 | toot['foreground'] = 'black' 157 | assert toot in text.get_all_tags() 158 | assert isinstance(toot['foreground'], teek.Color) 159 | assert toot['foreground'] == teek.Color(0, 0, 0) 160 | 161 | toot['foreground'] = teek.Color('blue') 162 | assert toot['foreground'] == teek.Color('blue') 163 | 164 | # misc other tag properties 165 | assert toot == toot 166 | assert toot != teek.Text(teek.Window()).get_tag('toot') # different widget 167 | assert toot != 123 168 | assert hash(toot) == hash(toot) 169 | assert repr(toot) == "" 170 | with pytest.raises(TypeError): 171 | del toot['foreground'] 172 | 173 | tag_names = {'sel', 'asd', 'toot'} 174 | for tag_name in tag_names: 175 | text.get_tag(tag_name).add((1, 4), (1, 8)) 176 | assert {tag.name for tag in text.get_all_tags((1, 6))} == tag_names 177 | 178 | 179 | def test_tag_creating_bug(): 180 | text = teek.Text(teek.Window()) 181 | a = text.get_tag('a') 182 | assert a in text.get_all_tags() 183 | 184 | 185 | def test_tag_lower_raise(): 186 | text = teek.Text(teek.Window()) 187 | a = text.get_tag('a') 188 | b = text.get_tag('b') 189 | 190 | # i don't know what else to do than test that nothing errors 191 | a.lower(b) 192 | a.raise_(b) 193 | a.lower() 194 | a.raise_() 195 | 196 | 197 | def test_tag_bind(): 198 | # i can't think of a better way to test this 199 | tag = teek.Text(teek.Window()).get_tag('asd') 200 | tag.bind('', print, event=True) 201 | tag.bindings['<1>'].disconnect(print) 202 | with pytest.raises(ValueError): 203 | tag.bindings['<1>'].disconnect(print) 204 | 205 | 206 | def test_marks(): 207 | text = teek.Text(teek.Window()) 208 | assert text.marks.keys() == {'insert', 'current'} 209 | assert text.marks['insert'] == text.start 210 | assert text.marks['current'] == text.start 211 | 212 | text.insert(text.start, 'hello world') 213 | text.marks['before space'] = text.start.forward(chars=5) 214 | assert text.marks['before space'] == text.start.forward(chars=5) 215 | del text.marks['before space'] 216 | assert 'before space' not in text.marks 217 | 218 | 219 | def test_scrolling(): 220 | text = teek.Text(teek.Window()) 221 | asd = [] 222 | 223 | def callback(x, y): 224 | asd.extend([x, y]) 225 | 226 | text.config['yscrollcommand'].connect(callback) 227 | text.insert(text.end, 'toot\ntoot\n' * text.config['height']) 228 | 229 | # scroll to end, and make sure everything is visible 230 | text.yview('moveto', 1) 231 | text.pack() 232 | teek.update() 233 | 234 | # this fails consistently in travis for some reason, but if i run this 235 | # locally in xvfb-run, it works fine 0_o 236 | if 'CI' not in os.environ: 237 | assert round(asd[-2], 1) == 0.5 238 | assert asd[-1] == 1.0 239 | 240 | # yview return type checks 241 | assert text.yview('moveto', 1) is None 242 | pair = text.yview() 243 | assert isinstance(pair, tuple) and len(pair) == 2 244 | assert all(isinstance(item, float) for item in pair) 245 | -------------------------------------------------------------------------------- /tests/widgets/test_window_widgets.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import os 3 | import time 4 | 5 | import pytest 6 | 7 | import teek 8 | 9 | 10 | SMILEY_PATH = os.path.join( 11 | os.path.dirname(os.path.abspath(__file__)), '..', 'data', 'smiley.gif') 12 | 13 | 14 | def test_window(): 15 | windows = [ 16 | (teek.Window("hello hello"), "hello hello"), 17 | (teek.Window(), None), 18 | ] 19 | 20 | for window, default_title in windows: 21 | assert window.winfo_toplevel() is window.toplevel 22 | assert isinstance(window.toplevel, teek.Toplevel) 23 | teek.update() # you can add more of these if the tests don't work 24 | 25 | assert window.wm_state == 'normal' 26 | if default_title is not None: 27 | assert window.title == default_title 28 | assert repr(default_title) in repr(window) 29 | 30 | window.title = "hello hello" 31 | assert window.title == "hello hello" 32 | 33 | not_a_window = teek.Frame(teek.Window()) 34 | assert not hasattr(not_a_window, 'title') 35 | 36 | 37 | @pytest.mark.skipif('CI' in os.environ, 38 | reason=("relies on non-guaranteed details about how " 39 | "window managers work or something like that")) 40 | def test_window_states(): 41 | window = teek.Window() 42 | for method, state in [(window.withdraw, 'withdrawn'), 43 | (window.iconify, 'iconic')]: 44 | method() 45 | teek.update() 46 | assert window.wm_state == state 47 | assert ("wm_state='%s'" % state) in repr(window) 48 | window.deiconify() 49 | teek.update() 50 | assert window.wm_state == 'normal' 51 | assert "wm_state='normal'" not in repr(window) 52 | 53 | window.wm_state = state # should do same as method() 54 | teek.update() 55 | assert window.wm_state == state 56 | window.deiconify() 57 | teek.update() 58 | assert window.wm_state == 'normal' 59 | 60 | 61 | def test_window_closing(): 62 | for window in [teek.Window(), teek.Toplevel()]: 63 | # on_delete_window should NOT be connected to anything by default 64 | with pytest.raises(ValueError): 65 | window.on_delete_window.disconnect(teek.quit) 66 | with pytest.raises(ValueError): 67 | window.on_delete_window.disconnect(window.destroy) 68 | 69 | # there are more callback tests elsewhere, but just to be sure 70 | window.on_delete_window.connect(window.destroy) 71 | window.on_delete_window.disconnect(window.destroy) 72 | with pytest.raises(ValueError): 73 | window.on_delete_window.disconnect(window.destroy) 74 | 75 | assert window.winfo_exists() 76 | window.destroy() 77 | assert not window.winfo_exists() 78 | 79 | 80 | def test_geometry(): 81 | window = teek.Window() 82 | 83 | # namedtuple features 84 | geometry = window.geometry() 85 | geometry2 = window.geometry() 86 | assert geometry == geometry2 87 | assert hash(geometry) == hash(geometry2) 88 | assert repr(geometry) == repr(geometry2) 89 | assert repr(geometry).startswith('Geometry(') 90 | 91 | for pair in [('width', 'height'), ('x', 'y')]: 92 | with pytest.raises(TypeError) as error: 93 | window.geometry(**{pair[0]: 123}) 94 | assert str(error.value) == 'specify both %s and %s, or neither' % pair 95 | 96 | 97 | def test_geometry_tkinter_error(): 98 | window = teek.Window() 99 | with pytest.raises(TypeError) as error: 100 | window.geometry('200x300') 101 | assert "use widget.geometry(width, height)" in str(error.value) 102 | 103 | 104 | @pytest.mark.skipif(platform.system() == "Windows", 105 | reason=("actual windows window manager behavior" 106 | "is different than the test expects")) 107 | def test_geometry_changes(): 108 | window = teek.Window() 109 | 110 | window.geometry(300, 400) 111 | teek.update() 112 | assert window.geometry()[:2] == (300, 400) 113 | 114 | window.geometry(x=123, y=456) 115 | teek.update() 116 | assert window.geometry() == (300, 400, 123, 456) 117 | 118 | window.geometry(100, 200, 300, 400) 119 | teek.update() 120 | assert window.geometry() == (100, 200, 300, 400) 121 | 122 | 123 | def test_minsize_maxsize(): 124 | window = teek.Window() 125 | assert isinstance(window.minsize, tuple) 126 | assert isinstance(window.maxsize, tuple) 127 | assert len(window.minsize) == 2 128 | assert len(window.maxsize) == 2 129 | assert window.minsize[0] == 1 130 | assert window.minsize[1] == 1 131 | assert window.maxsize[0] > 100 132 | assert window.maxsize[1] > 100 133 | 134 | window.minsize = (12, 34) 135 | window.maxsize = (56, 78) 136 | assert window.minsize == (12, 34) 137 | assert window.maxsize == (56, 78) 138 | 139 | 140 | def test_iconphoto(fake_command): 141 | image1 = teek.Image(file=SMILEY_PATH) 142 | image2 = image1.copy() 143 | 144 | widget = teek.Toplevel() 145 | 146 | with fake_command('wm') as called: 147 | widget.iconphoto(image1) 148 | widget.iconphoto(image1, image2) 149 | assert called == [ 150 | ['iconphoto', widget.to_tcl(), image1.to_tcl()], 151 | ['iconphoto', widget.to_tcl(), image1.to_tcl(), image2.to_tcl()], 152 | ] 153 | 154 | 155 | def test_transient(): 156 | window1 = teek.Window() 157 | window2 = teek.Window() 158 | toplevel = teek.Toplevel() 159 | 160 | window1.transient = window2 161 | assert window1.transient is window2.toplevel 162 | window1.transient = toplevel 163 | assert window1.transient is toplevel 164 | 165 | 166 | @pytest.mark.slow 167 | def test_wait_window(): 168 | window = teek.Window() 169 | 170 | start = time.time() 171 | teek.after(500, window.destroy) 172 | window.wait_window() 173 | end = time.time() 174 | 175 | assert end - start > 0.5 176 | 177 | 178 | # 'menu' is an option that Toplevel has and Frame doesn't have, so Window must 179 | # use its toplevel's menu option 180 | def test_window_menu_like_options_fallback_to_toplevel_options(): 181 | window = teek.Window() 182 | toplevel = window.toplevel 183 | frame = teek.Frame(window) 184 | menu = teek.Menu() 185 | 186 | assert 'menu' not in frame.config 187 | assert 'menu' in toplevel.config 188 | assert 'menu' in window.config 189 | 190 | with pytest.raises(KeyError): 191 | frame.config['menu'] 192 | assert toplevel.config['menu'] is None 193 | assert window.config['menu'] is None 194 | 195 | window.config['menu'] = menu 196 | assert window.config['menu'] is menu 197 | assert toplevel.config['menu'] is menu 198 | with pytest.raises(KeyError): 199 | frame.config['menu'] 200 | 201 | window.config['width'] = 100 202 | assert isinstance(window.config['width'], teek.ScreenDistance) 203 | -------------------------------------------------------------------------------- /tk-ttk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akuli/teek/c360fbfe086ca09cdd856a8636de05b24e1b7093/tk-ttk.png --------------------------------------------------------------------------------