PySimpleGUI/PySimpleGUI.py

23645 lines
1.6 MiB
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/python3
version = __version__ = "4.56.0.15 Unreleased"
_change_log = """
Changelog since 4.56.0 released to PyPI on 5-Jan-2022
4.56.0.1
set_options - added disable_modal_windows option to provide a single call to disable the modal feature globally (including popups)
4.56.0.2
Added OptionMenu to the list of tkinter widgets that are ignored when the grab anywhere feature is used
4.56.0.3
Slider - update the range FIRST and then the value in the update method (thank you Jason for the fix)
4.56.0.4
Updated docstrings for all Element.update methods to indicate that the helper function "pin" need to be used to keep an element
in place if visibility changes
4.56.0.5
Replaced sponsor tab with a tab about the udemy course as well as the buy me a coffee link.
4.56.0.6
Fixed event generation for Spin element. Also changed to use the "command" parameter to get the event. NOTE - still need to handle manual entry
4.56.0.7
New Emojis for 2022!
4.56.0.8
Changed +CICKED+ to +CLICKED+ (typo) in the table header
Added constant TABLE_CLICKED_INDICATOR that is the value '+CLICKED+' so that it can be referenced instead of user's hard cording a string
4.56.0.9
Added class method Text.fonts_installed_list - returns list of fonts as reported by tkinter
4.56.0.10
New coupon... hmmm... this is going to get tiring since coupons are forced to expire in a timeframe not our choosing
4.56.0.11
Horizontal scrollbar for Multline element (long awaited). New parameter added to control just this one scrollbar. The no_scrollbar existing parm
refers to the vertical scrollbar
4.56.0.12
Fix for font parameter only being applied to Text portion of popup_get_text. Should have been to the entire window.
Updated the internal keys to use the -KEY- coding convention. Was using the really old _KEY_ coding convention.
4.56.0.13
Added check for bad Image filename in Image.update. Will show an error popup now like the initial Image element creation error popup
4.56.0.14
New coupon
4.56.0.15
Addition of parameter paste (bool) to Input.update. Indicates that clipboard should be pasted into element
"""
__version__ = version.split()[0] # For PEP 396 and PEP 345
# The shortened version of version
try:
ver = version.split(' ')[0]
except:
ver = ''
# __version__ = version
port = 'PySimpleGUI'
# 8""""8 8""""8 8""""8 8 8 8
# 8 8 e e 8 e eeeeeee eeeee e eeee 8 " 8 8 8
# 8eeee8 8 8 8eeeee 8 8 8 8 8 8 8 8 8e 8e 8 8e
# 88 8eeee8 88 8e 8e 8 8 8eee8 8e 8eee 88 ee 88 8 88
# 88 88 e 88 88 88 8 8 88 88 88 88 8 88 8 88
# 88 88 8eee88 88 88 8 8 88 88eee 88ee 88eee8 88ee8 88
"""
Copyright 2018, 2019, 2020, 2021, 2022 PySimpleGUI(tm)
Before getting into the details, let's talk about the high level goals of the PySimpleGUI project.
From the inception these have been the project principals upon which it is all built
1. Fun - it's a serious goal of the project. If we're not having FUN while making stuff, then something's not right
2. Successful - you need to be successful or it's all for naught
3. You are the important party - It's your success that determines the success of PySimpleGUI
If these 3 things are kept at the forefront, then the rest tends to fall into place.
PySimpleGUI is a "system", not just a program. There are 4 components of the "PySimpleGUI system"
1. This software - PySimpleGUI.com
2. The documentation - PySimpleGUI.org
* PySimpleGUI.org
* Calls.PySimpleGUI.org
* Cookbook.PySimpleGUI.org
3. Demo Programs - Demos.PySimpleGUI.org
4. Support - Issues.PySimpleGUI.org
5. eCookbook - eCookbook.PySimpleGUI.org
Now available - "The Official PySimpleGUI Course" on Udemy!
https://www.udemy.com/pysimplegui
Watch for a coupon codes in the documentation on PySimpleGUI.org
Please consider sponsoring all open source developers that make software you or your business use. They need your help.
This software is available for your use under a LGPL3+ license
This notice, these first 150 lines of code shall remain unchanged
888 .d8888b. 8888888b. 888 .d8888b.
888 d88P Y88b 888 Y88b 888 d88P Y88b
888 888 888 888 888 888 .d88P
888 888 888 d88P 888 8888" 888
888 888 88888 8888888P" 888 "Y8b. 8888888
888 888 888 888 888 888 888 888
888 Y88b d88P 888 888 Y88b d88P
88888888 "Y8888P88 888 88888888 "Y8888P"
In addition to the normal publishing requirements of LGPL3+, these also apply:
1. These and all comments are to remain in the source code
2. The "Official" version of PySimpleGUI and the associated documentation lives on two (and **only** two) places:
1. GitHub - (http://www.PySimpleGUI.com) currently pointing at:
https://github.com/PySimpleGUI/PySimpleGUI
2. PyPI - pip install PySimpleGUI is the customary way of obtaining the latest release
THE official documentation location is:
Read the Docs (via http://www.PySimpleGUI.org). Currently is pointed at:
https://pysimplegui.readthedocs.io/en/latest/
If you've obtained this software in any other way, then those listed here, then SUPPORT WILL NOT BE PROVIDED.
3. If you use PySimpleGUI in your project/product, a notice of its use needs to be displayed in your readme file as per the license agreement
-----------------------------------------------------------------------------------------------------------------
The first bit of good news for you is that literally 100s of pages of documentation await you.
300 Demo Programs have been written as a "jump start" mechanism to get your running as quickly as possible.
Some general bits of advice:
Upgrade your software! python -m pip install --upgrade --no-cache-dir PySimpleGUI
If you're thinking of filing an Issue or posting a problem, Upgrade your software first
There are constantly something new and interesting coming out of this project so stay current if you can
The FASTEST WAY to learn PySimpleGUI is to begin to play with it, and to read the documentation.
http://www.PySimpleGUI.org
http://Calls.PySimpleGUI.org
http://Cookbook.PySimpleGUI.org
The User Manual and the Cookbook are both designed to paint some nice looking GUIs on your screen within 5 minutes of you deciding to PySimpleGUI out.
A final note from mike...
“Dont aim at success. The more you aim at it and make it a target, the more you are going to miss it.
For success, like happiness, cannot be pursued; it must ensue, and it only does so as the unintended side effect of ones personal dedication to a cause greater.”
— Viktor Frankl
I first saw this quote in a truncated format:
"Happiness, cannot be pursued; it must ensue, and it only does so as the unintended side effect of ones personal dedication to a cause greater."
Everyone is different, but my experience with the PySimpleGUI project matches this theory. It's taken a lifetime of trying and "failing" and trying
to find happiness before I finally figured this truth-for-me out. If I do a long list of things, and live life in a kind & loving way, then the
result is happiness. It's a biproduct, not a directly produced thing. This should be taught in school. Or maybe it can't.
I hope you find happiness, but more importantly, or maybe first, I hope you find that bigger-than-you thing. For me it's always been programming. It seems to be
the giving back part, not just the calling, that makes the happiness fusion-reactor operate.
"Thank you" has fueled this project. I'm incredibly grateful to have users that are in turn grateful. It's a feedback loop of gratitude. What a fantastic thing!
"""
# all of the tkinter involved imports
import tkinter as tk
from tkinter import filedialog
from tkinter.colorchooser import askcolor
from tkinter import ttk
import tkinter.scrolledtext as tkst
import tkinter.font
from uuid import uuid4
# end of tkinter specific imports
# get the tkinter detailed version
tclversion_detailed = tkinter.Tcl().eval('info patchlevel')
framework_version = tclversion_detailed
import time
import pickle
import calendar
import datetime
import textwrap
import inspect
import traceback
import difflib
import copy
import pprint
try: # Because Raspberry Pi is still on 3.4....it's not critical if this module isn't imported on the Pi
from typing import List, Any, Union, Tuple, Dict, SupportsAbs, Optional # because this code has to run on 2.7 can't use real type hints. Must do typing only in comments
except:
print('*** Skipping import of Typing module. "pip3 install typing" to remove this warning ***')
import random
import warnings
from math import floor
from math import fabs
from functools import wraps
try: # Because Raspberry Pi is still on 3.4....
# from subprocess import run, PIPE, Popen
import subprocess
except Exception as e:
print('** Import error {} **'.format(e))
import threading
import itertools
import os
import json
import configparser
import queue
try:
import webbrowser
webbrowser_available = True
except:
webbrowser_available = False
# used for github upgrades
import urllib.request
import urllib.error
import urllib.parse
import pydoc
from urllib import request
import os
import sys
import re
import tempfile
import ctypes
import platform
warnings.simplefilter('always', UserWarning)
g_time_start = 0
g_time_end = 0
g_time_delta = 0
# These timer routines are to help you quickly time portions of code. Place the timer_start call at the point
# you want to start timing and the timer_stop at the end point. The delta between the start and stop calls
# is returned from calling timer_stop
def timer_start():
"""
Time your code easily.... starts the timer.
Uses the time.time value, a technique known to not be terribly accurage, but tis' gclose enough for our purposes
"""
global g_time_start
g_time_start = time.time()
def timer_stop():
"""
Time your code easily.... stop the timer and print the number of milliseconds since the timer start
:return: delta in milliseconds from timer_start was called
:rtype: int
"""
global g_time_delta, g_time_end
g_time_end = time.time()
g_time_delta = g_time_end - g_time_start
return int(g_time_delta * 1000)
def _timeit(func):
"""
Put @_timeit as a decorator to a function to get the time spent in that function printed out
:param func: Decorated function
:type func:
:return: Execution time for the decorated function
:rtype:
"""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print('{} executed in {:.4f} seconds'.format(func.__name__, end - start))
return result
return wrapper
_timeit_counter = 0
MAX_TIMEIT_COUNT = 1000
_timeit_total = 0
def _timeit_summary(func):
"""
Same as the timeit decorator except that the value is shown as an averave
Put @_timeit_summary as a decorator to a function to get the time spent in that function printed out
:param func: Decorated function
:type func:
:return: Execution time for the decorated function
:rtype:
"""
@wraps(func)
def wrapper(*args, **kwargs):
global _timeit_counter, _timeit_total
start = time.time()
result = func(*args, **kwargs)
end = time.time()
_timeit_counter += 1
_timeit_total += end - start
if _timeit_counter > MAX_TIMEIT_COUNT:
print('{} executed in {:.4f} seconds'.format(func.__name__, _timeit_total / MAX_TIMEIT_COUNT))
_timeit_counter = 0
_timeit_total = 0
return result
return wrapper
def running_linux():
"""
Determines the OS is Linux by using sys.platform
Returns True if Linux
:return: True if sys.platform indicates running Linux
:rtype: (bool)
"""
return sys.platform.startswith('linux')
def running_mac():
"""
Determines the OS is Mac by using sys.platform
Returns True if Mac
:return: True if sys.platform indicates running Mac
:rtype: (bool)
"""
return sys.platform.startswith('darwin')
def running_windows():
"""
Determines the OS is Windows by using sys.platform
Returns True if Windows
:return: True if sys.platform indicates running Windows
:rtype: (bool)
"""
return sys.platform.startswith('win')
def running_trinket():
"""
A special case for Trinket. Checks both the OS and the number of environment variables
Currently, Trinket only has ONE environment variable. This fact is used to figure out if Trinket is being used.
Returns True if "Trinket" (in theory)
:return: True if sys.platform indicates Linux and the number of environment variables is 1
:rtype: (bool)
"""
if len(os.environ) == 1 and sys.platform.startswith('linux'):
return True
return False
def running_replit():
"""
A special case for REPLIT. Checks both the OS and for the existance of the number of environment variable REPL_OWNER
Currently, Trinket only has ONE environment variable. This fact is used to figure out if Trinket is being used.
Returns True if running on "replit"
:return: True if sys.platform indicates Linux and setting REPL_OWNER is found in the environment variables
:rtype: (bool)
"""
if 'REPL_OWNER' in os.environ and sys.platform.startswith('linux'):
return True
return False
# Handy python statements to increment and decrement with wrapping that I don't want to forget
# count = (count + (MAX - 1)) % MAX # Decrement - roll over to MAX from 0
# count = (count + 1) % MAX # Increment to MAX then roll over to 0
"""
Welcome to the "core" PySimpleGUI code....
It's a mess.... really... it's a mess internally... it's the external-facing interfaces that
are not a mess. The Elements and the methods for them are well-designed.
PEP8 - this code is far far from PEP8 compliant.
It was written PRIOR to learning that PEP8 existed.
I'll be honest.... started learning Python in Nov 2017, started writing PySimpleGUI in Feb 2018.
Released PySimpleGUI in July 2018. I knew so little about Python that my parameters were all named
using CamelCase. DOH! Someone on Reddit set me straight on that. So overnight I renamed all of the
parameters to lower case. Unfortunately, the internal naming conventions have been set. Mixing them
with PEP8 at this moment would be even MORE confusing.
Code I write now, outside PySimpleGUI, IS PEP8 compliant.
The variable and function naming in particular are not compliant. There is
liberal use of CamelVariableAndFunctionNames, but for anything externally facing, there are aliases
available for all functions. If you've got a serious enough problem with 100% PEP8 compliance
that you'll pass on this package, then that's your right and I invite you to do so. However, if
perhaps you're a practical thinker where it's the results that matter, then you'll have no
trouble with this code base. There is consisency however.
I truly hope you get a lot of enjoyment out of using PySimpleGUI. It came from good intentions.
"""
# ----====----====----==== Constants the user CAN safely change ====----====----====----#
# Base64 encoded GIF file
DEFAULT_BASE64_ICON = b'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'
DEFAULT_BASE64_ICON_16_BY_16 = b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAKCSURBVDhPVZNbSFRRFIb35YwXItBIGtDsiqENEUTRjJlZkJggPSUYBD0UhULElE6hBY6ID/ZSpD1IDxaCEPhUaFLRQyWRNxIJe8syMxCjMCbB07fOsaMt+GftvWf//7/2Whyt1sTei/fCpDqQBTrGOi9Myrk7URwhnQUfQLeOvErJuUQgADlK6gObvAOl5sHx0doHljwARFRiCpxG5J1sjPxALiYNgn9kiQ3gafdYUYzseCd+FICX7sShw7LR++q6cl3XHaXQHFdOJLxFsJtvKHnbUr1nqp01hhStpXAzo7TZZXOjJ+9orT9pY74aY3ZobZZYW8D/GpjM19Ob088fmJxW2tkC4AJt17Oeg2MLrHX6jXWes16w1sbBkrFWBTB2nTLpv5VJg7wGNhRDwCS0tR1cbECkidwMQohAdoScqiz8/FCZUKlPCgSWlQ71elOI1fcco9hCXp1kS7dX3u+qVOm2L4nW8qE4Neetvl8v83NOb++9703BcUI/cU3imuWV7JedKtv5LdFaMRzHLW+N+zJoVDZzRLj6SFNfPlMYwy5bDiRcCojmz15tKx+6hKPv7LvjrG/Q2RoOwjSyzNDlahyzA2dAJeNtFcMHA2cfLn24STNr6P4I728jJ7hvf/lEGuaXLnkRAp0PyFK+hlyLSJGyGWnKyeBi2oJU0IPIjNd15uuL2f2PJgueQBKhVRETCgNeYU+xaeEpnWaw8cQPRM7g/McT8eF0De9u7P+49TqXF7no98BDEEkdvvXem8LAtfJniFRB/A5XeiAiG2+/icgHVQUW5d5KyAhl3M2y+U+ysv1FDukyKGQW3Y+vHJWvU7mz8RJSPZgDd3H2RqiUUn8BSQuaBvGjGpsAAAAASUVORK5CYII='
DEFAULT_BASE64_LOADING_GIF = b'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'
# Old debugger logo
# PSG_DEBUGGER_LOGO = b'R0lGODlhMgAtAPcAAAAAADD/2akK/4yz0pSxyZWyy5u3zZ24zpW30pG52J250J+60aC60KS90aDC3a3E163F2K3F2bPI2bvO3rzP3qvJ4LHN4rnR5P/zuf/zuv/0vP/0vsDS38XZ6cnb6f/xw//zwv/yxf/1w//zyP/1yf/2zP/3z//30wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAAyAC0AAAj/AP8JHEiwoMGDCBMqXMiwoUOFAiJGXBigYoAPDxlK3CigwUGLIAOEyIiQI8cCBUOqJFnQpEkGA1XKZPlPgkuXBATK3JmRws2bB3TuXNmQw8+jQoeCbHj0qIGkSgNobNoUqlKIVJs++BfV4oiEWalaHVpyosCwJidw7Sr1YMQFBDn+y4qSbUW3AiDElXiWqoK1bPEKGLixr1jAXQ9GuGn4sN22Bl02roo4Kla+c8OOJbsQM9rNPJlORlr5asbPpTk/RP2YJGu7rjWnDm2RIQLZrSt3zgp6ZmqwmkHAng3ccWDEMe8Kpnw8JEHlkXnPdh6SxHPILaU/dp60LFUP07dfRq5aYntohAO0m+c+nvT6pVMPZ3jv8AJu8xktyNbw+ATJDtKFBx9NlA20gWU0DVQBYwZhsJMICRrkwEYJJGRCSBtEqGGCAQEAOw=='
PSG_DEBUGGER_LOGO = b'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'
UDEMY_ICON = b'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'
BLANK_BASE64 = b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAANSURBVBhXY2BgYGAAAAAFAAGKM+MAAAAAAElFTkSuQmCC'
BLANK_BASE64 = b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
DEFAULT_WINDOW_ICON = DEFAULT_BASE64_ICON
DEFAULT_ELEMENT_SIZE = (45, 1) # In CHARACTERS
DEFAULT_BUTTON_ELEMENT_SIZE = (10, 1) # In CHARACTERS
DEFAULT_MARGINS = (10, 5) # Margins for each LEFT/RIGHT margin is first term
DEFAULT_ELEMENT_PADDING = (5, 3) # Padding between elements (row, col) in pixels
DEFAULT_AUTOSIZE_TEXT = True
DEFAULT_AUTOSIZE_BUTTONS = True
DEFAULT_FONT = ("Helvetica", 10)
DEFAULT_TEXT_JUSTIFICATION = 'left'
DEFAULT_BORDER_WIDTH = 1
DEFAULT_AUTOCLOSE_TIME = 3 # time in seconds to show an autoclose form
DEFAULT_DEBUG_WINDOW_SIZE = (80, 20)
DEFAULT_WINDOW_LOCATION = (None, None)
MAX_SCROLLED_TEXT_BOX_HEIGHT = 50
DEFAULT_TOOLTIP_TIME = 400
DEFAULT_TOOLTIP_OFFSET = (0, -20)
DEFAULT_KEEP_ON_TOP = None
DEFAULT_SCALING = None
TOOLTIP_BACKGROUND_COLOR = "#ffffe0"
TOOLTIP_FONT = None
#################### COLOR STUFF ####################
BLUES = ("#082567", "#0A37A3", "#00345B")
PURPLES = ("#480656", "#4F2398", "#380474")
GREENS = ("#01826B", "#40A860", "#96D2AB", "#00A949", "#003532")
YELLOWS = ("#F3FB62", "#F0F595")
TANS = ("#FFF9D5", "#F4EFCF", "#DDD8BA")
NICE_BUTTON_COLORS = ((GREENS[3], TANS[0]),
('#000000', '#FFFFFF'),
('#FFFFFF', '#000000'),
(YELLOWS[0], PURPLES[1]),
(YELLOWS[0], GREENS[3]),
(YELLOWS[0], BLUES[2]))
COLOR_SYSTEM_DEFAULT = '1234567890' # A Magic Number kind of signal to PySimpleGUI that the color should not be set at all
DEFAULT_BUTTON_COLOR = ('white', BLUES[0]) # Foreground, Background (None, None) == System Default
OFFICIAL_PYSIMPLEGUI_BUTTON_COLOR = ('white', BLUES[0])
# The "default PySimpleGUI theme"
OFFICIAL_PYSIMPLEGUI_THEME = CURRENT_LOOK_AND_FEEL = 'Dark Blue 3'
DEFAULT_ERROR_BUTTON_COLOR = ("#FFFFFF", "#FF0000")
DEFAULT_BACKGROUND_COLOR = None
DEFAULT_ELEMENT_BACKGROUND_COLOR = None
DEFAULT_ELEMENT_TEXT_COLOR = COLOR_SYSTEM_DEFAULT
DEFAULT_TEXT_ELEMENT_BACKGROUND_COLOR = None
DEFAULT_TEXT_COLOR = COLOR_SYSTEM_DEFAULT
DEFAULT_INPUT_ELEMENTS_COLOR = COLOR_SYSTEM_DEFAULT
DEFAULT_INPUT_TEXT_COLOR = COLOR_SYSTEM_DEFAULT
DEFAULT_SCROLLBAR_COLOR = None
# DEFAULT_BUTTON_COLOR = (YELLOWS[0], PURPLES[0]) # (Text, Background) or (Color "on", Color) as a way to remember
# DEFAULT_BUTTON_COLOR = (GREENS[3], TANS[0]) # Foreground, Background (None, None) == System Default
# DEFAULT_BUTTON_COLOR = (YELLOWS[0], GREENS[4]) # Foreground, Background (None, None) == System Default
# DEFAULT_BUTTON_COLOR = ('white', 'black') # Foreground, Background (None, None) == System Default
# DEFAULT_BUTTON_COLOR = (YELLOWS[0], PURPLES[2]) # Foreground, Background (None, None) == System Default
# DEFAULT_PROGRESS_BAR_COLOR = (GREENS[2], GREENS[0]) # a nice green progress bar
# DEFAULT_PROGRESS_BAR_COLOR = (BLUES[1], BLUES[1]) # a nice green progress bar
# DEFAULT_PROGRESS_BAR_COLOR = (BLUES[0], BLUES[0]) # a nice green progress bar
# DEFAULT_PROGRESS_BAR_COLOR = (PURPLES[1],PURPLES[0]) # a nice purple progress bar
# A transparent button is simply one that matches the background
# TRANSPARENT_BUTTON = 'This constant has been depricated. You must set your button background = background it is on for it to be transparent appearing'
# --------------------------------------------------------------------------------
# Progress Bar Relief Choices
RELIEF_RAISED = 'raised'
RELIEF_SUNKEN = 'sunken'
RELIEF_FLAT = 'flat'
RELIEF_RIDGE = 'ridge'
RELIEF_GROOVE = 'groove'
RELIEF_SOLID = 'solid'
# These are the spepific themes that tkinter offers
THEME_DEFAULT = 'default' # this is a TTK theme, not a PSG theme!!!
THEME_WINNATIVE = 'winnative'
THEME_CLAM = 'clam'
THEME_ALT = 'alt'
THEME_CLASSIC = 'classic'
THEME_VISTA = 'vista'
THEME_XPNATIVE = 'xpnative'
THEME_LIST = ('default', 'winnative', 'clam', 'alt', 'classic', 'vista', 'xpnative')
# The theme to use by default for all windows
DEFAULT_TTK_THEME = THEME_DEFAULT
TTK_THEME_LIST = ('default', 'winnative', 'clam', 'alt', 'classic', 'vista', 'xpnative')
USE_TTK_BUTTONS = None
DEFAULT_PROGRESS_BAR_COLOR = ("#01826B", '#D0D0D0') # a nice green progress bar
DEFAULT_PROGRESS_BAR_COMPUTE = ('#000000', '#000000') # Means that the progress bar colors should be computed from other colors
DEFAULT_PROGRESS_BAR_COLOR_OFFICIAL = ("#01826B", '#D0D0D0') # a nice green progress bar
DEFAULT_PROGRESS_BAR_SIZE = (20, 20) # Size of Progress Bar (characters for length, pixels for width)
DEFAULT_PROGRESS_BAR_BORDER_WIDTH = 1
DEFAULT_PROGRESS_BAR_RELIEF = RELIEF_GROOVE
PROGRESS_BAR_STYLES = ('default', 'winnative', 'clam', 'alt', 'classic', 'vista', 'xpnative')
DEFAULT_PROGRESS_BAR_STYLE = DEFAULT_TTK_THEME
DEFAULT_METER_ORIENTATION = 'Horizontal'
DEFAULT_SLIDER_ORIENTATION = 'vertical'
DEFAULT_SLIDER_BORDER_WIDTH = 1
DEFAULT_SLIDER_RELIEF = tk.FLAT
DEFAULT_FRAME_RELIEF = tk.GROOVE
DEFAULT_LISTBOX_SELECT_MODE = tk.SINGLE
SELECT_MODE_MULTIPLE = tk.MULTIPLE
LISTBOX_SELECT_MODE_MULTIPLE = 'multiple'
SELECT_MODE_BROWSE = tk.BROWSE
LISTBOX_SELECT_MODE_BROWSE = 'browse'
SELECT_MODE_EXTENDED = tk.EXTENDED
LISTBOX_SELECT_MODE_EXTENDED = 'extended'
SELECT_MODE_SINGLE = tk.SINGLE
LISTBOX_SELECT_MODE_SINGLE = 'single'
TABLE_SELECT_MODE_NONE = tk.NONE
TABLE_SELECT_MODE_BROWSE = tk.BROWSE
TABLE_SELECT_MODE_EXTENDED = tk.EXTENDED
DEFAULT_TABLE_SELECT_MODE = TABLE_SELECT_MODE_EXTENDED
TABLE_CLICKED_INDICATOR = '+CLICKED+' # Part of the tuple returned as an event when a Table element has click events enabled
DEFAULT_MODAL_WINDOWS_ENABLED = True
TAB_LOCATION_TOP = 'top'
TAB_LOCATION_TOP_LEFT = 'topleft'
TAB_LOCATION_TOP_RIGHT = 'topright'
TAB_LOCATION_LEFT = 'left'
TAB_LOCATION_LEFT_TOP = 'lefttop'
TAB_LOCATION_LEFT_BOTTOM = 'leftbottom'
TAB_LOCATION_RIGHT = 'right'
TAB_LOCATION_RIGHT_TOP = 'righttop'
TAB_LOCATION_RIGHT_BOTTOM = 'rightbottom'
TAB_LOCATION_BOTTOM = 'bottom'
TAB_LOCATION_BOTTOM_LEFT = 'bottomleft'
TAB_LOCATION_BOTTOM_RIGHT = 'bottomright'
TITLE_LOCATION_TOP = tk.N
TITLE_LOCATION_BOTTOM = tk.S
TITLE_LOCATION_LEFT = tk.W
TITLE_LOCATION_RIGHT = tk.E
TITLE_LOCATION_TOP_LEFT = tk.NW
TITLE_LOCATION_TOP_RIGHT = tk.NE
TITLE_LOCATION_BOTTOM_LEFT = tk.SW
TITLE_LOCATION_BOTTOM_RIGHT = tk.SE
TEXT_LOCATION_TOP = tk.N
TEXT_LOCATION_BOTTOM = tk.S
TEXT_LOCATION_LEFT = tk.W
TEXT_LOCATION_RIGHT = tk.E
TEXT_LOCATION_TOP_LEFT = tk.NW
TEXT_LOCATION_TOP_RIGHT = tk.NE
TEXT_LOCATION_BOTTOM_LEFT = tk.SW
TEXT_LOCATION_BOTTOM_RIGHT = tk.SE
TEXT_LOCATION_CENTER = tk.CENTER
GRAB_ANYWHERE_IGNORE_THESE_WIDGETS = (ttk.Sizegrip, tk.Scale, ttk.Scrollbar, tk.scrolledtext.ScrolledText, tk.Scrollbar, tk.Entry, tk.Text, tk.PanedWindow, tk.Listbox, tk.OptionMenu)
# ----====----====----==== Constants the user should NOT f-with ====----====----====----#
ThisRow = 555666777 # magic number
# DEFAULT_WINDOW_ICON = ''
MESSAGE_BOX_LINE_WIDTH = 60
# "Special" Key Values.. reserved
# Key representing a Read timeout
EVENT_TIMEOUT = TIMEOUT_EVENT = TIMEOUT_KEY = '__TIMEOUT__'
WIN_CLOSED = WINDOW_CLOSED = None
WINDOW_CLOSE_ATTEMPTED_EVENT = WIN_X_EVENT = WIN_CLOSE_ATTEMPTED_EVENT = '-WINDOW CLOSE ATTEMPTED-'
TITLEBAR_MINIMIZE_KEY = '__TITLEBAR MINIMIZE__'
TITLEBAR_MAXIMIZE_KEY = '__TITLEBAR MAXIMIZE__'
TITLEBAR_CLOSE_KEY = '__TITLEBAR CLOSE__'
TITLEBAR_IMAGE_KEY = '__TITLEBAR IMAGE__'
TITLEBAR_DO_NOT_USE_AN_ICON = '__TITLEBAR_NO_ICON__'
# Key indicating should not create any return values for element
WRITE_ONLY_KEY = '__WRITE ONLY__'
MENU_DISABLED_CHARACTER = '!'
MENU_SHORTCUT_CHARACTER = '&'
MENU_KEY_SEPARATOR = '::'
MENU_SEPARATOR_LINE = '---'
MENU_RIGHT_CLICK_EDITME_EXIT = ['', ['Edit Me', 'Exit']]
MENU_RIGHT_CLICK_EDITME_VER_EXIT = ['', ['Edit Me', 'Version', 'Exit']]
MENU_RIGHT_CLICK_EDITME_VER_LOC_EXIT = ['', ['Edit Me', 'Version', 'File Location', 'Exit']]
MENU_RIGHT_CLICK_EDITME_VER_SETTINGS_EXIT = ['', ['Edit Me', 'Settings', 'Version', 'Exit']]
MENU_RIGHT_CLICK_EXIT = ['', ['Exit']]
MENU_RIGHT_CLICK_DISABLED = ['', []]
_MENU_RIGHT_CLICK_TABGROUP_DEFAULT = ['TABGROUP DEFAULT', []]
ENABLE_TK_WINDOWS = False
USE_CUSTOM_TITLEBAR = False
CUSTOM_TITLEBAR_BACKGROUND_COLOR = None
CUSTOM_TITLEBAR_TEXT_COLOR = None
CUSTOM_TITLEBAR_ICON = None
CUSTOM_TITLEBAR_FONT = None
TITLEBAR_METADATA_MARKER = 'This window has a titlebar'
CUSTOM_MENUBAR_METADATA_MARKER = 'This is a custom menubar'
SUPPRESS_ERROR_POPUPS = False
SUPPRESS_RAISE_KEY_ERRORS = True
SUPPRESS_KEY_GUESSING = False
ENABLE_TREEVIEW_869_PATCH = True
# These are now set based on the global settings file
ENABLE_MAC_NOTITLEBAR_PATCH = False
ENABLE_MAC_MODAL_DISABLE_PATCH = False
ENABLE_MAC_DISABLE_GRAB_ANYWHERE_WITH_TITLEBAR = True
OLD_TABLE_TREE_SELECTED_ROW_COLORS = ('#FFFFFF', '#4A6984')
ALTERNATE_TABLE_AND_TREE_SELECTED_ROW_COLORS = ('SystemHighlightText', 'SystemHighlight')
# Some handy unicode symbols
SYMBOL_SQUARE = '█'
SYMBOL_CIRCLE = '⚫'
SYMBOL_CIRCLE_OUTLINE = '◯'
SYMBOL_UP = '▲'
SYMBOL_RIGHT = '►'
SYMBOL_LEFT = '◄'
SYMBOL_DOWN = '▼'
SYMBOL_X = '❎'
SYMBOL_CHECK = '✅'
SYMBOL_BALLOT_X = '☒'
SYMBOL_BALLOT_CHECK = '☑'
SYMBOL_LEFT_DOUBLE = '«'
SYMBOL_RIGHT_DOUBLE = '»'
SYMBOL_LEFT_ARROWHEAD = '⮜'
SYMBOL_RIGHT_ARROWHEAD = '⮞'
SYMBOL_UP_ARROWHEAD = '⮝'
SYMBOL_DOWN_ARROWHEAD = '⮟'
if sum([int(i) for i in tclversion_detailed.split('.')]) > 19:
SYMBOL_TITLEBAR_MINIMIZE = '_'
SYMBOL_TITLEBAR_MAXIMIZE = '◻'
SYMBOL_TITLEBAR_CLOSE = ''
else:
SYMBOL_TITLEBAR_MINIMIZE = '_'
SYMBOL_TITLEBAR_MAXIMIZE = 'O'
SYMBOL_TITLEBAR_CLOSE = 'X'
#################### PATHS for user_settings APIs ####################
# These paths are passed to os.path.expanduser to get the default path for user_settings
# They can be changed using set_options
DEFAULT_USER_SETTINGS_WIN_PATH = r'~\AppData\Local\PySimpleGUI\settings'
DEFAULT_USER_SETTINGS_LINUX_PATH = r'~/.config/PySimpleGUI/settings'
DEFAULT_USER_SETTINGS_MAC_PATH = r'~/Library/Application Support/PySimpleGUI/settings'
DEFAULT_USER_SETTINGS_TRINKET_PATH = r'.'
DEFAULT_USER_SETTINGS_REPLIT_PATH = r'.'
DEFAULT_USER_SETTINGS_UNKNOWN_OS_PATH = r'~/Library/Application Support/PySimpleGUI/settings'
DEFAULT_USER_SETTINGS_PATH = None # value set by user to override all paths above
DEFAULT_USER_SETTINGS_PYSIMPLEGUI_PATH = None # location of the global PySimpleGUI settings
DEFAULT_USER_SETTINGS_PYSIMPLEGUI_FILENAME = '_PySimpleGUI_settings_global_.json' # location of the global PySimpleGUI settings
# ====================================================================== #
# One-liner functions that are handy as f_ck #
# ====================================================================== #
def rgb(red, green, blue):
"""
Given integer values of Red, Green, Blue, return a color string "#RRGGBB"
:param red: Red portion from 0 to 255
:type red: (int)
:param green: Green portion from 0 to 255
:type green: (int)
:param blue: Blue portion from 0 to 255
:type blue: (int)
:return: A single RGB String in the format "#RRGGBB" where each pair is a hex number.
:rtype: (str)
"""
red = min(int(red), 255) if red > 0 else 0
blue = min(int(blue), 255) if blue > 0 else 0
green = min(int(green), 255) if green > 0 else 0
return '#%02x%02x%02x' % (red, green, blue)
# ====================================================================== #
# Enums for types #
# ====================================================================== #
# ------------------------- Button types ------------------------- #
# uncomment this line and indent to go back to using Enums
BUTTON_TYPE_BROWSE_FOLDER = 1
BUTTON_TYPE_BROWSE_FILE = 2
BUTTON_TYPE_BROWSE_FILES = 21
BUTTON_TYPE_SAVEAS_FILE = 3
BUTTON_TYPE_CLOSES_WIN = 5
BUTTON_TYPE_CLOSES_WIN_ONLY = 6
BUTTON_TYPE_READ_FORM = 7
BUTTON_TYPE_REALTIME = 9
BUTTON_TYPE_CALENDAR_CHOOSER = 30
BUTTON_TYPE_COLOR_CHOOSER = 40
BUTTON_TYPE_SHOW_DEBUGGER = 50
BROWSE_FILES_DELIMITER = ';' # the delimiter to be used between each file in the returned string
FILE_TYPES_ALL_FILES = (("ALL Files", "*.* *"),)
BUTTON_DISABLED_MEANS_IGNORE = 'ignore'
# ------------------------- Element types ------------------------- #
ELEM_TYPE_TEXT = 'text'
ELEM_TYPE_INPUT_TEXT = 'input'
ELEM_TYPE_INPUT_COMBO = 'combo'
ELEM_TYPE_INPUT_OPTION_MENU = 'option menu'
ELEM_TYPE_INPUT_RADIO = 'radio'
ELEM_TYPE_INPUT_MULTILINE = 'multiline'
ELEM_TYPE_INPUT_CHECKBOX = 'checkbox'
ELEM_TYPE_INPUT_SPIN = 'spind'
ELEM_TYPE_BUTTON = 'button'
ELEM_TYPE_IMAGE = 'image'
ELEM_TYPE_CANVAS = 'canvas'
ELEM_TYPE_FRAME = 'frame'
ELEM_TYPE_GRAPH = 'graph'
ELEM_TYPE_TAB = 'tab'
ELEM_TYPE_TAB_GROUP = 'tabgroup'
ELEM_TYPE_INPUT_SLIDER = 'slider'
ELEM_TYPE_INPUT_LISTBOX = 'listbox'
ELEM_TYPE_OUTPUT = 'output'
ELEM_TYPE_COLUMN = 'column'
ELEM_TYPE_MENUBAR = 'menubar'
ELEM_TYPE_PROGRESS_BAR = 'progressbar'
ELEM_TYPE_BLANK = 'blank'
ELEM_TYPE_TABLE = 'table'
ELEM_TYPE_TREE = 'tree'
ELEM_TYPE_ERROR = 'error'
ELEM_TYPE_SEPARATOR = 'separator'
ELEM_TYPE_STATUSBAR = 'statusbar'
ELEM_TYPE_PANE = 'pane'
ELEM_TYPE_BUTTONMENU = 'buttonmenu'
ELEM_TYPE_TITLEBAR = 'titlebar'
ELEM_TYPE_SIZEGRIP = 'sizegrip'
# STRETCH == ERROR ELEMENT as a filler
# ------------------------- Popup Buttons Types ------------------------- #
POPUP_BUTTONS_YES_NO = 1
POPUP_BUTTONS_CANCELLED = 2
POPUP_BUTTONS_ERROR = 3
POPUP_BUTTONS_OK_CANCEL = 4
POPUP_BUTTONS_OK = 0
POPUP_BUTTONS_NO_BUTTONS = 5
# ------------------------------------------------------------------------- #
# ToolTip used by the Elements #
# ------------------------------------------------------------------------- #
class ToolTip:
"""
Create a tooltip for a given widget
(inspired by https://stackoverflow.com/a/36221216)
This is an INTERNALLY USED only class. Users should not refer to this class at all.
"""
def __init__(self, widget, text, timeout=DEFAULT_TOOLTIP_TIME):
"""
:param widget: The tkinter widget
:type widget: widget type varies
:param text: text for the tooltip. It can inslude \n
:type text: (str)
:param timeout: Time in milliseconds that mouse must remain still before tip is shown
:type timeout: (int)
"""
self.widget = widget
self.text = text
self.timeout = timeout
# self.wraplength = wraplength if wraplength else widget.winfo_screenwidth() // 2
self.tipwindow = None
self.id = None
self.x = self.y = 0
self.widget.bind("<Enter>", self.enter)
self.widget.bind("<Leave>", self.leave)
self.widget.bind("<ButtonPress>", self.leave)
def enter(self, event=None):
"""
Called by tkinter when mouse enters a widget
:param event: from tkinter. Has x,y coordinates of mouse
:type event:
"""
self.x = event.x
self.y = event.y
self.schedule()
def leave(self, event=None):
"""
Called by tktiner when mouse exits a widget
:param event: from tkinter. Event info that's not used by function.
:type event:
"""
self.unschedule()
self.hidetip()
def schedule(self):
"""
Schedule a timer to time how long mouse is hovering
"""
self.unschedule()
self.id = self.widget.after(self.timeout, self.showtip)
def unschedule(self):
"""
Cancel timer used to time mouse hover
"""
if self.id:
self.widget.after_cancel(self.id)
self.id = None
def showtip(self):
"""
Creates a topoltip window with the tooltip text inside of it
"""
if self.tipwindow:
return
x = self.widget.winfo_rootx() + self.x + DEFAULT_TOOLTIP_OFFSET[0]
y = self.widget.winfo_rooty() + self.y + DEFAULT_TOOLTIP_OFFSET[1]
self.tipwindow = tk.Toplevel(self.widget)
# if not sys.platform.startswith('darwin'):
try:
self.tipwindow.wm_overrideredirect(True)
# if running_mac() and ENABLE_MAC_NOTITLEBAR_PATCH:
if _mac_should_apply_notitlebar_patch():
self.tipwindow.wm_overrideredirect(False)
except Exception as e:
print('* Error performing wm_overrideredirect in showtip *', e)
self.tipwindow.wm_geometry("+%d+%d" % (x, y))
self.tipwindow.wm_attributes("-topmost", 1)
label = ttk.Label(self.tipwindow, text=self.text, justify=tk.LEFT,
background=TOOLTIP_BACKGROUND_COLOR, relief=tk.SOLID, borderwidth=1)
if TOOLTIP_FONT is not None:
label.config(font=TOOLTIP_FONT)
label.pack()
def hidetip(self):
"""
Destroy the tooltip window
"""
if self.tipwindow:
self.tipwindow.destroy()
self.tipwindow = None
# ---------------------------------------------------------------------- #
# Cascading structure.... Objects get larger #
# Button #
# Element #
# Row #
# Form #
# ---------------------------------------------------------------------- #
# ------------------------------------------------------------------------- #
# Element CLASS #
# ------------------------------------------------------------------------- #
class Element():
""" The base class for all Elements. Holds the basic description of an Element like size and colors """
def __init__(self, type, size=(None, None), auto_size_text=None, font=None, background_color=None, text_color=None, key=None, pad=None, tooltip=None,
visible=True, metadata=None):
"""
Element base class. Only used internally. User will not create an Element object by itself
:param type: The type of element. These constants all start with "ELEM_TYPE_"
:type type: (int) (could be enum)
:param size: w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int, int) | (None, None) | int
:param auto_size_text: True if the Widget should be shrunk to exactly fit the number of chars to show
:type auto_size_text: bool
:param font: specifies the font family, size. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param background_color: color of background. Can be in #RRGGBB format or a color name "black"
:type background_color: (str)
:param text_color: element's text color. Can be in #RRGGBB format or a color name "black"
:type text_color: (str)
:param key: Identifies an Element. Should be UNIQUE to this window.
:type key: str | int | tuple | object
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom). If an int is given, then auto-converted to tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param visible: set visibility state of the element (Default = True)
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
if size is not None and size != (None, None):
if isinstance(size, int):
size = (size, 1)
if isinstance(size, tuple) and len(size) == 1:
size = (size[0], 1)
if pad is not None and pad != (None, None):
if isinstance(pad, int):
pad = (pad, pad)
self.Size = size
self.Type = type
self.AutoSizeText = auto_size_text
self.Pad = pad
self.Font = font
self.TKStringVar = None
self.TKIntVar = None
self.TKText = None
self.TKEntry = None
self.TKImage = None
self.ttk_style_name = '' # set in the packer function
self._metadata = None # type: Any
self.ParentForm = None # type: Window
self.ParentContainer = None # will be a Form, Column, or Frame element # UNBIND
self.TextInputDefault = None
self.Position = (0, 0) # Default position Row 0, Col 0
self.BackgroundColor = background_color if background_color is not None else DEFAULT_ELEMENT_BACKGROUND_COLOR
self.TextColor = text_color if text_color is not None else DEFAULT_ELEMENT_TEXT_COLOR
self.Key = key # dictionary key for return values
self.Tooltip = tooltip
self.TooltipObject = None
self._visible = visible
self.TKRightClickMenu = None
self.Widget = None # Set when creating window. Has the main tkinter widget for element
self.Tearoff = False # needed because of right click menu code
self.ParentRowFrame = None # type tk.Frame
self.metadata = metadata
self.user_bind_dict = {} # Used when user defines a tkinter binding using bind method - convert bind string to key modifier
self.user_bind_event = None # Used when user defines a tkinter binding using bind method - event data from tkinter
self.pad_used = (0, 0) # the amount of pad used when was inserted into the layout
self._popup_menu_location = (None, None)
if not hasattr(self, 'DisabledTextColor'):
self.DisabledTextColor = None
if not hasattr(self, 'ItemFont'):
self.ItemFont = None
if not hasattr(self, 'RightClickMenu'):
self.RightClickMenu = None
@property
def visible(self):
"""
Returns visibility state for the element. This is a READONLY property
:return: Visibility state for element
:rtype: (bool)
"""
return self._visible
@property
def metadata(self):
"""
Metadata is an Element property that you can use at any time to hold any value
:return: the current metadata value
:rtype: (Any)
"""
return self._metadata
@metadata.setter
def metadata(self, value):
"""
Metadata is an Element property that you can use at any time to hold any value
:param value: Anything you want it to be
:type value: (Any)
"""
self._metadata = value
def _RightClickMenuCallback(self, event):
"""
Callback function that's called when a right click happens. Shows right click menu as result
:param event: information provided by tkinter about the event including x,y location of click
:type event:
"""
if self.Type == ELEM_TYPE_TAB_GROUP:
try:
index = self.Widget.index('@{},{}'.format(event.x,event.y))
tab = self.Widget.tab(index, 'text')
key = self.find_key_from_tab_name(tab)
tab_element = self.ParentForm.key_dict[key]
if tab_element.RightClickMenu is None: # if this tab didn't explicitly have a menu, then don't show anything
return
tab_element.TKRightClickMenu.tk_popup(event.x_root, event.y_root, 0)
self.TKRightClickMenu.grab_release()
except:
pass
return
self.TKRightClickMenu.tk_popup(event.x_root, event.y_root, 0)
self.TKRightClickMenu.grab_release()
if self.Type == ELEM_TYPE_GRAPH:
self._update_position_for_returned_values(event)
def _tearoff_menu_callback(self, parent, menu):
"""
Callback function that's called when a right click menu is torn off.
The reason for this function is to relocate the torn-off menu. It will default to 0,0 otherwise
This callback moves the right click menu window to the location of the current window
:param parent: information provided by tkinter - the parent of the Meny
:type parent:
:param menu: information provided by tkinter - the menu window
:type menu:
"""
if self._popup_menu_location == (None, None):
winx, winy = self.ParentForm.current_location()
else:
winx, winy = self._popup_menu_location
# self.ParentForm.TKroot.update()
self.ParentForm.TKroot.tk.call('wm', 'geometry', menu, "+{}+{}".format(winx, winy))
def _MenuItemChosenCallback(self, item_chosen): # TEXT Menu item callback
"""
Callback function called when user chooses a menu item from menubar, Button Menu or right click menu
:param item_chosen: String holding the value chosen.
:type item_chosen: str
"""
# print('IN MENU ITEM CALLBACK', item_chosen)
self.MenuItemChosen = item_chosen
self.ParentForm.LastButtonClicked = self.MenuItemChosen
self.ParentForm.FormRemainedOpen = True
_exit_mainloop(self.ParentForm)
# Window._window_that_exited = self.ParentForm
# self.ParentForm.TKroot.quit() # kick the users out of the mainloop
def _FindReturnKeyBoundButton(self, form):
"""
Searches for which Button has the flag Button.BindReturnKey set. It is called recursively when a
"Container Element" is encountered. Func has to walk entire window including these "sub-forms"
:param form: the Window object to search
:type form:
:return: Button Object if a button is found, else None
:rtype: Button | None
"""
for row in form.Rows:
for element in row:
if element.Type == ELEM_TYPE_BUTTON:
if element.BindReturnKey:
return element
if element.Type == ELEM_TYPE_COLUMN:
rc = self._FindReturnKeyBoundButton(element)
if rc is not None:
return rc
if element.Type == ELEM_TYPE_FRAME:
rc = self._FindReturnKeyBoundButton(element)
if rc is not None:
return rc
if element.Type == ELEM_TYPE_TAB_GROUP:
rc = self._FindReturnKeyBoundButton(element)
if rc is not None:
return rc
if element.Type == ELEM_TYPE_TAB:
rc = self._FindReturnKeyBoundButton(element)
if rc is not None:
return rc
if element.Type == ELEM_TYPE_PANE:
rc = self._FindReturnKeyBoundButton(element)
if rc is not None:
return rc
return None
def _TextClickedHandler(self, event):
"""
Callback that's called when a text element is clicked on with events enabled on the Text Element.
Result is that control is returned back to user (quits mainloop).
:param event:
:type event:
"""
# If this is a minimize button for a custom titlebar, then minimize the window
if self.Key == TITLEBAR_MINIMIZE_KEY:
if running_linux():
self.ParentForm.TKroot.wm_attributes("-type", "normal")
# self.ParentForm.TKroot.state('icon')
# return
# self.ParentForm.maximize()
self.ParentForm.TKroot.wm_overrideredirect(False)
# self.ParentForm.minimize()
# self.ParentForm.TKroot.wm_overrideredirect(False)
self.ParentForm.TKroot.iconify()
# self._skip_first_restore_callback = True
self.ParentForm.TKroot.bind('<Button-1>', self._titlebar_restore)
else:
self.ParentForm.TKroot.wm_overrideredirect(False)
self.ParentForm.Minimize()
self.ParentForm.TKroot.bind('<Expose>', self._titlebar_restore)
elif self.Key == TITLEBAR_MAXIMIZE_KEY:
if self.ParentForm.maximized:
self.ParentForm.normal()
else:
self.ParentForm.maximize()
elif self.Key == TITLEBAR_CLOSE_KEY:
self.ParentForm._OnClosingCallback()
else:
self._generic_callback_handler(self.DisplayText)
return
def _titlebar_restore(self, event):
if running_linux():
# if self._skip_first_restore_callback:
# self._skip_first_restore_callback = False
# return
self.ParentForm.TKroot.unbind('<Button-1>')
self.ParentForm.TKroot.deiconify()
# self.ParentForm.TKroot.wm_overrideredirect(True)
self.ParentForm.TKroot.wm_attributes("-type", 'dock')
else:
self.ParentForm.TKroot.unbind('<Expose>')
self.ParentForm.TKroot.wm_overrideredirect(True)
self.ParentForm.normal()
def _ReturnKeyHandler(self, event):
"""
Internal callback for the ENTER / RETURN key. Results in calling the ButtonCallBack for element that has the return key bound to it, just as if button was clicked.
:param event:
:type event:
"""
MyForm = self.ParentForm
button_element = self._FindReturnKeyBoundButton(MyForm)
if button_element is not None:
button_element.ButtonCallBack()
def _generic_callback_handler(self, alternative_to_key=None, force_key_to_be=None):
"""
Peforms the actions that were in many of the callback functions previously. Combined so that it's
easier to modify and is in 1 place now
:param event: From tkinter and is not used
:type event: Any
:param alternate_to_key: If key is None, then use this value instead
:type alternate_to_key: Any
"""
if force_key_to_be is not None:
self.ParentForm.LastButtonClicked = force_key_to_be
elif self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = alternative_to_key
self.ParentForm.FormRemainedOpen = True
_exit_mainloop(self.ParentForm)
# if self.ParentForm.CurrentlyRunningMainloop:
# Window._window_that_exited = self.ParentForm
# self.ParentForm.TKroot.quit() # kick the users out of the mainloop
def _ListboxSelectHandler(self, event):
"""
Internal callback function for when a listbox item is selected
:param event: Information from tkinter about the callback
:type event:
"""
self._generic_callback_handler('')
def _ComboboxSelectHandler(self, event):
"""
Internal callback function for when an entry is selected in a Combobox.
:param event: Event data from tkinter (not used)
:type event:
"""
self._generic_callback_handler('')
def _SpinboxSelectHandler(self):
"""
Internal callback function for when an entry is selected in a Combobox.
"""
self._generic_callback_handler('')
def _RadioHandler(self):
"""
Internal callback for when a radio button is selected and enable events was set for radio
"""
self._generic_callback_handler('')
def _CheckboxHandler(self):
"""
Internal callback for when a checkbnox is selected and enable events was set for checkbox
"""
self._generic_callback_handler('')
def _TabGroupSelectHandler(self, event):
"""
Internal callback for when a Tab is selected and enable events was set for TabGroup
:param event: Event data passed in by tkinter (not used)
:type event:
"""
self._generic_callback_handler('')
def _KeyboardHandler(self, event):
"""
Internal callback for when a key is pressed andd return keyboard events was set for window
:param event: Event data passed in by tkinter (not used)
:type event:
"""
self._generic_callback_handler('')
def _ClickHandler(self, event):
"""
Internal callback for when a mouse was clicked... I think.
:param event: Event data passed in by tkinter (not used)
:type event:
"""
self._generic_callback_handler('')
def _user_bind_callback(self, bind_string, event):
"""
Used when user binds a tkinter event directly to an element
:param bind_string: The event that was bound so can lookup the key modifier
:type bind_string: (str)
:param event: Event data passed in by tkinter (not used)
:type event: (Any)
"""
key_suffix = self.user_bind_dict.get(bind_string, '')
self.user_bind_event = event
if self.Type == ELEM_TYPE_GRAPH:
self._update_position_for_returned_values(event)
if self.Key is not None:
if isinstance(self.Key, str):
key = self.Key + str(key_suffix)
else:
key = (self.Key, key_suffix) # old way (pre 2021) was to make a brand new tuple
# key = self.Key + (key_suffix,) # in 2021 tried this. It will break existing applications though - if key is a tuple, add one more item
else:
key = bind_string
self._generic_callback_handler(force_key_to_be=key)
def bind(self, bind_string, key_modifier):
"""
Used to add tkinter events to an Element.
The tkinter specific data is in the Element's member variable user_bind_event
:param bind_string: The string tkinter expected in its bind function
:type bind_string: (str)
:param key_modifier: Additional data to be added to the element's key when event is returned
:type key_modifier: (str)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
self.Widget.bind(bind_string, lambda evt: self._user_bind_callback(bind_string, evt))
self.user_bind_dict[bind_string] = key_modifier
def unbind(self, bind_string):
"""
Removes a previously bound tkinter event from an Element.
:param bind_string: The string tkinter expected in its bind function
:type bind_string: (str)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
self.Widget.unbind(bind_string)
self.user_bind_dict.pop(bind_string, None)
def set_tooltip(self, tooltip_text):
"""
Called by application to change the tooltip text for an Element. Normally invoked using the Element Object such as: window.Element('key').SetToolTip('New tip').
:param tooltip_text: the text to show in tooltip.
:type tooltip_text: (str)
"""
if self.TooltipObject:
try:
self.TooltipObject.leave()
except:
pass
self.TooltipObject = ToolTip(self.Widget, text=tooltip_text, timeout=DEFAULT_TOOLTIP_TIME)
def set_focus(self, force=False):
"""
Sets the current focus to be on this element
:param force: if True will call focus_force otherwise calls focus_set
:type force: bool
"""
try:
if force:
self.Widget.focus_force()
else:
self.Widget.focus_set()
except:
print('Was unable to set focus. The Widget passed in was perhaps not present in this element? Check your elements .Widget property')
def block_focus(self, block=True):
"""
Enable or disable the element from getting focus by using the keyboard.
If the block parameter is True, then this element will not be given focus by using
the keyboard to go from one element to another.
You CAN click on the element and utilize it.
:param block: if True the element will not get focus via the keyboard
:type block: bool
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
try:
self.ParentForm.TKroot.focus_force()
if block:
self.Widget.configure(takefocus=0)
else:
self.Widget.configure(takefocus=1)
except:
print('Was unable to block the focus. Check your elements .Widget property')
def set_size(self, size=(None, None)):
"""
Changes the size of an element to a specific size.
It's possible to specify None for one of sizes so that only 1 of the element's dimensions are changed.
:param size: The size in characters, rows typically. In some cases they are pixels
:type size: (int, int)
"""
try:
if size[0] != None:
self.Widget.config(width=size[0])
except:
print('Warning, error setting width on element with key=', self.Key)
try:
if size[1] != None:
self.Widget.config(height=size[1])
except:
try:
self.Widget.config(length=size[1])
except:
print('Warning, error setting height on element with key=', self.Key)
if self.Type == ELEM_TYPE_GRAPH:
self.CanvasSize = size
def get_size(self):
"""
Return the size of an element in Pixels. Care must be taken as some elements use characters to specify their size but will return pixels when calling this get_size method.
:return: width and height of the element
:rtype: (int, int)
"""
try:
w = self.Widget.winfo_width()
h = self.Widget.winfo_height()
except:
print('Warning, error getting size of element', self.Key)
w = h = None
return w, h
def hide_row(self):
"""
Hide the entire row an Element is located on.
Use this if you must have all space removed when you are hiding an element, including the row container
"""
try:
self.ParentRowFrame.pack_forget()
except:
print('Warning, error hiding element row for key =', self.Key)
def unhide_row(self):
"""
Unhides (makes visible again) the row container that the Element is located on.
Note that it will re-appear at the bottom of the window / container, most likely.
"""
try:
self.ParentRowFrame.pack()
except:
print('Warning, error hiding element row for key =', self.Key)
def expand(self, expand_x=False, expand_y=False, expand_row=True):
"""
Causes the Element to expand to fill available space in the X and Y directions. Can specify which or both directions
:param expand_x: If True Element will expand in the Horizontal directions
:type expand_x: (bool)
:param expand_y: If True Element will expand in the Vertical directions
:type expand_y: (bool)
:param expand_row: If True the row containing the element will also expand. Without this your element is "trapped" within the row
:type expand_row: (bool)
"""
if expand_x and expand_y:
fill = tk.BOTH
elif expand_x:
fill = tk.X
elif expand_y:
fill = tk.Y
else:
return
if not self._widget_was_created():
return
self.Widget.pack(expand=True, fill=fill)
self.ParentRowFrame.pack(expand=expand_row, fill=fill)
if self.element_frame is not None:
self.element_frame.pack(expand=True, fill=fill)
def set_cursor(self, cursor=None, cursor_color=None):
"""
Sets the cursor for the current Element.
"Cursor" is used in 2 different ways in this call.
For the parameter "cursor" it's actually the mouse pointer.
If you do not want any mouse pointer, then use the string "none"
For the parameter "cursor_color" it's the color of the beam used when typing into an input element
:param cursor: The tkinter cursor name
:type cursor: (str)
:param cursor_color: color to set the "cursor" to
:type cursor_color: (str)
"""
if not self._widget_was_created():
return
if cursor is not None:
try:
self.Widget.config(cursor=cursor)
except Exception as e:
print('Warning bad cursor specified ', cursor)
print(e)
if cursor_color is not None:
try:
self.Widget.config(insertbackground=cursor_color)
except Exception as e:
print('Warning bad cursor color', cursor_color)
print(e)
def set_vscroll_position(self, percent_from_top):
"""
Attempts to set the vertical scroll postition for an element's Widget
:param percent_from_top: From 0 to 1.0, the percentage from the top to move scrollbar to
:type percent_from_top: (float)
"""
try:
self.Widget.yview_moveto(percent_from_top)
except Exception as e:
print('Warning setting the vertical scroll (yview_moveto failed)')
print(e)
def _widget_was_created(self):
"""
Determines if a Widget was created for this element.
:return: True if a Widget has been created previously (Widget is not None)
:rtype: (bool)
"""
if self.Widget is not None:
return True
else:
warnings.warn('You cannot Update element with key = {} until the window.read() is called or finalized=True when creating window'.format(self.Key),
UserWarning)
if not SUPPRESS_ERROR_POPUPS:
_error_popup_with_traceback('Unable to complete operation on element with key {}'.format(self.Key),
'You cannot perform operations (such as calling update) on an Element until:',
' window.read() is called or finalize=True when Window created.',
'Adding a "finalize=True" parameter to your Window creation will likely fix this.',
_create_error_message(),
)
return False
def _grab_anywhere_on_using_control_key(self):
"""
Turns on Grab Anywhere functionality AFTER a window has been created. Don't try on a window that's not yet
been Finalized or Read.
"""
self.Widget.bind("<Control-Button-1>", self.ParentForm._StartMove)
self.Widget.bind("<Control-ButtonRelease-1>", self.ParentForm._StopMove)
self.Widget.bind("<Control-B1-Motion>", self.ParentForm._OnMotion)
def _grab_anywhere_on(self):
"""
Turns on Grab Anywhere functionality AFTER a window has been created. Don't try on a window that's not yet
been Finalized or Read.
"""
self.Widget.bind("<ButtonPress-1>", self.ParentForm._StartMove)
self.Widget.bind("<ButtonRelease-1>", self.ParentForm._StopMove)
self.Widget.bind("<B1-Motion>", self.ParentForm._OnMotion)
def _grab_anywhere_off(self):
"""
Turns off Grab Anywhere functionality AFTER a window has been created. Don't try on a window that's not yet
been Finalized or Read.
"""
self.Widget.unbind("<ButtonPress-1>")
self.Widget.unbind("<ButtonRelease-1>")
self.Widget.unbind("<B1-Motion>")
def grab_anywhere_exclude(self):
"""
Excludes this element from being used by the grab_anywhere feature
Handy for elements like a Graph element when dragging is enabled. You want the Graph element to get the drag events instead of the window dragging.
"""
self.ParentForm._grab_anywhere_ignore_these_list.append(self.Widget)
def grab_anywhere_include(self):
"""
Includes this element in the grab_anywhere feature
This will allow you to make a Multline element drag a window for example
"""
self.ParentForm._grab_anywhere_include_these_list.append(self.Widget)
def set_right_click_menu(self, menu=None):
if menu == MENU_RIGHT_CLICK_DISABLED:
return
if menu is None:
menu = self.ParentForm.RightClickMenu
if menu is None:
return
if menu:
top_menu = tk.Menu(self.ParentForm.TKroot, tearoff=self.ParentForm.right_click_menu_tearoff, tearoffcommand=self._tearoff_menu_callback)
if self.ParentForm.right_click_menu_background_color not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(bg=self.ParentForm.right_click_menu_background_color)
if self.ParentForm.right_click_menu_text_color not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(fg=self.ParentForm.right_click_menu_text_color)
if self.ParentForm.right_click_menu_disabled_text_color not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(disabledforeground=self.ParentForm.right_click_menu_disabled_text_color)
if self.ParentForm.right_click_menu_font is not None:
top_menu.config(font=self.ParentForm.right_click_menu_font)
if self.ParentForm.right_click_menu_selected_colors[0] not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(activeforeground=self.ParentForm.right_click_menu_selected_colors[0])
if self.ParentForm.right_click_menu_selected_colors[1] not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(activebackground=self.ParentForm.right_click_menu_selected_colors[1])
AddMenuItem(top_menu, menu[1], self, right_click_menu=True)
self.TKRightClickMenu = top_menu
if self.ParentForm.RightClickMenu: # if the top level has a right click menu, then setup a callback for the Window itself
if self.ParentForm.TKRightClickMenu is None:
self.ParentForm.TKRightClickMenu = top_menu
if (running_mac()):
self.ParentForm.TKroot.bind('<ButtonRelease-2>', self.ParentForm._RightClickMenuCallback)
else:
self.ParentForm.TKroot.bind('<ButtonRelease-3>', self.ParentForm._RightClickMenuCallback)
if (running_mac()):
self.Widget.bind('<ButtonRelease-2>', self._RightClickMenuCallback)
else:
self.Widget.bind('<ButtonRelease-3>', self._RightClickMenuCallback)
def update(self, *args, **kwargs):
"""
A dummy update call. This will only be called if an element hasn't implemented an update method
It is provided here for docstring purposes. If you got here by browing code via PyCharm, know
that this is not the function that will be called. Your actual element's update method will be called.
If you call update, you must call window.refresh if you want the change to happen prior to your next
window.read() call. Normally uou don't do this as the window.read call is likely going to happen next.
"""
print('* Base Element Class update was called. Your element does not seem to have an update method')
def __call__(self, *args, **kwargs):
"""
Makes it possible to "call" an already existing element. When you do make the "call", it actually calls
the Update method for the element.
Example: If this text element was in yoiur layout:
sg.Text('foo', key='T')
Then you can call the Update method for that element by writing:
window.find_element('T')('new text value')
"""
return self.update(*args, **kwargs)
SetTooltip = set_tooltip
SetFocus = set_focus
# ---------------------------------------------------------------------- #
# Input Class #
# ---------------------------------------------------------------------- #
class Input(Element):
"""
Display a single text input field. Based on the tkinter Widget `Entry`
"""
def __init__(self, default_text='', size=(None, None), s=(None, None), disabled=False, password_char='',
justification=None, background_color=None, text_color=None, font=None, tooltip=None, border_width=None,
change_submits=False, enable_events=False, do_not_clear=True, key=None, k=None, focus=False, pad=None, p=None,
use_readonly_for_disable=True, readonly=False, disabled_readonly_background_color=None, disabled_readonly_text_color=None, expand_x=False, expand_y=False,
right_click_menu=None, visible=True, metadata=None):
"""
:param default_text: Text initially shown in the input box as a default value(Default value = ''). Will automatically be converted to string
:type default_text: (Any)
:param size: w=characters-wide, h=rows-high. If an int is supplied rather than a tuple, then a tuple is created width=int supplied and heigh=1
:type size: (int, int) | (int, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param password_char: Password character if this is a password field (Default value = '')
:type password_char: (char)
:param justification: justification for data display. Valid choices - left, right, center
:type justification: (str)
:param background_color: color of background in one of the color formats
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param font: specifies the font family, size. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param border_width: width of border around element in pixels
:type border_width: (int)
:param change_submits: * DEPRICATED DO NOT USE. Use `enable_events` instead
:type change_submits: (bool)
:param enable_events: If True then changes to this element are immediately reported as an event. Use this instead of change_submits (Default = False)
:type enable_events: (bool)
:param do_not_clear: If False then the field will be set to blank after ANY event (button, any event) (Default = True)
:type do_not_clear: (bool)
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param focus: Determines if initial focus should go to this element.
:type focus: (bool)
:param pad: Amount of padding to put around element. Normally (horizontal pixels, vertical pixels) but can be split apart further into ((horizontal left, horizontal right), (vertical above, vertical below)). If int is given, then converted to tuple (int, int) with the value provided duplicated
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param use_readonly_for_disable: If True (the default) tkinter state set to 'readonly'. Otherwise state set to 'disabled'
:type use_readonly_for_disable: (bool)
:param readonly: If True tkinter state set to 'readonly'. Use this in place of use_readonly_for_disable as another way of achieving readonly. Note cannot set BOTH readonly and disabled as tkinter only supplies a single flag
:type readonly: (bool)
:param disabled_readonly_background_color: If state is set to readonly or disabled, the color to use for the background
:type disabled_readonly_background_color: (str)
:param disabled_readonly_text_color: If state is set to readonly or disabled, the color to use for the text
:type disabled_readonly_text_color: (str)
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param visible: set visibility state of the element (Default = True)
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.DefaultText = default_text if default_text is not None else ''
self.PasswordCharacter = password_char
bg = background_color if background_color is not None else DEFAULT_INPUT_ELEMENTS_COLOR
fg = text_color if text_color is not None else DEFAULT_INPUT_TEXT_COLOR
self.Focus = focus
self.do_not_clear = do_not_clear
self.Justification = justification
self.Disabled = disabled
self.ChangeSubmits = change_submits or enable_events
self.RightClickMenu = right_click_menu
self.UseReadonlyForDisable = use_readonly_for_disable
self.disabled_readonly_background_color = disabled_readonly_background_color
self.disabled_readonly_text_color = disabled_readonly_text_color
self.ReadOnly = readonly
self.BorderWidth = border_width if border_width is not None else DEFAULT_BORDER_WIDTH
self.TKEntry = self.Widget = None # type: tk.Entry
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(ELEM_TYPE_INPUT_TEXT, size=sz, background_color=bg, text_color=fg, key=key, pad=pad,
font=font, tooltip=tooltip, visible=visible, metadata=metadata)
def update(self, value=None, disabled=None, select=None, visible=None, text_color=None, background_color=None, move_cursor_to='end', password_char=None, paste=None):
"""
Changes some of the settings for the Input Element. Must call `Window.Read` or `Window.Finalize` prior.
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: new text to display as default text in Input field
:type value: (str)
:param disabled: disable or enable state of the element (sets Entry Widget to readonly or normal)
:type disabled: (bool)
:param select: if True, then the text will be selected
:type select: (bool)
:param visible: change visibility of element
:type visible: (bool)
:param text_color: change color of text being typed
:type text_color: (str)
:param background_color: change color of the background
:type background_color: (str)
:param move_cursor_to: Moves the cursor to a particular offset. Defaults to 'end'
:type move_cursor_to: int | str
:param password_char: Password character if this is a password field
:type password_char: str
:param paste: If True "Pastes" the clipboard value into the element. If anything is selected it is replaced. The text is inserted at the current cursor location.
:type paste: bool
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if disabled is True:
self.TKEntry['state'] = 'readonly' if self.UseReadonlyForDisable else 'disabled'
elif disabled is False:
self.TKEntry['state'] = 'readonly' if self.ReadOnly else 'normal'
if background_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKEntry.configure(background=background_color)
if text_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKEntry.configure(fg=text_color)
if value is not None:
try:
self.TKStringVar.set(value)
except:
pass
self.DefaultText = value
if move_cursor_to == 'end':
self.TKEntry.icursor(tk.END)
elif move_cursor_to is not None:
self.TKEntry.icursor(move_cursor_to)
if select:
self.TKEntry.select_range(0, 'end')
if paste is True:
try:
self.TKEntry.delete('sel.first', 'sel.last')
except:
pass
self.TKEntry.insert("insert", clipboard_get())
if visible is False:
self.TKEntry.pack_forget()
elif visible is True:
self.TKEntry.pack(padx=self.pad_used[0], pady=self.pad_used[1])
# self.TKEntry.pack(padx=self.pad_used[0], pady=self.pad_used[1], in_=self.ParentRowFrame)
if visible is not None:
self._visible = visible
if password_char is not None:
self.TKEntry.configure(show=password_char)
self.PasswordCharacter = password_char
def get(self):
"""
Read and return the current value of the input element. Must call `Window.Read` or `Window.Finalize` prior
:return: current value of Input field or '' if error encountered
:rtype: (str)
"""
try:
text = self.TKStringVar.get()
except:
text = ''
return text
Get = get
Update = update
# ------------------------- INPUT TEXT Element lazy functions ------------------------- #
In = Input
InputText = Input
I = Input
# ---------------------------------------------------------------------- #
# Combo #
# ---------------------------------------------------------------------- #
class Combo(Element):
"""
ComboBox Element - A combination of a single-line input and a drop-down menu. User can type in their own value or choose from list.
"""
def __init__(self, values, default_value=None, size=(None, None), s=(None, None), auto_size_text=None, background_color=None, text_color=None, button_background_color=None, button_arrow_color=None, bind_return_key=False, change_submits=False, enable_events=False, disabled=False, key=None, k=None, pad=None, p=None, expand_x=False, expand_y=False, tooltip=None, readonly=False, font=None, visible=True, metadata=None):
"""
:param values: values to choose. While displayed as text, the items returned are what the caller supplied, not text
:type values: List[Any] or Tuple[Any]
:param default_value: Choice to be displayed as initial value. Must match one of values variable contents
:type default_value: (Any)
:param size: width, height. Width = characters-wide, height = NOTE it's the number of entries to show in the list. If an Int is passed rather than a tuple, then height is auto-set to 1 and width is value of the int
:type size: (int, int) | (None, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_text: True if element should be the same size as the contents
:type auto_size_text: (bool)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param button_background_color: The color of the background of the button on the combo box
:type button_background_color: (str)
:param button_arrow_color: The color of the arrow on the button on the combo box
:type button_arrow_color: (str)
:param bind_return_key: If True, then the return key will cause a the Combo to generate an event
:type bind_return_key: (bool)
:param change_submits: DEPRICATED DO NOT USE. Use `enable_events` instead
:type change_submits: (bool)
:param enable_events: Turns on the element specific events. Combo event is when a choice is made
:type enable_events: (bool)
:param disabled: set disable state for element
:type disabled: (bool)
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param tooltip: text that will appear when mouse hovers over this element
:type tooltip: (str)
:param readonly: make element readonly (user can't change). True means user cannot change
:type readonly: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.Values = values
self.DefaultValue = default_value
self.ChangeSubmits = change_submits or enable_events
self.Widget = self.TKCombo = None # type: ttk.Combobox
self.Disabled = disabled
self.Readonly = readonly
self.BindReturnKey = bind_return_key
bg = background_color if background_color else DEFAULT_INPUT_ELEMENTS_COLOR
fg = text_color if text_color is not None else DEFAULT_INPUT_TEXT_COLOR
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
if button_background_color is None:
self.button_background_color = theme_button_color()[1]
else:
self.button_background_color = button_background_color
if button_arrow_color is None:
self.button_arrow_color = theme_button_color()[0]
else:
self.button_arrow_color = button_arrow_color
super().__init__(ELEM_TYPE_INPUT_COMBO, size=sz, auto_size_text=auto_size_text, background_color=bg,
text_color=fg, key=key, pad=pad, tooltip=tooltip, font=font or DEFAULT_FONT, visible=visible, metadata=metadata)
def update(self, value=None, values=None, set_to_index=None, disabled=None, readonly=None, font=None, visible=None, size=(None, None)):
"""
Changes some of the settings for the Combo Element. Must call `Window.Read` or `Window.Finalize` prior.
Note that the state can be in 3 states only.... enabled, disabled, readonly even
though more combinations are available. The easy way to remember is that if you
change the readonly parameter then you are enabling the element.
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: change which value is current selected based on new list of previous list of choices
:type value: (Any)
:param values: change list of choices
:type values: List[Any]
:param set_to_index: change selection to a particular choice starting with index = 0
:type set_to_index: (int)
:param disabled: disable or enable state of the element
:type disabled: (bool)
:param readonly: if True make element readonly (user cannot change any choices). Enables the element if either choice are made.
:type readonly: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param visible: control visibility of element
:type visible: (bool)
:param size: width, height. Width = characters-wide, height = NOTE it's the number of entries to show in the list
:type size: (int, int)
"""
if size != (None, None):
if isinstance(size, int):
size = (size, 1)
if isinstance(size, tuple) and len(size) == 1:
size = (size[0], 1)
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if values is not None:
try:
self.TKCombo['values'] = values
# self.TKCombo.current(0) # don't set any value if a new set of values was made
except:
pass
self.Values = values
if value is None:
self.TKCombo.set('')
if size == (None, None):
max_line_len = max([len(str(l)) for l in self.Values]) if len(self.Values) else 0
if self.AutoSizeText is False:
width = self.Size[0]
else:
width = max_line_len + 1
# self.TKCombo.configure(width=width)
else:
self.TKCombo.configure(height=size[1])
self.TKCombo.configure(width=size[0])
if value is not None:
if value not in self.Values:
self.TKCombo.set(value)
else:
for index, v in enumerate(self.Values):
if v == value:
try:
self.TKCombo.current(index)
except:
pass
self.DefaultValue = value
break
if set_to_index is not None:
try:
self.TKCombo.current(set_to_index)
self.DefaultValue = self.Values[set_to_index]
except:
pass
if readonly:
self.Readonly = True
self.TKCombo['state'] = 'readonly'
elif readonly is False:
self.Readonly = False
self.TKCombo['state'] = 'enable'
if disabled is True:
self.TKCombo['state'] = 'disable'
elif disabled is False and self.Readonly is True:
self.TKCombo['state'] = 'readonly'
elif disabled is False and self.Readonly is False:
self.TKCombo['state'] = 'enable'
self.Disabled = disabled if disabled is not None else self.Disabled
if font is not None:
self.TKCombo.configure(font=font)
if visible is False:
self.TKCombo.pack_forget()
elif visible is True:
self.TKCombo.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if visible is not None:
self._visible = visible
def get(self):
"""
Returns the current (right now) value of the Combo. DO NOT USE THIS AS THE NORMAL WAY OF READING A COMBO!
You should be using values from your call to window.read instead. Know what you're doing if you use it.
:return: Returns the value of what is currently chosen
:rtype: Any | None
"""
try:
if self.TKCombo.current() == -1: # if the current value was not in the original list
value = self.TKCombo.get() # then get the value typed in by user
else:
value = self.Values[self.TKCombo.current()] # get value from original list given index
except:
value = None # only would happen if user closes window
return value
Get = get
Update = update
# ------------------------- INPUT COMBO Element lazy functions ------------------------- #
InputCombo = Combo
DropDown = InputCombo
Drop = InputCombo
DD = Combo
# ---------------------------------------------------------------------- #
# Option Menu #
# ---------------------------------------------------------------------- #
class OptionMenu(Element):
"""
Option Menu is an Element available ONLY on the tkinter port of PySimpleGUI. It's is a widget that is unique
to tkinter. However, it looks much like a ComboBox. Instead of an arrow to click to pull down the list of
choices, another little graphic is shown on the widget to indicate where you click. After clicking to activate,
it looks like a Combo Box that you scroll to select a choice.
"""
def __init__(self, values, default_value=None, size=(None, None), s=(None, None), disabled=False, auto_size_text=None, expand_x=False, expand_y=False,
background_color=None, text_color=None, key=None, k=None, pad=None, p=None, tooltip=None, visible=True, metadata=None):
"""
:param values: Values to be displayed
:type values: List[Any] or Tuple[Any]
:param default_value: the value to choose by default
:type default_value: (Any)
:param size: (width, height) size in characters (wide), height is ignored and present to be consistent with other elements
:type size: (int, int) (width, UNUSED)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param disabled: control enabled / disabled
:type disabled: (bool)
:param auto_size_text: True if size of Element should match the contents of the items
:type auto_size_text: (bool)
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param tooltip: text that will appear when mouse hovers over this element
:type tooltip: (str)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.Values = values
self.DefaultValue = default_value
self.Widget = self.TKOptionMenu = None # type: tk.OptionMenu
self.Disabled = disabled
bg = background_color if background_color else DEFAULT_INPUT_ELEMENTS_COLOR
fg = text_color if text_color is not None else DEFAULT_INPUT_TEXT_COLOR
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(ELEM_TYPE_INPUT_OPTION_MENU, size=sz, auto_size_text=auto_size_text, background_color=bg,
text_color=fg, key=key, pad=pad, tooltip=tooltip, visible=visible, metadata=metadata)
def update(self, value=None, values=None, disabled=None, visible=None, size=(None, None)):
"""
Changes some of the settings for the OptionMenu Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: the value to choose by default
:type value: (Any)
:param values: Values to be displayed
:type values: List[Any]
:param disabled: disable or enable state of the element
:type disabled: (bool)
:param visible: control visibility of element
:type visible: (bool)
:param size: (width, height) size in characters (wide), height is ignored and present to be consistent with other elements
:type size: (int, int) (width, UNUSED)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if values is not None:
self.Values = values
self.TKOptionMenu['menu'].delete(0, 'end')
# Insert list of new options (tk._setit hooks them up to var)
# self.TKStringVar.set(self.Values[0])
for new_value in self.Values:
self.TKOptionMenu['menu'].add_command(label=new_value, command=tk._setit(self.TKStringVar, new_value))
if value is None:
self.TKStringVar.set('')
if size == (None, None):
max_line_len = max([len(str(l)) for l in self.Values]) if len(self.Values) else 0
if self.AutoSizeText is False:
width = self.Size[0]
else:
width = max_line_len + 1
self.TKOptionMenu.configure(width=width)
else:
self.TKOptionMenu.configure(width=size[0])
if value is not None:
self.DefaultValue = value
self.TKStringVar.set(value)
if disabled == True:
self.TKOptionMenu['state'] = 'disabled'
elif disabled == False:
self.TKOptionMenu['state'] = 'normal'
if visible is False:
self.TKOptionMenu.pack_forget()
elif visible is True:
self.TKOptionMenu.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if visible is not None:
self._visible = visible
Update = update
# ------------------------- OPTION MENU Element lazy functions ------------------------- #
InputOptionMenu = OptionMenu
# ---------------------------------------------------------------------- #
# Listbox #
# ---------------------------------------------------------------------- #
class Listbox(Element):
"""
A List Box. Provide a list of values for the user to choose one or more of. Returns a list of selected rows
when a window.read() is executed.
"""
def __init__(self, values, default_values=None, select_mode=None, change_submits=False, enable_events=False,
bind_return_key=False, size=(None, None), s=(None, None), disabled=False, auto_size_text=None, font=None, no_scrollbar=False, horizontal_scroll=False,
background_color=None, text_color=None, highlight_background_color=None, highlight_text_color=None,
key=None, k=None, pad=None, p=None, tooltip=None, expand_x=False, expand_y=False,right_click_menu=None, visible=True, metadata=None):
"""
:param values: list of values to display. Can be any type including mixed types as long as they have __str__ method
:type values: List[Any] or Tuple[Any]
:param default_values: which values should be initially selected
:type default_values: List[Any]
:param select_mode: Select modes are used to determine if only 1 item can be selected or multiple and how they can be selected. Valid choices begin with "LISTBOX_SELECT_MODE_" and include: LISTBOX_SELECT_MODE_SINGLE LISTBOX_SELECT_MODE_MULTIPLE LISTBOX_SELECT_MODE_BROWSE LISTBOX_SELECT_MODE_EXTENDED
:type select_mode: [enum]
:param change_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type change_submits: (bool)
:param enable_events: Turns on the element specific events. Listbox generates events when an item is clicked
:type enable_events: (bool)
:param bind_return_key: If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: (bool)
:param size: w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int, int) | (int, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param disabled: set disable state for element
:type disabled: (bool)
:param auto_size_text: True if element should be the same size as the contents
:type auto_size_text: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param no_scrollbar: Controls if a scrollbar should be shown. If True, no scrollbar will be shown
:type no_scrollbar: (bool)
:param horizontal_scroll: Controls if a horizontal scrollbar should be shown. If True a horizontal scrollbar will be shown in addition to vertical
:type horizontal_scroll: (bool)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param highlight_background_color: color of the background when an item is selected. Defaults to normal text color (a reverse look)
:type highlight_background_color: (str)
:param highlight_text_color: color of the text when an item is selected. Defaults to the normal background color (a rerverse look)
:type highlight_text_color: (str)
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.Values = values
self.DefaultValues = default_values
self.TKListbox = None
self.ChangeSubmits = change_submits or enable_events
self.BindReturnKey = bind_return_key
self.Disabled = disabled
if select_mode == LISTBOX_SELECT_MODE_BROWSE:
self.SelectMode = SELECT_MODE_BROWSE
elif select_mode == LISTBOX_SELECT_MODE_EXTENDED:
self.SelectMode = SELECT_MODE_EXTENDED
elif select_mode == LISTBOX_SELECT_MODE_MULTIPLE:
self.SelectMode = SELECT_MODE_MULTIPLE
elif select_mode == LISTBOX_SELECT_MODE_SINGLE:
self.SelectMode = SELECT_MODE_SINGLE
else:
self.SelectMode = DEFAULT_LISTBOX_SELECT_MODE
bg = background_color if background_color is not None else theme_input_background_color()
fg = text_color if text_color is not None else theme_input_text_color()
self.HighlightBackgroundColor = highlight_background_color if highlight_background_color is not None else fg
self.HighlightTextColor = highlight_text_color if highlight_text_color is not None else bg
self.RightClickMenu = right_click_menu
self.vsb = None # type: tk.Scrollbar or None
self.hsb = None # type: tk.Scrollbar | None
self.TKListbox = self.Widget = None # type: tk.Listbox
self.element_frame = None # type: tk.Frame
self.NoScrollbar = no_scrollbar
self.HorizontalScroll = horizontal_scroll
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(ELEM_TYPE_INPUT_LISTBOX, size=sz, auto_size_text=auto_size_text, font=font,
background_color=bg, text_color=fg, key=key, pad=pad, tooltip=tooltip, visible=visible, metadata=metadata)
def update(self, values=None, disabled=None, set_to_index=None, scroll_to_index=None, select_mode=None, visible=None):
"""
Changes some of the settings for the Listbox Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param values: new list of choices to be shown to user
:type values: List[Any]
:param disabled: disable or enable state of the element
:type disabled: (bool)
:param set_to_index: highlights the item(s) indicated. If parm is an int one entry will be set. If is a list, then each entry in list is highlighted
:type set_to_index: int | list | tuple
:param scroll_to_index: scroll the listbox so that this index is the first shown
:type scroll_to_index: (int)
:param select_mode: changes the select mode according to tkinter's listbox widget
:type select_mode: (str)
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if disabled is True:
self.TKListbox.configure(state='disabled')
elif disabled is False:
self.TKListbox.configure(state='normal')
if values is not None:
self.TKListbox.delete(0, 'end')
for item in list(values):
self.TKListbox.insert(tk.END, item)
# self.TKListbox.selection_set(0, 0)
self.Values = list(values)
if set_to_index is not None:
self.TKListbox.selection_clear(0, len(self.Values)) # clear all listbox selections
if type(set_to_index) in (tuple, list):
for i in set_to_index:
try:
self.TKListbox.selection_set(i, i)
except:
warnings.warn('* Listbox Update selection_set failed with index {}*'.format(set_to_index))
else:
try:
self.TKListbox.selection_set(set_to_index, set_to_index)
except:
warnings.warn('* Listbox Update selection_set failed with index {}*'.format(set_to_index))
if visible is False:
self.element_frame.pack_forget()
if not self.NoScrollbar:
self.vsb.pack_forget()
elif visible is True:
self.element_frame.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if not self.NoScrollbar:
self.vsb.pack()
if scroll_to_index is not None and len(self.Values):
self.TKListbox.yview_moveto(scroll_to_index / len(self.Values))
if select_mode is not None:
try:
self.TKListbox.config(selectmode=select_mode)
except:
print('Listbox.update error trying to change mode to: ', select_mode)
if visible is not None:
self._visible = visible
def set_value(self, values):
"""
Set listbox highlighted choices
:param values: new values to choose based on previously set values
:type values: List[Any] | Tuple[Any]
"""
for index, item in enumerate(self.Values):
try:
if item in values:
self.TKListbox.selection_set(index)
else:
self.TKListbox.selection_clear(index)
except:
pass
self.DefaultValues = values
def get_list_values(self):
# type: (Listbox) -> List[Any]
"""
Returns list of Values provided by the user in the user's format
:return: List of values. Can be any / mixed types -> []
:rtype: List[Any]
"""
return self.Values
def get_indexes(self):
"""
Returns the items currently selected as a list of indexes
:return: A list of offsets into values that is currently selected
:rtype: List[int]
"""
return self.TKListbox.curselection()
def get(self):
"""
Returns the list of items currently selected in this listbox. It should be identical
to the value you would receive when performing a window.read() call.
:return: The list of currently selected items. The actual items are returned, not the indexes
:rtype: List[Any]
"""
try:
items = self.TKListbox.curselection()
value = [self.Values[int(item)] for item in items]
except:
value = []
return value
GetIndexes = get_indexes
GetListValues = get_list_values
SetValue = set_value
Update = update
LBox = Listbox
LB = Listbox
# ---------------------------------------------------------------------- #
# Radio #
# ---------------------------------------------------------------------- #
class Radio(Element):
"""
Radio Button Element - Used in a group of other Radio Elements to provide user with ability to select only
1 choice in a list of choices.
"""
def __init__(self, text, group_id, default=False, disabled=False, size=(None, None), s=(None, None), auto_size_text=None,
background_color=None, text_color=None, circle_color=None, font=None, key=None, k=None, pad=None, p=None, tooltip=None,
change_submits=False, enable_events=False, right_click_menu=None, expand_x=False, expand_y=False, visible=True, metadata=None):
"""
:param text: Text to display next to button
:type text: (str)
:param group_id: Groups together multiple Radio Buttons. Any type works
:type group_id: (Any)
:param default: Set to True for the one element of the group you want initially selected
:type default: (bool)
:param disabled: set disable state
:type disabled: (bool)
:param size: (w, h) w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int, int) | (None, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_text: if True will size the element to match the length of the text
:type auto_size_text: (bool)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param circle_color: color of background of the circle that has the dot selection indicator in it
:type circle_color: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param change_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type change_submits: (bool)
:param enable_events: Turns on the element specific events. Radio Button events happen when an item is selected
:type enable_events: (bool)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.InitialState = default
self.Text = text
self.Widget = self.TKRadio = None # type: tk.Radiobutton
self.GroupID = group_id
self.Value = None
self.Disabled = disabled
self.TextColor = text_color if text_color else theme_text_color()
self.RightClickMenu = right_click_menu
if circle_color is None:
# ---- compute color of circle background ---
try: # something in here will fail if a color is not specified in Hex
text_hsl = _hex_to_hsl(self.TextColor)
background_hsl = _hex_to_hsl(background_color if background_color else theme_background_color())
l_delta = abs(text_hsl[2] - background_hsl[2]) / 10
if text_hsl[2] > background_hsl[2]: # if the text is "lighter" than the background then make background darker
bg_rbg = _hsl_to_rgb(background_hsl[0], background_hsl[1], background_hsl[2] - l_delta)
else:
bg_rbg = _hsl_to_rgb(background_hsl[0], background_hsl[1], background_hsl[2] + l_delta)
self.CircleBackgroundColor = rgb(*bg_rbg)
except:
self.CircleBackgroundColor = background_color if background_color else theme_background_color()
else:
self.CircleBackgroundColor = circle_color
self.ChangeSubmits = change_submits or enable_events
self.EncodedRadioValue = None
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(ELEM_TYPE_INPUT_RADIO, size=sz, auto_size_text=auto_size_text, font=font,
background_color=background_color, text_color=self.TextColor, key=key, pad=pad,
tooltip=tooltip, visible=visible, metadata=metadata)
def update(self, value=None, text=None, background_color=None, text_color=None, circle_color=None, disabled=None, visible=None):
"""
Changes some of the settings for the Radio Button Element. Must call `Window.read` or `Window.finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: if True change to selected and set others in group to unselected
:type value: (bool)
:param text: Text to display next to radio button
:type text: (str)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text. Note this also changes the color of the selection dot
:type text_color: (str)
:param circle_color: color of background of the circle that has the dot selection indicator in it
:type circle_color: (str)
:param disabled: disable or enable state of the element
:type disabled: (bool)
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if value is not None:
try:
if value is True:
self.TKIntVar.set(self.EncodedRadioValue)
elif value is False:
if self.TKIntVar.get() == self.EncodedRadioValue:
self.TKIntVar.set(0)
except:
print('Error updating Radio')
self.InitialState = value
if text is not None:
self.Text = str(text)
self.TKRadio.configure(text=self.Text)
if background_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKRadio.configure(background=background_color)
self.BackgroundColor = background_color
if text_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKRadio.configure(fg=text_color)
self.TextColor = text_color
if circle_color not in (None, COLOR_SYSTEM_DEFAULT):
self.CircleBackgroundColor = circle_color
self.TKRadio.configure(selectcolor=self.CircleBackgroundColor) # The background of the radio button
elif text_color or background_color:
if self.TextColor not in (None, COLOR_SYSTEM_DEFAULT) and self.BackgroundColor not in (None, COLOR_SYSTEM_DEFAULT) and self.TextColor.startswith(
'#') and self.BackgroundColor.startswith('#'):
# ---- compute color of circle background ---
text_hsl = _hex_to_hsl(self.TextColor)
background_hsl = _hex_to_hsl(self.BackgroundColor if self.BackgroundColor else theme_background_color())
l_delta = abs(text_hsl[2] - background_hsl[2]) / 10
if text_hsl[2] > background_hsl[2]: # if the text is "lighter" than the background then make background darker
bg_rbg = _hsl_to_rgb(background_hsl[0], background_hsl[1], background_hsl[2] - l_delta)
else:
bg_rbg = _hsl_to_rgb(background_hsl[0], background_hsl[1], background_hsl[2] + l_delta)
self.CircleBackgroundColor = rgb(*bg_rbg)
self.TKRadio.configure(selectcolor=self.CircleBackgroundColor) # The background of the checkbox
if disabled is True:
self.TKRadio['state'] = 'disabled'
elif disabled is False:
self.TKRadio['state'] = 'normal'
if visible is False:
self.TKRadio.pack_forget()
elif visible is True:
self.TKRadio.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if visible is not None:
self._visible = visible
def reset_group(self):
"""
Sets all Radio Buttons in the group to not selected
"""
self.TKIntVar.set(0)
def get(self):
# type: (Radio) -> bool
"""
A snapshot of the value of Radio Button -> (bool)
:return: True if this radio button is selected
:rtype: (bool)
"""
return self.TKIntVar.get() == self.EncodedRadioValue
Get = get
ResetGroup = reset_group
Update = update
R = Radio
Rad = Radio
# ---------------------------------------------------------------------- #
# Checkbox #
# ---------------------------------------------------------------------- #
class Checkbox(Element):
"""
Checkbox Element - Displays a checkbox and text next to it
"""
def __init__(self, text, default=False, size=(None, None), s=(None, None), auto_size_text=None, font=None, background_color=None,
text_color=None, checkbox_color=None, change_submits=False, enable_events=False, disabled=False, key=None, k=None, pad=None, p=None, tooltip=None,
right_click_menu=None, expand_x=False, expand_y=False, visible=True, metadata=None):
"""
:param text: Text to display next to checkbox
:type text: (str)
:param default: Set to True if you want this checkbox initially checked
:type default: (bool)
:param size: (w, h) w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int, int) | (None, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_text: if True will size the element to match the length of the text
:type auto_size_text: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param checkbox_color: color of background of the box that has the check mark in it. The checkmark is the same color as the text
:type checkbox_color: (str)
:param change_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type change_submits: (bool)
:param enable_events: Turns on the element specific events. Checkbox events happen when an item changes
:type enable_events: (bool)
:param disabled: set disable state
:type disabled: (bool)
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.Text = text
self.InitialState = default
self.Value = None
self.TKCheckbutton = self.Widget = None # type: tk.Checkbutton
self.Disabled = disabled
self.TextColor = text_color if text_color else theme_text_color()
self.RightClickMenu = right_click_menu
# ---- compute color of circle background ---
if checkbox_color is None:
try: # something in here will fail if a color is not specified in Hex
text_hsl = _hex_to_hsl(self.TextColor)
background_hsl = _hex_to_hsl(background_color if background_color else theme_background_color())
l_delta = abs(text_hsl[2] - background_hsl[2]) / 10
if text_hsl[2] > background_hsl[2]: # if the text is "lighter" than the background then make background darker
bg_rbg = _hsl_to_rgb(background_hsl[0], background_hsl[1], background_hsl[2] - l_delta)
else:
bg_rbg = _hsl_to_rgb(background_hsl[0], background_hsl[1], background_hsl[2] + l_delta)
self.CheckboxBackgroundColor = rgb(*bg_rbg)
except:
self.CheckboxBackgroundColor = background_color if background_color else theme_background_color()
else:
self.CheckboxBackgroundColor = checkbox_color
self.ChangeSubmits = change_submits or enable_events
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(ELEM_TYPE_INPUT_CHECKBOX, size=sz, auto_size_text=auto_size_text, font=font,
background_color=background_color, text_color=self.TextColor, key=key, pad=pad,
tooltip=tooltip, visible=visible, metadata=metadata)
def get(self):
# type: (Checkbox) -> bool
"""
Return the current state of this checkbox
:return: Current state of checkbox
:rtype: (bool)
"""
return self.TKIntVar.get() != 0
def update(self, value=None, text=None, background_color=None, text_color=None, checkbox_color=None, disabled=None, visible=None):
"""
Changes some of the settings for the Checkbox Element. Must call `Window.Read` or `Window.Finalize` prior.
Note that changing visibility may cause element to change locations when made visible after invisible
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: if True checks the checkbox, False clears it
:type value: (bool)
:param text: Text to display next to checkbox
:type text: (str)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text. Note this also changes the color of the checkmark
:type text_color: (str)
:param disabled: disable or enable element
:type disabled: (bool)
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if value is not None:
value = bool(value)
try:
self.TKIntVar.set(value)
self.InitialState = value
except:
print('Checkbox update failed')
if disabled == True:
self.TKCheckbutton.configure(state='disabled')
elif disabled == False:
self.TKCheckbutton.configure(state='normal')
if text is not None:
self.Text = str(text)
self.TKCheckbutton.configure(text=self.Text)
if background_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKCheckbutton.configure(background=background_color)
self.BackgroundColor = background_color
if text_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKCheckbutton.configure(fg=text_color)
self.TextColor = text_color
# Color the checkbox itself
if checkbox_color not in (None, COLOR_SYSTEM_DEFAULT):
self.CheckboxBackgroundColor = checkbox_color
self.TKCheckbutton.configure(selectcolor=self.CheckboxBackgroundColor) # The background of the checkbox
elif text_color or background_color:
if self.CheckboxBackgroundColor is not None and self.TextColor is not None and self.BackgroundColor is not None and self.TextColor.startswith(
'#') and self.BackgroundColor.startswith('#'):
# ---- compute color of checkbox background ---
text_hsl = _hex_to_hsl(self.TextColor)
background_hsl = _hex_to_hsl(self.BackgroundColor if self.BackgroundColor else theme_background_color())
l_delta = abs(text_hsl[2] - background_hsl[2]) / 10
if text_hsl[2] > background_hsl[2]: # if the text is "lighter" than the background then make background darker
bg_rbg = _hsl_to_rgb(background_hsl[0], background_hsl[1], background_hsl[2] - l_delta)
else:
bg_rbg = _hsl_to_rgb(background_hsl[0], background_hsl[1], background_hsl[2] + l_delta)
self.CheckboxBackgroundColor = rgb(*bg_rbg)
self.TKCheckbutton.configure(selectcolor=self.CheckboxBackgroundColor) # The background of the checkbox
if visible is False:
self.TKCheckbutton.pack_forget()
elif visible is True:
self.TKCheckbutton.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if visible is not None:
self._visible = visible
Get = get
Update = update
# ------------------------- CHECKBOX Element lazy functions ------------------------- #
CB = Checkbox
CBox = Checkbox
Check = Checkbox
# ---------------------------------------------------------------------- #
# Spin #
# ---------------------------------------------------------------------- #
class Spin(Element):
"""
A spinner with up/down buttons and a single line of text. Choose 1 values from list
"""
def __init__(self, values, initial_value=None, disabled=False, change_submits=False, enable_events=False, readonly=False,
size=(None, None), s=(None, None), auto_size_text=None, font=None, background_color=None, text_color=None, key=None, k=None, pad=None, p=None,
tooltip=None, right_click_menu=None, expand_x=False, expand_y=False, visible=True, metadata=None):
"""
:param values: List of valid values
:type values: Tuple[Any] or List[Any]
:param initial_value: Initial item to show in window. Choose from list of values supplied
:type initial_value: (Any)
:param disabled: set disable state
:type disabled: (bool)
:param change_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type change_submits: (bool)
:param enable_events: Turns on the element specific events. Spin events happen when an item changes
:type enable_events: (bool)
:param readonly: Turns on the element specific events. Spin events happen when an item changes
:type readonly: (bool)
:param size: (w, h) w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int, int) | (None, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_text: if True will size the element to match the length of the text
:type auto_size_text: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.Values = values
self.DefaultValue = initial_value
self.ChangeSubmits = change_submits or enable_events
self.TKSpinBox = self.Widget = None # type: tk.Spinbox
self.Disabled = disabled
self.Readonly = readonly
self.RightClickMenu = right_click_menu
bg = background_color if background_color else DEFAULT_INPUT_ELEMENTS_COLOR
fg = text_color if text_color is not None else DEFAULT_INPUT_TEXT_COLOR
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(ELEM_TYPE_INPUT_SPIN, size=sz, auto_size_text=auto_size_text, font=font, background_color=bg, text_color=fg,
key=key, pad=pad, tooltip=tooltip, visible=visible, metadata=metadata)
return
def update(self, value=None, values=None, disabled=None, readonly=None, visible=None):
"""
Changes some of the settings for the Spin Element. Must call `Window.Read` or `Window.Finalize` prior
Note that the state can be in 3 states only.... enabled, disabled, readonly even
though more combinations are available. The easy way to remember is that if you
change the readonly parameter then you are enabling the element.
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: set the current value from list of choices
:type value: (Any)
:param values: set available choices
:type values: List[Any]
:param disabled: disable. Note disabled and readonly cannot be mixed. It must be one OR the other
:type disabled: (bool)
:param readonly: make element readonly. Note disabled and readonly cannot be mixed. It must be one OR the other
:type readonly: (bool)
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if values != None:
old_value = self.TKStringVar.get()
self.Values = values
self.TKSpinBox.configure(values=values)
self.TKStringVar.set(old_value)
if value is not None:
try:
self.TKStringVar.set(value)
self.DefaultValue = value
except:
pass
if readonly is True:
self.Readonly = True
self.TKSpinBox['state'] = 'readonly'
elif readonly is False:
self.Readonly = False
self.TKSpinBox['state'] = 'normal'
if disabled is True:
self.TKSpinBox['state'] = 'disable'
elif disabled is False:
if self.Readonly:
self.TKSpinBox['state'] = 'readonly'
else:
self.TKSpinBox['state'] = 'normal'
if visible is False:
self.TKSpinBox.pack_forget()
elif visible is True:
self.TKSpinBox.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if visible is not None:
self._visible = visible
def _SpinChangedHandler(self, event):
"""
Callback function. Used internally only. Called by tkinter when Spinbox Widget changes. Results in Window.Read() call returning
:param event: passed in from tkinter
:type event:
"""
# first, get the results table built
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = ''
self.ParentForm.FormRemainedOpen = True
_exit_mainloop(self.ParentForm)
# if self.ParentForm.CurrentlyRunningMainloop:
# Window._window_that_exited = self.ParentForm
# self.ParentForm.TKroot.quit() # kick the users out of the mainloop
def get(self):
"""
Return the current chosen value showing in spinbox.
This value will be the same as what was provided as list of choices. If list items are ints, then the
item returned will be an int (not a string)
:return: The currently visible entry
:rtype: (Any)
"""
value = self.TKStringVar.get()
for v in self.Values:
if str(v) == value:
value = v
break
return value
Get = get
Update = update
Sp = Spin # type: Spin
# ---------------------------------------------------------------------- #
# Multiline #
# ---------------------------------------------------------------------- #
class Multiline(Element):
"""
Multiline Element - Display and/or read multiple lines of text. This is both an input and output element.
Other PySimpleGUI ports have a separate MultilineInput and MultilineOutput elements. May want to split this
one up in the future too.
"""
def __init__(self, default_text='', enter_submits=False, disabled=False, autoscroll=False, border_width=None,
size=(None, None), s=(None, None), auto_size_text=None, background_color=None, text_color=None, horizontal_scroll=False, change_submits=False,
enable_events=False, do_not_clear=True, key=None, k=None, write_only=False, auto_refresh=False, reroute_stdout=False, reroute_stderr=False, reroute_cprint=False, echo_stdout_stderr=False, focus=False, font=None, pad=None, p=None, tooltip=None, justification=None, no_scrollbar=False, expand_x=False, expand_y=False, rstrip=True, right_click_menu=None, visible=True, metadata=None):
"""
:param default_text: Initial text to show
:type default_text: (Any)
:param enter_submits: if True, the Window.Read call will return is enter key is pressed in this element
:type enter_submits: (bool)
:param disabled: set disable state
:type disabled: (bool)
:param autoscroll: If True the contents of the element will automatically scroll as more data added to the end
:type autoscroll: (bool)
:param border_width: width of border around element in pixels
:type border_width: (int)
:param size: (w, h) w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int, int) | (None, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_text: if True will size the element to match the length of the text
:type auto_size_text: (bool)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param horizontal_scroll: Controls if a horizontal scrollbar should be shown. If True a horizontal scrollbar will be shown in addition to vertical
:type horizontal_scroll: (bool)
:param change_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type change_submits: (bool)
:param enable_events: Turns on the element specific events. Spin events happen when an item changes
:type enable_events: (bool)
:param do_not_clear: if False the element will be cleared any time the Window.Read call returns
:type do_not_clear: (bool)
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param write_only: If True then no entry will be added to the values dictionary when the window is read
:type write_only: bool
:param auto_refresh: If True then anytime the element is updated, the window will be refreshed so that the change is immediately displayed
:type auto_refresh: (bool)
:param reroute_stdout: If True then all output to stdout will be output to this element
:type reroute_stdout: (bool)
:param reroute_stderr: If True then all output to stderr will be output to this element
:type reroute_stderr: (bool)
:param reroute_cprint: If True your cprint calls will output to this element. It's the same as you calling cprint_set_output_destination
:type reroute_cprint: (bool)
:param echo_stdout_stderr: If True then output to stdout and stderr will be output to this element AND also to the normal console location
:type echo_stdout_stderr: (bool)
:param focus: if True initial focus will go to this element
:type focus: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param justification: text justification. left, right, center. Can use single characters l, r, c.
:type justification: (str)
:param no_scrollbar: If False then a vertical scrollbar will be shown (the default)
:type no_scrollbar: (bool)
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param rstrip: If True the value returned in will have whitespace stripped from the right side
:type rstrip: (bool)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.DefaultText = str(default_text)
self.EnterSubmits = enter_submits
bg = background_color if background_color else DEFAULT_INPUT_ELEMENTS_COLOR
self.Focus = focus
self.do_not_clear = do_not_clear
fg = text_color if text_color is not None else DEFAULT_INPUT_TEXT_COLOR
self.Autoscroll = autoscroll
self.Disabled = disabled
self.ChangeSubmits = change_submits or enable_events
self.RightClickMenu = right_click_menu
self.BorderWidth = border_width if border_width is not None else DEFAULT_BORDER_WIDTH
self.TagCounter = 0
self.TKText = self.Widget = None # type: tkst.ScrolledText
self.element_frame = None # type: tk.Frame
self.HorizontalScroll = horizontal_scroll
self.tags = set()
self.WriteOnly = write_only
self.AutoRefresh = auto_refresh
key = key if key is not None else k
self.previous_stdout = None
self.previous_stderr = None
self.reroute_cprint = reroute_cprint
self.echo_stdout_stderr = echo_stdout_stderr
self.Justification = 'left' if justification is None else justification
self.justification_tag = self.just_center_tag = self.just_left_tag = self.just_right_tag = None
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
self.rstrip = rstrip
if reroute_stdout:
self.reroute_stdout_to_here()
if reroute_stderr:
self.reroute_stderr_to_here()
self.no_scrollbar = no_scrollbar
self.hscrollbar = None # The horizontal scrollbar
sz = size if size != (None, None) else s
super().__init__(ELEM_TYPE_INPUT_MULTILINE, size=sz, auto_size_text=auto_size_text, background_color=bg,
text_color=fg, key=key, pad=pad, tooltip=tooltip, font=font or DEFAULT_FONT, visible=visible, metadata=metadata)
return
def update(self, value=None, disabled=None, append=False, font=None, text_color=None, background_color=None, text_color_for_value=None,
background_color_for_value=None, visible=None, autoscroll=None, justification=None, font_for_value=None):
"""
Changes some of the settings for the Multiline Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: new text to display
:type value: (Any)
:param disabled: disable or enable state of the element
:type disabled: (bool)
:param append: if True then new value will be added onto the end of the current value. if False then contents will be replaced.
:type append: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike for the entire element
:type font: (str or (str, int[, str]) or None)
:param text_color: color of the text
:type text_color: (str)
:param background_color: color of background
:type background_color: (str)
:param text_color_for_value: color of the new text being added (the value paramter)
:type text_color_for_value: (str)
:param background_color_for_value: color of the new background of the text being added (the value paramter)
:type background_color_for_value: (str)
:param visible: set visibility state of the element
:type visible: (bool)
:param autoscroll: if True then contents of element are scrolled down when new text is added to the end
:type autoscroll: (bool)
:param justification: text justification. left, right, center. Can use single characters l, r, c. Sets only for this value, not entire element
:type justification: (str)
:param font_for_value: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike for the value being updated
:type font_for_value: str | (str, int)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if autoscroll is not None:
self.Autoscroll = autoscroll
if justification is not None:
if justification.startswith('l'):
just_tag = 'left'
if justification.startswith('r'):
just_tag = 'right'
if justification.startswith('c'):
just_tag = 'center'
else:
just_tag = self.justification_tag
starting_point = self.Widget.index(tk.INSERT)
tag = None
if value is not None:
value = str(value)
if background_color_for_value is not None or text_color_for_value is not None or font_for_value is not None:
try:
tag = 'Multiline(' + str(text_color_for_value) + ',' + str(background_color_for_value) + ',' + str(font_for_value) + ')'
if tag not in self.tags:
self.tags.add(tag)
if background_color_for_value is not None:
self.TKText.tag_configure(tag, background=background_color_for_value)
if text_color_for_value is not None:
self.TKText.tag_configure(tag, foreground=text_color_for_value)
if font_for_value is not None:
self.TKText.tag_configure(tag, font=font_for_value)
except Exception as e:
print('* Multiline.update - bad color likely specified:', e)
if self.Disabled:
self.TKText.configure(state='normal')
try:
if not append:
self.TKText.delete('1.0', tk.END)
if tag is not None or just_tag is not None:
self.TKText.insert(tk.END, value, (just_tag, tag))
else:
self.TKText.insert(tk.END, value)
# self.TKText.tag_add(just_tag, starting_point, starting_point)
except Exception as e:
print("* Error setting multiline *", e)
if self.Disabled:
self.TKText.configure(state='disabled')
self.DefaultText = value
if self.Autoscroll:
self.TKText.see(tk.END)
if disabled is True:
self.TKText.configure(state='disabled')
self.Disabled = True
elif disabled is False:
self.TKText.configure(state='normal')
self.Disabled = False
if background_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKText.configure(background=background_color)
if text_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKText.configure(fg=text_color)
if font is not None:
self.TKText.configure(font=font)
if visible is False:
self.element_frame.pack_forget()
# if self.HorizontalScroll:
# self.hscrollbar.pack_forget()
elif visible is True:
self.element_frame.pack(padx=self.pad_used[0], pady=self.pad_used[1])
# self.hscrollbar.pack(side=tk.BOTTOM, fill='x')
if self.AutoRefresh and self.ParentForm:
try: # in case the window was destroyed
self.ParentForm.refresh()
except:
pass
if visible is not None:
self._visible = visible
def get(self):
"""
Return current contents of the Multiline Element
:return: current contents of the Multiline Element (used as an input type of Multiline
:rtype: (str)
"""
value = str(self.TKText.get(1.0, tk.END))
if self.rstrip:
return value.rstrip()
return value
def print(self, *args, end=None, sep=None, text_color=None, background_color=None, justification=None, font=None, colors=None, t=None, b=None, c=None,
autoscroll=True):
"""
Print like Python normally prints except route the output to a multiline element and also add colors if desired
colors -(str, str) or str. A combined text/background color definition in a single parameter
There are also "aliases" for text_color, background_color and colors (t, b, c)
t - An alias for color of the text (makes for shorter calls)
b - An alias for the background_color parameter
c - (str, str) - "shorthand" way of specifying color. (foreground, backgrouned)
c - str - can also be a string of the format "foreground on background" ("white on red")
With the aliases it's possible to write the same print but in more compact ways:
cprint('This will print white text on red background', c=('white', 'red'))
cprint('This will print white text on red background', c='white on red')
cprint('This will print white text on red background', text_color='white', background_color='red')
cprint('This will print white text on red background', t='white', b='red')
:param args: The arguments to print
:type args: (Any)
:param end: The end char to use just like print uses
:type end: (str)
:param sep: The separation character like print uses
:type sep: (str)
:param text_color: The color of the text
:type text_color: (str)
:param background_color: The background color of the line
:type background_color: (str)
:param justification: text justification. left, right, center. Can use single characters l, r, c. Sets only for this value, not entire element
:type justification: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike for the args being printed
:type font: (str or (str, int[, str]) or None)
:param colors: Either a tuple or a string that has both the text and background colors. Or just the text color
:type colors: (str) or (str, str)
:param t: Color of the text
:type t: (str)
:param b: The background color of the line
:type b: (str)
:param c: Either a tuple or a string that has both the text and background colors or just tex color (same as the color parm)
:type c: (str) or (str, str)
:param autoscroll: If True the contents of the element will automatically scroll as more data added to the end
:type autoscroll: (bool)
"""
kw_text_color = text_color or t
kw_background_color = background_color or b
dual_color = colors or c
try:
if isinstance(dual_color, tuple):
kw_text_color = dual_color[0]
kw_background_color = dual_color[1]
elif isinstance(dual_color, str):
if ' on ' in dual_color: # if has "on" in the string, then have both text and background
kw_text_color = dual_color.split(' on ')[0]
kw_background_color = dual_color.split(' on ')[1]
else: # if no "on" then assume the color string is just the text color
kw_text_color = dual_color
except Exception as e:
print('* multiline print warning * you messed up with color formatting', e)
_print_to_element(self, *args, end=end, sep=sep, text_color=kw_text_color, background_color=kw_background_color, justification=justification,
autoscroll=autoscroll, font=font)
def reroute_stdout_to_here(self):
"""
Sends stdout (prints) to this element
"""
self.previous_stdout = sys.stdout
sys.stdout = self
def reroute_stderr_to_here(self):
"""
Sends stderr to this element
"""
self.previous_stderr = sys.stderr
sys.stderr = self
def restore_stdout(self):
"""
Restore a previously re-reouted stdout back to the original destination
"""
if self.previous_stdout:
sys.stdout = self.previous_stdout
self.previous_stdout = None # indicate no longer routed here
def restore_stderr(self):
"""
Restore a previously re-reouted stderr back to the original destination
"""
if self.previous_stderr:
sys.stderr = self.previous_stderr
self.previous_stderr = None # indicate no longer routed here
def write(self, txt):
"""
Called by Python (not tkinter?) when stdout or stderr wants to write
:param txt: text of output
:type txt: (str)
"""
try:
self.update(txt, append=True)
if self.echo_stdout_stderr:
self.previous_stdout.write(txt)
except:
pass
def flush(self):
"""
Flush parameter was passed into a print statement.
For now doing nothing. Not sure what action should be taken to ensure a flush happens regardless.
"""
try:
self.previous_stdout.flush()
except:
pass
def __del__(self):
"""
If this Widget is deleted, be sure and restore the old stdout, stderr
"""
# These trys are here because found that if the init fails, then
# the variables holding the old stdout won't exist and will get an error
try:
self.restore_stdout()
except Exception as e:
pass
try:
self.restore_stderr()
except:
pass
Get = get
Update = update
ML = Multiline
MLine = Multiline
# ---------------------------------------------------------------------- #
# Text #
# ---------------------------------------------------------------------- #
class Text(Element):
"""
Text - Display some text in the window. Usually this means a single line of text. However, the text can also be multiple lines. If multi-lined there are no scroll bars.
"""
def __init__(self, text='', size=(None, None), s=(None, None), auto_size_text=None, click_submits=False, enable_events=False, relief=None, font=None,
text_color=None, background_color=None, border_width=None, justification=None, pad=None, p=None, key=None, k=None, right_click_menu=None, expand_x=False, expand_y=False, grab=None,
tooltip=None, visible=True, metadata=None):
"""
:param text: The text to display. Can include /n to achieve multiple lines. Will convert (optional) parameter into a string
:type text: Any
:param size: (w, h) w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int, int) | (int, None) | (None, None) | (int, ) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (int, None) | (None, None) | (int, ) | int
:param auto_size_text: if True size of the Text Element will be sized to fit the string provided in 'text' parm
:type auto_size_text: (bool)
:param click_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type click_submits: (bool)
:param enable_events: Turns on the element specific events. Text events happen when the text is clicked
:type enable_events: (bool)
:param relief: relief style around the text. Values are same as progress meter relief values. Should be a constant that is defined at starting with "RELIEF_" - `RELIEF_RAISED, RELIEF_SUNKEN, RELIEF_FLAT, RELIEF_RIDGE, RELIEF_GROOVE, RELIEF_SOLID`
:type relief: (str/enum)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param text_color: color of the text
:type text_color: (str)
:param background_color: color of background
:type background_color: (str)
:param border_width: number of pixels for the border (if using a relief)
:type border_width: (int)
:param justification: how string should be aligned within space provided by size. Valid choices = `left`, `right`, `center`
:type justification: (str)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str or int or tuple or object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param grab: If True can grab this element and move the window around. Default is False
:type grab: (bool)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.DisplayText = str(text)
self.TextColor = text_color if text_color else DEFAULT_TEXT_COLOR
self.Justification = justification
self.Relief = relief
self.ClickSubmits = click_submits or enable_events
if background_color is None:
bg = DEFAULT_TEXT_ELEMENT_BACKGROUND_COLOR
else:
bg = background_color
self.RightClickMenu = right_click_menu
self.TKRightClickMenu = None
self.BorderWidth = border_width
self.Grab = grab
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(ELEM_TYPE_TEXT, auto_size_text=auto_size_text, size=sz, background_color=bg, font=font if font else DEFAULT_FONT,
text_color=self.TextColor, pad=pad, key=key, tooltip=tooltip, visible=visible, metadata=metadata)
def update(self, value=None, background_color=None, text_color=None, font=None, visible=None):
"""
Changes some of the settings for the Text Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: new text to show
:type value: (str)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param visible: set visibility state of the element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if value is not None:
self.DisplayText = str(value)
self.TKStringVar.set(str(value))
if background_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKText.configure(background=background_color)
if text_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKText.configure(fg=text_color)
if font is not None:
self.TKText.configure(font=font)
if visible is False:
self.TKText.pack_forget()
elif visible is True:
self.TKText.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if visible is not None:
self._visible = visible
def get(self):
"""
Gets the current value of the displayed text
:return: The current value
:rtype: (str)
"""
try:
text = self.TKStringVar.get()
except:
text = ''
return text
@classmethod
def fonts_installed_list(cls):
"""
Returns a list of strings that tkinter reports as the installed fonts
:return: List of the installed font names
:rtype: List[str]
"""
# if no windows have been created (there is no hidden master root to rely on) then temporarily make a window so the measurement can happen
if Window.hidden_master_root is None:
root = tk.Tk()
else:
root = Window.hidden_master_root
fonts = list(tkinter.font.families())
fonts.sort()
if Window.hidden_master_root is None:
root.destroy()
return fonts
@classmethod
def char_width_in_pixels(cls, font, character='W'):
"""
Get the with of the character "W" in pixels for the font being passed in or
the character of your choosing if "W" is not a good representative character.
Cannot be used until a window has been created.
If an error occurs, 0 will be returned
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike, to be measured
:type font: (str or (str, int[, str]) or None)
:param character: specifies a SINGLE CHARACTER character to measure
:type character: (str)
:return: Width in pixels of "A"
:rtype: (int)
"""
# if no windows have been created (there is no hidden master root to rely on) then temporarily make a window so the measurement can happen
if Window.NumOpenWindows == 0:
root = tk.Tk()
else:
root = None
size = 0
try:
size = tkinter.font.Font(font=font).measure(character) # single character width
except Exception as e:
_error_popup_with_traceback('Exception retrieving char width in pixels', e)
if root is not None:
root.destroy()
return size
@classmethod
def char_height_in_pixels(cls, font):
"""
Get the height of a string if using the supplied font in pixels.
Cannot be used until a window has been created.
If an error occurs, 0 will be returned
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike, to be measured
:type font: (str or (str, int[, str]) or None)
:return: Height in pixels of "A"
:rtype: (int)
"""
# if no windows have been created (there is no hidden master root to rely on) then temporarily make a window so the measurement can happen
if Window.NumOpenWindows == 0:
root = tk.Tk()
else:
root = None
size = 0
try:
size = tkinter.font.Font(font=font).metrics('linespace')
except Exception as e:
_error_popup_with_traceback('Exception retrieving char height in pixels', e)
if root is not None:
root.destroy()
return size
@classmethod
def string_width_in_pixels(cls, font, string):
"""
Get the with of the supplied string in pixels for the font being passed in.
If an error occurs, 0 will be returned
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike, to be measured
:type font: (str or (str, int[, str]) or None)
:param string: the string to measure
:type string: str
:return: Width in pixels of string
:rtype: (int)
"""
# if no windows have been created (there is no hidden master root to rely on) then temporarily make a window so the measurement can happen
if Window.NumOpenWindows == 0:
root = tk.Tk()
else:
root = None
size = 0
try:
size = tkinter.font.Font(font=font).measure(string) # string's width
except Exception as e:
_error_popup_with_traceback('Exception retrieving string width in pixels', e)
if root is not None:
root.destroy()
return size
def _print_to_element(self, *args, end=None, sep=None, text_color=None, background_color=None, autoscroll=None, justification=None, font=None, append=None):
"""
Print like Python normally prints except route the output to a multiline element and also add colors if desired
:param multiline_element: The multiline element to be output to
:type multiline_element: (Multiline)
:param args: The arguments to print
:type args: List[Any]
:param end: The end char to use just like print uses
:type end: (str)
:param sep: The separation character like print uses
:type sep: (str)
:param text_color: color of the text
:type text_color: (str)
:param background_color: The background color of the line
:type background_color: (str)
:param autoscroll: If True (the default), the element will scroll to bottom after updating
:type autoscroll: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike for the value being updated
:type font: str | (str, int)
"""
end_str = str(end) if end is not None else '\n'
sep_str = str(sep) if sep is not None else ' '
outstring = ''
num_args = len(args)
for i, arg in enumerate(args):
outstring += str(arg)
if i != num_args - 1:
outstring += sep_str
outstring += end_str
if append:
outstring = self.get() + outstring
self.update(outstring, text_color=text_color, background_color=background_color, font=font)
try: # if the element is set to autorefresh, then refresh the parent window
if self.AutoRefresh:
self.ParentForm.refresh()
except:
pass
def print(self, *args, end=None, sep=None, text_color=None, background_color=None, justification=None, font=None, colors=None, t=None, b=None, c=None, autoscroll=True, append=True):
"""
Print like Python normally prints except route the output to a multiline element and also add colors if desired
colors -(str, str) or str. A combined text/background color definition in a single parameter
There are also "aliases" for text_color, background_color and colors (t, b, c)
t - An alias for color of the text (makes for shorter calls)
b - An alias for the background_color parameter
c - (str, str) - "shorthand" way of specifying color. (foreground, backgrouned)
c - str - can also be a string of the format "foreground on background" ("white on red")
With the aliases it's possible to write the same print but in more compact ways:
cprint('This will print white text on red background', c=('white', 'red'))
cprint('This will print white text on red background', c='white on red')
cprint('This will print white text on red background', text_color='white', background_color='red')
cprint('This will print white text on red background', t='white', b='red')
:param args: The arguments to print
:type args: (Any)
:param end: The end char to use just like print uses
:type end: (str)
:param sep: The separation character like print uses
:type sep: (str)
:param text_color: The color of the text
:type text_color: (str)
:param background_color: The background color of the line
:type background_color: (str)
:param justification: text justification. left, right, center. Can use single characters l, r, c. Sets only for this value, not entire element
:type justification: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike for the args being printed
:type font: (str or (str, int[, str]) or None)
:param colors: Either a tuple or a string that has both the text and background colors. Or just the text color
:type colors: (str) or (str, str)
:param t: Color of the text
:type t: (str)
:param b: The background color of the line
:type b: (str)
:param c: Either a tuple or a string that has both the text and background colors or just tex color (same as the color parm)
:type c: (str) or (str, str)
:param autoscroll: If True the contents of the element will automatically scroll as more data added to the end
:type autoscroll: (bool)
"""
kw_text_color = text_color or t
kw_background_color = background_color or b
dual_color = colors or c
try:
if isinstance(dual_color, tuple):
kw_text_color = dual_color[0]
kw_background_color = dual_color[1]
elif isinstance(dual_color, str):
if ' on ' in dual_color: # if has "on" in the string, then have both text and background
kw_text_color = dual_color.split(' on ')[0]
kw_background_color = dual_color.split(' on ')[1]
else: # if no "on" then assume the color string is just the text color
kw_text_color = dual_color
except Exception as e:
print('* multiline print warning * you messed up with color formatting', e)
self._print_to_element( *args, end=end, sep=sep, text_color=kw_text_color, background_color=kw_background_color, justification=justification, autoscroll=autoscroll, font=font, append=append)
Get = get
Update = update
# ------------------------- Text Element lazy functions ------------------------- #
Txt = Text # type: Text
T = Text # type: Text
# ---------------------------------------------------------------------- #
# StatusBar #
# ---------------------------------------------------------------------- #
class StatusBar(Element):
"""
A StatusBar Element creates the sunken text-filled strip at the bottom. Many Windows programs have this line
"""
def __init__(self, text, size=(None, None), s=(None, None), auto_size_text=None, click_submits=None, enable_events=False,
relief=RELIEF_SUNKEN, font=None, text_color=None, background_color=None, justification=None, pad=None, p=None,
key=None, k=None, right_click_menu=None, expand_x=False, expand_y=False, tooltip=None, visible=True, metadata=None):
"""
:param text: Text that is to be displayed in the widget
:type text: (str)
:param size: (w, h) w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int, int) | (int, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_text: True if size should fit the text length
:type auto_size_text: (bool)
:param click_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type click_submits: (bool)
:param enable_events: Turns on the element specific events. StatusBar events occur when the bar is clicked
:type enable_events: (bool)
:param relief: relief style. Values are same as progress meter relief values. Can be a constant or a string: `RELIEF_RAISED RELIEF_SUNKEN RELIEF_FLAT RELIEF_RIDGE RELIEF_GROOVE RELIEF_SOLID`
:type relief: (enum)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param text_color: color of the text
:type text_color: (str)
:param background_color: color of background
:type background_color: (str)
:param justification: how string should be aligned within space provided by size. Valid choices = `left`, `right`, `center`
:type justification: (str)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.DisplayText = text
self.TextColor = text_color if text_color else DEFAULT_TEXT_COLOR
self.Justification = justification
self.Relief = relief
self.ClickSubmits = click_submits or enable_events
if background_color is None:
bg = DEFAULT_TEXT_ELEMENT_BACKGROUND_COLOR
else:
bg = background_color
self.TKText = self.Widget = None # type: tk.Label
key = key if key is not None else k
self.RightClickMenu = right_click_menu
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(ELEM_TYPE_STATUSBAR, size=sz, auto_size_text=auto_size_text, background_color=bg,
font=font or DEFAULT_FONT, text_color=self.TextColor, pad=pad, key=key, tooltip=tooltip,
visible=visible, metadata=metadata)
return
def update(self, value=None, background_color=None, text_color=None, font=None, visible=None):
"""
Changes some of the settings for the Status Bar Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: new text to show
:type value: (str)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param visible: set visibility state of the element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if value is not None:
self.DisplayText = value
stringvar = self.TKStringVar
stringvar.set(value)
if background_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKText.configure(background=background_color)
if text_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKText.configure(fg=text_color)
if font is not None:
self.TKText.configure(font=font)
if visible is False:
self.TKText.pack_forget()
elif visible is True:
self.TKText.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if visible is not None:
self._visible = visible
Update = update
SBar = StatusBar
# ---------------------------------------------------------------------- #
# TKProgressBar #
# Emulate the TK ProgressBar using canvas and rectangles
# ---------------------------------------------------------------------- #
class TKProgressBar():
uniqueness_counter = 0
def __init__(self, root, max, length=400, width=DEFAULT_PROGRESS_BAR_SIZE[1], ttk_theme=DEFAULT_TTK_THEME, style_name='',
relief=DEFAULT_PROGRESS_BAR_RELIEF, border_width=DEFAULT_PROGRESS_BAR_BORDER_WIDTH,
orientation='horizontal', BarColor=(None, None), key=None):
"""
:param root: The root window bar is to be shown in
:type root: tk.Tk | tk.TopLevel
:param max: Maximum value the bar will be measuring
:type max: (int)
:param length: length in pixels of the bar
:type length: (int)
:param width: width in pixels of the bar
:type width: (int)
:param style_name: Progress bar style to use. Set in the packer function
:type style_name: (str)
:param ttk_theme: Progress bar style defined as one of these 'default', 'winnative', 'clam', 'alt', 'classic', 'vista', 'xpnative'
:type ttk_theme: (str)
:param relief: relief style. Values are same as progress meter relief values. Can be a constant or a string: `RELIEF_RAISED RELIEF_SUNKEN RELIEF_FLAT RELIEF_RIDGE RELIEF_GROOVE RELIEF_SOLID` (Default value = DEFAULT_PROGRESS_BAR_RELIEF)
:type relief: (str)
:param border_width: The amount of pixels that go around the outside of the bar
:type border_width: (int)
:param orientation: 'horizontal' or 'vertical' ('h' or 'v' work) (Default value = 'vertical')
:type orientation: (str)
:param BarColor: The 2 colors that make up a progress bar. One is the background, the other is the bar
:type BarColor: (str, str)
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
"""
self.Length = length
self.Width = width
self.Max = max
self.Orientation = orientation
self.Count = None
self.PriorCount = 0
self.style_name = style_name
TKProgressBar.uniqueness_counter += 1
if orientation.lower().startswith('h'):
s = ttk.Style()
s.theme_use(ttk_theme)
# self.style_name = str(key) + str(TKProgressBar.uniqueness_counter) + "my.Horizontal.TProgressbar"
if BarColor != COLOR_SYSTEM_DEFAULT and BarColor[0] != COLOR_SYSTEM_DEFAULT:
s.configure(self.style_name, background=BarColor[0], troughcolor=BarColor[1],
troughrelief=relief, borderwidth=border_width, thickness=width)
else:
s.configure(self.style_name, troughrelief=relief, borderwidth=border_width, thickness=width)
self.TKProgressBarForReal = ttk.Progressbar(root, maximum=self.Max, style=self.style_name, length=length, orient=tk.HORIZONTAL, mode='determinate')
else:
s = ttk.Style()
s.theme_use(ttk_theme)
# self.style_name = str(key) + str(TKProgressBar.uniqueness_counter) + "my.Vertical.TProgressbar"
if BarColor != COLOR_SYSTEM_DEFAULT and BarColor[0] != COLOR_SYSTEM_DEFAULT:
s.configure(self.style_name, background=BarColor[0],
troughcolor=BarColor[1], troughrelief=relief, borderwidth=border_width, thickness=width)
else:
s.configure(self.style_name, troughrelief=relief, borderwidth=border_width, thickness=width)
self.TKProgressBarForReal = ttk.Progressbar(root, maximum=self.Max, style=self.style_name, length=length, orient=tk.VERTICAL, mode='determinate')
def Update(self, count=None, max=None):
"""
Update the current value of the bar and/or update the maximum value the bar can reach
:param count: current value
:type count: (int)
:param max: the maximum value
:type max: (int)
"""
if max is not None:
self.Max = max
try:
self.TKProgressBarForReal.config(maximum=max)
except:
return False
if count is not None:
try:
self.TKProgressBarForReal['value'] = count
except:
return False
return True
# ---------------------------------------------------------------------- #
# TKOutput #
# New Type of TK Widget that's a Text Widget in disguise #
# Note that it's inherited from the TKFrame class so that the #
# Scroll bar will span the length of the frame #
# ---------------------------------------------------------------------- #
class TKOutput(tk.Frame):
"""
tkinter style class. Inherits Frame class from tkinter. Adds a tk.Text and a scrollbar together.
Note - This is NOT a user controlled class. Users should NOT be directly using it unless making an extention
to PySimpleGUI by directly manipulating tkinter.
"""
def __init__(self, parent, width, height, bd, background_color=None, text_color=None, echo_stdout_stderr=False, font=None, pad=None):
"""
:param parent: The "Root" that the Widget will be in
:type parent: tk.Tk | tk.Toplevel
:param width: Width in characters
:type width: (int)
:param height: height in rows
:type height: (int)
:param bd: Border Depth. How many pixels of border to show
:type bd: (int)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param echo_stdout_stderr: If True then output to stdout will be output to this element AND also to the normal console location
:type echo_stdout_stderr: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
"""
self.frame = tk.Frame(parent)
tk.Frame.__init__(self, self.frame)
self.output = tk.Text(self.frame, width=width, height=height, bd=bd, font=font)
if background_color and background_color != COLOR_SYSTEM_DEFAULT:
self.output.configure(background=background_color)
self.frame.configure(background=background_color)
if text_color and text_color != COLOR_SYSTEM_DEFAULT:
self.output.configure(fg=text_color)
self.output.configure(insertbackground=text_color)
self.vsb = tk.Scrollbar(self.frame, orient="vertical", command=self.output.yview)
self.output.configure(yscrollcommand=self.vsb.set)
self.output.pack(side="left", fill="both", expand=True)
self.vsb.pack(side="left", fill="y", expand=False)
self.frame.pack(side="left", padx=pad[0], pady=pad[1], expand=True, fill='y')
self.previous_stdout = sys.stdout
self.previous_stderr = sys.stderr
self.parent = parent
self.echo_stdout_stderr = echo_stdout_stderr
sys.stdout = self
sys.stderr = self
self.pack()
def write(self, txt):
"""
Called by Python (not tkinter?) when stdout or stderr wants to write
Refreshes the window after the write so that the change is immediately displayed
:param txt: text of output
:type txt: (str)
"""
try:
self.output.insert(tk.END, str(txt))
self.output.see(tk.END)
self.parent.update()
except:
pass
try:
if self.echo_stdout_stderr:
self.previous_stdout.write(txt)
except:
pass
def Close(self):
"""
Called when wanting to restore the old stdout/stderr
"""
sys.stdout = self.previous_stdout
sys.stderr = self.previous_stderr
def flush(self):
"""
Flush parameter was passed into a print statement.
For now doing nothing. Not sure what action should be taken to ensure a flush happens regardless.
"""
try:
if self.echo_stdout_stderr:
self.previous_stdout.flush()
except:
pass
def __del__(self):
"""
If this Widget is deleted, be sure and restore the old stdout, stderr
"""
sys.stdout = self.previous_stdout
sys.stderr = self.previous_stderr
# ---------------------------------------------------------------------- #
# Output #
# Routes stdout, stderr to a scrolled window #
# ---------------------------------------------------------------------- #
class Output(Element):
"""
Output Element - a multi-lined text area where stdout and stderr are re-routed to.
The Multiline Element is the superior and recommended method for showing the output of stdout.
The Multiline Element has been added to significantly while the Output element has not.
If you choose to use a Multiline element to replace an Output element, be sure an turn on the write_only paramter in the Multline
Of course, Output Element continues to operate and be backwards compatible, but you're missing out on
features such as routing the cprint output to the element.
"""
def __init__(self, size=(None, None), s=(None, None), background_color=None, text_color=None, pad=None, p=None, echo_stdout_stderr=False, font=None, tooltip=None,
key=None, k=None, right_click_menu=None, expand_x=False, expand_y=False, visible=True, metadata=None):
"""
:param size: (w, h) w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int, int) | (None, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param echo_stdout_stderr: If True then output to stdout will be output to this element AND also to the normal console location
:type echo_stdout_stderr: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self._TKOut = self.Widget = None # type: TKOutput
bg = background_color if background_color else DEFAULT_INPUT_ELEMENTS_COLOR
fg = text_color if text_color is not None else DEFAULT_INPUT_TEXT_COLOR
self.RightClickMenu = right_click_menu
key = key if key is not None else k
self.echo_stdout_stderr = echo_stdout_stderr
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(ELEM_TYPE_OUTPUT, size=sz, background_color=bg, text_color=fg, pad=pad, font=font,
tooltip=tooltip, key=key, visible=visible, metadata=metadata)
@property
def tk_out(self):
"""
Returns the TKOutput object used to create the element
:return: The TKOutput object
:rtype: (TKOutput)
"""
if self._TKOut is None:
print('*** Did you forget to call Finalize()? Your code should look something like: ***')
print('*** form = sg.Window("My Form").Layout(layout).Finalize() ***')
return self._TKOut
def update(self, value=None, visible=None):
"""
Changes some of the settings for the Output Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: string that will replace current contents of the output area
:type value: (str)
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if value is not None:
self._TKOut.output.delete('1.0', tk.END)
self._TKOut.output.insert(tk.END, value)
if visible is False:
self._TKOut.frame.pack_forget()
elif visible is True:
self._TKOut.frame.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if visible is not None:
self._visible = visible
def get(self):
"""
Returns the current contents of the output. Similar to Get method other Elements
:return: the current value of the output
:rtype: (str)
"""
return self._TKOut.output.get(1.0, tk.END)
def expand(self, expand_x=False, expand_y=False, expand_row=True):
"""
Causes the Element to expand to fill available space in the X and Y directions. Can specify which or both directions
:param expand_x: If True Element will expand in the Horizontal directions
:type expand_x: (bool)
:param expand_y: If True Element will expand in the Vertical directions
:type expand_y: (bool)
"""
if expand_x and expand_y:
fill = tk.BOTH
elif expand_x:
fill = tk.X
elif expand_y:
fill = tk.Y
else:
return
self._TKOut.output.pack(expand=True, fill=fill)
self._TKOut.frame.pack(expand=True, fill=fill)
self.ParentRowFrame.pack(expand=expand_row, fill=fill)
def __del__(self):
"""
Delete this element. Normally Elements do not have their delete method specified, but for this one
it's important that the underlying TKOut object get deleted so that the stdout will get restored properly
"""
self._TKOut.__del__()
TKOut = tk_out
Update = update
Get = get
# ---------------------------------------------------------------------- #
# Button Class #
# ---------------------------------------------------------------------- #
class Button(Element):
"""
Button Element - Defines all possible buttons. The shortcuts such as Submit, FileBrowse, ... each create a Button
"""
def __init__(self, button_text='', button_type=BUTTON_TYPE_READ_FORM, target=(None, None), tooltip=None,
file_types=FILE_TYPES_ALL_FILES, initial_folder=None, default_extension='', disabled=False, change_submits=False,
enable_events=False, image_filename=None, image_data=None, image_size=(None, None),
image_subsample=None, border_width=None, size=(None, None), s=(None, None), auto_size_button=None, button_color=None,
disabled_button_color=None,
highlight_colors=None, mouseover_colors=(None, None), use_ttk_buttons=None, font=None, bind_return_key=False, focus=False, pad=None, p=None, key=None,
k=None, right_click_menu=None, expand_x=False, expand_y=False, visible=True, metadata=None):
"""
:param button_text: Text to be displayed on the button
:type button_text: (str)
:param button_type: You should NOT be setting this directly. ONLY the shortcut functions set this
:type button_type: (int)
:param target: key or (row,col) target for the button. Note that -1 for column means 1 element to the left of this one. The constant ThisRow is used to indicate the current row. The Button itself is a valid target for some types of button
:type target: str | (int, int)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param file_types: the filetypes that will be used to match files. To indicate all files: (("ALL Files", "*.* *"),). Note - NOT SUPPORTED ON MAC
:type file_types: Tuple[(str, str), ...]
:param initial_folder: starting path for folders and files
:type initial_folder: (str)
:param default_extension: If no extension entered by user, add this to filename (only used in saveas dialogs)
:type default_extension: (str)
:param disabled: If True button will be created disabled. If BUTTON_DISABLED_MEANS_IGNORE then the button will be ignored rather than disabled using tkinter
:type disabled: (bool | str)
:param change_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type change_submits: (bool)
:param enable_events: Turns on the element specific events. If this button is a target, should it generate an event when filled in
:type enable_events: (bool)
:param image_filename: image filename if there is a button image. GIFs and PNGs only.
:type image_filename: (str)
:param image_data: Raw or Base64 representation of the image to put on button. Choose either filename or data
:type image_data: bytes | str
:param image_size: Size of the image in pixels (width, height)
:type image_size: (int, int)
:param image_subsample: amount to reduce the size of the image. Divides the size by this number. 2=1/2, 3=1/3, 4=1/4, etc
:type image_subsample: (int)
:param border_width: width of border around button in pixels
:type border_width: (int)
:param size: (w, h) w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int | None, int | None) | (None, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int | None, int | None) | (None, None) | int
:param auto_size_button: if True the button size is sized to fit the text
:type auto_size_button: (bool)
:param button_color: Color of button. default is from theme or the window. Easy to remember which is which if you say "ON" between colors. "red" on "green". Normally a tuple, but can be a simplified-button-color-string "foreground on background". Can be a single color if want to set only the background.
:type button_color: (str, str) | str | (int, int) | None
:param disabled_button_color: colors to use when button is disabled (text, background). Use None for a color if don't want to change. Only ttk buttons support both text and background colors. tk buttons only support changing text color
:type disabled_button_color: (str, str) | str
:param highlight_colors: colors to use when button has focus (has focus, does not have focus). None will use colors based on theme. Only used by Linux and only for non-TTK button
:type highlight_colors: (str, str)
:param mouseover_colors: Important difference between Linux & Windows! Linux - Colors when mouse moved over button. Windows - colors when button is pressed. The default is to switch the text and background colors (an inverse effect)
:type mouseover_colors: (str, str) | str
:param use_ttk_buttons: True = use ttk buttons. False = do not use ttk buttons. None (Default) = use ttk buttons only if on a Mac and not with button images
:type use_ttk_buttons: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param bind_return_key: If True the return key will cause this button to be pressed
:type bind_return_key: (bool)
:param focus: if True, initial focus will be put on this button
:type focus: (bool)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.AutoSizeButton = auto_size_button
self.BType = button_type
self.FileTypes = file_types
self.Widget = self.TKButton = None # type: tk.Button
self.Target = target
self.ButtonText = str(button_text)
self.RightClickMenu = right_click_menu
# Button colors can be a tuple (text, background) or a string with format "text on background"
# bc = button_color
# if button_color is None:
# bc = DEFAULT_BUTTON_COLOR
# else:
# try:
# if isinstance(button_color,str):
# bc = button_color.split(' on ')
# except Exception as e:
# print('* cprint warning * you messed up with color formatting', e)
# if bc[1] is None:
# bc = (bc[0], theme_button_color()[1])
# self.ButtonColor = bc
self.ButtonColor = button_color_to_tuple(button_color)
# experimental code to compute disabled button text color
# if disabled_button_color is None:
# try:
# disabled_button_color = (get_complimentary_hex(theme_button_color()[0]), theme_button_color()[1])
# # disabled_button_color = disabled_button_color
# except:
# print('* Problem computing disabled button color *')
self.DisabledButtonColor = button_color_to_tuple(disabled_button_color) if disabled_button_color is not None else (None, None)
self.ImageFilename = image_filename
self.ImageData = image_data
self.ImageSize = image_size
self.ImageSubsample = image_subsample
self.UserData = None
self.BorderWidth = border_width if border_width is not None else DEFAULT_BORDER_WIDTH
self.BindReturnKey = bind_return_key
self.Focus = focus
self.TKCal = None
self.calendar_default_date_M_D_Y = (None, None, None)
self.calendar_close_when_chosen = False
self.calendar_locale = None
self.calendar_format = None
self.calendar_location = (None, None)
self.calendar_no_titlebar = True
self.calendar_begin_at_sunday_plus = 0
self.calendar_month_names = None
self.calendar_day_abbreviations = None
self.calendar_title = ''
self.calendar_selection = ''
self.InitialFolder = initial_folder
self.DefaultExtension = default_extension
self.Disabled = disabled
self.ChangeSubmits = change_submits or enable_events
self.UseTtkButtons = use_ttk_buttons
self.ttk_style_name = '' # set in the packer function
self._files_delimiter = BROWSE_FILES_DELIMITER # used by the file browse button. used when multiple files are selected by user
if use_ttk_buttons is None and running_mac():
self.UseTtkButtons = True
# if image_filename or image_data:
# self.UseTtkButtons = False # if an image is to be displayed, then force the button to not be a TTK Button
if key is None and k is None:
_key = self.ButtonText
else:
_key = key if key is not None else k
if highlight_colors is not None:
self.HighlightColors = highlight_colors
else:
self.HighlightColors = self._compute_highlight_colors()
if mouseover_colors != (None, None):
self.MouseOverColors = button_color_to_tuple(mouseover_colors)
elif button_color != None:
self.MouseOverColors = (self.ButtonColor[1], self.ButtonColor[0])
else:
self.MouseOverColors = (theme_button_color()[1], theme_button_color()[0])
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
sz = size if size != (None, None) else s
super().__init__(ELEM_TYPE_BUTTON, size=sz, font=font, pad=pad, key=_key, tooltip=tooltip, visible=visible, metadata=metadata)
return
def _compute_highlight_colors(self):
"""
Determines the color to use to indicate the button has focus. This setting is only used by Linux.
:return: Pair of colors. (Highlight, Highlight Background)
:rtype: (str, str)
"""
highlight_color = highlight_background = COLOR_SYSTEM_DEFAULT
if self.ButtonColor != COLOR_SYSTEM_DEFAULT and theme_background_color() != COLOR_SYSTEM_DEFAULT:
highlight_background = theme_background_color()
if self.ButtonColor != COLOR_SYSTEM_DEFAULT and self.ButtonColor[0] != COLOR_SYSTEM_DEFAULT:
if self.ButtonColor[0] != theme_background_color():
highlight_color = self.ButtonColor[0]
else:
highlight_color = 'red'
return (highlight_color, highlight_background)
# Realtime button release callback
def ButtonReleaseCallBack(self, parm):
"""
Not a user callable function. Called by tkinter when a "realtime" button is released
:param parm: the event info from tkinter
:type parm:
"""
self.LastButtonClickedWasRealtime = False
self.ParentForm.LastButtonClicked = None
# Realtime button callback
def ButtonPressCallBack(self, parm):
"""
Not a user callable method. Callback called by tkinter when a "realtime" button is pressed
:param parm: Event info passed in by tkinter
:type parm:
"""
self.ParentForm.LastButtonClickedWasRealtime = True
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = self.ButtonText
# if self.ParentForm.CurrentlyRunningMainloop:
# Window._window_that_exited = self.ParentForm
# self.ParentForm.TKroot.quit() # kick out of loop if read was called
_exit_mainloop(self.ParentForm)
def _find_target(self):
target = self.Target
target_element = None
if target[0] == ThisRow:
target = [self.Position[0], target[1]]
if target[1] < 0:
target[1] = self.Position[1] + target[1]
strvar = None
should_submit_window = False
if target == (None, None):
strvar = self.TKStringVar
else:
if not isinstance(target, str):
if target[0] < 0:
target = [self.Position[0] + target[0], target[1]]
target_element = self.ParentContainer._GetElementAtLocation(target)
else:
target_element = self.ParentForm.find_element(target)
try:
strvar = target_element.TKStringVar
except:
pass
try:
if target_element.ChangeSubmits:
should_submit_window = True
except:
pass
return target_element, strvar, should_submit_window
# ------- Button Callback ------- #
def ButtonCallBack(self):
"""
Not user callable! Called by tkinter when a button is clicked. This is where all the fun begins!
"""
# global _my_windows
# print('Button callback')
# Button Callback - Parent = {self.ParentForm} Position = {self.Position}')
# Buttons modify targets or return from the form
# If modifying target, get the element object at the target and modify its StrVar
# target = self.Target
# target_element = None
# if target[0] == ThisRow:
# target = [self.Position[0], target[1]]
# if target[1] < 0:
# target[1] = self.Position[1] + target[1]
# strvar = None
# should_submit_window = False
# if target == (None, None):
# strvar = self.TKStringVar
# else:
# if not isinstance(target, str):
# if target[0] < 0:
# target = [self.Position[0] + target[0], target[1]]
# target_element = self.ParentContainer._GetElementAtLocation(target)
# else:
# target_element = self.ParentForm.find_element(target)
# try:
# strvar = target_element.TKStringVar
# except:
# pass
# try:
# if target_element.ChangeSubmits:
# should_submit_window = True
# except:
# pass
if self.Disabled == BUTTON_DISABLED_MEANS_IGNORE:
return
target_element, strvar, should_submit_window = self._find_target()
filetypes = FILE_TYPES_ALL_FILES if self.FileTypes is None else self.FileTypes
if self.BType == BUTTON_TYPE_BROWSE_FOLDER:
if running_mac(): # macs don't like seeing the parent window (go firgure)
folder_name = tk.filedialog.askdirectory(initialdir=self.InitialFolder) # show the 'get folder' dialog box
else:
folder_name = tk.filedialog.askdirectory(initialdir=self.InitialFolder, parent=self.ParentForm.TKroot) # show the 'get folder' dialog box
if folder_name:
try:
strvar.set(folder_name)
self.TKStringVar.set(folder_name)
except:
pass
elif self.BType == BUTTON_TYPE_BROWSE_FILE:
if running_mac():
file_name = tk.filedialog.askopenfilename(initialdir=self.InitialFolder) # show the 'get file' dialog box
else:
file_name = tk.filedialog.askopenfilename(filetypes=filetypes,
initialdir=self.InitialFolder, parent=self.ParentForm.TKroot) # show the 'get file' dialog box
if file_name:
strvar.set(file_name)
self.TKStringVar.set(file_name)
elif self.BType == BUTTON_TYPE_COLOR_CHOOSER:
color = tk.colorchooser.askcolor(parent=self.ParentForm.TKroot) # show the 'get file' dialog box
color = color[1] # save only the #RRGGBB portion
strvar.set(color)
self.TKStringVar.set(color)
elif self.BType == BUTTON_TYPE_BROWSE_FILES:
if running_mac():
file_name = tk.filedialog.askopenfilenames(initialdir=self.InitialFolder)
else:
file_name = tk.filedialog.askopenfilenames(filetypes=filetypes, initialdir=self.InitialFolder, parent=self.ParentForm.TKroot)
if file_name:
file_name = self._files_delimiter.join(file_name) # normally a ';'
strvar.set(file_name)
self.TKStringVar.set(file_name)
elif self.BType == BUTTON_TYPE_SAVEAS_FILE:
# show the 'get file' dialog box
if running_mac():
file_name = tk.filedialog.asksaveasfilename(defaultextension=self.DefaultExtension, initialdir=self.InitialFolder)
else:
file_name = tk.filedialog.asksaveasfilename(filetypes=filetypes, defaultextension=self.DefaultExtension, initialdir=self.InitialFolder,
parent=self.ParentForm.TKroot)
if file_name:
strvar.set(file_name)
self.TKStringVar.set(file_name)
elif self.BType == BUTTON_TYPE_CLOSES_WIN: # this is a return type button so GET RESULTS and destroy window
# first, get the results table built
# modify the Results table in the parent FlexForm object
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = self.ButtonText
self.ParentForm.FormRemainedOpen = False
self.ParentForm._Close()
_exit_mainloop(self.ParentForm)
if self.ParentForm.NonBlocking:
self.ParentForm.TKroot.destroy()
Window._DecrementOpenCount()
elif self.BType == BUTTON_TYPE_READ_FORM: # LEAVE THE WINDOW OPEN!! DO NOT CLOSE
# This is a PLAIN BUTTON
# first, get the results table built
# modify the Results table in the parent FlexForm object
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = self.ButtonText
self.ParentForm.FormRemainedOpen = True
_exit_mainloop(self.ParentForm)
elif self.BType == BUTTON_TYPE_CLOSES_WIN_ONLY: # special kind of button that does not exit main loop
self.ParentForm._Close(without_event=True)
self.ParentForm.TKroot.destroy() # close the window with tkinter
Window._DecrementOpenCount()
elif self.BType == BUTTON_TYPE_CALENDAR_CHOOSER: # this is a return type button so GET RESULTS and destroy window
# ------------ new chooser code -------------
self.ParentForm.LastButtonClicked = self.Key # key should have been generated already if not set by user
self.ParentForm.FormRemainedOpen = True
should_submit_window = False
_exit_mainloop(self.ParentForm)
# elif self.BType == BUTTON_TYPE_SHOW_DEBUGGER:
# **** DEPRICATED *****
# if self.ParentForm.DebuggerEnabled:
# show_debugger_popout_window()
if should_submit_window:
self.ParentForm.LastButtonClicked = target_element.Key
self.ParentForm.FormRemainedOpen = True
_exit_mainloop(self.ParentForm)
return
def update(self, text=None, button_color=(None, None), disabled=None, image_data=None, image_filename=None,
visible=None, image_subsample=None, disabled_button_color=(None, None), image_size=None):
"""
Changes some of the settings for the Button Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param text: sets button text
:type text: (str)
:param button_color: Color of button. default is from theme or the window. Easy to remember which is which if you say "ON" between colors. "red" on "green". Normally a tuple, but can be a simplified-button-color-string "foreground on background". Can be a single color if want to set only the background.
:type button_color: (str, str) | str | (int, int) | None
:param disabled: True/False to enable/disable at the GUI level. Use BUTTON_DISABLED_MEANS_IGNORE to ignore clicks (won't change colors)
:type disabled: (bool | str)
:param image_data: Raw or Base64 representation of the image to put on button. Choose either filename or data
:type image_data: bytes | str
:param image_filename: image filename if there is a button image. GIFs and PNGs only.
:type image_filename: (str)
:param disabled_button_color: colors to use when button is disabled (text, background). Use None for a color if don't want to change. Only ttk buttons support both text and background colors. tk buttons only support changing text color
:type disabled_button_color: (str, str)
:param visible: control visibility of element
:type visible: (bool)
:param image_subsample: amount to reduce the size of the image. Divides the size by this number. 2=1/2, 3=1/3, 4=1/4, etc
:type image_subsample: (int)
:param image_size: Size of the image in pixels (width, height)
:type image_size: (int, int)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self.UseTtkButtons:
style_name = self.ttk_style_name # created when made initial window (in the pack)
# style_name = str(self.Key) + 'custombutton.TButton'
button_style = ttk.Style()
if text is not None:
self.TKButton.configure(text=text)
self.ButtonText = text
if button_color != (None, None) and button_color != COLOR_SYSTEM_DEFAULT:
bc = button_color_to_tuple(button_color, self.ButtonColor)
# if isinstance(button_color, str):
# try:
# button_color = button_color.split(' on ')
# except Exception as e:
# print('** Error in formatting your button color **', button_color, e)
if self.UseTtkButtons:
if bc[0] not in (None, COLOR_SYSTEM_DEFAULT):
button_style.configure(style_name, foreground=bc[0])
if bc[1] not in (None, COLOR_SYSTEM_DEFAULT):
button_style.configure(style_name, background=bc[1])
else:
if bc[0] not in (None, COLOR_SYSTEM_DEFAULT):
self.TKButton.config(foreground=bc[0], activeforeground=bc[0])
if bc[1] not in (None, COLOR_SYSTEM_DEFAULT):
self.TKButton.config(background=bc[1], activebackground=bc[1])
self.ButtonColor = bc
if disabled is True:
self.TKButton['state'] = 'disabled'
self.Disabled = True
elif disabled is False:
self.TKButton['state'] = 'normal'
self.Disabled = False
elif disabled == BUTTON_DISABLED_MEANS_IGNORE:
self.TKButton['state'] = 'normal'
self.Disabled = BUTTON_DISABLED_MEANS_IGNORE
if image_data is not None:
image = tk.PhotoImage(data=image_data)
if image_subsample:
image = image.subsample(image_subsample)
if image_size is not None:
width, height = image_size
else:
width, height = image.width(), image.height()
if self.UseTtkButtons:
button_style.configure(style_name, image=image, width=width, height=height)
else:
self.TKButton.config(image=image, width=width, height=height)
self.TKButton.image = image
if image_filename is not None:
image = tk.PhotoImage(file=image_filename)
if image_subsample:
image = image.subsample(image_subsample)
if image_size is not None:
width, height = image_size
else:
width, height = image.width(), image.height()
if self.UseTtkButtons:
button_style.configure(style_name, image=image, width=width, height=height)
else:
self.TKButton.config(highlightthickness=0, image=image, width=width, height=height)
self.TKButton.image = image
if visible is False:
self.TKButton.pack_forget()
elif visible is True:
self.TKButton.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if disabled_button_color != (None, None) and disabled_button_color != COLOR_SYSTEM_DEFAULT:
if not self.UseTtkButtons:
self.TKButton['disabledforeground'] = disabled_button_color[0]
else:
if disabled_button_color[0] is not None:
button_style.map(style_name, foreground=[('disabled', disabled_button_color[0])])
if disabled_button_color[1] is not None:
button_style.map(style_name, background=[('disabled', disabled_button_color[1])])
self.DisabledButtonColor = (disabled_button_color[0] if disabled_button_color[0] is not None else self.DisabledButtonColor[0],
disabled_button_color[1] if disabled_button_color[1] is not None else self.DisabledButtonColor[1])
if visible is not None:
self._visible = visible
def get_text(self):
"""
Returns the current text shown on a button
:return: The text currently displayed on the button
:rtype: (str)
"""
return self.ButtonText
def click(self):
"""
Generates a click of the button as if the user clicked the button
Calls the tkinter invoke method for the button
"""
try:
self.TKButton.invoke()
except:
print('Exception clicking button')
Click = click
GetText = get_text
Update = update
# ------------------------- Button lazy functions ------------------------- #
B = Button
Btn = Button
# ---------------------------------------------------------------------- #
# ButtonMenu Class #
# ---------------------------------------------------------------------- #
class ButtonMenu(Element):
"""
The Button Menu Element. Creates a button that when clicked will show a menu similar to right click menu
"""
def __init__(self, button_text, menu_def, tooltip=None, disabled=False, image_source=None,
image_filename=None, image_data=None, image_size=(None, None), image_subsample=None, border_width=None,
size=(None, None), s=(None, None), auto_size_button=None, button_color=None, text_color=None, background_color=None, disabled_text_color=None,
font=None, item_font=None, pad=None, p=None, expand_x=False, expand_y=False, key=None, k=None, tearoff=False, visible=True, metadata=None):
"""
:param button_text: Text to be displayed on the button
:type button_text: (str)
:param menu_def: A list of lists of Menu items to show when this element is clicked. See docs for format as they are the same for all menu types
:type menu_def: List[List[str]]
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param disabled: If True button will be created disabled
:type disabled: (bool)
:param image_source: Image to place on button. Use INSTEAD of the image_filename and image_data. Unifies these into 1 easier to use parm
:type image_source: (str | bytes)
:param image_filename: image filename if there is a button image. GIFs and PNGs only.
:type image_filename: (str)
:param image_data: Raw or Base64 representation of the image to put on button. Choose either filename or data
:type image_data: bytes | str
:param image_size: Size of the image in pixels (width, height)
:type image_size: (int, int)
:param image_subsample: amount to reduce the size of the image. Divides the size by this number. 2=1/2, 3=1/3, 4=1/4, etc
:type image_subsample: (int)
:param border_width: width of border around button in pixels
:type border_width: (int)
:param size: (w, h) w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int, int) | (None, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: if True the button size is sized to fit the text
:type auto_size_button: (bool)
:param button_color: of button. Easy to remember which is which if you say "ON" between colors. "red" on "green"
:type button_color: (str, str) or str
:param background_color: color of the background
:type background_color: (str)
:param text_color: element's text color. Can be in #RRGGBB format or a color name "black"
:type text_color: (str)
:param disabled_text_color: color to use for text when item is disabled. Can be in #RRGGBB format or a color name "black"
:type disabled_text_color: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param item_font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike, for the menu items
:type item_font: (str or (str, int[, str]) or None)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param tearoff: Determines if menus should allow them to be torn off
:type tearoff: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.MenuDefinition = copy.deepcopy(menu_def)
self.AutoSizeButton = auto_size_button
self.ButtonText = button_text
self.ButtonColor = button_color_to_tuple(button_color)
# self.TextColor = self.ButtonColor[0]
# self.BackgroundColor = self.ButtonColor[1]
self.BackgroundColor = background_color if background_color is not None else theme_input_background_color()
self.TextColor = text_color if text_color is not None else theme_input_text_color()
self.DisabledTextColor = disabled_text_color if disabled_text_color is not None else COLOR_SYSTEM_DEFAULT
self.ItemFont = item_font
self.BorderWidth = border_width if border_width is not None else DEFAULT_BORDER_WIDTH
if image_source is not None:
if isinstance(image_source, str):
image_filename = image_source
elif isinstance(image_source, bytes):
image_data = image_source
else:
warnings.warn('ButtonMenu element - image_source is not a valid type: {}'.format(type(image_source)), UserWarning)
self.ImageFilename = image_filename
self.ImageData = image_data
self.ImageSize = image_size
self.ImageSubsample = image_subsample
self.Disabled = disabled
self.IsButtonMenu = True
self.MenuItemChosen = None
self.Widget = self.TKButtonMenu = None # type: tk.Menubutton
self.TKMenu = None # type: tk.Menu
self.part_of_custom_menubar = False
self.custom_menubar_key = None
# self.temp_size = size if size != (NONE, NONE) else
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(ELEM_TYPE_BUTTONMENU, size=sz, font=font, pad=pad, key=key, tooltip=tooltip,
text_color=self.TextColor, background_color=self.BackgroundColor, visible=visible, metadata=metadata)
self.Tearoff = tearoff
def _MenuItemChosenCallback(self, item_chosen): # ButtonMenu Menu Item Chosen Callback
"""
Not a user callable function. Called by tkinter when an item is chosen from the menu.
:param item_chosen: The menu item chosen.
:type item_chosen: (str)
"""
# print('IN MENU ITEM CALLBACK', item_chosen)
self.MenuItemChosen = item_chosen
self.ParentForm.LastButtonClicked = self.Key
self.ParentForm.FormRemainedOpen = True
# if self.ParentForm.CurrentlyRunningMainloop:
# self.ParentForm.TKroot.quit() # kick the users out of the mainloop
_exit_mainloop(self.ParentForm)
def update(self, menu_definition=None, visible=None, image_source=None, image_size=(None, None), image_subsample=None):
"""
Changes some of the settings for the ButtonMenu Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param menu_definition: (New menu definition (in menu definition format)
:type menu_definition: List[List]
:param visible: control visibility of element
:type visible: (bool)
:param image_source: new image if image is to be changed. Can be a filename or a base64 encoded byte-string
:type image_source: (str | bytes)
:param image_size: Size of the image in pixels (width, height)
:type image_size: (int, int)
:param image_subsample: amount to reduce the size of the image. Divides the size by this number. 2=1/2, 3=1/3, 4=1/4, etc
:type image_subsample: (int)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if menu_definition is not None:
self.MenuDefinition = copy.deepcopy(menu_definition)
top_menu = self.TKMenu = tk.Menu(self.TKButtonMenu, tearoff=self.Tearoff, font=self.ItemFont, tearoffcommand=self._tearoff_menu_callback)
if self.BackgroundColor not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(bg=self.BackgroundColor)
if self.TextColor not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(fg=self.TextColor)
if self.DisabledTextColor not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(disabledforeground=self.DisabledTextColor)
if self.ItemFont is not None:
top_menu.config(font=self.ItemFont)
AddMenuItem(self.TKMenu, self.MenuDefinition[1], self)
self.TKButtonMenu.configure(menu=self.TKMenu)
if image_source is not None:
filename = data = None
if image_source is not None:
if isinstance(image_source, bytes):
data = image_source
elif isinstance(image_source, str):
filename = image_source
else:
warnings.warn('ButtonMenu element - image_source is not a valid type: {}'.format(type(image_source)), UserWarning)
image = None
if filename is not None:
image = tk.PhotoImage(file=filename)
if image_subsample is not None:
image = image.subsample(image_subsample)
elif data is not None:
# if type(data) is bytes:
try:
image = tk.PhotoImage(data=data)
if image_subsample is not None:
image = image.subsample(image_subsample)
except Exception as e:
image = data
if image is not None:
if type(image) is not bytes:
width, height = image_size[0] if image_size[0] is not None else image.width(), image_size[1] if image_size[1] is not None else image.height()
else:
width, height = image_size
self.TKButtonMenu.config(image=image, compound=tk.CENTER, width=width, height=height)
self.TKButtonMenu.image = image
if visible is False:
self.TKButtonMenu.pack_forget()
elif visible is True:
self.TKButtonMenu.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if visible is not None:
self._visible = visible
def Click(self):
"""
Generates a click of the button as if the user clicked the button
Calls the tkinter invoke method for the button
"""
try:
self.TKMenu.invoke(1)
except:
print('Exception clicking button')
Update = update
BMenu = ButtonMenu
BM = ButtonMenu
# ---------------------------------------------------------------------- #
# ProgreessBar #
# ---------------------------------------------------------------------- #
class ProgressBar(Element):
"""
Progress Bar Element - Displays a colored bar that is shaded as progress of some operation is made
"""
def __init__(self, max_value, orientation=None, size=(None, None), s=(None, None), size_px=(None, None), auto_size_text=None, bar_color=None, style=None, border_width=None,
relief=None, key=None, k=None, pad=None, p=None, right_click_menu=None, expand_x=False, expand_y=False, visible=True, metadata=None):
"""
:param max_value: max value of progressbar
:type max_value: (int)
:param orientation: 'horizontal' or 'vertical'
:type orientation: (str)
:param size: Size of the bar. If horizontal (chars long, pixels wide), vert (chars high, pixels wide). Vert height measured using horizontal chars units.
:type size: (int, int) | (int, None)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None)
:param size_px: Size in pixels (length, width). Will be used in place of size parm if specified
:type size_px: (int, int) | (None, None)
:param auto_size_text: Not sure why this is here
:type auto_size_text: (bool)
:param bar_color: The 2 colors that make up a progress bar. Easy to remember which is which if you say "ON" between colors. "red" on "green".
:type bar_color: (str, str) or str
:param style: Progress bar style defined as one of these 'default', 'winnative', 'clam', 'alt', 'classic', 'vista', 'xpnative'
:type style: (str)
:param border_width: The amount of pixels that go around the outside of the bar
:type border_width: (int)
:param relief: relief style. Values are same as progress meter relief values. Can be a constant or a string: `RELIEF_RAISED RELIEF_SUNKEN RELIEF_FLAT RELIEF_RIDGE RELIEF_GROOVE RELIEF_SOLID` (Default value = DEFAULT_PROGRESS_BAR_RELIEF)
:type relief: (str)
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.MaxValue = max_value
self.TKProgressBar = None # type: TKProgressBar
self.Cancelled = False
self.NotRunning = True
self.Orientation = orientation if orientation else DEFAULT_METER_ORIENTATION
self.RightClickMenu = right_click_menu
self.ttk_style_name = None # set in the pack function so can use in the update
# Progress Bar colors can be a tuple (text, background) or a string with format "bar on background" - examples "red on white" or ("red", "white")
if bar_color is None:
bar_color = DEFAULT_PROGRESS_BAR_COLOR
else:
bar_color = _simplified_dual_color_to_tuple(bar_color, default=DEFAULT_PROGRESS_BAR_COLOR)
self.BarColor = bar_color # should be a tuple at this point
self.BarStyle = style if style else DEFAULT_TTK_THEME
self.BorderWidth = border_width if border_width else DEFAULT_PROGRESS_BAR_BORDER_WIDTH
self.Relief = relief if relief else DEFAULT_PROGRESS_BAR_RELIEF
self.BarExpired = False
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
self.size_px = size_px
super().__init__(ELEM_TYPE_PROGRESS_BAR, size=sz, auto_size_text=auto_size_text, key=key, pad=pad,
visible=visible, metadata=metadata)
# returns False if update failed
def update_bar(self, current_count, max=None):
"""
DEPRECATED BUT STILL USABLE - has been combined with the normal ProgressBar.update method.
Change what the bar shows by changing the current count and optionally the max count
:param current_count: sets the current value
:type current_count: (int)
:param max: changes the max value
:type max: (int)
"""
if self.ParentForm.TKrootDestroyed:
return False
self.TKProgressBar.Update(current_count, max=max)
try:
self.ParentForm.TKroot.update()
except:
Window._DecrementOpenCount()
# _my_windows.Decrement()
return False
return True
def update(self, current_count=None, max=None, bar_color=None, visible=None):
"""
Changes some of the settings for the ProgressBar Element. Must call `Window.Read` or `Window.Finalize` prior
Now has the ability to modify the count so that the update_bar method is not longer needed separately
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param current_count: sets the current value
:type current_count: (int)
:param max: changes the max value
:type max: (int)
:param bar_color: The 2 colors that make up a progress bar. Easy to remember which is which if you say "ON" between colors. "red" on "green".
:type bar_color: (str, str) or str
:param visible: control visibility of element
:type visible: (bool)
:return: Returns True if update was OK. False means something wrong with window or it was closed
:rtype: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return False
if self.ParentForm.TKrootDestroyed:
return False
if visible is False:
self.TKProgressBar.TKProgressBarForReal.pack_forget()
elif visible is True:
self.TKProgressBar.TKProgressBarForReal.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if visible is not None:
self._visible = visible
if bar_color is not None:
bar_color = _simplified_dual_color_to_tuple(bar_color, default=DEFAULT_PROGRESS_BAR_COLOR)
self.BarColor = bar_color
style = ttk.Style()
style.configure(self.ttk_style_name, background=bar_color[0], troughcolor=bar_color[1])
if current_count is not None:
self.TKProgressBar.Update(current_count, max=max)
try:
self.ParentForm.TKroot.update()
except:
# Window._DecrementOpenCount()
# _my_windows.Decrement()
return False
return True
Update = update
UpdateBar = update_bar
PBar = ProgressBar
Prog = ProgressBar
Progress = ProgressBar
# ---------------------------------------------------------------------- #
# Image #
# ---------------------------------------------------------------------- #
class Image(Element):
"""
Image Element - show an image in the window. Should be a GIF or a PNG only
"""
def __init__(self, source=None, filename=None, data=None, background_color=None, size=(None, None), s=(None, None), pad=None, p=None, key=None, k=None, tooltip=None, subsample=None, right_click_menu=None, expand_x=False, expand_y=False, visible=True, enable_events=False, metadata=None):
"""
:param source: A filename or a base64 bytes. Will automatically detect the type and fill in filename or data for you.
:type source: str | bytes | None
:param filename: image filename if there is a button image. GIFs and PNGs only.
:type filename: str | None
:param data: Raw or Base64 representation of the image to put on button. Choose either filename or data
:type data: bytes | str | None
:param background_color: color of background
:type background_color:
:param size: (width, height) size of image in pixels
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param subsample: amount to reduce the size of the image. Divides the size by this number. 2=1/2, 3=1/3, 4=1/4, etc
:type subsample: (int)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param enable_events: Turns on the element specific events. For an Image element, the event is "image clicked"
:type enable_events: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
if source is not None:
if isinstance(source, bytes):
data = source
elif isinstance(source, str):
filename = source
else:
warnings.warn('Image element - source is not a valid type: {}'.format(type(source)), UserWarning)
self.Filename = filename
self.Data = data
self.Widget = self.tktext_label = None # type: tk.Label
self.BackgroundColor = background_color
if data is None and filename is None:
self.Filename = ''
self.EnableEvents = enable_events
self.RightClickMenu = right_click_menu
self.AnimatedFrames = None
self.CurrentFrameNumber = 0
self.TotalAnimatedFrames = 0
self.LastFrameTime = 0
self.ImageSubsample = subsample
self.Source = filename if filename is not None else data
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(ELEM_TYPE_IMAGE, size=sz, background_color=background_color, pad=pad, key=key,
tooltip=tooltip, visible=visible, metadata=metadata)
return
def update(self, source=None, filename=None, data=None, size=(None, None), subsample=None, visible=None):
"""
Changes some of the settings for the Image Element. Must call `Window.Read` or `Window.Finalize` prior.
To clear an image that's been displayed, call with NONE of the options set. A blank update call will
delete the previously shown image.
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param source: A filename or a base64 bytes. Will automatically detect the type and fill in filename or data for you.
:type source: str | bytes | None
:param filename: filename to the new image to display.
:type filename: (str)
:param data: Base64 encoded string OR a tk.PhotoImage object
:type data: str | tkPhotoImage
:param size: (width, height) size of image in pixels
:type size: Tuple[int,int]
:param subsample: amount to reduce the size of the image. Divides the size by this number. 2=1/2, 3=1/3, 4=1/4, etc
:type subsample: (int)
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if source is not None:
if isinstance(source, bytes):
data = source
elif isinstance(source, str):
filename = source
else:
warnings.warn('Image element - source is not a valid type: {}'.format(type(source)), UserWarning)
image = None
if filename is not None:
try:
image = tk.PhotoImage(file=filename)
if subsample is not None:
image = image.subsample(subsample)
except Exception as e:
_error_popup_with_traceback('Exception updating Image element', e)
elif data is not None:
# if type(data) is bytes:
try:
image = tk.PhotoImage(data=data)
if subsample is not None:
image = image.subsample(subsample)
except Exception as e:
image = data
# return # an error likely means the window has closed so exit
if image is not None:
if type(image) is not bytes:
width, height = size[0] if size[0] is not None else image.width(), size[1] if size[1] is not None else image.height()
else:
width, height = size
try: # sometimes crashes if user closed with X
self.tktext_label.configure(image=image, width=width, height=height)
except Exception as e:
_error_popup_with_traceback('Exception updating Image element', e)
self.tktext_label.image = image
if visible is False:
self.tktext_label.pack_forget()
elif visible is True:
self.tktext_label.pack(padx=self.pad_used[0], pady=self.pad_used[1])
# if everything is set to None, then delete the image
if filename is None and image is None and visible is None and size == (None, None):
# Using a try because the image may have been previously deleted and don't want an error if that's happened
try:
self.tktext_label.configure(width=1, height=1, bd=0)
self.tktext_label.image = None
except:
pass
if visible is not None:
self._visible = visible
def update_animation(self, source, time_between_frames=0):
"""
Show an Animated GIF. Call the function as often as you like. The function will determine when to show the next frame and will automatically advance to the next frame at the right time.
NOTE - does NOT perform a sleep call to delay
:param source: Filename or Base64 encoded string containing Animated GIF
:type source: str | bytes | None
:param time_between_frames: Number of milliseconds to wait between showing frames
:type time_between_frames: (int)
"""
if self.Source != source:
self.AnimatedFrames = None
self.Source = source
if self.AnimatedFrames is None:
self.TotalAnimatedFrames = 0
self.AnimatedFrames = []
# Load up to 1000 frames of animation. stops when a bad frame is returns by tkinter
for i in range(1000):
if type(source) is not bytes:
try:
self.AnimatedFrames.append(tk.PhotoImage(file=source, format='gif -index %i' % (i)))
except Exception as e:
break
else:
try:
self.AnimatedFrames.append(tk.PhotoImage(data=source, format='gif -index %i' % (i)))
except Exception as e:
break
self.TotalAnimatedFrames = len(self.AnimatedFrames)
self.LastFrameTime = time.time()
self.CurrentFrameNumber = -1 # start at -1 because it is incremented before every frame is shown
# show the frame
now = time.time()
if time_between_frames:
if (now - self.LastFrameTime) * 1000 > time_between_frames:
self.LastFrameTime = now
self.CurrentFrameNumber = (self.CurrentFrameNumber + 1) % self.TotalAnimatedFrames
else: # don't reshow the frame again if not time for new frame
return
else:
self.CurrentFrameNumber = (self.CurrentFrameNumber + 1) % self.TotalAnimatedFrames
image = self.AnimatedFrames[self.CurrentFrameNumber]
try: # needed in case the window was closed with an "X"
self.tktext_label.configure(image=image, width=image.width(), heigh=image.height())
except Exception as e:
print('Exception in update_animation', e)
def update_animation_no_buffering(self, source, time_between_frames=0):
"""
Show an Animated GIF. Call the function as often as you like. The function will determine when to show the next frame and will automatically advance to the next frame at the right time.
NOTE - does NOT perform a sleep call to delay
:param source: Filename or Base64 encoded string containing Animated GIF
:type source: str | bytes
:param time_between_frames: Number of milliseconds to wait between showing frames
:type time_between_frames: (int)
"""
if self.Source != source:
self.AnimatedFrames = None
self.Source = source
self.frame_num = 0
now = time.time()
if time_between_frames:
if (now - self.LastFrameTime) * 1000 > time_between_frames:
self.LastFrameTime = now
else: # don't reshow the frame again if not time for new frame
return
# read a frame
while True:
if type(source) is not bytes:
try:
self.image = tk.PhotoImage(file=source, format='gif -index %i' % (self.frame_num))
self.frame_num += 1
except:
self.frame_num = 0
else:
try:
self.image = tk.PhotoImage(data=source, format='gif -index %i' % (self.frame_num))
self.frame_num += 1
except:
self.frame_num = 0
if self.frame_num:
break
try: # needed in case the window was closed with an "X"
self.tktext_label.configure(image=self.image, width=self.image.width(), heigh=self.image.height())
except:
pass
Update = update
UpdateAnimation = update_animation
Im = Image
# ---------------------------------------------------------------------- #
# Canvas #
# ---------------------------------------------------------------------- #
class Canvas(Element):
def __init__(self, canvas=None, background_color=None, size=(None, None), s=(None, None), pad=None, p=None, key=None, k=None, tooltip=None,
right_click_menu=None, expand_x=False, expand_y=False, visible=True, border_width=0, metadata=None):
"""
:param canvas: Your own tk.Canvas if you already created it. Leave blank to create a Canvas
:type canvas: (tk.Canvas)
:param background_color: color of background
:type background_color: (str)
:param size: (width in char, height in rows) size in pixels to make canvas
:type size: (int,int) | (None, None)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param border_width: width of border around element in pixels. Not normally used with Canvas element
:type border_width: (int)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.BackgroundColor = background_color if background_color is not None else DEFAULT_BACKGROUND_COLOR
self._TKCanvas = canvas
self.RightClickMenu = right_click_menu
self.BorderWidth = border_width
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(ELEM_TYPE_CANVAS, background_color=background_color, size=sz, pad=pad, key=key,
tooltip=tooltip, visible=visible, metadata=metadata)
return
@property
def tk_canvas(self):
"""
Returns the underlying tkiner Canvas widget
:return: The tkinter canvas widget
:rtype: (tk.Canvas)
"""
if self._TKCanvas is None:
print('*** Did you forget to call Finalize()? Your code should look something like: ***')
print('*** window = sg.Window("My Form", layout, finalize=True) ***')
return self._TKCanvas
TKCanvas = tk_canvas
# ---------------------------------------------------------------------- #
# Graph #
# ---------------------------------------------------------------------- #
class Graph(Element):
"""
Creates an area for you to draw on. The MAGICAL property this Element has is that you interact
with the element using your own coordinate system. This is an important point!! YOU define where the location
is for (0,0). Want (0,0) to be in the middle of the graph like a math 4-quadrant graph? No problem! Set your
lower left corner to be (-100,-100) and your upper right to be (100,100) and you've got yourself a graph with
(0,0) at the center.
One of THE coolest of the Elements.
You can also use float values. To do so, be sure and set the float_values parameter.
Mouse click and drag events are possible and return the (x,y) coordinates of the mouse
Drawing primitives return an "id" that is referenced when you want to operation on that item (e.g. to erase it)
"""
def __init__(self, canvas_size, graph_bottom_left, graph_top_right, background_color=None, pad=None, p=None,
change_submits=False, drag_submits=False, enable_events=False, motion_events=False, key=None, k=None, tooltip=None,
right_click_menu=None, expand_x=False, expand_y=False, visible=True, float_values=False, border_width=0, metadata=None):
"""
:param canvas_size: size of the canvas area in pixels
:type canvas_size: (int, int)
:param graph_bottom_left: (x,y) The bottoms left corner of your coordinate system
:type graph_bottom_left: (int, int)
:param graph_top_right: (x,y) The top right corner of your coordinate system
:type graph_top_right: (int, int)
:param background_color: background color of the drawing area
:type background_color: (str)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param change_submits: * DEPRICATED DO NOT USE. Use `enable_events` instead
:type change_submits: (bool)
:param drag_submits: if True and Events are enabled for the Graph, will report Events any time the mouse moves while button down
:type drag_submits: (bool)
:param enable_events: If True then clicks on the Graph are immediately reported as an event. Use this instead of change_submits
:type enable_events: (bool)
:param motion_events: If True then if no button is down and the mouse is moved, an event is generated with key = graph key + '+MOVE' (if key is a string)
:type motion_events: (bool)
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element (Default = True)
:type visible: (bool)
:param float_values: If True x,y coordinates are returned as floats, not ints
:type float_values: (bool)
:param border_width: width of border around element in pixels. Not normally used for Graph Elements
:type border_width: (int)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.CanvasSize = canvas_size
self.BottomLeft = graph_bottom_left
self.TopRight = graph_top_right
# self._TKCanvas = None # type: tk.Canvas
self._TKCanvas2 = self.Widget = None # type: tk.Canvas
self.ChangeSubmits = change_submits or enable_events
self.DragSubmits = drag_submits
self.ClickPosition = (None, None)
self.MouseButtonDown = False
self.Images = {}
self.RightClickMenu = right_click_menu
self.FloatValues = float_values
self.BorderWidth = border_width
key = key if key is not None else k
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
self.motion_events = motion_events
super().__init__(ELEM_TYPE_GRAPH, background_color=background_color, size=canvas_size, pad=pad, key=key,
tooltip=tooltip, visible=visible, metadata=metadata)
return
def _convert_xy_to_canvas_xy(self, x_in, y_in):
"""
Not user callable. Used to convert user's coordinates into the ones used by tkinter
:param x_in: The x coordinate to convert
:type x_in: int | float
:param y_in: The y coordinate to convert
:type y_in: int | float
:return: (int, int) The converted canvas coordinates
:rtype: (int, int)
"""
if None in (x_in, y_in):
return None, None
try:
scale_x = (self.CanvasSize[0] - 0) / (self.TopRight[0] - self.BottomLeft[0])
scale_y = (0 - self.CanvasSize[1]) / (self.TopRight[1] - self.BottomLeft[1])
except:
scale_x = scale_y = 0
new_x = 0 + scale_x * (x_in - self.BottomLeft[0])
new_y = self.CanvasSize[1] + scale_y * (y_in - self.BottomLeft[1])
return new_x, new_y
def _convert_canvas_xy_to_xy(self, x_in, y_in):
"""
Not user callable. Used to convert tkinter Canvas coords into user's coordinates
:param x_in: The x coordinate in canvas coordinates
:type x_in: (int)
:param y_in: (int) The y coordinate in canvas coordinates
:type y_in:
:return: The converted USER coordinates
:rtype: (int, int) | Tuple[float, float]
"""
if None in (x_in, y_in):
return None, None
scale_x = (self.CanvasSize[0] - 0) / (self.TopRight[0] - self.BottomLeft[0])
scale_y = (0 - self.CanvasSize[1]) / (self.TopRight[1] - self.BottomLeft[1])
new_x = x_in / scale_x + self.BottomLeft[0]
new_y = (y_in - self.CanvasSize[1]) / scale_y + self.BottomLeft[1]
if self.FloatValues:
return new_x, new_y
else:
return floor(new_x), floor(new_y)
def draw_line(self, point_from, point_to, color='black', width=1):
"""
Draws a line from one point to another point using USER'S coordinates. Can set the color and width of line
:param point_from: Starting point for line
:type point_from: (int, int) | Tuple[float, float]
:param point_to: Ending point for line
:type point_to: (int, int) | Tuple[float, float]
:param color: Color of the line
:type color: (str)
:param width: width of line in pixels
:type width: (int)
:return: id returned from tktiner or None if user closed the window. id is used when you
:rtype: int | None
"""
if point_from == (None, None):
return
converted_point_from = self._convert_xy_to_canvas_xy(point_from[0], point_from[1])
converted_point_to = self._convert_xy_to_canvas_xy(point_to[0], point_to[1])
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
try: # in case window was closed with an X
id = self._TKCanvas2.create_line(converted_point_from, converted_point_to, width=width, fill=color)
except:
id = None
return id
def draw_lines(self, points, color='black', width=1):
"""
Draw a series of lines given list of points
:param points: list of points that define the polygon
:type points: List[(int, int) | Tuple[float, float]]
:param color: Color of the line
:type color: (str)
:param width: width of line in pixels
:type width: (int)
:return: id returned from tktiner or None if user closed the window. id is used when you
:rtype: int | None
"""
converted_points = [self._convert_xy_to_canvas_xy(point[0], point[1]) for point in points]
try: # in case window was closed with an X
id = self._TKCanvas2.create_line(*converted_points, width=width, fill=color)
except:
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
id = None
return id
def draw_point(self, point, size=2, color='black'):
"""
Draws a "dot" at the point you specify using the USER'S coordinate system
:param point: Center location using USER'S coordinate system
:type point: (int, int) | Tuple[float, float]
:param size: Radius? (Or is it the diameter?) in user's coordinate values.
:type size: int | float
:param color: color of the point to draw
:type color: (str)
:return: id returned from tkinter that you'll need if you want to manipulate the point
:rtype: int | None
"""
if point == (None, None):
return
converted_point = self._convert_xy_to_canvas_xy(point[0], point[1])
size_converted = self._convert_xy_to_canvas_xy(point[0] + size, point[1])
size = size_converted[0] - converted_point[0]
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
try: # needed in case window was closed with an X
point1 = converted_point[0] - size // 2, converted_point[1] - size // 2
point2 = converted_point[0] + size // 2, converted_point[1] + size // 2
id = self._TKCanvas2.create_oval(point1[0], point1[1],
point2[0], point2[1],
width=0,
fill=color,
outline=color)
except:
id = None
return id
def draw_circle(self, center_location, radius, fill_color=None, line_color='black', line_width=1):
"""
Draws a circle, cenetered at the location provided. Can set the fill and outline colors
:param center_location: Center location using USER'S coordinate system
:type center_location: (int, int) | Tuple[float, float]
:param radius: Radius in user's coordinate values.
:type radius: int | float
:param fill_color: color of the point to draw
:type fill_color: (str)
:param line_color: color of the outer line that goes around the circle (sorry, can't set thickness)
:type line_color: (str)
:param line_width: width of the line around the circle, the outline, in pixels
:type line_width: (int)
:return: id returned from tkinter that you'll need if you want to manipulate the circle
:rtype: int | None
"""
if center_location == (None, None):
return
converted_point = self._convert_xy_to_canvas_xy(center_location[0], center_location[1])
radius_converted = self._convert_xy_to_canvas_xy(center_location[0] + radius, center_location[1])
radius = radius_converted[0] - converted_point[0]
# radius = radius_converted[1]-5
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
# print('Oval parms', int(converted_point[0]) - int(radius), int(converted_point[1]) - int(radius),
# int(converted_point[0]) + int(radius), int(converted_point[1]) + int(radius))
try: # needed in case the window was closed with an X
id = self._TKCanvas2.create_oval(int(converted_point[0]) - int(radius), int(converted_point[1]) - int(radius),
int(converted_point[0]) + int(radius), int(converted_point[1]) + int(radius), fill=fill_color,
outline=line_color, width=line_width)
except:
id = None
return id
def draw_oval(self, top_left, bottom_right, fill_color=None, line_color=None, line_width=1):
"""
Draws an oval based on coordinates in user coordinate system. Provide the location of a "bounding rectangle"
:param top_left: the top left point of bounding rectangle
:type top_left: (int, int) | Tuple[float, float]
:param bottom_right: the bottom right point of bounding rectangle
:type bottom_right: (int, int) | Tuple[float, float]
:param fill_color: color of the interrior
:type fill_color: (str)
:param line_color: color of outline of oval
:type line_color: (str)
:param line_width: width of the line around the oval, the outline, in pixels
:type line_width: (int)
:return: id returned from tkinter that you'll need if you want to manipulate the oval
:rtype: int | None
"""
converted_top_left = self._convert_xy_to_canvas_xy(top_left[0], top_left[1])
converted_bottom_right = self._convert_xy_to_canvas_xy(bottom_right[0], bottom_right[1])
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
try: # in case windows close with X
id = self._TKCanvas2.create_oval(converted_top_left[0], converted_top_left[1], converted_bottom_right[0],
converted_bottom_right[1], fill=fill_color, outline=line_color, width=line_width)
except:
id = None
return id
def draw_arc(self, top_left, bottom_right, extent, start_angle, style=None, arc_color='black', line_width=1, fill_color=None):
"""
Draws different types of arcs. Uses a "bounding box" to define location
:param top_left: the top left point of bounding rectangle
:type top_left: (int, int) | Tuple[float, float]
:param bottom_right: the bottom right point of bounding rectangle
:type bottom_right: (int, int) | Tuple[float, float]
:param extent: Andle to end drawing. Used in conjunction with start_angle
:type extent: (float)
:param start_angle: Angle to begin drawing. Used in conjunction with extent
:type start_angle: (float)
:param style: Valid choices are One of these Style strings- 'pieslice', 'chord', 'arc', 'first', 'last', 'butt', 'projecting', 'round', 'bevel', 'miter'
:type style: (str)
:param arc_color: color to draw arc with
:type arc_color: (str)
:param fill_color: color to fill the area
:type fill_color: (str)
:return: id returned from tkinter that you'll need if you want to manipulate the arc
:rtype: int | None
"""
converted_top_left = self._convert_xy_to_canvas_xy(top_left[0], top_left[1])
converted_bottom_right = self._convert_xy_to_canvas_xy(bottom_right[0], bottom_right[1])
tkstyle = tk.PIESLICE if style is None else style
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
try: # in case closed with X
id = self._TKCanvas2.create_arc(converted_top_left[0], converted_top_left[1], converted_bottom_right[0],
converted_bottom_right[1], extent=extent, start=start_angle, style=tkstyle,
outline=arc_color, width=line_width, fill=fill_color)
except Exception as e:
print('Error encountered drawing arc.', e)
id = None
return id
def draw_rectangle(self, top_left, bottom_right, fill_color=None, line_color=None, line_width=None):
"""
Draw a rectangle given 2 points. Can control the line and fill colors
:param top_left: the top left point of rectangle
:type top_left: (int, int) | Tuple[float, float]
:param bottom_right: the bottom right point of rectangle
:type bottom_right: (int, int) | Tuple[float, float]
:param fill_color: color of the interior
:type fill_color: (str)
:param line_color: color of outline
:type line_color: (str)
:param line_width: width of the line in pixels
:type line_width: (int)
:return: int | None id returned from tkinter that you'll need if you want to manipulate the rectangle
:rtype: int | None
"""
converted_top_left = self._convert_xy_to_canvas_xy(top_left[0], top_left[1])
converted_bottom_right = self._convert_xy_to_canvas_xy(bottom_right[0], bottom_right[1])
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
if line_width is None:
line_width = 1
try: # in case closed with X
id = self._TKCanvas2.create_rectangle(converted_top_left[0], converted_top_left[1],
converted_bottom_right[0],
converted_bottom_right[1], fill=fill_color, outline=line_color, width=line_width)
except:
id = None
return id
def draw_polygon(self, points, fill_color=None, line_color=None, line_width=None):
"""
Draw a polygon given list of points
:param points: list of points that define the polygon
:type points: List[(int, int) | Tuple[float, float]]
:param fill_color: color of the interior
:type fill_color: (str)
:param line_color: color of outline
:type line_color: (str)
:param line_width: width of the line in pixels
:type line_width: (int)
:return: id returned from tkinter that you'll need if you want to manipulate the rectangle
:rtype: int | None
"""
converted_points = [self._convert_xy_to_canvas_xy(point[0], point[1]) for point in points]
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
try: # in case closed with X
id = self._TKCanvas2.create_polygon(converted_points, fill=fill_color, outline=line_color, width=line_width)
except:
id = None
return id
def draw_text(self, text, location, color='black', font=None, angle=0, text_location=TEXT_LOCATION_CENTER):
"""
Draw some text on your graph. This is how you label graph number lines for example
:param text: text to display
:type text: (Any)
:param location: location to place first letter
:type location: (int, int) | Tuple[float, float]
:param color: text color
:type color: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param angle: Angle 0 to 360 to draw the text. Zero represents horizontal text
:type angle: (float)
:param text_location: "anchor" location for the text. Values start with TEXT_LOCATION_
:type text_location: (enum)
:return: id returned from tkinter that you'll need if you want to manipulate the text
:rtype: int | None
"""
text = str(text)
if location == (None, None):
return
converted_point = self._convert_xy_to_canvas_xy(location[0], location[1])
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
try: # in case closed with X
id = self._TKCanvas2.create_text(converted_point[0], converted_point[1], text=text, font=font, fill=color, angle=angle, anchor=text_location)
except:
id = None
return id
def draw_image(self, filename=None, data=None, location=(None, None)):
"""
Places an image onto your canvas. It's a really important method for this element as it enables so much
:param filename: if image is in a file, path and filename for the image. (GIF and PNG only!)
:type filename: (str)
:param data: if image is in Base64 format or raw? format then use instead of filename
:type data: str | bytes
:param location: the (x,y) location to place image's top left corner
:type location: (int, int) | Tuple[float, float]
:return: id returned from tkinter that you'll need if you want to manipulate the image
:rtype: int | None
"""
if location == (None, None):
return
if filename is not None:
image = tk.PhotoImage(file=filename)
elif data is not None:
# if type(data) is bytes:
try:
image = tk.PhotoImage(data=data)
except:
return None # an error likely means the window has closed so exit
converted_point = self._convert_xy_to_canvas_xy(location[0], location[1])
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
try: # in case closed with X
id = self._TKCanvas2.create_image(converted_point, image=image, anchor=tk.NW)
self.Images[id] = image
except:
id = None
return id
def erase(self):
"""
Erase the Graph - Removes all figures previously "drawn" using the Graph methods (e.g. DrawText)
"""
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
self.Images = {}
try: # in case window was closed with X
self._TKCanvas2.delete('all')
except:
pass
def delete_figure(self, id):
"""
Remove from the Graph the figure represented by id. The id is given to you anytime you call a drawing primitive
:param id: the id returned to you when calling one of the drawing methods
:type id: (int)
"""
try:
self._TKCanvas2.delete(id)
except:
print('DeleteFigure - bad ID {}'.format(id))
try:
del self.Images[id] # in case was an image. If wasn't an image, then will get exception
except:
pass
def update(self, background_color=None, visible=None):
"""
Changes some of the settings for the Graph Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param background_color: color of background
:type background_color: ???
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if background_color is not None and background_color != COLOR_SYSTEM_DEFAULT:
self._TKCanvas2.configure(background=background_color)
if visible is False:
self._TKCanvas2.pack_forget()
elif visible is True:
self._TKCanvas2.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if visible is not None:
self._visible = visible
def move(self, x_direction, y_direction):
"""
Moves the entire drawing area (the canvas) by some delta from the current position. Units are indicated in your coordinate system indicated number of ticks in your coordinate system
:param x_direction: how far to move in the "X" direction in your coordinates
:type x_direction: int | float
:param y_direction: how far to move in the "Y" direction in your coordinates
:type y_direction: int | float
"""
zero_converted = self._convert_xy_to_canvas_xy(0, 0)
shift_converted = self._convert_xy_to_canvas_xy(x_direction, y_direction)
shift_amount = (shift_converted[0] - zero_converted[0], shift_converted[1] - zero_converted[1])
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
self._TKCanvas2.move('all', shift_amount[0], shift_amount[1])
def move_figure(self, figure, x_direction, y_direction):
"""
Moves a previously drawn figure using a "delta" from current position
:param figure: Previously obtained figure-id. These are returned from all Draw methods
:type figure: (id)
:param x_direction: delta to apply to position in the X direction
:type x_direction: int | float
:param y_direction: delta to apply to position in the Y direction
:type y_direction: int | float
"""
zero_converted = self._convert_xy_to_canvas_xy(0, 0)
shift_converted = self._convert_xy_to_canvas_xy(x_direction, y_direction)
shift_amount = (shift_converted[0] - zero_converted[0], shift_converted[1] - zero_converted[1])
if figure is None:
print('* move_figure warning - your figure is None *')
return None
self._TKCanvas2.move(figure, shift_amount[0], shift_amount[1])
def relocate_figure(self, figure, x, y):
"""
Move a previously made figure to an arbitrary (x,y) location. This differs from the Move methods because it
uses absolute coordinates versus relative for Move
:param figure: Previously obtained figure-id. These are returned from all Draw methods
:type figure: (id)
:param x: location on X axis (in user coords) to move the upper left corner of the figure
:type x: int | float
:param y: location on Y axis (in user coords) to move the upper left corner of the figure
:type y: int | float
"""
zero_converted = self._convert_xy_to_canvas_xy(0, 0)
shift_converted = self._convert_xy_to_canvas_xy(x, y)
shift_amount = (shift_converted[0] - zero_converted[0], shift_converted[1] - zero_converted[1])
if figure is None:
print('*** WARNING - Your figure is None. It most likely means your did not Finalize your Window ***')
print('Call Window.Finalize() prior to all graph operations')
return None
xy = self._TKCanvas2.coords(figure)
self._TKCanvas2.move(figure, shift_converted[0] - xy[0], shift_converted[1] - xy[1])
def send_figure_to_back(self, figure):
"""
Changes Z-order of figures on the Graph. Sends the indicated figure to the back of all other drawn figures
:param figure: value returned by tkinter when creating the figure / drawing
:type figure: (int)
"""
self.TKCanvas.tag_lower(figure) # move figure to the "bottom" of all other figure
def bring_figure_to_front(self, figure):
"""
Changes Z-order of figures on the Graph. Brings the indicated figure to the front of all other drawn figures
:param figure: value returned by tkinter when creating the figure / drawing
:type figure: (int)
"""
self.TKCanvas.tag_raise(figure) # move figure to the "top" of all other figures
def get_figures_at_location(self, location):
"""
Returns a list of figures located at a particular x,y location within the Graph
:param location: point to check
:type location: (int, int) | Tuple[float, float]
:return: a list of previously drawn "Figures" (returned from the drawing primitives)
:rtype: List[int]
"""
x, y = self._convert_xy_to_canvas_xy(location[0], location[1])
ids = self.TKCanvas.find_overlapping(x, y, x, y)
return ids
def get_bounding_box(self, figure):
"""
Given a figure, returns the upper left and lower right bounding box coordinates
:param figure: a previously drawing figure
:type figure: object
:return: upper left x, upper left y, lower right x, lower right y
:rtype: Tuple[int, int, int, int] | Tuple[float, float, float, float]
"""
box = self.TKCanvas.bbox(figure)
top_left = self._convert_canvas_xy_to_xy(box[0], box[1])
bottom_right = self._convert_canvas_xy_to_xy(box[2], box[3])
return top_left, bottom_right
def change_coordinates(self, graph_bottom_left, graph_top_right):
"""
Changes the corrdinate system to a new one. The same 2 points in space are used to define the coorinate
system - the bottom left and the top right values of your graph.
:param graph_bottom_left: The bottoms left corner of your coordinate system
:type graph_bottom_left: (int, int) (x,y)
:param graph_top_right: The top right corner of your coordinate system
:type graph_top_right: (int, int) (x,y)
"""
self.BottomLeft = graph_bottom_left
self.TopRight = graph_top_right
@property
def tk_canvas(self):
"""
Returns the underlying tkiner Canvas widget
:return: The tkinter canvas widget
:rtype: (tk.Canvas)
"""
if self._TKCanvas2 is None:
print('*** Did you forget to call Finalize()? Your code should look something like: ***')
print('*** form = sg.Window("My Form").Layout(layout).Finalize() ***')
return self._TKCanvas2
# button release callback
def button_release_call_back(self, event):
"""
Not a user callable method. Used to get Graph click events. Called by tkinter when button is released
:param event: (event) event info from tkinter. Note not used in this method
:type event:
"""
if not self.DragSubmits: # only report mouse up for drag operations
return
self.ClickPosition = self._convert_canvas_xy_to_xy(event.x, event.y)
self.LastButtonClickedWasRealtime = not self.DragSubmits
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = '__GRAPH__' # need to put something rather than None
# if self.ParentForm.CurrentlyRunningMainloop:
# self.ParentForm.TKroot.quit()
_exit_mainloop(self.ParentForm)
if self.DragSubmits:
if isinstance(self.ParentForm.LastButtonClicked, str):
self.ParentForm.LastButtonClicked = self.ParentForm.LastButtonClicked + '+UP'
else:
self.ParentForm.LastButtonClicked = (self.ParentForm.LastButtonClicked, '+UP')
# self.ParentForm.LastButtonClicked += '+UP' # this is the old method that required string key
self.MouseButtonDown = False
# button callback
def button_press_call_back(self, event):
"""
Not a user callable method. Used to get Graph click events. Called by tkinter when button is released
:param event: (event) event info from tkinter. Contains the x and y coordinates of a click
:type event:
"""
self.ClickPosition = self._convert_canvas_xy_to_xy(event.x, event.y)
self.ParentForm.LastButtonClickedWasRealtime = self.DragSubmits
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = '__GRAPH__' # need to put something rather than None
# if self.ParentForm.CurrentlyRunningMainloop:
# self.ParentForm.TKroot.quit() # kick out of loop if read was called
_exit_mainloop(self.ParentForm)
self.MouseButtonDown = True
def _update_position_for_returned_values(self, event):
"""
Updates the variable that's used when the values dictionary is returned from a window read.
Not called by the user. It's called from another method/function that tkinter calledback
:param event: (event) event info from tkinter. Contains the x and y coordinates of a click
:type event:
"""
"""
Updates the variable that's used when the values dictionary is returned from a window read.
Not called by the user. It's called from another method/function that tkinter calledback
:param event: (event) event info from tkinter. Contains the x and y coordinates of a click
:type event:
"""
self.ClickPosition = self._convert_canvas_xy_to_xy(event.x, event.y)
# button callback
def motion_call_back(self, event):
"""
Not a user callable method. Used to get Graph mouse motion events. Called by tkinter when mouse moved
:param event: (event) event info from tkinter. Contains the x and y coordinates of a mouse
:type event:
"""
if not self.MouseButtonDown and not self.motion_events:
return
self.ClickPosition = self._convert_canvas_xy_to_xy(event.x, event.y)
self.ParentForm.LastButtonClickedWasRealtime = self.DragSubmits
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = '__GRAPH__' # need to put something rather than None
# if self.ParentForm.CurrentlyRunningMainloop:
# self.ParentForm.TKroot.quit() # kick out of loop if read was called
if self.motion_events and not self.MouseButtonDown:
if isinstance(self.ParentForm.LastButtonClicked, str):
self.ParentForm.LastButtonClicked = self.ParentForm.LastButtonClicked + '+MOVE'
else:
self.ParentForm.LastButtonClicked = (self.ParentForm.LastButtonClicked, '+MOVE')
_exit_mainloop(self.ParentForm)
BringFigureToFront = bring_figure_to_front
ButtonPressCallBack = button_press_call_back
ButtonReleaseCallBack = button_release_call_back
DeleteFigure = delete_figure
DrawArc = draw_arc
DrawCircle = draw_circle
DrawImage = draw_image
DrawLine = draw_line
DrawOval = draw_oval
DrawPoint = draw_point
DrawPolygon = draw_polygon
DrawLines = draw_lines
DrawRectangle = draw_rectangle
DrawText = draw_text
GetFiguresAtLocation = get_figures_at_location
GetBoundingBox = get_bounding_box
Erase = erase
MotionCallBack = motion_call_back
Move = move
MoveFigure = move_figure
RelocateFigure = relocate_figure
SendFigureToBack = send_figure_to_back
TKCanvas = tk_canvas
Update = update
G = Graph
# ---------------------------------------------------------------------- #
# Frame #
# ---------------------------------------------------------------------- #
class Frame(Element):
"""
A Frame Element that contains other Elements. Encloses with a line around elements and a text label.
"""
def __init__(self, title, layout, title_color=None, background_color=None, title_location=None,
relief=DEFAULT_FRAME_RELIEF, size=(None, None), s=(None, None), font=None, pad=None, p=None, border_width=None, key=None, k=None,
tooltip=None, right_click_menu=None, expand_x=False, expand_y=False, grab=None, visible=True, element_justification='left', vertical_alignment=None, metadata=None):
"""
:param title: text that is displayed as the Frame's "label" or title
:type title: (str)
:param layout: The layout to put inside the Frame
:type layout: List[List[Elements]]
:param title_color: color of the title text
:type title_color: (str)
:param background_color: background color of the Frame
:type background_color: (str)
:param title_location: location to place the text title. Choices include: TITLE_LOCATION_TOP TITLE_LOCATION_BOTTOM TITLE_LOCATION_LEFT TITLE_LOCATION_RIGHT TITLE_LOCATION_TOP_LEFT TITLE_LOCATION_TOP_RIGHT TITLE_LOCATION_BOTTOM_LEFT TITLE_LOCATION_BOTTOM_RIGHT
:type title_location: (enum)
:param relief: relief style. Values are same as other elements with reliefs. Choices include RELIEF_RAISED RELIEF_SUNKEN RELIEF_FLAT RELIEF_RIDGE RELIEF_GROOVE RELIEF_SOLID
:type relief: (enum)
:param size: (width, height) Sets an initial hard-coded size for the Frame. This used to be a problem, but was fixed in 4.53.0 and works better than Columns when using the size paramter
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param font: specifies the font family, size, etc. for the TITLE. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param border_width: width of border around element in pixels
:type border_width: (int)
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param grab: If True can grab this element and move the window around. Default is False
:type grab: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param element_justification: All elements inside the Frame will have this justification 'left', 'right', 'center' are valid values
:type element_justification: (str)
:param vertical_alignment: Place the Frame at the 'top', 'center', 'bottom' of the row (can also use t,c,r). Defaults to no setting (tkinter decides)
:type vertical_alignment: (str)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.UseDictionary = False
self.ReturnValues = None
self.ReturnValuesList = []
self.ReturnValuesDictionary = {}
self.DictionaryKeyCounter = 0
self.ParentWindow = None
self.Rows = []
# self.ParentForm = None
self.TKFrame = None
self.Title = title
self.Relief = relief
self.TitleLocation = title_location
self.BorderWidth = border_width
self.BackgroundColor = background_color if background_color is not None else DEFAULT_BACKGROUND_COLOR
self.RightClickMenu = right_click_menu
self.ContainerElemementNumber = Window._GetAContainerNumber()
self.ElementJustification = element_justification
self.VerticalAlignment = vertical_alignment
self.Widget = None # type: tk.LabelFrame
self.Grab = grab
self.Layout(layout)
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(ELEM_TYPE_FRAME, background_color=background_color, text_color=title_color, size=sz,
font=font, pad=pad, key=key, tooltip=tooltip, visible=visible, metadata=metadata)
return
def add_row(self, *args):
"""
Not recommended user call. Used to add rows of Elements to the Frame Element.
:param *args: The list of elements for this row
:type *args: List[Element]
"""
NumRows = len(self.Rows) # number of existing rows is our row number
CurrentRowNumber = NumRows # this row's number
CurrentRow = [] # start with a blank row and build up
# ------------------------- Add the elements to a row ------------------------- #
for i, element in enumerate(args): # Loop through list of elements and add them to the row
if type(element) == list:
PopupError('Error creating Frame layout',
'Layout has a LIST instead of an ELEMENT',
'This means you have a badly placed ]',
'The offensive list is:',
element,
'This list will be stripped from your layout',
keep_on_top=True
)
continue
elif callable(element) and not isinstance(element, Element):
PopupError('Error creating Frame layout',
'Layout has a FUNCTION instead of an ELEMENT',
'This likely means you are missing () from your layout',
'The offensive list is:',
element,
'This item will be stripped from your layout',
keep_on_top=True)
continue
if element.ParentContainer is not None:
warnings.warn(
'*** YOU ARE ATTEMPTING TO RESUSE AN ELEMENT IN YOUR LAYOUT! Once placed in a layout, an element cannot be used in another layout. ***',
UserWarning)
_error_popup_with_traceback('Error creating Frame layout',
'The layout specified has already been used',
'You MUST start witha "clean", unused layout every time you create a window',
'The offensive Element = ',
element,
'and has a key = ', element.Key,
'This item will be stripped from your layout',
'Hint - try printing your layout and matching the IDs "print(layout)"',
)
continue
element.Position = (CurrentRowNumber, i)
element.ParentContainer = self
CurrentRow.append(element)
if element.Key is not None:
self.UseDictionary = True
# ------------------------- Append the row to list of Rows ------------------------- #
self.Rows.append(CurrentRow)
def layout(self, rows):
"""
Can use like the Window.Layout method, but it's better to use the layout parameter when creating
:param rows: The rows of Elements
:type rows: List[List[Element]]
:return: Used for chaining
:rtype: (Frame)
"""
for row in rows:
try:
iter(row)
except TypeError:
PopupError('Error creating Frame layout',
'Your row is not an iterable (e.g. a list)',
'Instead of a list, the type found was {}'.format(type(row)),
'The offensive row = ',
row,
'This item will be stripped from your layout', keep_on_top=True, image=_random_error_emoji())
continue
self.AddRow(*row)
return self
def _GetElementAtLocation(self, location):
"""
Not user callable. Used to find the Element at a row, col position within the layout
:param location: (row, column) position of the element to find in layout
:type location: (int, int)
:return: (Element) The element found at the location
:rtype: (Element)
"""
(row_num, col_num) = location
row = self.Rows[row_num]
element = row[col_num]
return element
def update(self, value=None, visible=None):
"""
Changes some of the settings for the Frame Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: New text value to show on frame
:type value: (Any)
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if visible is False:
self.TKFrame.pack_forget()
elif visible is True:
self.TKFrame.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if value is not None:
self.TKFrame.config(text=str(value))
if visible is not None:
self._visible = visible
AddRow = add_row
Layout = layout
Update = update
Fr = Frame
# ---------------------------------------------------------------------- #
# Vertical Separator #
# ---------------------------------------------------------------------- #
class VerticalSeparator(Element):
"""
Vertical Separator Element draws a vertical line at the given location. It will span 1 "row". Usually paired with
Column Element if extra height is needed
"""
def __init__(self, color=None, pad=None, p=None, key=None, k=None):
"""
:param color: Color of the line. Defaults to theme's text color. Can be name or #RRGGBB format
:type color: (str)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
"""
key = key if key is not None else k
pad = pad if pad is not None else p
self.expand_x = None
self.expand_y = None
self.Orientation = 'vertical' # for now only vertical works
self.color = color if color is not None else theme_text_color()
super().__init__(ELEM_TYPE_SEPARATOR, pad=pad, key=key)
VSeperator = VerticalSeparator
VSeparator = VerticalSeparator
VSep = VerticalSeparator
# ---------------------------------------------------------------------- #
# Horizontal Separator #
# ---------------------------------------------------------------------- #
class HorizontalSeparator(Element):
"""
Horizontal Separator Element draws a Horizontal line at the given location.
"""
def __init__(self, color=None, pad=None, p=None, key=None, k=None):
"""
:param color: Color of the line. Defaults to theme's text color. Can be name or #RRGGBB format
:type color: (str)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
"""
self.Orientation = 'horizontal' # for now only vertical works
self.color = color if color is not None else theme_text_color()
self.expand_x = True
self.expand_y = None
key = key if key is not None else k
pad = pad if pad is not None else p
super().__init__(ELEM_TYPE_SEPARATOR, pad=pad, key=key)
HSeparator = HorizontalSeparator
HSep = HorizontalSeparator
# ---------------------------------------------------------------------- #
# Sizegrip #
# ---------------------------------------------------------------------- #
class Sizegrip(Element):
"""
Sizegrip element will be added to the bottom right corner of your window.
It should be placed on the last row of your window along with any other elements on that row.
The color will match the theme's background color.
"""
def __init__(self, background_color=None, pad=None, p=(0,0), key=None, k=None):
"""
Sizegrip Element
:param background_color: color to use for the background of the grip
:type background_color: str
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
"""
bg = background_color if background_color is not None else theme_background_color()
pad = pad if pad is not None else p
key = key if key is not None else k
super().__init__(ELEM_TYPE_SIZEGRIP, background_color=bg,key=key, pad=pad)
SGrip = Sizegrip
# ---------------------------------------------------------------------- #
# Tab #
# ---------------------------------------------------------------------- #
class Tab(Element):
"""
Tab Element is another "Container" element that holds a layout and displays a tab with text. Used with TabGroup only
Tabs are never placed directly into a layout. They are always "Contained" in a TabGroup layout
"""
def __init__(self, title, layout, title_color=None, background_color=None, font=None, pad=None, p=None, disabled=False,
border_width=None, key=None, k=None, tooltip=None, right_click_menu=None, expand_x=False, expand_y=False, visible=True, element_justification='left', image_source=None, image_subsample=None, metadata=None):
"""
:param title: text to show on the tab
:type title: (str)
:param layout: The element layout that will be shown in the tab
:type layout: List[List[Element]]
:param title_color: color of the tab text (note not currently working on tkinter)
:type title_color: (str)
:param background_color: color of background of the entire layout
:type background_color: (str)
:param font: NOT USED in the tkinter port
:type font: (str or (str, int[, str]) or None)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param disabled: If True button will be created disabled
:type disabled: (bool)
:param border_width: NOT USED in tkinter port
:type border_width: (int)
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param element_justification: All elements inside the Tab will have this justification 'left', 'right', 'center' are valid values
:type element_justification: (str)
:param image_source: A filename or a base64 bytes of an image to place on the Tab
:type image_source: str | bytes | None
:param image_subsample: amount to reduce the size of the image. Divides the size by this number. 2=1/2, 3=1/3, 4=1/4, etc
:type image_subsample: (int)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
filename = data = None
if image_source is not None:
if isinstance(image_source, bytes):
data = image_source
elif isinstance(image_source, str):
filename = image_source
else:
warnings.warn('Image element - source is not a valid type: {}'.format(type(image_source)), UserWarning)
self.Filename = filename
self.Data = data
self.ImageSubsample = image_subsample
self.UseDictionary = False
self.ReturnValues = None
self.ReturnValuesList = []
self.ReturnValuesDictionary = {}
self.DictionaryKeyCounter = 0
self.ParentWindow = None
self.Rows = []
self.TKFrame = None
self.Widget = None # type: tk.Frame
self.Title = title
self.BorderWidth = border_width
self.Disabled = disabled
self.ParentNotebook = None
self.TabID = None
self.BackgroundColor = background_color if background_color is not None else DEFAULT_BACKGROUND_COLOR
self.RightClickMenu = right_click_menu
self.ContainerElemementNumber = Window._GetAContainerNumber()
self.ElementJustification = element_justification
key = key if key is not None else k
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
self.Layout(layout)
super().__init__(ELEM_TYPE_TAB, background_color=background_color, text_color=title_color, font=font, pad=pad, key=key, tooltip=tooltip,
visible=visible, metadata=metadata)
return
def add_row(self, *args):
"""
Not recommended use call. Used to add rows of Elements to the Frame Element.
:param *args: The list of elements for this row
:type *args: List[Element]
"""
NumRows = len(self.Rows) # number of existing rows is our row number
CurrentRowNumber = NumRows # this row's number
CurrentRow = [] # start with a blank row and build up
# ------------------------- Add the elements to a row ------------------------- #
for i, element in enumerate(args): # Loop through list of elements and add them to the row
if type(element) == list:
PopupError('Error creating Tab layout',
'Layout has a LIST instead of an ELEMENT',
'This means you have a badly placed ]',
'The offensive list is:',
element,
'This list will be stripped from your layout', keep_on_top=True, image=_random_error_emoji()
)
continue
elif callable(element) and not isinstance(element, Element):
PopupError('Error creating Tab layout',
'Layout has a FUNCTION instead of an ELEMENT',
'This likely means you are missing () from your layout',
'The offensive list is:',
element,
'This item will be stripped from your layout', keep_on_top=True, image=_random_error_emoji())
continue
if element.ParentContainer is not None:
warnings.warn(
'*** YOU ARE ATTEMPTING TO RESUSE AN ELEMENT IN YOUR LAYOUT! Once placed in a layout, an element cannot be used in another layout. ***',
UserWarning)
PopupError('Error creating Tab layout',
'The layout specified has already been used',
'You MUST start witha "clean", unused layout every time you create a window',
'The offensive Element = ',
element,
'and has a key = ', element.Key,
'This item will be stripped from your layout',
'Hint - try printing your layout and matching the IDs "print(layout)"', keep_on_top=True, image=_random_error_emoji())
continue
element.Position = (CurrentRowNumber, i)
element.ParentContainer = self
CurrentRow.append(element)
if element.Key is not None:
self.UseDictionary = True
# ------------------------- Append the row to list of Rows ------------------------- #
self.Rows.append(CurrentRow)
def layout(self, rows):
"""
Not user callable. Use layout parameter instead. Creates the layout using the supplied rows of Elements
:param rows: List[List[Element]] The list of rows
:type rows: List[List[Element]]
:return: (Tab) used for chaining
:rtype:
"""
for row in rows:
try:
iter(row)
except TypeError:
PopupError('Error creating Tab layout',
'Your row is not an iterable (e.g. a list)',
'Instead of a list, the type found was {}'.format(type(row)),
'The offensive row = ',
row,
'This item will be stripped from your layout', keep_on_top=True, image=_random_error_emoji())
continue
self.AddRow(*row)
return self
def update(self, title=None, disabled=None, visible=None):
"""
Changes some of the settings for the Tab Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param title: tab title
:type title: (str)
:param disabled: disable or enable state of the element
:type disabled: (bool)
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
state = 'normal'
if disabled is not None:
self.Disabled = disabled
if disabled:
state = 'disabled'
if visible is False:
state = 'hidden'
if visible is not None:
self._visible = visible
self.ParentNotebook.tab(self.TabID, state=state)
if title is not None:
self.Title = str(title)
self.ParentNotebook.tab(self.TabID, text=self.Title)
# self.ParentNotebook.tab(self.ContainerElemementNumber-1, text=self.Title)
# if visible is False:
# self.ParentNotebook.pack_forget()
# elif visible is True:
# self.ParentNotebook.pack()
return self
def _GetElementAtLocation(self, location):
"""
Not user callable. Used to find the Element at a row, col position within the layout
:param location: (row, column) position of the element to find in layout
:type location: (int, int)
:return: The element found at the location
:rtype: (Element)
"""
(row_num, col_num) = location
row = self.Rows[row_num]
element = row[col_num]
return element
def select(self):
"""
Create a tkinter event that mimics user clicking on a tab. Must have called window.Finalize / Read first!
"""
# Use a try in case the window has been destoyed
try:
self.ParentNotebook.select(self.TabID)
except Exception as e:
print('Exception Selecting Tab {}'.format(e))
AddRow = add_row
Layout = layout
Select = select
Update = update
# ---------------------------------------------------------------------- #
# TabGroup #
# ---------------------------------------------------------------------- #
class TabGroup(Element):
"""
TabGroup Element groups together your tabs into the group of tabs you see displayed in your window
"""
def __init__(self, layout, tab_location=None, title_color=None, tab_background_color=None, selected_title_color=None, selected_background_color=None,
background_color=None, focus_color=None, font=None, change_submits=False, enable_events=False, pad=None, p=None, border_width=None, tab_border_width=None, theme=None, key=None, k=None,
size=(None, None), s=(None, None), tooltip=None, right_click_menu=None, expand_x=False, expand_y=False, visible=True, metadata=None):
"""
:param layout: Layout of Tabs. Different than normal layouts. ALL Tabs should be on first row
:type layout: List[List[Tab]]
:param tab_location: location that tabs will be displayed. Choices are left, right, top, bottom, lefttop, leftbottom, righttop, rightbottom, bottomleft, bottomright, topleft, topright
:type tab_location: (str)
:param title_color: color of text on tabs
:type title_color: (str)
:param tab_background_color: color of all tabs that are not selected
:type tab_background_color: (str)
:param selected_title_color: color of tab text when it is selected
:type selected_title_color: (str)
:param selected_background_color: color of tab when it is selected
:type selected_background_color: (str)
:param background_color: color of background area that tabs are located on
:type background_color: (str)
:param focus_color: color of focus indicator on the tabs
:type focus_color: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param change_submits: * DEPRICATED DO NOT USE. Use `enable_events` instead
:type change_submits: (bool)
:param enable_events: If True then switching tabs will generate an Event
:type enable_events: (bool)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param border_width: width of border around element in pixels
:type border_width: (int)
:param tab_border_width: width of border around the tabs
:type tab_border_width: (int)
:param theme: DEPRICATED - You can only specify themes using set options or when window is created. It's not possible to do it on an element basis
:type theme: (enum)
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param size: (width, height) w=pixels-wide, h=pixels-high. Either item in tuple can be None to indicate use the computed value and set only 1 direction
:type size: (int|None, int|None)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int|None, int|None)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.UseDictionary = False
self.ReturnValues = None
self.ReturnValuesList = []
self.ReturnValuesDictionary = {}
self.DictionaryKeyCounter = 0
self.ParentWindow = None
self.SelectedTitleColor = selected_title_color if selected_title_color is not None else LOOK_AND_FEEL_TABLE[CURRENT_LOOK_AND_FEEL]['TEXT']
self.SelectedBackgroundColor = selected_background_color if selected_background_color is not None else LOOK_AND_FEEL_TABLE[CURRENT_LOOK_AND_FEEL][
'BACKGROUND']
title_color = title_color if title_color is not None else LOOK_AND_FEEL_TABLE[CURRENT_LOOK_AND_FEEL]['TEXT_INPUT']
self.TabBackgroundColor = tab_background_color if tab_background_color is not None else LOOK_AND_FEEL_TABLE[CURRENT_LOOK_AND_FEEL]['INPUT']
self.Rows = []
self.TKNotebook = None # type: ttk.Notebook
self.Widget = None # type: ttk.Notebook
self.TabCount = 0
self.BorderWidth = border_width
self.BackgroundColor = background_color if background_color is not None else DEFAULT_BACKGROUND_COLOR
self.ChangeSubmits = change_submits or enable_events
self.TabLocation = tab_location
self.ElementJustification = 'left'
self.RightClickMenu = right_click_menu
self.TabBorderWidth = tab_border_width
self.FocusColor = focus_color
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
self.Layout(layout)
super().__init__(ELEM_TYPE_TAB_GROUP, size=sz, background_color=background_color, text_color=title_color, font=font,
pad=pad, key=key, tooltip=tooltip, visible=visible, metadata=metadata)
return
def add_row(self, *args):
"""
Not recommended user call. Used to add rows of Elements to the Frame Element.
:param *args: The list of elements for this row
:type *args: List[Element]
"""
NumRows = len(self.Rows) # number of existing rows is our row number
CurrentRowNumber = NumRows # this row's number
CurrentRow = [] # start with a blank row and build up
# ------------------------- Add the elements to a row ------------------------- #
for i, element in enumerate(args): # Loop through list of elements and add them to the row
if type(element) == list:
PopupError('Error creating Tab layout',
'Layout has a LIST instead of an ELEMENT',
'This means you have a badly placed ]',
'The offensive list is:',
element,
'This list will be stripped from your layout', keep_on_top=True, image=_random_error_emoji()
)
continue
elif callable(element) and not isinstance(element, Element):
PopupError('Error creating Tab layout',
'Layout has a FUNCTION instead of an ELEMENT',
'This likely means you are missing () from your layout',
'The offensive list is:',
element,
'This item will be stripped from your layout', keep_on_top=True, image=_random_error_emoji())
continue
if element.ParentContainer is not None:
warnings.warn(
'*** YOU ARE ATTEMPTING TO RESUSE AN ELEMENT IN YOUR LAYOUT! Once placed in a layout, an element cannot be used in another layout. ***',
UserWarning)
PopupError('Error creating Tab layout',
'The layout specified has already been used',
'You MUST start witha "clean", unused layout every time you create a window',
'The offensive Element = ',
element,
'and has a key = ', element.Key,
'This item will be stripped from your layout',
'Hint - try printing your layout and matching the IDs "print(layout)"', keep_on_top=True, image=_random_error_emoji())
continue
element.Position = (CurrentRowNumber, i)
element.ParentContainer = self
CurrentRow.append(element)
if element.Key is not None:
self.UseDictionary = True
# ------------------------- Append the row to list of Rows ------------------------- #
self.Rows.append(CurrentRow)
def layout(self, rows):
"""
Can use like the Window.Layout method, but it's better to use the layout parameter when creating
:param rows: The rows of Elements
:type rows: List[List[Element]]
:return: Used for chaining
:rtype: (Frame)
"""
for row in rows:
try:
iter(row)
except TypeError:
PopupError('Error creating Tab layout',
'Your row is not an iterable (e.g. a list)',
'Instead of a list, the type found was {}'.format(type(row)),
'The offensive row = ',
row,
'This item will be stripped from your layout', keep_on_top=True, image=_random_error_emoji())
continue
self.AddRow(*row)
return self
def _GetElementAtLocation(self, location):
"""
Not user callable. Used to find the Element at a row, col position within the layout
:param location: (row, column) position of the element to find in layout
:type location: (int, int)
:return: The element found at the location
:rtype: (Element)
"""
(row_num, col_num) = location
row = self.Rows[row_num]
element = row[col_num]
return element
def find_key_from_tab_name(self, tab_name):
"""
Searches through the layout to find the key that matches the text on the tab. Implies names should be unique
:param tab_name: name of a tab
:type tab_name: str
:return: Returns the key or None if no key found
:rtype: key | None
"""
for row in self.Rows:
for element in row:
if element.Title == tab_name:
return element.Key
return None
def get(self):
"""
Returns the current value for the Tab Group, which will be the currently selected tab's KEY or the text on
the tab if no key is defined. Returns None if an error occurs.
Note that this is exactly the same data that would be returned from a call to Window.Read. Are you sure you
are using this method correctly?
:return: The key of the currently selected tab or the tab's text if it has no key
:rtype: Any | None
"""
try:
value = self.TKNotebook.tab(self.TKNotebook.index('current'))['text']
tab_key = self.FindKeyFromTabName(value)
if tab_key is not None:
value = tab_key
except:
value = None
return value
def add_tab(self, tab_element):
"""
Add a new tab to an existing TabGroup
This call was written so that tabs can be added at runtime as your user performs operations.
Your Window should already be created and finalized.
:param tab_element: A Tab Element that has a layout in it
:type tab_element: Tab
"""
self.add_row(tab_element)
tab_element.TKFrame = tab_element.Widget = tk.Frame(self.TKNotebook)
form = self.ParentForm
form._BuildKeyDictForWindow(form, tab_element, form.AllKeysDict)
form.AllKeysDict[tab_element.Key] = tab_element
# Pack the tab's layout into the tab. NOTE - This does NOT pack the Tab itself... for that see below...
PackFormIntoFrame(tab_element, tab_element.TKFrame, self.ParentForm)
# - This is below - Perform the same operation that is performed when a Tab is packed into the window.
# If there's an image in the tab, then do the imagey-stuff
# ------------------- start of imagey-stuff -------------------
try:
if tab_element.Filename is not None:
photo = tk.PhotoImage(file=tab_element.Filename)
elif tab_element.Data is not None:
photo = tk.PhotoImage(data=tab_element.Data)
else:
photo = None
if tab_element.ImageSubsample and photo is not None:
photo = photo.subsample(tab_element.ImageSubsample)
# print('*ERROR laying out form.... Image Element has no image specified*')
except Exception as e:
photo = None
_error_popup_with_traceback('Your Window has an Tab Element with an IMAGE problem',
'The traceback will show you the Window with the problem layout',
'Look in this Window\'s layout for an Image tab_element that has a key of {}'.format(tab_element.Key),
'The error occuring is:', e)
tab_element.photo = photo
# add the label
if photo is not None:
width, height = photo.width(), photo.height()
tab_element.tktext_label = tk.Label(tab_element.ParentRowFrame, image=photo, width=width, height=height, bd=0)
else:
tab_element.tktext_label = tk.Label(tab_element.ParentRowFrame, bd=0)
# ------------------- end of imagey-stuff -------------------
state = 'normal'
if tab_element.Disabled:
state = 'disabled'
if tab_element.visible is False:
state = 'hidden'
if photo is not None:
self.TKNotebook.add(tab_element.TKFrame, text=tab_element.Title, compound=tk.LEFT, state=state, image=photo)
else:
self.TKNotebook.add(tab_element.TKFrame, text=tab_element.Title, state=state)
tab_element.ParentNotebook = self.TKNotebook
tab_element.TabID = self.TabCount
tab_element.ParentForm = self.ParentForm
self.TabCount += 1
if tab_element.BackgroundColor != COLOR_SYSTEM_DEFAULT and tab_element.BackgroundColor is not None:
tab_element.TKFrame.configure(background=tab_element.BackgroundColor, highlightbackground=tab_element.BackgroundColor,
highlightcolor=tab_element.BackgroundColor)
if tab_element.BorderWidth is not None:
tab_element.TKFrame.configure(borderwidth=tab_element.BorderWidth)
if tab_element.Tooltip is not None:
tab_element.TooltipObject = ToolTip(tab_element.TKFrame, text=tab_element.Tooltip, timeout=DEFAULT_TOOLTIP_TIME)
_add_right_click_menu(tab_element, form)
AddRow = add_row
FindKeyFromTabName = find_key_from_tab_name
Get = get
Layout = layout
# ---------------------------------------------------------------------- #
# Slider #
# ---------------------------------------------------------------------- #
class Slider(Element):
"""
A slider, horizontal or vertical
"""
def __init__(self, range=(None, None), default_value=None, resolution=None, tick_interval=None, orientation=None,
disable_number_display=False, border_width=None, relief=None, change_submits=False,
enable_events=False, disabled=False, size=(None, None), s=(None, None), font=None, background_color=None,
text_color=None, trough_color=None, key=None, k=None, pad=None, p=None, expand_x=False, expand_y=False, tooltip=None, visible=True, metadata=None):
"""
:param range: slider's range (min value, max value)
:type range: (int, int) | Tuple[float, float]
:param default_value: starting value for the slider
:type default_value: int | float
:param resolution: the smallest amount the slider can be moved
:type resolution: int | float
:param tick_interval: how often a visible tick should be shown next to slider
:type tick_interval: int | float
:param orientation: 'horizontal' or 'vertical' ('h' or 'v' also work)
:type orientation: (str)
:param disable_number_display: if True no number will be displayed by the Slider Element
:type disable_number_display: (bool)
:param border_width: width of border around element in pixels
:type border_width: (int)
:param relief: relief style. Use constants - RELIEF_RAISED RELIEF_SUNKEN RELIEF_FLAT RELIEF_RIDGE RELIEF_GROOVE RELIEF_SOLID
:type relief: str | None
:param change_submits: * DEPRICATED DO NOT USE. Use `enable_events` instead
:type change_submits: (bool)
:param enable_events: If True then moving the slider will generate an Event
:type enable_events: (bool)
:param disabled: set disable state for element
:type disabled: (bool)
:param size: (l=length chars/rows, w=width pixels)
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param background_color: color of slider's background
:type background_color: (str)
:param text_color: color of the slider's text
:type text_color: (str)
:param trough_color: color of the slider's trough
:type trough_color: (str)
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.TKScale = self.Widget = None # type: tk.Scale
self.Range = (1, 10) if range == (None, None) else range
self.DefaultValue = self.Range[0] if default_value is None else default_value
self.Orientation = orientation if orientation else DEFAULT_SLIDER_ORIENTATION
self.BorderWidth = border_width if border_width else DEFAULT_SLIDER_BORDER_WIDTH
self.Relief = relief if relief else DEFAULT_SLIDER_RELIEF
self.Resolution = 1 if resolution is None else resolution
self.ChangeSubmits = change_submits or enable_events
self.Disabled = disabled
self.TickInterval = tick_interval
self.DisableNumericDisplay = disable_number_display
self.TroughColor = trough_color or DEFAULT_SCROLLBAR_COLOR
sz = size if size != (None, None) else s
temp_size = sz
if temp_size == (None, None):
temp_size = (20, 20) if self.Orientation.startswith('h') else (8, 20)
key = key if key is not None else k
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(ELEM_TYPE_INPUT_SLIDER, size=temp_size, font=font, background_color=background_color,
text_color=text_color, key=key, pad=pad, tooltip=tooltip, visible=visible, metadata=metadata)
return
def update(self, value=None, range=(None, None), disabled=None, visible=None):
"""
Changes some of the settings for the Slider Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: sets current slider value
:type value: int | float
:param range: Sets a new range for slider
:type range: (int, int) | Tuple[float, float
:param disabled: disable or enable state of the element
:type disabled: (bool)
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if range != (None, None):
self.TKScale.config(from_=range[0], to_=range[1])
if value is not None:
try:
self.TKIntVar.set(value)
except:
pass
self.DefaultValue = value
if disabled == True:
self.TKScale['state'] = 'disabled'
elif disabled == False:
self.TKScale['state'] = 'normal'
if visible is False:
self.TKScale.pack_forget()
elif visible is True:
self.TKScale.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if visible is not None:
self._visible = visible
def _SliderChangedHandler(self, event):
"""
Not user callable. Callback function for when slider is moved.
:param event: (event) the event data provided by tkinter. Unknown format. Not used.
:type event:
"""
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = ''
self.ParentForm.FormRemainedOpen = True
# if self.ParentForm.CurrentlyRunningMainloop:
# self.ParentForm.TKroot.quit() # kick the users out of the mainloop
_exit_mainloop(self.ParentForm)
Update = update
Sl = Slider
# ---------------------------------------------------------------------- #
# TkFixedFrame (Used by Column) #
# ---------------------------------------------------------------------- #
class TkFixedFrame(tk.Frame):
"""
A tkinter frame that is used with Column Elements that do not have a scrollbar
"""
def __init__(self, master, **kwargs):
"""
:param master: The parent widget
:type master: (tk.Widget)
:param **kwargs: The keyword args
:type **kwargs:
"""
tk.Frame.__init__(self, master, **kwargs)
self.canvas = tk.Canvas(self)
self.canvas.pack(side="left", fill="both", expand=True)
# reset the view
self.canvas.xview_moveto(0)
self.canvas.yview_moveto(0)
# create a frame inside the canvas which will be scrolled with it
self.TKFrame = tk.Frame(self.canvas, **kwargs)
self.frame_id = self.canvas.create_window(0, 0, window=self.TKFrame, anchor="nw")
self.canvas.config(borderwidth=0, highlightthickness=0)
self.TKFrame.config(borderwidth=0, highlightthickness=0)
self.config(borderwidth=0, highlightthickness=0)
# ---------------------------------------------------------------------- #
# TkScrollableFrame (Used by Column) #
# ---------------------------------------------------------------------- #
class TkScrollableFrame(tk.Frame):
"""
A frame with one or two scrollbars. Used to make Columns with scrollbars
"""
def __init__(self, master, vertical_only, **kwargs):
"""
:param master: The parent widget
:type master: (tk.Widget)
:param vertical_only: if True the only a vertical scrollbar will be shown
:type vertical_only: (bool)
"""
tk.Frame.__init__(self, master, **kwargs)
# create a canvas object and a vertical scrollbar for scrolling it
# Okay, we're gonna make a list. Containing the y-min, x-min, y-max, and x-max of the frame
self.vscrollbar = tk.Scrollbar(self, orient=tk.VERTICAL)
self.vscrollbar.pack(side='right', fill="y", expand="false")
if not vertical_only:
self.hscrollbar = tk.Scrollbar(self, orient=tk.HORIZONTAL)
self.hscrollbar.pack(side='bottom', fill="x", expand="false")
self.canvas = tk.Canvas(self, yscrollcommand=self.vscrollbar.set, xscrollcommand=self.hscrollbar.set)
else:
self.canvas = tk.Canvas(self, yscrollcommand=self.vscrollbar.set)
self.canvas.pack(side="left", fill="both", expand=True)
self.vscrollbar.config(command=self.canvas.yview)
if not vertical_only:
self.hscrollbar.config(command=self.canvas.xview)
# reset the view
self.canvas.xview_moveto(0)
self.canvas.yview_moveto(0)
# create a frame inside the canvas which will be scrolled with it
self.TKFrame = tk.Frame(self.canvas, **kwargs)
self.frame_id = self.canvas.create_window(0, 0, window=self.TKFrame, anchor="nw")
self.canvas.config(borderwidth=0, highlightthickness=0)
self.TKFrame.config(borderwidth=0, highlightthickness=0)
self.config(borderwidth=0, highlightthickness=0)
# Canvas can be: master, canvas, TKFrame
# Chr0nic
self.unhookMouseWheel(None)
self.TKFrame.bind("<Enter>", self.hookMouseWheel)
self.TKFrame.bind("<Leave>", self.unhookMouseWheel)
# self.canvas.bind_all('<4>', self.yscroll, add='+')
# self.canvas.bind_all('<5>', self.yscroll, add='+')
# self.canvas.bind_all("<MouseWheel>", self.yscroll, add='+')
# self.canvas.bind_all("<Shift-MouseWheel>", self.xscroll, add='+')
self.bind('<Configure>', self.set_scrollregion)
# Chr0nic
def hookMouseWheel(self, e):
# print("enter")
VarHolder.canvas_holder = self.canvas
self.TKFrame.bind_all('<4>', self.yscroll, add='+')
self.TKFrame.bind_all('<5>', self.yscroll, add='+')
self.TKFrame.bind_all("<MouseWheel>", self.yscroll, add='+')
self.TKFrame.bind_all("<Shift-MouseWheel>", self.xscroll, add='+')
# Chr0nic
def unhookMouseWheel(self, e):
# print("leave")
VarHolder.canvas_holder = None
self.TKFrame.unbind_all('<4>')
self.TKFrame.unbind_all('<5>')
self.TKFrame.unbind_all("<MouseWheel>")
self.TKFrame.unbind_all("<Shift-MouseWheel>")
def resize_frame(self, e):
self.canvas.itemconfig(self.frame_id, height=e.height, width=e.width)
def yscroll(self, event):
if self.canvas.yview() == (0.0, 1.0):
return
if event.num == 5 or event.delta < 0:
self.canvas.yview_scroll(1, "unit")
elif event.num == 4 or event.delta > 0:
self.canvas.yview_scroll(-1, "unit")
def xscroll(self, event):
if event.num == 5 or event.delta < 0:
self.canvas.xview_scroll(1, "unit")
elif event.num == 4 or event.delta > 0:
self.canvas.xview_scroll(-1, "unit")
def bind_mouse_scroll(self, parent, mode):
# ~~ Windows only
parent.bind("<MouseWheel>", mode)
# ~~ Unix only
parent.bind("<Button-4>", mode)
parent.bind("<Button-5>", mode)
def set_scrollregion(self, event=None):
""" Set the scroll region on the canvas """
self.canvas.configure(scrollregion=self.canvas.bbox('all'))
# ---------------------------------------------------------------------- #
# Column #
# ---------------------------------------------------------------------- #
class Column(Element):
"""
A container element that is used to create a layout within your window's layout
"""
def __init__(self, layout, background_color=None, size=(None, None), s=(None, None), pad=None, p=None, scrollable=False,
vertical_scroll_only=False, right_click_menu=None, key=None, k=None, visible=True, justification=None, element_justification=None,
vertical_alignment=None, grab=None, expand_x=None, expand_y=None, metadata=None):
"""
:param layout: Layout that will be shown in the Column container
:type layout: List[List[Element]]
:param background_color: color of background of entire Column
:type background_color: (str)
:param size: (width, height) size in pixels (doesn't work quite right, sometimes only 1 dimension is set by tkinter. Use a Sizer Element to help set sizes
:type size: (int | None, int | None)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int | None, int | None)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param scrollable: if True then scrollbars will be added to the column
:type scrollable: (bool)
:param vertical_scroll_only: if Truen then no horizontal scrollbar will be shown
:type vertical_scroll_only: (bool)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set visibility state of the element
:type visible: (bool)
:param justification: set justification for the Column itself. Note entire row containing the Column will be affected
:type justification: (str)
:param element_justification: All elements inside the Column will have this justification 'left', 'right', 'center' are valid values
:type element_justification: (str)
:param vertical_alignment: Place the column at the 'top', 'center', 'bottom' of the row (can also use t,c,r). Defaults to no setting (tkinter decides)
:type vertical_alignment: (str)
:param grab: If True can grab this element and move the window around. Default is False
:type grab: (bool)
:param expand_x: If True the column will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the column will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.UseDictionary = False
self.ReturnValues = None
self.ReturnValuesList = []
self.ReturnValuesDictionary = {}
self.DictionaryKeyCounter = 0
self.ParentWindow = None
self.ParentPanedWindow = None
self.Rows = []
self.TKFrame = None
self.TKColFrame = None # type: tk.Frame
self.Scrollable = scrollable
self.VerticalScrollOnly = vertical_scroll_only
self.RightClickMenu = right_click_menu
bg = background_color if background_color is not None else DEFAULT_BACKGROUND_COLOR
self.ContainerElemementNumber = Window._GetAContainerNumber()
self.ElementJustification = element_justification
self.Justification = justification
self.VerticalAlignment = vertical_alignment
key = key if key is not None else k
self.Grab = grab
self.ExpandX = expand_x
self.ExpandY = expand_y
self.Layout(layout)
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
super().__init__(ELEM_TYPE_COLUMN, background_color=bg, size=sz, pad=pad, key=key, visible=visible, metadata=metadata)
return
def add_row(self, *args):
"""
Not recommended user call. Used to add rows of Elements to the Column Element.
:param *args: The list of elements for this row
:type *args: List[Element]
"""
NumRows = len(self.Rows) # number of existing rows is our row number
CurrentRowNumber = NumRows # this row's number
CurrentRow = [] # start with a blank row and build up
# ------------------------- Add the elements to a row ------------------------- #
for i, element in enumerate(args): # Loop through list of elements and add them to the row
if type(element) == list:
PopupError('Error creating Column layout',
'Layout has a LIST instead of an ELEMENT',
'This means you have a badly placed ]',
'The offensive list is:',
element,
'This list will be stripped from your layout', keep_on_top=True, image=_random_error_emoji()
)
continue
elif callable(element) and not isinstance(element, Element):
PopupError('Error creating Column layout',
'Layout has a FUNCTION instead of an ELEMENT',
'This likely means you are missing () from your layout',
'The offensive list is:',
element,
'This item will be stripped from your layout', keep_on_top=True, image=_random_error_emoji())
continue
if element.ParentContainer is not None:
warnings.warn(
'*** YOU ARE ATTEMPTING TO RESUSE AN ELEMENT IN YOUR LAYOUT! Once placed in a layout, an element cannot be used in another layout. ***',
UserWarning)
PopupError('Error creating Column layout',
'The layout specified has already been used',
'You MUST start witha "clean", unused layout every time you create a window',
'The offensive Element = ',
element,
'and has a key = ', element.Key,
'This item will be stripped from your layout',
'Hint - try printing your layout and matching the IDs "print(layout)"', keep_on_top=True, image=_random_error_emoji())
continue
element.Position = (CurrentRowNumber, i)
element.ParentContainer = self
CurrentRow.append(element)
if element.Key is not None:
self.UseDictionary = True
# ------------------------- Append the row to list of Rows ------------------------- #
self.Rows.append(CurrentRow)
def layout(self, rows):
"""
Can use like the Window.Layout method, but it's better to use the layout parameter when creating
:param rows: The rows of Elements
:type rows: List[List[Element]]
:return: Used for chaining
:rtype: (Column)
"""
for row in rows:
try:
iter(row)
except TypeError:
PopupError('Error creating Column layout',
'Your row is not an iterable (e.g. a list)',
'Instead of a list, the type found was {}'.format(type(row)),
'The offensive row = ',
row,
'This item will be stripped from your layout', keep_on_top=True, image=_random_error_emoji())
continue
self.AddRow(*row)
return self
def _GetElementAtLocation(self, location):
"""
Not user callable. Used to find the Element at a row, col position within the layout
:param location: (row, column) position of the element to find in layout
:type location: (int, int)
:return: The element found at the location
:rtype: (Element)
"""
(row_num, col_num) = location
row = self.Rows[row_num]
element = row[col_num]
return element
def update(self, visible=None):
"""
Changes some of the settings for the Column Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self.ExpandX and self.ExpandY:
expand = tk.BOTH
elif self.ExpandX:
expand = tk.X
elif self.ExpandY:
expand = tk.Y
else:
expand = None
if visible is False:
if self.TKColFrame:
self.TKColFrame.pack_forget()
if self.ParentPanedWindow:
self.ParentPanedWindow.remove(self.TKColFrame)
elif visible is True:
if self.TKColFrame:
self.TKColFrame.pack(padx=self.pad_used[0], pady=self.pad_used[1], fill=expand)
if self.ParentPanedWindow:
self.ParentPanedWindow.add(self.TKColFrame)
if visible is not None:
self._visible = visible
def contents_changed(self):
"""
When a scrollable column has part of its layout changed by making elements visible or invisible or the
layout is extended for the Column, then this method needs to be called so that the new scroll area
is computed to match the new contents.
"""
self.TKColFrame.canvas.config(scrollregion=self.TKColFrame.canvas.bbox('all'))
AddRow = add_row
Layout = layout
Update = update
Col = Column
# ---------------------------------------------------------------------- #
# Pane #
# ---------------------------------------------------------------------- #
class Pane(Element):
"""
A sliding Pane that is unique to tkinter. Uses Columns to create individual panes
"""
def __init__(self, pane_list, background_color=None, size=(None, None), s=(None, None), pad=None, p=None, orientation='vertical',
show_handle=True, relief=RELIEF_RAISED, handle_size=None, border_width=None, key=None, k=None, expand_x=None, expand_y=None, visible=True, metadata=None):
"""
:param pane_list: Must be a list of Column Elements. Each Column supplied becomes one pane that's shown
:type pane_list: List[Column] | Tuple[Column]
:param background_color: color of background
:type background_color: (str)
:param size: (width, height) w=characters-wide, h=rows-high How much room to reserve for the Pane
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param orientation: 'horizontal' or 'vertical' or ('h' or 'v'). Direction the Pane should slide
:type orientation: (str)
:param show_handle: if True, the handle is drawn that makes it easier to grab and slide
:type show_handle: (bool)
:param relief: relief style. Values are same as other elements that use relief values. RELIEF_RAISED RELIEF_SUNKEN RELIEF_FLAT RELIEF_RIDGE RELIEF_GROOVE RELIEF_SOLID
:type relief: (enum)
:param handle_size: Size of the handle in pixels
:type handle_size: (int)
:param border_width: width of border around element in pixels
:type border_width: (int)
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param expand_x: If True the column will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the column will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.UseDictionary = False
self.ReturnValues = None
self.ReturnValuesList = []
self.ReturnValuesDictionary = {}
self.DictionaryKeyCounter = 0
self.ParentWindow = None
self.Rows = []
self.TKFrame = None
self.PanedWindow = None
self.Orientation = orientation
self.PaneList = pane_list
self.ShowHandle = show_handle
self.Relief = relief
self.HandleSize = handle_size or 8
self.BorderDepth = border_width
bg = background_color if background_color is not None else DEFAULT_BACKGROUND_COLOR
self.Rows = [pane_list]
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(ELEM_TYPE_PANE, background_color=bg, size=sz, pad=pad, key=key, visible=visible, metadata=metadata)
return
def update(self, visible=None):
"""
Changes some of the settings for the Pane Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if visible is False:
self.PanedWindow.pack_forget()
elif visible is True:
self.PanedWindow.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if visible is not None:
self._visible = visible
Update = update
# ---------------------------------------------------------------------- #
# Calendar #
# ---------------------------------------------------------------------- #
class TKCalendar(ttk.Frame):
"""
This code was shamelessly lifted from moshekaplan's repository - moshekaplan/tkinter_components
NONE of this code is user callable. Stay away!
"""
# XXX ToDo: cget and configure
datetime = calendar.datetime.datetime
timedelta = calendar.datetime.timedelta
def __init__(self, master=None, target_element=None, close_when_chosen=True, default_date=(None, None, None), **kw):
"""WIDGET-SPECIFIC OPTIONS: locale, firstweekday, year, month, selectbackground, selectforeground """
self._TargetElement = target_element
default_mon, default_day, default_year = default_date
# remove custom options from kw before initializating ttk.Frame
fwday = kw.pop('firstweekday', calendar.MONDAY)
year = kw.pop('year', default_year or self.datetime.now().year)
month = kw.pop('month', default_mon or self.datetime.now().month)
locale = kw.pop('locale', None)
sel_bg = kw.pop('selectbackground', '#ecffc4')
sel_fg = kw.pop('selectforeground', '#05640e')
self.format = kw.pop('format')
if self.format is None:
self.format = '%Y-%m-%d %H:%M:%S'
self._date = self.datetime(year, month, default_day or 1)
self._selection = None # no date selected
self._master = master
self.close_when_chosen = close_when_chosen
ttk.Frame.__init__(self, master, **kw)
# instantiate proper calendar class
if locale is None:
self._cal = calendar.TextCalendar(fwday)
else:
self._cal = calendar.LocaleTextCalendar(fwday, locale)
self.__setup_styles() # creates custom styles
self.__place_widgets() # pack/grid used widgets
self.__config_calendar() # adjust calendar columns and setup tags
# configure a canvas, and proper bindings, for selecting dates
self.__setup_selection(sel_bg, sel_fg)
# store items ids, used for insertion later
self._items = [self._calendar.insert('', 'end', values='')
for _ in range(6)]
# insert dates in the currently empty calendar
self._build_calendar()
def __setitem__(self, item, value):
if item in ('year', 'month'):
raise AttributeError("attribute '%s' is not writeable" % item)
elif item == 'selectbackground':
self._canvas['background'] = value
elif item == 'selectforeground':
self._canvas.itemconfigure(self._canvas.text, item=value)
else:
ttk.Frame.__setitem__(self, item, value)
def __getitem__(self, item):
if item in ('year', 'month'):
return getattr(self._date, item)
elif item == 'selectbackground':
return self._canvas['background']
elif item == 'selectforeground':
return self._canvas.itemcget(self._canvas.text, 'fill')
else:
r = ttk.tclobjs_to_py({item: ttk.Frame.__getitem__(self, item)})
return r[item]
def __setup_styles(self):
# custom ttk styles
style = ttk.Style(self.master)
arrow_layout = lambda dir: (
[('Button.focus', {'children': [('Button.%sarrow' % dir, None)]})]
)
style.layout('L.TButton', arrow_layout('left'))
style.layout('R.TButton', arrow_layout('right'))
def __place_widgets(self):
# header frame and its widgets
hframe = ttk.Frame(self)
lbtn = ttk.Button(hframe, style='L.TButton', command=self._prev_month)
rbtn = ttk.Button(hframe, style='R.TButton', command=self._next_month)
self._header = ttk.Label(hframe, width=15, anchor='center')
# the calendar
self._calendar = ttk.Treeview(self, show='', selectmode='none', height=7)
# pack the widgets
hframe.pack(in_=self, side='top', pady=4, anchor='center')
lbtn.grid(in_=hframe)
self._header.grid(in_=hframe, column=1, row=0, padx=12)
rbtn.grid(in_=hframe, column=2, row=0)
self._calendar.pack(in_=self, expand=1, fill='both', side='bottom')
def __config_calendar(self):
cols = self._cal.formatweekheader(3).split()
self._calendar['columns'] = cols
self._calendar.tag_configure('header', background='grey90')
self._calendar.insert('', 'end', values=cols, tag='header')
# adjust its columns width
font = tkinter.font.Font()
maxwidth = max(font.measure(col) for col in cols)
for col in cols:
self._calendar.column(col, width=maxwidth, minwidth=maxwidth,
anchor='e')
def __setup_selection(self, sel_bg, sel_fg):
self._font = tkinter.font.Font()
self._canvas = canvas = tk.Canvas(self._calendar,
background=sel_bg, borderwidth=0, highlightthickness=0)
canvas.text = canvas.create_text(0, 0, fill=sel_fg, anchor='w')
canvas.bind('<ButtonPress-1>', lambda evt: canvas.place_forget())
self._calendar.bind('<Configure>', lambda evt: canvas.place_forget())
self._calendar.bind('<ButtonPress-1>', self._pressed)
def __minsize(self, evt):
width, height = self._calendar.master.geometry().split('x')
height = height[:height.index('+')]
self._calendar.master.minsize(width, height)
def _build_calendar(self):
year, month = self._date.year, self._date.month
# update header text (Month, YEAR)
header = self._cal.formatmonthname(year, month, 0)
self._header['text'] = header.title()
# update calendar shown dates
cal = self._cal.monthdayscalendar(year, month)
for indx, item in enumerate(self._items):
week = cal[indx] if indx < len(cal) else []
fmt_week = [('%02d' % day) if day else '' for day in week]
self._calendar.item(item, values=fmt_week)
def _show_selection(self, text, bbox):
""" Configure canvas for a new selection. """
x, y, width, height = bbox
textw = self._font.measure(text)
canvas = self._canvas
canvas.configure(width=width, height=height)
canvas.coords(canvas.text, width - textw, height / 2 - 1)
canvas.itemconfigure(canvas.text, text=text)
canvas.place(in_=self._calendar, x=x, y=y)
# Callbacks
def _pressed(self, evt):
""" Clicked somewhere in the calendar. """
x, y, widget = evt.x, evt.y, evt.widget
item = widget.identify_row(y)
column = widget.identify_column(x)
if not column or not item in self._items:
# clicked in the weekdays row or just outside the columns
return
item_values = widget.item(item)['values']
if not len(item_values): # row is empty for this month
return
text = item_values[int(column[1]) - 1]
if not text: # date is empty
return
bbox = widget.bbox(item, column)
if not bbox: # calendar not visible yet
return
# update and then show selection
text = '%02d' % text
self._selection = (text, item, column)
self._show_selection(text, bbox)
year, month = self._date.year, self._date.month
now = self.datetime.now()
try:
self._TargetElement.Update(
self.datetime(year, month, int(self._selection[0]), now.hour, now.minute, now.second).strftime(
self.format))
if self._TargetElement.ChangeSubmits:
self._TargetElement.ParentForm.LastButtonClicked = self._TargetElement.Key
self._TargetElement.ParentForm.FormRemainedOpen = True
self._TargetElement.ParentForm.TKroot.quit() # kick the users out of the mainloop
except:
pass
if self.close_when_chosen:
self._master.destroy()
def _prev_month(self):
"""Updated calendar to show the previous month."""
self._canvas.place_forget()
self._date = self._date - self.timedelta(days=1)
self._date = self.datetime(self._date.year, self._date.month, 1)
self._build_calendar() # reconstuct calendar
def _next_month(self):
"""Update calendar to show the next month."""
self._canvas.place_forget()
year, month = self._date.year, self._date.month
self._date = self._date + self.timedelta(
days=calendar.monthrange(year, month)[1] + 1)
self._date = self.datetime(self._date.year, self._date.month, 1)
self._build_calendar() # reconstruct calendar
# Properties
@property
def selection(self):
if not self._selection:
return None
year, month = self._date.year, self._date.month
return self.datetime(year, month, int(self._selection[0]))
# ---------------------------------------------------------------------- #
# Menu #
# ---------------------------------------------------------------------- #
class Menu(Element):
"""
Menu Element is the Element that provides a Menu Bar that goes across the top of the window, just below titlebar.
Here is an example layout. The "&" are shortcut keys ALT+key.
Is a List of - "Item String" + List
Where Item String is what will be displayed on the Menubar itself.
The List that follows the item represents the items that are shown then Menu item is clicked
Notice how an "entry" in a mennu can be a list which means it branches out and shows another menu, etc. (resursive)
menu_def = [['&File', ['!&Open', '&Save::savekey', '---', '&Properties', 'E&xit']],
['!&Edit', ['!&Paste', ['Special', 'Normal', ], 'Undo'], ],
['&Debugger', ['Popout', 'Launch Debugger']],
['&Toolbar', ['Command &1', 'Command &2', 'Command &3', 'Command &4']],
['&Help', '&About...'], ]
Finally, "keys" can be added to entries so make them unique. The "Save" entry has a key associated with it. You
can see it has a "::" which signifies the beginning of a key. The user will not see the key portion when the
menu is shown. The key portion is returned as part of the event.
"""
def __init__(self, menu_definition, background_color=None, text_color=None, disabled_text_color=None, size=(None, None), s=(None, None), tearoff=False,
font=None, pad=None, p=None, key=None, k=None, visible=True, metadata=None):
"""
:param menu_definition: The Menu definition specified using lists (docs explain the format)
:type menu_definition: List[List[Tuple[str, List[str]]]
:param background_color: color of the background
:type background_color: (str)
:param text_color: element's text color. Can be in #RRGGBB format or a color name "black"
:type text_color: (str)
:param disabled_text_color: color to use for text when item is disabled. Can be in #RRGGBB format or a color name "black"
:type disabled_text_color: (str)
:param size: Not used in the tkinter port
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None)
:param tearoff: if True, then can tear the menu off from the window ans use as a floating window. Very cool effect
:type tearoff: (bool)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.BackgroundColor = background_color if background_color is not None else theme_input_background_color()
self.TextColor = text_color if text_color is not None else theme_input_text_color()
self.DisabledTextColor = disabled_text_color if disabled_text_color is not None else COLOR_SYSTEM_DEFAULT
self.MenuDefinition = copy.deepcopy(menu_definition)
self.Widget = self.TKMenu = None # type: tk.Menu
self.MenuItemChosen = None
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
super().__init__(ELEM_TYPE_MENUBAR, background_color=self.BackgroundColor, text_color=self.TextColor, size=sz, pad=pad, key=key, visible=visible,
font=font, metadata=metadata)
# super().__init__(ELEM_TYPE_MENUBAR, background_color=COLOR_SYSTEM_DEFAULT, text_color=COLOR_SYSTEM_DEFAULT, size=sz, pad=pad, key=key, visible=visible, font=None, metadata=metadata)
self.Tearoff = tearoff
return
def _MenuItemChosenCallback(self, item_chosen): # Menu Menu Item Chosen Callback
"""
Not user callable. Called when some end-point on the menu (an item) has been clicked. Send the information back to the application as an event. Before event can be sent
:param item_chosen: the text that was clicked on / chosen from the menu
:type item_chosen: (str)
"""
# print('IN MENU ITEM CALLBACK', item_chosen)
self.MenuItemChosen = item_chosen
self.ParentForm.LastButtonClicked = item_chosen
self.ParentForm.FormRemainedOpen = True
# if self.ParentForm.CurrentlyRunningMainloop:
# self.ParentForm.TKroot.quit() # kick the users out of the mainloop
_exit_mainloop(self.ParentForm)
def update(self, menu_definition=None, visible=None):
"""
Update a menubar - can change the menu definition and visibility. The entire menu has to be specified
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param menu_definition: The menu definition list
:type menu_definition: List[List[Tuple[str, List[str]]]
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if menu_definition is not None:
self.MenuDefinition = copy.deepcopy(menu_definition)
self.TKMenu = tk.Menu(self.ParentForm.TKroot, tearoff=self.Tearoff, tearoffcommand=self._tearoff_menu_callback) # create the menubar
menubar = self.TKMenu
for menu_entry in self.MenuDefinition:
baritem = tk.Menu(menubar, tearoff=self.Tearoff, tearoffcommand=self._tearoff_menu_callback)
if self.BackgroundColor not in (COLOR_SYSTEM_DEFAULT, None):
baritem.config(bg=self.BackgroundColor)
if self.TextColor not in (COLOR_SYSTEM_DEFAULT, None):
baritem.config(fg=self.TextColor)
if self.DisabledTextColor not in (COLOR_SYSTEM_DEFAULT, None):
baritem.config(disabledforeground=self.DisabledTextColor)
if self.Font is not None:
baritem.config(font=self.Font)
if self.Font is not None:
baritem.config(font=self.Font)
pos = menu_entry[0].find(MENU_SHORTCUT_CHARACTER)
# print(pos)
if pos != -1:
if pos == 0 or menu_entry[0][pos - len(MENU_SHORTCUT_CHARACTER)] != "\\":
menu_entry[0] = menu_entry[0][:pos] + menu_entry[0][pos + len(MENU_SHORTCUT_CHARACTER):]
if menu_entry[0][0] == MENU_DISABLED_CHARACTER:
menubar.add_cascade(label=menu_entry[0][len(MENU_DISABLED_CHARACTER):], menu=baritem, underline=pos)
menubar.entryconfig(menu_entry[0][len(MENU_DISABLED_CHARACTER):], state='disabled')
else:
menubar.add_cascade(label=menu_entry[0], menu=baritem, underline=pos)
if len(menu_entry) > 1:
AddMenuItem(baritem, menu_entry[1], self)
if visible is False:
self.ParentForm.TKroot.configure(menu=[]) # this will cause the menubar to disappear
elif self.TKMenu is not None:
self.ParentForm.TKroot.configure(menu=self.TKMenu)
if visible is not None:
self._visible = visible
Update = update
MenuBar = Menu # another name for Menu to make it clear it's the Menu Bar
Menubar = Menu # another name for Menu to make it clear it's the Menu Bar
# ---------------------------------------------------------------------- #
# Table #
# ---------------------------------------------------------------------- #
class Table(Element):
def __init__(self, values, headings=None, visible_column_map=None, col_widths=None, def_col_width=10,
auto_size_columns=True, max_col_width=20, select_mode=None, display_row_numbers=False, num_rows=None,
row_height=None, font=None, justification='right', text_color=None, background_color=None,
alternating_row_color=None, selected_row_colors=(None, None), header_text_color=None, header_background_color=None, header_font=None,
row_colors=None, vertical_scroll_only=True, hide_vertical_scroll=False,
size=(None, None), s=(None, None), change_submits=False, enable_events=False, enable_click_events=False, right_click_selects=False, bind_return_key=False, pad=None, p=None,
key=None, k=None, tooltip=None, right_click_menu=None, expand_x=False, expand_y=False, visible=True, metadata=None):
"""
:param values: Your table data represented as a 2-dimensions table... a list of rows, with each row representing a row in your table.
:type values: List[List[str | int | float]]
:param headings: The headings to show on the top line
:type headings: List[str]
:param visible_column_map: One entry for each column. False indicates the column is not shown
:type visible_column_map: List[bool]
:param col_widths: Number of characters that each column will occupy
:type col_widths: List[int]
:param def_col_width: Default column width in characters
:type def_col_width: (int)
:param auto_size_columns: if True columns will be sized automatically
:type auto_size_columns: (bool)
:param max_col_width: Maximum width for all columns in characters
:type max_col_width: (int)
:param select_mode: Select Mode. Valid values start with "TABLE_SELECT_MODE_". Valid values are: TABLE_SELECT_MODE_NONE TABLE_SELECT_MODE_BROWSE TABLE_SELECT_MODE_EXTENDED
:type select_mode: (enum)
:param display_row_numbers: if True, the first column of the table will be the row #
:type display_row_numbers: (bool)
:param num_rows: The number of rows of the table to display at a time
:type num_rows: (int)
:param row_height: height of a single row in pixels
:type row_height: (int)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param justification: 'left', 'right', 'center' are valid choices
:type justification: (str)
:param text_color: color of the text
:type text_color: (str)
:param background_color: color of background
:type background_color: (str)
:param alternating_row_color: if set then every other row will have this color in the background.
:type alternating_row_color: (str)
:param selected_row_colors: Sets the text color and background color for a selected row. Same format as button colors - tuple ('red', 'yellow') or string 'red on yellow'. Defaults to theme's button color
:type selected_row_colors: str or (str, str)
:param header_text_color: sets the text color for the header
:type header_text_color: (str)
:param header_background_color: sets the background color for the header
:type header_background_color: (str)
:param header_font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type header_font: (str or (str, int[, str]) or None)
:param row_colors: list of tuples of (row, background color) OR (row, foreground color, background color). Sets the colors of listed rows to the color(s) provided (note the optional foreground color)
:type row_colors: List[Tuple[int, str] | Tuple[Int, str, str]]
:param vertical_scroll_only: if True only the vertical scrollbar will be visible
:type vertical_scroll_only: (bool)
:param hide_vertical_scroll: if True vertical scrollbar will be hidden
:type hide_vertical_scroll: (bool)
:param size: DO NOT USE! Use num_rows instead
:type size: (int, int)
:param change_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type change_submits: (bool)
:param enable_events: Turns on the element specific events. Table events happen when row is clicked
:type enable_events: (bool)
:param enable_click_events: Turns on the element click events that will give you (row, col) click data when the table is clicked
:type enable_click_events: (bool)
:param right_click_selects: If True, then right clicking a row will select that row if multiple rows are not currently selected
:type right_click_selects: (bool)
:param bind_return_key: if True, pressing return key will cause event coming from Table, ALSO a left button double click will generate an event if this parameter is True
:type bind_return_key: (bool)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.Values = values
self.ColumnHeadings = headings
self.ColumnsToDisplay = visible_column_map
self.ColumnWidths = col_widths
self.MaxColumnWidth = max_col_width
self.DefaultColumnWidth = def_col_width
self.AutoSizeColumns = auto_size_columns
self.BackgroundColor = background_color if background_color is not None else DEFAULT_BACKGROUND_COLOR
self.TextColor = text_color
self.HeaderTextColor = header_text_color if header_text_color is not None else LOOK_AND_FEEL_TABLE[CURRENT_LOOK_AND_FEEL]['TEXT_INPUT']
self.HeaderBackgroundColor = header_background_color if header_background_color is not None else LOOK_AND_FEEL_TABLE[CURRENT_LOOK_AND_FEEL]['INPUT']
self.HeaderFont = header_font
self.Justification = justification
self.InitialState = None
self.SelectMode = select_mode
self.DisplayRowNumbers = display_row_numbers
self.NumRows = num_rows if num_rows is not None else size[1]
self.RowHeight = row_height
self.Widget = self.TKTreeview = None # type: ttk.Treeview
self.AlternatingRowColor = alternating_row_color
self.VerticalScrollOnly = vertical_scroll_only
self.HideVerticalScroll = hide_vertical_scroll
self.SelectedRows = []
self.ChangeSubmits = change_submits or enable_events
self.BindReturnKey = bind_return_key
self.StartingRowNumber = 0 # When displaying row numbers, where to start
self.RowHeaderText = 'Row'
self.enable_click_events = enable_click_events
self.right_click_selects = right_click_selects
self.last_clicked_position = (None, None)
if selected_row_colors == (None, None):
# selected_row_colors = DEFAULT_TABLE_AND_TREE_SELECTED_ROW_COLORS
selected_row_colors = theme_button_color()
else:
try:
if isinstance(selected_row_colors, str):
selected_row_colors = selected_row_colors.split(' on ')
except Exception as e:
print('* Table Element Warning * you messed up with color formatting of Selected Row Color', e)
self.SelectedRowColors = selected_row_colors
self.RightClickMenu = right_click_menu
self.RowColors = row_colors
self.tree_ids = [] # ids returned when inserting items into table - will use to delete colors
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(ELEM_TYPE_TABLE, text_color=text_color, background_color=background_color, font=font,
size=sz, pad=pad, key=key, tooltip=tooltip, visible=visible, metadata=metadata)
return
def update(self, values=None, num_rows=None, visible=None, select_rows=None, alternating_row_color=None, row_colors=None):
"""
Changes some of the settings for the Table Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param values: A new 2-dimensional table to show
:type values: List[List[str | int | float]]
:param num_rows: How many rows to display at a time
:type num_rows: (int)
:param visible: if True then will be visible
:type visible: (bool)
:param select_rows: List of rows to select as if user did
:type select_rows: List[int]
:param alternating_row_color: the color to make every other row
:type alternating_row_color: (str)
:param row_colors: list of tuples of (row, background color) OR (row, foreground color, background color). Changes the colors of listed rows to the color(s) provided (note the optional foreground color)
:type row_colors: List[Tuple[int, str] | Tuple[Int, str, str]]
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if values is not None:
for id in self.tree_ids:
self.TKTreeview.item(id, tags=())
if self.BackgroundColor is not None and self.BackgroundColor != COLOR_SYSTEM_DEFAULT:
self.TKTreeview.tag_configure(id, background=self.BackgroundColor)
else:
self.TKTreeview.tag_configure(id, background='#FFFFFF', foreground='#000000')
if self.TextColor is not None and self.TextColor != COLOR_SYSTEM_DEFAULT:
self.TKTreeview.tag_configure(id, foreground=self.TextColor)
else:
self.TKTreeview.tag_configure(id, foreground='#000000')
children = self.TKTreeview.get_children()
for i in children:
self.TKTreeview.detach(i)
self.TKTreeview.delete(i)
children = self.TKTreeview.get_children()
self.tree_ids = []
for i, value in enumerate(values):
if self.DisplayRowNumbers:
value = [i + self.StartingRowNumber] + value
id = self.TKTreeview.insert('', 'end', text=value, iid=i + 1, values=value, tag=i)
if self.BackgroundColor is not None and self.BackgroundColor != COLOR_SYSTEM_DEFAULT:
self.TKTreeview.tag_configure(id, background=self.BackgroundColor)
else:
self.TKTreeview.tag_configure(id, background='#FFFFFF')
self.tree_ids.append(id)
self.Values = values
self.SelectedRows = []
if visible is False:
self.TKTreeview.pack_forget()
elif visible is True:
self.TKTreeview.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if num_rows is not None:
self.TKTreeview.config(height=num_rows)
if select_rows is not None:
rows_to_select = [i + 1 for i in select_rows]
self.TKTreeview.selection_set(rows_to_select)
if alternating_row_color is not None: # alternating colors
self.AlternatingRowColor = alternating_row_color
if self.AlternatingRowColor is not None:
for row in range(0, len(self.Values), 2):
self.TKTreeview.tag_configure(row, background=self.AlternatingRowColor)
if row_colors is not None: # individual row colors
self.RowColors = row_colors
for row_def in self.RowColors:
if len(row_def) == 2: # only background is specified
self.TKTreeview.tag_configure(row_def[0], background=row_def[1])
else:
self.TKTreeview.tag_configure(row_def[0], background=row_def[2], foreground=row_def[1])
if visible is not None:
self._visible = visible
def _treeview_selected(self, event):
"""
Not user callable. Callback function that is called when something is selected from Table.
Stores the selected rows in Element as they are being selected. If events enabled, then returns from Read
:param event: event information from tkinter
:type event: (unknown)
"""
# print('**-- in treeview selected --**')
selections = self.TKTreeview.selection()
self.SelectedRows = [int(x) - 1 for x in selections]
if self.ChangeSubmits:
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = ''
self.ParentForm.FormRemainedOpen = True
# if self.ParentForm.CurrentlyRunningMainloop:
# self.ParentForm.TKroot.quit()
_exit_mainloop(self.ParentForm)
def _treeview_double_click(self, event):
"""
Not user callable. Callback function that is called when something is selected from Table.
Stores the selected rows in Element as they are being selected. If events enabled, then returns from Read
:param event: event information from tkinter
:type event: (unknown)
"""
selections = self.TKTreeview.selection()
self.SelectedRows = [int(x) - 1 for x in selections]
if self.BindReturnKey: # Signifies BOTH a return key AND a double click
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = ''
self.ParentForm.FormRemainedOpen = True
# if self.ParentForm.CurrentlyRunningMainloop:
# self.ParentForm.TKroot.quit()
_exit_mainloop(self.ParentForm)
def _table_clicked(self, event):
"""
Not user callable. Callback function that is called a click happens on a table.
Stores the selected rows in Element as they are being selected. If events enabled, then returns from Read
:param event: event information from tkinter
:type event: (unknown)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
# popup(obj_to_string_single_obj(event))
try:
region = self.Widget.identify('region', event.x, event.y)
if region == 'heading':
row = -1
elif region == 'cell':
row = int(self.Widget.identify_row(event.y))-1
elif region == 'separator':
row = None
else:
row = None
col_identified = self.Widget.identify_column(event.x)
if col_identified: # Sometimes tkinter returns a value of '' which would cause an error if cast to an int
column = int(self.Widget.identify_column(event.x)[1:])-1-int(self.DisplayRowNumbers is True)
else:
column = None
except Exception as e:
warnings.warn('Error getting table click data for table with key= {}\nError: {}'.format(self.Key, e), UserWarning)
if not SUPPRESS_ERROR_POPUPS:
_error_popup_with_traceback('Unable to complete operation getting the clicked event for table with key {}'.format(self.Key), _create_error_message(), e, 'Event data:', obj_to_string_single_obj(event))
row = column = None
self.last_clicked_position = (row, column)
# update the rows being selected if appropriate
self.ParentForm.TKroot.update()
# self.TKTreeview.()
selections = self.TKTreeview.selection()
if self.right_click_selects and len(selections) <= 1:
if (event.num == 3 and not running_mac()) or (event.num == 2 and running_mac()):
if row != -1 and row is not None:
selections = [row+1]
self.TKTreeview.selection_set(selections)
# print(selections)
self.SelectedRows = [int(x) - 1 for x in selections]
# print('The new selected rows = ', self.SelectedRows)
if self.enable_click_events is True:
if self.Key is not None:
self.ParentForm.LastButtonClicked = (self.Key, TABLE_CLICKED_INDICATOR, (row, column))
else:
self.ParentForm.LastButtonClicked = ''
self.ParentForm.FormRemainedOpen = True
_exit_mainloop(self.ParentForm)
def get(self):
"""
Dummy function for tkinter port. In the Qt port you can read back the values in the table in case they were
edited. Don't know yet how to enable editing of a Tree in tkinter so just returning the values provided by
user when Table was created or Updated.
:return: the current table values (for now what was originally provided up updated)
:rtype: List[List[Any]]
"""
return self.Values
def get_last_clicked_position(self):
"""
Returns a tuple with the row and column of the cell that was last clicked.
Headers will have a row == -1 and the Row Number Column (if present) will have a column == -1
:return: The (row,col) position of the last cell clicked in the table
:rtype: (int | None, int | None)
"""
return self.last_clicked_position
Update = update
Get = get
# ---------------------------------------------------------------------- #
# Tree #
# ---------------------------------------------------------------------- #
class Tree(Element):
"""
Tree Element - Presents data in a tree-like manner, much like a file/folder browser. Uses the TreeData class
to hold the user's data and pass to the element for display.
"""
def __init__(self, data=None, headings=None, visible_column_map=None, col_widths=None, col0_width=10, col0_heading='',
def_col_width=10, auto_size_columns=True, max_col_width=20, select_mode=None, show_expanded=False,
change_submits=False, enable_events=False, font=None, justification='right', text_color=None,
background_color=None, selected_row_colors=(None, None), header_text_color=None, header_background_color=None, header_font=None, num_rows=None,
row_height=None, pad=None, p=None, key=None, k=None, tooltip=None,
right_click_menu=None, expand_x=False, expand_y=False, visible=True, metadata=None):
"""
:param data: The data represented using a PySimpleGUI provided TreeData class
:type data: (TreeData)
:param headings: List of individual headings for each column
:type headings: List[str]
:param visible_column_map: Determines if a column should be visible. If left empty, all columns will be shown
:type visible_column_map: List[bool]
:param col_widths: List of column widths so that individual column widths can be controlled
:type col_widths: List[int]
:param col0_width: Size of Column 0 which is where the row numbers will be optionally shown
:type col0_width: (int)
:param col0_heading: Text to be shown in the header for the left-most column
:type col0_heading: (str)
:param def_col_width: default column width
:type def_col_width: (int)
:param auto_size_columns: if True, the size of a column is determined using the contents of the column
:type auto_size_columns: (bool)
:param max_col_width: the maximum size a column can be
:type max_col_width: (int)
:param select_mode: Use same values as found on Table Element. Valid values include: TABLE_SELECT_MODE_NONE TABLE_SELECT_MODE_BROWSE TABLE_SELECT_MODE_EXTENDED
:type select_mode: (enum)
:param show_expanded: if True then the tree will be initially shown with all nodes completely expanded
:type show_expanded: (bool)
:param change_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type change_submits: (bool)
:param enable_events: Turns on the element specific events. Tree events happen when row is clicked
:type enable_events: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param justification: 'left', 'right', 'center' are valid choices
:type justification: (str)
:param text_color: color of the text
:type text_color: (str)
:param background_color: color of background
:type background_color: (str)
:param selected_row_colors: Sets the text color and background color for a selected row. Same format as button colors - tuple ('red', 'yellow') or string 'red on yellow'. Defaults to theme's button color
:type selected_row_colors: str or (str, str)
:param header_text_color: sets the text color for the header
:type header_text_color: (str)
:param header_background_color: sets the background color for the header
:type header_background_color: (str)
:param header_font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type header_font: (str or (str, int[, str]) or None)
:param num_rows: The number of rows of the table to display at a time
:type num_rows: (int)
:param row_height: height of a single row in pixels
:type row_height: (int)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[str] | str]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.image_dict = {}
self.TreeData = data
self.ColumnHeadings = headings
self.ColumnsToDisplay = visible_column_map
self.ColumnWidths = col_widths
self.MaxColumnWidth = max_col_width
self.DefaultColumnWidth = def_col_width
self.AutoSizeColumns = auto_size_columns
self.BackgroundColor = background_color if background_color is not None else DEFAULT_BACKGROUND_COLOR
self.TextColor = text_color
self.HeaderTextColor = header_text_color if header_text_color is not None else LOOK_AND_FEEL_TABLE[CURRENT_LOOK_AND_FEEL]['TEXT_INPUT']
self.HeaderBackgroundColor = header_background_color if header_background_color is not None else LOOK_AND_FEEL_TABLE[CURRENT_LOOK_AND_FEEL]['INPUT']
if selected_row_colors == (None, None):
# selected_row_colors = DEFAULT_TABLE_AND_TREE_SELECTED_ROW_COLORS
selected_row_colors = theme_button_color()
else:
try:
if isinstance(selected_row_colors, str):
selected_row_colors = selected_row_colors.split(' on ')
except Exception as e:
print('* Table Element Warning * you messed up with color formatting of Selected Row Color', e)
self.SelectedRowColors = selected_row_colors
self.HeaderFont = header_font
self.Justification = justification
self.InitialState = None
self.SelectMode = select_mode
self.ShowExpanded = show_expanded
self.NumRows = num_rows
self.Col0Width = col0_width
self.col0_heading = col0_heading
self.TKTreeview = None # type: ttk.Treeview
self.element_frame = None # type: tk.Frame
self.SelectedRows = []
self.ChangeSubmits = change_submits or enable_events
self.RightClickMenu = right_click_menu
self.RowHeight = row_height
self.IconList = {}
self.IdToKey = {'': ''}
self.KeyToID = {'': ''}
key = key if key is not None else k
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(ELEM_TYPE_TREE, text_color=text_color, background_color=background_color, font=font, pad=pad, key=key, tooltip=tooltip,
visible=visible, metadata=metadata)
return
def _treeview_selected(self, event):
"""
Not a user function. Callback function that happens when an item is selected from the tree. In this
method, it saves away the reported selections so they can be properly returned.
:param event: An event parameter passed in by tkinter. Not used
:type event: (Any)
"""
selections = self.TKTreeview.selection()
# self.SelectedRows = [x for x in selections]
self.SelectedRows = [self.IdToKey[x] for x in selections]
if self.ChangeSubmits:
MyForm = self.ParentForm
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = ''
self.ParentForm.FormRemainedOpen = True
# if self.ParentForm.CurrentlyRunningMainloop:
# self.ParentForm.TKroot.quit()
_exit_mainloop(self.ParentForm)
def add_treeview_data(self, node):
"""
Not a user function. Recursive method that inserts tree data into the tkinter treeview widget.
:param node: The node to insert. Will insert all nodes from starting point downward, recursively
:type node: (TreeData)
"""
if node.key != '':
if node.icon:
try:
if node.icon not in self.image_dict:
if type(node.icon) is bytes:
photo = tk.PhotoImage(data=node.icon)
else:
photo = tk.PhotoImage(file=node.icon)
self.image_dict[node.icon] = photo
else:
photo = self.image_dict.get(node.icon)
node.photo = photo
id = self.TKTreeview.insert(self.KeyToID[node.parent], 'end', iid=None, text=node.text,
values=node.values, open=self.ShowExpanded, image=node.photo)
self.IdToKey[id] = node.key
self.KeyToID[node.key] = id
except:
self.photo = None
else:
id = self.TKTreeview.insert(self.KeyToID[node.parent], 'end', iid=None, text=node.text,
values=node.values, open=self.ShowExpanded)
self.IdToKey[id] = node.key
self.KeyToID[node.key] = id
for node in node.children:
self.add_treeview_data(node)
def update(self, values=None, key=None, value=None, text=None, icon=None, visible=None):
"""
Changes some of the settings for the Tree Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param values: Representation of the tree
:type values: (TreeData)
:param key: identifies a particular item in tree to update
:type key: str | int | tuple | object
:param value: sets the node identified by key to a particular value
:type value: (Any)
:param text: sets the node identified by key to this string
:type text: (str)
:param icon: can be either a base64 icon or a filename for the icon
:type icon: bytes | str
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if values is not None:
children = self.TKTreeview.get_children()
for i in children:
self.TKTreeview.detach(i)
self.TKTreeview.delete(i)
children = self.TKTreeview.get_children()
self.TreeData = values
self.IdToKey = {'': ''}
self.KeyToID = {'': ''}
self.add_treeview_data(self.TreeData.root_node)
self.SelectedRows = []
if key is not None:
for id in self.IdToKey.keys():
if key == self.IdToKey[id]:
break
else:
id = None
print('** Key not found **')
else:
id = None
if id:
# item = self.TKTreeview.item(id)
if value is not None:
self.TKTreeview.item(id, values=value)
if text is not None:
self.TKTreeview.item(id, text=text)
if icon is not None:
try:
if type(icon) is bytes:
photo = tk.PhotoImage(data=icon)
else:
photo = tk.PhotoImage(file=icon)
self.TKTreeview.item(id, image=photo)
self.IconList[key] = photo # save so that it's not deleted (save reference)
except:
pass
# item = self.TKTreeview.item(id)
if visible is False:
self.TKTreeview.pack_forget()
elif visible is True:
self.TKTreeview.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if visible is not None:
self._visible = visible
return self
Update = update
class TreeData(object):
"""
Class that user fills in to represent their tree data. It's a very simple tree representation with a root "Node"
with possibly one or more children "Nodes". Each Node contains a key, text to display, list of values to display
and an icon. The entire tree is built using a single method, Insert. Nothing else is required to make the tree.
"""
class Node(object):
"""
Contains information about the individual node in the tree
"""
def __init__(self, parent, key, text, values, icon=None):
"""
Represents a node within the TreeData class
:param parent: The parent Node
:type parent: (TreeData.Node)
:param key: Used to uniquely identify this node
:type key: str | int | tuple | object
:param text: The text that is displayed at this node's location
:type text: (str)
:param values: The list of values that are displayed at this node
:type values: List[Any]
:param icon: just a icon
:type icon: str | bytes
"""
self.parent = parent # type: TreeData.Node
self.children = [] # type: List[TreeData.Node]
self.key = key # type: str
self.text = text # type: str
self.values = values # type: List[Any]
self.icon = icon # type: str | bytes
def _Add(self, node):
self.children.append(node)
def __init__(self):
"""
Instantiate the object, initializes the Tree Data, creates a root node for you
"""
self.tree_dict = {} # type: Dict[str, TreeData.Node]
self.root_node = self.Node("", "", 'root', [], None) # The root node
self.tree_dict[""] = self.root_node # Start the tree out with the root node
def _AddNode(self, key, node):
"""
Adds a node to tree dictionary (not user callable)
:param key: Uniquely identifies this Node
:type key: (str)
:param node: Node being added
:type node: (TreeData.Node)
"""
self.tree_dict[key] = node
def insert(self, parent, key, text, values, icon=None):
"""
Inserts a node into the tree. This is how user builds their tree, by Inserting Nodes
This is the ONLY user callable method in the TreeData class
:param parent: the parent Node
:type parent: (Node)
:param key: Used to uniquely identify this node
:type key: str | int | tuple | object
:param text: The text that is displayed at this node's location
:type text: (str)
:param values: The list of values that are displayed at this node
:type values: List[Any]
:param icon: icon
:type icon: str | bytes
"""
node = self.Node(parent, key, text, values, icon)
self.tree_dict[key] = node
parent_node = self.tree_dict[parent]
parent_node._Add(node)
def __repr__(self):
"""
Converts the TreeData into a printable version, nicely formatted
:return: (str) A formatted, text version of the TreeData
:rtype:
"""
return self._NodeStr(self.root_node, 1)
def _NodeStr(self, node, level):
"""
Does the magic of converting the TreeData into a nicely formatted string version
:param node: The node to begin printing the tree
:type node: (TreeData.Node)
:param level: The indentation level for string formatting
:type level: (int)
"""
return '\n'.join(
[str(node.key) + ' : ' + str(node.text) + ' [ ' + ', '.join([str(v) for v in node.values]) +' ]'] +
[' ' * 4 * level + self._NodeStr(child, level + 1) for child in node.children])
Insert = insert
# ---------------------------------------------------------------------- #
# Error Element #
# ---------------------------------------------------------------------- #
class ErrorElement(Element):
"""
A "dummy Element" that is returned when there are error conditions, like trying to find an element that's invalid
"""
def __init__(self, key=None, metadata=None):
"""
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key:
"""
self.Key = key
super().__init__(ELEM_TYPE_ERROR, key=key, metadata=metadata)
def update(self, silent_on_error=True, *args, **kwargs):
"""
Update method for the Error Element, an element that should not be directly used by developer
:param silent_on_error: if False, then a Popup window will be shown
:type silent_on_error: (bool)
:param *args: meant to "soak up" any normal parameters passed in
:type *args: (Any)
:param **kwargs: meant to "soak up" any keyword parameters that were passed in
:type **kwargs: (Any)
:return: returns 'self' so call can be chained
:rtype: (ErrorElement)
"""
print('** Your update is being ignored because you supplied a bad key earlier **')
return self
def get(self):
"""
One of the method names found in other Elements. Used here to return an error string in case it's called
:return: A warning text string.
:rtype: (str)
"""
return 'This is NOT a valid Element!\nSTOP trying to do things with it or I will have to crash at some point!'
Get = get
Update = update
# ---------------------------------------------------------------------- #
# Stretch Element #
# ---------------------------------------------------------------------- #
# This is for source code compatibility with tkinter version. No tkinter equivalent but you can fake it using a Text element that expands in the X direction
def Stretch(background_color=None):
"""
Acts like a Stretch element found in the Qt port.
Used in a Horizontal fashion. Placing one on each side of an element will enter the element.
Place one to the left and the element to the right will be right justified. See VStretch for vertical type
:param background_color: color of background may be needed because of how this is implemented
:type background_color: (str)
:return: (Text)
"""
return Text(font='_ 1', background_color=background_color, pad=(0,0), expand_x=True)
Push = Stretch
P = Stretch
def VStretch(background_color=None):
"""
Acts like a Stretch element found in the Qt port.
Used in a Vertical fashion.
:param background_color: color of background may be needed because of how this is implemented
:type background_color: (str)
:return: (Text)
"""
return Text(font='_ 1', background_color=background_color, pad=(0,0), expand_y=True)
VPush = VStretch
VP = VStretch
# ------------------------------------------------------------------------- #
# Window CLASS #
# ------------------------------------------------------------------------- #
class Window:
"""
Represents a single Window
"""
NumOpenWindows = 0
_user_defined_icon = None
hidden_master_root = None # type: tk.Tk
_animated_popup_dict = {} # type: Dict
_active_windows = {} # type: Dict[Window, tk.Tk()]
_move_all_windows = False # if one window moved, they will move
_window_that_exited = None # type: Window
_root_running_mainloop = None # type: tk.Tk() # (may be the hidden root or a window's root)
_timeout_key = None
_TKAfterID = None # timer that is used to run reads with timeouts
_window_running_mainloop = None # The window that is running the mainloop
_container_element_counter = 0 # used to get a number of Container Elements (Frame, Column, Tab)
_read_call_from_debugger = False
_timeout_0_counter = 0 # when timeout=0 then go through each window one at a time
_counter_for_ttk_widgets = 0
_floating_debug_window_build_needed = False
_main_debug_window_build_needed = False
def __init__(self, title, layout=None, default_element_size=None,
default_button_element_size=(None, None),
auto_size_text=None, auto_size_buttons=None, location=(None, None), relative_location=(None, None), size=(None, None),
element_padding=None, margins=(None, None), button_color=None, font=None,
progress_bar_color=(None, None), background_color=None, border_depth=None, auto_close=False,
auto_close_duration=DEFAULT_AUTOCLOSE_TIME, icon=None, force_toplevel=False,
alpha_channel=1, return_keyboard_events=False, use_default_focus=True, text_justification=None,
no_titlebar=False, grab_anywhere=False, grab_anywhere_using_control=True, keep_on_top=None, resizable=False, disable_close=False,
disable_minimize=False, right_click_menu=None, transparent_color=None, debugger_enabled=True,
right_click_menu_background_color=None, right_click_menu_text_color=None, right_click_menu_disabled_text_color=None,
right_click_menu_selected_colors=(None, None),
right_click_menu_font=None, right_click_menu_tearoff=False,
finalize=False, element_justification='left', ttk_theme=None, use_ttk_buttons=None, modal=False, enable_close_attempted_event=False,
titlebar_background_color=None, titlebar_text_color=None, titlebar_font=None, titlebar_icon=None,
use_custom_titlebar=None, scaling=None, metadata=None):
"""
:param title: The title that will be displayed in the Titlebar and on the Taskbar
:type title: (str)
:param layout: The layout for the window. Can also be specified in the Layout method
:type layout: List[List[Element]] | Tuple[Tuple[Element]]
:param default_element_size: size in characters (wide) and rows (high) for all elements in this window
:type default_element_size: (int, int) - (width, height)
:param default_button_element_size: (width, height) size in characters (wide) and rows (high) for all Button elements in this window
:type default_button_element_size: (int, int)
:param auto_size_text: True if Elements in Window should be sized to exactly fir the length of text
:type auto_size_text: (bool)
:param auto_size_buttons: True if Buttons in this Window should be sized to exactly fit the text on this.
:type auto_size_buttons: (bool)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param location: (x,y) location, in pixels, to locate the upper left corner of the window on the screen. Default is to center on screen.
:type location: (int, int)
:param size: (width, height) size in pixels for this window. Normally the window is autosized to fit contents, not set to an absolute size by the user. Try not to set this value. You risk, the contents being cut off, etc. Let the layout determine the window size instead
:type size: (int, int)
:param element_padding: Default amount of padding to put around elements in window (left/right, top/bottom) or ((left, right), (top, bottom)), or an int. If an int, then it's converted into a tuple (int, int)
:type element_padding: (int, int) or ((int, int),(int,int)) or int
:param margins: (left/right, top/bottom) Amount of pixels to leave inside the window's frame around the edges before your elements are shown.
:type margins: (int, int)
:param button_color: Default button colors for all buttons in the window
:type button_color: (str, str) or str
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param progress_bar_color: (bar color, background color) Sets the default colors for all progress bars in the window
:type progress_bar_color: (str, str)
:param background_color: color of background
:type background_color: (str)
:param border_depth: Default border depth (width) for all elements in the window
:type border_depth: (int)
:param auto_close: If True, the window will automatically close itself
:type auto_close: (bool)
:param auto_close_duration: Number of seconds to wait before closing the window
:type auto_close_duration: (int)
:param icon: Can be either a filename or Base64 value. For Windows if filename, it MUST be ICO format. For Linux, must NOT be ICO. Most portable is to use a Base64 of a PNG file. This works universally across all OS's
:type icon: (str | bytes)
:param force_toplevel: If True will cause this window to skip the normal use of a hidden master window
:type force_toplevel: (bool)
:param alpha_channel: Sets the opacity of the window. 0 = invisible 1 = completely visible. Values bewteen 0 & 1 will produce semi-transparent windows in SOME environments (The Raspberry Pi always has this value at 1 and cannot change.
:type alpha_channel: (float)
:param return_keyboard_events: if True key presses on the keyboard will be returned as Events from Read calls
:type return_keyboard_events: (bool)
:param use_default_focus: If True will use the default focus algorithm to set the focus to the "Correct" element
:type use_default_focus: (bool)
:param text_justification: Default text justification for all Text Elements in window
:type text_justification: 'left' | 'right' | 'center'
:param no_titlebar: If true, no titlebar nor frame will be shown on window. This means you cannot minimize the window and it will not show up on the taskbar
:type no_titlebar: (bool)
:param grab_anywhere: If True can use mouse to click and drag to move the window. Almost every location of the window will work except input fields on some systems
:type grab_anywhere: (bool)
:param grab_anywhere_using_control: If True can use CONTROL key + left mouse mouse to click and drag to move the window. DEFAULT is TRUE. Unlike normal grab anywhere, it works on all elements.
:type grab_anywhere_using_control: (bool)
:param keep_on_top: If True, window will be created on top of all other windows on screen. It can be bumped down if another window created with this parm
:type keep_on_top: (bool)
:param resizable: If True, allows the user to resize the window. Note the not all Elements will change size or location when resizing.
:type resizable: (bool)
:param disable_close: If True, the X button in the top right corner of the window will no work. Use with caution and always give a way out toyour users
:type disable_close: (bool)
:param disable_minimize: if True the user won't be able to minimize window. Good for taking over entire screen and staying that way.
:type disable_minimize: (bool)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param transparent_color: Any portion of the window that has this color will be completely transparent. You can even click through these spots to the window under this window.
:type transparent_color: (str)
:param debugger_enabled: If True then the internal debugger will be enabled
:type debugger_enabled: (bool)
:param right_click_menu_background_color: Background color for right click menus
:type right_click_menu_background_color: (str)
:param right_click_menu_text_color: Text color for right click menus
:type right_click_menu_text_color: (str)
:param right_click_menu_disabled_text_color: Text color for disabled right click menu items
:type right_click_menu_disabled_text_color: (str)
:param right_click_menu_selected_colors: Text AND background colors for a selected item. Can be a Tuple OR a color string. simplified-button-color-string "foreground on background". Can be a single color if want to set only the background. Normally a tuple, but can be a simplified-dual-color-string "foreground on background". Can be a single color if want to set only the background.
:type right_click_menu_selected_colors: (str, str) | str | Tuple
:param right_click_menu_font: Font for right click menus
:type right_click_menu_font: (str or (str, int[, str]) or None)
:param right_click_menu_tearoff: If True then all right click menus can be torn off
:type right_click_menu_tearoff: bool
:param finalize: If True then the Finalize method will be called. Use this rather than chaining .Finalize for cleaner code
:type finalize: (bool)
:param element_justification: All elements in the Window itself will have this justification 'left', 'right', 'center' are valid values
:type element_justification: (str)
:param ttk_theme: Set the tkinter ttk "theme" of the window. Default = DEFAULT_TTK_THEME. Sets all ttk widgets to this theme as their default
:type ttk_theme: (str)
:param use_ttk_buttons: Affects all buttons in window. True = use ttk buttons. False = do not use ttk buttons. None = use ttk buttons only if on a Mac
:type use_ttk_buttons: (bool)
:param modal: If True then this window will be the only window a user can interact with until it is closed
:type modal: (bool)
:param enable_close_attempted_event: If True then the window will not close when "X" clicked. Instead an event WINDOW_CLOSE_ATTEMPTED_EVENT if returned from window.read
:type enable_close_attempted_event: (bool)
:param titlebar_background_color: If custom titlebar indicated by use_custom_titlebar, then use this as background color
:type titlebar_background_color: (str | None)
:param titlebar_text_color: If custom titlebar indicated by use_custom_titlebar, then use this as text color
:type titlebar_text_color: (str | None)
:param titlebar_font: If custom titlebar indicated by use_custom_titlebar, then use this as title font
:type titlebar_font: (str or (str, int[, str]) or None)
:param titlebar_icon: If custom titlebar indicated by use_custom_titlebar, then use this as the icon (file or base64 bytes)
:type titlebar_icon: (bytes | str)
:param use_custom_titlebar: If True, then a custom titlebar will be used instead of the normal titlebar
:type use_custom_titlebar: bool
:param scaling: Apply scaling to the elements in the window. Can be set on a global basis using set_options
:type scaling: float
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self._metadata = None # type: Any
self.AutoSizeText = auto_size_text if auto_size_text is not None else DEFAULT_AUTOSIZE_TEXT
self.AutoSizeButtons = auto_size_buttons if auto_size_buttons is not None else DEFAULT_AUTOSIZE_BUTTONS
self.Title = str(title)
self.Rows = [] # a list of ELEMENTS for this row
self.DefaultElementSize = default_element_size if default_element_size is not None else DEFAULT_ELEMENT_SIZE
self.DefaultButtonElementSize = default_button_element_size if default_button_element_size != (
None, None) else DEFAULT_BUTTON_ELEMENT_SIZE
self.Location = location
self.RelativeLoction = relative_location
self.ButtonColor = button_color_to_tuple(button_color)
self.BackgroundColor = background_color if background_color else DEFAULT_BACKGROUND_COLOR
self.ParentWindow = None
self.Font = font if font else DEFAULT_FONT
self.RadioDict = {}
self.BorderDepth = border_depth
if icon:
self.WindowIcon = icon
elif Window._user_defined_icon is not None:
self.WindowIcon = Window._user_defined_icon
else:
self.WindowIcon = DEFAULT_WINDOW_ICON
self.AutoClose = auto_close
self.NonBlocking = False
self.TKroot = None # type: tk.Tk
self.TKrootDestroyed = False
self.CurrentlyRunningMainloop = False
self.FormRemainedOpen = False
self.TKAfterID = None
self.ProgressBarColor = progress_bar_color
self.AutoCloseDuration = auto_close_duration
self.RootNeedsDestroying = False
self.Shown = False
self.ReturnValues = None
self.ReturnValuesList = []
self.ReturnValuesDictionary = {}
self.DictionaryKeyCounter = 0
self.LastButtonClicked = None
self.LastButtonClickedWasRealtime = False
self.UseDictionary = False
self.UseDefaultFocus = use_default_focus
self.ReturnKeyboardEvents = return_keyboard_events
self.LastKeyboardEvent = None
self.TextJustification = text_justification
self.NoTitleBar = no_titlebar
self.Grab = grab_anywhere
self.GrabAnywhere = grab_anywhere
self.GrabAnywhereUsingControlKey = grab_anywhere_using_control
if keep_on_top is None and DEFAULT_KEEP_ON_TOP is not None:
keep_on_top = DEFAULT_KEEP_ON_TOP
elif keep_on_top is None:
keep_on_top = False
self.KeepOnTop = keep_on_top
self.ForceTopLevel = force_toplevel
self.Resizable = resizable
self._AlphaChannel = alpha_channel
self.Timeout = None
self.TimeoutKey = TIMEOUT_KEY
self.TimerCancelled = False
self.DisableClose = disable_close
self.DisableMinimize = disable_minimize
self._Hidden = False
self._Size = size
self.XFound = False
if element_padding is not None:
if isinstance(element_padding, int):
element_padding = (element_padding, element_padding)
if element_padding is None:
self.ElementPadding = DEFAULT_ELEMENT_PADDING
else:
self.ElementPadding = element_padding
self.RightClickMenu = right_click_menu
self.Margins = margins if margins != (None, None) else DEFAULT_MARGINS
self.ContainerElemementNumber = Window._GetAContainerNumber()
self.AllKeysDict = {}
self.TransparentColor = transparent_color
self.UniqueKeyCounter = 0
self.DebuggerEnabled = debugger_enabled
self.WasClosed = False
self.ElementJustification = element_justification
self.FocusSet = False
self.metadata = metadata
self.TtkTheme = ttk_theme or DEFAULT_TTK_THEME
self.UseTtkButtons = use_ttk_buttons if use_ttk_buttons is not None else USE_TTK_BUTTONS
self.user_bind_dict = {} # Used when user defines a tkinter binding using bind method - convert bind string to key modifier
self.user_bind_event = None # Used when user defines a tkinter binding using bind method - event data from tkinter
self.modal = modal
self.thread_queue = None # type: queue.Queue
self.thread_lock = None # type: threading.Lock
self.thread_timer = None # type: tk.Misc
self.thread_strvar = None # type: tk.StringVar
self.read_closed_window_count = 0
self.config_last_size = (None, None)
self.config_last_location = (None, None)
self.starting_window_position = (None, None)
self.not_completed_initial_movement = True
self.config_count = 0
self.saw_00 = False
self.maximized = False
self.right_click_menu_background_color = right_click_menu_background_color if right_click_menu_background_color is not None else theme_input_background_color()
self.right_click_menu_text_color = right_click_menu_text_color if right_click_menu_text_color is not None else theme_input_text_color()
self.right_click_menu_disabled_text_color = right_click_menu_disabled_text_color if right_click_menu_disabled_text_color is not None else COLOR_SYSTEM_DEFAULT
self.right_click_menu_font = right_click_menu_font if right_click_menu_font is not None else self.Font
self.right_click_menu_tearoff = right_click_menu_tearoff
self.auto_close_timer_needs_starting = False
self.finalize_in_progress = False
self.close_destroys_window = not enable_close_attempted_event if enable_close_attempted_event is not None else None
self.override_custom_titlebar = False
self.use_custom_titlebar = use_custom_titlebar or USE_CUSTOM_TITLEBAR
self.titlebar_background_color = titlebar_background_color
self.titlebar_text_color = titlebar_text_color
self.titlebar_font = titlebar_font
self.titlebar_icon = titlebar_icon
self.right_click_menu_selected_colors = _simplified_dual_color_to_tuple(right_click_menu_selected_colors,
(self.right_click_menu_background_color, self.right_click_menu_text_color))
self.TKRightClickMenu = None
self._grab_anywhere_ignore_these_list = []
self._grab_anywhere_include_these_list = []
self._has_custom_titlebar = use_custom_titlebar
self._mousex = self._mousey = 0
self._startx = self._starty = 0
self.scaling = scaling if scaling is not None else DEFAULT_SCALING
if self.use_custom_titlebar:
self.Margins = (0, 0)
self.NoTitleBar = True
if no_titlebar is True:
self.override_custom_titlebar = True
if layout is not None and type(layout) not in (list, tuple):
warnings.warn('Your layout is not a list or tuple... this is not good!')
if layout is not None:
self.Layout(layout)
if finalize:
self.Finalize()
if CURRENT_LOOK_AND_FEEL == 'Default':
print("Window will be a boring gray. Try removing the theme call entirely\n",
"You will get the default theme or the one set in global settings\n"
"If you seriously want this gray window and no more nagging, add theme('DefaultNoMoreNagging') or theme('Gray Gray Gray') for completely gray/System Defaults")
@classmethod
def _GetAContainerNumber(cls):
"""
Not user callable!
:return: A simple counter that makes each container element unique
:rtype:
"""
cls._container_element_counter += 1
return cls._container_element_counter
@classmethod
def _IncrementOpenCount(self):
"""
Not user callable! Increments the number of open windows
Note - there is a bug where this count easily gets out of sync. Issue has been opened already. No ill effects
"""
self.NumOpenWindows += 1
# print('+++++ INCREMENTING Num Open Windows = {} ---'.format(Window.NumOpenWindows))
@classmethod
def _DecrementOpenCount(self):
"""
Not user callable! Decrements the number of open windows
"""
self.NumOpenWindows -= 1 * (self.NumOpenWindows != 0) # decrement if not 0
# print('----- DECREMENTING Num Open Windows = {} ---'.format(Window.NumOpenWindows))
@classmethod
def get_screen_size(self):
"""
This is a "Class Method" meaning you call it by writing: width, height = Window.get_screen_size()
Returns the size of the "screen" as determined by tkinter. This can vary depending on your operating system and the number of monitors installed on your system. For Windows, the primary monitor's size is returns. On some multi-monitored Linux systems, the monitors are combined and the total size is reported as if one screen.
:return: Size of the screen in pixels as determined by tkinter
:rtype: (int, int)
"""
root = tk.Tk()
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
root.destroy()
return screen_width, screen_height
@property
def metadata(self):
"""
Metadata is available for all windows. You can set to any value.
:return: the current metadata value
:rtype: (Any)
"""
return self._metadata
@metadata.setter
def metadata(self, value):
"""
Metadata is available for all windows. You can set to any value.
:param value: Anything you want it to be
:type value: (Any)
"""
self._metadata = value
# ------------------------- Add ONE Row to Form ------------------------- #
def add_row(self, *args):
"""
Adds a single row of elements to a window's self.Rows variables.
Generally speaking this is NOT how users should be building Window layouts.
Users, create a single layout (a list of lists) and pass as a parameter to Window object, or call Window.Layout(layout)
:param *args: List[Elements]
:type *args:
"""
NumRows = len(self.Rows) # number of existing rows is our row number
CurrentRowNumber = NumRows # this row's number
CurrentRow = [] # start with a blank row and build up
# ------------------------- Add the elements to a row ------------------------- #
for i, element in enumerate(args): # Loop through list of elements and add them to the row
if isinstance(element, tuple) or isinstance(element, list):
self.add_row(*element)
continue
_error_popup_with_traceback('Error creating Window layout',
'Layout has a LIST instead of an ELEMENT',
'This means you have a badly placed ]',
'The offensive list is:',
element,
'This list will be stripped from your layout'
)
continue
elif callable(element) and not isinstance(element, Element):
_error_popup_with_traceback('Error creating Window layout',
'Layout has a FUNCTION instead of an ELEMENT',
'This likely means you are missing () from your layout',
'The offensive list is:',
element,
'This item will be stripped from your layout')
continue
if element.ParentContainer is not None:
warnings.warn(
'*** YOU ARE ATTEMPTING TO RESUSE AN ELEMENT IN YOUR LAYOUT! Once placed in a layout, an element cannot be used in another layout. ***',
UserWarning)
_error_popup_with_traceback('Error creating Window layout',
'You have broken the layout reuse rule! Thou shall not reuse layouts.',
'The layout specified has already been used',
'You MUST start witha "clean", unused layout every time you create a window',
'The offensive Element = ',
element,
'and has a key = ', element.Key,
'This item will be stripped from your layout',
'Hint - try printing your layout and matching the IDs "print(layout)"')
continue
element.Position = (CurrentRowNumber, i)
element.ParentContainer = self
CurrentRow.append(element)
# if this element is a titlebar, then automatically set the window margins to (0,0) and turn off normal titlebar
if element.metadata == TITLEBAR_METADATA_MARKER:
self.Margins = (0, 0)
self.NoTitleBar = True
# ------------------------- Append the row to list of Rows ------------------------- #
self.Rows.append(CurrentRow)
# ------------------------- Add Multiple Rows to Form ------------------------- #
def add_rows(self, rows):
"""
Loops through a list of lists of elements and adds each row, list, to the layout.
This is NOT the best way to go about creating a window. Sending the entire layout at one time and passing
it as a parameter to the Window call is better.
:param rows: A list of a list of elements
:type rows: List[List[Elements]]
"""
for row in rows:
try:
iter(row)
except TypeError:
_error_popup_with_traceback('Error Creating Window Layout', 'Error creating Window layout',
'Your row is not an iterable (e.g. a list)',
'Instead of a list, the type found was {}'.format(type(row)),
'The offensive row = ',
row,
'This item will be stripped from your layout')
continue
self.add_row(*row)
def layout(self, rows):
"""
Second of two preferred ways of telling a Window what its layout is. The other way is to pass the layout as
a parameter to Window object. The parameter method is the currently preferred method. This call to Layout
has been removed from examples contained in documents and in the Demo Programs. Trying to remove this call
from history and replace with sending as a parameter to Window.
:param rows: Your entire layout
:type rows: List[List[Elements]]
:return: self so that you can chain method calls
:rtype: (Window)
"""
if self.use_custom_titlebar and not self.override_custom_titlebar:
if self.titlebar_icon is not None:
icon = self.titlebar_icon
elif CUSTOM_TITLEBAR_ICON is not None:
icon = CUSTOM_TITLEBAR_ICON
elif self.titlebar_icon is not None:
icon = self.titlebar_icon
elif self.WindowIcon == DEFAULT_WINDOW_ICON:
icon = DEFAULT_BASE64_ICON_16_BY_16
else:
icon = None
new_rows = [[Titlebar(title=self.Title, icon=icon, text_color=self.titlebar_text_color, background_color=self.titlebar_background_color,
font=self.titlebar_font)]] + rows
else:
new_rows = rows
self.add_rows(new_rows)
self._BuildKeyDict()
if self._has_custom_titlebar_element():
self.Margins = (0, 0)
self.NoTitleBar = True
self._has_custom_titlebar = True
return self
def extend_layout(self, container, rows):
"""
Adds new rows to an existing container element inside of this window
If the container is a scrollable Column, you need to also call the contents_changed() method
:param container: The container Element the layout will be placed inside of
:type container: Frame | Column | Tab
:param rows: The layout to be added
:type rows: (List[List[Element]])
:return: (Window) self so could be chained
:rtype: (Window)
"""
column = Column(rows, pad=(0, 0))
if self == container:
frame = self.TKroot
elif isinstance(container.Widget, TkScrollableFrame):
frame = container.Widget.TKFrame
else:
frame = container.Widget
PackFormIntoFrame(column, frame, self)
# sg.PackFormIntoFrame(col, window.TKroot, window)
self.AddRow(column)
self.AllKeysDict = self._BuildKeyDictForWindow(self, column, self.AllKeysDict)
return self
def LayoutAndRead(self, rows, non_blocking=False):
"""
Deprecated!! Now your layout your window's rows (layout) and then separately call Read.
:param rows: The layout of the window
:type rows: List[List[Element]]
:param non_blocking: if True the Read call will not block
:type non_blocking: (bool)
"""
_error_popup_with_traceback('LayoutAndRead Depricated', 'Wow! You have been using PySimpleGUI for a very long time.',
'The Window.LayoutAndRead call is no longer supported')
raise DeprecationWarning(
'LayoutAndRead is no longer supported... change your call window.Layout(layout).Read()\nor window(title, layout).Read()')
# self.AddRows(rows)
# self._Show(non_blocking=non_blocking)
# return self.ReturnValues
def LayoutAndShow(self, rows):
"""
Deprecated - do not use any longer. Layout your window and then call Read. Or can add a Finalize call before the Read
"""
raise DeprecationWarning('LayoutAndShow is no longer supported... ')
def _Show(self, non_blocking=False):
"""
NOT TO BE CALLED BY USERS. INTERNAL ONLY!
It's this method that first shows the window to the user, collects results
:param non_blocking: if True, this is a non-blocking call
:type non_blocking: (bool)
:return: Tuple[Any, Dict] The event, values turple that is returned from Read calls
:rtype:
"""
self.Shown = True
# Compute num rows & num cols (it'll come in handy debugging)
self.NumRows = len(self.Rows)
self.NumCols = max(len(row) for row in self.Rows)
self.NonBlocking = non_blocking
# Search through entire form to see if any elements set the focus
# if not, then will set the focus to the first input element
found_focus = False
for row in self.Rows:
for element in row:
try:
if element.Focus:
found_focus = True
except:
pass
try:
if element.Key is not None:
self.UseDictionary = True
except:
pass
if not found_focus and self.UseDefaultFocus:
self.UseDefaultFocus = True
else:
self.UseDefaultFocus = False
# -=-=-=-=-=-=-=-=- RUN the GUI -=-=-=-=-=-=-=-=- ##
StartupTK(self)
# If a button or keyboard event happened but no results have been built, build the results
if self.LastKeyboardEvent is not None or self.LastButtonClicked is not None:
return _BuildResults(self, False, self)
return self.ReturnValues
# ------------------------- SetIcon - set the window's fav icon ------------------------- #
def set_icon(self, icon=None, pngbase64=None):
"""
Changes the icon that is shown on the title bar and on the task bar.
NOTE - The file type is IMPORTANT and depends on the OS!
Can pass in:
* filename which must be a .ICO icon file for windows, PNG file for Linux
* bytes object
* BASE64 encoded file held in a variable
:param icon: Filename or bytes object
:type icon: (str)
:param pngbase64: Base64 encoded image
:type pngbase64: (bytes)
"""
if type(icon) is bytes or pngbase64 is not None:
wicon = tkinter.PhotoImage(data=icon if icon is not None else pngbase64)
try:
self.TKroot.tk.call('wm', 'iconphoto', self.TKroot._w, wicon)
except:
wicon = tkinter.PhotoImage(data=DEFAULT_BASE64_ICON)
try:
self.TKroot.tk.call('wm', 'iconphoto', self.TKroot._w, wicon)
except:
pass
self.WindowIcon = wicon
return
wicon = icon
try:
self.TKroot.iconbitmap(icon)
except:
try:
wicon = tkinter.PhotoImage(file=icon)
self.TKroot.tk.call('wm', 'iconphoto', self.TKroot._w, wicon)
except:
try:
wicon = tkinter.PhotoImage(data=DEFAULT_BASE64_ICON)
try:
self.TKroot.tk.call('wm', 'iconphoto', self.TKroot._w, wicon)
except:
pass
except:
pass
self.WindowIcon = wicon
def _GetElementAtLocation(self, location):
"""
Given a (row, col) location in a layout, return the element located at that position
:param location: (int, int) Return the element located at (row, col) in layout
:type location:
:return: (Element) The Element located at that position in this window
:rtype:
"""
(row_num, col_num) = location
row = self.Rows[row_num]
element = row[col_num]
return element
def _GetDefaultElementSize(self):
"""
Returns the default elementSize
:return: (width, height) of the default element size
:rtype: (int, int)
"""
return self.DefaultElementSize
def _AutoCloseAlarmCallback(self):
"""
Function that's called by tkinter when autoclode timer expires. Closes the window
"""
try:
window = self
if window:
if window.NonBlocking:
self.Close()
else:
window._Close()
self.TKroot.quit()
self.RootNeedsDestroying = True
except:
pass
def _TimeoutAlarmCallback(self):
"""
Read Timeout Alarm callback. Will kick a mainloop call out of the tkinter event loop and cause it to return
"""
# first, get the results table built
# modify the Results table in the parent FlexForm object
# print('TIMEOUT CALLBACK')
if self.TimerCancelled:
# print('** timer was cancelled **')
return
self.LastButtonClicked = self.TimeoutKey
self.FormRemainedOpen = True
self.TKroot.quit() # kick the users out of the mainloop
def _calendar_chooser_button_clicked(self, elem):
"""
:param elem:
:type elem:
:return:
:rtype:
"""
target_element, strvar, should_submit_window = elem._find_target()
if elem.calendar_default_date_M_D_Y == (None, None, None):
now = datetime.datetime.now()
cur_month, cur_day, cur_year = now.month, now.day, now.year
else:
cur_month, cur_day, cur_year = elem.calendar_default_date_M_D_Y
date_chosen = popup_get_date(start_mon=cur_month, start_day=cur_day, start_year=cur_year, close_when_chosen=elem.calendar_close_when_chosen,
no_titlebar=elem.calendar_no_titlebar, begin_at_sunday_plus=elem.calendar_begin_at_sunday_plus,
locale=elem.calendar_locale, location=elem.calendar_location, month_names=elem.calendar_month_names,
day_abbreviations=elem.calendar_day_abbreviations, title=elem.calendar_title)
if date_chosen is not None:
month, day, year = date_chosen
now = datetime.datetime.now()
hour, minute, second = now.hour, now.minute, now.second
try:
date_string = calendar.datetime.datetime(year, month, day, hour, minute, second).strftime(elem.calendar_format)
except Exception as e:
print('Bad format string in calendar chooser button', e)
date_string = 'Bad format string'
if target_element is not None and target_element != elem:
target_element.update(date_string)
elif target_element == elem:
elem.calendar_selection = date_string
strvar.set(date_string)
elem.TKStringVar.set(date_string)
if should_submit_window:
self.LastButtonClicked = target_element.Key
results = _BuildResults(self, False, self)
else:
should_submit_window = False
return should_submit_window
# @_timeit_summary
def read(self, timeout=None, timeout_key=TIMEOUT_KEY, close=False):
"""
THE biggest deal method in the Window class! This is how you get all of your data from your Window.
Pass in a timeout (in milliseconds) to wait for a maximum of timeout milliseconds. Will return timeout_key
if no other GUI events happen first.
:param timeout: Milliseconds to wait until the Read will return IF no other GUI events happen first
:type timeout: (int)
:param timeout_key: The value that will be returned from the call if the timer expired
:type timeout_key: (Any)
:param close: if True the window will be closed prior to returning
:type close: (bool)
:return: (event, values)
:rtype: Tuple[(Any), Dict[Any, Any], List[Any], None]
"""
if Window._floating_debug_window_build_needed is True:
Window._floating_debug_window_build_needed = False
_Debugger.debugger._build_floating_window()
if Window._main_debug_window_build_needed is True:
Window._main_debug_window_build_needed = False
_Debugger.debugger._build_main_debugger_window()
# ensure called only 1 time through a single read cycle
if not Window._read_call_from_debugger:
_refresh_debugger()
Window._root_running_mainloop = self.TKroot
while True:
results = self._read(timeout=timeout, timeout_key=timeout_key)
# Post processing for Calendar Chooser Button
try:
if results[0] == timeout_key: # if a timeout, then not a calendar button
break
elem = self.find_element(results[0], silent_on_error=True) # get the element that caused the event
if elem.Type == ELEM_TYPE_BUTTON:
if elem.BType == BUTTON_TYPE_CALENDAR_CHOOSER:
if self._calendar_chooser_button_clicked(elem): # returns True if should break out
# results[0] = self.LastButtonClicked
results = self.ReturnValues
break
else:
continue
break
except:
break # wasn't a calendar button for sure
if close:
self.close()
return results
# @_timeit
def _read(self, timeout=None, timeout_key=TIMEOUT_KEY):
"""
THE biggest deal method in the Window class! This is how you get all of your data from your Window.
Pass in a timeout (in milliseconds) to wait for a maximum of timeout milliseconds. Will return timeout_key
if no other GUI events happen first.
:param timeout: Milliseconds to wait until the Read will return IF no other GUI events happen first
:type timeout: (int)
:param timeout_key: The value that will be returned from the call if the timer expired
:type timeout_key: (Any)
:return: (event, values) (event or timeout_key or None, Dictionary of values or List of values from all elements in the Window)
:rtype: Tuple[(Any), Dict[Any, Any], List[Any], None]
"""
# if there are events in the thread event queue, then return those events before doing anything else.
if self._queued_thread_event_available():
self.ReturnValues = results = _BuildResults(self, False, self)
return results
if self.finalize_in_progress and self.auto_close_timer_needs_starting:
self._start_autoclose_timer()
self.auto_close_timer_needs_starting = False
timeout = int(timeout) if timeout is not None else None
if timeout == 0: # timeout of zero runs the old readnonblocking
event, values = self._ReadNonBlocking()
if event is None:
event = timeout_key
if values is None:
event = None
return event, values # make event None if values was None and return
# Read with a timeout
self.Timeout = timeout
self.TimeoutKey = timeout_key
self.NonBlocking = False
if self.TKrootDestroyed:
self.read_closed_window_count += 1
if self.read_closed_window_count > 100:
popup_error('You have tried 100 times to read a closed window.', 'You need to add a check for event == WIN_CLOSED',
title='Trying to read a closed window')
return None, None
if not self.Shown:
self._Show()
else:
# if already have a button waiting, the return previously built results
if self.LastButtonClicked is not None and not self.LastButtonClickedWasRealtime:
results = _BuildResults(self, False, self)
self.LastButtonClicked = None
return results
InitializeResults(self)
if self._queued_thread_event_available():
self.ReturnValues = results = _BuildResults(self, False, self)
return results
# if the last button clicked was realtime, emulate a read non-blocking
# the idea is to quickly return realtime buttons without any blocks until released
if self.LastButtonClickedWasRealtime:
# clear the realtime flag if the element is not a button element (for example a graph element that is dragging)
if self.AllKeysDict.get(self.LastButtonClicked, None):
if self.AllKeysDict.get(self.LastButtonClicked).Type != ELEM_TYPE_BUTTON:
self.LastButtonClickedWasRealtime = False # stops from generating events until something changes
else: # it is possible for the key to not be in the dicitonary because it has a modifier. If so, then clear the realtime button flag
self.LastButtonClickedWasRealtime = False # stops from generating events until something changes
try:
rc = self.TKroot.update()
except:
self.TKrootDestroyed = True
Window._DecrementOpenCount()
# _my_windows.Decrement()
# print('ROOT Destroyed')
results = _BuildResults(self, False, self)
if results[0] != None and results[0] != timeout_key:
return results
else:
pass
# else:
# print("** REALTIME PROBLEM FOUND **", results)
if self.RootNeedsDestroying:
# print('*** DESTROYING really late***')
try:
self.TKroot.destroy()
except:
pass
# _my_windows.Decrement()
self.LastButtonClicked = None
return None, None
# normal read blocking code....
if timeout != None:
self.TimerCancelled = False
self.TKAfterID = self.TKroot.after(timeout, self._TimeoutAlarmCallback)
self.CurrentlyRunningMainloop = True
# self.TKroot.protocol("WM_DESTROY_WINDOW", self._OnClosingCallback)
# self.TKroot.protocol("WM_DELETE_WINDOW", self._OnClosingCallback)
Window._window_running_mainloop = self
Window._root_running_mainloop.mainloop()
# print('Out main')
self.CurrentlyRunningMainloop = False
# if self.LastButtonClicked != TIMEOUT_KEY:
try:
self.TKroot.after_cancel(self.TKAfterID)
del self.TKAfterID
except:
pass
# print('** tkafter cancel failed **')
self.TimerCancelled = True
if self.RootNeedsDestroying:
# print('*** DESTROYING LATE ***')
try:
self.TKroot.destroy()
except:
pass
Window._DecrementOpenCount()
# _my_windows.Decrement()
self.LastButtonClicked = None
return None, None
# if form was closed with X
if self.LastButtonClicked is None and self.LastKeyboardEvent is None and self.ReturnValues[0] is None:
Window._DecrementOpenCount()
# _my_windows.Decrement()
# Determine return values
if self.LastKeyboardEvent is not None or self.LastButtonClicked is not None:
results = _BuildResults(self, False, self)
if not self.LastButtonClickedWasRealtime:
self.LastButtonClicked = None
return results
else:
if self._queued_thread_event_available():
self.ReturnValues = results = _BuildResults(self, False, self)
return results
if not self.XFound and self.Timeout != 0 and self.Timeout is not None and self.ReturnValues[
0] is None: # Special Qt case because returning for no reason so fake timeout
self.ReturnValues = self.TimeoutKey, self.ReturnValues[1] # fake a timeout
elif not self.XFound and self.ReturnValues[0] is None: # Return a timeout event... can happen when autoclose used on another window
# print("*** Faking timeout ***")
self.ReturnValues = self.TimeoutKey, self.ReturnValues[1] # fake a timeout
return self.ReturnValues
def _ReadNonBlocking(self):
"""
Should be NEVER called directly by the user. The user can call Window.Read(timeout=0) to get same effect
:return: (event, values). (event or timeout_key or None, Dictionary of values or List of values from all elements in the Window)
:rtype: Tuple[(Any), Dict[Any, Any] | List[Any] | None]
"""
if self.TKrootDestroyed:
try:
self.TKroot.quit()
self.TKroot.destroy()
except:
pass
# print('DESTROY FAILED')
return None, None
if not self.Shown:
self._Show(non_blocking=True)
try:
rc = self.TKroot.update()
except:
self.TKrootDestroyed = True
Window._DecrementOpenCount()
# _my_windows.Decrement()
# print("read failed")
# return None, None
if self.RootNeedsDestroying:
# print('*** DESTROYING LATE ***', self.ReturnValues)
self.TKroot.destroy()
Window._DecrementOpenCount()
# _my_windows.Decrement()
self.Values = None
self.LastButtonClicked = None
return None, None
return _BuildResults(self, False, self)
def _start_autoclose_timer(self):
duration = DEFAULT_AUTOCLOSE_TIME if self.AutoCloseDuration is None else self.AutoCloseDuration
self.TKAfterID = self.TKroot.after(int(duration * 1000), self._AutoCloseAlarmCallback)
def finalize(self):
"""
Use this method to cause your layout to built into a real tkinter window. In reality this method is like
Read(timeout=0). It doesn't block and uses your layout to create tkinter widgets to represent the elements.
Lots of action!
:return: Returns 'self' so that method "Chaining" can happen (read up about it as it's very cool!)
:rtype: (Window)
"""
if self.TKrootDestroyed:
return self
self.finalize_in_progress = True
self.Read(timeout=1)
if self.AutoClose:
self.auto_close_timer_needs_starting = True
# add the window to the list of active windows
Window._active_windows[self] = Window.hidden_master_root
return self
# OLD CODE FOLLOWS
if not self.Shown:
self._Show(non_blocking=True)
try:
rc = self.TKroot.update()
except:
self.TKrootDestroyed = True
Window._DecrementOpenCount()
print('** Finalize failed **')
# _my_windows.Decrement()
# return None, None
return self
def refresh(self):
"""
Refreshes the window by calling tkroot.update(). Can sometimes get away with a refresh instead of a Read.
Use this call when you want something to appear in your Window immediately (as soon as this function is called).
If you change an element in a window, your change will not be visible until the next call to Window.read
or a call to Window.refresh()
:return: `self` so that method calls can be easily "chained"
:rtype: (Window)
"""
if self.TKrootDestroyed:
return self
try:
rc = self.TKroot.update()
except:
pass
return self
def fill(self, values_dict):
"""
Fill in elements that are input fields with data based on a 'values dictionary'
:param values_dict: pairs
:type values_dict: (Dict[Any, Any]) - {Element_key : value}
:return: returns self so can be chained with other methods
:rtype: (Window)
"""
FillFormWithValues(self, values_dict)
return self
def _find_closest_key(self, search_key):
if not isinstance(search_key, str):
search_key = str(search_key)
matches = difflib.get_close_matches(search_key, [str(k) for k in self.AllKeysDict.keys()])
if not len(matches):
return None
for k in self.AllKeysDict.keys():
if matches[0] == str(k):
return k
return matches[0] if len(matches) else None
def FindElement(self, key, silent_on_error=False):
"""
** Warning ** This call will eventually be depricated. **
It is suggested that you modify your code to use the recommended window[key] lookup or the PEP8 compliant window.find_element(key)
For now, you'll only see a message printed and the call will continue to funcation as before.
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key: str | int | tuple | object
:param silent_on_error: If True do not display popup nor print warning of key errors
:type silent_on_error: (bool)
:return: Return value can be: the Element that matches the supplied key if found; an Error Element if silent_on_error is False; None if silent_on_error True;
:rtype: Element | Error Element | None
"""
warnings.warn('Use of FindElement is not recommended.\nEither switch to the recommended window[key] format\nor the PEP8 compliant find_element',
UserWarning)
print('** Warning - FindElement should not be used to look up elements. window[key] or window.find_element are recommended. **')
return self.find_element(key, silent_on_error=silent_on_error)
def find_element(self, key, silent_on_error=False):
"""
Find element object associated with the provided key.
THIS METHOD IS NO LONGER NEEDED to be called by the user
You can perform the same operation by writing this statement:
element = window[key]
You can drop the entire "find_element" function name and use [ ] instead.
However, if you wish to perform a lookup without error checking, and don't have error popups turned
off globally, you'll need to make this call so that you can disable error checks on this call.
find_element is yypically used in combination with a call to element's Update method (or any other element method!):
window[key].update(new_value)
Versus the "old way"
window.FindElement(key).Update(new_value)
This call can be abbreviated to any of these:
find_element = FindElement == Element == Find
With find_element being the PEP8 compliant call that should be used.
Rememeber that this call will return None if no match is found which may cause your code to crash if not
checked for.
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key: str | int | tuple | object
:param silent_on_error: If True do not display popup nor print warning of key errors
:type silent_on_error: (bool)
:return: Return value can be: the Element that matches the supplied key if found; an Error Element if silent_on_error is False; None if silent_on_error True;
:rtype: Element | Error Element | None
"""
try:
element = self.AllKeysDict[key]
key_error = False
except KeyError:
closest_key = self._find_closest_key(key)
if not silent_on_error:
print('** Error looking up your element using the key: ', key, 'The closest matching key: ', closest_key)
_error_popup_with_traceback('Key Error', 'Problem finding your key ' + str(key), 'Closest match = ' + str(closest_key))
if not SUPPRESS_RAISE_KEY_ERRORS:
raise KeyError(key)
element = ErrorElement(key=key)
key_error = True
else:
element = ErrorElement(key=key)
return element
if key_error:
if closest_key is not None and not SUPPRESS_KEY_GUESSING:
element = self.AllKeysDict[closest_key]
return element
if not SUPPRESS_RAISE_KEY_ERRORS:
raise KeyError(key)
return element
Element = find_element # Shortcut function
Find = find_element # Shortcut function, most likely not used by many people.
Elem = find_element # NEW for 2019! More laziness... Another shortcut
def find_element_with_focus(self):
"""
Returns the Element that currently has focus as reported by tkinter. If no element is found None is returned!
:return: An Element if one has been found with focus or None if no element found
:rtype: Element | None
"""
element = _FindElementWithFocusInSubForm(self)
return element
def _BuildKeyDict(self):
"""
Used internally only! Not user callable
Builds a dictionary containing all elements with keys for this window.
"""
dict = {}
self.AllKeysDict = self._BuildKeyDictForWindow(self, self, dict)
def _BuildKeyDictForWindow(self, top_window, window, key_dict):
"""
Loop through all Rows and all Container Elements for this window and create the keys for all of them.
Note that the calls are recursive as all pathes must be walked
:param top_window: The highest level of the window
:type top_window: (Window)
:param window: The "sub-window" (container element) to be searched
:type window: Column | Frame | TabGroup | Pane | Tab
:param key_dict: The dictionary as it currently stands.... used as part of recursive call
:type key_dict:
:return: (dict) Dictionary filled with all keys in the window
:rtype:
"""
for row_num, row in enumerate(window.Rows):
for col_num, element in enumerate(row):
if element.Type == ELEM_TYPE_COLUMN:
key_dict = self._BuildKeyDictForWindow(top_window, element, key_dict)
if element.Type == ELEM_TYPE_FRAME:
key_dict = self._BuildKeyDictForWindow(top_window, element, key_dict)
if element.Type == ELEM_TYPE_TAB_GROUP:
key_dict = self._BuildKeyDictForWindow(top_window, element, key_dict)
if element.Type == ELEM_TYPE_PANE:
key_dict = self._BuildKeyDictForWindow(top_window, element, key_dict)
if element.Type == ELEM_TYPE_TAB:
key_dict = self._BuildKeyDictForWindow(top_window, element, key_dict)
if element.Key is None: # if no key has been assigned.... create one for input elements
if element.Type == ELEM_TYPE_BUTTON:
element.Key = element.ButtonText
elif element.Type == ELEM_TYPE_TAB:
element.Key = element.Title
if element.Type in (ELEM_TYPE_MENUBAR, ELEM_TYPE_BUTTONMENU,
ELEM_TYPE_INPUT_SLIDER, ELEM_TYPE_GRAPH, ELEM_TYPE_IMAGE,
ELEM_TYPE_INPUT_CHECKBOX, ELEM_TYPE_INPUT_LISTBOX, ELEM_TYPE_INPUT_COMBO,
ELEM_TYPE_INPUT_MULTILINE, ELEM_TYPE_INPUT_OPTION_MENU, ELEM_TYPE_INPUT_SPIN,
ELEM_TYPE_INPUT_RADIO, ELEM_TYPE_INPUT_TEXT, ELEM_TYPE_PROGRESS_BAR,
ELEM_TYPE_TABLE, ELEM_TYPE_TREE,
ELEM_TYPE_TAB_GROUP, ELEM_TYPE_SEPARATOR):
element.Key = top_window.DictionaryKeyCounter
top_window.DictionaryKeyCounter += 1
if element.Key is not None:
if element.Key in key_dict.keys():
if element.Type == ELEM_TYPE_BUTTON and WARN_DUPLICATE_BUTTON_KEY_ERRORS: # for Buttons see if should complain
warnings.warn('*** Duplicate key found in your layout {} ***'.format(element.Key), UserWarning)
warnings.warn('*** Replaced new key with {} ***'.format(str(element.Key) + str(self.UniqueKeyCounter)))
if not SUPPRESS_ERROR_POPUPS:
_error_popup_with_traceback('Duplicate key found in your layout', 'Dupliate key: {}'.format(element.Key),
'Is being replaced with: {}'.format(str(element.Key) + str(self.UniqueKeyCounter)),
'The line of code above shows you which layout, but does not tell you exactly where the element was defined',
'The element type is {}'.format(element.Type))
element.Key = str(element.Key) + str(self.UniqueKeyCounter)
self.UniqueKeyCounter += 1
key_dict[element.Key] = element
return key_dict
def element_list(self):
"""
Returns a list of all elements in the window
:return: List of all elements in the window and container elements in the window
:rtype: List[Element]
"""
return self._build_element_list()
def _build_element_list(self):
"""
Used internally only! Not user callable
Builds a dictionary containing all elements with keys for this window.
"""
elem_list = []
elem_list = self._build_element_list_for_form(self, self, elem_list)
return elem_list
def _build_element_list_for_form(self, top_window, window, elem_list):
"""
Loop through all Rows and all Container Elements for this window and create a list
Note that the calls are recursive as all pathes must be walked
:param top_window: The highest level of the window
:type top_window: (Window)
:param window: The "sub-window" (container element) to be searched
:type window: Column | Frame | TabGroup | Pane | Tab
:param elem_list: The element list as it currently stands.... used as part of recursive call
:type elem_list: ???
:return: List of all elements in this sub-window
:rtype: List[Element]
"""
for row_num, row in enumerate(window.Rows):
for col_num, element in enumerate(row):
elem_list.append(element)
if element.Type in (ELEM_TYPE_COLUMN, ELEM_TYPE_FRAME, ELEM_TYPE_TAB_GROUP, ELEM_TYPE_PANE, ELEM_TYPE_TAB):
elem_list = self._build_element_list_for_form(top_window, element, elem_list)
return elem_list
def save_to_disk(self, filename):
"""
Saves the values contained in each of the input areas of the form. Basically saves what would be returned from a call to Read. It takes these results and saves them to disk using pickle.
Note that every element in your layout that is to be saved must have a key assigned to it.
:param filename: Filename to save the values to in pickled form
:type filename: str
"""
try:
event, values = _BuildResults(self, False, self)
remove_these = []
for key in values:
if self.Element(key).Type == ELEM_TYPE_BUTTON:
remove_these.append(key)
for key in remove_these:
del values[key]
with open(filename, 'wb') as sf:
pickle.dump(values, sf)
except:
print('*** Error saving Window contents to disk ***')
def load_from_disk(self, filename):
"""
Restore values from a previous call to SaveToDisk which saves the returned values dictionary in Pickle format
:param filename: Pickle Filename to load
:type filename: (str)
"""
try:
with open(filename, 'rb') as df:
self.Fill(pickle.load(df))
except:
print('*** Error loading form to disk ***')
def get_screen_dimensions(self):
"""
Get the screen dimensions. NOTE - you must have a window already open for this to work (blame tkinter not me)
:return: Tuple containing width and height of screen in pixels
:rtype: Tuple[None, None] | Tuple[width, height]
"""
if self.TKrootDestroyed or self.TKroot is None:
return Window.get_screen_size()
screen_width = self.TKroot.winfo_screenwidth() # get window info to move to middle of screen
screen_height = self.TKroot.winfo_screenheight()
return screen_width, screen_height
def move(self, x, y):
"""
Move the upper left corner of this window to the x,y coordinates provided
:param x: x coordinate in pixels
:type x: (int)
:param y: y coordinate in pixels
:type y: (int)
"""
try:
self.TKroot.geometry("+%s+%s" % (x, y))
self.config_last_location = (int(x), (int(y)))
except:
pass
def move_to_center(self):
"""
Recenter your window after it's been moved or the size changed.
This is a conveinence method. There are no tkinter calls involved, only pure PySimpleGUI API calls.
"""
if not self._is_window_created('tried Window.move_to_center'):
return
screen_width, screen_height = self.get_screen_dimensions()
win_width, win_height = self.size
x, y = (screen_width - win_width)//2, (screen_height - win_height)//2
self.move(x, y)
def minimize(self):
"""
Minimize this window to the task bar
"""
if not self._is_window_created('tried Window.minimize'):
return
self.TKroot.iconify()
self.maximized = False
def maximize(self):
"""
Maximize the window. This is done differently on a windows system versus a linux or mac one. For non-Windows
the root attribute '-fullscreen' is set to True. For Windows the "root" state is changed to "zoomed"
The reason for the difference is the title bar is removed in some cases when using fullscreen option
"""
if not self._is_window_created('tried Window.maximize'):
return
if not running_linux():
self.TKroot.state('zoomed')
else:
self.TKroot.attributes('-fullscreen', True)
# this method removes the titlebar too
# self.TKroot.attributes('-fullscreen', True)
self.maximized = True
def normal(self):
"""
Restore a window to a non-maximized state. Does different things depending on platform. See Maximize for more.
"""
if not self._is_window_created('tried Window.normal'):
return
if self.TKroot.state() == 'iconic':
self.TKroot.deiconify()
else:
if not running_linux():
self.TKroot.state('normal')
else:
self.TKroot.attributes('-fullscreen', False)
self.maximized = False
def _StartMoveUsingControlKey(self, event):
"""
Used by "Grab Anywhere" style windows. This function is bound to mouse-down. It marks the beginning of a drag.
:param event: event information passed in by tkinter. Contains x,y position of mouse
:type event: (event)
"""
self._StartMove(event)
def _StartMoveGrabAnywhere(self, event):
"""
Used by "Grab Anywhere" style windows. This function is bound to mouse-down. It marks the beginning of a drag.
:param event: event information passed in by tkinter. Contains x,y position of mouse
:type event: (event)
"""
if (isinstance(event.widget, GRAB_ANYWHERE_IGNORE_THESE_WIDGETS) or event.widget in self._grab_anywhere_ignore_these_list) and event.widget not in self._grab_anywhere_include_these_list:
# print('Found widget to ignore in grab anywhere...')
return
self._StartMove(event)
def _StartMove(self, event):
try:
geometry = self.TKroot.geometry()
location = geometry[geometry.find('+')+1:].split('+')
self._startx = int(location[0])
self._starty = int(location[1])
self._mousex = event.x + event.widget.winfo_rootx()
self._mousey = event.y + event.widget.winfo_rooty()
# print(self._startx, self._starty)
# print(self._mousex, self._mousey)
# print(self.TKroot.geometry())
except:
pass
# print('Start move {},{} widget {}'.format(event.x,event.y, event.widget))
if Window._move_all_windows:
for window in Window._active_windows:
window._offsetx = event.x + event.widget.winfo_rootx() - window.TKroot.winfo_rootx()
window._offsety = event.y + event.widget.winfo_rooty() - window.TKroot.winfo_rooty()
def _StopMove(self, event):
"""
Used by "Grab Anywhere" style windows. This function is bound to mouse-up. It marks the ending of a drag.
Sets the position of the window to this final x,y coordinates
:param event: event information passed in by tkinter. Contains x,y position of mouse
:type event: (event)
"""
return
def _OnMotionUsingControlKey(self, event):
self._OnMotion(event)
def _OnMotionGrabAnywhere(self, event):
"""
Used by "Grab Anywhere" style windows. This function is bound to mouse motion. It actually moves the window
:param event: event information passed in by tkinter. Contains x,y position of mouse
:type event: (event)
"""
if (isinstance(event.widget, GRAB_ANYWHERE_IGNORE_THESE_WIDGETS) or event.widget in self._grab_anywhere_ignore_these_list) and event.widget not in self._grab_anywhere_include_these_list:
# print('Found widget to ignore in grab anywhere...')
return
self._OnMotion(event)
def _OnMotion(self, event):
try:
_mousex = event.x + event.widget.winfo_rootx()
_mousey = event.y + event.widget.winfo_rooty()
deltax = _mousex - self._mousex
deltay = _mousey - self._mousey
x = self._startx + deltax
y = self._starty + deltay
self.TKroot.geometry("+%s+%s" % (x, y)) # this is what really moves the window
if Window._move_all_windows:
for window in Window._active_windows:
deltax = window._offsetx
deltay = window._offsety
x = window.TKroot.winfo_pointerx() - deltax
y = window.TKroot.winfo_pointery() - deltay
window.TKroot.geometry("+%s+%s" % (x, y)) # this is what really moves the window
except Exception as e:
print('on motion error', e)
def _focus_callback(self, event):
print('Focus event = {} window = {}'.format(event, self.Title))
def _config_callback(self, event):
print('Config event = {} window = {}'.format(event, self.Title))
#
# try:
# deltax = event.x - self.TKroot.x
# deltay = event.y - self.TKroot.y
# x = self.TKroot.winfo_x() + deltax
# y = self.TKroot.winfo_y() + deltay
# self.TKroot.geometry("+%s+%s" % (x, y)) # this is what really moves the window
# # print('{},{}'.format(x,y))
#
# if Window._move_all_windows:
# for window in Window._active_windows:
# x = window.TKroot.winfo_x() + deltax
# y = window.TKroot.winfo_y() + deltay
# window.TKroot.geometry("+%s+%s" % (x, y)) # this is what really moves the window
# except Exception as e:
# print('on motion error {}'.format(e), 'title = {}'.format(window.Title))
"""
def _config_callback(self, event):
new_x = event.x
new_y = event.y
if self.not_completed_initial_movement:
if self.starting_window_position != (new_x, new_y):
return
self.not_completed_initial_movement = False
return
if not self.saw_00:
if new_x == 0 and new_y == 0:
self.saw_00 = True
# self.config_count += 1
# if self.config_count < 40:
# return
print('Move LOGIC')
if self.config_last_size != (event.width, event.height):
self.config_last_size = (event.width, event.height)
if self.config_last_location[0] != new_x or self.config_last_location[1] != new_y:
if self.config_last_location == (None, None):
self.config_last_location = (new_x, new_y)
return
deltax = self.config_last_location[0] - event.x
deltay = self.config_last_location[1] - event.y
if deltax == 0 and deltay == 0:
print('not moving so returning')
return
if Window._move_all_windows:
print('checking all windows')
for window in Window._active_windows:
if window == self:
continue
x = window.TKroot.winfo_x() + deltax
y = window.TKroot.winfo_y() + deltay
# window.TKroot.geometry("+%s+%s" % (x, y)) # this is what really moves the window
# window.config_last_location = (x,y)
"""
def _KeyboardCallback(self, event):
"""
Window keyboard callback. Called by tkinter. Will kick user out of the tkinter event loop. Should only be
called if user has requested window level keyboard events
:param event: object provided by tkinter that contains the key information
:type event: (event)
"""
self.LastButtonClicked = None
self.FormRemainedOpen = True
if event.char != '':
self.LastKeyboardEvent = event.char
else:
self.LastKeyboardEvent = str(event.keysym) + ':' + str(event.keycode)
# if not self.NonBlocking:
# _BuildResults(self, False, self)
_exit_mainloop(self)
def _MouseWheelCallback(self, event):
"""
Called by tkinter when a mouse wheel event has happened. Only called if keyboard events for the window
have been enabled
:param event: object sent in by tkinter that has the wheel direction
:type event: (event)
"""
self.LastButtonClicked = None
self.FormRemainedOpen = True
self.LastKeyboardEvent = 'MouseWheel:Down' if event.delta < 0 or event.num == 5 else 'MouseWheel:Up'
# if not self.NonBlocking:
# _BuildResults(self, False, self)
_exit_mainloop(self)
def _Close(self, without_event=False):
"""
The internal close call that does the real work of building. This method basically sets up for closing
but doesn't destroy the window like the User's version of Close does
:parm without_event: if True, then do not cause an event to be generated, "silently" close the window
:type without_event: (bool)
"""
try:
self.TKroot.update()
except:
pass
if not self.NonBlocking or not without_event:
_BuildResults(self, False, self)
if self.TKrootDestroyed:
return
self.TKrootDestroyed = True
self.RootNeedsDestroying = True
return
def close(self):
"""
Closes window. Users can safely call even if window has been destroyed. Should always call when done with
a window so that resources are properly freed up within your thread.
"""
try:
del Window._active_windows[self] # will only be in the list if window was explicitly finalized
except:
pass
try:
self.TKroot.update() # On Linux must call update if the user closed with X or else won't actually close the window
except:
pass
if self.TKrootDestroyed:
return
try:
self.TKroot.destroy()
self.TKroot.update()
Window._DecrementOpenCount()
except:
pass
# if down to 1 window, try and destroy the hidden window, if there is one
# if Window.NumOpenWindows == 1:
# try:
# Window.hidden_master_root.destroy()
# Window.NumOpenWindows = 0 # if no hidden window, then this won't execute
# except:
# pass
self.TKrootDestroyed = True
# Free up anything that was held in the layout and the root variables
self.Rows = None
self.TKroot = None
# IT FINALLY WORKED! 29-Oct-2018 was the first time this damned thing got called
def _OnClosingCallback(self):
"""
Internally used method ONLY. Not sure callable. tkinter calls this when the window is closed by clicking X
"""
# global _my_windows
# print('Got closing callback', self.DisableClose)
if self.DisableClose:
return
if self.CurrentlyRunningMainloop: # quit if this is the current mainloop, otherwise don't quit!
_exit_mainloop(self)
if self.close_destroys_window:
self.TKroot.destroy() # destroy this window
self.TKrootDestroyed = True
self.XFound = True
else:
self.LastButtonClicked = WINDOW_CLOSE_ATTEMPTED_EVENT
elif Window._root_running_mainloop == Window.hidden_master_root:
_exit_mainloop(self)
else:
if self.close_destroys_window:
self.TKroot.destroy() # destroy this window
self.XFound = True
else:
self.LastButtonClicked = WINDOW_CLOSE_ATTEMPTED_EVENT
if self.close_destroys_window:
self.RootNeedsDestroying = True
def disable(self):
"""
Disables window from taking any input from the user
"""
if not self._is_window_created('tried Window.disable'):
return
self.TKroot.attributes('-disabled', 1)
# self.TKroot.grab_set_global()
def enable(self):
"""
Re-enables window to take user input after having it be Disabled previously
"""
if not self._is_window_created('tried Window.enable'):
return
self.TKroot.attributes('-disabled', 0)
# self.TKroot.grab_release()
def hide(self):
"""
Hides the window from the screen and the task bar
"""
if not self._is_window_created('tried Window.hide'):
return
self._Hidden = True
self.TKroot.withdraw()
def un_hide(self):
"""
Used to bring back a window that was previously hidden using the Hide method
"""
if not self._is_window_created('tried Window.un_hide'):
return
if self._Hidden:
self.TKroot.deiconify()
self._Hidden = False
def disappear(self):
"""
Causes a window to "disappear" from the screen, but remain on the taskbar. It does this by turning the alpha
channel to 0. NOTE that on some platforms alpha is not supported. The window will remain showing on these
platforms. The Raspberry Pi for example does not have an alpha setting
"""
if not self._is_window_created('tried Window.disappear'):
return
self.TKroot.attributes('-alpha', 0)
def reappear(self):
"""
Causes a window previously made to "Disappear" (using that method). Does this by restoring the alpha channel
"""
if not self._is_window_created('tried Window.reappear'):
return
self.TKroot.attributes('-alpha', 255)
def set_alpha(self, alpha):
"""
Sets the Alpha Channel for a window. Values are between 0 and 1 where 0 is completely transparent
:param alpha: 0 to 1. 0 is completely transparent. 1 is completely visible and solid (can't see through)
:type alpha: (float)
"""
if not self._is_window_created('tried Window.set_alpha'):
return
self._AlphaChannel = alpha
self.TKroot.attributes('-alpha', alpha)
@property
def alpha_channel(self):
"""
A property that changes the current alpha channel value (internal value)
:return: the current alpha channel setting according to self, not read directly from tkinter
:rtype: (float)
"""
return self._AlphaChannel
@alpha_channel.setter
def alpha_channel(self, alpha):
"""
The setter method for this "property".
Planning on depricating so that a Set call is always used by users. This is more in line with the SDK
:param alpha: 0 to 1. 0 is completely transparent. 1 is completely visible and solid (can't see through)
:type alpha: (float)
"""
if not self._is_window_created('tried Window.alpha_channel'):
return
self._AlphaChannel = alpha
self.TKroot.attributes('-alpha', alpha)
def bring_to_front(self):
"""
Brings this window to the top of all other windows (perhaps may not be brought before a window made to "stay
on top")
"""
if not self._is_window_created('tried Window.bring_to_front'):
return
if running_windows():
try:
self.TKroot.wm_attributes("-topmost", 0)
self.TKroot.wm_attributes("-topmost", 1)
if not self.KeepOnTop:
self.TKroot.wm_attributes("-topmost", 0)
except Exception as e:
warnings.warn('Problem in Window.bring_to_front' + str(e), UserWarning)
else:
try:
self.TKroot.lift()
except:
pass
def send_to_back(self):
"""
Pushes this window to the bottom of the stack of windows. It is the opposite of BringToFront
"""
if not self._is_window_created('tried Window.send_to_back'):
return
try:
self.TKroot.lower()
except:
pass
def keep_on_top_set(self):
"""
Sets keep_on_top after a window has been created. Effect is the same
as if the window was created with this set. The Window is also brought
to the front
"""
if not self._is_window_created('tried Window.keep_on_top_set'):
return
self.KeepOnTop = True
self.bring_to_front()
try:
self.TKroot.wm_attributes("-topmost", 1)
except Exception as e:
warnings.warn('Problem in Window.keep_on_top_set trying to set wm_attributes topmost' + str(e), UserWarning)
def keep_on_top_clear(self):
"""
Clears keep_on_top after a window has been created. Effect is the same
as if the window was created with this set.
"""
if not self._is_window_created('tried Window.keep_on_top_clear'):
return
self.KeepOnTop = False
try:
self.TKroot.wm_attributes("-topmost", 0)
except Exception as e:
warnings.warn('Problem in Window.keep_on_top_clear trying to clear wm_attributes topmost' + str(e), UserWarning)
def current_location(self, more_accurate=False):
"""
Get the current location of the window's top left corner.
Sometimes, depending on the environment, the value returned does not include the titlebar,etc
A new option, more_accurate, can be used to get the theoretical upper leftmost corner of the window.
The titlebar and menubar are crated by the OS. It gets really confusing when running in a webpage (repl, trinket)
Thus, the values can appear top be "off" due to the sometimes unpredictable way the location is calculated.
:param more_accurate: If True, will use the window's geometry to get the topmost location with titlebar, menubar taken into account
:type more_accurate: (bool)
:return: The x and y location in tuple form (x,y)
:rtype: Tuple[(int | None), (int | None)]
"""
if not self._is_window_created('tried Window.current_location'):
return (None, None)
try:
if more_accurate:
geometry = self.TKroot.geometry()
location = geometry[geometry.find('+') + 1:].split('+')
x, y = int(location[0]), int(location[1])
else:
x, y = int(self.TKroot.winfo_x()), int(self.TKroot.winfo_y())
except Exception as e:
warnings.warn('Error in Window.current_location. Trouble getting x,y location\n' + str(e), UserWarning)
x, y = (None, None)
return (x,y)
def current_size_accurate(self):
"""
Get the current location of the window based on tkinter's geometry setting
:return: The x and y size in tuple form (x,y)
:rtype: Tuple[(int | None), (int | None)]
"""
if not self._is_window_created('tried Window.current_location'):
return (None, None)
try:
geometry = self.TKroot.geometry()
geometry_tuple = geometry.split('+')
window_size = geometry_tuple[0].split('x')
x, y = int(window_size[0]), int(window_size[1])
except Exception as e:
warnings.warn('Error in Window.current_size_accurate. Trouble getting x,y size\n{} {}'.format(geometry, geometry_tuple) + str(e), UserWarning)
x, y = (None, None)
return (x,y)
@property
def size(self):
"""
Return the current size of the window in pixels
:return: (width, height) of the window
:rtype: Tuple[(int), (int)] or Tuple[None, None]
"""
if not self._is_window_created('Tried to use Window.size property'):
return (None, None)
win_width = self.TKroot.winfo_width()
win_height = self.TKroot.winfo_height()
return win_width, win_height
@size.setter
def size(self, size):
"""
Changes the size of the window, if possible
:param size: (width, height) of the desired window size
:type size: (int, int)
"""
try:
self.TKroot.geometry("%sx%s" % (size[0], size[1]))
self.TKroot.update_idletasks()
except:
pass
def set_min_size(self, size):
"""
Changes the minimum size of the window. Note Window must be read or finalized first.
:param size: (width, height) tuple (int, int) of the desired window size in pixels
:type size: (int, int)
"""
if not self._is_window_created('tried Window.set_min_size'):
return
self.TKroot.minsize(size[0], size[1])
self.TKroot.update_idletasks()
def visibility_changed(self):
"""
When making an element in a column or someplace that has a scrollbar, then you'll want to call this function
prior to the column's contents_changed() method.
"""
self.refresh()
def set_transparent_color(self, color):
"""
Set the color that will be transparent in your window. Areas with this color will be SEE THROUGH.
:param color: Color string that defines the transparent color
:type color: (str)
"""
if not self._is_window_created('tried Window.set_transparent_color'):
return
try:
self.TKroot.attributes('-transparentcolor', color)
self.TransparentColor = color
except:
print('Transparent color not supported on this platform (windows only)')
def mouse_location(self):
"""
Return the (x,y) location of the mouse relative to the entire screen. It's the same location that
you would use to create a window, popup, etc.
:return: The location of the mouse pointer
:rtype: (int, int)
"""
if not self._is_window_created('tried Window.mouse_location'):
return (0,0)
return (self.TKroot.winfo_pointerx(), self.TKroot.winfo_pointery())
def grab_any_where_on(self):
"""
Turns on Grab Anywhere functionality AFTER a window has been created. Don't try on a window that's not yet
been Finalized or Read.
"""
if not self._is_window_created('tried Window.grab_any_where_on'):
return
self.TKroot.bind("<ButtonPress-1>", self._StartMoveGrabAnywhere)
self.TKroot.bind("<ButtonRelease-1>", self._StopMove)
self.TKroot.bind("<B1-Motion>", self._OnMotionGrabAnywhere)
def grab_any_where_off(self):
"""
Turns off Grab Anywhere functionality AFTER a window has been created. Don't try on a window that's not yet
been Finalized or Read.
"""
if not self._is_window_created('tried Window.grab_any_where_off'):
return
self.TKroot.unbind("<ButtonPress-1>")
self.TKroot.unbind("<ButtonRelease-1>")
self.TKroot.unbind("<B1-Motion>")
def _user_bind_callback(self, bind_string, event):
"""
Used when user binds a tkinter event directly to an element
:param bind_string: The event that was bound so can lookup the key modifier
:type bind_string: (str)
:param event: Event data passed in by tkinter (not used)
:type event:
"""
# print('bind callback', bind_string, event)
key = self.user_bind_dict.get(bind_string, '')
self.user_bind_event = event
if key is not None:
self.LastButtonClicked = key
else:
self.LastButtonClicked = bind_string
self.FormRemainedOpen = True
# if self.CurrentlyRunningMainloop:
# self.TKroot.quit()
_exit_mainloop(self)
def bind(self, bind_string, key):
"""
Used to add tkinter events to a Window.
The tkinter specific data is in the Window's member variable user_bind_event
:param bind_string: The string tkinter expected in its bind function
:type bind_string: (str)
:param key: The event that will be generated when the tkinter event occurs
:type key: str | int | tuple | object
"""
if not self._is_window_created('tried Window.bind'):
return
self.TKroot.bind(bind_string, lambda evt: self._user_bind_callback(bind_string, evt))
self.user_bind_dict[bind_string] = key
def _callback_main_debugger_window_create_keystroke(self, event):
"""
Called when user presses the key that creates the main debugger window
:param event: (event) not used. Passed in event info
:type event:
"""
Window._main_debug_window_build_needed = True
# _Debugger.debugger._build_main_debugger_window()
def _callback_popout_window_create_keystroke(self, event):
"""
Called when user presses the key that creates the floating debugger window
:param event: (event) not used. Passed in event info
:type event:
"""
Window._floating_debug_window_build_needed = True
# _Debugger.debugger._build_floating_window()
def enable_debugger(self):
"""
Enables the internal debugger. By default, the debugger IS enabled
"""
if not self._is_window_created('tried Window.enable_debugger'):
return
self.TKroot.bind('<Cancel>', self._callback_main_debugger_window_create_keystroke)
self.TKroot.bind('<Pause>', self._callback_popout_window_create_keystroke)
self.DebuggerEnabled = True
def disable_debugger(self):
"""
Disable the internal debugger. By default the debugger is ENABLED
"""
if not self._is_window_created('tried Window.disable_debugger'):
return
self.TKroot.unbind("<Cancel>")
self.TKroot.unbind("<Pause>")
self.DebuggerEnabled = False
def set_title(self, title):
"""
Change the title of the window
:param title: The string to set the title to
:type title: (str)
"""
if not self._is_window_created('tried Window.set_title'):
return
self.TKroot.wm_title(str(title))
def make_modal(self):
"""
Makes a window into a "Modal Window"
This means user will not be able to interact with other windows until this one is closed
NOTE - Sorry Mac users - you can't have modal windows.... lobby your tkinter Mac devs
"""
if not self._is_window_created('tried Window.make_modal'):
return
if running_mac() and ENABLE_MAC_MODAL_DISABLE_PATCH:
return
# if modal windows have been disabled globally
if not DEFAULT_MODAL_WINDOWS_ENABLED:
return
try:
self.TKroot.transient()
self.TKroot.grab_set()
self.TKroot.focus_force()
except Exception as e:
print('Exception trying to make modal', e)
def force_focus(self):
"""
Forces this window to take focus
"""
if not self._is_window_created('tried Window.force_focus'):
return
self.TKroot.focus_force()
def was_closed(self):
"""
Returns True if the window was closed
:return: True if the window is closed
:rtype: bool
"""
return self.TKrootDestroyed
def set_cursor(self, cursor):
"""
Sets the cursor for the window.
If you do not want any mouse pointer, then use the string "none"
:param cursor: The tkinter cursor name
:type cursor: (str)
"""
if not self._is_window_created('tried Window.set_cursor'):
return
try:
self.TKroot.config(cursor=cursor)
except Exception as e:
print('Warning bad cursor specified ', cursor)
print(e)
def ding(self, display_number=0):
"""
Make a "bell" sound. A capability provided by tkinter. Your window needs to be finalized prior to calling.
Ring a display's bell is the tkinter description of the call.
:param display_number: Passed to tkinter's bell method as parameter "displayof".
:type display_number: int
"""
if not self._is_window_created('tried Window.ding'):
return
try:
self.TKroot.bell(display_number)
except Exception as e:
if not SUPPRESS_ERROR_POPUPS:
_error_popup_with_traceback('Window.ding() - tkinter reported error from bell() call', e)
def _window_tkvar_changed_callback(self, *args):
"""
Internal callback function for when the thread
:param event: Information from tkinter about the callback
:type event:
"""
# print('Thread callback info', threading.current_thread())
# print(event)
# trace_details = traceback.format_stack()
# print(''.join(trace_details))
# self.thread_lock.acquire()
# if self.thread_timer:
# self.TKroot.after_cancel(id=self.thread_timer)
# self.thread_timer = None
# self.thread_lock.release()
if self._queued_thread_event_available():
self.FormRemainedOpen = True
_exit_mainloop(self)
def _create_thread_queue(self):
"""
Creates the queue used by threads to communicate with this window
"""
if self.thread_queue is None:
self.thread_queue = queue.Queue()
if self.thread_lock is None:
self.thread_lock = threading.Lock()
if self.thread_strvar is None:
self.thread_strvar = tk.StringVar()
self.thread_strvar.trace('w', self._window_tkvar_changed_callback)
def write_event_value(self, key, value):
"""
Adds a key & value tuple to the queue that is used by threads to communicate with the window
:param key: The key that will be returned as the event when reading the window
:type key: Any
:param value: The value that will be in the values dictionary
:type value: Any
"""
if self.thread_queue is None:
print('*** Warning Window.write_event_value - no thread queue found ***')
return
# self.thread_lock.acquire() # first lock the critical section
self.thread_queue.put(item=(key, value))
self.TKroot.tk.willdispatch() # brilliant bit of code provided by Giuliano who I owe a million thank yous!
self.thread_strvar.set('new item')
# self.thread_queue.put(item=(key, value))
# self.thread_strvar.set('new item')
# March 28 2021 - finally found a solution! It needs a little more work and a lock
# if no timer is running, then one should be started
# if self.thread_timer is None:
# print('Starting a timer')
# self.thread_timer = self.TKroot.after(1, self._window_tkvar_changed_callback)
# self.thread_lock.release()
def _queued_thread_event_read(self):
if self.thread_queue is None:
return None
try: # see if something has been posted to Queue
message = self.thread_queue.get_nowait()
except queue.Empty: # get_nowait() will get exception when Queue is empty
return None
return message
def _queued_thread_event_available(self):
if self.thread_queue is None:
return False
# self.thread_lock.acquire()
qsize = self.thread_queue.qsize()
if qsize == 0:
self.thread_timer = None
# self.thread_lock.release()
return qsize != 0
def _RightClickMenuCallback(self, event):
"""
When a right click menu is specified for an entire window, then this callback catches right clicks
that happen to the window itself, when there are no elements that are in that area.
The only portion that is not currently covered correctly is the row frame itself. There will still
be parts of the window, at the moment, that don't respond to a right click. It's getting there, bit
by bit.
Callback function that's called when a right click happens. Shows right click menu as result.
:param event: information provided by tkinter about the event including x,y location of click
:type event:
"""
# if there are widgets under the mouse, then see if it's the root only. If not, then let the widget (element) show their menu instead
x, y = self.TKroot.winfo_pointerxy()
widget = self.TKroot.winfo_containing(x, y)
if widget != self.TKroot:
return
self.TKRightClickMenu.tk_popup(event.x_root, event.y_root, 0)
self.TKRightClickMenu.grab_release()
def perform_long_operation(self, func, end_key):
"""
Call your function that will take a long time to execute. When it's complete, send an event
specified by the end_key.
:param func: A lambda or a function name with no parms
:type func: Any
:param end_key: The key that will be generated when the function returns
:type end_key: (Any)
:return: The id of the thread
:rtype: threading.Thread
"""
thread = threading.Thread(target=_long_func_thread, args=(self, end_key, func), daemon=True)
thread.start()
return thread
@property
def key_dict(self):
"""
Returns a dictionary with all keys and their corresponding elements
{ key : Element }
:return: Dictionary of keys and elements
:rtype: Dict[Any, Element]
"""
return self.AllKeysDict
# def __enter__(self):
# """
# WAS used with context managers which are no longer needed nor advised. It is here for legacy support and
# am afraid of removing right now
# :return: (window)
# :rtype:
# """
# return self
def __getitem__(self, key):
"""
Returns Element that matches the passed in key.
This is "called" by writing code as thus:
window['element key'].Update
:param key: The key to find
:type key: str | int | tuple | object
:rtype: Element | Input | Combo | OptionMenu | Listbox | Radio | Checkbox | Spin | Multiline | Text | StatusBar | Output | Button | ButtonMenu | ProgressBar | Image | Canvas | Graph | Frame | VerticalSeparator | HorizontalSeparator | Tab | TabGroup | Slider | Column | Pane | Menu | Table | Tree | ErrorElement | None
"""
return self.find_element(key)
def __call__(self, *args, **kwargs):
"""
Call window.read but without having to type it out.
window() == window.read()
window(timeout=50) == window.read(timeout=50)
:return: The famous event, values that Read returns.
:rtype: Tuple[Any, Dict[Any, Any]]
"""
return self.read(*args, **kwargs)
def _is_window_created(self, additional_message=''):
msg = str(additional_message)
if self.TKroot is None:
warnings.warn(
'You cannot perform operations on a Window until it is read or finalized. Adding a "finalize=True" parameter to your Window creation will fix this. ' + msg,
UserWarning)
if not SUPPRESS_ERROR_POPUPS:
_error_popup_with_traceback('You cannot perform operations on a Window until it is read or finalized.',
'Adding a "finalize=True" parameter to your Window creation will likely fix this', msg)
return False
return True
def _has_custom_titlebar_element(self):
for elem in self.AllKeysDict.values():
if elem.Key in (TITLEBAR_MAXIMIZE_KEY, TITLEBAR_CLOSE_KEY, TITLEBAR_IMAGE_KEY):
return True
if elem.metadata == TITLEBAR_METADATA_MARKER:
return True
return False
AddRow = add_row
AddRows = add_rows
AlphaChannel = alpha_channel
BringToFront = bring_to_front
Close = close
CurrentLocation = current_location
Disable = disable
DisableDebugger = disable_debugger
Disappear = disappear
Enable = enable
EnableDebugger = enable_debugger
Fill = fill
Finalize = finalize
# FindElement = find_element
FindElementWithFocus = find_element_with_focus
GetScreenDimensions = get_screen_dimensions
GrabAnyWhereOff = grab_any_where_off
GrabAnyWhereOn = grab_any_where_on
Hide = hide
Layout = layout
LoadFromDisk = load_from_disk
Maximize = maximize
Minimize = minimize
Move = move
Normal = normal
Read = read
Reappear = reappear
Refresh = refresh
SaveToDisk = save_to_disk
SendToBack = send_to_back
SetAlpha = set_alpha
SetIcon = set_icon
SetTransparentColor = set_transparent_color
Size = size
UnHide = un_hide
VisibilityChanged = visibility_changed
CloseNonBlocking = close
CloseNonBlockingForm = close
#
# def __exit__(self, *a):
# """
# WAS used with context managers which are no longer needed nor advised. It is here for legacy support and
# am afraid of removing right now
# :param *a: (?) Not sure what's passed in.
# :type *a:
# :return: Always returns False which was needed for context manager to work
# :rtype:
# """
# self.__del__()
# return False
#
# def __del__(self):
# # print('DELETING WINDOW')
# for row in self.Rows:
# for element in row:
# element.__del__()
# -------------------------------- PEP8-ify the Window Class USER Interfaces -------------------------------- #
FlexForm = Window
def _long_func_thread(window, end_key, original_func):
"""
Used to run long operations on the user's behalf. Called by the window object
:param window: The window that will get the event
:type window: (Window)
:param end_key: The event that will be sent when function returns
:type end_key: (Any)
:param original_func: The user's function that is called. Can be a function with no arguments or a lambda experession
:type original_func: (Any)
"""
return_value = original_func()
window.write_event_value(end_key, return_value)
def _exit_mainloop(exiting_window):
if exiting_window == Window._window_running_mainloop or Window._root_running_mainloop == Window.hidden_master_root:
Window._window_that_exited = exiting_window
if Window._root_running_mainloop is not None:
Window._root_running_mainloop.quit()
# print('** Exited window mainloop **')
def _timeout_alarm_callback_hidden():
"""
Read Timeout Alarm callback. Will kick a mainloop call out of the tkinter event loop and cause it to return
"""
del Window._TKAfterID
# first, get the results table built
# modify the Results table in the parent FlexForm object
# print('TIMEOUT CALLBACK')
Window._root_running_mainloop.quit() # kick the users out of the mainloop
# Get window that caused return
Window._window_that_exited = None
def read_all_windows(timeout=None, timeout_key=TIMEOUT_KEY):
"""
Reads all windows that are "active" when the call is made. "Active" means that it's been finalized or read.
If a window has not been finalized then it will not be considered an "active window"
If any of the active windows returns a value then the window and its event and values
are returned.
If no windows are open, then the value (None, WIN_CLOSED, None) will be returned
Since WIN_CLOSED is None, it means (None, None, None) is what's returned when no windows remain opened
:param timeout: Time in milliseconds to delay before a returning a timeout event
:type timeout: (int)
:param timeout_key: Key to return when a timeout happens. Defaults to the standard TIMEOUT_KEY
:type timeout_key: (Any)
:return: A tuple with the (Window, event, values dictionary/list)
:rtype: (Window, Any, Dict | List)
"""
if len(Window._active_windows) == 0:
return None, WIN_CLOSED, None
# first see if any queued events are waiting for any of the windows
for window in Window._active_windows.keys():
if window._queued_thread_event_available():
_BuildResults(window, False, window)
event, values = window.ReturnValues
return window, event, values
Window._root_running_mainloop = Window.hidden_master_root
Window._timeout_key = timeout_key
if timeout == 0:
window = list(Window._active_windows.keys())[Window._timeout_0_counter]
event, values = window._ReadNonBlocking()
if event is None:
event = timeout_key
if values is None:
event = None
Window._timeout_0_counter = (Window._timeout_0_counter + 1) % len(Window._active_windows)
return window, event, values
Window._timeout_0_counter = 0 # reset value if not reading with timeout 0 so ready next time needed
# setup timeout timer
if timeout != None:
try:
Window.hidden_master_root.after_cancel(Window._TKAfterID)
del Window._TKAfterID
except:
pass
Window._TKAfterID = Window.hidden_master_root.after(timeout, _timeout_alarm_callback_hidden)
# ------------ Call Mainloop ------------
Window._root_running_mainloop.mainloop()
try:
Window.hidden_master_root.after_cancel(Window._TKAfterID)
del Window._TKAfterID
except:
pass
# print('** tkafter cancel failed **')
# Get window that caused return
window = Window._window_that_exited
if window is None:
return None, timeout_key, None
if window.XFound:
event, values = None, None
window.close()
try:
del Window._active_windows[window]
except:
pass
# print('Error deleting window, but OK')
else:
_BuildResults(window, False, window)
event, values = window.ReturnValues
return window, event, values
###### ## ## ###### ######## ######## ## ##
## ## ## ## ## ## ## ## ### ###
## #### ## ## ## #### ####
###### ## ###### ## ###### ## ### ##
## ## ## ## ## ## ##
## ## ## ## ## ## ## ## ##
###### ## ###### ## ######## ## ##
######## ######## ### ## ##
## ## ## ## ## ## ##
## ## ## ## ## ####
## ######## ## ## ##
## ## ## ######### ##
## ## ## ## ## ##
## ## ## ## ## ##
# ------------------------------------------------------------------------- #
# SystemTray - class for implementing a psyeudo tray #
# ------------------------------------------------------------------------- #
# -------------------------------- System Tray Begins Here -------------------------------- #
# Feb 2020 - Just starting on this so code commented out for now. Basing on PySimpleGUIQt's implementation / call format
# -------------------------------------------------------------------
# fade in/out info and default window alpha
SYSTEM_TRAY_WIN_MARGINS = 160, 60 # from right edge of screen, from bottom of screen
SYSTEM_TRAY_MESSAGE_MAX_LINE_LENGTH = 50
# colors
SYSTEM_TRAY_MESSAGE_WIN_COLOR = "#282828"
SYSTEM_TRAY_MESSAGE_TEXT_COLOR = "#ffffff"
SYSTEM_TRAY_MESSAGE_DISPLAY_DURATION_IN_MILLISECONDS = 3000 # how long to display the window
SYSTEM_TRAY_MESSAGE_FADE_IN_DURATION = 1000 # how long to fade in / fade out the window
EVENT_SYSTEM_TRAY_ICON_DOUBLE_CLICKED = '__DOUBLE_CLICKED__'
EVENT_SYSTEM_TRAY_ICON_ACTIVATED = '__ACTIVATED__'
EVENT_SYSTEM_TRAY_MESSAGE_CLICKED = '__MESSAGE_CLICKED__'
# Base64 Images to use as icons in the window
_tray_icon_error = b'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAA3NCSVQICAjb4U/gAAAACXBIWXMAAADlAAAA5QGP5Zs8AAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAIpQTFRF////20lt30Bg30pg4FJc409g4FBe4E9f4U9f4U9g4U9f4E9g31Bf4E9f4E9f4E9f4E9f4E9f4FFh4Vdm4lhn42Bv5GNx5W575nJ/6HqH6HyI6YCM6YGM6YGN6oaR8Kev9MPI9cbM9snO9s3R+Nfb+dzg+d/i++vt/O7v/fb3/vj5//z8//7+////KofnuQAAABF0Uk5TAAcIGBktSYSXmMHI2uPy8/XVqDFbAAAA8UlEQVQ4y4VT15LCMBBTQkgPYem9d9D//x4P2I7vILN68kj2WtsAhyDO8rKuyzyLA3wjSnvi0Eujf3KY9OUP+kno651CvlB0Gr1byQ9UXff+py5SmRhhIS0oPj4SaUUCAJHxP9+tLb/ezU0uEYDUsCc+l5/T8smTIVMgsPXZkvepiMj0Tm5txQLENu7gSF7HIuMreRxYNkbmHI0u5Hk4PJOXkSMz5I3nyY08HMjbpOFylF5WswdJPmYeVaL28968yNfGZ2r9gvqFalJNUy2UWmq1Wa7di/3Kxl3tF1671YHRR04dWn3s9cXRV09f3vb1fwPD7z9j1WgeRgAAAABJRU5ErkJggg=='
_tray_icon_success = b'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAA3NCSVQICAjb4U/gAAAACXBIWXMAAAEKAAABCgEWpLzLAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAHJQTFRF////ZsxmbbZJYL9gZrtVar9VZsJcbMRYaMZVasFYaL9XbMFbasRZaMFZacRXa8NYasFaasJaasFZasJaasNZasNYasJYasJZasJZasJZasJZasJZasJYasJZasJZasJZasJZasJaasJZasJZasJZasJZ2IAizQAAACV0Uk5TAAUHCA8YGRobHSwtPEJJUVtghJeYrbDByNjZ2tvj6vLz9fb3/CyrN0oAAADnSURBVDjLjZPbWoUgFIQnbNPBIgNKiwwo5v1fsQvMvUXI5oqPf4DFOgCrhLKjC8GNVgnsJY3nKm9kgTsduVHU3SU/TdxpOp15P7OiuV/PVzk5L3d0ExuachyaTWkAkLFtiBKAqZHPh/yuAYSv8R7XE0l6AVXnwBNJUsE2+GMOzWL8k3OEW7a/q5wOIS9e7t5qnGExvF5Bvlc4w/LEM4Abt+d0S5BpAHD7seMcf7+ZHfclp10TlYZc2y2nOqc6OwruxUWx0rDjNJtyp6HkUW4bJn0VWdf/a7nDpj1u++PBOR694+Ftj/8PKNdnDLn/V8YAAAAASUVORK5CYII='
_tray_icon_halt = b'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAMAUExURQAAANswNuMPDO8HBO8FCe0HCu4IBu4IB+oLDeoLDu8JC+wKCu4JDO4LDOwKEe4OEO4OEeUQDewQDe0QDucVEuYcG+ccHOsQFuwWHe4fH/EGAvMEBfMFBvAHBPMGBfEGBvYCAfYDAvcDA/cDBPcDBfUDBvYEAPYEAfYEAvYEA/QGAPQGAfQGAvYEBPUEBvYFB/QGBPQGBfQHB/EFCvIHCPMHCfIHC/IFDfMHDPQGCPQGCfQGCvEIBPIIBfAIB/UIB/QICPYICfoBAPoBAfoBAvsBA/kCAPkCAfkCAvkCA/oBBPkCBPkCBfkCBvgCB/gEAPkEAfgEAvkEA/gGAfkGAvkEBPgEBfkEBv0AAP0AAfwAAvwAA/wCAPwCAfwCAvwCA/wABP0ABfwCBfwEAPwFA/ASD/ESFPAUEvAUE/EXFvAdH+kbIOobIeofIfEfIOcmKOohIukgJOggJesiKuwiKewoLe0tLO0oMOQ3OO43Oew4OfAhIPAhIfAiIPEiI+dDRe9ES+lQTOdSWupSUOhTUehSV+hUVu1QUO1RUe1SV+tTWe5SWOxXWOpYV+pZWelYXexaW+xaXO9aX+lZYeNhYOxjZ+lna+psbOttbehsbupscepucuxtcuxucep3fet7e+p/ffB6gOmKiu2Iie2Sk+2Qle2QluySlOyTleuYmvKFivCOjgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIxGNZsAAAEAdFJOU////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wBT9wclAAAACXBIWXMAAA7DAAAOwwHHb6hkAAACVElEQVQ4T22S93PTMBhADQdl791SSsuRARTKKHsn+STZBptAi6zIacous+w9yyxl7z1T1h8ptHLhrrzLD5+/987R2XZElZ/39tZsbGg42NdvF4pqcGMs4XEcozAB/oQeu6wGr5fkAZcKOUIIRgQXR723wgaXt/NSgcwlO1r3oARkATfhbmNMMCnlMZdz5J8RN9fVhglS5JA/pJUOJiYXoShCkz/flheDvpzlBCBmya5KcDG1sMSB+r/VQtG+YoFXlwN0Us4yeBXujPmWCOqNlVwX5zHntLH5iQ420YiqX9pqTZFSCrBGBc+InBUDAsbwLRlMC40fGJT8YLRwfnhY3v6/AUtDc9m5z0tRJBOAvHUaFchdY6+zDzEghHv1tUnrNCaIOw84Q2WQmkeO/Xopj1xFBREFr8ZZjuRhA++PEB+t05ggwBucpbH8i/n5C1ZU0EEEmRZnSMxoIYcarKigA0Cb1zpHAyZnGj21xqICAA9dcvo4UgEdZ41FBZSTzEOn30f6QeE3Vhl0gLN+2RGDzZPMHLHKoAO3MFy+ix4sDxFlvMXfrdNgFezy7qrXPaaJg0u27j5nneKrCjJ4pf4e3m4DVMcjNNNKxWnpo6jtnfnkunExB4GbuGKk5FNanpB1nJCjCsThJPAAJ8lVdSF5sSrklM2ZqmYdiC40G7Dfnhp57ZsQz6c3hylEO6ZoZQJxqiVgbhoQK3T6AIgU4rbjxthAPF6NAwAOAcS+ixlp/WBFJRDi0fj2RtcjWRwif8Qdu/w3EKLcu3/YslnrZzwo24UQQvwFCrp/iM1NnHwAAAAASUVORK5CYII='
_tray_icon_notallowed = b'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAMAUExURQAAAPcICPcLC/cMDPcQEPcSEvcXF/cYGPcaGvcbG/ccHPgxMfgyMvg0NPg5Ofg6Ovg7O/hBQfhCQvlFRflGRvljY/pkZPplZfpnZ/p2dgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMgEwNYAAAEAdFJOU////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wBT9wclAAAACXBIWXMAAA7DAAAOwwHHb6hkAAABE0lEQVQ4T4WT65bDIAiExWbbtN0m3Uua+P4P6g4jGtN4NvNL4DuCCC5WWobe++uwmEmtwNxJUTebcwWCt5jJBwsYcKf3NE4hTOOJxj1FEnBTz4NH6qH2jUcCGr/QLLpkQgHe/6VWJXVqFgBB4yI/KVCkBCoFgPrPHw0CWbwCL8RibBFwzQDQH62/QeAtHQBeADUIDbkF/UnmnkB1ixtERrN3xCgyuF5kMntHTCJXh2vyv+wIdMhvgTeCQJ0C2hBMgSKfZlM1wSLXZ5oqgs8sjSpaCQ2VVlfKhLU6fdZGSvyWz9JMb+NE4jt/Nwfm0yJZSkBpYDg7TcJGrjm0Z7jK0B6P/fHiHK8e9Pp/eSmuf1+vf4x/ralnCN9IrncAAAAASUVORK5CYII='
_tray_icon_stop = b'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAMAUExURQAAANsAANsBAdsCAtwEBNwFBdwHB9wICNwLC90MDN0NDd0PD90REd0SEt4TE94UFN4WFt4XF94ZGeAjI+AlJeEnJ+EpKeEqKuErK+EsLOEuLuIvL+IyMuIzM+M1NeM2NuM3N+M6OuM8POQ9PeQ+PuQ/P+RAQOVISOVJSeVKSuZLS+ZOTuZQUOZRUedSUudVVehbW+lhYeljY+poaOtvb+twcOtxcetzc+t0dOx3d+x4eOx6eu19fe1+fu2AgO2Cgu6EhO6Ghu6Hh+6IiO6Jie+Kiu+Li++MjO+Nje+Oju+QkPCUlPCVlfKgoPKkpPKlpfKmpvOrq/SurvSxsfSysvW4uPW6uvW7u/W8vPa9vfa+vvbAwPbCwvfExPfFxffGxvfHx/fIyPfJyffKyvjLy/jNzfjQ0PjR0fnS0vnU1PnY2Pvg4Pvi4vvj4/vl5fvm5vzo6Pzr6/3u7v3v7/3x8f3z8/309P719f729v739/74+P75+f76+v77+//8/P/9/f/+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPHCyoUAAAEAdFJOU////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wBT9wclAAAACXBIWXMAAA7DAAAOwwHHb6hkAAABnUlEQVQ4T33S50PTQBgG8D6lzLbsIUv2kD0FFWTvPWTvISDIUBGV1ecvj+8luZTR9P1wSe755XK5O4+hK4gn5bc7DcMBz/InQoMXeVjY4FXuCAtEyLUwQcTcFgq45JYQ4JqbwhMtV8IjeUJDjQ+5paqCyG9srEsGgoUlpeXpIjxA1nfyi2+Jqmo7Q9JeV+ODerpvBQTM8/ySzQ3t+xxoL7h7nJve5jd85M7wJq9McHaT8o6TwBTfIIfHQGzoAZ/YiSTSq8D5dSDQVqFADrJ5KFMLPaKLHQiQMQoscClezdgCB4CXD/jM90izR8g85UaKA3YAn4AejhV189acA5LX+DVOg00gnvfoVX/BRQsgbplNGqzLusgIffx1tDchiyRgdRbVHNdgRRZHQD9H1asm+PMzYyYMtoBU/sYgRxxgrmGtBRL/cnf5RL4zzCEHZF2QE14LoOWf6B9vMcJBG/iBxKo8dVtYnyStv6yuUq7FLfmqTzbLEOFest1GNGEemCjCPnKuwjm0LsLMbRBJWLkGr4WdO+Cl0HkYPBc6N4z//HcQqVwcOuIAAAAASUVORK5CYII='
_tray_icon_exclamation = b'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAMAUExURQAAAN0zM900NN01Nd02Nt03N944ON45Od46Ot47O98/P99BQd9CQt9DQ+FPT+JSUuJTU+JUVOJVVeJWVuNbW+ReXuVjY+Zra+dxceh4eOl7e+l8fOl+ful/f+qBgeqCguqDg+qFheuJieuLi+yPj+yQkO2Wlu+cnO+hofGqqvGtrfre3vrf3/ri4vvn5/75+f76+v/+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMQ8SQkAAAEAdFJOU////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wBT9wclAAAACXBIWXMAAA7DAAAOwwHHb6hkAAABJElEQVQ4T4WS63KCMBBGsyBai62X0otY0aq90ZZa3v/dtpvsJwTijOfXt7tnILOJYY9tNonjNCtQOlqhuKKG0RrNVjgkmIHBHgMId+h7zHSiwg2a9FNVVYScupETmjkd67o+CWpYwft+R6CpCgeUlq5AOyf45+8JsRUKFI6eQLkI3n5CIREBUekLxGaLpATCymRISiAszARJCYSxiZGUQKDLQoqgnPnFhUPOTWeRoZD3FvVZlmVHkE2OEM9iV71GVoZDBGUpAg9QWN5/jx+Ilsi9hz0q4VHOWD+hEF70yc1QEr1a4Q0F0S3eJDfLuv8T4QEFXduZE1rj+et7g6hzCDxF08N+X4DAu+6lUSTnc5wE5tx73ckSTV8QVoux3N88Rykw/wP3i+vwPKk17AAAAABJRU5ErkJggg=='
_tray_icon_none = None
SYSTEM_TRAY_MESSAGE_ICON_INFORMATION = _tray_icon_success
SYSTEM_TRAY_MESSAGE_ICON_WARNING = _tray_icon_exclamation
SYSTEM_TRAY_MESSAGE_ICON_CRITICAL = _tray_icon_stop
SYSTEM_TRAY_MESSAGE_ICON_NOICON = _tray_icon_none
# ------------------------------------------------------------------------- #
# Tray CLASS #
# ------------------------------------------------------------------------- #
class SystemTray:
"""
A "Simulated System Tray" that duplicates the API calls available to PySimpleGUIWx and PySimpleGUIQt users.
All of the functionality works. The icon is displayed ABOVE the system tray rather than inside of it.
"""
def __init__(self, menu=None, filename=None, data=None, data_base64=None, tooltip=None, metadata=None):
"""
SystemTray - create an icon in the system tray
:param menu: Menu definition. Example - ['UNUSED', ['My', 'Simple', '---', 'Menu', 'Exit']]
:type menu: List[List[List[str] or str]]
:param filename: filename for icon
:type filename: (str)
:param data: in-ram image for icon (same as data_base64 parm)
:type data: (bytes)
:param data_base64: base-64 data for icon
:type data_base64: (bytes)
:param tooltip: tooltip string
:type tooltip: (str)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self._metadata = None
self.Menu = menu
self.TrayIcon = None
self.Shown = False
self.MenuItemChosen = TIMEOUT_KEY
self.metadata = metadata
self.last_message_event = None
screen_size = Window.get_screen_size()
if filename:
image_elem = Image(filename=filename, background_color='red', enable_events=True, tooltip=tooltip, key='-IMAGE-')
elif data_base64:
image_elem = Image(data=data_base64, background_color='red', enable_events=True, tooltip=tooltip, key='-IMAGE-')
elif data:
image_elem = Image(data=data, background_color='red', enable_events=True, tooltip=tooltip, key='-IMAGE-')
else:
image_elem = Image(background_color='red', enable_events=True, tooltip=tooltip, key='-IMAGE-')
layout = [
[image_elem],
]
self.window = Window('Window Title', layout, element_padding=(0, 0), margins=(0, 0), grab_anywhere=True, no_titlebar=True, transparent_color='red',
keep_on_top=True, right_click_menu=menu, location=(screen_size[0] - 100, screen_size[1] - 100), finalize=True)
self.window['-IMAGE-'].bind('<Double-Button-1>', '+DOUBLE_CLICK')
@property
def metadata(self):
"""
Metadata is an SystemTray property that you can use at any time to hold any value
:return: the current metadata value
:rtype: (Any)
"""
return self._metadata
@metadata.setter
def metadata(self, value):
"""
Metadata is an SystemTray property that you can use at any time to hold any value
:param value: Anything you want it to be
:type value: (Any)
"""
self._metadata = value
def read(self, timeout=None):
"""
Reads the context menu
:param timeout: Optional. Any value other than None indicates a non-blocking read
:type timeout:
:return:
:rtype:
"""
if self.last_message_event != TIMEOUT_KEY and self.last_message_event is not None:
event = self.last_message_event
self.last_message_event = None
return event
event, values = self.window.read(timeout=timeout)
if event.endswith('DOUBLE_CLICK'):
return EVENT_SYSTEM_TRAY_ICON_DOUBLE_CLICKED
elif event == '-IMAGE-':
return EVENT_SYSTEM_TRAY_ICON_ACTIVATED
return event
def hide(self):
"""
Hides the icon
"""
self.window.hide()
def un_hide(self):
"""
Restores a previously hidden icon
"""
self.window.un_hide()
def show_message(self, title, message, filename=None, data=None, data_base64=None, messageicon=None,
time=(SYSTEM_TRAY_MESSAGE_FADE_IN_DURATION, SYSTEM_TRAY_MESSAGE_DISPLAY_DURATION_IN_MILLISECONDS)):
"""
Shows a balloon above icon in system tray
:param title: Title shown in balloon
:type title: str
:param message: Message to be displayed
:type message: str
:param filename: Optional icon filename
:type filename: str
:param data: Optional in-ram icon
:type data: b''
:param data_base64: Optional base64 icon
:type data_base64: b''
:param time: Amount of time to display message in milliseconds. If tuple, first item is fade in/out duration
:type time: int | (int, int)
:return: The event that happened during the display such as user clicked on message
:rtype: Any
"""
if isinstance(time, tuple):
fade_duraction, display_duration = time
else:
fade_duration = SYSTEM_TRAY_MESSAGE_FADE_IN_DURATION
display_duration = time
user_icon = data_base64 or filename or data or messageicon
event = self.notify(title, message, icon=user_icon, fade_in_duration=fade_duraction, display_duration_in_ms=display_duration)
self.last_message_event = event
return event
def close(self):
"""
Close the system tray window
"""
self.window.close()
def update(self, menu=None, tooltip=None, filename=None, data=None, data_base64=None, ):
"""
Updates the menu, tooltip or icon
:param menu: menu defintion
:type menu: ???
:param tooltip: string representing tooltip
:type tooltip: ???
:param filename: icon filename
:type filename: ???
:param data: icon raw image
:type data: ???
:param data_base64: icon base 64 image
:type data_base64: ???
"""
# Menu
if menu is not None:
top_menu = tk.Menu(self.window.TKroot, tearoff=False)
AddMenuItem(top_menu, menu[1], self.window['-IMAGE-'])
self.window['-IMAGE-'].TKRightClickMenu = top_menu
if filename:
self.window['-IMAGE-'].update(filename=filename)
elif data_base64:
self.window['-IMAGE-'].update(data=data_base64)
elif data:
self.window['-IMAGE-'].update(data=data)
if tooltip:
self.window['-IMAGE-'].set_tooltip(tooltip)
@classmethod
def notify(cls, title, message, icon=_tray_icon_success, display_duration_in_ms=SYSTEM_TRAY_MESSAGE_DISPLAY_DURATION_IN_MILLISECONDS,
fade_in_duration=SYSTEM_TRAY_MESSAGE_FADE_IN_DURATION, alpha=0.9, location=None):
"""
Displays a "notification window", usually in the bottom right corner of your display. Has an icon, a title, and a message
The window will slowly fade in and out if desired. Clicking on the window will cause it to move through the end the current "phase". For example, if the window was fading in and it was clicked, then it would immediately stop fading in and instead be fully visible. It's a way for the user to quickly dismiss the window.
:param title: Text to be shown at the top of the window in a larger font
:type title: (str)
:param message: Text message that makes up the majority of the window
:type message: (str)
:param icon: A base64 encoded PNG/GIF image or PNG/GIF filename that will be displayed in the window
:type icon: bytes | str
:param display_duration_in_ms: Number of milliseconds to show the window
:type display_duration_in_ms: (int)
:param fade_in_duration: Number of milliseconds to fade window in and out
:type fade_in_duration: (int)
:param alpha: Alpha channel. 0 - invisible 1 - fully visible
:type alpha: (float)
:param location: Location on the screen to display the window
:type location: (int, int)
:return: (int) reason for returning
:rtype: (int)
"""
messages = message.split('\n')
full_msg = ''
for m in messages:
m_wrap = textwrap.fill(m, SYSTEM_TRAY_MESSAGE_MAX_LINE_LENGTH)
full_msg += m_wrap + '\n'
message = full_msg[:-1]
win_msg_lines = message.count("\n") + 1
max_line = max(message.split('\n'))
screen_res_x, screen_res_y = Window.get_screen_size()
win_margin = SYSTEM_TRAY_WIN_MARGINS # distance from screen edges
win_width, win_height = 364, 66 + (14.8 * win_msg_lines)
layout = [[Graph(canvas_size=(win_width, win_height), graph_bottom_left=(0, win_height), graph_top_right=(win_width, 0), key="-GRAPH-",
background_color=SYSTEM_TRAY_MESSAGE_WIN_COLOR, enable_events=True)]]
win_location = location if location is not None else (screen_res_x - win_width - win_margin[0], screen_res_y - win_height - win_margin[1])
window = Window(title, layout, background_color=SYSTEM_TRAY_MESSAGE_WIN_COLOR, no_titlebar=True,
location=win_location, keep_on_top=True, alpha_channel=0, margins=(0, 0), element_padding=(0, 0), grab_anywhere=True, finalize=True)
window["-GRAPH-"].draw_rectangle((win_width, win_height), (-win_width, -win_height), fill_color=SYSTEM_TRAY_MESSAGE_WIN_COLOR,
line_color=SYSTEM_TRAY_MESSAGE_WIN_COLOR)
if type(icon) is bytes:
window["-GRAPH-"].draw_image(data=icon, location=(20, 20))
elif icon is not None:
window["-GRAPH-"].draw_image(filename=icon, location=(20, 20))
window["-GRAPH-"].draw_text(title, location=(64, 20), color=SYSTEM_TRAY_MESSAGE_TEXT_COLOR, font=("Helvetica", 12, "bold"),
text_location=TEXT_LOCATION_TOP_LEFT)
window["-GRAPH-"].draw_text(message, location=(64, 44), color=SYSTEM_TRAY_MESSAGE_TEXT_COLOR, font=("Helvetica", 9),
text_location=TEXT_LOCATION_TOP_LEFT)
window["-GRAPH-"].set_cursor('hand2')
if fade_in_duration:
for i in range(1, int(alpha * 100)): # fade in
window.set_alpha(i / 100)
event, values = window.read(timeout=fade_in_duration // 100)
if event != TIMEOUT_KEY:
window.set_alpha(1)
break
if event != TIMEOUT_KEY:
window.close()
return EVENT_SYSTEM_TRAY_MESSAGE_CLICKED if event == '-GRAPH-' else event
event, values = window(timeout=display_duration_in_ms)
if event == TIMEOUT_KEY:
for i in range(int(alpha * 100), 1, -1): # fade out
window.set_alpha(i / 100)
event, values = window.read(timeout=fade_in_duration // 100)
if event != TIMEOUT_KEY:
break
else:
window.set_alpha(alpha)
event, values = window(timeout=display_duration_in_ms)
window.close()
return EVENT_SYSTEM_TRAY_MESSAGE_CLICKED if event == '-GRAPH-' else event
Close = close
Hide = hide
Read = read
ShowMessage = show_message
UnHide = un_hide
Update = update
# ################################################################################
# ################################################################################
# END OF ELEMENT DEFINITIONS
# ################################################################################
# ################################################################################
# =========================================================================== #
# Button Lazy Functions so the caller doesn't have to define a bunch of stuff #
# =========================================================================== #
# ------------------------- A fake Element... the Pad Element ------------------------- #
def Sizer(h_pixels=0, v_pixels=0):
"""
"Pushes" out the size of whatever it is placed inside of. This includes Columns, Frames, Tabs and Windows
:param h_pixels: number of horizontal pixels
:type h_pixels: (int)
:param v_pixels: number of vertical pixels
:type v_pixels: (int)
:return: (Column) A column element that has a pad setting set according to parameters
:rtype: (Column)
"""
return Column([[]], pad=((h_pixels, 0), (v_pixels, 0)))
def pin(elem, vertical_alignment=None, shrink=True, expand_x=None, expand_y=None):
"""
Pin's an element provided into a layout so that when it's made invisible and visible again, it will
be in the correct place. Otherwise it will be placed at the end of its containing window/column.
:param elem: the element to put into the layout
:type elem: Element
:param vertical_alignment: Aligns elements vertically. 'top', 'center', 'bottom'. Can be shortened to 't', 'c', 'b'
:type vertical_alignment: str | None
:param shrink: If True, then the space will shrink down to a single pixel when hidden. False leaves the area large and blank
:type shrink: bool
:param expand_x: If True/False the value will be passed to the Column Elements used to make this feature
:type expand_x: (bool)
:param expand_y: If True/False the value will be passed to the Column Elements used to make this feature
:type expand_y: (bool)
:return: A column element containing the provided element
:rtype: Column
"""
if shrink:
# return Column([[elem, Canvas(size=(0, 0),background_color=elem.BackgroundColor, pad=(0, 0))]], pad=(0, 0), vertical_alignment=vertical_alignment, expand_x=expand_x, expand_y=expand_y)
return Column([[elem, Column([[]],pad=(0,0))]], pad=(0, 0), vertical_alignment=vertical_alignment, expand_x=expand_x, expand_y=expand_y)
else:
return Column([[elem]], pad=(0, 0), vertical_alignment=vertical_alignment, expand_x=expand_x, expand_y=expand_y)
def vtop(elem_or_row, expand_x=None, expand_y=None):
"""
Align an element or a row of elements to the top of the row that contains it
:param elem_or_row: the element or row of elements
:type elem_or_row: Element | List[Element] | Tuple[Element]
:param expand_x: If True/False the value will be passed to the Column Elements used to make this feature
:type expand_x: (bool)
:param expand_y: If True/False the value will be passed to the Column Elements used to make this feature
:type expand_y: (bool)
:return: A column element containing the provided element aligned to the top or list of elements (a row)
:rtype: Column | List[Column]
"""
if isinstance(elem_or_row, list) or isinstance(elem_or_row, tuple):
return [Column([[e]], pad=(0, 0), vertical_alignment='top', expand_x=expand_x, expand_y=expand_y) for e in elem_or_row]
return Column([[elem_or_row]], pad=(0, 0), vertical_alignment='top', expand_x=expand_x, expand_y=expand_y)
def vcenter(elem_or_row, expand_x=None, expand_y=None):
"""
Align an element or a row of elements to the center of the row that contains it
:param elem_or_row: the element or row of elements
:type elem_or_row: Element | List[Element] | Tuple[Element]
:param expand_x: If True/False the value will be passed to the Column Elements used to make this feature
:type expand_x: (bool)
:param expand_y: If True/False the value will be passed to the Column Elements used to make this feature
:type expand_y: (bool)
:return: A column element containing the provided element aligned to the center or list of elements (a row)
:rtype: Column | List[Column]
"""
if isinstance(elem_or_row, list) or isinstance(elem_or_row, tuple):
return [Column([[e]], pad=(0, 0), vertical_alignment='center') for e in elem_or_row]
return Column([[elem_or_row]], pad=(0, 0), vertical_alignment='center')
def vbottom(elem_or_row, expand_x=None, expand_y=None):
"""
Align an element or a row of elements to the bottom of the row that contains it
:param elem_or_row: the element or row of elements
:type elem_or_row: Element | List[Element] | Tuple[Element]
:param expand_x: If True/False the value will be passed to the Column Elements used to make this feature
:type expand_x: (bool)
:param expand_y: If True/False the value will be passed to the Column Elements used to make this feature
:type expand_y: (bool)
:return: A column element containing the provided element aligned to the bottom or list of elements (a row)
:rtype: Column | List[Column]
"""
if isinstance(elem_or_row, list) or isinstance(elem_or_row, tuple):
return [Column([[e]], pad=(0, 0), vertical_alignment='bottom') for e in elem_or_row]
return Column([[elem_or_row]], pad=(0, 0), vertical_alignment='bottom')
def Titlebar(title='', icon=None, text_color=None, background_color=None, font=None, key=None, k=None):
"""
A custom titlebar that replaces the OS provided titlebar, thus giving you control
the is not possible using the OS provided titlebar such as the color.
NOTE LINUX USERS - at the moment the minimize function is not yet working. Windows users
should have no problem and it should function as a normal window would.
This titlebar is created from a row of elements that is then encapulated into a
single Column element which is what the Titlebar returns to you.
A custom titlebar removes the margins from your window. Ify ou want the remainder
of your Window to have margins, place the layout after the Titlebar into a Column and
set the pad of that Column to the dimensions you would like your margins to have.
The Titlebar is a COLUMN element. You can thus call the update method for the column and
perform operations such as making the column visible/invisible
:param icon: Can be either a filename or Base64 byte string of a PNG. For Windows if filename, it MUST be ICO format. For Linux, must NOT be ICO
:type icon: str or bytes or None
:param title: The "title" to show in the titlebar
:type title: str
:param text_color: Text color for titlebar
:type text_color: str | None
:param background_color: Background color for titlebar
:type background_color: str | None
:param font: Font to be used for the text and the symbols
:type font: (str or (str, int[, str]) or None)
:param key: Identifies an Element. Should be UNIQUE to this window.
:type key: str | int | tuple | object | None
:param k: Exactly the same as key. Choose one of them to use
:type k: str | int | tuple | object | None
:return: A single Column element that has eveything in 1 element
:rtype: Column
"""
bc = background_color or CUSTOM_TITLEBAR_BACKGROUND_COLOR or theme_button_color()[1]
tc = text_color or CUSTOM_TITLEBAR_TEXT_COLOR or theme_button_color()[0]
font = font or CUSTOM_TITLEBAR_FONT or ('Helvetica', 12)
key = k or key
if isinstance(icon, bytes):
icon_and_text_portion = [Image(data=icon, background_color=bc, key=TITLEBAR_IMAGE_KEY)]
elif icon == TITLEBAR_DO_NOT_USE_AN_ICON:
icon_and_text_portion = []
elif icon is not None:
icon_and_text_portion = [Image(filename=icon, background_color=bc, key=TITLEBAR_IMAGE_KEY)]
elif CUSTOM_TITLEBAR_ICON is not None:
if isinstance(CUSTOM_TITLEBAR_ICON, bytes):
icon_and_text_portion = [Image(data=CUSTOM_TITLEBAR_ICON, background_color=bc, key=TITLEBAR_IMAGE_KEY)]
else:
icon_and_text_portion = [Image(filename=CUSTOM_TITLEBAR_ICON, background_color=bc, key=TITLEBAR_IMAGE_KEY)]
else:
icon_and_text_portion = [Image(data=DEFAULT_BASE64_ICON_16_BY_16, background_color=bc, key=TITLEBAR_IMAGE_KEY)]
icon_and_text_portion += [T(title, text_color=tc, background_color=bc, font=font, grab=True)]
return Column([[Column([icon_and_text_portion], pad=(0, 0), background_color=bc),
Column([[T(SYMBOL_TITLEBAR_MINIMIZE, text_color=tc, background_color=bc, enable_events=True, font=font, key=TITLEBAR_MINIMIZE_KEY),
Text(SYMBOL_TITLEBAR_MAXIMIZE, text_color=tc, background_color=bc, enable_events=True, font=font, key=TITLEBAR_MAXIMIZE_KEY),
Text(SYMBOL_TITLEBAR_CLOSE, text_color=tc, background_color=bc, font=font, enable_events=True, key=TITLEBAR_CLOSE_KEY), ]],
element_justification='r', expand_x=True, grab=True, pad=(0, 0), background_color=bc)]], expand_x=True, grab=True,
background_color=bc, pad=(0, 0), metadata=TITLEBAR_METADATA_MARKER, key=key)
# Not ready for prime time
def MenubarCustom(menu_definition, disabled_text_color=None, bar_font=None, font=None, tearoff=False, pad=None, p=None, background_color=None, text_color=None,
bar_background_color=None, bar_text_color=None, key=None, k=None):
"""
A custom Menubar that replaces the OS provided Menubar
Why?
Two reasons - 1. they look great (see custom titlebar) 2. if you have a custom titlebar, then you have to use a custom menubar if you want a menubar
:param menu_definition: The Menu definition specified using lists (docs explain the format)
:type menu_definition: List[List[Tuple[str, List[str]]]
:param disabled_text_color: color to use for text when item is disabled. Can be in #RRGGBB format or a color name "black"
:type disabled_text_color: (str)
:param bar_font: specifies the font family, size to be used for the chars in the bar itself
:type bar_font: (str or (str, int[, str]) or None)
:param font: specifies the font family, size to be used for the menu items
:type font: (str or (str, int[, str]) or None)
:param tearoff: if True, then can tear the menu off from the window ans use as a floating window. Very cool effect
:type tearoff: (bool)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param background_color: color to use for background of the menus that are displayed after making a section. Can be in #RRGGBB format or a color name "black". Defaults to the color of the bar text
:type background_color: (str)
:param text_color: color to use for the text of the many items in the displayed menus. Can be in #RRGGBB format or a color name "black". Defaults to the bar background
:type text_color: (str)
:param bar_background_color: color to use for the menubar. Can be in #RRGGBB format or a color name "black". Defaults to theme's button text color
:type bar_background_color: (str)
:param bar_text_color: color to use for the menu items text when item is disabled. Can be in #RRGGBB format or a color name "black". Defaults to theme's button background color
:type bar_text_color: (str)
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:returns: A Column element that has a series of ButtonMenu elements
:rtype: Column
"""
bar_bg = bar_background_color if bar_background_color is not None else theme_button_color()[0]
bar_text = bar_text_color if bar_text_color is not None else theme_button_color()[1]
menu_bg = background_color if background_color is not None else bar_text
menu_text = text_color if text_color is not None else bar_bg
pad = pad if pad is not None else p
row = []
for menu in menu_definition:
text = menu[0]
if MENU_SHORTCUT_CHARACTER in text:
text = text.replace(MENU_SHORTCUT_CHARACTER, '')
if text.startswith(MENU_DISABLED_CHARACTER):
disabled = True
text = text[len(MENU_DISABLED_CHARACTER):]
else:
disabled = False
button_menu = ButtonMenu(text, menu, border_width=0, button_color=(bar_text, bar_bg), key=text, pad=(0, 0), disabled=disabled, font=bar_font,
item_font=font, disabled_text_color=disabled_text_color, text_color=menu_text, background_color=menu_bg, tearoff=tearoff)
button_menu.part_of_custom_menubar = True
button_menu.custom_menubar_key = key if key is not None else k
row += [button_menu]
return Column([row], pad=pad, background_color=bar_bg, expand_x=True, key=key if key is not None else k)
# ------------------------- FOLDER BROWSE Element lazy function ------------------------- #
def FolderBrowse(button_text='Browse', target=(ThisRow, -1), initial_folder=None, tooltip=None, size=(None, None), s=(None, None),
auto_size_button=None, button_color=None, disabled=False, change_submits=False, enable_events=False,
font=None, pad=None, p=None, key=None, k=None, visible=True, metadata=None):
"""
:param button_text: text in the button (Default value = 'Browse')
:type button_text: (str)
:param target: target for the button (Default value = (ThisRow, -1))
:type target: str | (int, int)
:param initial_folder: starting path for folders and files
:type initial_folder: (str)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color:
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param change_submits: If True, pressing Enter key submits window (Default = False)
:type enable_events: (bool)
:param enable_events: Turns on the element specific events.(Default = False)
:type enable_events: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: The Button created
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_BROWSE_FOLDER, target=target,
initial_folder=initial_folder, tooltip=tooltip, size=size, s=s, auto_size_button=auto_size_button,
disabled=disabled, button_color=button_color, change_submits=change_submits,
enable_events=enable_events, font=font, pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
# ------------------------- FILE BROWSE Element lazy function ------------------------- #
def FileBrowse(button_text='Browse', target=(ThisRow, -1), file_types=FILE_TYPES_ALL_FILES, initial_folder=None,
tooltip=None, size=(None, None), s=(None, None), auto_size_button=None, button_color=None, change_submits=False,
enable_events=False, font=None, disabled=False,
pad=None, p=None, key=None, k=None, visible=True, metadata=None):
"""
:param button_text: text in the button (Default value = 'Browse')
:type button_text: (str)
:param target: key or (row,col) target for the button (Default value = (ThisRow, -1))
:type target: str | (int, int)
:param file_types: filter file types (Default value = (("ALL Files", "*.* *")))
:type file_types: Tuple[(str, str), ...]
:param initial_folder: starting path for folders and files
:type initial_folder:
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param change_submits: If True, pressing Enter key submits window (Default = False)
:type change_submits: (bool)
:param enable_events: Turns on the element specific events.(Default = False)
:type enable_events: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_BROWSE_FILE, target=target, file_types=file_types,
initial_folder=initial_folder, tooltip=tooltip, size=size, s=s, auto_size_button=auto_size_button,
change_submits=change_submits, enable_events=enable_events, disabled=disabled,
button_color=button_color, font=font, pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
# ------------------------- FILES BROWSE Element (Multiple file selection) lazy function ------------------------- #
def FilesBrowse(button_text='Browse', target=(ThisRow, -1), file_types=FILE_TYPES_ALL_FILES, disabled=False,
initial_folder=None, tooltip=None, size=(None, None), s=(None, None), auto_size_button=None, button_color=None,
change_submits=False, enable_events=False,
font=None, pad=None, p=None, key=None, k=None, visible=True, files_delimiter=BROWSE_FILES_DELIMITER, metadata=None):
"""
Allows browsing of multiple files. File list is returned as a single list with the delimiter defined using the files_delimiter parameter.
:param button_text: text in the button (Default value = 'Browse')
:type button_text: (str)
:param target: key or (row,col) target for the button (Default value = (ThisRow, -1))
:type target: str | (int, int)
:param file_types: (Default value = (("ALL Files", "*.* *")))
:type file_types: Tuple[(str, str), ...]
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param initial_folder: starting path for folders and files
:type initial_folder: (str)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param change_submits: If True, pressing Enter key submits window (Default = False)
:type change_submits: (bool)
:param enable_events: Turns on the element specific events.(Default = False)
:type enable_events: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param files_delimiter: String to place between files when multiple files are selected. Normally a ;
:type files_delimiter: str
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
button = Button(button_text=button_text, button_type=BUTTON_TYPE_BROWSE_FILES, target=target, file_types=file_types,
initial_folder=initial_folder, change_submits=change_submits, enable_events=enable_events,
tooltip=tooltip, size=size, s=s, auto_size_button=auto_size_button,
disabled=disabled, button_color=button_color, font=font, pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
button._files_delimiter = files_delimiter
return button
# ------------------------- FILE BROWSE Element lazy function ------------------------- #
def FileSaveAs(button_text='Save As...', target=(ThisRow, -1), file_types=FILE_TYPES_ALL_FILES, initial_folder=None,
default_extension='', disabled=False, tooltip=None, size=(None, None), s=(None, None), auto_size_button=None, button_color=None,
change_submits=False, enable_events=False, font=None,
pad=None, p=None, key=None, k=None, visible=True, metadata=None):
"""
:param button_text: text in the button (Default value = 'Save As...')
:type button_text: (str)
:param target: key or (row,col) target for the button (Default value = (ThisRow, -1))
:type target: str | (int, int)
:param file_types: (Default value = (("ALL Files", "*.* *")))
:type file_types: Tuple[(str, str), ...]
:param default_extension: If no extension entered by user, add this to filename (only used in saveas dialogs)
:type default_extension: (str)
:param initial_folder: starting path for folders and files
:type initial_folder: (str)
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param change_submits: If True, pressing Enter key submits window (Default = False)
:type change_submits: (bool)
:param enable_events: Turns on the element specific events.(Default = False)
:type enable_events: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_SAVEAS_FILE, target=target, file_types=file_types,
initial_folder=initial_folder, default_extension=default_extension, tooltip=tooltip, size=size, s=s, disabled=disabled,
auto_size_button=auto_size_button, button_color=button_color, change_submits=change_submits,
enable_events=enable_events, font=font, pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
# ------------------------- SAVE AS Element lazy function ------------------------- #
def SaveAs(button_text='Save As...', target=(ThisRow, -1), file_types=FILE_TYPES_ALL_FILES, initial_folder=None, default_extension='',
disabled=False, tooltip=None, size=(None, None), s=(None, None), auto_size_button=None, button_color=None,
change_submits=False, enable_events=False, font=None,
pad=None, p=None, key=None, k=None, visible=True, metadata=None):
"""
:param button_text: text in the button (Default value = 'Save As...')
:type button_text: (str)
:param target: key or (row,col) target for the button (Default value = (ThisRow, -1))
:type target: str | (int, int)
:param file_types: (Default value = (("ALL Files", "*.* *")))
:type file_types: Tuple[(str, str), ...]
:param default_extension: If no extension entered by user, add this to filename (only used in saveas dialogs)
:type default_extension: (str)
:param initial_folder: starting path for folders and files
:type initial_folder: (str)
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param change_submits: If True, pressing Enter key submits window (Default = False)
:type change_submits: (bool)
:param enable_events: Turns on the element specific events.(Default = False)
:type enable_events: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int :param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_SAVEAS_FILE, target=target, file_types=file_types,
initial_folder=initial_folder, default_extension=default_extension, tooltip=tooltip, size=size, s=s, disabled=disabled,
auto_size_button=auto_size_button, button_color=button_color, change_submits=change_submits,
enable_events=enable_events, font=font, pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
# ------------------------- SAVE BUTTON Element lazy function ------------------------- #
def Save(button_text='Save', size=(None, None), s=(None, None), auto_size_button=None, button_color=None, bind_return_key=True,
disabled=False, tooltip=None, font=None, focus=False, pad=None, p=None, key=None, k=None, visible=True, metadata=None):
"""
:param button_text: text in the button (Default value = 'Save')
:type button_text: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param bind_return_key: (Default = True) If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: (bool)
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param focus: if focus should be set to this
:type focus: idk_yetReally
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, s=s,
auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled,
bind_return_key=bind_return_key, focus=focus, pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
# ------------------------- SUBMIT BUTTON Element lazy function ------------------------- #
def Submit(button_text='Submit', size=(None, None), s=(None, None), auto_size_button=None, button_color=None, disabled=False,
bind_return_key=True, tooltip=None, font=None, focus=False, pad=None, p=None, key=None, k=None, visible=True, metadata=None):
"""
:param button_text: text in the button (Default value = 'Submit')
:type button_text: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param bind_return_key: (Default = True) If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: (bool)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param focus: if focus should be set to this
:type focus: idk_yetReally
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, s=s,
auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled,
bind_return_key=bind_return_key, focus=focus, pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
# ------------------------- OPEN BUTTON Element lazy function ------------------------- #
# ------------------------- OPEN BUTTON Element lazy function ------------------------- #
def Open(button_text='Open', size=(None, None), s=(None, None), auto_size_button=None, button_color=None, disabled=False,
bind_return_key=True, tooltip=None, font=None, focus=False, pad=None, p=None, key=None, k=None, visible=True, metadata=None):
"""
:param button_text: text in the button (Default value = 'Open')
:type button_text: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param bind_return_key: (Default = True) If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: (bool)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param focus: if focus should be set to this
:type focus: idk_yetReally
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, s=s,
auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled,
bind_return_key=bind_return_key, focus=focus, pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
# ------------------------- OK BUTTON Element lazy function ------------------------- #
def OK(button_text='OK', size=(None, None), s=(None, None), auto_size_button=None, button_color=None, disabled=False,
bind_return_key=True, tooltip=None, font=None, focus=False, pad=None, p=None, key=None, k=None, visible=True, metadata=None):
"""
:param button_text: text in the button (Default value = 'OK')
:type button_text: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param bind_return_key: (Default = True) If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: (bool)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param focus: if focus should be set to this
:type focus: idk_yetReally
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, s=s,
auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled,
bind_return_key=bind_return_key, focus=focus, pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
# ------------------------- YES BUTTON Element lazy function ------------------------- #
def Ok(button_text='Ok', size=(None, None), s=(None, None), auto_size_button=None, button_color=None, disabled=False,
bind_return_key=True, tooltip=None, font=None, focus=False, pad=None, p=None, key=None, k=None, visible=True, metadata=None):
"""
:param button_text: text in the button (Default value = 'Ok')
:type button_text: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param bind_return_key: (Default = True) If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: (bool)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param focus: if focus should be set to this
:type focus: idk_yetReally
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, s=s,
auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled,
bind_return_key=bind_return_key, focus=focus, pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
# ------------------------- CANCEL BUTTON Element lazy function ------------------------- #
def Cancel(button_text='Cancel', size=(None, None), s=(None, None), auto_size_button=None, button_color=None, disabled=False,
tooltip=None, font=None, bind_return_key=False, focus=False, pad=None, p=None, key=None, k=None, visible=True, metadata=None):
"""
:param button_text: text in the button (Default value = 'Cancel')
:type button_text: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param bind_return_key: (Default = False) If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: (bool)
:param focus: if focus should be set to this
:type focus:
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, s=s,
auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled,
bind_return_key=bind_return_key, focus=focus, pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
# ------------------------- QUIT BUTTON Element lazy function ------------------------- #
def Quit(button_text='Quit', size=(None, None), s=(None, None), auto_size_button=None, button_color=None, disabled=False, tooltip=None,
font=None, bind_return_key=False, focus=False, pad=None, p=None, key=None, k=None, visible=True, metadata=None):
"""
:param button_text: text in the button (Default value = 'Quit')
:type button_text: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param bind_return_key: (Default = False) If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: (bool)
:param focus: if focus should be set to this
:type focus: (bool)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, s=s,
auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled,
bind_return_key=bind_return_key, focus=focus, pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
# ------------------------- Exit BUTTON Element lazy function ------------------------- #
def Exit(button_text='Exit', size=(None, None), s=(None, None), auto_size_button=None, button_color=None, disabled=False, tooltip=None,
font=None, bind_return_key=False, focus=False, pad=None, p=None, key=None, k=None, visible=True, metadata=None):
"""
:param button_text: text in the button (Default value = 'Exit')
:type button_text: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param bind_return_key: (Default = False) If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: (bool)
:param focus: if focus should be set to this
:type focus:
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, s=s,
auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled,
bind_return_key=bind_return_key, focus=focus, pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
# ------------------------- YES BUTTON Element lazy function ------------------------- #
def Yes(button_text='Yes', size=(None, None), s=(None, None), auto_size_button=None, button_color=None, disabled=False, tooltip=None,
font=None, bind_return_key=True, focus=False, pad=None, p=None, key=None, k=None, visible=True, metadata=None):
"""
:param button_text: text in the button (Default value = 'Yes')
:type button_text: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param bind_return_key: (Default = True) If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: (bool)
:param focus: if focus should be set to this
:type focus:
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, s=s,
auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled,
bind_return_key=bind_return_key, focus=focus, pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
# ------------------------- NO BUTTON Element lazy function ------------------------- #
def No(button_text='No', size=(None, None), s=(None, None), auto_size_button=None, button_color=None, disabled=False, tooltip=None,
font=None, bind_return_key=False, focus=False, pad=None, p=None, key=None, k=None, visible=True, metadata=None):
"""
:param button_text: text in the button (Default value = 'No')
:type button_text: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param bind_return_key: (Default = False) If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: (bool)
:param focus: if focus should be set to this
:type focus:
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, s=s,
auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled,
bind_return_key=bind_return_key, focus=focus, pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
# ------------------------- NO BUTTON Element lazy function ------------------------- #
def Help(button_text='Help', size=(None, None), s=(None, None), auto_size_button=None, button_color=None, disabled=False, font=None,
tooltip=None, bind_return_key=False, focus=False, pad=None, p=None, key=None, k=None, visible=True, metadata=None):
"""
:param button_text: text in the button (Default value = 'Help')
:type button_text: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param bind_return_key: (Default = False) If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: (bool)
:param focus: if focus should be set to this
:type focus:
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, s=s,
auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled,
bind_return_key=bind_return_key, focus=focus, pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
# ------------------------- NO BUTTON Element lazy function ------------------------- #
def Debug(button_text='', size=(None, None), s=(None, None), auto_size_button=None, button_color=None, disabled=False, font=None,
tooltip=None, bind_return_key=False, focus=False, pad=None, p=None, key=None, k=None, visible=True, metadata=None):
"""
This Button has been changed in how it works!!
Your button has been replaced with a normal button that has the PySimpleGUI Debugger buggon logo on it.
In your event loop, you will need to check for the event of this button and then call:
show_debugger_popout_window()
:param button_text: text in the button (Default value = '')
:type button_text: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param bind_return_key: (Default = False) If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: (bool)
:param focus: if focus should be set to this
:type focus:
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
user_key = key if key is not None else k if k is not None else button_text
return Button(button_text='', button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, s=s,
auto_size_button=auto_size_button, button_color=theme_button_color(), font=font, disabled=disabled,
bind_return_key=bind_return_key, focus=focus, pad=pad, p=p, key=user_key, k=k, visible=visible, image_data=PSG_DEBUGGER_LOGO,
image_subsample=2, border_width=0, metadata=metadata)
# ------------------------- GENERIC BUTTON Element lazy function ------------------------- #
def SimpleButton(button_text, image_filename=None, image_data=None, image_size=(None, None), image_subsample=None,
border_width=None, tooltip=None, size=(None, None), s=(None, None), auto_size_button=None, button_color=None,
font=None, bind_return_key=False, disabled=False, focus=False, pad=None, p=None, key=None, k=None, metadata=None):
"""
DEPIRCATED
This Button should not be used.
:param button_text: text in the button
:type button_text: (str)
:param image_filename: image filename if there is a button image
:type image_filename: image filename if there is a button image
:param image_data: in-RAM image to be displayed on button
:type image_data: in-RAM image to be displayed on button
:param image_size: image size (O.K.)
:type image_size: (Default = (None))
:param image_subsample: amount to reduce the size of the image
:type image_subsample: amount to reduce the size of the image
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param bind_return_key: (Default = False) If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: (bool)
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param focus: if focus should be set to this
:type focus: idk_yetReally
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_CLOSES_WIN, image_filename=image_filename,
image_data=image_data, image_size=image_size, image_subsample=image_subsample,
border_width=border_width, tooltip=tooltip, disabled=disabled, size=size, s=s,
auto_size_button=auto_size_button, button_color=button_color, font=font,
bind_return_key=bind_return_key, focus=focus, pad=pad, p=p, key=key, k=k, metadata=metadata)
# ------------------------- CLOSE BUTTON Element lazy function ------------------------- #
def CloseButton(button_text, image_filename=None, image_data=None, image_size=(None, None), image_subsample=None,
border_width=None, tooltip=None, size=(None, None), s=(None, None), auto_size_button=None, button_color=None, font=None,
bind_return_key=False, disabled=False, focus=False, pad=None, p=None, key=None, k=None, metadata=None):
"""
DEPRICATED
This button should not be used. Instead explicitly close your windows by calling window.close() or by using
the close parameter in window.read
:param button_text: text in the button
:type button_text: (str)
:param image_filename: image filename if there is a button image
:type image_filename: image filename if there is a button image
:param image_data: in-RAM image to be displayed on button
:type image_data: in-RAM image to be displayed on button
:param image_size: image size (O.K.)
:type image_size: (Default = (None))
:param image_subsample: amount to reduce the size of the image
:type image_subsample: amount to reduce the size of the image
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param bind_return_key: (Default = False) If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: (bool)
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param focus: if focus should be set to this
:type focus: idk_yetReally
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_CLOSES_WIN, image_filename=image_filename,
image_data=image_data, image_size=image_size, image_subsample=image_subsample,
border_width=border_width, tooltip=tooltip, disabled=disabled, size=size, s=s,
auto_size_button=auto_size_button, button_color=button_color, font=font,
bind_return_key=bind_return_key, focus=focus, pad=pad, p=p, key=key, k=k, metadata=metadata)
CButton = CloseButton
# ------------------------- GENERIC BUTTON Element lazy function ------------------------- #
def ReadButton(button_text, image_filename=None, image_data=None, image_size=(None, None), image_subsample=None,
border_width=None, tooltip=None, size=(None, None), s=(None, None), auto_size_button=None, button_color=None, font=None,
bind_return_key=False, disabled=False, focus=False, pad=None, p=None, key=None, k=None, metadata=None):
"""
:param button_text: text in the button
:type button_text: (str)
:param image_filename: image filename if there is a button image
:type image_filename: image filename if there is a button image
:param image_data: in-RAM image to be displayed on button
:type image_data: in-RAM image to be displayed on button
:param image_size: image size (O.K.)
:type image_size: (Default = (None))
:param image_subsample: amount to reduce the size of the image
:type image_subsample: amount to reduce the size of the image
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param bind_return_key: (Default = False) If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: (bool)
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param focus: if focus should be set to this
:type focus: idk_yetReally
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param border_width: width of border around element
:type border_width: (int)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: Button created
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, image_filename=image_filename,
image_data=image_data, image_size=image_size, image_subsample=image_subsample,
border_width=border_width, tooltip=tooltip, size=size, s=s, disabled=disabled,
auto_size_button=auto_size_button, button_color=button_color, font=font,
bind_return_key=bind_return_key, focus=focus, pad=pad, p=p, key=key, k=k, metadata=metadata)
ReadFormButton = ReadButton
RButton = ReadFormButton
# ------------------------- Realtime BUTTON Element lazy function ------------------------- #
def RealtimeButton(button_text, image_filename=None, image_data=None, image_size=(None, None), image_subsample=None,
border_width=None, tooltip=None, size=(None, None), s=(None, None), auto_size_button=None, button_color=None,
font=None, disabled=False, bind_return_key=False, focus=False, pad=None, p=None, key=None, k=None, visible=True, metadata=None):
"""
:param button_text: text in the button
:type button_text: (str)
:param image_filename: image filename if there is a button image
:type image_filename: image filename if there is a button image
:param image_data: in-RAM image to be displayed on button
:type image_data: in-RAM image to be displayed on button
:param image_size: image size (O.K.)
:type image_size: (Default = (None))
:param image_subsample: amount to reduce the size of the image
:type image_subsample: amount to reduce the size of the image
:param border_width: width of border around element
:type border_width: (int)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param bind_return_key: (Default = False) If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: (bool)
:param focus: if focus should be set to this
:type focus: (bool)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: Button created
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_REALTIME, image_filename=image_filename,
image_data=image_data, image_size=image_size, image_subsample=image_subsample,
border_width=border_width, tooltip=tooltip, disabled=disabled, size=size, s=s,
auto_size_button=auto_size_button, button_color=button_color, font=font,
bind_return_key=bind_return_key, focus=focus, pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
# ------------------------- Dummy BUTTON Element lazy function ------------------------- #
def DummyButton(button_text, image_filename=None, image_data=None, image_size=(None, None), image_subsample=None,
border_width=None, tooltip=None, size=(None, None), s=(None, None), auto_size_button=None, button_color=None, font=None,
disabled=False, bind_return_key=False, focus=False, pad=None, p=None, key=None, k=None, visible=True, metadata=None):
"""
This is a special type of Button.
It will close the window but NOT send an event that the window has been closed.
It's used in conjunction with non-blocking windows to silently close them. They are used to
implement the non-blocking popup windows. They're also found in some Demo Programs, so look there for proper use.
:param button_text: text in the button
:type button_text: (str)
:param image_filename: image filename if there is a button image
:type image_filename: image filename if there is a button image
:param image_data: in-RAM image to be displayed on button
:type image_data: in-RAM image to be displayed on button
:param image_size: image size (O.K.)
:type image_size: (Default = (None))
:param image_subsample: amount to reduce the size of the image
:type image_subsample: amount to reduce the size of the image
:param border_width: width of border around element
:type border_width: (int)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param bind_return_key: (Default = False) If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: (bool)
:param focus: if focus should be set to this
:type focus: (bool)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_CLOSES_WIN_ONLY, image_filename=image_filename,
image_data=image_data, image_size=image_size, image_subsample=image_subsample,
border_width=border_width, tooltip=tooltip, size=size, s=s, auto_size_button=auto_size_button,
button_color=button_color, font=font, disabled=disabled, bind_return_key=bind_return_key, focus=focus,
pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
# ------------------------- Calendar Chooser Button lazy function ------------------------- #
def CalendarButton(button_text, target=(ThisRow, -1), close_when_date_chosen=True, default_date_m_d_y=(None, None, None),
image_filename=None, image_data=None, image_size=(None, None),
image_subsample=None, tooltip=None, border_width=None, size=(None, None), s=(None, None), auto_size_button=None,
button_color=None, disabled=False, font=None, bind_return_key=False, focus=False, pad=None, p=None, enable_events=None,
key=None, k=None, visible=True, locale=None, format='%Y-%m-%d %H:%M:%S', begin_at_sunday_plus=0, month_names=None, day_abbreviations=None,
title='Choose Date',
no_titlebar=True, location=(None, None), metadata=None):
"""
Button that will show a calendar chooser window. Fills in the target element with result
:param button_text: text in the button
:type button_text: (str)
:param target: Key or "coordinate" (see docs) of target element
:type target: (int, int) | Any
:param close_when_date_chosen: (Default = True)
:type close_when_date_chosen: bool
:param default_date_m_d_y: Beginning date to show
:type default_date_m_d_y: (int, int or None, int)
:param image_filename: image filename if there is a button image
:type image_filename: image filename if there is a button image
:param image_data: in-RAM image to be displayed on button
:type image_data: in-RAM image to be displayed on button
:param image_size: image size (O.K.)
:type image_size: (Default = (None))
:param image_subsample: amount to reduce the size of the image
:type image_subsample: amount to reduce the size of the image
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param border_width: width of border around element
:type border_width: width of border around element
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param bind_return_key: (Default = False) If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: bool
:param focus: if focus should be set to this
:type focus: bool
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param locale: defines the locale used to get day names
:type locale: str
:param format: formats result using this strftime format
:type format: str
:param month_names: optional list of month names to use (should be 12 items)
:type month_names: List[str]
:param day_abbreviations: optional list of abbreviations to display as the day of week
:type day_abbreviations: List[str]
:param title: Title shown on the date chooser window
:type title: (str)
:param no_titlebar: if True no titlebar will be shown on the date chooser window
:type no_titlebar: bool
:param location: Location on the screen (x,y) to show the calendar popup window
:type location: (int, int)
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: Anything you want to store along with this button
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
button = Button(button_text=button_text, button_type=BUTTON_TYPE_CALENDAR_CHOOSER, target=target,
image_filename=image_filename, image_data=image_data, image_size=image_size,
image_subsample=image_subsample, border_width=border_width, tooltip=tooltip, size=size, s=s,
auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled, enable_events=enable_events,
bind_return_key=bind_return_key, focus=focus, pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
button.calendar_close_when_chosen = close_when_date_chosen
button.calendar_default_date_M_D_Y = default_date_m_d_y
button.calendar_locale = locale
button.calendar_format = format
button.calendar_no_titlebar = no_titlebar
button.calendar_location = location
button.calendar_begin_at_sunday_plus = begin_at_sunday_plus
button.calendar_month_names = month_names
button.calendar_day_abbreviations = day_abbreviations
button.calendar_title = title
return button
# ------------------------- Calendar Chooser Button lazy function ------------------------- #
def ColorChooserButton(button_text, target=(ThisRow, -1), image_filename=None, image_data=None, image_size=(None, None),
image_subsample=None, tooltip=None, border_width=None, size=(None, None), s=(None, None), auto_size_button=None,
button_color=None, disabled=False, font=None, bind_return_key=False, focus=False, pad=None, p=None,
key=None, k=None, visible=True, metadata=None):
"""
:param button_text: text in the button
:type button_text: (str)
:param target: key or (row,col) target for the button. Note that -1 for column means 1 element to the left of this one. The constant ThisRow is used to indicate the current row. The Button itself is a valid target for some types of button
:type target: str | (int, int)
:type image_filename: (str)
:param image_filename: image filename if there is a button image. GIFs and PNGs only.
:type image_filename: (str)
:param image_data: Raw or Base64 representation of the image to put on button. Choose either filename or data
:type image_data: bytes | str
:param image_size: Size of the image in pixels (width, height)
:type image_size: (int, int)
:param image_subsample: amount to reduce the size of the image. Divides the size by this number. 2=1/2, 3=1/3, 4=1/4, etc
:type image_subsample: (int)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param border_width: width of border around element
:type border_width: (int)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: True if button size is determined by button text
:type auto_size_button: (bool)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param bind_return_key: If True, then the return key will cause a the Listbox to generate an event
:type bind_return_key: (bool)
:param focus: Determines if initial focus should go to this element.
:type focus: (bool)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: key for uniquely identify this element (for window.find_element)
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set initial visibility state of the Button
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
:return: returns a button
:rtype: (Button)
"""
return Button(button_text=button_text, button_type=BUTTON_TYPE_COLOR_CHOOSER, target=target,
image_filename=image_filename, image_data=image_data, image_size=image_size,
image_subsample=image_subsample, border_width=border_width, tooltip=tooltip, size=size, s=s,
auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled,
bind_return_key=bind_return_key, focus=focus, pad=pad, p=p, key=key, k=k, visible=visible, metadata=metadata)
##################################### ----- BUTTON Functions ------ ##################################################
def button_color_to_tuple(color_tuple_or_string, default=(None, None)):
"""
Convert a color tuple or color string into 2 components and returns them as a tuple
(Text Color, Button Background Color)
If None is passed in as the first parameter, then the theme's button color is
returned
:param color_tuple_or_string: Button color - tuple or a simplied color string with word "on" between color
:type color_tuple_or_string: str | (str, str)
:param default: The 2 colors to use if there is a problem. Otherwise defaults to the theme's button color
:type default: (str, str)
:return: (str | (str, str)
:rtype: str | (str, str)
"""
if default == (None, None):
color_tuple = _simplified_dual_color_to_tuple(color_tuple_or_string, default=theme_button_color())
elif color_tuple_or_string == COLOR_SYSTEM_DEFAULT:
color_tuple = (COLOR_SYSTEM_DEFAULT, COLOR_SYSTEM_DEFAULT)
else:
color_tuple = _simplified_dual_color_to_tuple(color_tuple_or_string, default=default)
return color_tuple
def _simplified_dual_color_to_tuple(color_tuple_or_string, default=(None, None)):
"""
Convert a color tuple or color string into 2 components and returns them as a tuple
(Text Color, Button Background Color)
If None is passed in as the first parameter, theme_
:param color_tuple_or_string: Button color - tuple or a simplied color string with word "on" between color
:type color_tuple_or_string: str | (str, str} | (None, None)
:param default: The 2 colors to use if there is a problem. Otherwise defaults to the theme's button color
:type default: (str, str)
:return: (str | (str, str)
:rtype: str | (str, str)
"""
if color_tuple_or_string is None or color_tuple_or_string == (None, None):
color_tuple_or_string = default
if color_tuple_or_string == COLOR_SYSTEM_DEFAULT:
return (COLOR_SYSTEM_DEFAULT, COLOR_SYSTEM_DEFAULT)
text_color = background_color = COLOR_SYSTEM_DEFAULT
try:
if isinstance(color_tuple_or_string, (tuple, list)):
if len(color_tuple_or_string) >= 2:
text_color = color_tuple_or_string[0] or default[0]
background_color = color_tuple_or_string[1] or default[1]
elif len(color_tuple_or_string) == 1:
background_color = color_tuple_or_string[0] or default[1]
elif isinstance(color_tuple_or_string, str):
color_tuple_or_string = color_tuple_or_string.lower()
split_colors = color_tuple_or_string.split(' on ')
if len(split_colors) >= 2:
text_color = split_colors[0].strip() or default[0]
background_color = split_colors[1].strip() or default[1]
elif len(split_colors) == 1:
split_colors = color_tuple_or_string.split('on')
if len(split_colors) == 1:
text_color, background_color = default[0], split_colors[0].strip()
else:
split_colors = split_colors[0].strip(), split_colors[1].strip()
text_color = split_colors[0] or default[0]
background_color = split_colors[1] or default[1]
# text_color, background_color = color_tuple_or_string, default[1]
else:
text_color, background_color = default
else:
if not SUPPRESS_ERROR_POPUPS:
_error_popup_with_traceback('** Badly formatted dual-color... not a tuple nor string **', color_tuple_or_string)
else:
print('** Badly formatted dual-color... not a tuple nor string **', color_tuple_or_string)
text_color, background_color = default
except Exception as e:
if not SUPPRESS_ERROR_POPUPS:
_error_popup_with_traceback('** Badly formatted button color **', color_tuple_or_string, e)
else:
print('** Badly formatted button color... not a tuple nor string **', color_tuple_or_string, e)
text_color, background_color = default
if isinstance(text_color, int):
text_color = "#%06X" % text_color
if isinstance(background_color, int):
background_color = "#%06X" % background_color
# print('converted button color', color_tuple_or_string, 'to', (text_color, background_color))
return (text_color, background_color)
##################################### ----- RESULTS ------ ##################################################
def AddToReturnDictionary(form, element, value):
form.ReturnValuesDictionary[element.Key] = value
# if element.Key is None:
# form.ReturnValuesDictionary[form.DictionaryKeyCounter] = value
# element.Key = form.DictionaryKeyCounter
# form.DictionaryKeyCounter += 1
# else:
# form.ReturnValuesDictionary[element.Key] = value
def AddToReturnList(form, value):
form.ReturnValuesList.append(value)
# ----------------------------------------------------------------------------#
# ------- FUNCTION InitializeResults. Sets up form results matrix --------#
def InitializeResults(form):
_BuildResults(form, True, form)
return
# ===== Radio Button RadVar encoding and decoding =====#
# ===== The value is simply the row * 1000 + col =====#
def DecodeRadioRowCol(RadValue):
container = RadValue // 100000
row = RadValue // 1000
col = RadValue % 1000
return container, row, col
def EncodeRadioRowCol(container, row, col):
RadValue = container * 100000 + row * 1000 + col
return RadValue
# ------- FUNCTION BuildResults. Form exiting so build the results to pass back ------- #
# format of return values is
# (Button Pressed, input_values)
def _BuildResults(form, initialize_only, top_level_form):
# Results for elements are:
# TEXT - Nothing
# INPUT - Read value from TK
# Button - Button Text and position as a Tuple
# Get the initialized results so we don't have to rebuild
# form.DictionaryKeyCounter = 0
form.ReturnValuesDictionary = {}
form.ReturnValuesList = []
_BuildResultsForSubform(form, initialize_only, top_level_form)
if not top_level_form.LastButtonClickedWasRealtime:
top_level_form.LastButtonClicked = None
return form.ReturnValues
def _BuildResultsForSubform(form, initialize_only, top_level_form):
event = top_level_form.LastButtonClicked
for row_num, row in enumerate(form.Rows):
for col_num, element in enumerate(row):
if element.Key is not None and WRITE_ONLY_KEY in str(element.Key):
continue
value = None
if element.Type == ELEM_TYPE_COLUMN:
element.DictionaryKeyCounter = top_level_form.DictionaryKeyCounter
element.ReturnValuesList = []
element.ReturnValuesDictionary = {}
_BuildResultsForSubform(element, initialize_only, top_level_form)
for item in element.ReturnValuesList:
AddToReturnList(top_level_form, item)
if element.UseDictionary:
top_level_form.UseDictionary = True
if element.ReturnValues[0] is not None: # if a button was clicked
event = element.ReturnValues[0]
if element.Type == ELEM_TYPE_FRAME:
element.DictionaryKeyCounter = top_level_form.DictionaryKeyCounter
element.ReturnValuesList = []
element.ReturnValuesDictionary = {}
_BuildResultsForSubform(element, initialize_only, top_level_form)
for item in element.ReturnValuesList:
AddToReturnList(top_level_form, item)
if element.UseDictionary:
top_level_form.UseDictionary = True
if element.ReturnValues[0] is not None: # if a button was clicked
event = element.ReturnValues[0]
if element.Type == ELEM_TYPE_PANE:
element.DictionaryKeyCounter = top_level_form.DictionaryKeyCounter
element.ReturnValuesList = []
element.ReturnValuesDictionary = {}
_BuildResultsForSubform(element, initialize_only, top_level_form)
for item in element.ReturnValuesList:
AddToReturnList(top_level_form, item)
if element.UseDictionary:
top_level_form.UseDictionary = True
if element.ReturnValues[0] is not None: # if a button was clicked
event = element.ReturnValues[0]
if element.Type == ELEM_TYPE_TAB_GROUP:
element.DictionaryKeyCounter = top_level_form.DictionaryKeyCounter
element.ReturnValuesList = []
element.ReturnValuesDictionary = {}
_BuildResultsForSubform(element, initialize_only, top_level_form)
for item in element.ReturnValuesList:
AddToReturnList(top_level_form, item)
if element.UseDictionary:
top_level_form.UseDictionary = True
if element.ReturnValues[0] is not None: # if a button was clicked
event = element.ReturnValues[0]
if element.Type == ELEM_TYPE_TAB:
element.DictionaryKeyCounter = top_level_form.DictionaryKeyCounter
element.ReturnValuesList = []
element.ReturnValuesDictionary = {}
_BuildResultsForSubform(element, initialize_only, top_level_form)
for item in element.ReturnValuesList:
AddToReturnList(top_level_form, item)
if element.UseDictionary:
top_level_form.UseDictionary = True
if element.ReturnValues[0] is not None: # if a button was clicked
event = element.ReturnValues[0]
if not initialize_only:
if element.Type == ELEM_TYPE_INPUT_TEXT:
try:
value = element.TKStringVar.get()
except:
value = ''
if not top_level_form.NonBlocking and not element.do_not_clear and not top_level_form.ReturnKeyboardEvents:
element.TKStringVar.set('')
elif element.Type == ELEM_TYPE_INPUT_CHECKBOX:
value = element.TKIntVar.get()
value = (value != 0)
elif element.Type == ELEM_TYPE_INPUT_RADIO:
RadVar = element.TKIntVar.get()
this_rowcol = EncodeRadioRowCol(form.ContainerElemementNumber, row_num, col_num)
# this_rowcol = element.EncodedRadioValue # could use the saved one
value = RadVar == this_rowcol
elif element.Type == ELEM_TYPE_BUTTON:
if top_level_form.LastButtonClicked == element.Key:
event = top_level_form.LastButtonClicked
if element.BType != BUTTON_TYPE_REALTIME: # Do not clear realtime buttons
top_level_form.LastButtonClicked = None
if element.BType == BUTTON_TYPE_CALENDAR_CHOOSER:
# value = None
value = element.calendar_selection
else:
try:
value = element.TKStringVar.get()
except:
value = None
elif element.Type == ELEM_TYPE_INPUT_COMBO:
element = element # type: Combo
# value = element.TKStringVar.get()
try:
if element.TKCombo.current() == -1: # if the current value was not in the original list
value = element.TKCombo.get()
else:
value = element.Values[element.TKCombo.current()] # get value from original list given index
except:
value = '*Exception occurred*'
elif element.Type == ELEM_TYPE_INPUT_OPTION_MENU:
value = element.TKStringVar.get()
elif element.Type == ELEM_TYPE_INPUT_LISTBOX:
try:
items = element.TKListbox.curselection()
value = [element.Values[int(item)] for item in items]
except Exception as e:
value = ''
elif element.Type == ELEM_TYPE_INPUT_SPIN:
try:
value = element.TKStringVar.get()
for v in element.Values:
if str(v) == value:
value = v
break
except:
value = 0
elif element.Type == ELEM_TYPE_INPUT_SLIDER:
try:
value = float(element.TKScale.get())
except:
value = 0
elif element.Type == ELEM_TYPE_INPUT_MULTILINE:
if element.WriteOnly: # if marked as "write only" when created, then don't include with the values being returned
continue
try:
value = element.TKText.get(1.0, tk.END)
if element.rstrip:
value = value.rstrip()
if not top_level_form.NonBlocking and not element.do_not_clear and not top_level_form.ReturnKeyboardEvents:
element.TKText.delete('1.0', tk.END)
except:
value = None
elif element.Type == ELEM_TYPE_TAB_GROUP:
try:
value = element.TKNotebook.tab(element.TKNotebook.index('current'))['text']
tab_key = element.FindKeyFromTabName(value)
if tab_key is not None:
value = tab_key
except:
value = None
elif element.Type == ELEM_TYPE_TABLE:
value = element.SelectedRows
elif element.Type == ELEM_TYPE_TREE:
value = element.SelectedRows
elif element.Type == ELEM_TYPE_GRAPH:
value = element.ClickPosition
elif element.Type == ELEM_TYPE_MENUBAR:
if element.MenuItemChosen is not None:
event = top_level_form.LastButtonClicked = element.MenuItemChosen
value = element.MenuItemChosen
element.MenuItemChosen = None
elif element.Type == ELEM_TYPE_BUTTONMENU:
element = element # type: ButtonMenu
value = element.MenuItemChosen
if element.part_of_custom_menubar:
if element.MenuItemChosen is not None:
value = event = element.MenuItemChosen
top_level_form.LastButtonClicked = element.MenuItemChosen
if element.custom_menubar_key is not None:
top_level_form.ReturnValuesDictionary[element.custom_menubar_key] = value
element.MenuItemChosen = None
else:
if element.custom_menubar_key not in top_level_form.ReturnValuesDictionary:
top_level_form.ReturnValuesDictionary[element.custom_menubar_key] = None
value = None
# if element.MenuItemChosen is not None:
# button_pressed_text = top_level_form.LastButtonClicked = element.MenuItemChosen
# value = element.MenuItemChosen
# element.MenuItemChosen = None
else:
value = None
# if an input type element, update the results
if element.Type not in (
ELEM_TYPE_BUTTON, ELEM_TYPE_TEXT, ELEM_TYPE_IMAGE, ELEM_TYPE_OUTPUT, ELEM_TYPE_PROGRESS_BAR, ELEM_TYPE_COLUMN, ELEM_TYPE_FRAME, ELEM_TYPE_SEPARATOR,
ELEM_TYPE_TAB):
if not (element.Type == ELEM_TYPE_BUTTONMENU and element.part_of_custom_menubar):
AddToReturnList(form, value)
AddToReturnDictionary(top_level_form, element, value)
elif (element.Type == ELEM_TYPE_BUTTON and
element.BType == BUTTON_TYPE_COLOR_CHOOSER and
element.Target == (None, None)) or \
(element.Type == ELEM_TYPE_BUTTON
and element.Key is not None and
(element.BType in (BUTTON_TYPE_SAVEAS_FILE, BUTTON_TYPE_BROWSE_FILE, BUTTON_TYPE_BROWSE_FILES,
BUTTON_TYPE_BROWSE_FOLDER, BUTTON_TYPE_CALENDAR_CHOOSER))):
AddToReturnList(form, value)
AddToReturnDictionary(top_level_form, element, value)
# if this is a column, then will fail so need to wrap with try
try:
if form.ReturnKeyboardEvents and form.LastKeyboardEvent is not None:
event = form.LastKeyboardEvent
form.LastKeyboardEvent = None
except:
pass
try:
form.ReturnValuesDictionary.pop(None, None) # clean up dictionary include None was included
except:
pass
# if no event was found
if not initialize_only and event is None and form == top_level_form:
queued_event_value = form._queued_thread_event_read()
if queued_event_value is not None:
event, value = queued_event_value
AddToReturnList(form, value)
form.ReturnValuesDictionary[event] = value
if not form.UseDictionary:
form.ReturnValues = event, form.ReturnValuesList
else:
form.ReturnValues = event, form.ReturnValuesDictionary
return form.ReturnValues
def fill_form_with_values(window, values_dict):
"""
Fills a window with values provided in a values dictionary { element_key : new_value }
:param window: The window object to fill
:type window: (Window)
:param values_dict: A dictionary with element keys as key and value is values parm for Update call
:type values_dict: (Dict[Any, Any])
:return: None
:rtype: None
"""
for element_key in values_dict:
try:
window.AllKeysDict[element_key].Update(values_dict[element_key])
except Exception as e:
print('Problem filling form. Perhaps bad key? This is a suspected bad key: {}'.format(element_key))
def _FindElementWithFocusInSubForm(form):
"""
Searches through a "sub-form" (can be a window or container) for the current element with focus
:param form: a Window, Column, Frame, or TabGroup (container elements)
:type form: container elements
:return: Element
:rtype: Element | None
"""
for row_num, row in enumerate(form.Rows):
for col_num, element in enumerate(row):
if element.Type == ELEM_TYPE_COLUMN:
matching_elem = _FindElementWithFocusInSubForm(element)
if matching_elem is not None:
return matching_elem
elif element.Type == ELEM_TYPE_FRAME:
matching_elem = _FindElementWithFocusInSubForm(element)
if matching_elem is not None:
return matching_elem
elif element.Type == ELEM_TYPE_TAB_GROUP:
matching_elem = _FindElementWithFocusInSubForm(element)
if matching_elem is not None:
return matching_elem
elif element.Type == ELEM_TYPE_TAB:
matching_elem = _FindElementWithFocusInSubForm(element)
if matching_elem is not None:
return matching_elem
elif element.Type == ELEM_TYPE_PANE:
matching_elem = _FindElementWithFocusInSubForm(element)
if matching_elem is not None:
return matching_elem
elif element.Type == ELEM_TYPE_INPUT_TEXT:
if element.TKEntry is not None:
if element.TKEntry is element.TKEntry.focus_get():
return element
elif element.Type == ELEM_TYPE_INPUT_MULTILINE:
if element.TKText is not None:
if element.TKText is element.TKText.focus_get():
return element
elif element.Type == ELEM_TYPE_BUTTON:
if element.TKButton is not None:
if element.TKButton is element.TKButton.focus_get():
return element
else: # The "Catch All" - if type isn't one of the above, try generic element.Widget
try:
if element.Widget is not None:
if element.Widget is element.Widget.focus_get():
return element
except:
return None
return None
def AddMenuItem(top_menu, sub_menu_info, element, is_sub_menu=False, skip=False, right_click_menu=False):
"""
Only to be used internally. Not user callable
:param top_menu: ???
:type top_menu: ???
:param sub_menu_info: ???
:type sub_menu_info:
:param element: ???
:type element: idk_yetReally
:param is_sub_menu: (Default = False)
:type is_sub_menu: (bool)
:param skip: (Default = False)
:type skip: (bool)
"""
return_val = None
if type(sub_menu_info) is str:
if not is_sub_menu and not skip:
pos = sub_menu_info.find(MENU_SHORTCUT_CHARACTER)
if pos != -1:
if pos < len(MENU_SHORTCUT_CHARACTER) or sub_menu_info[pos - len(MENU_SHORTCUT_CHARACTER)] != "\\":
sub_menu_info = sub_menu_info[:pos] + sub_menu_info[pos + len(MENU_SHORTCUT_CHARACTER):]
if sub_menu_info == '---':
top_menu.add('separator')
else:
try:
item_without_key = sub_menu_info[:sub_menu_info.index(MENU_KEY_SEPARATOR)]
except:
item_without_key = sub_menu_info
if item_without_key[0] == MENU_DISABLED_CHARACTER:
top_menu.add_command(label=item_without_key[len(MENU_DISABLED_CHARACTER):], underline=pos - 1,
command=lambda: element._MenuItemChosenCallback(sub_menu_info))
top_menu.entryconfig(item_without_key[len(MENU_DISABLED_CHARACTER):], state='disabled')
else:
top_menu.add_command(label=item_without_key, underline=pos,
command=lambda: element._MenuItemChosenCallback(sub_menu_info))
else:
i = 0
while i < (len(sub_menu_info)):
item = sub_menu_info[i]
if i != len(sub_menu_info) - 1:
if type(sub_menu_info[i + 1]) == list:
new_menu = tk.Menu(top_menu, tearoff=element.Tearoff)
# if a right click menu, then get styling from the top-level window
if right_click_menu:
window = element.ParentForm
if window.right_click_menu_background_color not in (COLOR_SYSTEM_DEFAULT, None):
new_menu.config(bg=window.right_click_menu_background_color)
if window.right_click_menu_text_color not in (COLOR_SYSTEM_DEFAULT, None):
new_menu.config(fg=window.right_click_menu_text_color)
if window.right_click_menu_disabled_text_color not in (COLOR_SYSTEM_DEFAULT, None):
new_menu.config(disabledforeground=window.right_click_menu_disabled_text_color)
if window.right_click_menu_font is not None:
new_menu.config(font=window.right_click_menu_font)
else:
if element.Font is not None:
new_menu.config(font=element.Font)
if element.BackgroundColor not in (COLOR_SYSTEM_DEFAULT, None):
new_menu.config(bg=element.BackgroundColor)
if element.TextColor not in (COLOR_SYSTEM_DEFAULT, None):
new_menu.config(fg=element.TextColor)
if element.DisabledTextColor not in (COLOR_SYSTEM_DEFAULT, None):
new_menu.config(disabledforeground=element.DisabledTextColor)
if element.ItemFont is not None:
new_menu.config(font=element.ItemFont)
return_val = new_menu
pos = sub_menu_info[i].find(MENU_SHORTCUT_CHARACTER)
if pos != -1:
if pos < len(MENU_SHORTCUT_CHARACTER) or sub_menu_info[i][pos - len(MENU_SHORTCUT_CHARACTER)] != "\\":
sub_menu_info[i] = sub_menu_info[i][:pos] + sub_menu_info[i][pos + len(MENU_SHORTCUT_CHARACTER):]
if sub_menu_info[i][0] == MENU_DISABLED_CHARACTER:
top_menu.add_cascade(label=sub_menu_info[i][len(MENU_DISABLED_CHARACTER):], menu=new_menu,
underline=pos, state='disabled')
else:
top_menu.add_cascade(label=sub_menu_info[i], menu=new_menu, underline=pos)
AddMenuItem(new_menu, sub_menu_info[i + 1], element, is_sub_menu=True, right_click_menu=right_click_menu)
i += 1 # skip the next one
else:
AddMenuItem(top_menu, item, element, right_click_menu=right_click_menu)
else:
AddMenuItem(top_menu, item, element, right_click_menu=right_click_menu)
i += 1
return return_val
# 888 888 d8b 888
# 888 888 Y8P 888
# 888 888 888
# 888888 888 888 888 88888b. 888888 .d88b. 888d888
# 888 888 .88P 888 888 "88b 888 d8P Y8b 888P"
# 888 888888K 888 888 888 888 88888888 888
# Y88b. 888 "88b 888 888 888 Y88b. Y8b. 888
# "Y888 888 888 888 888 888 "Y888 "Y8888 888
# My crappy tkinter code starts here. (search for "crappy" to get here quickly... that's the purpose if you hadn't caught on
"""
)
(
,
___)\
(_____)
(_______)
"""
# Chr0nic || This is probably *very* bad practice. But it works. Simple, but it works...
class VarHolder(object):
canvas_holder = None
def __init__(self):
self.canvas_holder = None
# Also, to get to the point in the code where each element's widget is created, look for element + "p lacement" (without the space)
# ======================== TK CODE STARTS HERE ========================================= #
def _fixed_map(style, style_name, option, highlight_colors=(None, None)):
# Fix for setting text colour for Tkinter 8.6.9
# From: https://core.tcl.tk/tk/info/509cafafae
# default_map = [elm for elm in style.map("Treeview", query_opt=option) if '!' not in elm[0]]
# custom_map = [elm for elm in style.map(style_name, query_opt=option) if '!' not in elm[0]]
default_map = [elm for elm in style.map("Treeview", query_opt=option) if '!' not in elm[0] and 'selected' not in elm[0]]
custom_map = [elm for elm in style.map(style_name, query_opt=option) if '!' not in elm[0] and 'selected' not in elm[0]]
if option == 'background':
custom_map.append(('selected', highlight_colors[1] if highlight_colors[1] is not None else ALTERNATE_TABLE_AND_TREE_SELECTED_ROW_COLORS[1]))
elif option == 'foreground':
custom_map.append(('selected', highlight_colors[0] if highlight_colors[0] is not None else ALTERNATE_TABLE_AND_TREE_SELECTED_ROW_COLORS[0]))
new_map = custom_map + default_map
return new_map
#
# new_map = [elm for elm in style.map(style_name, query_opt=option) if elm[:2] != ('!disabled', '!selected')]
#
# if option == 'background':
# new_map.append(('selected', highlight_colors[1] if highlight_colors[1] is not None else ALTERNATE_TABLE_AND_TREE_SELECTED_ROW_COLORS[1]))
# elif option == 'foreground':
# new_map.append(('selected', highlight_colors[0] if highlight_colors[0] is not None else ALTERNATE_TABLE_AND_TREE_SELECTED_ROW_COLORS[0]))
# return new_map
#
def _add_right_click_menu(element, toplevel_form):
if element.RightClickMenu == MENU_RIGHT_CLICK_DISABLED:
return
if element.RightClickMenu or toplevel_form.RightClickMenu:
menu = element.RightClickMenu or toplevel_form.RightClickMenu
top_menu = tk.Menu(toplevel_form.TKroot, tearoff=toplevel_form.right_click_menu_tearoff, tearoffcommand=element._tearoff_menu_callback)
if toplevel_form.right_click_menu_background_color not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(bg=toplevel_form.right_click_menu_background_color)
if toplevel_form.right_click_menu_text_color not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(fg=toplevel_form.right_click_menu_text_color)
if toplevel_form.right_click_menu_disabled_text_color not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(disabledforeground=toplevel_form.right_click_menu_disabled_text_color)
if toplevel_form.right_click_menu_font is not None:
top_menu.config(font=toplevel_form.right_click_menu_font)
if toplevel_form.right_click_menu_selected_colors[0] not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(activeforeground=toplevel_form.right_click_menu_selected_colors[0])
if toplevel_form.right_click_menu_selected_colors[1] not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(activebackground=toplevel_form.right_click_menu_selected_colors[1])
AddMenuItem(top_menu, menu[1], element, right_click_menu=True)
element.TKRightClickMenu = top_menu
if (running_mac()):
element.Widget.bind('<ButtonRelease-2>', element._RightClickMenuCallback)
else:
element.Widget.bind('<ButtonRelease-3>', element._RightClickMenuCallback)
class Stylist:
@staticmethod
def get_elements(layout):
"""Return a list of elements contained in the style"""
elements = []
element = layout[0][0]
elements.append(element)
sublayout = layout[0][1]
if 'children' in sublayout:
child_elements = Stylist.get_elements(sublayout['children'])
elements.extend(child_elements)
return elements
@staticmethod
def get_options(ttkstyle, theme=None):
style = ttk.Style()
if theme is not None:
style.theme_use(theme)
layout = style.layout(ttkstyle)
elements = Stylist.get_elements(layout)
options = []
for e in elements:
_opts = style.element_options(e)
if _opts:
options.extend(list(_opts))
return list(set(options))
@staticmethod
def create_style(base_style: str, theme=None, **kwargs):
style = ttk.Style()
if theme is not None:
style.theme_use(theme)
style_id = uuid4()
ttkstyle = '{}.{}'.format(style_id, base_style)
style.configure(ttkstyle, **kwargs)
return ttkstyle
# if __name__ == '__main__':
# root = tk.Tk()
#
# # find out what options are available for the theme and widget style
# options = Stylist.get_options('TFrame', 'default')
# print('The options for this style and theme are', options)
#
# # create a new style
# frame_style = Stylist.create_style('TFrame', 'alt', relief=tk.RAISED, borderwidth=1)
#
# # apply the new style
# ttk.Frame(style=frame_style, width=100, height=100).pack(padx=10, pady=10)
#
# root.mainloop()
# @_timeit
def PackFormIntoFrame(form, containing_frame, toplevel_form):
"""
:param form: a window class
:type form: (Window)
:param containing_frame: ???
:type containing_frame: ???
:param toplevel_form: ???
:type toplevel_form: (Window)
"""
# Old bindings
def yscroll_old(event):
try:
if event.num == 5 or event.delta < 0:
VarHolder.canvas_holder.yview_scroll(1, "unit")
elif event.num == 4 or event.delta > 0:
VarHolder.canvas_holder.yview_scroll(-1, "unit")
except:
pass
def xscroll_old(event):
try:
if event.num == 5 or event.delta < 0:
VarHolder.canvas_holder.xview_scroll(1, "unit")
elif event.num == 4 or event.delta > 0:
VarHolder.canvas_holder.xview_scroll(-1, "unit")
except:
pass
# Chr0nic
def testMouseHook2(em):
combo = em.TKCombo
combo.unbind_class("TCombobox", "<MouseWheel>")
combo.unbind_class("TCombobox", "<ButtonPress-4>")
combo.unbind_class("TCombobox", "<ButtonPress-5>")
containing_frame.unbind_all('<4>')
containing_frame.unbind_all('<5>')
containing_frame.unbind_all("<MouseWheel>")
containing_frame.unbind_all("<Shift-MouseWheel>")
# Chr0nic
def testMouseUnhook2(em):
containing_frame.bind_all('<4>', yscroll_old, add="+")
containing_frame.bind_all('<5>', yscroll_old, add="+")
containing_frame.bind_all("<MouseWheel>", yscroll_old, add="+")
containing_frame.bind_all("<Shift-MouseWheel>", xscroll_old, add="+")
# Chr0nic
def testMouseHook(em):
containing_frame.unbind_all('<4>')
containing_frame.unbind_all('<5>')
containing_frame.unbind_all("<MouseWheel>")
containing_frame.unbind_all("<Shift-MouseWheel>")
# Chr0nic
def testMouseUnhook(em):
containing_frame.bind_all('<4>', yscroll_old, add="+")
containing_frame.bind_all('<5>', yscroll_old, add="+")
containing_frame.bind_all("<MouseWheel>", yscroll_old, add="+")
containing_frame.bind_all("<Shift-MouseWheel>", xscroll_old, add="+")
def _char_width_in_pixels(font):
return tkinter.font.Font(font=font).measure('A') # single character width
def _char_height_in_pixels(font):
return tkinter.font.Font(font=font).metrics('linespace')
def _string_width_in_pixels(font, string):
return tkinter.font.Font(font=font).measure(string) # single character width
def _valid_theme(style, theme_name):
if theme_name in style.theme_names():
return True
_error_popup_with_traceback('Your Window has an invalid ttk theme specified',
'The traceback will show you the Window with the problem layout',
'** Invalid ttk theme specified {} **'.format(theme_name),
'\nValid choices include: {}'.format(style.theme_names()))
# print('** Invalid ttk theme specified {} **'.format(theme_name),
# '\nValid choices include: {}'.format(style.theme_names()))
return False
def _make_ttk_style_name(base_style, element):
Window._counter_for_ttk_widgets += 1
style_name = str(Window._counter_for_ttk_widgets) + '___' + str(element.Key) + base_style
element.ttk_style_name = style_name
return style_name
def _add_grab(element):
try:
if form.Grab is True or element.Grab is True:
# if something already about to the button, then don't do the grab stuff
if '<Button-1>' not in element.Widget.bind():
element.Widget.bind("<ButtonPress-1>", toplevel_form._StartMoveGrabAnywhere)
element.Widget.bind("<ButtonRelease-1>", toplevel_form._StopMove)
element.Widget.bind("<B1-Motion>", toplevel_form._OnMotionGrabAnywhere)
element.ParentRowFrame.bind("<ButtonPress-1>", toplevel_form._StartMoveGrabAnywhere)
element.ParentRowFrame.bind("<ButtonRelease-1>", toplevel_form._StopMove)
element.ParentRowFrame.bind("<B1-Motion>", toplevel_form._OnMotionGrabAnywhere)
if element.Type == ELEM_TYPE_COLUMN:
element.TKColFrame.canvas.bind("<ButtonPress-1>", toplevel_form._StartMoveGrabAnywhere)
element.TKColFrame.canvas.bind("<ButtonRelease-1>", toplevel_form._StopMove)
element.TKColFrame.canvas.bind("<B1-Motion>", toplevel_form._OnMotionGrabAnywhere)
except Exception as e:
pass
# print(e)
def _add_right_click_menu_and_grab(element):
if element.RightClickMenu == MENU_RIGHT_CLICK_DISABLED:
return
if element.Type == ELEM_TYPE_TAB_GROUP: # unless everything disabled, then need to always set a right click menu for tabgroups
if toplevel_form.RightClickMenu == MENU_RIGHT_CLICK_DISABLED:
return
menu = _MENU_RIGHT_CLICK_TABGROUP_DEFAULT
else:
menu = element.RightClickMenu or form.RightClickMenu or toplevel_form.RightClickMenu
if menu:
top_menu = tk.Menu(toplevel_form.TKroot, tearoff=toplevel_form.right_click_menu_tearoff, tearoffcommand=element._tearoff_menu_callback)
if toplevel_form.right_click_menu_background_color not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(bg=toplevel_form.right_click_menu_background_color)
if toplevel_form.right_click_menu_text_color not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(fg=toplevel_form.right_click_menu_text_color)
if toplevel_form.right_click_menu_disabled_text_color not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(disabledforeground=toplevel_form.right_click_menu_disabled_text_color)
if toplevel_form.right_click_menu_font is not None:
top_menu.config(font=toplevel_form.right_click_menu_font)
if toplevel_form.right_click_menu_selected_colors[0] not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(activeforeground=toplevel_form.right_click_menu_selected_colors[0])
if toplevel_form.right_click_menu_selected_colors[1] not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(activebackground=toplevel_form.right_click_menu_selected_colors[1])
AddMenuItem(top_menu, menu[1], element, right_click_menu=True)
element.TKRightClickMenu = top_menu
if toplevel_form.RightClickMenu: # if the top level has a right click menu, then setup a callback for the Window itself
if toplevel_form.TKRightClickMenu is None:
toplevel_form.TKRightClickMenu = top_menu
if (running_mac()):
toplevel_form.TKroot.bind('<ButtonRelease-2>', toplevel_form._RightClickMenuCallback)
else:
toplevel_form.TKroot.bind('<ButtonRelease-3>', toplevel_form._RightClickMenuCallback)
if (running_mac()):
element.Widget.bind('<ButtonRelease-2>', element._RightClickMenuCallback)
else:
element.Widget.bind('<ButtonRelease-3>', element._RightClickMenuCallback)
try:
if element.Type == ELEM_TYPE_COLUMN:
element.TKColFrame.canvas.bind('<ButtonRelease-3>', element._RightClickMenuCallback)
except:
pass
_add_grab(element)
def _add_expansion(element, row_should_expand, row_fill_direction):
expand = True
if element.expand_x and element.expand_y:
fill = tk.BOTH
row_fill_direction = tk.BOTH
row_should_expand = True
elif element.expand_x:
fill = tk.X
row_fill_direction = tk.X if row_fill_direction == tk.NONE else tk.BOTH if row_fill_direction == tk.Y else tk.X
elif element.expand_y:
fill = tk.Y
row_fill_direction = tk.Y if row_fill_direction == tk.NONE else tk.BOTH if row_fill_direction == tk.X else tk.Y
row_should_expand = True
else:
fill = tk.NONE
expand = False
return expand, fill, row_should_expand, row_fill_direction
tclversion_detailed = tkinter.Tcl().eval('info patchlevel')
# --------------------------------------------------------------------------- #
# **************** Use FlexForm to build the tkinter window ********** ----- #
# Building is done row by row. #
# WARNING - You can't use print in this function. If the user has rerouted #
# stdout then there will be an error saying the window isn't finalized #
# --------------------------------------------------------------------------- #
######################### LOOP THROUGH ROWS #########################
# *********** ------- Loop through ROWS ------- ***********#
for row_num, flex_row in enumerate(form.Rows):
######################### LOOP THROUGH ELEMENTS ON ROW #########################
# *********** ------- Loop through ELEMENTS ------- ***********#
# *********** Make TK Row ***********#
tk_row_frame = tk.Frame(containing_frame)
row_should_expand = False
row_fill_direction = tk.NONE
if form.ElementJustification is not None:
row_justify = form.ElementJustification
# elif toplevel_form.ElementJustification is not None:
# row_justify = toplevel_form.ElementJustification
else:
row_justify = 'l'
for col_num, element in enumerate(flex_row):
element.ParentRowFrame = tk_row_frame
element.element_frame = None # for elements that have a scrollbar too
element.ParentForm = toplevel_form # save the button's parent form object
if toplevel_form.Font and (element.Font == DEFAULT_FONT or element.Font is None):
font = toplevel_form.Font
elif element.Font is not None:
font = element.Font
else:
font = DEFAULT_FONT
# ------- Determine Auto-Size setting on a cascading basis ------- #
if element.AutoSizeText is not None: # if element overide
auto_size_text = element.AutoSizeText
elif toplevel_form.AutoSizeText is not None: # if form override
auto_size_text = toplevel_form.AutoSizeText
else:
auto_size_text = DEFAULT_AUTOSIZE_TEXT
element_type = element.Type
# Set foreground color
text_color = element.TextColor
elementpad = element.Pad if element.Pad is not None else toplevel_form.ElementPadding
element.pad_used = elementpad # store the value used back into the element
# Determine Element size
element_size = element.Size
if (element_size == (None, None) and element_type not in (
ELEM_TYPE_BUTTON, ELEM_TYPE_BUTTONMENU)): # user did not specify a size
element_size = toplevel_form.DefaultElementSize
elif (element_size == (None, None) and element_type in (ELEM_TYPE_BUTTON, ELEM_TYPE_BUTTONMENU)):
element_size = toplevel_form.DefaultButtonElementSize
else:
auto_size_text = False # if user has specified a size then it shouldn't autosize
border_depth = toplevel_form.BorderDepth if toplevel_form.BorderDepth is not None else DEFAULT_BORDER_WIDTH
try:
if element.BorderWidth is not None:
border_depth = element.BorderWidth
except:
pass
# ------------------------- COLUMN placement element ------------------------- #
if element_type == ELEM_TYPE_COLUMN:
element = element # type: Column
# ----------------------- SCROLLABLE Column ----------------------
if element.Scrollable:
element.Widget = element.TKColFrame = TkScrollableFrame(tk_row_frame, element.VerticalScrollOnly) # do not use yet! not working
PackFormIntoFrame(element, element.TKColFrame.TKFrame, toplevel_form)
element.TKColFrame.TKFrame.update()
if element.Size == (None, None): # if no size specified, use column width x column height/2
element.TKColFrame.canvas.config(width=element.TKColFrame.TKFrame.winfo_reqwidth(),
height=element.TKColFrame.TKFrame.winfo_reqheight() // 2)
else:
element.TKColFrame.canvas.config(width=element.TKColFrame.TKFrame.winfo_reqwidth(),
height=element.TKColFrame.TKFrame.winfo_reqheight() // 2)
if None not in (element.Size[0], element.Size[1]):
element.TKColFrame.canvas.config(width=element.Size[0], height=element.Size[1])
elif element.Size[1] is not None:
element.TKColFrame.canvas.config(height=element.Size[1])
elif element.Size[0] is not None:
element.TKColFrame.canvas.config(width=element.Size[0])
if not element.BackgroundColor in (None, COLOR_SYSTEM_DEFAULT):
element.TKColFrame.canvas.config(background=element.BackgroundColor)
element.TKColFrame.TKFrame.config(background=element.BackgroundColor, borderwidth=0, highlightthickness=0)
element.TKColFrame.config(background=element.BackgroundColor, borderwidth=0,
highlightthickness=0)
# ----------------------- PLAIN Column ----------------------
else:
if element.Size != (None, None):
element.Widget = element.TKColFrame = TkFixedFrame(tk_row_frame)
PackFormIntoFrame(element, element.TKColFrame.TKFrame, toplevel_form)
element.TKColFrame.TKFrame.update()
if None not in (element.Size[0], element.Size[1]):
element.TKColFrame.canvas.config(width=element.Size[0], height=element.Size[1])
elif element.Size[1] is not None:
element.TKColFrame.canvas.config(height=element.Size[1])
elif element.Size[0] is not None:
element.TKColFrame.canvas.config(width=element.Size[0])
if not element.BackgroundColor in (None, COLOR_SYSTEM_DEFAULT):
element.TKColFrame.canvas.config(background=element.BackgroundColor)
element.TKColFrame.TKFrame.config(background=element.BackgroundColor, borderwidth=0, highlightthickness=0)
else:
element.Widget = element.TKColFrame = tk.Frame(tk_row_frame)
PackFormIntoFrame(element, element.TKColFrame, toplevel_form)
if element.BackgroundColor not in (None, COLOR_SYSTEM_DEFAULT):
element.TKColFrame.config(background=element.BackgroundColor, borderwidth=0, highlightthickness=0)
if element.Justification is None:
pass
elif element.Justification.lower().startswith('l'):
row_justify = 'l'
elif element.Justification.lower().startswith('c'):
row_justify = 'c'
elif element.Justification.lower().startswith('r'):
row_justify = 'r'
# anchor=tk.NW
# side = tk.LEFT
# row_justify = element.Justification
element.Widget = element.TKColFrame
expand = True
if element.ExpandX and element.ExpandY:
fill = tk.BOTH
row_fill_direction = tk.BOTH
row_should_expand = True
elif element.ExpandX:
fill = tk.X
row_fill_direction = tk.X
elif element.ExpandY:
fill = tk.Y
row_fill_direction = tk.Y
row_should_expand = True
else:
fill = tk.NONE
expand = False
if element.VerticalAlignment is not None:
anchor = tk.CENTER # Default to center if a bad choice is made
if element.VerticalAlignment.lower().startswith('t'):
anchor = tk.N
if element.VerticalAlignment.lower().startswith('c'):
anchor = tk.CENTER
if element.VerticalAlignment.lower().startswith('b'):
anchor = tk.S
element.TKColFrame.pack(side=tk.LEFT, anchor=anchor, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
else:
element.TKColFrame.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
# element.TKColFrame.pack(side=side, padx=elementpad[0], pady=elementpad[1], expand=True, fill='both')
if element.visible is False:
element.TKColFrame.pack_forget()
# element.TKColFrame = element.TKColFrame
# if element.BackgroundColor != COLOR_SYSTEM_DEFAULT and element.BackgroundColor is not None:
# element.TKColFrame.configure(background=element.BackgroundColor,
# highlightbackground=element.BackgroundColor,
# highlightcolor=element.BackgroundColor)
_add_right_click_menu_and_grab(element)
# if element.Grab:
# element._grab_anywhere_on()
# row_should_expand = True
# ------------------------- Pane placement element ------------------------- #
if element_type == ELEM_TYPE_PANE:
bd = element.BorderDepth if element.BorderDepth is not None else border_depth
element.PanedWindow = element.Widget = tk.PanedWindow(tk_row_frame,
orient=tk.VERTICAL if element.Orientation.startswith(
'v') else tk.HORIZONTAL,
borderwidth=bd,
bd=bd,
)
if element.Relief is not None:
element.PanedWindow.configure(relief=element.Relief)
element.PanedWindow.configure(handlesize=element.HandleSize)
if element.ShowHandle:
element.PanedWindow.config(showhandle=True)
if element.Size != (None, None):
element.PanedWindow.config(width=element.Size[0], height=element.Size[1])
if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
element.PanedWindow.configure(background=element.BackgroundColor)
for pane in element.PaneList:
pane.Widget = pane.TKColFrame = tk.Frame(element.PanedWindow)
pane.ParentPanedWindow = element.PanedWindow
PackFormIntoFrame(pane, pane.TKColFrame, toplevel_form)
if pane.visible:
element.PanedWindow.add(pane.TKColFrame)
if pane.BackgroundColor != COLOR_SYSTEM_DEFAULT and pane.BackgroundColor is not None:
pane.TKColFrame.configure(background=pane.BackgroundColor,
highlightbackground=pane.BackgroundColor,
highlightcolor=pane.BackgroundColor)
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
element.PanedWindow.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
# element.PanedWindow.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=True, fill='both')
if element.visible is False:
element.PanedWindow.pack_forget()
# ------------------------- TEXT placement element ------------------------- #
elif element_type == ELEM_TYPE_TEXT:
# auto_size_text = element.AutoSizeText
element = element # type: Text
display_text = element.DisplayText # text to display
if auto_size_text is False:
width, height = element_size
else:
width, height = None, None
# lines = display_text.split('\n')
# max_line_len = max([len(l) for l in lines])
# num_lines = len(lines)
# if max_line_len > element_size[0]: # if text exceeds element size, the will have to wrap
# width = element_size[0]
# else:
# width = max_line_len
# height = num_lines
# ---===--- LABEL widget create and place --- #
element = element # type: Text
bd = element.BorderWidth if element.BorderWidth is not None else border_depth
stringvar = tk.StringVar()
element.TKStringVar = stringvar
stringvar.set(str(display_text))
if auto_size_text:
width = 0
if element.Justification is not None:
justification = element.Justification
elif toplevel_form.TextJustification is not None:
justification = toplevel_form.TextJustification
else:
justification = DEFAULT_TEXT_JUSTIFICATION
justify = tk.LEFT if justification.startswith('l') else tk.CENTER if justification.startswith('c') else tk.RIGHT
anchor = tk.NW if justification.startswith('l') else tk.N if justification.startswith('c') else tk.NE
tktext_label = element.Widget = tk.Label(tk_row_frame, textvariable=stringvar, width=width,
height=height, justify=justify, bd=bd, font=font)
# Set wrap-length for text (in PIXELS) == PAIN IN THE ASS
wraplen = tktext_label.winfo_reqwidth() # width of widget in Pixels
if auto_size_text or (not auto_size_text and height == 1): # if just 1 line high, ensure no wrap happens
wraplen = 0
tktext_label.configure(anchor=anchor, wraplen=wraplen) # set wrap to width of widget
if element.Relief is not None:
tktext_label.configure(relief=element.Relief)
if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
tktext_label.configure(background=element.BackgroundColor)
if element.TextColor != COLOR_SYSTEM_DEFAULT and element.TextColor is not None:
tktext_label.configure(fg=element.TextColor)
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
tktext_label.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
if element.visible is False:
tktext_label.pack_forget()
element.TKText = tktext_label
if element.ClickSubmits:
tktext_label.bind('<Button-1>', element._TextClickedHandler)
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element.TKText, text=element.Tooltip, timeout=DEFAULT_TOOLTIP_TIME)
_add_right_click_menu_and_grab(element)
if element.Grab:
element._grab_anywhere_on()
# ------------------------- BUTTON placement element non-ttk version ------------------------- #
elif (element_type == ELEM_TYPE_BUTTON and element.UseTtkButtons is False) or \
(element_type == ELEM_TYPE_BUTTON and element.UseTtkButtons is not True and toplevel_form.UseTtkButtons is not True):
element = element # type: Button
element.UseTtkButtons = False # indicate that ttk button was not used
stringvar = tk.StringVar()
element.TKStringVar = stringvar
element.Location = (row_num, col_num)
btext = element.ButtonText
btype = element.BType
if element.AutoSizeButton is not None:
auto_size = element.AutoSizeButton
else:
auto_size = toplevel_form.AutoSizeButtons
if auto_size is False or element.Size[0] is not None:
width, height = element_size
else:
width = 0
height = toplevel_form.DefaultButtonElementSize[1]
if element.ButtonColor != (None, None) and element.ButtonColor != DEFAULT_BUTTON_COLOR:
bc = element.ButtonColor
elif toplevel_form.ButtonColor != (None, None) and toplevel_form.ButtonColor != DEFAULT_BUTTON_COLOR:
bc = toplevel_form.ButtonColor
else:
bc = DEFAULT_BUTTON_COLOR
bd = element.BorderWidth
try:
if btype != BUTTON_TYPE_REALTIME:
tkbutton = element.Widget = tk.Button(tk_row_frame, text=btext, width=width, height=height,
command=element.ButtonCallBack, justify=tk.CENTER, bd=bd, font=font)
else:
tkbutton = element.Widget = tk.Button(tk_row_frame, text=btext, width=width, height=height,
justify=tk.CENTER, bd=bd, font=font)
tkbutton.bind('<ButtonRelease-1>', element.ButtonReleaseCallBack)
tkbutton.bind('<ButtonPress-1>', element.ButtonPressCallBack)
if bc != (None, None) and COLOR_SYSTEM_DEFAULT not in bc:
tkbutton.config(foreground=bc[0], background=bc[1])
else:
if bc[0] != COLOR_SYSTEM_DEFAULT:
tkbutton.config(foreground=bc[0])
if bc[1] != COLOR_SYSTEM_DEFAULT:
tkbutton.config(background=bc[1])
except Exception as e:
_error_popup_with_traceback('Button has a problem....',
'The traceback information will not show the line in your layout with the problem, but it does tell you which window.',
'Error {}'.format(e),
# 'Button Text: {}'.format(btext),
# 'Button key: {}'.format(element.Key),
# 'Color string: {}'.format(bc),
"Parent Window's Title: {}".format(toplevel_form.Title))
if bd == 0 and not running_mac():
tkbutton.config(relief=tk.FLAT)
element.TKButton = tkbutton # not used yet but save the TK button in case
if elementpad[0] == 0 or elementpad[1] == 0:
tkbutton.config(highlightthickness=0)
## -------------- TK Button With Image -------------- ##
if element.ImageFilename: # if button has an image on it
tkbutton.config(highlightthickness=0)
try:
photo = tk.PhotoImage(file=element.ImageFilename)
if element.ImageSubsample:
photo = photo.subsample(element.ImageSubsample)
if element.ImageSize != (None, None):
width, height = element.ImageSize
else:
width, height = photo.width(), photo.height()
except Exception as e:
_error_popup_with_traceback('Button Element error {}'.format(e), 'Image filename: {}'.format(element.ImageFilename),
'Button element key: {}'.format(element.Key),
"Parent Window's Title: {}".format(toplevel_form.Title))
tkbutton.config(image=photo, compound=tk.CENTER, width=width, height=height)
tkbutton.image = photo
if element.ImageData: # if button has an image on it
tkbutton.config(highlightthickness=0)
try:
photo = tk.PhotoImage(data=element.ImageData)
if element.ImageSubsample:
photo = photo.subsample(element.ImageSubsample)
if element.ImageSize != (None, None):
width, height = element.ImageSize
else:
width, height = photo.width(), photo.height()
tkbutton.config(image=photo, compound=tk.CENTER, width=width, height=height)
tkbutton.image = photo
except Exception as e:
_error_popup_with_traceback('Button Element error {}'.format(e),
'Problem using BASE64 Image data Image Susample',
'Buton element key: {}'.format(element.Key),
"Parent Window's Title: {}".format(toplevel_form.Title))
if width != 0:
wraplen = width * _char_width_in_pixels(font)
tkbutton.configure(wraplength=wraplen) # set wrap to width of widget
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
tkbutton.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
if element.visible is False:
tkbutton.pack_forget()
if element.BindReturnKey:
element.TKButton.bind('<Return>', element._ReturnKeyHandler)
if element.Focus is True or (toplevel_form.UseDefaultFocus and not toplevel_form.FocusSet):
toplevel_form.FocusSet = True
element.TKButton.bind('<Return>', element._ReturnKeyHandler)
element.TKButton.focus_set()
toplevel_form.TKroot.focus_force()
if element.Disabled is True:
element.TKButton['state'] = 'disabled'
if element.DisabledButtonColor != (None, None) and element.DisabledButtonColor != (COLOR_SYSTEM_DEFAULT, COLOR_SYSTEM_DEFAULT):
if element.DisabledButtonColor[0] not in (None, COLOR_SYSTEM_DEFAULT):
element.TKButton['disabledforeground'] = element.DisabledButtonColor[0]
if element.MouseOverColors[1] not in (COLOR_SYSTEM_DEFAULT, None):
tkbutton.config(activebackground=element.MouseOverColors[1])
if element.MouseOverColors[0] not in (COLOR_SYSTEM_DEFAULT, None):
tkbutton.config(activeforeground=element.MouseOverColors[0])
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element.TKButton, text=element.Tooltip,
timeout=DEFAULT_TOOLTIP_TIME)
try:
if element.HighlightColors[1] != COLOR_SYSTEM_DEFAULT:
tkbutton.config(highlightbackground=element.HighlightColors[1])
if element.HighlightColors[0] != COLOR_SYSTEM_DEFAULT:
tkbutton.config(highlightcolor=element.HighlightColors[0])
except Exception as e:
_error_popup_with_traceback('Button Element error {}'.format(e),
'Button element key: {}'.format(element.Key),
'Button text: {}'.format(btext),
'Has a bad highlight color {}'.format(element.HighlightColors),
"Parent Window's Title: {}".format(toplevel_form.Title))
# print('Button with text: ', btext, 'has a bad highlight color', element.HighlightColors)
_add_right_click_menu_and_grab(element)
# ------------------------- BUTTON placement element ttk version ------------------------- #
elif element_type == ELEM_TYPE_BUTTON:
element = element # type: Button
element.UseTtkButtons = True # indicate that ttk button was used
stringvar = tk.StringVar()
element.TKStringVar = stringvar
element.Location = (row_num, col_num)
btext = element.ButtonText
btype = element.BType
if element.AutoSizeButton is not None:
auto_size = element.AutoSizeButton
else:
auto_size = toplevel_form.AutoSizeButtons
if auto_size is False or element.Size[0] is not None:
width, height = element_size
else:
width = 0
height = toplevel_form.DefaultButtonElementSize[1]
if element.ButtonColor != (None, None) and element.ButtonColor != COLOR_SYSTEM_DEFAULT:
bc = element.ButtonColor
elif toplevel_form.ButtonColor != (None, None) and toplevel_form.ButtonColor != COLOR_SYSTEM_DEFAULT:
bc = toplevel_form.ButtonColor
else:
bc = DEFAULT_BUTTON_COLOR
bd = element.BorderWidth
if btype != BUTTON_TYPE_REALTIME:
tkbutton = element.Widget = ttk.Button(tk_row_frame, text=btext, width=width, command=element.ButtonCallBack)
else:
tkbutton = element.Widget = ttk.Button(tk_row_frame, text=btext, width=width)
tkbutton.bind('<ButtonRelease-1>', element.ButtonReleaseCallBack)
tkbutton.bind('<ButtonPress-1>', element.ButtonPressCallBack)
# Window._counter_for_ttk_widgets += 1
# style_name = str(Window._counter_for_ttk_widgets) + (element.Key) + 'custombutton.TButton'
style_name = _make_ttk_style_name('.custombutton.TButton', element)
button_style = ttk.Style()
if _valid_theme(button_style, toplevel_form.TtkTheme):
button_style.theme_use(toplevel_form.TtkTheme)
button_style.configure(style_name, font=font)
if bc != (None, None) and COLOR_SYSTEM_DEFAULT not in bc:
button_style.configure(style_name, foreground=bc[0], background=bc[1])
elif bc[0] != COLOR_SYSTEM_DEFAULT:
button_style.configure(style_name, foreground=bc[0])
elif bc[1] != COLOR_SYSTEM_DEFAULT:
button_style.configure(style_name, background=bc[1])
if bd == 0 and not running_mac():
button_style.configure(style_name, relief=tk.FLAT)
button_style.configure(style_name, borderwidth=0)
else:
button_style.configure(style_name, borderwidth=bd)
button_style.configure(style_name, justify=tk.CENTER)
if element.MouseOverColors[1] not in (COLOR_SYSTEM_DEFAULT, None):
button_style.map(style_name, background=[('active', element.MouseOverColors[1])])
if element.MouseOverColors[0] not in (COLOR_SYSTEM_DEFAULT, None):
button_style.map(style_name, foreground=[('active', element.MouseOverColors[0])])
if element.DisabledButtonColor[0] not in (COLOR_SYSTEM_DEFAULT, None):
button_style.map(style_name, foreground=[('disabled', element.DisabledButtonColor[0])])
if element.DisabledButtonColor[1] not in (COLOR_SYSTEM_DEFAULT, None):
button_style.map(style_name, background=[('disabled', element.DisabledButtonColor[1])])
if height > 1:
button_style.configure(style_name, padding=height * _char_height_in_pixels(font)) # should this be height instead?
if width != 0:
wraplen = width * _char_width_in_pixels(font) # width of widget in Pixels
button_style.configure(style_name, wraplength=wraplen) # set wrap to width of widget
## -------------- TTK Button With Image -------------- ##
if element.ImageFilename: # if button has an image on it
button_style.configure(style_name, borderwidth=0)
# tkbutton.configure(highlightthickness=0)
photo = tk.PhotoImage(file=element.ImageFilename)
if element.ImageSubsample:
photo = photo.subsample(element.ImageSubsample)
if element.ImageSize != (None, None):
width, height = element.ImageSize
else:
width, height = photo.width(), photo.height()
button_style.configure(style_name, image=photo, compound=tk.CENTER, width=width, height=height)
tkbutton.image = photo
if element.ImageData: # if button has an image on it
# tkbutton.configure(highlightthickness=0)
button_style.configure(style_name, borderwidth=0)
photo = tk.PhotoImage(data=element.ImageData)
if element.ImageSubsample:
photo = photo.subsample(element.ImageSubsample)
if element.ImageSize != (None, None):
width, height = element.ImageSize
else:
width, height = photo.width(), photo.height()
button_style.configure(style_name, image=photo, compound=tk.CENTER, width=width, height=height)
# tkbutton.configure(image=photo, compound=tk.CENTER, width=width, height=height)
tkbutton.image = photo
element.TKButton = tkbutton # not used yet but save the TK button in case
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
tkbutton.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
if element.visible is False:
tkbutton.pack_forget()
if element.BindReturnKey:
element.TKButton.bind('<Return>', element._ReturnKeyHandler)
if element.Focus is True or (toplevel_form.UseDefaultFocus and not toplevel_form.FocusSet):
toplevel_form.FocusSet = True
element.TKButton.bind('<Return>', element._ReturnKeyHandler)
element.TKButton.focus_set()
toplevel_form.TKroot.focus_force()
if element.Disabled is True:
element.TKButton['state'] = 'disabled'
tkbutton.configure(style=style_name) # IMPORTANT! Apply the style to the button!
_add_right_click_menu_and_grab(element)
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element.TKButton, text=element.Tooltip,
timeout=DEFAULT_TOOLTIP_TIME)
# ------------------------- BUTTONMENU placement element ------------------------- #
elif element_type == ELEM_TYPE_BUTTONMENU:
element = element # type: ButtonMenu
element.Location = (row_num, col_num)
btext = element.ButtonText
if element.AutoSizeButton is not None:
auto_size = element.AutoSizeButton
else:
auto_size = toplevel_form.AutoSizeButtons
if auto_size is False or element.Size[0] is not None:
width, height = element_size
else:
width = 0
height = toplevel_form.DefaultButtonElementSize[1]
if element.ButtonColor != (None, None) and element.ButtonColor != DEFAULT_BUTTON_COLOR:
bc = element.ButtonColor
elif toplevel_form.ButtonColor != (None, None) and toplevel_form.ButtonColor != DEFAULT_BUTTON_COLOR:
bc = toplevel_form.ButtonColor
else:
bc = DEFAULT_BUTTON_COLOR
bd = element.BorderWidth
if element.ItemFont is None:
element.ItemFont = font
tkbutton = element.Widget = tk.Menubutton(tk_row_frame, text=btext, width=width, height=height, justify=tk.LEFT, bd=bd, font=font)
element.TKButtonMenu = tkbutton
if bc != (None, None) and bc != COLOR_SYSTEM_DEFAULT and bc[1] != COLOR_SYSTEM_DEFAULT:
tkbutton.config(foreground=bc[0], background=bc[1])
elif bc[0] != COLOR_SYSTEM_DEFAULT:
tkbutton.config(foreground=bc[0])
if bd == 0 and not running_mac():
tkbutton.config(relief=RELIEF_FLAT)
elif bd != 0:
tkbutton.config(relief=RELIEF_RAISED)
element.TKButton = tkbutton # not used yet but save the TK button in case
wraplen = tkbutton.winfo_reqwidth() # width of widget in Pixels
if element.ImageFilename: # if button has an image on it
photo = tk.PhotoImage(file=element.ImageFilename)
if element.ImageSize != (None, None):
width, height = element.ImageSize
if element.ImageSubsample:
photo = photo.subsample(element.ImageSubsample)
else:
width, height = photo.width(), photo.height()
tkbutton.config(image=photo, compound=tk.CENTER, width=width, height=height)
tkbutton.image = photo
if element.ImageData: # if button has an image on it
photo = tk.PhotoImage(data=element.ImageData)
if element.ImageSize != (None, None):
width, height = element.ImageSize
if element.ImageSubsample:
photo = photo.subsample(element.ImageSubsample)
else:
width, height = photo.width(), photo.height()
tkbutton.config(image=photo, compound=tk.CENTER, width=width, height=height)
tkbutton.image = photo
if width != 0:
tkbutton.configure(wraplength=wraplen + 10) # set wrap to width of widget
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
tkbutton.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
menu_def = element.MenuDefinition
element.TKMenu = top_menu = tk.Menu(tkbutton, tearoff=element.Tearoff, font=element.ItemFont, tearoffcommand=element._tearoff_menu_callback)
if element.BackgroundColor not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(bg=element.BackgroundColor)
if element.TextColor not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(fg=element.TextColor)
if element.DisabledTextColor not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(disabledforeground=element.DisabledTextColor)
if element.ItemFont is not None:
top_menu.config(font=element.ItemFont)
AddMenuItem(top_menu, menu_def[1], element)
if elementpad[0] == 0 or elementpad[1] == 0:
tkbutton.config(highlightthickness=0)
tkbutton.configure(menu=top_menu)
element.TKMenu = top_menu
if element.visible is False:
tkbutton.pack_forget()
if element.Disabled == True:
element.TKButton['state'] = 'disabled'
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element.TKButton, text=element.Tooltip, timeout=DEFAULT_TOOLTIP_TIME)
# ------------------------- INPUT placement element ------------------------- #
elif element_type == ELEM_TYPE_INPUT_TEXT:
element = element # type: InputText
default_text = element.DefaultText
element.TKStringVar = tk.StringVar()
element.TKStringVar.set(default_text)
show = element.PasswordCharacter if element.PasswordCharacter else ""
bd = border_depth
if element.Justification is not None:
justification = element.Justification
else:
justification = DEFAULT_TEXT_JUSTIFICATION
justify = tk.LEFT if justification.startswith('l') else tk.CENTER if justification.startswith('c') else tk.RIGHT
# anchor = tk.NW if justification == 'left' else tk.N if justification == 'center' else tk.NE
element.TKEntry = element.Widget = tk.Entry(tk_row_frame, width=element_size[0],
textvariable=element.TKStringVar, bd=bd,
font=font, show=show, justify=justify)
if element.ChangeSubmits:
element.TKEntry.bind('<Key>', element._KeyboardHandler)
element.TKEntry.bind('<Return>', element._ReturnKeyHandler)
if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
element.TKEntry.configure(background=element.BackgroundColor)
if text_color is not None and text_color != COLOR_SYSTEM_DEFAULT:
element.TKEntry.configure(fg=text_color)
if element.disabled_readonly_background_color is not None:
element.TKEntry.config(readonlybackground=element.disabled_readonly_background_color)
if element.disabled_readonly_text_color is not None:
element.TKEntry.config(fg=element.disabled_readonly_text_color)
element.Widget.config(highlightthickness=0)
# element.pack_keywords = {'side':tk.LEFT, 'padx':elementpad[0], 'pady':elementpad[1], 'expand':False, 'fill':tk.NONE }
# element.TKEntry.pack(**element.pack_keywords)
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
element.TKEntry.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
if element.visible is False:
element.TKEntry.pack_forget()
if element.Focus is True or (toplevel_form.UseDefaultFocus and not toplevel_form.FocusSet):
toplevel_form.FocusSet = True
element.TKEntry.focus_set()
if element.Disabled:
element.TKEntry['state'] = 'readonly' if element.UseReadonlyForDisable else 'disabled'
if element.ReadOnly:
element.TKEntry['state'] = 'readonly'
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element.TKEntry, text=element.Tooltip, timeout=DEFAULT_TOOLTIP_TIME)
_add_right_click_menu_and_grab(element)
if theme_input_text_color() not in (COLOR_SYSTEM_DEFAULT, None):
element.Widget.config(insertbackground=theme_input_text_color())
# row_should_expand = True
# ------------------------- COMBO placement element ------------------------- #
elif element_type == ELEM_TYPE_INPUT_COMBO:
element = element # type: Combo
max_line_len = max([len(str(l)) for l in element.Values]) if len(element.Values) else 0
if auto_size_text is False:
width = element_size[0]
else:
width = max_line_len + 1
element.TKStringVar = tk.StringVar()
style_name = _make_ttk_style_name('.TCombobox', element)
combostyle = ttk.Style()
element.ttk_style = combostyle
if _valid_theme(combostyle, toplevel_form.TtkTheme):
combostyle.theme_use(toplevel_form.TtkTheme)
# Creates a unique name for each field element(Sure there is a better way to do this)
unique_field = _make_ttk_style_name('.TCombobox.field', element)
# Set individual widget options
try:
if element.TextColor not in (None, COLOR_SYSTEM_DEFAULT):
combostyle.configure(style_name, foreground=element.TextColor)
combostyle.configure(style_name, selectforeground=element.TextColor)
combostyle.configure(style_name, insertcolor=element.TextColor)
if element.BackgroundColor not in (None, COLOR_SYSTEM_DEFAULT):
combostyle.configure(style_name, selectbackground=element.BackgroundColor)
combostyle.map(style_name, fieldbackground=[('readonly', element.BackgroundColor)])
combostyle.configure(style_name, fieldbackground=element.BackgroundColor)
if element.button_arrow_color not in (None, COLOR_SYSTEM_DEFAULT):
combostyle.configure(style_name, arrowcolor=element.button_arrow_color)
if element.button_background_color not in (None, COLOR_SYSTEM_DEFAULT):
combostyle.configure(style_name, background=element.button_background_color)
except Exception as e:
_error_popup_with_traceback('Combo Element error {}'.format(e),
'Combo element key: {}'.format(element.Key),
'One of your colors is bad. Check the text, background, button background and button arrow colors',
"Parent Window's Title: {}".format(toplevel_form.Title))
# Strange code that is needed to set the font for the drop-down list
element._newfont = tkinter.font.Font(font=font)
tk_row_frame.option_add("*TCombobox*Listbox*Font", element._newfont)
element.TKCombo = element.Widget = ttk.Combobox(tk_row_frame, width=width, textvariable=element.TKStringVar, font=font, style=style_name)
# Chr0nic
element.TKCombo.bind("<Enter>", lambda event, em=element: testMouseHook2(em))
element.TKCombo.bind("<Leave>", lambda event, em=element: testMouseUnhook2(em))
if toplevel_form.UseDefaultFocus and not toplevel_form.FocusSet:
toplevel_form.FocusSet = True
element.TKCombo.focus_set()
if element.Size[1] != 1 and element.Size[1] is not None:
element.TKCombo.configure(height=element.Size[1])
element.TKCombo['values'] = element.Values
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
element.TKCombo.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
if element.visible is False:
element.TKCombo.pack_forget()
if element.DefaultValue is not None:
element.TKCombo.set(element.DefaultValue)
# for i, v in enumerate(element.Values):
# if v == element.DefaultValue:
# element.TKCombo.current(i)
# break
# elif element.Values:
# element.TKCombo.current(0)
if element.ChangeSubmits:
element.TKCombo.bind('<<ComboboxSelected>>', element._ComboboxSelectHandler)
if element.BindReturnKey:
element.TKCombo.bind('<Return>', element._ComboboxSelectHandler)
if element.Readonly:
element.TKCombo['state'] = 'readonly'
if element.Disabled is True: # note overrides readonly if disabled
element.TKCombo['state'] = 'disabled'
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element.TKCombo, text=element.Tooltip, timeout=DEFAULT_TOOLTIP_TIME)
_add_right_click_menu_and_grab(element)
# ------------------------- OPTIONMENU placement Element (Like ComboBox but different) element ------------------------- #
elif element_type == ELEM_TYPE_INPUT_OPTION_MENU:
max_line_len = max([len(str(l)) for l in element.Values])
if auto_size_text is False:
width = element_size[0]
else:
width = max_line_len
element.TKStringVar = tk.StringVar()
if element.DefaultValue:
element.TKStringVar.set(element.DefaultValue)
element.TKOptionMenu = element.Widget = tk.OptionMenu(tk_row_frame, element.TKStringVar, *element.Values)
element.TKOptionMenu.config(highlightthickness=0, font=font, width=width)
element.TKOptionMenu['menu'].config(font=font)
element.TKOptionMenu.config(borderwidth=border_depth)
if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
element.TKOptionMenu.configure(background=element.BackgroundColor)
element.TKOptionMenu['menu'].config(background=element.BackgroundColor)
if element.TextColor != COLOR_SYSTEM_DEFAULT and element.TextColor is not None:
element.TKOptionMenu.configure(fg=element.TextColor)
element.TKOptionMenu['menu'].config(fg=element.TextColor)
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
element.TKOptionMenu.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
if element.visible is False:
element.TKOptionMenu.pack_forget()
if element.Disabled == True:
element.TKOptionMenu['state'] = 'disabled'
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element.TKOptionMenu, text=element.Tooltip,
timeout=DEFAULT_TOOLTIP_TIME)
# ------------------------- LISTBOX placement element ------------------------- #
elif element_type == ELEM_TYPE_INPUT_LISTBOX:
element = element # type: Listbox
max_line_len = max([len(str(l)) for l in element.Values]) if len(element.Values) else 0
if auto_size_text is False:
width = element_size[0]
else:
width = max_line_len
element_frame = tk.Frame(tk_row_frame)
element.TKStringVar = tk.StringVar()
element.TKListbox = element.Widget = tk.Listbox(element_frame, height=element_size[1], width=width,
selectmode=element.SelectMode, font=font, exportselection=False)
element.Widget.config(highlightthickness=0)
for index, item in enumerate(element.Values):
element.TKListbox.insert(tk.END, item)
if element.DefaultValues is not None and item in element.DefaultValues:
element.TKListbox.selection_set(index)
if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
element.TKListbox.configure(background=element.BackgroundColor)
if element.HighlightBackgroundColor is not None and element.HighlightBackgroundColor != COLOR_SYSTEM_DEFAULT:
element.TKListbox.config(selectbackground=element.HighlightBackgroundColor)
if text_color is not None and text_color != COLOR_SYSTEM_DEFAULT:
element.TKListbox.configure(fg=text_color)
if element.HighlightTextColor is not None and element.HighlightTextColor != COLOR_SYSTEM_DEFAULT:
element.TKListbox.config(selectforeground=element.HighlightTextColor)
if element.ChangeSubmits:
element.TKListbox.bind('<<ListboxSelect>>', element._ListboxSelectHandler)
if not element.NoScrollbar:
# Vertical scrollbar
element.vsb = tk.Scrollbar(element_frame, orient="vertical", command=element.TKListbox.yview)
element.TKListbox.configure(yscrollcommand=element.vsb.set)
element.vsb.pack(side=tk.RIGHT, fill='y')
# Horizontal scrollbar
if element.HorizontalScroll:
hscrollbar = tk.Scrollbar(element_frame, orient=tk.HORIZONTAL)
hscrollbar.pack(side=tk.BOTTOM, fill='x')
hscrollbar.config(command=element.Widget.xview)
element.Widget.configure(xscrollcommand=hscrollbar.set)
element.hsb = hscrollbar
# Chr0nic
element.TKListbox.bind("<Enter>", lambda event, em=element: testMouseHook(em))
element.TKListbox.bind("<Leave>", lambda event, em=element: testMouseUnhook(em))
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
element_frame.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], fill=fill, expand=expand)
element.TKListbox.pack(side=tk.LEFT, fill=fill, expand=expand)
if element.visible is False:
element_frame.pack_forget()
element.vsb.pack_forget()
if element.BindReturnKey:
element.TKListbox.bind('<Return>', element._ListboxSelectHandler)
element.TKListbox.bind('<Double-Button-1>', element._ListboxSelectHandler)
if element.Disabled is True:
element.TKListbox['state'] = 'disabled'
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element.TKListbox, text=element.Tooltip,
timeout=DEFAULT_TOOLTIP_TIME)
element.element_frame = element_frame
_add_right_click_menu_and_grab(element)
# ------------------------- MULTILINE placement element ------------------------- #
elif element_type == ELEM_TYPE_INPUT_MULTILINE:
element = element # type: Multiline
width, height = element_size
bd = element.BorderWidth
element.element_frame = element_frame = tk.Frame(tk_row_frame)
if element.no_scrollbar:
element.TKText = element.Widget = tk.Text(element_frame, width=width, height=height, bd=bd, font=font, relief=RELIEF_SUNKEN)
else:
element.TKText = element.Widget = tk.scrolledtext.ScrolledText(element_frame, width=width, height=height, bd=bd, font=font, relief=RELIEF_SUNKEN)
# Horizontal scrollbar
if element.HorizontalScroll:
element.TKText.config(wrap='none')
hscrollbar = tk.Scrollbar(element_frame, orient=tk.HORIZONTAL)
hscrollbar.pack(side=tk.BOTTOM, fill='x')
hscrollbar.config(command=element.Widget.xview)
element.Widget.configure(xscrollcommand=hscrollbar.set)
element.hscrollbar = hscrollbar
# Chr0nic
element.TKText.bind("<Enter>", lambda event, em=element: testMouseHook(em))
element.TKText.bind("<Leave>", lambda event, em=element: testMouseUnhook(em))
else:
element.TKText.config(wrap='word')
if element.DefaultText:
element.TKText.insert(1.0, element.DefaultText) # set the default text
element.TKText.config(highlightthickness=0)
if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
element.TKText.configure(background=element.BackgroundColor)
element.TKText.tag_configure("center", justify='center')
element.TKText.tag_configure("left", justify='left')
element.TKText.tag_configure("right", justify='right')
if element.Justification.startswith('l'):
element.TKText.tag_add("left", 1.0, "end")
element.justification_tag = 'left'
elif element.Justification.startswith('r'):
element.TKText.tag_add("right", 1.0, "end")
element.justification_tag = 'right'
elif element.Justification.startswith('c'):
element.TKText.tag_add("center", 1.0, "end")
element.justification_tag = 'center'
# if DEFAULT_SCROLLBAR_COLOR not in (None, COLOR_SYSTEM_DEFAULT): # only works on Linux so not including it
# element.TKText.vbar.config(troughcolor=DEFAULT_SCROLLBAR_COLOR)
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
element.element_frame.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], fill=fill, expand=expand)
element.Widget.pack(side=tk.LEFT, fill=fill, expand=expand)
if element.visible is False:
element.element_frame.pack_forget()
else:
# Chr0nic
element.TKText.bind("<Enter>", lambda event, em=element: testMouseHook(em))
element.TKText.bind("<Leave>", lambda event, em=element: testMouseUnhook(em))
if element.ChangeSubmits:
element.TKText.bind('<Key>', element._KeyboardHandler)
if element.EnterSubmits:
element.TKText.bind('<Return>', element._ReturnKeyHandler)
if element.Focus is True or (toplevel_form.UseDefaultFocus and not toplevel_form.FocusSet):
toplevel_form.FocusSet = True
element.TKText.focus_set()
if text_color is not None and text_color != COLOR_SYSTEM_DEFAULT:
element.TKText.configure(fg=text_color)
if element.Disabled is True:
element.TKText['state'] = 'disabled'
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element.TKText, text=element.Tooltip, timeout=DEFAULT_TOOLTIP_TIME)
if element.reroute_cprint:
cprint_set_output_destination(toplevel_form, element.Key)
_add_right_click_menu_and_grab(element)
if theme_input_text_color() not in (COLOR_SYSTEM_DEFAULT, None):
element.Widget.config(insertbackground=theme_input_text_color())
# row_should_expand = True
# ------------------------- CHECKBOX pleacement element ------------------------- #
elif element_type == ELEM_TYPE_INPUT_CHECKBOX:
element = element # type: Checkbox
width = 0 if auto_size_text else element_size[0]
default_value = element.InitialState
element.TKIntVar = tk.IntVar()
element.TKIntVar.set(default_value if default_value is not None else 0)
if element.ChangeSubmits:
element.TKCheckbutton = element.Widget = tk.Checkbutton(tk_row_frame, anchor=tk.NW,
text=element.Text, width=width,
variable=element.TKIntVar, bd=border_depth,
font=font,
command=element._CheckboxHandler)
else:
element.TKCheckbutton = element.Widget = tk.Checkbutton(tk_row_frame, anchor=tk.NW,
text=element.Text, width=width,
variable=element.TKIntVar, bd=border_depth,
font=font)
if element.Disabled:
element.TKCheckbutton.configure(state='disable')
if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
element.TKCheckbutton.configure(background=element.BackgroundColor)
element.TKCheckbutton.configure(selectcolor=element.CheckboxBackgroundColor) # The background of the checkbox
element.TKCheckbutton.configure(activebackground=element.BackgroundColor)
if text_color is not None and text_color != COLOR_SYSTEM_DEFAULT:
element.TKCheckbutton.configure(fg=text_color)
element.Widget.configure(highlightthickness=1)
if element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
element.TKCheckbutton.config(highlightbackground=element.BackgroundColor)
if element.TextColor != COLOR_SYSTEM_DEFAULT:
element.TKCheckbutton.config(highlightcolor=element.TextColor)
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
element.TKCheckbutton.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
if element.visible is False:
element.TKCheckbutton.pack_forget()
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element.TKCheckbutton, text=element.Tooltip,
timeout=DEFAULT_TOOLTIP_TIME)
_add_right_click_menu_and_grab(element)
# ------------------------- PROGRESS placement element ------------------------- #
elif element_type == ELEM_TYPE_PROGRESS_BAR:
element = element # type: ProgressBar
if element.size_px != (None, None):
progress_length, progress_width = element.size_px
else:
width = element_size[0]
fnt = tkinter.font.Font()
char_width = fnt.measure('A') # single character width
progress_length = width * char_width
progress_width = element_size[1]
direction = element.Orientation
if element.BarColor != (None, None): # if element has a bar color, use it
bar_color = element.BarColor
else:
bar_color = DEFAULT_PROGRESS_BAR_COLOR
if element.Orientation.lower().startswith('h'):
base_style_name = ".Horizontal.TProgressbar"
else:
base_style_name = ".Vertical.TProgressbar"
style_name = _make_ttk_style_name(base_style_name, element)
element.TKProgressBar = TKProgressBar(tk_row_frame, element.MaxValue, progress_length, progress_width,
orientation=direction, BarColor=bar_color,
border_width=element.BorderWidth, relief=element.Relief,
ttk_theme=toplevel_form.TtkTheme, key=element.Key, style_name=style_name)
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
element.TKProgressBar.TKProgressBarForReal.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
if element.visible is False:
element.TKProgressBar.TKProgressBarForReal.pack_forget()
element.Widget = element.TKProgressBar.TKProgressBarForReal
_add_right_click_menu_and_grab(element)
# ------------------------- RADIO placement element ------------------------- #
elif element_type == ELEM_TYPE_INPUT_RADIO:
element = element # type: Radio
width = 0 if auto_size_text else element_size[0]
default_value = element.InitialState
ID = element.GroupID
# see if ID has already been placed
value = EncodeRadioRowCol(form.ContainerElemementNumber, row_num,
col_num) # value to set intvar to if this radio is selected
element.EncodedRadioValue = value
if ID in toplevel_form.RadioDict:
RadVar = toplevel_form.RadioDict[ID]
else:
RadVar = tk.IntVar()
toplevel_form.RadioDict[ID] = RadVar
element.TKIntVar = RadVar # store the RadVar in Radio object
if default_value: # if this radio is the one selected, set RadVar to match
element.TKIntVar.set(value)
if element.ChangeSubmits:
element.TKRadio = element.Widget = tk.Radiobutton(tk_row_frame, anchor=tk.NW, text=element.Text,
width=width,
variable=element.TKIntVar, value=value,
bd=border_depth, font=font,
command=element._RadioHandler)
else:
element.TKRadio = element.Widget = tk.Radiobutton(tk_row_frame, anchor=tk.NW, text=element.Text,
width=width,
variable=element.TKIntVar, value=value,
bd=border_depth, font=font)
if not element.BackgroundColor in (None, COLOR_SYSTEM_DEFAULT):
element.TKRadio.configure(background=element.BackgroundColor)
element.TKRadio.configure(selectcolor=element.CircleBackgroundColor)
element.TKRadio.configure(activebackground=element.BackgroundColor)
if text_color is not None and text_color != COLOR_SYSTEM_DEFAULT:
element.TKRadio.configure(fg=text_color)
element.Widget.configure(highlightthickness=1)
if element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
element.TKRadio.config(highlightbackground=element.BackgroundColor)
if element.TextColor != COLOR_SYSTEM_DEFAULT:
element.TKRadio.config(highlightcolor=element.TextColor)
if element.Disabled:
element.TKRadio['state'] = 'disabled'
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
element.TKRadio.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
if element.visible is False:
element.TKRadio.pack_forget()
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element.TKRadio, text=element.Tooltip, timeout=DEFAULT_TOOLTIP_TIME)
_add_right_click_menu_and_grab(element)
# ------------------------- SPIN placement element ------------------------- #
elif element_type == ELEM_TYPE_INPUT_SPIN:
element = element # type: Spin
width, height = element_size
width = 0 if auto_size_text else element_size[0]
element.TKStringVar = tk.StringVar()
element.TKSpinBox = element.Widget = tk.Spinbox(tk_row_frame, values=element.Values,
textvariable=element.TKStringVar,
width=width, bd=border_depth)
if element.DefaultValue is not None:
element.TKStringVar.set(element.DefaultValue)
element.TKSpinBox.configure(font=font) # set wrap to width of widget
if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
element.TKSpinBox.configure(background=element.BackgroundColor)
element.TKSpinBox.configure(buttonbackground=element.BackgroundColor)
element.Widget.config(highlightthickness=0)
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
element.TKSpinBox.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
if element.visible is False:
element.TKSpinBox.pack_forget()
if text_color is not None and text_color != COLOR_SYSTEM_DEFAULT:
element.TKSpinBox.configure(fg=text_color)
if element.ChangeSubmits:
element.TKSpinBox.configure(command=element._SpinboxSelectHandler)
# element.TKSpinBox.bind('<ButtonRelease-1>', element._SpinChangedHandler)
# element.TKSpinBox.bind('<Up>', element._SpinChangedHandler)
# element.TKSpinBox.bind('<Down>', element._SpinChangedHandler)
if element.Readonly:
element.TKSpinBox['state'] = 'readonly'
if element.Disabled is True: # note overrides readonly if disabled
element.TKSpinBox['state'] = 'disabled'
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element.TKSpinBox, text=element.Tooltip, timeout=DEFAULT_TOOLTIP_TIME)
if theme_input_text_color() not in (COLOR_SYSTEM_DEFAULT, None):
element.Widget.config(insertbackground=theme_input_text_color())
_add_right_click_menu_and_grab(element)
# ------------------------- OUTPUT placement element ------------------------- #
elif element_type == ELEM_TYPE_OUTPUT:
element = element # type: Output
width, height = element_size
element._TKOut = element.Widget = TKOutput(tk_row_frame, width=width, height=height, bd=border_depth,
background_color=element.BackgroundColor,
text_color=text_color, font=font,
pad=elementpad, echo_stdout_stderr=element.echo_stdout_stderr)
element._TKOut.output.configure(takefocus=0) # make it so that Output does not get focus
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
element._TKOut.pack(side=tk.LEFT, expand=expand, fill=fill)
if element.visible is False:
element._TKOut.frame.pack_forget()
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element._TKOut, text=element.Tooltip, timeout=DEFAULT_TOOLTIP_TIME)
_add_right_click_menu_and_grab(element)
# row_should_expand = True
# ------------------------- IMAGE placement element ------------------------- #
elif element_type == ELEM_TYPE_IMAGE:
element = element # type: Image
try:
if element.Filename is not None:
photo = tk.PhotoImage(file=element.Filename)
elif element.Data is not None:
photo = tk.PhotoImage(data=element.Data)
else:
photo = None
if element.ImageSubsample and photo is not None:
photo = photo.subsample(element.ImageSubsample)
# print('*ERROR laying out form.... Image Element has no image specified*')
except Exception as e:
photo = None
_error_popup_with_traceback('Your Window has an Image Element with a problem',
'The traceback will show you the Window with the problem layout',
'Look in this Window\'s layout for an Image element that has a key of {}'.format(element.Key),
'The error occuring is:', e)
if photo is not None:
if element_size == (None, None) or element_size is None or element_size == toplevel_form.DefaultElementSize:
width, height = photo.width(), photo.height()
else:
width, height = element_size
element.tktext_label = tk.Label(tk_row_frame, image=photo, width=width, height=height, bd=0)
else:
element.tktext_label = tk.Label(tk_row_frame, bd=0)
if not element.BackgroundColor in (None, COLOR_SYSTEM_DEFAULT):
element.tktext_label.config(background=element.BackgroundColor)
element.tktext_label.image = photo
# tktext_label.configure(anchor=tk.NW, image=photo)
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
element.tktext_label.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
if element.visible is False:
element.tktext_label.pack_forget()
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element.tktext_label, text=element.Tooltip,
timeout=DEFAULT_TOOLTIP_TIME)
if element.EnableEvents and element.tktext_label is not None:
element.tktext_label.bind('<ButtonPress-1>', element._ClickHandler)
element.Widget = element.tktext_label
_add_right_click_menu_and_grab(element)
# ------------------------- Canvas placement element ------------------------- #
elif element_type == ELEM_TYPE_CANVAS:
element = element # type: Canvas
width, height = element_size
if element._TKCanvas is None:
element._TKCanvas = tk.Canvas(tk_row_frame, width=width, height=height, bd=border_depth)
else:
element._TKCanvas.master = tk_row_frame
if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
element._TKCanvas.configure(background=element.BackgroundColor, highlightthickness=0)
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
element._TKCanvas.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
if element.visible is False:
element._TKCanvas.pack_forget()
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element._TKCanvas, text=element.Tooltip,
timeout=DEFAULT_TOOLTIP_TIME)
element.Widget = element._TKCanvas
_add_right_click_menu_and_grab(element)
# ------------------------- Graph placement element ------------------------- #
elif element_type == ELEM_TYPE_GRAPH:
element = element # type: Graph
width, height = element_size
# I don't know why TWO canvases were being defined, on inside the other. Was it so entire canvas can move?
# if element._TKCanvas is None:
# element._TKCanvas = tk.Canvas(tk_row_frame, width=width, height=height, bd=border_depth)
# else:
# element._TKCanvas.master = tk_row_frame
element._TKCanvas2 = element.Widget = tk.Canvas(tk_row_frame, width=width, height=height,
bd=border_depth)
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
element._TKCanvas2.pack(side=tk.LEFT, expand=expand, fill=fill)
element._TKCanvas2.addtag_all('mytag')
if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
element._TKCanvas2.configure(background=element.BackgroundColor, highlightthickness=0)
# element._TKCanvas.configure(background=element.BackgroundColor, highlightthickness=0)
element._TKCanvas2.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
if element.visible is False:
# element._TKCanvas.pack_forget()
element._TKCanvas2.pack_forget()
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element._TKCanvas2, text=element.Tooltip,
timeout=DEFAULT_TOOLTIP_TIME)
if element.ChangeSubmits:
element._TKCanvas2.bind('<ButtonRelease-1>', element.ButtonReleaseCallBack)
element._TKCanvas2.bind('<ButtonPress-1>', element.ButtonPressCallBack)
if element.DragSubmits:
element._TKCanvas2.bind('<Motion>', element.MotionCallBack)
_add_right_click_menu_and_grab(element)
# ------------------------- MENU placement element ------------------------- #
elif element_type == ELEM_TYPE_MENUBAR:
element = element # type: MenuBar
menu_def = element.MenuDefinition
element.TKMenu = element.Widget = tk.Menu(toplevel_form.TKroot, tearoff=element.Tearoff,
tearoffcommand=element._tearoff_menu_callback) # create the menubar
menubar = element.TKMenu
if font is not None: # if a font is used, make sure it's saved in the element
element.Font = font
for menu_entry in menu_def:
baritem = tk.Menu(menubar, tearoff=element.Tearoff, tearoffcommand=element._tearoff_menu_callback)
if element.BackgroundColor not in (COLOR_SYSTEM_DEFAULT, None):
baritem.config(bg=element.BackgroundColor)
if element.TextColor not in (COLOR_SYSTEM_DEFAULT, None):
baritem.config(fg=element.TextColor)
if element.DisabledTextColor not in (COLOR_SYSTEM_DEFAULT, None):
baritem.config(disabledforeground=element.DisabledTextColor)
if font is not None:
baritem.config(font=font)
pos = menu_entry[0].find(MENU_SHORTCUT_CHARACTER)
# print(pos)
if pos != -1:
if pos == 0 or menu_entry[0][pos - len(MENU_SHORTCUT_CHARACTER)] != "\\":
menu_entry[0] = menu_entry[0][:pos] + menu_entry[0][pos + 1:]
if menu_entry[0][0] == MENU_DISABLED_CHARACTER:
menubar.add_cascade(label=menu_entry[0][len(MENU_DISABLED_CHARACTER):], menu=baritem,
underline=pos - 1)
menubar.entryconfig(menu_entry[0][len(MENU_DISABLED_CHARACTER):], state='disabled')
else:
menubar.add_cascade(label=menu_entry[0], menu=baritem, underline=pos)
if len(menu_entry) > 1:
AddMenuItem(baritem, menu_entry[1], element)
toplevel_form.TKroot.configure(menu=element.TKMenu)
# ------------------------- Frame placement element ------------------------- #
elif element_type == ELEM_TYPE_FRAME:
element = element # type: Frame
labeled_frame = element.Widget = tk.LabelFrame(tk_row_frame, text=element.Title, relief=element.Relief)
element.TKFrame = labeled_frame
PackFormIntoFrame(element, labeled_frame, toplevel_form)
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
if element.VerticalAlignment is not None:
anchor = tk.CENTER # Default to center if a bad choice is made
if element.VerticalAlignment.lower().startswith('t'):
anchor = tk.N
if element.VerticalAlignment.lower().startswith('c'):
anchor = tk.CENTER
if element.VerticalAlignment.lower().startswith('b'):
anchor = tk.S
labeled_frame.pack(side=tk.LEFT, anchor=anchor, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
else:
labeled_frame.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
if element.Size != (None, None):
labeled_frame.config(width=element.Size[0], height=element.Size[1])
labeled_frame.pack_propagate(0)
if not element.visible:
labeled_frame.pack_forget()
if element.BackgroundColor != COLOR_SYSTEM_DEFAULT and element.BackgroundColor is not None:
labeled_frame.configure(background=element.BackgroundColor,
highlightbackground=element.BackgroundColor,
highlightcolor=element.BackgroundColor)
if element.TextColor != COLOR_SYSTEM_DEFAULT and element.TextColor is not None:
labeled_frame.configure(foreground=element.TextColor)
if font is not None:
labeled_frame.configure(font=font)
if element.TitleLocation is not None:
labeled_frame.configure(labelanchor=element.TitleLocation)
if element.BorderWidth is not None:
labeled_frame.configure(borderwidth=element.BorderWidth)
if element.Tooltip is not None:
element.TooltipObject = ToolTip(labeled_frame, text=element.Tooltip, timeout=DEFAULT_TOOLTIP_TIME)
_add_right_click_menu_and_grab(element)
# row_should_expand=True
# ------------------------- Tab placement element ------------------------- #
elif element_type == ELEM_TYPE_TAB:
element = element # type: Tab
element.TKFrame = element.Widget = tk.Frame(form.TKNotebook)
PackFormIntoFrame(element, element.TKFrame, toplevel_form)
state = 'normal'
if element.Disabled:
state = 'disabled'
if element.visible is False:
state = 'hidden'
# this code will add an image to the tab. Use it when adding the image on a tab enhancement
try:
if element.Filename is not None:
photo = tk.PhotoImage(file=element.Filename)
elif element.Data is not None:
photo = tk.PhotoImage(data=element.Data)
else:
photo = None
if element.ImageSubsample and photo is not None:
photo = photo.subsample(element.ImageSubsample)
# print('*ERROR laying out form.... Image Element has no image specified*')
except Exception as e:
photo = None
_error_popup_with_traceback('Your Window has an Tab Element with an IMAGE problem',
'The traceback will show you the Window with the problem layout',
'Look in this Window\'s layout for an Image element that has a key of {}'.format(element.Key),
'The error occuring is:', e)
element.photo = photo
if photo is not None:
if element_size == (None, None) or element_size is None or element_size == toplevel_form.DefaultElementSize:
width, height = photo.width(), photo.height()
else:
width, height = element_size
element.tktext_label = tk.Label(tk_row_frame, image=photo, width=width, height=height, bd=0)
else:
element.tktext_label = tk.Label(tk_row_frame, bd=0)
if photo is not None:
form.TKNotebook.add(element.TKFrame, text=element.Title, compound=tk.LEFT, state=state,image=photo)
# element.photo_image = tk.PhotoImage(data=DEFAULT_BASE64_ICON)
# form.TKNotebook.add(element.TKFrame, text=element.Title, compound=tk.LEFT, state=state,image = element.photo_image)
form.TKNotebook.add(element.TKFrame, text=element.Title, state=state)
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
form.TKNotebook.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], fill=fill, expand=expand)
element.ParentNotebook = form.TKNotebook
element.TabID = form.TabCount
form.TabCount += 1
if element.BackgroundColor != COLOR_SYSTEM_DEFAULT and element.BackgroundColor is not None:
element.TKFrame.configure(background=element.BackgroundColor,
highlightbackground=element.BackgroundColor,
highlightcolor=element.BackgroundColor)
# if element.BorderWidth is not None:
# element.TKFrame.configure(borderwidth=element.BorderWidth)
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element.TKFrame, text=element.Tooltip, timeout=DEFAULT_TOOLTIP_TIME)
_add_right_click_menu_and_grab(element)
# row_should_expand = True
# ------------------------- TabGroup placement element ------------------------- #
elif element_type == ELEM_TYPE_TAB_GROUP:
element = element # type: TabGroup
# custom_style = str(element.Key) + 'customtab.TNotebook'
custom_style = _make_ttk_style_name('.customtab.TNotebook', element)
style = ttk.Style()
style.theme_use(toplevel_form.TtkTheme)
if element.TabLocation is not None:
position_dict = {'left': 'w', 'right': 'e', 'top': 'n', 'bottom': 's', 'lefttop': 'wn',
'leftbottom': 'ws', 'righttop': 'en', 'rightbottom': 'es', 'bottomleft': 'sw',
'bottomright': 'se', 'topleft': 'nw', 'topright': 'ne'}
try:
tab_position = position_dict[element.TabLocation]
except:
tab_position = position_dict['top']
style.configure(custom_style, tabposition=tab_position)
if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
style.configure(custom_style, background=element.BackgroundColor)
# FINALLY the proper styling to get tab colors!
if element.SelectedTitleColor is not None and element.SelectedTitleColor != COLOR_SYSTEM_DEFAULT:
style.map(custom_style + '.Tab', foreground=[("selected", element.SelectedTitleColor)])
if element.SelectedBackgroundColor is not None and element.SelectedBackgroundColor != COLOR_SYSTEM_DEFAULT:
style.map(custom_style + '.Tab', background=[("selected", element.SelectedBackgroundColor)])
if element.TabBackgroundColor is not None and element.TabBackgroundColor != COLOR_SYSTEM_DEFAULT:
style.configure(custom_style + '.Tab', background=element.TabBackgroundColor)
if element.TextColor is not None and element.TextColor != COLOR_SYSTEM_DEFAULT:
style.configure(custom_style + '.Tab', foreground=element.TextColor)
if element.BorderWidth is not None:
style.configure(custom_style, borderwidth=element.BorderWidth)
if element.TabBorderWidth is not None:
style.configure(custom_style + '.Tab', borderwidth=element.TabBorderWidth) # if ever want to get rid of border around the TABS themselves
if element.FocusColor is not None:
style.configure(custom_style + '.Tab', focuscolor=element.FocusColor)
style.configure(custom_style + '.Tab', font=font)
element.Style = style
element.StyleName = custom_style
element.TKNotebook = element.Widget = ttk.Notebook(tk_row_frame, style=custom_style)
PackFormIntoFrame(element, toplevel_form.TKroot, toplevel_form)
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
element.TKNotebook.pack(anchor=tk.SW, side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], fill=fill, expand=expand)
if element.ChangeSubmits:
element.TKNotebook.bind('<<NotebookTabChanged>>', element._TabGroupSelectHandler)
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element.TKNotebook, text=element.Tooltip, timeout=DEFAULT_TOOLTIP_TIME)
if element.Size != (None, None):
element.TKNotebook.configure(width=element.Size[0], height=element.Size[1])
_add_right_click_menu_and_grab(element)
# row_should_expand = True
# ------------------- SLIDER placement element ------------------------- #
elif element_type == ELEM_TYPE_INPUT_SLIDER:
element = element # type: Slider
slider_length = element_size[0] * _char_width_in_pixels(font)
slider_width = element_size[1]
element.TKIntVar = tk.IntVar()
element.TKIntVar.set(element.DefaultValue)
if element.Orientation.startswith('v'):
range_from = element.Range[1]
range_to = element.Range[0]
slider_length += DEFAULT_MARGINS[1] * (element_size[0] * 2) # add in the padding
else:
range_from = element.Range[0]
range_to = element.Range[1]
if element.ChangeSubmits:
tkscale = element.Widget = tk.Scale(tk_row_frame, orient=element.Orientation,
variable=element.TKIntVar,
from_=range_from, to_=range_to, resolution=element.Resolution,
length=slider_length, width=slider_width,
bd=element.BorderWidth,
relief=element.Relief, font=font,
tickinterval=element.TickInterval,
command=element._SliderChangedHandler)
else:
tkscale = element.Widget = tk.Scale(tk_row_frame, orient=element.Orientation,
variable=element.TKIntVar,
from_=range_from, to_=range_to, resolution=element.Resolution,
length=slider_length, width=slider_width,
bd=element.BorderWidth,
relief=element.Relief, font=font,
tickinterval=element.TickInterval)
tkscale.config(highlightthickness=0)
if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
tkscale.configure(background=element.BackgroundColor)
if element.TroughColor != COLOR_SYSTEM_DEFAULT:
tkscale.config(troughcolor=element.TroughColor)
if element.DisableNumericDisplay:
tkscale.config(showvalue=0)
if text_color is not None and text_color != COLOR_SYSTEM_DEFAULT:
tkscale.configure(fg=text_color)
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
tkscale.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
if element.visible is False:
tkscale.pack_forget()
element.TKScale = tkscale
if element.Disabled == True:
element.TKScale['state'] = 'disabled'
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element.TKScale, text=element.Tooltip, timeout=DEFAULT_TOOLTIP_TIME)
_add_right_click_menu_and_grab(element)
# ------------------------- TABLE placement element ------------------------- #
elif element_type == ELEM_TYPE_TABLE:
element = element # type: Table
element.element_frame = frame = tk.Frame(tk_row_frame)
element.table_frame = frame
height = element.NumRows
if element.Justification.startswith('l'):
anchor = tk.W
elif element.Justification.startswith('r'):
anchor = tk.E
else:
anchor = tk.CENTER
column_widths = {}
# create column width list
for row in element.Values:
for i, col in enumerate(row):
col_width = min(len(str(col)), element.MaxColumnWidth)
try:
if col_width > column_widths[i]:
column_widths[i] = col_width
except:
column_widths[i] = col_width
if element.ColumnsToDisplay is None:
displaycolumns = element.ColumnHeadings if element.ColumnHeadings is not None else element.Values[0]
else:
displaycolumns = []
for i, should_display in enumerate(element.ColumnsToDisplay):
if should_display:
if element.ColumnHeadings is not None:
displaycolumns.append(element.ColumnHeadings[i])
else:
displaycolumns.append(str(i))
column_headings = element.ColumnHeadings if element.ColumnHeadings is not None else displaycolumns
if element.DisplayRowNumbers: # if display row number, tack on the numbers to front of columns
displaycolumns = [element.RowHeaderText, ] + displaycolumns
if column_headings is not None:
column_headings = [element.RowHeaderText, ] + element.ColumnHeadings
else:
column_headings = [element.RowHeaderText, ] + displaycolumns
element.TKTreeview = element.Widget = ttk.Treeview(frame, columns=column_headings,
displaycolumns=displaycolumns, show='headings',
height=height,
selectmode=element.SelectMode, )
treeview = element.TKTreeview
if element.DisplayRowNumbers:
treeview.heading(element.RowHeaderText, text=element.RowHeaderText) # make a dummy heading
row_number_header_width =_string_width_in_pixels(element.HeaderFont, element.RowHeaderText) + 10
row_number_width = _string_width_in_pixels(font, str(len(element.Values))) + 10
row_number_width = max(row_number_header_width, row_number_width)
treeview.column(element.RowHeaderText, width=row_number_width, minwidth=10, anchor=anchor, stretch=0)
headings = element.ColumnHeadings if element.ColumnHeadings is not None else element.Values[0]
for i, heading in enumerate(headings):
treeview.heading(heading, text=heading)
if element.AutoSizeColumns:
col_width = column_widths.get(i, len(heading)) # in case more headings than there are columns of data
width = max(col_width * _char_width_in_pixels(font), len(heading)*_char_width_in_pixels(element.HeaderFont))
else:
try:
width = element.ColumnWidths[i] * _char_width_in_pixels(font)
except:
width = element.DefaultColumnWidth * _char_width_in_pixels(font)
treeview.column(heading, width=width, minwidth=10, anchor=anchor, stretch=element.expand_x)
# Insert values into the tree
for i, value in enumerate(element.Values):
if element.DisplayRowNumbers:
value = [i + element.StartingRowNumber] + value
id = treeview.insert('', 'end', text=value, iid=i + 1, values=value, tag=i)
element.tree_ids.append(id)
if element.AlternatingRowColor is not None: # alternating colors
for row in range(0, len(element.Values), 2):
treeview.tag_configure(row, background=element.AlternatingRowColor)
if element.RowColors is not None: # individual row colors
for row_def in element.RowColors:
if len(row_def) == 2: # only background is specified
treeview.tag_configure(row_def[0], background=row_def[1])
else:
treeview.tag_configure(row_def[0], background=row_def[2], foreground=row_def[1])
# ------ Do Styling of Colors -----
# style_name = str(element.Key) + 'customtable.Treeview'
style_name = _make_ttk_style_name( '.customtable.Treeview', element)
table_style = ttk.Style()
element.ttk_style = table_style
table_style.theme_use(toplevel_form.TtkTheme)
if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
table_style.configure(style_name, background=element.BackgroundColor, fieldbackground=element.BackgroundColor, )
if element.SelectedRowColors[1] is not None:
table_style.map(style_name, background=_fixed_map(table_style, style_name, 'background', element.SelectedRowColors))
if element.TextColor is not None and element.TextColor != COLOR_SYSTEM_DEFAULT:
table_style.configure(style_name, foreground=element.TextColor)
if element.SelectedRowColors[0] is not None:
table_style.map(style_name, foreground=_fixed_map(table_style, style_name, 'foreground', element.SelectedRowColors))
if element.RowHeight is not None:
table_style.configure(style_name, rowheight=element.RowHeight)
else:
table_style.configure(style_name, rowheight=_char_height_in_pixels(font))
if element.HeaderTextColor is not None and element.HeaderTextColor != COLOR_SYSTEM_DEFAULT:
table_style.configure(style_name + '.Heading', foreground=element.HeaderTextColor)
if element.HeaderBackgroundColor is not None and element.HeaderBackgroundColor != COLOR_SYSTEM_DEFAULT:
table_style.configure(style_name + '.Heading', background=element.HeaderBackgroundColor)
if element.HeaderFont is not None:
table_style.configure(style_name + '.Heading', font=element.HeaderFont)
else:
table_style.configure(style_name + '.Heading', font=font)
table_style.configure(style_name, font=font)
treeview.configure(style=style_name)
# scrollable_frame.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=True, fill='both')
if element.enable_click_events is True:
treeview.bind('<Button-1>', element._table_clicked)
if element.right_click_selects:
if running_mac():
treeview.bind('<Button-2>', element._table_clicked)
else:
treeview.bind('<Button-3>', element._table_clicked)
treeview.bind("<<TreeviewSelect>>", element._treeview_selected)
if element.BindReturnKey:
treeview.bind('<Return>', element._treeview_double_click)
treeview.bind('<Double-Button-1>', element._treeview_double_click)
if not element.HideVerticalScroll:
scrollbar = tk.Scrollbar(frame)
scrollbar.pack(side=tk.RIGHT, fill='y')
scrollbar.config(command=treeview.yview)
treeview.configure(yscrollcommand=scrollbar.set)
if not element.VerticalScrollOnly:
hscrollbar = tk.Scrollbar(frame, orient=tk.HORIZONTAL)
hscrollbar.pack(side=tk.BOTTOM, fill='x')
hscrollbar.config(command=treeview.xview)
treeview.configure(xscrollcommand=hscrollbar.set)
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
element.TKTreeview.pack(side=tk.LEFT, padx=0, pady=0, expand=expand, fill=fill)
if element.visible is False:
element.TKTreeview.pack_forget()
frame.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element.TKTreeview, text=element.Tooltip,
timeout=DEFAULT_TOOLTIP_TIME)
_add_right_click_menu_and_grab(element)
if tclversion_detailed == '8.6.9' and ENABLE_TREEVIEW_869_PATCH:
# print('*** tk version 8.6.9 detected.... patching ttk treeview code ***')
table_style.map(style_name,
foreground=_fixed_map(table_style, style_name, 'foreground', element.SelectedRowColors),
background=_fixed_map(table_style, style_name, 'background', element.SelectedRowColors))
# ------------------------- Tree placement element ------------------------- #
elif element_type == ELEM_TYPE_TREE:
element = element # type: Tree
element.element_frame = element_frame = tk.Frame(tk_row_frame)
height = element.NumRows
if element.Justification.startswith('l'): # justification
anchor = tk.W
elif element.Justification.startswith('r'):
anchor = tk.E
else:
anchor = tk.CENTER
if element.ColumnsToDisplay is None: # Which cols to display
displaycolumns = element.ColumnHeadings
else:
displaycolumns = []
for i, should_display in enumerate(element.ColumnsToDisplay):
if should_display:
displaycolumns.append(element.ColumnHeadings[i])
column_headings = element.ColumnHeadings
# ------------- GET THE TREEVIEW WIDGET -------------
element.TKTreeview = element.Widget = ttk.Treeview(element_frame, columns=column_headings,
displaycolumns=displaycolumns, show='tree headings',
height=height,
selectmode=element.SelectMode)
treeview = element.TKTreeview
max_widths = {}
for key, node in element.TreeData.tree_dict.items():
for i, value in enumerate(node.values):
max_width = max_widths.get(i, 0)
if len(str(value)) > max_width:
max_widths[i] = len(str(value))
for i, heading in enumerate(element.ColumnHeadings): # Configure cols + headings
treeview.heading(heading, text=heading)
if element.AutoSizeColumns:
max_width = max_widths.get(i, 0)
max_width = max(max_width, len(heading))
width = min(element.MaxColumnWidth, max_width+1)
else:
try:
width = element.ColumnWidths[i]
except:
width = element.DefaultColumnWidth
treeview.column(heading, width=width * _char_width_in_pixels(font) + 10, anchor=anchor)
def add_treeview_data(node):
"""
:param node:
:type node:
"""
if node.key != '':
if node.icon:
if node.icon not in element.image_dict:
if type(node.icon) is bytes:
photo = tk.PhotoImage(data=node.icon)
else:
photo = tk.PhotoImage(file=node.icon)
element.image_dict[node.icon] = photo
else:
photo = element.image_dict.get(node.icon)
node.photo = photo
try:
id = treeview.insert(element.KeyToID[node.parent], 'end', iid=None, text=node.text, values=node.values, open=element.ShowExpanded, image=node.photo)
element.IdToKey[id] = node.key
element.KeyToID[node.key] = id
except Exception as e:
print('Error inserting image into tree', e)
else:
id = treeview.insert(element.KeyToID[node.parent], 'end', iid=None, text=node.text, values=node.values, open=element.ShowExpanded)
element.IdToKey[id] = node.key
element.KeyToID[node.key] = id
for node in node.children:
add_treeview_data(node)
add_treeview_data(element.TreeData.root_node)
treeview.column('#0', width=element.Col0Width * _char_width_in_pixels(font), anchor=tk.W)
treeview.heading('#0', text=element.col0_heading)
# ----- configure colors -----
# style_name = str(element.Key) + '.Treeview'
style_name = _make_ttk_style_name('.Treeview', element)
tree_style = ttk.Style()
tree_style.theme_use(toplevel_form.TtkTheme)
if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
tree_style.configure(style_name, background=element.BackgroundColor, fieldbackground=element.BackgroundColor)
if element.SelectedRowColors[1] is not None:
tree_style.map(style_name, background=_fixed_map(tree_style, style_name, 'background', element.SelectedRowColors))
if element.TextColor is not None and element.TextColor != COLOR_SYSTEM_DEFAULT:
tree_style.configure(style_name, foreground=element.TextColor)
if element.SelectedRowColors[0] is not None:
tree_style.map(style_name, foreground=_fixed_map(tree_style, style_name, 'foreground', element.SelectedRowColors))
if element.HeaderTextColor is not None and element.HeaderTextColor != COLOR_SYSTEM_DEFAULT:
tree_style.configure(style_name + '.Heading', foreground=element.HeaderTextColor)
if element.HeaderBackgroundColor is not None and element.HeaderBackgroundColor != COLOR_SYSTEM_DEFAULT:
tree_style.configure(style_name + '.Heading', background=element.HeaderBackgroundColor)
if element.HeaderFont is not None:
tree_style.configure(style_name + '.Heading', font=element.HeaderFont)
else:
tree_style.configure(style_name + '.Heading', font=font)
tree_style.configure(style_name, font=font)
if element.RowHeight:
tree_style.configure(style_name, rowheight=element.RowHeight)
else:
tree_style.configure(style_name, rowheight=_char_height_in_pixels(font))
treeview.configure(style=style_name) # IMPORTANT! Be sure and set the style name for this widget
element.scrollbar = scrollbar = tk.Scrollbar(element_frame)
scrollbar.pack(side=tk.RIGHT, fill='y')
scrollbar.config(command=treeview.yview)
treeview.configure(yscrollcommand=scrollbar.set)
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
element.TKTreeview.pack(side=tk.LEFT, padx=0, pady=0, expand=expand, fill=fill)
if element.visible is False:
element.TKTreeview.pack_forget()
element_frame.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=expand, fill=fill)
treeview.bind("<<TreeviewSelect>>", element._treeview_selected)
if element.Tooltip is not None: # tooltip
element.TooltipObject = ToolTip(element.TKTreeview, text=element.Tooltip,
timeout=DEFAULT_TOOLTIP_TIME)
_add_right_click_menu_and_grab(element)
if tclversion_detailed == '8.6.9' and ENABLE_TREEVIEW_869_PATCH:
# print('*** tk version 8.6.9 detected.... patching ttk treeview code ***')
tree_style.map(style_name,
foreground=_fixed_map(tree_style, style_name, 'foreground', element.SelectedRowColors),
background=_fixed_map(tree_style, style_name, 'background', element.SelectedRowColors))
# ------------------------- Separator placement element ------------------------- #
elif element_type == ELEM_TYPE_SEPARATOR:
element = element # type: VerticalSeparator
# style_name = str(element.Key) + "Line.TSeparator"
style_name = _make_ttk_style_name(".Line.TSeparator", element)
style = ttk.Style()
style.theme_use(toplevel_form.TtkTheme)
if element.color is not None:
style.configure(style_name, background=element.color)
separator = element.Widget = ttk.Separator(tk_row_frame, orient=element.Orientation, )
expand, fill, row_should_expand, row_fill_direction = _add_expansion(element, row_should_expand, row_fill_direction)
if element.Orientation.startswith('h'):
separator.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], fill=tk.X, expand=True)
else:
separator.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], fill=tk.Y, expand=False)
element.Widget.configure(style=style_name) # IMPORTANT! Apply the style
# ------------------------- SizeGrip placement element ------------------------- #
elif element_type == ELEM_TYPE_SIZEGRIP:
element = element # type: Sizegrip
style_name = "Sizegrip.TSizegrip"
style = ttk.Style()
style.theme_use(toplevel_form.TtkTheme)
size_grip = element.Widget = ttk.Sizegrip(tk_row_frame)
toplevel_form.sizegrip_widget = size_grip
# if no size is specified, then use the background color for the window
if element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
style.configure(style_name, background=element.BackgroundColor)
else:
style.configure(style_name, background=toplevel_form.TKroot['bg'])
size_grip.configure(style=style_name)
size_grip.pack(side=tk.BOTTOM, anchor='se', padx=elementpad[0], pady=elementpad[1], fill=tk.X, expand=True)
# tricky part of sizegrip... it shouldn't cause the row to expand, but should expand and should add X axis if
# not already filling in that direction. Otherwise, leaves things alone!
# row_should_expand = True
row_fill_direction = tk.BOTH if row_fill_direction in (tk.Y, tk.BOTH) else tk.X
# ------------------------- StatusBar placement element ------------------------- #
elif element_type == ELEM_TYPE_STATUSBAR:
# auto_size_text = element.AutoSizeText
display_text = element.DisplayText # text to display
if auto_size_text is False:
width, height = element_size
else:
lines = display_text.split('\n')
max_line_len = max([len(l) for l in lines])
num_lines = len(lines)
if max_line_len > element_size[0]: # if text exceeds element size, the will have to wrap
width = element_size[0]
else:
width = max_line_len
height = num_lines
# ---===--- LABEL widget create and place --- #
stringvar = tk.StringVar()
element.TKStringVar = stringvar
stringvar.set(display_text)
if auto_size_text:
width = 0
if element.Justification is not None:
justification = element.Justification
elif toplevel_form.TextJustification is not None:
justification = toplevel_form.TextJustification
else:
justification = DEFAULT_TEXT_JUSTIFICATION
justify = tk.LEFT if justification.startswith('l') else tk.CENTER if justification.startswith('c') else tk.RIGHT
anchor = tk.NW if justification.startswith('l') else tk.N if justification.startswith('c') else tk.NE
# tktext_label = tk.Label(tk_row_frame, textvariable=stringvar, width=width, height=height,
# justify=justify, bd=border_depth, font=font)
tktext_label = element.Widget = tk.Label(tk_row_frame, textvariable=stringvar, width=width,
height=height,
justify=justify, bd=border_depth, font=font)
# Set wrap-length for text (in PIXELS) == PAIN IN THE ASS
wraplen = tktext_label.winfo_reqwidth() + 40 # width of widget in Pixels
if not auto_size_text and height == 1:
wraplen = 0
# print("wraplen, width, height", wraplen, width, height)
tktext_label.configure(anchor=anchor, wraplen=wraplen) # set wrap to width of widget
if element.Relief is not None:
tktext_label.configure(relief=element.Relief)
if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
tktext_label.configure(background=element.BackgroundColor)
if element.TextColor != COLOR_SYSTEM_DEFAULT and element.TextColor is not None:
tktext_label.configure(fg=element.TextColor)
tktext_label.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], fill=tk.X, expand=True)
row_fill_direction = tk.X
if element.visible is False:
tktext_label.pack_forget()
element.TKText = tktext_label
if element.ClickSubmits:
tktext_label.bind('<Button-1>', element._TextClickedHandler)
if element.Tooltip is not None:
element.TooltipObject = ToolTip(element.TKText, text=element.Tooltip, timeout=DEFAULT_TOOLTIP_TIME)
_add_right_click_menu_and_grab(element)
# ............................DONE WITH ROW pack the row of widgets ..........................#
# done with row, pack the row of widgets
# tk_row_frame.grid(row=row_num+2, sticky=tk.NW, padx=DEFAULT_MARGINS[0])
anchor = 'nw'
if row_justify.lower().startswith('c'):
anchor = 'n'
side = tk.LEFT
elif row_justify.lower().startswith('r'):
anchor = 'ne'
side = tk.RIGHT
elif row_justify.lower().startswith('l'):
anchor = 'nw'
side = tk.LEFT
# elif toplevel_form.ElementJustification.lower().startswith('c'):
# anchor = 'n'
# side = tk.TOP
# elif toplevel_form.ElementJustification.lower().startswith('r'):
# anchor = 'ne'
# side = tk.TOP
# else:
# anchor = 'nw'
# side = tk.TOP
# row_should_expand = False
# if form.RightClickMenu:
# menu = form.RightClickMenu
# top_menu = tk.Menu(toplevel_form.TKroot, tearoff=False)
# AddMenuItem(top_menu, menu[1], form)
# tk_row_frame.bind('<Button-3>', form._RightClickMenuCallback)
tk_row_frame.pack(side=tk.TOP, anchor=anchor, padx=0, pady=0, expand=row_should_expand, fill=row_fill_direction)
if form.BackgroundColor is not None and form.BackgroundColor != COLOR_SYSTEM_DEFAULT:
tk_row_frame.configure(background=form.BackgroundColor)
return
def _no_titlebar_setup(window):
"""
Does the operations required to turn off the titlebar for the window.
The Raspberry Pi required the settings to be make after the window's creation.
Calling twice seems to have had better overall results so that's what's currently done.
The MAC has been the problem with this feature. It's been a chronic problem on the Mac.
:param window: window to turn off the titlebar if indicated in the settings
:type window: Window
"""
try:
if window.NoTitleBar:
if running_linux():
# window.TKroot.wm_attributes("-type", 'splash')
window.TKroot.wm_attributes("-type", 'dock')
else:
window.TKroot.wm_overrideredirect(True)
# Special case for Mac. Need to clear flag again if not tkinter version 8.6.10+
# Previously restricted patch to only certain tkinter versions. Now use the patch setting exclusively regardless of tk ver
# if running_mac() and ENABLE_MAC_NOTITLEBAR_PATCH and (sum([int(i) for i in tclversion_detailed.split('.')]) < 24):
# if running_mac() and ENABLE_MAC_NOTITLEBAR_PATCH:
if _mac_should_apply_notitlebar_patch():
print('* Applying Mac no_titlebar patch *')
window.TKroot.wm_overrideredirect(False)
except Exception as e:
warnings.warn('** Problem setting no titlebar {} **'.format(e), UserWarning)
def _convert_window_to_tk(window):
"""
:type window: (Window)
"""
master = window.TKroot
master.title(window.Title)
InitializeResults(window)
PackFormIntoFrame(window, master, window)
window.TKroot.configure(padx=window.Margins[0], pady=window.Margins[1])
# ....................................... DONE creating and laying out window ..........................#
if window._Size != (None, None):
master.geometry("%sx%s" % (window._Size[0], window._Size[1]))
screen_width = master.winfo_screenwidth() # get window info to move to middle of screen
screen_height = master.winfo_screenheight()
if window.Location != (None, None):
x, y = window.Location
elif DEFAULT_WINDOW_LOCATION != (None, None):
x, y = DEFAULT_WINDOW_LOCATION
else:
master.update_idletasks() # don't forget to do updates or values are bad
win_width = master.winfo_width()
win_height = master.winfo_height()
x = screen_width / 2 - win_width / 2
y = screen_height / 2 - win_height / 2
if y + win_height > screen_height:
y = screen_height - win_height
if x + win_width > screen_width:
x = screen_width - win_width
if window.RelativeLoction != (None, None):
x += window.RelativeLoction[0]
y += window.RelativeLoction[1]
move_string = '+%i+%i' % (int(x), int(y))
master.geometry(move_string)
window.config_last_location = (int(x), (int(y)))
window.TKroot.x = int(x)
window.TKroot.y = int(y)
window.starting_window_position = (int(x), (int(y)))
master.update_idletasks() # don't forget
master.geometry(move_string)
master.update_idletasks() # don't forget
_no_titlebar_setup(window)
return
# ----====----====----====----====----==== STARTUP TK ====----====----====----====----====----#
def StartupTK(window):
"""
NOT user callable
Creates the window (for real) lays out all the elements, etc. It's a HUGE set of things it does. It's the basic
"porting layer" that will change depending on the GUI framework PySimpleGUI is running on top of.
:param window: you window object
:type window: (Window)
"""
window = window # type: Window
# global _my_windows
# ow = _my_windows.NumOpenWindows
ow = Window.NumOpenWindows
# print('Starting TK open Windows = {}'.format(ow))
if ENABLE_TK_WINDOWS:
root = tk.Tk()
elif not ow and not window.ForceTopLevel:
# if first window being created, make a throwaway, hidden master root. This stops one user
# window from becoming the child of another user window. All windows are children of this
# hidden window
Window._IncrementOpenCount()
Window.hidden_master_root = tk.Tk()
Window.hidden_master_root.attributes('-alpha', 0) # HIDE this window really really really
# if not running_mac():
try:
Window.hidden_master_root.wm_overrideredirect(True)
except Exception as e:
if not running_mac():
print('* Error performing wm_overrideredirect while hiding the hidden master root*', e)
Window.hidden_master_root.withdraw()
# root = tk.Toplevel(Window.hidden_master_root) # This code caused problems when running with timeout=0 and closed with X
root = tk.Toplevel(class_=window.Title)
else:
# root = tk.Toplevel(Window.hidden_master_root) # This code caused problems when running with timeout=0 and closed with X
root = tk.Toplevel(class_=window.Title)
if window.DebuggerEnabled:
root.bind('<Cancel>', window._callback_main_debugger_window_create_keystroke)
root.bind('<Pause>', window._callback_popout_window_create_keystroke)
# root.bind('<Cancel>', Debugger._build_main_debugger_window)
# root.bind('<Pause>', Debugger._build_floating_window)
try:
if not running_mac() or \
(running_mac() and not window.NoTitleBar) or \
(running_mac() and window.NoTitleBar and not _mac_should_apply_notitlebar_patch()):
root.attributes('-alpha', 0) # hide window while building it. makes for smoother 'paint'
except Exception as e:
print('*** Exception setting alpha channel to zero while creating window ***', e)
if window.BackgroundColor is not None and window.BackgroundColor != COLOR_SYSTEM_DEFAULT:
root.configure(background=window.BackgroundColor)
Window._IncrementOpenCount()
window.TKroot = root
window._create_thread_queue()
# for the Raspberry Pi. Need to set the attributes here, prior to the building of the window
# so going ahead and doing it for all platforms, in addition to doing it after the window is packed
_no_titlebar_setup(window)
if not window.Resizable:
root.resizable(False, False)
if window.DisableMinimize:
root.attributes("-toolwindow", 1)
if window.KeepOnTop:
root.wm_attributes("-topmost", 1)
if window.TransparentColor is not None:
window.SetTransparentColor(window.TransparentColor)
if window.scaling is not None:
root.tk.call('tk', 'scaling', window.scaling)
# root.protocol("WM_DELETE_WINDOW", MyFlexForm.DestroyedCallback())
# root.bind('<Destroy>', MyFlexForm.DestroyedCallback())
_convert_window_to_tk(window)
# Make moveable window
if (window.GrabAnywhere is not False and not (
window.NonBlocking and window.GrabAnywhere is not True)):
if not (ENABLE_MAC_DISABLE_GRAB_ANYWHERE_WITH_TITLEBAR and running_mac() and not window.NoTitleBar):
root.bind("<ButtonPress-1>", window._StartMoveGrabAnywhere)
root.bind("<ButtonRelease-1>", window._StopMove)
root.bind("<B1-Motion>", window._OnMotionGrabAnywhere)
if (window.GrabAnywhereUsingControlKey is not False and not (
window.NonBlocking and window.GrabAnywhereUsingControlKey is not True)):
root.bind("<Control-Button-1>", window._StartMoveUsingControlKey)
root.bind("<Control-ButtonRelease-1>", window._StopMove)
root.bind("<Control-B1-Motion>", window._OnMotionUsingControlKey)
window.set_icon(window.WindowIcon)
try:
root.attributes('-alpha', 1 if window.AlphaChannel is None else window.AlphaChannel) # Make window visible again
except:
pass
if window.ReturnKeyboardEvents and not window.NonBlocking:
root.bind("<KeyRelease>", window._KeyboardCallback)
root.bind("<MouseWheel>", window._MouseWheelCallback)
root.bind("<Button-4>", window._MouseWheelCallback)
root.bind("<Button-5>", window._MouseWheelCallback)
elif window.ReturnKeyboardEvents:
root.bind("<Key>", window._KeyboardCallback)
root.bind("<MouseWheel>", window._MouseWheelCallback)
root.bind("<Button-4>", window._MouseWheelCallback)
root.bind("<Button-5>", window._MouseWheelCallback)
if window.NoTitleBar:
window.TKroot.focus_force()
if window.AutoClose:
# if the window is being finalized, then don't start the autoclose timer
if not window.finalize_in_progress:
window._start_autoclose_timer()
# duration = DEFAULT_AUTOCLOSE_TIME if window.AutoCloseDuration is None else window.AutoCloseDuration
# window.TKAfterID = root.after(int(duration * 1000), window._AutoCloseAlarmCallback)
if window.Timeout != None:
window.TKAfterID = root.after(int(window.Timeout), window._TimeoutAlarmCallback)
if window.NonBlocking:
window.TKroot.protocol("WM_DESTROY_WINDOW", window._OnClosingCallback)
window.TKroot.protocol("WM_DELETE_WINDOW", window._OnClosingCallback)
else: # it's a blocking form
# print('..... CALLING MainLoop')
window.CurrentlyRunningMainloop = True
window.TKroot.protocol("WM_DESTROY_WINDOW", window._OnClosingCallback)
window.TKroot.protocol("WM_DELETE_WINDOW", window._OnClosingCallback)
if window.modal:
window.make_modal()
# window.TKroot.bind("<Configure>", window._config_callback)
# ----------------------------------- tkinter mainloop call -----------------------------------
Window._window_running_mainloop = window
Window._root_running_mainloop = window.TKroot
window.TKroot.mainloop()
window.CurrentlyRunningMainloop = False
window.TimerCancelled = True
# print('..... BACK from MainLoop')
if not window.FormRemainedOpen:
Window._DecrementOpenCount()
# _my_windows.Decrement()
if window.RootNeedsDestroying:
try:
window.TKroot.destroy()
except:
pass
window.RootNeedsDestroying = False
return
def _set_icon_for_tkinter_window(root, icon=None, pngbase64=None):
"""
At the moment, this function is only used by the get_filename or folder with the no_window option set.
Changes the icon that is shown on the title bar and on the task bar.
NOTE - The file type is IMPORTANT and depends on the OS!
Can pass in:
* filename which must be a .ICO icon file for windows, PNG file for Linux
* bytes object
* BASE64 encoded file held in a variable
:param root: The window being modified
:type root: (tk.Tk or tk.TopLevel)
:param icon: Filename or bytes object
:type icon: (str | bytes)
:param pngbase64: Base64 encoded image
:type pngbase64: (bytes)
"""
if type(icon) is bytes or pngbase64 is not None:
wicon = tkinter.PhotoImage(data=icon if icon is not None else pngbase64)
try:
root.tk.call('wm', 'iconphoto', root._w, wicon)
except:
wicon = tkinter.PhotoImage(data=DEFAULT_BASE64_ICON)
try:
root.tk.call('wm', 'iconphoto', root._w, wicon)
except Exception as e:
print('Set icon exception', e)
pass
return
wicon = icon
try:
root.iconbitmap(icon)
except Exception as e:
try:
wicon = tkinter.PhotoImage(file=icon)
root.tk.call('wm', 'iconphoto', root._w, wicon)
except Exception as e:
try:
wicon = tkinter.PhotoImage(data=DEFAULT_BASE64_ICON)
try:
root.tk.call('wm', 'iconphoto', root._w, wicon)
except Exception as e:
print('Set icon exception', e)
pass
except:
print('Set icon exception', e)
pass
# ==============================_GetNumLinesNeeded ==#
# Helper function for determining how to wrap text #
# ===================================================#
def _GetNumLinesNeeded(text, max_line_width):
if max_line_width == 0:
return 1
lines = text.split('\n')
num_lines = len(lines) # number of original lines of text
max_line_len = max([len(l) for l in lines]) # longest line
lines_used = []
for L in lines:
lines_used.append(len(L) // max_line_width + (len(L) % max_line_width > 0)) # fancy math to round up
total_lines_needed = sum(lines_used)
return total_lines_needed
# ============================== PROGRESS METER ========================================== #
def convert_args_to_single_string(*args):
"""
:param *args:
:type *args:
"""
max_line_total, width_used, total_lines, = 0, 0, 0
single_line_message = ''
# loop through args and built a SINGLE string from them
for message in args:
# fancy code to check if string and convert if not is not need. Just always convert to string :-)
# if not isinstance(message, str): message = str(message)
message = str(message)
longest_line_len = max([len(l) for l in message.split('\n')])
width_used = max(longest_line_len, width_used)
max_line_total = max(max_line_total, width_used)
lines_needed = _GetNumLinesNeeded(message, width_used)
total_lines += lines_needed
single_line_message += message + '\n'
return single_line_message, width_used, total_lines
METER_REASON_CANCELLED = 'cancelled'
METER_REASON_CLOSED = 'closed'
METER_REASON_REACHED_MAX = 'finished'
METER_OK = True
METER_STOPPED = False
class QuickMeter(object):
active_meters = {}
exit_reasons = {}
def __init__(self, title, current_value, max_value, key, *args, orientation='v', bar_color=(None, None), button_color=(None, None),
size=DEFAULT_PROGRESS_BAR_SIZE, border_width=None, grab_anywhere=False, no_titlebar=False, keep_on_top=None, no_button=False):
"""
:param title: text to display in element
:type title: (str)
:param current_value: current value
:type current_value: (int)
:param max_value: max value of QuickMeter
:type max_value: (int)
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key: str | int | tuple | object
:param *args: stuff to output
:type *args: (Any)
:param orientation: 'horizontal' or 'vertical' ('h' or 'v' work) (Default value = 'vertical' / 'v')
:type orientation: (str)
:param bar_color: color of a bar line
:type bar_color: (str, str)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param size: (w,h) w=characters-wide, h=rows-high (Default value = DEFAULT_PROGRESS_BAR_SIZE)
:type size: (int, int)
:param border_width: width of border around element
:type border_width: (int)
:param grab_anywhere: If True: can grab anywhere to move the window (Default = False)
:type grab_anywhere: (bool)
:param no_titlebar: If True: window will be created without a titlebar
:type no_titlebar: (bool)
:param keep_on_top: If True the window will remain above all current windows
:type keep_on_top: (bool)
:param no_button: If True: window will be created without a cancel button
:type no_button: (bool)
"""
self.start_time = datetime.datetime.utcnow()
self.key = key
self.orientation = orientation
self.bar_color = bar_color
self.size = size
self.grab_anywhere = grab_anywhere
self.button_color = button_color
self.border_width = border_width
self.no_titlebar = no_titlebar
self.title = title
self.current_value = current_value
self.max_value = max_value
self.close_reason = None
self.keep_on_top = keep_on_top
self.no_button = no_button
self.window = self.BuildWindow(*args)
def BuildWindow(self, *args):
layout = []
if self.orientation.lower().startswith('h'):
col = []
col += [[T(''.join(map(lambda x: str(x) + '\n', args)),
key='-OPTMSG-')]] ### convert all *args into one string that can be updated
col += [[T('', size=(30, 10), key='-STATS-')],
[ProgressBar(max_value=self.max_value, orientation='h', key='-PROG-', size=self.size,
bar_color=self.bar_color)]]
if not self.no_button:
col += [[Cancel(button_color=self.button_color), Stretch()]]
layout = [Column(col)]
else:
col = [[ProgressBar(max_value=self.max_value, orientation='v', key='-PROG-', size=self.size,
bar_color=self.bar_color)]]
col2 = []
col2 += [[T(''.join(map(lambda x: str(x) + '\n', args)),
key='-OPTMSG-')]] ### convert all *args into one string that can be updated
col2 += [[T('', size=(30, 10), key='-STATS-')]]
if not self.no_button:
col2 += [[Cancel(button_color=self.button_color), Stretch()]]
layout = [Column(col), Column(col2)]
self.window = Window(self.title, grab_anywhere=self.grab_anywhere, border_depth=self.border_width, no_titlebar=self.no_titlebar, disable_close=True, keep_on_top=self.keep_on_top)
self.window.Layout([layout]).Finalize()
return self.window
def UpdateMeter(self, current_value, max_value, *args): ### support for *args when updating
self.current_value = current_value
self.max_value = max_value
self.window.Element('-PROG-').UpdateBar(self.current_value, self.max_value)
self.window.Element('-STATS-').Update('\n'.join(self.ComputeProgressStats()))
self.window.Element('-OPTMSG-').Update(
value=''.join(map(lambda x: str(x) + '\n', args))) ### update the string with the args
event, values = self.window.read(timeout=0)
if event in ('Cancel', None) or current_value >= max_value:
exit_reason = METER_REASON_CANCELLED if event in ('Cancel', None) else METER_REASON_REACHED_MAX if current_value >= max_value else METER_STOPPED
self.window.close()
del (QuickMeter.active_meters[self.key])
QuickMeter.exit_reasons[self.key] = exit_reason
return QuickMeter.exit_reasons[self.key]
return METER_OK
def ComputeProgressStats(self):
utc = datetime.datetime.utcnow()
time_delta = utc - self.start_time
total_seconds = time_delta.total_seconds()
if not total_seconds:
total_seconds = 1
try:
time_per_item = total_seconds / self.current_value
except:
time_per_item = 1
seconds_remaining = (self.max_value - self.current_value) * time_per_item
time_remaining = str(datetime.timedelta(seconds=seconds_remaining))
time_remaining_short = (time_remaining).split(".")[0]
time_delta_short = str(time_delta).split(".")[0]
total_time = time_delta + datetime.timedelta(seconds=seconds_remaining)
total_time_short = str(total_time).split(".")[0]
self.stat_messages = [
'{} of {}'.format(self.current_value, self.max_value),
'{} %'.format(100 * self.current_value // self.max_value),
'',
' {:6.2f} Iterations per Second'.format(self.current_value / total_seconds),
' {:6.2f} Seconds per Iteration'.format(total_seconds / (self.current_value if self.current_value else 1)),
'',
'{} Elapsed Time'.format(time_delta_short),
'{} Time Remaining'.format(time_remaining_short),
'{} Estimated Total Time'.format(total_time_short)]
return self.stat_messages
def one_line_progress_meter(title, current_value, max_value, *args, key='OK for 1 meter', orientation='v', bar_color=(None, None), button_color=None, size=DEFAULT_PROGRESS_BAR_SIZE, border_width=None, grab_anywhere=False, no_titlebar=False, keep_on_top=None, no_button=False):
"""
:param title: text to display in eleemnt
:type title: (str)
:param current_value: current value
:type current_value: (int)
:param max_value: max value of QuickMeter
:type max_value: (int)
:param *args: stuff to output
:type *args: (Any)
:param key: Used to differentiate between mutliple meters. Used to cancel meter early. Now optional as there is a default value for single meters
:type key: str | int | tuple | object
:param orientation: 'horizontal' or 'vertical' ('h' or 'v' work) (Default value = 'vertical' / 'v')
:type orientation: (str)
:param bar_color: color of a bar line
:type bar_color: Tuple(str, str)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param size: (w,h) w=characters-wide, h=rows-high (Default value = DEFAULT_PROGRESS_BAR_SIZE)
:type size: (int, int)
:param border_width: width of border around element
:type border_width: (int)
:param grab_anywhere: If True: can grab anywhere to move the window (Default = False)
:type grab_anywhere: (bool)
:param no_titlebar: If True: no titlebar will be shown on the window
:type no_titlebar: (bool)
:param keep_on_top: If True the window will remain above all current windows
:type keep_on_top: (bool)
:param no_button: If True: window will be created without a cancel button
:type no_button: (bool)
:return: True if updated successfully. False if user closed the meter with the X or Cancel button
:rtype: (bool)
"""
if key not in QuickMeter.active_meters:
meter = QuickMeter(title, current_value, max_value, key, *args, orientation=orientation, bar_color=bar_color, button_color=button_color, size=size, border_width=border_width, grab_anywhere=grab_anywhere, no_titlebar=no_titlebar, keep_on_top=keep_on_top, no_button=no_button)
QuickMeter.active_meters[key] = meter
QuickMeter.exit_reasons[key] = None
else:
meter = QuickMeter.active_meters[key]
rc = meter.UpdateMeter(current_value, max_value, *args) ### pass the *args to to UpdateMeter function
OneLineProgressMeter.exit_reasons = getattr(OneLineProgressMeter, 'exit_reasons', QuickMeter.exit_reasons)
exit_reason = OneLineProgressMeter.exit_reasons.get(key)
return METER_OK if exit_reason in (None, METER_REASON_REACHED_MAX) else METER_STOPPED
def one_line_progress_meter_cancel(key='OK for 1 meter'):
"""
Cancels and closes a previously created One Line Progress Meter window
:param key: Key used when meter was created
:type key: (Any)
:return: None
:rtype: None
"""
try:
meter = QuickMeter.active_meters[key]
meter.window.Close()
del (QuickMeter.active_meters[key])
QuickMeter.exit_reasons[key] = METER_REASON_CANCELLED
except: # meter is already deleted
return
def get_complimentary_hex(color):
"""
:param color: color string, like "#RRGGBB"
:type color: (str)
:return: color string, like "#RRGGBB"
:rtype: (str)
"""
# strip the # from the beginning
color = color[1:]
# convert the string into hex
color = int(color, 16)
# invert the three bytes
# as good as substracting each of RGB component by 255(FF)
comp_color = 0xFFFFFF ^ color
# convert the color back to hex by prefixing a #
comp_color = "#%06X" % comp_color
return comp_color
# ======================== EasyPrint =====#
# ===================================================#
class _DebugWin():
debug_window = None
def __init__(self, size=(None, None), location=(None, None), relative_location=(None, None), font=None, no_titlebar=False, no_button=False,
grab_anywhere=False, keep_on_top=None, do_not_reroute_stdout=True, echo_stdout=False, resizable=True):
"""
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param location: Location of upper left corner of the window
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param no_titlebar: If True no titlebar will be shown
:type no_titlebar: (bool)
:param no_button: show button
:type no_button: (bool)
:param grab_anywhere: If True: can grab anywhere to move the window (Default = False)
:type grab_anywhere: (bool)
:param location: Location of upper left corner of the window
:type location: (int, int)
:param do_not_reroute_stdout: bool value
:type do_not_reroute_stdout: (bool)
:param echo_stdout: If True stdout is sent to both the console and the debug window
:type echo_stdout: (bool)
:param resizable: if True, makes the window resizble
:type resizable: (bool)
"""
# Show a form that's a running counter
self.size = size
self.location = location
self.relative_location = relative_location
self.font = font
self.no_titlebar = no_titlebar
self.no_button = no_button
self.grab_anywhere = grab_anywhere
self.keep_on_top = keep_on_top
self.do_not_reroute_stdout = do_not_reroute_stdout
self.echo_stdout = echo_stdout
self.resizable = resizable
win_size = size if size != (None, None) else DEFAULT_DEBUG_WINDOW_SIZE
self.output_element = Multiline(size=win_size, autoscroll=True, auto_refresh=True, reroute_stdout=False if do_not_reroute_stdout else True, echo_stdout_stderr=self.echo_stdout, reroute_stderr=False if do_not_reroute_stdout else True, expand_x=True, expand_y=True, key='-MULTILINE-')
if no_button:
self.layout = [[self.output_element]]
else:
self.layout = [[self.output_element],
[DummyButton('Quit'), Stretch()]]
self.layout[-1] += [Sizegrip()]
self.window = Window('Debug Window', self.layout, no_titlebar=no_titlebar, auto_size_text=True, location=location, relative_location=relative_location,
font=font or ('Courier New', 10), grab_anywhere=grab_anywhere, keep_on_top=keep_on_top, finalize=False, resizable=resizable)
return
def Print(self, *args, end=None, sep=None, text_color=None, background_color=None, erase_all=False, font=None):
sepchar = sep if sep is not None else ' '
endchar = end if end is not None else '\n'
if self.window is None: # if window was destroyed already re-open it
self.__init__(size=self.size, location=self.location, relative_location=self.relative_location, font=self.font, no_titlebar=self.no_titlebar,
no_button=self.no_button, grab_anywhere=self.grab_anywhere, keep_on_top=self.keep_on_top,
do_not_reroute_stdout=self.do_not_reroute_stdout, resizable=self.resizable, echo_stdout=self.echo_stdout)
event, values = self.window.read(timeout=0)
if event == 'Quit' or event is None:
self.Close()
self.__init__(size=self.size, location=self.location, relative_location=self.relative_location, font=self.font, no_titlebar=self.no_titlebar,
no_button=self.no_button, grab_anywhere=self.grab_anywhere, keep_on_top=self.keep_on_top,
do_not_reroute_stdout=self.do_not_reroute_stdout, resizable=self.resizable, echo_stdout=self.echo_stdout)
event, values = self.window.read(timeout=0)
if erase_all:
self.window['-MULTILINE-'].update('')
if self.do_not_reroute_stdout:
end_str = str(end) if end is not None else '\n'
sep_str = str(sep) if sep is not None else ' '
outstring = ''
num_args = len(args)
for i, arg in enumerate(args):
outstring += str(arg)
if i != num_args - 1:
outstring += sep_str
outstring += end_str
self.output_element.Update(outstring, append=True, text_color_for_value=text_color, background_color_for_value=background_color,
font_for_value=font)
else:
print(*args, sep=sepchar, end=endchar)
def Close(self):
if self.window.XFound: # increment the number of open windows to get around a bug with debug windows
Window._IncrementOpenCount()
self.window.close()
self.window = None
def easy_print(*args, size=(None, None), end=None, sep=None, location=(None, None), relative_location=(None, None), font=None, no_titlebar=False,
no_button=False, grab_anywhere=False, keep_on_top=None, do_not_reroute_stdout=True, echo_stdout=False, text_color=None, background_color=None, colors=None, c=None,
erase_all=False, resizable=True):
"""
Works like a "print" statement but with windowing options. Routes output to the "Debug Window"
In addition to the normal text and background colors, you can use a "colors" tuple/string
The "colors" or "c" parameter defines both the text and background in a single parm.
It can be a tuple or a single single. Both text and background colors need to be specified
colors -(str, str) or str. A combined text/background color definition in a single parameter
c - (str, str) - Colors tuple has format (foreground, backgrouned)
c - str - can also be a string of the format "foreground on background" ("white on red")
:param *args: stuff to output
:type *args: (Any)
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param end: end character
:type end: (str)
:param sep: separator character
:type sep: (str)
:param location: Location of upper left corner of the window
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param no_titlebar: If True no titlebar will be shown
:type no_titlebar: (bool)
:param no_button: don't show button
:type no_button: (bool)
:param grab_anywhere: If True: can grab anywhere to move the window (Default = False)
:type grab_anywhere: (bool)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param keep_on_top: If True the window will remain above all current windows
:type keep_on_top: (bool)
:param location: Location of upper left corner of the window
:type location: (int, int)
:param do_not_reroute_stdout: do not reroute stdout and stderr. If False, both stdout and stderr will reroute to here
:type do_not_reroute_stdout: (bool)
:param echo_stdout: If True stdout is sent to both the console and the debug window
:type echo_stdout: (bool)
:param colors: Either a tuple or a string that has both the text and background colors
:type colors: (str) or (str, str)
:param c: Either a tuple or a string that has both the text and background colors
:type c: (str) or (str, str)
:param resizable: if True, the user can resize the debug window. Default is True
:type resizable: (bool)
:param erase_all: If True when erase the output before printing
:type erase_all: (bool)
:return:
:rtype:
"""
if _DebugWin.debug_window is None:
_DebugWin.debug_window = _DebugWin(size=size, location=location, relative_location=relative_location, font=font, no_titlebar=no_titlebar,
no_button=no_button, grab_anywhere=grab_anywhere, keep_on_top=keep_on_top,
do_not_reroute_stdout=do_not_reroute_stdout, echo_stdout=echo_stdout, resizable=resizable)
txt_color, bg_color = _parse_colors_parm(c or colors)
_DebugWin.debug_window.Print(*args, end=end, sep=sep, text_color=text_color or txt_color, background_color=background_color or bg_color,
erase_all=erase_all, font=font)
def easy_print_close():
"""
Close a previously opened EasyPrint window
:return:
:rtype:
"""
if _DebugWin.debug_window is not None:
_DebugWin.debug_window.Close()
_DebugWin.debug_window = None
# d8b 888
# Y8P 888
# 888
# .d8888b 88888b. 888d888 888 88888b. 888888
# d88P" 888 "88b 888P" 888 888 "88b 888
# 888 888 888 888 888 888 888 888
# Y88b. 888 d88P 888 888 888 888 Y88b.
# "Y8888P 88888P" 888 888 888 888 "Y888
# 888
# 888
# 888
CPRINT_DESTINATION_WINDOW = None
CPRINT_DESTINATION_MULTILINE_ELMENT_KEY = None
def cprint_set_output_destination(window, multiline_key):
"""
Sets up the color print (cprint) output destination
:param window: The window that the cprint call will route the output to
:type window: (Window)
:param multiline_key: Key for the Multiline Element where output will be sent
:type multiline_key: (Any)
:return: None
:rtype: None
"""
global CPRINT_DESTINATION_WINDOW, CPRINT_DESTINATION_MULTILINE_ELMENT_KEY
CPRINT_DESTINATION_WINDOW = window
CPRINT_DESTINATION_MULTILINE_ELMENT_KEY = multiline_key
def cprint(*args, end=None, sep=' ', text_color=None, font=None, t=None, background_color=None, b=None, colors=None, c=None, window=None, key=None,
justification=None, autoscroll=True):
"""
Color print to a multiline element in a window of your choice.
Must have EITHER called cprint_set_output_destination prior to making this call so that the
window and element key can be saved and used here to route the output, OR used the window
and key parameters to the cprint function to specicy these items.
args is a variable number of things you want to print.
end - The end char to use just like print uses
sep - The separation character like print uses
text_color - The color of the text
key - overrides the previously defined Multiline key
window - overrides the previously defined window to output to
background_color - The color of the background
colors -(str, str) or str. A combined text/background color definition in a single parameter
There are also "aliases" for text_color, background_color and colors (t, b, c)
t - An alias for color of the text (makes for shorter calls)
b - An alias for the background_color parameter
c - (str, str) - "shorthand" way of specifying color. (foreground, backgrouned)
c - str - can also be a string of the format "foreground on background" ("white on red")
With the aliases it's possible to write the same print but in more compact ways:
cprint('This will print white text on red background', c=('white', 'red'))
cprint('This will print white text on red background', c='white on red')
cprint('This will print white text on red background', text_color='white', background_color='red')
cprint('This will print white text on red background', t='white', b='red')
:param *args: stuff to output
:type *args: (Any)
:param text_color: Color of the text
:type text_color: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike for the value being updated
:type font: (str or (str, int[, str]) or None)
:param background_color: The background color of the line
:type background_color: (str)
:param colors: Either a tuple or a string that has both the text and background colors "text on background" or just the text color
:type colors: (str) or (str, str)
:param t: Color of the text
:type t: (str)
:param b: The background color of the line
:type b: (str)
:param c: Either a tuple or a string. Same as the color parm
:type c: (str) or (str, str)
:param end: end character
:type end: (str)
:param sep: separator character
:type sep: (str)
:param key: key of multiline to output to (if you want to override the one previously set)
:type key: (Any)
:param window: Window containing the multiline to output to (if you want to override the one previously set)
:type window: (Window)
:param justification: text justification. left, right, center. Can use single characters l, r, c. Sets only for this value, not entire element
:type justification: (str)
:param autoscroll: If True the contents of the element will automatically scroll as more data added to the end
:type autoscroll: (bool)
"""
destination_key = CPRINT_DESTINATION_MULTILINE_ELMENT_KEY if key is None else key
destination_window = window or CPRINT_DESTINATION_WINDOW
if (destination_window is None and window is None) or (destination_key is None and key is None):
print('** Warning ** Attempting to perform a cprint without a valid window & key',
'Will instead print on Console',
'You can specify window and key in this cprint call, or set ahead of time using cprint_set_output_destination')
print(*args)
return
kw_text_color = text_color or t
kw_background_color = background_color or b
dual_color = colors or c
try:
if isinstance(dual_color, tuple):
kw_text_color = dual_color[0]
kw_background_color = dual_color[1]
elif isinstance(dual_color, str):
if ' on ' in dual_color: # if has "on" in the string, then have both text and background
kw_text_color = dual_color.split(' on ')[0]
kw_background_color = dual_color.split(' on ')[1]
else: # if no "on" then assume the color string is just the text color
kw_text_color = dual_color
except Exception as e:
print('* cprint warning * you messed up with color formatting', e)
mline = destination_window.find_element(destination_key, silent_on_error=True) # type: Multiline
try:
# mline = destination_window[destination_key] # type: Multiline
if end is None:
mline.print(*args, text_color=kw_text_color, background_color=kw_background_color, end='', sep=sep, justification=justification, font=font,
autoscroll=autoscroll)
mline.print('', justification=justification, autoscroll=autoscroll)
else:
mline.print(*args, text_color=kw_text_color, background_color=kw_background_color, end=end, sep=sep, justification=justification, font=font,
autoscroll=autoscroll)
except Exception as e:
print('** cprint error trying to print to the multiline. Printing to console instead **', e)
print(*args, end=end, sep=sep)
# ------------------------------------------------------------------------------------------------ #
# A print-like call that can be used to output to a multiline element as if it's an Output element #
# ------------------------------------------------------------------------------------------------ #
def _print_to_element(multiline_element, *args, end=None, sep=None, text_color=None, background_color=None, autoscroll=None, justification=None, font=None):
"""
Print like Python normally prints except route the output to a multiline element and also add colors if desired
:param multiline_element: The multiline element to be output to
:type multiline_element: (Multiline)
:param args: The arguments to print
:type args: List[Any]
:param end: The end char to use just like print uses
:type end: (str)
:param sep: The separation character like print uses
:type sep: (str)
:param text_color: color of the text
:type text_color: (str)
:param background_color: The background color of the line
:type background_color: (str)
:param autoscroll: If True (the default), the element will scroll to bottom after updating
:type autoscroll: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike for the value being updated
:type font: str | (str, int)
"""
end_str = str(end) if end is not None else '\n'
sep_str = str(sep) if sep is not None else ' '
outstring = ''
num_args = len(args)
for i, arg in enumerate(args):
outstring += str(arg)
if i != num_args - 1:
outstring += sep_str
outstring += end_str
multiline_element.update(outstring, append=True, text_color_for_value=text_color, background_color_for_value=background_color, autoscroll=autoscroll,
justification=justification, font_for_value=font)
try: # if the element is set to autorefresh, then refresh the parent window
if multiline_element.AutoRefresh:
multiline_element.ParentForm.refresh()
except:
pass
def _parse_colors_parm(colors):
"""
Parse a colors parameter into its separate colors.
Some functions accept a dual colors string/tuple.
This function parses the parameter into the component colors
:param colors: Either a tuple or a string that has both the text and background colors
:type colors: (str) or (str, str)
:return: tuple with the individual text and background colors
:rtype: (str, str)
"""
if colors is None:
return None, None
dual_color = colors
kw_text_color = kw_background_color = None
try:
if isinstance(dual_color, tuple):
kw_text_color = dual_color[0]
kw_background_color = dual_color[1]
elif isinstance(dual_color, str):
if ' on ' in dual_color: # if has "on" in the string, then have both text and background
kw_text_color = dual_color.split(' on ')[0]
kw_background_color = dual_color.split(' on ')[1]
else: # if no "on" then assume the color string is just the text color
kw_text_color = dual_color
except Exception as e:
print('* warning * you messed up with color formatting', e)
return kw_text_color, kw_background_color
# ============================== set_global_icon ====#
# Sets the icon to be used by default #
# ===================================================#
def set_global_icon(icon):
"""
Sets the icon which will be used any time a window is created if an icon is not provided when the
window is created.
:param icon: Either a Base64 byte string or a filename
:type icon: bytes | str
"""
Window._user_defined_icon = icon
# ============================== set_options ========#
# Sets the icon to be used by default #
# ===================================================#
def set_options(icon=None, button_color=None, element_size=(None, None), button_element_size=(None, None),
margins=(None, None),
element_padding=(None, None), auto_size_text=None, auto_size_buttons=None, font=None, border_width=None,
slider_border_width=None, slider_relief=None, slider_orientation=None,
autoclose_time=None, message_box_line_width=None,
progress_meter_border_depth=None, progress_meter_style=None,
progress_meter_relief=None, progress_meter_color=None, progress_meter_size=None,
text_justification=None, background_color=None, element_background_color=None,
text_element_background_color=None, input_elements_background_color=None, input_text_color=None,
scrollbar_color=None, text_color=None, element_text_color=None, debug_win_size=(None, None),
window_location=(None, None), error_button_color=(None, None), tooltip_time=None, tooltip_font=None, use_ttk_buttons=None, ttk_theme=None,
suppress_error_popups=None, suppress_raise_key_errors=None, suppress_key_guessing=None,warn_button_key_duplicates=False, enable_treeview_869_patch=None,
enable_mac_notitlebar_patch=None, use_custom_titlebar=None, titlebar_background_color=None, titlebar_text_color=None, titlebar_font=None,
titlebar_icon=None, user_settings_path=None, pysimplegui_settings_path=None, pysimplegui_settings_filename=None, keep_on_top=None, dpi_awareness=None, scaling=None,
disable_modal_windows=None):
"""
:param icon: Can be either a filename or Base64 value. For Windows if filename, it MUST be ICO format. For Linux, must NOT be ICO. Most portable is to use a Base64 of a PNG file. This works universally across all OS's
:type icon: bytes | str
:param button_color: Color of the button (text, background)
:type button_color: (str, str) or str
:param element_size: element size (width, height) in characters
:type element_size: (int, int)
:param button_element_size: Size of button
:type button_element_size: (int, int)
:param margins: (left/right, top/bottom) tkinter margins around outsize. Amount of pixels to leave inside the window's frame around the edges before your elements are shown.
:type margins: (int, int)
:param element_padding: Default amount of padding to put around elements in window (left/right, top/bottom) or ((left, right), (top, bottom))
:type element_padding: (int, int) or ((int, int),(int,int))
:param auto_size_text: True if the Widget should be shrunk to exactly fit the number of chars to show
:type auto_size_text: bool
:param auto_size_buttons: True if Buttons in this Window should be sized to exactly fit the text on this.
:type auto_size_buttons: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param border_width: width of border around element
:type border_width: (int)
:param slider_border_width: Width of the border around sliders
:type slider_border_width: (int)
:param slider_relief: Type of relief to use for sliders
:type slider_relief: (str)
:param slider_orientation: ???
:type slider_orientation: ???
:param autoclose_time: ???
:type autoclose_time: ???
:param message_box_line_width: ???
:type message_box_line_width: ???
:param progress_meter_border_depth: ???
:type progress_meter_border_depth: ???
:param progress_meter_style: You can no longer set a progress bar style. All ttk styles must be the same for the window
:type progress_meter_style: ???
:param progress_meter_relief:
:type progress_meter_relief: ???
:param progress_meter_color: ???
:type progress_meter_color: ???
:param progress_meter_size: ???
:type progress_meter_size: ???
:param text_justification: Default text justification for all Text Elements in window
:type text_justification: 'left' | 'right' | 'center'
:param background_color: color of background
:type background_color: (str)
:param element_background_color: element background color
:type element_background_color: (str)
:param text_element_background_color: text element background color
:type text_element_background_color: (str)
:param input_elements_background_color: Default color to use for the background of input elements
:type input_elements_background_color: (str)
:param input_text_color: Default color to use for the text for Input elements
:type input_text_color: (str)
:param scrollbar_color: Default color to use for the slider trough
:type scrollbar_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param element_text_color: Default color to use for Text elements
:type element_text_color: (str)
:param debug_win_size: window size
:type debug_win_size: (int, int)
:param window_location: Default location to place windows. Not setting will center windows on the display
:type window_location: (int, int) | None
:param error_button_color: (Default = (None))
:type error_button_color: ???
:param tooltip_time: time in milliseconds to wait before showing a tooltip. Default is 400ms
:type tooltip_time: (int)
:param tooltip_font: font to use for all tooltips
:type tooltip_font: str or Tuple[str, int] or Tuple[str, int, str]
:param use_ttk_buttons: if True will cause all buttons to be ttk buttons
:type use_ttk_buttons: (bool)
:param ttk_theme: Theme to use with ttk widgets. Choices (on Windows) include - 'default', 'winnative', 'clam', 'alt', 'classic', 'vista', 'xpnative'
:type ttk_theme: (str)
:param suppress_error_popups: If True then error popups will not be shown if generated internally to PySimpleGUI
:type suppress_error_popups: (bool)
:param suppress_raise_key_errors: If True then key errors won't be raised (you'll still get popup error)
:type suppress_raise_key_errors: (bool)
:param suppress_key_guessing: If True then key errors won't try and find closest matches for you
:type suppress_key_guessing: (bool)
:param warn_button_key_duplicates: If True then duplicate Button Keys generate warnings (not recommended as they're expected)
:type warn_button_key_duplicates: (bool)
:param enable_treeview_869_patch: If True, then will use the treeview color patch for tk 8.6.9
:type enable_treeview_869_patch: (bool)
:param enable_mac_notitlebar_patch: If True then Windows with no titlebar use an alternative technique when tkinter version < 8.6.10
:type enable_mac_notitlebar_patch: (bool)
:param use_custom_titlebar: If True then a custom titlebar is used instead of the normal system titlebar
:type use_custom_titlebar: (bool)
:param titlebar_background_color: If custom titlebar indicated by use_custom_titlebar, then use this as background color
:type titlebar_background_color: str | None
:param titlebar_text_color: If custom titlebar indicated by use_custom_titlebar, then use this as text color
:type titlebar_text_color: str | None
:param titlebar_font: If custom titlebar indicated by use_custom_titlebar, then use this as title font
:type titlebar_font: (str or (str, int[, str]) or None) | None
:param titlebar_icon: If custom titlebar indicated by use_custom_titlebar, then use this as the icon (file or base64 bytes)
:type titlebar_icon: bytes | str
:param user_settings_path: default path for user_settings API calls. Expanded with os.path.expanduser so can contain ~ to represent user
:type user_settings_path: (str)
:param pysimplegui_settings_path: default path for the global PySimpleGUI user_settings
:type pysimplegui_settings_path: (str)
:param pysimplegui_settings_filename: default filename for the global PySimpleGUI user_settings
:type pysimplegui_settings_filename: (str)
:param keep_on_top: If True then all windows will automatically be set to keep_on_top=True
:type keep_on_top: (bool)
:param dpi_awareness: If True then will turn on DPI awareness (Windows only at the moment)
:type dpi_awareness: (bool)
:param scaling: Sets the default scaling for all windows including popups, etc.
:type scaling: (float)
:param disable_modal_windows: If True then all windows, including popups, will not be modal windows
:type disable_modal_windows: (bool)
:return: None
:rtype: None
"""
global DEFAULT_ELEMENT_SIZE
global DEFAULT_BUTTON_ELEMENT_SIZE
global DEFAULT_MARGINS # Margins for each LEFT/RIGHT margin is first term
global DEFAULT_ELEMENT_PADDING # Padding between elements (row, col) in pixels
global DEFAULT_AUTOSIZE_TEXT
global DEFAULT_AUTOSIZE_BUTTONS
global DEFAULT_FONT
global DEFAULT_BORDER_WIDTH
global DEFAULT_AUTOCLOSE_TIME
global DEFAULT_BUTTON_COLOR
global MESSAGE_BOX_LINE_WIDTH
global DEFAULT_PROGRESS_BAR_BORDER_WIDTH
global DEFAULT_PROGRESS_BAR_STYLE
global DEFAULT_PROGRESS_BAR_RELIEF
global DEFAULT_PROGRESS_BAR_COLOR
global DEFAULT_PROGRESS_BAR_SIZE
global DEFAULT_TEXT_JUSTIFICATION
global DEFAULT_DEBUG_WINDOW_SIZE
global DEFAULT_SLIDER_BORDER_WIDTH
global DEFAULT_SLIDER_RELIEF
global DEFAULT_SLIDER_ORIENTATION
global DEFAULT_BACKGROUND_COLOR
global DEFAULT_INPUT_ELEMENTS_COLOR
global DEFAULT_ELEMENT_BACKGROUND_COLOR
global DEFAULT_TEXT_ELEMENT_BACKGROUND_COLOR
global DEFAULT_SCROLLBAR_COLOR
global DEFAULT_TEXT_COLOR
global DEFAULT_WINDOW_LOCATION
global DEFAULT_ELEMENT_TEXT_COLOR
global DEFAULT_INPUT_TEXT_COLOR
global DEFAULT_TOOLTIP_TIME
global DEFAULT_ERROR_BUTTON_COLOR
global DEFAULT_TTK_THEME
global USE_TTK_BUTTONS
global TOOLTIP_FONT
global SUPPRESS_ERROR_POPUPS
global SUPPRESS_RAISE_KEY_ERRORS
global SUPPRESS_KEY_GUESSING
global WARN_DUPLICATE_BUTTON_KEY_ERRORS
global ENABLE_TREEVIEW_869_PATCH
global ENABLE_MAC_NOTITLEBAR_PATCH
global USE_CUSTOM_TITLEBAR
global CUSTOM_TITLEBAR_BACKGROUND_COLOR
global CUSTOM_TITLEBAR_TEXT_COLOR
global CUSTOM_TITLEBAR_ICON
global CUSTOM_TITLEBAR_FONT
global DEFAULT_USER_SETTINGS_PATH
global DEFAULT_USER_SETTINGS_PYSIMPLEGUI_PATH
global DEFAULT_USER_SETTINGS_PYSIMPLEGUI_FILENAME
global DEFAULT_KEEP_ON_TOP
global DEFAULT_SCALING
global DEFAULT_MODAL_WINDOWS_ENABLED
global _pysimplegui_user_settings
# global _my_windows
if icon:
Window._user_defined_icon = icon
# _my_windows._user_defined_icon = icon
if button_color != None:
if button_color == COLOR_SYSTEM_DEFAULT:
DEFAULT_BUTTON_COLOR = (COLOR_SYSTEM_DEFAULT, COLOR_SYSTEM_DEFAULT)
else:
DEFAULT_BUTTON_COLOR = button_color
if element_size != (None, None):
DEFAULT_ELEMENT_SIZE = element_size
if button_element_size != (None, None):
DEFAULT_BUTTON_ELEMENT_SIZE = button_element_size
if margins != (None, None):
DEFAULT_MARGINS = margins
if element_padding != (None, None):
DEFAULT_ELEMENT_PADDING = element_padding
if auto_size_text != None:
DEFAULT_AUTOSIZE_TEXT = auto_size_text
if auto_size_buttons != None:
DEFAULT_AUTOSIZE_BUTTONS = auto_size_buttons
if font != None:
DEFAULT_FONT = font
if border_width != None:
DEFAULT_BORDER_WIDTH = border_width
if autoclose_time != None:
DEFAULT_AUTOCLOSE_TIME = autoclose_time
if message_box_line_width != None:
MESSAGE_BOX_LINE_WIDTH = message_box_line_width
if progress_meter_border_depth != None:
DEFAULT_PROGRESS_BAR_BORDER_WIDTH = progress_meter_border_depth
if progress_meter_style != None:
warnings.warn('You can no longer set a progress bar style. All ttk styles must be the same for the window', UserWarning)
# DEFAULT_PROGRESS_BAR_STYLE = progress_meter_style
if progress_meter_relief != None:
DEFAULT_PROGRESS_BAR_RELIEF = progress_meter_relief
if progress_meter_color != None:
DEFAULT_PROGRESS_BAR_COLOR = progress_meter_color
if progress_meter_size != None:
DEFAULT_PROGRESS_BAR_SIZE = progress_meter_size
if slider_border_width != None:
DEFAULT_SLIDER_BORDER_WIDTH = slider_border_width
if slider_orientation != None:
DEFAULT_SLIDER_ORIENTATION = slider_orientation
if slider_relief != None:
DEFAULT_SLIDER_RELIEF = slider_relief
if text_justification != None:
DEFAULT_TEXT_JUSTIFICATION = text_justification
if background_color != None:
DEFAULT_BACKGROUND_COLOR = background_color
if text_element_background_color != None:
DEFAULT_TEXT_ELEMENT_BACKGROUND_COLOR = text_element_background_color
if input_elements_background_color != None:
DEFAULT_INPUT_ELEMENTS_COLOR = input_elements_background_color
if element_background_color != None:
DEFAULT_ELEMENT_BACKGROUND_COLOR = element_background_color
if window_location != (None, None):
DEFAULT_WINDOW_LOCATION = window_location
if debug_win_size != (None, None):
DEFAULT_DEBUG_WINDOW_SIZE = debug_win_size
if text_color != None:
DEFAULT_TEXT_COLOR = text_color
if scrollbar_color != None:
DEFAULT_SCROLLBAR_COLOR = scrollbar_color
if element_text_color != None:
DEFAULT_ELEMENT_TEXT_COLOR = element_text_color
if input_text_color is not None:
DEFAULT_INPUT_TEXT_COLOR = input_text_color
if tooltip_time is not None:
DEFAULT_TOOLTIP_TIME = tooltip_time
if error_button_color != (None, None):
DEFAULT_ERROR_BUTTON_COLOR = error_button_color
if ttk_theme is not None:
DEFAULT_TTK_THEME = ttk_theme
if use_ttk_buttons is not None:
USE_TTK_BUTTONS = use_ttk_buttons
if tooltip_font is not None:
TOOLTIP_FONT = tooltip_font
if suppress_error_popups is not None:
SUPPRESS_ERROR_POPUPS = suppress_error_popups
if suppress_raise_key_errors is not None:
SUPPRESS_RAISE_KEY_ERRORS = suppress_raise_key_errors
if suppress_key_guessing is not None:
SUPPRESS_KEY_GUESSING = suppress_key_guessing
if warn_button_key_duplicates is not None:
WARN_DUPLICATE_BUTTON_KEY_ERRORS = warn_button_key_duplicates
if enable_treeview_869_patch is not None:
ENABLE_TREEVIEW_869_PATCH = enable_treeview_869_patch
if enable_mac_notitlebar_patch is not None:
ENABLE_MAC_NOTITLEBAR_PATCH = enable_mac_notitlebar_patch
if use_custom_titlebar is not None:
USE_CUSTOM_TITLEBAR = use_custom_titlebar
if titlebar_background_color is not None:
CUSTOM_TITLEBAR_BACKGROUND_COLOR = titlebar_background_color
if titlebar_text_color is not None:
CUSTOM_TITLEBAR_TEXT_COLOR = titlebar_text_color
if titlebar_font is not None:
CUSTOM_TITLEBAR_FONT = titlebar_font
if titlebar_icon is not None:
CUSTOM_TITLEBAR_ICON = titlebar_icon
if user_settings_path is not None:
DEFAULT_USER_SETTINGS_PATH = user_settings_path
if pysimplegui_settings_path is not None:
DEFAULT_USER_SETTINGS_PYSIMPLEGUI_PATH = pysimplegui_settings_path
if pysimplegui_settings_filename is not None:
DEFAULT_USER_SETTINGS_PYSIMPLEGUI_FILENAME = pysimplegui_settings_filename
if pysimplegui_settings_filename is not None or pysimplegui_settings_filename is not None:
_pysimplegui_user_settings = UserSettings(filename=DEFAULT_USER_SETTINGS_PYSIMPLEGUI_FILENAME,
path=DEFAULT_USER_SETTINGS_PYSIMPLEGUI_PATH)
if keep_on_top is not None:
DEFAULT_KEEP_ON_TOP = keep_on_top
if dpi_awareness is True:
if running_windows():
if platform.release() == "7":
ctypes.windll.user32.SetProcessDPIAware()
elif platform.release() == "8" or platform.release() == "10":
ctypes.windll.shcore.SetProcessDpiAwareness(1)
if scaling is not None:
DEFAULT_SCALING = scaling
if disable_modal_windows is not None:
DEFAULT_MODAL_WINDOWS_ENABLED = not disable_modal_windows
return True
# ----------------------------------------------------------------- #
# .########.##.....##.########.##.....##.########..######.
# ....##....##.....##.##.......###...###.##.......##....##
# ....##....##.....##.##.......####.####.##.......##......
# ....##....#########.######...##.###.##.######....######.
# ....##....##.....##.##.......##.....##.##.............##
# ....##....##.....##.##.......##.....##.##.......##....##
# ....##....##.....##.########.##.....##.########..######.
# ----------------------------------------------------------------- #
# The official Theme code
#################### ChangeLookAndFeel #######################
# Predefined settings that will change the colors and styles #
# of the elements. #
##############################################################
LOOK_AND_FEEL_TABLE = {
"SystemDefault": {"BACKGROUND": COLOR_SYSTEM_DEFAULT, "TEXT": COLOR_SYSTEM_DEFAULT, "INPUT": COLOR_SYSTEM_DEFAULT, "TEXT_INPUT": COLOR_SYSTEM_DEFAULT,
"SCROLL": COLOR_SYSTEM_DEFAULT, "BUTTON": OFFICIAL_PYSIMPLEGUI_BUTTON_COLOR, "PROGRESS": COLOR_SYSTEM_DEFAULT, "BORDER": 1,
"SLIDER_DEPTH": 1, "PROGRESS_DEPTH": 0, },
"SystemDefaultForReal": {"BACKGROUND": COLOR_SYSTEM_DEFAULT, "TEXT": COLOR_SYSTEM_DEFAULT, "INPUT": COLOR_SYSTEM_DEFAULT,
"TEXT_INPUT": COLOR_SYSTEM_DEFAULT, "SCROLL": COLOR_SYSTEM_DEFAULT, "BUTTON": COLOR_SYSTEM_DEFAULT,
"PROGRESS": COLOR_SYSTEM_DEFAULT, "BORDER": 1, "SLIDER_DEPTH": 1, "PROGRESS_DEPTH": 0, },
"SystemDefault1": {"BACKGROUND": COLOR_SYSTEM_DEFAULT, "TEXT": COLOR_SYSTEM_DEFAULT, "INPUT": COLOR_SYSTEM_DEFAULT, "TEXT_INPUT": COLOR_SYSTEM_DEFAULT,
"SCROLL": COLOR_SYSTEM_DEFAULT, "BUTTON": COLOR_SYSTEM_DEFAULT, "PROGRESS": COLOR_SYSTEM_DEFAULT, "BORDER": 1, "SLIDER_DEPTH": 1,
"PROGRESS_DEPTH": 0, },
"Material1": {"BACKGROUND": "#E3F2FD", "TEXT": "#000000", "INPUT": "#86A8FF", "TEXT_INPUT": "#000000", "SCROLL": "#86A8FF",
"BUTTON": ("#FFFFFF", "#5079D3"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 0, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"ACCENT1": "#FF0266", "ACCENT2": "#FF5C93", "ACCENT3": "#C5003C", },
"Material2": {"BACKGROUND": "#FAFAFA", "TEXT": "#000000", "INPUT": "#004EA1", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#5EA7FF",
"BUTTON": ("#FFFFFF", "#0079D3"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 0, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"ACCENT1": "#FF0266", "ACCENT2": "#FF5C93", "ACCENT3": "#C5003C", },
"Reddit": {"BACKGROUND": "#ffffff", "TEXT": "#1a1a1b", "INPUT": "#dae0e6", "TEXT_INPUT": "#222222", "SCROLL": "#a5a4a4", "BUTTON": ("#FFFFFF", "#0079d3"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, "ACCENT1": "#ff5414", "ACCENT2": "#33a8ff",
"ACCENT3": "#dbf0ff", },
"Topanga": {"BACKGROUND": "#282923", "TEXT": "#E7DB74", "INPUT": "#393a32", "TEXT_INPUT": "#E7C855", "SCROLL": "#E7C855", "BUTTON": ("#E7C855", "#284B5A"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, "ACCENT1": "#c15226", "ACCENT2": "#7a4d5f",
"ACCENT3": "#889743", },
"GreenTan": {"BACKGROUND": "#9FB8AD", "TEXT": '#000000', "INPUT": "#F7F3EC", "TEXT_INPUT": "#000000", "SCROLL": "#F7F3EC", "BUTTON": ("#FFFFFF", "#475841"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"Dark": {"BACKGROUND": "#404040", "TEXT": "#FFFFFF", "INPUT": "#4D4D4D", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#707070", "BUTTON": ("#FFFFFF", "#004F00"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"LightGreen": {"BACKGROUND": "#B7CECE", "TEXT": "#000000", "INPUT": "#FDFFF7", "TEXT_INPUT": "#000000", "SCROLL": "#FDFFF7",
"BUTTON": ("#FFFFFF", "#658268"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "ACCENT1": "#76506d",
"ACCENT2": "#5148f1", "ACCENT3": "#0a1c84", "PROGRESS_DEPTH": 0, },
"Dark2": {"BACKGROUND": "#404040", "TEXT": "#FFFFFF", "INPUT": "#FFFFFF", "TEXT_INPUT": "#000000", "SCROLL": "#707070", "BUTTON": ("#FFFFFF", "#004F00"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"Black": {"BACKGROUND": "#000000", "TEXT": "#FFFFFF", "INPUT": "#4D4D4D", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#707070", "BUTTON": ("#000000", "#FFFFFF"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"Tan": {"BACKGROUND": "#fdf6e3", "TEXT": "#268bd1", "INPUT": "#eee8d5", "TEXT_INPUT": "#6c71c3", "SCROLL": "#eee8d5", "BUTTON": ("#FFFFFF", "#063542"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"TanBlue": {"BACKGROUND": "#e5dece", "TEXT": "#063289", "INPUT": "#f9f8f4", "TEXT_INPUT": "#242834", "SCROLL": "#eee8d5", "BUTTON": ("#FFFFFF", "#063289"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkTanBlue": {"BACKGROUND": "#242834", "TEXT": "#dfe6f8", "INPUT": "#97755c", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#a9afbb",
"BUTTON": ("#FFFFFF", "#063289"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkAmber": {"BACKGROUND": "#2c2825", "TEXT": "#fdcb52", "INPUT": "#705e52", "TEXT_INPUT": "#fdcb52", "SCROLL": "#705e52",
"BUTTON": ("#000000", "#fdcb52"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkBlue": {"BACKGROUND": "#1a2835", "TEXT": "#d1ecff", "INPUT": "#335267", "TEXT_INPUT": "#acc2d0", "SCROLL": "#1b6497", "BUTTON": ("#000000", "#fafaf8"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"Reds": {"BACKGROUND": "#280001", "TEXT": "#FFFFFF", "INPUT": "#d8d584", "TEXT_INPUT": "#000000", "SCROLL": "#763e00", "BUTTON": ("#000000", "#daad28"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"Green": {"BACKGROUND": "#82a459", "TEXT": "#000000", "INPUT": "#d8d584", "TEXT_INPUT": "#000000", "SCROLL": "#e3ecf3", "BUTTON": ("#FFFFFF", "#517239"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"BluePurple": {"BACKGROUND": "#A5CADD", "TEXT": "#6E266E", "INPUT": "#E0F5FF", "TEXT_INPUT": "#000000", "SCROLL": "#E0F5FF",
"BUTTON": ("#FFFFFF", "#303952"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"Purple": {"BACKGROUND": "#B0AAC2", "TEXT": "#000000", "INPUT": "#F2EFE8", "SCROLL": "#F2EFE8", "TEXT_INPUT": "#000000", "BUTTON": ("#000000", "#C2D4D8"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"BlueMono": {"BACKGROUND": "#AAB6D3", "TEXT": "#000000", "INPUT": "#F1F4FC", "SCROLL": "#F1F4FC", "TEXT_INPUT": "#000000", "BUTTON": ("#FFFFFF", "#7186C7"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"GreenMono": {"BACKGROUND": "#A8C1B4", "TEXT": "#000000", "INPUT": "#DDE0DE", "SCROLL": "#E3E3E3", "TEXT_INPUT": "#000000",
"BUTTON": ("#FFFFFF", "#6D9F85"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"BrownBlue": {"BACKGROUND": "#64778d", "TEXT": "#FFFFFF", "INPUT": "#f0f3f7", "SCROLL": "#A6B2BE", "TEXT_INPUT": "#000000",
"BUTTON": ("#FFFFFF", "#283b5b"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"BrightColors": {"BACKGROUND": "#b4ffb4", "TEXT": "#000000", "INPUT": "#ffff64", "SCROLL": "#ffb482", "TEXT_INPUT": "#000000",
"BUTTON": ("#000000", "#ffa0dc"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"NeutralBlue": {"BACKGROUND": "#92aa9d", "TEXT": "#000000", "INPUT": "#fcfff6", "SCROLL": "#fcfff6", "TEXT_INPUT": "#000000",
"BUTTON": ("#000000", "#d0dbbd"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"Kayak": {"BACKGROUND": "#a7ad7f", "TEXT": "#000000", "INPUT": "#e6d3a8", "SCROLL": "#e6d3a8", "TEXT_INPUT": "#000000", "BUTTON": ("#FFFFFF", "#5d907d"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"SandyBeach": {"BACKGROUND": "#efeccb", "TEXT": "#012f2f", "INPUT": "#e6d3a8", "SCROLL": "#e6d3a8", "TEXT_INPUT": "#012f2f",
"BUTTON": ("#FFFFFF", "#046380"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"TealMono": {"BACKGROUND": "#a8cfdd", "TEXT": "#000000", "INPUT": "#dfedf2", "SCROLL": "#dfedf2", "TEXT_INPUT": "#000000", "BUTTON": ("#FFFFFF", "#183440"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"Default": {"BACKGROUND": COLOR_SYSTEM_DEFAULT, "TEXT": COLOR_SYSTEM_DEFAULT, "INPUT": COLOR_SYSTEM_DEFAULT, "TEXT_INPUT": COLOR_SYSTEM_DEFAULT,
"SCROLL": COLOR_SYSTEM_DEFAULT, "BUTTON": OFFICIAL_PYSIMPLEGUI_BUTTON_COLOR, "PROGRESS": COLOR_SYSTEM_DEFAULT, "BORDER": 1, "SLIDER_DEPTH": 1,
"PROGRESS_DEPTH": 0, },
"Default1": {"BACKGROUND": COLOR_SYSTEM_DEFAULT, "TEXT": COLOR_SYSTEM_DEFAULT, "INPUT": COLOR_SYSTEM_DEFAULT, "TEXT_INPUT": COLOR_SYSTEM_DEFAULT,
"SCROLL": COLOR_SYSTEM_DEFAULT, "BUTTON": COLOR_SYSTEM_DEFAULT, "PROGRESS": COLOR_SYSTEM_DEFAULT, "BORDER": 1, "SLIDER_DEPTH": 1,
"PROGRESS_DEPTH": 0, },
"DefaultNoMoreNagging": {"BACKGROUND": COLOR_SYSTEM_DEFAULT, "TEXT": COLOR_SYSTEM_DEFAULT, "INPUT": COLOR_SYSTEM_DEFAULT,
"TEXT_INPUT": COLOR_SYSTEM_DEFAULT, "SCROLL": COLOR_SYSTEM_DEFAULT, "BUTTON": OFFICIAL_PYSIMPLEGUI_BUTTON_COLOR,
"PROGRESS": COLOR_SYSTEM_DEFAULT, "BORDER": 1, "SLIDER_DEPTH": 1, "PROGRESS_DEPTH": 0, },
"GrayGrayGray": {"BACKGROUND": COLOR_SYSTEM_DEFAULT, "TEXT": COLOR_SYSTEM_DEFAULT, "INPUT": COLOR_SYSTEM_DEFAULT, "TEXT_INPUT": COLOR_SYSTEM_DEFAULT,
"SCROLL": COLOR_SYSTEM_DEFAULT, "BUTTON": COLOR_SYSTEM_DEFAULT, "PROGRESS": COLOR_SYSTEM_DEFAULT, "BORDER": 1, "SLIDER_DEPTH": 1,
"PROGRESS_DEPTH": 0, },
"LightBlue": {"BACKGROUND": "#E3F2FD", "TEXT": "#000000", "INPUT": "#86A8FF", "TEXT_INPUT": "#000000", "SCROLL": "#86A8FF",
"BUTTON": ("#FFFFFF", "#5079D3"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 0, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"ACCENT1": "#FF0266", "ACCENT2": "#FF5C93", "ACCENT3": "#C5003C", },
"LightGrey": {"BACKGROUND": "#FAFAFA", "TEXT": "#000000", "INPUT": "#004EA1", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#5EA7FF",
"BUTTON": ("#FFFFFF", "#0079D3"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 0, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"ACCENT1": "#FF0266", "ACCENT2": "#FF5C93", "ACCENT3": "#C5003C", },
"LightGrey1": {"BACKGROUND": "#ffffff", "TEXT": "#1a1a1b", "INPUT": "#dae0e6", "TEXT_INPUT": "#222222", "SCROLL": "#a5a4a4",
"BUTTON": ("#FFFFFF", "#0079d3"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"ACCENT1": "#ff5414", "ACCENT2": "#33a8ff", "ACCENT3": "#dbf0ff", },
"DarkBrown": {"BACKGROUND": "#282923", "TEXT": "#E7DB74", "INPUT": "#393a32", "TEXT_INPUT": "#E7C855", "SCROLL": "#E7C855",
"BUTTON": ("#E7C855", "#284B5A"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"ACCENT1": "#c15226", "ACCENT2": "#7a4d5f", "ACCENT3": "#889743", },
"LightGreen1": {"BACKGROUND": "#9FB8AD", "TEXT": "#000000", "INPUT": "#F7F3EC", "TEXT_INPUT": "#000000", "SCROLL": "#F7F3EC",
"BUTTON": ("#FFFFFF", "#475841"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkGrey": {"BACKGROUND": "#404040", "TEXT": "#FFFFFF", "INPUT": "#4D4D4D", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#707070", "BUTTON": ("#FFFFFF", "#004F00"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"LightGreen2": {"BACKGROUND": "#B7CECE", "TEXT": "#000000", "INPUT": "#FDFFF7", "TEXT_INPUT": "#000000", "SCROLL": "#FDFFF7",
"BUTTON": ("#FFFFFF", "#658268"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "ACCENT1": "#76506d",
"ACCENT2": "#5148f1", "ACCENT3": "#0a1c84", "PROGRESS_DEPTH": 0, },
"DarkGrey1": {"BACKGROUND": "#404040", "TEXT": "#FFFFFF", "INPUT": "#FFFFFF", "TEXT_INPUT": "#000000", "SCROLL": "#707070",
"BUTTON": ("#FFFFFF", "#004F00"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkBlack": {"BACKGROUND": "#000000", "TEXT": "#FFFFFF", "INPUT": "#4D4D4D", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#707070",
"BUTTON": ("#000000", "#FFFFFF"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"LightBrown": {"BACKGROUND": "#fdf6e3", "TEXT": "#268bd1", "INPUT": "#eee8d5", "TEXT_INPUT": "#6c71c3", "SCROLL": "#eee8d5",
"BUTTON": ("#FFFFFF", "#063542"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"LightBrown1": {"BACKGROUND": "#e5dece", "TEXT": "#063289", "INPUT": "#f9f8f4", "TEXT_INPUT": "#242834", "SCROLL": "#eee8d5",
"BUTTON": ("#FFFFFF", "#063289"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkBlue1": {"BACKGROUND": "#242834", "TEXT": "#dfe6f8", "INPUT": "#97755c", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#a9afbb",
"BUTTON": ("#FFFFFF", "#063289"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkBrown1": {"BACKGROUND": "#2c2825", "TEXT": "#fdcb52", "INPUT": "#705e52", "TEXT_INPUT": "#fdcb52", "SCROLL": "#705e52",
"BUTTON": ("#000000", "#fdcb52"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkBlue2": {"BACKGROUND": "#1a2835", "TEXT": "#d1ecff", "INPUT": "#335267", "TEXT_INPUT": "#acc2d0", "SCROLL": "#1b6497",
"BUTTON": ("#000000", "#fafaf8"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkBrown2": {"BACKGROUND": "#280001", "TEXT": "#FFFFFF", "INPUT": "#d8d584", "TEXT_INPUT": "#000000", "SCROLL": "#763e00",
"BUTTON": ("#000000", "#daad28"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkGreen": {"BACKGROUND": "#82a459", "TEXT": "#000000", "INPUT": "#d8d584", "TEXT_INPUT": "#000000", "SCROLL": "#e3ecf3",
"BUTTON": ("#FFFFFF", "#517239"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"LightBlue1": {"BACKGROUND": "#A5CADD", "TEXT": "#6E266E", "INPUT": "#E0F5FF", "TEXT_INPUT": "#000000", "SCROLL": "#E0F5FF",
"BUTTON": ("#FFFFFF", "#303952"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"LightPurple": {"BACKGROUND": "#B0AAC2", "TEXT": "#000000", "INPUT": "#F2EFE8", "SCROLL": "#F2EFE8", "TEXT_INPUT": "#000000",
"BUTTON": ("#000000", "#C2D4D8"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"LightBlue2": {"BACKGROUND": "#AAB6D3", "TEXT": "#000000", "INPUT": "#F1F4FC", "SCROLL": "#F1F4FC", "TEXT_INPUT": "#000000",
"BUTTON": ("#FFFFFF", "#7186C7"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"LightGreen3": {"BACKGROUND": "#A8C1B4", "TEXT": "#000000", "INPUT": "#DDE0DE", "SCROLL": "#E3E3E3", "TEXT_INPUT": "#000000",
"BUTTON": ("#FFFFFF", "#6D9F85"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkBlue3": {"BACKGROUND": "#64778d", "TEXT": "#FFFFFF", "INPUT": "#f0f3f7", "SCROLL": "#A6B2BE", "TEXT_INPUT": "#000000",
"BUTTON": ("#FFFFFF", "#283b5b"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"LightGreen4": {"BACKGROUND": "#b4ffb4", "TEXT": "#000000", "INPUT": "#ffff64", "SCROLL": "#ffb482", "TEXT_INPUT": "#000000",
"BUTTON": ("#000000", "#ffa0dc"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"LightGreen5": {"BACKGROUND": "#92aa9d", "TEXT": "#000000", "INPUT": "#fcfff6", "SCROLL": "#fcfff6", "TEXT_INPUT": "#000000",
"BUTTON": ("#000000", "#d0dbbd"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"LightBrown2": {"BACKGROUND": "#a7ad7f", "TEXT": "#000000", "INPUT": "#e6d3a8", "SCROLL": "#e6d3a8", "TEXT_INPUT": "#000000",
"BUTTON": ("#FFFFFF", "#5d907d"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"LightBrown3": {"BACKGROUND": "#efeccb", "TEXT": "#012f2f", "INPUT": "#e6d3a8", "SCROLL": "#e6d3a8", "TEXT_INPUT": "#012f2f",
"BUTTON": ("#FFFFFF", "#046380"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"LightBlue3": {"BACKGROUND": "#a8cfdd", "TEXT": "#000000", "INPUT": "#dfedf2", "SCROLL": "#dfedf2", "TEXT_INPUT": "#000000",
"BUTTON": ("#FFFFFF", "#183440"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"LightBrown4": {"BACKGROUND": "#d7c79e", "TEXT": "#a35638", "INPUT": "#9dab86", "TEXT_INPUT": "#000000", "SCROLL": "#a35638",
"BUTTON": ("#FFFFFF", "#a35638"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#a35638", "#9dab86", "#e08f62", "#d7c79e"], },
"DarkTeal": {"BACKGROUND": "#003f5c", "TEXT": "#fb5b5a", "INPUT": "#bc4873", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#bc4873", "BUTTON": ("#FFFFFF", "#fb5b5a"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#003f5c", "#472b62", "#bc4873", "#fb5b5a"], },
"DarkPurple": {"BACKGROUND": "#472b62", "TEXT": "#fb5b5a", "INPUT": "#bc4873", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#bc4873",
"BUTTON": ("#FFFFFF", "#472b62"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#003f5c", "#472b62", "#bc4873", "#fb5b5a"], },
"LightGreen6": {"BACKGROUND": "#eafbea", "TEXT": "#1f6650", "INPUT": "#6f9a8d", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#1f6650",
"BUTTON": ("#FFFFFF", "#1f6650"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#1f6650", "#6f9a8d", "#ea5e5e", "#eafbea"], },
"DarkGrey2": {"BACKGROUND": "#2b2b28", "TEXT": "#f8f8f8", "INPUT": "#f1d6ab", "TEXT_INPUT": "#000000", "SCROLL": "#f1d6ab",
"BUTTON": ("#2b2b28", "#e3b04b"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#2b2b28", "#e3b04b", "#f1d6ab", "#f8f8f8"], },
"LightBrown6": {"BACKGROUND": "#f9b282", "TEXT": "#8f4426", "INPUT": "#de6b35", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#8f4426",
"BUTTON": ("#FFFFFF", "#8f4426"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#8f4426", "#de6b35", "#64ccda", "#f9b282"], },
"DarkTeal1": {"BACKGROUND": "#396362", "TEXT": "#ffe7d1", "INPUT": "#f6c89f", "TEXT_INPUT": "#000000", "SCROLL": "#f6c89f",
"BUTTON": ("#ffe7d1", "#4b8e8d"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#396362", "#4b8e8d", "#f6c89f", "#ffe7d1"], },
"LightBrown7": {"BACKGROUND": "#f6c89f", "TEXT": "#396362", "INPUT": "#4b8e8d", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#396362",
"BUTTON": ("#FFFFFF", "#396362"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#396362", "#4b8e8d", "#f6c89f", "#ffe7d1"], },
"DarkPurple1": {"BACKGROUND": "#0c093c", "TEXT": "#fad6d6", "INPUT": "#eea5f6", "TEXT_INPUT": "#000000", "SCROLL": "#eea5f6",
"BUTTON": ("#FFFFFF", "#df42d1"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#0c093c", "#df42d1", "#eea5f6", "#fad6d6"], },
"DarkGrey3": {"BACKGROUND": "#211717", "TEXT": "#dfddc7", "INPUT": "#f58b54", "TEXT_INPUT": "#000000", "SCROLL": "#f58b54",
"BUTTON": ("#dfddc7", "#a34a28"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#211717", "#a34a28", "#f58b54", "#dfddc7"], },
"LightBrown8": {"BACKGROUND": "#dfddc7", "TEXT": "#211717", "INPUT": "#a34a28", "TEXT_INPUT": "#dfddc7", "SCROLL": "#211717",
"BUTTON": ("#dfddc7", "#a34a28"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#211717", "#a34a28", "#f58b54", "#dfddc7"], },
"DarkBlue4": {"BACKGROUND": "#494ca2", "TEXT": "#e3e7f1", "INPUT": "#c6cbef", "TEXT_INPUT": "#000000", "SCROLL": "#c6cbef",
"BUTTON": ("#FFFFFF", "#8186d5"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#494ca2", "#8186d5", "#c6cbef", "#e3e7f1"], },
"LightBlue4": {"BACKGROUND": "#5c94bd", "TEXT": "#470938", "INPUT": "#1a3e59", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#470938",
"BUTTON": ("#FFFFFF", "#470938"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#470938", "#1a3e59", "#5c94bd", "#f2d6eb"], },
"DarkTeal2": {"BACKGROUND": "#394a6d", "TEXT": "#c0ffb3", "INPUT": "#52de97", "TEXT_INPUT": "#000000", "SCROLL": "#52de97",
"BUTTON": ("#c0ffb3", "#394a6d"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#394a6d", "#3c9d9b", "#52de97", "#c0ffb3"], },
"DarkTeal3": {"BACKGROUND": "#3c9d9b", "TEXT": "#c0ffb3", "INPUT": "#52de97", "TEXT_INPUT": "#000000", "SCROLL": "#52de97",
"BUTTON": ("#c0ffb3", "#394a6d"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#394a6d", "#3c9d9b", "#52de97", "#c0ffb3"], },
"DarkPurple5": {"BACKGROUND": "#730068", "TEXT": "#f6f078", "INPUT": "#01d28e", "TEXT_INPUT": "#000000", "SCROLL": "#01d28e",
"BUTTON": ("#f6f078", "#730068"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#730068", "#434982", "#01d28e", "#f6f078"], },
"DarkPurple2": {"BACKGROUND": "#202060", "TEXT": "#b030b0", "INPUT": "#602080", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#602080",
"BUTTON": ("#FFFFFF", "#202040"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#202040", "#202060", "#602080", "#b030b0"], },
"DarkBlue5": {"BACKGROUND": "#000272", "TEXT": "#ff6363", "INPUT": "#a32f80", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#a32f80",
"BUTTON": ("#FFFFFF", "#341677"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#000272", "#341677", "#a32f80", "#ff6363"], },
"LightGrey2": {"BACKGROUND": "#f6f6f6", "TEXT": "#420000", "INPUT": "#d4d7dd", "TEXT_INPUT": "#420000", "SCROLL": "#420000",
"BUTTON": ("#420000", "#d4d7dd"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#420000", "#d4d7dd", "#eae9e9", "#f6f6f6"], },
"LightGrey3": {"BACKGROUND": "#eae9e9", "TEXT": "#420000", "INPUT": "#d4d7dd", "TEXT_INPUT": "#420000", "SCROLL": "#420000",
"BUTTON": ("#420000", "#d4d7dd"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#420000", "#d4d7dd", "#eae9e9", "#f6f6f6"], },
"DarkBlue6": {"BACKGROUND": "#01024e", "TEXT": "#ff6464", "INPUT": "#8b4367", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#8b4367",
"BUTTON": ("#FFFFFF", "#543864"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#01024e", "#543864", "#8b4367", "#ff6464"], },
"DarkBlue7": {"BACKGROUND": "#241663", "TEXT": "#eae7af", "INPUT": "#a72693", "TEXT_INPUT": "#eae7af", "SCROLL": "#a72693",
"BUTTON": ("#eae7af", "#160f30"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#160f30", "#241663", "#a72693", "#eae7af"], },
"LightBrown9": {"BACKGROUND": "#f6d365", "TEXT": "#3a1f5d", "INPUT": "#c83660", "TEXT_INPUT": "#f6d365", "SCROLL": "#3a1f5d",
"BUTTON": ("#f6d365", "#c83660"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#3a1f5d", "#c83660", "#e15249", "#f6d365"], },
"DarkPurple3": {"BACKGROUND": "#6e2142", "TEXT": "#ffd692", "INPUT": "#e16363", "TEXT_INPUT": "#ffd692", "SCROLL": "#e16363",
"BUTTON": ("#ffd692", "#943855"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#6e2142", "#943855", "#e16363", "#ffd692"], },
"LightBrown10": {"BACKGROUND": "#ffd692", "TEXT": "#6e2142", "INPUT": "#943855", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#6e2142",
"BUTTON": ("#FFFFFF", "#6e2142"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#6e2142", "#943855", "#e16363", "#ffd692"], },
"DarkPurple4": {"BACKGROUND": "#200f21", "TEXT": "#f638dc", "INPUT": "#5a3d5c", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#5a3d5c",
"BUTTON": ("#FFFFFF", "#382039"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#200f21", "#382039", "#5a3d5c", "#f638dc"], },
"LightBlue5": {"BACKGROUND": "#b2fcff", "TEXT": "#3e64ff", "INPUT": "#5edfff", "TEXT_INPUT": "#000000", "SCROLL": "#3e64ff",
"BUTTON": ("#FFFFFF", "#3e64ff"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#3e64ff", "#5edfff", "#b2fcff", "#ecfcff"], },
"DarkTeal4": {"BACKGROUND": "#464159", "TEXT": "#c7f0db", "INPUT": "#8bbabb", "TEXT_INPUT": "#000000", "SCROLL": "#8bbabb",
"BUTTON": ("#FFFFFF", "#6c7b95"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#464159", "#6c7b95", "#8bbabb", "#c7f0db"], },
"LightTeal": {"BACKGROUND": "#c7f0db", "TEXT": "#464159", "INPUT": "#6c7b95", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#464159",
"BUTTON": ("#FFFFFF", "#464159"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#464159", "#6c7b95", "#8bbabb", "#c7f0db"], },
"DarkTeal5": {"BACKGROUND": "#8bbabb", "TEXT": "#464159", "INPUT": "#6c7b95", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#464159",
"BUTTON": ("#c7f0db", "#6c7b95"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#464159", "#6c7b95", "#8bbabb", "#c7f0db"], },
"LightGrey4": {"BACKGROUND": "#faf5ef", "TEXT": "#672f2f", "INPUT": "#99b19c", "TEXT_INPUT": "#672f2f", "SCROLL": "#672f2f",
"BUTTON": ("#672f2f", "#99b19c"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#672f2f", "#99b19c", "#d7d1c9", "#faf5ef"], },
"LightGreen7": {"BACKGROUND": "#99b19c", "TEXT": "#faf5ef", "INPUT": "#d7d1c9", "TEXT_INPUT": "#000000", "SCROLL": "#d7d1c9",
"BUTTON": ("#FFFFFF", "#99b19c"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#672f2f", "#99b19c", "#d7d1c9", "#faf5ef"], },
"LightGrey5": {"BACKGROUND": "#d7d1c9", "TEXT": "#672f2f", "INPUT": "#99b19c", "TEXT_INPUT": "#672f2f", "SCROLL": "#672f2f",
"BUTTON": ("#FFFFFF", "#672f2f"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#672f2f", "#99b19c", "#d7d1c9", "#faf5ef"], },
"DarkBrown3": {"BACKGROUND": "#a0855b", "TEXT": "#f9f6f2", "INPUT": "#f1d6ab", "TEXT_INPUT": "#000000", "SCROLL": "#f1d6ab",
"BUTTON": ("#FFFFFF", "#38470b"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#38470b", "#a0855b", "#f1d6ab", "#f9f6f2"], },
"LightBrown11": {"BACKGROUND": "#f1d6ab", "TEXT": "#38470b", "INPUT": "#a0855b", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#38470b",
"BUTTON": ("#f9f6f2", "#a0855b"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#38470b", "#a0855b", "#f1d6ab", "#f9f6f2"], },
"DarkRed": {"BACKGROUND": "#83142c", "TEXT": "#f9d276", "INPUT": "#ad1d45", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#ad1d45", "BUTTON": ("#f9d276", "#ad1d45"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#44000d", "#83142c", "#ad1d45", "#f9d276"], },
"DarkTeal6": {"BACKGROUND": "#204969", "TEXT": "#fff7f7", "INPUT": "#dadada", "TEXT_INPUT": "#000000", "SCROLL": "#dadada",
"BUTTON": ("#000000", "#fff7f7"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#204969", "#08ffc8", "#dadada", "#fff7f7"], },
"DarkBrown4": {"BACKGROUND": "#252525", "TEXT": "#ff0000", "INPUT": "#af0404", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#af0404",
"BUTTON": ("#FFFFFF", "#252525"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#252525", "#414141", "#af0404", "#ff0000"], },
"LightYellow": {"BACKGROUND": "#f4ff61", "TEXT": "#27aa80", "INPUT": "#32ff6a", "TEXT_INPUT": "#000000", "SCROLL": "#27aa80",
"BUTTON": ("#f4ff61", "#27aa80"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#27aa80", "#32ff6a", "#a8ff3e", "#f4ff61"], },
"DarkGreen1": {"BACKGROUND": "#2b580c", "TEXT": "#fdef96", "INPUT": "#f7b71d", "TEXT_INPUT": "#000000", "SCROLL": "#f7b71d",
"BUTTON": ("#fdef96", "#2b580c"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#2b580c", "#afa939", "#f7b71d", "#fdef96"], },
"LightGreen8": {"BACKGROUND": "#c8dad3", "TEXT": "#63707e", "INPUT": "#93b5b3", "TEXT_INPUT": "#000000", "SCROLL": "#63707e",
"BUTTON": ("#FFFFFF", "#63707e"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#63707e", "#93b5b3", "#c8dad3", "#f2f6f5"], },
"DarkTeal7": {"BACKGROUND": "#248ea9", "TEXT": "#fafdcb", "INPUT": "#aee7e8", "TEXT_INPUT": "#000000", "SCROLL": "#aee7e8",
"BUTTON": ("#000000", "#fafdcb"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#248ea9", "#28c3d4", "#aee7e8", "#fafdcb"], },
"DarkBlue8": {"BACKGROUND": "#454d66", "TEXT": "#d9d872", "INPUT": "#58b368", "TEXT_INPUT": "#000000", "SCROLL": "#58b368",
"BUTTON": ("#000000", "#009975"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#009975", "#454d66", "#58b368", "#d9d872"], },
"DarkBlue9": {"BACKGROUND": "#263859", "TEXT": "#ff6768", "INPUT": "#6b778d", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#6b778d",
"BUTTON": ("#ff6768", "#263859"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#17223b", "#263859", "#6b778d", "#ff6768"], },
"DarkBlue10": {"BACKGROUND": "#0028ff", "TEXT": "#f1f4df", "INPUT": "#10eaf0", "TEXT_INPUT": "#000000", "SCROLL": "#10eaf0",
"BUTTON": ("#f1f4df", "#24009c"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#24009c", "#0028ff", "#10eaf0", "#f1f4df"], },
"DarkBlue11": {"BACKGROUND": "#6384b3", "TEXT": "#e6f0b6", "INPUT": "#b8e9c0", "TEXT_INPUT": "#000000", "SCROLL": "#b8e9c0",
"BUTTON": ("#e6f0b6", "#684949"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#684949", "#6384b3", "#b8e9c0", "#e6f0b6"], },
"DarkTeal8": {"BACKGROUND": "#71a0a5", "TEXT": "#212121", "INPUT": "#665c84", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#212121",
"BUTTON": ("#fab95b", "#665c84"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#212121", "#665c84", "#71a0a5", "#fab95b"], },
"DarkRed1": {"BACKGROUND": "#c10000", "TEXT": "#eeeeee", "INPUT": "#dedede", "TEXT_INPUT": "#000000", "SCROLL": "#dedede", "BUTTON": ("#c10000", "#eeeeee"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#c10000", "#ff4949", "#dedede", "#eeeeee"], },
"LightBrown5": {"BACKGROUND": "#fff591", "TEXT": "#e41749", "INPUT": "#f5587b", "TEXT_INPUT": "#000000", "SCROLL": "#e41749",
"BUTTON": ("#fff591", "#e41749"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#e41749", "#f5587b", "#ff8a5c", "#fff591"], },
"LightGreen9": {"BACKGROUND": "#f1edb3", "TEXT": "#3b503d", "INPUT": "#4a746e", "TEXT_INPUT": "#f1edb3", "SCROLL": "#3b503d",
"BUTTON": ("#f1edb3", "#3b503d"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#3b503d", "#4a746e", "#c8cf94", "#f1edb3"], "DESCRIPTION": ["Green", "Turquoise", "Yellow"], },
"DarkGreen2": {"BACKGROUND": "#3b503d", "TEXT": "#f1edb3", "INPUT": "#c8cf94", "TEXT_INPUT": "#000000", "SCROLL": "#c8cf94",
"BUTTON": ("#f1edb3", "#3b503d"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#3b503d", "#4a746e", "#c8cf94", "#f1edb3"], "DESCRIPTION": ["Green", "Turquoise", "Yellow"], },
"LightGray1": {"BACKGROUND": "#f2f2f2", "TEXT": "#222831", "INPUT": "#393e46", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#222831",
"BUTTON": ("#f2f2f2", "#222831"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#222831", "#393e46", "#f96d00", "#f2f2f2"], "DESCRIPTION": ["#000000", "Grey", "Orange", "Grey", "Autumn"], },
"DarkGrey4": {"BACKGROUND": "#52524e", "TEXT": "#e9e9e5", "INPUT": "#d4d6c8", "TEXT_INPUT": "#000000", "SCROLL": "#d4d6c8",
"BUTTON": ("#FFFFFF", "#9a9b94"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#52524e", "#9a9b94", "#d4d6c8", "#e9e9e5"], "DESCRIPTION": ["Grey", "Pastel", "Winter"], },
"DarkBlue12": {"BACKGROUND": "#324e7b", "TEXT": "#f8f8f8", "INPUT": "#86a6df", "TEXT_INPUT": "#000000", "SCROLL": "#86a6df",
"BUTTON": ("#FFFFFF", "#5068a9"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#324e7b", "#5068a9", "#86a6df", "#f8f8f8"], "DESCRIPTION": ["Blue", "Grey", "Cold", "Winter"], },
"DarkPurple6": {"BACKGROUND": "#070739", "TEXT": "#e1e099", "INPUT": "#c327ab", "TEXT_INPUT": "#e1e099", "SCROLL": "#c327ab",
"BUTTON": ("#e1e099", "#521477"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#070739", "#521477", "#c327ab", "#e1e099"], "DESCRIPTION": ["#000000", "Purple", "Yellow", "Dark"], },
"DarkPurple7": {"BACKGROUND": "#191930", "TEXT": "#B1B7C5", "INPUT": "#232B5C", "TEXT_INPUT": "#D0E3E7", "SCROLL": "#B1B7C5",
"BUTTON": ("#272D38", "#B1B7C5"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkBlue13": {"BACKGROUND": "#203562", "TEXT": "#e3e8f8", "INPUT": "#c0c5cd", "TEXT_INPUT": "#000000", "SCROLL": "#c0c5cd",
"BUTTON": ("#FFFFFF", "#3e588f"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#203562", "#3e588f", "#c0c5cd", "#e3e8f8"], "DESCRIPTION": ["Blue", "Grey", "Wedding", "Cold"], },
"DarkBrown5": {"BACKGROUND": "#3c1b1f", "TEXT": "#f6e1b5", "INPUT": "#e2bf81", "TEXT_INPUT": "#000000", "SCROLL": "#e2bf81",
"BUTTON": ("#3c1b1f", "#f6e1b5"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#3c1b1f", "#b21e4b", "#e2bf81", "#f6e1b5"], "DESCRIPTION": ["Brown", "Red", "Yellow", "Warm"], },
"DarkGreen3": {"BACKGROUND": "#062121", "TEXT": "#eeeeee", "INPUT": "#e4dcad", "TEXT_INPUT": "#000000", "SCROLL": "#e4dcad",
"BUTTON": ("#eeeeee", "#181810"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#062121", "#181810", "#e4dcad", "#eeeeee"], "DESCRIPTION": ["#000000", "#000000", "Brown", "Grey"], },
"DarkBlack1": {"BACKGROUND": "#181810", "TEXT": "#eeeeee", "INPUT": "#e4dcad", "TEXT_INPUT": "#000000", "SCROLL": "#e4dcad",
"BUTTON": ("#FFFFFF", "#062121"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#062121", "#181810", "#e4dcad", "#eeeeee"], "DESCRIPTION": ["#000000", "#000000", "Brown", "Grey"], },
"DarkGrey5": {"BACKGROUND": "#343434", "TEXT": "#f3f3f3", "INPUT": "#e9dcbe", "TEXT_INPUT": "#000000", "SCROLL": "#e9dcbe",
"BUTTON": ("#FFFFFF", "#8e8b82"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#343434", "#8e8b82", "#e9dcbe", "#f3f3f3"], "DESCRIPTION": ["Grey", "Brown"], },
"LightBrown12": {"BACKGROUND": "#8e8b82", "TEXT": "#f3f3f3", "INPUT": "#e9dcbe", "TEXT_INPUT": "#000000", "SCROLL": "#e9dcbe",
"BUTTON": ("#f3f3f3", "#8e8b82"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#343434", "#8e8b82", "#e9dcbe", "#f3f3f3"], "DESCRIPTION": ["Grey", "Brown"], },
"DarkTeal9": {"BACKGROUND": "#13445a", "TEXT": "#fef4e8", "INPUT": "#446878", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#446878",
"BUTTON": ("#fef4e8", "#446878"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#13445a", "#970747", "#446878", "#fef4e8"], "DESCRIPTION": ["Red", "Grey", "Blue", "Wedding", "Retro"], },
"DarkBlue14": {"BACKGROUND": "#21273d", "TEXT": "#f1f6f8", "INPUT": "#b9d4f1", "TEXT_INPUT": "#000000", "SCROLL": "#b9d4f1",
"BUTTON": ("#FFFFFF", "#6a759b"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#21273d", "#6a759b", "#b9d4f1", "#f1f6f8"], "DESCRIPTION": ["Blue", "#000000", "Grey", "Cold", "Winter"], },
"LightBlue6": {"BACKGROUND": "#f1f6f8", "TEXT": "#21273d", "INPUT": "#6a759b", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#21273d",
"BUTTON": ("#f1f6f8", "#6a759b"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#21273d", "#6a759b", "#b9d4f1", "#f1f6f8"], "DESCRIPTION": ["Blue", "#000000", "Grey", "Cold", "Winter"], },
"DarkGreen4": {"BACKGROUND": "#044343", "TEXT": "#e4e4e4", "INPUT": "#045757", "TEXT_INPUT": "#e4e4e4", "SCROLL": "#045757",
"BUTTON": ("#e4e4e4", "#045757"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#222222", "#044343", "#045757", "#e4e4e4"], "DESCRIPTION": ["#000000", "Turquoise", "Grey", "Dark"], },
"DarkGreen5": {"BACKGROUND": "#1b4b36", "TEXT": "#e0e7f1", "INPUT": "#aebd77", "TEXT_INPUT": "#000000", "SCROLL": "#aebd77",
"BUTTON": ("#FFFFFF", "#538f6a"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#1b4b36", "#538f6a", "#aebd77", "#e0e7f1"], "DESCRIPTION": ["Green", "Grey"], },
"DarkTeal10": {"BACKGROUND": "#0d3446", "TEXT": "#d8dfe2", "INPUT": "#71adb5", "TEXT_INPUT": "#000000", "SCROLL": "#71adb5",
"BUTTON": ("#FFFFFF", "#176d81"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#0d3446", "#176d81", "#71adb5", "#d8dfe2"], "DESCRIPTION": ["Grey", "Turquoise", "Winter", "Cold"], },
"DarkGrey6": {"BACKGROUND": "#3e3e3e", "TEXT": "#ededed", "INPUT": "#68868c", "TEXT_INPUT": "#ededed", "SCROLL": "#68868c",
"BUTTON": ("#FFFFFF", "#405559"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#3e3e3e", "#405559", "#68868c", "#ededed"], "DESCRIPTION": ["Grey", "Turquoise", "Winter"], },
"DarkTeal11": {"BACKGROUND": "#405559", "TEXT": "#ededed", "INPUT": "#68868c", "TEXT_INPUT": "#ededed", "SCROLL": "#68868c",
"BUTTON": ("#ededed", "#68868c"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#3e3e3e", "#405559", "#68868c", "#ededed"], "DESCRIPTION": ["Grey", "Turquoise", "Winter"], },
"LightBlue7": {"BACKGROUND": "#9ed0e0", "TEXT": "#19483f", "INPUT": "#5c868e", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#19483f",
"BUTTON": ("#FFFFFF", "#19483f"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#19483f", "#5c868e", "#ff6a38", "#9ed0e0"], "DESCRIPTION": ["Orange", "Blue", "Turquoise"], },
"LightGreen10": {"BACKGROUND": "#d8ebb5", "TEXT": "#205d67", "INPUT": "#639a67", "TEXT_INPUT": "#FFFFFF", "SCROLL": "#205d67",
"BUTTON": ("#d8ebb5", "#205d67"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#205d67", "#639a67", "#d9bf77", "#d8ebb5"], "DESCRIPTION": ["Blue", "Green", "Brown", "Vintage"], },
"DarkBlue15": {"BACKGROUND": "#151680", "TEXT": "#f1fea4", "INPUT": "#375fc0", "TEXT_INPUT": "#f1fea4", "SCROLL": "#375fc0",
"BUTTON": ("#f1fea4", "#1c44ac"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#151680", "#1c44ac", "#375fc0", "#f1fea4"], "DESCRIPTION": ["Blue", "Yellow", "Cold"], },
"DarkBlue16": {"BACKGROUND": "#1c44ac", "TEXT": "#f1fea4", "INPUT": "#375fc0", "TEXT_INPUT": "#f1fea4", "SCROLL": "#375fc0",
"BUTTON": ("#f1fea4", "#151680"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#151680", "#1c44ac", "#375fc0", "#f1fea4"], "DESCRIPTION": ["Blue", "Yellow", "Cold"], },
"DarkTeal12": {"BACKGROUND": "#004a7c", "TEXT": "#fafafa", "INPUT": "#e8f1f5", "TEXT_INPUT": "#000000", "SCROLL": "#e8f1f5",
"BUTTON": ("#fafafa", "#005691"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#004a7c", "#005691", "#e8f1f5", "#fafafa"], "DESCRIPTION": ["Grey", "Blue", "Cold", "Winter"], },
"LightBrown13": {"BACKGROUND": "#ebf5ee", "TEXT": "#921224", "INPUT": "#bdc6b8", "TEXT_INPUT": "#921224", "SCROLL": "#921224",
"BUTTON": ("#FFFFFF", "#921224"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#921224", "#bdc6b8", "#bce0da", "#ebf5ee"], "DESCRIPTION": ["Red", "Blue", "Grey", "Vintage", "Wedding"], },
"DarkBlue17": {"BACKGROUND": "#21294c", "TEXT": "#f9f2d7", "INPUT": "#f2dea8", "TEXT_INPUT": "#000000", "SCROLL": "#f2dea8",
"BUTTON": ("#f9f2d7", "#141829"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#141829", "#21294c", "#f2dea8", "#f9f2d7"], "DESCRIPTION": ["#000000", "Blue", "Yellow"], },
"DarkBrown6": {"BACKGROUND": "#785e4d", "TEXT": "#f2eee3", "INPUT": "#baaf92", "TEXT_INPUT": "#000000", "SCROLL": "#baaf92",
"BUTTON": ("#FFFFFF", "#785e4d"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#785e4d", "#ff8426", "#baaf92", "#f2eee3"], "DESCRIPTION": ["Grey", "Brown", "Orange", "Autumn"], },
"DarkGreen6": {"BACKGROUND": "#5c715e", "TEXT": "#f2f9f1", "INPUT": "#ddeedf", "TEXT_INPUT": "#000000", "SCROLL": "#ddeedf",
"BUTTON": ("#f2f9f1", "#5c715e"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#5c715e", "#b6cdbd", "#ddeedf", "#f2f9f1"], "DESCRIPTION": ["Grey", "Green", "Vintage"], },
"DarkGreen7": {"BACKGROUND": "#0C231E", "TEXT": "#efbe1c", "INPUT": "#153C33", "TEXT_INPUT": "#efbe1c", "SCROLL": "#153C33",
"BUTTON": ("#efbe1c", "#153C33"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkGrey7": {"BACKGROUND": "#4b586e", "TEXT": "#dddddd", "INPUT": "#574e6d", "TEXT_INPUT": "#dddddd", "SCROLL": "#574e6d",
"BUTTON": ("#dddddd", "#43405d"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#43405d", "#4b586e", "#574e6d", "#dddddd"], "DESCRIPTION": ["Grey", "Winter", "Cold"], },
"DarkRed2": {"BACKGROUND": "#ab1212", "TEXT": "#f6e4b5", "INPUT": "#cd3131", "TEXT_INPUT": "#f6e4b5", "SCROLL": "#cd3131", "BUTTON": ("#f6e4b5", "#ab1212"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#ab1212", "#1fad9f", "#cd3131", "#f6e4b5"], "DESCRIPTION": ["Turquoise", "Red", "Yellow"], },
"LightGrey6": {"BACKGROUND": "#e3e3e3", "TEXT": "#233142", "INPUT": "#455d7a", "TEXT_INPUT": "#e3e3e3", "SCROLL": "#233142",
"BUTTON": ("#e3e3e3", "#455d7a"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0,
"COLOR_LIST": ["#233142", "#455d7a", "#f95959", "#e3e3e3"], "DESCRIPTION": ["#000000", "Blue", "Red", "Grey"], },
"HotDogStand": {"BACKGROUND": "red", "TEXT": "yellow", "INPUT": "yellow", "TEXT_INPUT": "#000000", "SCROLL": "yellow", "BUTTON": ("red", "yellow"),
"PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkGrey8": {"BACKGROUND": "#19232D", "TEXT": "#ffffff", "INPUT": "#32414B", "TEXT_INPUT": "#ffffff", "SCROLL": "#505F69",
"BUTTON": ("#ffffff", "#32414B"), "PROGRESS": ("#505F69", "#32414B"), "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkGrey9": {"BACKGROUND": "#36393F", "TEXT": "#DCDDDE", "INPUT": "#40444B", "TEXT_INPUT": "#ffffff", "SCROLL": "#202225",
"BUTTON": ("#202225", "#B9BBBE"), "PROGRESS": ("#202225", "#40444B"), "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkGrey10": {"BACKGROUND": "#1c1e23", "TEXT": "#cccdcf", "INPUT": "#272a31", "TEXT_INPUT": "#8b9fde", "SCROLL": "#313641",
"BUTTON": ("#f5f5f6", "#2e3d5a"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkGrey11": {"BACKGROUND": "#1c1e23", "TEXT": "#cccdcf", "INPUT": "#313641", "TEXT_INPUT": "#cccdcf", "SCROLL": "#313641",
"BUTTON": ("#f5f5f6", "#313641"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkGrey12": {"BACKGROUND": "#1c1e23", "TEXT": "#8b9fde", "INPUT": "#313641", "TEXT_INPUT": "#8b9fde", "SCROLL": "#313641",
"BUTTON": ("#cccdcf", "#2e3d5a"), "PROGRESS": DEFAULT_PROGRESS_BAR_COMPUTE, "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkGrey13": {"BACKGROUND": "#1c1e23", "TEXT": "#cccdcf", "INPUT": "#272a31", "TEXT_INPUT": "#cccdcf", "SCROLL": "#313641",
"BUTTON": ("#8b9fde", "#313641"), "PROGRESS": ("#cccdcf", "#272a31"), "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkGrey14": {"BACKGROUND": "#24292e", "TEXT": "#fafbfc", "INPUT": "#1d2125", "TEXT_INPUT": "#fafbfc", "SCROLL": "#1d2125",
"BUTTON": ("#fafbfc", "#155398"), "PROGRESS": ("#155398", "#1d2125"), "BORDER": 1, "SLIDER_DEPTH": 0, "PROGRESS_DEPTH": 0, },
"DarkBrown7": {"BACKGROUND": "#2c2417", "TEXT": "#baa379", "INPUT": "#baa379", "TEXT_INPUT": "#000000", "SCROLL": "#392e1c",
"BUTTON": ("#000000", "#baa379"), "PROGRESS": ("#baa379", "#453923"), "BORDER": 1, "SLIDER_DEPTH": 1, "PROGRESS_DEPTH": 0, },
"Python": {"BACKGROUND": "#3d7aab", "TEXT": "#ffde56", "INPUT": "#295273", "TEXT_INPUT": "#ffde56", "SCROLL": "#295273", "BUTTON": ("#ffde56", "#295273"),
"PROGRESS": ("#ffde56", "#295273"), "BORDER": 1, "SLIDER_DEPTH": 1, "PROGRESS_DEPTH": 0, },
}
def list_of_look_and_feel_values():
"""
Get a list of the valid values to pass into your call to change_look_and_feel
:return: list of valid string values
:rtype: List[str]
"""
return sorted(list(LOOK_AND_FEEL_TABLE.keys()))
def theme(new_theme=None):
"""
Sets / Gets the current Theme. If none is specified then returns the current theme.
This call replaces the ChangeLookAndFeel / change_look_and_feel call which only sets the theme.
:param new_theme: the new theme name to use
:type new_theme: (str)
:return: the currently selected theme
:rtype: (str)
"""
global TRANSPARENT_BUTTON
if new_theme is not None:
change_look_and_feel(new_theme)
TRANSPARENT_BUTTON = (theme_background_color(), theme_background_color())
return CURRENT_LOOK_AND_FEEL
def theme_background_color(color=None):
"""
Sets/Returns the background color currently in use
Used for Windows and containers (Column, Frame, Tab) and tables
:param color: new background color to use (optional)
:type color: (str)
:return: color string of the background color currently in use
:rtype: (str)
"""
if color is not None:
set_options(background_color=color)
return DEFAULT_BACKGROUND_COLOR
# This "constant" is misleading but rather than remove and break programs, will try this method instead
TRANSPARENT_BUTTON = (theme_background_color(), theme_background_color()) # replaces an older version that had hardcoded numbers
def theme_element_background_color(color=None):
"""
Sets/Returns the background color currently in use for all elements except containers
:return: (str) - color string of the element background color currently in use
:rtype: (str)
"""
if color is not None:
set_options(element_background_color=color)
return DEFAULT_ELEMENT_BACKGROUND_COLOR
def theme_text_color(color=None):
"""
Sets/Returns the text color currently in use
:return: (str) - color string of the text color currently in use
:rtype: (str)
"""
if color is not None:
set_options(text_color=color)
return DEFAULT_TEXT_COLOR
def theme_text_element_background_color(color=None):
"""
Sets/Returns the background color for text elements
:return: (str) - color string of the text background color currently in use
:rtype: (str)
"""
if color is not None:
set_options(text_element_background_color=color)
return DEFAULT_TEXT_ELEMENT_BACKGROUND_COLOR
def theme_input_background_color(color=None):
"""
Sets/Returns the input element background color currently in use
:return: (str) - color string of the input element background color currently in use
:rtype: (str)
"""
if color is not None:
set_options(input_elements_background_color=color)
return DEFAULT_INPUT_ELEMENTS_COLOR
def theme_input_text_color(color=None):
"""
Sets/Returns the input element entry color (not the text but the thing that's displaying the text)
:return: (str) - color string of the input element color currently in use
:rtype: (str)
"""
if color is not None:
set_options(input_text_color=color)
return DEFAULT_INPUT_TEXT_COLOR
def theme_button_color(color=None):
"""
Sets/Returns the button color currently in use
:return: (str, str) - TUPLE with color strings of the button color currently in use (button text color, button background color)
:rtype: (str, str)
"""
if color is not None:
if color == COLOR_SYSTEM_DEFAULT:
color_tuple = (COLOR_SYSTEM_DEFAULT, COLOR_SYSTEM_DEFAULT)
else:
color_tuple = button_color_to_tuple(color, (None, None))
if color_tuple == (None, None):
if not SUPPRESS_ERROR_POPUPS:
popup_error('theme_button_color - bad color string passed in', color)
else:
print('** Badly formatted button color... not a tuple nor string **', color)
set_options(button_color=color) # go ahead and try with their string
else:
set_options(button_color=color_tuple)
return DEFAULT_BUTTON_COLOR
def theme_progress_bar_color(color=None):
"""
Sets/Returns the progress bar colors by the current color theme
:return: (str, str) - TUPLE with color strings of the ProgressBar color currently in use(button text color, button background color)
:rtype: (str, str)
"""
if color is not None:
set_options(progress_meter_color=color)
return DEFAULT_PROGRESS_BAR_COLOR
def theme_slider_color(color=None):
"""
Sets/Returns the slider color (used for sliders)
:return: color string of the slider color currently in use
:rtype: (str)
"""
if color is not None:
set_options(scrollbar_color=color)
return DEFAULT_SCROLLBAR_COLOR
def theme_border_width(border_width=None):
"""
Sets/Returns the border width currently in use
Used by non ttk elements at the moment
:return: border width currently in use
:rtype: (int)
"""
if border_width is not None:
set_options(border_width=border_width)
return DEFAULT_BORDER_WIDTH
def theme_slider_border_width(border_width=None):
"""
Sets/Returns the slider border width currently in use
:return: border width currently in use for sliders
:rtype: (int)
"""
if border_width is not None:
set_options(slider_border_width=border_width)
return DEFAULT_SLIDER_BORDER_WIDTH
def theme_progress_bar_border_width(border_width=None):
"""
Sets/Returns the progress meter border width currently in use
:return: border width currently in use for progress meters
:rtype: (int)
"""
if border_width is not None:
set_options(progress_meter_border_depth=border_width)
return DEFAULT_PROGRESS_BAR_BORDER_WIDTH
def theme_element_text_color(color=None):
"""
Sets/Returns the text color used by elements that have text as part of their display (Tables, Trees and Sliders)
:return: color string currently in use
:rtype: (str)
"""
if color is not None:
set_options(element_text_color=color)
return DEFAULT_ELEMENT_TEXT_COLOR
def theme_list():
"""
Returns a sorted list of the currently available color themes
:return: A sorted list of the currently available color themes
:rtype: List[str]
"""
return list_of_look_and_feel_values()
def theme_add_new(new_theme_name, new_theme_dict):
"""
Add a new theme to the dictionary of themes
:param new_theme_name: text to display in element
:type new_theme_name: (str)
:param new_theme_dict: text to display in element
:type new_theme_dict: (dict)
"""
global LOOK_AND_FEEL_TABLE
try:
LOOK_AND_FEEL_TABLE[new_theme_name] = new_theme_dict
except Exception as e:
print('Exception during adding new theme {}'.format(e))
def theme_global(new_theme=None):
"""
Sets / Gets the global PySimpleGUI Theme. If none is specified then returns the global theme from user settings
:param new_theme: the new theme name to use
:type new_theme: (str)
:return: the currently selected theme
:rtype: (str)
"""
if new_theme is not None:
pysimplegui_user_settings.set('-theme-', new_theme)
theme(new_theme)
return new_theme
else:
return pysimplegui_user_settings.get('-theme-', CURRENT_LOOK_AND_FEEL)
def theme_previewer(columns=12, scrollable=False, scroll_area_size=(None, None), search_string=None, location=(None, None)):
"""
Displays a "Quick Reference Window" showing all of the different Look and Feel settings that are available.
They are sorted alphabetically. The legacy color names are mixed in, but otherwise they are sorted into Dark and Light halves
:param columns: The number of themes to display per row
:type columns: int
:param scrollable: If True then scrollbars will be added
:type scrollable: bool
:param scroll_area_size: Size of the scrollable area (The Column Element used to make scrollable)
:type scroll_area_size: (int, int)
:param search_string: If specified then only themes containing this string will be shown
:type search_string: str
:param location: Location on the screen to place the window. Defaults to the center like all windows
:type location: (int, int)
"""
current_theme = theme()
# Show a "splash" type message so the user doesn't give up waiting
popup_quick_message('Hang on for a moment, this will take a bit to create....', keep_on_top=True, background_color='red', text_color='#FFFFFF',
auto_close=True, non_blocking=True)
web = False
win_bg = 'black'
def sample_layout():
return [[Text('Text element'), InputText('Input data here', size=(10, 1))],
[Button('Ok'), Button('Disabled', disabled=True), Slider((1, 10), orientation='h', size=(5, 15))]]
names = list_of_look_and_feel_values()
names.sort()
if search_string not in (None, ''):
names = [name for name in names if search_string.lower().replace(" ", "") in name.lower().replace(" ", "")]
if search_string not in (None, ''):
layout = [[Text('Themes containing "{}"'.format(search_string), font='Default 18', background_color=win_bg)]]
else:
layout = [[Text('List of all themes', font='Default 18', background_color=win_bg)]]
col_layout = []
row = []
for count, theme_name in enumerate(names):
theme(theme_name)
if not count % columns:
col_layout += [row]
row = []
row += [Frame(theme_name, sample_layout() if not web else [[T(theme_name)]] + sample_layout(), pad=(2, 2))]
if row:
col_layout += [row]
layout += [[Column(col_layout, scrollable=scrollable, size=scroll_area_size, pad=(0, 0), background_color=win_bg, key='-COL-')]]
window = Window('Preview of Themes', layout, background_color=win_bg, resizable=True, location=location, keep_on_top=True, finalize=True, modal=True)
window['-COL-'].expand(True, True, True) # needed so that col will expand with the window
window.read(close=True)
theme(current_theme)
preview_all_look_and_feel_themes = theme_previewer
def _theme_preview_window_swatches():
# Begin the layout with a header
layout = [[Text('Themes as color swatches', text_color='white', background_color='black', font='Default 25')],
[Text('Tooltip and right click a color to get the value', text_color='white', background_color='black', font='Default 15')],
[Text('Left click a color to copy to clipboard', text_color='white', background_color='black', font='Default 15')]]
layout = [[Column(layout, element_justification='c', background_color='black')]]
# Create the pain part, the rows of Text with color swatches
for i, theme_name in enumerate(theme_list()):
theme(theme_name)
colors = [theme_background_color(), theme_text_color(), theme_input_background_color(),
theme_input_text_color()]
if theme_button_color() != COLOR_SYSTEM_DEFAULT:
colors.append(theme_button_color()[0])
colors.append(theme_button_color()[1])
colors = list(set(colors)) # de-duplicate items
row = [T(theme(), background_color='black', text_color='white', size=(20, 1), justification='r')]
for color in colors:
if color != COLOR_SYSTEM_DEFAULT:
row.append(T(SYMBOL_SQUARE, text_color=color, background_color='black', pad=(0, 0), font='DEFAUlT 20', right_click_menu=['Nothing', [color]],
tooltip=color, enable_events=True, key=(i, color)))
layout += [row]
# place layout inside of a Column so that it's scrollable
layout = [[Column(layout, size=(500, 900), scrollable=True, vertical_scroll_only=True, background_color='black')]]
# finish the layout by adding an exit button
layout += [[B('Exit')]]
# create and return Window that uses the layout
return Window('Theme Color Swatches', layout, background_color='black', finalize=True, keep_on_top=True)
def theme_previewer_swatches():
"""
Display themes in a window as color swatches.
Click on a color swatch to see the hex value printed on the console.
If you hover over a color or right click it you'll also see the hext value.
"""
current_theme = theme()
popup_quick_message('This is going to take a minute...', text_color='white', background_color='red', font='Default 20', keep_on_top=True)
window = _theme_preview_window_swatches()
theme(OFFICIAL_PYSIMPLEGUI_THEME)
# col_height = window.get_screen_size()[1]-200
# if window.size[1] > 100:
# window.size = (window.size[0], col_height)
# window.move(window.get_screen_size()[0] // 2 - window.size[0] // 2, 0)
while True: # Event Loop
event, values = window.read()
if event == WIN_CLOSED or event == 'Exit':
break
if isinstance(event, tuple): # someone clicked a swatch
chosen_color = event[1]
else:
if event[0] == '#': # someone right clicked
chosen_color = event
else:
chosen_color = ''
print('Copied to clipboard color = ', chosen_color)
clipboard_set(chosen_color)
# window.TKroot.clipboard_clear()
# window.TKroot.clipboard_append(chosen_color)
window.close()
theme(current_theme)
def change_look_and_feel(index, force=False):
"""
Change the "color scheme" of all future PySimpleGUI Windows.
The scheme are string names that specify a group of colors. Background colors, text colors, button colors.
There are 13 different color settings that are changed at one time using a single call to ChangeLookAndFeel
The look and feel table itself has these indexes into the dictionary LOOK_AND_FEEL_TABLE.
The original list was (prior to a major rework and renaming)... these names still work...
In Nov 2019 a new Theme Formula was devised to make choosing a theme easier:
The "Formula" is:
["Dark" or "Light"] Color Number
Colors can be Blue Brown Grey Green Purple Red Teal Yellow Black
The number will vary for each pair. There are more DarkGrey entries than there are LightYellow for example.
Default = The default settings (only button color is different than system default)
Default1 = The full system default including the button (everything's gray... how sad... don't be all gray... please....)
:param index: the name of the index into the Look and Feel table (does not have to be exact, can be "fuzzy")
:type index: (str)
:param force: no longer used
:type force: (bool)
:return: None
:rtype: None
"""
global CURRENT_LOOK_AND_FEEL
# if running_mac() and not force:
# print('*** Changing look and feel is not supported on Mac platform ***')
# return
requested_theme_name = index
theme_names_list = list_of_look_and_feel_values()
# normalize available l&f values by setting all to lower case
lf_values_lowercase = [item.lower() for item in theme_names_list]
# option 1
opt1 = requested_theme_name.replace(' ', '').lower()
# option 3 is option 1 with gray replaced with grey
opt3 = opt1.replace('gray', 'grey')
# option 2 (reverse lookup)
optx = requested_theme_name.lower().split(' ')
optx.reverse()
opt2 = ''.join(optx)
# search for valid l&f name
if requested_theme_name in theme_names_list:
ix = theme_names_list.index(requested_theme_name)
elif opt1 in lf_values_lowercase:
ix = lf_values_lowercase.index(opt1)
elif opt2 in lf_values_lowercase:
ix = lf_values_lowercase.index(opt2)
elif opt3 in lf_values_lowercase:
ix = lf_values_lowercase.index(opt3)
else:
ix = random.randint(0, len(lf_values_lowercase) - 1)
print('** Warning - {} Theme is not a valid theme. Change your theme call. **'.format(index))
print('valid values are', list_of_look_and_feel_values())
print('Instead, please enjoy a random Theme named {}'.format(list_of_look_and_feel_values()[ix]))
selection = theme_names_list[ix]
CURRENT_LOOK_AND_FEEL = selection
try:
colors = LOOK_AND_FEEL_TABLE[selection]
# Color the progress bar using button background and input colors...unless they're the same
if colors['PROGRESS'] != COLOR_SYSTEM_DEFAULT:
if colors['PROGRESS'] == DEFAULT_PROGRESS_BAR_COMPUTE:
if colors['BUTTON'][1] != colors['INPUT'] and colors['BUTTON'][1] != colors['BACKGROUND']:
colors['PROGRESS'] = colors['BUTTON'][1], colors['INPUT']
else: # if the same, then use text input on top of input color
colors['PROGRESS'] = (colors['TEXT_INPUT'], colors['INPUT'])
else:
colors['PROGRESS'] = DEFAULT_PROGRESS_BAR_COLOR_OFFICIAL
# call to change all the colors
SetOptions(background_color=colors['BACKGROUND'],
text_element_background_color=colors['BACKGROUND'],
element_background_color=colors['BACKGROUND'],
text_color=colors['TEXT'],
input_elements_background_color=colors['INPUT'],
# button_color=colors['BUTTON'] if not running_mac() else None,
button_color=colors['BUTTON'],
progress_meter_color=colors['PROGRESS'],
border_width=colors['BORDER'],
slider_border_width=colors['SLIDER_DEPTH'],
progress_meter_border_depth=colors['PROGRESS_DEPTH'],
scrollbar_color=(colors['SCROLL']),
element_text_color=colors['TEXT'],
input_text_color=colors['TEXT_INPUT'])
except: # most likely an index out of range
print('** Warning - Theme value not valid. Change your theme call. **')
print('valid values are', list_of_look_and_feel_values())
# ------------------------ Color processing functions ------------------------
def _hex_to_hsl(hex):
r, g, b = _hex_to_rgb(hex)
return _rgb_to_hsl(r, g, b)
def _hex_to_rgb(hex):
hex = hex.lstrip('#')
hlen = len(hex)
return tuple(int(hex[i:i + hlen // 3], 16) for i in range(0, hlen, hlen // 3))
def _rgb_to_hsl(r, g, b):
r = float(r)
g = float(g)
b = float(b)
high = max(r, g, b)
low = min(r, g, b)
h, s, v = ((high + low) / 2,) * 3
if high == low:
h = s = 0.0
else:
d = high - low
l = (high + low) / 2
s = d / (2 - high - low) if l > 0.5 else d / (high + low)
h = {
r: (g - b) / d + (6 if g < b else 0),
g: (b - r) / d + 2,
b: (r - g) / d + 4,
}[high]
h /= 6
return h, s, v
def _hsl_to_rgb(h, s, l):
def hue_to_rgb(p, q, t):
t += 1 if t < 0 else 0
t -= 1 if t > 1 else 0
if t < 1 / 6: return p + (q - p) * 6 * t
if t < 1 / 2: return q
if t < 2 / 3: p + (q - p) * (2 / 3 - t) * 6
return p
if s == 0:
r, g, b = l, l, l
else:
q = l * (1 + s) if l < 0.5 else l + s - l * s
p = 2 * l - q
r = hue_to_rgb(p, q, h + 1 / 3)
g = hue_to_rgb(p, q, h)
b = hue_to_rgb(p, q, h - 1 / 3)
return r, g, b
def _hsv_to_hsl(h, s, v):
l = 0.5 * v * (2 - s)
s = v * s / (1 - fabs(2 * l - 1))
return h, s, l
def _hsl_to_hsv(h, s, l):
v = (2 * l + s * (1 - fabs(2 * l - 1))) / 2
s = 2 * (v - l) / v
return h, s, v
# Converts an object's contents into a nice printable string. Great for dumping debug data
def obj_to_string_single_obj(obj):
"""
Dumps an Object's values as a formatted string. Very nicely done. Great way to display an object's member variables in human form
Returns only the top-most object's variables instead of drilling down to dispolay more
:param obj: The object to display
:type obj: (Any)
:return: Formatted output of the object's values
:rtype: (str)
"""
if obj is None:
return 'None'
return str(obj.__class__) + '\n' + '\n'.join(
(repr(item) + ' = ' + repr(obj.__dict__[item]) for item in sorted(obj.__dict__)))
def obj_to_string(obj, extra=' '):
"""
Dumps an Object's values as a formatted string. Very nicely done. Great way to display an object's member variables in human form
:param obj: The object to display
:type obj: (Any)
:param extra: extra stuff (Default value = ' ')
:type extra: (str)
:return: Formatted output of the object's values
:rtype: (str)
"""
if obj is None:
return 'None'
return str(obj.__class__) + '\n' + '\n'.join(
(extra + (str(item) + ' = ' +
(ObjToString(obj.__dict__[item], extra + ' ') if hasattr(obj.__dict__[item], '__dict__') else str(
obj.__dict__[item])))
for item in sorted(obj.__dict__)))
# ...######..##.......####.########..########...#######.....###....########..########.
# ..##....##.##........##..##.....##.##.....##.##.....##...##.##...##.....##.##.....##
# ..##.......##........##..##.....##.##.....##.##.....##..##...##..##.....##.##.....##
# ..##.......##........##..########..########..##.....##.##.....##.########..##.....##
# ..##.......##........##..##........##.....##.##.....##.#########.##...##...##.....##
# ..##....##.##........##..##........##.....##.##.....##.##.....##.##....##..##.....##
# ...######..########.####.##........########...#######..##.....##.##.....##.########.
# ................................................................................
# ..########.##.....##.##....##..######..########.####..#######..##....##..######.
# ..##.......##.....##.###...##.##....##....##.....##..##.....##.###...##.##....##
# ..##.......##.....##.####..##.##..........##.....##..##.....##.####..##.##......
# ..######...##.....##.##.##.##.##..........##.....##..##.....##.##.##.##..######.
# ..##.......##.....##.##..####.##..........##.....##..##.....##.##..####.......##
# ..##.......##.....##.##...###.##....##....##.....##..##.....##.##...###.##....##
# ..##........#######..##....##..######.....##....####..#######..##....##..######.
def clipboard_set(new_value):
"""
Sets the clipboard to a specific value.
IMPORTANT NOTE - Your PySimpleGUI application needs to remain running until you've pasted
your clipboard. This is a tkinter limitation. A workaround was found for Windows, but you still
need to stay running for Linux systems.
:param new_value: value to set the clipboard to. Will be converted to a string
:type new_value: (str | bytes)
"""
# Create and use a temp window
root = tk.Tk()
root.withdraw()
root.clipboard_clear()
root.clipboard_append(str(new_value))
root.update()
root.destroy()
def clipboard_get():
"""
Gets the clipboard current value.
:return: The current value of the clipboard
:rtype: (str)
"""
# Create and use a temp window
root = tk.Tk()
root.withdraw()
try:
value = root.clipboard_get()
except:
value = ''
root.update()
root.destroy()
return value
# MM"""""""`YM
# MM mmmmm M
# M' .M .d8888b. 88d888b. dP dP 88d888b. .d8888b.
# MM MMMMMMMM 88' `88 88' `88 88 88 88' `88 Y8ooooo.
# MM MMMMMMMM 88. .88 88. .88 88. .88 88. .88 88
# MM MMMMMMMM `88888P' 88Y888P' `88888P' 88Y888P' `88888P'
# MMMMMMMMMMMM 88 88
# dP dP
# ------------------------------------------------------------------------------------------------------------------ #
# ===================================== Upper PySimpleGUI ======================================================== #
# ------------------------------------------------------------------------------------------------------------------ #
# ----------------------------------- The mighty Popup! ------------------------------------------------------------ #
def popup(*args, title=None, button_color=None, background_color=None, text_color=None, button_type=POPUP_BUTTONS_OK, auto_close=False,
auto_close_duration=None, custom_text=(None, None), non_blocking=False, icon=None, line_width=None, font=None, no_titlebar=False, grab_anywhere=False,
keep_on_top=None, location=(None, None), relative_location=(None, None), any_key_closes=False, image=None, modal=True):
"""
Popup - Display a popup Window with as many parms as you wish to include. This is the GUI equivalent of the
"print" statement. It's also great for "pausing" your program's flow until the user can read some error messages.
If this popup doesn't have the features you want, then you can easily make your own. Popups can be accomplished in 1 line of code:
choice, _ = sg.Window('Continue?', [[sg.T('Do you want to continue?')], [sg.Yes(s=10), sg.No(s=10)]], disable_close=True).read(close=True)
:param *args: Variable number of your arguments. Load up the call with stuff to see!
:type *args: (Any)
:param title: Optional title for the window. If none provided, the first arg will be used instead.
:type title: (str)
:param button_color: Color of the buttons shown (text color, button color)
:type button_color: (str, str) | None
:param background_color: Window's background color
:type background_color: (str)
:param text_color: text color
:type text_color: (str)
:param button_type: NOT USER SET! Determines which pre-defined buttons will be shown (Default value = POPUP_BUTTONS_OK). There are many Popup functions and they call Popup, changing this parameter to get the desired effect.
:type button_type: (int)
:param auto_close: If True the window will automatically close
:type auto_close: (bool)
:param auto_close_duration: time in seconds to keep window open before closing it automatically
:type auto_close_duration: (int)
:param custom_text: A string or pair of strings that contain the text to display on the buttons
:type custom_text: (str, str) | str
:param non_blocking: If True then will immediately return from the function without waiting for the user's input.
:type non_blocking: (bool)
:param icon: icon to display on the window. Same format as a Window call
:type icon: str | bytes
:param line_width: Width of lines in characters. Defaults to MESSAGE_BOX_LINE_WIDTH
:type line_width: (int)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: str | Tuple[font_name, size, modifiers]
:param no_titlebar: If True will not show the frame around the window and the titlebar across the top
:type no_titlebar: (bool)
:param grab_anywhere: If True can grab anywhere to move the window. If no_titlebar is True, grab_anywhere should likely be enabled too
:type grab_anywhere: (bool)
:param location: Location on screen to display the top left corner of window. Defaults to window centered on screen
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param keep_on_top: If True the window will remain above all current windows
:type keep_on_top: (bool)
:param any_key_closes: If True then will turn on return_keyboard_events for the window which will cause window to close as soon as any key is pressed. Normally the return key only will close the window. Default is false.
:type any_key_closes: (bool)
:param image: Image to include at the top of the popup window
:type image: (str) or (bytes)
:param modal: If True then makes the popup will behave like a Modal window... all other windows are non-operational until this one is closed. Default = True
:type modal: bool
:return: Returns text of the button that was pressed. None will be returned if user closed window with X
:rtype: str | None
"""
if not args:
args_to_print = ['']
else:
args_to_print = args
if line_width != None:
local_line_width = line_width
else:
local_line_width = MESSAGE_BOX_LINE_WIDTH
_title = title if title is not None else args_to_print[0]
layout = [[]]
max_line_total, total_lines = 0, 0
if image is not None:
if isinstance(image, str):
layout += [[Image(filename=image)]]
else:
layout += [[Image(data=image)]]
for message in args_to_print:
# fancy code to check if string and convert if not is not need. Just always convert to string :-)
# if not isinstance(message, str): message = str(message)
message = str(message)
if message.count('\n'): # if there are line breaks, then wrap each segment separately
# message_wrapped = message # used to just do this, but now breaking into smaller pieces
message_wrapped = ''
msg_list = message.split('\n') # break into segments that will each be wrapped
message_wrapped = '\n'.join([textwrap.fill(msg, local_line_width) for msg in msg_list])
else:
message_wrapped = textwrap.fill(message, local_line_width)
message_wrapped_lines = message_wrapped.count('\n') + 1
longest_line_len = max([len(l) for l in message.split('\n')])
width_used = min(longest_line_len, local_line_width)
max_line_total = max(max_line_total, width_used)
# height = _GetNumLinesNeeded(message, width_used)
height = message_wrapped_lines
layout += [[
Text(message_wrapped, auto_size_text=True, text_color=text_color, background_color=background_color)]]
total_lines += height
if non_blocking:
PopupButton = DummyButton # important to use or else button will close other windows too!
else:
PopupButton = Button
# show either an OK or Yes/No depending on paramater
if custom_text != (None, None):
if type(custom_text) is not tuple:
layout += [[PopupButton(custom_text, size=(len(custom_text), 1), button_color=button_color, focus=True,
bind_return_key=True)]]
elif custom_text[1] is None:
layout += [[
PopupButton(custom_text[0], size=(len(custom_text[0]), 1), button_color=button_color, focus=True,
bind_return_key=True)]]
else:
layout += [[PopupButton(custom_text[0], button_color=button_color, focus=True, bind_return_key=True,
size=(len(custom_text[0]), 1)),
PopupButton(custom_text[1], button_color=button_color, size=(len(custom_text[1]), 1))]]
elif button_type is POPUP_BUTTONS_YES_NO:
layout += [[PopupButton('Yes', button_color=button_color, focus=True, bind_return_key=True, pad=((20, 5), 3),
size=(5, 1)), PopupButton('No', button_color=button_color, size=(5, 1))]]
elif button_type is POPUP_BUTTONS_CANCELLED:
layout += [[
PopupButton('Cancelled', button_color=button_color, focus=True, bind_return_key=True, pad=((20, 0), 3))]]
elif button_type is POPUP_BUTTONS_ERROR:
layout += [[PopupButton('Error', size=(6, 1), button_color=button_color, focus=True, bind_return_key=True,
pad=((20, 0), 3))]]
elif button_type is POPUP_BUTTONS_OK_CANCEL:
layout += [[PopupButton('OK', size=(6, 1), button_color=button_color, focus=True, bind_return_key=True),
PopupButton('Cancel', size=(6, 1), button_color=button_color)]]
elif button_type is POPUP_BUTTONS_NO_BUTTONS:
pass
else:
layout += [[PopupButton('OK', size=(5, 1), button_color=button_color, focus=True, bind_return_key=True,
pad=((20, 0), 3))]]
window = Window(_title, layout, auto_size_text=True, background_color=background_color, button_color=button_color,
auto_close=auto_close, auto_close_duration=auto_close_duration, icon=icon, font=font,
no_titlebar=no_titlebar, grab_anywhere=grab_anywhere, keep_on_top=keep_on_top, location=location, relative_location=relative_location, return_keyboard_events=any_key_closes,
modal=modal)
if non_blocking:
button, values = window.read(timeout=0)
else:
button, values = window.read()
window.close()
del window
return button
# ============================== MsgBox============#
# Lazy function. Same as calling Popup with parms #
# This function WILL Disappear perhaps today #
# ==================================================#
# MsgBox is the legacy call and should not be used any longer
def MsgBox(*args):
"""
Do not call this anymore it will raise exception. Use Popups instead
:param *args:
:type *args:
"""
raise DeprecationWarning('MsgBox is no longer supported... change your call to Popup')
# ======================== Scrolled Text Box =====#
# ===================================================#
def popup_scrolled(*args, title=None, button_color=None, background_color=None, text_color=None, yes_no=False, auto_close=False, auto_close_duration=None,
size=(None, None), location=(None, None), relative_location=(None, None), non_blocking=False, no_titlebar=False, grab_anywhere=False, keep_on_top=None, font=None,
image=None, icon=None, modal=True, no_sizegrip=False):
"""
Show a scrolled Popup window containing the user's text that was supplied. Use with as many items to print as you
want, just like a print statement.
:param *args: Variable number of items to display
:type *args: (Any)
:param title: Title to display in the window.
:type title: (str)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param yes_no: If True, displays Yes and No buttons instead of Ok
:type yes_no: (bool)
:param auto_close: if True window will close itself
:type auto_close: (bool)
:param auto_close_duration: Older versions only accept int. Time in seconds until window will close
:type auto_close_duration: int | float
:param size: (w,h) w=characters-wide, h=rows-high
:type size: (int, int)
:param location: Location on the screen to place the upper left corner of the window
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param non_blocking: if True the call will immediately return rather than waiting on user input
:type non_blocking: (bool)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param no_titlebar: If True no titlebar will be shown
:type no_titlebar: (bool)
:param grab_anywhere: If True, than can grab anywhere to move the window (Default = False)
:type grab_anywhere: (bool)
:param keep_on_top: If True the window will remain above all current windows
:type keep_on_top: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param image: Image to include at the top of the popup window
:type image: (str) or (bytes)
:param icon: filename or base64 string to be used for the window's icon
:type icon: bytes | str
:param modal: If True then makes the popup will behave like a Modal window... all other windows are non-operational until this one is closed. Default = True
:type modal: bool
:param no_sizegrip: If True no Sizegrip will be shown when there is no titlebar. It's only shown if there is no titlebar
:type no_sizegrip: (bool)
:return: Returns text of the button that was pressed. None will be returned if user closed window with X
:rtype: str | None | TIMEOUT_KEY
"""
if not args: return
width, height = size
width = width if width else MESSAGE_BOX_LINE_WIDTH
layout = [[]]
if image is not None:
if isinstance(image, str):
layout += [[Image(filename=image)]]
else:
layout += [[Image(data=image)]]
max_line_total, max_line_width, total_lines, height_computed = 0, 0, 0, 0
complete_output = ''
for message in args:
# fancy code to check if string and convert if not is not need. Just always convert to string :-)
# if not isinstance(message, str): message = str(message)
message = str(message)
longest_line_len = max([len(l) for l in message.split('\n')])
width_used = min(longest_line_len, width)
max_line_total = max(max_line_total, width_used)
max_line_width = width
lines_needed = _GetNumLinesNeeded(message, width_used)
height_computed += lines_needed + 1
complete_output += message + '\n'
total_lines += lines_needed
height_computed = MAX_SCROLLED_TEXT_BOX_HEIGHT if height_computed > MAX_SCROLLED_TEXT_BOX_HEIGHT else height_computed
if height:
height_computed = height
layout += [[Multiline(complete_output, size=(max_line_width, height_computed), background_color=background_color, text_color=text_color, expand_x=True,
expand_y=True, k='-MLINE-')]]
pad = max_line_total - 15 if max_line_total > 15 else 1
# show either an OK or Yes/No depending on paramater
button = DummyButton if non_blocking else Button
if yes_no:
layout += [[Text('', size=(pad, 1), auto_size_text=False, background_color=background_color), button('Yes'), button('No')]]
else:
layout += [[Text('', size=(pad, 1), auto_size_text=False, background_color=background_color),
button('OK', size=(5, 1), button_color=button_color)]]
if no_titlebar and no_sizegrip is not True:
layout += [[Sizegrip()]]
window = Window(title or args[0], layout, auto_size_text=True, button_color=button_color, auto_close=auto_close,
auto_close_duration=auto_close_duration, location=location, relative_location=relative_location, resizable=True, font=font, background_color=background_color,
no_titlebar=no_titlebar, grab_anywhere=grab_anywhere, keep_on_top=keep_on_top, modal=modal, icon=icon)
if non_blocking:
button, values = window.read(timeout=0)
else:
button, values = window.read()
window.close()
del window
return button
# ============================== sprint ======#
# Is identical to the Scrolled Text Box #
# Provides a crude 'print' mechanism but in a #
# GUI environment #
# This is in addition to the Print function #
# which routes output to a "Debug Window" #
# ============================================#
# --------------------------- popup_no_buttons ---------------------------
def popup_no_buttons(*args, title=None, background_color=None, text_color=None, auto_close=False,
auto_close_duration=None, non_blocking=False, icon=None, line_width=None, font=None,
no_titlebar=False, grab_anywhere=False, keep_on_top=None, location=(None, None), relative_location=(None, None), image=None, modal=True):
"""Show a Popup but without any buttons
:param *args: Variable number of items to display
:type *args: (Any)
:param title: Title to display in the window.
:type title: (str)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param auto_close: if True window will close itself
:type auto_close: (bool)
:param auto_close_duration: Older versions only accept int. Time in seconds until window will close
:type auto_close_duration: int | float
:param non_blocking: If True then will immediately return from the function without waiting for the user's input. (Default = False)
:type non_blocking: (bool)
:param icon: filename or base64 string to be used for the window's icon
:type icon: bytes | str
:param line_width: Width of lines in characters
:type line_width: (int)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param no_titlebar: If True no titlebar will be shown
:type no_titlebar: (bool)
:param grab_anywhere: If True, than can grab anywhere to move the window (Default = False)
:type grab_anywhere: (bool)
:param location: Location of upper left corner of the window
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param image: Image to include at the top of the popup window
:type image: (str) or (bytes)
:param modal: If True then makes the popup will behave like a Modal window... all other windows are non-operational until this one is closed. Default = True
:type modal: bool
:return: Returns text of the button that was pressed. None will be returned if user closed window with X
:rtype: str | None | TIMEOUT_KEY """
Popup(*args, title=title, background_color=background_color, text_color=text_color,
button_type=POPUP_BUTTONS_NO_BUTTONS,
auto_close=auto_close, auto_close_duration=auto_close_duration, non_blocking=non_blocking, icon=icon,
line_width=line_width,
font=font, no_titlebar=no_titlebar, grab_anywhere=grab_anywhere, keep_on_top=keep_on_top, location=location, relative_location=relative_location, image=image, modal=modal)
# --------------------------- popup_non_blocking ---------------------------
def popup_non_blocking(*args, title=None, button_type=POPUP_BUTTONS_OK, button_color=None, background_color=None,
text_color=None, auto_close=False, auto_close_duration=None, non_blocking=True, icon=None,
line_width=None, font=None, no_titlebar=False, grab_anywhere=False, keep_on_top=None,
location=(None, None), relative_location=(None, None), image=None, modal=False):
"""
Show Popup window and immediately return (does not block)
:param *args: Variable number of items to display
:type *args: (Any)
:param title: Title to display in the window.
:type title: (str)
:param button_type: Determines which pre-defined buttons will be shown (Default value = POPUP_BUTTONS_OK).
:type button_type: (int)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param auto_close: if True window will close itself
:type auto_close: (bool)
:param auto_close_duration: Older versions only accept int. Time in seconds until window will close
:type auto_close_duration: int | float
:param non_blocking: if True the call will immediately return rather than waiting on user input
:type non_blocking: (bool)
:param icon: filename or base64 string to be used for the window's icon
:type icon: bytes | str
:param line_width: Width of lines in characters
:type line_width: (int)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param no_titlebar: If True no titlebar will be shown
:type no_titlebar: (bool)
:param grab_anywhere: If True: can grab anywhere to move the window (Default = False)
:type grab_anywhere: (bool)
:param location: Location of upper left corner of the window
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param image: Image to include at the top of the popup window
:type image: (str) or (bytes)
:param modal: If True then makes the popup will behave like a Modal window... all other windows are non-operational until this one is closed. Default = False
:type modal: bool
:return: Reason for popup closing
:rtype: str | None
"""
return popup(*args, title=title, button_color=button_color, background_color=background_color, text_color=text_color,
button_type=button_type,
auto_close=auto_close, auto_close_duration=auto_close_duration, non_blocking=non_blocking, icon=icon,
line_width=line_width,
font=font, no_titlebar=no_titlebar, grab_anywhere=grab_anywhere, keep_on_top=keep_on_top, location=location, relative_location=relative_location, image=image, modal=modal)
# --------------------------- popup_quick - a NonBlocking, Self-closing Popup ---------------------------
def popup_quick(*args, title=None, button_type=POPUP_BUTTONS_OK, button_color=None, background_color=None,
text_color=None, auto_close=True, auto_close_duration=2, non_blocking=True, icon=None, line_width=None,
font=None, no_titlebar=False, grab_anywhere=False, keep_on_top=None, location=(None, None), relative_location=(None, None), image=None, modal=False):
"""
Show Popup box that doesn't block and closes itself
:param *args: Variable number of items to display
:type *args: (Any)
:param title: Title to display in the window.
:type title: (str)
:param button_type: Determines which pre-defined buttons will be shown (Default value = POPUP_BUTTONS_OK).
:type button_type: (int)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param auto_close: if True window will close itself
:type auto_close: (bool)
:param auto_close_duration: Older versions only accept int. Time in seconds until window will close
:type auto_close_duration: int | float
:param non_blocking: if True the call will immediately return rather than waiting on user input
:type non_blocking: (bool)
:param icon: filename or base64 string to be used for the window's icon
:type icon: bytes | str
:param line_width: Width of lines in characters
:type line_width: (int)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param no_titlebar: If True no titlebar will be shown
:type no_titlebar: (bool)
:param grab_anywhere: If True: can grab anywhere to move the window (Default = False)
:type grab_anywhere: (bool)
:param keep_on_top: If True the window will remain above all current windows
:type keep_on_top: (bool)
:param location: Location of upper left corner of the window
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param image: Image to include at the top of the popup window
:type image: (str) or (bytes)
:param modal: If True then makes the popup will behave like a Modal window... all other windows are non-operational until this one is closed. Default = False
:type modal: bool
:return: Returns text of the button that was pressed. None will be returned if user closed window with X
:rtype: str | None | TIMEOUT_KEY
"""
return popup(*args, title=title, button_color=button_color, background_color=background_color, text_color=text_color,
button_type=button_type,
auto_close=auto_close, auto_close_duration=auto_close_duration, non_blocking=non_blocking, icon=icon,
line_width=line_width,
font=font, no_titlebar=no_titlebar, grab_anywhere=grab_anywhere, keep_on_top=keep_on_top, location=location, relative_location=relative_location, image=image, modal=modal)
# --------------------------- popup_quick_message - a NonBlocking, Self-closing Popup with no titlebar and no buttons ---------------------------
def popup_quick_message(*args, title=None, button_type=POPUP_BUTTONS_NO_BUTTONS, button_color=None, background_color=None,
text_color=None, auto_close=True, auto_close_duration=2, non_blocking=True, icon=None, line_width=None,
font=None, no_titlebar=True, grab_anywhere=False, keep_on_top=None, location=(None, None), relative_location=(None, None), image=None, modal=False):
"""
Show Popup window with no titlebar, doesn't block, and auto closes itself.
:param *args: Variable number of items to display
:type *args: (Any)
:param title: Title to display in the window.
:type title: (str)
:param button_type: Determines which pre-defined buttons will be shown (Default value = POPUP_BUTTONS_OK).
:type button_type: (int)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param keep_on_top: If True the window will remain above all current windows
:type keep_on_top: (bool)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param auto_close: if True window will close itself
:type auto_close: (bool)
:param auto_close_duration: Older versions only accept int. Time in seconds until window will close
:type auto_close_duration: int | float
:param non_blocking: if True the call will immediately return rather than waiting on user input
:type non_blocking: (bool)
:param icon: filename or base64 string to be used for the window's icon
:type icon: bytes | str
:param line_width: Width of lines in characters
:type line_width: (int)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param no_titlebar: If True no titlebar will be shown
:type no_titlebar: (bool)
:param grab_anywhere: If True: can grab anywhere to move the window (Default = False)
:type grab_anywhere: (bool)
:param location: Location of upper left corner of the window
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param image: Image to include at the top of the popup window
:type image: (str) or (bytes)
:param modal: If True then makes the popup will behave like a Modal window... all other windows are non-operational until this one is closed. Default = False
:type modal: bool
:return: Returns text of the button that was pressed. None will be returned if user closed window with X
:rtype: str | None | TIMEOUT_KEY
"""
return popup(*args, title=title, button_color=button_color, background_color=background_color, text_color=text_color,
button_type=button_type,
auto_close=auto_close, auto_close_duration=auto_close_duration, non_blocking=non_blocking, icon=icon,
line_width=line_width,
font=font, no_titlebar=no_titlebar, grab_anywhere=grab_anywhere, keep_on_top=keep_on_top, location=location, relative_location=relative_location, image=image, modal=modal)
# --------------------------- PopupNoTitlebar ---------------------------
def popup_no_titlebar(*args, title=None, button_type=POPUP_BUTTONS_OK, button_color=None, background_color=None,
text_color=None, auto_close=False, auto_close_duration=None, non_blocking=False, icon=None,
line_width=None, font=None, grab_anywhere=True, keep_on_top=None, location=(None, None), relative_location=(None, None), image=None, modal=True):
"""
Display a Popup without a titlebar. Enables grab anywhere so you can move it
:param *args: Variable number of items to display
:type *args: (Any)
:param title: Title to display in the window.
:type title: (str)
:param button_type: Determines which pre-defined buttons will be shown (Default value = POPUP_BUTTONS_OK).
:type button_type: (int)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param auto_close: if True window will close itself
:type auto_close: (bool)
:param auto_close_duration: Older versions only accept int. Time in seconds until window will close
:type auto_close_duration: int | float
:param non_blocking: if True the call will immediately return rather than waiting on user input
:type non_blocking: (bool)
:param icon: filename or base64 string to be used for the window's icon
:type icon: bytes | str
:param line_width: Width of lines in characters
:type line_width: (int)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param grab_anywhere: If True: can grab anywhere to move the window (Default = False)
:type grab_anywhere: (bool)
:param keep_on_top: If True the window will remain above all current windows
:type keep_on_top: (bool)
:param location: Location of upper left corner of the window
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param image: Image to include at the top of the popup window
:type image: (str) or (bytes)
:param modal: If True then makes the popup will behave like a Modal window... all other windows are non-operational until this one is closed. Default = True
:type modal: bool
:return: Returns text of the button that was pressed. None will be returned if user closed window with X
:rtype: str | None | TIMEOUT_KEY
"""
return popup(*args, title=title, button_color=button_color, background_color=background_color, text_color=text_color,
button_type=button_type,
auto_close=auto_close, auto_close_duration=auto_close_duration, non_blocking=non_blocking, icon=icon,
line_width=line_width,
font=font, no_titlebar=True, grab_anywhere=grab_anywhere, keep_on_top=keep_on_top, location=location, relative_location=relative_location, image=image, modal=modal)
# --------------------------- PopupAutoClose ---------------------------
def popup_auto_close(*args, title=None, button_type=POPUP_BUTTONS_OK, button_color=None, background_color=None, text_color=None,
auto_close=True, auto_close_duration=None, non_blocking=False, icon=None,
line_width=None, font=None, no_titlebar=False, grab_anywhere=False, keep_on_top=None,
location=(None, None), relative_location=(None, None), image=None, modal=True):
"""Popup that closes itself after some time period
:param *args: Variable number of items to display
:type *args: (Any)
:param title: Title to display in the window.
:type title: (str)
:param button_type: Determines which pre-defined buttons will be shown (Default value = POPUP_BUTTONS_OK).
:type button_type: (int)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param auto_close: if True window will close itself
:type auto_close: (bool)
:param auto_close_duration: Older versions only accept int. Time in seconds until window will close
:type auto_close_duration: int | float
:param non_blocking: if True the call will immediately return rather than waiting on user input
:type non_blocking: (bool)
:param icon: filename or base64 string to be used for the window's icon
:type icon: bytes | str
:param line_width: Width of lines in characters
:type line_width: (int)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param no_titlebar: If True no titlebar will be shown
:type no_titlebar: (bool)
:param grab_anywhere: If True: can grab anywhere to move the window (Default = False)
:type grab_anywhere: (bool)
:param keep_on_top: If True the window will remain above all current windows
:type keep_on_top: (bool)
:param location: Location of upper left corner of the window
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param image: Image to include at the top of the popup window
:type image: (str) or (bytes)
:param modal: If True then makes the popup will behave like a Modal window... all other windows are non-operational until this one is closed. Default = True
:type modal: bool
:return: Returns text of the button that was pressed. None will be returned if user closed window with X
:rtype: str | None | TIMEOUT_KEY
"""
return popup(*args, title=title, button_color=button_color, background_color=background_color, text_color=text_color,
button_type=button_type,
auto_close=auto_close, auto_close_duration=auto_close_duration, non_blocking=non_blocking, icon=icon,
line_width=line_width,
font=font, no_titlebar=no_titlebar, grab_anywhere=grab_anywhere, keep_on_top=keep_on_top, location=location, relative_location=relative_location, image=image, modal=modal)
# --------------------------- popup_error ---------------------------
def popup_error(*args, title=None, button_color=(None, None), background_color=None, text_color=None, auto_close=False,
auto_close_duration=None, non_blocking=False, icon=None, line_width=None, font=None,
no_titlebar=False, grab_anywhere=False, keep_on_top=None, location=(None, None), relative_location=(None, None), image=None, modal=True):
"""
Popup with colored button and 'Error' as button text
:param *args: Variable number of items to display
:type *args: (Any)
:param title: Title to display in the window.
:type title: (str)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param auto_close: if True window will close itself
:type auto_close: (bool)
:param auto_close_duration: Older versions only accept int. Time in seconds until window will close
:type auto_close_duration: int | float
:param non_blocking: if True the call will immediately return rather than waiting on user input
:type non_blocking: (bool)
:param icon: filename or base64 string to be used for the window's icon
:type icon: bytes | str
:param line_width: Width of lines in characters
:type line_width: (int)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param no_titlebar: If True no titlebar will be shown
:type no_titlebar: (bool)
:param grab_anywhere: If True: can grab anywhere to move the window (Default = False)
:type grab_anywhere: (bool)
:param keep_on_top: If True the window will remain above all current windows
:type keep_on_top: (bool)
:param location: Location of upper left corner of the window
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param image: Image to include at the top of the popup window
:type image: (str) or (bytes)
:param modal: If True then makes the popup will behave like a Modal window... all other windows are non-operational until this one is closed. Default = True
:type modal: bool
:return: Returns text of the button that was pressed. None will be returned if user closed window with X
:rtype: str | None | TIMEOUT_KEY
"""
tbutton_color = DEFAULT_ERROR_BUTTON_COLOR if button_color == (None, None) else button_color
return popup(*args, title=title, button_type=POPUP_BUTTONS_ERROR, background_color=background_color, text_color=text_color,
non_blocking=non_blocking, icon=icon, line_width=line_width, button_color=tbutton_color,
auto_close=auto_close,
auto_close_duration=auto_close_duration, font=font, no_titlebar=no_titlebar, grab_anywhere=grab_anywhere,
keep_on_top=keep_on_top, location=location, relative_location=relative_location, image=image, modal=modal)
# --------------------------- popup_cancel ---------------------------
def popup_cancel(*args, title=None, button_color=None, background_color=None, text_color=None, auto_close=False,
auto_close_duration=None, non_blocking=False, icon=None, line_width=None, font=None,
no_titlebar=False, grab_anywhere=False, keep_on_top=None, location=(None, None), relative_location=(None, None), image=None, modal=True):
"""
Display Popup with "cancelled" button text
:param *args: Variable number of items to display
:type *args: (Any)
:param title: Title to display in the window.
:type title: (str)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param auto_close: if True window will close itself
:type auto_close: (bool)
:param auto_close_duration: Older versions only accept int. Time in seconds until window will close
:type auto_close_duration: int | float
:param non_blocking: if True the call will immediately return rather than waiting on user input
:type non_blocking: (bool)
:param icon: filename or base64 string to be used for the window's icon
:type icon: bytes | str
:param line_width: Width of lines in characters
:type line_width: (int)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param no_titlebar: If True no titlebar will be shown
:type no_titlebar: (bool)
:param grab_anywhere: If True: can grab anywhere to move the window (Default = False)
:type grab_anywhere: (bool)
:param keep_on_top: If True the window will remain above all current windows
:type keep_on_top: (bool)
:param location: Location of upper left corner of the window
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param image: Image to include at the top of the popup window
:type image: (str) or (bytes)
:param modal: If True then makes the popup will behave like a Modal window... all other windows are non-operational until this one is closed. Default = True
:type modal: bool
:return: Returns text of the button that was pressed. None will be returned if user closed window with X
:rtype: str | None | TIMEOUT_KEY
"""
return popup(*args, title=title, button_type=POPUP_BUTTONS_CANCELLED, background_color=background_color,
text_color=text_color,
non_blocking=non_blocking, icon=icon, line_width=line_width, button_color=button_color, auto_close=auto_close,
auto_close_duration=auto_close_duration, font=font, no_titlebar=no_titlebar, grab_anywhere=grab_anywhere,
keep_on_top=keep_on_top, location=location, relative_location=relative_location, image=image, modal=modal)
# --------------------------- popup_ok ---------------------------
def popup_ok(*args, title=None, button_color=None, background_color=None, text_color=None, auto_close=False,
auto_close_duration=None, non_blocking=False, icon=None, line_width=None, font=None,
no_titlebar=False, grab_anywhere=False, keep_on_top=None, location=(None, None), relative_location=(None, None), image=None, modal=True):
"""
Display Popup with OK button only
:param *args: Variable number of items to display
:type *args: (Any)
:param title: Title to display in the window.
:type title: (str)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param auto_close: if True window will close itself
:type auto_close: (bool)
:param auto_close_duration: Older versions only accept int. Time in seconds until window will close
:type auto_close_duration: int | float
:param non_blocking: if True the call will immediately return rather than waiting on user input
:type non_blocking: (bool)
:param icon: filename or base64 string to be used for the window's icon
:type icon: bytes | str
:param line_width: Width of lines in characters
:type line_width: (int)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param no_titlebar: If True no titlebar will be shown
:type no_titlebar: (bool)
:param grab_anywhere: If True: can grab anywhere to move the window (Default = False)
:type grab_anywhere: (bool)
:param keep_on_top: If True the window will remain above all current windows
:type keep_on_top: (bool)
:param location: Location of upper left corner of the window
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param image: Image to include at the top of the popup window
:type image: (str) or (bytes)
:param modal: If True then makes the popup will behave like a Modal window... all other windows are non-operational until this one is closed. Default = True
:type modal: bool
:return: Returns text of the button that was pressed. None will be returned if user closed window with X
:rtype: str | None | TIMEOUT_KEY
"""
return popup(*args, title=title, button_type=POPUP_BUTTONS_OK, background_color=background_color, text_color=text_color,
non_blocking=non_blocking, icon=icon, line_width=line_width, button_color=button_color, auto_close=auto_close,
auto_close_duration=auto_close_duration, font=font, no_titlebar=no_titlebar, grab_anywhere=grab_anywhere,
keep_on_top=keep_on_top, location=location, relative_location=relative_location, image=image, modal=modal)
# --------------------------- popup_ok_cancel ---------------------------
def popup_ok_cancel(*args, title=None, button_color=None, background_color=None, text_color=None, auto_close=False,
auto_close_duration=None, non_blocking=False, icon=DEFAULT_WINDOW_ICON, line_width=None, font=None,
no_titlebar=False, grab_anywhere=False, keep_on_top=None, location=(None, None), relative_location=(None, None), image=None, modal=True):
"""
Display popup with OK and Cancel buttons
:param *args: Variable number of items to display
:type *args: (Any)
:param title: Title to display in the window.
:type title: (str)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param auto_close: if True window will close itself
:type auto_close: (bool)
:param auto_close_duration: Older versions only accept int. Time in seconds until window will close
:type auto_close_duration: int | float
:param non_blocking: if True the call will immediately return rather than waiting on user input
:type non_blocking: (bool)
:param icon: filename or base64 string to be used for the window's icon
:type icon: bytes | str
:param line_width: Width of lines in characters
:type line_width: (int)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param no_titlebar: If True no titlebar will be shown
:type no_titlebar: (bool)
:param grab_anywhere: If True: can grab anywhere to move the window (Default = False)
:type grab_anywhere: (bool)
:param keep_on_top: If True the window will remain above all current windows
:type keep_on_top: (bool)
:param location: Location of upper left corner of the window
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param image: Image to include at the top of the popup window
:type image: (str) or (bytes)
:param modal: If True then makes the popup will behave like a Modal window... all other windows are non-operational until this one is closed. Default = True
:type modal: bool
:return: clicked button
:rtype: "OK" | "Cancel" | None
"""
return popup(*args, title=title, button_type=POPUP_BUTTONS_OK_CANCEL, background_color=background_color,
text_color=text_color,
non_blocking=non_blocking, icon=icon, line_width=line_width, button_color=button_color,
auto_close=auto_close, auto_close_duration=auto_close_duration, font=font, no_titlebar=no_titlebar,
grab_anywhere=grab_anywhere, keep_on_top=keep_on_top, location=location, relative_location=relative_location, image=image, modal=modal)
# --------------------------- popup_yes_no ---------------------------
def popup_yes_no(*args, title=None, button_color=None, background_color=None, text_color=None, auto_close=False,
auto_close_duration=None, non_blocking=False, icon=None, line_width=None, font=None,
no_titlebar=False, grab_anywhere=False, keep_on_top=None, location=(None, None), relative_location=(None, None), image=None, modal=True):
"""
Display Popup with Yes and No buttons
:param *args: Variable number of items to display
:type *args: (Any)
:param title: Title to display in the window.
:type title: (str)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param auto_close: if True window will close itself
:type auto_close: (bool)
:param auto_close_duration: Older versions only accept int. Time in seconds until window will close
:type auto_close_duration: int | float
:param non_blocking: if True the call will immediately return rather than waiting on user input
:type non_blocking: (bool)
:param icon: filename or base64 string to be used for the window's icon
:type icon: bytes | str
:param line_width: Width of lines in characters
:type line_width: (int)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param no_titlebar: If True no titlebar will be shown
:type no_titlebar: (bool)
:param grab_anywhere: If True: can grab anywhere to move the window (Default = False)
:type grab_anywhere: (bool)
:param keep_on_top: If True the window will remain above all current windows
:type keep_on_top: (bool)
:param location: Location of upper left corner of the window
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param image: Image to include at the top of the popup window
:type image: (str) or (bytes)
:param modal: If True then makes the popup will behave like a Modal window... all other windows are non-operational until this one is closed. Default = True
:type modal: bool
:return: clicked button
:rtype: "Yes" | "No" | None
"""
return popup(*args, title=title, button_type=POPUP_BUTTONS_YES_NO, background_color=background_color,
text_color=text_color,
non_blocking=non_blocking, icon=icon, line_width=line_width, button_color=button_color,
auto_close=auto_close, auto_close_duration=auto_close_duration, font=font, no_titlebar=no_titlebar,
grab_anywhere=grab_anywhere, keep_on_top=keep_on_top, location=location, relative_location=relative_location, image=image, modal=modal)
##############################################################################
# The popup_get_____ functions - Will return user input #
##############################################################################
# --------------------------- popup_get_folder ---------------------------
def popup_get_folder(message, title=None, default_path='', no_window=False, size=(None, None), button_color=None,
background_color=None, text_color=None, icon=None, font=None, no_titlebar=False,
grab_anywhere=False, keep_on_top=None, location=(None, None), relative_location=(None, None), initial_folder=None, image=None, modal=True, history=False,
history_setting_filename=None):
"""
Display popup with text entry field and browse button so that a folder can be chosen.
:param message: message displayed to user
:type message: (str)
:param title: Window title
:type title: (str)
:param default_path: path to display to user as starting point (filled into the input field)
:type default_path: (str)
:param no_window: if True, no PySimpleGUI window will be shown. Instead just the tkinter dialog is shown
:type no_window: (bool)
:param size: (width, height) of the InputText Element
:type size: (int, int)
:param button_color: button color (foreground, background)
:type button_color: (str, str) or str
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param icon: filename or base64 string to be used for the window's icon
:type icon: bytes | str
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param no_titlebar: If True no titlebar will be shown
:type no_titlebar: (bool)
:param grab_anywhere: If True: can grab anywhere to move the window (Default = False)
:type grab_anywhere: (bool)
:param keep_on_top: If True the window will remain above all current windows
:type keep_on_top: (bool)
:param location: Location of upper left corner of the window
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param initial_folder: location in filesystem to begin browsing
:type initial_folder: (str)
:param image: Image to include at the top of the popup window
:type image: (str) or (bytes)
:param modal: If True then makes the popup will behave like a Modal window... all other windows are non-operational until this one is closed. Default = True
:type modal: bool
:param history: If True then enable a "history" feature that will display previous entries used. Uses settings filename provided or default if none provided
:type history: bool
:param history_setting_filename: Filename to use for the User Settings. Will store list of previous entries in this settings file
:type history_setting_filename: (str)
:return: string representing the path chosen, None if cancelled or window closed with X
:rtype: str | None
"""
# First setup the history settings file if history feature is enabled
if history and history_setting_filename is not None:
try:
history_settings = UserSettings(history_setting_filename)
except Exception as e:
_error_popup_with_traceback('popup_get_folder - Something is wrong with your supplied history settings filename',
'Exception: {}'.format(e))
return None
elif history:
history_settings_filename = os.path.basename(inspect.stack()[1].filename)
history_settings_filename = os.path.splitext(history_settings_filename)[0] + '.json'
history_settings = UserSettings(history_settings_filename)
else:
history_settings = None
# global _my_windows
if no_window:
if not Window.hidden_master_root:
# if first window being created, make a throwaway, hidden master root. This stops one user
# window from becoming the child of another user window. All windows are children of this
# hidden window
Window._IncrementOpenCount()
Window.hidden_master_root = tk.Tk()
Window.hidden_master_root.attributes('-alpha', 0) # HIDE this window really really really
# if not running_mac():
try:
Window.hidden_master_root.wm_overrideredirect(True)
except Exception as e:
print('* Error performing wm_overrideredirect while hiding hidden master root in get folder *', e)
Window.hidden_master_root.withdraw()
root = tk.Toplevel()
try:
root.attributes('-alpha', 0) # hide window while building it. makes for smoother 'paint'
# if not running_mac():
try:
root.wm_overrideredirect(True)
except Exception as e:
print('* Error performing wm_overrideredirect while hiding the window during creation in get folder *', e)
root.withdraw()
except:
pass
folder_name = tk.filedialog.askdirectory(
initialdir=initial_folder,
) # show the 'get folder' dialog box
root.destroy()
return folder_name
browse_button = FolderBrowse(initial_folder=initial_folder)
if image is not None:
if isinstance(image, str):
layout = [[Image(filename=image)]]
else:
layout = [[Image(data=image)]]
else:
layout = [[]]
layout += [[Text(message, auto_size_text=True, text_color=text_color, background_color=background_color)]]
if not history:
layout += [[InputText(default_text=default_path, size=size, key='-INPUT-'), browse_button]]
else:
file_list = history_settings.get('-PSG folder list-', [])
last_entry = file_list[0] if file_list else ''
layout += [[Combo(file_list, default_value=last_entry, key='-INPUT-', size=size if size != (None, None) else (80, 1), bind_return_key=True),
browse_button, Button('Clear History', tooltip='Clears the list of folders shown in the combobox')]]
layout += [[Button('Ok', size=(6, 1), bind_return_key=True), Button('Cancel', size=(6, 1))]]
window = Window(title=title or message, layout=layout, icon=icon, auto_size_text=True, button_color=button_color,
font=font, background_color=background_color, no_titlebar=no_titlebar, grab_anywhere=grab_anywhere, keep_on_top=keep_on_top,
location=location, relative_location=relative_location, modal=modal)
while True:
event, values = window.read()
if event in ('Cancel', WIN_CLOSED):
break
elif event == 'Clear History':
history_settings.set('-PSG folder list-', [])
window['-INPUT-'].update('', [])
popup_quick_message('History of Previous Choices Cleared', background_color='red', text_color='white', font='_ 20', keep_on_top=True)
elif event in ('Ok', '-INPUT-'):
if values['-INPUT-'] != '':
if history_settings is not None:
list_of_entries = history_settings.get('-PSG folder list-', [])
if values['-INPUT-'] in list_of_entries:
list_of_entries.remove(values['-INPUT-'])
list_of_entries.insert(0, values['-INPUT-'])
history_settings.set('-PSG folder list-', list_of_entries)
break
window.close()
del window
if event in ('Cancel', WIN_CLOSED):
return None
return values['-INPUT-']
# --------------------------- popup_get_file ---------------------------
def popup_get_file(message, title=None, default_path='', default_extension='', save_as=False, multiple_files=False,
file_types=FILE_TYPES_ALL_FILES,
no_window=False, size=(None, None), button_color=None, background_color=None, text_color=None,
icon=None, font=None, no_titlebar=False, grab_anywhere=False, keep_on_top=None,
location=(None, None), relative_location=(None, None), initial_folder=None, image=None, files_delimiter=BROWSE_FILES_DELIMITER, modal=True, history=False, show_hidden=True,
history_setting_filename=None):
"""
Display popup window with text entry field and browse button so that a file can be chosen by user.
:param message: message displayed to user
:type message: (str)
:param title: Window title
:type title: (str)
:param default_path: path to display to user as starting point (filled into the input field)
:type default_path: (str)
:param default_extension: If no extension entered by user, add this to filename (only used in saveas dialogs)
:type default_extension: (str)
:param save_as: if True, the "save as" dialog is shown which will verify before overwriting
:type save_as: (bool)
:param multiple_files: if True, then allows multiple files to be selected that are returned with ';' between each filename
:type multiple_files: (bool)
:param file_types: List of extensions to show using wildcards. All files (the default) = (("ALL Files", "*.* *"),)
:type file_types: Tuple[Tuple[str,str]]
:param no_window: if True, no PySimpleGUI window will be shown. Instead just the tkinter dialog is shown
:type no_window: (bool)
:param size: (width, height) of the InputText Element or Combo element if using history feature
:type size: (int, int)
:param button_color: Color of the button (text, background)
:type button_color: (str, str) or str
:param background_color: background color of the entire window
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param icon: filename or base64 string to be used for the window's icon
:type icon: bytes | str
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param no_titlebar: If True no titlebar will be shown
:type no_titlebar: (bool)
:param grab_anywhere: If True: can grab anywhere to move the window (Default = False)
:type grab_anywhere: (bool)
:param keep_on_top: If True the window will remain above all current windows
:type keep_on_top: (bool)
:param location: Location of upper left corner of the window
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param initial_folder: location in filesystem to begin browsing
:type initial_folder: (str)
:param image: Image to include at the top of the popup window
:type image: (str) or (bytes)
:param files_delimiter: String to place between files when multiple files are selected. Normally a ;
:type files_delimiter: str
:param modal: If True then makes the popup will behave like a Modal window... all other windows are non-operational until this one is closed. Default = True
:type modal: bool
:param history: If True then enable a "history" feature that will display previous entries used. Uses settings filename provided or default if none provided
:type history: bool
:param show_hidden: If True then enables the checkbox in the system dialog to select hidden files to be shown
:type show_hidden: bool
:param history_setting_filename: Filename to use for the User Settings. Will store list of previous entries in this settings file
:type history_setting_filename: (str)
:return: string representing the file(s) chosen, None if cancelled or window closed with X
:rtype: str | None
"""
# First setup the history settings file if history feature is enabled
if history and history_setting_filename is not None:
try:
history_settings = UserSettings(history_setting_filename)
except Exception as e:
_error_popup_with_traceback('popup_get_file - Something is wrong with your supplied history settings filename',
'Exception: {}'.format(e))
return None
elif history:
history_settings_filename = os.path.basename(inspect.stack()[1].filename)
history_settings_filename = os.path.splitext(history_settings_filename)[0] + '.json'
history_settings = UserSettings(history_settings_filename)
else:
history_settings = None
if icon is None:
icon = Window._user_defined_icon or DEFAULT_BASE64_ICON
if no_window:
if not Window.hidden_master_root:
# if first window being created, make a throwaway, hidden master root. This stops one user
# window from becoming the child of another user window. All windows are children of this
# hidden window
Window._IncrementOpenCount()
Window.hidden_master_root = tk.Tk()
Window.hidden_master_root.attributes('-alpha', 0) # HIDE this window really really really
# if not running_mac():
try:
Window.hidden_master_root.wm_overrideredirect(True)
except Exception as e:
print('* Error performing wm_overrideredirect in get file hiding the master root *', e)
Window.hidden_master_root.withdraw()
root = tk.Toplevel()
try:
root.attributes('-alpha', 0) # hide window while building it. makes for smoother 'paint'
# if not running_mac():
try:
root.wm_overrideredirect(True)
except Exception as e:
print('* Error performing wm_overrideredirect in get file *', e)
root.withdraw()
except:
pass
if show_hidden is False:
try:
# call a dummy dialog with an impossible option to initialize the file
# dialog without really getting a dialog window; this will throw a
# TclError, so we need a try...except :
try:
root.tk.call('tk_getOpenFile', '-foobarbaz')
except tk.TclError:
pass
# now set the magic variables accordingly
root.tk.call('set', '::tk::dialog::file::showHiddenBtn', '1')
root.tk.call('set', '::tk::dialog::file::showHiddenVar', '0')
except:
pass
if root and icon is not None:
_set_icon_for_tkinter_window(root, icon=icon)
# for Macs, setting parent=None fixes a warning problem.
if save_as:
filename = tk.filedialog.asksaveasfilename(filetypes=file_types,
initialdir=initial_folder,
initialfile=default_path,
parent=root if not running_mac() else None,
defaultextension=default_extension) # show the 'get file' dialog box
elif multiple_files:
filename = tk.filedialog.askopenfilenames(filetypes=file_types,
initialdir=initial_folder,
initialfile=default_path,
parent=root if not running_mac() else None,
defaultextension=default_extension) # show the 'get file' dialog box
else:
filename = tk.filedialog.askopenfilename(filetypes=file_types,
initialdir=initial_folder,
initialfile=default_path,
parent=root if not running_mac() else None,
defaultextension=default_extension) # show the 'get files' dialog box
root.destroy()
if not multiple_files and type(filename) in (tuple, list):
if len(filename): # only if not 0 length, otherwise will get an error
filename = filename[0]
return filename
if save_as:
browse_button = SaveAs(file_types=file_types, initial_folder=initial_folder, default_extension=default_extension)
elif multiple_files:
browse_button = FilesBrowse(file_types=file_types, initial_folder=initial_folder, files_delimiter=files_delimiter)
else:
browse_button = FileBrowse(file_types=file_types, initial_folder=initial_folder)
if image is not None:
if isinstance(image, str):
layout = [[Image(filename=image)]]
else:
layout = [[Image(data=image)]]
else:
layout = [[]]
layout += [[Text(message, auto_size_text=True, text_color=text_color, background_color=background_color)]]
if not history:
layout += [[InputText(default_text=default_path, size=size, key='-INPUT-'), browse_button]]
else:
file_list = history_settings.get("-PSG file list-", [])
last_entry = file_list[0] if file_list else ''
layout += [[Combo(file_list, default_value=last_entry, key='-INPUT-', size=size if size != (None, None) else (80, 1), bind_return_key=True),
browse_button, Button('Clear History', tooltip='Clears the list of files shown in the combobox')]]
layout += [[Button('Ok', size=(6, 1), bind_return_key=True), Button('Cancel', size=(6, 1))]]
window = Window(title=title or message, layout=layout, icon=icon, auto_size_text=True, button_color=button_color,
font=font, background_color=background_color, no_titlebar=no_titlebar, grab_anywhere=grab_anywhere, keep_on_top=keep_on_top, location=location, relative_location=relative_location, modal=modal, finalize=True)
if running_linux() and show_hidden is True:
window.TKroot.tk.eval('catch {tk_getOpenFile -badoption}') # dirty hack to force autoloading of Tk's file dialog code
window.TKroot.setvar('::tk::dialog::file::showHiddenBtn', 1) # enable the "show hidden files" checkbox (it's necessary)
window.TKroot.setvar('::tk::dialog::file::showHiddenVar', 0) # start with the hidden files... well... hidden
while True:
event, values = window.read()
if event in ('Cancel', WIN_CLOSED):
break
elif event == 'Clear History':
history_settings.set('-PSG file list-', [])
window['-INPUT-'].update('', [])
popup_quick_message('History of Previous Choices Cleared', background_color='red', text_color='white', font='_ 20', keep_on_top=True)
elif event in ('Ok', '-INPUT-'):
if values['-INPUT-'] != '':
if history_settings is not None:
list_of_entries = history_settings.get('-PSG file list-', [])
if values['-INPUT-'] in list_of_entries:
list_of_entries.remove(values['-INPUT-'])
list_of_entries.insert(0, values['-INPUT-'])
history_settings.set('-PSG file list-', list_of_entries)
break
window.close();
del window
if event in ('Cancel', WIN_CLOSED):
return None
return values['-INPUT-']
# --------------------------- popup_get_text ---------------------------
def popup_get_text(message, title=None, default_text='', password_char='', size=(None, None), button_color=None,
background_color=None, text_color=None, icon=None, font=None, no_titlebar=False,
grab_anywhere=False, keep_on_top=None, location=(None, None), relative_location=(None, None), image=None, modal=True):
"""
Display Popup with text entry field. Returns the text entered or None if closed / cancelled
:param message: message displayed to user
:type message: (str)
:param title: Window title
:type title: (str)
:param default_text: default value to put into input area
:type default_text: (str)
:param password_char: character to be shown instead of actually typed characters
:type password_char: (str)
:param size: (width, height) of the InputText Element
:type size: (int, int)
:param button_color: Color of the button (text, background)
:type button_color: (str, str) or str
:param background_color: background color of the entire window
:type background_color: (str)
:param text_color: color of the message text
:type text_color: (str)
:param icon: filename or base64 string to be used for the window's icon
:type icon: bytes | str
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param no_titlebar: If True no titlebar will be shown
:type no_titlebar: (bool)
:param grab_anywhere: If True can click and drag anywhere in the window to move the window
:type grab_anywhere: (bool)
:param keep_on_top: If True the window will remain above all current windows
:type keep_on_top: (bool)
:param location: (x,y) Location on screen to display the upper left corner of window
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param image: Image to include at the top of the popup window
:type image: (str) or (bytes)
:param modal: If True then makes the popup will behave like a Modal window... all other windows are non-operational until this one is closed. Default = True
:type modal: bool
:return: Text entered or None if window was closed or cancel button clicked
:rtype: str | None
"""
if image is not None:
if isinstance(image, str):
layout = [[Image(filename=image)]]
else:
layout = [[Image(data=image)]]
else:
layout = [[]]
layout += [[Text(message, auto_size_text=True, text_color=text_color, background_color=background_color)],
[InputText(default_text=default_text, size=size, key='-INPUT-', password_char=password_char)],
[Button('Ok', size=(6, 1), bind_return_key=True), Button('Cancel', size=(6, 1))]]
window = Window(title=title or message, layout=layout, icon=icon, auto_size_text=True, button_color=button_color, no_titlebar=no_titlebar,
background_color=background_color, grab_anywhere=grab_anywhere, keep_on_top=keep_on_top, location=location, relative_location=relative_location, finalize=True, modal=modal, font=font)
button, values = window.read()
window.close()
del window
if button != 'Ok':
return None
else:
path = values['-INPUT-']
return path
def popup_get_date(start_mon=None, start_day=None, start_year=None, begin_at_sunday_plus=0, no_titlebar=True, title='Choose Date', keep_on_top=True,
location=(None, None), relative_location=(None, None), close_when_chosen=False, icon=None, locale=None, month_names=None, day_abbreviations=None, modal=True):
"""
Display a calendar window, get the user's choice, return as a tuple (mon, day, year)
:param start_mon: The starting month
:type start_mon: (int)
:param start_day: The starting day - optional. Set to None or 0 if no date to be chosen at start
:type start_day: int | None
:param start_year: The starting year
:type start_year: (int)
:param begin_at_sunday_plus: Determines the left-most day in the display. 0=sunday, 1=monday, etc
:type begin_at_sunday_plus: (int)
:param icon: Same as Window icon parameter. Can be either a filename or Base64 value. For Windows if filename, it MUST be ICO format. For Linux, must NOT be ICO
:type icon: (str | bytes)
:param location: (x,y) location on the screen to place the top left corner of your window. Default is to center on screen
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param title: Title that will be shown on the window
:type title: (str)
:param close_when_chosen: If True, the window will close and function return when a day is clicked
:type close_when_chosen: (bool)
:param locale: locale used to get the day names
:type locale: (str)
:param no_titlebar: If True no titlebar will be shown
:type no_titlebar: (bool)
:param keep_on_top: If True the window will remain above all current windows
:type keep_on_top: (bool)
:param month_names: optional list of month names to use (should be 12 items)
:type month_names: List[str]
:param day_abbreviations: optional list of abbreviations to display as the day of week
:type day_abbreviations: List[str]
:param modal: If True then makes the popup will behave like a Modal window... all other windows are non-operational until this one is closed. Default = True
:type modal: bool
:return: Tuple containing (month, day, year) of chosen date or None if was cancelled
:rtype: None | (int, int, int)
"""
if month_names is not None and len(month_names) != 12:
if not SUPPRESS_ERROR_POPUPS:
popup_error('Incorrect month names list specified. Must have 12 entries.', 'Your list:', month_names)
if day_abbreviations is not None and len(day_abbreviations) != 7:
if not SUPPRESS_ERROR_POPUPS:
popup_error('Incorrect day abbreviation list. Must have 7 entries.', 'Your list:', day_abbreviations)
day_font = 'TkFixedFont 9'
mon_year_font = 'TkFixedFont 10'
arrow_font = 'TkFixedFont 7'
now = datetime.datetime.now()
cur_month, cur_day, cur_year = now.month, now.day, now.year
cur_month = start_mon or cur_month
if start_mon is not None:
cur_day = start_day
else:
cur_day = cur_day
cur_year = start_year or cur_year
def update_days(window, month, year, begin_at_sunday_plus):
[window[(week, day)].update('') for day in range(7) for week in range(6)]
weeks = calendar.monthcalendar(year, month)
month_days = list(itertools.chain.from_iterable([[0 for _ in range(8 - begin_at_sunday_plus)]] + weeks))
if month_days[6] == 0:
month_days = month_days[7:]
if month_days[6] == 0:
month_days = month_days[7:]
for i, day in enumerate(month_days):
offset = i
if offset >= 6 * 7:
break
window[(offset // 7, offset % 7)].update(str(day) if day else '')
def make_days_layout():
days_layout = []
for week in range(6):
row = []
for day in range(7):
row.append(T('', size=(4, 1), justification='c', font=day_font, key=(week, day), enable_events=True, pad=(0, 0)))
days_layout.append(row)
return days_layout
# Create table of month names and week day abbreviations
if day_abbreviations is None or len(day_abbreviations) != 7:
fwday = calendar.SUNDAY
try:
if locale is not None:
_cal = calendar.LocaleTextCalendar(fwday, locale)
else:
_cal = calendar.TextCalendar(fwday)
day_names = _cal.formatweekheader(3).split()
except Exception as e:
print('Exception building day names from locale', locale, e)
day_names = ('Sun', 'Mon', 'Tue', 'Wed', 'Th', 'Fri', 'Sat')
else:
day_names = day_abbreviations
mon_names = month_names if month_names is not None and len(month_names) == 12 else [calendar.month_name[i] for i in range(1, 13)]
days_layout = make_days_layout()
layout = [[B('◄◄', font=arrow_font, border_width=0, key='-YEAR-DOWN-', pad=((10, 2), 2)),
B('◄', font=arrow_font, border_width=0, key='-MON-DOWN-', pad=(0, 2)),
Text('{} {}'.format(mon_names[cur_month - 1], cur_year), size=(16, 1), justification='c', font=mon_year_font, key='-MON-YEAR-', pad=(0, 2)),
B('►', font=arrow_font, border_width=0, key='-MON-UP-', pad=(0, 2)),
B('►►', font=arrow_font, border_width=0, key='-YEAR-UP-', pad=(2, 2))]]
layout += [[Col([[T(day_names[i - (7 - begin_at_sunday_plus) % 7], size=(4, 1), font=day_font, background_color=theme_text_color(),
text_color=theme_background_color(), pad=(0, 0)) for i in range(7)]], background_color=theme_text_color(), pad=(0, 0))]]
layout += days_layout
if not close_when_chosen:
layout += [[Button('Ok', border_width=0, font='TkFixedFont 8'), Button('Cancel', border_width=0, font='TkFixedFont 8')]]
window = Window(title, layout, no_titlebar=no_titlebar, grab_anywhere=True, keep_on_top=keep_on_top, font='TkFixedFont 12', use_default_focus=False,
location=location, relative_location=relative_location, finalize=True, icon=icon)
update_days(window, cur_month, cur_year, begin_at_sunday_plus)
prev_choice = chosen_mon_day_year = None
if cur_day:
chosen_mon_day_year = cur_month, cur_day, cur_year
for week in range(6):
for day in range(7):
if window[(week, day)].DisplayText == str(cur_day):
window[(week, day)].update(background_color=theme_text_color(), text_color=theme_background_color())
prev_choice = (week, day)
break
if modal:
window.make_modal()
while True: # Event Loop
event, values = window.read()
if event in (None, 'Cancel'):
chosen_mon_day_year = None
break
if event == 'Ok':
break
if event in ('-MON-UP-', '-MON-DOWN-', '-YEAR-UP-', '-YEAR-DOWN-'):
cur_month += (event == '-MON-UP-')
cur_month -= (event == '-MON-DOWN-')
cur_year += (event == '-YEAR-UP-')
cur_year -= (event == '-YEAR-DOWN-')
if cur_month > 12:
cur_month = 1
cur_year += 1
elif cur_month < 1:
cur_month = 12
cur_year -= 1
window['-MON-YEAR-'].update('{} {}'.format(mon_names[cur_month - 1], cur_year))
update_days(window, cur_month, cur_year, begin_at_sunday_plus)
if prev_choice:
window[prev_choice].update(background_color=theme_background_color(), text_color=theme_text_color())
elif type(event) is tuple:
if window[event].DisplayText != "":
chosen_mon_day_year = cur_month, int(window[event].DisplayText), cur_year
if prev_choice:
window[prev_choice].update(background_color=theme_background_color(), text_color=theme_text_color())
window[event].update(background_color=theme_text_color(), text_color=theme_background_color())
prev_choice = event
if close_when_chosen:
break
window.close()
return chosen_mon_day_year
# --------------------------- PopupAnimated ---------------------------
def popup_animated(image_source, message=None, background_color=None, text_color=None, font=None, no_titlebar=True, grab_anywhere=True, keep_on_top=True,
location=(None, None), relative_location=(None, None), alpha_channel=None, time_between_frames=0, transparent_color=None, title='', icon=None):
"""
Show animation one frame at a time. This function has its own internal clocking meaning you can call it at any frequency
and the rate the frames of video is shown remains constant. Maybe your frames update every 30 ms but your
event loop is running every 10 ms. You don't have to worry about delaying, just call it every time through the
loop.
:param image_source: Either a filename or a base64 string. Use None to close the window.
:type image_source: str | bytes | None
:param message: An optional message to be shown with the animation
:type message: (str)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: str | tuple
:param no_titlebar: If True then the titlebar and window frame will not be shown
:type no_titlebar: (bool)
:param grab_anywhere: If True then you can move the window just clicking anywhere on window, hold and drag
:type grab_anywhere: (bool)
:param keep_on_top: If True then Window will remain on top of all other windows currently shownn
:type keep_on_top: (bool)
:param location: (x,y) location on the screen to place the top left corner of your window. Default is to center on screen
:type location: (int, int)
:param relative_location: (x,y) location relative to the default location of the window, in pixels. Normally the window centers. This location is relative to the location the window would be created. Note they can be negative.
:type relative_location: (int, int)
:param alpha_channel: Window transparency 0 = invisible 1 = completely visible. Values between are see through
:type alpha_channel: (float)
:param time_between_frames: Amount of time in milliseconds between each frame
:type time_between_frames: (int)
:param transparent_color: This color will be completely see-through in your window. Can even click through
:type transparent_color: (str)
:param title: Title that will be shown on the window
:type title: (str)
:param icon: Same as Window icon parameter. Can be either a filename or Base64 byte string. For Windows if filename, it MUST be ICO format. For Linux, must NOT be ICO
:type icon: str | bytes
:return: True if the window updated OK. False if the window was closed
:rtype: bool
"""
if image_source is None:
for image in Window._animated_popup_dict:
window = Window._animated_popup_dict[image]
window.close()
Window._animated_popup_dict = {}
return
if image_source not in Window._animated_popup_dict:
if type(image_source) is bytes or len(image_source) > 300:
layout = [[Image(data=image_source, background_color=background_color, key='-IMAGE-')], ]
else:
layout = [[Image(filename=image_source, background_color=background_color, key='-IMAGE-', )], ]
if message:
layout.append([Text(message, background_color=background_color, text_color=text_color, font=font)])
window = Window(title, layout, no_titlebar=no_titlebar, grab_anywhere=grab_anywhere,
keep_on_top=keep_on_top, background_color=background_color, location=location,
alpha_channel=alpha_channel, element_padding=(0, 0), margins=(0, 0),
transparent_color=transparent_color, finalize=True, element_justification='c', icon=icon, relative_location=relative_location)
Window._animated_popup_dict[image_source] = window
else:
window = Window._animated_popup_dict[image_source]
window['-IMAGE-'].update_animation(image_source, time_between_frames=time_between_frames)
event, values = window.read(1)
if event == WIN_CLOSED:
return False
# window.refresh() # call refresh instead of Read to save significant CPU time
return True
# Popup Notify
def popup_notify(*args, title='', icon=SYSTEM_TRAY_MESSAGE_ICON_INFORMATION, display_duration_in_ms=SYSTEM_TRAY_MESSAGE_DISPLAY_DURATION_IN_MILLISECONDS,
fade_in_duration=SYSTEM_TRAY_MESSAGE_FADE_IN_DURATION, alpha=0.9, location=None):
"""
Displays a "notification window", usually in the bottom right corner of your display. Has an icon, a title, and a message. It is more like a "toaster" window than the normal popups.
The window will slowly fade in and out if desired. Clicking on the window will cause it to move through the end the current "phase". For example, if the window was fading in and it was clicked, then it would immediately stop fading in and instead be fully visible. It's a way for the user to quickly dismiss the window.
The return code specifies why the call is returning (e.g. did the user click the message to dismiss it)
:param title: Text to be shown at the top of the window in a larger font
:type title: (str)
:param message: Text message that makes up the majority of the window
:type message: (str)
:param icon: A base64 encoded PNG/GIF image or PNG/GIF filename that will be displayed in the window
:type icon: bytes | str
:param display_duration_in_ms: Number of milliseconds to show the window
:type display_duration_in_ms: (int)
:param fade_in_duration: Number of milliseconds to fade window in and out
:type fade_in_duration: (int)
:param alpha: Alpha channel. 0 - invisible 1 - fully visible
:type alpha: (float)
:param location: Location on the screen to display the window
:type location: (int, int)
:return: reason for returning
:rtype: (int)
"""
if not args:
args_to_print = ['']
else:
args_to_print = args
output = ''
max_line_total, total_lines, local_line_width = 0, 0, SYSTEM_TRAY_MESSAGE_MAX_LINE_LENGTH
for message in args_to_print:
# fancy code to check if string and convert if not is not need. Just always convert to string :-)
# if not isinstance(message, str): message = str(message)
message = str(message)
if message.count('\n'):
message_wrapped = message
else:
message_wrapped = textwrap.fill(message, local_line_width)
message_wrapped_lines = message_wrapped.count('\n') + 1
longest_line_len = max([len(l) for l in message.split('\n')])
width_used = min(longest_line_len, local_line_width)
max_line_total = max(max_line_total, width_used)
# height = _GetNumLinesNeeded(message, width_used)
height = message_wrapped_lines
output += message_wrapped + '\n'
total_lines += height
message = output
# def __init__(self, menu=None, filename=None, data=None, data_base64=None, tooltip=None, metadata=None):
return SystemTray.notify(title=title, message=message, icon=icon, display_duration_in_ms=display_duration_in_ms, fade_in_duration=fade_in_duration,
alpha=alpha, location=location)
def popup_menu(window, element, menu_def, title=None, location=(None, None)):
"""
Makes a "popup menu"
This type of menu is what you get when a normal menu or a right click menu is torn off
The settings for the menu are obtained from the window parameter's Window
:param window: The window associated with the popup menu. The theme and right click menu settings for this window will be used
:type window: Window
:param element: An element in your window to associate the menu to. It can be any element
:type element: Element
:param menu_def: A menu definition. This will be the same format as used for Right Click Menus1
:type menu_def: List[List[ List[str] | str ]]
:param title: The title that will be shown on the torn off menu window. Defaults to window titlr
:type title: str
:param location: The location on the screen to place the window
:type location: (int, int) | (None, None)
"""
element._popup_menu_location = location
top_menu = tk.Menu(window.TKroot, tearoff=True, tearoffcommand=element._tearoff_menu_callback)
if window.right_click_menu_background_color not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(bg=window.right_click_menu_background_color)
if window.right_click_menu_text_color not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(fg=window.right_click_menu_text_color)
if window.right_click_menu_disabled_text_color not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(disabledforeground=window.right_click_menu_disabled_text_color)
if window.right_click_menu_font is not None:
top_menu.config(font=window.right_click_menu_font)
if window.right_click_menu_selected_colors[0] != COLOR_SYSTEM_DEFAULT:
top_menu.config(activeforeground=window.right_click_menu_selected_colors[0])
if window.right_click_menu_selected_colors[1] != COLOR_SYSTEM_DEFAULT:
top_menu.config(activebackground=window.right_click_menu_selected_colors[1])
top_menu.config(title=window.Title if title is None else title)
AddMenuItem(top_menu, menu_def[1], element, right_click_menu=True)
# element.Widget.bind('<Button-3>', element._RightClickMenuCallback)
top_menu.invoke(0)
def popup_error_with_traceback(title, *messages):
"""
Show an error message and as many additoinal lines of messages as you want.
Will show the same error window as PySimpleGUI uses internally. Has a button to
take the user to the line of code you called this popup from
:param title: The title that will be shown in the popup's titlebar and in the first line of the window
:type title: str
:param messages: A variable number of lines of messages you wish to show your user
:type messages: *Any
"""
# For now, call the function that PySimpleGUI uses internally
_error_popup_with_traceback(str(title), *messages)
def _error_popup_with_traceback(title, *args):
if SUPPRESS_ERROR_POPUPS:
return
trace_details = traceback.format_stack()
error_message = ''
file_info_pysimplegui = None
for line in reversed(trace_details):
if __file__ not in line:
file_info_pysimplegui = line.split(",")[0]
error_message = line
break
if file_info_pysimplegui is None:
_error_popup_with_code(title, None, None, 'Did not find your traceback info', *args)
return
error_parts = None
if error_message != '':
error_parts = error_message.split(', ')
if len(error_parts) < 4:
error_message = error_parts[0] + '\n' + error_parts[1] + '\n' + ''.join(error_parts[2:])
if error_parts is None:
print('*** Error popup attempted but unable to parse error details ***')
print(trace_details)
return
filename = error_parts[0][error_parts[0].index('File ') + 5:]
line_num = error_parts[1][error_parts[1].index('line ') + 5:]
_error_popup_with_code(title, filename, line_num, error_message, *args)
def _error_popup_with_code(title, filename=None, line_num=None, *args):
layout = [[Text('ERROR'), Text(title)],
[Image(data=_random_error_emoji())]]
# make max length of line be 90 chars and allow the height to vary based on number of needed lines
lines = [str(msg).split('\n') for msg in args]
max_line_len = 0
for line in lines:
max_line_len = max(max_line_len, max([len(s) for s in line]))
layout += [[Text(str(msg), size=(min(max_line_len, 90), None))] for msg in args]
layout += [[Button('Close'), Button('Take me to error'), Button('Kill Application', button_color='white on red')]]
window = Window(title, layout, keep_on_top=True)
while True:
event, values = window.read()
if event in ('Close', WIN_CLOSED):
break
if event == 'Kill Application':
window.close()
popup_quick_message('KILLING APP! BYE!', font='_ 18', keep_on_top=True, text_color='white', background_color='red', non_blocking=False)
sys.exit()
if event == 'Take me to error' and filename is not None and line_num is not None:
execute_editor(filename, line_num)
window.close()
#####################################################################
# Animated window while shell command is executed
#####################################################################
def _process_thread(*args):
global __shell_process__
# start running the command with arugments
try:
__shell_process__ = subprocess.run(args, shell=True, stdout=subprocess.PIPE)
except Exception as e:
print('Exception running process args = {}'.format(args))
__shell_process__ = None
def shell_with_animation(command, args=None, image_source=DEFAULT_BASE64_LOADING_GIF, message=None, background_color=None, text_color=None, font=None,
no_titlebar=True, grab_anywhere=True, keep_on_top=True, location=(None, None), alpha_channel=None, time_between_frames=100,
transparent_color=None):
"""
Execute a "shell command" (anything capable of being launched using subprocess.run) and
while the command is running, show an animated popup so that the user knows that a long-running
command is being executed. Without this mechanism, the GUI appears locked up.
:param command: The command to run
:type command: (str)
:param args: List of arguments
:type args: List[str]
:param image_source: Either a filename or a base64 string.
:type image_source: str | bytes
:param message: An optional message to be shown with the animation
:type message: (str)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: str | tuple
:param no_titlebar: If True then the titlebar and window frame will not be shown
:type no_titlebar: (bool)
:param grab_anywhere: If True then you can move the window just clicking anywhere on window, hold and drag
:type grab_anywhere: (bool)
:param keep_on_top: If True then Window will remain on top of all other windows currently shownn
:type keep_on_top: (bool)
:param location: (x,y) location on the screen to place the top left corner of your window. Default is to center on screen
:type location: (int, int)
:param alpha_channel: Window transparency 0 = invisible 1 = completely visible. Values between are see through
:type alpha_channel: (float)
:param time_between_frames: Amount of time in milliseconds between each frame
:type time_between_frames: (int)
:param transparent_color: This color will be completely see-through in your window. Can even click through
:type transparent_color: (str)
:return: The resulting string output from stdout
:rtype: (str)
"""
global __shell_process__
real_args = [command]
if args is not None:
for arg in args:
real_args.append(arg)
# real_args.append(args)
thread = threading.Thread(target=_process_thread, args=real_args, daemon=True)
thread.start()
# Poll to see if the thread is still running. If so, then continue showing the animation
while True:
popup_animated(image_source=image_source, message=message, time_between_frames=time_between_frames, transparent_color=transparent_color,
text_color=text_color, background_color=background_color, font=font, no_titlebar=no_titlebar, grab_anywhere=grab_anywhere,
keep_on_top=keep_on_top, location=location, alpha_channel=alpha_channel)
thread.join(timeout=time_between_frames / 1000)
if not thread.is_alive():
break
popup_animated(None) # stop running the animation
output = __shell_process__.__str__().replace('\\r\\n', '\n') # fix up the output string
output = output[output.index("stdout=b'") + 9:-2]
return output
#######################################################################
# 8888888888
# 888
# 888
# 8888888 888d888 888d888 .d88b. 888d888
# 888 888P" 888P" d88""88b 888P"
# 888 888 888 888 888 888
# 888 888 888 Y88..88P 888
# 8888888888 888 888 "Y88P" 888
#
#
#
# 888b d888
# 8888b d8888
# 88888b.d88888
# 888Y88888P888 .d88b. .d8888b .d8888b 8888b. .d88b. .d88b.
# 888 Y888P 888 d8P Y8b 88K 88K "88b d88P"88b d8P Y8b
# 888 Y8P 888 88888888 "Y8888b. "Y8888b. .d888888 888 888 88888888
# 888 " 888 Y8b. X88 X88 888 888 Y88b 888 Y8b.
# 888 888 "Y8888 88888P' 88888P' "Y888888 "Y88888 "Y8888
# 888
# Y8b d88P
# "Y88P"
# Code to make messages to help user find errors in their code
#######################################################################
def _create_error_message():
"""
Creates an error message containing the filename and line number of the users
code that made the call into PySimpleGUI
:return: Error string to display with file, line number, and line of code
:rtype: str
"""
called_func = inspect.stack()[1].function
trace_details = traceback.format_stack()
error_message = ''
file_info_pysimplegui = trace_details[-1].split(",")[0]
for line in reversed(trace_details):
if line.split(",")[0] != file_info_pysimplegui:
error_message = line
break
if error_message != '':
error_parts = error_message.split(', ')
if len(error_parts) < 4:
error_message = error_parts[0] + '\n' + error_parts[1] + '\n' + ''.join(error_parts[2:])
return 'The PySimpleGUI internal reporting function is ' + called_func + '\n' + \
'The error originated from:\n' + error_message
# .d8888b. 888 888 d8b
# d88P Y88b 888 888 Y8P
# Y88b. 888 888
# "Y888b. .d88b. 888888 888888 888 88888b. .d88b. .d8888b
# "Y88b. d8P Y8b 888 888 888 888 "88b d88P"88b 88K
# "888 88888888 888 888 888 888 888 888 888 "Y8888b.
# Y88b d88P Y8b. Y88b. Y88b. 888 888 888 Y88b 888 X88
# "Y8888P" "Y8888 "Y888 "Y888 888 888 888 "Y88888 88888P'
# 888
# Y8b d88P
# "Y88P"
# Interface to saving / loading user program settings in json format
# This is a new set of APIs supplied by PySimpleGUI that enables users to easily set/save/load individual
# settings. They are automatically saved to a JSON file. If no file/path is specified then a filename is
# created from the source file filename.
class UserSettings:
# A reserved settings object for use by the setting functions. It's a way for users
# to access the user settings without diarectly using the UserSettings class
_default_for_function_interface = None # type: UserSettings
def __init__(self, filename=None, path=None, silent_on_error=False, autosave=True, use_config_file=None, convert_bools_and_none=True):
"""
User Settings
:param filename: The name of the file to use. Can be a full path and filename or just filename
:type filename: (str or None)
:param path: The folder that the settings file will be stored in. Do not include the filename.
:type path: (str or None)
:param silent_on_error: If True errors will not be reported
:type silent_on_error: (bool)
:param autosave: If True the settings file is saved after every update
:type autosave: (bool)
:param use_config_file: If True then the file format will be a config.ini rather than json
:type use_config_file: (bool)
:param convert_bools_and_none: If True then "True", "False", "None" will be converted to the Python values True, False, None when using INI files. Default is TRUE
:type convert_bools_and_none: (bool)
"""
self.path = path
self.filename = filename
self.full_filename = None
self.dict = {}
self.default_value = None
self.silent_on_error = silent_on_error
self.autosave = autosave
if filename is not None and filename.endswith('.ini') and use_config_file is None:
warnings.warn('[UserSettings] You have specified a filename with .ini extension but did not set use_config_file. Setting use_config_file for you.', UserWarning)
use_config_file = True
self.use_config_file = use_config_file
# self.retain_config_comments = retain_config_comments
self.convert_bools = convert_bools_and_none
if use_config_file:
self.config = configparser.ConfigParser()
self.config.optionxform = str
# self.config_dict = {}
self.section_class_dict = {} # type: dict[_SectionDict]
if filename is not None or path is not None:
self.load(filename=filename, path=path)
########################################################################################################
## FIRST is the _SectionDict helper class
## It is typically not directly accessed, although it is possible to call delete_section, get, set
########################################################################################################
class _SectionDict:
item_count = 0
def __init__(self, section_name, section_dict, config, user_settings_parent): # (str, Dict, configparser.ConfigParser)
"""
The Section Dictionary. It holds the values for a section.
:param section_name: Name of the section
:type section_name: str
:param section_dict: Dictionary of values for the section
:type section_dict: dict
:param config: The configparser object
:type config: configparser.ConfigParser
:param user_settings_parent: The parent UserSettings object that hdas this section
:type user_settings_parent: UserSettings
"""
self.section_name = section_name
self.section_dict = section_dict # type: Dict
self.new_section = False
self.config = config # type: configparser.ConfigParser
self.user_settings_parent = user_settings_parent # type: UserSettings
UserSettings._SectionDict.item_count += 1
if self.user_settings_parent.convert_bools:
for key, value in self.section_dict.items():
if value == 'True':
value = True
self.section_dict[key] = value
elif value == 'False':
value = False
self.section_dict[key] = value
elif value == 'None':
value = None
self.section_dict[key] = value
# print(f'++++++ making a new SectionDict with name = {section_name}')
def __repr__(self):
"""
Converts the settings dictionary into a string for easy display
:return: the dictionary as a string
:rtype: (str)
"""
return_string = '{}:\n'.format(self.section_name)
for entry in self.section_dict.keys():
return_string += ' {} : {}\n'.format(entry, self.section_dict[entry])
return return_string
def get(self, key, default=None):
"""
Returns the value of a specified setting. If the setting is not found in the settings dictionary, then
the user specified default value will be returned. It no default is specified and nothing is found, then
the "default value" is returned. This default can be specified in this call, or previously defined
by calling set_default. If nothing specified now or previously, then None is returned as default.
:param key: Key used to lookup the setting in the settings dictionary
:type key: (Any)
:param default: Value to use should the key not be found in the dictionary
:type default: (Any)
:return: Value of specified settings
:rtype: (Any)
"""
value = self.section_dict.get(key, default)
if self.user_settings_parent.convert_bools:
if value == 'True':
value = True
elif value == 'False':
value = False
return value
def set(self, key, value):
value = str(value) # all values must be strings
if self.new_section:
self.config.add_section(self.section_name)
self.new_section = False
self.config.set(section=self.section_name, option=key, value=value)
self.section_dict[key] = value
if self.user_settings_parent.autosave:
self.user_settings_parent.save()
def delete_section(self):
# print(f'** Section Dict deleting section = {self.section_name}')
self.config.remove_section(section=self.section_name)
del self.user_settings_parent.section_class_dict[self.section_name]
if self.user_settings_parent.autosave:
self.user_settings_parent.save()
def __getitem__(self, item):
# print('*** In SectionDict Get ***')
return self.get(item)
def __setitem__(self, item, value):
"""
Enables setting a setting by using [ ] notation like a dictionary.
Your code will have this kind of design pattern:
settings = sg.UserSettings()
settings[item] = value
:param item: The key for the setting to change. Needs to be a hashable type. Basically anything but a list
:type item: Any
:param value: The value to set the setting to
:type value: Any
"""
# print(f'*** In SectionDict SET *** item = {item} value = {value}')
self.set(item, value)
self.section_dict[item] = value
def __delitem__(self, item):
"""
Delete an individual user setting. This is the same as calling delete_entry. The syntax
for deleting the item using this manner is:
del settings['entry']
:param item: The key for the setting to delete
:type item: Any
"""
# print(f'** In SectionDict delete! section name = {self.section_name} item = {item} ')
self.config.remove_option(section=self.section_name, option=item)
try:
del self.section_dict[item]
except Exception as e:
pass
# print(e)
if self.user_settings_parent.autosave:
self.user_settings_parent.save()
########################################################################################################
def __repr__(self):
"""
Converts the settings dictionary into a string for easy display
:return: the dictionary as a string
:rtype: (str)
"""
if not self.use_config_file:
return pprint.pformat(self.dict)
else:
# rvalue = '-------------------- Settings ----------------------\n'
rvalue = ''
for name, section in self.section_class_dict.items():
rvalue += str(section)
# rvalue += '\n-------------------- Settings End----------------------\n'
rvalue += '\n'
return rvalue
# return str(self.dict) # previouisly returned just a string version of the dictionary
def set_default_value(self, default):
"""
Set the value that will be returned if a requested setting is not found
:param default: value to be returned if a setting is not found in the settings dictionary
:type default: Any
"""
self.default_value = default
def _compute_filename(self, filename=None, path=None):
"""
Creates the full filename given the path or the filename or both.
:param filename: The name of the file to use. Can be a full path and filename or just filename
:type filename: (str or None)
:param path: The folder that the settings file will be stored in. Do not include the filename.
:type path: (str or None)
:return: Tuple with (full filename, path, filename)
:rtype: Tuple[str, str, str]
"""
if filename is not None:
dirname_from_filename = os.path.dirname(filename) # see if a path was provided as part of filename
if dirname_from_filename:
path = dirname_from_filename
filename = os.path.basename(filename)
elif self.filename is not None:
filename = self.filename
else:
if not self.use_config_file:
filename = os.path.splitext(os.path.basename(sys.modules["__main__"].__file__))[0] + '.json'
else:
filename = os.path.splitext(os.path.basename(sys.modules["__main__"].__file__))[0] + '.ini'
if path is None:
if self.path is not None:
# path = self.path
path = os.path.expanduser(self.path) # expand user provided path in case it has user ~ in it. Don't think it'll hurt
elif DEFAULT_USER_SETTINGS_PATH is not None: # if user set the path manually system-wide using set options
path = os.path.expanduser(DEFAULT_USER_SETTINGS_PATH)
elif running_trinket():
path = os.path.expanduser(DEFAULT_USER_SETTINGS_TRINKET_PATH)
elif running_replit():
path = os.path.expanduser(DEFAULT_USER_SETTINGS_REPLIT_PATH)
elif running_windows():
path = os.path.expanduser(DEFAULT_USER_SETTINGS_WIN_PATH)
elif running_linux():
path = os.path.expanduser(DEFAULT_USER_SETTINGS_LINUX_PATH)
elif running_mac():
path = os.path.expanduser(DEFAULT_USER_SETTINGS_MAC_PATH)
else:
path = '.'
full_filename = os.path.join(path, filename)
return (full_filename, path, filename)
def set_location(self, filename=None, path=None):
"""
Sets the location of the settings file
:param filename: The name of the file to use. Can be a full path and filename or just filename
:type filename: (str or None)
:param path: The folder that the settings file will be stored in. Do not include the filename.
:type path: (str or None)
"""
cfull_filename, cpath, cfilename = self._compute_filename(filename=filename, path=path)
self.filename = cfilename
self.path = cpath
self.full_filename = cfull_filename
def get_filename(self, filename=None, path=None):
"""
Sets the filename and path for your settings file. Either paramter can be optional.
If you don't choose a path, one is provided for you that is OS specific
Windows path default = users/name/AppData/Local/PySimpleGUI/settings.
If you don't choose a filename, your application's filename + '.json' will be used.
Normally the filename and path are split in the user_settings calls. However for this call they
can be combined so that the filename contains both the path and filename.
:param filename: The name of the file to use. Can be a full path and filename or just filename
:type filename: (str or None)
:param path: The folder that the settings file will be stored in. Do not include the filename.
:type path: (str or None)
:return: The full pathname of the settings file that has both the path and filename combined.
:rtype: (str)
"""
if filename is not None or path is not None or (filename is None and path is None and self.full_filename is None):
self.set_location(filename=filename, path=path)
self.read()
return self.full_filename
#
# def merge_comments_from_file(self, full_filename):
# print('--- merging comments -----')
# merged_lines = []
# with open(full_filename, 'r') as f:
# new_file_contents = f.readlines()
# current_section = ''
# for line in new_file_contents:
# if len(line) == 0: # skip blank lines
# merged_lines.append(line)
# continue
# if line[0] == '[': # if a new section
# current_section = line[:line.index(']')]
# merged_lines.append(line)
# continue
# if len(line.lstrip()):
# if line.lstrip()[0] == '#': # if a comment line, save it
# merged_lines.append(line)
# # Process a line with an = in it
# try:
# key = line[:line.index('=')]
# merged_lines.append(line)
# except:
# merged_lines.append(line)
# print('--- merging complete ----')
# print(*merged_lines)
#
def save(self, filename=None, path=None):
"""
Saves the current settings dictionary. If a filename or path is specified in the call, then it will override any
previously specitfied filename to create a new settings file. The settings dictionary is then saved to the newly defined file.
:param filename: The fFilename to save to. Can specify a path or just the filename. If no filename specified, then the caller's filename will be used.
:type filename: (str or None)
:param path: The (optional) path to use to save the file.
:type path: (str or None)
:return: The full path and filename used to save the settings
:rtype: (str)
"""
if filename is not None or path is not None:
self.set_location(filename=filename, path=path)
try:
if not os.path.exists(self.path):
os.makedirs(self.path)
with open(self.full_filename, 'w') as f:
if not self.use_config_file:
json.dump(self.dict, f)
else:
self.config.write(f)
except Exception as e:
if not self.silent_on_error:
_error_popup_with_traceback('UserSettings.save error', '*** UserSettings.save() Error saving settings to file:***\n', self.full_filename, e)
# if self.use_config_file and self.retain_config_comments:
# self.merge_comments_from_file(self.full_filename)
return self.full_filename
def load(self, filename=None, path=None):
"""
Specifies the path and filename to use for the settings and reads the contents of the file.
The filename can be a full filename including a path, or the path can be specified separately.
If no filename is specified, then the caller's filename will be used with the extension ".json"
:param filename: Filename to load settings from (and save to in the future)
:type filename: (str or None)
:param path: Path to the file. Defaults to a specific folder depending on the operating system
:type path: (str or None)
:return: The settings dictionary (i.e. all settings)
:rtype: (dict)
"""
if filename is not None or path is not None or self.full_filename is None:
self.set_location(filename, path)
self.read()
return self.dict
def delete_file(self, filename=None, path=None):
"""
Deltes the filename and path for your settings file. Either paramter can be optional.
If you don't choose a path, one is provided for you that is OS specific
Windows path default = users/name/AppData/Local/PySimpleGUI/settings.
If you don't choose a filename, your application's filename + '.json' will be used
Also sets your current dictionary to a blank one.
:param filename: The name of the file to use. Can be a full path and filename or just filename
:type filename: (str or None)
:param path: The folder that the settings file will be stored in. Do not include the filename.
:type path: (str or None)
"""
if filename is not None or path is not None or (filename is None and path is None):
self.set_location(filename=filename, path=path)
try:
os.remove(self.full_filename)
except Exception as e:
if not self.silent_on_error:
_error_popup_with_traceback('UserSettings delete_file warning ***', 'Exception trying to perform os.remove', e)
# print(_create_error_message())
self.dict = {}
def write_new_dictionary(self, settings_dict):
"""
Writes a specified dictionary to the currently defined settings filename.
:param settings_dict: The dictionary to be written to the currently defined settings file
:type settings_dict: (dict)
"""
if self.full_filename is None:
self.set_location()
self.dict = settings_dict
self.save()
# def as_dict(config):
# """
# Converts a ConfigParser object into a dictionary.
#
# The resulting dictionary has sections as keys which point to a dict of the
# sections options as key => value pairs.
# """
# the_dict = {}
# for section in config.sections():
# the_dict[section] = {}
# for key, val in config.items(section):
# the_dict[section][key] = val
# return the_dict
def read(self):
"""
Reads settings file and returns the dictionary.
If you have anything changed in an existing settings dictionary, you will lose your changes.
:return: settings dictionary
:rtype: (dict)
"""
if self.full_filename is None:
return {}
try:
if os.path.exists(self.full_filename):
with open(self.full_filename, 'r') as f:
if not self.use_config_file: # if using json
self.dict = json.load(f)
else: # if using a config file
self.config.read_file(f)
# Make a dictionary of SectionDict classses. Keys are the config.sections().
self.section_class_dict = {}
for section in self.config.sections():
section_dict = dict(self.config[section])
self.section_class_dict[section] = self._SectionDict(section, section_dict, self.config, self)
self.dict = self.section_class_dict
self.config_sections = self.config.sections()
# self.config_dict = {section_name : dict(self.config[section_name]) for section_name in self.config.sections()}
# if self.retain_config_comments:
# self.config_file_contents = f.readlines()
except Exception as e:
if not self.silent_on_error:
_error_popup_with_traceback('User Settings read warning', 'Error reading settings from file', self.full_filename, e)
# print('*** UserSettings.read - Error reading settings from file: ***\n', self.full_filename, e)
# print(_create_error_message())
return self.dict
def exists(self, filename=None, path=None):
"""
Check if a particular settings file exists. Returns True if file exists
:param filename: The name of the file to use. Can be a full path and filename or just filename
:type filename: (str or None)
:param path: The folder that the settings file will be stored in. Do not include the filename.
:type path: (str or None)
"""
cfull_filename, cpath, cfilename = self._compute_filename(filename=filename, path=path)
if os.path.exists(cfull_filename):
return True
return False
def delete_entry(self, key, section=None):
"""
Deletes an individual entry. If no filename has been specified up to this point,
then a default filename will be used.
After value has been deleted, the settings file is written to disk.
:param key: Setting to be deleted. Can be any valid dictionary key type (i.e. must be hashable)
:type key: (Any)
"""
if self.full_filename is None:
self.set_location()
self.read()
if not self.use_config_file: # Is using JSON file
if key in self.dict:
del self.dict[key]
if self.autosave:
self.save()
else:
if not self.silent_on_error:
_error_popup_with_traceback('User Settings delete_entry Warning - key', key, ' not found in settings')
else:
if section is not None:
section_dict = self.get(section)
# print(f'** Trying to delete an entry with a config file in use ** id of section_dict = {id(section_dict)}')
# section_dict = self.section_class_dict[section]
del self.get(section)[key]
# del section_dict[key]
# del section_dict[key]
def delete_section(self, section):
"""
Deletes a section with the name provided in the section parameter. Your INI file will be saved afterwards if auto-save enabled (default is ON)
:param section: Name of the section to delete
:type section: str
"""
if not self.use_config_file:
return
section_dict = self.section_class_dict.get(section, None)
section_dict.delete_section()
del self.section_class_dict[section]
if self.autosave:
self.save()
def set(self, key, value):
"""
Sets an individual setting to the specified value. If no filename has been specified up to this point,
then a default filename will be used.
After value has been modified, the settings file is written to disk.
Note that this call is not value for a config file normally. If it is, then the key is assumed to be the
Section key and the value written will be the default value.
:param key: Setting to be saved. Can be any valid dictionary key type
:type key: (Any)
:param value: Value to save as the setting's value. Can be anything
:type value: (Any)
:return: value that key was set to
:rtype: (Any)
"""
if self.full_filename is None:
self.set_location()
# if not autosaving, then don't read the file or else will lose changes
if not self.use_config_file:
if self.autosave or self.dict == {}:
self.read()
self.dict[key] = value
else:
self.section_class_dict[key].set(value, self.default_value)
if self.autosave:
self.save()
return value
def get(self, key, default=None):
"""
Returns the value of a specified setting. If the setting is not found in the settings dictionary, then
the user specified default value will be returned. It no default is specified and nothing is found, then
the "default value" is returned. This default can be specified in this call, or previously defined
by calling set_default. If nothing specified now or previously, then None is returned as default.
:param key: Key used to lookup the setting in the settings dictionary
:type key: (Any)
:param default: Value to use should the key not be found in the dictionary
:type default: (Any)
:return: Value of specified settings
:rtype: (Any)
"""
if self.default_value is not None:
default = self.default_value
if self.full_filename is None:
self.set_location()
if self.autosave or self.dict == {}:
self.read()
if not self.use_config_file:
value = self.dict.get(key, default)
else:
value = self.section_class_dict.get(key, None)
if key not in list(self.section_class_dict.keys()):
self.section_class_dict[key] = self._SectionDict(key, {}, self.config, self)
value = self.section_class_dict[key]
value.new_section = True
return value
def get_dict(self):
"""
Returns the current settings dictionary. If you've not setup the filename for the
settings, a default one will be used and then read.
Note that you can display the dictionary in text format by printing the object itself.
:return: The current settings dictionary
:rtype: Dict
"""
if self.full_filename is None:
self.set_location()
if self.autosave or self.dict == {}:
self.read()
self.save()
return self.dict
def __setitem__(self, item, value):
"""
Enables setting a setting by using [ ] notation like a dictionary.
Your code will have this kind of design pattern:
settings = sg.UserSettings()
settings[item] = value
:param item: The key for the setting to change. Needs to be a hashable type. Basically anything but a list
:type item: Any
:param value: The value to set the setting to
:type value: Any
"""
return self.set(item, value)
def __getitem__(self, item):
"""
Enables accessing a setting using [ ] notation like a dictionary.
If the entry does not exist, then the default value will be returned. This default
value is None unless user sets by calling UserSettings.set_default_value(default_value)
:param item: The key for the setting to change. Needs to be a hashable type. Basically anything but a list
:type item: Any
:return: The setting value
:rtype: Any
"""
return self.get(item, self.default_value)
def __delitem__(self, item):
"""
Delete an individual user setting. This is the same as calling delete_entry. The syntax
for deleting the item using this manner is:
del settings['entry']
:param item: The key for the setting to delete
:type item: Any
"""
if self.use_config_file:
return self.get(item)
else:
self.delete_entry(key=item)
# Create a singleton for the settings information so that the settings functions can be used
if UserSettings._default_for_function_interface is None:
UserSettings._default_for_function_interface = UserSettings()
def user_settings_filename(filename=None, path=None):
"""
Sets the filename and path for your settings file. Either paramter can be optional.
If you don't choose a path, one is provided for you that is OS specific
Windows path default = users/name/AppData/Local/PySimpleGUI/settings.
If you don't choose a filename, your application's filename + '.json' will be used.
Normally the filename and path are split in the user_settings calls. However for this call they
can be combined so that the filename contains both the path and filename.
:param filename: The name of the file to use. Can be a full path and filename or just filename
:type filename: (str)
:param path: The folder that the settings file will be stored in. Do not include the filename.
:type path: (str)
:return: The full pathname of the settings file that has both the path and filename combined.
:rtype: (str)
"""
settings = UserSettings._default_for_function_interface
return settings.get_filename(filename, path)
def user_settings_delete_filename(filename=None, path=None):
"""
Deltes the filename and path for your settings file. Either paramter can be optional.
If you don't choose a path, one is provided for you that is OS specific
Windows path default = users/name/AppData/Local/PySimpleGUI/settings.
If you don't choose a filename, your application's filename + '.json' will be used
Also sets your current dictionary to a blank one.
:param filename: The name of the file to use. Can be a full path and filename or just filename
:type filename: (str)
:param path: The folder that the settings file will be stored in. Do not include the filename.
:type path: (str)
"""
settings = UserSettings._default_for_function_interface
settings.delete_file(filename, path)
def user_settings_set_entry(key, value):
"""
Sets an individual setting to the specified value. If no filename has been specified up to this point,
then a default filename will be used.
After value has been modified, the settings file is written to disk.
:param key: Setting to be saved. Can be any valid dictionary key type
:type key: (Any)
:param value: Value to save as the setting's value. Can be anything
:type value: (Any)
"""
settings = UserSettings._default_for_function_interface
settings.set(key, value)
def user_settings_delete_entry(key):
"""
Deletes an individual entry. If no filename has been specified up to this point,
then a default filename will be used.
After value has been deleted, the settings file is written to disk.
:param key: Setting to be saved. Can be any valid dictionary key type (hashable)
:type key: (Any)
"""
settings = UserSettings._default_for_function_interface
settings.delete_entry(key)
def user_settings_get_entry(key, default=None):
"""
Returns the value of a specified setting. If the setting is not found in the settings dictionary, then
the user specified default value will be returned. It no default is specified and nothing is found, then
None is returned. If the key isn't in the dictionary, then it will be added and the settings file saved.
If no filename has been specified up to this point, then a default filename will be assigned and used.
The settings are SAVED prior to returning.
:param key: Key used to lookup the setting in the settings dictionary
:type key: (Any)
:param default: Value to use should the key not be found in the dictionary
:type default: (Any)
:return: Value of specified settings
:rtype: (Any)
"""
settings = UserSettings._default_for_function_interface
return settings.get(key, default)
def user_settings_save(filename=None, path=None):
"""
Saves the current settings dictionary. If a filename or path is specified in the call, then it will override any
previously specitfied filename to create a new settings file. The settings dictionary is then saved to the newly defined file.
:param filename: The fFilename to save to. Can specify a path or just the filename. If no filename specified, then the caller's filename will be used.
:type filename: (str)
:param path: The (optional) path to use to save the file.
:type path: (str)
:return: The full path and filename used to save the settings
:rtype: (str)
"""
settings = UserSettings._default_for_function_interface
return settings.save(filename, path)
def user_settings_load(filename=None, path=None):
"""
Specifies the path and filename to use for the settings and reads the contents of the file.
The filename can be a full filename including a path, or the path can be specified separately.
If no filename is specified, then the caller's filename will be used with the extension ".json"
:param filename: Filename to load settings from (and save to in the future)
:type filename: (str)
:param path: Path to the file. Defaults to a specific folder depending on the operating system
:type path: (str)
:return: The settings dictionary (i.e. all settings)
:rtype: (dict)
"""
settings = UserSettings._default_for_function_interface
return settings.load(filename, path)
def user_settings_file_exists(filename=None, path=None):
"""
Determines if a settings file exists. If so a boolean True is returned.
If either a filename or a path is not included, then the appropriate default
will be used.
:param filename: Filename to check
:type filename: (str)
:param path: Path to the file. Defaults to a specific folder depending on the operating system
:type path: (str)
:return: True if the file exists
:rtype: (bool)
"""
settings = UserSettings._default_for_function_interface
return settings.exists(filename=filename, path=path)
def user_settings_write_new_dictionary(settings_dict):
"""
Writes a specified dictionary to the currently defined settings filename.
:param settings_dict: The dictionary to be written to the currently defined settings file
:type settings_dict: (dict)
"""
settings = UserSettings._default_for_function_interface
settings.write_new_dictionary(settings_dict)
def user_settings_silent_on_error(silent_on_error=False):
"""
Used to control the display of error messages. By default, error messages are displayed to stdout.
:param silent_on_error: If True then all error messages are silenced (not displayed on the console)
:type silent_on_error: (bool)
"""
settings = UserSettings._default_for_function_interface
settings.silent_on_error = silent_on_error
def user_settings():
"""
Returns the current settings dictionary. If you've not setup the filename for the
settings, a default one will be used and then read.
:return: The current settings dictionary as a dictionary or a nicely formatted string representing it
:rtype: (dict or str)
"""
settings = UserSettings._default_for_function_interface
return settings.get_dict()
def user_settings_object():
"""
Returns the object that is used for the function version of this API.
With this object you can use the object interface, print it out in a nice format, etc.
:return: The UserSettings obect used for the function level interface
:rtype: (UserSettings)
"""
return UserSettings._default_for_function_interface
'''
'########:'##::::'##:'########::'######::'##::::'##:'########:'########:
##.....::. ##::'##:: ##.....::'##... ##: ##:::: ##:... ##..:: ##.....::
##::::::::. ##'##::: ##::::::: ##:::..:: ##:::: ##:::: ##:::: ##:::::::
######:::::. ###:::: ######::: ##::::::: ##:::: ##:::: ##:::: ######:::
##...:::::: ## ##::: ##...:::: ##::::::: ##:::: ##:::: ##:::: ##...::::
##:::::::: ##:. ##:: ##::::::: ##::: ##: ##:::: ##:::: ##:::: ##:::::::
########: ##:::. ##: ########:. ######::. #######::::: ##:::: ########:
........::..:::::..::........:::......::::.......::::::..:::::........::
:::'###::::'########::'####::'######::
::'## ##::: ##.... ##:. ##::'##... ##:
:'##:. ##:: ##:::: ##:: ##:: ##:::..::
'##:::. ##: ########::: ##::. ######::
#########: ##.....:::: ##:::..... ##:
##.... ##: ##::::::::: ##::'##::: ##:
##:::: ##: ##::::::::'####:. ######::
..:::::..::..:::::::::....:::......:::
These are the functions used to implement the subprocess APIs (Exec APIs) of PySimpleGUI
'''
def execute_command_subprocess(command, *args, wait=False, cwd=None, pipe_output=False, merge_stderr_with_stdout=True, stdin=None):
"""
Runs the specified command as a subprocess.
By default the call is non-blocking.
The function will immediately return without waiting for the process to complete running. You can use the returned Popen object to communicate with the subprocess and get the results.
Returns a subprocess Popen object.
:param command: Filename to load settings from (and save to in the future)
:type command: (str)
:param *args: Variable number of arguments that are passed to the program being started as command line parms
:type *args: (Any)
:param wait: If True then wait for the subprocess to finish
:type wait: (bool)
:param cwd: Working directory to use when executing the subprocess
:type cwd: (str))
:param pipe_output: If True then output from the subprocess will be piped. You MUST empty the pipe by calling execute_get_results or your subprocess will block until no longer full
:type pipe_output: (bool)
:param merge_stderr_with_stdout: If True then output from the subprocess stderr will be merged with stdout. The result is ALL output will be on stdout.
:type merge_stderr_with_stdout: (bool)
:param stdin: Value passed to the Popen call. Defaults to subprocess.DEVNULL so that the pyinstaller created executable work correctly
:type stdin: (bool)
:return: Popen object
:rtype: (subprocess.Popen)
"""
if stdin is None:
stdin = subprocess.DEVNULL
try:
if args is not None:
expanded_args = ' '.join(args)
# print('executing subprocess command:',command, 'args:',expanded_args)
if command[0] != '"' and ' ' in command:
command = '"' + command + '"'
# print('calling popen with:', command +' '+ expanded_args)
# sp = subprocess.Popen(command +' '+ expanded_args, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cwd)
if pipe_output:
if merge_stderr_with_stdout:
sp = subprocess.Popen(command + ' ' + expanded_args, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=cwd, stdin=stdin)
else:
sp = subprocess.Popen(command + ' ' + expanded_args, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd, stdin=stdin)
else:
sp = subprocess.Popen(command + ' ' + expanded_args, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cwd, stdin=stdin)
else:
sp = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd, stdin=stdin)
if wait:
out, err = sp.communicate()
if out:
print(out.decode("utf-8"))
if err:
print(err.decode("utf-8"))
except Exception as e:
warnings.warn('Error in execute_command_subprocess {}'.format(e), UserWarning)
_error_popup_with_traceback('Error in execute_command_subprocess', e, 'command={}'.format(command), 'args={}'.format(args), 'cwd={}'.format(cwd))
sp = None
return sp
def execute_py_file(pyfile, parms=None, cwd=None, interpreter_command=None, wait=False, pipe_output=False, merge_stderr_with_stdout=True):
"""
Executes a Python file.
The interpreter to use is chosen based on this priority order:
1. interpreter_command paramter
2. global setting "-python command-"
3. the interpreter running running PySimpleGUI
:param pyfile: the file to run
:type pyfile: (str)
:param parms: parameters to pass on the command line
:type parms: (str)
:param cwd: the working directory to use
:type cwd: (str)
:param interpreter_command: the command used to invoke the Python interpreter
:type interpreter_command: (str)
:param wait: the working directory to use
:type wait: (bool)
:param pipe_output: If True then output from the subprocess will be piped. You MUST empty the pipe by calling execute_get_results or your subprocess will block until no longer full
:type pipe_output: (bool)
:param merge_stderr_with_stdout: If True then output from the subprocess stderr will be merged with stdout. The result is ALL output will be on stdout.
:type merge_stderr_with_stdout: (bool)
:return: Popen object
:rtype: (subprocess.Popen) | None
"""
if cwd is None:
# if the specific file is not found (not an absolute path) then assume it's relative to '.'
if not os.path.exists(pyfile):
cwd = '.'
if pyfile[0] != '"' and ' ' in pyfile:
pyfile = '"' + pyfile + '"'
if interpreter_command is not None:
python_program = interpreter_command
else:
# use the version CURRENTLY RUNNING if nothing is specified. Previously used the one from the settings file
pysimplegui_user_settings.load() # Refresh the settings just in case they've changed via another program
python_program = pysimplegui_user_settings.get('-python command-', '')
if python_program == '': # if no interpreter set in the settings, then use the current one
python_program = sys.executable
# python_program = 'python' if running_windows() else 'python3'
if parms is not None and python_program:
sp = execute_command_subprocess(python_program, pyfile, parms, wait=wait, cwd=cwd, pipe_output=pipe_output, merge_stderr_with_stdout=merge_stderr_with_stdout)
elif python_program:
sp = execute_command_subprocess(python_program, pyfile, wait=wait, cwd=cwd, pipe_output=pipe_output, merge_stderr_with_stdout=merge_stderr_with_stdout)
else:
print('execute_py_file - No interpreter has been configured')
sp = None
return sp
def execute_py_get_interpreter():
"""
Returns the command that is currently running. Previously returned the one from the system settings, but
have determined that the one currently running is the better choice.
:return: Full path to python interpreter (uses sys.executable)
:rtype: (str)
"""
pysimplegui_user_settings.load() # Refresh the settings just in case they've changed via another program
interpreter = pysimplegui_user_settings.get('-python command-', '')
if interpreter == '':
interpreter = sys.executable
return interpreter
def execute_editor(file_to_edit, line_number=None):
"""
Runs the editor that was configured in the global settings and opens the file to a specific line number.
Two global settings keys are used.
'-editor program-' the command line used to startup your editor. It's set
in the global settings window or by directly manipulating the PySimpleGUI settings object
'-editor format string-' a string containing 3 "tokens" that describes the command that is executed
<editor> <file> <line>
:param file_to_edit: the full path to the file to edit
:type file_to_edit: (str)
:param line_number: optional line number to place the cursor
:type line_number: (int)
:return: Popen object
:rtype: (subprocess.Popen) | None
"""
if file_to_edit is not None and len(file_to_edit) != 0 and file_to_edit[0] not in ('\"', "\'") and ' ' in file_to_edit:
file_to_edit = '"' + file_to_edit + '"'
pysimplegui_user_settings.load() # Refresh the settings just in case they've changed via another program
editor_program = pysimplegui_user_settings.get('-editor program-', None)
if editor_program is not None:
format_string = pysimplegui_user_settings.get('-editor format string-', None)
# if no format string, then just launch the editor with the filename
if not format_string or line_number is None:
sp = execute_command_subprocess(editor_program, file_to_edit)
else:
command = _create_full_editor_command(file_to_edit, line_number, format_string)
# print('final command line = ', command)
sp = execute_command_subprocess(editor_program, command)
else:
print('No editor has been configured in the global settings')
sp = None
return sp
def execute_get_results(subprocess_id, timeout=None):
"""
Get the text results of a previously executed execute call
Returns a tuple of the strings (stdout, stderr)
:param subprocess_id: a Popen subprocess ID returned from a previous execute call
:type subprocess_id: (subprocess.Popen)
:param timeout: Time in fractions of a second to wait. Returns '','' if timeout. Default of None means wait forever
:type timeout: (None | float)
:returns: Tuple with 2 strings (stdout, stderr)
:rtype: (str | None , str | None)
"""
out_decoded = err_decoded = None
if subprocess_id is not None:
try:
out, err = subprocess_id.communicate(timeout=timeout)
if out:
out_decoded = out.decode("utf-8")
if err:
err_decoded = err.decode("utf-8")
except ValueError:
# will get an error if stdout and stderr are combined and attempt to read stderr
# so ignore the error that would be generated
pass
except Exception as e:
popup_error('Error in execute_get_results', e)
return out_decoded, err_decoded
def execute_subprocess_still_running(subprocess_id):
"""
Returns True is the subprocess ID provided is for a process that is still running
:param subprocess_id: ID previously returned from Exec API calls that indicate this value is returned
:type subprocess_id: (subprocess.Popen)
:return: True if the subproces is running
:rtype: bool
"""
if subprocess_id.poll() == 0:
return False
return True
def execute_file_explorer(folder_to_open=''):
"""
The global settings has a setting called - "-explorer program-"
It defines the program to run when this function is called.
The optional folder paramter specified which path should be opened.
:param folder_to_open: The path to open in the explorer program
:type folder_to_open: str
:return: Popen object
:rtype: (subprocess.Popen) | None
"""
pysimplegui_user_settings.load() # Refresh the settings just in case they've changed via another program
explorer_program = pysimplegui_user_settings.get('-explorer program-', None)
if explorer_program is not None:
sp = execute_command_subprocess(explorer_program, folder_to_open)
else:
print('No file explorer has been configured in the global settings')
sp = None
return sp
def execute_find_callers_filename():
"""
Returns the first filename found in a traceback that is not the nsame of this file (__file__)
Used internally with the debugger for example.
:return: filename of the caller, asseumed to be the first non PySimpleGUI file
:rtype: str
"""
try: # lots can go wrong so wrapping the entire thing
trace_details = traceback.format_stack()
file_info_pysimplegui, error_message = None, ''
for line in reversed(trace_details):
if __file__ not in line:
file_info_pysimplegui = line.split(",")[0]
error_message = line
break
if file_info_pysimplegui is None:
return ''
error_parts = None
if error_message != '':
error_parts = error_message.split(', ')
if len(error_parts) < 4:
error_message = error_parts[0] + '\n' + error_parts[1] + '\n' + ''.join(error_parts[2:])
if error_parts is None:
print('*** Error popup attempted but unable to parse error details ***')
print(trace_details)
return ''
filename = error_parts[0][error_parts[0].index('File ') + 5:]
return filename
except:
return ''
def _create_full_editor_command(file_to_edit, line_number, edit_format_string):
"""
The global settings has a setting called - "-editor format string-"
It uses 3 "tokens" to describe how to invoke the editor in a way that starts at a specific line #
<editor> <file> <line>
:param file_to_edit:
:type file_to_edit: str
:param edit_format_string:
:type edit_format_string: str
:return:
:rtype:
"""
command = edit_format_string
command = command.replace('<editor>', '')
command = command.replace('<file>', file_to_edit)
command = command.replace('<line>', str(line_number) if line_number is not None else '')
return command
def _get_editor():
"""
Get the path to the editor based on user settings or on PySimpleGUI's global settings
:return: Path to the editor
:rtype: str
"""
try: # in case running with old version of PySimpleGUI that doesn't have a global PSG settings path
global_editor = pysimplegui_user_settings.get('-editor program-')
except:
global_editor = ''
return user_settings_get_entry('-editor program-', global_editor)
'''
'##::::'##::::'###:::::'######::::::'######::'########::'########::'######::'####:'########:'####::'######::
###::'###:::'## ##:::'##... ##::::'##... ##: ##.... ##: ##.....::'##... ##:. ##:: ##.....::. ##::'##... ##:
####'####::'##:. ##:: ##:::..::::: ##:::..:: ##:::: ##: ##::::::: ##:::..::: ##:: ##:::::::: ##:: ##:::..::
## ### ##:'##:::. ##: ##::::::::::. ######:: ########:: ######::: ##:::::::: ##:: ######:::: ##:: ##:::::::
##. #: ##: #########: ##:::::::::::..... ##: ##.....::: ##...:::: ##:::::::: ##:: ##...::::: ##:: ##:::::::
##:.:: ##: ##.... ##: ##::: ##::::'##::: ##: ##:::::::: ##::::::: ##::: ##:: ##:: ##:::::::: ##:: ##::: ##:
##:::: ##: ##:::: ##:. ######:::::. ######:: ##:::::::: ########:. ######::'####: ##:::::::'####:. ######::
..:::::..::..:::::..:::......:::::::......:::..:::::::::........:::......:::....::..::::::::....:::......:::
'''
'''
The Mac problems have been significant enough to warrant the addition of a series of settings that allow
users to turn specific patches and features on or off depending on their setup. There is not enough information
available to make this process more atuomatic.
'''
# Dictionary of Mac Patches. Used to find the key in the global settings and the default value
MAC_PATCH_DICT = {'Enable No Titlebar Patch' : ('-mac feature enable no titlebar patch-', False),
'Disable Modal Windows' : ('-mac feature disable modal windows-', True),
'Disable Grab Anywhere with Titlebar' : ('-mac feature disable grab anywhere with titlebar-', True)}
def _read_mac_global_settings():
"""
Reads the settings from the PySimpleGUI Global Settings and sets variables that
are used at runtime to control how certain features behave
"""
global ENABLE_MAC_MODAL_DISABLE_PATCH
global ENABLE_MAC_NOTITLEBAR_PATCH
global ENABLE_MAC_DISABLE_GRAB_ANYWHERE_WITH_TITLEBAR
ENABLE_MAC_MODAL_DISABLE_PATCH = pysimplegui_user_settings.get(MAC_PATCH_DICT['Disable Modal Windows'][0],
MAC_PATCH_DICT['Disable Modal Windows'][1])
ENABLE_MAC_NOTITLEBAR_PATCH = pysimplegui_user_settings.get(MAC_PATCH_DICT['Enable No Titlebar Patch'][0],
MAC_PATCH_DICT['Enable No Titlebar Patch'][1])
ENABLE_MAC_DISABLE_GRAB_ANYWHERE_WITH_TITLEBAR = pysimplegui_user_settings.get(MAC_PATCH_DICT['Disable Grab Anywhere with Titlebar'][0],
MAC_PATCH_DICT['Disable Grab Anywhere with Titlebar'][1])
def _mac_should_apply_notitlebar_patch():
"""
Uses a combination of the tkinter version number and the setting from the global settings
to determine if the notitlebar patch should be applied
:return: True if should apply the no titlebar patch on the Mac
:rtype: (bool)
"""
if not running_mac():
return False
try:
tver = [int(n) for n in framework_version.split('.')]
if tver[0] == 8 and tver[1] == 6 and tver[2] < 10 and ENABLE_MAC_NOTITLEBAR_PATCH:
return True
except Exception as e:
warnings.warn('Exception while trying to parse tkinter version {} Error = {}'.format(framework_version, e), UserWarning)
return False
def main_mac_feature_control():
"""
Window to set settings that will be used across all PySimpleGUI programs that choose to use them.
Use set_options to set the path to the folder for all PySimpleGUI settings.
:return: True if settings were changed
:rtype: (bool)
"""
current_theme = theme()
theme('dark red')
layout = [[T('Mac PySimpleGUI Feature Control', font='DEFAIULT 18')],
[T('Use this window to enable / disable features.')],
[T('Unfortunately, on some releases of tkinter on the Mac, there are problems that')],
[T('create the need to enable and disable sets of features. This window facilitates the control.')],
[T('Feature Control / Settings', font='_ 16 bold')],
[T('You are running tkinter version:', font='_ 12 bold'), T(framework_version, font='_ 12 bold')]]
for key, value in MAC_PATCH_DICT.items():
layout += [[Checkbox(key, k=value[0], default=pysimplegui_user_settings.get(value[0], value[1]))]]
layout += [[T('Currently the no titlebar patch ' + ('WILL' if _mac_should_apply_notitlebar_patch() else 'WILL NOT') + ' be applied')],
[T('The no titlebar patch will ONLY be applied on tkinter versions < 8.6.10')]]
layout += [[Button('Ok'), Button('Cancel')]]
window = Window('Mac Feature Control', layout, keep_on_top=True, finalize=True )
while True:
event, values = window.read()
if event in ('Cancel', WIN_CLOSED):
break
if event == 'Ok':
for key, value in values.items():
print('setting {} to {}'.format(key, value))
pysimplegui_user_settings.set(key, value)
break
window.close()
theme(current_theme)
'''
'########::'########:'########::'##::::'##::'######::::'######:::'########:'########::
##.... ##: ##.....:: ##.... ##: ##:::: ##:'##... ##::'##... ##:: ##.....:: ##.... ##:
##:::: ##: ##::::::: ##:::: ##: ##:::: ##: ##:::..::: ##:::..::: ##::::::: ##:::: ##:
##:::: ##: ######::: ########:: ##:::: ##: ##::'####: ##::'####: ######::: ########::
##:::: ##: ##...:::: ##.... ##: ##:::: ##: ##::: ##:: ##::: ##:: ##...:::: ##.. ##:::
##:::: ##: ##::::::: ##:::: ##: ##:::: ##: ##::: ##:: ##::: ##:: ##::::::: ##::. ##::
########:: ########: ########::. #######::. ######:::. ######::: ########: ##:::. ##:
........:::........::........::::.......::::......:::::......::::........::..:::::..::
'''
#####################################################################################################
# Debugger
#####################################################################################################
red_x = b"R0lGODlhEAAQAPeQAIsAAI0AAI4AAI8AAJIAAJUAAJQCApkAAJoAAJ4AAJkJCaAAAKYAAKcAAKcCAKcDA6cGAKgAAKsAAKsCAKwAAK0AAK8AAK4CAK8DAqUJAKULAKwLALAAALEAALIAALMAALMDALQAALUAALYAALcEALoAALsAALsCALwAAL8AALkJAL4NAL8NAKoTAKwbAbEQALMVAL0QAL0RAKsREaodHbkQELMsALg2ALk3ALs+ALE2FbgpKbA1Nbc1Nb44N8AAAMIWAMsvAMUgDMcxAKVABb9NBbVJErFYEq1iMrtoMr5kP8BKAMFLAMxKANBBANFCANJFANFEB9JKAMFcANFZANZcANpfAMJUEMZVEc5hAM5pAMluBdRsANR8AM9YOrdERMpIQs1UVMR5WNt8X8VgYMdlZcxtYtx4YNF/btp9eraNf9qXXNCCZsyLeNSLd8SSecySf82kd9qqc9uBgdyBgd+EhN6JgtSIiNuJieGHhOGLg+GKhOKamty1ste4sNO+ueenp+inp+HHrebGrefKuOPTzejWzera1O7b1vLb2/bl4vTu7fbw7ffx7vnz8f///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAJAALAAAAAAQABAAAAjUACEJHEiwYEEABniQKfNFgQCDkATQwAMokEU+PQgUFDAjjR09e/LUmUNnh8aBCcCgUeRmzBkzie6EeQBAoAAMXuA8ciRGCaJHfXzUMCAQgYooWN48anTokR8dQk4sELggBhQrU9Q8evSHiJQgLCIIfMDCSZUjhbYuQkLFCRAMAiOQGGLE0CNBcZYmaRIDLqQFGF60eTRoSxc5jwjhACFWIAgMLtgUocJFy5orL0IQRHAiQgsbRZYswbEhBIiCCH6EiJAhAwQMKU5DjHCi9gnZEHMTDAgAOw=="
COLOR_SCHEME = 'dark grey 13'
DEBUGGER_POPOUT_THEME = 'dark grey 13'
WIDTH_VARIABLES = 23
WIDTH_RESULTS = 46
WIDTH_WATCHER_VARIABLES = 20
WIDTH_WATCHER_RESULTS = 60
WIDTH_LOCALS = 80
NUM_AUTO_WATCH = 9
MAX_LINES_PER_RESULT_FLOATING = 4
MAX_LINES_PER_RESULT_MAIN = 3
POPOUT_WINDOW_FONT = 'Sans 8'
DEBUGGER_VARIABLE_DETAILS_FONT = 'Courier 10'
class _Debugger:
debugger = None
'''
# # ######
## ## ## # # # # # ###### ##### # # #### #### ###### #####
# # # # # # # ## # # # # # # # # # # # # # # #
# # # # # # # # # # # ##### ##### # # # # ##### # #
# # ###### # # # # # # # # # # # # ### # ### # #####
# # # # # # ## # # # # # # # # # # # # # #
# # # # # # # ###### ###### ##### #### #### #### ###### # #
'''
def __init__(self):
self.watcher_window = None # type: Window
self.popout_window = None # type: Window
self.local_choices = {}
self.myrc = ''
self.custom_watch = ''
self.locals = {}
self.globals = {}
self.popout_choices = {}
# Includes the DUAL PANE (now 2 tabs)! Don't forget REPL is there too!
def _build_main_debugger_window(self, location=(None, None)):
old_theme = theme()
theme(COLOR_SCHEME)
def InVar(key1):
row1 = [T(' '),
I(key=key1, size=(WIDTH_VARIABLES, 1)),
T('', key=key1 + 'CHANGED_', size=(WIDTH_RESULTS, 1)), B('Detail', key=key1 + 'DETAIL_'),
B('Obj', key=key1 + 'OBJ_'), ]
return row1
variables_frame = [InVar('_VAR0_'),
InVar('_VAR1_'),
InVar('_VAR2_'), ]
interactive_frame = [[T('>>> '), In(size=(83, 1), key='-REPL-',
tooltip='Type in any "expression" or "statement"\n and it will be disaplayed below.\nPress RETURN KEY instead of "Go"\nbutton for faster use'),
B('Go', bind_return_key=True, visible=True)],
[Multiline(size=(93, 26), key='-OUTPUT-', autoscroll=True, do_not_clear=True)], ]
autowatch_frame = [[Button('Choose Variables To Auto Watch', key='-LOCALS-'),
Button('Clear All Auto Watches'),
Button('Show All Variables', key='-SHOW_ALL-'),
Button('Locals', key='-ALL_LOCALS-'),
Button('Globals', key='-GLOBALS-'),
Button('Popout', key='-POPOUT-')]]
var_layout = []
for i in range(NUM_AUTO_WATCH):
var_layout.append([T('', size=(WIDTH_WATCHER_VARIABLES, 1), key='_WATCH%s_' % i),
T('', size=(WIDTH_WATCHER_RESULTS, MAX_LINES_PER_RESULT_MAIN), key='_WATCH%s_RESULT_' % i,
)])
col1 = [
# [Frame('Auto Watches', autowatch_frame+variable_values, title_color='blue')]
[Frame('Auto Watches', autowatch_frame + var_layout, title_color=theme_button_color()[0])]
]
col2 = [
[Frame('Variables or Expressions to Watch', variables_frame, title_color=theme_button_color()[0]), ],
[Frame('REPL-Light - Press Enter To Execute Commands', interactive_frame, title_color=theme_button_color()[0]), ]
]
# Tab based layout
layout = [[Text('Debugging: ' + self._find_users_code())],
[TabGroup([[Tab('Variables', col1), Tab('REPL & Watches', col2)]])]]
# ------------------------------- Create main window -------------------------------
window = Window("PySimpleGUI Debugger", layout, icon=PSG_DEBUGGER_LOGO, margins=(0, 0), location=location, keep_on_top=True,
right_click_menu=[[''], ['Exit', ]])
Window._read_call_from_debugger = True
window.finalize()
Window._read_call_from_debugger = False
window.Element('_VAR1_').SetFocus()
self.watcher_window = window
theme(old_theme)
return window
'''
# # ####### #
## ## ## # # # # # # ###### # # ##### # #### #### #####
# # # # # # # ## # # # # # ## # # # # # # # # #
# # # # # # # # # ##### # # ##### # # # # # # # # # # #
# # ###### # # # # # # # # # # # # # # # # # #####
# # # # # # ## # # # # # ## # # # # # # #
# # # # # # # ####### ## ###### # # # ####### #### #### #
'''
def _refresh_main_debugger_window(self, mylocals, myglobals):
if not self.watcher_window: # if there is no window setup, nothing to do
return False
event, values = self.watcher_window.read(timeout=1)
if event in (None, 'Exit', '_EXIT_', '-EXIT-'): # EXIT BUTTON / X BUTTON
try:
self.watcher_window.close()
except:
pass
self.watcher_window = None
return False
# ------------------------------- Process events from REPL Tab -------------------------------
cmd = values['-REPL-'] # get the REPL entered
# BUTTON - GO (NOTE - This button is invisible!!)
if event == 'Go': # GO BUTTON
self.watcher_window.Element('-REPL-').Update('')
self.watcher_window.Element('-OUTPUT-').Update(">>> {}\n".format(cmd), append=True, autoscroll=True)
try:
result = eval('{}'.format(cmd), myglobals, mylocals)
except Exception as e:
if sys.version_info[0] < 3:
result = 'Not available in Python 2'
else:
try:
result = exec('{}'.format(cmd), myglobals, mylocals)
except Exception as e:
result = 'Exception {}\n'.format(e)
self.watcher_window.Element('-OUTPUT-').Update('{}\n'.format(result), append=True, autoscroll=True)
# BUTTON - DETAIL
elif event.endswith('_DETAIL_'): # DETAIL BUTTON
var = values['_VAR{}_'.format(event[4])]
try:
result = str(eval(str(var), myglobals, mylocals))
except:
result = ''
popup_scrolled(str(values['_VAR{}_'.format(event[4])]) + '\n' + result, title=var, non_blocking=True, font=DEBUGGER_VARIABLE_DETAILS_FONT)
# BUTTON - OBJ
elif event.endswith('_OBJ_'): # OBJECT BUTTON
var = values['_VAR{}_'.format(event[4])]
try:
result = ObjToStringSingleObj(mylocals[var])
except Exception as e:
try:
result = eval('{}'.format(var), myglobals, mylocals)
result = ObjToStringSingleObj(result)
except Exception as e:
result = '{}\nError showing object {}'.format(e, var)
popup_scrolled(str(var) + '\n' + str(result), title=var, non_blocking=True, font=DEBUGGER_VARIABLE_DETAILS_FONT)
# ------------------------------- Process Watch Tab -------------------------------
# BUTTON - Choose Locals to see
elif event == '-LOCALS-': # Show all locals BUTTON
self._choose_auto_watches(mylocals)
# BUTTON - Locals (quick popup)
elif event == '-ALL_LOCALS-':
self._display_all_vars(mylocals)
# BUTTON - Globals (quick popup)
elif event == '-GLOBALS-':
self._display_all_vars(myglobals)
# BUTTON - clear all
elif event == 'Clear All Auto Watches':
if popup_yes_no('Do you really want to clear all Auto-Watches?', 'Really Clear??') == 'Yes':
self.local_choices = {}
self.custom_watch = ''
# BUTTON - Popout
elif event == '-POPOUT-':
if not self.popout_window:
self._build_floating_window()
# BUTTON - Show All
elif event == '-SHOW_ALL-':
for key in self.locals:
self.local_choices[key] = not key.startswith('_')
# -------------------- Process the manual "watch list" ------------------
for i in range(3):
key = '_VAR{}_'.format(i)
out_key = '_VAR{}_CHANGED_'.format(i)
self.myrc = ''
if self.watcher_window.Element(key):
var = values[key]
try:
result = eval(str(var), myglobals, mylocals)
except:
result = ''
self.watcher_window.Element(out_key).Update(str(result))
else:
self.watcher_window.Element(out_key).Update('')
# -------------------- Process the automatic "watch list" ------------------
slot = 0
for key in self.local_choices:
if key == '-CUSTOM_WATCH-':
continue
if self.local_choices[key]:
self.watcher_window.Element('_WATCH{}_'.format(slot)).Update(key)
try:
self.watcher_window.Element('_WATCH{}_RESULT_'.format(slot), silent_on_error=True).Update(mylocals[key])
except:
self.watcher_window.Element('_WATCH{}_RESULT_'.format(slot)).Update('')
slot += 1
if slot + int(not self.custom_watch in (None, '')) >= NUM_AUTO_WATCH:
break
# If a custom watch was set, display that value in the window
if self.custom_watch:
self.watcher_window.Element('_WATCH{}_'.format(slot)).Update(self.custom_watch)
try:
self.myrc = eval(self.custom_watch, myglobals, mylocals)
except:
self.myrc = ''
self.watcher_window.Element('_WATCH{}_RESULT_'.format(slot)).Update(self.myrc)
slot += 1
# blank out all of the slots not used (blank)
for i in range(slot, NUM_AUTO_WATCH):
self.watcher_window.Element('_WATCH{}_'.format(i)).Update('')
self.watcher_window.Element('_WATCH{}_RESULT_'.format(i)).Update('')
return True # return indicating the window stayed open
def _find_users_code(self):
try: # lots can go wrong so wrapping the entire thing
trace_details = traceback.format_stack()
file_info_pysimplegui, error_message = None, ''
for line in reversed(trace_details):
if __file__ not in line:
file_info_pysimplegui = line.split(",")[0]
error_message = line
break
if file_info_pysimplegui is None:
return ''
error_parts = None
if error_message != '':
error_parts = error_message.split(', ')
if len(error_parts) < 4:
error_message = error_parts[0] + '\n' + error_parts[1] + '\n' + ''.join(error_parts[2:])
if error_parts is None:
print('*** Error popup attempted but unable to parse error details ***')
print(trace_details)
return ''
filename = error_parts[0][error_parts[0].index('File ') + 5:]
return filename
except:
return
'''
###### # #
# # #### ##### # # ##### # # # # # # ##### #### # #
# # # # # # # # # # # # # # ## # # # # # # #
###### # # # # # # # # # # # # # # # # # # # # #
# # # ##### # # ##### # # # # # # # # # # # # ## #
# # # # # # # # # # # # ## # # # # ## ##
# #### # #### # ## ## # # # ##### #### # #
###### # # #
# # # # # # ##### #### # # # # # # ## ##### ####
# # # # ## ## # # # # # # # # # # # # # #
# # # # # ## # # # #### # # # # # # # # # # ####
# # # # # # ##### # ####### # # # # ###### ##### #
# # # # # # # # # # # # # # # # # # # # #
###### #### # # # #### # # ###### ###### # # # # # ####
'''
# displays them into a single text box
def _display_all_vars(self, dict):
num_cols = 3
output_text = ''
num_lines = 2
cur_col = 0
out_text = 'All of your Vars'
longest_line = max([len(key) for key in dict])
line = []
sorted_dict = {}
for key in sorted(dict.keys()):
sorted_dict[key] = dict[key]
for key in sorted_dict:
value = dict[key]
wrapped_list = textwrap.wrap(str(value), 60)
wrapped_text = '\n'.join(wrapped_list)
out_text += '{} - {}\n'.format(key, wrapped_text)
if cur_col + 1 == num_cols:
cur_col = 0
num_lines += len(wrapped_list)
else:
cur_col += 1
popup_scrolled(out_text, non_blocking=True)
'''
##### # #
# # # # #### #### #### ###### # # # ## ##### #### # #
# # # # # # # # # # # # # # # # # # #
# ###### # # # # #### ##### # # # # # # # ######
# # # # # # # # # # # # ###### # # # #
# # # # # # # # # # # # # # # # # # # # #
##### # # #### #### #### ###### ## ## # # # #### # #
# # # #
# # ## ##### # ## ##### # ###### #### # # # # # #
# # # # # # # # # # # # # # # # # # ## #
# # # # # # # # # ##### # ##### #### # # # # # # #
# # ###### ##### # ###### # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # ##
# # # # # # # # ##### ###### ###### #### ## ## # # #
'''
def _choose_auto_watches(self, my_locals):
old_theme = theme()
theme(COLOR_SCHEME)
num_cols = 3
output_text = ''
num_lines = 2
cur_col = 0
layout = [[Text('Choose your "Auto Watch" variables', font='ANY 14', text_color='red')]]
longest_line = max([len(key) for key in my_locals])
line = []
sorted_dict = {}
for key in sorted(my_locals.keys()):
sorted_dict[key] = my_locals[key]
for key in sorted_dict:
line.append(CB(key, key=key, size=(longest_line, 1),
default=self.local_choices[key] if key in self.local_choices else False))
if cur_col + 1 == num_cols:
cur_col = 0
layout.append(line)
line = []
else:
cur_col += 1
if cur_col:
layout.append(line)
layout += [
[Text('Custom Watch (any expression)'), Input(default_text=self.custom_watch, size=(40, 1), key='-CUSTOM_WATCH-')]]
layout += [
[Ok(), Cancel(), Button('Clear All'), Button('Select [almost] All', key='-AUTO_SELECT-')]]
window = Window('All Locals', layout, icon=PSG_DEBUGGER_LOGO, finalize=True)
while True: # event loop
event, values = window.read()
if event in (None, 'Cancel', '-EXIT-'):
break
elif event == 'Ok':
self.local_choices = values
self.custom_watch = values['-CUSTOM_WATCH-']
break
elif event == 'Clear All':
popup_quick_message('Cleared Auto Watches', auto_close=True, auto_close_duration=3, non_blocking=True,
text_color='red', font='ANY 18')
for key in sorted_dict:
window.Element(key).Update(False)
window.Element('-CUSTOM_WATCH-').Update('')
elif event == 'Select All':
for key in sorted_dict:
window.Element(key).Update(False)
elif event == '-AUTO_SELECT-':
for key in sorted_dict:
window.Element(key).Update(not key.startswith('_'))
# exited event loop
window.Close()
theme(old_theme)
'''
###### #######
# # # # # # ##### # # #### ## ##### # # # ####
# # # # # # # # # # # # # # # # ## # # #
###### # # # # # # ##### # # # # # # # # # # #
# # # # # # # # # # # # ###### # # # # # # ###
# # # # # # # # # # # # # # # # # ## # #
###### #### # ###### ##### # ###### #### # # # # # # ####
# #
# # # # # # ##### #### # #
# # # # ## # # # # # # #
# # # # # # # # # # # # #
# # # # # # # # # # # # ## #
# # # # # ## # # # # ## ##
## ## # # # ##### #### # #
'''
def _build_floating_window(self, location=(None, None)):
"""
:param location:
:type location:
"""
if self.popout_window: # if floating window already exists, close it first
self.popout_window.Close()
old_theme = theme()
theme(DEBUGGER_POPOUT_THEME)
num_cols = 2
width_var = 15
width_value = 30
layout = []
line = []
col = 0
# self.popout_choices = self.local_choices
self.popout_choices = {}
if self.popout_choices == {}: # if nothing chosen, then choose all non-_ variables
for key in sorted(self.locals.keys()):
self.popout_choices[key] = not key.startswith('_')
width_var = max([len(key) for key in self.popout_choices])
for key in self.popout_choices:
if self.popout_choices[key] is True:
value = str(self.locals.get(key))
h = min(len(value) // width_value + 1, MAX_LINES_PER_RESULT_FLOATING)
line += [Text('{}'.format(key), size=(width_var, 1), font=POPOUT_WINDOW_FONT),
Text(' = ', font=POPOUT_WINDOW_FONT),
Text(value, key=key, size=(width_value, h), font=POPOUT_WINDOW_FONT)]
if col + 1 < num_cols:
line += [VerticalSeparator(), T(' ')]
col += 1
if col >= num_cols:
layout.append(line)
line = []
col = 0
if col != 0:
layout.append(line)
layout = [[T(SYMBOL_X, enable_events=True, key='-EXIT-', font='_ 7')], [Column(layout)]]
Window._read_call_from_debugger = True
self.popout_window = Window('Floating', layout, alpha_channel=0, no_titlebar=True, grab_anywhere=True,
element_padding=(0, 0), margins=(0, 0), keep_on_top=True,
right_click_menu=['&Right', ['Debugger::RightClick', 'Exit::RightClick']], location=location, finalize=True)
Window._read_call_from_debugger = False
if location == (None, None):
screen_size = self.popout_window.GetScreenDimensions()
self.popout_window.Move(screen_size[0] - self.popout_window.Size[0], 0)
self.popout_window.SetAlpha(1)
theme(old_theme)
return True
'''
######
# # ###### ###### ##### ###### #### # #
# # # # # # # # # #
###### ##### ##### # # ##### #### ######
# # # # ##### # # # #
# # # # # # # # # # #
# # ###### # # # ###### #### # #
#######
# # #### ## ##### # # # ####
# # # # # # # # ## # # #
##### # # # # # # # # # # #
# # # # ###### # # # # # # ###
# # # # # # # # # ## # #
# ###### #### # # # # # # ####
# #
# # # # # # ##### #### # #
# # # # ## # # # # # # #
# # # # # # # # # # # # #
# # # # # # # # # # # # ## #
# # # # # ## # # # # ## ##
## ## # # # ##### #### # #
'''
def _refresh_floating_window(self):
if not self.popout_window:
return
for key in self.popout_choices:
if self.popout_choices[key] is True and key in self.locals:
if key is not None and self.popout_window is not None:
self.popout_window.Element(key, silent_on_error=True).Update(self.locals.get(key))
event, values = self.popout_window.read(timeout=5)
if event in (None, '_EXIT_', 'Exit::RightClick', '-EXIT-'):
self.popout_window.Close()
self.popout_window = None
elif event == 'Debugger::RightClick':
show_debugger_window()
# 888 888 .d8888b. d8888 888 888 888 888
# 888 888 d88P Y88b d88888 888 888 888 888
# 888 888 888 888 d88P888 888 888 888 888
# 888 888 .d8888b .d88b. 888d888 888 d88P 888 888 888 8888b. 88888b. 888 .d88b.
# 888 888 88K d8P Y8b 888P" 888 d88P 888 888 888 "88b 888 "88b 888 d8P Y8b
# 888 888 "Y8888b. 88888888 888 888 888 d88P 888 888 888 .d888888 888 888 888 88888888
# Y88b. .d88P X88 Y8b. 888 Y88b d88P d8888888888 888 888 888 888 888 d88P 888 Y8b.
# "Y88888P" 88888P' "Y8888 888 "Y8888P" d88P 888 888 888 "Y888888 88888P" 888 "Y8888
# 8888888888 888 d8b
# 888 888 Y8P
# 888 888
# 8888888 888 888 88888b. .d8888b 888888 888 .d88b. 88888b. .d8888b
# 888 888 888 888 "88b d88P" 888 888 d88""88b 888 "88b 88K
# 888 888 888 888 888 888 888 888 888 888 888 888 "Y8888b.
# 888 Y88b 888 888 888 Y88b. Y88b. 888 Y88..88P 888 888 X88
# 888 "Y88888 888 888 "Y8888P "Y888 888 "Y88P" 888 888 88888P'
def show_debugger_window(location=(None, None), *args):
"""
Shows the large main debugger window
:param location: Locations (x,y) on the screen to place upper left corner of the window
:type location: (int, int)
:return: None
:rtype: None
"""
if _Debugger.debugger is None:
_Debugger.debugger = _Debugger()
debugger = _Debugger.debugger
frame = inspect.currentframe()
prev_frame = inspect.currentframe().f_back
# frame, *others = inspect.stack()[1]
try:
debugger.locals = frame.f_back.f_locals
debugger.globals = frame.f_back.f_globals
finally:
del frame
if not debugger.watcher_window:
debugger.watcher_window = debugger._build_main_debugger_window(location=location)
return True
def show_debugger_popout_window(location=(None, None), *args):
"""
Shows the smaller "popout" window. Default location is the upper right corner of your screen
:param location: Locations (x,y) on the screen to place upper left corner of the window
:type location: (int, int)
:return: None
:rtype: None
"""
if _Debugger.debugger is None:
_Debugger.debugger = _Debugger()
debugger = _Debugger.debugger
frame = inspect.currentframe()
prev_frame = inspect.currentframe().f_back
# frame = inspect.getframeinfo(prev_frame)
# frame, *others = inspect.stack()[1]
try:
debugger.locals = frame.f_back.f_locals
debugger.globals = frame.f_back.f_globals
finally:
del frame
if debugger.popout_window:
debugger.popout_window.Close()
debugger.popout_window = None
debugger._build_floating_window(location=location)
def _refresh_debugger():
"""
Refreshes the debugger windows. USERS should NOT be calling this function. Within PySimpleGUI it is called for the USER every time the Window.Read function is called.
:return: return code False if user closed the main debugger window.
:rtype: (bool)
"""
if _Debugger.debugger is None:
_Debugger.debugger = _Debugger()
debugger = _Debugger.debugger
Window._read_call_from_debugger = True
# frame = inspect.currentframe()
# frame = inspect.currentframe().f_back
frame, *others = inspect.stack()[1]
try:
debugger.locals = frame.f_back.f_locals
debugger.globals = frame.f_back.f_globals
finally:
del frame
debugger._refresh_floating_window() if debugger.popout_window else None
rc = debugger._refresh_main_debugger_window(debugger.locals, debugger.globals) if debugger.watcher_window else False
Window._read_call_from_debugger = False
return rc
def get_versions():
"""
Returns a human-readable string of version numbers for:
Python version
PySimpleGUI Port (tkinter in this case)
tkinter version
PySimpleGUI version
The location of the PySimpleGUI.py file
The format is a newline between each value and descriptive text for each line
:return:
:rtype: str
"""
versions = "Python version: {}.{}.{}\nPort: {}\ntkinter version: {}\nPySimpleGUI version: {}\nPySimpleGUI filename: {}".format(sys.version_info.major,
sys.version_info.minor,
sys.version_info.micro, port,
tclversion_detailed, ver,
__file__)
return versions
# ==================================================#
#
# MM""""""""`M oo oo
# MM mmmmmmmM
# M` MMMM 88d8b.d8b. .d8888b. dP dP
# MM MMMMMMMM 88'`88'`88 88' `88 88 88
# MM MMMMMMMM 88 88 88 88. .88 88 88
# MM .M dP dP dP `88888P' 88 dP
# MMMMMMMMMMMM 88
# dP
# M""MMM""MMM""M dP dP
# M MMM MMM M 88 88
# M MMP MMP M .d8888b. 88d888b. 88 .d888b88
# M MM' MM' .M 88' `88 88' `88 88 88' `88
# M `' . '' .MM 88. .88 88 88 88. .88
# M .d .dMMM `88888P' dP dP `88888P8
# MMMMMMMMMMMMMM
#
# When things look bleak, show your user an emoji
# and a little hope
# ==================================================#
EMOJI_BASE64_FACEPALM = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6REQ2OTE2M0Q2RENEMTFFQkEwODdCNUZBQjI1NEUxQTAiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6REQ2OTE2M0U2RENEMTFFQkEwODdCNUZBQjI1NEUxQTAiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpERDY5MTYzQjZEQ0QxMUVCQTA4N0I1RkFCMjU0RTFBMCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpERDY5MTYzQzZEQ0QxMUVCQTA4N0I1RkFCMjU0RTFBMCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PrG3klYAABf+SURBVHjaxFoJcFzV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'
EMOJI_BASE64_FRUSTRATED = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Q0YzMDVFRjE2Q0U4MTFFQkFGM0JBRTY5MjFEM0EyRkMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6Q0YzMDVFRjI2Q0U4MTFFQkFGM0JBRTY5MjFEM0EyRkMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpDRjMwNUVFRjZDRTgxMUVCQUYzQkFFNjkyMUQzQTJGQyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpDRjMwNUVGMDZDRTgxMUVCQUYzQkFFNjkyMUQzQTJGQyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PrCGivkAABgCSURBVHja3FpneFTX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'
EMOJI_BASE64_NOTUNDERSTANDING = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NTVEMDZBRDE2Q0U5MTFFQjg5MjRENTNGQzVCQkI1N0MiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NTVEMDZBRDI2Q0U5MTFFQjg5MjRENTNGQzVCQkI1N0MiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo1NUQwNkFDRjZDRTkxMUVCODkyNEQ1M0ZDNUJCQjU3QyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo1NUQwNkFEMDZDRTkxMUVCODkyNEQ1M0ZDNUJCQjU3QyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PoEgddAAABw3SURBVHjarFoJdBzV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'
EMOJI_BASE64_PONDER = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RTk4OEJBQUY2RENEMTFFQjk3NEFFODU4Qjk5Qjk2NjAiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RTk4OEJBQjA2RENEMTFFQjk3NEFFODU4Qjk5Qjk2NjAiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpFOTg4QkFBRDZEQ0QxMUVCOTc0QUU4NThCOTlCOTY2MCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpFOTg4QkFBRTZEQ0QxMUVCOTc0QUU4NThCOTlCOTY2MCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PohXc4wAABYiSURBVHjatFoLeFTl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'
EMOJI_BASE64_SAD = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QjhEQzE3NTU2Q0U4MTFFQjk0NEJFQkEyMDc5NzVGRjciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QjhEQzE3NTY2Q0U4MTFFQjk0NEJFQkEyMDc5NzVGRjciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpCOERDMTc1MzZDRTgxMUVCOTQ0QkVCQTIwNzk3NUZGNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpCOERDMTc1NDZDRTgxMUVCOTQ0QkVCQTIwNzk3NUZGNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PqiratEAABX+SURBVHjavFoJcFz1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'
EMOJI_BASE64_SKEPTICAL = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MkM3RkFGNjQ2Q0U5MTFFQjg0NTRGNTdCQTc3ODhGNDUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MkM3RkFGNjU2Q0U5MTFFQjg0NTRGNTdCQTc3ODhGNDUiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoyQzdGQUY2MjZDRTkxMUVCODQ1NEY1N0JBNzc4OEY0NSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoyQzdGQUY2MzZDRTkxMUVCODQ1NEY1N0JBNzc4OEY0NSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PnKdXnkAABU6SURBVHjaxFppdBXn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'
EMOJI_BASE64_THINK = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDkwMDIzODE2RENFMTFFQkIzQkQ4NjYzRTEwOEUyNUQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MDkwMDIzODI2RENFMTFFQkIzQkQ4NjYzRTEwOEUyNUQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowOTAwMjM3RjZEQ0UxMUVCQjNCRDg2NjNFMTA4RTI1RCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowOTAwMjM4MDZEQ0UxMUVCQjNCRDg2NjNFMTA4RTI1RCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PraG6u0AABkKSURBVHjavFoJeFzl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'
EMOJI_BASE64_DREAMING = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Mzg4NEExODE2Q0U5MTFFQkFDNUU4NkQ5MTIxMzQ2QzAiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6Mzg4NEExODI2Q0U5MTFFQkFDNUU4NkQ5MTIxMzQ2QzAiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDozODg0QTE3RjZDRTkxMUVCQUM1RTg2RDkxMjEzNDZDMCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDozODg0QTE4MDZDRTkxMUVCQUM1RTg2RDkxMjEzNDZDMCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PqXm5wAAABe2SURBVHjazFoJdFzl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'
EMOJI_BASE64_WEARY = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RTg0MzYzM0I2Q0U4MTFFQkFDNENFQjAwN0U2RjM1RTYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RTg0MzYzM0M2Q0U4MTFFQkFDNENFQjAwN0U2RjM1RTYiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpFODQzNjMzOTZDRTgxMUVCQUM0Q0VCMDA3RTZGMzVFNiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpFODQzNjMzQTZDRTgxMUVCQUM0Q0VCMDA3RTZGMzVFNiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PuojFwAAABVfSURBVHjatFoJkFxX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'
EMOJI_BASE64_YIKES = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDdDQTU3NTM2Q0U5MTFFQkEzNkM4MDYzQzlCMUIyNTMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDdDQTU3NTQ2Q0U5MTFFQkEzNkM4MDYzQzlCMUIyNTMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0N0NBNTc1MTZDRTkxMUVCQTM2QzgwNjNDOUIxQjI1MyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0N0NBNTc1MjZDRTkxMUVCQTM2QzgwNjNDOUIxQjI1MyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv+eLOAAABVWSURBVHjaxFoJcBzV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'
EMOJI_BASE64_HAPPY_GASP = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QjdGRDYyQjk3NkRGMTFFQjg5NzdERjY5MzJBQjA1RkMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QjdGRDYyQkE3NkRGMTFFQjg5NzdERjY5MzJBQjA1RkMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpCN0ZENjJCNzc2REYxMUVCODk3N0RGNjkzMkFCMDVGQyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpCN0ZENjJCODc2REYxMUVCODk3N0RGNjkzMkFCMDVGQyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Ptkir7oAABWySURBVHjatFoJdF1l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'
EMOJI_BASE64_HAPPY_IDEA = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RUVEQTM3QTg3NkRGMTFFQjk1QkZCNjE0QUI5MUYwQzciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RUVEQTM3QTk3NkRGMTFFQjk1QkZCNjE0QUI5MUYwQzciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpFRURBMzdBNjc2REYxMUVCOTVCRkI2MTRBQjkxRjBDNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpFRURBMzdBNzc2REYxMUVCOTVCRkI2MTRBQjkxRjBDNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PjcEIkMAABZPSURBVHjavFoJdBzV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'
EMOJI_BASE64_HAPPY_JOY = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NjUxQTFBQzI3NkRGMTFFQkI5NDZCQjFDNkFDNjQ2OTUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NjUxQTFBQzM3NkRGMTFFQkI5NDZCQjFDNkFDNjQ2OTUiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo2NTFBMUFDMDc2REYxMUVCQjk0NkJCMUM2QUM2NDY5NSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo2NTFBMUFDMTc2REYxMUVCQjk0NkJCMUM2QUM2NDY5NSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PoEmG4sAABmjSURBVHjaxFoJeBxn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'
EMOJI_BASE64_HAPPY_LAUGH = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QkFBNDU4OUE3NkRFMTFFQkJCQzhBRTkwQjJGMzAyNUMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QkFBNDU4OUI3NkRFMTFFQkJCQzhBRTkwQjJGMzAyNUMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpCQUE0NTg5ODc2REUxMUVCQkJDOEFFOTBCMkYzMDI1QyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpCQUE0NTg5OTc2REUxMUVCQkJDOEFFOTBCMkYzMDI1QyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Po9obewAABRZSURBVHjaxFoJkBTX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'
EMOJI_BASE64_HAPPY_STARE = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RDgzMzhCODM3NkRGMTFFQjkwODBENDUzNzk4QzVBOEMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RDgzMzhCODQ3NkRGMTFFQjkwODBENDUzNzk4QzVBOEMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpEODMzOEI4MTc2REYxMUVCOTA4MEQ0NTM3OThDNUE4QyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpEODMzOEI4Mjc2REYxMUVCOTA4MEQ0NTM3OThDNUE4QyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PrgNzd4AABMDSURBVHjaxFp5kBz1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'
ICON_BUY_ME_A_COFFEE = b'iVBORw0KGgoAAAANSUhEUgAAAIIAAAAeCAIAAABvxVGSAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxMAAAsTAQCanBgAAAGVaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pg0KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPg0KICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPg0KICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIj4NCiAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+DQogICAgPC9yZGY6RGVzY3JpcHRpb24+DQogIDwvcmRmOlJERj4NCjwveDp4bXBtZXRhPg0KPD94cGFja2V0IGVuZD0iciI/PkPeCmYAAAvnSURBVGhD7ZkLVFVVGsf/9325XN7vhzwEARVQFLTEBE1QySeplZNl2gyKzlJrmlottXRmZTPN1HIm1/SY8jFlZWVODpVZ0zg5PjIyBJQURRHkqYD3ci/3Pd93OOBF4BKOrZgWv3UW7rPP3vvs8733VeLYmIFBfmyk4r+D/KgMqmFAMKiGAUHfucHhwJbC+npLDwpTOGzLk31CtErxfpCbpW81FDW0bZPdvnx5nnjfgcPhOH2qtPSVdevTg8WuQW4WV0FJb7ZV6swvHrs8ceKEa93Q6XQhoWFH9O5nmky1eos4Z5CbwpU37C6pf98aFxweYTKbxa5uyOVypUxy/tBHe+cMEbt6RK6E1QyHHbBDIhc7fwJIpbDTR3UgkXAQ7z+uvMFkcxjMNp3eZjbLhUvW9eJOgwGtRrvF4rSV7ngHYdUuTLgb03+JeU9jdDbv/ieApzfy30D4cBI/BWmkzsGSP3Oz/7jyhjarfWXBmafyFJrUKtIxmTL9Jd2TyiVSSIW/kjI88aL0ifGpMT5u4rTuRIzEkq2wtOFqNaQy+A3Bsd048JKw+/9fJIhNw72bsXM1KkvZy3PXI2QEtt4NqMQh3xtXVqmWS73VCrtO7e+NAB8E+iHIHyGBCA5AkB8CfEH9PqQYqyrMw+WL3X1Yh9vy8fJSvPQgTn2GkVOhEEITqVSEGv3VSvsUFyuQj9rgsIp3N8BGdMN4Z+iRTbicaDfA9hdR28OLDVNfx65APV5BaNPD7jTFQW2nt9OthLbUQ9TqIziMCNCUV4jtHnDA1AKrVamQu1zH3R8mA67W8F5p3yY9jM2w2aHxwugsURZRiYgfxw3/IRg6hhudkAM5Q24Ynw6FCtFJmLkWyZm8DzctpjyIaXm8ZuewzKVYsQPTVgkf3xUPH6TnIi0HGk8o1WKnM8mTkfc65q3nldtRa5B2F6Yswsw1CI3mHg8/2KwwGblNYVbjg2u1kHZEBe9ALHoWedsQncwfTtNz1yFvO0bP6K4JV+IjiYV5qYsr7MZWidHkdJk7Gq2SS3WSAI3WbneZl7Q+vFe1AuHxmLEaKXNx8gBoythZyFrNgqBdzPgV0h+C4xpy1iJ7FRxtwkwHC3HJixQ7hVsBnyAs2IgZ+Vj0PIalY/Y6pM7C/I0Yfy9Gz8bcx8XBY2dj4mKyEYxbCP9wYWYHJKBlr2BSHqauwdo9WPy84DdOULif9ST0DUjKxqgc7vELY4FOXY30h7lnwWZIbfAMh8UIk1C/SK3QeENXJYqUFE9bCo5n3UxZAZsBOY8gbiJkCkxd0d0LXRUtjUbL7wt1wbHDz/0pDtKeBG2HRYezloZPyptmxfmKnd0hC/X0Q/47bMLs0YBvGGRyhI3EtQb2DHdv7vn6PWhDEZKAb/eyTZEVKFUYNQ1VJdxun0iQxbUZMWo2qk/hzUcx+3FMXcmr7V7PSWhsLtRuMLWx0Z0/igN/wfKd4kQRB6Y8DJU7dq6C1h8LfoumGrZOZ8mMyYH+Kv71GobeDn0jf+eMNRyOCp5FzmPY9xvMfgr+kdB6w2yEuRlSDRRa9iqDXlwhZBiC47BnI8LiEJkKD28Mm4DDb/Cyd+YLL+siT1dq8FLJQ6Jjtr+7l6pSsasnNmzYGNuyX7zpEXcvNFfjyNuov8ACihyOrDWoLkJwLCpPwGbDkCSOPJfLWBkqN9ScE+WenAWVFpfPcJukwPUuhWszrCZyMRx8FWYLLp/E8Mk4fxzlx9jbGAp3HvCLxDfv8wotDWiuvz6dIkncHThRgKoyJGezYVZ8zU8pV1EcM9EByIqIMbh4HFfrUPQPXPwWPiGIGoP9f+T90z5bm3k/Gj+oPdDaJMrQ3Y+X0tMjgfAkWMyoOY3MZfjuIEIToXRD+VeYkodLJymdQqJwdkFXQUkhk3gbG/V6vaR3aFjj5QvBHrRoL9AY2iJ9auFHXFHUncPXBbhSheHZcPNA7Vk+Rgy7jWXUWAXPMP7CJkp65EMapC/mRvNF/puSDb9QbhAkNaMOFSfYpig6k85KPoNDAr9wWC0wG6D1Y8/LyMf4+/DFy9yZ9XN4+fHcoBg22/Ij3I4dzxmVvIpeOm4+steKSZWm66/AqMeHL0DXhKAofsWFEjRWQncF0x7lj1Jp4BnI3iwVfsvx9OJd6Sj/EXYORwollv4VMiWO7uHSlqbc9xxC4tjJtL7Ifpg9uANXaiAZe6Otrk4QSi9YrVbdxbMeSmeXvgEbf5WhRTBwgbAEtnoSDQVKijBuGsRlsE+01iM6hQfQplQKzHqMv5MgQY+ZiemPimqgWXRVHoddeCnJlAZQ4LLpEBAPQxMccjZG2n75Uex6BMX/ROQoJE2HVcilJD7aiKER/mEcKEj9JDuZg9NAJbmFIBpzG0ctxgGVEukPsBBJahYLxyXfcMhkmL8J3iE8VyFF8p2Yu4kHt7bAy5fzH3khOc2xXdi+EoZmRKTy6fXEXmzPR10FxtzF/kE+0YErNRBpgarCwm/Em5640tAQbGuUCW7RMxIzF6y+UQgdhogRmLyYjeJaPY6+yYpx92U/JbeguDdpGUcJ6py7DiveRGw6vnyNxZSRhxlrUfwpzhbygm6BbGi151k0NNhrCNp0aKlnKZA90unEIwCTHuKnVBlXFMMviPMHhZdWIXBfqeY1c57A/Vt4PF3eobhjCRRuOPWlELLVqCpCfCbiU7heenALByLKAVR3hURjaBqkchx9G++u4/IvYQpvdfaTrDlS/MwnkbcDidns5TYLx2H60rRZ3EN89T4aKhE7Dun34+hb/KoOZE9PjhKbPUGR8ojRKyNzsqPTlrtSWlraeGhP+hDPXvVARpQ2n212/AIkzYBPOIr24cPfcVxKykLyNATF4vOtbBAxt+PwLpwsgE8EGs7j4xcwJAWhCWz7X72NT18Sg7tvAFJzceLvqBeClaUVdWWoLoNMw48SMvhpSx2XxQmZnHWyVuLqZex7VgzFrVfZlsNHoroUH2zi6mXCz3gP721AU60wAqivQMIkjJ6D8EQU78eHf0DDOYydwx8SEI0jb+CLbayDtFz+kYaM7NAOVBRyiKPPpN1uW45LpUidh4gUjJ2H0Tm824AYxNyGEZMxcRGKCjhSOdHHL6wtZts9nxvvzL3HYrGYzRaTyWQ0mpRKhVqtor8KpbKspOQ+aVH2UB9xQo9oPdgx1e5cXBvb+MTQjm8QYsaj/gwunuZHKhWMBtaHrY2Vd9fjHIuIj5/jdMIxUkCuQHQiKsvEgl3sF6zEzZ1F03aNC2LyiQn3cmK8UIiiTziZX8cOuUrI81J4+iIyibVICdkZuQwaFQwmLnn5FQ6oldAGceSkT6CJFJqWv479WxA1GkHxOPkRJjyAHcvwwMs4sguH3sKo6YifCGMLij/GhVLEjEXKLH7pd/9G2X+cXYHoQw3kA9/U6DcdrHzq13ZZUDPJSiolx5CQJO312PeuxM0atSI1WKvsesL6X5EgYTzmP4PCDzBmDvZuQCllVOeNC3LpGerv9MzOMZ09PeJitd4JjMYvXsU76/iUvnAzqyFmArbew+cYUuHudRQHhGXp6oz8N9xep4/cQAE2JUTrqZQk+itHJdiS4mwjY+2Jw2yj4m0pITa1zDZ1qNet1gFhRWYeas+guIBFpOuoAq/jQmrOEqd2++UaF6v1DtUFdjsnACpM6Q10dOCUK+MDHbmaKFh+IDTaueH2On2ogSBNaJQyQ2O3E38rSmqkw3x7/0XvpvENgX8EB9CosWgzcFy+OUn9oBgbWAEBURgxCWYTqo/DzRP+ARiaipZaofDtB99DDUCkl/pCZVebcsCug9SuVlO5dstRebDyfUKRupCPUQZKGAMP2tXFQv5lgiqCU5/jLBW7EizdwdV5yQHhdNYP+qiU2rl0zVR8udXhq79Ui0s1wlWNim9x+DvPhYlBXfVzKzA2c4lCZ2M6bXzwTEcqHmhIcL4QEhvXSAd3wqDDpRNcyx3+G8qFwro/9P1/0USt3rz3dKPkhhRgR4K/JiPKW7y9tUgl/AOc/mrXCmcA0m6E7TGz0yD7HUK/lxoG+aH5ASL7IP1nUA0DgkE1DACA/wLdLG/w2vOeEgAAAABJRU5ErkJggg=='
EMOJI_BASE64_HAPPY_RELIEF = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDBFNzI5NUU3NkRGMTFFQkI0RUZFM0E0MERFOUExQUEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDBFNzI5NUY3NkRGMTFFQkI0RUZFM0E0MERFOUExQUEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0MEU3Mjk1Qzc2REYxMUVCQjRFRkUzQTQwREU5QTFBQSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0MEU3Mjk1RDc2REYxMUVCQjRFRkUzQTQwREU5QTFBQSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsnMYvAAABTwSURBVHjatFoJdFzV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'
EMOJI_BASE64_HAPPY_BIG_SMILE = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NEMyMDNFQTY3OTk2MTFFQjg3NzRCNjNENENFODAxNDYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NEMyMDNFQTc3OTk2MTFFQjg3NzRCNjNENENFODAxNDYiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0QzIwM0VBNDc5OTYxMUVCODc3NEI2M0Q0Q0U4MDE0NiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0QzIwM0VBNTc5OTYxMUVCODc3NEI2M0Q0Q0U4MDE0NiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PrjncAkAABMkSURBVHja3FppcFzVlf7ue713S62tpbZkLbZsS14xDhi8xTgYMEXZmbDMAE4YEjIFsxXDVKZIaqom4UeGGkIVQ4XMhGGqJikPRRKKYnOKxQSCMbFl47EA71hetHdLLam71Xu/9+a777Vai2O7ZcxMFSrd6uW9fu9+95zzne+c+4RhGPgy/yn4kv996QHaZn7xyCOPoLOz85I/1OjZ0rmFXCW+8fFa/G4ODL3GrRj+BhcqDCiVPKOMp6gcTg6dI8uR44gKgZG+NGIpDaN8P6QB4QQvqIjJa6vi0iBWrlyJp556qjSAe/fuRUdHR0mL4wCungOsrfNiw/xGtHrcmBOsR01zs12trFDhtqdho48oijVp+adz5jphZgkxmQSGRoDT55AaG8NQJIaB7rM4QsDvDQIHMsCJUiaSSqVKt6Db7b6oP1craK5Tsb2tHnd9bR1Wrr6+DvXzWjBn/iLaqpqmjfKOXZw955ajTYxcKXN007ZN2XE09fbjup4+fOf9PRjf14l9J4axY0DHyykD8Qv++CJztpXqy5Uq/PNt+N61bXjwtm1VgbWbV6Fq0WrA1UJzpIGxQ8DALoI7TZCFFRWzixeHC5jfxrEY2HgDfN1nsHnXu9j82jt49FgIj/dr+O+E/jlj8DyrcZLtNqxuq8Sz99/nXXnr9pthb1rLI1VA/AwQ+i8gRpfO6ZOgxGUygrxEZvJj03zggYXAumuw5KVXsGNvB27tSOBvhnWMXhGAMsCX27A5uKzpxXu/f2PF6s3b0Gs0IkyDdUQG0M/AyWhfR1bcjZzdTfZwcjg4T4E8L50vXF7i1ejggtShcExkXgfPVni2yqNOIrOTexzmFbL02RS8+QQ8egr2xQk4gzEEroree9ULu+d9OpD8RthA6HMDXKxiWeCqRS9U/uzNivDiedhJYGQ9PDtAb0wXgtL2f8D1kn/rOb4FNLe+tmbVj+761cFIdgtpN3PZedAnYPfXlP9b+h+fr1nTPg92Ml40D/w0NAXcBS+qF4e0irTO1CFtO3FcoAQlZRTcNwGcW7cNXQ/+5IY2p/iB+nksOFfgTxfdtnzDDWv8WJI6zBMNDI0dwGOJ/Qg4U1yAhOlKTiNtOqZacEo5aRtdrujmpnNOZwaN3+qFFTJMd7Y+57kc8ioZeWXhRBouxHUvRvM+RDJenI35kBipQG61B2p79d8FOod/Majj7KwA6taC2lqq8deP3dSDRvVNEggznkYTjj+Gm9JRdH0CDNKSGabsBMdo1novR45W1jTrVeY7JnAzW2gFjHYuu0psUgLbeHeVn+0263sHb+NyWq8OO+D3ANc2AsFFlhWPh3lNXsvBrPDOSvjPHse3Q0n80JgVQA6y9cprlmF148pVnJnfYolcB050RvH0s8DRk/woHBAyg0sEwno1RIFCRYFahHJxvzOFfkESGdKWhVUofG9wZby2LL62AfjLB8xsiQTXOUuQ7Uwl8wK46+A5/AuNkpyVi9YBW9atESrK2qybqTkMHN2Df/ox0B0vh33hPKhOl+leFkALjCh+nkUSLIIsgEqnTTMJXkehadPjcby8qwupdA73brfSlvSQKmqK5ma0Oc9hBX+4r2SSKaerzPVgU/vSCgILWDJS68Krv+5C97AD9kWLYZRVwpBCzWaH4MTUHGMwm+JIQslnLN+UvlTCEFoOaiZZ+H0GgqIhPxJBTo7RMWjl1XAtWojdewU+PWK578TyLVgIhVJx/axcVNFQ3RLEgmBDldQvxEf7d3WgYz/zVU0NDCf9RMtb5+YySNbNR//6O5Coa4U31IX6PS/BEzoN3e68dClDcFlvBQav/wbiTcvgjIUQ7NgJ98fvI0+xaqRT0IcGoAfqoDu9OHRoHG3tkx5eT3ReG9bYZwOQbt9YF0C9qyZQSEDDJJVOnOsn1gXVJCGjOLlksBWfPvg0MrW1khoRXboKw8s2YsV/PEyQZ6CrF761oPVy7nIc/faTiLUvA/KWs4S+sgVLfv4wfB/u5CI5YGSz0DJMLRVVOH16HPG41J4WaZXTyUiGrSQte6FCubSLznWhvmEOgdtrOAveMXUEHx8cRUZxwnB7uXIWEQhasW/Dn1ngUoUiiK9Zfu5df7d5/KLWy2cRJphYG8ElJ3+v+TzoufUhCJerSMNGKkkh72e5QfYetNhXHvJyOh4PAizTAiXHoEdBY6083ea3/GD0AD49KhnMy7hzFsjA+ksGmjAl5U0kOaTk96WUOYEWYKZ45rpkKoLQKwKMb90MNoMW1BnvMjd2d09yk9NpAqziaZUlA+QlayslNht/kw9hvO8zDA7xPl7feT8u6z56vpPzc1nP0ZIAevs/O38GdDZ3pAd2LQNDUS06oTfohmKWG32900/3++HinMtmI9XKvZ6CBdNHEBlKYGSMt/FMByhXtOGDX8F38piZn8xw5avvs2P8/gXz+EU7AmThQOdbqPy4Y9rvHfTD5refYzwWVEDBY4w8ScflwQgL5FzOykbmIvE3XhXu2eRBn3R/09cSHyHGoI4zRhSHrBIm3dNQbHDEhrH8Px/B0MrNZNN5JJaznPQu2BNjlwQIWkfNpLBkxw8QOrIF441L4YwOIHBwFzxhEhTlihQShqYVAQrmXtkFkEPGnwmAt2mwlqdkgIoUKMj00YJdJkCpHYW5mjNyNK9uS8VMS05a1nFpcBO/p5UU5r2GPb+ZFDf8zkwxZlNmioNJ2iSrMmWCIYmygkPZVCulzgZgJm8q98OMwTgVROFGF5BdMk40KQ5NqUUlImbXqJv4vSSUojK6UOHMlc+ThOSYOCYNrODCJckfA5iUK4Txw8WFK0Vu6SQAmRpsqbgFkhaXr0ZBp848XwISum7mQ5l68p5yE6y06HnnF5OnVVxNRSMFfp+B9GwARscTKDaLnI6C73AihmmlP65IklXz0LXt71F9bA/Kzn3K+IywhoyaMkzI+JkgCwmaAkBzec1En/UHEGtZgZFF12HBK0/CO3jadHOTXGZ23bkgMlLUKYUgxbeRcMxCbHO9w2PRKZRaZl0YksW4UGYCnnFjOWl7fMQkmujyq82EbY+NEGCsCFDRstYcVYcZu5rTg5zXjxx1rcme/UNwxCNWapCWou8Z+hT3kStL35RllBwT+GMxJJUajJcMMKmjNxyZTIrl5UymTh1J3lDPUUzb7VajcwZZuKJhlNNykaoNpsFz/irkKqsuXFToBV/TrOTuP9NJgCNWPJuxUSgspbsWikdjfNyUaR6PdQpVHFIpjHJ9R0vOg71phAYGC7qOk6jmAldVWGpCuqk+HjsP4ERcBfe/NqlMtII6zF5g5AvnCOt98MDr01jFvN8UTxFkUEHJVsG5WGnMShcEGFEFBXOpAG0KBoYjCOcSFsAK1l1Byk2DdZlwuaHFojDklZXp5KHZXag++gECB3ZZibuUclBYyT3Y8VtUnuyg9VzWdemaenJ8es6U59LdGxuLhApOCd0j6InqyJcu1RSEe8PoGYlY6p6FO1YsldEcp7hQzXSRHxmCHidQKSlM8S3MSchjC1/+CaoO/cFsC8BxAaBKQbnwnJr9v8d8kosZU4wxKazzI8PWtQvuKRxOk8ikfGueVyRURGi3WBad+dmUSzGmst5RdPb34/q6JsuKq1cBz7+YQo5FqWAASGtqoyPmMgopqRRrA0Kw3BYE3/avf4HBdXcitPFupOa2sQqZvo4ipcFz5ihqf/c8gnte5BqxdGLlYkwUyhMIJtpSHgr9sTBqawzMbbDyoKSCcz2Q+WG/mFXBay36+52H8dDVa6x4aW8DFs0HPhkKQ5m7ABpX2SQATsjQs9O6e5YbpDDn1WcQePd5pBoWIV3bhHxZlWkNO5nSGe6Bu/cEbEwjJqmY4kCfbH9MlAtyMCxUJ9Fw4RaTv8rKLHKR+a/7HMYGOK0Fs20bsuTaR4BjWgoVZmeBZ92yGej86TDsldUQtUFotKIhd3Vm1n1ygkxUmtvHpJ2h+P4IZSf2n6deZMtDc/mm92YmSEVeg6Si0HIqhwh1w6VkcM01VsaS9WAflWT3ADoZGN2zsqCwtgfOHevCvjOnsaWxGThyEmilFTesMdBx4Dgc/krolQHo/nKr6yl9hssqXc0ELGdhGGaXzbC7Lt7QNTcBVbN6kO4uKOoVluiKkYcqVVHfKTj1JG67jaK6waokJIt+xjn1j+EVqYMUMRsXtTYfjVNj+OU772HLQw8WQHPed98DLF8BvPbWKEb6R5GXpnV4oMhKn6WMlD1CdVlqxVSm1ih2zoqxJaYclXfTrOYpYxzRYQiypS2fJGYDrQuBrbfAZE+5jjLcJYkf6sRoKI+XL7uzPajj1dd/h49vvhFX1TAXDg5bF5cA94YdSNNFNs3PIhGJobsnhijTI/M0klnVlGKmFpXMqqgFPaoU24PCJJO8pUNpcZWy0Oc2zPiqpJVaGe+j9IwDfQ4sXKWhdV4eicKOnLTePpL0kTP4RURH92UDTBlInYzgkWeexdsPPwybbA/IFcxlTdUGd5nAV1YJ1Pop4PidbAbJMT6uYWxMA7OIOalkAsUKQBpPxo9s/Uk1Ims6mbjL/dZ7+eqzHAEfdZEeB4V5r1xuYqMTOHEC2PkmjnWl8bhWwrbGRfeGzuTx3huH8FfiGfz8T+6AIput0pvKudrhmCi26yWhytX3+y9cCFxya1Cf3NqWhk5nhJliq3yGWfOxvsZhFjgv/Qa9XWPYPqJh6HNvn8kVOpnDc8pBxPpDePqmm1AnmWzVAh1HelUMjArUVxrIaxZITbsyu2VyjU6HBPwEt7hJhxT/uz8A3n0fezvH8N2IhqOlXuuSu3tyM+Z4Fr+OnMPe0zvwDx178c0VV2sVc/0K9nXZ0N6Yg9dlFC0wwfZTC46p76d29cWU1wnlJ63VNajgVFhFq19Dx24DBz7Cma5B/OxMFv8eyV+4NLrsPXoJkozVHc7jb88extP7T+L2hurcNtWlLH05JiraWwwzlmTjTXYb5G6RrTDMtKhMzwxG4UkLGVvS+jJxSwKVMSybSgePkwMGcj2fJPVD5IGXhjT8Nm4gol/GQ1mz2p+V1w9rXNwUnvisD084hL7geA8WugSWkyzbK8vQHPTBT2AeDieJ1KsrolbYbGKqGak5s4JhTAJNk3wyBDreH0MkmsBp3uRoFvrhUBYniTs024cOPhfAqX8JwxynRjM4xY9vWF/SioW0x38HS8nli+u876cal3tlWpCdckFTuXsOf3wkqm2iVUglyMvcO/4FPTJ3xXbYfYo5lgQdWMcYu06oosIuUOYQOZcy0jVtT0J1iNaWSvsv81IQ5LKjQjP2DWbxYUzD8XH9/xGg5ASmqaBhPRLgkDUECwVlrhtra3yOrb7KqlXuYLPHVV0HYbObPRyt0GCadhVFrarKZO6Ix2PQmFhtucx350T6x7OJ+MHhaPL13hQ6krr1UJQUUbxPH50jbHyRAKUbLXPhm7eu9j6+sN1Z71JGlWTaQIx16bETAsn6r8Ivn+KxtjGLysxpfjr/UQOpUO2JBCJklTSDUKlq8Inh/o3X1v7Pxu2LmFOZV91M+KkMtJMn0PdWJ753JIkXNeMLAki1VnvTmpaf/PNzdwaVzF5S3kfIxzMIMUft3Gng3UGHSZl6Ljvl4jrrNRvr3rzVmTAEJg5Lfe31uOBy1pE5R7lQCWTSeVy3FthyM1BH0aB6zPpNzUTRlPw+nhzoxDtDRukPAQGzeJyy3o7td3xrfVAZf4Uy/kMSSsZsm5fRFOERQS3nKnbBhLmHouMw6qiGl+L3aEGO4Fx2A9u2AffcY4nnFPWgQvoNBGpQW1sLm9uDMDVvucvamjC35eibTrLV1i1oqrPhztm6aEkAaRvnqjbH/Ve3sq4bPTX5K7qL7HyHxlhROD1m+0IULHcIQewmsFE643ECfTvbiq1fF7j9dgO3sDp49FFgDQvqdFqKBAPlZT64fH70D6mm/Jve3QXW8dxlLXjAaT2aeWUBNqjYdMuG7HKX/eT0/TyiGR6RxnTDLlsZhvWo1kHMwR8wbyKtm+2zHls1fryvFf0hpSicH3gAWLzYSvjS+g5fGUbiLoxGZ/RyKAZ8AeDmDbi2XsX6KwpQ3qelBt/ZuJZvtekHJDmGhmRfxE8XtbPs0U2LnUYVVrOSkY+FuCZ2llUNu0/W4Jk3qjCxSlLpbN06pTvh9WA86zavCX0GSKbRTeuhNFbgfvVKAqxS0LB+OTbVN8/YBefNM/zc289XW50px6SDenmSn5C7UU7HTJBFC81PQ/Zb8lhQk54284mHhaSlnU4boloFegYo3fIzAPJezawT1yzDFoZM4IoBDNqw5atrUGPy7QyKHmOK6O4TcFYGTPc0zHjV6EPnUMbAkQ97RWVCkAcyOn54Yxf+/JZ4ceZdzP87dkz2kVVSq+KrQk8PC/vE+TpRtjDXX4dgUMWNVwSgR8C+oA7bly0pdKKnWE8WsJLxesLlcFZUFh9OkH9xghqmLeMFcB5W7E9sOYUf3ReBarduGQ7TvUNTu/MGvUCBvawCJ3uc5qPOmnZ+LK5cDsyvwX22EvnjfwUYAFVUViz+8QynAAAAAElFTkSuQmCC'
EMOJI_BASE64_HAPPY_CONTENT = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QjA0RDRDODM3OTk2MTFFQjk3QzQ5NjQxMjRDQTk0MEMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QjA0RDRDODQ3OTk2MTFFQjk3QzQ5NjQxMjRDQTk0MEMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpCMDRENEM4MTc5OTYxMUVCOTdDNDk2NDEyNENBOTQwQyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpCMDRENEM4Mjc5OTYxMUVCOTdDNDk2NDEyNENBOTQwQyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PrB9/b8AABLJSURBVHja3Fp5dB3V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'
EMOJI_BASE64_HAPPY_HEARTS = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NURFMEJBMjk3OTk2MTFFQjk1MTZENDVGRTM1MDlFRjYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NURFMEJBMkE3OTk2MTFFQjk1MTZENDVGRTM1MDlFRjYiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo1REUwQkEyNzc5OTYxMUVCOTUxNkQ0NUZFMzUwOUVGNiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo1REUwQkEyODc5OTYxMUVCOTUxNkQ0NUZFMzUwOUVGNiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PjWLaKkAABhuSURBVHja3FoJcF3V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'
EMOJI_BASE64_HAPPY_THUMBS_UP = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6N0M3M0ExQkM3OTk2MTFFQkFDREU4QkE2OUQ2Njk5ODUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6N0M3M0ExQkQ3OTk2MTFFQkFDREU4QkE2OUQ2Njk5ODUiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3QzczQTFCQTc5OTYxMUVCQUNERThCQTY5RDY2OTk4NSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3QzczQTFCQjc5OTYxMUVCQUNERThCQTY5RDY2OTk4NSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PrSTufUAABP3SURBVHja3Fp5cF31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'
EMOJI_BASE64_HAPPY_WINK = b'iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OTFDMjA3MTk3OTk2MTFFQjg3QzdFQTc4QzI5RjM3OTMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OTFDMjA3MUE3OTk2MTFFQjg3QzdFQTc4QzI5RjM3OTMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5MUMyMDcxNzc5OTYxMUVCODdDN0VBNzhDMjlGMzc5MyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5MUMyMDcxODc5OTYxMUVCODdDN0VBNzhDMjlGMzc5MyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PhbG1A0AABOISURBVHja3FoJcFz1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'
EMOJI_BASE64_CRY = b'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UmVhZHlxyWU8AAADJmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODFFRjMyQzU3NjZBMTFFQzk5RjlGOEVGNzI0N0Q5MkUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODFFRjMyQzY3NjZBMTFFQzk5RjlGOEVGNzI0N0Q5MkUiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4MUVGMzJDMzc2NkExMUVDOTlGOUY4RUY3MjQ3RDkyRSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4MUVGMzJDNDc2NkExMUVDOTlGOUY4RUY3MjQ3RDkyRSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pn1wWbgAABOOSURBVHja3FoJkBzVef66p+ee2Z3Z+97VsZIWCQmEOASWBCXJSFy2MQFMKnawcJwEJ6SCyxSpJEXKYMc5IAVVxIVtHELZ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'
EMOJI_BASE64_DEAD = b'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UmVhZHlxyWU8AAADJmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OERFNzRCQkY3NjZBMTFFQzkyQjU5MzI2RDQ4OTlFMTgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OERFNzRCQzA3NjZBMTFFQzkyQjU5MzI2RDQ4OTlFMTgiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4REU3NEJCRDc2NkExMUVDOTJCNTkzMjZENDg5OUUxOCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4REU3NEJCRTc2NkExMUVDOTJCNTkzMjZENDg5OUUxOCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pmz6ud4AABNfSURBVHja3FoJkBzleX3dPffMzt73ag+dK4nViSLABolDgMDYYNmGBMJlm0DASYXEFWKCj1Bgp0wljp2EmFA+ioBJkB2D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'
EMOJI_BASE64_FINGERS_CROSSED = b'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UmVhZHlxyWU8AAADJmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NEY2RkNFOUY3NjZBMTFFQ0JCNjdDOEVFREIxMDEwREEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NEY2RkNFQTA3NjZBMTFFQ0JCNjdDOEVFREIxMDEwREEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0RjZGQ0U5RDc2NkExMUVDQkI2N0M4RUVEQjEwMTBEQSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0RjZGQ0U5RTc2NkExMUVDQkI2N0M4RUVEQjEwMTBEQSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvIpiW8AABYsSURBVHja7FpncBzneX529+72+uGAQ2+EQIAEQUKkRJEU1SiZjiUlskpmbMeSx3bcFDfNuMQtjmVnRilj/7CVWC4Zy5Ft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'
EMOJI_BASE64_GUESS = b'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UmVhZHlxyWU8AAADJmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QjBBQTA4OUU3NjZBMTFFQ0I1QzNGQjM3M0JDOEI4MTAiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QjBBQTA4OUY3NjZBMTFFQ0I1QzNGQjM3M0JDOEI4MTAiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpCMEFBMDg5Qzc2NkExMUVDQjVDM0ZCMzczQkM4QjgxMCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpCMEFBMDg5RDc2NkExMUVDQjVDM0ZCMzczQkM4QjgxMCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PtYnwnMAABh/SURBVHja3FppdBzllb1VvXerW2pJrd2yJVmysC1vmMVgsCEQGwJJCDOEQAiTwGQyDJyTQMjChOw5SVhCDslMZk5mhgQI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'
EMOJI_BASE64_CLAP = b'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UmVhZHlxyWU8AAADJmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NTlFNEJBQ0Q3NjZBMTFFQzkxMUM5MzNGMEU4QTJEMkMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NTlFNEJBQ0U3NjZBMTFFQzkxMUM5MzNGMEU4QTJEMkMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo1OUU0QkFDQjc2NkExMUVDOTExQzkzM0YwRThBMkQyQyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo1OUU0QkFDQzc2NkExMUVDOTExQzkzM0YwRThBMkQyQyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PpRQcoQAABN2SURBVHja7Fp5dFzldf+9ZfYZzYxWa7Mkb8JavGCxGLwJm8VsBwiEpTgQ3JjStCclCZSQhARSWs5Je2jLSctJAsGNwaFs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'
EMOJI_BASE64_NO_HEAR = b'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UmVhZHlxyWU8AAADJmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDU0N0I2QUU3NjZBMTFFQ0JBMTJCNUY1RjE3MDA3QTQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDU0N0I2QUY3NjZBMTFFQ0JBMTJCNUY1RjE3MDA3QTQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0NTQ3QjZBQzc2NkExMUVDQkExMkI1RjVGMTcwMDdBNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0NTQ3QjZBRDc2NkExMUVDQkExMkI1RjVGMTcwMDdBNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PkaeQgQAABVrSURBVHja7FoJdFzldf7eNqtGI432xZJlLZZkA8YGb6wmGFyMISEkxCZtoTShTQiQQ5MGSA5pmnIIoQlNWNwcmgTXYXHK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'
EMOJI_BASE64_NO_SEE = b'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UmVhZHlxyWU8AAADJmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MzU4MEM5QjA3NjZBMTFFQ0I0RDFDM0JGRDAxREI4MzYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MzU4MEM5QjE3NjZBMTFFQ0I0RDFDM0JGRDAxREI4MzYiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDozNTgwQzlBRTc2NkExMUVDQjREMUMzQkZEMDFEQjgzNiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDozNTgwQzlBRjc2NkExMUVDQjREMUMzQkZEMDFEQjgzNiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Ptt4wcgAABQcSURBVHja7FppcFvXdf7ee9gBggtAggQ3kZSofbVkbXZsRd7kJk7cyk1ST5xMGmebJuP0RzJum7SJE6fLpJ7Y03Qyjtt4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'
EMOJI_BASE64_NO_SPEAK = b'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UmVhZHlxyWU8AAADJmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MkNFRTlDNDc3NjZBMTFFQ0E0NEVEMEU3NzYzNTM5NjMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MkNFRTlDNDg3NjZBMTFFQ0E0NEVEMEU3NzYzNTM5NjMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoyQ0VFOUM0NTc2NkExMUVDQTQ0RUQwRTc3NjM1Mzk2MyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoyQ0VFOUM0Njc2NkExMUVDQTQ0RUQwRTc3NjM1Mzk2MyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PjUQN8AAABUSSURBVHja7FppdBzVlf6qu3pfJLX2HVle5VUYgyE2igBbxmTweMgQlsQxECdxEpPtkGVOyDowk4QTCGQOITlMEkKcsCTg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'
EMOJI_BASE64_PRAY = b'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UmVhZHlxyWU8AAADJmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NkQ3NThDMjQ3NjZBMTFFQ0I4MzFDMjVGNjQ4NEE3MDIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NkQ3NThDMjU3NjZBMTFFQ0I4MzFDMjVGNjQ4NEE3MDIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo2RDc1OEMyMjc2NkExMUVDQjgzMUMyNUY2NDg0QTcwMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo2RDc1OEMyMzc2NkExMUVDQjgzMUMyNUY2NDg0QTcwMiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pil+3MoAABWHSURBVHjaxFoJeFzVdf7fm30kjaTRLmuxZUvygo2MHRuDHewAhkASMC0QklDHDiShSUqgab+2KQkkDR9p01JCE5OQ1ElL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'
EMOJI_BASE64_ZIPPED_SHUT = b'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UmVhZHlxyWU8AAADJmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NzZEQ0M2NzE3NjZBMTFFQ0E2M0NDMDgwQ0Q2ODFFRTciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NzZEQ0M2NzI3NjZBMTFFQ0E2M0NDMDgwQ0Q2ODFFRTciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NkRDQzY2Rjc2NkExMUVDQTYzQ0MwODBDRDY4MUVFNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NkRDQzY3MDc2NkExMUVDQTYzQ0MwODBDRDY4MUVFNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PqXvtfMAABTzSURBVHja3FppdFzleX7uMrtmpJFGGm2WsGzLCxjbGLMTtpwYs8SkSQ+0h+JA0iUkOZQEkpbTJE0ICaS0NCc5pG0oPiGn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'
EMOJI_BASE64_NO_HEAR = b'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UmVhZHlxyWU8AAADJmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDU0N0I2QUU3NjZBMTFFQ0JBMTJCNUY1RjE3MDA3QTQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDU0N0I2QUY3NjZBMTFFQ0JBMTJCNUY1RjE3MDA3QTQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0NTQ3QjZBQzc2NkExMUVDQkExMkI1RjVGMTcwMDdBNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0NTQ3QjZBRDc2NkExMUVDQkExMkI1RjVGMTcwMDdBNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PkaeQgQAABVrSURBVHja7FoJdFzldf7eNqtGI432xZJlLZZkA8YGb6wmGFyMISEkxCZtoTShTQiQQ5MGSA5pmnIIoQlNWNwcmgTXYXHK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'
EMOJI_BASE64_HAPPY_LIST = [EMOJI_BASE64_HAPPY_STARE, EMOJI_BASE64_HAPPY_LAUGH, EMOJI_BASE64_HAPPY_JOY, EMOJI_BASE64_HAPPY_IDEA, EMOJI_BASE64_HAPPY_GASP,
EMOJI_BASE64_HAPPY_RELIEF, EMOJI_BASE64_HAPPY_WINK, EMOJI_BASE64_HAPPY_THUMBS_UP, EMOJI_BASE64_HAPPY_HEARTS,
EMOJI_BASE64_HAPPY_CONTENT, EMOJI_BASE64_HAPPY_BIG_SMILE, EMOJI_BASE64_PRAY, EMOJI_BASE64_GUESS, EMOJI_BASE64_FINGERS_CROSSED,
EMOJI_BASE64_CLAP]
EMOJI_BASE64_SAD_LIST = [EMOJI_BASE64_YIKES, EMOJI_BASE64_WEARY, EMOJI_BASE64_DREAMING, EMOJI_BASE64_THINK, EMOJI_BASE64_SKEPTICAL, EMOJI_BASE64_FACEPALM,
EMOJI_BASE64_FRUSTRATED, EMOJI_BASE64_PONDER, EMOJI_BASE64_NOTUNDERSTANDING, EMOJI_BASE64_CRY, EMOJI_BASE64_DEAD, EMOJI_BASE64_ZIPPED_SHUT,
EMOJI_BASE64_NO_HEAR, EMOJI_BASE64_NO_SEE, EMOJI_BASE64_NO_SPEAK]
EMOJI_BASE64_LIST = EMOJI_BASE64_HAPPY_LIST + EMOJI_BASE64_SAD_LIST
def _random_error_emoji():
c = random.choice(EMOJI_BASE64_SAD_LIST)
return c
def _random_happy_emoji():
c = random.choice(EMOJI_BASE64_HAPPY_LIST)
return c
'''
M"""""`'"""`YM
M mm. mm. M
M MMM MMM M .d8888b. 88d888b. .d8888b.
M MMM MMM M 88' `88 88' `88 88ooood8
M MMM MMM M 88. .88 88 88. ...
M MMM MMM M `88888P' dP `88888P'
MMMMMMMMMMMMMM
M#"""""""'M .d8888P dP dP
## mmmm. `M 88' 88 88
#' .M .d8888b. .d8888b. .d8888b. 88baaa. 88aaa88
M# MMMb.'YM 88' `88 Y8ooooo. 88ooood8 88` `88 88
M# MMMM' M 88. .88 88 88. ... 8b. .d8 88
M# .;M `88888P8 `88888P' `88888P' `Y888P' dP
M#########M
M""M
M M
M M 88d8b.d8b. .d8888b. .d8888b. .d8888b. .d8888b.
M M 88'`88'`88 88' `88 88' `88 88ooood8 Y8ooooo.
M M 88 88 88 88. .88 88. .88 88. ... 88
M M dP dP dP `88888P8 `8888P88 `88888P' `88888P'
MMMM .88
d8888P
'''
'''
90 x 90 pixel images
These images are intentionally a little large so that you can use the image_subsample to reduce their size.
This offers more flexibility for use in a main window (larger) or perhaps a titlebar (smaller)
'''
HEART_FLAT_BASE64 = b'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'
HEART_3D_BASE64 = b'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'
RED_X_BASE64 = b'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'
GREEN_CHECK_BASE64 = b'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'
# ==========================================================================#
# MP""""""`MM dP dP .8888b
# M mmmmm..M 88 88 88 "
# M. `YM d8888P .d8888b. 88d888b. d8888P .d8888b. 88aaa
# MMMMMMM. M 88 88' `88 88' `88 88 88' `88 88
# M. .MMM' M 88 88. .88 88 88 88. .88 88
# Mb. .dM dP `88888P8 dP dP `88888P' dP
# MMMMMMMMMMM
#
# dP dP oo dP dP
# dP dP dP dP
# 88d8b.d8b. .d8888b. dP 88d888b.
# 88'`88'`88 88' `88 88 88' `88
# 88 88 88 88. .88 88 88 88
# dP dP dP `88888P8 dP dP dP
#
#
# MM""""""""`M dP
# MM mmmmmmmM 88
# M` MMMM 88d888b. d8888P 88d888b. dP dP
# MM MMMMMMMM 88' `88 88 88' `88 88 88
# MM MMMMMMMM 88 88 88 88 88. .88
# MM .M dP dP dP dP `8888P88
# MMMMMMMMMMMM .88
# d8888P
# MM"""""""`YM oo dP
# MM mmmmm M 88
# M' .M .d8888b. dP 88d888b. d8888P .d8888b.
# MM MMMMMMMM 88' `88 88 88' `88 88 Y8ooooo.
# MM MMMMMMMM 88. .88 88 88 88 88 88
# MM MMMMMMMM `88888P' dP dP dP dP `88888P'
# MMMMMMMMMMMM
# ==========================================================================#
# M"""""`'"""`YM oo
# M mm. mm. M
# M MMM MMM M .d8888b. dP 88d888b.
# M MMM MMM M 88' `88 88 88' `88
# M MMM MMM M 88. .88 88 88 88
# M MMM MMM M `88888P8 dP dP dP
# MMMMMMMMMMMMMM
#
# MM"""""""`YM dP MM'"""""`MM oo dP M""MMMMM""MM dP
# MM mmmmm M 88 M' .mmm. `M 88 M MMMMM MM 88
# M' .M .d8888b. .d8888b. d8888P M MMMMMMMM dP d8888P M `M dP dP 88d888b.
# MM MMMMMMMM 88' `88 Y8ooooo. 88 M MMM `M 88 88 M MMMMM MM 88 88 88' `88
# MM MMMMMMMM 88. .88 88 88 M. `MMM' .M 88 88 M MMMMM MM 88. .88 88. .88
# MM MMMMMMMM `88888P' `88888P' dP MM. .MM dP dP M MMMMM MM `88888P' 88Y8888'
# MMMMMMMMMMMM MMMMMMMMMMM MMMMMMMMMMMM
#
# M""M
# M M
# M M .d8888b. .d8888b. dP dP .d8888b.
# M M Y8ooooo. Y8ooooo. 88 88 88ooood8
# M M 88 88 88. .88 88. ...
# M M `88888P' `88888P' `88888P' `88888P'
# MMMM
def _github_issue_post_make_markdown(issue_type, operating_system, os_ver, psg_port, psg_ver, gui_ver, python_ver,
python_exp, prog_exp, used_gui, gui_notes,
cb_docs, cb_demos, cb_demo_port, cb_readme_other, cb_command_line, cb_issues, cb_github,
detailed_desc, code, project_details):
body = \
"""
## Type of Issue (Enhancement, Error, Bug, Question)
{}
----------------------------------------
## Environment
#### Operating System
{} version {}
#### PySimpleGUI Port (tkinter, Qt, Wx, Web)
{}
----------------------------------------
## Versions
#### Python version (`sg.sys.version`)
{}
#### PySimpleGUI Version (`sg.__version__`)
{}
#### GUI Version (tkinter (`sg.tclversion_detailed`), PySide2, WxPython, Remi)
{}
""".format(issue_type, operating_system,os_ver, psg_port,python_ver, psg_ver, gui_ver, project_details)
body2 = \
"""
---------------------
## Your Experience In Months or Years (optional)
{} Years Python programming experience
{} Years Programming experience overall
{} Have used another Python GUI Framework? (tkinter, Qt, etc) (yes/no is fine)
{}
---------------------
## Troubleshooting
These items may solve your problem. Please check those you've done by changing - [ ] to - [X]
- [{}] Searched main docs for your problem www.PySimpleGUI.org
- [{}] Looked for Demo Programs that are similar to your goal Demos.PySimpleGUI.org
- [{}] If not tkinter - looked for Demo Programs for specific port
- [{}] For non tkinter - Looked at readme for your specific port if not PySimpleGUI (Qt, WX, Remi)
- [{}] Run your program outside of your debugger (from a command line)
- [{}] Searched through Issues (open and closed) to see if already reported Issues.PySimpleGUI.org
- [{}] Tried using the PySimpleGUI.py file on GitHub. Your problem may have already been fixed but not released
## Detailed Description
{}
#### Code To Duplicate
```python
{}
```
#### Screenshot, Sketch, or Drawing
-------------------------
{}
""".format(python_exp, prog_exp, used_gui, gui_notes,
cb_docs, cb_demos, cb_demo_port, cb_readme_other, cb_command_line, cb_issues, cb_github,
detailed_desc, code if len(code) > 10 else '# Paste your code here',
'## Watcha Makin?\n' + str(project_details) if project_details else '')
return body + body2
def _github_issue_post_make_github_link(title, body):
pysimplegui_url = "https://github.com/PySimpleGUI/PySimpleGUI"
pysimplegui_issues = "{}/issues/new?".format(pysimplegui_url)
# Fix body cuz urllib can't do it smfh
getVars = {'title': str(title), 'body': str(body)}
return (pysimplegui_issues + urllib.parse.urlencode(getVars).replace("%5Cn", "%0D"))
#########################################################################################################
def _github_issue_post_validate(values, checklist, issue_types):
issue_type = None
for itype in issue_types:
if values[itype]:
issue_type = itype
break
if issue_type is None:
popup_error('Must choose issue type')
return False
if values['-OS WIN-']:
operating_system = 'Windows'
os_ver = values['-OS WIN VER-']
elif values['-OS LINUX-']:
operating_system = 'Linux'
os_ver = values['-OS LINUX VER-']
elif values['-OS MAC-']:
operating_system = 'Mac'
os_ver = values['-OS MAC VER-']
elif values['-OS OTHER-']:
operating_system = 'Other'
os_ver = values['-OS OTHER VER-']
else:
popup_error('Must choose Operating System')
return False
if os_ver == '':
popup_error('Must fill in an OS Version')
return False
checkboxes = any([values[('-CB-', i)] for i in range(len(checklist))])
if not checkboxes:
popup_error('None of the checkboxes were checked.... you need to have tried something...anything...')
return False
title = values['-TITLE-'].strip()
if len(title) == 0:
popup_error("Title can't be blank")
return False
elif title[1:len(title) - 1] == issue_type:
popup_error("Title can't be blank (only the type of issue isn't enough)")
return False
if len(values['-ML DETAILS-']) < 4:
popup_error("A little more details would be awesome")
return False
return True
def _github_issue_help():
heading_font = '_ 12 bold underline'
text_font = '_ 10'
def HelpText(text):
return Text(text, size=(80, None), font=text_font)
help_why = \
""" Let's start with a review of the Goals of the PySimpleGUI project
1. To have fun
2. For you to be successful
This form is as important as the documentation and the demo programs to meeting those goals.
The GitHub Issue GUI is here to help you more easily log issues on the PySimpleGUI GitHub Repo. """
help_goals = \
""" The goals of using GitHub Issues for PySimpleGUI question, problems and suggestions are:
* Give you direct access to engineers with the most knowledge of PySimpleGUI
* Answer your questions in the most precise and correct way possible
* Provide the highest quality solutions possible
* Give you a checklist of things to try that may solve the problem
* A single, searchable database of known problems and their workarounds
* Provide a place for the PySimpleGUI project to directly provide support to users
* A list of requested enhancements
* An easy to use interface to post code and images
* A way to track the status and have converstaions about issues
* Enable multiple people to help users """
help_explain = \
""" GitHub does not provide a "form" that normal bug-tracking-databases provide. As a result, a form was created specifically for the PySimpleGUI project.
The most obvious questions about this form are
* Why is there a form? Other projects don't have one?
* My question is an easy one, why does it still need a form?
The answer is:
I want you to get your question answered with the highest quality answer possible as quickly as possible.
The longer answer - For quite a while there was no form. It resulted the same back and forth, multiple questions comversation. "What version are you running?" "What OS are you using?" These waste precious time.
If asking nicely helps... PLEASE ... please fill out the form.
I can assure you that this form is not here to punish you. It doesn't exist to make you angry and frustrated. It's not here for any purpose than to try and get you support and make PySimpleGUI better. """
help_experience = \
""" Not many Bug-tracking systems ask about you as a user. Your experience in programming, programming in Python and programming a GUI are asked to provide you with the best possible answer. Here's why it's helpful. You're a human being, with a past, and a some amount of experience. Being able to taylor the reply to your issue in a way that fits you and your experience will result in a reply that's efficient and clear. It's not something normally done but perhaps it should be. It's meant to provide you with a personal response.
If you've been programming for a month, the person answering your question can answer your question in a way that's understandable to you. Similarly, if you've been programming for 20 years and have used multiple Python GUI frameworks, then you are unlikely to need as much explanation. You'll also have a richer GUI vocabularly. It's meant to try and give you a peronally crafted response that's on your wavelength. Fun & success... Remember those are our shared goals"""
help_steps = \
""" The steps to log an issue are:
1. Fill in the form
2. Click Post Issue """
# layout = [ [T('Goals', font=heading_font, pad=(0,0))],
# [HelpText(help_goals)],
# [T('Why?', font=heading_font, pad=(0,0))],
# [HelpText(help_why)],
# [T('FAQ', font=heading_font, pad=(0,0))],
# [HelpText(help_explain)],
# [T('Experience (optional)', font=heading_font)],
# [HelpText(help_experience)],
# [T('Steps', font=heading_font, pad=(0,0))],
# [HelpText(help_steps)],
# [B('Close')]]
t_goals = Tab('Goals', [[HelpText(help_goals)]])
t_why = Tab('Why', [[HelpText(help_why)]])
t_faq = Tab('FAQ', [[HelpText(help_explain)]])
t_exp = Tab('Experience', [[HelpText(help_experience)]])
t_steps = Tab('Steps', [[HelpText(help_steps)]])
layout = [[TabGroup([[t_goals, t_why, t_faq, t_exp, t_steps]])],
[B('Close')]]
Window('GitHub Issue GUI Help', layout, keep_on_top=True).read(close=True)
return
def main_open_github_issue():
font_frame = '_ 14'
issue_types = ('Question', 'Bug', 'Enhancement', 'Error Message')
frame_type = [[Radio(t, 1, size=(10, 1), enable_events=True, k=t)] for t in issue_types]
v_size = (15, 1)
frame_versions = [[T('Python', size=v_size), In(sys.version, size=(20, 1), k='-VER PYTHON-')],
[T('PySimpleGUI', size=v_size), In(ver, size=(20, 1), k='-VER PSG-')],
[T('tkinter', size=v_size), In(tclversion_detailed, size=(20, 1), k='-VER TK-')]]
frame_platforms = [[T('OS '), T('Details')],
[Radio('Windows', 2, running_windows(), size=(8, 1), k='-OS WIN-'), In(size=(8, 1), k='-OS WIN VER-')],
[Radio('Linux', 2, running_linux(), size=(8, 1), k='-OS LINUX-'), In(size=(8, 1), k='-OS LINUX VER-')],
[Radio('Mac', 2, running_mac(), size=(8, 1), k='-OS MAC-'), In(size=(8, 1), k='-OS MAC VER-')],
[Radio('Other', 2, size=(8, 1), k='-OS OTHER-'), In(size=(8, 1), k='-OS OTHER VER-')]]
col_experience = [[T('Optional Experience Info')],
[In(size=(4, 1), k='-EXP PROG-'), T('Years Programming')],
[In(size=(4, 1), k='-EXP PYTHON-'), T('Years Writing Python')],
[CB('Previously programmed a GUI', k='-CB PRIOR GUI-')],
[T('Share more if you want....')],
[In(size=(25, 1), k='-EXP NOTES-', expand_x=True)]]
checklist = (('Searched main docs for your problem', 'www.PySimpleGUI.org'),
('Looked for Demo Programs that are similar to your goal ', 'http://Demos.PySimpleGUI.org'),
('If not tkinter - looked for Demo Programs for specific port', ''),
('For non tkinter - Looked at readme for your specific port if not PySimpleGUI (Qt, WX, Remi)', ''),
('Run your program outside of your debugger (from a command line)', ''),
('Searched through Issues (open and closed) to see if already reported', 'http://Issues.PySimpleGUI.org'),
('Tried using the PySimpleGUI.py file on GitHub. Your problem may have already been fixed vut not released.', ''))
checklist_col1 = Col([[CB(c, k=('-CB-', i)), T(t, k='-T{}-'.format(i), enable_events=True)] for i, (c, t) in enumerate(checklist[:4])], k='-C FRAME CBs1-')
checklist_col2 = Col([[CB(c, k=('-CB-', i + 4)), T(t, k='-T{}-'.format(i + 4), enable_events=True)] for i, (c, t) in enumerate(checklist[4:])], pad=(0, 0),
k='-C FRAME CBs2-')
checklist_tabgropup = TabGroup(
[[Tab('Checklist 1 *', [[checklist_col1]], expand_x=True, expand_y=True), Tab('Checklist 2 *', [[checklist_col2]]), Tab('Experience', col_experience, k='-Tab Exp-', pad=(0, 0))]], expand_x=True, expand_y=True)
frame_details = [[Multiline(size=(65, 10), font='Courier 10', k='-ML DETAILS-', expand_x=True, expand_y=True)]]
tooltip_project_details = 'If you care to share a little about your project,\nthen by all means tell us what you are making!'
frame_project_details = [[Multiline(size=(65, 10), font='Courier 10', k='-ML PROJECT DETAILS-', expand_x=True, expand_y=True, tooltip=tooltip_project_details)]]
frame_code = [[Multiline(size=(80, 10), font='Courier 8', k='-ML CODE-', expand_x=True, expand_y=True)]]
frame_markdown = [[Multiline(size=(80, 10), font='Courier 8', k='-ML MARKDOWN-', expand_x=True, expand_y=True)]]
top_layout = [[Col([[Text('Open A GitHub Issue (* = Required Info)', font='_ 15')]], expand_x=True),
Col([[B('Help')]])
],
[Frame('Title *', [[Input(k='-TITLE-', size=(50, 1), font='_ 14', focus=True)]], font=font_frame)],
# Image(data=EMOJI_BASE64_WEARY)],
vtop([
Frame('Platform *', frame_platforms, font=font_frame),
Frame('Type of Issue *', frame_type, font=font_frame),
Frame('Versions *', frame_versions, font=font_frame),
])]
middle_layout = [
[Frame('Checklist * (note that you can click the links)', [[checklist_tabgropup]], font=font_frame, k='-CLIST FRAME-', expand_x=True, expand_y=True)],
[HorizontalSeparator()],
[T(SYMBOL_DOWN + ' If you need more room for details grab the dot and drag to expand', background_color='red', text_color='white')]]
bottom_layout = [[TabGroup([[Tab('Details *', frame_details, pad=(0, 0)),
Tab('SHORT Code to duplicate Program *', frame_code, pad=(0, 0)),
Tab('Your Project Details (optional)', frame_project_details, pad=(0, 0)),
Tab('Markdown Output', frame_markdown, pad=(0, 0)),
]], k='-TABGROUP-', expand_x=True, expand_y=True),
]]
layout_pane = Pane([Col(middle_layout), Col(bottom_layout)], key='-PANE-', expand_x=True, expand_y=True)
layout = [
[pin(B(SYMBOL_DOWN, pad=(0, 0), k='-HIDE CLIST-', tooltip='Hide/show upper sections of window')), pin(Col(top_layout, k='-TOP COL-'))],
[layout_pane],
[Col([[B('Post Issue'), B('Create Markdown Only'), B('Quit')]])]]
window = Window('Open A GitHub Issue', layout, finalize=True, resizable=True, enable_close_attempted_event=True, margins=(0, 0))
# for i in range(len(checklist)):
[window['-T{}-'.format(i)].set_cursor('hand1') for i in range(len(checklist))]
# window['-TABGROUP-'].expand(True, True, True)
# window['-ML CODE-'].expand(True, True, True)
# window['-ML DETAILS-'].expand(True, True, True)
# window['-ML MARKDOWN-'].expand(True, True, True)
# window['-PANE-'].expand(True, True, True)
window.bring_to_front()
while True: # Event Loop
event, values = window.read()
# print(event, values)
if event in (WINDOW_CLOSE_ATTEMPTED_EVENT, 'Quit'):
if popup_yes_no('Do you really want to exit?',
'If you have not clicked Post Issue button and then clicked "Submit New Issue" button '
'then your issue will not have been submitted to GitHub.\n'
'If you are having trouble with PySimpleGUI opening your browser, consider generating '
'the markdown, copying it to a text file, and then using it later to manually paste into a new issue '
'\n'
'Are you sure you want to quit?',
image=EMOJI_BASE64_PONDER,
) == 'Yes':
break
if event == WIN_CLOSED:
break
if event in ['-T{}-'.format(i) for i in range(len(checklist))]:
webbrowser.open_new_tab(window[event].get())
if event in issue_types:
title = str(values['-TITLE-'])
if len(title) != 0:
if title[0] == '[' and title.find(']'):
title = title[title.find(']') + 1:]
title = title.strip()
window['-TITLE-'].update('[{}] {}'.format(event, title))
if event == '-HIDE CLIST-':
window['-TOP COL-'].update(visible=not window['-TOP COL-'].visible)
window['-HIDE CLIST-'].update(text=SYMBOL_UP if window['-HIDE CLIST-'].get_text() == SYMBOL_DOWN else SYMBOL_DOWN)
if event == 'Help':
_github_issue_help()
elif event in ('Post Issue', 'Create Markdown Only'):
issue_type = None
for itype in issue_types:
if values[itype]:
issue_type = itype
break
if issue_type is None:
popup_error('Must choose issue type')
continue
if values['-OS WIN-']:
operating_system = 'Windows'
os_ver = values['-OS WIN VER-']
elif values['-OS LINUX-']:
operating_system = 'Linux'
os_ver = values['-OS LINUX VER-']
elif values['-OS MAC-']:
operating_system = 'Mac'
os_ver = values['-OS MAC VER-']
elif values['-OS OTHER-']:
operating_system = 'Other'
os_ver = values['-OS OTHER VER-']
else:
popup_error('Must choose Operating System')
continue
checkboxes = ['X' if values[('-CB-', i)] else ' ' for i in range(len(checklist))]
if not _github_issue_post_validate(values, checklist, issue_types):
continue
cb_dict = {'cb_docs': checkboxes[0], 'cb_demos': checkboxes[1], 'cb_demo_port': checkboxes[2], 'cb_readme_other': checkboxes[3],
'cb_command_line': checkboxes[4], 'cb_issues': checkboxes[5], 'cb_github': checkboxes[6], 'detailed_desc': values['-ML DETAILS-'],
'code': values['-ML CODE-'],
'project_details': values['-ML PROJECT DETAILS-'].rstrip()}
markdown = _github_issue_post_make_markdown(issue_type, operating_system, os_ver, 'tkinter', values['-VER PSG-'], values['-VER TK-'],
values['-VER PYTHON-'],
values['-EXP PYTHON-'],values['-EXP PROG-'], 'Yes' if values['-CB PRIOR GUI-'] else 'No',
values['-EXP NOTES-'],
**cb_dict)
window['-ML MARKDOWN-'].update(markdown)
link = _github_issue_post_make_github_link(values['-TITLE-'], window['-ML MARKDOWN-'].get())
if event == 'Post Issue':
webbrowser.open_new_tab(link)
else:
popup('Your markdown code is in the Markdown tab', keep_on_top=True)
window.close()
'''
MM'"""""`MM oo dP M""MMMMM""MM dP
M' .mmm. `M 88 M MMMMM MM 88
M MMMMMMMM dP d8888P M `M dP dP 88d888b.
M MMM `M 88 88 M MMMMM MM 88 88 88' `88
M. `MMM' .M 88 88 M MMMMM MM 88. .88 88. .88
MM. .MM dP dP M MMMMM MM `88888P' 88Y8888'
MMMMMMMMMMM MMMMMMMMMMMM
M""MMMMM""M dP
M MMMMM M 88
M MMMMM M 88d888b. .d8888b. 88d888b. .d8888b. .d888b88 .d8888b.
M MMMMM M 88' `88 88' `88 88' `88 88' `88 88' `88 88ooood8
M `MMM' M 88. .88 88. .88 88 88. .88 88. .88 88. ...
Mb dM 88Y888P' `8888P88 dP `88888P8 `88888P8 `88888P'
MMMMMMMMMMM 88 .88
dP d8888P
'''
'''
M""""""""M dP dP
Mmmm mmmM 88 88
MMMM MMMM 88d888b. 88d888b. .d8888b. .d8888b. .d888b88
MMMM MMMM 88' `88 88' `88 88ooood8 88' `88 88' `88
MMMM MMMM 88 88 88 88. ... 88. .88 88. .88
MMMM MMMM dP dP dP `88888P' `88888P8 `88888P8
MMMMMMMMMM
'''
def _the_github_upgrade_thread(window, sp):
"""
The thread that's used to run the subprocess so that the GUI can continue and the stdout/stderror is collected
:param window:
:param sp:
:return:
"""
window.write_event_value('-THREAD-', (sp, '===THEAD STARTING==='))
window.write_event_value('-THREAD-', (sp, '----- STDOUT & STDERR Follows ----'))
for line in sp.stdout:
oline = line.decode().rstrip()
window.write_event_value('-THREAD-', (sp, oline))
# DO NOT CHECK STDERR because it won't exist anymore. The subprocess code now combines stdout and stderr
# window.write_event_value('-THREAD-', (sp, '----- STDERR ----'))
# for line in sp.stderr:
# oline = line.decode().rstrip()
# window.write_event_value('-THREAD-', (sp, oline))
window.write_event_value('-THREAD-', (sp, '===THEAD DONE==='))
def _copy_files_from_github():
"""Update the local PySimpleGUI installation from Github"""
github_url = 'https://raw.githubusercontent.com/PySimpleGUI/PySimpleGUI/master/'
#files = ["PySimpleGUI.py", "setup.py"]
files = ["PySimpleGUI.py"]
# add a temp directory
temp_dir = tempfile.TemporaryDirectory()
psg_dir = os.path.join(temp_dir.name, 'PySimpleGUI')
path = psg_dir
os.mkdir(path)
# path = os.path.abspath('temp')
# download the files
downloaded = []
for file in files:
with request.urlopen(github_url + file) as response:
with open(os.path.join(path, file), 'wb') as f:
f.write(response.read())
downloaded.append(file)
# get the new version number if possible
with open(os.path.join(path, files[0]), encoding='utf-8') as f:
text_data = f.read()
package_version = "Unknown"
match = re.search(r'__version__ = \"([\d\.]+)', text_data)
if match:
package_version = match.group(1)
# create a setup.py file from scratch
setup_text = ''.join([
"import setuptools\n",
"setuptools.setup(",
"name='PySimpleGUI',",
"author='PySimpleGUI',"
"author_email='PySimpleGUI@PySimpleGUI.org',",
"description='Unreleased Development Version',",
"url='https://github.com/PySimpleGUI/PySimpleGUI',"
"packages=setuptools.find_packages(),",
"version='", package_version, "',",
"entry_points={",
"'gui_scripts': [",
"'psgissue=PySimpleGUI.PySimpleGUI:main_open_github_issue',",
"'psgmain=PySimpleGUI.PySimpleGUI:_main_entry_point',",
"'psgupgrade=PySimpleGUI.PySimpleGUI:_upgrade_entry_point',",
"'psghelp=PySimpleGUI.PySimpleGUI:main_sdk_help',",
"'psgver=PySimpleGUI.PySimpleGUI:main_get_debug_data',",
"'psgsettings=PySimpleGUI.PySimpleGUI:main_global_pysimplegui_settings',",
"],",
"},)"
])
with open(os.path.join(temp_dir.name, 'setup.py'), 'w', encoding='utf-8') as f:
f.write(setup_text)
# create an __init__.py file
with open(os.path.join(path, '__init__.py'), 'w', encoding='utf-8') as f:
f.writelines([
'name="PySimpleGUI"\n',
'from .PySimpleGUI import *\n',
'from .PySimpleGUI import __version__'
])
# install the pysimplegui package from local dist
# https://pip.pypa.io/en/stable/user_guide/?highlight=subprocess#using-pip-from-your-program
# subprocess.check_call([sys.executable, '-m', 'pip', 'install', path])
# python_command = execute_py_get_interpreter()
python_command = sys.executable # always use the currently running interpreter to perform the pip!
if 'pythonw' in python_command:
python_command = python_command.replace('pythonw', 'python')
layout = [[Text('Pip Upgrade Progress')],
[Multiline(s=(90,30), k='-MLINE-', reroute_cprint=True, write_only=True)],
[Button('Downloading...', k='-EXIT-')]]
window = Window('Pip Upgrade', layout, finalize=True, keep_on_top=True, modal=True, disable_close=True)
window.disable_debugger()
cprint('The value of sys.executable = ', sys.executable, c='white on red')
# if not python_command:
# python_command = sys.executable
cprint('Installing with the Python interpreter =', python_command, c='white on purple')
sp = execute_command_subprocess(python_command, '-m pip install', temp_dir.name, pipe_output=True)
threading.Thread(target=_the_github_upgrade_thread, args=(window, sp), daemon=True).start()
while True:
event, values = window.read()
if event == WIN_CLOSED or (event == '-EXIT-' and window['-EXIT-'].ButtonText == 'Done'):
break
if event == '-THREAD-':
cprint(values['-THREAD-'][1])
if values['-THREAD-'][1] == '===THEAD DONE===':
window['-EXIT-'].update(text='Done', button_color='white on red')
window.close()
# cleanup and remove files
temp_dir.cleanup()
return package_version
def _upgrade_from_github():
mod_version = _copy_files_from_github()
popup("*** SUCCESS ***", "PySimpleGUI.py installed version:", mod_version,
"For python located at:", os.path.dirname(sys.executable), keep_on_top=True, background_color='red',
text_color='white')
def _upgrade_gui():
try:
cur_ver = version[:version.index('\n')]
except:
cur_ver = version
if popup_yes_no('* WARNING *',
'You are about to upgrade your PySimpleGUI package previously installed via pip to the latest version location on the GitHub server.',
'You are running verrsion {}'.format(cur_ver),
'',
'Are you sure you want to overwrite this release?', title='Are you sure you want to overwrite?',
keep_on_top=True) == 'Yes':
_upgrade_from_github()
else:
popup_quick_message('Cancelled upgrade\nNothing overwritten', background_color='red', text_color='white', keep_on_top=True, non_blocking=False)
# main_upgrade_from_github = _upgrade_gui
def _upgrade_entry_point():
"""
This function is entered via the psgupgrade.exe file.
It is needed so that the exe file will exit and thus allow itself to be overwritten which
is what the upgrade will do.
It simply runs the PySimpleGUI.py file with a command line argument "upgrade" which will
actually do the upgrade.
"""
interpreter = sys.executable
if 'pythonw' in interpreter:
interpreter = interpreter.replace('pythonw', 'python')
execute_py_file(__file__, 'upgrade', interpreter_command=interpreter)
def _main_entry_point():
# print('Restarting main as a new process...(needed in case you want to GitHub Upgrade)')
# Relaunch using the same python interpreter that was used to run this function
interpreter = sys.executable
if 'pythonw' in interpreter:
interpreter = interpreter.replace('pythonw', 'python')
execute_py_file(__file__, interpreter_command=interpreter)
main_upgrade_from_github = _upgrade_entry_point
####################################################################################################
# M"""""`'"""`YM oo
# M mm. mm. M
# M MMM MMM M .d8888b. dP 88d888b.
# M MMM MMM M 88' `88 88 88' `88
# M MMM MMM M 88. .88 88 88 88
# M MMM MMM M `88888P8 dP dP dP
# MMMMMMMMMMMMMM
#
# MM'"""""`MM dP
# M' .mmm. `M 88
# M MMMMMMMM .d8888b. d8888P
# M MMM `M 88ooood8 88
# M. `MMM' .M 88. ... 88
# MM. .MM `88888P' dP
# MMMMMMMMMMM
#
# M""""""'YMM dP
# M mmmm. `M 88
# M MMMMM M .d8888b. 88d888b. dP dP .d8888b.
# M MMMMM M 88ooood8 88' `88 88 88 88' `88
# M MMMM' .M 88. ... 88. .88 88. .88 88. .88
# M .MM `88888P' 88Y8888' `88888P' `8888P88
# MMMMMMMMMMM .88
# d8888P
# M""""""'YMM dP
# M mmmm. `M 88
# M MMMMM M .d8888b. d8888P .d8888b.
# M MMMMM M 88' `88 88 88' `88
# M MMMM' .M 88. .88 88 88. .88
# M .MM `88888P8 dP `88888P8
# MMMMMMMMMMM
def main_get_debug_data(suppress_popup=False):
"""
Collect up and display the data needed to file GitHub issues.
This function will place the information on the clipboard.
You MUST paste the information from the clipboard prior to existing your application (except on Windows).
:param suppress_popup: If True no popup window will be shown. The string will be only returned, not displayed
:type suppress_popup: (bool)
:returns: String containing the information to place into the GitHub Issue
:rtype: (str)
"""
message = \
"""Python version: {}
port: tkinter
tkinter version: {}
PySimpleGUI version: {}
PySimpleGUI filename: {}""".format(sys.version, tclversion_detailed, ver, __file__)
clipboard_set(message)
# create a temp window so that the clipboard can be set
# root = tk.Tk()
# root.withdraw()
# root.clipboard_clear()
# root.clipboard_append(message)
# root.update()
# root.destroy()
if not suppress_popup:
popup_scrolled('*** Version information copied to your clipboard. Paste into your GitHub Issue. ***\n',
message, title='Select and copy this info to your GitHub Issue', keep_on_top=True, size=(100, 10))
return message
# ..######...##........#######..########.....###....##.........
# .##....##..##.......##.....##.##.....##...##.##...##.........
# .##........##.......##.....##.##.....##..##...##..##.........
# .##...####.##.......##.....##.########..##.....##.##.........
# .##....##..##.......##.....##.##.....##.#########.##.........
# .##....##..##.......##.....##.##.....##.##.....##.##.........
# ..######...########..#######..########..##.....##.########...
# ..######..########.########.########.########.####.##....##..######....######.
# .##....##.##..........##.......##.......##.....##..###...##.##....##..##....##
# .##.......##..........##.......##.......##.....##..####..##.##........##......
# ..######..######......##.......##.......##.....##..##.##.##.##...####..######.
# .......##.##..........##.......##.......##.....##..##..####.##....##........##
# .##....##.##..........##.......##.......##.....##..##...###.##....##..##....##
# ..######..########....##.......##.......##....####.##....##..######....######.
def main_global_pysimplegui_settings_erase():
"""
*** WARNING ***
Deletes the PySimpleGUI settings file without asking for verification
"""
print('********** WARNING - you are deleting your PySimpleGUI settings file **********')
print('The file being deleted is:', pysimplegui_user_settings.full_filename)
def main_global_pysimplegui_settings():
"""
Window to set settings that will be used across all PySimpleGUI programs that choose to use them.
Use set_options to set the path to the folder for all PySimpleGUI settings.
:return: True if settings were changed
:rtype: (bool)
"""
settings = pysimplegui_user_settings.read()
editor_format_dict = {
'pycharm': '<editor> --line <line> <file>',
'notepad++': '<editor> -n<line> <file>',
'sublime': '<editor> <file>:<line>',
'vim': '<editor> +<line> <file>',
'wing': '<editor> <file>:<line>',
'visual studio': '<editor> <file> /command "edit.goto <line>"',
'atom': '<editor> <file>:<line>',
'spyder': '<editor> <file>',
'thonny': '<editor> <file>',
'pydev': '<editor> <file>:<line>',
'idle': '<editor> <file>'}
tooltip = 'Format strings for some popular editors/IDEs:\n' + \
'PyCharm - <editor> --line <line> <file>\n' + \
'Notepad++ - <editor> -n<line> <file>\n' + \
'Sublime - <editor> <file>:<line>\n' + \
'vim - <editor> +<line> <file>\n' + \
'wing - <editor> <file>:<line>\n' + \
'Visual Studio - <editor> <file> /command "edit.goto <line>"\n' + \
'Atom - <editor> <file>:<line>\n' + \
'Spyder - <editor> <file>\n' + \
'Thonny - <editor> <file>\n' + \
'PyDev - <editor> <file>:<line>\n' + \
'IDLE - <editor> <file>\n'
tooltip_file_explorer = 'This is the program you normally use to "Browse" for files\n' + \
'For Windows this is normally "explorer". On Linux "nemo" is sometimes used.'
tooltip_theme = 'The normal default theme for PySimpleGUI is "Dark Blue 13\n' + \
'If you do not call theme("theme name") by your program to change the theme, then the default is used.\n' + \
'This setting allows you to set the theme that PySimpleGUI will use for ALL of your programs that\n' + \
'do not set a theme specifically.'
layout = [[T('Global PySimpleGUI Settings', font='DEFAIULT 18')],
[T('Python Interpreter (normally leave blank)', font='_ 16')],
[T('Command to run a python program:'), In(settings.get('-python command-', ''), k='-PYTHON COMMAND-', enable_events=True), FileBrowse()],
[T('Editor Settings', font='_ 16')],
[T('Command to invoke your editor:'), In(settings.get('-editor program-', ''), k='-EDITOR PROGRAM-', enable_events=True), FileBrowse()],
[T('String to launch your editor to edit at a particular line #.')],
[T('Use tags <editor> <file> <line> to specify the string')],
[T('that will be executed to edit python files using your editor')],
[T('Edit Format String (hover for tooltip)', tooltip=tooltip),
In(settings.get('-editor format string-', '<editor> <file>'), k='-EDITOR FORMAT-', tooltip=tooltip)],
[T('File Explorer Program', font='_ 16', tooltip=tooltip_file_explorer)],
[In(settings.get('-explorer program-', ''), k='-EXPLORER PROGRAM-', tooltip=tooltip_file_explorer)],
[T('Theme', font='_ 16')],
[T('Leave blank for "official" PySimpleGUI default theme: {}'.format(OFFICIAL_PYSIMPLEGUI_THEME))],
[T('Default Theme For All Programs:'),
Combo([''] + theme_list(), settings.get('-theme-', None), readonly=True, k='-THEME-', tooltip=tooltip_theme)],
# [T('Buttons (Leave Unchecked To Use Default) NOT YET IMPLEMENTED!', font='_ 16')],
# [Checkbox('Always use TTK buttons'), CBox('Always use TK Buttons')],
[B('Ok', bind_return_key=True), B('Cancel'), B('Mac Patch Control')],
]
window = Window('Settings', layout, keep_on_top=True)
while True:
event, values = window.read()
if event in ('Cancel', WIN_CLOSED):
break
if event == 'Ok':
new_theme = OFFICIAL_PYSIMPLEGUI_THEME if values['-THEME-'] == '' else values['-THEME-']
pysimplegui_user_settings.set('-editor program-', values['-EDITOR PROGRAM-'])
pysimplegui_user_settings.set('-explorer program-', values['-EXPLORER PROGRAM-'])
pysimplegui_user_settings.set('-editor format string-', values['-EDITOR FORMAT-'])
pysimplegui_user_settings.set('-python command-', values['-PYTHON COMMAND-'])
pysimplegui_user_settings.set('-theme-', new_theme)
theme(new_theme)
window.close()
return True
elif event == '-EDITOR PROGRAM-':
for key in editor_format_dict.keys():
if key in values['-EDITOR PROGRAM-'].lower():
window['-EDITOR FORMAT-'].update(value=editor_format_dict[key])
elif event == 'Mac Patch Control':
main_mac_feature_control()
# re-read the settings in case they changed
_read_mac_global_settings()
window.close()
return False
# ..######..########..##....##....##.....##.########.##.......########.
# .##....##.##.....##.##...##.....##.....##.##.......##.......##.....##
# .##.......##.....##.##..##......##.....##.##.......##.......##.....##
# ..######..##.....##.#####.......#########.######...##.......########.
# .......##.##.....##.##..##......##.....##.##.......##.......##.......
# .##....##.##.....##.##...##.....##.....##.##.......##.......##.......
# ..######..########..##....##....##.....##.########.########.##.......
def main_sdk_help():
"""
Display a window that will display the docstrings for each PySimpleGUI Element and the Window object
"""
online_help_links = {
'Button': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#button-element',
'ButtonMenu': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#buttonmenu-element',
'Canvas': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#canvas-element',
'Checkbox': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#checkbox-element',
'Column': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#column-element',
'Combo': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#combo-element',
'Frame': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#frame-element',
'Graph': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#graph-element',
'HorizontalSeparator': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#horizontalseparator-element',
'Image': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#image-element',
'Input': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#input-element',
'Listbox': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#listbox-element',
'Menu': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#menu-element',
'MenubarCustom': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#menubarcustom-element',
'Multiline': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#multiline-element',
'OptionMenu': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#optionmenu-element',
'Output': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#output-element',
'Pane': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#pane-element',
'ProgressBar': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#progressbar-element',
'Radio': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#radio-element',
'Slider': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#slider-element',
'Spin': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#spin-element',
'StatusBar': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#statusbar-element',
'Tab': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#tab-element',
'TabGroup': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#tabgroup-element',
'Table': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#table-element',
'Text': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#text-element',
'Titlebar': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#titlebar-element',
'Tree': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#tree-element',
'VerticalSeparator': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#verticalseparator-element',
'Window': r'https://pysimplegui.readthedocs.io/en/latest/call%20reference/#window',
}
NOT_AN_ELEMENT = 'Not An Element'
element_classes = Element.__subclasses__()
element_names = {element.__name__: element for element in element_classes}
element_names['Window'] = Window
element_classes.append(Window)
element_arg_default_dict, element_arg_default_dict_update = {}, {}
vars3 = [m for m in inspect.getmembers(sys.modules[__name__])]
functions = [m for m in inspect.getmembers(sys.modules[__name__], inspect.isfunction)]
functions_names_lower = [f for f in functions if f[0][0].islower()]
functions_names_upper = [f for f in functions if f[0][0].isupper()]
functions_names = sorted(functions_names_lower) + sorted(functions_names_upper)
for element in element_classes:
# Build info about init method
args = inspect.getfullargspec(element.__init__).args[1:]
defaults = inspect.getfullargspec(element.__init__).defaults
# print('------------- {element}----------')
# print(args)
# print(defaults)
if len(args) != len(defaults):
diff = len(args) - len(defaults)
defaults = ('NO DEFAULT',) * diff + defaults
args_defaults = []
for i, a in enumerate(args):
args_defaults.append((a, defaults[i]))
element_arg_default_dict[element.__name__] = args_defaults
# Build info about update method
try:
args = inspect.getfullargspec(element.update).args[1:]
defaults = inspect.getfullargspec(element.update).defaults
if args is None or defaults is None:
element_arg_default_dict_update[element.__name__] = (('', ''),)
continue
if len(args) != len(defaults):
diff = len(args) - len(defaults)
defaults = ('NO DEFAULT',) * diff + defaults
args_defaults = []
for i, a in enumerate(args):
args_defaults.append((a, defaults[i]))
element_arg_default_dict_update[element.__name__] = args_defaults if len(args_defaults) else (('', ''),)
except Exception as e:
pass
# Add on the pseudo-elements
element_names['MenubarCustom'] = MenubarCustom
element_names['Titlebar'] = Titlebar
buttons = [[B(e, pad=(0, 0), size=(22, 1), font='Courier 10')] for e in sorted(element_names.keys())]
buttons += [[B('Func Search', pad=(0, 0), size=(22, 1), font='Courier 10')]]
button_col = Col(buttons, vertical_alignment='t')
mline_col = Column([[Multiline(size=(100, 46), key='-ML-', write_only=True, reroute_stdout=True, font='Courier 10', expand_x=True, expand_y=True)],
[T(size=(80, 1), font='Courier 10 underline', k='-DOC LINK-', enable_events=True)]], pad=(0, 0), expand_x=True, expand_y=True, vertical_alignment='t')
layout = [[button_col, mline_col]]
layout += [[CBox('Summary Only', enable_events=True, k='-SUMMARY-'), CBox('Display Only PEP8 Functions', default=True, k='-PEP8-')]]
# layout += [[Button('Exit', size=(15, 1))]]
window = Window('SDK API Call Reference', layout, resizable=True, use_default_focus=False, keep_on_top=True, icon=EMOJI_BASE64_THINK, finalize=True, right_click_menu=MENU_RIGHT_CLICK_EDITME_EXIT)
window['-DOC LINK-'].set_cursor('hand1')
online_help_link = ''
ml = window['-ML-']
current_element = ''
try:
while True: # Event Loop
event, values = window.read()
if event in (WIN_CLOSED, 'Exit'):
break
if event == '-DOC LINK-':
if webbrowser_available and online_help_link:
webbrowser.open_new_tab(online_help_link)
if event == '-SUMMARY-':
event = current_element
if event in element_names.keys():
current_element = event
window['-ML-'].update('')
online_help_link = online_help_links.get(event, '')
window['-DOC LINK-'].update(online_help_link)
if not values['-SUMMARY-']:
elem = element_names[event]
ml.print(pydoc.help(elem))
# print the aliases for the class
ml.print('\n--- Shortcut Aliases for Class ---')
for v in vars3:
if elem == v[1] and elem.__name__ != v[0]:
print(v[0])
ml.print('\n--- Init Parms ---')
else:
elem = element_names[event]
if inspect.isfunction(elem):
ml.print('Not a class...It is a function', background_color='red', text_color='white')
else:
element_methods = [m[0] for m in inspect.getmembers(Element, inspect.isfunction) if not m[0].startswith('_') and not m[0][0].isupper()]
methods = inspect.getmembers(elem, inspect.isfunction)
methods = [m[0] for m in methods if not m[0].startswith('_') and not m[0][0].isupper()]
unique_methods = [m for m in methods if m not in element_methods and not m[0][0].isupper()]
properties = inspect.getmembers(elem, lambda o: isinstance(o, property))
properties = [p[0] for p in properties if not p[0].startswith('_')]
ml.print('--- Methods ---', background_color='red', text_color='white')
ml.print('\n'.join(methods))
ml.print('--- Properties ---', background_color='red', text_color='white')
ml.print('\n'.join(properties))
if elem != NOT_AN_ELEMENT:
if issubclass(elem, Element):
ml.print('Methods Unique to This Element', background_color='red', text_color='white')
ml.print('\n'.join(unique_methods))
ml.print('========== Init Parms ==========', background_color='#FFFF00', text_color='black')
elem_text_name = event
for parm, default in element_arg_default_dict[elem_text_name]:
ml.print('{:18}'.format(parm), end=' = ')
ml.print(default, end=',\n')
if elem_text_name in element_arg_default_dict_update:
ml.print('========== Update Parms ==========', background_color='#FFFF00', text_color='black')
for parm, default in element_arg_default_dict_update[elem_text_name]:
ml.print('{:18}'.format(parm), end=' = ')
ml.print(default, end=',\n')
ml.set_vscroll_position(0) # scroll to top of multoline
elif event == 'Func Search':
search_string = popup_get_text('Search for this in function list:', keep_on_top=True)
if search_string is not None:
online_help_link = ''
window['-DOC LINK-'].update('')
ml.update('')
for f_entry in functions_names:
f = f_entry[0]
if search_string in f.lower() and not f.startswith('_'):
if (values['-PEP8-'] and not f[0].isupper()) or not values['-PEP8-']:
if values['-SUMMARY-']:
ml.print(f)
else:
ml.print('=========== ' + f + '===========', background_color='#FFFF00', text_color='black')
ml.print(pydoc.help(f_entry[1]))
ml.set_vscroll_position(0) # scroll to top of multoline
except Exception as e:
_error_popup_with_traceback('Exception in SDK reference', e)
window.close()
# oo
#
# 88d8b.d8b. .d8888b. dP 88d888b.
# 88'`88'`88 88' `88 88 88' `88
# 88 88 88 88. .88 88 88 88
# dP dP dP `88888P8 dP dP dP
#
#
# M""MMM""MMM""M oo dP
# M MMM MMM M 88
# M MMP MMP M dP 88d888b. .d888b88 .d8888b. dP dP dP
# M MM' MM' .M 88 88' `88 88' `88 88' `88 88 88 88
# M `' . '' .MM 88 88 88 88. .88 88. .88 88.88b.88'
# M .d .dMMM dP dP dP `88888P8 `88888P' 8888P Y8P
# MMMMMMMMMMMMMM
#
# MP""""""`MM dP dP dP
# M mmmmm..M 88 88 88
# M. `YM d8888P .d8888b. 88d888b. d8888P .d8888b. 88d888b. .d8888b. 88d888b. .d8888b.
# MMMMMMM. M 88 88' `88 88' `88 88 Y8ooooo. 88' `88 88ooood8 88' `88 88ooood8
# M. .MMM' M 88 88. .88 88 88 88 88 88 88. ... 88 88. ...
# Mb. .dM dP `88888P8 dP dP `88888P' dP dP `88888P' dP `88888P'
# MMMMMMMMMMM
def _main_switch_theme():
layout = [
[Text('Click a look and feel color to see demo window')],
[Listbox(values=theme_list(),
size=(20, 20), key='-LIST-')],
[Button('Choose'), Button('Cancel')]]
window = Window('Change Themes', layout)
event, values = window.read(close=True)
if event == 'Choose':
theme_name = values['-LIST-'][0]
theme(theme_name)
def _create_main_window():
"""
Creates the main test harness window.
:return: The test window
:rtype: Window
"""
# theme('dark blue 3')
# theme('dark brown 2')
# theme('dark')
# theme('dark red')
# theme('Light Green 6')
# theme('Dark Grey 8')
tkversion = tkinter.TkVersion
tclversion = tkinter.TclVersion
tclversion_detailed = tkinter.Tcl().eval('info patchlevel')
print('Starting up PySimpleGUI Diagnostic & Help System')
print('PySimpleGUI long version = ', version)
print('PySimpleGUI Version ', ver, '\ntcl ver = {}'.format(tclversion),
'tkinter version = {}'.format(tkversion), '\nPython Version {}'.format(sys.version))
print('tcl detailed version = {}'.format(tclversion_detailed))
print('PySimpleGUI.py location', __file__)
# ------ Menu Definition ------ #
menu_def = [['&File', ['!&Open', '&Save::savekey', '---', '&Properties', 'E&xit']],
['&Edit', ['&Paste', ['Special', 'Normal', '!Disabled'], 'Undo'], ],
['&Debugger', ['Popout', 'Launch Debugger']],
['!&Disabled', ['Popout', 'Launch Debugger']],
['&Toolbar', ['Command &1', 'Command &2', 'Command &3', 'Command &4']],
['&Help', '&About...'], ]
button_menu_def = ['unused', ['&Paste', ['Special', 'Normal', '!Disabled'], 'Undo', 'Exit'], ]
treedata = TreeData()
treedata.Insert("", '_A_', 'Tree Item 1', [1, 2, 3], )
treedata.Insert("", '_B_', 'B', [4, 5, 6], )
treedata.Insert("_A_", '_A1_', 'Sub Item 1', ['can', 'be', 'anything'], )
treedata.Insert("", '_C_', 'C', [], )
treedata.Insert("_C_", '_C1_', 'C1', ['or'], )
treedata.Insert("_A_", '_A2_', 'Sub Item 2', [None, None])
treedata.Insert("_A1_", '_A3_', 'A30', ['getting deep'])
treedata.Insert("_C_", '_C2_', 'C2', ['nothing', 'at', 'all'])
for i in range(100):
treedata.Insert('_C_', i, i, [])
frame1 = [
[Input('Input Text', size=(25, 1)), ],
[Multiline(size=(30, 5), default_text='Multiline Input')],
]
frame2 = [
# [ProgressBar(100, bar_color=('red', 'green'), orientation='h')],
[Listbox(['Listbox 1', 'Listbox 2', 'Listbox 3'], select_mode=SELECT_MODE_EXTENDED, size=(20, 5), no_scrollbar=True)],
[Combo(['Combo item %s' % i for i in range(5)], size=(20, 3), default_value='Combo item 2', key='-COMBO1-', )],
[Combo(['Combo item %s' % i for i in range(5)], size=(20, 3), font='Courier 20', default_value='Combo item 2', key='-COMBO2-', )],
# [Combo(['Combo item 1', 2,3,4], size=(20, 3), readonly=False, text_color='blue', background_color='red', key='-COMBO2-')],
[Spin([1, 2, 3, 'a', 'b', 'c'], initial_value='a', size=(4, 3))],
]
frame3 = [
[Checkbox('Checkbox1', True), Checkbox('Checkbox1')],
[Radio('Radio Button1', 1), Radio('Radio Button2', 1, default=True, tooltip='Radio 2')],
[T('', size=(1, 4))],
]
frame4 = [
[Slider(range=(0, 100), orientation='v', size=(7, 15), default_value=40, key='-SLIDER1-'),
Slider(range=(0, 100), orientation='h', size=(11, 15), default_value=40, key='-SLIDER2-'), ],
]
matrix = [[str(x * y) for x in range(1, 5)] for y in range(1, 8)]
frame5 = [[
Table(values=matrix, headings=matrix[0],
auto_size_columns=False, display_row_numbers=True, change_submits=False, justification='right',
num_rows=10, alternating_row_color='lightblue', key='-TABLE-',
col_widths=[5, 5, 5, 5], size=(400, 200), ),
T(' '),
Tree(data=treedata, headings=['col1', 'col2', 'col3'], change_submits=True, auto_size_columns=True,
num_rows=10, col0_width=10, key='-TREE-', show_expanded=True, )],[VStretch()]]
frame7 = [[Image(EMOJI_BASE64_HAPPY_HEARTS, enable_events=True, k='-EMOJI-HEARTS-'), T('Do you'), Image(HEART_3D_BASE64, subsample=3, enable_events=True, k='-HEART-'), T('so far?')],
[T('Want to be taught PySimpleGUI? Then maybe the "Official PySimpleGUI Course" on Udemy is for you.')],
[T('Coupon codes are sometimes around so check docs, announcements, easter eggs on this page, to see specials.')],
[B(image_data=UDEMY_ICON, enable_events=True, k='-UDEMY-')],
[T('It is financially draining to operate a project this huge. ANY help helps and is very appreciated')],
[B(image_data=ICON_BUY_ME_A_COFFEE, enable_events=True, k='-COFFEE-')]]
pop_test_tab_layout = [
[T('Popup tests... good idea!'),Image(EMOJI_BASE64_HAPPY_IDEA), Push(), B('Popup', k='P '), B('No Titlebar', k='P NoTitle'), B('Not Modal', k='P NoModal'), B('Non Blocking', k='P NoBlock'), B('Auto Close', k='P AutoClose')],
[T('Get popups too!'), Push(), B('Get File'), B('Get Folder'), B('Get Date'), B('Get Text')]]
graph_elem = Graph((600, 250), (0, 0), (800, 300), key='+GRAPH+')
frame6 = [[graph_elem]]
global_settings_tab_layout = [[T('Global Settings:', font='_ 15')],
[T('Settings Filename:'), T(pysimplegui_user_settings.full_filename, s=(50,2))],
[T('Settings Dictionary:'), MLine(pysimplegui_user_settings, size=(50,10))],
]
themes_tab_layout = [[T('You can see a preview of the themes, the color swatches, or switch themes for this window')],
[T('If you want to change the default theme for PySimpleGUI, use the Global Settings')],
[B('Themes'), B('Theme Swatches'), B('Switch Themes')]]
tab1 = Tab('Graph\n', frame6, tooltip='Graph is in here', title_color='red')
tab2 = Tab('CB, Radio\nList, Combo',
[[Frame('Multiple Choice Group', frame2, title_color='#FFFFFF', tooltip='Checkboxes, radio buttons, etc', vertical_alignment='t',),
Frame('Binary Choice Group', frame3, title_color='#FFFFFF', tooltip='Binary Choice', vertical_alignment='t', ), ]])
# tab3 = Tab('Table and Tree', [[Frame('Structured Data Group', frame5, title_color='red', element_justification='l')]], tooltip='tab 3', title_color='red', )
tab3 = Tab('Table &\nTree', [[Column(frame5, element_justification='l', vertical_alignment='t')]], tooltip='tab 3', title_color='red', k='-TAB TABLE-')
tab4 = Tab('Sliders\n', [[Frame('Variable Choice Group', frame4, title_color='blue')]], tooltip='tab 4', title_color='red', k='-TAB VAR-')
tab5 = Tab('Input\nMultiline', [[Frame('TextInput', frame1, title_color='blue')]], tooltip='tab 5', title_color='red', k='-TAB TEXT-')
tab6 = Tab('Course or\nSponsor', frame7, k='-TAB SPONSOR-')
tab7 = Tab('Popups\n', pop_test_tab_layout, k='-TAB POPUP-')
tab8 = Tab('Themes\n', themes_tab_layout, k='-TAB THEMES-')
tab9 = Tab('Global\nSettings', global_settings_tab_layout, k='-TAB GlOBAL SETTINGS-')
def VerLine(version, description, justification='r', size=(40, 1)):
return [T(version, justification=justification, font='Any 12', text_color='yellow', size=size, pad=(0,0)), vtop(T(description, font='Any 12', pad=(0,0)))]
layout_top = Column([
[Image(EMOJI_BASE64_HAPPY_BIG_SMILE, enable_events=True, key='-LOGO-', tooltip='This is PySimpleGUI logo'),
Image(data=DEFAULT_BASE64_LOADING_GIF, enable_events=True, key='-IMAGE-'),
Text('PySimpleGUI Test Harness\nYou are running PySimpleGUI.py file instead of importing', font='ANY 15',
tooltip='My tooltip', key='-TEXT1-')],
VerLine(ver, 'PySimpleGUI Version') + [Image(HEART_3D_BASE64, subsample=4)],
VerLine('{}/{}'.format(tkversion, tclversion), 'TK/TCL Versions'),
VerLine(tclversion_detailed, 'detailed tkinter version'),
VerLine(os.path.dirname(os.path.abspath(__file__)), 'PySimpleGUI Location', size=(40, 2)),
VerLine(sys.version, 'Python Version', size=(40, 2))], pad=(0, 0))
layout_bottom = [
[B(SYMBOL_DOWN, pad=(0, 0), k='-HIDE TABS-'),
pin(Col([[TabGroup([[tab1, tab2, tab3, tab6, tab4, tab5, tab7, tab8, tab9]], key='-TAB_GROUP-')]], k='-TAB GROUP COL-'))],
[B('Button', highlight_colors=('yellow', 'red'),pad=(1, 0)), B('Hide Stuff',pad=(1, 0), metadata='my metadata'),
B('ttk Button', use_ttk_buttons=True, tooltip='This is a TTK Button',pad=(1, 0)),
B('See-through Mode', tooltip='Make the background transparent',pad=(1, 0)),
B('Upgrade PySimpleGUI from GitHub', button_color='white on red', key='-INSTALL-',pad=(1, 0)),
B('Global Settings', tooltip='Settings across all PySimpleGUI programs',pad=(1, 0)),
B('Exit', tooltip='Exit button',pad=(1, 0))],
# [B(image_data=ICON_BUY_ME_A_COFFEE,pad=(1, 0), key='-COFFEE-'),
[B(image_data=UDEMY_ICON,pad=(1, 0), key='-UDEMY-'),
B('SDK Reference', pad=(1, 0)), B('Open GitHub Issue',pad=(1, 0)), B('Versions for GitHub',pad=(1, 0)),
ButtonMenu('ButtonMenu', button_menu_def, pad=(1, 0),key='-BMENU-', tearoff=True)
]]
layout = [[]]
if not USE_CUSTOM_TITLEBAR:
layout += [[Menu(menu_def, key='-MENU-', font='Courier 15', background_color='red', text_color='white', disabled_text_color='yellow', tearoff=True)]]
else:
layout += [[MenubarCustom(menu_def, key='-MENU-', font='Courier 15', bar_background_color=theme_background_color(), bar_text_color=theme_text_color(),
background_color='red', text_color='white', disabled_text_color='yellow')]]
layout += [[layout_top] + [ProgressBar(max_value=800, size=(20, 25), orientation='v', key='+PROGRESS+')]]
layout += layout_bottom
window = Window('PySimpleGUI Main Test Harness', layout,
# font=('Helvetica', 18),
# background_color='black',
right_click_menu=['&Right', ['Right', '!&Click', '&Menu', 'E&xit', 'Properties']],
# transparent_color= '#9FB8AD',
resizable=True,
keep_on_top=True,
element_justification='left', # justify contents to the left
metadata='My window metadata',
finalize=True,
grab_anywhere=True,
enable_close_attempted_event=True,
# ttk_theme=THEME_ALT,
# icon=PSG_DEBUGGER_LOGO,
# icon=PSGDebugLogo,
)
# window['-SPONSOR-'].set_cursor(cursor='hand2')
window._see_through = False
return window
# M"""""`'"""`YM oo
# M mm. mm. M
# M MMM MMM M .d8888b. dP 88d888b.
# M MMM MMM M 88' `88 88 88' `88
# M MMM MMM M 88. .88 88 88 88
# M MMM MMM M `88888P8 dP dP dP
# MMMMMMMMMMMMMM
def main():
"""
The PySimpleGUI "Test Harness". This is meant to be a super-quick test of the Elements.
"""
window = _create_main_window()
set_options(keep_on_top=True)
graph_elem = window['+GRAPH+']
i = 0
# Don't use the debug window
# Print('', location=(0, 0), font='Courier 10', size=(100, 20), grab_anywhere=True)
# print(window.element_list())
while True: # Event Loop
event, values = window.read(timeout=5)
if event != TIMEOUT_KEY:
print(event, values)
# Print(event, text_color='white', background_color='red', end='')
# Print(values)
if event == WIN_CLOSED or event == WIN_CLOSE_ATTEMPTED_EVENT or event == 'Exit' or (event == '-BMENU-' and values['-BMENU-'] == 'Exit'):
break
if i < 800:
graph_elem.DrawLine((i, 0), (i, random.randint(0, 300)), width=1, color='#{:06x}'.format(random.randint(0, 0xffffff)))
else:
graph_elem.Move(-1, 0)
graph_elem.DrawLine((i, 0), (i, random.randint(0, 300)), width=1, color='#{:06x}'.format(random.randint(0, 0xffffff)))
window['+PROGRESS+'].UpdateBar(i % 800)
window.Element('-IMAGE-').UpdateAnimation(DEFAULT_BASE64_LOADING_GIF, time_between_frames=50)
i += 1
if event == 'Button':
window.Element('-TEXT1-').SetTooltip('NEW TEXT')
window.Element('-MENU-').Update(visible=True)
elif event.startswith('Hide'):
# window.Normal()
window.Element('-MENU-').Update(visible=False)
elif event == 'Popout':
show_debugger_popout_window()
elif event == 'Launch Debugger':
show_debugger_window()
elif event == 'About...':
popup('About this program...', 'You are looking at the test harness for the PySimpleGUI program', version, keep_on_top=True, image=DEFAULT_BASE64_ICON)
elif event.startswith('See'):
window._see_through = not window._see_through
window.set_transparent_color(theme_background_color() if window._see_through else '')
elif event == '-INSTALL-':
_upgrade_gui()
elif event == 'Popup':
popup('This is your basic popup')
elif event == 'Get File':
popup_scrolled('Returned:', popup_get_file('Get File'))
elif event == 'Get Folder':
popup_scrolled('Returned:', popup_get_folder('Get Folder'))
elif event == 'Get Date':
popup_scrolled('Returned:', popup_get_date())
elif event == 'Get Text':
popup_scrolled('Returned:', popup_get_text('Enter some text'))
elif event.startswith('-UDEMY-'):
webbrowser.open_new_tab(r'https://udemy.com/PySimpleGUI')
elif event.startswith('-SPONSOR-'):
if webbrowser_available:
webbrowser.open_new_tab(r'https://www.paypal.me/pythongui')
elif event == '-COFFEE-':
if webbrowser_available:
# webbrowser.open_new_tab(r'https://udemy.com/PySimpleGUI')
webbrowser.open_new_tab(r'https://www.buymeacoffee.com/PySimpleGUI')
elif event in ('-EMOJI-HEARTS-', '-HEART-'):
popup_scrolled("Oh look! It's a Udemy discsount coupon!", '25506BBB290573A0BCB4')
elif event == 'Themes':
search_string = popup_get_text('Enter a search term or leave blank for all themes', 'Show Available Themes', keep_on_top=True)
if search_string is not None:
theme_previewer(search_string=search_string)
elif event == 'Theme Swatches':
theme_previewer_swatches()
elif event == 'Switch Themes':
window.close()
_main_switch_theme()
window = _create_main_window()
graph_elem = window['+GRAPH+']
elif event == '-HIDE TABS-':
window['-TAB GROUP COL-'].update(visible=window['-TAB GROUP COL-'].metadata == True)
window['-TAB GROUP COL-'].metadata = not window['-TAB GROUP COL-'].metadata
window['-HIDE TABS-'].update(text=SYMBOL_UP if window['-TAB GROUP COL-'].metadata else SYMBOL_DOWN)
elif event == 'SDK Reference':
main_sdk_help()
elif event == 'Global Settings':
if main_global_pysimplegui_settings():
theme(pysimplegui_user_settings.get('-theme-', OFFICIAL_PYSIMPLEGUI_THEME))
window.close()
window = _create_main_window()
graph_elem = window['+GRAPH+']
elif event.startswith('P '):
if event == 'P ':
popup('Normal Popup - Modal', keep_on_top=True)
elif event == 'P NoTitle':
popup_no_titlebar('No titlebar', keep_on_top=True)
elif event == 'P NoModal':
popup('Normal Popup - Not Modal', 'You can interact with main window menubar ',
'but will have no effect immediately', 'button clicks will happen after you close this popup', modal=False, keep_on_top=True)
elif event == 'P NoBlock':
popup_non_blocking('Non-blocking', 'The background window should still be running', keep_on_top=True)
elif event == 'P AutoClose':
popup_auto_close('Will autoclose in 3 seconds', auto_close_duration=3, keep_on_top=True)
elif event == 'Versions for GitHub':
main_get_debug_data()
elif event == 'Open GitHub Issue':
window.minimize()
main_open_github_issue()
window.normal()
i += 1
# _refresh_debugger()
print('event = ', event)
window.close()
# ------------------------ PEP8-ify The SDK ------------------------#
ChangeLookAndFeel = change_look_and_feel
ConvertArgsToSingleString = convert_args_to_single_string
EasyPrint = easy_print
Print = easy_print
eprint = easy_print
sgprint = easy_print
PrintClose = easy_print_close
sgprint_close = easy_print_close
EasyPrintClose = easy_print_close
FillFormWithValues = fill_form_with_values
GetComplimentaryHex = get_complimentary_hex
ListOfLookAndFeelValues = list_of_look_and_feel_values
ObjToString = obj_to_string
ObjToStringSingleObj = obj_to_string_single_obj
OneLineProgressMeter = one_line_progress_meter
OneLineProgressMeterCancel = one_line_progress_meter_cancel
Popup = popup
PopupNoFrame = popup_no_titlebar
popup_no_frame = popup_no_titlebar
PopupNoBorder = popup_no_titlebar
popup_no_border = popup_no_titlebar
PopupAnnoying = popup_no_titlebar
popup_annoying = popup_no_titlebar
PopupAnimated = popup_animated
PopupAutoClose = popup_auto_close
PopupCancel = popup_cancel
PopupError = popup_error
PopupGetFile = popup_get_file
PopupGetFolder = popup_get_folder
PopupGetText = popup_get_text
PopupNoButtons = popup_no_buttons
PopupNoTitlebar = popup_no_titlebar
PopupNoWait = popup_non_blocking
popup_no_wait = popup_non_blocking
PopupNonBlocking = popup_non_blocking
PopupOK = popup_ok
PopupOKCancel = popup_ok_cancel
PopupQuick = popup_quick
PopupQuickMessage = popup_quick_message
PopupScrolled = popup_scrolled
PopupTimed = popup_auto_close
popup_timed = popup_auto_close
PopupYesNo = popup_yes_no
RGB = rgb
SetGlobalIcon = set_global_icon
SetOptions = set_options
sprint = popup_scrolled
ScrolledTextBox = popup_scrolled
TimerStart = timer_start
TimerStop = timer_stop
test = main
sdk_help = main_sdk_help
pysimplegui_user_settings = UserSettings(filename=DEFAULT_USER_SETTINGS_PYSIMPLEGUI_FILENAME, path=DEFAULT_USER_SETTINGS_PYSIMPLEGUI_PATH)
# ------------------------ Set the "Official PySimpleGUI Theme Colors" ------------------------
theme(theme_global())
# See if running on Trinket. If Trinket, then use custom titlebars since Trinket doesn't supply any
if running_trinket():
USE_CUSTOM_TITLEBAR = True
if tclversion_detailed.startswith('8.5'):
warnings.warn('You are running a VERY old version of tkinter {}. You cannot use PNG formatted images for example. Please upgrade to 8.6.x'.format(tclversion_detailed), UserWarning)
_read_mac_global_settings()
# if running_mac():
# print('Your Mac patches are:')
# print('Modal windows disabled:', ENABLE_MAC_MODAL_DISABLE_PATCH)
# print('No titlebar patch:', ENABLE_MAC_NOTITLEBAR_PATCH)
# print('No grab anywhere allowed with titlebar:', ENABLE_MAC_DISABLE_GRAB_ANYWHERE_WITH_TITLEBAR)
# print('Currently the no titlebar patch ' + ('WILL' if _mac_should_apply_notitlebar_patch() else 'WILL NOT') + ' be applied')
# -------------------------------- ENTRY POINT IF RUN STANDALONE -------------------------------- #
if __name__ == '__main__':
# To execute the upgrade from command line, type:
# python -m PySimpleGUI.PySimpleGUI upgrade
if len(sys.argv) > 1 and sys.argv[1] == 'upgrade':
_upgrade_gui()
exit(0)
elif len(sys.argv) > 1 and sys.argv[1] == 'help':
main_sdk_help()
exit(0)
main()
exit(0)