LOTS of new doc strings. Renamed some functions to begin with _. Window.Show, ReadNonBlocking, Added giant licensing info

This commit is contained in:
MikeTheWatchGuy 2019-07-06 11:51:03 -04:00
parent 9adb0e3691
commit dab3f1f1f3
1 changed files with 248 additions and 109 deletions

View File

@ -1,8 +1,103 @@
#!/usr/bin/python3
version = __version__ = "4.1.0.5 Unreleased"
version = __version__ = "4.1.0.8 Unreleased"
# 888888ba .d88888b oo dP .88888. dP dP dP
# 88 `8b 88. "' 88 d8' `88 88 88 88
# a88aaaa8P' dP dP `Y88888b. dP 88d8b.d8b. 88d888b. 88 .d8888b. 88 88 88 88
# 88 88 88 `8b 88 88'`88'`88 88' `88 88 88ooood8 88 YP88 88 88 88
# 88 88. .88 d8' .8P 88 88 88 88 88. .88 88 88. ... Y8. .88 Y8. .8P 88
# dP `8888P88 Y88888P dP dP dP dP 88Y888P' dP `88888P' `88888' `Y88888P' dP
# .88 88
# d8888P dP
# __ __
# / | / |
# $$ | ______ ______ ______ $$ |
# $$ | / \ / \ / \ $$ |
# $$ | /$$$$$$ |/$$$$$$ | $$$$$$ |$$ |
# $$ | $$ $$ |$$ | $$ | / $$ |$$ |
# $$ |_____ $$$$$$$$/ $$ \__$$ |/$$$$$$$ |$$ |
# $$ |$$ |$$ $$ |$$ $$ |$$ |
# $$$$$$$$/ $$$$$$$/ $$$$$$$ | $$$$$$$/ $$/
# / \__$$ |
# $$ $$/
# $$$$$$/
"""
Copyright 2018, 2019 PySimpleGUI.org
OK, let's get the bullshit out of the way
This software is available for your use under a MODIFIED LGPL3+ license
This notice, these first 83 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"
And just what the fuck is that? Well, it's LPGL3+ and these FOUR simple stipulations.
1. These and all comments are to remain in this document
2. You will not post this software in a repository or a location for others to download from:
A. Unless you have made 10 lines of changes
3. Forking is OK and does NOT require any changes as long as it is obvious forked and stated on the page
where your software is being hosted. For example, GitHub does a fantastic job of indicating if a repository
is the result of a fork.
4. 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. 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.
Please don't waste anyone's time by filing an Issue unless you have a genuine copy of the software.
-----------------------------------------------------------------------------------------------------------------
I absolutely hate having to include that nonsense, but every word is there for solid reasons.
How about having FUN with this package?? Terrible note to begin this journey of actually having fun making
GUI based applications so I'll try to make it up to you.
The first bit of good news for you is that literally 100s of pages of documentation await you. And nearly 200
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! 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://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.
"""
# do the Python 2 or 3 check so the right tkinter stuff can get pulled in
import sys
if sys.version_info[0] >= 3:
import tkinter as tk
from tkinter import filedialog
@ -10,7 +105,7 @@ if sys.version_info[0] >= 3:
from tkinter import ttk
import tkinter.scrolledtext as tkst
import tkinter.font
else:
else: # Do NOT remove any of these regardless of what your IDE or lint says. They are transformed in the 3 to 2 process
import Tkinter as tk
import tkFileDialog
import ttk
@ -24,19 +119,10 @@ import pickle
import calendar
import textwrap
import inspect
from typing import List, Any, Union
from typing import List, Any, Union, Tuple, Dict
from random import randint
import warnings
# 888888ba .d88888b oo dP .88888. dP dP dP
# 88 `8b 88. "' 88 d8' `88 88 88 88
# a88aaaa8P' dP dP `Y88888b. dP 88d8b.d8b. 88d888b. 88 .d8888b. 88 88 88 88
# 88 88 88 `8b 88 88'`88'`88 88' `88 88 88ooood8 88 YP88 88 88 88
# 88 88. .88 d8' .8P 88 88 88 88 88. .88 88 88. ... Y8. .88 Y8. .8P 88
# dP `8888P88 Y88888P dP dP dP dP 88Y888P' dP `88888P' `88888' `Y88888P' dP
# .88 88
# d8888P dP
g_time_start = 0
@ -1315,7 +1401,7 @@ class Spin(Element):
"""
:param values: List[Any] List of valid values
:param initial_value: [Any] Initial item to show in window. Choose from list of values supplied
:param initial_value: (Any) Initial item to show in window. Choose from list of values supplied
:param disabled: (bool) set disable state
:param change_submits: (bool) DO NOT USE. Only listed for backwards compat - Use enable_events instead
:param enable_events: (bool) Turns on the element specific events. Spin events happen when an item changes
@ -1411,7 +1497,9 @@ class Spin(Element):
# ---------------------------------------------------------------------- #
class Multiline(Element):
"""
Multiline Element - Display and read multiple lines of text. This is both an input and output 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,
@ -1452,8 +1540,7 @@ class Multiline(Element):
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.TKText = self.Widget = None # type: tk.scrolledtext.ScrolledText
self.TKText = self.Widget = None # type: tkst.ScrolledText
super().__init__(ELEM_TYPE_INPUT_MULTILINE, size=size, 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)
return
@ -1508,14 +1595,14 @@ class Multiline(Element):
"""
Return current contents of the Multiline Element
:return: (str) current contents of the Multiline Input element
:return: (str) current contents of the Multiline Element (used as an input type of Multiline
"""
return self.TKText.get(1.0, tk.END)
def SetFocus(self, force=False):
"""
Moves the focus to this Multiline
Moves the focus (that little blinking cursor) to this Multiline Element
:param force: (bool). If True, will call focus_force instead of focus_set
"""
@ -1537,8 +1624,7 @@ class Multiline(Element):
# ---------------------------------------------------------------------- #
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.
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), auto_size_text=None, click_submits=False, enable_events=False,
@ -1619,28 +1705,28 @@ T = 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), 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,
key=None, tooltip=None, visible=True):
"""
:param text: (required) text that is to be displayed in the widget
:param text: Text that is to be displayed in the widget
:param size: (w,h) w=characters-wide, h=rows-high
:param auto_size_text: True if size should fit the text length
:param click_submits: ????????????????????
:param enable_events: Turns on the element specific events.(Default = False)
: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 = RELIEF_SUNKEN)
:param font: specifies the font family, size, etc
:param text_color: color of the text
:param background_color: color of background
:param justification: justification for data display
:param pad: Amount of padding to put around element
:param key: Used with window.FindElement and with return values to uniquely identify this element to uniquely identify this element
:param click_submits: (bool) DO NOT USE. Only listed for backwards compat - Use enable_events instead
:param enable_events: (bool) Turns on the element specific events. StatusBar events occur when the bar is clicked
: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`
:param font: Union[str, tuple] specifies the font family, size, etc
:param text_color: (str) color of the text
:param background_color: (str) color of background
:param justification: (str) how string should be aligned within space provided by size. Valid choices = `left`, `right`, `center`
:param pad: (int, int) or ((int, int),(int,int)) Amount of padding to put around element (left/right, top/bottom) or ((left, right), (top, bottom))
:param key: (Any) Used with window.FindElement and with return values to uniquely identify this element to uniquely identify this element
:param tooltip: (str) text, that will appear when mouse hovers over the element
:param visible: set visibility state of the element (Default = True)
:param visible: (bool) set visibility state of the element
"""
self.DisplayText = text
@ -1663,11 +1749,11 @@ class StatusBar(Element):
"""
Changes some of the settings for the Status Bar Element. Must call `Window.Read` or `Window.Finalize` prior
:param value: ??????????????????????????
:param background_color: color of background
:param text_color: color of the text
:param font: specifies the font family, size, etc
:param visible: (bool) control visibility of element
:param value: (str) new text to show
:param background_color: (str) color of background
:param text_color: (str) color of the text
:param font: Union[str, tuple] specifies the font family, size, etc
:param visible: (bool) set visibility state of the element
"""
if value is not None:
@ -4625,39 +4711,38 @@ class Window:
disable_minimize=False, right_click_menu=None, transparent_color=None, debugger_enabled=True):
"""
:param title:
:param layout:
:param default_element_size: (Default value = DEFAULT_ELEMENT_SIZE)
:param default_button_element_size:
:param auto_size_text: True if size should fit the text length
:param auto_size_buttons:
:param location: (Default = (None))
:param size: (w,h) w=characters-wide, h=rows-high (Default = (None))
:param element_padding:
:param margins: (Default = (None))
:param button_color: button color (foreground, background)
:param font: specifies the font family, size, etc
:param progress_bar_color: (Default = (None))
:param background_color: color of background
:param border_depth:
:param auto_close: (Default = False)
:param auto_close_duration: (Default value = DEFAULT_AUTOCLOSE_TIME)
:param icon: Icon to display. Filled in with default icon in init (Default value = None)
:param force_toplevel: (Default = False)
:param alpha_channel: (Default value = 1)
:param return_keyboard_events: (Default = False)
:param use_default_focus: (Default = True)
:param text_justification:
:param no_titlebar: (Default = False)
:param grab_anywhere: If True can grab anywhere to move the window (Default = False)
:param location: Location on screen to display
:param resizable: (Default = False)
:param disable_close: (Default = False)
:param disable_minimize: (Default = False)
:param title: (str) The title that will be displayed in the Titlebar and on the Taskbar
:param layout: List[List[Elements]] The layout for the window. Can also be specified in the Layout method
:param default_element_size: Tuple[int, int] (width, height) size in characters (wide) and rows (high) for all elements in this window
:param default_button_element_size: Tuple[int, int] (width, height) size in characters (wide) and rows (high) for all Button elements in this window
:param auto_size_text: (bool) True if Elements in Window should be sized to exactly fir the length of text
:param auto_size_buttons: (bool) True if Buttons in this Window should be sized to exactly fit the text on this.
:param location: Tuple[int, int] (x,y) location, in pixels, to locate the upper left corner of the window on the screen. Default is to center on screen.
:param size: Tuple[int, int] (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
:param element_padding: Tuple[int, int] or ((int, int),(int,int)) Default amount of padding to put around elements in window (left/right, top/bottom) or ((left, right), (top, bottom))
:param margins: Tuple[int, int] (left/right, top/bottom) Amount of pixels to leave inside the window's frame around the edges before your elements are shown.
:param button_color: Tuple[str, str] (text color, button color) Default button colors for all buttons in the window
:param font: Union[str, tuple] specifies the font family, size. Uses one of two font specifications formats
:param progress_bar_color: Tuple[str, str] (bar color, background color) Sets the default colors for all progress bars in the window
:param background_color: (str) color of background
:param border_depth: (int) Default border depth (width) for all elements in the window
:param auto_close: (bool) If True, the window will automatically close itself
:param auto_close_duration: (int) Number of seconds to wait before closing the window
:param icon: Union[str, str] Can be either a filename or Base64 value.
:param force_toplevel: (bool) If True will cause this window to skip the normal use of a hidden master window
:param alpha_channel: (float) 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.
:param return_keyboard_events: (bool) if True key presses on the keyboard will be returned as Events from Read calls
:param use_default_focus: (bool) If True will use the default focus algorithm to set the focus to the "Correct" element
:param text_justification: (str) Union ['left', 'right', 'center'] Default text justification for all Text Elements in window
:param no_titlebar: (bool) 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
:param grab_anywhere: (bool) 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
:param keep_on_top: (bool) 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
:param resizable: (bool) If True, allows the user to resize the window. Note the not all Elements will change size or location when resizing.
:param disable_close: (bool) 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
:param disable_minimize: (bool) if True the user won't be able to minimize window. Good for taking over entire screen and staying that way.
:param right_click_menu: List[List[str]] see "Right Click Menus" for format
:param transparent_color:
:param debugger_enabled: (Default = True)
:param transparent_color: (str) 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.
:param debugger_enabled: (bool) If True then the internal debugger will be enabled
"""
self.AutoSizeText = auto_size_text if auto_size_text is not None else DEFAULT_AUTOSIZE_TEXT
@ -4742,10 +4827,12 @@ class Window:
# ------------------------- Add ONE Row to Form ------------------------- #
def AddRow(self, *args):
"""Parms are a variable number of Elements
:param *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]
"""
NumRows = len(self.Rows) # number of existing rows is our row number
CurrentRowNumber = NumRows # this row's number
@ -4780,8 +4867,9 @@ class Window:
# ------------------------- Add Multiple Rows to Form ------------------------- #
def AddRows(self, rows):
"""
Loops through a list of lists of elements and adds each row, list, to the layout
:param rows:
:param rows: List[List[Elements]] A list of a list of elements
"""
for row in rows:
@ -4789,9 +4877,12 @@ class Window:
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:
:param rows: List[List[Elements]] Your entire layout
"""
self.AddRows(rows)
self.BuildKeyDict()
@ -4799,31 +4890,32 @@ class Window:
def LayoutAndRead(self, rows, non_blocking=False):
"""
Deprecated. Now you layout your window's rows (layout) and then separately call Read.
:param rows:
:param non_blocking: (Default = False)
"""
raise DeprecationWarning(
'LayoutAndRead is no longer supported... change your call window.Layout(layout).Read()')
'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)
# 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
:param rows:
"""
raise DeprecationWarning('LayoutAndShow is no longer supported... ')
# ------------------------- ShowForm THIS IS IT! ------------------------- #
def Show(self, non_blocking=False):
def _Show(self, non_blocking=False):
"""
NOT TO BE CALLED BY USERS. INTERNAL ONLY!
It's this
:param non_blocking: (Default = False)
:param non_blocking: (bool) if True, this is a non-blocking call
"""
self.Shown = True
# Compute num rows & num cols (it'll come in handy debugging)
@ -4861,9 +4953,13 @@ class Window:
# ------------------------- SetIcon - set the window's fav icon ------------------------- #
def SetIcon(self, icon=None, pngbase64=None):
"""
Sets the icon that is shown on the title bar and on the task bar. Can pass in:
* a filename which must be a .ICO icon file.
* a bytes object
* a BASE64 encoded file held in a variable
:param icon:
:param pngbase64:
:param icon: (str) Filename or bytes object
:param pngbase64: (str) Base64 encoded GIF or PNG file
"""
if type(icon) is bytes:
@ -4890,21 +4986,32 @@ class Window:
def _GetElementAtLocation(self, location):
"""
Given a (row, col) location in a layout, return the element located at that position
:param location:
:param location: Tuple[int, int] Return the element located at (row, col) in layout
:return: (Element) The Element located at that position in this window
"""
(row_num, col_num) = location
row = self.Rows[row_num]
element = row[col_num]
return element
def _GetDefaultElementSize(self):
""" """
"""
Returns the default elementSize
:return: Tuple[int, int] (width, height) of the default element size
"""
return self.DefaultElementSize
def _AutoCloseAlarmCallback(self):
""" """
"""
Function that's called by tkinter when autoclode timer expires. Closes the window
:return: None
"""
try:
window = self
if window:
@ -4918,7 +5025,11 @@ class Window:
pass
def _TimeoutAlarmCallback(self):
""" """
"""
Read Timeout Alarm callback. Will kick a mainloop call out of the tkinter event loop and cause it to return
:return: None
"""
# first, get the results table built
# modify the Results table in the parent FlexForm object
# print('TIMEOUT CALLBACK')
@ -4931,17 +5042,21 @@ class Window:
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: timeout to wait, till Read will execute itself
:param timeout_key: (Default value = TIMEOUT_KEY)
:param timeout: (int) Milliseconds to wait until the Read will return IF no other GUI events happen first
:param timeout_key: (Any) The value that will be returned from the call if the timer expired
:return: Tuple[(Any), Union[Dict[Any:Any], List[Any], None] (event, values)
(event or timeout_key or None, Dictionary of values or List of values from all elements in the Window
"""
# ensure called only 1 time through a single read cycle
if not Window.read_call_from_debugger:
_refresh_debugger()
timeout = int(timeout) if timeout is not None else None
if timeout == 0: # timeout of zero runs the old readnonblocking
event, values = self.ReadNonBlocking()
event, values = self._ReadNonBlocking()
if event is None:
event = timeout_key
if values is None:
@ -4954,7 +5069,7 @@ class Window:
if self.TKrootDestroyed:
return None, None
if not self.Shown:
self.Show()
self._Show()
else:
# if already have a button waiting, the return previously built results
if self.LastButtonClicked is not None and not self.LastButtonClickedWasRealtime:
@ -5044,8 +5159,13 @@ class Window:
self.ReturnValues = self.TimeoutKey, self.ReturnValues[1] # fake a timeout
return self.ReturnValues
def ReadNonBlocking(self):
""" """
def _ReadNonBlocking(self):
"""
Should be never called directly by the user. The user can call Window.Read(timeout=0) to get same effect
:return: Tuple[(Any), Union[Dict[Any:Any], List[Any], None] (event, values)
(event or timeout_key or None, Dictionary of values or List of values from all elements in the Window
"""
if self.TKrootDestroyed:
try:
self.TKroot.quit()
@ -5055,7 +5175,7 @@ class Window:
# print('DESTROY FAILED')
return None, None
if not self.Shown:
self.Show(non_blocking=True)
self._Show(non_blocking=True)
try:
rc = self.TKroot.update()
except:
@ -5075,11 +5195,18 @@ class Window:
return BuildResults(self, False, self)
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: (Window) Returns 'self' so that method "Chaining" can happen (read up about it as it's very cool!)
"""
if self.TKrootDestroyed:
return self
if not self.Shown:
self.Show(non_blocking=True)
self._Show(non_blocking=True)
try:
rc = self.TKroot.update()
except:
@ -5089,8 +5216,17 @@ class Window:
# 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).
Without this call your changes to a Window will not be visible to the user until the next Read call
:return: Tuple[(Any), Union[Dict[Any:Any], List[Any], None] (event, values)
(event or timeout_key or None, Dictionary of values or List of values from all elements in the Window
"""
if self.TKrootDestroyed:
return self
try:
@ -5350,6 +5486,7 @@ class Window:
pass
# if down to 1 window, try and destroy the hidden window, if there is one
if Window.NumOpenWindows == 1:
# print('Trying to destroy hidden')
try:
Window.hidden_master_root.destroy()
Window.NumOpenWindows = 0 # if no hidden window, then this won't execute
@ -8122,7 +8259,6 @@ def StartupTK(my_flex_form: Window):
# print('Starting TK open Windows = {}'.format(ow))
if ENABLE_TK_WINDOWS:
root = tk.Tk()
Window.IncrementOpenCount()
elif not ow and not my_flex_form.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
@ -10127,10 +10263,13 @@ class _Debugger():
try:
result = eval('{}'.format(cmd), myglobals, mylocals)
except Exception as e:
try:
result = exec('{}'.format(cmd), myglobals, mylocals)
except Exception as e:
result = 'Exception {}\n'.format(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