From df92528fdbfccb9d1711f3c3a35d9721d88cc680 Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Sun, 24 Apr 2022 01:18:18 +0100 Subject: [PATCH 01/29] Initial refactor and tests of the text_box class (and derived classes) Removes Python 2.7 compatibility! (re)add the mouse click behaviour that the refactor had removed (re) add docstrings for the text_box and derived methods (used by sphinx) Move Al's test_easygui.py integration tests to the 'tests' folder for consistency use 1x README.rst instead of (duplicate) .txt and .md README files tweak conda meta.yaml so that bld.bat and build.sh aren't needed rename 'test_cases' -> 'demos' change mose_click_handlers -> MouseClickHandler class remove test_travis.py since we now have enough *real* tests, and Travis is working nicely remove duplicate 'parse_hotkey' method remove 'developer information' remove unused methods (exception_format, uniquify_list_of_strings, getFileDialogTitle, to_string) remove unused constant definitions remove tk 8.0+ warning since it looks like it is now bundled for all 'current' python (v3.7+) Tests will live in the 'tests' directory Demos will live in the 'demos' folder Remove some 'about' files to de-clutter the project. Aim: have more functional files, and less 'print this' type files everywhere. In order to do this, move the 'version' information to __init__.py * use it from the demo boxes *and* from setup.py Moving things from easygui.boxes.__init__ to easygui.boxes.utils ... in preparation of getting rid of boxes directory entirely. I want to keep the easygui.__init__ for import control, and move utilites to 'utils' add more interesting things to the button_box.py default parameter values --- .travis.yml | 2 +- easygui/boxes/about.py => HISTORY.rst | 31 - MANIFEST.in | 2 +- README.md => README.rst | 0 README.txt | 122 - bld.bat | 8 - build.sh | 9 - {test_cases => demos}/SimpleCv.py | 4 +- .../The Pirates of the Caribean Game.py | 14 +- {easygui/boxes => demos}/__init__.py | 0 {test_cases => demos}/audio/intro.mp3 | Bin {test_cases => demos}/audio/intro.ogg | Bin {test_cases => demos}/books.xml | 0 demos/button_box.py | 28 + {easygui/boxes => demos}/demo.py | 61 +- demos/demo_multi_fillable_box.py | 66 + demos/demo_text_box.py | 131 + {test_cases => demos}/dice.py | 5 +- {test_cases => demos}/flash_multiple_rb.py | 4 +- {test_cases => demos}/geo_quiz.py | 0 {test_cases => demos}/gps_auto_update.py | 0 {test_cases => demos}/hex_entry.py | 0 {test_cases => demos}/images/cross.gif | Bin {test_cases => demos}/images/daffy duck.gif | Bin {test_cases => demos}/images/dave.gif | Bin {test_cases => demos}/images/globe.jpg | Bin {test_cases => demos}/images/mickey.gif | Bin {test_cases => demos}/images/minnie.gif | Bin .../images}/python_and_check_logo.gif | Bin .../images}/python_and_check_logo.jpg | Bin .../images}/python_and_check_logo.png | Bin {test_cases => demos}/images/tick.gif | Bin {easygui => demos/images}/zzzzz.gif | Bin {test_cases => demos}/multienter_backspace.py | 0 .../multiple_disney_images.py | 10 +- {test_cases => demos}/pi.jpg | Bin {test_cases => demos}/result.png | Bin {test_cases => demos}/text2binary.py | 0 {test_cases => demos}/xml_parse.py | 5 +- developer_information/BUILD INSTRUCTIONS.txt | 95 - developer_information/DEVELOPER_NOTES.txt | 17 - developer_information/Git-branching-model.png | Bin 193289 -> 0 bytes easygui/__init__.py | 79 +- easygui/__main__.py | 2 - easygui/boxes/base_boxes.py | 27 - easygui/boxes/button_box.py | 507 --- easygui/boxes/choice_box.py | 530 --- easygui/boxes/derived_boxes.py | 428 --- easygui/boxes/diropen_box.py | 64 - easygui/boxes/fileboxsetup.py | 164 - easygui/boxes/fileopen_box.py | 130 - easygui/boxes/filesave_box.py | 89 - easygui/boxes/fillable_box.py | 177 - easygui/boxes/global_state.py | 29 - easygui/boxes/multi_fillable_box.py | 507 --- easygui/boxes/text_box.py | 584 --- easygui/boxes/utils.py | 233 -- easygui/button_box.py | 367 ++ easygui/choice_box.py | 324 ++ easygui/easygui.py | 86 - easygui/{boxes => }/egstore.py | 0 easygui/file_boxes.py | 51 + easygui/fillable_box.py | 207 + easygui/global_state.py | 14 + easygui/multi_fillable_box.py | 118 + easygui/text_box.py | 238 ++ easygui/utilities.py | 154 + meta.yaml | 3 + test/__init__.py => requirements.txt | 0 setup.py | 7 +- sphinx/index.rst | 3356 ++++++++++++++++- test/test_travis.py | 6 - test_cases/__init__.py | 0 test_cases/file_open_box.py | 11 - tests/__init__.py | 2 + {test_cases => tests}/test_easygui.py | 23 +- tests/test_multi_fillable_box.py | 17 + tests/test_text_box.py | 158 + tests/tet_choice_box.py | 57 + 79 files changed, 5383 insertions(+), 3980 deletions(-) rename easygui/boxes/about.py => HISTORY.rst (95%) rename README.md => README.rst (100%) delete mode 100644 README.txt delete mode 100644 bld.bat delete mode 100644 build.sh rename {test_cases => demos}/SimpleCv.py (90%) rename {test_cases => demos}/The Pirates of the Caribean Game.py (55%) rename {easygui/boxes => demos}/__init__.py (100%) rename {test_cases => demos}/audio/intro.mp3 (100%) rename {test_cases => demos}/audio/intro.ogg (100%) rename {test_cases => demos}/books.xml (100%) create mode 100644 demos/button_box.py rename {easygui/boxes => demos}/demo.py (90%) create mode 100644 demos/demo_multi_fillable_box.py create mode 100644 demos/demo_text_box.py rename {test_cases => demos}/dice.py (92%) rename {test_cases => demos}/flash_multiple_rb.py (80%) rename {test_cases => demos}/geo_quiz.py (100%) rename {test_cases => demos}/gps_auto_update.py (100%) rename {test_cases => demos}/hex_entry.py (100%) rename {test_cases => demos}/images/cross.gif (100%) rename {test_cases => demos}/images/daffy duck.gif (100%) rename {test_cases => demos}/images/dave.gif (100%) rename {test_cases => demos}/images/globe.jpg (100%) rename {test_cases => demos}/images/mickey.gif (100%) rename {test_cases => demos}/images/minnie.gif (100%) rename {easygui => demos/images}/python_and_check_logo.gif (100%) rename {easygui => demos/images}/python_and_check_logo.jpg (100%) rename {easygui => demos/images}/python_and_check_logo.png (100%) rename {test_cases => demos}/images/tick.gif (100%) rename {easygui => demos/images}/zzzzz.gif (100%) rename {test_cases => demos}/multienter_backspace.py (100%) rename {test_cases => demos}/multiple_disney_images.py (68%) rename {test_cases => demos}/pi.jpg (100%) rename {test_cases => demos}/result.png (100%) rename {test_cases => demos}/text2binary.py (100%) rename {test_cases => demos}/xml_parse.py (90%) delete mode 100644 developer_information/BUILD INSTRUCTIONS.txt delete mode 100644 developer_information/DEVELOPER_NOTES.txt delete mode 100644 developer_information/Git-branching-model.png delete mode 100644 easygui/__main__.py delete mode 100644 easygui/boxes/base_boxes.py delete mode 100644 easygui/boxes/button_box.py delete mode 100644 easygui/boxes/choice_box.py delete mode 100644 easygui/boxes/derived_boxes.py delete mode 100644 easygui/boxes/diropen_box.py delete mode 100644 easygui/boxes/fileboxsetup.py delete mode 100644 easygui/boxes/fileopen_box.py delete mode 100644 easygui/boxes/filesave_box.py delete mode 100644 easygui/boxes/fillable_box.py delete mode 100644 easygui/boxes/global_state.py delete mode 100644 easygui/boxes/multi_fillable_box.py delete mode 100644 easygui/boxes/text_box.py delete mode 100644 easygui/boxes/utils.py create mode 100644 easygui/button_box.py create mode 100644 easygui/choice_box.py delete mode 100644 easygui/easygui.py rename easygui/{boxes => }/egstore.py (100%) create mode 100644 easygui/file_boxes.py create mode 100644 easygui/fillable_box.py create mode 100644 easygui/global_state.py create mode 100644 easygui/multi_fillable_box.py create mode 100644 easygui/text_box.py create mode 100644 easygui/utilities.py rename test/__init__.py => requirements.txt (100%) delete mode 100644 test/test_travis.py delete mode 100644 test_cases/__init__.py delete mode 100644 test_cases/file_open_box.py create mode 100644 tests/__init__.py rename {test_cases => tests}/test_easygui.py (70%) create mode 100644 tests/test_multi_fillable_box.py create mode 100644 tests/test_text_box.py create mode 100644 tests/tet_choice_box.py diff --git a/.travis.yml b/.travis.yml index c55bf16..15cf31a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python python: - - "2.7" +# - "2.7" # killed it to simplify a lot of things ... hopefully everyone is on Py3.x now - "3.4" - "3.5" - "3.6" diff --git a/easygui/boxes/about.py b/HISTORY.rst similarity index 95% rename from easygui/boxes/about.py rename to HISTORY.rst index f39955c..52de779 100644 --- a/easygui/boxes/about.py +++ b/HISTORY.rst @@ -1,29 +1,3 @@ -""" - -.. moduleauthor:: easygui developers and Stephen Raymond Ferg -.. default-domain:: py -.. highlight:: python - -""" -try: - from .derived_boxes import codebox -except (SystemError, ValueError, ImportError): - from derived_boxes import codebox - -eg_version = '0.98.2-RELEASED' -egversion = eg_version - - -def abouteasygui(): - """ - Shows the EasyGUI revision history. - """ - codebox("About EasyGui\n{}".format(eg_version), - "EasyGui", EASYGUI_ABOUT_INFORMATION) - return None - - -EASYGUI_ABOUT_INFORMATION = ''' 0.98.2 ======================================================================== @@ -254,8 +228,3 @@ def abouteasygui(): * Fixed a bug that was preventing Linux users from copying text out of a textbox and a codebox. This was not a problem for Windows users. - -''' - -if __name__ == '__main__': - abouteasygui() diff --git a/MANIFEST.in b/MANIFEST.in index 4bf4483..c4bf456 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1 @@ -include README.md \ No newline at end of file +include README.rst \ No newline at end of file diff --git a/README.md b/README.rst similarity index 100% rename from README.md rename to README.rst diff --git a/README.txt b/README.txt deleted file mode 100644 index 536dc0d..0000000 --- a/README.txt +++ /dev/null @@ -1,122 +0,0 @@ -EasyGUI -======= - -EasyGUI is a module for very simple, very easy GUI programming in Python. EasyGUI is different from other GUI -libraries in that EasyGUI is NOT event-driven. Instead, all GUI interactions are invoked by simple function calls. - -EasyGUI runs on Python 2 and 3, and does not have any dependencies beyond python and Tk. - -Example Usage -------------- - - >>> import easygui - >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) - 1 - >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') - 'OK' - >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) - 'Chocolate' - - -Full documentation is always available. - -For the most-recent production version: -. - -For our develop version which will be released next: -. - -0.98.2 -======================================================================== -Several updates and fixes thanks to Al and others. - -0.98.0 -======================================================================== -This is an exciting time for easygui. We continue to make good progress with refactoring as -well as some enhancements and bug fixes here and there. - -We would like to welcome Juanjo Denis-Corrales to the team. He is responsible for lots of good new work -this release. Of course we appreciate the work of everyone who contributed. - -NOTE: I decided in this release to change the API a bit. Please consult the function documentation for details. - -BUG FIXES ---------- - * Made changes guessing at fixes to any IDLE problems. Please report any problems found. - -ENHANCEMENTS ------------- - * Refactored the easygui.py file into several smaller files to improve our ability to manage the code - * Added callbacks to allow for more dynamic dialogs. See the docs for usage. - * Added class access to dialogs so properties may be changed. - * in integerbox, allowed lower and upper bounds to be None which means they aren't checked. - -KNOWN ISSUES ------------- - * There were previous issues when using easygui with the IDLE IDE. I hope I resolved these problems, however, - I've never actually been able to repeat them. Please report any problems found in github. - -OTHER CHANGES -------------- - * Centralized the Python 2 versus Python 3 "compatibility layer" into boxes/utils.py - - -0.97.4 -======================================================================== -This is a minor bug-fix release to address python 3 import errors. - -0.97.3 -======================================================================== -We are happy to release version 0.97.3 of easygui. The intent of this release is to address some basic -functionality issues as well as improve easygui in the ways people have asked. - -Robert Lugg (me) was searching for a GUI library for my python work. I saw easygui and liked very much its -paradigm. Stephen Ferg, the creator and developer of easygui, graciously allowed me to start development -back up. With the help of Alexander Zawadzki, Horst Jens, and others I set a goal to release before the -end of 2014. - -We rely on user feedback so please bring up problems, ideas, or just say how you are using easygui. - -BUG FIXES ---------- - * sourceforge #4: easygui docs contain bad references to easygui_pydoc.html - * sourceforge #6: no index.html in docs download file. Updated to sphinx which as autolinking. - * sourceforge #8: unicode issues with file*box. Fixed all that I knew how. - * sourceforge #12: Cannot Exit with 'X'. Now X and escape either return "cancel_button", if set, or None - -ENHANCEMENTS ------------- - * Added ability to specify default_choice and cancel_choice for button widgets (See API docs) - * True and False are returned instead of 1 and 0 for several boxes - * Allow user to map keyboard keys to buttons by enclosing a hotkey in square braces like: "Pick [M]e", which would assign - keyboard key M to that button. Double braces hide that character, and keysyms are allowed: - [[q]]Exit Would show Exit on the button, and the button would be controlled by the q key - []Help Would show Help on the button, and the button would be controlled by the F1 function key - NOTE: We are still working on the exact syntax of these key mappings as Enter, space, and arrows are already being - used. - * Escape and the windows 'X' button always work in buttonboxes. Those return None in that case. - * sourceforge #9: let fileopenbox open multiple files. Added optional argument 'multiple' - * Location of dialogs on screen is preserved. This isn't perfect yet, but now, at least, the dialogs don't - always reset to their default position! - * added some, but not all of the bugs/enhancements developed by Robbie Brook: - http://all-you-need-is-tech.blogspot.com/2013/01/improving-easygui-for-python.html - -KNOWN ISSUES ------------- - * In the documentation, there were previous references to issues when using the IDLE IDE. I haven't - experienced those, but also didn't do anything to fix them, so they may still be there. Please report - any problems and we'll try to address them - * I am fairly new to contributing to open source, so I don't understand packaging, pypi, etc. There - are likely problems as well as better ways to do things. Again, I appreciate any help or guidance. - -Other Changes (that you likely don't care about) ------------------------------------------------- - * Restructured loading of image files to try PIL first throw error if file doesn't exist. - * Converted docs to sphinx with just a bit of doctest. Most content was retained from the old site, so - there might be some redundancies still. Please make any suggested improvements. - * Set up a GitHub repository for development: https://github.com/robertlugg/easygui - * Improved output/packaging for Debian distribution - -EasyGui is licensed under what is generally known as -the "modified BSD license" (aka "revised BSD", "new BSD", "3-clause BSD"). -This license is GPL-compatible but less restrictive than GPL. diff --git a/bld.bat b/bld.bat deleted file mode 100644 index 87b1481..0000000 --- a/bld.bat +++ /dev/null @@ -1,8 +0,0 @@ -"%PYTHON%" setup.py install -if errorlevel 1 exit 1 - -:: Add more build steps here, if they are necessary. - -:: See -:: http://docs.continuum.io/conda/build.html -:: for a list of environment variables that are set during the build process. diff --git a/build.sh b/build.sh deleted file mode 100644 index 4d7fc03..0000000 --- a/build.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -$PYTHON setup.py install - -# Add more build steps here, if they are necessary. - -# See -# http://docs.continuum.io/conda/build.html -# for a list of environment variables that are set during the build process. diff --git a/test_cases/SimpleCv.py b/demos/SimpleCv.py similarity index 90% rename from test_cases/SimpleCv.py rename to demos/SimpleCv.py index 01ad111..4f8e8c7 100644 --- a/test_cases/SimpleCv.py +++ b/demos/SimpleCv.py @@ -1,5 +1,7 @@ __author__ = 'Future Engineer' +import easygui.button_box + """ from: http://stackoverflow.com/questions/27873818/easygui-and-simplecv-typeerror-module-object-is-not-callable @@ -22,7 +24,7 @@ # Set a breakpoint code.interact("Code paused. Hit ctrl-D when ready to continue", local=dict(globals(), **locals())) while True: - eg.msgbox("""Welcome to my program!""", image = "pi.jpg") + easygui.button_box.msgbox("""Welcome to my program!""", image ="pi.jpg") msgbox("Select img ") nam=fileopenbox(filetypes=['*']) print(nam) diff --git a/test_cases/The Pirates of the Caribean Game.py b/demos/The Pirates of the Caribean Game.py similarity index 55% rename from test_cases/The Pirates of the Caribean Game.py rename to demos/The Pirates of the Caribean Game.py index eb84146..5ebf302 100644 --- a/test_cases/The Pirates of the Caribean Game.py +++ b/demos/The Pirates of the Caribean Game.py @@ -4,6 +4,8 @@ """ import sys +import easygui.button_box + sys.path.append('..') import easygui @@ -16,21 +18,21 @@ name = easygui.enterbox("Arrg its me Davy Jones whats your name ye scallywab") txt = "Do you fear DEATH {}? Lets play a game if ye win ye can go if ye lose" txt += " then you are my a sailer on my ship the flying dutchman forever AHAAAA!" -easygui.msgbox(txt.format(name)) -easygui.msgbox("The game be simple ye get 15 chances to guess a number between 1 and 100. Ye be ready?") +easygui.button_box.msgbox(txt.format(name)) +easygui.button_box.msgbox("The game be simple ye get 15 chances to guess a number between 1 and 100. Ye be ready?") while guess != secret and tries < 15: guess = easygui.integerbox("What's your guess "+name) if not guess: break if guess < secret: - easygui.msgbox(str(guess) + " is too low "+name) + easygui.button_box.msgbox(str(guess) + " is too low " + name) elif guess > secret: - easygui.msgbox(str(guess) + " is too high "+name) + easygui.button_box.msgbox(str(guess) + " is too high " + name) tries += 1 if guess == secret: - easygui.msgbox("Arrg ye got it in {}. You can go.".format(tries)) + easygui.button_box.msgbox("Arrg ye got it in {}. You can go.".format(tries)) if tries == 15: - easygui.msgbox("NO more guesses for ye. You're mine forever now {} !! AHAAHAA!!!".format(name)) + easygui.button_box.msgbox("NO more guesses for ye. You're mine forever now {} !! AHAAHAA!!!".format(name)) diff --git a/easygui/boxes/__init__.py b/demos/__init__.py similarity index 100% rename from easygui/boxes/__init__.py rename to demos/__init__.py diff --git a/test_cases/audio/intro.mp3 b/demos/audio/intro.mp3 similarity index 100% rename from test_cases/audio/intro.mp3 rename to demos/audio/intro.mp3 diff --git a/test_cases/audio/intro.ogg b/demos/audio/intro.ogg similarity index 100% rename from test_cases/audio/intro.ogg rename to demos/audio/intro.ogg diff --git a/test_cases/books.xml b/demos/books.xml similarity index 100% rename from test_cases/books.xml rename to demos/books.xml diff --git a/demos/button_box.py b/demos/button_box.py new file mode 100644 index 0000000..fcf8c00 --- /dev/null +++ b/demos/button_box.py @@ -0,0 +1,28 @@ +import os + +from easygui import buttonbox + + +def demo_buttonbox_1(): + print("hello from the demo") + value = buttonbox( + title="First demo", + msg="bonjour", + choices=["Button[1]", "Button[2]", "Button[3]"], + default_choice="Button[2]") + print("Return: {}".format(value)) + + +def demo_buttonbox_2(): + package_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) ;# My parent's directory + images = list() + images.append(os.path.join(package_dir, "python_and_check_logo.gif")) + images.append(os.path.join(package_dir, "zzzzz.gif")) + images.append(os.path.join(package_dir, "python_and_check_logo.png")) + images = [images, images, images, images, ] + value = buttonbox( + title="Second demo", + msg="Now is a good time to press buttons and show images", + choices=['ok', 'cancel'], + images=images) + print("Return: {}".format(value)) \ No newline at end of file diff --git a/easygui/boxes/demo.py b/demos/demo.py similarity index 90% rename from easygui/boxes/demo.py rename to demos/demo.py index 30a0680..d8e0e9f 100644 --- a/easygui/boxes/demo.py +++ b/demos/demo.py @@ -8,37 +8,22 @@ import os import sys +from tkinter import TkVersion + +from easygui import ROOT_DIR, diropenbox, fileopenbox, filesavebox, buttonbox, ynbox, ccbox, boolbox, indexbox, msgbox, \ + version +from easygui.text_box import textbox, codebox, exceptionbox +from easygui.multi_fillable_box import multenterbox +from easygui.multi_fillable_box import multpasswordbox + +from easygui.fillable_box import integerbox +from easygui.fillable_box import enterbox +from easygui.fillable_box import passwordbox + +from easygui.choice_box import choicebox +from easygui.choice_box import multchoicebox + -try: - from . import utils as ut - from .button_box import buttonbox - from .text_box import textbox - from .diropen_box import diropenbox - from .fileopen_box import fileopenbox - from .filesave_box import filesavebox - from .multi_fillable_box import multenterbox - from .multi_fillable_box import multpasswordbox - - from .derived_boxes import ynbox - from .derived_boxes import ccbox - from .derived_boxes import boolbox - from .derived_boxes import indexbox - from .derived_boxes import msgbox - from .derived_boxes import integerbox - from .derived_boxes import enterbox - from .derived_boxes import exceptionbox - from .derived_boxes import codebox - from .derived_boxes import passwordbox - - from .choice_box import choicebox - from .choice_box import multchoicebox - - from . import about - from .about import eg_version - from .about import abouteasygui -except (SystemError, ValueError, ImportError): - print("Please run demo.py from outside the package") - exit() # -------------------------------------------------------------- # # test/demo easygui @@ -82,7 +67,6 @@ def __init__(self): ("fileopenbox", demo_fileopenbox), ("diropenbox", demo_diropenbox), ("exceptionbox", demo_exceptionbox), - ("About EasyGui", demo_about), ("Help", demo_help), ] @@ -115,10 +99,10 @@ def easygui_demo(): msg = [] msg.append("Pick the kind of box that you wish to demo.") msg.append(" * Python version {}".format(sys.version)) - msg.append(" * EasyGui version {}".format(eg_version)) - msg.append(" * Tk version {}".format(ut.TkVersion)) + msg.append(" * EasyGui version {}".format(version)) + msg.append(" * Tk version {}".format(TkVersion)) intro_message = "\n".join(msg) - title = "EasyGui " + eg_version + title = "EasyGui " + version # Table that relates keys in choicebox with functions to execute descriptions = demos.list_descriptions() preselected = 0 @@ -272,12 +256,6 @@ def demo_integerbox(): return reply -def demo_about(): - reply = abouteasygui() - print("Reply was: {!r}".format(reply)) - return reply - - def demo_enterbox(): image = os.path.join(package_dir, "python_and_check_logo.gif") message = ("Enter the name of your best friend." @@ -379,7 +357,8 @@ def demo_passwordbox(): def demo_help(): - codebox("EasyGui Help", text=about.EASYGUI_ABOUT_INFORMATION) + with open(os.path.join(ROOT_DIR, "HISTORY.rst")) as f: + codebox("EasyGui Help", text=f.read()) return None diff --git a/demos/demo_multi_fillable_box.py b/demos/demo_multi_fillable_box.py new file mode 100644 index 0000000..a91e157 --- /dev/null +++ b/demos/demo_multi_fillable_box.py @@ -0,0 +1,66 @@ +from easygui import multenterbox + + +def demo1(): + msg = "Enter your personal information" + title = "Credit Card Application" + fieldNames = ["Name", "Street Address", "City", "State", "ZipCode"] + fieldValues = [] # we start with blanks for the values + + # make sure that none of the fields was left blank + while True: + + fieldValues = multenterbox(msg, title, fieldNames, fieldValues) + cancelled = fieldValues is None + errors = [] + if cancelled: + pass + else: # check for errors + for name, value in zip(fieldNames, fieldValues): + if value.strip() == "": + errors.append('"{}" is a required field.'.format(name)) + + all_ok = not errors + + if cancelled or all_ok: + break # no problems found + + msg = "\n".join(errors) + + print("Reply was: {}".format(fieldValues)) + + +class Demo2(): + + def __init__(self): + msg = "Without flicker. Enter your personal information" + title = "Credit Card Application" + fieldNames = ["Name", "Street Address", "City", "State", "ZipCode"] + fieldValues = [] # we start with blanks for the values + + fieldValues = multenterbox(msg, title, fieldNames, fieldValues, + callback=self.check_for_blank_fields) + print("Reply was: {}".format(fieldValues)) + + def check_for_blank_fields(self, box): + # make sure that none of the fields was left blank + cancelled = box.values is None + errors = [] + if cancelled: + pass + else: # check for errors + for name, value in zip(box.fields, box.values): + if value.strip() == "": + errors.append('"{}" is a required field.'.format(name)) + + all_ok = not errors + + if cancelled or all_ok: + box.stop() # no problems found + + box.msg = "\n".join(errors) + + +if __name__ == '__main__': + demo1() + Demo2() \ No newline at end of file diff --git a/demos/demo_text_box.py b/demos/demo_text_box.py new file mode 100644 index 0000000..746cff7 --- /dev/null +++ b/demos/demo_text_box.py @@ -0,0 +1,131 @@ +from easygui import textbox + +def demo_1(): + + title = "Demo of textbox: Classic box" + + gnexp = ("This is a demo of the classic textbox call, " + "you can see it closes when ok is pressed.\n\n") + + challenge = "INSERT A TEXT WITH MORE THAN TWO PARAGRAPHS" + + text = "Insert your text here\n" + + msg = gnexp + challenge + + finished = False + while True: + + text = textbox(msg, title, text) + escaped = not text + if escaped or finished: + break + + if text.count("\n") >= 2: + msg = (u"You did it right! Press OK") + finished = True + else: + msg = u"You did it wrong! Try again!\n" + challenge + + +class Demo2(object): + + """ Program that challenges the user to write 5 a's """ + + def __init__(self): + """ Set and run the program """ + + title = "Demo of textbox: Classic box with callback" + + gnexp = ("This is a demo of the textbox with a callback, " + "it doesn't flicker!.\n\n") + + msg = "INSERT A TEXT WITH FIVE OR MORE A\'s" + + text_snippet = "Insert your text here" + + self.finished = False + + textbox(gnexp + msg, title, text_snippet, False, + callback=self.check_answer, run=True) + + def check_answer(self, box): + """ Callback from TextBox + + Parameters + ----------- + box: object + object containing parameters and methods to communicate with the ui + + Returns + ------- + nothing: + its return is through the box object + """ + + if self.finished: + box.stop() + + if box.text.lower().count("a") >= 5: + box.msg = u"\n\nYou did it right! Press OK button to continue." + box.stop() + self.finished + else: + box.msg = u"\n\nMore a's are needed!" + + +class Demo3(object): + + """ Program that challenges the user to find a typo """ + + def __init__(self): + """ Set and run the program """ + + self.finished = False + + title = "Demo of textbox: Object with callback" + + msg = ("This is a demo of the textbox set as " + "an object with a callback, " + "you can configure it and when you are finished, " + "you run it.\n\nThere is a typo in it. Find and correct it.") + + text_snippet = "Hello" # This text wont show + + box = textbox( + msg, title, text_snippet, False, callback=self.check_answer, run=False) + + box.text = ( + "It was the west of times, and it was the worst of times. " + "The rich ate cake, and the poor had cake recommended to them, " + "but wished only for enough cash to buy bread." + "The time was ripe for revolution! ") + + box.run() + + def check_answer(self, box): + """ Callback from TextBox + + Parameters + ---------- + box: object + object containing parameters and methods to communicate with the ui + + Returns + ------- + nothing: + its return is through the box object + """ + if self.finished: + box.stop() + + if "best" in box.text: + box.msg = u"\n\nYou did right! Press OK button to continue." + self.finished = True + else: + box.msg = u"\n\nLook to the west!" + +if __name__ == '__main__': + demo_1() + Demo2() + Demo3() \ No newline at end of file diff --git a/test_cases/dice.py b/demos/dice.py similarity index 92% rename from test_cases/dice.py rename to demos/dice.py index dca5ecf..ad7822c 100644 --- a/test_cases/dice.py +++ b/demos/dice.py @@ -1,4 +1,7 @@ __author__ = 'Robert' + +import easygui.button_box + """ from: http://stackoverflow.com/questions/20317314/python-function-in-a-while-loop-ruining-it-for-me @@ -24,7 +27,7 @@ def get_user_input(target_value, dice_rolls): choices = dice_choices else: choices = operator_choices - var = eg.indexbox(''.join(raw_user_input), "Target value: {}".format(target_value), choices) + var = easygui.button_box.indexbox(''.join(raw_user_input), "Target value: {}".format(target_value), choices) if var is None: raise ValueError("Dialog closed with invalid entry") choice = choices[var] diff --git a/test_cases/flash_multiple_rb.py b/demos/flash_multiple_rb.py similarity index 80% rename from test_cases/flash_multiple_rb.py rename to demos/flash_multiple_rb.py index f9da794..1aae19c 100644 --- a/test_cases/flash_multiple_rb.py +++ b/demos/flash_multiple_rb.py @@ -1,5 +1,7 @@ import sys +import easygui.button_box + sys.path.append('..') import easygui @@ -7,7 +9,7 @@ choices = ["on", "off", "forward", "backward", "right", "left"] inp = '' while inp != "None": #happens when the user presses ESC - inp = easygui.buttonbox("controller","robot", choices) + inp = easygui.button_box.buttonbox("controller", "robot", choices) if inp == "forward": pass elif inp == "backward": diff --git a/test_cases/geo_quiz.py b/demos/geo_quiz.py similarity index 100% rename from test_cases/geo_quiz.py rename to demos/geo_quiz.py diff --git a/test_cases/gps_auto_update.py b/demos/gps_auto_update.py similarity index 100% rename from test_cases/gps_auto_update.py rename to demos/gps_auto_update.py diff --git a/test_cases/hex_entry.py b/demos/hex_entry.py similarity index 100% rename from test_cases/hex_entry.py rename to demos/hex_entry.py diff --git a/test_cases/images/cross.gif b/demos/images/cross.gif similarity index 100% rename from test_cases/images/cross.gif rename to demos/images/cross.gif diff --git a/test_cases/images/daffy duck.gif b/demos/images/daffy duck.gif similarity index 100% rename from test_cases/images/daffy duck.gif rename to demos/images/daffy duck.gif diff --git a/test_cases/images/dave.gif b/demos/images/dave.gif similarity index 100% rename from test_cases/images/dave.gif rename to demos/images/dave.gif diff --git a/test_cases/images/globe.jpg b/demos/images/globe.jpg similarity index 100% rename from test_cases/images/globe.jpg rename to demos/images/globe.jpg diff --git a/test_cases/images/mickey.gif b/demos/images/mickey.gif similarity index 100% rename from test_cases/images/mickey.gif rename to demos/images/mickey.gif diff --git a/test_cases/images/minnie.gif b/demos/images/minnie.gif similarity index 100% rename from test_cases/images/minnie.gif rename to demos/images/minnie.gif diff --git a/easygui/python_and_check_logo.gif b/demos/images/python_and_check_logo.gif similarity index 100% rename from easygui/python_and_check_logo.gif rename to demos/images/python_and_check_logo.gif diff --git a/easygui/python_and_check_logo.jpg b/demos/images/python_and_check_logo.jpg similarity index 100% rename from easygui/python_and_check_logo.jpg rename to demos/images/python_and_check_logo.jpg diff --git a/easygui/python_and_check_logo.png b/demos/images/python_and_check_logo.png similarity index 100% rename from easygui/python_and_check_logo.png rename to demos/images/python_and_check_logo.png diff --git a/test_cases/images/tick.gif b/demos/images/tick.gif similarity index 100% rename from test_cases/images/tick.gif rename to demos/images/tick.gif diff --git a/easygui/zzzzz.gif b/demos/images/zzzzz.gif similarity index 100% rename from easygui/zzzzz.gif rename to demos/images/zzzzz.gif diff --git a/test_cases/multienter_backspace.py b/demos/multienter_backspace.py similarity index 100% rename from test_cases/multienter_backspace.py rename to demos/multienter_backspace.py diff --git a/test_cases/multiple_disney_images.py b/demos/multiple_disney_images.py similarity index 68% rename from test_cases/multiple_disney_images.py rename to demos/multiple_disney_images.py index a6a34be..cdd93c1 100644 --- a/test_cases/multiple_disney_images.py +++ b/demos/multiple_disney_images.py @@ -9,11 +9,13 @@ import sys +import easygui.button_box + sys.path.append('..') import easygui as eg # A welcome message -reply = eg.msgbox("Welcome to the quiz", "Quiz!") +reply = easygui.button_box.msgbox("Welcome to the quiz", "Quiz!") if reply is None: exit() @@ -26,14 +28,14 @@ images.append('images/dave.gif') image = "mickey.gif" choices = ["Mickey", "Minnie", "Daffy Duck", "Dave"] - reply = eg.buttonbox("Click on mickey:", images=images, choices=['Cancel']) + reply = easygui.button_box.buttonbox("Click on mickey:", images=images, choices=['Cancel']) print(reply) if reply is None or reply=='Cancel': break if reply == images[0]: - eg.msgbox("Well done!", "Correct") + easygui.button_box.msgbox("Well done!", "Correct") break else: - eg.msgbox("Wrong", "Failure") + easygui.button_box.msgbox("Wrong", "Failure") diff --git a/test_cases/pi.jpg b/demos/pi.jpg similarity index 100% rename from test_cases/pi.jpg rename to demos/pi.jpg diff --git a/test_cases/result.png b/demos/result.png similarity index 100% rename from test_cases/result.png rename to demos/result.png diff --git a/test_cases/text2binary.py b/demos/text2binary.py similarity index 100% rename from test_cases/text2binary.py rename to demos/text2binary.py diff --git a/test_cases/xml_parse.py b/demos/xml_parse.py similarity index 90% rename from test_cases/xml_parse.py rename to demos/xml_parse.py index badb225..4bfcb16 100644 --- a/test_cases/xml_parse.py +++ b/demos/xml_parse.py @@ -1,4 +1,7 @@ __author__ = 'Robert' + +import easygui.button_box + """ from: http://stackoverflow.com/questions/27003762/python-displaying-variable-with-multiple-xml-tags-inside-message-box @@ -29,4 +32,4 @@ books.append('{0} | {1} | {2}'.format(item_name, item_desc, item_status)) # Create message box to display print_xml. - eg.msgbox('\n'.join(books), title="XML Reader") \ No newline at end of file + easygui.button_box.msgbox('\n'.join(books), title="XML Reader") \ No newline at end of file diff --git a/developer_information/BUILD INSTRUCTIONS.txt b/developer_information/BUILD INSTRUCTIONS.txt deleted file mode 100644 index 3f934be..0000000 --- a/developer_information/BUILD INSTRUCTIONS.txt +++ /dev/null @@ -1,95 +0,0 @@ -Introduction: -------------- - -These instructions explain how to actually deploy easygui - -Documentation -------------- -Documentation is auto-built by readthedocs. - -Creating a package ------------------- -* Update README.md and README.txt to include the current release note text -* Update sphinx/conf.py with new version and release date -* Update easygui/easygui.py with new version in eg_version. This should be fixed. -* Update setup.py with new version. update it with any new packages (such as easygui.boxes) -* Update sourceforge site - - -Creating a pypi package ------------------------ - -Links: -Install/packaging: - https://packaging.python.org/en/latest/distributing.html - http://stackoverflow.com/questions/22051360/a-simple-hello-world-setuptools-package-and-installing-it-with-pip - https://hynek.me/articles/sharing-your-labor-of-love-pypi-quick-and-dirty/ -Things I needed to do: -install meld diff tool -pip install --upgrade pip -pip install -U twine -pip install -U wheel -pip install --upgrade virtualenv - -to package it up: -python setup.py sdist -d releases\0.97.4 --formats=gztar,zip -python setup.py bdist_wheel -d releases\0.97.4 - -to package it into an egg: -python setup.py bdist_egg -...but to actually import it (instead of install), I need to change its filename to remove the - - -to tag GitHub repository -git push --tags -git fetch --tags -git tag -a ... -git push --tags - - -- These create two directories: build and dist - -check confirm install works: -virtualenv 27_dist -.\Scripts\activate.bat -then: pip install ...releases..zipfile... -python - import easygui - easygui.egdemo() - - - -To upload to pypi: - -twine upload releases\0.97.4\* - - -Create a .pypirc file in my Users/rlugg (home) directory. The file like: -[distutils] -index-servers= - test-server - pypi - -[test-server] -repository = https://testpypi.python.org/pypi -username = Robert.Lugg -password = XXX - -[pypi] -repository = https://pypi.python.org/pypi -username = Robert.Lugg -password = XXX - -Then: -python setup.py register -r test-server -python setup.py sdist upload -r test-server -python setup.py bdist_wheel upload -r test-server - -xxxxxxxxxxxxxxxxxxxxxxxxxxx -OLD INFO: Not needed. -* Update sourceforge website with 'documentation' directory - sftp: web.sourceforge.net - user: robertlugg (for example) - password: your sourceforge password -* Update pypi docs website: You can now host documentation at http://pythonhosted.org/easygui. To upload documentation, prepare a .zip file that is unpacked into this URL. Only static pages are supported. The zip file must have a top-level "index.html". ------ -# python setup-docs.py sdist -d releases\0.9.3 --formats=gztar,zip # FOR NOW, use 7-zip to create a .tar file diff --git a/developer_information/DEVELOPER_NOTES.txt b/developer_information/DEVELOPER_NOTES.txt deleted file mode 100644 index d99050d..0000000 --- a/developer_information/DEVELOPER_NOTES.txt +++ /dev/null @@ -1,17 +0,0 @@ -Getting Linux up and running. - -Use virtualbox and download a ready-made vdi file for something like experimentalOS - - - - -Setup up git to use meld as default -git config --global merge.tool meld - -Issues with ElementaryOS0.2. I needed, for some reason to install this: -sudo apt-get install python-tk - -Git Stuff: -* We will follow the process described in Git-branching-model.pdf -* To remove a file from Git, but still leave it in the local directory: git rm --cached file.txt -* Tag files when released: $ git tag -a "0.97.3" -m "0.97.3 (2014-12-27)" diff --git a/developer_information/Git-branching-model.png b/developer_information/Git-branching-model.png deleted file mode 100644 index bb110b7bfeee67f7bb94335973fbfdca1eb68285..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 193289 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%0A+$F0`(KfU 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 RXuZL7vTukF-}`Xp{{Z9TNbLXs diff --git a/easygui/__init__.py b/easygui/__init__.py index 4b2c716..811f758 100644 --- a/easygui/__init__.py +++ b/easygui/__init__.py @@ -1,58 +1,37 @@ """ -Hello from easygui/__init__.py +easygui/__init__.py + +__all__ must be defined in order for Sphinx to generate the API automatically. """ -# __all__ must be defined in order for Sphinx to generate the API automatically. -__all__ = ['buttonbox', - 'diropenbox', - 'fileopenbox', - 'filesavebox', - 'textbox', - 'ynbox', - 'ccbox', - 'boolbox', - 'indexbox', - 'msgbox', - 'integerbox', - 'multenterbox', - 'enterbox', - 'exceptionbox', - 'choicebox', - 'codebox', - 'passwordbox', - 'multpasswordbox', - 'multchoicebox', - 'EgStore', - 'eg_version', - 'egversion', - 'abouteasygui', - 'egdemo', +__all__ = [ + # boxes using the ButtonBox class + "buttonbox", "msgbox", "boolbox", "ynbox", "ccbox", "indexbox", + # boxes using the ChoiceBox class + "choicebox", "multchoicebox", + # filedialog boxes that directly use existing tkinter implementation + "fileopenbox", "filesavebox", "diropenbox", + # boxes using the FillableBox class + "fillablebox", "enterbox", "passwordbox", "integerbox", + # boxes using the MultiBox class + "multenterbox", "multpasswordbox", + # boxes using the TextBox class + "textbox", "codebox", "exceptionbox", + # errata + 'EgStore', 'version' ] -# Import all functions that form the API -from .boxes.button_box import buttonbox -from .boxes.diropen_box import diropenbox -from .boxes.fileopen_box import fileopenbox -from .boxes.filesave_box import filesavebox - -from .boxes.text_box import textbox +from .button_box import buttonbox, msgbox, boolbox, ynbox, ccbox, indexbox +from .choice_box import choicebox, multchoicebox +from .egstore import EgStore +from .file_boxes import fileopenbox, filesavebox, diropenbox +from .fillable_box import fillablebox, enterbox, passwordbox, integerbox +from .multi_fillable_box import multenterbox, multpasswordbox +from .text_box import textbox, codebox, exceptionbox +import tkinter as tk # python 3 -from .boxes.derived_boxes import ynbox -from .boxes.derived_boxes import ccbox -from .boxes.derived_boxes import boolbox -from .boxes.derived_boxes import indexbox -from .boxes.derived_boxes import msgbox -from .boxes.derived_boxes import integerbox -from .boxes.multi_fillable_box import multenterbox -from .boxes.derived_boxes import enterbox -from .boxes.derived_boxes import exceptionbox -from .boxes.choice_box import choicebox -from .boxes.derived_boxes import codebox -from .boxes.derived_boxes import passwordbox -from .boxes.multi_fillable_box import multpasswordbox -from .boxes.choice_box import multchoicebox -from .boxes.egstore import EgStore, read_or_create_settings -from .boxes.about import eg_version, egversion, abouteasygui -from .boxes.demo import easygui_demo as egdemo +import os +ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +version = "0.98.3" \ No newline at end of file diff --git a/easygui/__main__.py b/easygui/__main__.py deleted file mode 100644 index 6705118..0000000 --- a/easygui/__main__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .boxes.demo import easygui_demo -easygui_demo() \ No newline at end of file diff --git a/easygui/boxes/base_boxes.py b/easygui/boxes/base_boxes.py deleted file mode 100644 index 7bc67d4..0000000 --- a/easygui/boxes/base_boxes.py +++ /dev/null @@ -1,27 +0,0 @@ -""" - -.. moduleauthor:: easygui developers and Stephen Raymond Ferg -.. default-domain:: py -.. highlight:: python - -Version |release| -""" - -boxRoot = None - - -def bindArrows(widget): - - widget.bind("", tabRight) - widget.bind("", tabLeft) - - widget.bind("", tabRight) - widget.bind("", tabLeft) - - -def tabRight(event): - boxRoot.event_generate("") - - -def tabLeft(event): - boxRoot.event_generate("") diff --git a/easygui/boxes/button_box.py b/easygui/boxes/button_box.py deleted file mode 100644 index 53545ac..0000000 --- a/easygui/boxes/button_box.py +++ /dev/null @@ -1,507 +0,0 @@ -""" - -.. moduleauthor:: easygui developers and Stephen Raymond Ferg -.. default-domain:: py -.. highlight:: python - -Version |release| -""" - -import os -import re - -try: - from . import global_state - from . import utils as ut - from .text_box import textbox -except (SystemError, ValueError, ImportError): - import global_state - import utils as ut - from text_box import textbox - -try: - import tkinter as tk # python 3 - import tkinter.font as tk_Font -except (SystemError, ValueError, ImportError): - import Tkinter as tk # python 2 - import tkFont as tk_Font - - -def demo_buttonbox_1(): - print("hello from the demo") - value = buttonbox( - title="First demo", - msg="bonjour", - choices=["Button[1]", "Button[2]", "Button[3]"], - default_choice="Button[2]") - print("Return: {}".format(value)) - - -def demo_buttonbox_2(): - package_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) ;# My parent's directory - images = list() - images.append(os.path.join(package_dir, "python_and_check_logo.gif")) - images.append(os.path.join(package_dir, "zzzzz.gif")) - images.append(os.path.join(package_dir, "python_and_check_logo.png")) - images = [images, images, images, images, ] - value = buttonbox( - title="Second demo", - msg="Now is a good time to press buttons and show images", - choices=['ok', 'cancel'], - images=images) - print("Return: {}".format(value)) - -# REF: http://stackoverflow.com/questions/1835018/python-check-if-an-object-is-a-list-or-tuple-but-not-string -def is_sequence(arg): - return hasattr(arg, "__getitem__") or hasattr(arg, "__iter__") - -def is_string(arg): - ret_val = None - try: - ret_val = isinstance(arg, basestring) #Python 2 - except: - ret_val = isinstance(arg, str) #Python 3 - return ret_val - -def buttonbox(msg="", - title=" ", - choices=("Button[1]", "Button[2]", "Button[3]"), - image=None, - images=None, - default_choice=None, - cancel_choice=None, - callback=None, - run=True): - """ - Display a message, a title, an image, and a set of buttons. - The buttons are defined by the members of the choices argument. - - :param str msg: the msg to be displayed - :param str title: the window title - :param list choices: a list or tuple of the choices to be displayed - :param str image: (Only here for backward compatibility) - :param str images: Filename of image or iterable or iteratable of iterable to display - :param str default_choice: The choice you want highlighted when the gui appears - :return: the text of the button that the user selected - - - - """ - - if image and images: - raise ValueError("Specify 'images' parameter only for buttonbox.") - if image: - images = image - bb = ButtonBox( - msg=msg, - title=title, - choices=choices, - images=images, - default_choice=default_choice, - cancel_choice=cancel_choice, - callback=callback) - if not run: - return bb - else: - reply = bb.run() - return reply - - -class ButtonBox(object): - """ Display various types of button boxes - - This object separates user from ui, defines which methods can - the user invoke and which properties can he change. - - It also calls the ui in defined ways, so if other gui - library can be used (wx, qt) without breaking anything for the user. - """ - - def __init__(self, msg, title, choices, images, default_choice, cancel_choice, callback): - """ Create box object - - Parameters - ---------- - msg : string - text displayed in the message area (instructions...) - title : str - the window title - choices : iterable of strings - build a button for each string in choices - images : iterable of filenames, or an iterable of iterables of filenames - displays each image - default_choice : string - one of the strings in choices to be the default selection - cancel_choice : string - if X or is pressed, it appears as if this button was pressed. - callback: function - if set, this function will be called when any button is pressed. - - Returns - ------- - object - The box object - """ - - self.callback = callback - self.ui = GUItk(msg, title, choices, images, default_choice, cancel_choice, self.callback_ui) - - def run(self): - """ Start the ui """ - self.ui.run() - ret_val = self._text - self.ui = None - return ret_val - - def stop(self): - """ Stop the ui """ - self.ui.stop() - - def callback_ui(self, ui, command): - """ This method is executed when buttons or x is pressed in the ui. - """ - if command == 'update': # Any button was pressed - self._text = ui.choice - self._choice_rc = ui.choice_rc - if self.callback: - # If a callback was set, call main process - self.callback(self) - else: - self.stop() - elif command == 'x': - self.stop() - self._text = None - elif command == 'cancel': - self.stop() - self._text = None - - # methods to change properties -------------- - @property - def msg(self): - """Text in msg Area""" - return self._msg - - @msg.setter - def msg(self, msg): - self._msg = self.to_string(msg) - self.ui.set_msg(self._msg) - - @msg.deleter - def msg(self): - self._msg = "" - self.ui.set_msg(self._msg) - - @property - def choice(self): - """ Name of button selected """ - return self._text - - @property - def choice_rc(self): - """ The row/column of the selected button (as a tuple) """ - return self._choice_rc - - # Methods to validate what will be sent to ui --------- - - def to_string(self, something): - try: - basestring # python 2 - except NameError: - basestring = str # Python 3 - - if isinstance(something, basestring): - return something - try: - text = "".join(something) # convert a list or a tuple to a string - except: - textbox( - "Exception when trying to convert {} to text in self.textArea" - .format(type(something))) - sys.exit(16) - return text - - -class GUItk(object): - """ This is the object that contains the tk root object""" - - def __init__(self, msg, title, choices, images, default_choice, cancel_choice, callback): - """ Create ui object - - Parameters - ---------- - msg : string - text displayed in the message area (instructions...) - title : str - the window title - choices : iterable of strings - build a button for each string in choices - images : iterable of filenames, or an iterable of iterables of filenames - displays each image - default_choice : string - one of the strings in choices to be the default selection - cancel_choice : string - if X or is pressed, it appears as if this button was pressed. - callback: function - if set, this function will be called when any button is pressed. - - - Returns - ------- - object - The ui object - """ - self._title = title - self._msg = msg - self._choices = choices - self._default_choice = default_choice - self._cancel_choice = cancel_choice - self.callback = callback - self._choice_text = None - self._choice_rc = None - self._images = list() - - self.boxRoot = tk.Tk() - # self.boxFont = tk_Font.Font( - # family=global_state.PROPORTIONAL_FONT_FAMILY, - # size=global_state.PROPORTIONAL_FONT_SIZE) - - self.boxFont = tk_Font.nametofont("TkFixedFont") - self.width_in_chars = global_state.fixw_font_line_length - - # default_font.configure(size=global_state.PROPORTIONAL_FONT_SIZE) - - self.configure_root(title) - - self.create_msg_widget(msg) - - self.create_images_frame() - - self.create_images(images) - - self.create_buttons_frame() - - self.create_buttons(choices, default_choice) - - - @property - def choice(self): - return self._choice_text - - @property - def choice_rc(self): - return self._choice_rc - - # Run and stop methods --------------------------------------- - - def run(self): - self.boxRoot.mainloop() - self.boxRoot.destroy() - - def stop(self): - # Get the current position before quitting - #self.get_pos() - self.boxRoot.quit() - - # Methods to change content --------------------------------------- - def set_msg(self, msg): - self.messageArea.config(state=tk.NORMAL) - self.messageArea.delete(1.0, tk.END) - self.messageArea.insert(tk.END, msg) - self.messageArea.config(state=tk.DISABLED) - # Adjust msg height - self.messageArea.update() - self.set_msg_height() - self.messageArea.update() - - def set_msg_height(self): - message_content = self.messageArea.get("1.0", tk.END) - lines = message_content.split("\n") - width = self.messageArea["width"] - num_lines = len(lines) - num_wordwraps = sum(len(line) // width for line in lines if len(line) != width) - height = num_lines + num_wordwraps + 1 - self.messageArea.configure(height=height) - - def set_pos(self, pos): - self.boxRoot.geometry(pos) - - def get_pos(self): - # The geometry() method sets a size for the window and positions it on - # the screen. The first two parameters are width and height of - # the window. The last two parameters are x and y screen coordinates. - # geometry("250x150+300+300") - geom = self.boxRoot.geometry() # "628x672+300+200" - global_state.window_position = '+' + geom.split('+', 1)[1] - - # Methods executing when a key is pressed ------------------------------- - def x_pressed(self): - self._choice_text = self._cancel_choice - self.callback(self, command='x') - - def cancel_pressed(self, event): - self._choice_text = self._cancel_choice - self.callback(self, command='cancel') - - def button_pressed(self, button_text, button_rc): - self._choice_text = button_text - self._choice_rc = button_rc - self.callback(self, command='update') - - def hotkey_pressed(self, event=None): - """ - Handle an event that is generated by a person interacting with a button. It may be a button press - or a key press. - - TODO: Enhancement: Allow hotkey to be specified in filename of image as a shortcut too!!! - """ - - # Determine window location and save to global - # TODO: Not sure where this goes, but move it out of here! - m = re.match(r"(\d+)x(\d+)([-+]\d+)([-+]\d+)", self.boxRoot.geometry()) - if not m: - raise ValueError( - "failed to parse geometry string: {}".format(self.boxRoot.geometry())) - width, height, xoffset, yoffset = [int(s) for s in m.groups()] - global_state.window_position = '{0:+g}{1:+g}'.format(xoffset, yoffset) - - # Hotkeys - if self._buttons: - for button_name, button in self._buttons.items(): - hotkey_pressed = event.keysym - if event.keysym != event.char: # A special character - hotkey_pressed = '<{}>'.format(event.keysym) - if button['hotkey'] == hotkey_pressed: - self._choice_text = button_name - self.callback(self, command='update') - return - print("Event not understood") - - # Auxiliary methods ----------------------------------------------- - def calc_character_width(self): - char_width = self.boxFont.measure('W') - return char_width - - # Initial configuration methods --------------------------------------- - # These ones are just called once, at setting. - - def configure_root(self, title): - self.boxRoot.title(title) - - self.set_pos(global_state.window_position) - - # Resize setup - self.boxRoot.columnconfigure(0, weight=10) - self.boxRoot.minsize(100, 200) - # Quit when x button pressed - self.boxRoot.protocol('WM_DELETE_WINDOW', self.x_pressed) - self.boxRoot.bind("", self.cancel_pressed) - self.boxRoot.iconname('Dialog') - self.boxRoot.attributes("-topmost", True) # Put the dialog box in focus. - - def create_msg_widget(self, msg): - - if msg is None: - msg = "" - - self.messageArea = tk.Text( - self.boxRoot, - width=self.width_in_chars, - state=tk.DISABLED, - padx=(global_state.default_hpad_in_chars) * - self.calc_character_width(), - relief="flat", - background=self.boxRoot.config()["background"][-1], - pady=global_state.default_hpad_in_chars * - self.calc_character_width(), - wrap=tk.WORD, - ) - self.set_msg(msg) - self.messageArea.grid(row=0) - self.boxRoot.rowconfigure(0, weight=10, minsize='10m') - - def create_images_frame(self): - self.imagesFrame = tk.Frame(self.boxRoot) - row = 1 - self.imagesFrame.grid(row=row) - self.boxRoot.rowconfigure(row, weight=10, minsize='10m') - - def create_images(self, filenames): - """ - Create one or more images in the dialog. - :param filenames: - May be a filename (which will generate a single image), a list of filenames (which will generate - a row of images), or a list of list of filename (which will create a 2D array of buttons. - :return: - """ - if filenames is None: - return - # Convert to a list of lists of filenames regardless of input - if is_string(filenames): - filenames = [[filenames,],] - elif is_sequence(filenames) and is_string(filenames[0]): - filenames = [filenames,] - elif is_sequence(filenames) and is_sequence(filenames[0]) and is_string(filenames[0][0]): - pass - else: - raise ValueError("Incorrect images argument.") - - images = list() - for _r, images_row in enumerate(filenames): - row_number = len(filenames) - _r - for column_number, filename in enumerate(images_row): - this_image = dict() - try: - this_image['tk_image'] = ut.load_tk_image(filename) - except Exception as e: - print(e) - this_image['tk_image'] = None - this_image['widget'] = tk.Button( - self.imagesFrame, - takefocus=1, - compound=tk.TOP) - if this_image['widget'] is not None: - this_image['widget'].configure(image=this_image['tk_image']) - fn = lambda text=filename, row=_r, column=column_number: self.button_pressed(text, (row, column)) - this_image['widget'].configure(command=fn) - sticky_dir = tk.N+tk.S+tk.E+tk.W - this_image['widget'].grid(row=row_number, column=column_number, sticky=sticky_dir, padx='1m', pady='1m', ipadx='2m', ipady='1m') - self.imagesFrame.rowconfigure(row_number, weight=10, minsize='10m') - self.imagesFrame.columnconfigure(column_number, weight=10) - images.append(this_image) - self._images = images # Image objects must live, so place them in self. Otherwise, they will be deleted. - - def create_buttons_frame(self): - self.buttonsFrame = tk.Frame(self.boxRoot) - self.buttonsFrame.grid(row=2, column=0) - - def create_buttons(self, choices, default_choice): - unique_choices = ut.uniquify_list_of_strings(choices) - # Create buttons dictionary and Tkinter widgets - buttons = dict() - i_hack = 0 - for row, (button_text, unique_button_text) in enumerate(zip(choices, unique_choices)): - this_button = dict() - this_button['original_text'] = button_text - this_button['clean_text'], this_button['hotkey'], hotkey_position = ut.parse_hotkey(button_text) - this_button['widget'] = tk.Button( - self.buttonsFrame, - takefocus=1, - text=this_button['clean_text'], - underline=hotkey_position) - fn = lambda text=button_text, row=row, column=0: self.button_pressed(text, (row, column)) - this_button['widget'].configure(command=fn) - this_button['widget'].grid(row=0, column=i_hack, padx='1m', pady='1m', ipadx='2m', ipady='1m') - self.buttonsFrame.columnconfigure(i_hack, weight=10) - i_hack += 1 - buttons[unique_button_text] = this_button - self._buttons = buttons - if default_choice in buttons: - buttons[default_choice]['widget'].focus_force() - # Bind hotkeys - for hk in [button['hotkey'] for button in buttons.values() if button['hotkey']]: - self.boxRoot.bind_all(hk, lambda e: self.hotkey_pressed(e), add=True) - - -if __name__ == '__main__': - demo_buttonbox_1() - demo_buttonbox_2() \ No newline at end of file diff --git a/easygui/boxes/choice_box.py b/easygui/boxes/choice_box.py deleted file mode 100644 index 294906d..0000000 --- a/easygui/boxes/choice_box.py +++ /dev/null @@ -1,530 +0,0 @@ -import string -import sys - -from easygui.boxes.utils import mouse_click_handlers - -if sys.version_info < (3, 10): - from collections import Sequence -else: - from collections.abc import Sequence - -try: - from . import global_state - from .base_boxes import bindArrows -except (SystemError, ValueError, ImportError): - import global_state - from base_boxes import bindArrows - -try: - import tkinter as tk # python 3 - import tkinter.font as tk_Font -except: - import Tkinter as tk # python 2 - import tkFont as tk_Font - - -def choicebox(msg="Pick an item", title="", choices=None, preselect=0, - callback=None, - run=True): - """ - The ``choicebox()`` provides a list of choices in a list box to choose - from. The choices are specified in a sequence (a tuple or a list). - - import easygui - msg ="What is your favorite flavor?" - title = "Ice Cream Survey" - choices = ["Vanilla", "Chocolate", "Strawberry", "Rocky Road"] - choice = easygui.choicebox(msg, title, choices) # choice is a string - - :param str msg: the msg to be displayed - :param str title: the window title - :param list choices: a list or tuple of the choices to be displayed - :param preselect: Which item, if any are preselected when dialog appears - :return: A string of the selected choice or None if cancelled - """ - mb = ChoiceBox(msg, title, choices, preselect=preselect, - multiple_select=False, - callback=callback) - if run: - reply = mb.run() - return reply - else: - return mb - - -def multchoicebox(msg="Pick an item", title="", choices=None, - preselect=0, callback=None, - run=True): - """ - The ``multchoicebox()`` function provides a way for a user to select - from a list of choices. The interface looks just like the ``choicebox()`` - function's dialog box, but the user may select zero, one, or multiple choices. - - The choices are specified in a sequence (a tuple or a list). - - import easygui - msg ="What is your favorite flavor?" - title = "Ice Cream Survey" - choices = ["Vanilla", "Chocolate", "Strawberry", "Rocky Road"] - choice = easygui.multchoicebox(msg, title, choices) - - - :param str msg: the msg to be displayed - :param str title: the window title - :param list choices: a list or tuple of the choices to be displayed - :param preselect: Which item, if any are preselected when dialog appears - :return: A list of strings of the selected choices or None if cancelled. - """ - mb = ChoiceBox(msg, title, choices, preselect=preselect, - multiple_select=True, - callback=callback) - if run: - reply = mb.run() - return reply - else: - return mb - - -# Utility function. But, is it generic enough to be moved out of here? -def make_list_or_none(obj, cast_type=None): - # ------------------------------------------------------------------- - # for an object passed in, put it in standardized form. - # It may be None. Just return None - # If it is a scalar, attempt to cast it into cast_type. Raise error - # if not possible. Convert scalar to a single-element list. - # If it is a collections.Sequence (including a scalar converted to let), - # then cast each element to cast_type. Raise error if any cannot be converted. - # ------------------------------------------------------------------- - ret_val = obj - if ret_val is None: - return None - # Convert any non-sequence to single-element list - if not isinstance(obj, Sequence): - if cast_type is not None: - try: - ret_val = cast_type(obj) - except Exception as e: - raise Exception("Value {} cannot be converted to type: {}".format(obj, cast_type)) - ret_val = [ret_val,] - # Convert all elements to cast_type - if cast_type is not None: - try: - ret_val = [cast_type(elem) for elem in ret_val] - except: - raise Exception("Not all values in {}\n can be converted to type: {}".format(ret_val, cast_type)) - return ret_val - - -class ChoiceBox(object): - - def __init__(self, msg, title, choices, preselect, multiple_select, callback): - - self.callback = callback - - if choices is None: - # Use default choice selections if none were specified: - choices = ('Choice 1', 'Choice 2') - self.choices = self.to_list_of_str(choices) - - # Convert preselect to always be a list or None. - preselect_list = make_list_or_none(preselect, cast_type=int) - if not multiple_select and len(preselect_list)>1: - raise ValueError("Multiple selections not allowed, yet preselect has multiple values:{}".format(preselect_list)) - - self.ui = GUItk(msg, title, self.choices, preselect_list, multiple_select, - self.callback_ui) - - def run(self): - """ Start the ui """ - self.ui.run() - self.ui = None - return self.choices - - def stop(self): - """ Stop the ui """ - self.ui.stop() - - def callback_ui(self, ui, command, choices): - """ This method is executed when ok, cancel, or x is pressed in the ui. - """ - if command == 'update': # OK was pressed - self.choices = choices - if self.callback: - # If a callback was set, call main process - self.callback(self) - else: - self.stop() - elif command == 'x': - self.stop() - self.choices = None - elif command == 'cancel': - self.stop() - self.choices = None - - # methods to change properties -------------- - - @property - def msg(self): - """Text in msg Area""" - return self._msg - - @msg.setter - def msg(self, msg): - self.ui.set_msg(msg) - - @msg.deleter - def msg(self): - self._msg = "" - self.ui.set_msg(self._msg) - - # Methods to validate what will be sent to ui --------- - - def to_list_of_str(self, choices): - choices = [str(c) for c in choices] - - while len(choices) < 2: - raise ValueError('at least two choices need to be specified') - - return choices - - - -class GUItk(object): - - """ This object contains the tk root object. - It draws the window, waits for events and communicates them - to MultiBox, together with the entered values. - - The position in wich it is drawn comes from a global variable. - - It also accepts commands from Multibox to change its message. - """ - - def __init__(self, msg, title, choices, preselect, multiple_select, callback): - - self.callback = callback - - self.choices = choices - - self.width_in_chars = global_state.prop_font_line_length - # Initialize self.selected_choices - # This is the value that will be returned if the user clicks the close - # icon - # self.selected_choices = None - - self.multiple_select = multiple_select - - self.boxRoot = tk.Tk() - - self.boxFont = tk_Font.nametofont("TkTextFont") - - self.config_root(title) - - self.set_pos(global_state.window_position) # GLOBAL POSITION - - self.create_msg_widget(msg) - - self.create_choicearea() - - self.create_ok_button() - - self.create_cancel_button() - - self.create_special_buttons() - - self.preselect_choice(preselect) - - self.choiceboxWidget.focus_force() - - # Run and stop methods --------------------------------------- - - def run(self): - self.boxRoot.mainloop() # run it! - self.boxRoot.destroy() # Close the window - - def stop(self): - # Get the current position before quitting - self.get_pos() - - self.boxRoot.quit() - - def x_pressed(self): - self.callback(self, command='x', choices=self.get_choices()) - - def cancel_pressed(self, event): - self.callback(self, command='cancel', choices=self.get_choices()) - - def ok_pressed(self, event): - self.callback(self, command='update', choices=self.get_choices()) - - # Methods to change content --------------------------------------- - - # Methods to change content --------------------------------------- - - def set_msg(self, msg): - self.messageArea.config(state=tk.NORMAL) - self.messageArea.delete(1.0, tk.END) - self.messageArea.insert(tk.END, msg) - self.messageArea.config(state=tk.DISABLED) - # Adjust msg height - self.messageArea.update() - numlines = self.get_num_lines(self.messageArea) - self.set_msg_height(numlines) - self.messageArea.update() - # put the focus on the entryWidget - - def set_msg_height(self, numlines): - self.messageArea.configure(height=numlines) - - def get_num_lines(self, widget): - end_position = widget.index(tk.END) # '4.0' - end_line = end_position.split('.')[0] # 4 - return int(end_line) + 1 # 5 - - def set_pos(self, pos=None): - if not pos: - pos = global_state.window_position - self.boxRoot.geometry(pos) - - def get_pos(self): - # The geometry() method sets a size for the window and positions it on - # the screen. The first two parameters are width and height of - # the window. The last two parameters are x and y screen coordinates. - # geometry("250x150+300+300") - geom = self.boxRoot.geometry() # "628x672+300+200" - global_state.window_position = '+' + geom.split('+', 1)[1] - - def preselect_choice(self, preselect): - if preselect != None: - for v in preselect: - self.choiceboxWidget.select_set(v) - self.choiceboxWidget.activate(v) - - def get_choices(self): - choices_index = self.choiceboxWidget.curselection() - if not choices_index: - return None - if self.multiple_select: - selected_choices = [self.choiceboxWidget.get(index) - for index in choices_index] - else: - selected_choices = self.choiceboxWidget.get(choices_index) - - return selected_choices - - # Auxiliary methods ----------------------------------------------- - def calc_character_width(self): - char_width = self.boxFont.measure('W') - return char_width - - def config_root(self, title): - - screen_width = self.boxRoot.winfo_screenwidth() - screen_height = self.boxRoot.winfo_screenheight() - self.root_width = int((screen_width * 0.8)) - root_height = int((screen_height * 0.5)) - - self.boxRoot.title(title) - self.boxRoot.iconname('Dialog') - self.boxRoot.expand = tk.NO - # self.boxRoot.minsize(width=62 * self.calc_character_width()) - - self.set_pos() - - self.boxRoot.protocol('WM_DELETE_WINDOW', self.x_pressed) - self.boxRoot.bind('', self.KeyboardListener) - self.boxRoot.bind("", self.cancel_pressed) - - self.boxRoot.attributes("-topmost", True) # Put the dialog box in focus. - - def create_msg_widget(self, msg): - - if msg is None: - msg = "" - - self.msgFrame = tk.Frame( - self.boxRoot, - padx=2 * self.calc_character_width(), - - ) - self.messageArea = tk.Text( - self.msgFrame, - width=self.width_in_chars, - state=tk.DISABLED, - background=self.boxRoot.config()["background"][-1], - relief='flat', - padx=(global_state.default_hpad_in_chars * - self.calc_character_width()), - pady=(global_state.default_hpad_in_chars * - self.calc_character_width()), - wrap=tk.WORD, - - ) - self.set_msg(msg) - - self.msgFrame.pack(side=tk.TOP, expand=1, fill='both') - - self.messageArea.pack(side=tk.TOP, expand=1, fill='both') - - def create_choicearea(self): - - self.choiceboxFrame = tk.Frame(master=self.boxRoot) - self.choiceboxFrame.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.YES) - - lines_to_show = min(len(self.choices), 20) - - # -------- put the self.choiceboxWidget in the self.choiceboxFrame --- - self.choiceboxWidget = tk.Listbox(self.choiceboxFrame, - height=lines_to_show, - borderwidth="1m", relief="flat", - bg="white" - ) - - if self.multiple_select: - self.choiceboxWidget.configure(selectmode=tk.MULTIPLE) - - # self.choiceboxWidget.configure(font=(global_state.PROPORTIONAL_FONT_FAMILY, - # global_state.PROPORTIONAL_FONT_SIZE)) - - # add a vertical scrollbar to the frame - rightScrollbar = tk.Scrollbar(self.choiceboxFrame, orient=tk.VERTICAL, - command=self.choiceboxWidget.yview) - self.choiceboxWidget.configure(yscrollcommand=rightScrollbar.set) - - # add a horizontal scrollbar to the frame - bottomScrollbar = tk.Scrollbar(self.choiceboxFrame, - orient=tk.HORIZONTAL, - command=self.choiceboxWidget.xview) - self.choiceboxWidget.configure(xscrollcommand=bottomScrollbar.set) - - # pack the Listbox and the scrollbars. - # Note that although we must define - # the textArea first, we must pack it last, - # so that the bottomScrollbar will - # be located properly. - - bottomScrollbar.pack(side=tk.BOTTOM, fill=tk.X) - rightScrollbar.pack(side=tk.RIGHT, fill=tk.Y) - - self.choiceboxWidget.pack( - side=tk.LEFT, padx="1m", pady="1m", expand=tk.YES, fill=tk.BOTH) - - # Insert choices widgets - for choice in self.choices: - self.choiceboxWidget.insert(tk.END, choice) - - # Bind the keyboard events - self.choiceboxWidget.bind("", self.ok_pressed) - self.choiceboxWidget.bind("", - self.ok_pressed) - - def create_ok_button(self): - - self.buttonsFrame = tk.Frame(self.boxRoot) - self.buttonsFrame.pack(side=tk.TOP, expand=tk.YES, pady=0) - - # put the buttons in the self.buttonsFrame - okButton = tk.Button(self.buttonsFrame, takefocus=tk.YES, - text="OK", height=1, width=6) - bindArrows(okButton) - okButton.pack(expand=tk.NO, side=tk.RIGHT, padx='2m', pady='1m', - ipady="1m", ipadx="2m") - - # for the commandButton, bind activation events - okButton.bind("", self.ok_pressed) - okButton.bind("", self.ok_pressed) - - mouse_handlers = mouse_click_handlers(self.ok_pressed) - for selectionEvent in global_state.STANDARD_SELECTION_EVENTS_MOUSE: - okButton.bind("<%s>" % selectionEvent, mouse_handlers[selectionEvent]) - - def create_cancel_button(self): - cancelButton = tk.Button(self.buttonsFrame, takefocus=tk.YES, - text="Cancel", height=1, width=6) - bindArrows(cancelButton) - cancelButton.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', - ipady="1m", ipadx="2m") - cancelButton.bind("", self.cancel_pressed) - - mouse_handlers = mouse_click_handlers(self.cancel_pressed) - for selectionEvent in global_state.STANDARD_SELECTION_EVENTS_MOUSE: - cancelButton.bind("<%s>" % selectionEvent, mouse_handlers[selectionEvent]) - - def create_special_buttons(self): - # add special buttons for multiple select features - if not self.multiple_select: - return - - selectAllButton = tk.Button( - self.buttonsFrame, text="Select All", height=1, width=6) - selectAllButton.pack(expand=tk.NO, side=tk.LEFT, padx='2m', - pady='1m', - ipady="1m", ipadx="2m") - - clearAllButton = tk.Button(self.buttonsFrame, text="Clear All", - height=1, width=6) - clearAllButton.pack(expand=tk.NO, side=tk.LEFT, - padx='2m', pady='1m', - ipady="1m", ipadx="2m") - - selectAllButton.bind("", self.choiceboxSelectAll) - bindArrows(selectAllButton) - clearAllButton.bind("", self.choiceboxClearAll) - bindArrows(clearAllButton) - - def KeyboardListener(self, event): - key = event.keysym - if len(key) <= 1: - if key in string.printable: - # Find the key in the liglobal_state. - # before we clear the list, remember the selected member - try: - start_n = int(self.choiceboxWidget.curselection()[0]) - except IndexError: - start_n = -1 - - # clear the selection. - self.choiceboxWidget.selection_clear(0, 'end') - - # start from previous selection +1 - for n in range(start_n + 1, len(self.choices)): - item = self.choices[n] - if item[0].lower() == key.lower(): - self.choiceboxWidget.selection_set(first=n) - self.choiceboxWidget.see(n) - return - else: - # has not found it so loop from top - for n, item in enumerate(self.choices): - if item[0].lower() == key.lower(): - self.choiceboxWidget.selection_set(first=n) - self.choiceboxWidget.see(n) - return - - # nothing matched -- we'll look for the next logical choice - for n, item in enumerate(self.choices): - if item[0].lower() > key.lower(): - if n > 0: - self.choiceboxWidget.selection_set( - first=(n - 1)) - else: - self.choiceboxWidget.selection_set(first=0) - self.choiceboxWidget.see(n) - return - - # still no match (nothing was greater than the key) - # we set the selection to the first item in the list - lastIndex = len(self.choices) - 1 - self.choiceboxWidget.selection_set(first=lastIndex) - self.choiceboxWidget.see(lastIndex) - return - - def choiceboxClearAll(self, event): - self.choiceboxWidget.selection_clear(0, len(self.choices) - 1) - - def choiceboxSelectAll(self, event): - self.choiceboxWidget.selection_set(0, len(self.choices) - 1) - -if __name__ == '__main__': - users_choice = multchoicebox(choices=['choice1', 'choice2']) - print("User's choice is: {}".format(users_choice)) diff --git a/easygui/boxes/derived_boxes.py b/easygui/boxes/derived_boxes.py deleted file mode 100644 index e8c92d0..0000000 --- a/easygui/boxes/derived_boxes.py +++ /dev/null @@ -1,428 +0,0 @@ -""" - -.. moduleauthor:: easygui developers and Stephen Raymond Ferg -.. default-domain:: py -.. highlight:: python - -""" - -try: - from .fillable_box import __fillablebox - from .button_box import buttonbox - from . import text_box as tb - from . import utils as ut -except (SystemError, ValueError, ImportError): - from fillable_box import __fillablebox - from button_box import buttonbox - import text_box as tb - import utils as ut - -# ------------------------------------------------------------------- -# various boxes built on top of the basic buttonbox -# ----------------------------------------------------------------------- - -# ----------------------------------------------------------------------- -# ynbox -# ----------------------------------------------------------------------- - - -def ynbox(msg="Shall I continue?", title=" ", - choices=("[]Yes", "[]No"), image=None, - default_choice='[]Yes', cancel_choice='[]No'): - """ - The ``ynbox()`` offers a choice of Yes and No, and returns either ``True`` or ``False``. - - import easygui - result = easygui.ynbox('Is a hot dog a sandwich?', 'Hot Dog Question') - if result == True: - easygui.msgbox('That is an interesting answer.') - else: - easygui.msgbox('Well, that is your opinion.') - - :param msg: the msg to be displayed - :type msg: str - :param str title: the window title - :param list choices: a list or tuple of the choices to be displayed - :param str image: Filename of image to display - :param str default_choice: The choice you want highlighted - when the gui appears - :param str cancel_choice: If the user presses the 'X' close, which - button should be pressed - - :return: True if 'Yes' or dialog is cancelled, False if 'No' - """ - return boolbox(msg=msg, - title=title, - choices=choices, - image=image, - default_choice=default_choice, - cancel_choice=cancel_choice) - -# ----------------------------------------------------------------------- -# ccbox -# ----------------------------------------------------------------------- - - -def ccbox(msg="Shall I continue?", title=" ", - choices=("C[o]ntinue", "C[a]ncel"), image=None, - default_choice='Continue', cancel_choice='Cancel'): - """ - The ``ccbox()`` function offers a choice of Continue and Cancel, and returns either True (for continue) or False (for cancel). - - import easygui - msg = "Do you want to continue?" - title = "Please Confirm" - if easygui.ccbox(msg, title): # Show a Continue/Cancel dialog. - pass # User chose Continue. - else: # User chose Cancel. - sys.exit() - - :param str msg: the msg to be displayed - :param str title: the window title - :param list choices: a list or tuple of the choices to be displayed - :param str image: Filename of image to display - :param str default_choice: The choice you want highlighted - when the gui appears - :param str cancel_choice: If the user presses the 'X' close, - which button should be pressed - - :return: True if 'Continue' or dialog is cancelled, False if 'Cancel' - """ - return boolbox(msg=msg, - title=title, - choices=choices, - image=image, - default_choice=default_choice, - cancel_choice=cancel_choice) - -# ----------------------------------------------------------------------- -# boolbox -# ----------------------------------------------------------------------- - - -def boolbox(msg="Shall I continue?", title=" ", - choices=("[T]rue", "[F]alse"), image=None, - default_choice='[T]rue', cancel_choice='[F]alse'): - """ - The ``boolbox()`` (boolean box) displays two buttons. Returns returns - ``True`` if the first button is chosen. Otherwise returns ``False``. - - import easygui - message = "What do they say?" - title = "Romantic Question" - if easygui.boolbox(message, title, ["They love me", "They love me not"]): - easygui.msgbox('You should send them flowers.') - else: - easygui.msgbox('It was not meant to be.') - - :param str msg: The message shown in the center of the dialog window. - :param str title: The window title text. - :param list choices: A list or tuple of strings for the buttons' text. - :param str image: The filename of an image to display in the dialog window. - :param str default_choice: The text of the default selected button. - :param str cancel_choice: If the user presses the 'X' close, which button - should be pressed - :return: `True` if first button pressed or dialog is cancelled, `False` - if second button is pressed. - """ - if len(choices) != 2: - raise AssertionError( - 'boolbox() takes exactly 2 choices! Consider using indexbox() instead.' - ) - - reply = buttonbox(msg=msg, - title=title, - choices=choices, - image=image, - default_choice=default_choice, - cancel_choice=cancel_choice) - - if reply == choices[0]: - return True # The first button (True) was selected. - elif reply == choices[1]: - return False # The second button (False) was selected. - elif reply is None: - return None # The window was closed. - - assert False, "The user selected an unexpected response." - - -# ----------------------------------------------------------------------- -# indexbox -# ----------------------------------------------------------------------- -def indexbox(msg="Shall I continue?", title=" ", - choices=("Yes", "No"), image=None, - default_choice='Yes', cancel_choice='No'): - """ - The ``indexbox()`` function displays a set of buttons, and returns the - index of the selected button. For example, if you invoked index box with - three choices (A, B, C), indexbox would return 0 if the user picked A, 1 - if he picked B, and 2 if he picked C. - - import easygui - result = easygui.indexbox('Which door do you choose?', 'Win Prizes!', choices=['Door 1', 'Door 2', 'Door 3']) - if result == 2: - easygui.msgbox('You win a new car!') - else: - easygui.msgbox('Better luck next time.') - - :param str msg: the msg to be displayed - :param str title: the window title - :param list choices: a list or tuple of the choices to be displayed - :param str image: Filename of image to display - :param str default_choice: The choice you want highlighted - when the gui appears - :param str cancel_choice: If the user presses the 'X' close, - which button should be pressed - :return: the index of the choice selected, starting from 0 - """ - reply = buttonbox(msg=msg, - title=title, - choices=choices, - image=image, - default_choice=default_choice, - cancel_choice=cancel_choice) - if reply is None: - return None - for i, choice in enumerate(choices): - if reply == choice: - return i - msg = ("There is a program logic error in the EasyGui code " - "for indexbox.\nreply={0}, choices={1}".format( - reply, choices)) - raise AssertionError(msg) - - -# ----------------------------------------------------------------------- -# msgbox -# ----------------------------------------------------------------------- -def msgbox(msg="(Your message goes here)", title=" ", - ok_button="OK", image=None, root=None): - """ - The ``msgbox()`` function displays a text message and offers an OK - button. The message text appears in the center of the window, the title - text appears in the title bar, and you can replace the "OK" default text - on the button. Here is the signature:: - - def msgbox(msg="(Your message goes here)", title="", ok_button="OK"): - .... - - The clearest way to override the button text is to do it with a keyword - argument, like this:: - - easygui.msgbox("Backup complete!", ok_button="Good job!") - - Here are a couple of examples:: - - easygui.msgbox("Hello, world!") - - :param str msg: the msg to be displayed - :param str title: the window title - :param str ok_button: text to show in the button - :param str image: Filename of image to display - :param tk_widget root: Top-level Tk widget - :return: the text of the ok_button - """ - if not isinstance(ok_button, ut.basestring): - raise AssertionError( - "The 'ok_button' argument to msgbox must be a string.") - - return buttonbox(msg=msg, - title=title, - choices=[ok_button], - image=image, - default_choice=ok_button, - cancel_choice=ok_button) - - -def convert_to_type(input_value, new_type, input_value_name=None): - """ - Attempts to convert input_value to type new_type and throws error if it can't. - - If input_value is None, None is returned - If new_type is None, input_value is returned unchanged - :param input_value: Value to be converted - :param new_type: Type to convert to - :param input_value_name: If not None, used in error message if input_value cannot be converted - :return: input_value converted to new_type, or None - """ - if input_value is None or new_type is None: - return input_value - - exception_string = ( - 'value {0}:{1} must be of type {2}.') - ret_value = new_type(input_value) -# except ValueError: -# raise ValueError( -# exception_string.format('default', default, type(default))) - return ret_value - - -# ------------------------------------------------------------------- -# integerbox -# ------------------------------------------------------------------- -def integerbox(msg="", title=" ", default=None, - lowerbound=0, upperbound=99, image=None, root=None): - """ - Show a box in which a user can enter an integer. - - In addition to arguments for msg and title, this function accepts - integer arguments for "default", "lowerbound", and "upperbound". - - The default, lowerbound, or upperbound may be None. - - When the user enters some text, the text is checked to verify that it - can be converted to an integer between the lowerbound and upperbound. - - If it can be, the integer (not the text) is returned. - - If it cannot, then an error msg is displayed, and the integerbox is - redisplayed. - - If the user cancels the operation, None is returned. - - :param str msg: the msg to be displayed - :param str title: the window title - :param int default: The default value to return - :param int lowerbound: The lower-most value allowed - :param int upperbound: The upper-most value allowed - :param str image: Filename of image to display - :param tk_widget root: Top-level Tk widget - :return: the integer value entered by the user - - """ - - if not msg: - msg = "Enter an integer between {0} and {1}".format( - lowerbound, upperbound) - - # Validate the arguments for default, lowerbound and upperbound and - # convert to integers - default = convert_to_type(default, int, "default") - lowerbound = convert_to_type(lowerbound, int, "lowerbound") - upperbound = convert_to_type(upperbound, int, "upperbound") - - while True: - reply = enterbox(msg, title, default, image=image, root=root) - if reply is None: - return None - try: - reply = convert_to_type(reply, int) - except ValueError: - msgbox('The value that you entered:\n\t"{}"\nis not an integer.'.format(reply), "Error") - continue - if lowerbound is not None: - if reply < lowerbound: - msgbox('The value that you entered is less than the lower bound of {}.'.format(lowerbound), "Error") - continue - if upperbound is not None: - if reply > upperbound: - msgbox('The value that you entered is greater than the upper bound of {}.'.format(upperbound), "Error") - continue - # reply has passed all validation checks. - # It is an integer between the specified bounds. - break - return reply - - - - - - - -# ------------------------------------------------------------------- -# enterbox -# ------------------------------------------------------------------- -def enterbox(msg="Enter something.", title=" ", default="", - strip=True, image=None, root=None): - """ - Show a box in which a user can enter some text. - - You may optionally specify some default text, which will appear in the - enterbox when it is displayed. - - Example:: - - import easygui - reply = easygui.enterbox('Enter your life story:') - if reply: - easygui.msgbox('Thank you for your response.') - else: - easygui.msgbox('Your response has been discarded.') - - :param str msg: the msg to be displayed. - :param str title: the window title - :param str default: value returned if user does not change it - :param bool strip: If True, the return value will have - its whitespace stripped before being returned - :return: the text that the user entered, or None if they cancel - the operation. - """ - result = __fillablebox( - msg, title, default=default, mask=None, image=image, root=root) - if result and strip: - result = result.strip() - return result - - -def passwordbox(msg="Enter your password.", title=" ", default="", - image=None, root=None): - """ - Show a box in which a user can enter a password. - The text is masked with asterisks, so the password is not displayed. - - :param str msg: the msg to be displayed. - :param str title: the window title - :param str default: value returned if user does not change it - :return: the text that the user entered, or None if they cancel - the operation. - """ - return __fillablebox(msg, title, default, mask="*", - image=image, root=root) - - -# ----------------------------------------------------------------------- -# exceptionbox -# ----------------------------------------------------------------------- -def exceptionbox(msg=None, title=None): - """ - Display a box that gives information about - an exception that has just been raised. - - The caller may optionally pass in a title for the window, or a - msg to accompany the error information. - - Note that you do not need to (and cannot) pass an exception object - as an argument. The latest exception will automatically be used. - - :param str msg: the msg to be displayed - :param str title: the window title - :return: None - - """ - if title is None: - title = "Error Report" - if msg is None: - msg = "An error (exception) has occurred in the program." - - codebox(msg, title, ut.exception_format()) - - -# ------------------------------------------------------------------- -# codebox -# ------------------------------------------------------------------- - -def codebox(msg="", title=" ", text=""): - """ - Display some text in a monospaced font, with no line wrapping. - This function is suitable for displaying code and text that is - formatted using spaces. - - The text parameter should be a string, or a list or tuple of lines to be - displayed in the textbox. - - :param str msg: the msg to be displayed - :param str title: the window title - :param str text: what to display in the textbox - """ - return tb.textbox(msg, title, text, codebox=True) diff --git a/easygui/boxes/diropen_box.py b/easygui/boxes/diropen_box.py deleted file mode 100644 index fa626b1..0000000 --- a/easygui/boxes/diropen_box.py +++ /dev/null @@ -1,64 +0,0 @@ -""" - -.. moduleauthor:: easygui developers and Stephen Raymond Ferg -.. default-domain:: py -.. highlight:: python - -Version |release| -""" - - -import os -try: - from . import utils as ut -except (SystemError, ValueError, ImportError): - import utils as ut - -try: - import tkinter as tk # python 3 - import tkinter.font as tk_Font -except: - import Tkinter as tk # python 2 - import tkFont as tk_Font - - -# ------------------------------------------------------------------- -# diropenbox -# ------------------------------------------------------------------- -def diropenbox(msg=None, title=None, default=None): - """ - A dialog to get a directory name. - - Returns the name of a directory, or None if user chose to cancel. - - If the "default" argument specifies a directory name, and that - directory exists, then the dialog box will start with that directory. - - :param str msg: used in the window title on some platforms - :param str title: the window title - :param str default: starting directory when dialog opens - :return: Normalized path selected by user - """ - title = ut.getFileDialogTitle(msg, title) - localRoot = tk.Tk() - localRoot.withdraw() - localRoot.attributes("-topmost", True) - if not default: - default = None - localRoot.update() #fix ghost window issue #119 on mac. - f = ut.tk_FileDialog.askdirectory( - parent=localRoot, title=title, initialdir=default, initialfile=None - ) - localRoot.destroy() - if not f: - return None - return os.path.normpath(f) - - -if __name__ == '__main__': - print("Hello from base_boxes") - my_dir = diropenbox( - "You really should open a file", - title="Open a dir", - default='./') - print("directory {} selected.".format(my_dir)) diff --git a/easygui/boxes/fileboxsetup.py b/easygui/boxes/fileboxsetup.py deleted file mode 100644 index 739f904..0000000 --- a/easygui/boxes/fileboxsetup.py +++ /dev/null @@ -1,164 +0,0 @@ -""" - -.. moduleauthor:: easygui developers and Stephen Raymond Ferg -.. default-domain:: py -.. highlight:: python - -Version |release| - -""" -import os -try: - from . import utils as ut -except (SystemError, ValueError, ImportError): - import utils as ut - -try: - import tkinter as tk # python 3 - import tkinter.font as tk_Font -except: - import Tkinter as tk # python 2 - import tkFont as tk_Font - -# ------------------------------------------------------------------- -# -# fileboxSetup -# -# ------------------------------------------------------------------- - - -def fileboxSetup(default, filetypes): - if not default: - default = os.path.join(".", "*") - initialdir, initialfile = os.path.split(default) - if not initialdir: - initialdir = "." - if not initialfile: - initialfile = "*" - initialbase, initialext = os.path.splitext(initialfile) - initialFileTypeObject = FileTypeObject(initialfile) - - allFileTypeObject = FileTypeObject("*") - ALL_filetypes_was_specified = False - - if not filetypes: - filetypes = list() - filetypeObjects = list() - - for filemask in filetypes: - fto = FileTypeObject(filemask) - - if fto.isAll(): - ALL_filetypes_was_specified = True # remember this - - if fto == initialFileTypeObject: - initialFileTypeObject.add(fto) # add fto to initialFileTypeObject - else: - filetypeObjects.append(fto) - - # ------------------------------------------------------------------ - # make sure that the list of filetypes includes the ALL FILES type. - # ------------------------------------------------------------------ - if ALL_filetypes_was_specified: - pass - elif allFileTypeObject == initialFileTypeObject: - pass - else: - filetypeObjects.insert(0, allFileTypeObject) - # ------------------------------------------------------------------ - # Make sure that the list includes the initialFileTypeObject - # in the position in the list that will make it the default. - # This changed between Python version 2.5 and 2.6 - # ------------------------------------------------------------------ - if len(filetypeObjects) == 0: - filetypeObjects.append(initialFileTypeObject) - - if initialFileTypeObject in (filetypeObjects[0], filetypeObjects[-1]): - pass - else: - if ut.runningPython27: - filetypeObjects.append(initialFileTypeObject) - else: - filetypeObjects.insert(0, initialFileTypeObject) - - filetypes = [fto.toTuple() for fto in filetypeObjects] - - return initialbase, initialfile, initialdir, filetypes - - # Hotkeys - if buttons: - for button_name, button in buttons.items(): - hotkey_pressed = event.keysym - if event.keysym != event.char: # A special character - hotkey_pressed = '<{}>'.format(event.keysym) - if button['hotkey'] == hotkey_pressed: - __replyButtonText = button_name - boxRoot.quit() - return - - print("Event not understood") - - -# ------------------------------------------------------------------- -# class FileTypeObject for use with fileopenbox -# ------------------------------------------------------------------- -class FileTypeObject: - - def __init__(self, filemask): - if len(filemask) == 0: - raise AssertionError('Filetype argument is empty.') - - self.masks = list() - - if isinstance(filemask, ut.basestring): # a str or unicode - self.initializeFromString(filemask) - - elif isinstance(filemask, list): - if len(filemask) < 2: - raise AssertionError('Invalid filemask.\n' - + 'List contains less than 2 members: "{}"'.format(filemask)) - else: - self.name = filemask[-1] - self.masks = list(filemask[:-1]) - else: - raise AssertionError('Invalid filemask: "{}"'.format(filemask)) - - def __eq__(self, other): - if self.name == other.name: - return True - return False - - def add(self, other): - for mask in other.masks: - if mask in self.masks: - pass - else: - self.masks.append(mask) - - def toTuple(self): - return self.name, tuple(self.masks) - - def isAll(self): - if self.name == "All files": - return True - return False - - def initializeFromString(self, filemask): - # remove everything except the extension from the filemask - self.ext = os.path.splitext(filemask)[1] - if self.ext == "": - self.ext = ".*" - if self.ext == ".": - self.ext = ".*" - self.name = self.getName() - self.masks = ["*" + self.ext] - - def getName(self): - e = self.ext - file_types = {".*": "All", ".txt": "Text", - ".py": "Python", ".pyc": "Python", ".xls": "Excel"} - if e in file_types: - return '{} files'.format(file_types[e]) - if e.startswith("."): - return '{} files'.format(e[1:].upper()) - return '{} files'.format(e.upper()) diff --git a/easygui/boxes/fileopen_box.py b/easygui/boxes/fileopen_box.py deleted file mode 100644 index ea94eaf..0000000 --- a/easygui/boxes/fileopen_box.py +++ /dev/null @@ -1,130 +0,0 @@ -from __future__ import print_function -""" - -.. moduleauthor:: easygui developers and Stephen Raymond Ferg -.. default-domain:: py -.. highlight:: python - -Version |release| -""" - -import os -try: - from . import utils as ut - from . import fileboxsetup as fbs -except (SystemError, ValueError, ImportError): - import utils as ut - import fileboxsetup as fbs - -tk = ut.tk - - - -# ------------------------------------------------------------------- -# fileopenbox -# ------------------------------------------------------------------- - - -def fileopenbox(msg=None, title=None, default='*', filetypes=None, multiple=False): - """ - Displays an "open file" dialog box and returns the selected file as a string. - - The "default" argument specifies a filepath that (normally) - contains one or more wildcards. - - fileopenbox() will display only files that match the default filepath. - If omitted, defaults to "\\*" (all files in the current directory). - - WINDOWS EXAMPLE:: - - ...default="c:/myjunk/*.py" - - will open in directory c:\\myjunk\\ and show all Python files. - - WINDOWS EXAMPLE:: - - ...default="c:/myjunk/test*.py" - - will open in directory c:\\myjunk\\ and show all Python files - whose names begin with "test". - - - Note that on Windows, fileopenbox automatically changes the path - separator to the Windows path separator (backslash). - - **About the "filetypes" argument** - - If specified, it should contain a list of items, - where each item is either: - - - a string containing a filemask # e.g. "\\*.txt" - - a list of strings, where all of the strings except the last one - are filemasks (each beginning with "\\*.", - such as "\\*.txt" for text files, "\\*.py" for Python files, etc.). - and the last string contains a filetype description - - EXAMPLE:: - - filetypes = ["*.css", ["*.htm", "*.html", "HTML files"] ] - - .. note:: If the filetypes list does not contain ("All files","*"), it will be added. - - If the filetypes list does not contain a filemask that includes - the extension of the "default" argument, it will be added. - For example, if default="\\*abc.py" - and no filetypes argument was specified, then - "\\*.py" will automatically be added to the filetypes argument. - - :param str msg: the msg to be displayed. - :param str title: the window title - :param str default: filepath with wildcards - :param object filetypes: filemasks that a user can choose, e.g. "\\*.txt" - :param bool multiple: If true, more than one file can be selected - :return: the name of a file, or None if user chose to cancel - """ - localRoot = tk.Tk() - localRoot.withdraw() - localRoot.attributes("-topmost", True) - - initialbase, initialfile, initialdir, filetypes = fbs.fileboxSetup( - default, filetypes) - - # ------------------------------------------------------------ - # if initialfile contains no wildcards; we don't want an - # initial file. It won't be used anyway. - # Also: if initialbase is simply "*", we don't want an - # initialfile; it is not doing any useful work. - # ------------------------------------------------------------ - if (initialfile.find("*") < 0) and (initialfile.find("?") < 0): - initialfile = None - elif initialbase == "*": - initialfile = None - - func = ut.tk_FileDialog.askopenfilenames if multiple else ut.tk_FileDialog.askopenfilename - ret_val = func(parent=localRoot, - title=ut.getFileDialogTitle(msg, title), - initialdir=initialdir, initialfile=initialfile, - filetypes=filetypes - ) - if not ret_val or ret_val == '': - localRoot.destroy() - return None - if multiple: - f = [os.path.normpath(x) for x in localRoot.tk.splitlist(ret_val)] - else: - try: - f = os.path.normpath(ret_val) - except AttributeError as e: - print("ret_val is {}".format(ret_val)) - raise e - localRoot.destroy() - - if not f: - return None - return f - - -if __name__ == '__main__': - print("Hello from file open box") - ret_val = fileopenbox("Please select a file", "My File Open dialog") - print("Return value is:{}".format(ret_val)) diff --git a/easygui/boxes/filesave_box.py b/easygui/boxes/filesave_box.py deleted file mode 100644 index 4d453b8..0000000 --- a/easygui/boxes/filesave_box.py +++ /dev/null @@ -1,89 +0,0 @@ -""" - -.. moduleauthor:: easygui developers and Stephen Raymond Ferg -.. default-domain:: py -.. highlight:: python - -Version |release| -""" - - -import os - -try: - from . import utils as ut - from . import fileboxsetup as fbs -except (SystemError, ValueError, ImportError): - import utils as ut - import fileboxsetup as fbs - -try: - import tkinter as tk # python 3 - import tkinter.font as tk_Font -except: - import Tkinter as tk # python 2 - import tkFont as tk_Font - - - -# ------------------------------------------------------------------- -# filesavebox -# ------------------------------------------------------------------- - - -def filesavebox(msg=None, title=None, default="", filetypes=None): - """ - A file to get the name of a file to save. - Returns the name of a file, or None if user chose to cancel. - - **About the "default" argument** - - The ``default`` argument specifies the path and "glob pattern" for file - names. The "\\*" value, for example, sets the open file dialog to the - current working directory and showing all files. - - For another example, setting the ``default`` argument to ``"C:/myjunk/*.py"`` - sets the open file dialog to the C:\\myjunk folder and showing only files - that have the .py file extension. This glob pattern at the end of the - ``default`` argument is required: passing ``"C:/myjunk"`` would not set the - open file dialog to the C:\\myjunk folder, but rather to the C:\\ folder - and "myjunk" as the initial filename. - - Note that on Windows, ``fileopenbox()`` automatically changes the path - separator to the Windows path separator (backslash). - - - The "filetypes" argument works like the "filetypes" argument to - fileopenbox. - - :param str msg: the msg to be displayed. - :param str title: the window title - :param str default: default filename to return - :param object filetypes: filemasks that a user can choose, e.g. " \\*.txt" - :return: the name of a file, or None if user chose to cancel - """ - - localRoot = tk.Tk() - localRoot.withdraw() - localRoot.attributes("-topmost", True) - - initialbase, initialfile, initialdir, filetypes = fbs.fileboxSetup( - default, filetypes) - - f = ut.tk_FileDialog.asksaveasfilename( - parent=localRoot, - title=ut.getFileDialogTitle( - msg, title), - initialfile=initialfile, initialdir=initialdir, - filetypes=filetypes - ) - localRoot.destroy() - if not f: - return None - return os.path.normpath(f) - - -if __name__ == '__main__': - print("Hello from file save box") - ret_val = filesavebox("Please select a file to save to", "My File Save dialog") - print("Return value is:{}".format(ret_val)) \ No newline at end of file diff --git a/easygui/boxes/fillable_box.py b/easygui/boxes/fillable_box.py deleted file mode 100644 index f5fbf3b..0000000 --- a/easygui/boxes/fillable_box.py +++ /dev/null @@ -1,177 +0,0 @@ -try: - from . import utils as ut - from . import global_state - from .base_boxes import bindArrows -except (SystemError, ValueError, ImportError): - import utils as ut - import global_state - from base_boxes import bindArrows - -try: - import tkinter as tk # python 3 - import tkinter.font as tk_Font -except: - import Tkinter as tk # python 2 - import tkFont as tk_Font - -# TODO: bindArrows seems to be in the wrong place. - - -boxRoot = None -entryWidget = None -__enterboxText = '' -__enterboxDefaultText = '' -cancelButton = None -okButton = None - - -def __fillablebox(msg, title="", default="", mask=None, image=None, root=None): - """ - Show a box in which a user can enter some text. - You may optionally specify some default text, which will appear in the - enterbox when it is displayed. - Returns the text that the user entered, or None if they cancel the operation. - """ - - global boxRoot, __enterboxText, __enterboxDefaultText - global cancelButton, entryWidget, okButton - - if title is None: - title = "" - if default is None: - default = "" - __enterboxDefaultText = default - __enterboxText = __enterboxDefaultText - - if root: - root.withdraw() - boxRoot = tk.Toplevel(master=root) - boxRoot.withdraw() - else: - boxRoot = tk.Tk() - boxRoot.withdraw() - - boxRoot.protocol('WM_DELETE_WINDOW', __enterboxQuit) - boxRoot.title(title) - boxRoot.iconname('Dialog') - boxRoot.geometry(global_state.window_position) - boxRoot.bind("", __enterboxCancel) - - # ------------- define the messageFrame --------------------------------- - messageFrame = tk.Frame(master=boxRoot) - messageFrame.pack(side=tk.TOP, fill=tk.BOTH) - - # ------------- define the imageFrame --------------------------------- - try: - tk_Image = ut.load_tk_image(image) - except Exception as inst: - print(inst) - tk_Image = None - if tk_Image: - imageFrame = tk.Frame(master=boxRoot) - imageFrame.pack(side=tk.TOP, fill=tk.BOTH) - label = tk.Label(imageFrame, image=tk_Image) - label.image = tk_Image # keep a reference! - label.pack(side=tk.TOP, expand=tk.YES, fill=tk.X, padx='1m', pady='1m') - - # ------------- define the buttonsFrame --------------------------------- - buttonsFrame = tk.Frame(master=boxRoot) - buttonsFrame.pack(side=tk.TOP, fill=tk.BOTH) - - # ------------- define the entryFrame --------------------------------- - entryFrame = tk.Frame(master=boxRoot) - entryFrame.pack(side=tk.TOP, fill=tk.BOTH) - - # ------------- define the buttonsFrame --------------------------------- - buttonsFrame = tk.Frame(master=boxRoot) - buttonsFrame.pack(side=tk.TOP, fill=tk.BOTH) - - # -------------------- the msg widget ---------------------------- - messageWidget = tk.Message(messageFrame, width="4.5i", text=msg) - messageWidget.configure( - font=(global_state.PROPORTIONAL_FONT_FAMILY, global_state.PROPORTIONAL_FONT_SIZE)) - messageWidget.pack( - side=tk.RIGHT, expand=1, fill=tk.BOTH, padx='3m', pady='3m') - - # --------- entryWidget ---------------------------------------------- - entryWidget = tk.Entry(entryFrame, width=40) - bindArrows(entryWidget) - entryWidget.configure( - font=(global_state.PROPORTIONAL_FONT_FAMILY, global_state.TEXT_ENTRY_FONT_SIZE)) - if mask: - entryWidget.configure(show=mask) - entryWidget.pack(side=tk.LEFT, padx="3m") - entryWidget.bind("", __enterboxGetText) - entryWidget.bind("", __enterboxCancel) - # put text into the entryWidget - entryWidget.insert(0, __enterboxDefaultText) - - # ------------------ ok button ------------------------------- - okButton = tk.Button(buttonsFrame, takefocus=1, text="OK") - bindArrows(okButton) - okButton.pack( - expand=1, side=tk.LEFT, padx='3m', pady='3m', ipadx='2m', ipady='1m') - - # for the commandButton, bind activation events to the activation event - # handler - commandButton = okButton - handler = __enterboxGetText - for selectionEvent in global_state.STANDARD_SELECTION_EVENTS: - commandButton.bind("<{}>".format(selectionEvent), handler) - - mouse_handlers = ut.mouse_click_handlers(__enterboxGetText) - for selectionEvent in global_state.STANDARD_SELECTION_EVENTS_MOUSE: - okButton.bind("<%s>" % selectionEvent, mouse_handlers[selectionEvent]) - - # ------------------ cancel button ------------------------------- - cancelButton = tk.Button(buttonsFrame, takefocus=1, text="Cancel") - bindArrows(cancelButton) - cancelButton.pack( - expand=1, side=tk.RIGHT, padx='3m', pady='3m', ipadx='2m', ipady='1m') - - # for the commandButton, bind activation events to the activation event - # handler - commandButton = cancelButton - handler = __enterboxCancel - for selectionEvent in global_state.STANDARD_SELECTION_EVENTS: - commandButton.bind("<{}>".format(selectionEvent), handler) - mouse_handlers = ut.mouse_click_handlers(__enterboxCancel) - for selectionEvent in global_state.STANDARD_SELECTION_EVENTS_MOUSE: - cancelButton.bind("<%s>" % selectionEvent, mouse_handlers[selectionEvent]) - - - # ------------------- time for action! ----------------- - entryWidget.focus_force() # put the focus on the entryWidget - boxRoot.deiconify() - boxRoot.mainloop() # run it! - - # -------- after the run has completed ---------------------------------- - if root: - root.deiconify() - boxRoot.destroy() # button_click didn't destroy boxRoot, so we do it now - return __enterboxText - - -def __enterboxQuit(): - return __enterboxCancel(None) - - -def __enterboxCancel(event): - global __enterboxText - - __enterboxText = None - boxRoot.quit() - - -def __enterboxGetText(event): - global __enterboxText - - __enterboxText = entryWidget.get() - boxRoot.quit() - - -def __enterboxRestore(event): - global entryWidget - - entryWidget.delete(0, len(entryWidget.get())) - entryWidget.insert(0, __enterboxDefaultText) diff --git a/easygui/boxes/global_state.py b/easygui/boxes/global_state.py deleted file mode 100644 index 86d74ac..0000000 --- a/easygui/boxes/global_state.py +++ /dev/null @@ -1,29 +0,0 @@ -""" - -.. moduleauthor:: easygui developers and Stephen Raymond Ferg -.. default-domain:: py -.. highlight:: python - -Version |release| -""" - -# Starting and global variables - -window_position = "+300+200" - -PROPORTIONAL_FONT_FAMILY = ("MS", "Sans", "Serif") -MONOSPACE_FONT_FAMILY = "Courier" - -PROPORTIONAL_FONT_SIZE = 10 -# a little smaller, because it is more legible at a smaller size -MONOSPACE_FONT_SIZE = 9 -TEXT_ENTRY_FONT_SIZE = 12 # a little larger makes it easier to see - - -STANDARD_SELECTION_EVENTS = ["Return", "space"] -STANDARD_SELECTION_EVENTS_MOUSE = ["Enter", "Leave", "ButtonRelease-1"] - -prop_font_line_length = 62 -fixw_font_line_length = 80 -num_lines_displayed = 50 -default_hpad_in_chars = 2 diff --git a/easygui/boxes/multi_fillable_box.py b/easygui/boxes/multi_fillable_box.py deleted file mode 100644 index f512add..0000000 --- a/easygui/boxes/multi_fillable_box.py +++ /dev/null @@ -1,507 +0,0 @@ -""" - -.. moduleauthor:: easygui developers and Stephen Raymond Ferg -.. default-domain:: py -.. highlight:: python - -Version |release| -""" -from easygui.boxes.utils import mouse_click_handlers - -try: - from . import global_state -except: - import global_state - -try: - import tkinter as tk # python 3 -except: - import Tkinter as tk # python 2 - -# ----------------------------------------------------------------------- -# multpasswordbox -# ----------------------------------------------------------------------- - - -def multpasswordbox(msg="Fill in values for the fields.", - title=" ", fields=tuple(), values=tuple(), - callback=None, run=True): - r""" - Same interface as multenterbox. But in multpassword box, - the last of the fields is assumed to be a password, and - is masked with asterisks. - - :param str msg: the msg to be displayed. - :param str title: the window title - :param list fields: a list of fieldnames. - :param list values: a list of field values - :return: String - - **Example** - - Here is some example code, that shows how values returned from - multpasswordbox can be checked for validity before they are accepted:: - - msg = "Enter logon information" - title = "Demo of multpasswordbox" - fieldNames = ["Server ID", "User ID", "Password"] - fieldValues = [] # we start with blanks for the values - fieldValues = multpasswordbox(msg,title, fieldNames) - - # make sure that none of the fields was left blank - while 1: - if fieldValues is None: break - errmsg = "" - for i in range(len(fieldNames)): - if fieldValues[i].strip() == "": - errmsg = errmsg + ('"%s" is a required field.\n\n' % - fieldNames[i]) - if errmsg == "": break # no problems found - fieldValues = multpasswordbox(errmsg, title, - fieldNames, fieldValues) - - print("Reply was: %s" % str(fieldValues)) - - """ - if run: - mb = MultiBox(msg, title, fields, values, mask_last=True, - callback=callback) - - reply = mb.run() - - return reply - - else: - - mb = MultiBox(msg, title, fields, values, mask_last=True, - callback=callback) - - return mb - - -# ------------------------------------------------------------------- -# multenterbox -# ------------------------------------------------------------------- -# TODO RL: Should defaults be list constructors. -# i think after multiple calls, the value is retained. -# TODO RL: Rename/alias to multienterbox? -# default should be None and then in the logic create an empty liglobal_state. -def multenterbox(msg="Fill in values for the fields.", title=" ", - fields=[], values=[], callback=None, run=True): - r""" - Show screen with multiple data entry fields. - - If there are fewer values than names, the list of values is padded with - empty strings until the number of values is the same as the number - of names. - - If there are more values than names, the list of values - is truncated so that there are as many values as names. - - Returns a list of the values of the fields, - or None if the user cancels the operation. - - Here is some example code, that shows how values returned from - multenterbox can be checked for validity before they are accepted:: - - msg = "Enter your personal information" - title = "Credit Card Application" - fieldNames = ["Name","Street Address","City","State","ZipCode"] - fieldValues = [] # we start with blanks for the values - fieldValues = multenterbox(msg,title, fieldNames) - - # make sure that none of the fields was left blank - while 1: - if fieldValues is None: break - errmsg = "" - for i in range(len(fieldNames)): - if fieldValues[i].strip() == "": - errmsg += ('"%s" is a required field.\n\n' % fieldNames[i]) - if errmsg == "": - break # no problems found - fieldValues = multenterbox(errmsg, title, fieldNames, fieldValues) - - print("Reply was: %s" % str(fieldValues)) - - :param str msg: the msg to be displayed. - :param str title: the window title - :param list fields: a list of fieldnames. - :param list values: a list of field values - :return: String - """ - if run: - mb = MultiBox(msg, title, fields, values, mask_last=False, - callback=callback) - reply = mb.run() - return reply - else: - mb = MultiBox(msg, title, fields, values, mask_last=False, - callback=callback) - return mb - - -class MultiBox(object): - - """ Show multiple data entry fields - - This object does a number of things: - - - chooses a GUI framework (wx, qt) - - checks the data sent to the GUI - - performs the logic (button ok should close the window?) - - defines what methods the user can invoke and - what properties he can change. - - calls the ui in defined ways, so other gui - frameworks can be used without breaking anything to the user - """ - - def __init__(self, msg, title, fields, values, mask_last, callback): - """ Create box object - - Parameters - ---------- - msg : string - text displayed in the message area (instructions...) - title : str - the window title - fields: list - names of fields - values: list - initial values - callback: function - if set, this function will be called when OK is pressed - run: bool - if True, a box object will be created and returned, but not run - - Returns - ------- - self - The MultiBox object - """ - - self.callback = callback - - self.fields, self.values = self.check_fields(fields, values) - - self.ui = GUItk(msg, title, self.fields, self.values, - mask_last, self.callback_ui) - - def run(self): - """ Start the ui """ - self.ui.run() - self.ui = None - return self.values - - def stop(self): - """ Stop the ui """ - self.ui.stop() - - def callback_ui(self, ui, command, values): - """ This method is executed when ok, cancel, or x is pressed in the ui. - """ - if command == 'update': # OK was pressed - self.values = values - if self.callback: - # If a callback was set, call main process - self.callback(self) - else: - self.stop() - elif command == 'x': - self.stop() - self.values = None - elif command == 'cancel': - self.stop() - self.values = None - - # methods to change properties -------------- - - @property - def msg(self): - """Text in msg Area""" - return self._msg - - @msg.setter - def msg(self, msg): - self.ui.set_msg(msg) - - @msg.deleter - def msg(self): - self._msg = "" - self.ui.set_msg(self._msg) - - # Methods to validate what will be sent to ui --------- - - def check_fields(self, fields, values): - if len(fields) == 0: - return None - - fields = list(fields[:]) # convert possible tuples to a list - values = list(values[:]) # convert possible tuples to a list - - # TODO RL: The following seems incorrect when values>fields. Replace - # below with zip? - if len(values) == len(fields): - pass - elif len(values) > len(fields): - fields = fields[0:len(values)] - else: - while len(values) < len(fields): - values.append("") - - return fields, values - - -class GUItk(object): - - """ This object contains the tk root object. - It draws the window, waits for events and communicates them - to MultiBox, together with the entered values. - - The position in wich it is drawn comes from a global variable. - - It also accepts commands from Multibox to change its message. - """ - - def __init__(self, msg, title, fields, values, mask_last, callback): - - self.callback = callback - - self.boxRoot = tk.Tk() - - self.create_root(title) - - self.set_pos(global_state.window_position) # GLOBAL POSITION - - self.create_msg_widget(msg) - - self.create_entryWidgets(fields, values, mask_last) - - self.create_buttons() - - self.entryWidgets[0].focus_force() # put the focus on the entryWidget - - # Run and stop methods --------------------------------------- - - def run(self): - self.boxRoot.mainloop() # run it! - self.boxRoot.destroy() # Close the window - - def stop(self): - # Get the current position before quitting - self.get_pos() - - self.boxRoot.quit() - - def x_pressed(self): - self.callback(self, command='x', values=self.get_values()) - - def cancel_pressed(self, event): - self.callback(self, command='cancel', values=self.get_values()) - - def ok_pressed(self, event): - self.callback(self, command='update', values=self.get_values()) - - # Methods to change content --------------------------------------- - - def set_msg(self, msg): - self.messageWidget.configure(text=msg) - self.entryWidgets[0].focus_force() # put the focus on the entryWidget - - def set_pos(self, pos): - self.boxRoot.geometry(pos) - - def get_pos(self): - # The geometry() method sets a size for the window and positions it on - # the screen. The first two parameters are width and height of - # the window. The last two parameters are x and y screen coordinates. - # geometry("250x150+300+300") - geom = self.boxRoot.geometry() # "628x672+300+200" - global_state.window_position = '+' + geom.split('+', 1)[1] - - def get_values(self): - values = [] - for entryWidget in self.entryWidgets: - values.append(entryWidget.get()) - return values - - # Initial configuration methods --------------------------------------- - # These ones are just called once, at setting. - - def create_root(self, title): - - self.boxRoot.protocol('WM_DELETE_WINDOW', self.x_pressed) - self.boxRoot.title(title) - self.boxRoot.iconname('Dialog') - self.boxRoot.bind("", self.cancel_pressed) - self.boxRoot.attributes("-topmost", True) # Put the dialog box in focus. - - def create_msg_widget(self, msg): - # -------------------- the msg widget ---------------------------- - self.messageWidget = tk.Message(self.boxRoot, width="4.5i", text=msg) - self.messageWidget.configure( - font=(global_state.PROPORTIONAL_FONT_FAMILY, global_state.PROPORTIONAL_FONT_SIZE)) - self.messageWidget.pack( - side=tk.TOP, expand=1, fill=tk.BOTH, padx='3m', pady='3m') - - def create_entryWidgets(self, fields, values, mask_last): - - self.entryWidgets = [] - - lastWidgetIndex = len(fields) - 1 - - for widgetIndex in range(len(fields)): - name = fields[widgetIndex] - value = values[widgetIndex] - entryFrame = tk.Frame(master=self.boxRoot) - entryFrame.pack(side=tk.TOP, fill=tk.BOTH) - - # --------- entryWidget ------------------------------------------- - labelWidget = tk.Label(entryFrame, text=name) - labelWidget.pack(side=tk.LEFT) - - entryWidget = tk.Entry(entryFrame, width=40, highlightthickness=2) - self.entryWidgets.append(entryWidget) - entryWidget.configure( - font=(global_state.PROPORTIONAL_FONT_FAMILY, global_state.TEXT_ENTRY_FONT_SIZE)) - entryWidget.pack(side=tk.RIGHT, padx="3m") - - self.bindArrows(entryWidget) - - entryWidget.bind("", self.ok_pressed) - entryWidget.bind("", self.cancel_pressed) - - # for the last entryWidget, if this is a multpasswordbox, - # show the contents as just asterisks - if widgetIndex == lastWidgetIndex: - if mask_last: - self.entryWidgets[widgetIndex].configure(show="*") - - # put text into the entryWidget - if value is None: - value = '' - self.entryWidgets[widgetIndex].insert( - 0, '{}'.format(value)) - - def create_buttons(self): - self.buttonsFrame = tk.Frame(master=self.boxRoot) - self.buttonsFrame.pack(side=tk.BOTTOM) - - self.create_cancel_button() - self.create_ok_button() - - def create_ok_button(self): - - okButton = tk.Button(self.buttonsFrame, takefocus=1, text="OK") - self.bindArrows(okButton) - okButton.pack(expand=1, side=tk.LEFT, padx='3m', pady='3m', - ipadx='2m', ipady='1m') - - # for the commandButton, bind activation events to the activation event - # handler - commandButton = okButton - handler = self.ok_pressed - for selectionEvent in global_state.STANDARD_SELECTION_EVENTS: - commandButton.bind("<%s>" % selectionEvent, handler) - - mouse_handlers = mouse_click_handlers(self.ok_pressed) - for selectionEvent in global_state.STANDARD_SELECTION_EVENTS_MOUSE: - commandButton.bind("<%s>" % selectionEvent, mouse_handlers[selectionEvent]) - - - def create_cancel_button(self): - - cancelButton = tk.Button(self.buttonsFrame, takefocus=1, text="Cancel") - self.bindArrows(cancelButton) - cancelButton.pack(expand=1, side=tk.LEFT, padx='3m', pady='3m', - ipadx='2m', ipady='1m') - - # for the commandButton, bind activation events to the activation event - # handler - commandButton = cancelButton - handler = self.cancel_pressed - for selectionEvent in global_state.STANDARD_SELECTION_EVENTS: - commandButton.bind("<%s>" % selectionEvent, handler) - - mouse_handlers = mouse_click_handlers(self.cancel_pressed) - for selectionEvent in global_state.STANDARD_SELECTION_EVENTS_MOUSE: - commandButton.bind("<%s>" % selectionEvent, mouse_handlers[selectionEvent]) - - - def bindArrows(self, widget): - - widget.bind("", self.tabRight) - widget.bind("", self.tabLeft) - - widget.bind("", self.tabRight) - widget.bind("", self.tabLeft) - - def tabRight(self, event): - self.boxRoot.event_generate("") - - def tabLeft(self, event): - self.boxRoot.event_generate("") - - -def demo1(): - msg = "Enter your personal information" - title = "Credit Card Application" - fieldNames = ["Name", "Street Address", "City", "State", "ZipCode"] - fieldValues = [] # we start with blanks for the values - - # make sure that none of the fields was left blank - while True: - - fieldValues = multenterbox(msg, title, fieldNames, fieldValues) - cancelled = fieldValues is None - errors = [] - if cancelled: - pass - else: # check for errors - for name, value in zip(fieldNames, fieldValues): - if value.strip() == "": - errors.append('"{}" is a required field.'.format(name)) - - all_ok = not errors - - if cancelled or all_ok: - break # no problems found - - msg = "\n".join(errors) - - print("Reply was: {}".format(fieldValues)) - - -class Demo2(): - - def __init__(self): - msg = "Without flicker. Enter your personal information" - title = "Credit Card Application" - fieldNames = ["Name", "Street Address", "City", "State", "ZipCode"] - fieldValues = [] # we start with blanks for the values - - fieldValues = multenterbox(msg, title, fieldNames, fieldValues, - callback=self.check_for_blank_fields) - print("Reply was: {}".format(fieldValues)) - - def check_for_blank_fields(self, box): - # make sure that none of the fields was left blank - cancelled = box.values is None - errors = [] - if cancelled: - pass - else: # check for errors - for name, value in zip(box.fields, box.values): - if value.strip() == "": - errors.append('"{}" is a required field.'.format(name)) - - all_ok = not errors - - if cancelled or all_ok: - box.stop() # no problems found - - box.msg = "\n".join(errors) - - -if __name__ == '__main__': - demo1() - Demo2() diff --git a/easygui/boxes/text_box.py b/easygui/boxes/text_box.py deleted file mode 100644 index 86216c5..0000000 --- a/easygui/boxes/text_box.py +++ /dev/null @@ -1,584 +0,0 @@ -""" - -.. moduleauthor:: easygui developers and Stephen Raymond Ferg -.. default-domain:: py -.. highlight:: python - -Version |release| -""" - - -import sys - -from easygui.boxes.utils import mouse_click_handlers - -try: - from . import global_state -except (SystemError, ValueError, ImportError): - import global_state - -try: - import tkinter as tk # python 3 - import tkinter.font as tk_Font -except: - import Tkinter as tk # python 2 - import tkFont as tk_Font - - -def demo_textbox(): - demo_1() - Demo2() - Demo3() - - -def demo_1(): - - title = "Demo of textbox: Classic box" - - gnexp = ("This is a demo of the classic textbox call, " - "you can see it closes when ok is pressed.\n\n") - - challenge = "INSERT A TEXT WITH MORE THAN TWO PARAGRAPHS" - - text = "Insert your text here\n" - - msg = gnexp + challenge - - finished = False - while True: - - text = textbox(msg, title, text) - escaped = not text - if escaped or finished: - break - - if text.count("\n") >= 2: - msg = (u"You did it right! Press OK") - finished = True - else: - msg = u"You did it wrong! Try again!\n" + challenge - - -class Demo2(object): - - """ Program that challenges the user to write 5 a's """ - - def __init__(self): - """ Set and run the program """ - - title = "Demo of textbox: Classic box with callback" - - gnexp = ("This is a demo of the textbox with a callback, " - "it doesn't flicker!.\n\n") - - msg = "INSERT A TEXT WITH FIVE OR MORE A\'s" - - text_snippet = "Insert your text here" - - self.finished = False - - textbox(gnexp + msg, title, text_snippet, False, - callback=self.check_answer, run=True) - - def check_answer(self, box): - """ Callback from TextBox - - Parameters - ----------- - box: object - object containing parameters and methods to communicate with the ui - - Returns - ------- - nothing: - its return is through the box object - """ - - if self.finished: - box.stop() - - if box.text.lower().count("a") >= 5: - box.msg = u"\n\nYou did it right! Press OK button to continue." - box.stop() - self.finished - else: - box.msg = u"\n\nMore a's are needed!" - - -class Demo3(object): - - """ Program that challenges the user to find a typo """ - - def __init__(self): - """ Set and run the program """ - - self.finished = False - - title = "Demo of textbox: Object with callback" - - msg = ("This is a demo of the textbox set as " - "an object with a callback, " - "you can configure it and when you are finished, " - "you run it.\n\nThere is a typo in it. Find and correct it.") - - text_snippet = "Hello" # This text wont show - - box = textbox( - msg, title, text_snippet, False, callback=self.check_answer, run=False) - - box.text = ( - "It was the west of times, and it was the worst of times. " - "The rich ate cake, and the poor had cake recommended to them, " - "but wished only for enough cash to buy bread." - "The time was ripe for revolution! ") - - box.run() - - def check_answer(self, box): - """ Callback from TextBox - - Parameters - ---------- - box: object - object containing parameters and methods to communicate with the ui - - Returns - ------- - nothing: - its return is through the box object - """ - if self.finished: - box.stop() - - if "best" in box.text: - box.msg = u"\n\nYou did right! Press OK button to continue." - self.finished = True - else: - box.msg = u"\n\nLook to the west!" - - -def textbox(msg="", title=" ", text="", - codebox=False, callback=None, run=True): - """Displays a dialog box with a large, multi-line text box, and returns - the entered text as a string. The message text is displayed in a - proportional font and wraps. - - Parameters - ---------- - msg : string - text displayed in the message area (instructions...) - title : str - the window title - text: str, list or tuple - text displayed in textAreas (editable) - codebox: bool - if True, don't wrap and width is set to 80 chars - callback: function - if set, this function will be called when OK is pressed - run: bool - if True, a box object will be created and returned, but not run - - Returns - ------- - None - If cancel is pressed - str - If OK is pressed returns the contents of textArea - - """ - - tb = TextBox(msg=msg, title=title, text=text, - codebox=codebox, callback=callback) - if not run: - return tb - else: - reply = tb.run() - return reply - - -class TextBox(object): - - """ Display a message and a text to edit - - This object separates user from ui, defines which methods can - the user invoke and which properties can he change. - - It also calls the ui in defined ways, so if other gui - library can be used (wx, qt) without breaking anything for the user. - """ - - def __init__(self, msg, title, text, codebox, callback=lambda *args, **kwargs: True): - """ Create box object - - Parameters - ---------- - msg : string - text displayed in the message area (instructions...) - title : str - the window title - text: str, list or tuple - text displayed in textAres (editable) - codebox: bool - if True, don't wrap and width is set to 80 chars - callback: function - if set, this function will be called when OK is pressed - - Returns - ------- - object - The box object - """ - - self.callback = callback - self.ui = GUItk(msg, title, text, codebox, self.callback_ui) - self.text = text - - def run(self): - """ Start the ui """ - self.ui.run() - self.ui = None - return self._text - - def stop(self): - """ Stop the ui """ - self.ui.stop() - - def callback_ui(self, ui, command, text): - """ This method is executed when ok, cancel, or x is pressed in the ui. - """ - if command == 'update': # OK was pressed - self._text = text - if self.callback: - # If a callback was set, call main process - self.callback(self) - else: - self.stop() - elif command == 'x': - self.stop() - self._text = None - elif command == 'cancel': - self.stop() - self._text = None - - # methods to change properties -------------- - @property - def text(self): - """Text in text Area""" - return self._text - - @text.setter - def text(self, text): - self._text = self.to_string(text) - self.ui.set_text(self._text) - - @text.deleter - def text(self): - self._text = "" - self.ui.set_text(self._text) - - @property - def msg(self): - """Text in msg Area""" - return self._msg - - @msg.setter - def msg(self, msg): - self._msg = self.to_string(msg) - self.ui.set_msg(self._msg) - - @msg.deleter - def msg(self): - self._msg = "" - self.ui.set_msg(self._msg) - - # Methods to validate what will be sent to ui --------- - - def to_string(self, something): - try: - basestring # python 2 - except NameError: - basestring = str # Python 3 - - if isinstance(something, basestring): - return something - try: - text = "".join(something) # convert a list or a tuple to a string - except: - textbox( - "Exception when trying to convert {} to text in self.textArea" - .format(type(something))) - sys.exit(16) - return text - - -class GUItk(object): - - """ This is the object that contains the tk root object""" - - def __init__(self, msg, title, text, codebox, callback): - """ Create ui object - - Parameters - ---------- - msg : string - text displayed in the message area (instructions...) - title : str - the window title - text: str, list or tuple - text displayed in textAres (editable) - codebox: bool - if True, don't wrap, and width is set to 80 chars - callback: function - if set, this function will be called when OK is pressed - - Returns - ------- - object - The ui object - """ - - self.callback = callback - - self.boxRoot = tk.Tk() - # self.boxFont = tk_Font.Font( - # family=global_state.PROPORTIONAL_FONT_FAMILY, - # size=global_state.PROPORTIONAL_FONT_SIZE) - - wrap_text = not codebox - if wrap_text: - self.boxFont = tk_Font.nametofont("TkTextFont") - self.width_in_chars = global_state.prop_font_line_length - else: - self.boxFont = tk_Font.nametofont("TkFixedFont") - self.width_in_chars = global_state.fixw_font_line_length - - # default_font.configure(size=global_state.PROPORTIONAL_FONT_SIZE) - - self.configure_root(title) - - self.create_msg_widget(msg) - - self.create_text_area(wrap_text) - - self.create_buttons_frame() - - self.create_cancel_button() - - self.create_ok_button() - - # Run and stop methods --------------------------------------- - - def run(self): - self.boxRoot.mainloop() - self.boxRoot.destroy() - - def stop(self): - # Get the current position before quitting - self.get_pos() - self.boxRoot.quit() - - # Methods to change content --------------------------------------- - - def set_msg(self, msg): - self.messageArea.config(state=tk.NORMAL) - self.messageArea.delete(1.0, tk.END) - self.messageArea.insert(tk.END, msg) - self.messageArea.config(state=tk.DISABLED) - # Adjust msg height - self.messageArea.update() - numlines = self.get_num_lines(self.messageArea) - self.set_msg_height(numlines) - self.messageArea.update() - - def set_msg_height(self, numlines): - self.messageArea.configure(height=numlines) - - def get_num_lines(self, widget): - end_position = widget.index(tk.END) # '4.0' - end_line = end_position.split('.')[0] # 4 - return int(end_line) # 5 - - def set_text(self, text): - self.textArea.delete(1.0, tk.END) - self.textArea.insert(tk.END, text, "normal") - self.textArea.focus() - - def set_pos(self, pos): - self.boxRoot.geometry(pos) - - def get_pos(self): - # The geometry() method sets a size for the window and positions it on - # the screen. The first two parameters are width and height of - # the window. The last two parameters are x and y screen coordinates. - # geometry("250x150+300+300") - geom = self.boxRoot.geometry() # "628x672+300+200" - global_state.window_position = '+' + geom.split('+', 1)[1] - - def get_text(self): - return self.textArea.get(0.0, 'end-1c') - - # Methods executing when a key is pressed ------------------------------- - def x_pressed(self): - self.callback(self, command='x', text=self.get_text()) - - def cancel_pressed(self, event): - self.callback(self, command='cancel', text=self.get_text()) - - def ok_button_pressed(self, event): - self.callback(self, command='update', text=self.get_text()) - - # Auxiliary methods ----------------------------------------------- - def calc_character_width(self): - char_width = self.boxFont.measure('W') - return char_width - - # Initial configuration methods --------------------------------------- - # These ones are just called once, at setting. - - def configure_root(self, title): - - self.boxRoot.title(title) - - self.set_pos(global_state.window_position) - - # Quit when x button pressed - self.boxRoot.protocol('WM_DELETE_WINDOW', self.x_pressed) - self.boxRoot.bind("", self.cancel_pressed) - - self.boxRoot.iconname('Dialog') - - self.boxRoot.attributes("-topmost", True) # Put the dialog box in focus. - - def create_msg_widget(self, msg): - - if msg is None: - msg = "" - - self.msgFrame = tk.Frame( - self.boxRoot, - padx=1.25 * self.calc_character_width(), - - ) - self.messageArea = tk.Text( - self.msgFrame, - width=self.width_in_chars, - state=tk.DISABLED, - padx=(global_state.default_hpad_in_chars) * - self.calc_character_width(), - pady=self.calc_character_width(), - wrap=tk.WORD, - - ) - self.set_msg(msg) - - self.msgFrame.pack(fill='x') - - self.messageArea.pack(fill='x') - - def create_text_area(self, wrap_text): - """ - Put a textArea in the top frame - Put and configure scrollbars - """ - - self.textFrame = tk.Frame( - self.boxRoot, - padx=1.25 * self.calc_character_width(), - ) - - self.textFrame.pack(side=tk.TOP) - # self.textFrame.grid(row=1, column=0, sticky=tk.EW) - - self.textArea = tk.Text( - self.textFrame, - padx=global_state.default_hpad_in_chars * - self.calc_character_width(), - pady=global_state.default_hpad_in_chars * - self.calc_character_width(), - height=25, # lines. Note: a user-set arg would be preferable to hardcoded value - width=self.width_in_chars, # chars of the current font - ) - - if wrap_text: - self.textArea.configure(wrap=tk.WORD) - else: - self.textArea.configure(wrap=tk.NONE) - - # some simple keybindings for scrolling - self.boxRoot.bind("", self.textArea.yview_scroll(1, tk.PAGES)) - self.boxRoot.bind( - "", self.textArea.yview_scroll(-1, tk.PAGES)) - - self.boxRoot.bind("", self.textArea.xview_scroll(1, tk.PAGES)) - self.boxRoot.bind("", self.textArea.xview_scroll(-1, tk.PAGES)) - - self.boxRoot.bind("", self.textArea.yview_scroll(1, tk.UNITS)) - self.boxRoot.bind("", self.textArea.yview_scroll(-1, tk.UNITS)) - - # add a vertical scrollbar to the frame - rightScrollbar = tk.Scrollbar( - self.textFrame, orient=tk.VERTICAL, command=self.textArea.yview) - self.textArea.configure(yscrollcommand=rightScrollbar.set) - - # add a horizontal scrollbar to the frame - bottomScrollbar = tk.Scrollbar( - self.textFrame, orient=tk.HORIZONTAL, command=self.textArea.xview) - self.textArea.configure(xscrollcommand=bottomScrollbar.set) - - # pack the textArea and the scrollbars. Note that although - # we must define the textArea first, we must pack it last, - # so that the bottomScrollbar will be located properly. - - # Note that we need a bottom scrollbar only for code. - # Text will be displayed with wordwrap, so we don't need to have - # a horizontal scroll for it. - - if not wrap_text: - bottomScrollbar.pack(side=tk.BOTTOM, fill=tk.X) - rightScrollbar.pack(side=tk.RIGHT, fill=tk.Y) - - self.textArea.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.YES) - - def create_buttons_frame(self): - - self.buttonsFrame = tk.Frame(self.boxRoot, - # background="green", - - ) - self.buttonsFrame.pack(side=tk.TOP) - - def create_cancel_button(self): - # put the buttons in the buttonsFrame - self.cancelButton = tk.Button( - self.buttonsFrame, takefocus=tk.YES, text="Cancel", - height=1, width=6) - self.cancelButton.pack( - expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", - ipadx="2m") - - # for the commandButton, bind activation events to the activation event - # handler - self.cancelButton.bind("", self.cancel_pressed) - mouse_handlers = mouse_click_handlers(self.cancel_pressed) - for selectionEvent in global_state.STANDARD_SELECTION_EVENTS_MOUSE: - self.cancelButton.bind("<%s>" % selectionEvent, mouse_handlers[selectionEvent]) - - def create_ok_button(self): - # put the buttons in the buttonsFrame - self.okButton = tk.Button( - self.buttonsFrame, takefocus=tk.YES, text="OK", height=1, width=6) - self.okButton.pack( - expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", - ipadx="2m") - - # for the commandButton, bind activation events to the activation event - # handler - self.okButton.bind("", self.ok_button_pressed) - mouse_handlers = mouse_click_handlers(self.ok_button_pressed) - for selectionEvent in global_state.STANDARD_SELECTION_EVENTS_MOUSE: - self.okButton.bind("<%s>" % selectionEvent, mouse_handlers[selectionEvent]) - - - -if __name__ == '__main__': - demo_textbox() diff --git a/easygui/boxes/utils.py b/easygui/boxes/utils.py deleted file mode 100644 index 93d358d..0000000 --- a/easygui/boxes/utils.py +++ /dev/null @@ -1,233 +0,0 @@ -""" - -.. moduleauthor:: easygui developers and Stephen Raymond Ferg -.. default-domain:: py -.. highlight:: python - -Version |release| - -""" - -import os -import sys -import traceback - -# A set of variables and functions to centralize differences between -# python 2 and 3 -runningPython27 = False -runningPython34 = False -if 0x020700F0 <= sys.hexversion <= 0x030000F0: - runningPython27 = True -if 0x030400F0 <= sys.hexversion <= 0x040000F0: - runningPython34 = True -if not runningPython27 and not runningPython34: - raise Exception("You must run on Python 2.7+ or Python 3.4+") - -# Import Tkinter, the tk filedialog, and put everything in tkinter into -# the current namespace -try: - import tkinter as tk # python3 - # TODO: Ultimately this should go away once everything stops using it. - from tkinter import * - import tkinter.filedialog as tk_FileDialog - import tkinter.font as tk_Font -except ImportError: - try: - import Tkinter as tk # python2 - # TODO: Ultimately this should go away once everything stops using it. - from Tkinter import * - import tkFileDialog as tk_FileDialog - import tkFont as tk_Font - - except ImportError: - raise ImportError("Unable to find tkinter package.") - -if tk.TkVersion < 8.0: - raise ImportError("You must use python-tk (tkinter) version 8.0 or higher") - - -# Try to import the Python Image Library. If it doesn't exist, only .gif -# images are supported. -try: - from PIL import Image as PILImage - from PIL import ImageTk as PILImageTk -except: - pass - -# Code should use 'basestring' anywhere you might think to use the system 'str'. This is all to support -# Python 2. If 2 ever goes away, this logic can go away and uses of -# utils.basestring should be changed to just str -if runningPython27: - basestring = basestring -if runningPython34: - basestring = str - - -# ----------------------------------------------------------------------- -# exception_format -# ----------------------------------------------------------------------- -def exception_format(): - """ - Convert exception info into a string suitable for display. - """ - return "".join(traceback.format_exception( - sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2] - )) - - -# ------------------------------------------------------------------- -# utility routines -# ------------------------------------------------------------------- -# These routines are used by several other functions in the EasyGui module. - -def uniquify_list_of_strings(input_list): - """ - Ensure that every string within input_list is unique. - :param list input_list: List of strings - :return: New list with unique names as needed. - """ - output_list = list() - for i, item in enumerate(input_list): - tempList = input_list[:i] + input_list[i + 1:] - if item not in tempList: - output_list.append(item) - else: - output_list.append('{0}_{1}'.format(item, i)) - return output_list - -import re - - -def parse_hotkey(text): - """ - Extract a desired hotkey from the text. The format to enclose - the hotkey in square braces - as in Button_[1] which would assign the keyboard key 1 to that button. - The one will be included in the - button text. To hide they key, use double square braces as in: Ex[[qq]] - it , which would assign - the q key to the Exit button. Special keys such as may also be - used: Move [] for a full - list of special keys, see this reference: http://infohoglobal_state.nmt.edu/tcc/help/ - pubs/tkinter/web/key-names.html - :param text: - :return: list containing cleaned text, hotkey, and hotkey position within - cleaned text. - """ - - ret_val = [text, None, None] # Default return values - if text is None: - return ret_val - - # Single character, remain visible - res = re.search(r'(?<=\[).(?=\])', text) - if res: - start = res.start(0) - end = res.end(0) - caption = text[:start - 1] + text[start:end] + text[end + 1:] - ret_val = [caption, text[start:end], start - 1] - - # Single character, hide it - res = re.search(r'(?<=\[\[).(?=\]\])', text) - if res: - start = res.start(0) - end = res.end(0) - caption = text[:start - 2] + text[end + 2:] - ret_val = [caption, text[start:end], None] - - # a Keysym. Always hide it - res = re.search(r'(?<=\[\<).+(?=\>\])', text) - if res: - start = res.start(0) - end = res.end(0) - caption = text[:start - 2] + text[end + 2:] - ret_val = [caption, '<{}>'.format(text[start:end]), None] - - return ret_val - - -def load_tk_image(filename, tk_master=None): - """ - Load in an image file and return as a tk Image. - - Loads an image. If the PIL library is available use it. otherwise use the tk method. - - NOTE: tk_master is required if there are more than one Tk() instances, which there are very often. - REF: http://stackoverflow.com/a/23229091/2184122 - - :param filename: image filename to load - :param tk_master: root object (Tk()) - :return: tk Image object - """ - - if filename is None: - return None - - if not os.path.isfile(filename): - raise ValueError( - 'Image file {} does not exist.'.format(filename)) - - tk_image = None - - filename = os.path.normpath(filename) - _, ext = os.path.splitext(filename) - - try: - pil_image = PILImage.open(filename) - tk_image = PILImageTk.PhotoImage(pil_image, master=tk_master) - except: - try: - # Fallback if PIL isn't available - tk_image = tk.PhotoImage(file=filename, master=tk_master) - except: - msg = "Cannot load {}. Check to make sure it is an image file.".format( - filename) - try: - _ = PILImage - except: - msg += "\nPIL library isn't installed. If it isn't installed, only .gif files can be used." - raise ValueError(msg) - return tk_image - - -# ------------------------------------------------------------------- -# getFileDialogTitle -# ------------------------------------------------------------------- -def getFileDialogTitle(msg, title): - """ - Create nicely-formatted string based on arguments msg and title - :param msg: the msg to be displayed - :param title: the window title - :return: None - """ - if msg and title: - return "%s - %s" % (title, msg) - if msg and not title: - return str(msg) - if title and not msg: - return str(title) - return None # no message and no title - - -if __name__ == '__main__': - print("Hello from utils") - - -def mouse_click_handlers(callback): - ns = {"mouse_on_button": True} - - def handler_enter(event): - ns["mouse_on_button"] = True - - def handler_leave(event): - ns["mouse_on_button"] = False - - def handler_button(event): - if ns["mouse_on_button"]: - return callback(event) - - return None - - return {"Enter": handler_enter, - "Leave": handler_leave, - "ButtonRelease-1": handler_button} diff --git a/easygui/button_box.py b/easygui/button_box.py new file mode 100644 index 0000000..d69b1cf --- /dev/null +++ b/easygui/button_box.py @@ -0,0 +1,367 @@ +import tkinter as tk + +from easygui.utilities import load_tk_image, get_width_and_padding, parse_hotkey + + +def buttonbox(msg="buttonbox options", title=" ", choices=("Button[1]", "Button[2]", "Button[3]"), + image=None, images=None, default_choice=None, cancel_choice=None, + callback=None, run=True): + """ Display a message, a title, an image, and a set of buttons. + The buttons are defined by the members of the choices argument. + :param str msg: the msg to be displayed + :param str title: the window title + :param list choices: a list or tuple of the choices to be displayed + :param str image: (Only here for backward compatibility) + :param str images: Filename of image or iterable or iteratable of iterable to display + :param str default_choice: The choice you want highlighted when the gui appears + :param cancel_choice: + :param callback: + :param run: + :return: the text of the button that the user selected OR the buttonbox object if run was False + """ + if image and images: + raise ValueError("Cannot run buttonbox() with both 'image' and 'images' - pick one!") + if image: + images = image + bb = ButtonBox( + msg=msg, + title=title, + choices=choices, + images=images, + default_choice=default_choice, + cancel_choice=cancel_choice, + callback=callback) + return bb.run() if run else bb + + +def boolbox(msg="[T]rue or [F]alse?", title=" ", choices=("[T]rue", "[F]alse"), image=None, + default_choice='[T]rue', cancel_choice='[F]alse'): + """ + Display a boolean dialog window with True/False options. + + The function returns `True` if the first button is selected, and returns + `False` if the second button is selected. The function returns `None` + if the user closes the dialog window. + + :param str msg: The message shown in the center of the dialog window. + :param str title: The window title text. + :param list choices: A list or tuple of strings for the buttons' text. + :param str image: The filename of an image to display in the dialog window. + :param str default_choice: The text of the default selected button. + :param str cancel_choice: If the user presses the 'X' close, which button + should be pressed + :return: `True` if first button pressed, `False` if second button is pressed, or None if the window was cancelled. + """ + + if len(choices) != 2: + raise AssertionError('boolbox takes exactly 2 choices! Consider using indexbox instead') + + reply = buttonbox(msg, title, choices, image, default_choice=default_choice, cancel_choice=cancel_choice) + if reply == choices[0]: + return True # The first button (True) was selected. + elif reply == choices[1]: + return False # The second button (False) was selected. + elif reply is None: + return None # The window was closed. + + +def ynbox(msg="Yes or No ?", title=" ", choices=("[]Yes", "[]No"), image=None, + default_choice='[]Yes', cancel_choice='[]No'): + """ + The ``ynbox()`` offers a choice of Yes and No, and returns either ``True`` or ``False``. + + import easygui + result = easygui.ynbox('Is a hot dog a sandwich?', 'Hot Dog Question') + if result == True: + easygui.msgbox('That is an interesting answer.') + else: + easygui.msgbox('Well, that is your opinion.') + + :param msg: the msg to be displayed + :param str title: the window title + :param list choices: a list or tuple of the choices to be displayed + :param str image: Filename of image to display + :param str default_choice: The choice you want highlighted when the gui appears + :param str cancel_choice: If the user presses the 'X' close, which button should be pressed + :return: True if 'Yes', False if 'No', None if dialog was cancelled + """ + return boolbox(msg, title, choices, image, default_choice=default_choice, cancel_choice=cancel_choice) + + +def ccbox(msg="C[o]ntinue or C[a]ncel?", title=" ", choices=("C[o]ntinue", "C[a]ncel"), image=None, + default_choice='C[o]ntinue', cancel_choice='C[a]ncel'): + """ + The ``ccbox()`` function offers a choice of Continue (returns True) and Cancel (returns False) + + import easygui + if easygui.ccbox("Do you want to continue?", "Please Confirm"): + pass # User chose Continue. + else: # User chose Cancel. + sys.exit() + + :param str msg: the msg to be displayed + :param str title: the window title + :param list choices: a list or tuple of the choices to be displayed + :param str image: Filename of image to display + :param str default_choice: The choice you want highlighted when the gui appears + :param str cancel_choice: If the user presses the 'X' close, which button should be pressed + :return: True if 'Continue' or dialog is cancelled, False if 'Cancel' + """ + return boolbox(msg, title, choices, image, default_choice=default_choice, cancel_choice=cancel_choice) + + +def indexbox(msg="Options: ", title=" ", choices=("Door 1", "Door 2", "Door 3"), image=None, + default_choice='Door 1', cancel_choice='Door 3'): + """ + The ``indexbox()`` function displays a set of buttons, and returns the + index of the selected button. For example, if you invoked index box with + three choices (A, B, C), indexbox would return 0 if the user picked A, 1 + if he picked B, and 2 if he picked C. + + import easygui + result = easygui.indexbox('Which door do you choose?', 'Win Prizes!', choices=['Door 1', 'Door 2', 'Door 3']) + if result == 2: + easygui.msgbox('You win a new car!') + else: + easygui.msgbox('Better luck next time.') + + :param str msg: the msg to be displayed + :param str title: the window title + :param list choices: a list or tuple of the choices to be displayed + :param str image: Filename of image to display + :param str default_choice: The choice you want highlighted when the gui appears + :param str cancel_choice: If the user presses the 'X' close, which button should be pressed + :return: the index of the choice selected, starting from 0 + """ + reply = buttonbox(msg, title, choices, image, default_choice=default_choice, cancel_choice=cancel_choice) + try: + return list(choices).index(reply) + except ValueError: + msg = ("EasyGui indexbox could not determine the index of choice {} in choices {}.".format(reply, choices)) + raise AssertionError(msg) + + +def msgbox(msg="(Your message goes here)", title=" ", ok_button="OK", image=None): + """ + The ``msgbox()`` function displays a text message and offers an OK + button. The message text appears in the center of the window, the title + text appears in the title bar, and you can replace the "OK" default text + on the button. Here is the signature:: + + def msgbox(msg="(Your message goes here)", title="", ok_button="OK"): + .... + + The clearest way to override the button text is to do it with a keyword + argument, like this:: + + easygui.msgbox("Backup complete!", ok_button="Good job!") + + Here are a couple of examples:: + + easygui.msgbox("Hello, world!") + + :param str msg: the msg to be displayed + :param str title: the window title + :param str ok_button: text to show in the button + :param str image: Filename of image to display + :return: the text of the ok_button + """ + return buttonbox(msg, title, choices=[ok_button], image=image, default_choice=ok_button, cancel_choice=ok_button) + + +class ButtonBox(object): + """ Display various types of button boxes + + This object separates user from ui, defines which methods can + the user invoke and which properties can he change. + """ + + def __init__(self, msg, title, choices, images, default_choice, cancel_choice, callback): + """ Create box object + + Parameters + ---------- + msg : string + text displayed in the message area (instructions...) + title : str + the window title + choices : iterable of strings + build a button for each string in choices + images : iterable of filenames, or an iterable of iterables of filenames + displays each image + default_choice : string + one of the strings in choices to be the default selection + cancel_choice : string + if X or is pressed, it appears as if this button was pressed. + callback: function + if set, this function will be called when any button is pressed. + + """ + + self._user_specified_callback = callback + self._text_to_return_on_cancel = cancel_choice + + self.choice_text = None + + self._images = [] + self._buttons = [] + + self.box_root = self._configure_box_root(title) + self.message_area = self._configure_message_area(box_root=self.box_root) + self._set_msg_area('' if msg is None else msg) + self.images_frame = self._create_images_frame(images) + self.buttons_frame = self._create_buttons_frame(choices, default_choice) + + def _configure_box_root(self, title): + box_root = tk.Tk() + box_root.title(title) + box_root.iconname('Dialog') + box_root.geometry('600x400+100+100') + box_root.protocol('WM_DELETE_WINDOW', self._x_pressed) # Quit when x button pressed + box_root.bind("", self._cancel_button_pressed) + return box_root + + @staticmethod + def _configure_message_area(box_root): + padding, width_in_chars = get_width_and_padding(monospace=False) + message_frame = tk.Frame(box_root, padx=padding) + message_frame.grid() + message_area = tk.Text(master=message_frame, width=width_in_chars, padx=padding, pady=padding, wrap=tk.WORD) + message_area.grid() + return message_area + + def _set_msg_area(self, msg): + self.message_area.delete(1.0, tk.END) + self.message_area.insert(tk.END, msg) + num_lines, _ = self.message_area.index(tk.END).split('.') + self.message_area.configure(height=int(num_lines)) + self.message_area.update() + + @staticmethod + def _convert_to_a_list_of_lists(filenames): + """ return a list of lists, handling all of the different allowed types of 'filenames' input """ + if type(filenames) is str: + return [[filenames, ], ] + elif type(filenames[0]) is str: + return [filenames, ] + elif type(filenames[0][0]) is str: + return filenames + raise ValueError("Incorrect images argument.") + + def _create_images_frame(self, filenames): + images_frame = tk.Frame(self.box_root) + row = 1 + images_frame.grid(row=row) + self.box_root.rowconfigure(row, weight=10, minsize='10m') + + if filenames is None: + return + + filename_array = self._convert_to_a_list_of_lists(filenames) + for row, list_of_filenames in enumerate(filename_array): + for column, filename in enumerate(list_of_filenames): + try: + tk_image = load_tk_image(filename, tk_master=images_frame) + except Exception as e: + print(e) + tk_image = None + widget = tk.Button( + master=images_frame, + takefocus=1, + compound=tk.TOP, + image=tk_image, + command=lambda text=filename: self._button_pressed(text) + ) + widget.grid(row=row, column=column, sticky=tk.NSEW, padx='1m', pady='1m', ipadx='2m', ipady='1m') + + image = {'tk_image': tk_image, 'widget': widget} + images_frame.rowconfigure(row, weight=10, minsize='10m') + images_frame.columnconfigure(column, weight=10) + self._images.append(image) # Prevent image deletion by keeping them on self + return images_frame + + def _create_buttons_frame(self, choices, default_choice): + buttons_frame = tk.Frame(self.box_root) + buttons_frame.grid(row=2) + + for column, button_text in enumerate(choices): + clean_text, hotkey, hotkey_position = parse_hotkey(button_text) + widget = tk.Button( + master=buttons_frame, + takefocus=1, + text=clean_text, + underline=hotkey_position, + command=lambda text=button_text: self._button_pressed(text) + ) + widget.grid(row=0, column=column, padx='1m', pady='1m', ipadx='2m', ipady='1m') + button = { + 'original_text': button_text, + 'clean_text': clean_text, + 'hotkey': hotkey, + 'widget': widget + } + buttons_frame.columnconfigure(column, weight=10) + self._buttons.append(button) + + for button in self._buttons: + if button['original_text'] == default_choice: + button['widget'].focus_force() + + if button['hotkey'] is not None: + self.box_root.bind_all(button['hotkey'], lambda e: self._hotkey_pressed(e), add=True) + + return buttons_frame + + def _callback(self, command): + if command == 'update': # OK was pressed + if self._user_specified_callback: + # If a callback was set, call main process + self._user_specified_callback() + else: + self.stop() + elif command in ('x', 'cancel'): + self.stop() + + def run(self): + self.box_root.mainloop() + self.box_root.destroy() + return self.choice_text + + def stop(self): + self.box_root.quit() + + # Methods executing when a key is pressed + def _x_pressed(self): + self._callback(command='x') + self.choice_text = self._text_to_return_on_cancel + + def _cancel_button_pressed(self, _): + self._callback(command='cancel') + self.choice_text = self._text_to_return_on_cancel + + def _button_pressed(self, button_text): + self._callback(command='update') + self.choice_text = button_text + + def _hotkey_pressed(self, event=None): + """ Handle an event that is generated by a person interacting with a button """ + if event.keysym != event.char: # A special character + hotkey_pressed = '<{}>'.format(event.keysym) + else: + hotkey_pressed = event.keysym + + for button in self._buttons: + if button['hotkey'] == hotkey_pressed: + self._callback(command='update') + self.choice_text = button['original_text'] + + return # some key was pressed, but no hotkey registered to it + + +if __name__ == '__main__': + buttonbox() + boolbox() + ynbox() + ccbox() + indexbox() + msgbox() diff --git a/easygui/choice_box.py b/easygui/choice_box.py new file mode 100644 index 0000000..6d2a14c --- /dev/null +++ b/easygui/choice_box.py @@ -0,0 +1,324 @@ +import string +import tkinter as tk + +from easygui.utilities import get_num_lines, get_width_and_padding, bindArrows, MouseClickHandler + + +def choicebox(msg="Pick an item", title="", choices=None, preselect=[], callback=None, run=True): + """ + Present the user with a list of choices. + return the choice that he selects. + + :param str msg: the msg to be displayed + :param str title: the window title + :param list choices: a list or tuple of the choices to be displayed + :param preselect: optional list of pre-selected choices, a subset of the choices argument + :param callback: + :param run: + :return: List containing choice selected or None if cancelled + """ + cb = ChoiceBox(msg, title, choices, preselect=preselect, multiple_select=False, callback=callback) + if run: + reply = cb.run() + return reply + else: + return cb + + +def multchoicebox(msg="Pick an item", title="", choices=None, preselect=[], callback=None, run=True): + """ + The ``multchoicebox()`` function provides a way for a user to select + from a list of choices. The interface looks just like the ``choicebox()`` + function's dialog box, but the user may select zero, one, or multiple choices. + + The choices are specified in a sequence (a tuple or a list). + + import easygui + msg ="What is your favorite flavor?" + title = "Ice Cream Survey" + choices = ["Vanilla", "Chocolate", "Strawberry", "Rocky Road"] + choice = easygui.multchoicebox(msg, title, choices) + + + :param str msg: the msg to be displayed + :param str title: the window title + :param list choices: a list or tuple of the choices to be displayed + :param preselect: Which item, if any are preselected when dialog appears + :return: A list of strings of the selected choices or None if cancelled. + """ + mcb = ChoiceBox(msg, title, choices, preselect=preselect, multiple_select=True, callback=callback) + if run: + reply = mcb.run() + return reply + else: + return mcb + + +class ChoiceBox(object): + + def __init__(self, msg, title, choices, preselect, multiple_select, callback): + + if not multiple_select and len(preselect)>1: + raise ValueError("Multiple selections not allowed, yet preselect has multiple values:{}".format(preselect)) + + self._multiple_select = multiple_select + self._user_specified_callback = callback + if choices is None: + # Use default choice selections if none were specified: + choices = ('Choice 1', 'Choice 2') + self.choices = [str(c) for c in choices] + + self.box_root = self._configure_box_root(title) + + self.message_area = self._configure_message_area(self.box_root) + self._set_msg_area("" if msg is None else msg) + + self.create_choice_area() + + self.create_ok_button() + self.create_cancel_button() + self.create_special_buttons() + self.preselect_choice(preselect) + self.choiceboxWidget.focus_force() + + def run(self): + self.box_root.mainloop() # run it! + self.box_root.destroy() # close the window + return self.choices + + def stop(self): + self.box_root.quit() + + def x_pressed(self): + self.stop() + self.choices = None + + def cancel_button_pressed(self, event): + self.stop() + self.choices = None + + def ok_button_pressed(self, event): + self.choices = self.get_choices() + if self._user_specified_callback: + # If a _user_specified_callback was set, call main process + self._user_specified_callback(self) + else: + self.stop() + + def _set_msg_area(self, msg): + self.message_area.config(state=tk.NORMAL) # necessary but I don't know why + self.message_area.delete(1.0, tk.END) + self.message_area.insert(tk.END, msg) + numlines = get_num_lines(self.message_area) + self.message_area.configure(height=numlines) + self.message_area.update() + + def preselect_choice(self, preselect): + if preselect != None: + for v in preselect: + self.choiceboxWidget.select_set(v) + self.choiceboxWidget.activate(v) + + def get_choices(self): + choices_index = self.choiceboxWidget.curselection() + if not choices_index: + return None + if self._multiple_select: + selected_choices = [self.choiceboxWidget.get(index) + for index in choices_index] + else: + selected_choices = self.choiceboxWidget.get(choices_index) + + return selected_choices + + def _configure_box_root(self, title): + box_root = tk.Tk() + box_root.title(title) + box_root.iconname('Dialog') + box_root.protocol('WM_DELETE_WINDOW', self.x_pressed) + box_root.bind('', self.KeyboardListener) + box_root.bind("", self.cancel_button_pressed) + return box_root + + @staticmethod + def _configure_message_area(box_root): + padding, width_in_chars = get_width_and_padding(monospace=False) + + message_frame = tk.Frame(box_root, padx=padding) + message_frame.pack(side=tk.TOP, expand=1, fill='both') + + message_area = tk.Text(master=message_frame, + width=width_in_chars, + state=tk.DISABLED, + background=box_root.config()["background"][-1], + relief='flat', + padx=padding, + pady=padding, + wrap=tk.WORD) + message_area.pack(side=tk.TOP, expand=1, fill='both') + return message_area + + def create_choice_area(self): + + self.choiceboxFrame = tk.Frame(master=self.box_root) + self.choiceboxFrame.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.YES) + + lines_to_show = min(len(self.choices), 20) + + # -------- put the self.choiceboxWidget in the self.choiceboxFrame --- + self.choiceboxWidget = tk.Listbox(self.choiceboxFrame, + height=lines_to_show, + borderwidth="1m", relief="flat", + bg="white" + ) + + if self._multiple_select: + self.choiceboxWidget.configure(selectmode=tk.MULTIPLE) + + # self.choiceboxWidget.configure(font=(global_state.PROPORTIONAL_FONT_FAMILY, + # global_state.PROPORTIONAL_FONT_SIZE)) + + # add a vertical scrollbar to the frame + rightScrollbar = tk.Scrollbar(self.choiceboxFrame, orient=tk.VERTICAL, + command=self.choiceboxWidget.yview) + self.choiceboxWidget.configure(yscrollcommand=rightScrollbar.set) + + # add a horizontal scrollbar to the frame + bottomScrollbar = tk.Scrollbar(self.choiceboxFrame, + orient=tk.HORIZONTAL, + command=self.choiceboxWidget.xview) + self.choiceboxWidget.configure(xscrollcommand=bottomScrollbar.set) + + # pack the Listbox and the scrollbars. + # Note that although we must define + # the textArea first, we must pack it last, + # so that the bottomScrollbar will + # be located properly. + + bottomScrollbar.pack(side=tk.BOTTOM, fill=tk.X) + rightScrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + self.choiceboxWidget.pack( + side=tk.LEFT, padx="1m", pady="1m", expand=tk.YES, fill=tk.BOTH) + + # Insert choices widgets + for choice in self.choices: + self.choiceboxWidget.insert(tk.END, choice) + + # Bind the keyboard events + self.choiceboxWidget.bind("", self.ok_button_pressed) + self.choiceboxWidget.bind("", + self.ok_button_pressed) + + def create_ok_button(self): + + self.buttonsFrame = tk.Frame(self.box_root) + self.buttonsFrame.pack(side=tk.TOP, expand=tk.YES, pady=0) + + # put the buttons in the self.buttonsFrame + okButton = tk.Button(self.buttonsFrame, takefocus=tk.YES, + text="OK", height=1, width=6) + bindArrows(okButton) + okButton.pack(expand=tk.NO, side=tk.RIGHT, padx='2m', pady='1m', + ipady="1m", ipadx="2m") + + # for the commandButton, bind activation events + okButton.bind("", self.ok_button_pressed) + okButton.bind("", self.ok_button_pressed) + + ok_click_handler = MouseClickHandler(callback=self.ok_button_pressed) + okButton.bind("", ok_click_handler.enter) + okButton.bind("", ok_click_handler.leave) + okButton.bind("", ok_click_handler.release) + + def create_cancel_button(self): + cancelButton = tk.Button(self.buttonsFrame, takefocus=tk.YES, + text="Cancel", height=1, width=6) + bindArrows(cancelButton) + cancelButton.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', + ipady="1m", ipadx="2m") + cancelButton.bind("", self.cancel_button_pressed) + + cancel_click_handler = MouseClickHandler(callback=self.cancel_button_pressed) + cancelButton.bind("", cancel_click_handler.enter) + cancelButton.bind("", cancel_click_handler.leave) + cancelButton.bind("", cancel_click_handler.release) + + def create_special_buttons(self): + # add special buttons for multiple select features + if not self._multiple_select: + return + + selectAllButton = tk.Button( + self.buttonsFrame, text="Select All", height=1, width=6) + selectAllButton.pack(expand=tk.NO, side=tk.LEFT, padx='2m', + pady='1m', + ipady="1m", ipadx="2m") + + clearAllButton = tk.Button(self.buttonsFrame, text="Clear All", + height=1, width=6) + clearAllButton.pack(expand=tk.NO, side=tk.LEFT, + padx='2m', pady='1m', + ipady="1m", ipadx="2m") + + selectAllButton.bind("", self.choiceboxSelectAll) + bindArrows(selectAllButton) + clearAllButton.bind("", self.choiceboxClearAll) + bindArrows(clearAllButton) + + def KeyboardListener(self, event): + key = event.keysym + if len(key) <= 1: + if key in string.printable: + # Find the key in the liglobal_state. + # before we clear the list, remember the selected member + try: + start_n = int(self.choiceboxWidget.curselection()[0]) + except IndexError: + start_n = -1 + + # clear the selection. + self.choiceboxWidget.selection_clear(0, 'end') + + # start from previous selection +1 + for n in range(start_n + 1, len(self.choices)): + item = self.choices[n] + if item[0].lower() == key.lower(): + self.choiceboxWidget.selection_set(first=n) + self.choiceboxWidget.see(n) + return + else: + # has not found it so loop from top + for n, item in enumerate(self.choices): + if item[0].lower() == key.lower(): + self.choiceboxWidget.selection_set(first=n) + self.choiceboxWidget.see(n) + return + + # nothing matched -- we'll look for the next logical choice + for n, item in enumerate(self.choices): + if item[0].lower() > key.lower(): + if n > 0: + self.choiceboxWidget.selection_set( + first=(n - 1)) + else: + self.choiceboxWidget.selection_set(first=0) + self.choiceboxWidget.see(n) + return + + # still no match (nothing was greater than the key) + # we set the selection to the first item in the list + lastIndex = len(self.choices) - 1 + self.choiceboxWidget.selection_set(first=lastIndex) + self.choiceboxWidget.see(lastIndex) + return + + def choiceboxClearAll(self, event): + self.choiceboxWidget.selection_clear(0, len(self.choices) - 1) + + def choiceboxSelectAll(self, event): + self.choiceboxWidget.selection_set(0, len(self.choices) - 1) + +if __name__ == '__main__': + users_choice = multchoicebox(choices=['choice1', 'choice2']) + print("User's choice is: {}".format(users_choice)) diff --git a/easygui/easygui.py b/easygui/easygui.py deleted file mode 100644 index 9d050be..0000000 --- a/easygui/easygui.py +++ /dev/null @@ -1,86 +0,0 @@ -""" - -.. moduleauthor:: easygui developers and Stephen Raymond Ferg -.. default-domain:: py -.. highlight:: python - -Version |release| - -ABOUT EASYGUI -============= - -EasyGUI provides an easy-to-use interface for simple GUI interaction -with a user. It does not require the programmer to know anything about -tkinter, frames, widgets, callbacks or lambda. All GUI interactions are -invoked by simple function calls that return results. - -.. warning:: Using EasyGUI with IDLE - - You may encounter problems using IDLE to run programs that use EasyGUI. Try it - and find out. EasyGUI is a collection of Tkinter routines that run their own - event loops. IDLE is also a Tkinter application, with its own event loop. The - two may conflict, with unpredictable results. If you find that you have - problems, try running your EasyGUI program outside of IDLE. - -.. note:: EasyGUI requires Tk release 8.0 or greater. - -LICENSE INFORMATION -=================== -EasyGUI version |version| - -Copyright (c) 2014, Stephen Raymond Ferg - -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. - - 3. The name of the author may not be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, -STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING -IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -ABOUT THE EASYGUI LICENSE -------------------------- -| This license is what is generally known as the "modified BSD license", -| aka "revised BSD", "new BSD", "3-clause BSD". -| See http://www.opensource.org/licenses/bsd-license.php -| -| This license is GPL-compatible. -| See ``_ -| See http://www.gnu.org/licenses/license-liglobal_state.html#GPLCompatibleLicenses -| -| The BSD License is less restrictive than GPL. -| It allows software released under the license to be incorporated into proprietary products. -| Works based on the software may be released under a proprietary license or as closed source software. -| ``_ - -API -=== -""" - - -if __name__ == '__main__': - from boxes.demo import easygui_demo - easygui_demo() - - # from boxes.alt_text_box import demo_textbox - # demo_textbox() diff --git a/easygui/boxes/egstore.py b/easygui/egstore.py similarity index 100% rename from easygui/boxes/egstore.py rename to easygui/egstore.py diff --git a/easygui/file_boxes.py b/easygui/file_boxes.py new file mode 100644 index 0000000..4b09078 --- /dev/null +++ b/easygui/file_boxes.py @@ -0,0 +1,51 @@ +from os.path import normpath + +from tkinter import filedialog + + +def diropenbox(title='Open', default_directory=None): + """ + A dialog to get a directory name. + + Returns the name of a directory, or None if user chose to cancel. + + If the "default" argument specifies a directory name, and that + directory exists, then the dialog box will start with that directory. + + :param str msg: used in the window title + :param str msg: used in the window title on some platforms + :param str title: the window title + :param str default: starting directory when dialog opens + :return: Normalized path selected by user + """ + directory_path = filedialog.askdirectory(title=title, initialdir=default_directory) + return None if directory_path is None else normpath(directory_path) + + +def filesavebox(title='Save As', default_directory="", filetypes=None): + """A dialog box to get the name of a file. Return the name of a file, or None if user chose to cancel """ + file_path = filedialog.asksaveasfilename(title=title, initialdir=default_directory, filetypes=filetypes) + return None if file_path is None else normpath(file_path) + + +def fileopenbox(title='Open', default_directory='*', filetypes=(), multiple=False): + """ A dialog to get a file name. + fileopenbox automatically changes the path separator to backslash on Windows. + + :param str title: the window title + :param str default_directory: filepath to search, may contain wildcards, defaults to all files in current directory + :param object filetypes: a file pattern OR a list of file patterns and a file type description + eg. ["*.css", ["*.htm", "*.html", "HTML files"] ] + If the filetypes list does not contain ("All files","*"), it will be added. + :param bool multiple: allow selection of more than one file + :return: the name of a file, or None if user chose to cancel + """ + kwargs = dict(title=title, initialdir=default_directory, filetypes=filetypes) + + if multiple: + filenames = filedialog.askopenfilenames(**kwargs) + return None if filenames is None else [normpath(x) for x in filenames] + + else: + filename = filedialog.askopenfilename(**kwargs) + return None if filename is None else normpath(filename) diff --git a/easygui/fillable_box.py b/easygui/fillable_box.py new file mode 100644 index 0000000..514702c --- /dev/null +++ b/easygui/fillable_box.py @@ -0,0 +1,207 @@ +import tkinter as tk + +from easygui import msgbox +from easygui.global_state import GLOBAL_WINDOW_POSITION, PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE, \ + TEXT_ENTRY_FONT_SIZE +from easygui.utilities import load_tk_image, bindArrows, MouseClickHandler + + +def integerbox(msg=None, title=" ", default=None, lowerbound=0, upperbound=99, image=None, root=None): + """ + Show a box in which a user can enter an integer. + + In addition to arguments for msg and title, this function accepts + integer arguments for "default", "lowerbound", and "upperbound". + + The default, lowerbound, or upperbound may be None. + + When the user enters some text, the text is checked to verify that it + can be converted to an integer between the lowerbound and upperbound. + + If it can be, the integer (not the text) is returned. + + If it cannot, then an error msg is displayed, and the integerbox is + redisplayed. + + If the user cancels the operation, None is returned. + + :param str msg: the msg to be displayed + :param str title: the window title + :param int default: The default value to return + :param int lowerbound: The lower-most value allowed + :param int upperbound: The upper-most value allowed + :param str image: Filename of image to display + :param tk_widget root: Top-level Tk widget + :return: the integer value entered by the user + """ + msg = "Enter an integer between {0} and {1}".format(lowerbound, upperbound) if msg is None else msg + + while True: + result = FillableBox(msg, title, default, image=image, root=root).run() + if result is None: + return None + + try: + result = int(result) + except ValueError: + msgbox('The value that you entered:\n\t"{}"\nis not an integer.'.format(result), "Error") + continue + + if lowerbound and result < int(lowerbound): + msgbox('The value that you entered is less than the lower bound of {}.'.format(lowerbound), "Error") + elif upperbound and result > int(upperbound): + msgbox('The value that you entered is greater than the upper bound of {}.'.format(upperbound), "Error") + else: + return result # validation passed! + + +def enterbox(msg="Enter something.", title=" ", default="", strip=True, image=None, root=None): + """ + Show a box in which a user can enter some text. + + You may optionally specify some default text, which will appear in the + enterbox when it is displayed. + + Example:: + + import easygui + reply = easygui.enterbox('Enter your life story:') + if reply: + easygui.msgbox('Thank you for your response.') + else: + easygui.msgbox('Your response has been discarded.') + + :param str msg: the msg to be displayed. + :param str title: the window title + :param str default: value returned if user does not change it + :param bool strip: If True, the return value will have + its whitespace stripped before being returned + :return: the text that the user entered, or None if they cancel + the operation. + """ + result = FillableBox(msg, title, default, image=image, root=root).run() + if result and strip: + result = result.strip() + return result + + +def passwordbox(msg="Enter your password.", title="", default="", image=None, root=None): + """ + Show a box in which a user can enter a password. + The text is masked with asterisks, so the password is not displayed. + + :param str msg: the msg to be displayed. + :param str title: the window title + :param str default: value returned if user does not change it + :return: the text that the user entered, or None if they cancel + the operation. + """ + return FillableBox(msg, title, default, mask="*", image=image, root=root).run() + + +def fillablebox(msg, title="", default=None, mask=None, image=None, root=None): + """ + Show a box in which a user can enter some text. + :param str msg: the msg to be displayed. + :param str title: the window title + :param str default: default value populated, returned if user does not change it + :return: the text that the user entered, or None if he cancels the operation. + """ + return FillableBox(msg, title, default, mask, image, root).run() + + +class FillableBox(object): + def __init__(self, msg, default, title, mask=None, image=None, root=None): + self.return_value = '' if default is None else default + self.pre_existing_root = root + self.box_root = None + self.entry_widget = None + + if root: + root.withdraw() + self.box_root = tk.Toplevel(master=root) + self.box_root.withdraw() + else: + self.box_root = tk.Tk() + self.box_root.withdraw() + + self.box_root.protocol('WM_DELETE_WINDOW', self._cancel_pressed) + self.box_root.title(title) + self.box_root.iconname('Dialog') + self.box_root.geometry(GLOBAL_WINDOW_POSITION) + self.box_root.bind("", self._cancel_pressed) + + message_frame = tk.Frame(master=self.box_root) + message_frame.pack(side=tk.TOP, fill=tk.BOTH) + + try: + tk_image = load_tk_image(image) + except Exception as e: + print(e) + tk_image = None + if tk_image: + image_frame = tk.Frame(master=self.box_root) + image_frame.pack(side=tk.TOP, fill=tk.BOTH) + label = tk.Label(image_frame, image=tk_image) + label.image = tk_image # keep a reference! + label.pack(side=tk.TOP, expand=tk.YES, fill=tk.X, padx='1m', pady='1m') + + buttons_frame = tk.Frame(master=self.box_root) + buttons_frame.pack(side=tk.TOP, fill=tk.BOTH) + + entry_frame = tk.Frame(master=self.box_root) + entry_frame.pack(side=tk.TOP, fill=tk.BOTH) + + buttons_frame = tk.Frame(master=self.box_root) + buttons_frame.pack(side=tk.TOP, fill=tk.BOTH) + + message_widget = tk.Message(message_frame, width="4.5i", text=msg) + message_widget.configure(font=(PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE)) + message_widget.pack(side=tk.RIGHT, expand=1, fill=tk.BOTH, padx='3m', pady='3m') + + entry_widget = tk.Entry(entry_frame, width=40) + entry_widget.configure(font=(PROPORTIONAL_FONT_FAMILY, TEXT_ENTRY_FONT_SIZE)) + if mask: + entry_widget.configure(show=mask) + entry_widget.pack(side=tk.LEFT, padx="3m") + entry_widget.bind("", self._ok_pressed) + entry_widget.bind("", self._cancel_pressed) + entry_widget.insert(0, self.return_value) # put text into the entry_widget + self.entry_widget = entry_widget # save a reference - we need to get text from this widget later + + ok_button = tk.Button(buttons_frame, takefocus=1, text="OK") + bindArrows(ok_button) + ok_button.pack(expand=1, side=tk.LEFT, padx='3m', pady='3m', ipadx='2m', ipady='1m') + ok_button.bind("", self._ok_pressed) + ok_click_handler = MouseClickHandler(callback=self._ok_pressed) + ok_button.bind("", ok_click_handler.enter) + ok_button.bind("", ok_click_handler.leave) + ok_button.bind("", ok_click_handler.release) + + cancel_button = tk.Button(buttons_frame, takefocus=1, text="Cancel") + cancel_button.pack(expand=1, side=tk.RIGHT, padx='3m', pady='3m', ipadx='2m', ipady='1m') + cancel_button.bind("", self._cancel_pressed) + cancel_click_handler = MouseClickHandler(callback=self._cancel_pressed) + cancel_button.bind("", cancel_click_handler.enter) + cancel_button.bind("", cancel_click_handler.leave) + cancel_button.bind("", cancel_click_handler.release) + + self.entry_widget.focus_force() # put the focus on the self.entry_widget + self.box_root.deiconify() + + def _cancel_pressed(self, *args): + self.return_value = None + self.box_root.quit() + + def _ok_pressed(self, *args): + self.return_value = self.entry_widget.get() + self.box_root.quit() + + def run(self): + self.box_root.mainloop() # run it! + + # -------- after the run has completed ---------------------------------- + if self.pre_existing_root: + self.pre_existing_root.deiconify() + self.box_root.destroy() # button_click didn't destroy self.boxRoot, so we do it now + return self.return_value diff --git a/easygui/global_state.py b/easygui/global_state.py new file mode 100644 index 0000000..d208bbc --- /dev/null +++ b/easygui/global_state.py @@ -0,0 +1,14 @@ +GLOBAL_WINDOW_POSITION = "+300+200" +PROPORTIONAL_FONT_FAMILY = ("MS", "Sans", "Serif") + +PROPORTIONAL_FONT_SIZE = 10 +TEXT_ENTRY_FONT_SIZE = 12 # a little larger makes it easier to see + +PROP_FONT_LINE_LENGTH = 62 +FIXW_FONT_LINE_LENGTH = 80 + +DEFAULT_PADDING = 2 +REGULAR_FONT_WIDTH = 13 +FIXED_FONT_WIDTH = 7 +boxRoot = None + diff --git a/easygui/multi_fillable_box.py b/easygui/multi_fillable_box.py new file mode 100644 index 0000000..d4fca16 --- /dev/null +++ b/easygui/multi_fillable_box.py @@ -0,0 +1,118 @@ +import tkinter as tk + +from easygui.global_state import GLOBAL_WINDOW_POSITION, PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE, \ + TEXT_ENTRY_FONT_SIZE +from easygui.utilities import MouseClickHandler + + +def multpasswordbox(msg="Fill in values for the fields.", title=" ", fields=None, values=None, callback=None, run=True): + """ + Show dialog box with multiple data entry fields. + The last of the fields is assumed to be a password, and is masked with asterisks. + + :param str msg: the msg to be displayed. + :param str title: the window title + :param list fields: a list of fieldnames. + :param list values: a list of field values + :return: String + """ + mb = MultiBox(msg, title, fields, values, mask_last=True, callback=callback) + return mb.run() if run else mb + + +def multenterbox(msg="Fill in values for the fields.", title=" ", fields=None, values=None, callback=None, run=True): + """ + Show dialog box with multiple data entry fields. + + :param str msg: the msg to be displayed. + :param str title: the window title + :param list fields: a list of fieldnames. + :param list values: a list of field values + :return: String + """ + mb = MultiBox(msg, title, fields, values, mask_last=False, callback=callback) + return mb.run() if run else mb + + +class MultiBox(object): + def __init__(self, msg, title, fields=None, values=None, mask_last=False, callback=None): + self.fields, self.values = self._process_fields_and_values(fields, values) + self.user_defined_callback = callback + + self.boxRoot = tk.Tk() + self.boxRoot.protocol('WM_DELETE_WINDOW', self._cancel_pressed) + self.boxRoot.title(title) + self.boxRoot.iconname('Dialog') + self.boxRoot.bind("", self._cancel_pressed) + self.boxRoot.geometry(GLOBAL_WINDOW_POSITION) + + message_widget = tk.Message(self.boxRoot, width="4.5i", text=msg) + message_widget.configure(font=(PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE)) + message_widget.pack(side=tk.TOP, expand=1, fill=tk.BOTH, padx='3m', pady='3m') + + self.entry_widgets = [] + for field, value in zip(self.fields, self.values): + entry_frame = tk.Frame(master=self.boxRoot) + entry_frame.pack(side=tk.TOP, fill=tk.BOTH) + + label_widget = tk.Label(entry_frame, text=field) + label_widget.pack(side=tk.LEFT) + + entry_widget = tk.Entry(entry_frame, width=40, highlightthickness=2) + self.entry_widgets.append(entry_widget) + entry_widget.configure(font=(PROPORTIONAL_FONT_FAMILY, TEXT_ENTRY_FONT_SIZE)) + entry_widget.pack(side=tk.RIGHT, padx="3m") + entry_widget.bind("", self._ok_pressed) + entry_widget.bind("", self._cancel_pressed) + entry_widget.insert(0, '' if value is None else value) + + if mask_last: + self.entry_widgets[-1].configure(show="*") + + buttons_frame = tk.Frame(master=self.boxRoot) + buttons_frame.pack(side=tk.BOTTOM) + + cancel_button = tk.Button(buttons_frame, takefocus=1, text="Cancel") + cancel_button.pack(expand=1, side=tk.LEFT, padx='3m', pady='3m', ipadx='2m', ipady='1m') + cancel_button.bind("", self._cancel_pressed) + cancel_click_handler = MouseClickHandler(callback=self._cancel_pressed) + cancel_button.bind("", cancel_click_handler.enter) + cancel_button.bind("", cancel_click_handler.leave) + cancel_button.bind("", cancel_click_handler.release) + + ok_button = tk.Button(buttons_frame, takefocus=1, text="OK") + ok_button.pack(expand=1, side=tk.LEFT, padx='3m', pady='3m', ipadx='2m', ipady='1m') + ok_button.bind("", self._ok_pressed) + ok_click_handler = MouseClickHandler(callback=self._ok_pressed) + ok_button.bind("", ok_click_handler.enter) + ok_button.bind("", ok_click_handler.leave) + ok_button.bind("", ok_click_handler.release) + + self.entry_widgets[0].focus_force() # put the focus on the entry_widget + + def run(self): + self.boxRoot.mainloop() # run it! + self.boxRoot.destroy() # Close the window + return self.values + + def _cancel_pressed(self, *args): + self.values = None + self.boxRoot.quit() + + def _ok_pressed(self, _): + self.values = self._get_values() + if self.user_defined_callback: + self.user_defined_callback(self) + self.boxRoot.quit() + + def _get_values(self): + return [widget.get() for widget in self.entry_widgets] + + @staticmethod + def _process_fields_and_values(fields, values): + fields = [] if fields is None else list(fields) + values = [] if values is None else list(values) + padding_required = len(fields) - len(values) + if padding_required > 0: + values.extend([""] * padding_required) + return fields, values diff --git a/easygui/text_box.py b/easygui/text_box.py new file mode 100644 index 0000000..1e75423 --- /dev/null +++ b/easygui/text_box.py @@ -0,0 +1,238 @@ +import sys +import traceback + +import tkinter as tk +from tkinter import font + +from easygui.global_state import GLOBAL_WINDOW_POSITION +from easygui.utilities import get_num_lines, get_width_and_padding, MouseClickHandler + + +def textbox(msg='', title='', text='', codebox=False, callback=None, run=True): + """ + Displays a dialog box with a large, multi-line text box, and returns + the entered text as a string. The message text is displayed in a + proportional font and wraps. + + Parameters + ---------- + msg : string + text displayed in the message area (instructions...) + title : str + the window title + text: str, list or tuple + text displayed in textAreas (editable) + codebox: bool + if True, don't wrap and width is set to 80 chars + callback: function + if set, this function will be called when OK is pressed + run: bool + if True, a box object will be created and returned, but not run + + Returns + ------- + None + If cancel is pressed + str + If OK is pressed returns the contents of textArea. + TextBox + If the 'run' argument was False + """ + tb = TextBox(msg=msg, title=title, text=text, codebox=codebox, callback=callback) + if run: + text = tb.run() + return text + return tb + + +def codebox(msg='', title='', text=''): + """ + Helper method similar to textbox, displays text in a monospaced font which is useful for code. + + The text parameter should be a string, or a list or tuple of lines to be displayed in the textbox. + + :param str msg: the msg to be displayed + :param str title: the window title + :param str text: what to display in the textbox + """ + return textbox(msg, title, text, codebox=True) + + +def exceptionbox(msg='An error (exception) has occurred in the program.', title='Error Report'): + """ Display a box that gives information about the latest exception that has been raised. + + Display a box that gives information about the latest exception that has been raised. + + The caller may optionally pass in a title for the window, or a msg to accompany the error information. + + :param str msg: the msg to be displayed + :param str title: the window title + :return: None""" + + def format_exception_for_display(): + return "".join(traceback.format_exception(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2])) + + codebox(msg, title, format_exception_for_display()) + + +class TextBox(object): + """ Display a message, and an editable text field pre-populated with 'text' """ + + def __init__(self, msg, title, text, codebox, callback): + """ + :param msg: str displayed in the message area (instructions...) + :param title: str used as the window title + :param text: str displayed in textArea (editable) + :param codebox: bool (if true) don't wrap, set width to 80 chars, use monospace font + :param callback: optional function to be called when OK is pressed + """ + self._user_specified_callback = callback + self._text = text + self.msg = msg + + self.box_root = self._configure_box_root(title) + self.message_area = self._configure_message_area(box_root=self.box_root, code_box=codebox) + self._set_msg_area("" if msg is None else msg) + + self.MONOSPACE_FONT = font.Font(family='Courier') + self.text_area = self._configure_text_area(box_root=self.box_root, code_box=codebox) + self._set_text() + self._configure_buttons() + + + def _configure_box_root(self, title): + box_root = tk.Tk() + box_root.title(title) + box_root.iconname('Dialog') + box_root.geometry(GLOBAL_WINDOW_POSITION) + box_root.protocol('WM_DELETE_WINDOW', self.x_pressed) # Quit when x button pressed + box_root.bind("", self.cancel_button_pressed) + return box_root + + @staticmethod + def _configure_message_area(box_root, code_box): + padding, width_in_chars = get_width_and_padding(code_box) + + message_frame = tk.Frame(box_root, padx=padding) + message_frame.pack(side=tk.TOP, expand=1, fill='both') + + message_area = tk.Text(master=message_frame, + width=width_in_chars, + padx=padding, + pady=padding, + wrap=tk.WORD) + message_area.pack(side=tk.TOP, expand=1, fill='both') + return message_area + + def _configure_text_area(self, box_root, code_box): + padding, width_in_chars = get_width_and_padding(code_box) + + text_frame = tk.Frame(box_root, padx=padding, ) + text_frame.pack(side=tk.TOP) + + text_area = tk.Text(text_frame, padx=padding, pady=padding, height=25, width=width_in_chars) + text_area.configure(wrap=tk.NONE if code_box else tk.WORD) + + vertical_scrollbar = tk.Scrollbar(text_frame, orient=tk.VERTICAL, command=text_area.yview) + text_area.configure(yscrollcommand=vertical_scrollbar.set) + + horizontal_scrollbar = tk.Scrollbar(text_frame, orient=tk.HORIZONTAL, command=text_area.xview) + text_area.configure(xscrollcommand=horizontal_scrollbar.set) + + if code_box: + text_area.configure(font=self.MONOSPACE_FONT) + # no word-wrapping for code so we need a horizontal scroll bar + horizontal_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) + + vertical_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # pack textArea last so bottom scrollbar displays properly + text_area.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.YES) + + box_root.bind("", text_area.yview_scroll(1, tk.PAGES)) + box_root.bind("", text_area.yview_scroll(-1, tk.PAGES)) + + box_root.bind("", text_area.xview_scroll(1, tk.PAGES)) + box_root.bind("", text_area.xview_scroll(-1, tk.PAGES)) + + box_root.bind("", text_area.yview_scroll(1, tk.UNITS)) + box_root.bind("", text_area.yview_scroll(-1, tk.UNITS)) + + return text_area + + def _set_msg_area(self, msg): + self.message_area.delete(1.0, tk.END) + self.message_area.insert(tk.END, msg) + num_lines = get_num_lines(message_area=self.message_area) + self.message_area.configure(height=int(num_lines)) + self.message_area.update() + + def run(self): + self.box_root.mainloop() + self.box_root.destroy() + return self.text + + def stop(self): + self.box_root.quit() + + def _configure_buttons(self): + buttons_frame = tk.Frame(self.box_root) + buttons_frame.pack(side=tk.TOP) + + cancel_button = tk.Button(buttons_frame, takefocus=tk.YES, text="Cancel", height=1, width=6) + cancel_button.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", ipadx="2m") + cancel_button.bind("", self.cancel_button_pressed) + cancel_click_handler = MouseClickHandler(callback=self.cancel_button_pressed) + cancel_button.bind("", cancel_click_handler.enter) + cancel_button.bind("", cancel_click_handler.leave) + cancel_button.bind("", cancel_click_handler.release) + + ok_button = tk.Button(buttons_frame, takefocus=tk.YES, text="OK", height=1, width=6) + ok_button.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", ipadx="2m") + ok_button.bind("", self.ok_button_pressed) + ok_click_handler = MouseClickHandler(callback=self.ok_button_pressed) + ok_button.bind("", ok_click_handler.enter) + ok_button.bind("", ok_click_handler.leave) + ok_button.bind("", ok_click_handler.release) + + @property + def text(self): + """ Get _text which may be None if cancel was pressed""" + return self._text + + @text.setter + def text(self, text): + self._text = text + if text is not None: # cancel sets text=None but this is meaningless to the tk box content + self._set_text() + + def _get_text(self): + """ Used by the callback to get the text_area content""" + return self.text_area.get(1.0, 'end-1c') + + def _set_text(self): + self.text_area.delete(1.0, tk.END) + self.text_area.insert(tk.END, self._text, "normal") + self.text_area.focus() + + # Methods executing when a key is pressed + def x_pressed(self, _): + self.callback(command='x') + + def cancel_button_pressed(self, _): + self.callback(command='cancel') + + def ok_button_pressed(self, _): + self.callback(command='update') + + def callback(self, command): + if command == 'update': # OK was pressed + self.text = self._get_text() + if self._user_specified_callback: + # If a callback was set, call main process + self._user_specified_callback(self) + else: + self.stop() + elif command in ('x', 'cancel'): + self.stop() + self.text = None diff --git a/easygui/utilities.py b/easygui/utilities.py new file mode 100644 index 0000000..a2cc90d --- /dev/null +++ b/easygui/utilities.py @@ -0,0 +1,154 @@ +import os +import re +import tkinter as tk + +from easygui.global_state import PROP_FONT_LINE_LENGTH, FIXW_FONT_LINE_LENGTH, DEFAULT_PADDING, REGULAR_FONT_WIDTH, \ + FIXED_FONT_WIDTH, boxRoot + + +def parse_hotkey(text): + """ + Extract a desired hotkey from the text. The format to enclose + the hotkey in square braces + as in Button_[1] which would assign the keyboard key 1 to that button. + The one will be included in the + button text. To hide they key, use double square braces as in: Ex[[qq]] + it , which would assign + the q key to the Exit button. Special keys such as may also be + used: Move [] for a full + list of special keys, see this reference: http://infohoglobal_state.nmt.edu/tcc/help/ + pubs/tkinter/web/key-names.html + :param text: + :return: list containing cleaned text, hotkey, and hotkey position within + cleaned text. + """ + + ret_val = [text, None, None] # Default return values + if text is None: + return ret_val + + # Single character, remain visible + res = re.search(r'(?<=\[).(?=\])', text) + if res: + start = res.start(0) + end = res.end(0) + caption = text[:start - 1] + text[start:end] + text[end + 1:] + ret_val = [caption, text[start:end], start - 1] + + # Single character, hide it + res = re.search(r'(?<=\[\[).(?=\]\])', text) + if res: + start = res.start(0) + end = res.end(0) + caption = text[:start - 2] + text[end + 2:] + ret_val = [caption, text[start:end], None] + + # a Keysym. Always hide it + res = re.search(r'(?<=\[\<).+(?=\>\])', text) + if res: + start = res.start(0) + end = res.end(0) + caption = text[:start - 2] + text[end + 2:] + ret_val = [caption, '<{}>'.format(text[start:end]), None] + + return ret_val + + +def load_tk_image(filename, tk_master=None): + """ + Load in an image file and return as a tk Image. + + Loads an image. If the PIL library is available use it. otherwise use the tk method. + + NOTE: tk_master is required if there are more than one Tk() instances, which there are very often. + REF: http://stackoverflow.com/a/23229091/2184122 + + :param filename: image filename to load + :param tk_master: root object (Tk()) + :return: tk Image object + """ + + if filename is None: + return None + + if not os.path.isfile(filename): + raise ValueError( + 'Image file {} does not exist.'.format(filename)) + + tk_image = None + + filename = os.path.normpath(filename) + _, ext = os.path.splitext(filename) + + try: + # Try to import the Python Image Library. If it doesn't exist, only .gif + from PIL import Image as PILImage + from PIL import ImageTk as PILImageTk + pil_image = PILImage.open(filename) + tk_image = PILImageTk.PhotoImage(pil_image, master=tk_master) + except: + try: + # Fallback if PIL isn't available + tk_image = tk.PhotoImage(file=filename, master=tk_master) + except: + msg = "Cannot load {}. Check to make sure it is an image file.".format( + filename) + try: + _ = PILImage + except: + msg += "\nPIL library isn't installed. If it isn't installed, only .gif files can be used." + raise ValueError(msg) + return tk_image + + +def get_num_lines(message_area): + num_lines, _ = message_area.index(tk.END).split('.') + return num_lines + + +def get_width_and_padding(monospace): + if monospace: + padding = DEFAULT_PADDING * FIXED_FONT_WIDTH + width_in_chars = FIXW_FONT_LINE_LENGTH + else: + padding = DEFAULT_PADDING * REGULAR_FONT_WIDTH + width_in_chars = PROP_FONT_LINE_LENGTH + return padding, width_in_chars + + +def bindArrows(widget): + widget.bind("", tabRight) + widget.bind("", tabLeft) + + widget.bind("", tabRight) + widget.bind("", tabLeft) + + +def tabRight(event): + boxRoot.event_generate("") + + +def tabLeft(event): + boxRoot.event_generate("") + + +class MouseClickHandler: + """ Handle mouse events with state to store whether the mouse is actually on a button or not. + This lets us ensure that: + * button presses are handled on mouse-release, *not* immediately on mouse-click + * callbacks are only called if there is a mouse-button release *and* your cursor is still on the button + """ + + def __init__(self, callback): + self._callback = callback + self._mouse_is_on_button = False + + def enter(self, _): + self._mouse_is_on_button = True + + def leave(self, _): + self._mouse_is_on_button = False + + def release(self, event): + if self._mouse_is_on_button: + return self._callback(event) \ No newline at end of file diff --git a/meta.yaml b/meta.yaml index a66d89a..ea3dc06 100644 --- a/meta.yaml +++ b/meta.yaml @@ -2,6 +2,9 @@ package: name: easygui version: "0.98.3.0" +build: + script: python setup.py install + source: path: . diff --git a/test/__init__.py b/requirements.txt similarity index 100% rename from test/__init__.py rename to requirements.txt diff --git a/setup.py b/setup.py index 8179e06..8724166 100644 --- a/setup.py +++ b/setup.py @@ -1,16 +1,13 @@ import distutils.core ## WARNING: Although the following import appears to do nothing, it is required for bdist_wheel to be recognized -from setuptools import setup, find_packages - -version = "0.98.3" -release = "0.98.3" +from easygui import version desc = list() desc.append('EasyGUI is a module for very simple, very easy GUI programming in Python. ') desc.append('EasyGUI is different from other GUI generators in that EasyGUI is NOT event-driven. ') desc.append('Instead, all GUI interactions are invoked by simple function calls.') -with open('README.md', "r", encoding='utf-8') as f: +with open('README.rst', "r", encoding='utf-8') as f: long_description = f.read() distutils.core.setup( diff --git a/sphinx/index.rst b/sphinx/index.rst index 8066196..641f9be 100644 --- a/sphinx/index.rst +++ b/sphinx/index.rst @@ -14,7 +14,3361 @@ EasyGUI runs on Python 2 and 3, and does not have any dependencies outside the P Example Usage ------------- - >>> import easygui +import easygui.button_box >>> import easygui + >>> easygui.button_box.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.button_box.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.button_box.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.button_box.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.button_box.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.button_box.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.button_box.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.button_box.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.button_box.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.button_box.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.button_box.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.button_box.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' +import easygui.button_box >>> easygui.button_box.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.button_box.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.button_box.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.button_box.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.button_box.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.button_box.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.button_box.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.button_box.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.button_box.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.button_box.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.button_box.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.button_box.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.button_box.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.button_box.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True +import easygui.button_box >>> easygui.button_box.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' + >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes + True + >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK + 'OK' + import easygui.button_box >>> easygui.button_box.buttonbox('Click on your favorite flavor.', 'Favorite Flavor', ('Chocolate', 'Vanilla', 'Strawberry')) # click Chocolate + 'Chocolate' >>> easygui.ynbox('Shall I continue?', 'Title', ('Yes', 'No')) # click Yes True >>> easygui.msgbox('This is a basic message box.', 'Title Goes Here') # click OK diff --git a/test/test_travis.py b/test/test_travis.py deleted file mode 100644 index 5a1aa34..0000000 --- a/test/test_travis.py +++ /dev/null @@ -1,6 +0,0 @@ -def test_always_passes(): - assert True - -# @staticmethod -# def test_always_fails(): -# assert False diff --git a/test_cases/__init__.py b/test_cases/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test_cases/file_open_box.py b/test_cases/file_open_box.py deleted file mode 100644 index 98282e2..0000000 --- a/test_cases/file_open_box.py +++ /dev/null @@ -1,11 +0,0 @@ -__author__ = 'Robert' -""" -from: -http://stackoverflow.com/questions/25087169/python-easygui-cant-select-file - -""" -import sys -sys.path.append('..') -import easygui -f = easygui.fileopenbox() -print(f) \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..bc38561 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +WAIT_0_MILLISECONDS = 0 +WAIT_1_MILLISECONDS = 1 \ No newline at end of file diff --git a/test_cases/test_easygui.py b/tests/test_easygui.py similarity index 70% rename from test_cases/test_easygui.py rename to tests/test_easygui.py index d5ce42f..4f85465 100644 --- a/test_cases/test_easygui.py +++ b/tests/test_easygui.py @@ -1,5 +1,3 @@ -import easygui - import inspect import os import time @@ -7,6 +5,8 @@ from pynput.keyboard import Key, Controller +import easygui.button_box + KEYBOARD = Controller() FOLDER_OF_THIS_FILE = os.path.dirname(os.path.abspath(__file__)) GUI_WAIT = 0.6 # if tests start failing, maybe try bumping this up a bit (though that'll slow the tests down) @@ -38,8 +38,8 @@ def run(self): def test_test_images_exist(): - assert os.path.exists(os.path.join(FOLDER_OF_THIS_FILE, 'pi.jpg')) - assert os.path.exists(os.path.join(FOLDER_OF_THIS_FILE, 'result.png')) + assert os.path.exists(os.path.join(FOLDER_OF_THIS_FILE, '../demos/pi.jpg')) + assert os.path.exists(os.path.join(FOLDER_OF_THIS_FILE, '../demos/result.png')) def test_spacebar_clicks_choice(): """ @@ -52,31 +52,32 @@ def test_spacebar_clicks_choice(): (('Message', 'Title'), {}, 'OK'), # custom message and title ((), dict(ok_button='Button'), 'Button'), # custom button text (('Message', 'Title'), dict(ok_button='Button'), 'Button'), # combo of all three - ((), dict(image=os.path.join(FOLDER_OF_THIS_FILE, 'pi.jpg')), 'OK'), # test jpg - ((), dict(image=os.path.join(FOLDER_OF_THIS_FILE, 'result.png')), 'OK'), # test png + ((), dict(image=os.path.join(FOLDER_OF_THIS_FILE, '../demos/pi.jpg')), 'OK'), # test jpg + ((), dict(image=os.path.join(FOLDER_OF_THIS_FILE, '../demos/result.png')), 'OK'), # test png ) for args, kwargs, expected in parameters: k = KeyPresses(' ') k.start() - assert easygui.msgbox(*args, **kwargs) == expected + actual = easygui.button_box.msgbox(*args, **kwargs) + assert actual == expected def test_buttonbox(): # Test hitting space to click OK with different default buttons: t = KeyPresses(' ') t.start() print('Line', inspect.currentframe().f_lineno) - assert easygui.buttonbox('Message', 'Title', choices=('Button[1]', 'Button[2]', 'Button[3]'), default_choice='Button[1]') == 'Button[1]' + assert easygui.button_box.buttonbox('Message', 'Title', choices=('Button[1]', 'Button[2]', 'Button[3]'), default_choice='Button[1]') == 'Button[1]' t = KeyPresses(' ') t.start() print('Line', inspect.currentframe().f_lineno) - assert easygui.buttonbox('Message', 'Title', choices=('Button[1]', 'Button[2]', 'Button[3]'), default_choice='Button[2]') == 'Button[2]' + assert easygui.button_box.buttonbox('Message', 'Title', choices=('Button[1]', 'Button[2]', 'Button[3]'), default_choice='Button[2]') == 'Button[2]' t = KeyPresses(' ') t.start() print('Line', inspect.currentframe().f_lineno) - assert easygui.buttonbox('Message', 'Title', choices=('Button[1]', 'Button[2]', 'Button[3]'), default_choice='Button[3]') == 'Button[3]' + assert easygui.button_box.buttonbox('Message', 'Title', choices=('Button[1]', 'Button[2]', 'Button[3]'), default_choice='Button[3]') == 'Button[3]' # Test hitting Esc to close. # TODO: If button boxes aren't given a default choice, then their window won't be in focus and this test hangs. @@ -89,4 +90,4 @@ def test_buttonbox(): t = KeyPresses([Key.esc]) t.start() print('Line', inspect.currentframe().f_lineno) - assert easygui.buttonbox(default_choice='Button[1]') is None + assert easygui.button_box.buttonbox(default_choice='Button[1]') is None diff --git a/tests/test_multi_fillable_box.py b/tests/test_multi_fillable_box.py new file mode 100644 index 0000000..9c72769 --- /dev/null +++ b/tests/test_multi_fillable_box.py @@ -0,0 +1,17 @@ +import easygui +import easygui.global_state +from easygui import multenterbox +from tests import WAIT_0_MILLISECONDS + + +def test__multenterbox__cancel_results_in_run_returning_none(): + meb = multenterbox(msg="test msg", title="test title", + fields=['f1', 'f2'], values=['v1', 'v2'], + callback=None, run=False) + + def simulate_user_cancel_press(meb_instance): + meb_instance._cancel_pressed('ignored button handler arg') + + meb.boxRoot.after(WAIT_0_MILLISECONDS, simulate_user_cancel_press, meb) + actual = meb.run() + assert actual is None diff --git a/tests/test_text_box.py b/tests/test_text_box.py new file mode 100644 index 0000000..a769447 --- /dev/null +++ b/tests/test_text_box.py @@ -0,0 +1,158 @@ +import unittest + +from mock import patch, Mock, ANY + +from easygui.text_box import TextBox, textbox +from tests import WAIT_0_MILLISECONDS, WAIT_1_MILLISECONDS +import tkinter as tk + +MODBASE = 'easygui.text_box' + +TEST_MESSAGE = 'example message' +TEST_TITLE = 'example title' +TEST_TEXT = 'example text' +TEST_CODEBOX = False +TEST_CALLBACK = Mock() +TEST_ARGS = [TEST_MESSAGE, TEST_TITLE, TEST_TEXT, TEST_CODEBOX, TEST_CALLBACK] + + +def test__textbox_method__instantiates_textbox_class_and_runs_it(): + """ Test that the textbox() method calls the underlying TextBox class in the expected way """ + with patch(MODBASE + '.TextBox') as mock_text_box_class: + mock_text_box_instance = Mock() + mock_text_box_instance.run = Mock(return_value='return text') + mock_text_box_class.return_value = mock_text_box_instance + + return_text = textbox(*TEST_ARGS, run=True) + + mock_text_box_class.assert_called_once_with( + msg=TEST_MESSAGE, + title=TEST_TITLE, + text=TEST_TEXT, + codebox=TEST_CODEBOX, + callback=TEST_CALLBACK + ) + mock_text_box_instance.run.assert_called_once_with() + assert return_text == 'return text' + + +class TestTextBox(unittest.TestCase): + def setUp(self): + self.tb = TextBox(*TEST_ARGS) + + def test_instantiation(self): + # Instance attributes should be configured: + self.assertEqual(self.tb.text, TEST_TEXT) + self.assertEqual(self.tb.msg, TEST_MESSAGE) + self.assertEqual(self.tb._user_specified_callback, TEST_CALLBACK) + + # The following Tk widgets should also have been created: + isinstance(self.tb.box_root, tk.Tk) + isinstance(self.tb.message_area, tk.Tk) + isinstance(self.tb.text_area, tk.Tk) + + # And configured: + self.assertEqual(self.tb.message_area.get(0.0, 'end-1c'), TEST_MESSAGE) + self.assertEqual(self.tb.text_area.get(0.0, 'end-1c'), TEST_TEXT) + + def test_run(self): + self.tb.box_root = Mock() + return_value = self.tb.run() + self.assertEqual(return_value, TEST_TEXT) + self.tb.box_root.mainloop.assert_called_once_with() + self.tb.box_root.destroy.assert_called_once_with() + + def test_stop(self): + self.tb.box_root = Mock() + self.tb.stop() + self.tb.box_root.quit.assert_called_once_with() + + def test_set_msg_area(self): + new_msg = 'some new text' + self.tb._set_msg_area(msg=new_msg) + self.assertEqual(self.tb.message_area.get(1.0, 'end-1c'), new_msg) + + def test_get_text(self): + actual = self.tb._get_text() + self.assertEqual(actual, TEST_TEXT) + + def test_set_text(self): + new_text = 'some new text' + self.tb.text = new_text + self.assertEqual(self.tb.text_area.get(1.0, 'end-1c'), new_text) + + +class TestTextBoxIntegration(unittest.TestCase): + + def test_textbox_x_results_in_run_returning_None(self): + tb = textbox(*TEST_ARGS, run=False) + + def simulate_user_x_press(tb_instance): + tb_instance.x_pressed('ignored button handler arg') + + tb.box_root.after(WAIT_0_MILLISECONDS, simulate_user_x_press, tb) + actual = tb.run() + self.assertEqual(actual, None) + + def test_textbox_cancel_button_pressed_results_in_run_returning_None(self): + tb = textbox(*TEST_ARGS, run=False) + + def simulate_cancel_button_pressed(tb_instance): + tb_instance.cancel_button_pressed('ignored button handler arg') + + tb.box_root.after(WAIT_0_MILLISECONDS, simulate_cancel_button_pressed, tb) + actual = tb.run() + + self.assertEqual(actual, None) + + def test_textbox_ok_pressed_calls_user_defined_callback(self): + tb = textbox(*TEST_ARGS, run=False) + + def simulate_ok_button_pressed(tb_instance): + tb_instance.ok_button_pressed('ignored button handler arg') + + def stop_running(tb_instance): + tb_instance.stop() + + tb.box_root.after(WAIT_0_MILLISECONDS, simulate_ok_button_pressed, tb) + tb.box_root.after(WAIT_1_MILLISECONDS, stop_running, tb) + actual = tb.run() + + TEST_CALLBACK.assert_called_once_with(ANY) + self.assertEqual(actual, TEST_TEXT) + + def test_textbox_ok_pressed_with_no_user_defined_callback(self): + tb = textbox( + msg=TEST_MESSAGE, + title=TEST_TITLE, + text=TEST_TEXT, + codebox=TEST_CODEBOX, + callback=None, + run=False + ) + + def simulate_ok_button_pressed(tb_instance): + tb_instance.ok_button_pressed('ignored button handler arg') + + tb.box_root.after(WAIT_0_MILLISECONDS, simulate_ok_button_pressed, tb) + actual = tb.run() + + # tb.stop() happens because no user _user_specified_callback is set + # the initial text value is unchanged, and is returned from run() + self.assertEqual(actual, TEST_TEXT) + + +class TestTextBoxCodeBox(unittest.TestCase): + + def test_instantiation_codebox(self): + tb = textbox( + msg=TEST_MESSAGE, + title=TEST_TITLE, + text=TEST_TEXT * 100, + codebox=True, + callback=None, + run=False + ) + + # cget returns strings so the monospace assertion is a bit messy: + self.assertEqual(tb.text_area.cget('font'), str(tb.MONOSPACE_FONT)) diff --git a/tests/tet_choice_box.py b/tests/tet_choice_box.py new file mode 100644 index 0000000..e084335 --- /dev/null +++ b/tests/tet_choice_box.py @@ -0,0 +1,57 @@ +import unittest + +from mock import patch, Mock + +from easygui.choice_box import choicebox, multchoicebox + +MODBASE = 'easygui.choice_box' + +TEST_MESSAGE = 'example message' +TEST_TITLE = 'example title' +TEST_CHOICES = ['choice 1', 'choice 2'] +TEST_PRESELECT = [] +TEST_CALLBACK = Mock() + +TEST_RETURN_TEXT = 'return text' + + +@patch(MODBASE + '.ChoiceBox') +class TestChoiceBoxUtilities(unittest.TestCase): + def configureMocks(self, mock_choicebox_class): + self.mock_choicebox_instance = Mock() + self.mock_choicebox_instance.run = Mock(return_value=TEST_RETURN_TEXT) + mock_choicebox_class.return_value = self.mock_choicebox_instance + + def test_choicebox_method_instantiates_choicebox_class_correctly_and_runs_it(self, mock_choicebox_class): + self.configureMocks(mock_choicebox_class) + + return_text = choicebox(msg=TEST_MESSAGE, title=TEST_TITLE, choices=TEST_CHOICES, preselect=TEST_PRESELECT, + callback=TEST_CALLBACK, run=True) + + self.assertEqual(return_text, TEST_RETURN_TEXT) + mock_choicebox_class.assert_called_once_with( + TEST_MESSAGE, + TEST_TITLE, + TEST_CHOICES, + preselect=TEST_PRESELECT, + multiple_select=False, # this is the only difference between choicebox() and multchoicebox() + callback=TEST_CALLBACK + ) + self.mock_choicebox_instance.run.assert_called_once_with() + + def test_multchoicebox_method_instantiates_choicebox_class_correctly_and_runs_it(self, mock_choicebox_class): + self.configureMocks(mock_choicebox_class) + + return_text = multchoicebox(msg=TEST_MESSAGE, title=TEST_TITLE, choices=TEST_CHOICES, preselect=TEST_PRESELECT, + callback=TEST_CALLBACK, run=True) + + self.assertEqual(return_text, TEST_RETURN_TEXT) + mock_choicebox_class.assert_called_once_with( + TEST_MESSAGE, + TEST_TITLE, + TEST_CHOICES, + preselect=TEST_PRESELECT, + multiple_select=True, # this is the only difference between choicebox() and multchoicebox() + callback=TEST_CALLBACK + ) + self.mock_choicebox_instance.run.assert_called_once_with() From f361aba1cce6892d02df6b682415d5cfd8c3edd2 Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Thu, 28 Apr 2022 21:34:53 +0100 Subject: [PATCH 02/29] remove BindArrows which don't seem to work (just generate errors when used) --- easygui/__init__.py | 1 - easygui/choice_box.py | 6 +----- easygui/fillable_box.py | 3 +-- easygui/global_state.py | 2 -- easygui/utilities.py | 18 +----------------- 5 files changed, 3 insertions(+), 27 deletions(-) diff --git a/easygui/__init__.py b/easygui/__init__.py index 811f758..403dee4 100644 --- a/easygui/__init__.py +++ b/easygui/__init__.py @@ -29,7 +29,6 @@ from .fillable_box import fillablebox, enterbox, passwordbox, integerbox from .multi_fillable_box import multenterbox, multpasswordbox from .text_box import textbox, codebox, exceptionbox -import tkinter as tk # python 3 import os diff --git a/easygui/choice_box.py b/easygui/choice_box.py index 6d2a14c..c1b3a4e 100644 --- a/easygui/choice_box.py +++ b/easygui/choice_box.py @@ -1,7 +1,7 @@ import string import tkinter as tk -from easygui.utilities import get_num_lines, get_width_and_padding, bindArrows, MouseClickHandler +from easygui.utilities import get_num_lines, get_width_and_padding, MouseClickHandler def choicebox(msg="Pick an item", title="", choices=None, preselect=[], callback=None, run=True): @@ -218,7 +218,6 @@ def create_ok_button(self): # put the buttons in the self.buttonsFrame okButton = tk.Button(self.buttonsFrame, takefocus=tk.YES, text="OK", height=1, width=6) - bindArrows(okButton) okButton.pack(expand=tk.NO, side=tk.RIGHT, padx='2m', pady='1m', ipady="1m", ipadx="2m") @@ -234,7 +233,6 @@ def create_ok_button(self): def create_cancel_button(self): cancelButton = tk.Button(self.buttonsFrame, takefocus=tk.YES, text="Cancel", height=1, width=6) - bindArrows(cancelButton) cancelButton.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", ipadx="2m") cancelButton.bind("", self.cancel_button_pressed) @@ -262,9 +260,7 @@ def create_special_buttons(self): ipady="1m", ipadx="2m") selectAllButton.bind("", self.choiceboxSelectAll) - bindArrows(selectAllButton) clearAllButton.bind("", self.choiceboxClearAll) - bindArrows(clearAllButton) def KeyboardListener(self, event): key = event.keysym diff --git a/easygui/fillable_box.py b/easygui/fillable_box.py index 514702c..3628ae1 100644 --- a/easygui/fillable_box.py +++ b/easygui/fillable_box.py @@ -3,7 +3,7 @@ from easygui import msgbox from easygui.global_state import GLOBAL_WINDOW_POSITION, PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE, \ TEXT_ENTRY_FONT_SIZE -from easygui.utilities import load_tk_image, bindArrows, MouseClickHandler +from easygui.utilities import load_tk_image, MouseClickHandler def integerbox(msg=None, title=" ", default=None, lowerbound=0, upperbound=99, image=None, root=None): @@ -170,7 +170,6 @@ def __init__(self, msg, default, title, mask=None, image=None, root=None): self.entry_widget = entry_widget # save a reference - we need to get text from this widget later ok_button = tk.Button(buttons_frame, takefocus=1, text="OK") - bindArrows(ok_button) ok_button.pack(expand=1, side=tk.LEFT, padx='3m', pady='3m', ipadx='2m', ipady='1m') ok_button.bind("", self._ok_pressed) ok_click_handler = MouseClickHandler(callback=self._ok_pressed) diff --git a/easygui/global_state.py b/easygui/global_state.py index d208bbc..d26ef4f 100644 --- a/easygui/global_state.py +++ b/easygui/global_state.py @@ -10,5 +10,3 @@ DEFAULT_PADDING = 2 REGULAR_FONT_WIDTH = 13 FIXED_FONT_WIDTH = 7 -boxRoot = None - diff --git a/easygui/utilities.py b/easygui/utilities.py index a2cc90d..345d01c 100644 --- a/easygui/utilities.py +++ b/easygui/utilities.py @@ -3,7 +3,7 @@ import tkinter as tk from easygui.global_state import PROP_FONT_LINE_LENGTH, FIXW_FONT_LINE_LENGTH, DEFAULT_PADDING, REGULAR_FONT_WIDTH, \ - FIXED_FONT_WIDTH, boxRoot + FIXED_FONT_WIDTH def parse_hotkey(text): @@ -116,22 +116,6 @@ def get_width_and_padding(monospace): return padding, width_in_chars -def bindArrows(widget): - widget.bind("", tabRight) - widget.bind("", tabLeft) - - widget.bind("", tabRight) - widget.bind("", tabLeft) - - -def tabRight(event): - boxRoot.event_generate("") - - -def tabLeft(event): - boxRoot.event_generate("") - - class MouseClickHandler: """ Handle mouse events with state to store whether the mouse is actually on a button or not. This lets us ensure that: From d7f1ce62f2e98689280648ceefdb4e8d0bd56e5d Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Thu, 28 Apr 2022 21:47:06 +0100 Subject: [PATCH 03/29] rename test requirements file so it sits next to regular requirements file in the package --- .travis.yml | 2 +- test-requirements.txt => requirements_for_tests.txt | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename test-requirements.txt => requirements_for_tests.txt (100%) diff --git a/.travis.yml b/.travis.yml index 15cf31a..f2d164a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,4 +14,4 @@ services: # command to run tests script: pytest -install: pip install -r test-requirements.txt \ No newline at end of file +install: pip install -r requirements_for_tests.txt \ No newline at end of file diff --git a/test-requirements.txt b/requirements_for_tests.txt similarity index 100% rename from test-requirements.txt rename to requirements_for_tests.txt From 7fc779bc69d42be3f31bf80bc69ea7067e452032 Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Thu, 28 Apr 2022 21:56:19 +0100 Subject: [PATCH 04/29] inline get_num_lines() which basically just called a tk internal method --- easygui/__init__.py | 6 +++--- easygui/button_box.py | 4 ++-- easygui/choice_box.py | 6 +++--- easygui/text_box.py | 6 +++--- easygui/utilities.py | 7 +------ 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/easygui/__init__.py b/easygui/__init__.py index 403dee4..cc16aa2 100644 --- a/easygui/__init__.py +++ b/easygui/__init__.py @@ -22,6 +22,8 @@ 'EgStore', 'version' ] +import os + from .button_box import buttonbox, msgbox, boolbox, ynbox, ccbox, indexbox from .choice_box import choicebox, multchoicebox from .egstore import EgStore @@ -30,7 +32,5 @@ from .multi_fillable_box import multenterbox, multpasswordbox from .text_box import textbox, codebox, exceptionbox -import os - ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -version = "0.98.3" \ No newline at end of file +version = "0.98.3" diff --git a/easygui/button_box.py b/easygui/button_box.py index d69b1cf..58327c9 100644 --- a/easygui/button_box.py +++ b/easygui/button_box.py @@ -233,8 +233,8 @@ def _configure_message_area(box_root): def _set_msg_area(self, msg): self.message_area.delete(1.0, tk.END) self.message_area.insert(tk.END, msg) - num_lines, _ = self.message_area.index(tk.END).split('.') - self.message_area.configure(height=int(num_lines)) + line, char = self.message_area.index(tk.END).split('.') + self.message_area.configure(height=int(line)) self.message_area.update() @staticmethod diff --git a/easygui/choice_box.py b/easygui/choice_box.py index c1b3a4e..9ff9957 100644 --- a/easygui/choice_box.py +++ b/easygui/choice_box.py @@ -1,7 +1,7 @@ import string import tkinter as tk -from easygui.utilities import get_num_lines, get_width_and_padding, MouseClickHandler +from easygui.utilities import get_width_and_padding, MouseClickHandler def choicebox(msg="Pick an item", title="", choices=None, preselect=[], callback=None, run=True): @@ -109,8 +109,8 @@ def _set_msg_area(self, msg): self.message_area.config(state=tk.NORMAL) # necessary but I don't know why self.message_area.delete(1.0, tk.END) self.message_area.insert(tk.END, msg) - numlines = get_num_lines(self.message_area) - self.message_area.configure(height=numlines) + line, char = self.message_area.index(tk.END).split('.') + self.message_area.configure(height=int(line)) self.message_area.update() def preselect_choice(self, preselect): diff --git a/easygui/text_box.py b/easygui/text_box.py index 1e75423..2af712f 100644 --- a/easygui/text_box.py +++ b/easygui/text_box.py @@ -5,7 +5,7 @@ from tkinter import font from easygui.global_state import GLOBAL_WINDOW_POSITION -from easygui.utilities import get_num_lines, get_width_and_padding, MouseClickHandler +from easygui.utilities import get_width_and_padding, MouseClickHandler def textbox(msg='', title='', text='', codebox=False, callback=None, run=True): @@ -163,8 +163,8 @@ def _configure_text_area(self, box_root, code_box): def _set_msg_area(self, msg): self.message_area.delete(1.0, tk.END) self.message_area.insert(tk.END, msg) - num_lines = get_num_lines(message_area=self.message_area) - self.message_area.configure(height=int(num_lines)) + line, char = self.message_area.index(tk.END).split('.') + self.message_area.configure(height=int(line)) self.message_area.update() def run(self): diff --git a/easygui/utilities.py b/easygui/utilities.py index 345d01c..f421548 100644 --- a/easygui/utilities.py +++ b/easygui/utilities.py @@ -101,11 +101,6 @@ def load_tk_image(filename, tk_master=None): return tk_image -def get_num_lines(message_area): - num_lines, _ = message_area.index(tk.END).split('.') - return num_lines - - def get_width_and_padding(monospace): if monospace: padding = DEFAULT_PADDING * FIXED_FONT_WIDTH @@ -135,4 +130,4 @@ def leave(self, _): def release(self, event): if self._mouse_is_on_button: - return self._callback(event) \ No newline at end of file + return self._callback(event) From f5f623836e9e72e1e9b2ee131e7b0eab4663c41a Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Thu, 28 Apr 2022 22:19:29 +0100 Subject: [PATCH 05/29] refactor FillableBox: title goes before default (like all the other boxes) --- easygui/fillable_box.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/easygui/fillable_box.py b/easygui/fillable_box.py index 3628ae1..9fdc41c 100644 --- a/easygui/fillable_box.py +++ b/easygui/fillable_box.py @@ -37,7 +37,7 @@ def integerbox(msg=None, title=" ", default=None, lowerbound=0, upperbound=99, i msg = "Enter an integer between {0} and {1}".format(lowerbound, upperbound) if msg is None else msg while True: - result = FillableBox(msg, title, default, image=image, root=root).run() + result = FillableBox(msg, default, title, image=image, root=root).run() if result is None: return None @@ -79,7 +79,7 @@ def enterbox(msg="Enter something.", title=" ", default="", strip=True, image=No :return: the text that the user entered, or None if they cancel the operation. """ - result = FillableBox(msg, title, default, image=image, root=root).run() + result = FillableBox(msg, default, title, image=image, root=root).run() if result and strip: result = result.strip() return result @@ -96,7 +96,7 @@ def passwordbox(msg="Enter your password.", title="", default="", image=None, ro :return: the text that the user entered, or None if they cancel the operation. """ - return FillableBox(msg, title, default, mask="*", image=image, root=root).run() + return FillableBox(msg, default, title, mask="*", image=image, root=root).run() def fillablebox(msg, title="", default=None, mask=None, image=None, root=None): @@ -107,11 +107,11 @@ def fillablebox(msg, title="", default=None, mask=None, image=None, root=None): :param str default: default value populated, returned if user does not change it :return: the text that the user entered, or None if he cancels the operation. """ - return FillableBox(msg, title, default, mask, image, root).run() + return FillableBox(msg, default, title, mask, image, root).run() class FillableBox(object): - def __init__(self, msg, default, title, mask=None, image=None, root=None): + def __init__(self, msg, title, default, mask=None, image=None, root=None): self.return_value = '' if default is None else default self.pre_existing_root = root self.box_root = None From 5c261ee80baec7577e41084a61adb1795b2dd5cd Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Thu, 28 Apr 2022 22:50:05 +0100 Subject: [PATCH 06/29] Use an AbstractBox to DRY out the five custom box classes --- easygui/button_box.py | 24 +++---------------- easygui/choice_box.py | 24 +++---------------- easygui/fillable_box.py | 24 ++++++------------- easygui/multi_fillable_box.py | 29 ++++++++++------------- easygui/text_box.py | 27 ++++----------------- easygui/utilities.py | 40 +++++++++++++++++++++++++++++++- tests/test_multi_fillable_box.py | 2 +- 7 files changed, 69 insertions(+), 101 deletions(-) diff --git a/easygui/button_box.py b/easygui/button_box.py index 58327c9..dc94ded 100644 --- a/easygui/button_box.py +++ b/easygui/button_box.py @@ -1,6 +1,6 @@ import tkinter as tk -from easygui.utilities import load_tk_image, get_width_and_padding, parse_hotkey +from easygui.utilities import load_tk_image, get_width_and_padding, parse_hotkey, AbstractBox def buttonbox(msg="buttonbox options", title=" ", choices=("Button[1]", "Button[2]", "Button[3]"), @@ -169,7 +169,7 @@ def msgbox(msg="(Your message goes here)", title="", ok_button="OK"): return buttonbox(msg, title, choices=[ok_button], image=image, default_choice=ok_button, cancel_choice=ok_button) -class ButtonBox(object): +class ButtonBox(AbstractBox): """ Display various types of button boxes This object separates user from ui, defines which methods can @@ -198,29 +198,18 @@ def __init__(self, msg, title, choices, images, default_choice, cancel_choice, c """ + super().__init__(msg, title) self._user_specified_callback = callback self._text_to_return_on_cancel = cancel_choice - self.choice_text = None - self._images = [] self._buttons = [] - self.box_root = self._configure_box_root(title) self.message_area = self._configure_message_area(box_root=self.box_root) self._set_msg_area('' if msg is None else msg) self.images_frame = self._create_images_frame(images) self.buttons_frame = self._create_buttons_frame(choices, default_choice) - def _configure_box_root(self, title): - box_root = tk.Tk() - box_root.title(title) - box_root.iconname('Dialog') - box_root.geometry('600x400+100+100') - box_root.protocol('WM_DELETE_WINDOW', self._x_pressed) # Quit when x button pressed - box_root.bind("", self._cancel_button_pressed) - return box_root - @staticmethod def _configure_message_area(box_root): padding, width_in_chars = get_width_and_padding(monospace=False) @@ -230,13 +219,6 @@ def _configure_message_area(box_root): message_area.grid() return message_area - def _set_msg_area(self, msg): - self.message_area.delete(1.0, tk.END) - self.message_area.insert(tk.END, msg) - line, char = self.message_area.index(tk.END).split('.') - self.message_area.configure(height=int(line)) - self.message_area.update() - @staticmethod def _convert_to_a_list_of_lists(filenames): """ return a list of lists, handling all of the different allowed types of 'filenames' input """ diff --git a/easygui/choice_box.py b/easygui/choice_box.py index 9ff9957..2360130 100644 --- a/easygui/choice_box.py +++ b/easygui/choice_box.py @@ -1,7 +1,7 @@ import string import tkinter as tk -from easygui.utilities import get_width_and_padding, MouseClickHandler +from easygui.utilities import get_width_and_padding, MouseClickHandler, AbstractBox def choicebox(msg="Pick an item", title="", choices=None, preselect=[], callback=None, run=True): @@ -54,10 +54,10 @@ def multchoicebox(msg="Pick an item", title="", choices=None, preselect=[], call return mcb -class ChoiceBox(object): +class ChoiceBox(AbstractBox): def __init__(self, msg, title, choices, preselect, multiple_select, callback): - + super().__init__(msg, title) if not multiple_select and len(preselect)>1: raise ValueError("Multiple selections not allowed, yet preselect has multiple values:{}".format(preselect)) @@ -68,7 +68,6 @@ def __init__(self, msg, title, choices, preselect, multiple_select, callback): choices = ('Choice 1', 'Choice 2') self.choices = [str(c) for c in choices] - self.box_root = self._configure_box_root(title) self.message_area = self._configure_message_area(self.box_root) self._set_msg_area("" if msg is None else msg) @@ -105,14 +104,6 @@ def ok_button_pressed(self, event): else: self.stop() - def _set_msg_area(self, msg): - self.message_area.config(state=tk.NORMAL) # necessary but I don't know why - self.message_area.delete(1.0, tk.END) - self.message_area.insert(tk.END, msg) - line, char = self.message_area.index(tk.END).split('.') - self.message_area.configure(height=int(line)) - self.message_area.update() - def preselect_choice(self, preselect): if preselect != None: for v in preselect: @@ -131,15 +122,6 @@ def get_choices(self): return selected_choices - def _configure_box_root(self, title): - box_root = tk.Tk() - box_root.title(title) - box_root.iconname('Dialog') - box_root.protocol('WM_DELETE_WINDOW', self.x_pressed) - box_root.bind('', self.KeyboardListener) - box_root.bind("", self.cancel_button_pressed) - return box_root - @staticmethod def _configure_message_area(box_root): padding, width_in_chars = get_width_and_padding(monospace=False) diff --git a/easygui/fillable_box.py b/easygui/fillable_box.py index 9fdc41c..086c531 100644 --- a/easygui/fillable_box.py +++ b/easygui/fillable_box.py @@ -1,9 +1,9 @@ import tkinter as tk from easygui import msgbox -from easygui.global_state import GLOBAL_WINDOW_POSITION, PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE, \ +from easygui.global_state import PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE, \ TEXT_ENTRY_FONT_SIZE -from easygui.utilities import load_tk_image, MouseClickHandler +from easygui.utilities import load_tk_image, MouseClickHandler, AbstractBox def integerbox(msg=None, title=" ", default=None, lowerbound=0, upperbound=99, image=None, root=None): @@ -110,26 +110,16 @@ def fillablebox(msg, title="", default=None, mask=None, image=None, root=None): return FillableBox(msg, default, title, mask, image, root).run() -class FillableBox(object): +class FillableBox(AbstractBox): def __init__(self, msg, title, default, mask=None, image=None, root=None): - self.return_value = '' if default is None else default - self.pre_existing_root = root - self.box_root = None - self.entry_widget = None - if root: root.withdraw() self.box_root = tk.Toplevel(master=root) self.box_root.withdraw() - else: - self.box_root = tk.Tk() - self.box_root.withdraw() - - self.box_root.protocol('WM_DELETE_WINDOW', self._cancel_pressed) - self.box_root.title(title) - self.box_root.iconname('Dialog') - self.box_root.geometry(GLOBAL_WINDOW_POSITION) - self.box_root.bind("", self._cancel_pressed) + super().__init__(msg, title) + self.return_value = '' if default is None else default + self.pre_existing_root = root + self.entry_widget = None message_frame = tk.Frame(master=self.box_root) message_frame.pack(side=tk.TOP, fill=tk.BOTH) diff --git a/easygui/multi_fillable_box.py b/easygui/multi_fillable_box.py index d4fca16..ce1969c 100644 --- a/easygui/multi_fillable_box.py +++ b/easygui/multi_fillable_box.py @@ -1,8 +1,8 @@ import tkinter as tk -from easygui.global_state import GLOBAL_WINDOW_POSITION, PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE, \ +from easygui.global_state import PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE, \ TEXT_ENTRY_FONT_SIZE -from easygui.utilities import MouseClickHandler +from easygui.utilities import MouseClickHandler, AbstractBox def multpasswordbox(msg="Fill in values for the fields.", title=" ", fields=None, values=None, callback=None, run=True): @@ -34,25 +34,20 @@ def multenterbox(msg="Fill in values for the fields.", title=" ", fields=None, v return mb.run() if run else mb -class MultiBox(object): +class MultiBox(AbstractBox): def __init__(self, msg, title, fields=None, values=None, mask_last=False, callback=None): + super().__init__(msg, title) + self.fields, self.values = self._process_fields_and_values(fields, values) self.user_defined_callback = callback - self.boxRoot = tk.Tk() - self.boxRoot.protocol('WM_DELETE_WINDOW', self._cancel_pressed) - self.boxRoot.title(title) - self.boxRoot.iconname('Dialog') - self.boxRoot.bind("", self._cancel_pressed) - self.boxRoot.geometry(GLOBAL_WINDOW_POSITION) - - message_widget = tk.Message(self.boxRoot, width="4.5i", text=msg) + message_widget = tk.Message(self.box_root, width="4.5i", text=msg) message_widget.configure(font=(PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE)) message_widget.pack(side=tk.TOP, expand=1, fill=tk.BOTH, padx='3m', pady='3m') self.entry_widgets = [] for field, value in zip(self.fields, self.values): - entry_frame = tk.Frame(master=self.boxRoot) + entry_frame = tk.Frame(master=self.box_root) entry_frame.pack(side=tk.TOP, fill=tk.BOTH) label_widget = tk.Label(entry_frame, text=field) @@ -69,7 +64,7 @@ def __init__(self, msg, title, fields=None, values=None, mask_last=False, callba if mask_last: self.entry_widgets[-1].configure(show="*") - buttons_frame = tk.Frame(master=self.boxRoot) + buttons_frame = tk.Frame(master=self.box_root) buttons_frame.pack(side=tk.BOTTOM) cancel_button = tk.Button(buttons_frame, takefocus=1, text="Cancel") @@ -91,19 +86,19 @@ def __init__(self, msg, title, fields=None, values=None, mask_last=False, callba self.entry_widgets[0].focus_force() # put the focus on the entry_widget def run(self): - self.boxRoot.mainloop() # run it! - self.boxRoot.destroy() # Close the window + self.box_root.mainloop() # run it! + self.box_root.destroy() # Close the window return self.values def _cancel_pressed(self, *args): self.values = None - self.boxRoot.quit() + self.box_root.quit() def _ok_pressed(self, _): self.values = self._get_values() if self.user_defined_callback: self.user_defined_callback(self) - self.boxRoot.quit() + self.box_root.quit() def _get_values(self): return [widget.get() for widget in self.entry_widgets] diff --git a/easygui/text_box.py b/easygui/text_box.py index 2af712f..64b8f96 100644 --- a/easygui/text_box.py +++ b/easygui/text_box.py @@ -1,11 +1,9 @@ import sys -import traceback - import tkinter as tk +import traceback from tkinter import font -from easygui.global_state import GLOBAL_WINDOW_POSITION -from easygui.utilities import get_width_and_padding, MouseClickHandler +from easygui.utilities import get_width_and_padding, MouseClickHandler, AbstractBox def textbox(msg='', title='', text='', codebox=False, callback=None, run=True): @@ -75,7 +73,7 @@ def format_exception_for_display(): codebox(msg, title, format_exception_for_display()) -class TextBox(object): +class TextBox(AbstractBox): """ Display a message, and an editable text field pre-populated with 'text' """ def __init__(self, msg, title, text, codebox, callback): @@ -86,11 +84,10 @@ def __init__(self, msg, title, text, codebox, callback): :param codebox: bool (if true) don't wrap, set width to 80 chars, use monospace font :param callback: optional function to be called when OK is pressed """ + super().__init__(msg, title) self._user_specified_callback = callback self._text = text - self.msg = msg - self.box_root = self._configure_box_root(title) self.message_area = self._configure_message_area(box_root=self.box_root, code_box=codebox) self._set_msg_area("" if msg is None else msg) @@ -99,16 +96,6 @@ def __init__(self, msg, title, text, codebox, callback): self._set_text() self._configure_buttons() - - def _configure_box_root(self, title): - box_root = tk.Tk() - box_root.title(title) - box_root.iconname('Dialog') - box_root.geometry(GLOBAL_WINDOW_POSITION) - box_root.protocol('WM_DELETE_WINDOW', self.x_pressed) # Quit when x button pressed - box_root.bind("", self.cancel_button_pressed) - return box_root - @staticmethod def _configure_message_area(box_root, code_box): padding, width_in_chars = get_width_and_padding(code_box) @@ -160,12 +147,6 @@ def _configure_text_area(self, box_root, code_box): return text_area - def _set_msg_area(self, msg): - self.message_area.delete(1.0, tk.END) - self.message_area.insert(tk.END, msg) - line, char = self.message_area.index(tk.END).split('.') - self.message_area.configure(height=int(line)) - self.message_area.update() def run(self): self.box_root.mainloop() diff --git a/easygui/utilities.py b/easygui/utilities.py index f421548..300977a 100644 --- a/easygui/utilities.py +++ b/easygui/utilities.py @@ -3,9 +3,47 @@ import tkinter as tk from easygui.global_state import PROP_FONT_LINE_LENGTH, FIXW_FONT_LINE_LENGTH, DEFAULT_PADDING, REGULAR_FONT_WIDTH, \ - FIXED_FONT_WIDTH + FIXED_FONT_WIDTH, GLOBAL_WINDOW_POSITION +class AbstractBox(object): + """ + The following boxes have commonalities, so we can abstract some code here to a parent class + ButtonBox: def __init__(self, msg, title, choices, images, default_choice, cancel_choice, callback): + ChoiceBox: def __init__(self, msg, title, choices, preselect, multiple_select, callback): + FillableBox: def __init__(self, msg, title, default, mask=None, image=None, root=None): + MultiFillableBox: def __init__(self, msg, title, fields=None, values=None, mask_last=False, callback=None): + TextBox def __init__(self, msg, title, text, codebox, callback): + """ + + def __init__(self, msg, title) -> None: + super().__init__() + self.msg = msg + self.box_root = self._configure_box_root(title) + self.message_area = NotImplemented + + def _configure_box_root(self, title): + box_root = tk.Tk() + box_root.title(title) + box_root.iconname('Dialog') + box_root.geometry(GLOBAL_WINDOW_POSITION) + box_root.protocol('WM_DELETE_WINDOW', self.x_pressed) # Quit when x button pressed + box_root.bind("", self.cancel_button_pressed) + return box_root + + def _set_msg_area(self, msg): + self.message_area.delete(1.0, tk.END) + self.message_area.insert(tk.END, msg) + line, char = self.message_area.index(tk.END).split('.') + self.message_area.configure(height=int(line)) + self.message_area.update() + + def x_pressed(self, _): + raise NotImplemented + + def cancel_button_pressed(self, _): + raise NotImplemented + def parse_hotkey(text): """ Extract a desired hotkey from the text. The format to enclose diff --git a/tests/test_multi_fillable_box.py b/tests/test_multi_fillable_box.py index 9c72769..11a5fa3 100644 --- a/tests/test_multi_fillable_box.py +++ b/tests/test_multi_fillable_box.py @@ -12,6 +12,6 @@ def test__multenterbox__cancel_results_in_run_returning_none(): def simulate_user_cancel_press(meb_instance): meb_instance._cancel_pressed('ignored button handler arg') - meb.boxRoot.after(WAIT_0_MILLISECONDS, simulate_user_cancel_press, meb) + meb.box_root.after(WAIT_0_MILLISECONDS, simulate_user_cancel_press, meb) actual = meb.run() assert actual is None From efb2b7693aac0853671fd4ebab288c36dba073b6 Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Thu, 28 Apr 2022 22:57:16 +0100 Subject: [PATCH 07/29] Bugfix? x_pressed for choice_box.py had wrong signature --- easygui/choice_box.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easygui/choice_box.py b/easygui/choice_box.py index 2360130..1ccc6d1 100644 --- a/easygui/choice_box.py +++ b/easygui/choice_box.py @@ -88,7 +88,7 @@ def run(self): def stop(self): self.box_root.quit() - def x_pressed(self): + def x_pressed(self, _): self.stop() self.choices = None From 85fedbcd285b24e5ee2b72cfd38023c2f04e17fb Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Thu, 28 Apr 2022 23:08:06 +0100 Subject: [PATCH 08/29] fixes to button box so it works with the AbstractBox parent --- easygui/button_box.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/easygui/button_box.py b/easygui/button_box.py index dc94ded..db85813 100644 --- a/easygui/button_box.py +++ b/easygui/button_box.py @@ -313,11 +313,11 @@ def stop(self): self.box_root.quit() # Methods executing when a key is pressed - def _x_pressed(self): + def x_pressed(self, _): self._callback(command='x') self.choice_text = self._text_to_return_on_cancel - def _cancel_button_pressed(self, _): + def cancel_button_pressed(self, _): self._callback(command='cancel') self.choice_text = self._text_to_return_on_cancel From 73a38077b2449f474c934f412330e133c8d6c3af Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Thu, 28 Apr 2022 23:08:55 +0100 Subject: [PATCH 09/29] add __name__ == main driver to text_box.py --- easygui/text_box.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/easygui/text_box.py b/easygui/text_box.py index 64b8f96..54ccd00 100644 --- a/easygui/text_box.py +++ b/easygui/text_box.py @@ -217,3 +217,8 @@ def callback(self, command): elif command in ('x', 'cancel'): self.stop() self.text = None + + +if __name__ == '__main__': + result = textbox("test message here") + print("textbox() return value was: {}".format(result)) \ No newline at end of file From 383c338267b644d35e169c9dbed85abe6f1236bd Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Thu, 28 Apr 2022 23:21:15 +0100 Subject: [PATCH 10/29] removed 'x_pressed' callback which didn't seem to work at all --- easygui/button_box.py | 4 ---- easygui/choice_box.py | 4 ---- easygui/text_box.py | 3 --- easygui/utilities.py | 4 ---- tests/test_text_box.py | 10 ---------- 5 files changed, 25 deletions(-) diff --git a/easygui/button_box.py b/easygui/button_box.py index db85813..6c510e2 100644 --- a/easygui/button_box.py +++ b/easygui/button_box.py @@ -313,10 +313,6 @@ def stop(self): self.box_root.quit() # Methods executing when a key is pressed - def x_pressed(self, _): - self._callback(command='x') - self.choice_text = self._text_to_return_on_cancel - def cancel_button_pressed(self, _): self._callback(command='cancel') self.choice_text = self._text_to_return_on_cancel diff --git a/easygui/choice_box.py b/easygui/choice_box.py index 1ccc6d1..eab92db 100644 --- a/easygui/choice_box.py +++ b/easygui/choice_box.py @@ -88,10 +88,6 @@ def run(self): def stop(self): self.box_root.quit() - def x_pressed(self, _): - self.stop() - self.choices = None - def cancel_button_pressed(self, event): self.stop() self.choices = None diff --git a/easygui/text_box.py b/easygui/text_box.py index 54ccd00..4b5fe5c 100644 --- a/easygui/text_box.py +++ b/easygui/text_box.py @@ -197,9 +197,6 @@ def _set_text(self): self.text_area.focus() # Methods executing when a key is pressed - def x_pressed(self, _): - self.callback(command='x') - def cancel_button_pressed(self, _): self.callback(command='cancel') diff --git a/easygui/utilities.py b/easygui/utilities.py index 300977a..74397fe 100644 --- a/easygui/utilities.py +++ b/easygui/utilities.py @@ -27,7 +27,6 @@ def _configure_box_root(self, title): box_root.title(title) box_root.iconname('Dialog') box_root.geometry(GLOBAL_WINDOW_POSITION) - box_root.protocol('WM_DELETE_WINDOW', self.x_pressed) # Quit when x button pressed box_root.bind("", self.cancel_button_pressed) return box_root @@ -38,9 +37,6 @@ def _set_msg_area(self, msg): self.message_area.configure(height=int(line)) self.message_area.update() - def x_pressed(self, _): - raise NotImplemented - def cancel_button_pressed(self, _): raise NotImplemented diff --git a/tests/test_text_box.py b/tests/test_text_box.py index a769447..b418918 100644 --- a/tests/test_text_box.py +++ b/tests/test_text_box.py @@ -84,16 +84,6 @@ def test_set_text(self): class TestTextBoxIntegration(unittest.TestCase): - def test_textbox_x_results_in_run_returning_None(self): - tb = textbox(*TEST_ARGS, run=False) - - def simulate_user_x_press(tb_instance): - tb_instance.x_pressed('ignored button handler arg') - - tb.box_root.after(WAIT_0_MILLISECONDS, simulate_user_x_press, tb) - actual = tb.run() - self.assertEqual(actual, None) - def test_textbox_cancel_button_pressed_results_in_run_returning_None(self): tb = textbox(*TEST_ARGS, run=False) From 132b51d2dd122deb5395249347862c210b9b7a0f Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Thu, 28 Apr 2022 23:45:28 +0100 Subject: [PATCH 11/29] refactor all the boxes to return a self.return_value which can then be consistently put in the AbstractBox class --- demos/demo_multi_fillable_box.py | 4 ++-- easygui/button_box.py | 16 ++++------------ easygui/choice_box.py | 32 ++++++++++++-------------------- easygui/multi_fillable_box.py | 13 ++++--------- easygui/text_box.py | 16 ++++++---------- easygui/utilities.py | 10 ++++++++++ 6 files changed, 38 insertions(+), 53 deletions(-) diff --git a/demos/demo_multi_fillable_box.py b/demos/demo_multi_fillable_box.py index a91e157..962f4e9 100644 --- a/demos/demo_multi_fillable_box.py +++ b/demos/demo_multi_fillable_box.py @@ -44,12 +44,12 @@ def __init__(self): def check_for_blank_fields(self, box): # make sure that none of the fields was left blank - cancelled = box.values is None + cancelled = box.return_value is None errors = [] if cancelled: pass else: # check for errors - for name, value in zip(box.fields, box.values): + for name, value in zip(box.fields, box.return_value): if value.strip() == "": errors.append('"{}" is a required field.'.format(name)) diff --git a/easygui/button_box.py b/easygui/button_box.py index 6c510e2..da157a4 100644 --- a/easygui/button_box.py +++ b/easygui/button_box.py @@ -201,7 +201,7 @@ def __init__(self, msg, title, choices, images, default_choice, cancel_choice, c super().__init__(msg, title) self._user_specified_callback = callback self._text_to_return_on_cancel = cancel_choice - self.choice_text = None + self.return_value = None self._images = [] self._buttons = [] @@ -304,22 +304,14 @@ def _callback(self, command): elif command in ('x', 'cancel'): self.stop() - def run(self): - self.box_root.mainloop() - self.box_root.destroy() - return self.choice_text - - def stop(self): - self.box_root.quit() - # Methods executing when a key is pressed def cancel_button_pressed(self, _): self._callback(command='cancel') - self.choice_text = self._text_to_return_on_cancel + self.return_value = self._text_to_return_on_cancel def _button_pressed(self, button_text): self._callback(command='update') - self.choice_text = button_text + self.return_value = button_text def _hotkey_pressed(self, event=None): """ Handle an event that is generated by a person interacting with a button """ @@ -331,7 +323,7 @@ def _hotkey_pressed(self, event=None): for button in self._buttons: if button['hotkey'] == hotkey_pressed: self._callback(command='update') - self.choice_text = button['original_text'] + self.return_value = button['original_text'] return # some key was pressed, but no hotkey registered to it diff --git a/easygui/choice_box.py b/easygui/choice_box.py index eab92db..7f89893 100644 --- a/easygui/choice_box.py +++ b/easygui/choice_box.py @@ -66,7 +66,7 @@ def __init__(self, msg, title, choices, preselect, multiple_select, callback): if choices is None: # Use default choice selections if none were specified: choices = ('Choice 1', 'Choice 2') - self.choices = [str(c) for c in choices] + self.return_value = [str(c) for c in choices] self.message_area = self._configure_message_area(self.box_root) @@ -80,20 +80,12 @@ def __init__(self, msg, title, choices, preselect, multiple_select, callback): self.preselect_choice(preselect) self.choiceboxWidget.focus_force() - def run(self): - self.box_root.mainloop() # run it! - self.box_root.destroy() # close the window - return self.choices - - def stop(self): - self.box_root.quit() - def cancel_button_pressed(self, event): self.stop() - self.choices = None + self.return_value = None def ok_button_pressed(self, event): - self.choices = self.get_choices() + self.return_value = self.get_choices() if self._user_specified_callback: # If a _user_specified_callback was set, call main process self._user_specified_callback(self) @@ -141,7 +133,7 @@ def create_choice_area(self): self.choiceboxFrame = tk.Frame(master=self.box_root) self.choiceboxFrame.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.YES) - lines_to_show = min(len(self.choices), 20) + lines_to_show = min(len(self.return_value), 20) # -------- put the self.choiceboxWidget in the self.choiceboxFrame --- self.choiceboxWidget = tk.Listbox(self.choiceboxFrame, @@ -180,7 +172,7 @@ def create_choice_area(self): side=tk.LEFT, padx="1m", pady="1m", expand=tk.YES, fill=tk.BOTH) # Insert choices widgets - for choice in self.choices: + for choice in self.return_value: self.choiceboxWidget.insert(tk.END, choice) # Bind the keyboard events @@ -255,22 +247,22 @@ def KeyboardListener(self, event): self.choiceboxWidget.selection_clear(0, 'end') # start from previous selection +1 - for n in range(start_n + 1, len(self.choices)): - item = self.choices[n] + for n in range(start_n + 1, len(self.return_value)): + item = self.return_value[n] if item[0].lower() == key.lower(): self.choiceboxWidget.selection_set(first=n) self.choiceboxWidget.see(n) return else: # has not found it so loop from top - for n, item in enumerate(self.choices): + for n, item in enumerate(self.return_value): if item[0].lower() == key.lower(): self.choiceboxWidget.selection_set(first=n) self.choiceboxWidget.see(n) return # nothing matched -- we'll look for the next logical choice - for n, item in enumerate(self.choices): + for n, item in enumerate(self.return_value): if item[0].lower() > key.lower(): if n > 0: self.choiceboxWidget.selection_set( @@ -282,16 +274,16 @@ def KeyboardListener(self, event): # still no match (nothing was greater than the key) # we set the selection to the first item in the list - lastIndex = len(self.choices) - 1 + lastIndex = len(self.return_value) - 1 self.choiceboxWidget.selection_set(first=lastIndex) self.choiceboxWidget.see(lastIndex) return def choiceboxClearAll(self, event): - self.choiceboxWidget.selection_clear(0, len(self.choices) - 1) + self.choiceboxWidget.selection_clear(0, len(self.return_value) - 1) def choiceboxSelectAll(self, event): - self.choiceboxWidget.selection_set(0, len(self.choices) - 1) + self.choiceboxWidget.selection_set(0, len(self.return_value) - 1) if __name__ == '__main__': users_choice = multchoicebox(choices=['choice1', 'choice2']) diff --git a/easygui/multi_fillable_box.py b/easygui/multi_fillable_box.py index ce1969c..9894115 100644 --- a/easygui/multi_fillable_box.py +++ b/easygui/multi_fillable_box.py @@ -38,7 +38,7 @@ class MultiBox(AbstractBox): def __init__(self, msg, title, fields=None, values=None, mask_last=False, callback=None): super().__init__(msg, title) - self.fields, self.values = self._process_fields_and_values(fields, values) + self.fields, self.return_value = self._process_fields_and_values(fields, values) self.user_defined_callback = callback message_widget = tk.Message(self.box_root, width="4.5i", text=msg) @@ -46,7 +46,7 @@ def __init__(self, msg, title, fields=None, values=None, mask_last=False, callba message_widget.pack(side=tk.TOP, expand=1, fill=tk.BOTH, padx='3m', pady='3m') self.entry_widgets = [] - for field, value in zip(self.fields, self.values): + for field, value in zip(self.fields, self.return_value): entry_frame = tk.Frame(master=self.box_root) entry_frame.pack(side=tk.TOP, fill=tk.BOTH) @@ -85,17 +85,12 @@ def __init__(self, msg, title, fields=None, values=None, mask_last=False, callba self.entry_widgets[0].focus_force() # put the focus on the entry_widget - def run(self): - self.box_root.mainloop() # run it! - self.box_root.destroy() # Close the window - return self.values - def _cancel_pressed(self, *args): - self.values = None + self.return_value = None self.box_root.quit() def _ok_pressed(self, _): - self.values = self._get_values() + self.return_value = self._get_values() if self.user_defined_callback: self.user_defined_callback(self) self.box_root.quit() diff --git a/easygui/text_box.py b/easygui/text_box.py index 4b5fe5c..5ac5c48 100644 --- a/easygui/text_box.py +++ b/easygui/text_box.py @@ -147,15 +147,6 @@ def _configure_text_area(self, box_root, code_box): return text_area - - def run(self): - self.box_root.mainloop() - self.box_root.destroy() - return self.text - - def stop(self): - self.box_root.quit() - def _configure_buttons(self): buttons_frame = tk.Frame(self.box_root) buttons_frame.pack(side=tk.TOP) @@ -187,6 +178,11 @@ def text(self, text): if text is not None: # cancel sets text=None but this is meaningless to the tk box content self._set_text() + @property + def return_value(self): + """ In order to work like all the other boxes, need to define 'return value'""" + return self._text + def _get_text(self): """ Used by the callback to get the text_area content""" return self.text_area.get(1.0, 'end-1c') @@ -213,7 +209,7 @@ def callback(self, command): self.stop() elif command in ('x', 'cancel'): self.stop() - self.text = None + self.text = None # TODO: does this need to be before stop?? if __name__ == '__main__': diff --git a/easygui/utilities.py b/easygui/utilities.py index 74397fe..f018a03 100644 --- a/easygui/utilities.py +++ b/easygui/utilities.py @@ -21,6 +21,7 @@ def __init__(self, msg, title) -> None: self.msg = msg self.box_root = self._configure_box_root(title) self.message_area = NotImplemented + self.return_value = None def _configure_box_root(self, title): box_root = tk.Tk() @@ -40,6 +41,15 @@ def _set_msg_area(self, msg): def cancel_button_pressed(self, _): raise NotImplemented + def run(self): + self.box_root.mainloop() + self.box_root.destroy() + return self.return_value + + def stop(self): + self.box_root.quit() + + def parse_hotkey(text): """ Extract a desired hotkey from the text. The format to enclose From e5d13e9d4c589c04a7986d478fcb4daef33dbf8b Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Fri, 29 Apr 2022 00:01:09 +0100 Subject: [PATCH 12/29] refactor all boxes to use the same cancel_button_pressed (except ButtonBox which is special) --- easygui/button_box.py | 18 ++++++------------ easygui/choice_box.py | 4 ---- easygui/fillable_box.py | 10 +++------- easygui/multi_fillable_box.py | 10 +++------- easygui/text_box.py | 25 +++++++++---------------- easygui/utilities.py | 3 ++- tests/test_multi_fillable_box.py | 2 +- 7 files changed, 24 insertions(+), 48 deletions(-) diff --git a/easygui/button_box.py b/easygui/button_box.py index da157a4..0f97ade 100644 --- a/easygui/button_box.py +++ b/easygui/button_box.py @@ -294,23 +294,17 @@ def _create_buttons_frame(self, choices, default_choice): return buttons_frame - def _callback(self, command): - if command == 'update': # OK was pressed - if self._user_specified_callback: - # If a callback was set, call main process - self._user_specified_callback() - else: - self.stop() - elif command in ('x', 'cancel'): - self.stop() - # Methods executing when a key is pressed def cancel_button_pressed(self, _): - self._callback(command='cancel') self.return_value = self._text_to_return_on_cancel + self.stop() def _button_pressed(self, button_text): - self._callback(command='update') + if self._user_specified_callback: + # If a callback was set, call main process + self._user_specified_callback() + else: + self.stop() self.return_value = button_text def _hotkey_pressed(self, event=None): diff --git a/easygui/choice_box.py b/easygui/choice_box.py index 7f89893..687468c 100644 --- a/easygui/choice_box.py +++ b/easygui/choice_box.py @@ -80,10 +80,6 @@ def __init__(self, msg, title, choices, preselect, multiple_select, callback): self.preselect_choice(preselect) self.choiceboxWidget.focus_force() - def cancel_button_pressed(self, event): - self.stop() - self.return_value = None - def ok_button_pressed(self, event): self.return_value = self.get_choices() if self._user_specified_callback: diff --git a/easygui/fillable_box.py b/easygui/fillable_box.py index 086c531..5ec187d 100644 --- a/easygui/fillable_box.py +++ b/easygui/fillable_box.py @@ -155,7 +155,7 @@ def __init__(self, msg, title, default, mask=None, image=None, root=None): entry_widget.configure(show=mask) entry_widget.pack(side=tk.LEFT, padx="3m") entry_widget.bind("", self._ok_pressed) - entry_widget.bind("", self._cancel_pressed) + entry_widget.bind("", self.cancel_button_pressed) entry_widget.insert(0, self.return_value) # put text into the entry_widget self.entry_widget = entry_widget # save a reference - we need to get text from this widget later @@ -169,8 +169,8 @@ def __init__(self, msg, title, default, mask=None, image=None, root=None): cancel_button = tk.Button(buttons_frame, takefocus=1, text="Cancel") cancel_button.pack(expand=1, side=tk.RIGHT, padx='3m', pady='3m', ipadx='2m', ipady='1m') - cancel_button.bind("", self._cancel_pressed) - cancel_click_handler = MouseClickHandler(callback=self._cancel_pressed) + cancel_button.bind("", self.cancel_button_pressed) + cancel_click_handler = MouseClickHandler(callback=self.cancel_button_pressed) cancel_button.bind("", cancel_click_handler.enter) cancel_button.bind("", cancel_click_handler.leave) cancel_button.bind("", cancel_click_handler.release) @@ -178,10 +178,6 @@ def __init__(self, msg, title, default, mask=None, image=None, root=None): self.entry_widget.focus_force() # put the focus on the self.entry_widget self.box_root.deiconify() - def _cancel_pressed(self, *args): - self.return_value = None - self.box_root.quit() - def _ok_pressed(self, *args): self.return_value = self.entry_widget.get() self.box_root.quit() diff --git a/easygui/multi_fillable_box.py b/easygui/multi_fillable_box.py index 9894115..73d4c1d 100644 --- a/easygui/multi_fillable_box.py +++ b/easygui/multi_fillable_box.py @@ -58,7 +58,7 @@ def __init__(self, msg, title, fields=None, values=None, mask_last=False, callba entry_widget.configure(font=(PROPORTIONAL_FONT_FAMILY, TEXT_ENTRY_FONT_SIZE)) entry_widget.pack(side=tk.RIGHT, padx="3m") entry_widget.bind("", self._ok_pressed) - entry_widget.bind("", self._cancel_pressed) + entry_widget.bind("", self.cancel_button_pressed) entry_widget.insert(0, '' if value is None else value) if mask_last: @@ -69,8 +69,8 @@ def __init__(self, msg, title, fields=None, values=None, mask_last=False, callba cancel_button = tk.Button(buttons_frame, takefocus=1, text="Cancel") cancel_button.pack(expand=1, side=tk.LEFT, padx='3m', pady='3m', ipadx='2m', ipady='1m') - cancel_button.bind("", self._cancel_pressed) - cancel_click_handler = MouseClickHandler(callback=self._cancel_pressed) + cancel_button.bind("", self.cancel_button_pressed) + cancel_click_handler = MouseClickHandler(callback=self.cancel_button_pressed) cancel_button.bind("", cancel_click_handler.enter) cancel_button.bind("", cancel_click_handler.leave) cancel_button.bind("", cancel_click_handler.release) @@ -85,10 +85,6 @@ def __init__(self, msg, title, fields=None, values=None, mask_last=False, callba self.entry_widgets[0].focus_force() # put the focus on the entry_widget - def _cancel_pressed(self, *args): - self.return_value = None - self.box_root.quit() - def _ok_pressed(self, _): self.return_value = self._get_values() if self.user_defined_callback: diff --git a/easygui/text_box.py b/easygui/text_box.py index 5ac5c48..9e51ac5 100644 --- a/easygui/text_box.py +++ b/easygui/text_box.py @@ -183,6 +183,10 @@ def return_value(self): """ In order to work like all the other boxes, need to define 'return value'""" return self._text + @return_value.setter + def return_value(self, text): + self.text = text + def _get_text(self): """ Used by the callback to get the text_area content""" return self.text_area.get(1.0, 'end-1c') @@ -192,24 +196,13 @@ def _set_text(self): self.text_area.insert(tk.END, self._text, "normal") self.text_area.focus() - # Methods executing when a key is pressed - def cancel_button_pressed(self, _): - self.callback(command='cancel') - def ok_button_pressed(self, _): - self.callback(command='update') - - def callback(self, command): - if command == 'update': # OK was pressed - self.text = self._get_text() - if self._user_specified_callback: - # If a callback was set, call main process - self._user_specified_callback(self) - else: - self.stop() - elif command in ('x', 'cancel'): + self.text = self._get_text() + if self._user_specified_callback: + # If a callback was set, call main process + self._user_specified_callback(self) + else: self.stop() - self.text = None # TODO: does this need to be before stop?? if __name__ == '__main__': diff --git a/easygui/utilities.py b/easygui/utilities.py index f018a03..37178f7 100644 --- a/easygui/utilities.py +++ b/easygui/utilities.py @@ -39,7 +39,8 @@ def _set_msg_area(self, msg): self.message_area.update() def cancel_button_pressed(self, _): - raise NotImplemented + self.return_value = None + self.box_root.quit() def run(self): self.box_root.mainloop() diff --git a/tests/test_multi_fillable_box.py b/tests/test_multi_fillable_box.py index 11a5fa3..1287c96 100644 --- a/tests/test_multi_fillable_box.py +++ b/tests/test_multi_fillable_box.py @@ -10,7 +10,7 @@ def test__multenterbox__cancel_results_in_run_returning_none(): callback=None, run=False) def simulate_user_cancel_press(meb_instance): - meb_instance._cancel_pressed('ignored button handler arg') + meb_instance.cancel_button_pressed('ignored button handler arg') meb.box_root.after(WAIT_0_MILLISECONDS, simulate_user_cancel_press, meb) actual = meb.run() From 2d181050d64b52393ec81540fa4bc31fd439acfa Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Sat, 30 Apr 2022 16:36:34 +0100 Subject: [PATCH 13/29] choice_box.py: various tidy-ups main change the preselect changing list -> tuple (because mutable arguments bad) --- easygui/choice_box.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/easygui/choice_box.py b/easygui/choice_box.py index 687468c..09cf998 100644 --- a/easygui/choice_box.py +++ b/easygui/choice_box.py @@ -4,7 +4,7 @@ from easygui.utilities import get_width_and_padding, MouseClickHandler, AbstractBox -def choicebox(msg="Pick an item", title="", choices=None, preselect=[], callback=None, run=True): +def choicebox(msg="Pick an item", title="", choices=None, preselect=(), callback=None, run=True): """ Present the user with a list of choices. return the choice that he selects. @@ -13,19 +13,15 @@ def choicebox(msg="Pick an item", title="", choices=None, preselect=[], callback :param str title: the window title :param list choices: a list or tuple of the choices to be displayed :param preselect: optional list of pre-selected choices, a subset of the choices argument - :param callback: - :param run: + :param callback: user defined callback to be performed when selection is complete + :param run: whether to call .run() on the box immediately or return an instance :return: List containing choice selected or None if cancelled """ cb = ChoiceBox(msg, title, choices, preselect=preselect, multiple_select=False, callback=callback) - if run: - reply = cb.run() - return reply - else: - return cb + return cb.run() if run else cb -def multchoicebox(msg="Pick an item", title="", choices=None, preselect=[], callback=None, run=True): +def multchoicebox(msg="Pick an item", title="", choices=None, preselect=(), callback=None, run=True): """ The ``multchoicebox()`` function provides a way for a user to select from a list of choices. The interface looks just like the ``choicebox()`` @@ -44,6 +40,8 @@ def multchoicebox(msg="Pick an item", title="", choices=None, preselect=[], call :param str title: the window title :param list choices: a list or tuple of the choices to be displayed :param preselect: Which item, if any are preselected when dialog appears + :param callback: user defined callback to be performed when selection is complete + :param run: whether to call .run() on the box immediately or return an instance :return: A list of strings of the selected choices or None if cancelled. """ mcb = ChoiceBox(msg, title, choices, preselect=preselect, multiple_select=True, callback=callback) @@ -58,7 +56,7 @@ class ChoiceBox(AbstractBox): def __init__(self, msg, title, choices, preselect, multiple_select, callback): super().__init__(msg, title) - if not multiple_select and len(preselect)>1: + if len(preselect) > 1 and not multiple_select: raise ValueError("Multiple selections not allowed, yet preselect has multiple values:{}".format(preselect)) self._multiple_select = multiple_select @@ -68,7 +66,6 @@ def __init__(self, msg, title, choices, preselect, multiple_select, callback): choices = ('Choice 1', 'Choice 2') self.return_value = [str(c) for c in choices] - self.message_area = self._configure_message_area(self.box_root) self._set_msg_area("" if msg is None else msg) From cccfd52036113f112124e3f49516d5b20f658fee Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Sat, 30 Apr 2022 17:24:21 +0100 Subject: [PATCH 14/29] text_box.py: update docstrings, refactor, change parameter 'codebox' -> monospace mixed feelings about the parameter rename (breaking change!) but otherwise reasonable tidy-up --- demos/demo_text_box.py | 6 +-- demos/text2binary.py | 2 +- easygui/text_box.py | 104 +++++++++++++++++------------------------ tests/test_text_box.py | 38 +++++---------- 4 files changed, 57 insertions(+), 93 deletions(-) diff --git a/demos/demo_text_box.py b/demos/demo_text_box.py index 746cff7..5d1d0c5 100644 --- a/demos/demo_text_box.py +++ b/demos/demo_text_box.py @@ -46,8 +46,7 @@ def __init__(self): self.finished = False - textbox(gnexp + msg, title, text_snippet, False, - callback=self.check_answer, run=True) + textbox(gnexp + msg, title, text_snippet, callback=self.check_answer, run=True) def check_answer(self, box): """ Callback from TextBox @@ -92,8 +91,7 @@ def __init__(self): text_snippet = "Hello" # This text wont show - box = textbox( - msg, title, text_snippet, False, callback=self.check_answer, run=False) + box = textbox(msg, title, text_snippet, callback=self.check_answer, run=False) box.text = ( "It was the west of times, and it was the worst of times. " diff --git a/demos/text2binary.py b/demos/text2binary.py index 1a5f277..f582a96 100644 --- a/demos/text2binary.py +++ b/demos/text2binary.py @@ -8,5 +8,5 @@ sys.path.append('..') import easygui -Plain = easygui.textbox(msg='Enter Message', title='OTP', text=u'Hi', codebox=1) +Plain = easygui.textbox(msg='Enter Message', title='OTP', text=u'Hi') print(repr(Plain)) #If there is no trailing newline, its OK \ No newline at end of file diff --git a/easygui/text_box.py b/easygui/text_box.py index 9e51ac5..5b2813c 100644 --- a/easygui/text_box.py +++ b/easygui/text_box.py @@ -1,104 +1,84 @@ -import sys import tkinter as tk -import traceback from tkinter import font from easygui.utilities import get_width_and_padding, MouseClickHandler, AbstractBox -def textbox(msg='', title='', text='', codebox=False, callback=None, run=True): +def textbox(msg='', title='', text='', callback=None, run=True): """ - Displays a dialog box with a large, multi-line text box, and returns - the entered text as a string. The message text is displayed in a - proportional font and wraps. - - Parameters - ---------- - msg : string - text displayed in the message area (instructions...) - title : str - the window title - text: str, list or tuple - text displayed in textAreas (editable) - codebox: bool - if True, don't wrap and width is set to 80 chars - callback: function - if set, this function will be called when OK is pressed - run: bool - if True, a box object will be created and returned, but not run - - Returns - ------- - None - If cancel is pressed - str - If OK is pressed returns the contents of textArea. - TextBox - If the 'run' argument was False + Displays a dialog box with a large, multi-line text box, and returns the entered text as a string. + The message text is displayed in a proportional font and wraps. + :param str msg: text displayed in the message area (instructions...) + :param str title: the window title + :param text: text displayed in textAreas (editable) + :param function callback: if set, this function will be called when OK is pressed + :param bool run: if True, a box object will be created and returned, but not run + :return: + if 'run' is True then the box is run AND Ok is pressed -> returns the content of the TextArea + if 'run' is True then the box is run AND Cancel is pressed -> returns None + else 'run' is False then the box is not run -> returns an instance TextBox """ - tb = TextBox(msg=msg, title=title, text=text, codebox=codebox, callback=callback) - if run: - text = tb.run() - return text - return tb + tb = TextBox(msg=msg, title=title, text=text, monospace=False, callback=callback) + return tb.run() if run else tb -def codebox(msg='', title='', text=''): +def codebox(msg='', title='', text='', callback=None, run=True): """ Helper method similar to textbox, displays text in a monospaced font which is useful for code. - The text parameter should be a string, or a list or tuple of lines to be displayed in the textbox. - - :param str msg: the msg to be displayed + :param str msg: text displayed in the message area (instructions...) :param str title: the window title - :param str text: what to display in the textbox + :param str text: text displayed in textAreas (editable) + :param function callback: if set, this function will be called when OK is pressed + :param bool run: if True, a box object will be created and returned, but not run + :return: + if 'run' is True then the box is run AND Ok is pressed -> returns the content of the TextArea + if 'run' is True then the box is run AND Cancel is pressed -> returns None + else 'run' is False then the box is not run -> returns an instance TextBox """ - return textbox(msg, title, text, codebox=True) + cb = TextBox(msg, title, text, monospace=True, callback=callback) + return cb.run() if run else cb def exceptionbox(msg='An error (exception) has occurred in the program.', title='Error Report'): - """ Display a box that gives information about the latest exception that has been raised. - + """ Display a box that gives information about the latest exception that has been raised. - - The caller may optionally pass in a title for the window, or a msg to accompany the error information. - - :param str msg: the msg to be displayed - :param str title: the window title - :return: None""" - - def format_exception_for_display(): - return "".join(traceback.format_exception(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2])) - - codebox(msg, title, format_exception_for_display()) + :param str msg: [optional] msg to be displayed above exception + :param str title: [optional] the window title + :return: None + """ + import sys + import traceback + text = "".join(traceback.format_exception(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2])) + TextBox(msg, title, text=text, monospace=True).run() class TextBox(AbstractBox): """ Display a message, and an editable text field pre-populated with 'text' """ - def __init__(self, msg, title, text, codebox, callback): + def __init__(self, msg, title, text, monospace, callback=None): """ :param msg: str displayed in the message area (instructions...) :param title: str used as the window title :param text: str displayed in textArea (editable) - :param codebox: bool (if true) don't wrap, set width to 80 chars, use monospace font + :param monospace: bool (if true) don't wrap, set width to 80 chars, use monospace font :param callback: optional function to be called when OK is pressed """ super().__init__(msg, title) self._user_specified_callback = callback self._text = text - self.message_area = self._configure_message_area(box_root=self.box_root, code_box=codebox) + self.message_area = self._configure_message_area(box_root=self.box_root, monospace=monospace) self._set_msg_area("" if msg is None else msg) self.MONOSPACE_FONT = font.Font(family='Courier') - self.text_area = self._configure_text_area(box_root=self.box_root, code_box=codebox) + self.text_area = self._configure_text_area(box_root=self.box_root, code_box=monospace) self._set_text() self._configure_buttons() @staticmethod - def _configure_message_area(box_root, code_box): - padding, width_in_chars = get_width_and_padding(code_box) + def _configure_message_area(box_root, monospace): + padding, width_in_chars = get_width_and_padding(monospace) message_frame = tk.Frame(box_root, padx=padding) message_frame.pack(side=tk.TOP, expand=1, fill='both') @@ -128,7 +108,7 @@ def _configure_text_area(self, box_root, code_box): if code_box: text_area.configure(font=self.MONOSPACE_FONT) - # no word-wrapping for code so we need a horizontal scroll bar + # no word-wrapping for code, so we need a horizontal scroll bar horizontal_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) vertical_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) @@ -207,4 +187,4 @@ def ok_button_pressed(self, _): if __name__ == '__main__': result = textbox("test message here") - print("textbox() return value was: {}".format(result)) \ No newline at end of file + print("textbox() return value was: {}".format(result)) diff --git a/tests/test_text_box.py b/tests/test_text_box.py index b418918..f652df8 100644 --- a/tests/test_text_box.py +++ b/tests/test_text_box.py @@ -1,19 +1,19 @@ +import tkinter as tk import unittest from mock import patch, Mock, ANY -from easygui.text_box import TextBox, textbox +from easygui.text_box import TextBox, textbox, codebox from tests import WAIT_0_MILLISECONDS, WAIT_1_MILLISECONDS -import tkinter as tk MODBASE = 'easygui.text_box' TEST_MESSAGE = 'example message' TEST_TITLE = 'example title' TEST_TEXT = 'example text' -TEST_CODEBOX = False +TEST_MONOSPACE = False TEST_CALLBACK = Mock() -TEST_ARGS = [TEST_MESSAGE, TEST_TITLE, TEST_TEXT, TEST_CODEBOX, TEST_CALLBACK] +TEST_ARGS = [TEST_MESSAGE, TEST_TITLE, TEST_TEXT, TEST_MONOSPACE, TEST_CALLBACK] def test__textbox_method__instantiates_textbox_class_and_runs_it(): @@ -23,13 +23,13 @@ def test__textbox_method__instantiates_textbox_class_and_runs_it(): mock_text_box_instance.run = Mock(return_value='return text') mock_text_box_class.return_value = mock_text_box_instance - return_text = textbox(*TEST_ARGS, run=True) + return_text = textbox(TEST_MESSAGE, TEST_TITLE, TEST_TEXT, TEST_CALLBACK, run=True) mock_text_box_class.assert_called_once_with( msg=TEST_MESSAGE, title=TEST_TITLE, text=TEST_TEXT, - codebox=TEST_CODEBOX, + monospace=TEST_MONOSPACE, callback=TEST_CALLBACK ) mock_text_box_instance.run.assert_called_once_with() @@ -85,7 +85,7 @@ def test_set_text(self): class TestTextBoxIntegration(unittest.TestCase): def test_textbox_cancel_button_pressed_results_in_run_returning_None(self): - tb = textbox(*TEST_ARGS, run=False) + tb = textbox(run=False) def simulate_cancel_button_pressed(tb_instance): tb_instance.cancel_button_pressed('ignored button handler arg') @@ -96,7 +96,7 @@ def simulate_cancel_button_pressed(tb_instance): self.assertEqual(actual, None) def test_textbox_ok_pressed_calls_user_defined_callback(self): - tb = textbox(*TEST_ARGS, run=False) + tb = textbox(text=TEST_TEXT, callback=TEST_CALLBACK, run=False) def simulate_ok_button_pressed(tb_instance): tb_instance.ok_button_pressed('ignored button handler arg') @@ -112,14 +112,7 @@ def stop_running(tb_instance): self.assertEqual(actual, TEST_TEXT) def test_textbox_ok_pressed_with_no_user_defined_callback(self): - tb = textbox( - msg=TEST_MESSAGE, - title=TEST_TITLE, - text=TEST_TEXT, - codebox=TEST_CODEBOX, - callback=None, - run=False - ) + tb = textbox(msg=TEST_MESSAGE, title=TEST_TITLE, text=TEST_TEXT, callback=TEST_CALLBACK, run=False) def simulate_ok_button_pressed(tb_instance): tb_instance.ok_button_pressed('ignored button handler arg') @@ -134,15 +127,8 @@ def simulate_ok_button_pressed(tb_instance): class TestTextBoxCodeBox(unittest.TestCase): - def test_instantiation_codebox(self): - tb = textbox( - msg=TEST_MESSAGE, - title=TEST_TITLE, - text=TEST_TEXT * 100, - codebox=True, - callback=None, - run=False - ) + def test_instantiation_codebox(self): + cb = codebox(msg=TEST_MESSAGE, title=TEST_TITLE, text=TEST_TEXT * 100, callback=TEST_CALLBACK, run=False) # cget returns strings so the monospace assertion is a bit messy: - self.assertEqual(tb.text_area.cget('font'), str(tb.MONOSPACE_FONT)) + self.assertEqual(cb.text_area.cget('font'), str(cb.MONOSPACE_FONT)) From 817ce1ccfce0739ad9b0e93f512855065444a3f3 Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Sat, 30 Apr 2022 17:44:34 +0100 Subject: [PATCH 15/29] all boxes: pull callback (self._user_specified_callback) up to the base AbstractBox --- easygui/button_box.py | 3 +-- easygui/choice_box.py | 3 +-- easygui/fillable_box.py | 2 +- easygui/multi_fillable_box.py | 7 +++---- easygui/text_box.py | 3 +-- easygui/utilities.py | 3 ++- 6 files changed, 9 insertions(+), 12 deletions(-) diff --git a/easygui/button_box.py b/easygui/button_box.py index 0f97ade..88b3035 100644 --- a/easygui/button_box.py +++ b/easygui/button_box.py @@ -198,8 +198,7 @@ def __init__(self, msg, title, choices, images, default_choice, cancel_choice, c """ - super().__init__(msg, title) - self._user_specified_callback = callback + super().__init__(msg, title, callback) self._text_to_return_on_cancel = cancel_choice self.return_value = None self._images = [] diff --git a/easygui/choice_box.py b/easygui/choice_box.py index 09cf998..cf33175 100644 --- a/easygui/choice_box.py +++ b/easygui/choice_box.py @@ -55,12 +55,11 @@ def multchoicebox(msg="Pick an item", title="", choices=None, preselect=(), call class ChoiceBox(AbstractBox): def __init__(self, msg, title, choices, preselect, multiple_select, callback): - super().__init__(msg, title) + super().__init__(msg, title, callback) if len(preselect) > 1 and not multiple_select: raise ValueError("Multiple selections not allowed, yet preselect has multiple values:{}".format(preselect)) self._multiple_select = multiple_select - self._user_specified_callback = callback if choices is None: # Use default choice selections if none were specified: choices = ('Choice 1', 'Choice 2') diff --git a/easygui/fillable_box.py b/easygui/fillable_box.py index 5ec187d..459443d 100644 --- a/easygui/fillable_box.py +++ b/easygui/fillable_box.py @@ -116,7 +116,7 @@ def __init__(self, msg, title, default, mask=None, image=None, root=None): root.withdraw() self.box_root = tk.Toplevel(master=root) self.box_root.withdraw() - super().__init__(msg, title) + super().__init__(msg, title, callback=None) self.return_value = '' if default is None else default self.pre_existing_root = root self.entry_widget = None diff --git a/easygui/multi_fillable_box.py b/easygui/multi_fillable_box.py index 73d4c1d..6b492e9 100644 --- a/easygui/multi_fillable_box.py +++ b/easygui/multi_fillable_box.py @@ -36,10 +36,9 @@ def multenterbox(msg="Fill in values for the fields.", title=" ", fields=None, v class MultiBox(AbstractBox): def __init__(self, msg, title, fields=None, values=None, mask_last=False, callback=None): - super().__init__(msg, title) + super().__init__(msg, title, callback) self.fields, self.return_value = self._process_fields_and_values(fields, values) - self.user_defined_callback = callback message_widget = tk.Message(self.box_root, width="4.5i", text=msg) message_widget.configure(font=(PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE)) @@ -87,8 +86,8 @@ def __init__(self, msg, title, fields=None, values=None, mask_last=False, callba def _ok_pressed(self, _): self.return_value = self._get_values() - if self.user_defined_callback: - self.user_defined_callback(self) + if self._user_specified_callback: + self._user_specified_callback(self) self.box_root.quit() def _get_values(self): diff --git a/easygui/text_box.py b/easygui/text_box.py index 5b2813c..bcebcce 100644 --- a/easygui/text_box.py +++ b/easygui/text_box.py @@ -64,8 +64,7 @@ def __init__(self, msg, title, text, monospace, callback=None): :param monospace: bool (if true) don't wrap, set width to 80 chars, use monospace font :param callback: optional function to be called when OK is pressed """ - super().__init__(msg, title) - self._user_specified_callback = callback + super().__init__(msg, title, callback) self._text = text self.message_area = self._configure_message_area(box_root=self.box_root, monospace=monospace) diff --git a/easygui/utilities.py b/easygui/utilities.py index 37178f7..26c9c76 100644 --- a/easygui/utilities.py +++ b/easygui/utilities.py @@ -16,8 +16,9 @@ class AbstractBox(object): TextBox def __init__(self, msg, title, text, codebox, callback): """ - def __init__(self, msg, title) -> None: + def __init__(self, msg, title, callback) -> None: super().__init__() + self._user_specified_callback = callback self.msg = msg self.box_root = self._configure_box_root(title) self.message_area = NotImplemented From 5ca8935df883f63e5cd162d964f460c5154b1902 Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Sun, 1 May 2022 00:36:18 +0100 Subject: [PATCH 16/29] WIP rfactor of text box (incomplete!!) --- easygui/text_box.py | 91 ++++++++++++++++-------------------------- easygui/utilities.py | 12 +++--- tests/test_text_box.py | 6 +-- 3 files changed, 44 insertions(+), 65 deletions(-) diff --git a/easygui/text_box.py b/easygui/text_box.py index bcebcce..471434b 100644 --- a/easygui/text_box.py +++ b/easygui/text_box.py @@ -1,3 +1,4 @@ +import textwrap import tkinter as tk from tkinter import font @@ -61,72 +62,46 @@ def __init__(self, msg, title, text, monospace, callback=None): :param msg: str displayed in the message area (instructions...) :param title: str used as the window title :param text: str displayed in textArea (editable) - :param monospace: bool (if true) don't wrap, set width to 80 chars, use monospace font + :param monospace: bool (if true) don't wrap, set width to 80 witdh_in_chars, use monospace font :param callback: optional function to be called when OK is pressed """ super().__init__(msg, title, callback) self._text = text - self.message_area = self._configure_message_area(box_root=self.box_root, monospace=monospace) - self._set_msg_area("" if msg is None else msg) - - self.MONOSPACE_FONT = font.Font(family='Courier') - self.text_area = self._configure_text_area(box_root=self.box_root, code_box=monospace) - self._set_text() - self._configure_buttons() - - @staticmethod - def _configure_message_area(box_root, monospace): padding, width_in_chars = get_width_and_padding(monospace) - - message_frame = tk.Frame(box_root, padx=padding) - message_frame.pack(side=tk.TOP, expand=1, fill='both') - - message_area = tk.Text(master=message_frame, - width=width_in_chars, - padx=padding, - pady=padding, - wrap=tk.WORD) - message_area.pack(side=tk.TOP, expand=1, fill='both') - return message_area - - def _configure_text_area(self, box_root, code_box): - padding, width_in_chars = get_width_and_padding(code_box) - - text_frame = tk.Frame(box_root, padx=padding, ) - text_frame.pack(side=tk.TOP) - - text_area = tk.Text(text_frame, padx=padding, pady=padding, height=25, width=width_in_chars) - text_area.configure(wrap=tk.NONE if code_box else tk.WORD) - - vertical_scrollbar = tk.Scrollbar(text_frame, orient=tk.VERTICAL, command=text_area.yview) - text_area.configure(yscrollcommand=vertical_scrollbar.set) - - horizontal_scrollbar = tk.Scrollbar(text_frame, orient=tk.HORIZONTAL, command=text_area.xview) - text_area.configure(xscrollcommand=horizontal_scrollbar.set) - - if code_box: - text_area.configure(font=self.MONOSPACE_FONT) + self.message = tk.Label( + master=self.box_root, + text='\n'.join(textwrap.wrap(msg, width_in_chars)), + width=width_in_chars, + padx=padding, + pady=padding + ) + self.message.pack(side=tk.TOP, expand=1, fill='both') + + self.text_frame = tk.Frame(self.box_root, padx=padding, ) + self.text_frame.pack(side=tk.TOP) + self.text_area = tk.Text(self.text_frame, padx=padding, pady=padding, height=25, width=width_in_chars) + self.text_area.configure(wrap=tk.NONE if monospace else tk.WORD) + vertical_scrollbar = tk.Scrollbar(self.text_frame, orient=tk.VERTICAL, command=self.text_area.yview) + self.text_area.configure(yscrollcommand=vertical_scrollbar.set) + horizontal_scrollbar = tk.Scrollbar(self.text_frame, orient=tk.HORIZONTAL, command=self.text_area.xview) + self.text_area.configure(xscrollcommand=horizontal_scrollbar.set) + if monospace: + monospace_font = font.Font(family='Courier') + self.text_area.configure(font=monospace_font) # no word-wrapping for code, so we need a horizontal scroll bar horizontal_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) - vertical_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - # pack textArea last so bottom scrollbar displays properly - text_area.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.YES) - - box_root.bind("", text_area.yview_scroll(1, tk.PAGES)) - box_root.bind("", text_area.yview_scroll(-1, tk.PAGES)) - - box_root.bind("", text_area.xview_scroll(1, tk.PAGES)) - box_root.bind("", text_area.xview_scroll(-1, tk.PAGES)) - - box_root.bind("", text_area.yview_scroll(1, tk.UNITS)) - box_root.bind("", text_area.yview_scroll(-1, tk.UNITS)) - - return text_area + self.text_area.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.YES) + self.box_root.bind("", self.text_area.yview_scroll(1, tk.PAGES)) + self.box_root.bind("", self.text_area.yview_scroll(-1, tk.PAGES)) + self.box_root.bind("", self.text_area.xview_scroll(1, tk.PAGES)) + self.box_root.bind("", self.text_area.xview_scroll(-1, tk.PAGES)) + self.box_root.bind("", self.text_area.yview_scroll(1, tk.UNITS)) + self.box_root.bind("", self.text_area.yview_scroll(-1, tk.UNITS)) + self._set_text() - def _configure_buttons(self): buttons_frame = tk.Frame(self.box_root) buttons_frame.pack(side=tk.TOP) @@ -185,5 +160,9 @@ def ok_button_pressed(self, _): if __name__ == '__main__': - result = textbox("test message here") + result = textbox("test message here .... should wrap if the line goes quite long ... like, really long") + print("textbox() return value was: {}".format(result)) + + code_result = codebox("some code goes here should not wrap even if the line becomes really, " + "really, really long") print("textbox() return value was: {}".format(result)) diff --git a/easygui/utilities.py b/easygui/utilities.py index 26c9c76..4a8d060 100644 --- a/easygui/utilities.py +++ b/easygui/utilities.py @@ -21,7 +21,7 @@ def __init__(self, msg, title, callback) -> None: self._user_specified_callback = callback self.msg = msg self.box_root = self._configure_box_root(title) - self.message_area = NotImplemented + self.message = NotImplemented self.return_value = None def _configure_box_root(self, title): @@ -33,11 +33,11 @@ def _configure_box_root(self, title): return box_root def _set_msg_area(self, msg): - self.message_area.delete(1.0, tk.END) - self.message_area.insert(tk.END, msg) - line, char = self.message_area.index(tk.END).split('.') - self.message_area.configure(height=int(line)) - self.message_area.update() + self.message.delete(1.0, tk.END) + self.message.insert(tk.END, msg) + line, char = self.message.index(tk.END).split('.') + self.message.configure(height=int(line)) + self.message.update() def cancel_button_pressed(self, _): self.return_value = None diff --git a/tests/test_text_box.py b/tests/test_text_box.py index f652df8..bf00ae0 100644 --- a/tests/test_text_box.py +++ b/tests/test_text_box.py @@ -48,11 +48,11 @@ def test_instantiation(self): # The following Tk widgets should also have been created: isinstance(self.tb.box_root, tk.Tk) - isinstance(self.tb.message_area, tk.Tk) + isinstance(self.tb.message, tk.Tk) isinstance(self.tb.text_area, tk.Tk) # And configured: - self.assertEqual(self.tb.message_area.get(0.0, 'end-1c'), TEST_MESSAGE) + self.assertEqual(self.tb.message.get(0.0, 'end-1c'), TEST_MESSAGE) self.assertEqual(self.tb.text_area.get(0.0, 'end-1c'), TEST_TEXT) def test_run(self): @@ -70,7 +70,7 @@ def test_stop(self): def test_set_msg_area(self): new_msg = 'some new text' self.tb._set_msg_area(msg=new_msg) - self.assertEqual(self.tb.message_area.get(1.0, 'end-1c'), new_msg) + self.assertEqual(self.tb.message.get(1.0, 'end-1c'), new_msg) def test_get_text(self): actual = self.tb._get_text() From 7829ca0e7ae28003bebff9300509fe45150c1f09 Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Mon, 2 May 2022 19:23:12 +0100 Subject: [PATCH 17/29] Handle Close Window (X button) actions correctly Without a .protocol() binding for this the root.destroy() can get called twice causing errors --- easygui/utilities.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/easygui/utilities.py b/easygui/utilities.py index 4a8d060..04c30d7 100644 --- a/easygui/utilities.py +++ b/easygui/utilities.py @@ -30,6 +30,7 @@ def _configure_box_root(self, title): box_root.iconname('Dialog') box_root.geometry(GLOBAL_WINDOW_POSITION) box_root.bind("", self.cancel_button_pressed) + box_root.protocol('WM_DELETE_WINDOW', self.cancel_button_pressed) return box_root def _set_msg_area(self, msg): @@ -39,7 +40,15 @@ def _set_msg_area(self, msg): self.message.configure(height=int(line)) self.message.update() - def cancel_button_pressed(self, _): + def cancel_button_pressed(self, *args): + """ + Set the return value to None so that and quit the mainloop() + Care: may be called: + * with zero args when handling a window close action + * with one arg when handling an Escape button precessed binding + :param args: zero or more args + :return: None + """ self.return_value = None self.box_root.quit() From 07320ae10a388c366779d81820a5a27ebfe75b4f Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Tue, 3 May 2022 18:47:23 +0100 Subject: [PATCH 18/29] More changes to text_box - about as much as I can push into AbstractBox --- easygui/text_box.py | 127 +++++++++++++++++-------------------------- easygui/utilities.py | 55 ++++++++++++++++--- 2 files changed, 97 insertions(+), 85 deletions(-) diff --git a/easygui/text_box.py b/easygui/text_box.py index 471434b..a9beee4 100644 --- a/easygui/text_box.py +++ b/easygui/text_box.py @@ -1,8 +1,7 @@ -import textwrap import tkinter as tk from tkinter import font -from easygui.utilities import get_width_and_padding, MouseClickHandler, AbstractBox +from easygui.utilities import get_width_and_padding, AbstractBox, bind_to_mouse def textbox(msg='', title='', text='', callback=None, run=True): @@ -65,104 +64,80 @@ def __init__(self, msg, title, text, monospace, callback=None): :param monospace: bool (if true) don't wrap, set width to 80 witdh_in_chars, use monospace font :param callback: optional function to be called when OK is pressed """ - super().__init__(msg, title, callback) - self._text = text - - padding, width_in_chars = get_width_and_padding(monospace) - self.message = tk.Label( - master=self.box_root, - text='\n'.join(textwrap.wrap(msg, width_in_chars)), - width=width_in_chars, - padx=padding, - pady=padding - ) - self.message.pack(side=tk.TOP, expand=1, fill='both') - - self.text_frame = tk.Frame(self.box_root, padx=padding, ) - self.text_frame.pack(side=tk.TOP) - self.text_area = tk.Text(self.text_frame, padx=padding, pady=padding, height=25, width=width_in_chars) - self.text_area.configure(wrap=tk.NONE if monospace else tk.WORD) - vertical_scrollbar = tk.Scrollbar(self.text_frame, orient=tk.VERTICAL, command=self.text_area.yview) - self.text_area.configure(yscrollcommand=vertical_scrollbar.set) - horizontal_scrollbar = tk.Scrollbar(self.text_frame, orient=tk.HORIZONTAL, command=self.text_area.xview) - self.text_area.configure(xscrollcommand=horizontal_scrollbar.set) - if monospace: - monospace_font = font.Font(family='Courier') - self.text_area.configure(font=monospace_font) - # no word-wrapping for code, so we need a horizontal scroll bar - horizontal_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) - vertical_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - # pack textArea last so bottom scrollbar displays properly - self.text_area.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.YES) - self.box_root.bind("", self.text_area.yview_scroll(1, tk.PAGES)) - self.box_root.bind("", self.text_area.yview_scroll(-1, tk.PAGES)) - self.box_root.bind("", self.text_area.xview_scroll(1, tk.PAGES)) - self.box_root.bind("", self.text_area.xview_scroll(-1, tk.PAGES)) - self.box_root.bind("", self.text_area.yview_scroll(1, tk.UNITS)) - self.box_root.bind("", self.text_area.yview_scroll(-1, tk.UNITS)) - self._set_text() + super().__init__(msg, title, callback, monospace=monospace) + self.text_area = self.configure_text_widget(monospace) + self.text = text + self.set_buttons() + def set_buttons(self): buttons_frame = tk.Frame(self.box_root) buttons_frame.pack(side=tk.TOP) cancel_button = tk.Button(buttons_frame, takefocus=tk.YES, text="Cancel", height=1, width=6) cancel_button.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", ipadx="2m") cancel_button.bind("", self.cancel_button_pressed) - cancel_click_handler = MouseClickHandler(callback=self.cancel_button_pressed) - cancel_button.bind("", cancel_click_handler.enter) - cancel_button.bind("", cancel_click_handler.leave) - cancel_button.bind("", cancel_click_handler.release) + bind_to_mouse(cancel_button, self.cancel_button_pressed) ok_button = tk.Button(buttons_frame, takefocus=tk.YES, text="OK", height=1, width=6) ok_button.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", ipadx="2m") ok_button.bind("", self.ok_button_pressed) - ok_click_handler = MouseClickHandler(callback=self.ok_button_pressed) - ok_button.bind("", ok_click_handler.enter) - ok_button.bind("", ok_click_handler.leave) - ok_button.bind("", ok_click_handler.release) + bind_to_mouse(ok_button, self.ok_button_pressed) - @property - def text(self): - """ Get _text which may be None if cancel was pressed""" - return self._text + def configure_text_widget(self, monospace): + padding, width_in_chars = get_width_and_padding(monospace=monospace) - @text.setter - def text(self, text): - self._text = text - if text is not None: # cancel sets text=None but this is meaningless to the tk box content - self._set_text() + text_frame = tk.Frame(self.box_root, padx=padding) + text_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) - @property - def return_value(self): - """ In order to work like all the other boxes, need to define 'return value'""" - return self._text + text_area = tk.Text( + text_frame, + padx=padding, + pady=padding, + height=25, + width=width_in_chars, + wrap=tk.NONE if monospace else tk.WORD, + ) + vertical_scrollbar = tk.Scrollbar(text_frame, orient=tk.VERTICAL, command=text_area.yview) + text_area.configure(yscrollcommand=vertical_scrollbar.set) + horizontal_scrollbar = tk.Scrollbar(text_frame, orient=tk.HORIZONTAL, command=text_area.xview) + text_area.configure(xscrollcommand=horizontal_scrollbar.set) - @return_value.setter - def return_value(self, text): - self.text = text + if monospace: + monospace_font = font.Font(family='Courier') + text_area.configure(font=monospace_font) + # no word-wrapping for code, so we may need a horizontal scroll bar + horizontal_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) + + vertical_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + # pack textArea last so bottom scrollbar displays properly + text_area.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.YES) - def _get_text(self): - """ Used by the callback to get the text_area content""" + self.box_root.bind("", text_area.yview_scroll(1, tk.PAGES)) + self.box_root.bind("", text_area.yview_scroll(-1, tk.PAGES)) + self.box_root.bind("", text_area.xview_scroll(1, tk.PAGES)) + self.box_root.bind("", text_area.xview_scroll(-1, tk.PAGES)) + self.box_root.bind("", text_area.yview_scroll(1, tk.UNITS)) + self.box_root.bind("", text_area.yview_scroll(-1, tk.UNITS)) + return text_area + + @property + def text(self): + """ Get _text which may be None if cancel was pressed""" return self.text_area.get(1.0, 'end-1c') - def _set_text(self): + @text.setter + def text(self, text): self.text_area.delete(1.0, tk.END) - self.text_area.insert(tk.END, self._text, "normal") + self.text_area.insert(tk.END, text, "normal") self.text_area.focus() - def ok_button_pressed(self, _): - self.text = self._get_text() - if self._user_specified_callback: - # If a callback was set, call main process - self._user_specified_callback(self) - else: - self.stop() + def _set_return_value(self): + self.return_value = self.text_area.get(1.0, 'end-1c') if __name__ == '__main__': - result = textbox("test message here .... should wrap if the line goes quite long ... like, really long") + result = textbox("some message text for a textbox") print("textbox() return value was: {}".format(result)) - code_result = codebox("some code goes here should not wrap even if the line becomes really, " - "really, really long") + code_result = codebox("some message text for a codebox") print("textbox() return value was: {}".format(result)) diff --git a/easygui/utilities.py b/easygui/utilities.py index 04c30d7..670e4e3 100644 --- a/easygui/utilities.py +++ b/easygui/utilities.py @@ -16,14 +16,16 @@ class AbstractBox(object): TextBox def __init__(self, msg, title, text, codebox, callback): """ - def __init__(self, msg, title, callback) -> None: + def __init__(self, msg, title, callback, monospace=False) -> None: super().__init__() self._user_specified_callback = callback - self.msg = msg self.box_root = self._configure_box_root(title) - self.message = NotImplemented + self.set_message(msg, monospace) self.return_value = None + def _set_return_value(self): + raise NotImplemented + def _configure_box_root(self, title): box_root = tk.Tk() box_root.title(title) @@ -33,12 +35,32 @@ def _configure_box_root(self, title): box_root.protocol('WM_DELETE_WINDOW', self.cancel_button_pressed) return box_root - def _set_msg_area(self, msg): - self.message.delete(1.0, tk.END) - self.message.insert(tk.END, msg) - line, char = self.message.index(tk.END).split('.') - self.message.configure(height=int(line)) - self.message.update() + def set_message(self, msg, monospace): + """ + # TODO: fix bug that the line count does not include wrapped lines + # height = message.tk.call((self.mess\age._w, "count", "-update", "-displaylines", "1.0", "end")) + :param msg: + :param bool monospace: whether the message shold be monospace or proportional text + :return: + """ + padding, width_in_chars = get_width_and_padding(monospace=monospace) + + message_frame = tk.Frame(self.box_root, padx=padding) + message_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + + message_text = tk.Text( + master=message_frame, + width=width_in_chars, + padx=padding, + pady=padding, + wrap=tk.WORD, + # wrap=tk.NONE if monospace else tk.WORD + ) + message_text.insert(tk.END, msg) + message_text.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + line, char = message_text.index(tk.END).split('.') + message_text.configure(height=int(line)) + message_text.configure(state=tk.DISABLED) def cancel_button_pressed(self, *args): """ @@ -52,6 +74,14 @@ def cancel_button_pressed(self, *args): self.return_value = None self.box_root.quit() + def ok_button_pressed(self, _): + self._set_return_value() + if self._user_specified_callback: + # If a callback was set, call main process + self._user_specified_callback(self) + else: + self.stop() + def run(self): self.box_root.mainloop() self.box_root.destroy() @@ -186,3 +216,10 @@ def leave(self, _): def release(self, event): if self._mouse_is_on_button: return self._callback(event) + + +def bind_to_mouse(button, callback): + handler = MouseClickHandler(callback=callback) + button.bind("", handler.enter) + button.bind("", handler.leave) + button.bind("", handler.release) From 3011550e4428907424c4cc865d03ef40dc1161b7 Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Wed, 4 May 2022 07:46:05 +0100 Subject: [PATCH 19/29] More changes to text_box: * expose the message (for future updates, if needed) * pull the button setting up to the AbstractBox --- easygui/button_box.py | 7 ++----- easygui/text_box.py | 14 -------------- easygui/utilities.py | 40 ++++++++++++++++++++++++++++++++-------- tests/test_easygui.py | 2 +- tests/test_text_box.py | 19 ++++++++++--------- 5 files changed, 45 insertions(+), 37 deletions(-) diff --git a/easygui/button_box.py b/easygui/button_box.py index 88b3035..23c73f9 100644 --- a/easygui/button_box.py +++ b/easygui/button_box.py @@ -204,15 +204,12 @@ def __init__(self, msg, title, choices, images, default_choice, cancel_choice, c self._images = [] self._buttons = [] - self.message_area = self._configure_message_area(box_root=self.box_root) - self._set_msg_area('' if msg is None else msg) self.images_frame = self._create_images_frame(images) self.buttons_frame = self._create_buttons_frame(choices, default_choice) - @staticmethod - def _configure_message_area(box_root): + def configure_message_widget(self, monospace): padding, width_in_chars = get_width_and_padding(monospace=False) - message_frame = tk.Frame(box_root, padx=padding) + message_frame = tk.Frame(self.box_root, padx=padding) message_frame.grid() message_area = tk.Text(master=message_frame, width=width_in_chars, padx=padding, pady=padding, wrap=tk.WORD) message_area.grid() diff --git a/easygui/text_box.py b/easygui/text_box.py index a9beee4..2b8826f 100644 --- a/easygui/text_box.py +++ b/easygui/text_box.py @@ -69,20 +69,6 @@ def __init__(self, msg, title, text, monospace, callback=None): self.text = text self.set_buttons() - def set_buttons(self): - buttons_frame = tk.Frame(self.box_root) - buttons_frame.pack(side=tk.TOP) - - cancel_button = tk.Button(buttons_frame, takefocus=tk.YES, text="Cancel", height=1, width=6) - cancel_button.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", ipadx="2m") - cancel_button.bind("", self.cancel_button_pressed) - bind_to_mouse(cancel_button, self.cancel_button_pressed) - - ok_button = tk.Button(buttons_frame, takefocus=tk.YES, text="OK", height=1, width=6) - ok_button.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", ipadx="2m") - ok_button.bind("", self.ok_button_pressed) - bind_to_mouse(ok_button, self.ok_button_pressed) - def configure_text_widget(self, monospace): padding, width_in_chars = get_width_and_padding(monospace=monospace) diff --git a/easygui/utilities.py b/easygui/utilities.py index 670e4e3..3e879de 100644 --- a/easygui/utilities.py +++ b/easygui/utilities.py @@ -20,7 +20,8 @@ def __init__(self, msg, title, callback, monospace=False) -> None: super().__init__() self._user_specified_callback = callback self.box_root = self._configure_box_root(title) - self.set_message(msg, monospace) + self.msg_widget = self.configure_message_widget(monospace) + self.msg = msg self.return_value = None def _set_return_value(self): @@ -35,11 +36,10 @@ def _configure_box_root(self, title): box_root.protocol('WM_DELETE_WINDOW', self.cancel_button_pressed) return box_root - def set_message(self, msg, monospace): + def configure_message_widget(self, monospace): """ # TODO: fix bug that the line count does not include wrapped lines # height = message.tk.call((self.mess\age._w, "count", "-update", "-displaylines", "1.0", "end")) - :param msg: :param bool monospace: whether the message shold be monospace or proportional text :return: """ @@ -56,11 +56,35 @@ def set_message(self, msg, monospace): wrap=tk.WORD, # wrap=tk.NONE if monospace else tk.WORD ) - message_text.insert(tk.END, msg) - message_text.pack(side=tk.TOP, fill=tk.BOTH, expand=True) - line, char = message_text.index(tk.END).split('.') - message_text.configure(height=int(line)) - message_text.configure(state=tk.DISABLED) + return message_text + + @property + def msg(self): + return self.msg_widget.get(1.0, tk.END) + + @msg.setter + def msg(self, message): + self.msg_widget.configure(state=tk.NORMAL) + self.msg_widget.delete(1.0, tk.END) + self.msg_widget.insert(tk.END, message) + self.msg_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + line, char = self.msg_widget.index(tk.END).split('.') + self.msg_widget.configure(height=int(line)) + self.msg_widget.configure(state=tk.DISABLED) + + def set_buttons(self): + buttons_frame = tk.Frame(self.box_root) + buttons_frame.pack(side=tk.TOP) + + cancel_button = tk.Button(buttons_frame, takefocus=tk.YES, text="Cancel", height=1, width=6) + cancel_button.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", ipadx="2m") + cancel_button.bind("", self.cancel_button_pressed) + bind_to_mouse(cancel_button, self.cancel_button_pressed) + + ok_button = tk.Button(buttons_frame, takefocus=tk.YES, text="OK", height=1, width=6) + ok_button.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", ipadx="2m") + ok_button.bind("", self.ok_button_pressed) + bind_to_mouse(ok_button, self.ok_button_pressed) def cancel_button_pressed(self, *args): """ diff --git a/tests/test_easygui.py b/tests/test_easygui.py index 4f85465..344c639 100644 --- a/tests/test_easygui.py +++ b/tests/test_easygui.py @@ -36,11 +36,11 @@ def run(self): KEYBOARD.type(self.keyPresses) - def test_test_images_exist(): assert os.path.exists(os.path.join(FOLDER_OF_THIS_FILE, '../demos/pi.jpg')) assert os.path.exists(os.path.join(FOLDER_OF_THIS_FILE, '../demos/result.png')) + def test_spacebar_clicks_choice(): """ Test that the spacebar selects a choice. diff --git a/tests/test_text_box.py b/tests/test_text_box.py index bf00ae0..23c8732 100644 --- a/tests/test_text_box.py +++ b/tests/test_text_box.py @@ -1,5 +1,6 @@ import tkinter as tk import unittest +from tkinter import font from mock import patch, Mock, ANY @@ -43,22 +44,22 @@ def setUp(self): def test_instantiation(self): # Instance attributes should be configured: self.assertEqual(self.tb.text, TEST_TEXT) - self.assertEqual(self.tb.msg, TEST_MESSAGE) + self.assertEqual(self.tb.msg.strip(), TEST_MESSAGE) self.assertEqual(self.tb._user_specified_callback, TEST_CALLBACK) # The following Tk widgets should also have been created: isinstance(self.tb.box_root, tk.Tk) - isinstance(self.tb.message, tk.Tk) + isinstance(self.tb.msg_widget, tk.Tk) isinstance(self.tb.text_area, tk.Tk) # And configured: - self.assertEqual(self.tb.message.get(0.0, 'end-1c'), TEST_MESSAGE) + self.assertEqual(self.tb.msg_widget.get(0.0, 'end-1c'), TEST_MESSAGE) self.assertEqual(self.tb.text_area.get(0.0, 'end-1c'), TEST_TEXT) def test_run(self): self.tb.box_root = Mock() return_value = self.tb.run() - self.assertEqual(return_value, TEST_TEXT) + self.assertEqual(return_value, None ) self.tb.box_root.mainloop.assert_called_once_with() self.tb.box_root.destroy.assert_called_once_with() @@ -69,11 +70,11 @@ def test_stop(self): def test_set_msg_area(self): new_msg = 'some new text' - self.tb._set_msg_area(msg=new_msg) - self.assertEqual(self.tb.message.get(1.0, 'end-1c'), new_msg) + self.tb.msg = new_msg + self.assertEqual(self.tb.msg_widget.get(1.0, 'end-1c'), new_msg) def test_get_text(self): - actual = self.tb._get_text() + actual = self.tb.text self.assertEqual(actual, TEST_TEXT) def test_set_text(self): @@ -112,7 +113,7 @@ def stop_running(tb_instance): self.assertEqual(actual, TEST_TEXT) def test_textbox_ok_pressed_with_no_user_defined_callback(self): - tb = textbox(msg=TEST_MESSAGE, title=TEST_TITLE, text=TEST_TEXT, callback=TEST_CALLBACK, run=False) + tb = textbox(msg=TEST_MESSAGE, title=TEST_TITLE, text=TEST_TEXT, run=False) def simulate_ok_button_pressed(tb_instance): tb_instance.ok_button_pressed('ignored button handler arg') @@ -131,4 +132,4 @@ def test_instantiation_codebox(self): cb = codebox(msg=TEST_MESSAGE, title=TEST_TITLE, text=TEST_TEXT * 100, callback=TEST_CALLBACK, run=False) # cget returns strings so the monospace assertion is a bit messy: - self.assertEqual(cb.text_area.cget('font'), str(cb.MONOSPACE_FONT)) + self.assertEqual(cb.text_area.cget('font'), "font1") # a monospace font From 2dae2145b52390b4641326a4fa73feb9f0b6ce29 Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Wed, 4 May 2022 08:09:31 +0100 Subject: [PATCH 20/29] refactor multi_fillable_box --- easygui/multi_fillable_box.py | 86 ++++++++++++----------------------- 1 file changed, 28 insertions(+), 58 deletions(-) diff --git a/easygui/multi_fillable_box.py b/easygui/multi_fillable_box.py index 6b492e9..0526602 100644 --- a/easygui/multi_fillable_box.py +++ b/easygui/multi_fillable_box.py @@ -1,8 +1,8 @@ import tkinter as tk +from itertools import zip_longest -from easygui.global_state import PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE, \ - TEXT_ENTRY_FONT_SIZE -from easygui.utilities import MouseClickHandler, AbstractBox +from easygui.global_state import PROPORTIONAL_FONT_FAMILY, TEXT_ENTRY_FONT_SIZE +from easygui.utilities import AbstractBox def multpasswordbox(msg="Fill in values for the fields.", title=" ", fields=None, values=None, callback=None, run=True): @@ -14,6 +14,8 @@ def multpasswordbox(msg="Fill in values for the fields.", title=" ", fields=None :param str title: the window title :param list fields: a list of fieldnames. :param list values: a list of field values + :param callback: user specified callback to use when 'ok' is pressed + :param run: whether to run or not when called :return: String """ mb = MultiBox(msg, title, fields, values, mask_last=True, callback=callback) @@ -28,6 +30,8 @@ def multenterbox(msg="Fill in values for the fields.", title=" ", fields=None, v :param str title: the window title :param list fields: a list of fieldnames. :param list values: a list of field values + :param callback: user specified callback to use when 'ok' is pressed + :param run: whether to run or not when called :return: String """ mb = MultiBox(msg, title, fields, values, mask_last=False, callback=callback) @@ -37,67 +41,33 @@ def multenterbox(msg="Fill in values for the fields.", title=" ", fields=None, v class MultiBox(AbstractBox): def __init__(self, msg, title, fields=None, values=None, mask_last=False, callback=None): super().__init__(msg, title, callback) + self.entry_widgets = [] + self.configure_entry_widgets(fields, values, mask_last) + self.set_buttons() + self.entry_widgets[0].focus_force() # put the focus on the first entry widget - self.fields, self.return_value = self._process_fields_and_values(fields, values) - - message_widget = tk.Message(self.box_root, width="4.5i", text=msg) - message_widget.configure(font=(PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE)) - message_widget.pack(side=tk.TOP, expand=1, fill=tk.BOTH, padx='3m', pady='3m') + def configure_entry_widgets(self, fields, values, mask_last): + assert len(fields) <= len(values), "There are fewer fields than values! Values can be blank but fields cannot." + for field, value in zip_longest(fields, values, fillvalue=""): - self.entry_widgets = [] - for field, value in zip(self.fields, self.return_value): - entry_frame = tk.Frame(master=self.box_root) - entry_frame.pack(side=tk.TOP, fill=tk.BOTH) + frame = tk.Frame(master=self.box_root) + frame.pack(side=tk.TOP, fill=tk.BOTH) - label_widget = tk.Label(entry_frame, text=field) + label_widget = tk.Label(frame, text=field) label_widget.pack(side=tk.LEFT) - entry_widget = tk.Entry(entry_frame, width=40, highlightthickness=2) - self.entry_widgets.append(entry_widget) + entry_widget = tk.Entry(frame, width=40, highlightthickness=2) entry_widget.configure(font=(PROPORTIONAL_FONT_FAMILY, TEXT_ENTRY_FONT_SIZE)) entry_widget.pack(side=tk.RIGHT, padx="3m") - entry_widget.bind("", self._ok_pressed) - entry_widget.bind("", self.cancel_button_pressed) entry_widget.insert(0, '' if value is None else value) + if mask_last: + entry_widget.configure(show="*") + self.entry_widgets.append(entry_widget) + + def _set_return_value(self): + self.return_value = [widget.get() for widget in self.entry_widgets] + - if mask_last: - self.entry_widgets[-1].configure(show="*") - - buttons_frame = tk.Frame(master=self.box_root) - buttons_frame.pack(side=tk.BOTTOM) - - cancel_button = tk.Button(buttons_frame, takefocus=1, text="Cancel") - cancel_button.pack(expand=1, side=tk.LEFT, padx='3m', pady='3m', ipadx='2m', ipady='1m') - cancel_button.bind("", self.cancel_button_pressed) - cancel_click_handler = MouseClickHandler(callback=self.cancel_button_pressed) - cancel_button.bind("", cancel_click_handler.enter) - cancel_button.bind("", cancel_click_handler.leave) - cancel_button.bind("", cancel_click_handler.release) - - ok_button = tk.Button(buttons_frame, takefocus=1, text="OK") - ok_button.pack(expand=1, side=tk.LEFT, padx='3m', pady='3m', ipadx='2m', ipady='1m') - ok_button.bind("", self._ok_pressed) - ok_click_handler = MouseClickHandler(callback=self._ok_pressed) - ok_button.bind("", ok_click_handler.enter) - ok_button.bind("", ok_click_handler.leave) - ok_button.bind("", ok_click_handler.release) - - self.entry_widgets[0].focus_force() # put the focus on the entry_widget - - def _ok_pressed(self, _): - self.return_value = self._get_values() - if self._user_specified_callback: - self._user_specified_callback(self) - self.box_root.quit() - - def _get_values(self): - return [widget.get() for widget in self.entry_widgets] - - @staticmethod - def _process_fields_and_values(fields, values): - fields = [] if fields is None else list(fields) - values = [] if values is None else list(values) - padding_required = len(fields) - len(values) - if padding_required > 0: - values.extend([""] * padding_required) - return fields, values +if __name__ == '__main__': + result = multenterbox(msg="example message", title="example title", fields=["1", "2", "3"], values=["a", "b", "c"]) + print(f"Return value: {result}") From aaf3b3babfd7b00a7fad941beca430cef8f5391a Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Thu, 5 May 2022 21:34:01 +0100 Subject: [PATCH 21/29] refactor fillable_box.py requires pulling out msg setting so that we can (for this box) put an image at the top and set msg underneath, which is hard to do if the msg setting happens in the super().__init__() method --- easygui/button_box.py | 6 +- easygui/choice_box.py | 4 +- easygui/fillable_box.py | 116 ++++++++++++---------------------- easygui/multi_fillable_box.py | 4 +- easygui/text_box.py | 4 +- easygui/utilities.py | 5 +- 6 files changed, 57 insertions(+), 82 deletions(-) diff --git a/easygui/button_box.py b/easygui/button_box.py index 23c73f9..145e6ad 100644 --- a/easygui/button_box.py +++ b/easygui/button_box.py @@ -198,7 +198,9 @@ def __init__(self, msg, title, choices, images, default_choice, cancel_choice, c """ - super().__init__(msg, title, callback) + super().__init__(title, callback) + self.msg_widget = self.configure_message_widget(monospace=False) + self.msg = msg self._text_to_return_on_cancel = cancel_choice self.return_value = None self._images = [] @@ -312,7 +314,7 @@ def _hotkey_pressed(self, event=None): for button in self._buttons: if button['hotkey'] == hotkey_pressed: - self._callback(command='update') + self._user_specified_callback(command='update') self.return_value = button['original_text'] return # some key was pressed, but no hotkey registered to it diff --git a/easygui/choice_box.py b/easygui/choice_box.py index cf33175..dba137a 100644 --- a/easygui/choice_box.py +++ b/easygui/choice_box.py @@ -55,7 +55,9 @@ def multchoicebox(msg="Pick an item", title="", choices=None, preselect=(), call class ChoiceBox(AbstractBox): def __init__(self, msg, title, choices, preselect, multiple_select, callback): - super().__init__(msg, title, callback) + super().__init__( title, callback) + self.msg_widget = self.configure_message_widget(monospace=False) + self.msg = msg if len(preselect) > 1 and not multiple_select: raise ValueError("Multiple selections not allowed, yet preselect has multiple values:{}".format(preselect)) diff --git a/easygui/fillable_box.py b/easygui/fillable_box.py index 459443d..42f5602 100644 --- a/easygui/fillable_box.py +++ b/easygui/fillable_box.py @@ -3,10 +3,10 @@ from easygui import msgbox from easygui.global_state import PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE, \ TEXT_ENTRY_FONT_SIZE -from easygui.utilities import load_tk_image, MouseClickHandler, AbstractBox +from easygui.utilities import load_tk_image, MouseClickHandler, AbstractBox, get_width_and_padding -def integerbox(msg=None, title=" ", default=None, lowerbound=0, upperbound=99, image=None, root=None): +def integerbox(msg=None, title=" ", default=None, lowerbound=0, upperbound=99, image=None): """ Show a box in which a user can enter an integer. @@ -37,7 +37,7 @@ def integerbox(msg=None, title=" ", default=None, lowerbound=0, upperbound=99, i msg = "Enter an integer between {0} and {1}".format(lowerbound, upperbound) if msg is None else msg while True: - result = FillableBox(msg, default, title, image=image, root=root).run() + result = FillableBox(msg, default, title, image=image).run() if result is None: return None @@ -55,7 +55,7 @@ def integerbox(msg=None, title=" ", default=None, lowerbound=0, upperbound=99, i return result # validation passed! -def enterbox(msg="Enter something.", title=" ", default="", strip=True, image=None, root=None): +def enterbox(msg="Enter something.", title=" ", default="", strip=True, image=None): """ Show a box in which a user can enter some text. @@ -79,13 +79,13 @@ def enterbox(msg="Enter something.", title=" ", default="", strip=True, image=No :return: the text that the user entered, or None if they cancel the operation. """ - result = FillableBox(msg, default, title, image=image, root=root).run() + result = FillableBox(msg, default, title, image=image).run() if result and strip: result = result.strip() return result -def passwordbox(msg="Enter your password.", title="", default="", image=None, root=None): +def passwordbox(msg="Enter your password.", title="", default="", image=None): """ Show a box in which a user can enter a password. The text is masked with asterisks, so the password is not displayed. @@ -96,10 +96,10 @@ def passwordbox(msg="Enter your password.", title="", default="", image=None, ro :return: the text that the user entered, or None if they cancel the operation. """ - return FillableBox(msg, default, title, mask="*", image=image, root=root).run() + return FillableBox(msg, default, title, mask="*", image=image).run() -def fillablebox(msg, title="", default=None, mask=None, image=None, root=None): +def fillablebox(msg, title="", default=None, mask=None, image=None): """ Show a box in which a user can enter some text. :param str msg: the msg to be displayed. @@ -107,86 +107,54 @@ def fillablebox(msg, title="", default=None, mask=None, image=None, root=None): :param str default: default value populated, returned if user does not change it :return: the text that the user entered, or None if he cancels the operation. """ - return FillableBox(msg, default, title, mask, image, root).run() + return FillableBox(msg, default, title, mask, image).run() class FillableBox(AbstractBox): - def __init__(self, msg, title, default, mask=None, image=None, root=None): - if root: - root.withdraw() - self.box_root = tk.Toplevel(master=root) - self.box_root.withdraw() - super().__init__(msg, title, callback=None) - self.return_value = '' if default is None else default - self.pre_existing_root = root - self.entry_widget = None - - message_frame = tk.Frame(master=self.box_root) - message_frame.pack(side=tk.TOP, fill=tk.BOTH) + def __init__(self, msg, title, default, mask=None, image=None): + super().__init__(title, callback=None) + self.return_value = default - try: - tk_image = load_tk_image(image) - except Exception as e: - print(e) - tk_image = None - if tk_image: - image_frame = tk.Frame(master=self.box_root) - image_frame.pack(side=tk.TOP, fill=tk.BOTH) - label = tk.Label(image_frame, image=tk_image) - label.image = tk_image # keep a reference! - label.pack(side=tk.TOP, expand=tk.YES, fill=tk.X, padx='1m', pady='1m') - - buttons_frame = tk.Frame(master=self.box_root) - buttons_frame.pack(side=tk.TOP, fill=tk.BOTH) + self.configure_image(image) - entry_frame = tk.Frame(master=self.box_root) - entry_frame.pack(side=tk.TOP, fill=tk.BOTH) + self.msg_widget = self.configure_message_widget(monospace=False) + self.msg = msg - buttons_frame = tk.Frame(master=self.box_root) - buttons_frame.pack(side=tk.TOP, fill=tk.BOTH) + self.entry_widget = self.conigure_entry_widget(mask) + self.entry_widget.focus_force() # put the focus on the self.entry_widget - message_widget = tk.Message(message_frame, width="4.5i", text=msg) - message_widget.configure(font=(PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE)) - message_widget.pack(side=tk.RIGHT, expand=1, fill=tk.BOTH, padx='3m', pady='3m') + self.set_buttons() + self.box_root.deiconify() - entry_widget = tk.Entry(entry_frame, width=40) + def conigure_entry_widget(self, mask): + padding, width_in_chars = get_width_and_padding(monospace=False) + entry_frame = tk.Frame(master=self.box_root) + entry_frame.pack(side=tk.TOP, fill=tk.BOTH) + entry_widget = tk.Entry(entry_frame, width=width_in_chars) entry_widget.configure(font=(PROPORTIONAL_FONT_FAMILY, TEXT_ENTRY_FONT_SIZE)) if mask: entry_widget.configure(show=mask) - entry_widget.pack(side=tk.LEFT, padx="3m") - entry_widget.bind("", self._ok_pressed) - entry_widget.bind("", self.cancel_button_pressed) + entry_widget.pack(side=tk.LEFT, padx=padding) entry_widget.insert(0, self.return_value) # put text into the entry_widget - self.entry_widget = entry_widget # save a reference - we need to get text from this widget later - - ok_button = tk.Button(buttons_frame, takefocus=1, text="OK") - ok_button.pack(expand=1, side=tk.LEFT, padx='3m', pady='3m', ipadx='2m', ipady='1m') - ok_button.bind("", self._ok_pressed) - ok_click_handler = MouseClickHandler(callback=self._ok_pressed) - ok_button.bind("", ok_click_handler.enter) - ok_button.bind("", ok_click_handler.leave) - ok_button.bind("", ok_click_handler.release) - - cancel_button = tk.Button(buttons_frame, takefocus=1, text="Cancel") - cancel_button.pack(expand=1, side=tk.RIGHT, padx='3m', pady='3m', ipadx='2m', ipady='1m') - cancel_button.bind("", self.cancel_button_pressed) - cancel_click_handler = MouseClickHandler(callback=self.cancel_button_pressed) - cancel_button.bind("", cancel_click_handler.enter) - cancel_button.bind("", cancel_click_handler.leave) - cancel_button.bind("", cancel_click_handler.release) + return entry_widget - self.entry_widget.focus_force() # put the focus on the self.entry_widget - self.box_root.deiconify() + def configure_image(self, image): + image_frame = tk.Frame(master=self.box_root) + image_frame.pack(side=tk.TOP, fill=tk.BOTH) + tk_image = load_tk_image(image) + label = tk.Label(image_frame, image=tk_image) + label.image = tk_image # keep a reference! + label.pack(side=tk.TOP, expand=tk.YES, fill=tk.X, padx='1m', pady='1m') - def _ok_pressed(self, *args): + def _set_return_value(self): self.return_value = self.entry_widget.get() - self.box_root.quit() - def run(self): - self.box_root.mainloop() # run it! - # -------- after the run has completed ---------------------------------- - if self.pre_existing_root: - self.pre_existing_root.deiconify() - self.box_root.destroy() # button_click didn't destroy self.boxRoot, so we do it now - return self.return_value +if __name__ == '__main__': + fillablebox( + msg="message", + title="title", + default="blah", + mask="*", + image=".\\..\\demos\\images\\dave.gif" + ) \ No newline at end of file diff --git a/easygui/multi_fillable_box.py b/easygui/multi_fillable_box.py index 0526602..b775a27 100644 --- a/easygui/multi_fillable_box.py +++ b/easygui/multi_fillable_box.py @@ -40,7 +40,9 @@ def multenterbox(msg="Fill in values for the fields.", title=" ", fields=None, v class MultiBox(AbstractBox): def __init__(self, msg, title, fields=None, values=None, mask_last=False, callback=None): - super().__init__(msg, title, callback) + super().__init__(title, callback) + self.msg_widget = self.configure_message_widget(monospace=False) + self.msg = msg self.entry_widgets = [] self.configure_entry_widgets(fields, values, mask_last) self.set_buttons() diff --git a/easygui/text_box.py b/easygui/text_box.py index 2b8826f..a9e9332 100644 --- a/easygui/text_box.py +++ b/easygui/text_box.py @@ -64,7 +64,9 @@ def __init__(self, msg, title, text, monospace, callback=None): :param monospace: bool (if true) don't wrap, set width to 80 witdh_in_chars, use monospace font :param callback: optional function to be called when OK is pressed """ - super().__init__(msg, title, callback, monospace=monospace) + super().__init__(title, callback) + self.msg_widget = self.configure_message_widget(monospace) + self.msg = msg self.text_area = self.configure_text_widget(monospace) self.text = text self.set_buttons() diff --git a/easygui/utilities.py b/easygui/utilities.py index 3e879de..9db13ed 100644 --- a/easygui/utilities.py +++ b/easygui/utilities.py @@ -16,13 +16,12 @@ class AbstractBox(object): TextBox def __init__(self, msg, title, text, codebox, callback): """ - def __init__(self, msg, title, callback, monospace=False) -> None: + def __init__(self, title, callback) -> None: super().__init__() self._user_specified_callback = callback self.box_root = self._configure_box_root(title) - self.msg_widget = self.configure_message_widget(monospace) - self.msg = msg self.return_value = None + self.msg_widget = NotImplemented def _set_return_value(self): raise NotImplemented From df3e1de162286c7a77cfcf1bda0978d24807428f Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Thu, 5 May 2022 22:14:11 +0100 Subject: [PATCH 22/29] refactor choice_box.py - including removal of the (unused?) keyboard listener --- easygui/choice_box.py | 262 +++++++++++------------------------------- 1 file changed, 68 insertions(+), 194 deletions(-) diff --git a/easygui/choice_box.py b/easygui/choice_box.py index dba137a..1780612 100644 --- a/easygui/choice_box.py +++ b/easygui/choice_box.py @@ -1,10 +1,9 @@ -import string import tkinter as tk -from easygui.utilities import get_width_and_padding, MouseClickHandler, AbstractBox +from easygui.utilities import AbstractBox, bind_to_mouse -def choicebox(msg="Pick an item", title="", choices=None, preselect=(), callback=None, run=True): +def choicebox(msg="Pick an item", title="", choices=('Choice 1', 'Choice 2'), preselect=(), callback=None, run=True): """ Present the user with a list of choices. return the choice that he selects. @@ -21,7 +20,8 @@ def choicebox(msg="Pick an item", title="", choices=None, preselect=(), callback return cb.run() if run else cb -def multchoicebox(msg="Pick an item", title="", choices=None, preselect=(), callback=None, run=True): +def multchoicebox(msg="Pick an item", title="", choices=('Choice 1', 'Choice 2'), preselect=(), callback=None, + run=True): """ The ``multchoicebox()`` function provides a way for a user to select from a list of choices. The interface looks just like the ``choicebox()`` @@ -55,230 +55,104 @@ def multchoicebox(msg="Pick an item", title="", choices=None, preselect=(), call class ChoiceBox(AbstractBox): def __init__(self, msg, title, choices, preselect, multiple_select, callback): - super().__init__( title, callback) + super().__init__(title, callback) + assert multiple_select or len(preselect) <= 1, "multiple_select needs to be True for multiple preselect use " + self.msg_widget = self.configure_message_widget(monospace=False) self.msg = msg - if len(preselect) > 1 and not multiple_select: - raise ValueError("Multiple selections not allowed, yet preselect has multiple values:{}".format(preselect)) self._multiple_select = multiple_select - if choices is None: - # Use default choice selections if none were specified: - choices = ('Choice 1', 'Choice 2') - self.return_value = [str(c) for c in choices] - - self.message_area = self._configure_message_area(self.box_root) - self._set_msg_area("" if msg is None else msg) + self.choices = [str(c) for c in choices] - self.create_choice_area() + self.choicebox_widget = self.create_choice_area() + self.buttonsFrame = tk.Frame(self.box_root) + self.buttonsFrame.pack(side=tk.TOP, expand=tk.YES, pady=0) self.create_ok_button() self.create_cancel_button() - self.create_special_buttons() - self.preselect_choice(preselect) - self.choiceboxWidget.focus_force() + if self._multiple_select: + self.create_special_buttons() - def ok_button_pressed(self, event): - self.return_value = self.get_choices() - if self._user_specified_callback: - # If a _user_specified_callback was set, call main process - self._user_specified_callback(self) - else: - self.stop() + self.preselect_choice(preselect) + self.choicebox_widget.focus_force() def preselect_choice(self, preselect): - if preselect != None: - for v in preselect: - self.choiceboxWidget.select_set(v) - self.choiceboxWidget.activate(v) - - def get_choices(self): - choices_index = self.choiceboxWidget.curselection() - if not choices_index: - return None + for v in preselect: + self.choicebox_widget.select_set(v) + self.choicebox_widget.activate(v) + + def _set_return_value(self): + choices_index = self.choicebox_widget.curselection() if self._multiple_select: - selected_choices = [self.choiceboxWidget.get(index) - for index in choices_index] + self.return_value = [self.choicebox_widget.get(index) for index in choices_index] else: - selected_choices = self.choiceboxWidget.get(choices_index) - - return selected_choices - - @staticmethod - def _configure_message_area(box_root): - padding, width_in_chars = get_width_and_padding(monospace=False) - - message_frame = tk.Frame(box_root, padx=padding) - message_frame.pack(side=tk.TOP, expand=1, fill='both') - - message_area = tk.Text(master=message_frame, - width=width_in_chars, - state=tk.DISABLED, - background=box_root.config()["background"][-1], - relief='flat', - padx=padding, - pady=padding, - wrap=tk.WORD) - message_area.pack(side=tk.TOP, expand=1, fill='both') - return message_area + self.return_value = self.choicebox_widget.get(choices_index) def create_choice_area(self): + choicebox_frame = tk.Frame(master=self.box_root) + choicebox_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.YES) - self.choiceboxFrame = tk.Frame(master=self.box_root) - self.choiceboxFrame.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.YES) - - lines_to_show = min(len(self.return_value), 20) - - # -------- put the self.choiceboxWidget in the self.choiceboxFrame --- - self.choiceboxWidget = tk.Listbox(self.choiceboxFrame, - height=lines_to_show, - borderwidth="1m", relief="flat", - bg="white" - ) - - if self._multiple_select: - self.choiceboxWidget.configure(selectmode=tk.MULTIPLE) - - # self.choiceboxWidget.configure(font=(global_state.PROPORTIONAL_FONT_FAMILY, - # global_state.PROPORTIONAL_FONT_SIZE)) + choicebox_widget = tk.Listbox( + master=choicebox_frame, + height=(min(len(self.choices), 20)), + borderwidth="1m", relief="flat", + bg="white", + selectmode=tk.MULTIPLE if self._multiple_select else tk.SINGLE, + ) # add a vertical scrollbar to the frame - rightScrollbar = tk.Scrollbar(self.choiceboxFrame, orient=tk.VERTICAL, - command=self.choiceboxWidget.yview) - self.choiceboxWidget.configure(yscrollcommand=rightScrollbar.set) + vertical_scrollbar = tk.Scrollbar(choicebox_frame, orient=tk.VERTICAL, command=choicebox_widget.yview) + choicebox_widget.configure(yscrollcommand=vertical_scrollbar.set) # add a horizontal scrollbar to the frame - bottomScrollbar = tk.Scrollbar(self.choiceboxFrame, - orient=tk.HORIZONTAL, - command=self.choiceboxWidget.xview) - self.choiceboxWidget.configure(xscrollcommand=bottomScrollbar.set) - - # pack the Listbox and the scrollbars. - # Note that although we must define - # the textArea first, we must pack it last, - # so that the bottomScrollbar will - # be located properly. + horizontal_scrollbar = tk.Scrollbar(choicebox_frame, orient=tk.HORIZONTAL, command=choicebox_widget.xview) + choicebox_widget.configure(xscrollcommand=horizontal_scrollbar.set) - bottomScrollbar.pack(side=tk.BOTTOM, fill=tk.X) - rightScrollbar.pack(side=tk.RIGHT, fill=tk.Y) - - self.choiceboxWidget.pack( - side=tk.LEFT, padx="1m", pady="1m", expand=tk.YES, fill=tk.BOTH) + # pack everything - order is important so that horizontal scrollbar displays correctly + horizontal_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) + vertical_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + choicebox_widget.pack(side=tk.LEFT, padx="1m", pady="1m", expand=tk.YES, fill=tk.BOTH) # Insert choices widgets - for choice in self.return_value: - self.choiceboxWidget.insert(tk.END, choice) + for choice in self.choices: + choicebox_widget.insert(tk.END, choice) # Bind the keyboard events - self.choiceboxWidget.bind("", self.ok_button_pressed) - self.choiceboxWidget.bind("", - self.ok_button_pressed) + choicebox_widget.bind("", self.ok_button_pressed) + return choicebox_widget def create_ok_button(self): + ok_button = tk.Button(self.buttonsFrame, takefocus=tk.YES, text="OK", height=1, width=6) + ok_button.pack(expand=tk.NO, side=tk.RIGHT, padx='2m', pady='1m', ipady="1m", ipadx="2m") + ok_button.bind("", self.ok_button_pressed) + ok_button.bind("", self.ok_button_pressed) + bind_to_mouse(ok_button, self.ok_button_pressed) - self.buttonsFrame = tk.Frame(self.box_root) - self.buttonsFrame.pack(side=tk.TOP, expand=tk.YES, pady=0) - - # put the buttons in the self.buttonsFrame - okButton = tk.Button(self.buttonsFrame, takefocus=tk.YES, - text="OK", height=1, width=6) - okButton.pack(expand=tk.NO, side=tk.RIGHT, padx='2m', pady='1m', - ipady="1m", ipadx="2m") + def create_cancel_button(self): + cancel_button = tk.Button(self.buttonsFrame, takefocus=tk.YES, text="Cancel", height=1, width=6) + cancel_button.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", ipadx="2m") + cancel_button.bind("", self.cancel_button_pressed) + bind_to_mouse(cancel_button, self.cancel_button_pressed) - # for the commandButton, bind activation events - okButton.bind("", self.ok_button_pressed) - okButton.bind("", self.ok_button_pressed) + def create_special_buttons(self): + # add special buttons for multiple select features + select_all_button = tk.Button(self.buttonsFrame, text="Select All", height=1, width=6) + select_all_button.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", ipadx="2m") + bind_to_mouse(select_all_button, self.select_all) - ok_click_handler = MouseClickHandler(callback=self.ok_button_pressed) - okButton.bind("", ok_click_handler.enter) - okButton.bind("", ok_click_handler.leave) - okButton.bind("", ok_click_handler.release) + clear_all_button = tk.Button(self.buttonsFrame, text="Clear All", height=1, width=6) + clear_all_button.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", ipadx="2m") + bind_to_mouse(clear_all_button, self.clear_all) - def create_cancel_button(self): - cancelButton = tk.Button(self.buttonsFrame, takefocus=tk.YES, - text="Cancel", height=1, width=6) - cancelButton.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', - ipady="1m", ipadx="2m") - cancelButton.bind("", self.cancel_button_pressed) + def clear_all(self, event): + self.choicebox_widget.selection_clear(0, len(self.choices) - 1) - cancel_click_handler = MouseClickHandler(callback=self.cancel_button_pressed) - cancelButton.bind("", cancel_click_handler.enter) - cancelButton.bind("", cancel_click_handler.leave) - cancelButton.bind("", cancel_click_handler.release) + def select_all(self, event): + self.choicebox_widget.selection_set(0, len(self.choices) - 1) - def create_special_buttons(self): - # add special buttons for multiple select features - if not self._multiple_select: - return - - selectAllButton = tk.Button( - self.buttonsFrame, text="Select All", height=1, width=6) - selectAllButton.pack(expand=tk.NO, side=tk.LEFT, padx='2m', - pady='1m', - ipady="1m", ipadx="2m") - - clearAllButton = tk.Button(self.buttonsFrame, text="Clear All", - height=1, width=6) - clearAllButton.pack(expand=tk.NO, side=tk.LEFT, - padx='2m', pady='1m', - ipady="1m", ipadx="2m") - - selectAllButton.bind("", self.choiceboxSelectAll) - clearAllButton.bind("", self.choiceboxClearAll) - - def KeyboardListener(self, event): - key = event.keysym - if len(key) <= 1: - if key in string.printable: - # Find the key in the liglobal_state. - # before we clear the list, remember the selected member - try: - start_n = int(self.choiceboxWidget.curselection()[0]) - except IndexError: - start_n = -1 - - # clear the selection. - self.choiceboxWidget.selection_clear(0, 'end') - - # start from previous selection +1 - for n in range(start_n + 1, len(self.return_value)): - item = self.return_value[n] - if item[0].lower() == key.lower(): - self.choiceboxWidget.selection_set(first=n) - self.choiceboxWidget.see(n) - return - else: - # has not found it so loop from top - for n, item in enumerate(self.return_value): - if item[0].lower() == key.lower(): - self.choiceboxWidget.selection_set(first=n) - self.choiceboxWidget.see(n) - return - - # nothing matched -- we'll look for the next logical choice - for n, item in enumerate(self.return_value): - if item[0].lower() > key.lower(): - if n > 0: - self.choiceboxWidget.selection_set( - first=(n - 1)) - else: - self.choiceboxWidget.selection_set(first=0) - self.choiceboxWidget.see(n) - return - - # still no match (nothing was greater than the key) - # we set the selection to the first item in the list - lastIndex = len(self.return_value) - 1 - self.choiceboxWidget.selection_set(first=lastIndex) - self.choiceboxWidget.see(lastIndex) - return - - def choiceboxClearAll(self, event): - self.choiceboxWidget.selection_clear(0, len(self.return_value) - 1) - - def choiceboxSelectAll(self, event): - self.choiceboxWidget.selection_set(0, len(self.return_value) - 1) if __name__ == '__main__': users_choice = multchoicebox(choices=['choice1', 'choice2']) print("User's choice is: {}".format(users_choice)) + users_choice = choicebox(msg="pick only one", choices=['live', 'let_die']) + print("User's choice is: {}".format(users_choice)) From b30f0481c50bd941064ca7017eb675b1f5e1b332 Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Thu, 5 May 2022 22:36:46 +0100 Subject: [PATCH 23/29] minor refactor to button_box.py --- easygui/button_box.py | 45 +++++++++++++------------------------------ easygui/utilities.py | 19 +++++++++++++++++- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/easygui/button_box.py b/easygui/button_box.py index 145e6ad..07f3684 100644 --- a/easygui/button_box.py +++ b/easygui/button_box.py @@ -1,6 +1,7 @@ import tkinter as tk -from easygui.utilities import load_tk_image, get_width_and_padding, parse_hotkey, AbstractBox +from easygui.utilities import load_tk_image, get_width_and_padding, parse_hotkey, AbstractBox, \ + convert_to_a_list_of_lists def buttonbox(msg="buttonbox options", title=" ", choices=("Button[1]", "Button[2]", "Button[3]"), @@ -201,7 +202,7 @@ def __init__(self, msg, title, choices, images, default_choice, cancel_choice, c super().__init__(title, callback) self.msg_widget = self.configure_message_widget(monospace=False) self.msg = msg - self._text_to_return_on_cancel = cancel_choice + self.cancel_value = cancel_choice self.return_value = None self._images = [] self._buttons = [] @@ -210,6 +211,7 @@ def __init__(self, msg, title, choices, images, default_choice, cancel_choice, c self.buttons_frame = self._create_buttons_frame(choices, default_choice) def configure_message_widget(self, monospace): + """ Overriding the base class - we cannot mix pack() and grid() so this message widget uses pack() """ padding, width_in_chars = get_width_and_padding(monospace=False) message_frame = tk.Frame(self.box_root, padx=padding) message_frame.grid() @@ -217,34 +219,16 @@ def configure_message_widget(self, monospace): message_area.grid() return message_area - @staticmethod - def _convert_to_a_list_of_lists(filenames): - """ return a list of lists, handling all of the different allowed types of 'filenames' input """ - if type(filenames) is str: - return [[filenames, ], ] - elif type(filenames[0]) is str: - return [filenames, ] - elif type(filenames[0][0]) is str: - return filenames - raise ValueError("Incorrect images argument.") - def _create_images_frame(self, filenames): images_frame = tk.Frame(self.box_root) row = 1 images_frame.grid(row=row) self.box_root.rowconfigure(row, weight=10, minsize='10m') - if filenames is None: - return - - filename_array = self._convert_to_a_list_of_lists(filenames) + filename_array = convert_to_a_list_of_lists(filenames) for row, list_of_filenames in enumerate(filename_array): for column, filename in enumerate(list_of_filenames): - try: - tk_image = load_tk_image(filename, tk_master=images_frame) - except Exception as e: - print(e) - tk_image = None + tk_image = load_tk_image(filename, tk_master=images_frame) widget = tk.Button( master=images_frame, takefocus=1, @@ -253,11 +237,10 @@ def _create_images_frame(self, filenames): command=lambda text=filename: self._button_pressed(text) ) widget.grid(row=row, column=column, sticky=tk.NSEW, padx='1m', pady='1m', ipadx='2m', ipady='1m') - - image = {'tk_image': tk_image, 'widget': widget} images_frame.rowconfigure(row, weight=10, minsize='10m') images_frame.columnconfigure(column, weight=10) - self._images.append(image) # Prevent image deletion by keeping them on self + # keep a reference to each image on self to prevent deletion + self._images.append({'tk_image': tk_image, 'widget': widget}) return images_frame def _create_buttons_frame(self, choices, default_choice): @@ -292,18 +275,13 @@ def _create_buttons_frame(self, choices, default_choice): return buttons_frame - # Methods executing when a key is pressed - def cancel_button_pressed(self, _): - self.return_value = self._text_to_return_on_cancel - self.stop() - def _button_pressed(self, button_text): + self.return_value = button_text if self._user_specified_callback: # If a callback was set, call main process self._user_specified_callback() else: self.stop() - self.return_value = button_text def _hotkey_pressed(self, event=None): """ Handle an event that is generated by a person interacting with a button """ @@ -314,8 +292,11 @@ def _hotkey_pressed(self, event=None): for button in self._buttons: if button['hotkey'] == hotkey_pressed: - self._user_specified_callback(command='update') self.return_value = button['original_text'] + if self._user_specified_callback: + self._user_specified_callback(self) + else: + self.stop() return # some key was pressed, but no hotkey registered to it diff --git a/easygui/utilities.py b/easygui/utilities.py index 9db13ed..feec91c 100644 --- a/easygui/utilities.py +++ b/easygui/utilities.py @@ -22,6 +22,7 @@ def __init__(self, title, callback) -> None: self.box_root = self._configure_box_root(title) self.return_value = None self.msg_widget = NotImplemented + self.cancel_value = None def _set_return_value(self): raise NotImplemented @@ -94,7 +95,7 @@ def cancel_button_pressed(self, *args): :param args: zero or more args :return: None """ - self.return_value = None + self.return_value = self.cancel_value self.box_root.quit() def ok_button_pressed(self, _): @@ -246,3 +247,19 @@ def bind_to_mouse(button, callback): button.bind("", handler.enter) button.bind("", handler.leave) button.bind("", handler.release) + + +def convert_to_a_list_of_lists(filenames): + """ historically the 'filenames' argument could be + ... a list of lists OR a list OR a string! + Converting all flavours of input to list-of-lists simplifies subsequent handling + """ + if filenames is None: + return [[], ] + elif type(filenames) is str: + return [[filenames, ], ] + elif type(filenames[0]) is str: + return [filenames, ] + elif type(filenames[0][0]) is str: + return filenames + raise ValueError("Incorrect images argument.") From 2a11bb24465dfd4dab989ddd1819039bdcd6265f Mon Sep 17 00:00:00 2001 From: Tomhe Date: Wed, 3 Aug 2022 01:27:45 +0300 Subject: [PATCH 24/29] import sooner from collections.abc --- easygui/boxes/choice_box.py | 530 ++++++++++++++++++++++++++++++++++++ 1 file changed, 530 insertions(+) create mode 100644 easygui/boxes/choice_box.py diff --git a/easygui/boxes/choice_box.py b/easygui/boxes/choice_box.py new file mode 100644 index 0000000..3fe8007 --- /dev/null +++ b/easygui/boxes/choice_box.py @@ -0,0 +1,530 @@ +import string +import sys + +from easygui.boxes.utils import mouse_click_handlers + +if sys.version_info < (3, 3): + from collections import Sequence +else: + from collections.abc import Sequence + +try: + from . import global_state + from .base_boxes import bindArrows +except (SystemError, ValueError, ImportError): + import global_state + from base_boxes import bindArrows + +try: + import tkinter as tk # python 3 + import tkinter.font as tk_Font +except: + import Tkinter as tk # python 2 + import tkFont as tk_Font + + +def choicebox(msg="Pick an item", title="", choices=None, preselect=0, + callback=None, + run=True): + """ + The ``choicebox()`` provides a list of choices in a list box to choose + from. The choices are specified in a sequence (a tuple or a list). + + import easygui + msg ="What is your favorite flavor?" + title = "Ice Cream Survey" + choices = ["Vanilla", "Chocolate", "Strawberry", "Rocky Road"] + choice = easygui.choicebox(msg, title, choices) # choice is a string + + :param str msg: the msg to be displayed + :param str title: the window title + :param list choices: a list or tuple of the choices to be displayed + :param preselect: Which item, if any are preselected when dialog appears + :return: A string of the selected choice or None if cancelled + """ + mb = ChoiceBox(msg, title, choices, preselect=preselect, + multiple_select=False, + callback=callback) + if run: + reply = mb.run() + return reply + else: + return mb + + +def multchoicebox(msg="Pick an item", title="", choices=None, + preselect=0, callback=None, + run=True): + """ + The ``multchoicebox()`` function provides a way for a user to select + from a list of choices. The interface looks just like the ``choicebox()`` + function's dialog box, but the user may select zero, one, or multiple choices. + + The choices are specified in a sequence (a tuple or a list). + + import easygui + msg ="What is your favorite flavor?" + title = "Ice Cream Survey" + choices = ["Vanilla", "Chocolate", "Strawberry", "Rocky Road"] + choice = easygui.multchoicebox(msg, title, choices) + + + :param str msg: the msg to be displayed + :param str title: the window title + :param list choices: a list or tuple of the choices to be displayed + :param preselect: Which item, if any are preselected when dialog appears + :return: A list of strings of the selected choices or None if cancelled. + """ + mb = ChoiceBox(msg, title, choices, preselect=preselect, + multiple_select=True, + callback=callback) + if run: + reply = mb.run() + return reply + else: + return mb + + +# Utility function. But, is it generic enough to be moved out of here? +def make_list_or_none(obj, cast_type=None): + # ------------------------------------------------------------------- + # for an object passed in, put it in standardized form. + # It may be None. Just return None + # If it is a scalar, attempt to cast it into cast_type. Raise error + # if not possible. Convert scalar to a single-element list. + # If it is a collections.Sequence (including a scalar converted to let), + # then cast each element to cast_type. Raise error if any cannot be converted. + # ------------------------------------------------------------------- + ret_val = obj + if ret_val is None: + return None + # Convert any non-sequence to single-element list + if not isinstance(obj, Sequence): + if cast_type is not None: + try: + ret_val = cast_type(obj) + except Exception as e: + raise Exception("Value {} cannot be converted to type: {}".format(obj, cast_type)) + ret_val = [ret_val,] + # Convert all elements to cast_type + if cast_type is not None: + try: + ret_val = [cast_type(elem) for elem in ret_val] + except: + raise Exception("Not all values in {}\n can be converted to type: {}".format(ret_val, cast_type)) + return ret_val + + +class ChoiceBox(object): + + def __init__(self, msg, title, choices, preselect, multiple_select, callback): + + self.callback = callback + + if choices is None: + # Use default choice selections if none were specified: + choices = ('Choice 1', 'Choice 2') + self.choices = self.to_list_of_str(choices) + + # Convert preselect to always be a list or None. + preselect_list = make_list_or_none(preselect, cast_type=int) + if not multiple_select and len(preselect_list)>1: + raise ValueError("Multiple selections not allowed, yet preselect has multiple values:{}".format(preselect_list)) + + self.ui = GUItk(msg, title, self.choices, preselect_list, multiple_select, + self.callback_ui) + + def run(self): + """ Start the ui """ + self.ui.run() + self.ui = None + return self.choices + + def stop(self): + """ Stop the ui """ + self.ui.stop() + + def callback_ui(self, ui, command, choices): + """ This method is executed when ok, cancel, or x is pressed in the ui. + """ + if command == 'update': # OK was pressed + self.choices = choices + if self.callback: + # If a callback was set, call main process + self.callback(self) + else: + self.stop() + elif command == 'x': + self.stop() + self.choices = None + elif command == 'cancel': + self.stop() + self.choices = None + + # methods to change properties -------------- + + @property + def msg(self): + """Text in msg Area""" + return self._msg + + @msg.setter + def msg(self, msg): + self.ui.set_msg(msg) + + @msg.deleter + def msg(self): + self._msg = "" + self.ui.set_msg(self._msg) + + # Methods to validate what will be sent to ui --------- + + def to_list_of_str(self, choices): + choices = [str(c) for c in choices] + + while len(choices) < 2: + raise ValueError('at least two choices need to be specified') + + return choices + + + +class GUItk(object): + + """ This object contains the tk root object. + It draws the window, waits for events and communicates them + to MultiBox, together with the entered values. + + The position in wich it is drawn comes from a global variable. + + It also accepts commands from Multibox to change its message. + """ + + def __init__(self, msg, title, choices, preselect, multiple_select, callback): + + self.callback = callback + + self.choices = choices + + self.width_in_chars = global_state.prop_font_line_length + # Initialize self.selected_choices + # This is the value that will be returned if the user clicks the close + # icon + # self.selected_choices = None + + self.multiple_select = multiple_select + + self.boxRoot = tk.Tk() + + self.boxFont = tk_Font.nametofont("TkTextFont") + + self.config_root(title) + + self.set_pos(global_state.window_position) # GLOBAL POSITION + + self.create_msg_widget(msg) + + self.create_choicearea() + + self.create_ok_button() + + self.create_cancel_button() + + self.create_special_buttons() + + self.preselect_choice(preselect) + + self.choiceboxWidget.focus_force() + + # Run and stop methods --------------------------------------- + + def run(self): + self.boxRoot.mainloop() # run it! + self.boxRoot.destroy() # Close the window + + def stop(self): + # Get the current position before quitting + self.get_pos() + + self.boxRoot.quit() + + def x_pressed(self): + self.callback(self, command='x', choices=self.get_choices()) + + def cancel_pressed(self, event): + self.callback(self, command='cancel', choices=self.get_choices()) + + def ok_pressed(self, event): + self.callback(self, command='update', choices=self.get_choices()) + + # Methods to change content --------------------------------------- + + # Methods to change content --------------------------------------- + + def set_msg(self, msg): + self.messageArea.config(state=tk.NORMAL) + self.messageArea.delete(1.0, tk.END) + self.messageArea.insert(tk.END, msg) + self.messageArea.config(state=tk.DISABLED) + # Adjust msg height + self.messageArea.update() + numlines = self.get_num_lines(self.messageArea) + self.set_msg_height(numlines) + self.messageArea.update() + # put the focus on the entryWidget + + def set_msg_height(self, numlines): + self.messageArea.configure(height=numlines) + + def get_num_lines(self, widget): + end_position = widget.index(tk.END) # '4.0' + end_line = end_position.split('.')[0] # 4 + return int(end_line) + 1 # 5 + + def set_pos(self, pos=None): + if not pos: + pos = global_state.window_position + self.boxRoot.geometry(pos) + + def get_pos(self): + # The geometry() method sets a size for the window and positions it on + # the screen. The first two parameters are width and height of + # the window. The last two parameters are x and y screen coordinates. + # geometry("250x150+300+300") + geom = self.boxRoot.geometry() # "628x672+300+200" + global_state.window_position = '+' + geom.split('+', 1)[1] + + def preselect_choice(self, preselect): + if preselect != None: + for v in preselect: + self.choiceboxWidget.select_set(v) + self.choiceboxWidget.activate(v) + + def get_choices(self): + choices_index = self.choiceboxWidget.curselection() + if not choices_index: + return None + if self.multiple_select: + selected_choices = [self.choiceboxWidget.get(index) + for index in choices_index] + else: + selected_choices = self.choiceboxWidget.get(choices_index) + + return selected_choices + + # Auxiliary methods ----------------------------------------------- + def calc_character_width(self): + char_width = self.boxFont.measure('W') + return char_width + + def config_root(self, title): + + screen_width = self.boxRoot.winfo_screenwidth() + screen_height = self.boxRoot.winfo_screenheight() + self.root_width = int((screen_width * 0.8)) + root_height = int((screen_height * 0.5)) + + self.boxRoot.title(title) + self.boxRoot.iconname('Dialog') + self.boxRoot.expand = tk.NO + # self.boxRoot.minsize(width=62 * self.calc_character_width()) + + self.set_pos() + + self.boxRoot.protocol('WM_DELETE_WINDOW', self.x_pressed) + self.boxRoot.bind('', self.KeyboardListener) + self.boxRoot.bind("", self.cancel_pressed) + + self.boxRoot.attributes("-topmost", True) # Put the dialog box in focus. + + def create_msg_widget(self, msg): + + if msg is None: + msg = "" + + self.msgFrame = tk.Frame( + self.boxRoot, + padx=2 * self.calc_character_width(), + + ) + self.messageArea = tk.Text( + self.msgFrame, + width=self.width_in_chars, + state=tk.DISABLED, + background=self.boxRoot.config()["background"][-1], + relief='flat', + padx=(global_state.default_hpad_in_chars * + self.calc_character_width()), + pady=(global_state.default_hpad_in_chars * + self.calc_character_width()), + wrap=tk.WORD, + + ) + self.set_msg(msg) + + self.msgFrame.pack(side=tk.TOP, expand=1, fill='both') + + self.messageArea.pack(side=tk.TOP, expand=1, fill='both') + + def create_choicearea(self): + + self.choiceboxFrame = tk.Frame(master=self.boxRoot) + self.choiceboxFrame.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.YES) + + lines_to_show = min(len(self.choices), 20) + + # -------- put the self.choiceboxWidget in the self.choiceboxFrame --- + self.choiceboxWidget = tk.Listbox(self.choiceboxFrame, + height=lines_to_show, + borderwidth="1m", relief="flat", + bg="white" + ) + + if self.multiple_select: + self.choiceboxWidget.configure(selectmode=tk.MULTIPLE) + + # self.choiceboxWidget.configure(font=(global_state.PROPORTIONAL_FONT_FAMILY, + # global_state.PROPORTIONAL_FONT_SIZE)) + + # add a vertical scrollbar to the frame + rightScrollbar = tk.Scrollbar(self.choiceboxFrame, orient=tk.VERTICAL, + command=self.choiceboxWidget.yview) + self.choiceboxWidget.configure(yscrollcommand=rightScrollbar.set) + + # add a horizontal scrollbar to the frame + bottomScrollbar = tk.Scrollbar(self.choiceboxFrame, + orient=tk.HORIZONTAL, + command=self.choiceboxWidget.xview) + self.choiceboxWidget.configure(xscrollcommand=bottomScrollbar.set) + + # pack the Listbox and the scrollbars. + # Note that although we must define + # the textArea first, we must pack it last, + # so that the bottomScrollbar will + # be located properly. + + bottomScrollbar.pack(side=tk.BOTTOM, fill=tk.X) + rightScrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + self.choiceboxWidget.pack( + side=tk.LEFT, padx="1m", pady="1m", expand=tk.YES, fill=tk.BOTH) + + # Insert choices widgets + for choice in self.choices: + self.choiceboxWidget.insert(tk.END, choice) + + # Bind the keyboard events + self.choiceboxWidget.bind("", self.ok_pressed) + self.choiceboxWidget.bind("", + self.ok_pressed) + + def create_ok_button(self): + + self.buttonsFrame = tk.Frame(self.boxRoot) + self.buttonsFrame.pack(side=tk.TOP, expand=tk.YES, pady=0) + + # put the buttons in the self.buttonsFrame + okButton = tk.Button(self.buttonsFrame, takefocus=tk.YES, + text="OK", height=1, width=6) + bindArrows(okButton) + okButton.pack(expand=tk.NO, side=tk.RIGHT, padx='2m', pady='1m', + ipady="1m", ipadx="2m") + + # for the commandButton, bind activation events + okButton.bind("", self.ok_pressed) + okButton.bind("", self.ok_pressed) + + mouse_handlers = mouse_click_handlers(self.ok_pressed) + for selectionEvent in global_state.STANDARD_SELECTION_EVENTS_MOUSE: + okButton.bind("<%s>" % selectionEvent, mouse_handlers[selectionEvent]) + + def create_cancel_button(self): + cancelButton = tk.Button(self.buttonsFrame, takefocus=tk.YES, + text="Cancel", height=1, width=6) + bindArrows(cancelButton) + cancelButton.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', + ipady="1m", ipadx="2m") + cancelButton.bind("", self.cancel_pressed) + + mouse_handlers = mouse_click_handlers(self.cancel_pressed) + for selectionEvent in global_state.STANDARD_SELECTION_EVENTS_MOUSE: + cancelButton.bind("<%s>" % selectionEvent, mouse_handlers[selectionEvent]) + + def create_special_buttons(self): + # add special buttons for multiple select features + if not self.multiple_select: + return + + selectAllButton = tk.Button( + self.buttonsFrame, text="Select All", height=1, width=6) + selectAllButton.pack(expand=tk.NO, side=tk.LEFT, padx='2m', + pady='1m', + ipady="1m", ipadx="2m") + + clearAllButton = tk.Button(self.buttonsFrame, text="Clear All", + height=1, width=6) + clearAllButton.pack(expand=tk.NO, side=tk.LEFT, + padx='2m', pady='1m', + ipady="1m", ipadx="2m") + + selectAllButton.bind("", self.choiceboxSelectAll) + bindArrows(selectAllButton) + clearAllButton.bind("", self.choiceboxClearAll) + bindArrows(clearAllButton) + + def KeyboardListener(self, event): + key = event.keysym + if len(key) <= 1: + if key in string.printable: + # Find the key in the liglobal_state. + # before we clear the list, remember the selected member + try: + start_n = int(self.choiceboxWidget.curselection()[0]) + except IndexError: + start_n = -1 + + # clear the selection. + self.choiceboxWidget.selection_clear(0, 'end') + + # start from previous selection +1 + for n in range(start_n + 1, len(self.choices)): + item = self.choices[n] + if item[0].lower() == key.lower(): + self.choiceboxWidget.selection_set(first=n) + self.choiceboxWidget.see(n) + return + else: + # has not found it so loop from top + for n, item in enumerate(self.choices): + if item[0].lower() == key.lower(): + self.choiceboxWidget.selection_set(first=n) + self.choiceboxWidget.see(n) + return + + # nothing matched -- we'll look for the next logical choice + for n, item in enumerate(self.choices): + if item[0].lower() > key.lower(): + if n > 0: + self.choiceboxWidget.selection_set( + first=(n - 1)) + else: + self.choiceboxWidget.selection_set(first=0) + self.choiceboxWidget.see(n) + return + + # still no match (nothing was greater than the key) + # we set the selection to the first item in the list + lastIndex = len(self.choices) - 1 + self.choiceboxWidget.selection_set(first=lastIndex) + self.choiceboxWidget.see(lastIndex) + return + + def choiceboxClearAll(self, event): + self.choiceboxWidget.selection_clear(0, len(self.choices) - 1) + + def choiceboxSelectAll(self, event): + self.choiceboxWidget.selection_set(0, len(self.choices) - 1) + +if __name__ == '__main__': + users_choice = multchoicebox(choices=['choice1', 'choice2']) + print("User's choice is: {}".format(users_choice)) From d2aab78254c6a3f3f7d599e9fd3eac40b3855ee2 Mon Sep 17 00:00:00 2001 From: Tom Herman Date: Sat, 7 Oct 2023 01:39:18 +0300 Subject: [PATCH 25/29] make sure boxRoot isn't None --- easygui/boxes/base_boxes.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 easygui/boxes/base_boxes.py diff --git a/easygui/boxes/base_boxes.py b/easygui/boxes/base_boxes.py new file mode 100644 index 0000000..9e9595c --- /dev/null +++ b/easygui/boxes/base_boxes.py @@ -0,0 +1,29 @@ +""" + +.. moduleauthor:: easygui developers and Stephen Raymond Ferg +.. default-domain:: py +.. highlight:: python + +Version |release| +""" + +boxRoot = None + + +def bindArrows(widget): + + widget.bind("", tabRight) + widget.bind("", tabLeft) + + widget.bind("", tabRight) + widget.bind("", tabLeft) + + +def tabRight(event): + if boxRoot: + boxRoot.event_generate("") + + +def tabLeft(event): + if boxRoot: + boxRoot.event_generate("") From 313e15db4359e575428211084e1b8bc051c4f4b5 Mon Sep 17 00:00:00 2001 From: IronBoy Date: Wed, 10 Jul 2024 16:46:16 +0800 Subject: [PATCH 26/29] Update version information. --- HISTORY.rst | 44 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 52de779..681b1c5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,3 +1,35 @@ +""" + +.. moduleauthor:: easygui developers and Stephen Raymond Ferg +.. default-domain:: py +.. highlight:: python + +""" +try: + from .derived_boxes import codebox +except (SystemError, ValueError, ImportError): + from derived_boxes import codebox + +eg_version = '0.98.3-RELEASED' +egversion = eg_version + + +def abouteasygui(): + """ + Shows the EasyGUI revision history. + """ + codebox("About EasyGui\n{}".format(eg_version), + "EasyGui", EASYGUI_ABOUT_INFORMATION) + return None + + +EASYGUI_ABOUT_INFORMATION = ''' + +0.98.3 +======================================================================== +Update collections.abc import location (old location was deprecated since version 3.3, removed in version 3.10) +See: https://docs.python.org/3.9/library/collections.html#module-collections +for details Add some unit test coverage and test automation for TravisCI. 0.98.2 ======================================================================== @@ -22,13 +54,12 @@ ENHANCEMENTS * Refactored the easygui.py file into several smaller files to improve our ability to manage the code * Added callbacks to allow for more dynamic dialogs. See the docs for usage. * Added class access to dialogs so properties may be changed. - * Improved button boxes ability to resize during window resize by converting to Tkinter grid from packer. KNOWN ISSUES ------------ - * (old) In the documentation, there were previous references to issues when using the IDLE IDE. I haven't - experienced those, but also didn't do anything to fix them, so they may still be there. Please report - any problems and we'll try to address them + * There were previous issues when using easygui with the IDLE IDE. + I hope I resolved these problems, however, I've never actually been able to repeat them. + Please report any problems found in github. OTHER CHANGES ------------- @@ -228,3 +259,8 @@ BUG FIXES * Fixed a bug that was preventing Linux users from copying text out of a textbox and a codebox. This was not a problem for Windows users. + +''' + +if __name__ == '__main__': + abouteasygui() From dee0d51ddf254c921a91f5ef214d4418bef4d1b6 Mon Sep 17 00:00:00 2001 From: IronBoy Date: Wed, 10 Jul 2024 17:03:25 +0800 Subject: [PATCH 27/29] Update about.py --- HISTORY.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 681b1c5..0b3881b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -58,8 +58,8 @@ ENHANCEMENTS KNOWN ISSUES ------------ * There were previous issues when using easygui with the IDLE IDE. - I hope I resolved these problems, however, I've never actually been able to repeat them. - Please report any problems found in github. + I hope I resolved these problems, however, I've never actually been able to repeat them. + Please report any problems found in github. OTHER CHANGES ------------- From 8e8fd159a2570d9abe2522b3401a280293443cb8 Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Sun, 24 Apr 2022 01:18:18 +0100 Subject: [PATCH 28/29] Initial refactor and tests of the text_box class (and derived classes) Removes Python 2.7 compatibility! (re)add the mouse click behaviour that the refactor had removed (re) add docstrings for the text_box and derived methods (used by sphinx) Move Al's test_easygui.py integration tests to the 'tests' folder for consistency use 1x README.rst instead of (duplicate) .txt and .md README files tweak conda meta.yaml so that bld.bat and build.sh aren't needed rename 'test_cases' -> 'demos' change mose_click_handlers -> MouseClickHandler class remove test_travis.py since we now have enough *real* tests, and Travis is working nicely remove duplicate 'parse_hotkey' method remove 'developer information' remove unused methods (exception_format, uniquify_list_of_strings, getFileDialogTitle, to_string) remove unused constant definitions remove tk 8.0+ warning since it looks like it is now bundled for all 'current' python (v3.7+) Tests will live in the 'tests' directory Demos will live in the 'demos' folder Remove some 'about' files to de-clutter the project. Aim: have more functional files, and less 'print this' type files everywhere. In order to do this, move the 'version' information to __init__.py * use it from the demo boxes *and* from setup.py Moving things from easygui.boxes.__init__ to easygui.boxes.utils ... in preparation of getting rid of boxes directory entirely. I want to keep the easygui.__init__ for import control, and move utilites to 'utils' add more interesting things to the button_box.py default parameter values --- HISTORY.rst | 7 +- demos/demo_multi_fillable_box.py | 4 +- demos/demo_text_box.py | 6 +- easygui/__init__.py | 7 +- easygui/boxes/base_boxes.py | 29 -- easygui/boxes/choice_box.py | 530 ------------------------------- easygui/button_box.py | 109 +++++-- easygui/choice_box.py | 312 +++++++++++++----- easygui/fillable_box.py | 131 +++++--- easygui/global_state.py | 2 + easygui/multi_fillable_box.py | 107 +++++-- easygui/text_box.py | 267 +++++++++++----- easygui/utilities.py | 157 ++------- tests/test_easygui.py | 2 +- tests/test_multi_fillable_box.py | 4 +- tests/test_text_box.py | 63 ++-- 16 files changed, 755 insertions(+), 982 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 0b3881b..e84337c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,3 +1,4 @@ + """ .. moduleauthor:: easygui developers and Stephen Raymond Ferg @@ -24,7 +25,6 @@ def abouteasygui(): EASYGUI_ABOUT_INFORMATION = ''' - 0.98.3 ======================================================================== Update collections.abc import location (old location was deprecated since version 3.3, removed in version 3.10) @@ -259,8 +259,3 @@ BUG FIXES * Fixed a bug that was preventing Linux users from copying text out of a textbox and a codebox. This was not a problem for Windows users. - -''' - -if __name__ == '__main__': - abouteasygui() diff --git a/demos/demo_multi_fillable_box.py b/demos/demo_multi_fillable_box.py index 962f4e9..a91e157 100644 --- a/demos/demo_multi_fillable_box.py +++ b/demos/demo_multi_fillable_box.py @@ -44,12 +44,12 @@ def __init__(self): def check_for_blank_fields(self, box): # make sure that none of the fields was left blank - cancelled = box.return_value is None + cancelled = box.values is None errors = [] if cancelled: pass else: # check for errors - for name, value in zip(box.fields, box.return_value): + for name, value in zip(box.fields, box.values): if value.strip() == "": errors.append('"{}" is a required field.'.format(name)) diff --git a/demos/demo_text_box.py b/demos/demo_text_box.py index 5d1d0c5..746cff7 100644 --- a/demos/demo_text_box.py +++ b/demos/demo_text_box.py @@ -46,7 +46,8 @@ def __init__(self): self.finished = False - textbox(gnexp + msg, title, text_snippet, callback=self.check_answer, run=True) + textbox(gnexp + msg, title, text_snippet, False, + callback=self.check_answer, run=True) def check_answer(self, box): """ Callback from TextBox @@ -91,7 +92,8 @@ def __init__(self): text_snippet = "Hello" # This text wont show - box = textbox(msg, title, text_snippet, callback=self.check_answer, run=False) + box = textbox( + msg, title, text_snippet, False, callback=self.check_answer, run=False) box.text = ( "It was the west of times, and it was the worst of times. " diff --git a/easygui/__init__.py b/easygui/__init__.py index cc16aa2..811f758 100644 --- a/easygui/__init__.py +++ b/easygui/__init__.py @@ -22,8 +22,6 @@ 'EgStore', 'version' ] -import os - from .button_box import buttonbox, msgbox, boolbox, ynbox, ccbox, indexbox from .choice_box import choicebox, multchoicebox from .egstore import EgStore @@ -31,6 +29,9 @@ from .fillable_box import fillablebox, enterbox, passwordbox, integerbox from .multi_fillable_box import multenterbox, multpasswordbox from .text_box import textbox, codebox, exceptionbox +import tkinter as tk # python 3 + +import os ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -version = "0.98.3" +version = "0.98.3" \ No newline at end of file diff --git a/easygui/boxes/base_boxes.py b/easygui/boxes/base_boxes.py index 9e9595c..e69de29 100644 --- a/easygui/boxes/base_boxes.py +++ b/easygui/boxes/base_boxes.py @@ -1,29 +0,0 @@ -""" - -.. moduleauthor:: easygui developers and Stephen Raymond Ferg -.. default-domain:: py -.. highlight:: python - -Version |release| -""" - -boxRoot = None - - -def bindArrows(widget): - - widget.bind("", tabRight) - widget.bind("", tabLeft) - - widget.bind("", tabRight) - widget.bind("", tabLeft) - - -def tabRight(event): - if boxRoot: - boxRoot.event_generate("") - - -def tabLeft(event): - if boxRoot: - boxRoot.event_generate("") diff --git a/easygui/boxes/choice_box.py b/easygui/boxes/choice_box.py index 3fe8007..e69de29 100644 --- a/easygui/boxes/choice_box.py +++ b/easygui/boxes/choice_box.py @@ -1,530 +0,0 @@ -import string -import sys - -from easygui.boxes.utils import mouse_click_handlers - -if sys.version_info < (3, 3): - from collections import Sequence -else: - from collections.abc import Sequence - -try: - from . import global_state - from .base_boxes import bindArrows -except (SystemError, ValueError, ImportError): - import global_state - from base_boxes import bindArrows - -try: - import tkinter as tk # python 3 - import tkinter.font as tk_Font -except: - import Tkinter as tk # python 2 - import tkFont as tk_Font - - -def choicebox(msg="Pick an item", title="", choices=None, preselect=0, - callback=None, - run=True): - """ - The ``choicebox()`` provides a list of choices in a list box to choose - from. The choices are specified in a sequence (a tuple or a list). - - import easygui - msg ="What is your favorite flavor?" - title = "Ice Cream Survey" - choices = ["Vanilla", "Chocolate", "Strawberry", "Rocky Road"] - choice = easygui.choicebox(msg, title, choices) # choice is a string - - :param str msg: the msg to be displayed - :param str title: the window title - :param list choices: a list or tuple of the choices to be displayed - :param preselect: Which item, if any are preselected when dialog appears - :return: A string of the selected choice or None if cancelled - """ - mb = ChoiceBox(msg, title, choices, preselect=preselect, - multiple_select=False, - callback=callback) - if run: - reply = mb.run() - return reply - else: - return mb - - -def multchoicebox(msg="Pick an item", title="", choices=None, - preselect=0, callback=None, - run=True): - """ - The ``multchoicebox()`` function provides a way for a user to select - from a list of choices. The interface looks just like the ``choicebox()`` - function's dialog box, but the user may select zero, one, or multiple choices. - - The choices are specified in a sequence (a tuple or a list). - - import easygui - msg ="What is your favorite flavor?" - title = "Ice Cream Survey" - choices = ["Vanilla", "Chocolate", "Strawberry", "Rocky Road"] - choice = easygui.multchoicebox(msg, title, choices) - - - :param str msg: the msg to be displayed - :param str title: the window title - :param list choices: a list or tuple of the choices to be displayed - :param preselect: Which item, if any are preselected when dialog appears - :return: A list of strings of the selected choices or None if cancelled. - """ - mb = ChoiceBox(msg, title, choices, preselect=preselect, - multiple_select=True, - callback=callback) - if run: - reply = mb.run() - return reply - else: - return mb - - -# Utility function. But, is it generic enough to be moved out of here? -def make_list_or_none(obj, cast_type=None): - # ------------------------------------------------------------------- - # for an object passed in, put it in standardized form. - # It may be None. Just return None - # If it is a scalar, attempt to cast it into cast_type. Raise error - # if not possible. Convert scalar to a single-element list. - # If it is a collections.Sequence (including a scalar converted to let), - # then cast each element to cast_type. Raise error if any cannot be converted. - # ------------------------------------------------------------------- - ret_val = obj - if ret_val is None: - return None - # Convert any non-sequence to single-element list - if not isinstance(obj, Sequence): - if cast_type is not None: - try: - ret_val = cast_type(obj) - except Exception as e: - raise Exception("Value {} cannot be converted to type: {}".format(obj, cast_type)) - ret_val = [ret_val,] - # Convert all elements to cast_type - if cast_type is not None: - try: - ret_val = [cast_type(elem) for elem in ret_val] - except: - raise Exception("Not all values in {}\n can be converted to type: {}".format(ret_val, cast_type)) - return ret_val - - -class ChoiceBox(object): - - def __init__(self, msg, title, choices, preselect, multiple_select, callback): - - self.callback = callback - - if choices is None: - # Use default choice selections if none were specified: - choices = ('Choice 1', 'Choice 2') - self.choices = self.to_list_of_str(choices) - - # Convert preselect to always be a list or None. - preselect_list = make_list_or_none(preselect, cast_type=int) - if not multiple_select and len(preselect_list)>1: - raise ValueError("Multiple selections not allowed, yet preselect has multiple values:{}".format(preselect_list)) - - self.ui = GUItk(msg, title, self.choices, preselect_list, multiple_select, - self.callback_ui) - - def run(self): - """ Start the ui """ - self.ui.run() - self.ui = None - return self.choices - - def stop(self): - """ Stop the ui """ - self.ui.stop() - - def callback_ui(self, ui, command, choices): - """ This method is executed when ok, cancel, or x is pressed in the ui. - """ - if command == 'update': # OK was pressed - self.choices = choices - if self.callback: - # If a callback was set, call main process - self.callback(self) - else: - self.stop() - elif command == 'x': - self.stop() - self.choices = None - elif command == 'cancel': - self.stop() - self.choices = None - - # methods to change properties -------------- - - @property - def msg(self): - """Text in msg Area""" - return self._msg - - @msg.setter - def msg(self, msg): - self.ui.set_msg(msg) - - @msg.deleter - def msg(self): - self._msg = "" - self.ui.set_msg(self._msg) - - # Methods to validate what will be sent to ui --------- - - def to_list_of_str(self, choices): - choices = [str(c) for c in choices] - - while len(choices) < 2: - raise ValueError('at least two choices need to be specified') - - return choices - - - -class GUItk(object): - - """ This object contains the tk root object. - It draws the window, waits for events and communicates them - to MultiBox, together with the entered values. - - The position in wich it is drawn comes from a global variable. - - It also accepts commands from Multibox to change its message. - """ - - def __init__(self, msg, title, choices, preselect, multiple_select, callback): - - self.callback = callback - - self.choices = choices - - self.width_in_chars = global_state.prop_font_line_length - # Initialize self.selected_choices - # This is the value that will be returned if the user clicks the close - # icon - # self.selected_choices = None - - self.multiple_select = multiple_select - - self.boxRoot = tk.Tk() - - self.boxFont = tk_Font.nametofont("TkTextFont") - - self.config_root(title) - - self.set_pos(global_state.window_position) # GLOBAL POSITION - - self.create_msg_widget(msg) - - self.create_choicearea() - - self.create_ok_button() - - self.create_cancel_button() - - self.create_special_buttons() - - self.preselect_choice(preselect) - - self.choiceboxWidget.focus_force() - - # Run and stop methods --------------------------------------- - - def run(self): - self.boxRoot.mainloop() # run it! - self.boxRoot.destroy() # Close the window - - def stop(self): - # Get the current position before quitting - self.get_pos() - - self.boxRoot.quit() - - def x_pressed(self): - self.callback(self, command='x', choices=self.get_choices()) - - def cancel_pressed(self, event): - self.callback(self, command='cancel', choices=self.get_choices()) - - def ok_pressed(self, event): - self.callback(self, command='update', choices=self.get_choices()) - - # Methods to change content --------------------------------------- - - # Methods to change content --------------------------------------- - - def set_msg(self, msg): - self.messageArea.config(state=tk.NORMAL) - self.messageArea.delete(1.0, tk.END) - self.messageArea.insert(tk.END, msg) - self.messageArea.config(state=tk.DISABLED) - # Adjust msg height - self.messageArea.update() - numlines = self.get_num_lines(self.messageArea) - self.set_msg_height(numlines) - self.messageArea.update() - # put the focus on the entryWidget - - def set_msg_height(self, numlines): - self.messageArea.configure(height=numlines) - - def get_num_lines(self, widget): - end_position = widget.index(tk.END) # '4.0' - end_line = end_position.split('.')[0] # 4 - return int(end_line) + 1 # 5 - - def set_pos(self, pos=None): - if not pos: - pos = global_state.window_position - self.boxRoot.geometry(pos) - - def get_pos(self): - # The geometry() method sets a size for the window and positions it on - # the screen. The first two parameters are width and height of - # the window. The last two parameters are x and y screen coordinates. - # geometry("250x150+300+300") - geom = self.boxRoot.geometry() # "628x672+300+200" - global_state.window_position = '+' + geom.split('+', 1)[1] - - def preselect_choice(self, preselect): - if preselect != None: - for v in preselect: - self.choiceboxWidget.select_set(v) - self.choiceboxWidget.activate(v) - - def get_choices(self): - choices_index = self.choiceboxWidget.curselection() - if not choices_index: - return None - if self.multiple_select: - selected_choices = [self.choiceboxWidget.get(index) - for index in choices_index] - else: - selected_choices = self.choiceboxWidget.get(choices_index) - - return selected_choices - - # Auxiliary methods ----------------------------------------------- - def calc_character_width(self): - char_width = self.boxFont.measure('W') - return char_width - - def config_root(self, title): - - screen_width = self.boxRoot.winfo_screenwidth() - screen_height = self.boxRoot.winfo_screenheight() - self.root_width = int((screen_width * 0.8)) - root_height = int((screen_height * 0.5)) - - self.boxRoot.title(title) - self.boxRoot.iconname('Dialog') - self.boxRoot.expand = tk.NO - # self.boxRoot.minsize(width=62 * self.calc_character_width()) - - self.set_pos() - - self.boxRoot.protocol('WM_DELETE_WINDOW', self.x_pressed) - self.boxRoot.bind('', self.KeyboardListener) - self.boxRoot.bind("", self.cancel_pressed) - - self.boxRoot.attributes("-topmost", True) # Put the dialog box in focus. - - def create_msg_widget(self, msg): - - if msg is None: - msg = "" - - self.msgFrame = tk.Frame( - self.boxRoot, - padx=2 * self.calc_character_width(), - - ) - self.messageArea = tk.Text( - self.msgFrame, - width=self.width_in_chars, - state=tk.DISABLED, - background=self.boxRoot.config()["background"][-1], - relief='flat', - padx=(global_state.default_hpad_in_chars * - self.calc_character_width()), - pady=(global_state.default_hpad_in_chars * - self.calc_character_width()), - wrap=tk.WORD, - - ) - self.set_msg(msg) - - self.msgFrame.pack(side=tk.TOP, expand=1, fill='both') - - self.messageArea.pack(side=tk.TOP, expand=1, fill='both') - - def create_choicearea(self): - - self.choiceboxFrame = tk.Frame(master=self.boxRoot) - self.choiceboxFrame.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.YES) - - lines_to_show = min(len(self.choices), 20) - - # -------- put the self.choiceboxWidget in the self.choiceboxFrame --- - self.choiceboxWidget = tk.Listbox(self.choiceboxFrame, - height=lines_to_show, - borderwidth="1m", relief="flat", - bg="white" - ) - - if self.multiple_select: - self.choiceboxWidget.configure(selectmode=tk.MULTIPLE) - - # self.choiceboxWidget.configure(font=(global_state.PROPORTIONAL_FONT_FAMILY, - # global_state.PROPORTIONAL_FONT_SIZE)) - - # add a vertical scrollbar to the frame - rightScrollbar = tk.Scrollbar(self.choiceboxFrame, orient=tk.VERTICAL, - command=self.choiceboxWidget.yview) - self.choiceboxWidget.configure(yscrollcommand=rightScrollbar.set) - - # add a horizontal scrollbar to the frame - bottomScrollbar = tk.Scrollbar(self.choiceboxFrame, - orient=tk.HORIZONTAL, - command=self.choiceboxWidget.xview) - self.choiceboxWidget.configure(xscrollcommand=bottomScrollbar.set) - - # pack the Listbox and the scrollbars. - # Note that although we must define - # the textArea first, we must pack it last, - # so that the bottomScrollbar will - # be located properly. - - bottomScrollbar.pack(side=tk.BOTTOM, fill=tk.X) - rightScrollbar.pack(side=tk.RIGHT, fill=tk.Y) - - self.choiceboxWidget.pack( - side=tk.LEFT, padx="1m", pady="1m", expand=tk.YES, fill=tk.BOTH) - - # Insert choices widgets - for choice in self.choices: - self.choiceboxWidget.insert(tk.END, choice) - - # Bind the keyboard events - self.choiceboxWidget.bind("", self.ok_pressed) - self.choiceboxWidget.bind("", - self.ok_pressed) - - def create_ok_button(self): - - self.buttonsFrame = tk.Frame(self.boxRoot) - self.buttonsFrame.pack(side=tk.TOP, expand=tk.YES, pady=0) - - # put the buttons in the self.buttonsFrame - okButton = tk.Button(self.buttonsFrame, takefocus=tk.YES, - text="OK", height=1, width=6) - bindArrows(okButton) - okButton.pack(expand=tk.NO, side=tk.RIGHT, padx='2m', pady='1m', - ipady="1m", ipadx="2m") - - # for the commandButton, bind activation events - okButton.bind("", self.ok_pressed) - okButton.bind("", self.ok_pressed) - - mouse_handlers = mouse_click_handlers(self.ok_pressed) - for selectionEvent in global_state.STANDARD_SELECTION_EVENTS_MOUSE: - okButton.bind("<%s>" % selectionEvent, mouse_handlers[selectionEvent]) - - def create_cancel_button(self): - cancelButton = tk.Button(self.buttonsFrame, takefocus=tk.YES, - text="Cancel", height=1, width=6) - bindArrows(cancelButton) - cancelButton.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', - ipady="1m", ipadx="2m") - cancelButton.bind("", self.cancel_pressed) - - mouse_handlers = mouse_click_handlers(self.cancel_pressed) - for selectionEvent in global_state.STANDARD_SELECTION_EVENTS_MOUSE: - cancelButton.bind("<%s>" % selectionEvent, mouse_handlers[selectionEvent]) - - def create_special_buttons(self): - # add special buttons for multiple select features - if not self.multiple_select: - return - - selectAllButton = tk.Button( - self.buttonsFrame, text="Select All", height=1, width=6) - selectAllButton.pack(expand=tk.NO, side=tk.LEFT, padx='2m', - pady='1m', - ipady="1m", ipadx="2m") - - clearAllButton = tk.Button(self.buttonsFrame, text="Clear All", - height=1, width=6) - clearAllButton.pack(expand=tk.NO, side=tk.LEFT, - padx='2m', pady='1m', - ipady="1m", ipadx="2m") - - selectAllButton.bind("", self.choiceboxSelectAll) - bindArrows(selectAllButton) - clearAllButton.bind("", self.choiceboxClearAll) - bindArrows(clearAllButton) - - def KeyboardListener(self, event): - key = event.keysym - if len(key) <= 1: - if key in string.printable: - # Find the key in the liglobal_state. - # before we clear the list, remember the selected member - try: - start_n = int(self.choiceboxWidget.curselection()[0]) - except IndexError: - start_n = -1 - - # clear the selection. - self.choiceboxWidget.selection_clear(0, 'end') - - # start from previous selection +1 - for n in range(start_n + 1, len(self.choices)): - item = self.choices[n] - if item[0].lower() == key.lower(): - self.choiceboxWidget.selection_set(first=n) - self.choiceboxWidget.see(n) - return - else: - # has not found it so loop from top - for n, item in enumerate(self.choices): - if item[0].lower() == key.lower(): - self.choiceboxWidget.selection_set(first=n) - self.choiceboxWidget.see(n) - return - - # nothing matched -- we'll look for the next logical choice - for n, item in enumerate(self.choices): - if item[0].lower() > key.lower(): - if n > 0: - self.choiceboxWidget.selection_set( - first=(n - 1)) - else: - self.choiceboxWidget.selection_set(first=0) - self.choiceboxWidget.see(n) - return - - # still no match (nothing was greater than the key) - # we set the selection to the first item in the list - lastIndex = len(self.choices) - 1 - self.choiceboxWidget.selection_set(first=lastIndex) - self.choiceboxWidget.see(lastIndex) - return - - def choiceboxClearAll(self, event): - self.choiceboxWidget.selection_clear(0, len(self.choices) - 1) - - def choiceboxSelectAll(self, event): - self.choiceboxWidget.selection_set(0, len(self.choices) - 1) - -if __name__ == '__main__': - users_choice = multchoicebox(choices=['choice1', 'choice2']) - print("User's choice is: {}".format(users_choice)) diff --git a/easygui/button_box.py b/easygui/button_box.py index 07f3684..d69b1cf 100644 --- a/easygui/button_box.py +++ b/easygui/button_box.py @@ -1,7 +1,6 @@ import tkinter as tk -from easygui.utilities import load_tk_image, get_width_and_padding, parse_hotkey, AbstractBox, \ - convert_to_a_list_of_lists +from easygui.utilities import load_tk_image, get_width_and_padding, parse_hotkey def buttonbox(msg="buttonbox options", title=" ", choices=("Button[1]", "Button[2]", "Button[3]"), @@ -170,7 +169,7 @@ def msgbox(msg="(Your message goes here)", title="", ok_button="OK"): return buttonbox(msg, title, choices=[ok_button], image=image, default_choice=ok_button, cancel_choice=ok_button) -class ButtonBox(AbstractBox): +class ButtonBox(object): """ Display various types of button boxes This object separates user from ui, defines which methods can @@ -199,36 +198,73 @@ def __init__(self, msg, title, choices, images, default_choice, cancel_choice, c """ - super().__init__(title, callback) - self.msg_widget = self.configure_message_widget(monospace=False) - self.msg = msg - self.cancel_value = cancel_choice - self.return_value = None + self._user_specified_callback = callback + self._text_to_return_on_cancel = cancel_choice + + self.choice_text = None + self._images = [] self._buttons = [] + self.box_root = self._configure_box_root(title) + self.message_area = self._configure_message_area(box_root=self.box_root) + self._set_msg_area('' if msg is None else msg) self.images_frame = self._create_images_frame(images) self.buttons_frame = self._create_buttons_frame(choices, default_choice) - def configure_message_widget(self, monospace): - """ Overriding the base class - we cannot mix pack() and grid() so this message widget uses pack() """ + def _configure_box_root(self, title): + box_root = tk.Tk() + box_root.title(title) + box_root.iconname('Dialog') + box_root.geometry('600x400+100+100') + box_root.protocol('WM_DELETE_WINDOW', self._x_pressed) # Quit when x button pressed + box_root.bind("", self._cancel_button_pressed) + return box_root + + @staticmethod + def _configure_message_area(box_root): padding, width_in_chars = get_width_and_padding(monospace=False) - message_frame = tk.Frame(self.box_root, padx=padding) + message_frame = tk.Frame(box_root, padx=padding) message_frame.grid() message_area = tk.Text(master=message_frame, width=width_in_chars, padx=padding, pady=padding, wrap=tk.WORD) message_area.grid() return message_area + def _set_msg_area(self, msg): + self.message_area.delete(1.0, tk.END) + self.message_area.insert(tk.END, msg) + num_lines, _ = self.message_area.index(tk.END).split('.') + self.message_area.configure(height=int(num_lines)) + self.message_area.update() + + @staticmethod + def _convert_to_a_list_of_lists(filenames): + """ return a list of lists, handling all of the different allowed types of 'filenames' input """ + if type(filenames) is str: + return [[filenames, ], ] + elif type(filenames[0]) is str: + return [filenames, ] + elif type(filenames[0][0]) is str: + return filenames + raise ValueError("Incorrect images argument.") + def _create_images_frame(self, filenames): images_frame = tk.Frame(self.box_root) row = 1 images_frame.grid(row=row) self.box_root.rowconfigure(row, weight=10, minsize='10m') - filename_array = convert_to_a_list_of_lists(filenames) + if filenames is None: + return + + filename_array = self._convert_to_a_list_of_lists(filenames) for row, list_of_filenames in enumerate(filename_array): for column, filename in enumerate(list_of_filenames): - tk_image = load_tk_image(filename, tk_master=images_frame) + try: + tk_image = load_tk_image(filename, tk_master=images_frame) + except Exception as e: + print(e) + tk_image = None widget = tk.Button( master=images_frame, takefocus=1, @@ -237,10 +273,11 @@ def _create_images_frame(self, filenames): command=lambda text=filename: self._button_pressed(text) ) widget.grid(row=row, column=column, sticky=tk.NSEW, padx='1m', pady='1m', ipadx='2m', ipady='1m') + + image = {'tk_image': tk_image, 'widget': widget} images_frame.rowconfigure(row, weight=10, minsize='10m') images_frame.columnconfigure(column, weight=10) - # keep a reference to each image on self to prevent deletion - self._images.append({'tk_image': tk_image, 'widget': widget}) + self._images.append(image) # Prevent image deletion by keeping them on self return images_frame def _create_buttons_frame(self, choices, default_choice): @@ -275,14 +312,37 @@ def _create_buttons_frame(self, choices, default_choice): return buttons_frame - def _button_pressed(self, button_text): - self.return_value = button_text - if self._user_specified_callback: - # If a callback was set, call main process - self._user_specified_callback() - else: + def _callback(self, command): + if command == 'update': # OK was pressed + if self._user_specified_callback: + # If a callback was set, call main process + self._user_specified_callback() + else: + self.stop() + elif command in ('x', 'cancel'): self.stop() + def run(self): + self.box_root.mainloop() + self.box_root.destroy() + return self.choice_text + + def stop(self): + self.box_root.quit() + + # Methods executing when a key is pressed + def _x_pressed(self): + self._callback(command='x') + self.choice_text = self._text_to_return_on_cancel + + def _cancel_button_pressed(self, _): + self._callback(command='cancel') + self.choice_text = self._text_to_return_on_cancel + + def _button_pressed(self, button_text): + self._callback(command='update') + self.choice_text = button_text + def _hotkey_pressed(self, event=None): """ Handle an event that is generated by a person interacting with a button """ if event.keysym != event.char: # A special character @@ -292,11 +352,8 @@ def _hotkey_pressed(self, event=None): for button in self._buttons: if button['hotkey'] == hotkey_pressed: - self.return_value = button['original_text'] - if self._user_specified_callback: - self._user_specified_callback(self) - else: - self.stop() + self._callback(command='update') + self.choice_text = button['original_text'] return # some key was pressed, but no hotkey registered to it diff --git a/easygui/choice_box.py b/easygui/choice_box.py index 1780612..6d2a14c 100644 --- a/easygui/choice_box.py +++ b/easygui/choice_box.py @@ -1,9 +1,10 @@ +import string import tkinter as tk -from easygui.utilities import AbstractBox, bind_to_mouse +from easygui.utilities import get_num_lines, get_width_and_padding, bindArrows, MouseClickHandler -def choicebox(msg="Pick an item", title="", choices=('Choice 1', 'Choice 2'), preselect=(), callback=None, run=True): +def choicebox(msg="Pick an item", title="", choices=None, preselect=[], callback=None, run=True): """ Present the user with a list of choices. return the choice that he selects. @@ -12,16 +13,19 @@ def choicebox(msg="Pick an item", title="", choices=('Choice 1', 'Choice 2'), pr :param str title: the window title :param list choices: a list or tuple of the choices to be displayed :param preselect: optional list of pre-selected choices, a subset of the choices argument - :param callback: user defined callback to be performed when selection is complete - :param run: whether to call .run() on the box immediately or return an instance + :param callback: + :param run: :return: List containing choice selected or None if cancelled """ cb = ChoiceBox(msg, title, choices, preselect=preselect, multiple_select=False, callback=callback) - return cb.run() if run else cb + if run: + reply = cb.run() + return reply + else: + return cb -def multchoicebox(msg="Pick an item", title="", choices=('Choice 1', 'Choice 2'), preselect=(), callback=None, - run=True): +def multchoicebox(msg="Pick an item", title="", choices=None, preselect=[], callback=None, run=True): """ The ``multchoicebox()`` function provides a way for a user to select from a list of choices. The interface looks just like the ``choicebox()`` @@ -40,8 +44,6 @@ def multchoicebox(msg="Pick an item", title="", choices=('Choice 1', 'Choice 2') :param str title: the window title :param list choices: a list or tuple of the choices to be displayed :param preselect: Which item, if any are preselected when dialog appears - :param callback: user defined callback to be performed when selection is complete - :param run: whether to call .run() on the box immediately or return an instance :return: A list of strings of the selected choices or None if cancelled. """ mcb = ChoiceBox(msg, title, choices, preselect=preselect, multiple_select=True, callback=callback) @@ -52,107 +54,271 @@ def multchoicebox(msg="Pick an item", title="", choices=('Choice 1', 'Choice 2') return mcb -class ChoiceBox(AbstractBox): +class ChoiceBox(object): def __init__(self, msg, title, choices, preselect, multiple_select, callback): - super().__init__(title, callback) - assert multiple_select or len(preselect) <= 1, "multiple_select needs to be True for multiple preselect use " - self.msg_widget = self.configure_message_widget(monospace=False) - self.msg = msg + if not multiple_select and len(preselect)>1: + raise ValueError("Multiple selections not allowed, yet preselect has multiple values:{}".format(preselect)) self._multiple_select = multiple_select + self._user_specified_callback = callback + if choices is None: + # Use default choice selections if none were specified: + choices = ('Choice 1', 'Choice 2') self.choices = [str(c) for c in choices] - self.choicebox_widget = self.create_choice_area() + self.box_root = self._configure_box_root(title) + + self.message_area = self._configure_message_area(self.box_root) + self._set_msg_area("" if msg is None else msg) + + self.create_choice_area() - self.buttonsFrame = tk.Frame(self.box_root) - self.buttonsFrame.pack(side=tk.TOP, expand=tk.YES, pady=0) self.create_ok_button() self.create_cancel_button() - if self._multiple_select: - self.create_special_buttons() - + self.create_special_buttons() self.preselect_choice(preselect) - self.choicebox_widget.focus_force() + self.choiceboxWidget.focus_force() - def preselect_choice(self, preselect): - for v in preselect: - self.choicebox_widget.select_set(v) - self.choicebox_widget.activate(v) + def run(self): + self.box_root.mainloop() # run it! + self.box_root.destroy() # close the window + return self.choices + + def stop(self): + self.box_root.quit() + + def x_pressed(self): + self.stop() + self.choices = None + + def cancel_button_pressed(self, event): + self.stop() + self.choices = None - def _set_return_value(self): - choices_index = self.choicebox_widget.curselection() + def ok_button_pressed(self, event): + self.choices = self.get_choices() + if self._user_specified_callback: + # If a _user_specified_callback was set, call main process + self._user_specified_callback(self) + else: + self.stop() + + def _set_msg_area(self, msg): + self.message_area.config(state=tk.NORMAL) # necessary but I don't know why + self.message_area.delete(1.0, tk.END) + self.message_area.insert(tk.END, msg) + numlines = get_num_lines(self.message_area) + self.message_area.configure(height=numlines) + self.message_area.update() + + def preselect_choice(self, preselect): + if preselect != None: + for v in preselect: + self.choiceboxWidget.select_set(v) + self.choiceboxWidget.activate(v) + + def get_choices(self): + choices_index = self.choiceboxWidget.curselection() + if not choices_index: + return None if self._multiple_select: - self.return_value = [self.choicebox_widget.get(index) for index in choices_index] + selected_choices = [self.choiceboxWidget.get(index) + for index in choices_index] else: - self.return_value = self.choicebox_widget.get(choices_index) + selected_choices = self.choiceboxWidget.get(choices_index) + + return selected_choices + + def _configure_box_root(self, title): + box_root = tk.Tk() + box_root.title(title) + box_root.iconname('Dialog') + box_root.protocol('WM_DELETE_WINDOW', self.x_pressed) + box_root.bind('', self.KeyboardListener) + box_root.bind("", self.cancel_button_pressed) + return box_root + + @staticmethod + def _configure_message_area(box_root): + padding, width_in_chars = get_width_and_padding(monospace=False) + + message_frame = tk.Frame(box_root, padx=padding) + message_frame.pack(side=tk.TOP, expand=1, fill='both') + + message_area = tk.Text(master=message_frame, + width=width_in_chars, + state=tk.DISABLED, + background=box_root.config()["background"][-1], + relief='flat', + padx=padding, + pady=padding, + wrap=tk.WORD) + message_area.pack(side=tk.TOP, expand=1, fill='both') + return message_area def create_choice_area(self): - choicebox_frame = tk.Frame(master=self.box_root) - choicebox_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.YES) - choicebox_widget = tk.Listbox( - master=choicebox_frame, - height=(min(len(self.choices), 20)), - borderwidth="1m", relief="flat", - bg="white", - selectmode=tk.MULTIPLE if self._multiple_select else tk.SINGLE, - ) + self.choiceboxFrame = tk.Frame(master=self.box_root) + self.choiceboxFrame.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.YES) + + lines_to_show = min(len(self.choices), 20) + + # -------- put the self.choiceboxWidget in the self.choiceboxFrame --- + self.choiceboxWidget = tk.Listbox(self.choiceboxFrame, + height=lines_to_show, + borderwidth="1m", relief="flat", + bg="white" + ) + + if self._multiple_select: + self.choiceboxWidget.configure(selectmode=tk.MULTIPLE) + + # self.choiceboxWidget.configure(font=(global_state.PROPORTIONAL_FONT_FAMILY, + # global_state.PROPORTIONAL_FONT_SIZE)) # add a vertical scrollbar to the frame - vertical_scrollbar = tk.Scrollbar(choicebox_frame, orient=tk.VERTICAL, command=choicebox_widget.yview) - choicebox_widget.configure(yscrollcommand=vertical_scrollbar.set) + rightScrollbar = tk.Scrollbar(self.choiceboxFrame, orient=tk.VERTICAL, + command=self.choiceboxWidget.yview) + self.choiceboxWidget.configure(yscrollcommand=rightScrollbar.set) # add a horizontal scrollbar to the frame - horizontal_scrollbar = tk.Scrollbar(choicebox_frame, orient=tk.HORIZONTAL, command=choicebox_widget.xview) - choicebox_widget.configure(xscrollcommand=horizontal_scrollbar.set) + bottomScrollbar = tk.Scrollbar(self.choiceboxFrame, + orient=tk.HORIZONTAL, + command=self.choiceboxWidget.xview) + self.choiceboxWidget.configure(xscrollcommand=bottomScrollbar.set) + + # pack the Listbox and the scrollbars. + # Note that although we must define + # the textArea first, we must pack it last, + # so that the bottomScrollbar will + # be located properly. - # pack everything - order is important so that horizontal scrollbar displays correctly - horizontal_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) - vertical_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - choicebox_widget.pack(side=tk.LEFT, padx="1m", pady="1m", expand=tk.YES, fill=tk.BOTH) + bottomScrollbar.pack(side=tk.BOTTOM, fill=tk.X) + rightScrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + self.choiceboxWidget.pack( + side=tk.LEFT, padx="1m", pady="1m", expand=tk.YES, fill=tk.BOTH) # Insert choices widgets for choice in self.choices: - choicebox_widget.insert(tk.END, choice) + self.choiceboxWidget.insert(tk.END, choice) # Bind the keyboard events - choicebox_widget.bind("", self.ok_button_pressed) - return choicebox_widget + self.choiceboxWidget.bind("", self.ok_button_pressed) + self.choiceboxWidget.bind("", + self.ok_button_pressed) def create_ok_button(self): - ok_button = tk.Button(self.buttonsFrame, takefocus=tk.YES, text="OK", height=1, width=6) - ok_button.pack(expand=tk.NO, side=tk.RIGHT, padx='2m', pady='1m', ipady="1m", ipadx="2m") - ok_button.bind("", self.ok_button_pressed) - ok_button.bind("", self.ok_button_pressed) - bind_to_mouse(ok_button, self.ok_button_pressed) - def create_cancel_button(self): - cancel_button = tk.Button(self.buttonsFrame, takefocus=tk.YES, text="Cancel", height=1, width=6) - cancel_button.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", ipadx="2m") - cancel_button.bind("", self.cancel_button_pressed) - bind_to_mouse(cancel_button, self.cancel_button_pressed) + self.buttonsFrame = tk.Frame(self.box_root) + self.buttonsFrame.pack(side=tk.TOP, expand=tk.YES, pady=0) - def create_special_buttons(self): - # add special buttons for multiple select features - select_all_button = tk.Button(self.buttonsFrame, text="Select All", height=1, width=6) - select_all_button.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", ipadx="2m") - bind_to_mouse(select_all_button, self.select_all) + # put the buttons in the self.buttonsFrame + okButton = tk.Button(self.buttonsFrame, takefocus=tk.YES, + text="OK", height=1, width=6) + bindArrows(okButton) + okButton.pack(expand=tk.NO, side=tk.RIGHT, padx='2m', pady='1m', + ipady="1m", ipadx="2m") - clear_all_button = tk.Button(self.buttonsFrame, text="Clear All", height=1, width=6) - clear_all_button.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", ipadx="2m") - bind_to_mouse(clear_all_button, self.clear_all) + # for the commandButton, bind activation events + okButton.bind("", self.ok_button_pressed) + okButton.bind("", self.ok_button_pressed) - def clear_all(self, event): - self.choicebox_widget.selection_clear(0, len(self.choices) - 1) + ok_click_handler = MouseClickHandler(callback=self.ok_button_pressed) + okButton.bind("", ok_click_handler.enter) + okButton.bind("", ok_click_handler.leave) + okButton.bind("", ok_click_handler.release) - def select_all(self, event): - self.choicebox_widget.selection_set(0, len(self.choices) - 1) + def create_cancel_button(self): + cancelButton = tk.Button(self.buttonsFrame, takefocus=tk.YES, + text="Cancel", height=1, width=6) + bindArrows(cancelButton) + cancelButton.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', + ipady="1m", ipadx="2m") + cancelButton.bind("", self.cancel_button_pressed) + + cancel_click_handler = MouseClickHandler(callback=self.cancel_button_pressed) + cancelButton.bind("", cancel_click_handler.enter) + cancelButton.bind("", cancel_click_handler.leave) + cancelButton.bind("", cancel_click_handler.release) + def create_special_buttons(self): + # add special buttons for multiple select features + if not self._multiple_select: + return + + selectAllButton = tk.Button( + self.buttonsFrame, text="Select All", height=1, width=6) + selectAllButton.pack(expand=tk.NO, side=tk.LEFT, padx='2m', + pady='1m', + ipady="1m", ipadx="2m") + + clearAllButton = tk.Button(self.buttonsFrame, text="Clear All", + height=1, width=6) + clearAllButton.pack(expand=tk.NO, side=tk.LEFT, + padx='2m', pady='1m', + ipady="1m", ipadx="2m") + + selectAllButton.bind("", self.choiceboxSelectAll) + bindArrows(selectAllButton) + clearAllButton.bind("", self.choiceboxClearAll) + bindArrows(clearAllButton) + + def KeyboardListener(self, event): + key = event.keysym + if len(key) <= 1: + if key in string.printable: + # Find the key in the liglobal_state. + # before we clear the list, remember the selected member + try: + start_n = int(self.choiceboxWidget.curselection()[0]) + except IndexError: + start_n = -1 + + # clear the selection. + self.choiceboxWidget.selection_clear(0, 'end') + + # start from previous selection +1 + for n in range(start_n + 1, len(self.choices)): + item = self.choices[n] + if item[0].lower() == key.lower(): + self.choiceboxWidget.selection_set(first=n) + self.choiceboxWidget.see(n) + return + else: + # has not found it so loop from top + for n, item in enumerate(self.choices): + if item[0].lower() == key.lower(): + self.choiceboxWidget.selection_set(first=n) + self.choiceboxWidget.see(n) + return + + # nothing matched -- we'll look for the next logical choice + for n, item in enumerate(self.choices): + if item[0].lower() > key.lower(): + if n > 0: + self.choiceboxWidget.selection_set( + first=(n - 1)) + else: + self.choiceboxWidget.selection_set(first=0) + self.choiceboxWidget.see(n) + return + + # still no match (nothing was greater than the key) + # we set the selection to the first item in the list + lastIndex = len(self.choices) - 1 + self.choiceboxWidget.selection_set(first=lastIndex) + self.choiceboxWidget.see(lastIndex) + return + + def choiceboxClearAll(self, event): + self.choiceboxWidget.selection_clear(0, len(self.choices) - 1) + + def choiceboxSelectAll(self, event): + self.choiceboxWidget.selection_set(0, len(self.choices) - 1) if __name__ == '__main__': users_choice = multchoicebox(choices=['choice1', 'choice2']) print("User's choice is: {}".format(users_choice)) - users_choice = choicebox(msg="pick only one", choices=['live', 'let_die']) - print("User's choice is: {}".format(users_choice)) diff --git a/easygui/fillable_box.py b/easygui/fillable_box.py index 42f5602..514702c 100644 --- a/easygui/fillable_box.py +++ b/easygui/fillable_box.py @@ -1,12 +1,12 @@ import tkinter as tk from easygui import msgbox -from easygui.global_state import PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE, \ +from easygui.global_state import GLOBAL_WINDOW_POSITION, PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE, \ TEXT_ENTRY_FONT_SIZE -from easygui.utilities import load_tk_image, MouseClickHandler, AbstractBox, get_width_and_padding +from easygui.utilities import load_tk_image, bindArrows, MouseClickHandler -def integerbox(msg=None, title=" ", default=None, lowerbound=0, upperbound=99, image=None): +def integerbox(msg=None, title=" ", default=None, lowerbound=0, upperbound=99, image=None, root=None): """ Show a box in which a user can enter an integer. @@ -37,7 +37,7 @@ def integerbox(msg=None, title=" ", default=None, lowerbound=0, upperbound=99, i msg = "Enter an integer between {0} and {1}".format(lowerbound, upperbound) if msg is None else msg while True: - result = FillableBox(msg, default, title, image=image).run() + result = FillableBox(msg, title, default, image=image, root=root).run() if result is None: return None @@ -55,7 +55,7 @@ def integerbox(msg=None, title=" ", default=None, lowerbound=0, upperbound=99, i return result # validation passed! -def enterbox(msg="Enter something.", title=" ", default="", strip=True, image=None): +def enterbox(msg="Enter something.", title=" ", default="", strip=True, image=None, root=None): """ Show a box in which a user can enter some text. @@ -79,13 +79,13 @@ def enterbox(msg="Enter something.", title=" ", default="", strip=True, image=No :return: the text that the user entered, or None if they cancel the operation. """ - result = FillableBox(msg, default, title, image=image).run() + result = FillableBox(msg, title, default, image=image, root=root).run() if result and strip: result = result.strip() return result -def passwordbox(msg="Enter your password.", title="", default="", image=None): +def passwordbox(msg="Enter your password.", title="", default="", image=None, root=None): """ Show a box in which a user can enter a password. The text is masked with asterisks, so the password is not displayed. @@ -96,10 +96,10 @@ def passwordbox(msg="Enter your password.", title="", default="", image=None): :return: the text that the user entered, or None if they cancel the operation. """ - return FillableBox(msg, default, title, mask="*", image=image).run() + return FillableBox(msg, title, default, mask="*", image=image, root=root).run() -def fillablebox(msg, title="", default=None, mask=None, image=None): +def fillablebox(msg, title="", default=None, mask=None, image=None, root=None): """ Show a box in which a user can enter some text. :param str msg: the msg to be displayed. @@ -107,54 +107,101 @@ def fillablebox(msg, title="", default=None, mask=None, image=None): :param str default: default value populated, returned if user does not change it :return: the text that the user entered, or None if he cancels the operation. """ - return FillableBox(msg, default, title, mask, image).run() + return FillableBox(msg, title, default, mask, image, root).run() -class FillableBox(AbstractBox): - def __init__(self, msg, title, default, mask=None, image=None): - super().__init__(title, callback=None) - self.return_value = default +class FillableBox(object): + def __init__(self, msg, default, title, mask=None, image=None, root=None): + self.return_value = '' if default is None else default + self.pre_existing_root = root + self.box_root = None + self.entry_widget = None - self.configure_image(image) + if root: + root.withdraw() + self.box_root = tk.Toplevel(master=root) + self.box_root.withdraw() + else: + self.box_root = tk.Tk() + self.box_root.withdraw() - self.msg_widget = self.configure_message_widget(monospace=False) - self.msg = msg + self.box_root.protocol('WM_DELETE_WINDOW', self._cancel_pressed) + self.box_root.title(title) + self.box_root.iconname('Dialog') + self.box_root.geometry(GLOBAL_WINDOW_POSITION) + self.box_root.bind("", self._cancel_pressed) - self.entry_widget = self.conigure_entry_widget(mask) - self.entry_widget.focus_force() # put the focus on the self.entry_widget + message_frame = tk.Frame(master=self.box_root) + message_frame.pack(side=tk.TOP, fill=tk.BOTH) - self.set_buttons() - self.box_root.deiconify() + try: + tk_image = load_tk_image(image) + except Exception as e: + print(e) + tk_image = None + if tk_image: + image_frame = tk.Frame(master=self.box_root) + image_frame.pack(side=tk.TOP, fill=tk.BOTH) + label = tk.Label(image_frame, image=tk_image) + label.image = tk_image # keep a reference! + label.pack(side=tk.TOP, expand=tk.YES, fill=tk.X, padx='1m', pady='1m') + + buttons_frame = tk.Frame(master=self.box_root) + buttons_frame.pack(side=tk.TOP, fill=tk.BOTH) - def conigure_entry_widget(self, mask): - padding, width_in_chars = get_width_and_padding(monospace=False) entry_frame = tk.Frame(master=self.box_root) entry_frame.pack(side=tk.TOP, fill=tk.BOTH) - entry_widget = tk.Entry(entry_frame, width=width_in_chars) + + buttons_frame = tk.Frame(master=self.box_root) + buttons_frame.pack(side=tk.TOP, fill=tk.BOTH) + + message_widget = tk.Message(message_frame, width="4.5i", text=msg) + message_widget.configure(font=(PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE)) + message_widget.pack(side=tk.RIGHT, expand=1, fill=tk.BOTH, padx='3m', pady='3m') + + entry_widget = tk.Entry(entry_frame, width=40) entry_widget.configure(font=(PROPORTIONAL_FONT_FAMILY, TEXT_ENTRY_FONT_SIZE)) if mask: entry_widget.configure(show=mask) - entry_widget.pack(side=tk.LEFT, padx=padding) + entry_widget.pack(side=tk.LEFT, padx="3m") + entry_widget.bind("", self._ok_pressed) + entry_widget.bind("", self._cancel_pressed) entry_widget.insert(0, self.return_value) # put text into the entry_widget - return entry_widget + self.entry_widget = entry_widget # save a reference - we need to get text from this widget later + + ok_button = tk.Button(buttons_frame, takefocus=1, text="OK") + bindArrows(ok_button) + ok_button.pack(expand=1, side=tk.LEFT, padx='3m', pady='3m', ipadx='2m', ipady='1m') + ok_button.bind("", self._ok_pressed) + ok_click_handler = MouseClickHandler(callback=self._ok_pressed) + ok_button.bind("", ok_click_handler.enter) + ok_button.bind("", ok_click_handler.leave) + ok_button.bind("", ok_click_handler.release) + + cancel_button = tk.Button(buttons_frame, takefocus=1, text="Cancel") + cancel_button.pack(expand=1, side=tk.RIGHT, padx='3m', pady='3m', ipadx='2m', ipady='1m') + cancel_button.bind("", self._cancel_pressed) + cancel_click_handler = MouseClickHandler(callback=self._cancel_pressed) + cancel_button.bind("", cancel_click_handler.enter) + cancel_button.bind("", cancel_click_handler.leave) + cancel_button.bind("", cancel_click_handler.release) + + self.entry_widget.focus_force() # put the focus on the self.entry_widget + self.box_root.deiconify() - def configure_image(self, image): - image_frame = tk.Frame(master=self.box_root) - image_frame.pack(side=tk.TOP, fill=tk.BOTH) - tk_image = load_tk_image(image) - label = tk.Label(image_frame, image=tk_image) - label.image = tk_image # keep a reference! - label.pack(side=tk.TOP, expand=tk.YES, fill=tk.X, padx='1m', pady='1m') + def _cancel_pressed(self, *args): + self.return_value = None + self.box_root.quit() - def _set_return_value(self): + def _ok_pressed(self, *args): self.return_value = self.entry_widget.get() + self.box_root.quit() + def run(self): + self.box_root.mainloop() # run it! -if __name__ == '__main__': - fillablebox( - msg="message", - title="title", - default="blah", - mask="*", - image=".\\..\\demos\\images\\dave.gif" - ) \ No newline at end of file + # -------- after the run has completed ---------------------------------- + if self.pre_existing_root: + self.pre_existing_root.deiconify() + self.box_root.destroy() # button_click didn't destroy self.boxRoot, so we do it now + return self.return_value diff --git a/easygui/global_state.py b/easygui/global_state.py index d26ef4f..d208bbc 100644 --- a/easygui/global_state.py +++ b/easygui/global_state.py @@ -10,3 +10,5 @@ DEFAULT_PADDING = 2 REGULAR_FONT_WIDTH = 13 FIXED_FONT_WIDTH = 7 +boxRoot = None + diff --git a/easygui/multi_fillable_box.py b/easygui/multi_fillable_box.py index b775a27..d4fca16 100644 --- a/easygui/multi_fillable_box.py +++ b/easygui/multi_fillable_box.py @@ -1,8 +1,8 @@ import tkinter as tk -from itertools import zip_longest -from easygui.global_state import PROPORTIONAL_FONT_FAMILY, TEXT_ENTRY_FONT_SIZE -from easygui.utilities import AbstractBox +from easygui.global_state import GLOBAL_WINDOW_POSITION, PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE, \ + TEXT_ENTRY_FONT_SIZE +from easygui.utilities import MouseClickHandler def multpasswordbox(msg="Fill in values for the fields.", title=" ", fields=None, values=None, callback=None, run=True): @@ -14,8 +14,6 @@ def multpasswordbox(msg="Fill in values for the fields.", title=" ", fields=None :param str title: the window title :param list fields: a list of fieldnames. :param list values: a list of field values - :param callback: user specified callback to use when 'ok' is pressed - :param run: whether to run or not when called :return: String """ mb = MultiBox(msg, title, fields, values, mask_last=True, callback=callback) @@ -30,46 +28,91 @@ def multenterbox(msg="Fill in values for the fields.", title=" ", fields=None, v :param str title: the window title :param list fields: a list of fieldnames. :param list values: a list of field values - :param callback: user specified callback to use when 'ok' is pressed - :param run: whether to run or not when called :return: String """ mb = MultiBox(msg, title, fields, values, mask_last=False, callback=callback) return mb.run() if run else mb -class MultiBox(AbstractBox): +class MultiBox(object): def __init__(self, msg, title, fields=None, values=None, mask_last=False, callback=None): - super().__init__(title, callback) - self.msg_widget = self.configure_message_widget(monospace=False) - self.msg = msg - self.entry_widgets = [] - self.configure_entry_widgets(fields, values, mask_last) - self.set_buttons() - self.entry_widgets[0].focus_force() # put the focus on the first entry widget + self.fields, self.values = self._process_fields_and_values(fields, values) + self.user_defined_callback = callback + + self.boxRoot = tk.Tk() + self.boxRoot.protocol('WM_DELETE_WINDOW', self._cancel_pressed) + self.boxRoot.title(title) + self.boxRoot.iconname('Dialog') + self.boxRoot.bind("", self._cancel_pressed) + self.boxRoot.geometry(GLOBAL_WINDOW_POSITION) - def configure_entry_widgets(self, fields, values, mask_last): - assert len(fields) <= len(values), "There are fewer fields than values! Values can be blank but fields cannot." - for field, value in zip_longest(fields, values, fillvalue=""): + message_widget = tk.Message(self.boxRoot, width="4.5i", text=msg) + message_widget.configure(font=(PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE)) + message_widget.pack(side=tk.TOP, expand=1, fill=tk.BOTH, padx='3m', pady='3m') - frame = tk.Frame(master=self.box_root) - frame.pack(side=tk.TOP, fill=tk.BOTH) + self.entry_widgets = [] + for field, value in zip(self.fields, self.values): + entry_frame = tk.Frame(master=self.boxRoot) + entry_frame.pack(side=tk.TOP, fill=tk.BOTH) - label_widget = tk.Label(frame, text=field) + label_widget = tk.Label(entry_frame, text=field) label_widget.pack(side=tk.LEFT) - entry_widget = tk.Entry(frame, width=40, highlightthickness=2) + entry_widget = tk.Entry(entry_frame, width=40, highlightthickness=2) + self.entry_widgets.append(entry_widget) entry_widget.configure(font=(PROPORTIONAL_FONT_FAMILY, TEXT_ENTRY_FONT_SIZE)) entry_widget.pack(side=tk.RIGHT, padx="3m") + entry_widget.bind("", self._ok_pressed) + entry_widget.bind("", self._cancel_pressed) entry_widget.insert(0, '' if value is None else value) - if mask_last: - entry_widget.configure(show="*") - self.entry_widgets.append(entry_widget) - - def _set_return_value(self): - self.return_value = [widget.get() for widget in self.entry_widgets] - -if __name__ == '__main__': - result = multenterbox(msg="example message", title="example title", fields=["1", "2", "3"], values=["a", "b", "c"]) - print(f"Return value: {result}") + if mask_last: + self.entry_widgets[-1].configure(show="*") + + buttons_frame = tk.Frame(master=self.boxRoot) + buttons_frame.pack(side=tk.BOTTOM) + + cancel_button = tk.Button(buttons_frame, takefocus=1, text="Cancel") + cancel_button.pack(expand=1, side=tk.LEFT, padx='3m', pady='3m', ipadx='2m', ipady='1m') + cancel_button.bind("", self._cancel_pressed) + cancel_click_handler = MouseClickHandler(callback=self._cancel_pressed) + cancel_button.bind("", cancel_click_handler.enter) + cancel_button.bind("", cancel_click_handler.leave) + cancel_button.bind("", cancel_click_handler.release) + + ok_button = tk.Button(buttons_frame, takefocus=1, text="OK") + ok_button.pack(expand=1, side=tk.LEFT, padx='3m', pady='3m', ipadx='2m', ipady='1m') + ok_button.bind("", self._ok_pressed) + ok_click_handler = MouseClickHandler(callback=self._ok_pressed) + ok_button.bind("", ok_click_handler.enter) + ok_button.bind("", ok_click_handler.leave) + ok_button.bind("", ok_click_handler.release) + + self.entry_widgets[0].focus_force() # put the focus on the entry_widget + + def run(self): + self.boxRoot.mainloop() # run it! + self.boxRoot.destroy() # Close the window + return self.values + + def _cancel_pressed(self, *args): + self.values = None + self.boxRoot.quit() + + def _ok_pressed(self, _): + self.values = self._get_values() + if self.user_defined_callback: + self.user_defined_callback(self) + self.boxRoot.quit() + + def _get_values(self): + return [widget.get() for widget in self.entry_widgets] + + @staticmethod + def _process_fields_and_values(fields, values): + fields = [] if fields is None else list(fields) + values = [] if values is None else list(values) + padding_required = len(fields) - len(values) + if padding_required > 0: + values.extend([""] * padding_required) + return fields, values diff --git a/easygui/text_box.py b/easygui/text_box.py index a9e9332..1e75423 100644 --- a/easygui/text_box.py +++ b/easygui/text_box.py @@ -1,131 +1,238 @@ +import sys +import traceback + import tkinter as tk from tkinter import font -from easygui.utilities import get_width_and_padding, AbstractBox, bind_to_mouse +from easygui.global_state import GLOBAL_WINDOW_POSITION +from easygui.utilities import get_num_lines, get_width_and_padding, MouseClickHandler -def textbox(msg='', title='', text='', callback=None, run=True): +def textbox(msg='', title='', text='', codebox=False, callback=None, run=True): """ - Displays a dialog box with a large, multi-line text box, and returns the entered text as a string. - The message text is displayed in a proportional font and wraps. - :param str msg: text displayed in the message area (instructions...) - :param str title: the window title - :param text: text displayed in textAreas (editable) - :param function callback: if set, this function will be called when OK is pressed - :param bool run: if True, a box object will be created and returned, but not run - :return: - if 'run' is True then the box is run AND Ok is pressed -> returns the content of the TextArea - if 'run' is True then the box is run AND Cancel is pressed -> returns None - else 'run' is False then the box is not run -> returns an instance TextBox + Displays a dialog box with a large, multi-line text box, and returns + the entered text as a string. The message text is displayed in a + proportional font and wraps. + + Parameters + ---------- + msg : string + text displayed in the message area (instructions...) + title : str + the window title + text: str, list or tuple + text displayed in textAreas (editable) + codebox: bool + if True, don't wrap and width is set to 80 chars + callback: function + if set, this function will be called when OK is pressed + run: bool + if True, a box object will be created and returned, but not run + + Returns + ------- + None + If cancel is pressed + str + If OK is pressed returns the contents of textArea. + TextBox + If the 'run' argument was False """ - tb = TextBox(msg=msg, title=title, text=text, monospace=False, callback=callback) - return tb.run() if run else tb + tb = TextBox(msg=msg, title=title, text=text, codebox=codebox, callback=callback) + if run: + text = tb.run() + return text + return tb -def codebox(msg='', title='', text='', callback=None, run=True): +def codebox(msg='', title='', text=''): """ Helper method similar to textbox, displays text in a monospaced font which is useful for code. + The text parameter should be a string, or a list or tuple of lines to be displayed in the textbox. - :param str msg: text displayed in the message area (instructions...) + + :param str msg: the msg to be displayed :param str title: the window title - :param str text: text displayed in textAreas (editable) - :param function callback: if set, this function will be called when OK is pressed - :param bool run: if True, a box object will be created and returned, but not run - :return: - if 'run' is True then the box is run AND Ok is pressed -> returns the content of the TextArea - if 'run' is True then the box is run AND Cancel is pressed -> returns None - else 'run' is False then the box is not run -> returns an instance TextBox + :param str text: what to display in the textbox """ - cb = TextBox(msg, title, text, monospace=True, callback=callback) - return cb.run() if run else cb + return textbox(msg, title, text, codebox=True) def exceptionbox(msg='An error (exception) has occurred in the program.', title='Error Report'): - """ + """ Display a box that gives information about the latest exception that has been raised. + Display a box that gives information about the latest exception that has been raised. - :param str msg: [optional] msg to be displayed above exception - :param str title: [optional] the window title - :return: None - """ - import sys - import traceback - text = "".join(traceback.format_exception(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2])) - TextBox(msg, title, text=text, monospace=True).run() + + The caller may optionally pass in a title for the window, or a msg to accompany the error information. + + :param str msg: the msg to be displayed + :param str title: the window title + :return: None""" + + def format_exception_for_display(): + return "".join(traceback.format_exception(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2])) + + codebox(msg, title, format_exception_for_display()) -class TextBox(AbstractBox): +class TextBox(object): """ Display a message, and an editable text field pre-populated with 'text' """ - def __init__(self, msg, title, text, monospace, callback=None): + def __init__(self, msg, title, text, codebox, callback): """ :param msg: str displayed in the message area (instructions...) :param title: str used as the window title :param text: str displayed in textArea (editable) - :param monospace: bool (if true) don't wrap, set width to 80 witdh_in_chars, use monospace font + :param codebox: bool (if true) don't wrap, set width to 80 chars, use monospace font :param callback: optional function to be called when OK is pressed """ - super().__init__(title, callback) - self.msg_widget = self.configure_message_widget(monospace) + self._user_specified_callback = callback + self._text = text self.msg = msg - self.text_area = self.configure_text_widget(monospace) - self.text = text - self.set_buttons() - - def configure_text_widget(self, monospace): - padding, width_in_chars = get_width_and_padding(monospace=monospace) - - text_frame = tk.Frame(self.box_root, padx=padding) - text_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) - - text_area = tk.Text( - text_frame, - padx=padding, - pady=padding, - height=25, - width=width_in_chars, - wrap=tk.NONE if monospace else tk.WORD, - ) + + self.box_root = self._configure_box_root(title) + self.message_area = self._configure_message_area(box_root=self.box_root, code_box=codebox) + self._set_msg_area("" if msg is None else msg) + + self.MONOSPACE_FONT = font.Font(family='Courier') + self.text_area = self._configure_text_area(box_root=self.box_root, code_box=codebox) + self._set_text() + self._configure_buttons() + + + def _configure_box_root(self, title): + box_root = tk.Tk() + box_root.title(title) + box_root.iconname('Dialog') + box_root.geometry(GLOBAL_WINDOW_POSITION) + box_root.protocol('WM_DELETE_WINDOW', self.x_pressed) # Quit when x button pressed + box_root.bind("", self.cancel_button_pressed) + return box_root + + @staticmethod + def _configure_message_area(box_root, code_box): + padding, width_in_chars = get_width_and_padding(code_box) + + message_frame = tk.Frame(box_root, padx=padding) + message_frame.pack(side=tk.TOP, expand=1, fill='both') + + message_area = tk.Text(master=message_frame, + width=width_in_chars, + padx=padding, + pady=padding, + wrap=tk.WORD) + message_area.pack(side=tk.TOP, expand=1, fill='both') + return message_area + + def _configure_text_area(self, box_root, code_box): + padding, width_in_chars = get_width_and_padding(code_box) + + text_frame = tk.Frame(box_root, padx=padding, ) + text_frame.pack(side=tk.TOP) + + text_area = tk.Text(text_frame, padx=padding, pady=padding, height=25, width=width_in_chars) + text_area.configure(wrap=tk.NONE if code_box else tk.WORD) + vertical_scrollbar = tk.Scrollbar(text_frame, orient=tk.VERTICAL, command=text_area.yview) text_area.configure(yscrollcommand=vertical_scrollbar.set) + horizontal_scrollbar = tk.Scrollbar(text_frame, orient=tk.HORIZONTAL, command=text_area.xview) text_area.configure(xscrollcommand=horizontal_scrollbar.set) - if monospace: - monospace_font = font.Font(family='Courier') - text_area.configure(font=monospace_font) - # no word-wrapping for code, so we may need a horizontal scroll bar + if code_box: + text_area.configure(font=self.MONOSPACE_FONT) + # no word-wrapping for code so we need a horizontal scroll bar horizontal_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) vertical_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + # pack textArea last so bottom scrollbar displays properly text_area.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.YES) - self.box_root.bind("", text_area.yview_scroll(1, tk.PAGES)) - self.box_root.bind("", text_area.yview_scroll(-1, tk.PAGES)) - self.box_root.bind("", text_area.xview_scroll(1, tk.PAGES)) - self.box_root.bind("", text_area.xview_scroll(-1, tk.PAGES)) - self.box_root.bind("", text_area.yview_scroll(1, tk.UNITS)) - self.box_root.bind("", text_area.yview_scroll(-1, tk.UNITS)) + box_root.bind("", text_area.yview_scroll(1, tk.PAGES)) + box_root.bind("", text_area.yview_scroll(-1, tk.PAGES)) + + box_root.bind("", text_area.xview_scroll(1, tk.PAGES)) + box_root.bind("", text_area.xview_scroll(-1, tk.PAGES)) + + box_root.bind("", text_area.yview_scroll(1, tk.UNITS)) + box_root.bind("", text_area.yview_scroll(-1, tk.UNITS)) + return text_area + def _set_msg_area(self, msg): + self.message_area.delete(1.0, tk.END) + self.message_area.insert(tk.END, msg) + num_lines = get_num_lines(message_area=self.message_area) + self.message_area.configure(height=int(num_lines)) + self.message_area.update() + + def run(self): + self.box_root.mainloop() + self.box_root.destroy() + return self.text + + def stop(self): + self.box_root.quit() + + def _configure_buttons(self): + buttons_frame = tk.Frame(self.box_root) + buttons_frame.pack(side=tk.TOP) + + cancel_button = tk.Button(buttons_frame, takefocus=tk.YES, text="Cancel", height=1, width=6) + cancel_button.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", ipadx="2m") + cancel_button.bind("", self.cancel_button_pressed) + cancel_click_handler = MouseClickHandler(callback=self.cancel_button_pressed) + cancel_button.bind("", cancel_click_handler.enter) + cancel_button.bind("", cancel_click_handler.leave) + cancel_button.bind("", cancel_click_handler.release) + + ok_button = tk.Button(buttons_frame, takefocus=tk.YES, text="OK", height=1, width=6) + ok_button.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", ipadx="2m") + ok_button.bind("", self.ok_button_pressed) + ok_click_handler = MouseClickHandler(callback=self.ok_button_pressed) + ok_button.bind("", ok_click_handler.enter) + ok_button.bind("", ok_click_handler.leave) + ok_button.bind("", ok_click_handler.release) + @property def text(self): """ Get _text which may be None if cancel was pressed""" - return self.text_area.get(1.0, 'end-1c') + return self._text @text.setter def text(self, text): - self.text_area.delete(1.0, tk.END) - self.text_area.insert(tk.END, text, "normal") - self.text_area.focus() - - def _set_return_value(self): - self.return_value = self.text_area.get(1.0, 'end-1c') + self._text = text + if text is not None: # cancel sets text=None but this is meaningless to the tk box content + self._set_text() + def _get_text(self): + """ Used by the callback to get the text_area content""" + return self.text_area.get(1.0, 'end-1c') -if __name__ == '__main__': - result = textbox("some message text for a textbox") - print("textbox() return value was: {}".format(result)) + def _set_text(self): + self.text_area.delete(1.0, tk.END) + self.text_area.insert(tk.END, self._text, "normal") + self.text_area.focus() - code_result = codebox("some message text for a codebox") - print("textbox() return value was: {}".format(result)) + # Methods executing when a key is pressed + def x_pressed(self, _): + self.callback(command='x') + + def cancel_button_pressed(self, _): + self.callback(command='cancel') + + def ok_button_pressed(self, _): + self.callback(command='update') + + def callback(self, command): + if command == 'update': # OK was pressed + self.text = self._get_text() + if self._user_specified_callback: + # If a callback was set, call main process + self._user_specified_callback(self) + else: + self.stop() + elif command in ('x', 'cancel'): + self.stop() + self.text = None diff --git a/easygui/utilities.py b/easygui/utilities.py index feec91c..a2cc90d 100644 --- a/easygui/utilities.py +++ b/easygui/utilities.py @@ -3,116 +3,7 @@ import tkinter as tk from easygui.global_state import PROP_FONT_LINE_LENGTH, FIXW_FONT_LINE_LENGTH, DEFAULT_PADDING, REGULAR_FONT_WIDTH, \ - FIXED_FONT_WIDTH, GLOBAL_WINDOW_POSITION - - -class AbstractBox(object): - """ - The following boxes have commonalities, so we can abstract some code here to a parent class - ButtonBox: def __init__(self, msg, title, choices, images, default_choice, cancel_choice, callback): - ChoiceBox: def __init__(self, msg, title, choices, preselect, multiple_select, callback): - FillableBox: def __init__(self, msg, title, default, mask=None, image=None, root=None): - MultiFillableBox: def __init__(self, msg, title, fields=None, values=None, mask_last=False, callback=None): - TextBox def __init__(self, msg, title, text, codebox, callback): - """ - - def __init__(self, title, callback) -> None: - super().__init__() - self._user_specified_callback = callback - self.box_root = self._configure_box_root(title) - self.return_value = None - self.msg_widget = NotImplemented - self.cancel_value = None - - def _set_return_value(self): - raise NotImplemented - - def _configure_box_root(self, title): - box_root = tk.Tk() - box_root.title(title) - box_root.iconname('Dialog') - box_root.geometry(GLOBAL_WINDOW_POSITION) - box_root.bind("", self.cancel_button_pressed) - box_root.protocol('WM_DELETE_WINDOW', self.cancel_button_pressed) - return box_root - - def configure_message_widget(self, monospace): - """ - # TODO: fix bug that the line count does not include wrapped lines - # height = message.tk.call((self.mess\age._w, "count", "-update", "-displaylines", "1.0", "end")) - :param bool monospace: whether the message shold be monospace or proportional text - :return: - """ - padding, width_in_chars = get_width_and_padding(monospace=monospace) - - message_frame = tk.Frame(self.box_root, padx=padding) - message_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) - - message_text = tk.Text( - master=message_frame, - width=width_in_chars, - padx=padding, - pady=padding, - wrap=tk.WORD, - # wrap=tk.NONE if monospace else tk.WORD - ) - return message_text - - @property - def msg(self): - return self.msg_widget.get(1.0, tk.END) - - @msg.setter - def msg(self, message): - self.msg_widget.configure(state=tk.NORMAL) - self.msg_widget.delete(1.0, tk.END) - self.msg_widget.insert(tk.END, message) - self.msg_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=True) - line, char = self.msg_widget.index(tk.END).split('.') - self.msg_widget.configure(height=int(line)) - self.msg_widget.configure(state=tk.DISABLED) - - def set_buttons(self): - buttons_frame = tk.Frame(self.box_root) - buttons_frame.pack(side=tk.TOP) - - cancel_button = tk.Button(buttons_frame, takefocus=tk.YES, text="Cancel", height=1, width=6) - cancel_button.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", ipadx="2m") - cancel_button.bind("", self.cancel_button_pressed) - bind_to_mouse(cancel_button, self.cancel_button_pressed) - - ok_button = tk.Button(buttons_frame, takefocus=tk.YES, text="OK", height=1, width=6) - ok_button.pack(expand=tk.NO, side=tk.LEFT, padx='2m', pady='1m', ipady="1m", ipadx="2m") - ok_button.bind("", self.ok_button_pressed) - bind_to_mouse(ok_button, self.ok_button_pressed) - - def cancel_button_pressed(self, *args): - """ - Set the return value to None so that and quit the mainloop() - Care: may be called: - * with zero args when handling a window close action - * with one arg when handling an Escape button precessed binding - :param args: zero or more args - :return: None - """ - self.return_value = self.cancel_value - self.box_root.quit() - - def ok_button_pressed(self, _): - self._set_return_value() - if self._user_specified_callback: - # If a callback was set, call main process - self._user_specified_callback(self) - else: - self.stop() - - def run(self): - self.box_root.mainloop() - self.box_root.destroy() - return self.return_value - - def stop(self): - self.box_root.quit() + FIXED_FONT_WIDTH, boxRoot def parse_hotkey(text): @@ -210,6 +101,11 @@ def load_tk_image(filename, tk_master=None): return tk_image +def get_num_lines(message_area): + num_lines, _ = message_area.index(tk.END).split('.') + return num_lines + + def get_width_and_padding(monospace): if monospace: padding = DEFAULT_PADDING * FIXED_FONT_WIDTH @@ -220,6 +116,22 @@ def get_width_and_padding(monospace): return padding, width_in_chars +def bindArrows(widget): + widget.bind("", tabRight) + widget.bind("", tabLeft) + + widget.bind("", tabRight) + widget.bind("", tabLeft) + + +def tabRight(event): + boxRoot.event_generate("") + + +def tabLeft(event): + boxRoot.event_generate("") + + class MouseClickHandler: """ Handle mouse events with state to store whether the mouse is actually on a button or not. This lets us ensure that: @@ -239,27 +151,4 @@ def leave(self, _): def release(self, event): if self._mouse_is_on_button: - return self._callback(event) - - -def bind_to_mouse(button, callback): - handler = MouseClickHandler(callback=callback) - button.bind("", handler.enter) - button.bind("", handler.leave) - button.bind("", handler.release) - - -def convert_to_a_list_of_lists(filenames): - """ historically the 'filenames' argument could be - ... a list of lists OR a list OR a string! - Converting all flavours of input to list-of-lists simplifies subsequent handling - """ - if filenames is None: - return [[], ] - elif type(filenames) is str: - return [[filenames, ], ] - elif type(filenames[0]) is str: - return [filenames, ] - elif type(filenames[0][0]) is str: - return filenames - raise ValueError("Incorrect images argument.") + return self._callback(event) \ No newline at end of file diff --git a/tests/test_easygui.py b/tests/test_easygui.py index 344c639..4f85465 100644 --- a/tests/test_easygui.py +++ b/tests/test_easygui.py @@ -36,11 +36,11 @@ def run(self): KEYBOARD.type(self.keyPresses) + def test_test_images_exist(): assert os.path.exists(os.path.join(FOLDER_OF_THIS_FILE, '../demos/pi.jpg')) assert os.path.exists(os.path.join(FOLDER_OF_THIS_FILE, '../demos/result.png')) - def test_spacebar_clicks_choice(): """ Test that the spacebar selects a choice. diff --git a/tests/test_multi_fillable_box.py b/tests/test_multi_fillable_box.py index 1287c96..9c72769 100644 --- a/tests/test_multi_fillable_box.py +++ b/tests/test_multi_fillable_box.py @@ -10,8 +10,8 @@ def test__multenterbox__cancel_results_in_run_returning_none(): callback=None, run=False) def simulate_user_cancel_press(meb_instance): - meb_instance.cancel_button_pressed('ignored button handler arg') + meb_instance._cancel_pressed('ignored button handler arg') - meb.box_root.after(WAIT_0_MILLISECONDS, simulate_user_cancel_press, meb) + meb.boxRoot.after(WAIT_0_MILLISECONDS, simulate_user_cancel_press, meb) actual = meb.run() assert actual is None diff --git a/tests/test_text_box.py b/tests/test_text_box.py index 23c8732..a769447 100644 --- a/tests/test_text_box.py +++ b/tests/test_text_box.py @@ -1,20 +1,19 @@ -import tkinter as tk import unittest -from tkinter import font from mock import patch, Mock, ANY -from easygui.text_box import TextBox, textbox, codebox +from easygui.text_box import TextBox, textbox from tests import WAIT_0_MILLISECONDS, WAIT_1_MILLISECONDS +import tkinter as tk MODBASE = 'easygui.text_box' TEST_MESSAGE = 'example message' TEST_TITLE = 'example title' TEST_TEXT = 'example text' -TEST_MONOSPACE = False +TEST_CODEBOX = False TEST_CALLBACK = Mock() -TEST_ARGS = [TEST_MESSAGE, TEST_TITLE, TEST_TEXT, TEST_MONOSPACE, TEST_CALLBACK] +TEST_ARGS = [TEST_MESSAGE, TEST_TITLE, TEST_TEXT, TEST_CODEBOX, TEST_CALLBACK] def test__textbox_method__instantiates_textbox_class_and_runs_it(): @@ -24,13 +23,13 @@ def test__textbox_method__instantiates_textbox_class_and_runs_it(): mock_text_box_instance.run = Mock(return_value='return text') mock_text_box_class.return_value = mock_text_box_instance - return_text = textbox(TEST_MESSAGE, TEST_TITLE, TEST_TEXT, TEST_CALLBACK, run=True) + return_text = textbox(*TEST_ARGS, run=True) mock_text_box_class.assert_called_once_with( msg=TEST_MESSAGE, title=TEST_TITLE, text=TEST_TEXT, - monospace=TEST_MONOSPACE, + codebox=TEST_CODEBOX, callback=TEST_CALLBACK ) mock_text_box_instance.run.assert_called_once_with() @@ -44,22 +43,22 @@ def setUp(self): def test_instantiation(self): # Instance attributes should be configured: self.assertEqual(self.tb.text, TEST_TEXT) - self.assertEqual(self.tb.msg.strip(), TEST_MESSAGE) + self.assertEqual(self.tb.msg, TEST_MESSAGE) self.assertEqual(self.tb._user_specified_callback, TEST_CALLBACK) # The following Tk widgets should also have been created: isinstance(self.tb.box_root, tk.Tk) - isinstance(self.tb.msg_widget, tk.Tk) + isinstance(self.tb.message_area, tk.Tk) isinstance(self.tb.text_area, tk.Tk) # And configured: - self.assertEqual(self.tb.msg_widget.get(0.0, 'end-1c'), TEST_MESSAGE) + self.assertEqual(self.tb.message_area.get(0.0, 'end-1c'), TEST_MESSAGE) self.assertEqual(self.tb.text_area.get(0.0, 'end-1c'), TEST_TEXT) def test_run(self): self.tb.box_root = Mock() return_value = self.tb.run() - self.assertEqual(return_value, None ) + self.assertEqual(return_value, TEST_TEXT) self.tb.box_root.mainloop.assert_called_once_with() self.tb.box_root.destroy.assert_called_once_with() @@ -70,11 +69,11 @@ def test_stop(self): def test_set_msg_area(self): new_msg = 'some new text' - self.tb.msg = new_msg - self.assertEqual(self.tb.msg_widget.get(1.0, 'end-1c'), new_msg) + self.tb._set_msg_area(msg=new_msg) + self.assertEqual(self.tb.message_area.get(1.0, 'end-1c'), new_msg) def test_get_text(self): - actual = self.tb.text + actual = self.tb._get_text() self.assertEqual(actual, TEST_TEXT) def test_set_text(self): @@ -85,8 +84,18 @@ def test_set_text(self): class TestTextBoxIntegration(unittest.TestCase): + def test_textbox_x_results_in_run_returning_None(self): + tb = textbox(*TEST_ARGS, run=False) + + def simulate_user_x_press(tb_instance): + tb_instance.x_pressed('ignored button handler arg') + + tb.box_root.after(WAIT_0_MILLISECONDS, simulate_user_x_press, tb) + actual = tb.run() + self.assertEqual(actual, None) + def test_textbox_cancel_button_pressed_results_in_run_returning_None(self): - tb = textbox(run=False) + tb = textbox(*TEST_ARGS, run=False) def simulate_cancel_button_pressed(tb_instance): tb_instance.cancel_button_pressed('ignored button handler arg') @@ -97,7 +106,7 @@ def simulate_cancel_button_pressed(tb_instance): self.assertEqual(actual, None) def test_textbox_ok_pressed_calls_user_defined_callback(self): - tb = textbox(text=TEST_TEXT, callback=TEST_CALLBACK, run=False) + tb = textbox(*TEST_ARGS, run=False) def simulate_ok_button_pressed(tb_instance): tb_instance.ok_button_pressed('ignored button handler arg') @@ -113,7 +122,14 @@ def stop_running(tb_instance): self.assertEqual(actual, TEST_TEXT) def test_textbox_ok_pressed_with_no_user_defined_callback(self): - tb = textbox(msg=TEST_MESSAGE, title=TEST_TITLE, text=TEST_TEXT, run=False) + tb = textbox( + msg=TEST_MESSAGE, + title=TEST_TITLE, + text=TEST_TEXT, + codebox=TEST_CODEBOX, + callback=None, + run=False + ) def simulate_ok_button_pressed(tb_instance): tb_instance.ok_button_pressed('ignored button handler arg') @@ -128,8 +144,15 @@ def simulate_ok_button_pressed(tb_instance): class TestTextBoxCodeBox(unittest.TestCase): - def test_instantiation_codebox(self): - cb = codebox(msg=TEST_MESSAGE, title=TEST_TITLE, text=TEST_TEXT * 100, callback=TEST_CALLBACK, run=False) + def test_instantiation_codebox(self): + tb = textbox( + msg=TEST_MESSAGE, + title=TEST_TITLE, + text=TEST_TEXT * 100, + codebox=True, + callback=None, + run=False + ) # cget returns strings so the monospace assertion is a bit messy: - self.assertEqual(cb.text_area.cget('font'), "font1") # a monospace font + self.assertEqual(tb.text_area.cget('font'), str(tb.MONOSPACE_FONT)) From 1c513508f50c1e42175a470098a897c5fe2649dd Mon Sep 17 00:00:00 2001 From: Alexander Zawadzki Date: Sun, 30 Mar 2025 15:01:12 +0100 Subject: [PATCH 29/29] convert some Unittest format tests to Pytest but intermittent tk imports still fail Looks like the same issue reported in: https://stackoverflow.com/questions/71443540/intermittent-pytest-failures-complaining-about-missing-tcl-files-even-though-the --- easygui/button_box.py | 2 +- easygui/utilities.py | 2 +- setup.py | 11 +- sphinx/conf.py | 4 +- .../{tet_choice_box.py => test_choice_box.py} | 2 +- tests/test_easygui.py | 5 +- tests/test_multi_fillable_box.py | 2 - tests/test_text_box.py | 168 ++++++++---------- 8 files changed, 82 insertions(+), 114 deletions(-) rename tests/{tet_choice_box.py => test_choice_box.py} (98%) diff --git a/easygui/button_box.py b/easygui/button_box.py index d69b1cf..793484f 100644 --- a/easygui/button_box.py +++ b/easygui/button_box.py @@ -10,7 +10,7 @@ def buttonbox(msg="buttonbox options", title=" ", choices=("Button[1]", "Button[ The buttons are defined by the members of the choices argument. :param str msg: the msg to be displayed :param str title: the window title - :param list choices: a list or tuple of the choices to be displayed + :param tuple choices: a list or tuple of the choices to be displayed :param str image: (Only here for backward compatibility) :param str images: Filename of image or iterable or iteratable of iterable to display :param str default_choice: The choice you want highlighted when the gui appears diff --git a/easygui/utilities.py b/easygui/utilities.py index a2cc90d..8d4d4c5 100644 --- a/easygui/utilities.py +++ b/easygui/utilities.py @@ -86,7 +86,7 @@ def load_tk_image(filename, tk_master=None): from PIL import ImageTk as PILImageTk pil_image = PILImage.open(filename) tk_image = PILImageTk.PhotoImage(pil_image, master=tk_master) - except: + except ModuleNotFoundError: try: # Fallback if PIL isn't available tk_image = tk.PhotoImage(file=filename, master=tk_master) diff --git a/setup.py b/setup.py index 8724166..44bf726 100644 --- a/setup.py +++ b/setup.py @@ -30,17 +30,14 @@ 'Intended Audience :: Education', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Topic :: Software Development :: User Interfaces', ] ) diff --git a/sphinx/conf.py b/sphinx/conf.py index 6ac6e62..2e5a0de 100644 --- a/sphinx/conf.py +++ b/sphinx/conf.py @@ -280,8 +280,8 @@ def suppress_module_docstring(app, what, name, obj, options, lines): def functions_to_headers(app, what, name, obj, options, lines): if what == 'function': - lines.insert(0,'.. py:function:: freddy') # Not needed - lines.insert(1,'') + lines.insert_at(0, '.. py:function:: freddy') # Not needed + lines.insert_at(1, '') print(lines[0]) print(obj) print(options) diff --git a/tests/tet_choice_box.py b/tests/test_choice_box.py similarity index 98% rename from tests/tet_choice_box.py rename to tests/test_choice_box.py index e084335..1cfe805 100644 --- a/tests/tet_choice_box.py +++ b/tests/test_choice_box.py @@ -1,6 +1,6 @@ import unittest -from mock import patch, Mock +from unittest.mock import patch, Mock from easygui.choice_box import choicebox, multchoicebox diff --git a/tests/test_easygui.py b/tests/test_easygui.py index 4f85465..3eb04e2 100644 --- a/tests/test_easygui.py +++ b/tests/test_easygui.py @@ -52,8 +52,9 @@ def test_spacebar_clicks_choice(): (('Message', 'Title'), {}, 'OK'), # custom message and title ((), dict(ok_button='Button'), 'Button'), # custom button text (('Message', 'Title'), dict(ok_button='Button'), 'Button'), # combo of all three - ((), dict(image=os.path.join(FOLDER_OF_THIS_FILE, '../demos/pi.jpg')), 'OK'), # test jpg - ((), dict(image=os.path.join(FOLDER_OF_THIS_FILE, '../demos/result.png')), 'OK'), # test png + ((), dict(image=os.path.join(FOLDER_OF_THIS_FILE, '../demos/images/python_and_check_logo.gif')), 'OK'), # gif + ((), dict(image=os.path.join(FOLDER_OF_THIS_FILE, '../demos/images/python_and_check_logo.png')), 'OK'), # png + # ((), dict(image=os.path.join(FOLDER_OF_THIS_FILE, '../demos/images/python_and_check_logo.jpg')), 'OK'), # jpg ... requires PIL library ) for args, kwargs, expected in parameters: diff --git a/tests/test_multi_fillable_box.py b/tests/test_multi_fillable_box.py index 9c72769..9ca660d 100644 --- a/tests/test_multi_fillable_box.py +++ b/tests/test_multi_fillable_box.py @@ -1,5 +1,3 @@ -import easygui -import easygui.global_state from easygui import multenterbox from tests import WAIT_0_MILLISECONDS diff --git a/tests/test_text_box.py b/tests/test_text_box.py index a769447..47dce82 100644 --- a/tests/test_text_box.py +++ b/tests/test_text_box.py @@ -1,19 +1,20 @@ +import tkinter as tk import unittest +from unittest.mock import patch, Mock, ANY -from mock import patch, Mock, ANY +import pytest -from easygui.text_box import TextBox, textbox +from easygui.text_box import TextBox, textbox, codebox from tests import WAIT_0_MILLISECONDS, WAIT_1_MILLISECONDS -import tkinter as tk MODBASE = 'easygui.text_box' TEST_MESSAGE = 'example message' TEST_TITLE = 'example title' TEST_TEXT = 'example text' -TEST_CODEBOX = False +TEST_MONOSPACE = False TEST_CALLBACK = Mock() -TEST_ARGS = [TEST_MESSAGE, TEST_TITLE, TEST_TEXT, TEST_CODEBOX, TEST_CALLBACK] +TEST_ARGS = [TEST_MESSAGE, TEST_TITLE, TEST_TEXT, TEST_MONOSPACE, TEST_CALLBACK] def test__textbox_method__instantiates_textbox_class_and_runs_it(): @@ -23,136 +24,107 @@ def test__textbox_method__instantiates_textbox_class_and_runs_it(): mock_text_box_instance.run = Mock(return_value='return text') mock_text_box_class.return_value = mock_text_box_instance - return_text = textbox(*TEST_ARGS, run=True) + return_text = textbox(TEST_MESSAGE, TEST_TITLE, TEST_TEXT, TEST_CALLBACK, run=True) mock_text_box_class.assert_called_once_with( msg=TEST_MESSAGE, title=TEST_TITLE, text=TEST_TEXT, - codebox=TEST_CODEBOX, + monospace=TEST_MONOSPACE, callback=TEST_CALLBACK ) mock_text_box_instance.run.assert_called_once_with() assert return_text == 'return text' -class TestTextBox(unittest.TestCase): - def setUp(self): - self.tb = TextBox(*TEST_ARGS) - - def test_instantiation(self): - # Instance attributes should be configured: - self.assertEqual(self.tb.text, TEST_TEXT) - self.assertEqual(self.tb.msg, TEST_MESSAGE) - self.assertEqual(self.tb._user_specified_callback, TEST_CALLBACK) +@pytest.fixture() +def test_textbox(): + yield TextBox(*TEST_ARGS) - # The following Tk widgets should also have been created: - isinstance(self.tb.box_root, tk.Tk) - isinstance(self.tb.message_area, tk.Tk) - isinstance(self.tb.text_area, tk.Tk) - # And configured: - self.assertEqual(self.tb.message_area.get(0.0, 'end-1c'), TEST_MESSAGE) - self.assertEqual(self.tb.text_area.get(0.0, 'end-1c'), TEST_TEXT) +def test_instantiation(test_textbox): + # Instance attributes should be configured: + assert test_textbox.text == TEST_TEXT + assert test_textbox.msg.strip() == TEST_MESSAGE + assert test_textbox._user_specified_callback == TEST_CALLBACK - def test_run(self): - self.tb.box_root = Mock() - return_value = self.tb.run() - self.assertEqual(return_value, TEST_TEXT) - self.tb.box_root.mainloop.assert_called_once_with() - self.tb.box_root.destroy.assert_called_once_with() + # The following Tk widgets should also have been created: + assert isinstance(test_textbox.box_root, tk.Tk) + assert isinstance(test_textbox.msg_widget, tk.Text) + assert isinstance(test_textbox.text_area, tk.Text) - def test_stop(self): - self.tb.box_root = Mock() - self.tb.stop() - self.tb.box_root.quit.assert_called_once_with() + # And configured: + assert test_textbox.msg_widget.get(0.0, 'end-1c') == TEST_MESSAGE + assert test_textbox.text_area.get(0.0, 'end-1c') == TEST_TEXT - def test_set_msg_area(self): - new_msg = 'some new text' - self.tb._set_msg_area(msg=new_msg) - self.assertEqual(self.tb.message_area.get(1.0, 'end-1c'), new_msg) - def test_get_text(self): - actual = self.tb._get_text() - self.assertEqual(actual, TEST_TEXT) +def test_run(test_textbox): + with unittest.mock.patch.object(test_textbox, 'box_root') as mockroot: + assert test_textbox.run() is None + mockroot.mainloop.assert_called_once_with() + mockroot.destroy.assert_called_once_with() - def test_set_text(self): - new_text = 'some new text' - self.tb.text = new_text - self.assertEqual(self.tb.text_area.get(1.0, 'end-1c'), new_text) +def test_stop(test_textbox): + with unittest.mock.patch.object(test_textbox, 'box_root') as mockroot: + test_textbox.stop() + mockroot.quit.assert_called_once_with() -class TestTextBoxIntegration(unittest.TestCase): - def test_textbox_x_results_in_run_returning_None(self): - tb = textbox(*TEST_ARGS, run=False) +def test_set_msg_area(test_textbox): + new_msg = 'some new text' + test_textbox.msg = new_msg + assert test_textbox.msg_widget.get(1.0, 'end-1c') == new_msg - def simulate_user_x_press(tb_instance): - tb_instance.x_pressed('ignored button handler arg') - tb.box_root.after(WAIT_0_MILLISECONDS, simulate_user_x_press, tb) - actual = tb.run() - self.assertEqual(actual, None) +def test_set_text(test_textbox): + assert test_textbox.text == TEST_TEXT - def test_textbox_cancel_button_pressed_results_in_run_returning_None(self): - tb = textbox(*TEST_ARGS, run=False) + new_text = 'some new text' + test_textbox.text = new_text + assert test_textbox.text_area.get(1.0, 'end-1c') == new_text - def simulate_cancel_button_pressed(tb_instance): - tb_instance.cancel_button_pressed('ignored button handler arg') - tb.box_root.after(WAIT_0_MILLISECONDS, simulate_cancel_button_pressed, tb) - actual = tb.run() +def test_textbox_cancel_button_pressed_results_in_run_returning_None(): + tb = textbox(run=False) - self.assertEqual(actual, None) + def simulate_cancel_button_pressed(tb_instance): + tb_instance.cancel_button_pressed('ignored button handler arg') - def test_textbox_ok_pressed_calls_user_defined_callback(self): - tb = textbox(*TEST_ARGS, run=False) + tb.box_root.after(WAIT_0_MILLISECONDS, simulate_cancel_button_pressed, tb) + assert tb.run() is None - def simulate_ok_button_pressed(tb_instance): - tb_instance.ok_button_pressed('ignored button handler arg') - def stop_running(tb_instance): - tb_instance.stop() +def test_textbox_ok_pressed_calls_user_defined_callback(): + user_defined_callback = Mock() + tb = textbox(text=TEST_TEXT, callback=user_defined_callback, run=False) - tb.box_root.after(WAIT_0_MILLISECONDS, simulate_ok_button_pressed, tb) - tb.box_root.after(WAIT_1_MILLISECONDS, stop_running, tb) - actual = tb.run() + def simulate_ok_button_pressed(tb_instance): + tb_instance.ok_button_pressed('ignored button handler arg') - TEST_CALLBACK.assert_called_once_with(ANY) - self.assertEqual(actual, TEST_TEXT) + def stop_running(tb_instance): + tb_instance.stop() - def test_textbox_ok_pressed_with_no_user_defined_callback(self): - tb = textbox( - msg=TEST_MESSAGE, - title=TEST_TITLE, - text=TEST_TEXT, - codebox=TEST_CODEBOX, - callback=None, - run=False - ) + tb.box_root.after(WAIT_0_MILLISECONDS, simulate_ok_button_pressed, tb) + tb.box_root.after(WAIT_1_MILLISECONDS, stop_running, tb) + assert tb.run() == TEST_TEXT + user_defined_callback.assert_called_once_with(ANY) - def simulate_ok_button_pressed(tb_instance): - tb_instance.ok_button_pressed('ignored button handler arg') - tb.box_root.after(WAIT_0_MILLISECONDS, simulate_ok_button_pressed, tb) - actual = tb.run() +def test_textbox_ok_pressed_with_no_user_defined_callback(): + tb = textbox(msg=TEST_MESSAGE, title=TEST_TITLE, text=TEST_TEXT, run=False) - # tb.stop() happens because no user _user_specified_callback is set - # the initial text value is unchanged, and is returned from run() - self.assertEqual(actual, TEST_TEXT) + def simulate_ok_button_pressed(tb_instance): + tb_instance.ok_button_pressed('ignored button handler arg') + tb.box_root.after(WAIT_0_MILLISECONDS, simulate_ok_button_pressed, tb) + # tb.stop() happens because no user _user_specified_callback is set + # the initial text value is unchanged, and is returned from run() + assert tb.run() == TEST_TEXT -class TestTextBoxCodeBox(unittest.TestCase): - def test_instantiation_codebox(self): - tb = textbox( - msg=TEST_MESSAGE, - title=TEST_TITLE, - text=TEST_TEXT * 100, - codebox=True, - callback=None, - run=False - ) +def test_instantiation_codebox(): + cb = codebox(msg=TEST_MESSAGE, title=TEST_TITLE, text=TEST_TEXT * 100, callback=TEST_CALLBACK, run=False) - # cget returns strings so the monospace assertion is a bit messy: - self.assertEqual(tb.text_area.cget('font'), str(tb.MONOSPACE_FONT)) + # cget returns strings so the monospace assertion is a bit messy: + assert cb.text_area.cget('font') == "font1" # a monospace font