From beda40595324a4d3976fe13be8766f4721c146c7 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 19 Feb 2026 08:49:56 +0000 Subject: [PATCH] first commit --- Dockerfile | 11 + Dockerfile.save | 12 + PySimpleGUIWeb.py | 8199 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 18 + Satellite.png | Bin 0 -> 26620 bytes Shannon.png | Bin 0 -> 125262 bytes Shannon.py | 1028 ++++++ Shannon.py.save | 1024 ++++++ Shannon_Dict.py | 330 ++ Shannon_Real.db | Bin 0 -> 8192 bytes Shannon_Theory.db | Bin 0 -> 8192 bytes command.txt | 2 + docker-compose.yml | 18 + requirements.txt | 25 + slim.report.json | 505 +++ 15 files changed, 11172 insertions(+) create mode 100644 Dockerfile create mode 100644 Dockerfile.save create mode 100644 PySimpleGUIWeb.py create mode 100644 README.md create mode 100644 Satellite.png create mode 100644 Shannon.png create mode 100644 Shannon.py create mode 100644 Shannon.py.save create mode 100644 Shannon_Dict.py create mode 100644 Shannon_Real.db create mode 100644 Shannon_Theory.db create mode 100644 command.txt create mode 100644 docker-compose.yml create mode 100644 requirements.txt create mode 100644 slim.report.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6a745fd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.11-slim + +WORKDIR /usr/src/app + +COPY requirements.txt ./ +RUN apt-get update -y && apt-get install -y tk tcl +RUN pip install --force-reinstall -r requirements.txt + +COPY . . + +CMD [ "python", "./Shannon.py" ] diff --git a/Dockerfile.save b/Dockerfile.save new file mode 100644 index 0000000..04353fc --- /dev/null +++ b/Dockerfile.save @@ -0,0 +1,12 @@ +FROM python:3.11-slim + +WORKDIR /usr/src/app + +COPY requirements.txt ./ +echo "http://nova.clouds.archive.ubuntu.com/ubuntu jammy/main amd64 Packages" > +RUN apt install tk/jammy +RUN pip install --force-reinstall -r requirements.txt + +COPY . . + +CMD [ "python", "./Shannon.py" ] diff --git a/PySimpleGUIWeb.py b/PySimpleGUIWeb.py new file mode 100644 index 0000000..e89619b --- /dev/null +++ b/PySimpleGUIWeb.py @@ -0,0 +1,8199 @@ +#usr/bin/python3 + +version = __version__ = "0.39.0.6 Unreleased\n , VSeparator added (spelling error), added default key for one_line_progress_meter, auto-add keys to tables & trees, Graph.draw_image now uses image_data property instead of calling set_image, added theme_add_new, changed Remi call menu_item.set_on_click_listener to menu_item.onclick.connect so it can run with latest Remi" + +port = 'PySimpleGUIWeb' + +import sys +import datetime +import textwrap +import pickle +import threading +from queue import Queue +import remi +import logging +import traceback +import os +import base64, binascii +import mimetypes +from random import randint +import time +import pkg_resources + + +# from typing import List, Any, Union, Tuple, Dict # For doing types in comments. perhaps not required + + +try: + from io import StringIO +except: + from cStringIO import StringIO + +###### ##### ##### # # ### # # +# # # # # # # # # ##### # ###### # # # # # # # # ###### ##### +# # # # # # ## ## # # # # # # # # # # # # # # +###### # ##### # # ## # # # # ##### # #### # # # # # # ##### ##### +# # # # # # ##### # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # +# # ##### # # # # ###### ###### ##### ##### ### ## ## ###### ##### + +""" + Welcome to the "core" PySimpleGUIWeb code.... + + This special port of the PySimpleGUI SDK to the browser is made possible by the magic of Remi + + https://github.com/dddomodossola/remi + + To be clear, PySimpleGUI would not be able to run in a web browser without this important GUI Framework + It may not be as widely known at tkinter or Qt, but it should be. Just as those are the best of the desktop + GUI frameworks, Remi is THE framework for doing Web Page GUIs in Python. Nothing else like it exists. + + ::::::::: :::::::::: ::: ::: ::::::::::: + :+: :+: :+: :+:+: :+:+: :+: + +:+ +:+ +:+ +:+ +:+:+ +:+ +:+ + +#++:++#: +#++:++# +#+ +:+ +#+ +#+ + +#+ +#+ +#+ +#+ +#+ +#+ + #+# #+# #+# #+# #+# #+# + ### ### ########## ### ### ########### + +""" + +g_time_start = 0 +g_time_end = 0 +g_time_delta = 0 + + + +def TimerStart(): + global g_time_start + + g_time_start = time.time() + + +def TimerStop(): + global g_time_delta, g_time_end + + g_time_end = time.time() + g_time_delta = g_time_end - g_time_start + print(g_time_delta*1000) + +# Because looks matter... +DEFAULT_BASE64_ICON = b'iVBORw0KGgoAAAANSUhEUgAAACEAAAAgCAMAAACrZuH4AAAABGdBTUEAALGPC/xhBQAAAwBQTFRFAAAAMGmYMGqZMWqaMmubMmycM22dNGuZNm2bNm6bNG2dN26cNG6dNG6eNW+fN3CfOHCeOXGfNXCgNnGhN3KiOHOjOXSjOHSkOnWmOnamOnanPHSiPXakPnalO3eoPnimO3ioPHioPHmpPHmqPXqqPnurPnusPnytP3yuQHimQnurQn2sQH2uQX6uQH6vR32qRn+sSXujSHynTH2mTn+nSX6pQH6wTIGsTYKuTYSvQoCxQoCyRIK0R4S1RYS2Roa4SIe4SIe6SIi7Soq7SYm8SYq8Sou+TY2/UYStUYWvVIWtUYeyVoewUIi0VIizUI6+Vo+8WImxXJG5YI2xZI+xZ5CzZJC0ZpG1b5a3apW4aZm/cZi4dJ2/eJ69fJ+9XZfEZZnCZJzHaZ/Jdp/AeKTI/tM8/9Q7/9Q8/9Q9/9Q+/tQ//9VA/9ZA/9ZB/9ZC/9dD/9ZE/tdJ/9dK/9hF/9hG/9hH/9hI/9hJ/9hK/9lL/9pK/9pL/thO/9pM/9pN/9tO/9tP/9xP/tpR/9xQ/9xR/9xS/9xT/91U/91V/t1W/95W/95X/95Y/95Z/99a/99b/txf/txh/txk/t5l/t1q/t5v/+Bb/+Bc/+Bd/+Be/+Bf/+Bg/+Fh/+Fi/+Jh/+Ji/uJk/uJl/+Jm/+Rm/uJo/+Ro/+Rr/+Zr/+Vs/+Vu/+Zs/+Zu/uF0/uVw/+dw/+dz/+d2/uB5/uB6/uJ9/uR7/uR+/uV//+hx/+hy/+h0/+h2/+l4/+l7/+h8gKXDg6vLgazOhKzMiqrEj6/KhK/Qka/Hk7HJlLHJlLPMmLTLmbbOkLXSmLvXn77XoLrPpr/Tn8DaocLdpcHYrcjdssfZus/g/uOC/uOH/uaB/uWE/uaF/uWK/+qA/uqH/uqI/uuN/uyM/ueS/ueW/ueY/umQ/uqQ/uuS/uuW/uyU/uyX/uqa/uue/uye/uyf/u6f/uyq/u+r/u+t/vCm/vCp/vCu/vCy/vC2/vK2/vO8/vO/wtTjwtXlzdrl/vTA/vPQAAAAiNpY5gAAAQB0Uk5T////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AFP3ByUAAAAJcEhZcwAAFw8AABcPASe7rwsAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjEuMWMqnEsAAAKUSURBVDhPhdB3WE1xHMdxt5JV0dANoUiyd8kqkey996xclUuTlEKidO3qVnTbhIyMW/bee5NskjJLmR/f3++cK/94vP76Ps/n/Zx7z6mE/6koJowcK154vvHOL/GsKCZXkUgkWlf4vWGWq5tsDz+JWIzSokAiqXGe7nWu3HxhEYof7fhOqp1GtptQuMruVhQdxZ05U5G47tYUHbQ4oah6Fg9Z4ubm7i57JhQjdHS0RSzUPoG17u6zZTKZh8c8XlytqW9YWUOH1LqFOZ6enl5ec+XybFb0rweM1tPTM6yuq6vLs0lYJJfLvb19fHwDWGF0jh5lYNAe4/QFemOwxtfXz8/fPyBgwVMqzAcCF4ybAZ2MRCexJGBhYGBQUHDw4u1UHDG1G2ZqB/Q1MTHmzAE+kpCwL1RghlTaBt/6SaXS2kx9YH1IaOjSZST8vfA9JtoDnSngGgL7wkg4WVkofA9mcF1Sx8zMzBK4v3wFiYiMVLxlEy9u21syFhYNmgN7IyJXEYViNZvEYoCVVWOmUVvgQVSUQqGIjolRFvOAFd8HWVs34VoA+6OjY2JjY5Vxm4BC1UuhGG5jY9OUaQXci1MqlfHx8YmqjyhOViW9ZsUN29akJRmPFwkJCZsTSXIpilJffXiTzorLXYgtcxRJKpUqKTklJQ0oSt9FP/EonxVdNY4jla1kK4q2ZB6mIr+AipvduzFUzMSOtLT09IyMzMxtJKug/F0u/6dTexAWDcXXLGEjapKjfsILOLKEuYiSnTQeYCt3UHhbwEHjGMrETfBJU5zq5dSTcXC8hLJccSWP2cgLXHPu7cQNAcpyxF1dyjehAKb0cSYUAOXCUw6V8OFPgevTXFymC+fPPLU677Nw/1X8A/AbfAKGulaqFlIAAAAASUVORK5CYII=' + + + +# ----====----====----==== Constants the user CAN safely change ====----====----====----# +DEFAULT_WINDOW_ICON = 'default_icon.ico' +DEFAULT_ELEMENT_SIZE = (250, 26) # In pixels +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", 15) +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_OUTPUT_ELEMENT_SIZE = (40, 10) +DEFAULT_WINDOW_LOCATION = (None, None) +MAX_SCROLLED_TEXT_BOX_HEIGHT = 50 +DEFAULT_TOOLTIP_TIME = 400 + +DEFAULT_PIXELS_TO_CHARS_SCALING = (10,26) # 1 character represents x by y pixels +DEFAULT_PIXEL_TO_CHARS_CUTOFF = 20 # number of chars that triggers using pixels instead of chars + +#################### 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' # Colors should never be this long + +DEFAULT_BUTTON_COLOR = ('white', BLUES[0]) # Foreground, Background (None, None) == System Default +OFFICIAL_PYSIMPLEGUI_BUTTON_COLOR = ('white', BLUES[0]) # Colors should never be this long + +CURRENT_LOOK_AND_FEEL = 'DarkBlue3' + + +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' + +DEFAULT_PROGRESS_BAR_COLOR = (GREENS[0], '#D0D0D0') # a nice green progress bar +DEFAULT_PROGRESS_BAR_COLOR_OFFICIAL = (GREENS[0], '#D0D0D0') # a nice green progress bar +DEFAULT_PROGRESS_BAR_SIZE = (25, 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' +DEFAULT_METER_ORIENTATION = 'horizontal' +DEFAULT_SLIDER_ORIENTATION = 'vertical' +DEFAULT_SLIDER_BORDER_WIDTH = 1 +DEFAULT_SLIDER_RELIEF = 00000 +DEFAULT_FRAME_RELIEF = 00000 + + +DEFAULT_LISTBOX_SELECT_MODE = 'extended' +SELECT_MODE_MULTIPLE = 'multiple' +LISTBOX_SELECT_MODE_MULTIPLE = 'multiple' +SELECT_MODE_BROWSE = 'browse' +LISTBOX_SELECT_MODE_BROWSE = 'browse' +SELECT_MODE_EXTENDED = 'extended' +LISTBOX_SELECT_MODE_EXTENDED = 'extended' +SELECT_MODE_SINGLE = 'single' +LISTBOX_SELECT_MODE_SINGLE = 'single' +SELECT_MODE_CONTIGUOUS = 'contiguous' +LISTBOX_SELECT_MODE_CONTIGUOUS = 'contiguous' + +TABLE_SELECT_MODE_NONE = 00000 +TABLE_SELECT_MODE_BROWSE = 00000 +TABLE_SELECT_MODE_EXTENDED = 00000 +DEFAULT_TABLE_SECECT_MODE = TABLE_SELECT_MODE_EXTENDED + +TITLE_LOCATION_TOP = 00000 +TITLE_LOCATION_BOTTOM = 00000 +TITLE_LOCATION_LEFT = 00000 +TITLE_LOCATION_RIGHT = 00000 +TITLE_LOCATION_TOP_LEFT = 00000 +TITLE_LOCATION_TOP_RIGHT = 00000 +TITLE_LOCATION_BOTTOM_LEFT = 00000 +TITLE_LOCATION_BOTTOM_RIGHT = 00000 + +THEME_DEFAULT = 'default' +THEME_WINNATIVE = 'winnative' +THEME_CLAM = 'clam' +THEME_ALT = 'alt' +THEME_CLASSIC = 'classic' +THEME_VISTA = 'vista' +THEME_XPNATIVE = 'xpnative' + +# DEFAULT_METER_ORIENTATION = 'Vertical' +# ----====----====----==== 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__' +# Window closed event (user closed with X or destroyed using OS) +WIN_CLOSED = WINDOW_CLOSED = None + +# Key indicating should not create any return values for element +WRITE_ONLY_KEY = '__WRITE ONLY__' + +# MENU Constants, can be changed by user if desired +MENU_DISABLED_CHARACTER = '!' +MENU_KEY_SEPARATOR = '::' + + + + +# a shameful global variable. This represents the top-level window information. Needed because opening a second window is different than opening the first. +class MyWindows(): + def __init__(self): + self._NumOpenWindows = 0 + self.user_defined_icon = None + self.hidden_master_root = None + + def Decrement(self): + self._NumOpenWindows -= 1 * (self._NumOpenWindows != 0) # decrement if not 0 + # print('---- DECREMENTING Num Open Windows = {} ---'.format(self._NumOpenWindows)) + + def Increment(self): + self._NumOpenWindows += 1 + # print('++++ INCREMENTING Num Open Windows = {} ++++'.format(self._NumOpenWindows)) + + +_my_windows = MyWindows() # terrible hack using globals... means need a class for collecing windows + + +# ====================================================================== # +# One-liner functions that are handy as f_ck # +# ====================================================================== # +def RGB(red, green, blue): return '#%02x%02x%02x' % (red, green, blue) + + +# ====================================================================== # +# Enums for types # +# ====================================================================== # +# ------------------------- Button types ------------------------- # +# todo Consider removing the Submit, Cancel types... they are just 'RETURN' type in reality +# uncomment this line and indent to go back to using Enums +# Was enum previously ButtonType(Enum): +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 + +BROWSE_FILES_DELIMITER = ';' # the delimeter to be used between each file in the returned string + +# ------------------------- Element types ------------------------- # +# These used to be enums ElementType(Enum): +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_MULTILINE_OUTPUT = 'multioutput' +ELEM_TYPE_INPUT_CHECKBOX = 'checkbox' +ELEM_TYPE_INPUT_SPIN = 'spin' +ELEM_TYPE_BUTTON = 'button' +ELEM_TYPE_BUTTONMENU = 'buttonmenu' +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' + +# ------------------------- 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 + + +# ---------------------------------------------------------------------- # +# Cascading structure.... Objects get larger # +# Button # +# Element # +# Row # +# Form # +# ---------------------------------------------------------------------- # +# ------------------------------------------------------------------------- # +# Element CLASS # +# ------------------------------------------------------------------------- # +class Element(): + def __init__(self, elem_type, size=(None, None), auto_size_text=None, font=None, background_color=None, text_color=None, + key=None, pad=None, tooltip=None, visible=True, size_px=(None, None), metadata=None): + + if elem_type != ELEM_TYPE_GRAPH: + self.Size = convert_tkinter_size_to_Wx(size) + else: + self.Size = size + if size_px != (None, None): + self.Size = size_px + self.Type = elem_type + self.AutoSizeText = auto_size_text + # self.Pad = DEFAULT_ELEMENT_PADDING if pad is None else pad + self.Pad = pad + if font is not None and type(font) is not str: + self.Font = font + elif font is not None: + self.Font = font.split(' ') + else: + self.Font = font + + self.TKStringVar = None + self.TKIntVar = None + self.TKText = None + self.TKEntry = None + self.TKImage = None + + self.ParentForm = None # type: Window + self.ParentContainer = None # will be a Form, Column, or Frame element + 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.metadata = metadata # type: Any + + + # ------------------------- REMI CHANGED CALLBACK ----------------------- + # called when a widget has changed and the element has events enabled + def _ChangedCallback(self, widget, *args): + # type: (Element, remi.Widget, Any) -> None + # print(f'Callback {args}') + self.ParentForm.LastButtonClicked = self.Key if self.Key is not None else '' + self.ParentForm.MessageQueue.put(self.ParentForm.LastButtonClicked) + + def Update(self, widget, background_color=None, text_color=None, font=None, visible=None, disabled=None, tooltip=None): + if font is not None: + font_info = font_parse_string(font) # family, point size, other + widget.style['font-family'] = font_info[0] + widget.style['font-size'] = '{}px'.format(font_info[1]) + + if background_color not in (None, COLOR_SYSTEM_DEFAULT): + widget.style['background-color'] = background_color + if text_color not in (None, COLOR_SYSTEM_DEFAULT): + widget.style['color'] = text_color + + if disabled: + widget.set_enabled(False) + elif disabled is False: + widget.set_enabled(True) + if visible is False: + widget.attributes['hidden'] = 'true' + elif visible is True: + del(widget.attributes['hidden']) + if tooltip is not None: + widget.attributes['title'] = tooltip + + + # if font: + # widget.SetFont(font_to_wx_font(font)) + # if text_color not in (None, COLOR_SYSTEM_DEFAULT): + # widget.SetForegroundColour(text_color) + # if background_color not in (None, COLOR_SYSTEM_DEFAULT): + # widget.SetBackgroundColour(background_color) + # if visible is True: + # widget.Show() + # self.ParentForm.VisibilityChanged() + # elif visible is False: + # widget.Hide() + # self.ParentForm.VisibilityChanged() + # if disabled: + # widget.Enable(False) + # elif disabled is False: + # widget.Enable(True) + # if tooltip is not None: + # widget.SetToolTip(tooltip) + if visible is False: + widget.attributes['hidden'] = 'true' + elif visible is True: + del(widget.attributes['hidden']) + + + 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 your layout: + sg.Text('foo', key='T') + Then you can call the Update method for that element by writing: + window.FindElement('T')('new text value') + + :param args: + :param kwargs: + :return: + """ + return self.Update(*args, **kwargs) + + +# ---------------------------------------------------------------------- # +# Input Class # +# ---------------------------------------------------------------------- # +class InputText(Element): + def __init__(self, default_text='', size=(None, None), disabled=False, password_char='', + justification=None, background_color=None, text_color=None, font=None, tooltip=None, + change_submits=False, enable_events=False, + do_not_clear=True, key=None, focus=False, pad=None, visible=True, size_px=(None, None)): + ''' + Input a line of text Element + :param default_text: Default value to display + :param size: Size of field in characters + :param password_char: If non-blank, will display this character for every character typed + :param background_color: Color for Element. Text or RGB Hex + ''' + self.DefaultText = default_text + 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 or 'left' + self.Disabled = disabled + self.ChangeSubmits = change_submits or enable_events + self.QT_QLineEdit = None + self.ValueWasChanged = False + self.Widget = None # type: remi.gui.TextInput + super().__init__(ELEM_TYPE_INPUT_TEXT, size=size, background_color=bg, text_color=fg, key=key, pad=pad, + font=font, tooltip=tooltip, visible=visible, size_px=size_px) + + def _InputTextCallback(self,widget, key, keycode, ctrl, shift, alt): + # print(f'text widget value = {widget.get_value()}') + # widget.set_value('') + # widget.set_value(value) + self.ParentForm.LastButtonClicked = key + self.ParentForm.MessageQueue.put(self.ParentForm.LastButtonClicked) + widget.set_value(widget.get_value()+key) + return (key, keycode, ctrl, shift, alt) + + def Update(self, value=None, disabled=None, select=None, background_color=None, text_color=None, font=None, visible=None): + if value is not None: + self.Widget.set_value(str(value)) + if disabled is True: + self.Widget.set_enabled(False) + elif disabled is False: + self.Widget.set_enabled(True) + + def Get(self): + return self.Widget.get_value() + + + get = Get + update = Update + + class TextInput_raw_onkeyup(remi.gui.TextInput): + @remi.gui.decorate_set_on_listener("(self, emitter, key, keycode, ctrl, shift, alt)") + @remi.gui.decorate_event_js("""var params={};params['key']=event.key; + params['keycode']=(event.which||event.keyCode); + params['ctrl']=event.ctrlKey; + params['shift']=event.shiftKey; + params['alt']=event.altKey; + sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params); + event.stopPropagation();event.preventDefault();return false;""") + def onkeyup(self, key, keycode, ctrl, shift, alt): + return (key, keycode, ctrl, shift, alt) + + @remi.gui.decorate_set_on_listener("(self, emitter, key, keycode, ctrl, shift, alt)") + @remi.gui.decorate_event_js("""var params={};params['key']=event.key; + params['keycode']=(event.which||event.keyCode); + params['ctrl']=event.ctrlKey; + params['shift']=event.shiftKey; + params['alt']=event.altKey; + sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params); + event.stopPropagation();event.preventDefault();return false;""") + def onkeydown(self, key, keycode, ctrl, shift, alt): + return (key, keycode, ctrl, shift, alt) + + +# ------------------------- INPUT TEXT Element lazy functions ------------------------- # +In = InputText +Input = InputText +I = InputText + +# ---------------------------------------------------------------------- # +# Combo # +# ---------------------------------------------------------------------- # +class Combo(Element): + def __init__(self, values, default_value=None, size=(None, None), auto_size_text=None, background_color=None, + text_color=None, change_submits=False, enable_events=False, disabled=False, key=None, pad=None, tooltip=None, + readonly=False, visible_items=10, font=None, auto_complete=True, visible=True, size_px=(None,None)): + ''' + Input Combo Box Element (also called Dropdown box) + :param values: + :param size: Size of field in characters + :param auto_size_text: True if should shrink field to fit the default text + :param background_color: Color for Element. Text or RGB Hex + ''' + self.Values = [str(v) for v in values] + self.DefaultValue = default_value + self.ChangeSubmits = change_submits or enable_events + # self.InitializeAsDisabled = disabled + self.Disabled = disabled + self.Readonly = readonly + 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.VisibleItems = visible_items + self.AutoComplete = auto_complete + self.Widget = None # type: remi.gui.DropDown + super().__init__(ELEM_TYPE_INPUT_COMBO, 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, size_px=size_px) + + + + def Update(self, value=None, values=None, set_to_index=None, disabled=None, readonly=None, background_color=None, text_color=None, font=None, visible=None): + if values is not None: + self.Widget.empty() + for i, item in enumerate(values): + self.Widget.append(value=item, key=str(i)) + if value: + self.Widget.select_by_value(value) + if set_to_index is not None: + try: # just in case a bad index is passed in + self.Widget.select_by_key(str(set_to_index)) + except: + pass + + super().Update(self.Widget, background_color=background_color, text_color=text_color, font=font, visible=visible, disabled=disabled) + + update = Update + + +# ------------------------- INPUT COMBO Element lazy functions ------------------------- # +InputCombo = Combo +DropDown = Combo +Drop = Combo + + +# ---------------------------------------------------------------------- # +# Option Menu # +# ---------------------------------------------------------------------- # +class OptionMenu(Element): + def __init__(self, values, default_value=None, size=(None, None), disabled=False, auto_size_text=None, + background_color=None, text_color=None, key=None, pad=None, tooltip=None): + ''' + InputOptionMenu + :param values: + :param default_value: + :param size: + :param disabled: + :param auto_size_text: + :param background_color: + :param text_color: + :param key: + :param pad: + :param tooltip: + ''' + self.Values = values + self.DefaultValue = default_value + self.TKOptionMenu = None + 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 + + super().__init__(ELEM_TYPE_INPUT_OPTION_MENU, size=size, auto_size_text=auto_size_text, background_color=bg, + text_color=fg, key=key, pad=pad, tooltip=tooltip) + + def Update(self, value=None, values=None, disabled=None): + if values is not None: + self.Values = values + if self.Values is not None: + for index, v in enumerate(self.Values): + if v == value: + try: + self.TKStringVar.set(value) + except: + pass + self.DefaultValue = value + break + if disabled == True: + self.TKOptionMenu['state'] = 'disabled' + elif disabled == False: + self.TKOptionMenu['state'] = 'normal' + + + +# ------------------------- OPTION MENU Element lazy functions ------------------------- # +InputOptionMenu = OptionMenu + + + +# ---------------------------------------------------------------------- # +# Listbox # +# ---------------------------------------------------------------------- # +class Listbox(Element): + def __init__(self, values, default_values=None, select_mode=None, change_submits=False, enable_events=False, bind_return_key=False, size=(None, None), disabled=False, auto_size_text=None, font=None, background_color=None, text_color=None, key=None, pad=None, tooltip=None, visible=True, size_px=(None,None)): + """ + + :param values: + :param default_values: + :param select_mode: + :param change_submits: + :param enable_events: + :param bind_return_key: + :param size: + :param disabled: + :param auto_size_text: + :param font: + :param background_color: + :param text_color: + :param key: + :param pad: + :param tooltip: + :param visible: + :param size_px: + """ + 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 + elif select_mode == LISTBOX_SELECT_MODE_CONTIGUOUS: + self.SelectMode = SELECT_MODE_CONTIGUOUS + else: + self.SelectMode = DEFAULT_LISTBOX_SELECT_MODE + 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.Widget = None # type: remi.gui.ListView + tsize = size # convert tkinter size to pixels + if size[0] is not None and size[0] < 100: + tsize = size[0]*DEFAULT_PIXELS_TO_CHARS_SCALING[0], size[1]*DEFAULT_PIXELS_TO_CHARS_SCALING[1] + + super().__init__(ELEM_TYPE_INPUT_LISTBOX, size=tsize, auto_size_text=auto_size_text, font=font, + background_color=bg, text_color=fg, key=key, pad=pad, tooltip=tooltip, visible=visible, size_px=size_px) + + def Update(self, values=None, disabled=None, set_to_index=None,background_color=None, text_color=None, font=None, visible=None): + if values is not None: + self.Values = values + self.Widget.empty() + for item in values: + self.Widget.append(remi.gui.ListItem(item)) + # if disabled == True: + # self.QT_ListWidget.setDisabled(True) + # elif disabled == False: + # self.QT_ListWidget.setDisabled(False) + # if set_to_index is not None: + # self.QT_ListWidget.setCurrentRow(set_to_index) + super().Update(self.Widget, background_color=background_color, text_color=text_color, font=font, visible=visible, disabled=disabled) + + return + + # def SetValue(self, values): + # # for index, item in enumerate(self.Values): + # for index, value in enumerate(self.Values): + # item = self.QT_ListWidget.item(index) + # if value in values: + # self.QT_ListWidget.setItemSelected(item, True) + + + def GetListValues(self): + return self.Values + + get_list_values = GetListValues + update = Update + + +# ---------------------------------------------------------------------- # +# Radio # +# ---------------------------------------------------------------------- # +class Radio(Element): + def __init__(self, text, group_id, default=False, disabled=False, size=(None, None), auto_size_text=None, + background_color=None, text_color=None, font=None, key=None, pad=None, tooltip=None, + change_submits=False): + ''' + Radio Button Element + :param text: + :param group_id: + :param default: + :param disabled: + :param size: + :param auto_size_text: + :param background_color: + :param text_color: + :param font: + :param key: + :param pad: + :param tooltip: + :param change_submits: + ''' + self.InitialState = default + self.Text = text + self.TKRadio = None + self.GroupID = group_id + self.Value = None + self.Disabled = disabled + self.TextColor = text_color or DEFAULT_TEXT_COLOR + self.ChangeSubmits = change_submits + + print('*** WARNING - Radio Buttons are not yet available on PySimpleGUIWeb ***') + + super().__init__(ELEM_TYPE_INPUT_RADIO, size=size, auto_size_text=auto_size_text, font=font, + background_color=background_color, text_color=self.TextColor, key=key, pad=pad, + tooltip=tooltip) + + def Update(self, value=None, disabled=None): + print('*** NOT IMPLEMENTED ***') + location = EncodeRadioRowCol(self.Position[0], self.Position[1]) + if value is not None: + try: + self.TKIntVar.set(location) + except: + pass + self.InitialState = value + if disabled == True: + self.TKRadio['state'] = 'disabled' + elif disabled == False: + self.TKRadio['state'] = 'normal' + + update = Update + + +# ---------------------------------------------------------------------- # +# Checkbox # +# ---------------------------------------------------------------------- # +class Checkbox(Element): + def __init__(self, text, default=False, size=(None, None), auto_size_text=None, font=None, background_color=None, + text_color=None, change_submits=False, enable_events=False, disabled=False, key=None, pad=None, tooltip=None, visible=True, size_px=(None,None)): + ''' + Checkbox Element + :param text: + :param default: + :param size: + :param auto_size_text: + :param font: + :param background_color: + :param text_color: + :param change_submits: + :param disabled: + :param key: + :param pad: + :param tooltip: + ''' + self.Text = text + self.InitialState = default + self.Disabled = disabled + self.TextColor = text_color if text_color else DEFAULT_TEXT_COLOR + self.ChangeSubmits = change_submits or enable_events + self.Widget = None # type: remi.gui.CheckBox + + super().__init__(ELEM_TYPE_INPUT_CHECKBOX, size=size, auto_size_text=auto_size_text, font=font, + background_color=background_color, text_color=self.TextColor, key=key, pad=pad, + tooltip=tooltip, visible=visible, size_px=size_px) + + def _ChangedCallback(self, widget, value): + # type: (remi.Widget, Any) -> None + # print(f'text widget value = {widget.get_value()}') + self.ParentForm.LastButtonClicked = self.Key + self.ParentForm.MessageQueue.put(self.ParentForm.LastButtonClicked) + + + def Get(self): + return self.Widget.get_value() + + def Update(self, value=None, disabled=None): + if value is not None: + self.Widget.set_value(value) + if disabled == True: + self.Widget.set_enabled(False) + elif disabled == False: + self.Widget.set_enabled(True) + + get = Get + update = Update + + +# ------------------------- CHECKBOX Element lazy functions ------------------------- # +CB = Checkbox +CBox = Checkbox +Check = Checkbox + + +# ---------------------------------------------------------------------- # +# Spin # +# ---------------------------------------------------------------------- # + +class Spin(Element): + # Values = None + # TKSpinBox = None + def __init__(self, values, initial_value=None, disabled=False, change_submits=False, enable_events=False, size=(None, None), readonly=True, auto_size_text=None, font=None, background_color=None, text_color=None, key=None, pad=None, + tooltip=None, visible=True, size_px=(None,None)): + ''' + Spinner Element + :param values: + :param initial_value: + :param disabled: + :param change_submits: + :param size: + :param auto_size_text: + :param font: + :param background_color: + :param text_color: + :param key: + :param pad: + :param tooltip: + ''' + self.Values = values + self.DefaultValue = initial_value or values[0] + self.ChangeSubmits = change_submits or enable_events + 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 + self.CurrentValue = self.DefaultValue + self.ReadOnly = readonly + self.Widget = None # type: remi.gui.SpinBox + super().__init__(ELEM_TYPE_INPUT_SPIN, size, auto_size_text, font=font, background_color=bg, text_color=fg, + key=key, pad=pad, tooltip=tooltip, visible=visible, size_px=size_px) + return + + + def Update(self, value=None, values=None, disabled=None, background_color=None, text_color=None, font=None, visible=None): + if value is not None: + self.Widget.set_value(value) + super().Update(self.Widget, background_color=background_color, text_color=text_color, font=font,visible=visible) + + def Get(self): + return self.Widget.get_value() + + get = Get + update = Update + + +# ---------------------------------------------------------------------- # +# Multiline # +# ---------------------------------------------------------------------- # +class Multiline(Element): + def __init__(self, default_text='', enter_submits=False, disabled=False, autoscroll=False, size=(None, None), + auto_size_text=None, background_color=None, text_color=None, change_submits=False, enable_events=False, do_not_clear=True, + key=None, write_only=False, focus=False, font=None, pad=None, tooltip=None, visible=True, size_px=(None,None)): + ''' + Multiline Element + :param default_text: + :param enter_submits: + :param disabled: + :param autoscroll: + :param size: + :param auto_size_text: + :param background_color: + :param text_color: + :param do_not_clear: + :param key: + :param focus: + :param pad: + :param tooltip: + :param font: + ''' + self.DefaultText = 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.WriteOnly = write_only + if size[0] is not None and size[0] < 100: + size = size[0]*DEFAULT_PIXELS_TO_CHARS_SCALING[0], size[1]*DEFAULT_PIXELS_TO_CHARS_SCALING[1] + self.Widget = None # type: remi.gui.TextInput + + 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, size_px=size_px) + return + + def _InputTextCallback(self, widget:remi.Widget, value, keycode): + # print(f'text widget value = {widget.get_value()}') + self.ParentForm.LastButtonClicked = chr(int(keycode)) + self.ParentForm.MessageQueue.put(self.ParentForm.LastButtonClicked) + + def Update(self, value=None, disabled=None, append=False, background_color=None, text_color=None, font=None, visible=None, autoscroll=None): + if value is not None and not append: + self.Widget.set_value(value) + elif value is not None and append: + text = self.Widget.get_value() + str(value) + self.Widget.set_value(text) + # if background_color is not None: + # self.WxTextCtrl.SetBackgroundColour(background_color) + # if text_color is not None: + # self.WxTextCtrl.SetForegroundColour(text_color) + # if font is not None: + # self.WxTextCtrl.SetFont(font) + # if disabled: + # self.WxTextCtrl.Enable(True) + # elif disabled is False: + # self.WxTextCtrl.Enable(False) + super().Update(self.Widget, background_color=background_color, text_color=text_color, font=font, visible=visible) + + + def print(self, *args, end=None, sep=None, text_color=None, background_color=None): + """ + Print like Python normally prints except route the output to a multline element and also add colors if desired + + :param args: List[Any] The arguments to print + :param end: (str) The end char to use just like print uses + :param sep: (str) The separation character like print uses + :param text_color: The color of the text + :param background_color: The background color of the line + """ + _print_to_element(self, *args, end=end, sep=sep, text_color=text_color, background_color=background_color) + + + + update = Update + +ML = Multiline +MLine = Multiline + + +# ---------------------------------------------------------------------- # +# Multiline Output # +# ---------------------------------------------------------------------- # +class MultilineOutput(Element): + def __init__(self, default_text='', enter_submits=False, disabled=False, autoscroll=False, size=(None, None), auto_size_text=None, background_color=None, + text_color=None, change_submits=False, enable_events=False, do_not_clear=True, key=None, focus=False, font=None, pad=None, tooltip=None, + visible=True, size_px=(None, None)): + ''' + Multiline Element + :param default_text: + :param enter_submits: + :param disabled: + :param autoscroll: + :param size: + :param auto_size_text: + :param background_color: + :param text_color: + :param do_not_clear: + :param key: + :param focus: + :param pad: + :param tooltip: + :param font: + ''' + self.DefaultText = 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 + tsize = size # convert tkinter size to pixels + if size[0] is not None and size[0] < 100: + tsize = size[0] * DEFAULT_PIXELS_TO_CHARS_SCALING[0], size[1] * DEFAULT_PIXELS_TO_CHARS_SCALING[1] + self.Widget = None # type: remi.gui.TextInput + self.CurrentValue = '' + + super().__init__(ELEM_TYPE_MULTILINE_OUTPUT, size=tsize, 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, size_px=size_px) + return + + def Update(self, value=None, disabled=None, append=False, background_color=None, text_color=None, font=None, visible=None, autoscroll=None): + autoscroll = self.Autoscroll if autoscroll is None else autoscroll + if value is not None and not append: + self.Widget.set_value(str(value)) + self.CurrentValue = str(value) + elif value is not None and append: + self.CurrentValue = self.CurrentValue + str(value) + self.Widget.set_value(self.CurrentValue) + self.Widget._set_updated() + app = self.ParentForm.App + + if hasattr(app, "websockets"): + app.execute_javascript( + 'element=document.getElementById("%(id)s"); element.innerHTML=`%(content)s`; if(%(autoscroll)s){element.scrollTop=999999;} ' % { + "id": self.Widget.identifier, "content": self.Widget.get_value(), "autoscroll": 'true' if autoscroll else 'false'}) + + super().Update(self.Widget, background_color=background_color, text_color=text_color, font=font, visible=visible) + + def print(self, *args, end=None, sep=None, text_color=None, background_color=None): + """ + Print like Python normally prints except route the output to a multline element and also add colors if desired + + :param args: List[Any] The arguments to print + :param end: (str) The end char to use just like print uses + :param sep: (str) The separation character like print uses + :param text_color: The color of the text + :param background_color: The background color of the line + """ + _print_to_element(self, *args, end=end, sep=sep, text_color=text_color, background_color=background_color) + + + + + + update = Update + + +# ---------------------------------------------------------------------- # +# Text # +# ---------------------------------------------------------------------- # +class Text(Element): + def __init__(self, text='', size=(None, None), auto_size_text=None, click_submits=None, enable_events=False, relief=None, border_width=None, font=None, text_color=None, background_color=None, justification=None, pad=None, margins=None, key=None, tooltip=None, visible=True, size_px=(None,None), metadata=None): + """ + Text + :param text: + :param size: + :param auto_size_text: + :param click_submits: + :param enable_events: + :param relief: + :param font: + :param text_color: + :param background_color: + :param justification: + :param pad: + :param margins: + :param key: + :param tooltip: + :param visible: + :param size_px: + """ + 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 + self.Margins = margins + self.size_px = size_px + if background_color is None: + bg = DEFAULT_TEXT_ELEMENT_BACKGROUND_COLOR + else: + bg = background_color + pixelsize = size + if size[1] is not None and size[1] < 10: + pixelsize = size[0]*10, size[1]*20 + self.BorderWidth = border_width if border_width is not None else DEFAULT_BORDER_WIDTH + self.Disabled = False + self.Widget = None #type: remi.gui.Label + + super().__init__(ELEM_TYPE_TEXT, pixelsize, auto_size_text, background_color=bg, font=font if font else DEFAULT_FONT, + text_color=self.TextColor, pad=pad, key=key, tooltip=tooltip, size_px=size_px, visible=visible, metadata=metadata) + return + + def Update(self, value=None, background_color=None, text_color=None, font=None, visible=None): + if value is not None: + self.Widget.set_text(str(value)) + super().Update(self.Widget, background_color=background_color, text_color=text_color, font=font, visible=visible) + + update = Update + +# ------------------------- Text Element lazy functions ------------------------- # +Txt = Text +T = Text + + + +# ---------------------------------------------------------------------- # +# Output # +# Routes stdout, stderr to a scrolled window # +# ---------------------------------------------------------------------- # +class Output(Element): + def __init__(self, size=(None, None), background_color=None, text_color=None, pad=None, font=None, tooltip=None, + key=None, visible=True, size_px=(None,None), disabled=False): + ''' + Output Element + :param size: + :param background_color: + :param text_color: + :param pad: + :param font: + :param tooltip: + :param 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 + fg = text_color if text_color is not None else 'black' if DEFAULT_INPUT_TEXT_COLOR == COLOR_SYSTEM_DEFAULT else DEFAULT_INPUT_TEXT_COLOR + self.Disabled = disabled + self.Widget = None # type: remi.gui.TextInput + if size_px == (None, None) and size == (None, None): + size = DEFAULT_OUTPUT_ELEMENT_SIZE + if size[0] is not None and size[0] < 100: + size = size[0]*DEFAULT_PIXELS_TO_CHARS_SCALING[0], size[1]*DEFAULT_PIXELS_TO_CHARS_SCALING[1] + super().__init__(ELEM_TYPE_OUTPUT, size=size, size_px=size_px, visible=visible, background_color=bg, text_color=fg, pad=pad, font=font, tooltip=tooltip, key=key) + + + def Update(self, value=None, disabled=None, append=False, background_color=None, text_color=None, font=None, visible=None): + if value is not None and not append: + self.Widget.set_value(str(value)) + self.CurrentValue = str(value) + elif value is not None and append: + self.CurrentValue = self.CurrentValue + '\n' + str(value) + self.Widget.set_value(self.CurrentValue) + self.Widget._set_updated() + app = self.ParentForm.App + if hasattr(app, "websockets"): + app.execute_javascript('element=document.getElementById("%(id)s"); element.innerHTML=`%(content)s`; element.scrollTop=999999; ' % { + "id":self.Widget.identifier, "content":self.Widget.get_value()}) + + super().Update(self.Widget, background_color=background_color, text_color=text_color, font=font, visible=visible) + + update = Update + + +# ---------------------------------------------------------------------- # +# Button Class # +# ---------------------------------------------------------------------- # +class Button(Element): + def __init__(self, button_text='', button_type=BUTTON_TYPE_READ_FORM, target=(None, None), tooltip=None, + file_types=(("ALL Files", "*"),), initial_folder=None, 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), auto_size_button=None, button_color=None, font=None, bind_return_key=False, + focus=False, pad=None, key=None, visible=True, size_px=(None,None)): + ''' + Button Element + :param button_text: + :param button_type: + :param target: + :param tooltip: + :param file_types: + :param initial_folder: + :param disabled: + :param image_filename: + :param image_size: + :param image_subsample: + :param border_width: + :param size: + :param auto_size_button: + :param button_color: + :param default_value: + :param font: + :param bind_return_key: + :param focus: + :param pad: + :param key: + ''' + self.AutoSizeButton = auto_size_button + self.BType = button_type + self.FileTypes = file_types + self.TKButton = None + self.Target = target + self.ButtonText = str(button_text) + self.ButtonColor = button_color if button_color else DEFAULT_BUTTON_COLOR + self.TextColor = self.ButtonColor[0] + self.BackgroundColor = self.ButtonColor[1] + 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.CalendarCloseWhenChosen = None + self.DefaultDate_M_D_Y = (None, None, None) + self.InitialFolder = initial_folder + self.Disabled = disabled + self.ChangeSubmits = change_submits or enable_events + self.QT_QPushButton = None + self.ColorChosen = None + self.Relief = None + # self.temp_size = size if size != (NONE, NONE) else + self.Widget = None # type: remi.gui.Button + super().__init__(ELEM_TYPE_BUTTON, size=size, font=font, pad=pad, key=key, tooltip=tooltip, text_color=self.TextColor, background_color=self.BackgroundColor, visible=visible, size_px=size_px) + return + + + + # ------- Button Callback ------- # + def _ButtonCallBack(self, event): + + # print('Button callback') + + # print(f'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.FindElement(target) + try: + strvar = target_element.TKStringVar + except: + pass + try: + if target_element.ChangeSubmits: + should_submit_window = True + except: + pass + filetypes = (("ALL Files", "*"),) if self.FileTypes is None else self.FileTypes + if self.BType == BUTTON_TYPE_BROWSE_FOLDER: # Browse Folder + wx_types = convert_tkinter_filetypes_to_wx(self.FileTypes) + if self.InitialFolder: + dialog = wx.DirDialog(self.ParentForm.MasterFrame, style=wx.FD_OPEN) + else: + dialog = wx.DirDialog(self.ParentForm.MasterFrame) + folder_name = '' + if dialog.ShowModal() == wx.ID_OK: + folder_name = dialog.GetPath() + if folder_name != '': + if target_element.Type == ELEM_TYPE_BUTTON: + target_element.FileOrFolderName = folder_name + else: + target_element.Update(folder_name) + elif self.BType == BUTTON_TYPE_BROWSE_FILE: # Browse File + qt_types = convert_tkinter_filetypes_to_wx(self.FileTypes) + if self.InitialFolder: + dialog = wx.FileDialog(self.ParentForm.MasterFrame,defaultDir=self.InitialFolder, wildcard=qt_types, style=wx.FD_OPEN) + else: + dialog = wx.FileDialog(self.ParentForm.MasterFrame, wildcard=qt_types, style=wx.FD_OPEN) + file_name = '' + if dialog.ShowModal() == wx.ID_OK: + file_name = dialog.GetPath() + else: + file_name = '' + if file_name != '': + if target_element.Type == ELEM_TYPE_BUTTON: + target_element.FileOrFolderName = file_name + else: + target_element.Update(file_name) + elif self.BType == BUTTON_TYPE_BROWSE_FILES: # Browse Files + qt_types = convert_tkinter_filetypes_to_wx(self.FileTypes) + if self.InitialFolder: + dialog = wx.FileDialog(self.ParentForm.MasterFrame,defaultDir=self.InitialFolder, wildcard=qt_types, style=wx.FD_MULTIPLE) + else: + dialog = wx.FileDialog(self.ParentForm.MasterFrame, wildcard=qt_types, style=wx.FD_MULTIPLE) + file_names = '' + if dialog.ShowModal() == wx.ID_OK: + file_names = dialog.GetPaths() + else: + file_names = '' + if file_names != '': + file_names = BROWSE_FILES_DELIMITER.join(file_names) + if target_element.Type == ELEM_TYPE_BUTTON: + target_element.FileOrFolderName = file_names + else: + target_element.Update(file_names) + elif self.BType == BUTTON_TYPE_SAVEAS_FILE: # Save As File + qt_types = convert_tkinter_filetypes_to_wx(self.FileTypes) + if self.InitialFolder: + dialog = wx.FileDialog(self.ParentForm.MasterFrame,defaultDir=self.InitialFolder, wildcard=qt_types, style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) + else: + dialog = wx.FileDialog(self.ParentForm.MasterFrame, wildcard=qt_types, style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) + file_name = '' + if dialog.ShowModal() == wx.ID_OK: + file_name = dialog.GetPath() + else: + file_name = '' + if file_name != '': + if target_element.Type == ELEM_TYPE_BUTTON: + target_element.FileOrFolderName = file_name + else: + target_element.Update(file_name) + elif self.BType == BUTTON_TYPE_COLOR_CHOOSER: # Color Chooser + qcolor = QColorDialog.getColor() + rgb_color = qcolor.getRgb() + color= '#' + ''.join('%02x'% i for i in rgb_color[:3]) + if self.Target == (None, None): + self.FileOrFolderName = color + else: + target_element.Update(color) + elif self.BType == BUTTON_TYPE_CLOSES_WIN: # Closes 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 + if self.ParentForm.CurrentlyRunningMainloop: + self.ParentForm.App.ExitMainLoop() + self.ParentForm.IgnoreClose = True + self.ParentForm.MasterFrame.Close() + if self.ParentForm.NonBlocking: + Window._DecrementOpenCount() + self.ParentForm._Close() + elif self.BType == BUTTON_TYPE_READ_FORM: # Read 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 + element_callback_quit_mainloop(self) + elif self.BType == BUTTON_TYPE_CLOSES_WIN_ONLY: # special kind of button that does not exit main loop + element_callback_quit_mainloop(self) + self.ParentForm._Close() + Window._DecrementOpenCount() + elif self.BType == BUTTON_TYPE_CALENDAR_CHOOSER: # this is a return type button so GET RESULTS and destroy window + should_submit_window = False + + if should_submit_window: + self.ParentForm.LastButtonClicked = target_element.Key + self.ParentForm.FormRemainedOpen = True + self.ParentForm.MessageQueue.put(self.ParentForm.LastButtonClicked) + return + + + def Update(self, text=None, button_color=(None, None), disabled=None, image_data=None, image_filename=None, font=None, visible=None, image_subsample=None, image_size=(None,None)): + if text is not None: + self.Widget.set_text(str(text)) + fg, bg = button_color + if image_data: + self.Widget.empty() + simage = SuperImage(image_data) + if image_size is not (None, None): + simage.set_size(image_size[0], image_size[1]) + self.Widget.append(simage) + if image_filename: + self.Widget.empty() + simage = SuperImage(image_filename) + if image_size is not (None, None): + simage.set_size(image_size[0], image_size[1]) + self.Widget.append(simage) + + super().Update(self.Widget, background_color=bg, text_color=fg, disabled=disabled, font=font, visible=visible) + + + def GetText(self): + return self.Widget.get_text() + + get_text = GetText + update = Update + +# ------------------------- Button lazy functions ------------------------- # +B = Button +Btn = Button +Butt = Button + + +def convert_tkinter_filetypes_to_wx(filetypes): + wx_filetypes = '' + for item in filetypes: + filetype = item[0] + ' (' + item[1] + ')|'+ item[1] + wx_filetypes += filetype + return wx_filetypes + + + + + +# ---------------------------------------------------------------------- # +# ProgreessBar # +# ---------------------------------------------------------------------- # +class ProgressBar(Element): + def __init__(self, max_value, orientation=None, size=(None, None), auto_size_text=None, bar_color=(None, None), + style=None, border_width=None, relief=None, key=None, pad=None): + ''' + ProgressBar Element + :param max_value: + :param orientation: + :param size: + :param auto_size_text: + :param bar_color: + :param style: + :param border_width: + :param relief: + :param key: + :param pad: + ''' + self.MaxValue = max_value + self.TKProgressBar = None + self.Cancelled = False + self.NotRunning = True + self.Orientation = orientation if orientation else DEFAULT_METER_ORIENTATION + self.BarColor = bar_color + self.BarStyle = style if style else DEFAULT_PROGRESS_BAR_STYLE + 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 + super().__init__(ELEM_TYPE_PROGRESS_BAR, size=size, auto_size_text=auto_size_text, key=key, pad=pad) + + # returns False if update failed + def UpdateBar(self, current_count, max=None): + print('*** NOT IMPLEMENTED ***') + return + if self.ParentForm.TKrootDestroyed: + return False + self.TKProgressBar.Update(current_count, max=max) + try: + self.ParentForm.TKroot.update() + except: + _my_windows.Decrement() + return False + return True + + update_bar = UpdateBar + +# ---------------------------------------------------------------------- # +# Image # +# ---------------------------------------------------------------------- # +class Image(Element): + def __init__(self, filename=None, data=None, background_color=None, size=(None, None), pad=None, key=None, + tooltip=None, right_click_menu=None, visible=True, enable_events=False): + ''' + Image Element + :param filename: + :param data: + :param background_color: + :param size: + :param pad: + :param key: + :param tooltip: + ''' + self.Filename = filename if filename else None # note that Remi expects a / at the front of resource files + self.Data = data + self.tktext_label = None + self.BackgroundColor = background_color + self.Disabled = False + self.EnableEvents = enable_events + sz = (0,0) if size == (None, None) else size + self.Widget = None #type: SuperImage + # if data is None and filename is None: # it is OK to have no image specified when intially creating + # print('* Warning... no image specified in Image Element! *') + super().__init__(ELEM_TYPE_IMAGE, size=sz, background_color=background_color, pad=pad, key=key, + tooltip=tooltip, visible=visible) + return + + def Update(self, filename=None, data=None, size=(None,None), visible=None): + if data is not None: + self.Widget.load(data) + # decoded = base64.b64decode(data) + # with open(r'.\decoded.out', 'wb') as f: + # f.write(decoded) + # filename = r'.\decoded.out' + if filename is not None: + self.Widget.load(filename) + # self.Widget.set_image(filename=filename) + # if size != (None, None): + # self.Widget.style['height'] = '{}px'.format(size[1]) + # self.Widget.style['width'] = '{}px'.format(size[0]) + super().Update(self.Widget, visible=visible) + + update = Update + + + +# class SuperImageOld(remi.gui.Image): +# def __init__(self, file_path_name=None, **kwargs): +# image = file_path_name +# super(SuperImage, self).__init__(image, **kwargs) +# +# self.imagedata = None +# self.mimetype = None +# self.encoding = None +# if image is None: +# return +# self.load(image) +# +# def load(self, file_path_name): +# if type(file_path_name) is bytes or len(file_path_name) > 200: +# try: +# self.imagedata = base64.b64decode(file_path_name, validate=True) +# except binascii.Error: +# self.imagedata = file_path_name +# else: +# self.mimetype, self.encoding = mimetypes.guess_type(file_path_name) +# with open(file_path_name, 'rb') as f: +# self.imagedata = f.read() +# self.refresh() +# +# def refresh(self): +# i = int(time.time() * 1e6) +# self.attributes['src'] = "/%s/get_image_data?update_index=%d" % (id(self), i) +# +# def get_image_data(self, update_index): +# headers = {'Content-type': self.mimetype if self.mimetype else 'application/octet-stream'} +# return [self.imagedata, headers] + + +class SuperImage(remi.gui.Image): + def __init__(self, file_path_name=None, **kwargs): + """ + This new app_instance variable is causing lots of problems. I do not know the value of the App + when I create this image. + :param app_instance: + :param file_path_name: + :param kwargs: + """ + # self.app_instance = app_instance + image = file_path_name + super(SuperImage, self).__init__(image, **kwargs) + + self.imagedata = None + self.mimetype = None + self.encoding = None + if not image: return + self.load(image) + + def load(self, file_path_name): + if type(file_path_name) is bytes: + try: + #here a base64 image is received + self.imagedata = base64.b64decode(file_path_name, validate=True) + self.attributes['src'] = "/%s/get_image_data?update_index=%s" % (id(self), str(time.time())) + except binascii.Error: + #here an image data is received (opencv image) + self.imagedata = file_path_name + self.refresh() + self.refresh() + else: + #here a filename is received + self.attributes['src'] = remi.gui.load_resource(file_path_name) + """print(f'***** Loading file = {file_path_name}') + self.mimetype, self.encoding = mimetypes.guess_type(file_path_name) + with open(file_path_name, 'rb') as f: + self.imagedata = f.read()""" + self.refresh() + + def refresh(self): + i = int(time.time() * 1e6) + # self.app_instance.execute_javascript(""" + if Window.App is not None: + Window.App.execute_javascript(""" + var url = '/%(id)s/get_image_data?update_index=%(frame_index)s'; + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = 'blob' + xhr.onload = function(e){ + var urlCreator = window.URL || window.webkitURL; + var imageUrl = urlCreator.createObjectURL(this.response); + document.getElementById('%(id)s').src = imageUrl; + } + xhr.send(); + """ % {'id': id(self), 'frame_index':i}) + + def get_image_data(self, update_index): + headers = {'Content-type': self.mimetype if self.mimetype else 'application/octet-stream'} + return [self.imagedata, headers] + + +# ---------------------------------------------------------------------- # +# Graph # +# ---------------------------------------------------------------------- # +class Graph(Element): + def __init__(self, canvas_size, graph_bottom_left, graph_top_right, background_color=None, pad=None, + change_submits=False, drag_submits=False, size_px=(None,None), enable_events=False, key=None, visible=True, disabled=False, tooltip=None): + ''' + Graph Element + :param canvas_size: + :param graph_bottom_left: + :param graph_top_right: + :param background_color: + :param pad: + :param key: + :param tooltip: + ''' + self.CanvasSize = canvas_size + self.BottomLeft = graph_bottom_left + self.TopRight = graph_top_right + self.ChangeSubmits = change_submits or enable_events + self.DragSubmits = drag_submits + self.ClickPosition = (None, None) + self.MouseButtonDown = False + self.Disabled = disabled + self.Widget = None # type: remi.gui.Svg + self.SvgGroup = None # type: remi.gui.SvgSubcontainer + super().__init__(ELEM_TYPE_GRAPH, size=canvas_size, size_px=size_px, visible=visible, background_color=background_color, pad=pad, tooltip=tooltip, key=key) + return + + def _convert_xy_to_canvas_xy(self, x_in, y_in): + 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 = 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): + if None in (x_in, y_in): + return None, None + x_in, y_in = int(x_in), int(y_in) + 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] + return int(new_x), int(new_y) + + def DrawLine(self, point_from, point_to, color='black', width=1): + if point_from == (None, None) or color is 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.Widget 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 + line = remi.gui.SvgLine(converted_point_from[0], converted_point_from[1], converted_point_to[0], converted_point_to[1]) + line.set_stroke(width, color) + self.SvgGroup.append([line,]) + return line + + def DrawPoint(self, point, size=2, color='black'): + if point == (None, None): + return + converted_point = self._convert_xy_to_canvas_xy(point[0], point[1]) + if self.Widget 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 + rpoint = remi.gui.SvgCircle(converted_point[0], converted_point[1], size) + rpoint.set_stroke(size, color) + rpoint.set_fill(color) + self.SvgGroup.append([rpoint,]) + return rpoint + + + def DrawCircle(self, center_location, radius, fill_color=None, line_color='black'): + if center_location == (None, None): + return + converted_point = self._convert_xy_to_canvas_xy(center_location[0], center_location[1]) + if self.Widget 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 + rpoint = remi.gui.SvgCircle(converted_point[0], converted_point[1], radius=radius) + rpoint.set_fill(fill_color) + rpoint.set_stroke(color=line_color) + self.SvgGroup.append([rpoint,]) + return rpoint + + + def DrawOval(self, top_left, bottom_right, fill_color=None, line_color=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.Widget 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 + return + + + # def DrawArc(self, top_left, bottom_right, extent, start_angle, style=None, arc_color='black'): + # 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 + # return + + def DrawRectangle(self, top_left, bottom_right, fill_color=None, line_color='black'): + 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.Widget 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 + + rpoint = remi.gui.SvgRectangle(converted_top_left[0], converted_top_left[1], abs(converted_bottom_right[0]-converted_top_left[0]), abs(converted_top_left[1] - converted_bottom_right[1])) + rpoint.set_stroke(width=1, color=line_color) + if fill_color is not None: + rpoint.set_fill(fill_color) + else: + rpoint.set_fill('transparent') + self.SvgGroup.append([rpoint,]) + return rpoint + + + + def DrawText(self, text, location, color='black', font=None, angle=0): + text = str(text) + if location == (None, None): + return + converted_point = self._convert_xy_to_canvas_xy(location[0], location[1]) + if self.Widget 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 + + rpoint = remi.gui.SvgText(converted_point[0], converted_point[1], text) + self.SvgGroup.append([rpoint,]) + # self.SvgGroup.redraw() + return rpoint + + + def DrawImage(self, data=None, image_source=None, location=(None, None), size=(100, 100)): + if location == (None, None): + return + if data is not None: + image_source = data.decode('utf-8') if type(data) is bytes else data + converted_point = self._convert_xy_to_canvas_xy(location[0], location[1]) + if self.Widget 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 + + rpoint = remi.gui.SvgImage('', converted_point[0], converted_point[0], size[0], size[1]) + + if type(image_source) is bytes or len(image_source) > 200: + # rpoint.set_image("data:image/svg;base64,%s"%image_source) + rpoint.image_data = "data:image/svg;base64,%s"%image_source + else: + mimetype, encoding = mimetypes.guess_type(image_source) + with open(image_source, 'rb') as f: + data = f.read() + b64 = base64.b64encode(data) + b64_str = b64.decode("utf-8") + image_string = "data:image/svg;base64,%s"%b64_str + # rpoint.set_image(image_string) + rpoint.image_data = image_string + self.SvgGroup.append([rpoint,]) + rpoint.redraw() + self.SvgGroup.redraw() + return rpoint + + def Erase(self): + if self.Widget 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.Widget.empty() + self.SvgGroup = remi.gui.SvgSubcontainer(0, 0, "100%", "100%") + self.Widget.append(self.SvgGroup) + + def Update(self, background_color): + if self.Widget 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 self.BackgroundColor not in (None, COLOR_SYSTEM_DEFAULT): + self.Widget.style['background-color'] = self.BackgroundColor + + def Move(self, x_direction, y_direction): + # TODO - IT's still not working yet! I'm trying!! + + # self.MoveFigure(self.SvgGroup, x_direction,y_direction) + # return + 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.Widget 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(self.SvgGroup.attributes) + cur_x = float(self.SvgGroup.attributes['x']) + cur_y = float(self.SvgGroup.attributes['y']) + self.SvgGroup.set_position(cur_x - x_direction,cur_y - y_direction) + self.SvgGroup.redraw() + + + def Relocate(self, x, y): + shift_converted = self._convert_xy_to_canvas_xy(x, y) + if self.Widget 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 + # figure.empty() + self.SvgGroup.set_position(shift_converted[0], shift_converted[1]) + self.SvgGroup.redraw() + + + def MoveFigure(self, figure, x_direction, y_direction): + figure = figure # type: remi.gui.SvgCircle + 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('*** 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 + print(figure.attributes) + try: + cur_x = float(figure.attributes['x']) + cur_y = float(figure.attributes['y']) + figure.set_position(cur_x - x_direction,cur_y - y_direction) + except: + cur_x1 = float(figure.attributes['x1']) + cur_x2 = float(figure.attributes['x2']) + cur_y1 = float(figure.attributes['y1']) + cur_y2 = float(figure.attributes['y2']) + figure.set_coords(cur_x1-x_direction, cur_y1-y_direction, cur_x2-x_direction, cur_y2-x_direction) + figure.redraw() + + def RelocateFigure(self, figure, x, y): + figure = figure #type: remi.gui.SvgCircle + 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 + # figure.empty() + figure.set_position(shift_converted[0], shift_converted[1]) + figure.redraw() + + + def DeleteFigure(self, figure): + figure = figure # type: remi.gui.SvgCircle + 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 + self.SvgGroup.remove_child(figure) + del figure + + 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: Tuple[int, int] (x,y) The bottoms left corner of your coordinate system + :param graph_top_right: Tuple[int, int] (x,y) The top right corner of your coordinate system + """ + self.BottomLeft = graph_bottom_left + self.TopRight = graph_top_right + + + def _MouseDownCallback(self, widget, x,y, *args): + # print(f'Mouse down {x,y}') + self.MouseButtonDown = True + + def _MouseUpCallback(self, widget, x,y, *args): + self.ClickPosition = self._convert_canvas_xy_to_xy(int(x), int(y)) + self.MouseButtonDown = False + if self.ChangeSubmits: + # self.ClickPosition = (None, None) + self.ParentForm.LastButtonClicked = self.Key if self.Key is not None else '' + self.ParentForm.MessageQueue.put(self.ParentForm.LastButtonClicked) + + # def ClickCallback(self, emitter, x, y): + def ClickCallback(self, widget:remi.gui.Svg, *args): + return + self.ClickPosition = (None, None) + self.ParentForm.LastButtonClicked = self.Key if self.Key is not None else '' + self.ParentForm.MessageQueue.put(self.ParentForm.LastButtonClicked) + + def _DragCallback(self, emitter, x, y): + if not self.MouseButtonDown: # only return drag events when mouse is down + return + # print(f'In Drag Callback') + self.ClickPosition = self._convert_canvas_xy_to_xy(x, y) + # print(f'Position {self.ClickPosition}') + self.ParentForm.LastButtonClicked = self.Key if self.Key is not None else '' + self.ParentForm.MessageQueue.put(self.ParentForm.LastButtonClicked) + + + + click_callback = ClickCallback + delete_figure = DeleteFigure + draw_circle = DrawCircle + draw_image = DrawImage + draw_line = DrawLine + draw_oval = DrawOval + draw_point = DrawPoint + draw_rectangle = DrawRectangle + draw_text = DrawText + erase = Erase + move = Move + move_figure = MoveFigure + relocate = Relocate + relocate_figure = RelocateFigure + update = Update + + +# ---------------------------------------------------------------------- # +# Frame # +# ---------------------------------------------------------------------- # + +# First the REMI implementation of a frame + +class CLASSframe( remi.gui.VBox ): + def __init__(self, title, *args, **kwargs): + super( CLASSframe, self ).__init__(*args, **kwargs) + self.style.update({"overflow":"visible","border-width":"1px","border-style":"solid","border-color":"#7d7d7d"}) + self.frame_label = remi.gui.Label('frame label') + self.frame_label.style.update({"position":"relative","overflow":"auto","background-color":"#ffffff","border-width":"1px","border-style":"solid","top":"-7px","width":"0px","height":"0px","left":"10px"}) + self.append(self.frame_label,'frame_label') + self.set_title(title) + + def set_title(self, title): + self.frame_label.set_text(title) + + +class Frame(Element): + def __init__(self, title, layout, title_color=None, background_color=None, title_location=None, + relief=DEFAULT_FRAME_RELIEF, element_justification='left', size=(None, None), font=None, pad=None, border_width=None, key=None, + tooltip=None): + ''' + Frame Element + :param title: + :param layout: + :param title_color: + :param background_color: + :param title_location: + :param relief: + :param size: + :param font: + :param pad: + :param border_width: + :param key: + :param tooltip: + ''' + 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.Justification = 'left' + self.ElementJustification = element_justification + self.Widget = None # type: CLASSframe + + + + self.Layout(layout) + + super().__init__(ELEM_TYPE_FRAME, background_color=background_color, text_color=title_color, size=size, + font=font, pad=pad, key=key, tooltip=tooltip) + return + + def AddRow(self, *args): + ''' Parms are a variable number of Elements ''' + 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 + 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): + for row in rows: + self.AddRow(*row) + + def _GetElementAtLocation(self, location): + (row_num, col_num) = location + row = self.Rows[row_num] + element = row[col_num] + return element + + add_row = AddRow + layout = Layout + + +# ---------------------------------------------------------------------- # +# Separator # +# Routes stdout, stderr to a scrolled window # +# ---------------------------------------------------------------------- # +class VerticalSeparator(Element): + def __init__(self, pad=None): + ''' + VerticalSeperator - A separator that spans only 1 row in a vertical fashion + :param pad: + ''' + self.Orientation = 'vertical' # for now only vertical works + + super().__init__(ELEM_TYPE_SEPARATOR, pad=pad) + + +VSeperator = VerticalSeparator +VSeparator = VerticalSeparator +VSep = VerticalSeparator + + +# ---------------------------------------------------------------------- # +# Tab # +# ---------------------------------------------------------------------- # +class Tab(Element): + def __init__(self, title, layout, title_color=None, background_color=None, font=None, pad=None, disabled=False, element_justification='left', border_width=None, key=None, tooltip=None): + ''' + Tab Element + :param title: + :param layout: + :param title_color: + :param background_color: + :param font: + :param pad: + :param disabled: + :param border_width: + :param key: + :param tooltip: + ''' + self.UseDictionary = False + self.ReturnValues = None + self.ReturnValuesList = [] + self.ReturnValuesDictionary = {} + self.DictionaryKeyCounter = 0 + self.ParentWindow = None + self.Rows = [] + self.TKFrame = None + self.Title = title + self.BorderWidth = border_width + self.Disabled = disabled + self.ParentNotebook = None + self.Justification = 'left' + self.ElementJustification = element_justification + self.TabID = None + self.BackgroundColor = background_color if background_color is not None else DEFAULT_BACKGROUND_COLOR + self.Widget = None # type: remi.gui.HBox + self._Layout(layout) + + super().__init__(ELEM_TYPE_TAB, background_color=background_color, text_color=title_color, font=font, pad=pad, + key=key, tooltip=tooltip) + return + + def _AddRow(self, *args): + ''' Parms are a variable number of Elements ''' + 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 + 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): + for row in rows: + self._AddRow(*row) + return self + + # def Update(self, disabled=None): # TODO Disable / enable of tabs is not complete + # print('*** Tab.Update is not implemented ***') + # return + # if disabled is None: + # return + # self.Disabled = disabled + # state = 'disabled' if disabled is True else 'normal' + # self.ParentNotebook.tab(self.TabID, state=state) + # return self + + def _GetElementAtLocation(self, location): + (row_num, col_num) = location + row = self.Rows[row_num] + element = row[col_num] + return element + + +# ---------------------------------------------------------------------- # +# TabGroup # +# ---------------------------------------------------------------------- # +class TabGroup(Element): + def __init__(self, layout, tab_location=None, title_color=None, selected_title_color=None, background_color=None, + font=None, change_submits=False, enable_events=False,pad=None, border_width=None, theme=None, key=None, tooltip=None, visible=True): + ''' + TabGroup Element + :param layout: + :param tab_location: + :param title_color: + :param selected_title_color: + :param background_color: + :param font: + :param change_submits: + :param pad: + :param border_width: + :param theme: + :param key: + :param tooltip: + ''' + self.UseDictionary = False + self.ReturnValues = None + self.ReturnValuesList = [] + self.ReturnValuesDictionary = {} + self.DictionaryKeyCounter = 0 + self.ParentWindow = None + self.SelectedTitleColor = selected_title_color + self.Rows = [] + self.TKNotebook = None + self.Widget = None # type: remi.gui.TabBox + self.Justification = 'left' + self.TabCount = 0 + self.BorderWidth = border_width + self.Theme = theme + self.BackgroundColor = background_color if background_color is not None else DEFAULT_BACKGROUND_COLOR + self.ChangeSubmits = enable_events or change_submits + self.TabLocation = tab_location + self.Visible = visible + self.Disabled = False + self._Layout(layout) + + super().__init__(ELEM_TYPE_TAB_GROUP, background_color=background_color, text_color=title_color, font=font, + pad=pad, key=key, tooltip=tooltip) + return + + def _AddRow(self, *args): + ''' Parms are a variable number of Elements ''' + 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 + 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): + for row in rows: + self._AddRow(*row) + + def _GetElementAtLocation(self, location): + (row_num, col_num) = location + row = self.Rows[row_num] + element = row[col_num] + return element + + def FindKeyFromTabName(self, tab_name): + for row in self.Rows: + for element in row: + if element.Title == tab_name: + return element.Key + return None + + find_key_from_tab_name = FindKeyFromTabName + + +# ---------------------------------------------------------------------- # +# Slider # +# ---------------------------------------------------------------------- # +class Slider(Element): + def __init__(self, range=(None, None), default_value=None, resolution=None, tick_interval=None, orientation=None, + border_width=None, relief=None, change_submits=False, enable_events=False, disabled=False, size=(None, None), font=None, + background_color=None, text_color=None, key=None, pad=None, tooltip=None, visible=True, size_px=(None,None)): + """ + + :param range: + :param default_value: + :param resolution: + :param tick_interval: + :param orientation: + :param border_width: + :param relief: + :param change_submits: + :param enable_events: + :param disabled: + :param visible: + :param size_px: + """ + self.TKScale = None + 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 + temp_size = size + if temp_size == (None, None): + temp_size = (200, 20) if self.Orientation.startswith('h') else (200, 20) + elif size[0] is not None and size[0] < 100: + temp_size = size[0]*10, size[1]*3 + self.Widget = None # type: remi.gui.Slider + + 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, size_px=size_px) + return + + def Update(self, value=None, range=(None, None), disabled=None, visible=None): + if value is not None: + self.Widget.set_value(value) + self.DefaultValue = value + if range != (None, None): + self.Widget.attributes['min'] = '{}'.format(range[0]) + self.Widget.attributes['max'] = '{}'.format(range[1]) + super().Update(self.Widget, disabled=disabled, visible=visible) + + def _SliderCallback(self, widget:remi.Widget, value): + self.ParentForm.LastButtonClicked = self.Key if self.Key is not None else '' + self.ParentForm.MessageQueue.put(self.ParentForm.LastButtonClicked) + + update = Update + +# +# ---------------------------------------------------------------------- # +# Column # +# ---------------------------------------------------------------------- # +class Column(Element): + def __init__(self, layout, background_color=None, size=(None, None), pad=None, scrollable=False, vertical_scroll_only=False, element_justification='left', key=None): + ''' + Column Element + :param layout: + :param background_color: + :param size: + :param pad: + :param scrollable: + :param key: + ''' + self.UseDictionary = False + self.ReturnValues = None + self.ReturnValuesList = [] + self.ReturnValuesDictionary = {} + self.DictionaryKeyCounter = 0 + self.ParentWindow = None + self.Rows = [] + self.TKFrame = None + self.Scrollable = scrollable + self.VerticalScrollOnly = vertical_scroll_only + self.ElementJustification = element_justification + # self.ImageFilename = image_filename + # self.ImageData = image_data + # self.ImageSize = image_size + # self.ImageSubsample = image_subsample + # bg = background_color if background_color is not None else DEFAULT_BACKGROUND_COLOR + + self.Layout(layout) + + super().__init__(ELEM_TYPE_COLUMN, background_color=background_color, size=size, pad=pad, key=key) + return + + def AddRow(self, *args): + ''' Parms are a variable number of Elements ''' + 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 + 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): + for row in rows: + self.AddRow(*row) + + def _GetElementAtLocation(self, location): + (row_num, col_num) = location + row = self.Rows[row_num] + element = row[col_num] + return element + + add_row = AddRow + layout = Layout + + +Col = Column + + +# ---------------------------------------------------------------------- # +# Menu # +# ---------------------------------------------------------------------- # +class Menu(Element): + def __init__(self, menu_definition, background_color=COLOR_SYSTEM_DEFAULT, text_color=None, size=(None, None), tearoff=False, pad=None, key=None, disabled=False, font=None): + ''' + Menu Element + :param menu_definition: + :param background_color: + :param size: + :param tearoff: + :param pad: + :param key: + ''' + back_color = background_color if background_color is not None else DEFAULT_BACKGROUND_COLOR + self.MenuDefinition = menu_definition + self.TKMenu = None + self.Tearoff = tearoff + self.Widget = None # type: remi.gui.MenuBar + self.MenuItemChosen = None + self.Disabled = disabled + + super().__init__(ELEM_TYPE_MENUBAR, background_color=back_color, text_color=text_color, size=size, pad=pad, key=key, font=font) + return + + + + def _ChangedCallbackMenu(self, widget, *user_data): + widget = widget # type: remi.gui.MenuItem + chosen = user_data[0] + self.MenuItemChosen = chosen + self.ParentForm.LastButtonClicked = chosen + self.ParentForm.MessageQueue.put(chosen) + + +# ---------------------------------------------------------------------- # +# 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, row_header_text='Row', starting_row_num=0, num_rows=None, row_height=None, font=None, justification='right', text_color=None, background_color=None, alternating_row_color=None, row_colors=None, vertical_scroll_only=True, disabled=False, + size=(None, None), change_submits=False, enable_events=False, bind_return_key=False, pad=None, key=None, tooltip=None, right_click_menu=None, visible=True, size_px=(None, None)): + ''' + Table + :param values: + :param headings: + :param visible_column_map: + :param col_widths: + :param def_col_width: + :param auto_size_columns: + :param max_col_width: + :param select_mode: + :param display_row_numbers: + :param num_rows: + :param row_height: + :param font: + :param justification: + :param text_color: + :param background_color: + :param alternating_row_color: + :param size: + :param change_submits: + :param enable_events: + :param bind_return_key: + :param pad: + :param key: + :param tooltip: + :param right_click_menu: + :param visible: + ''' + 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.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.TKTreeview = None + self.AlternatingRowColor = alternating_row_color + self.VerticalScrollOnly = vertical_scroll_only + self.SelectedRows = [] + self.ChangeSubmits = change_submits or enable_events + self.BindReturnKey = bind_return_key + self.StartingRowNumber = starting_row_num # When displaying row numbers, where to start + self.RowHeaderText = row_header_text + self.RightClickMenu = right_click_menu + self.RowColors = row_colors + self.Disabled = disabled + self.SelectedItem = None + self.SelectedRow = None + self.Widget = None # type: remi.Table + + super().__init__(ELEM_TYPE_TABLE, text_color=text_color, background_color=background_color, font=font, + size=size, pad=pad, key=key, tooltip=tooltip, visible=visible, size_px=size_px) + return + + def Update(self, values=None): + print('*** Table Update not yet supported ***') + 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.TKTreeview.delete(*self.TKTreeview.get_children()) + for i, value in enumerate(values): + if self.DisplayRowNumbers: + value = [i + self.StartingRowNumber] + value + id = self.TKTreeview.insert('', 'end', text=i, iid=i + 1, values=value, tag=i % 2) + if self.AlternatingRowColor is not None: + self.TKTreeview.tag_configure(1, background=self.AlternatingRowColor) + self.Values = values + self.SelectedRows = [] + + + def _on_table_row_click(self, table, row, item): + # self.SelectedRow = row # type: remi.gui.TableRow + self.SelectedItem = item.get_text() + index = -1 + # each widget (and specifically in this case the table) has a _render_children_list attribute that + # is an ordered list of the children keys + # first, we search for the row in the children dictionary + for key, value in table.children.items(): + if value == row: + # if the row is found, we get the index in the ordered list + index = table._render_children_list.index(key) + break + self.SelectedRow = index + if self.ChangeSubmits: + self.ParentForm.LastButtonClicked = self.Key if self.Key is not None else '' + self.ParentForm.MessageQueue.put(self.ParentForm.LastButtonClicked) + else: + self.ParentForm.LastButtonClicked = '' + + + + +# ---------------------------------------------------------------------- # +# Tree # +# ---------------------------------------------------------------------- # +class Tree(Element): + def __init__(self, data=None, headings=None, visible_column_map=None, col_widths=None, col0_width=10, + def_col_width=10, auto_size_columns=True, max_col_width=20, select_mode=None, show_expanded=False, + change_submits=False, font=None, + justification='right', text_color=None, background_color=None, num_rows=None, pad=None, key=None, + tooltip=None): + ''' + Tree Element + :param headings: + :param visible_column_map: + :param col_widths: + :param def_col_width: + :param auto_size_columns: + :param max_col_width: + :param select_mode: + :param font: + :param justification: + :param text_color: + :param background_color: + :param num_rows: + :param pad: + :param key: + :param tooltip: + ''' + 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.Justification = justification + self.InitialState = None + self.SelectMode = select_mode + self.ShowExpanded = show_expanded + self.NumRows = num_rows + self.Col0Width = col0_width + self.TKTreeview = None + self.SelectedRows = [] + self.ChangeSubmits = change_submits + + print('*** Tree Element not yet supported ***') + + super().__init__(ELEM_TYPE_TREE, text_color=text_color, background_color=background_color, font=font, pad=pad, + key=key, tooltip=tooltip) + + + def add_treeview_data(self, node): + # print(f'Inserting {node.key} under parent {node.parent}') + if node.key != '': + self.TKTreeview.insert(node.parent, 'end', node.key, text=node.text, values=node.values, + open=self.ShowExpanded) + for node in node.children: + self.add_treeview_data(node) + + def Update(self, values=None, key=None, value=None, text=None): + print('*** Tree Element not yet supported ***') + 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.add_treeview_data(self.TreeData.root_node) + self.SelectedRows = [] + if key is not None: + item = self.TKTreeview.item(key) + if value is not None: + self.TKTreeview.item(key, values=value) + if text is not None: + self.TKTreeview.item(key, text=text) + item = self.TKTreeview.item(key) + return self + + update = Update + + +class TreeData(object): + class Node(object): + def __init__(self, parent, key, text, values): + self.parent = parent + self.children = [] + self.key = key + self.text = text + self.values = values + + def _Add(self, node): + self.children.append(node) + + def __init__(self): + self.tree_dict = {} + self.root_node = self.Node("", "", 'root', []) + self.tree_dict[""] = self.root_node + + def _AddNode(self, key, node): + self.tree_dict[key] = node + + def Insert(self, parent, key, text, values): + node = self.Node(parent, key, text, values) + self.tree_dict[key] = node + parent_node = self.tree_dict[parent] + parent_node._Add(node) + + def __repr__(self): + return self._NodeStr(self.root_node, 1) + + def _NodeStr(self, node, level): + return '\n'.join( + [str(node.key) + ' : ' + str(node.text)] + + [' ' * 4 * level + self._NodeStr(child, level + 1) for child in node.children]) + + insert = Insert + +# ---------------------------------------------------------------------- # +# Error Element # +# ---------------------------------------------------------------------- # +class ErrorElement(Element): + def __init__(self, key=None): + ''' + Error Element + :param key: + ''' + self.Key = key + + super().__init__(ELEM_TYPE_ERROR, key=key) + return + + def Update(self, *args, **kwargs): + PopupError('Keyword error in Update', + 'You need to stop this madness and check your spelling', + 'Bad key = {}'.format(self.Key), + 'Your bad line of code may resemble this:', + 'window.FindElement("{}")'.format(self.Key)) + return self + + def Get(self): + 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 + + +# ------------------------------------------------------------------------- # +# Window CLASS # +# ------------------------------------------------------------------------- # +class Window: + + _NumOpenWindows = 0 + user_defined_icon = None + hidden_master_root = None + QTApplication = None + active_popups = {} + highest_level_app = None + stdout_is_rerouted = False + stdout_string_io = None + stdout_location = None + port_number = 6900 + active_windows = [ ] # type: [Window] + App = None # type: remi.App + + def __init__(self, title, layout=None, default_element_size=DEFAULT_ELEMENT_SIZE, default_button_element_size=(None, None), + auto_size_text=None, auto_size_buttons=None, location=(None, None), size=(None, None), + element_padding=None, button_color=None, font=None, + progress_bar_color=(None, None), background_color=None, border_depth=None, auto_close=False, + auto_close_duration=None, icon=DEFAULT_BASE64_ICON, force_toplevel=False, + alpha_channel=1, return_keyboard_events=False, return_key_down_events=False, use_default_focus=True, text_justification=None, + no_titlebar=False, grab_anywhere=False, keep_on_top=False, resizable=True, disable_close=False,margins=(None, None), element_justification='left', + disable_minimize=False, background_image=None, finalize=False, + web_debug=False, web_ip='0.0.0.0', web_port=0, web_start_browser=True, web_update_interval=.0000001, web_multiple_instance=False ): + ''' + + :param title: + :param default_element_size: + :param default_button_element_size: + :param auto_size_text: + :param auto_size_buttons: + :param location: + :param size: + :param element_padding: + :param button_color: + :param font: + :param progress_bar_color: + :param background_color: + :param border_depth: + :param auto_close: + :param auto_close_duration: + :param icon: + :param force_toplevel: + :param alpha_channel: + :param return_keyboard_events: + :param use_default_focus: + :param text_justification: + :param no_titlebar: + :param grab_anywhere: + :param keep_on_top: + :param resizable: + :param disable_close: + :param background_image: + ''' + 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 = title + self.Rows = [] # a list of ELEMENTS for this row + self.DefaultElementSize = convert_tkinter_size_to_Wx(default_element_size) + self.DefaultButtonElementSize = convert_tkinter_size_to_Wx( + default_button_element_size) if default_button_element_size != ( + None, None) else DEFAULT_BUTTON_ELEMENT_SIZE + self.Location = location + self.ButtonColor = button_color if button_color else DEFAULT_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 + self.WindowIcon = icon if icon is not None else Window.user_defined_icon + self.AutoClose = auto_close + self.NonBlocking = False + self.TKroot = None + 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.AllKeysDict = {} + self.LastButtonClicked = None + self.LastButtonClickedWasRealtime = False + self.UseDictionary = False + self.UseDefaultFocus = use_default_focus + self.ReturnKeyboardEvents = return_keyboard_events + self.ReturnKeyDownEvents = return_key_down_events + self.KeyInfoDict = {} + self.LastKeyboardEvent = None + self.TextJustification = text_justification + self.NoTitleBar = no_titlebar + self.GrabAnywhere = grab_anywhere + self.KeepOnTop = keep_on_top + self.ForcefTopLevel = force_toplevel + self.Resizable = resizable + self._AlphaChannel = alpha_channel + self.Timeout = None + self.TimeoutKey = TIMEOUT_KEY + self.TimerCancelled = False + self.DisableClose = disable_close + self._Hidden = False + # self.QTApplication = None + # self.QT_QMainWindow = None + self._Size = size + self.ElementPadding = element_padding or DEFAULT_ELEMENT_PADDING + self.FocusElement = None + self.BackgroundImage = background_image + self.XFound = False + self.DisableMinimize = disable_minimize + self.OutputElementForStdOut = None # type: Output + self.Justification = 'left' + self.ElementJustification = element_justification + self.IgnoreClose = False + self.thread_id = None + self.App = None # type: Window.MyApp + self.web_debug = web_debug + self.web_ip = web_ip + self.web_port = web_port + self.web_start_browser = web_start_browser + self.web_update_interval = web_update_interval + self.web_multiple_instance = web_multiple_instance + self.MessageQueue = Queue() + self.master_widget = None # type: remi.gui.VBox + self.UniqueKeyCounter = 0 + + if layout is not None: + self.Layout(layout) + if finalize: + self.Finalize() + + @classmethod + def IncrementOpenCount(self): + self._NumOpenWindows += 1 + # print('+++++ INCREMENTING Num Open Windows = {} ---'.format(Window._NumOpenWindows)) + + @classmethod + def _DecrementOpenCount(self): + self._NumOpenWindows -= 1 * (self._NumOpenWindows != 0) # decrement if not 0 + # print('----- DECREMENTING Num Open Windows = {} ---'.format(Window._NumOpenWindows)) + + # ------------------------- Add ONE Row to Form ------------------------- # + def AddRow(self, *args): + ''' Parms are a variable number of Elements ''' + 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 + element.Position = (CurrentRowNumber, i) + element.ParentContainer = self + CurrentRow.append(element) + # ------------------------- Append the row to list of Rows ------------------------- # + self.Rows.append(CurrentRow) + + # ------------------------- Add Multiple Rows to Form ------------------------- # + def AddRows(self, rows): + for row in rows: + self.AddRow(*row) + + def Layout(self, rows): + self.AddRows(rows) + self._BuildKeyDict() + return self + + def LayoutAndRead(self, rows, non_blocking=False): + raise DeprecationWarning( + 'LayoutAndRead is no longer supported... change your call to window.Layout(layout).Read()') + # self.AddRows(rows) + # self.Show(non_blocking=non_blocking) + # return self.ReturnValues + + def LayoutAndShow(self, rows): + raise DeprecationWarning('LayoutAndShow is no longer supported... change your call to LayoutAndRead') + + # ------------------------- ShowForm THIS IS IT! ------------------------- # + def Show(self, non_blocking=False): + 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) + + + + + 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. + Use the close parameter to close the window after reading + + :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 + :param close: (bool) if True the window will be closed prior to returning + :return: Tuple[(Any), Union[Dict[Any:Any]], List[Any], None] (event, values) + """ + results = self._read(timeout=timeout, timeout_key=timeout_key) + if close: + self.close() + + return results + + + + def _read(self, timeout=None, timeout_key=TIMEOUT_KEY): + # 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 not self.Shown: + self.Show() + # if already have a button waiting, the return previously built results + if self.LastButtonClicked is not None and not self.LastButtonClickedWasRealtime: + # print(f'*** Found previous clicked saved {self.LastButtonClicked}') + results = BuildResults(self, False, self) + self.LastButtonClicked = None + return results + InitializeResults(self) + # 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: + # print(f'RTime down {self.LastButtonClicked}' ) + try: + rc = self.TKroot.update() + except: + self.TKrootDestroyed = True + Window._DecrementOpenCount() + results = BuildResults(self, False, self) + if results[0] != None and results[0] != timeout_key: + return results + else: + pass + + # else: + # print("** REALTIME PROBLEM FOUND **", results) + # print('****************** CALLING MESSAGE QUEUE GET ***********************') + self.CurrentlyRunningMainloop = True + if timeout is not None: + try: + self.LastButtonClicked = self.MessageQueue.get(timeout=(timeout if timeout else .001)/1000) + # print(f'Got event {self.LastButtonClicked}') + except: # timeout + self.LastButtonClicked = timeout_key + else: + self.LastButtonClicked = self.MessageQueue.get() + # print(f'Got event {self.LastButtonClicked}') + # print('--------------------- BACK FROM MESSAGE QUEUE GET ----------------------') + + results = BuildResults(self, False, self) + return results + # print(f'In main {self.Title}') + ################################# CALL GUWxTextCtrlI MAINLOOP ############################ + # self.App.MainLoop() + # self.CurrentlyRunningMainloop = False + # self.TimerCancelled = True + # if timer: + # timer.Stop() + # if Window.stdout_is_rerouted: + # sys.stdout = Window.stdout_location + # if self.RootNeedsDestroying: + # self.LastButtonClicked = None + # self.App.Close() + # try: + # self.MasterFrame.Close() + # except: + # pass + # Window._DecrementOpenCount() + # if form was closed with X + # if self.LastButtonClicked is None and self.LastKeyboardEvent is None and self.ReturnValues[0] is None: + # Window._DecrementOpenCount() + # 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 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: # TODO HIGHLY EXPERIMENTAL... added due to tray icon interaction + # print("*** Faking timeout ***") + # self.ReturnValues = self.TimeoutKey, self.ReturnValues[1] # fake a timeout + # return self.ReturnValues + + def _ReadNonBlocking(self): + if self.TKrootDestroyed: + return None, None + if not self.Shown: + self.Show(non_blocking=True) + # event = wx.Event() + # self.App.QueueEvent(event) + timer = wx.Timer(self.App) + self.App.Bind(wx.EVT_TIMER, self.timer_timeout) + timer.Start(milliseconds=0, oneShot=wx.TIMER_ONE_SHOT) + self.CurrentlyRunningMainloop = True + # print(f'In main {self.Title}') + ################################# CALL GUWxTextCtrlI MAINLOOP ############################ + + self.App.MainLoop() + if Window.stdout_is_rerouted: + sys.stdout = Window.stdout_location + # self.LastButtonClicked = 'TEST' + self.CurrentlyRunningMainloop = False + timer.Stop() + # while self.App.HasPendingEvents(): + # self.App.ProcessPendingEvents() + return BuildResults(self, False, self) + + + # ------------------------- SetIcon - set the window's fav icon ------------------------- # + def SetIcon(self, icon=None, pngbase64=None): + pass + + def _GetElementAtLocation(self, location): + (row_num, col_num) = location + row = self.Rows[row_num] + element = row[col_num] + return element + + def _GetDefaultElementSize(self): + return self.DefaultElementSize + + def _AutoCloseAlarmCallback(self): + try: + window = self + if window: + if window.NonBlocking: + self.CloseNonBlockingForm() + else: + window._Close() + if self.CurrentlyRunningMainloop: + self.QTApplication.exit() # kick the users out of the mainloop + self.RootNeedsDestroying = True + self.QT_QMainWindow.close() + + except: + pass + + def timer_timeout(self, event): + # first, get the results table built + # modify the Results table in the parent FlexForm object + # print('timer timeout') + if self.TimerCancelled: + return + self.LastButtonClicked = self.TimeoutKey + self.FormRemainedOpen = True + if self.CurrentlyRunningMainloop: + self.App.ExitMainLoop() + + def non_block_timer_timeout(self, event): + # print('non-blocking timer timeout') + self.App.ExitMainLoop() + + def autoclose_timer_callback(self, frame): + # print('*** AUTOCLOSE TIMEOUT CALLBACK ***', frame) + try: + frame.Close() + except: + pass # if user has already closed the frame will get an error + + if self.CurrentlyRunningMainloop: + self.App.ExitMainLoop() + + def on_key_down(self, emitter, key, keycode, ctrl, shift, alt): + self.LastButtonClicked = 'DOWN'+key + self.MessageQueue.put(self.LastButtonClicked) + self.KeyInfoDict = { 'key':key, 'keycode':keycode, 'ctrl': ctrl, 'shift':shift, 'alt':alt } + + def on_key_up(self, emitter, key, keycode, ctrl, shift, alt): + self.LastButtonClicked = key + self.MessageQueue.put(self.LastButtonClicked) + self.KeyInfoDict = { 'key':key, 'keycode':keycode, 'ctrl': ctrl, 'shift':shift, 'alt':alt } + + + def callback_keyboard_char(self, event): + self.LastButtonClicked = None + self.FormRemainedOpen = True + if event.ClassName == 'wxMouseEvent': + if event.WheelRotation < 0: + self.LastKeyboardEvent = 'MouseWheel:Down' + else: + self.LastKeyboardEvent = 'MouseWheel:Up' + else: + self.LastKeyboardEvent = event.GetKeyCode() + if not self.NonBlocking: + BuildResults(self, False, self) + if self.CurrentlyRunningMainloop: # quit if this is the current mainloop, otherwise don't quit! + self.App.ExitMainLoop() # kick the users out of the mainloop + if event.ClassName != 'wxMouseEvent': + event.DoAllowNextEvent() + + def Finalize(self): + if self.TKrootDestroyed: + return self + if not self.Shown: + self.Show(non_blocking=True) + # else: + # try: + # self.QTApplication.processEvents() # refresh the window + # except: + # print('* ERROR FINALIZING *') + # self.TKrootDestroyed = True + # Window._DecrementOpenCount() + return self + + def Refresh(self): + # self.QTApplication.processEvents() # refresh the window + return self + + def VisibilityChanged(self): + self.SizeChanged() + return self + + def Fill(self, values_dict): + _FillFormWithValues(self, values_dict) + return self + + def FindElement(self, key, silent_on_error=False): + try: + element = self.AllKeysDict[key] + except KeyError: + element = None + if element is None: + if not silent_on_error: + print('*** WARNING = FindElement did not find the key. Please check your key\'s spelling ***') + PopupError('Keyword error in FindElement Call', + 'Bad key = {}'.format(key), + 'Your bad line of code may resemble this:', + 'window.FindElement("{}")'.format(key)) + return ErrorElement(key=key) + else: + return False + return element + + Element = FindElement # shortcut function definition + + def _BuildKeyDict(self): + dict = {} + self.AllKeysDict = self._BuildKeyDictForWindow(self,self, dict) + # print(f'keys built = {self.AllKeysDict}') + + def _BuildKeyDictForWindow(self, top_window, window, key_dict): + 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_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 + if element.Type in (ELEM_TYPE_MENUBAR, ELEM_TYPE_BUTTONMENU, ELEM_TYPE_CANVAS, + 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_TABLE, ELEM_TYPE_TREE, + ELEM_TYPE_INPUT_TEXT): + element.Key = top_window.DictionaryKeyCounter + top_window.DictionaryKeyCounter += 1 + if element.Key is not None: + if element.Key in key_dict.keys(): + print('*** Duplicate key found in your layout {} ***'.format(element.Key)) if element.Type != ELEM_TYPE_BUTTON else None + element.Key = element.Key + str(self.UniqueKeyCounter) + self.UniqueKeyCounter += 1 + print('*** Replaced new key with {} ***'.format(element.Key)) if element.Type != ELEM_TYPE_BUTTON else None + key_dict[element.Key] = element + return key_dict + + def FindElementWithFocus(self): + return self.FocusElement + # element = _FindElementWithFocusInSubForm(self) + # return element + + def SaveToDisk(self, filename): + try: + results = BuildResults(self, False, self) + with open(filename, 'wb') as sf: + pickle.dump(results[1], sf) + except: + print('*** Error saving form to disk ***') + + def LoadFromDisk(self, filename): + try: + with open(filename, 'rb') as df: + self.Fill(pickle.load(df)) + except: + print('*** Error loading form to disk ***') + + def GetScreenDimensions(self): # TODO - Not sure what to return so (0,0) for now + size = (0,0) + return size + + def Move(self, x, y): + self.MasterFrame.SetPosition((x, y)) + + def Minimize(self): + self.MasterFrame.Iconize() + + def Maximize(self): + self.MasterFrame.Maximize() + + def _Close(self): + if not self.NonBlocking: + BuildResults(self, False, self) + if self.TKrootDestroyed: + return None + self.TKrootDestroyed = True + self.RootNeedsDestroying = True + self.Close() + + def Close(self): + if len(Window.active_windows) != 0: + del(Window.active_windows[-1]) # delete current window from active windows + if len(Window.active_windows) != 0: + window = Window.active_windows[-1] # get prior window to change to + Window.App.set_root_widget(window.master_widget) + else: + self.App.close() + self.App.server.server_starter_instance._alive = False + self.App.server.server_starter_instance._sserver.shutdown() + return + + self.App.close() + self.App.server.server_starter_instance._alive = False + self.App.server.server_starter_instance._sserver.shutdown() + + CloseNonBlockingForm = Close + CloseNonBlocking = Close + + def Disable(self): + self.MasterFrame.Enable(False) + + def Enable(self): + self.MasterFrame.Enable(True) + + def Hide(self): + self._Hidden = True + self.master_widget.attributes['hidden'] = 'true' + # self.MasterFrame.Hide() + return + + def UnHide(self): + if self._Hidden: + del(self.master_widget.attributes['hidden']) + self._Hidden = False + + def Disappear(self): + self.MasterFrame.SetTransparent(0) + + def Reappear(self): + self.MasterFrame.SetTransparent(255) + + def SetAlpha(self, alpha): + ''' + Change the window's transparency + :param alpha: From 0 to 1 with 0 being completely transparent + :return: + ''' + self._AlphaChannel = alpha * 255 + if self._AlphaChannel is not None: + self.MasterFrame.SetTransparent(self._AlphaChannel) + + @property + def AlphaChannel(self): + return self._AlphaChannel + + @AlphaChannel.setter + def AlphaChannel(self, alpha): + self.SetAlpha(alpha) + + def BringToFront(self): + self.MasterFrame.ToggleWindowStyle(wx.STAY_ON_TOP) + + def CurrentLocation(self): + location = self.MasterFrame.GetPosition() + return location + + + @property + def Size(self): + size = self.MasterFrame.GetSize() + return size + + @Size.setter + def Size(self, size): + self.MasterFrame.SetSize(size[0], size[1]) + + def SizeChanged(self): + size = self.Size + self.Size = size[0] + 1, size[1] + 1 + self.Size = size + self.MasterFrame.SetSizer(self.OuterSizer) + self.OuterSizer.Fit(self.MasterFrame) + + 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: (Any) The key to find + :return: Union[Element, None] The element found or None if no element was found + """ + try: + return self.Element(key) + except Exception as e: + print('The key you passed in is no good. Key = {}*'.format(key)) + return None + + 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) + + :param args: + :param kwargs: + :return: Tuple[Any, Dict[Any:Any]] The famous event, values that Read returns. + """ + return self.Read(*args, **kwargs) + + + + add_row = AddRow + add_rows = AddRows + alpha_channel = AlphaChannel + bring_to_front = BringToFront + close = Close + current_location = CurrentLocation + disable = Disable + disappear = Disappear + element = Element + enable = Enable + fill = Fill + finalize = Finalize + find_element = FindElement + find_element_with_focus = FindElementWithFocus + get_screen_dimensions = GetScreenDimensions + hide = Hide + increment_open_count = IncrementOpenCount + layout = Layout + layout_and_read = LayoutAndRead + layout_and_show = LayoutAndShow + load_from_disk = LoadFromDisk + maximize = Maximize + minimize = Minimize + move = Move + num_open_windows = _NumOpenWindows + read = Read + reappear = Reappear + refresh = Refresh + save_to_disk = SaveToDisk + set_alpha = SetAlpha + set_icon = SetIcon + show = Show + size = Size + size_changed = SizeChanged + un_hide = UnHide + visibility_changed = VisibilityChanged + + + + + + def remi_thread(self): + # print('Remi Thread started') + logging.getLogger('remi').setLevel(logging.CRITICAL) + logging.getLogger('remi').disabled = True + logging.getLogger('remi.server.ws').disabled = True + logging.getLogger('remi.server').disabled = True + logging.getLogger('remi.request').disabled = True + # use this code to start the application instead of the **start** call + # s = remi.Server(self.MyApp, start=True, title=self.Title, address='0.0.0.0', port=8081, start_browser=True, userdata=(self,), multiple_instance=False, update_interval=.001) + + # logging.getLogger('remi').setLevel(level=logging.CRITICAL) + # logging.getLogger('remi').disabled = True + # logging.disable(logging.CRITICAL) + # s = remi.server.StandaloneServer(self.MyApp, width=1100, height=600) + # s.start() + Window.port_number += 1 + try: + remi.start(self.MyApp, + title=self.Title, + debug=self.web_debug, + address=self.web_ip, + port=self.web_port, + multiple_instance=self.web_multiple_instance, + start_browser=self.web_start_browser, + update_interval=self.web_update_interval, userdata=(self,)) + + except: + print('*** ERROR Caught inside Remi ***') + print(traceback.format_exc()) + # remi.start(self.MyApp, title=self.Title ,debug=False, userdata=(self,), standalone=True) # standalone=True) + + # remi.start(self.MyApp, standalone=True, debug=True, userdata=(self,) ) # Can't do this because of a threading problem + print('Returned from Remi Start command... now sending None event') + + self.MessageQueue.put(None) # if returned from start call, then the window has been destroyed and a None event should be generated + + class MyApp(remi.App): + def __init__(self,*args, userdata2=None): + # self.window = window # type: Window + # print(args[-1]) + if userdata2 is None: + userdata = args[-1].userdata + self.window = userdata[0] # type: Window + else: + self.window = userdata2 # type: Window + self.master_widget = None + # print("new App instance %s" % str(id(self))) + # self.window.App = self + #Window.App = self + self.lines_shown = [] + + if userdata2 is None: + # res_path = os.path.dirname(os.path.abspath(__file__)) + # print('res path', res_path) + super(Window.MyApp, self).__init__(*args, static_file_path={'C':'c:','c':'c:','D':'d:', 'd':'d:', 'E':'e:', 'e':'e:', 'dot':'.', '.':'.'}) + + def _instance(self): + remi.App._instance(self) + self.window.App = remi.server.clients[self.session] + + def log_message(self, *args, **kwargs): + pass + + def idle(self): + if Window.stdout_is_rerouted: + Window.stdout_string_io.seek(0) + lines = Window.stdout_string_io.readlines() + # lines.reverse() + # self.window.OutputElementForStdOut.Widget.set_text("".join(lines)) + # self.window.OutputElementForStdOut.Update("".join(lines)) + if lines != self.lines_shown: + self.window.OutputElementForStdOut.Update("".join(lines)) + self.lines_shown = lines + + def main(self, name='world'): + # margin 0px auto allows to center the app to the screen + # self.master_widget = remi.gui.VBox() + # self.master_widget.style['justify-content'] = 'flex-start' + # self.master_widget.style['align-items'] = 'baseline' + # if self.window.BackgroundColor not in (None, COLOR_SYSTEM_DEFAULT): + # self.master_widget.style['background-color'] = self.window.BackgroundColor + # try: + # PackFormIntoFrame(self.window, self.master_widget, self.window) + # except: + # print('* ERROR PACKING FORM *') + # print(traceback.format_exc()) + # + # if self.window.BackgroundImage: + # self.master_widget.style['background-image'] = "url('{}')".format('/'+self.window.BackgroundImage) + # # print(f'background info',self.master_widget.attributes['background-image'] ) + # + # if not self.window.DisableClose: + # # add the following 3 lines to your app and the on_window_close method to make the console close automatically + # tag = remi.gui.Tag(_type='script') + # tag.add_child("javascript", """window.onunload=function(e){sendCallback('%s','%s');return "close?";};""" % ( + # str(id(self)), "on_window_close")) + # self.master_widget.add_child("onunloadevent", tag) + + self.master_widget = setup_remi_window(self, self.window) + self.window.master_widget = self.master_widget + # if self.window.WindowIcon: + # print('placing icon') + # self.page.children['head'].set_icon_file("/res:logo.png") + # self.page.children['head'].set_icon_data( base64_data=self.window.WindowIcon, mimetype="image/png" ) + + self.window.MessageQueue.put('Layout complete') # signal the main code that the layout is all done + return self.master_widget # returning the root widget + + + def on_window_close(self): + # here you can handle the unload + print("app closing") + self.close() + self.server.server_starter_instance._alive = False + self.server.server_starter_instance._sserver.shutdown() + # self.window.MessageQueue.put(None) + print("server stopped") + +FlexForm = Window + + + + +# =========================================================================== # +# Stops the mainloop and sets the event information # +# =========================================================================== # + +def element_callback_quit_mainloop(element): + if element.Key is not None: + element.ParentForm.LastButtonClicked = element.Key + else: + element.ParentForm.LastButtonClicked = '' + try: + element.ParentForm.LastButtonClicked = element.Key if element.Key is not None else element.ButtonText + except: + element.ParentForm.LastButtonClicked = element.Key + # print(f'Putting into message queue {element.ParentForm.LastButtonClicked}') + element.ParentForm.MessageQueue.put(element.ParentForm.LastButtonClicked) + + +def quit_mainloop(window): + window.App.ExitMainLoop() + + +# =========================================================================== # +# Stops the mainloop and sets the event information # +# =========================================================================== # +def convert_tkinter_size_to_Wx(size): + """ + Converts size in characters to size in pixels + :param size: size in characters, rows + :return: size in pixels, pixels + """ + qtsize = size + if size[1] is not None and size[1] < DEFAULT_PIXEL_TO_CHARS_CUTOFF: # change from character based size to pixels (roughly) + qtsize = size[0]*DEFAULT_PIXELS_TO_CHARS_SCALING[0], size[1]*DEFAULT_PIXELS_TO_CHARS_SCALING[1] + return qtsize + + +def base64_to_style_image(base64_image): + x ="url('data:image/png;base64," + x += str(base64_image) + x += "')" + # print(x) + return x + + +def font_parse_string(font): + """ + Convert from font string/tyuple into a Qt style sheet string + :param font: "Arial 10 Bold" or ('Arial', 10, 'Bold) + :return: style string that can be combined with other style strings + """ + + if font is None: + return '' + + if type(font) is str: + _font = font.split(' ') + else: + _font = font + family = _font[0] + point_size = int(_font[1]) + + style = _font[2:] if len(_font) > 1 else None + + # underline = 'underline' in _font[2:] + # bold = 'bold' in _font + + return family, point_size, style + + + + +# ################################################################################ +# ################################################################################ +# END OF ELEMENT DEFINITIONS +# ################################################################################ +# ################################################################################ + + +# =========================================================================== # +# Button Lazy Functions so the caller doesn't have to define a bunch of stuff # +# =========================================================================== # + + +# ------------------------- FOLDER BROWSE Element lazy function ------------------------- # +def FolderBrowse(button_text='Browse', target=(ThisRow, -1), initial_folder=None, tooltip=None, size=(None, None), + auto_size_button=None, button_color=None, disabled=False, change_submits=False, font=None, pad=None, + key=None): + return Button(button_text=button_text, button_type=BUTTON_TYPE_BROWSE_FOLDER, target=target, + initial_folder=initial_folder, tooltip=tooltip, size=size, auto_size_button=auto_size_button, + disabled=disabled, button_color=button_color, change_submits=change_submits, font=font, pad=pad, + key=key) + + +# ------------------------- FILE BROWSE Element lazy function ------------------------- # +def FileBrowse(button_text='Browse', target=(ThisRow, -1), file_types=(("ALL Files", "*.*"),), initial_folder=None, + tooltip=None, size=(None, None), auto_size_button=None, button_color=None, change_submits=False, + font=None, disabled=False, + pad=None, key=None): + 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, auto_size_button=auto_size_button, + change_submits=change_submits, disabled=disabled, button_color=button_color, font=font, pad=pad, + key=key) + + +# ------------------------- FILES BROWSE Element (Multiple file selection) lazy function ------------------------- # +def FilesBrowse(button_text='Browse', target=(ThisRow, -1), file_types=(("ALL Files", "*.*"),), disabled=False, + initial_folder=None, tooltip=None, size=(None, None), auto_size_button=None, button_color=None, + change_submits=False, + font=None, pad=None, key=None): + return 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, tooltip=tooltip, size=size, + auto_size_button=auto_size_button, + disabled=disabled, button_color=button_color, font=font, pad=pad, key=key) + + +# ------------------------- FILE BROWSE Element lazy function ------------------------- # +def FileSaveAs(button_text='Save As...', target=(ThisRow, -1), file_types=(("ALL Files", "*.*"),), initial_folder=None, + disabled=False, tooltip=None, size=(None, None), auto_size_button=None, button_color=None, + change_submits=False, font=None, + pad=None, key=None): + return Button(button_text=button_text, button_type=BUTTON_TYPE_SAVEAS_FILE, target=target, file_types=file_types, + initial_folder=initial_folder, tooltip=tooltip, size=size, disabled=disabled, + auto_size_button=auto_size_button, button_color=button_color, change_submits=change_submits, + font=font, pad=pad, key=key) + + +# ------------------------- SAVE AS Element lazy function ------------------------- # +def SaveAs(button_text='Save As...', target=(ThisRow, -1), file_types=(("ALL Files", "*.*"),), initial_folder=None, + disabled=False, tooltip=None, size=(None, None), auto_size_button=None, button_color=None, + change_submits=False, font=None, + pad=None, key=None): + return Button(button_text=button_text, button_type=BUTTON_TYPE_SAVEAS_FILE, target=target, file_types=file_types, + initial_folder=initial_folder, tooltip=tooltip, size=size, disabled=disabled, + auto_size_button=auto_size_button, button_color=button_color, change_submits=change_submits, + font=font, pad=pad, key=key) + + +# ------------------------- SAVE BUTTON Element lazy function ------------------------- # +def Save(button_text='Save', size=(None, None), auto_size_button=None, button_color=None, bind_return_key=True, + disabled=False, tooltip=None, font=None, focus=False, pad=None, key=None): + return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, + auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + + +# ------------------------- SUBMIT BUTTON Element lazy function ------------------------- # +def Submit(button_text='Submit', size=(None, None), auto_size_button=None, button_color=None, disabled=False, + bind_return_key=True, tooltip=None, font=None, focus=False, pad=None, key=None): + return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, + auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + + +# ------------------------- OPEN BUTTON Element lazy function ------------------------- # +def Open(button_text='Open', size=(None, None), auto_size_button=None, button_color=None, disabled=False, + bind_return_key=True, tooltip=None, font=None, focus=False, pad=None, key=None): + return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, + auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + + +# ------------------------- OK BUTTON Element lazy function ------------------------- # +def OK(button_text='OK', size=(None, None), auto_size_button=None, button_color=None, disabled=False, + bind_return_key=True, tooltip=None, font=None, focus=False, pad=None, key=None): + return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, + auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + + +# ------------------------- YES BUTTON Element lazy function ------------------------- # +def Ok(button_text='Ok', size=(None, None), auto_size_button=None, button_color=None, disabled=False, + bind_return_key=True, tooltip=None, font=None, focus=False, pad=None, key=None): + return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, + auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + + +# ------------------------- CANCEL BUTTON Element lazy function ------------------------- # +def Cancel(button_text='Cancel', size=(None, None), auto_size_button=None, button_color=None, disabled=False, + tooltip=None, font=None, bind_return_key=False, focus=False, pad=None, key=None): + return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, + auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + + +# ------------------------- QUIT BUTTON Element lazy function ------------------------- # +def Quit(button_text='Quit', size=(None, None), auto_size_button=None, button_color=None, disabled=False, tooltip=None, + font=None, bind_return_key=False, focus=False, pad=None, key=None): + return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, + auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + + +# ------------------------- Exit BUTTON Element lazy function ------------------------- # +def Exit(button_text='Exit', size=(None, None), auto_size_button=None, button_color=None, disabled=False, tooltip=None, + font=None, bind_return_key=False, focus=False, pad=None, key=None): + return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, + auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + + + +# ------------------------- Up arrow BUTTON Element lazy function ------------------------- # +def Up(button_text='â–²', size=(None, None), auto_size_button=None, button_color=None, disabled=False, tooltip=None, + font=None, bind_return_key=True, focus=False, pad=None, key=None): + return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, + auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + +# ------------------------- Down arrow BUTTON Element lazy function ------------------------- # +def Down(button_text='â–¼', size=(None, None), auto_size_button=None, button_color=None, disabled=False, tooltip=None, + font=None, bind_return_key=True, focus=False, pad=None, key=None): + return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, + auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + +# ------------------------- Left arrow BUTTON Element lazy function ------------------------- # +def Left(button_text='â—„', size=(None, None), auto_size_button=None, button_color=None, disabled=False, tooltip=None, + font=None, bind_return_key=True, focus=False, pad=None, key=None): + return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, + auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + + +# ------------------------- Right arrow BUTTON Element lazy function ------------------------- # +def Right(button_text='â–º', size=(None, None), auto_size_button=None, button_color=None, disabled=False, tooltip=None, + font=None, bind_return_key=True, focus=False, pad=None, key=None): + return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, + auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + + + +# ------------------------- YES BUTTON Element lazy function ------------------------- # +def Yes(button_text='Yes', size=(None, None), auto_size_button=None, button_color=None, disabled=False, tooltip=None, + font=None, bind_return_key=True, focus=False, pad=None, key=None): + return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, + auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + + +# ------------------------- NO BUTTON Element lazy function ------------------------- # +def No(button_text='No', size=(None, None), auto_size_button=None, button_color=None, disabled=False, tooltip=None, + font=None, bind_return_key=False, focus=False, pad=None, key=None): + return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, + auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + + +# ------------------------- NO BUTTON Element lazy function ------------------------- # +def Help(button_text='Help', size=(None, None), auto_size_button=None, button_color=None, disabled=False, font=None, + tooltip=None, bind_return_key=False, focus=False, pad=None, key=None): + return Button(button_text=button_text, button_type=BUTTON_TYPE_READ_FORM, tooltip=tooltip, size=size, + auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + + +# ------------------------- 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), auto_size_button=None, button_color=None, + font=None, bind_return_key=False, disabled=False, focus=False, pad=None, key=None): + 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, + auto_size_button=auto_size_button, button_color=button_color, font=font, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + + +# ------------------------- 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), auto_size_button=None, button_color=None, font=None, + bind_return_key=False, disabled=False, focus=False, pad=None, key=None): + 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, + auto_size_button=auto_size_button, button_color=button_color, font=font, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + + +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), auto_size_button=None, button_color=None, font=None, + bind_return_key=False, disabled=False, focus=False, pad=None, key=None): + 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, disabled=disabled, + auto_size_button=auto_size_button, button_color=button_color, font=font, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + + +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), auto_size_button=None, button_color=None, + font=None, disabled=False, bind_return_key=False, focus=False, pad=None, key=None): + 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, + auto_size_button=auto_size_button, button_color=button_color, font=font, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + + +# ------------------------- 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), auto_size_button=None, button_color=None, font=None, + disabled=False, bind_return_key=False, focus=False, pad=None, key=None): + 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, auto_size_button=auto_size_button, + button_color=button_color, font=font, disabled=disabled, bind_return_key=bind_return_key, focus=focus, + pad=pad, key=key) + + +# ------------------------- Calendar Chooser Button lazy function ------------------------- # +def CalendarButton(button_text, target=(None, None), 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), auto_size_button=None, + button_color=None, disabled=False, font=None, bind_return_key=False, focus=False, pad=None, + key=None): + 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, + auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + button.CalendarCloseWhenChosen = close_when_date_chosen + button.DefaultDate_M_D_Y = default_date_m_d_y + return button + + +# ------------------------- Calendar Chooser Button lazy function ------------------------- # +def ColorChooserButton(button_text, target=(None, None), image_filename=None, image_data=None, image_size=(None, None), + image_subsample=None, tooltip=None, border_width=None, size=(None, None), auto_size_button=None, + button_color=None, disabled=False, font=None, bind_return_key=False, focus=False, pad=None, + key=None): + 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, + auto_size_button=auto_size_button, button_color=button_color, font=font, disabled=disabled, + bind_return_key=bind_return_key, focus=focus, pad=pad, key=key) + + +##################################### ----- RESULTS ------ ################################################## + +def AddToReturnDictionary(form, element, value): + form.ReturnValuesDictionary[element.Key] = value + return + 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): + row = RadValue // 1000 + col = RadValue % 1000 + return row, col + + +def EncodeRadioRowCol(row, col): + RadValue = 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): + button_pressed_text = 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 + button_pressed_text = 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 + button_pressed_text = 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 + button_pressed_text = 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 + button_pressed_text = element.ReturnValues[0] + + if not initialize_only: + if element.Type == ELEM_TYPE_INPUT_TEXT: + element = element # type: InputText + value = element.Widget.get_value() + if not top_level_form.NonBlocking and not element.do_not_clear and not top_level_form.ReturnKeyboardEvents: + element.Widget.set_value('') + elif element.Type == ELEM_TYPE_INPUT_CHECKBOX: + element = element # type: Checkbox + value = element.Widget.get_value() + elif element.Type == ELEM_TYPE_INPUT_RADIO: + # RadVar = element.TKIntVar.get() + # this_rowcol = EncodeRadioRowCol(row_num, col_num) + value = False + elif element.Type == ELEM_TYPE_BUTTON: + if top_level_form.LastButtonClicked == element.ButtonText: + button_pressed_text = 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: + try: + value = element.TKCal.selection + except: + value = None + else: + try: + value = element.TKStringVar.get() + except: + value = None + elif element.Type == ELEM_TYPE_INPUT_COMBO: + element = element # type: Combo + value = element.Widget.get_value() + elif element.Type == ELEM_TYPE_INPUT_OPTION_MENU: + # value = element.TKStringVar.get() + value = None + elif element.Type == ELEM_TYPE_INPUT_LISTBOX: + element = element # type: Listbox + value = element.Widget.get_value() + value = [value,] + # items = element.TKListbox.curselection() + # value = [element.Values[int(item)] for item in items] + elif element.Type == ELEM_TYPE_INPUT_SPIN: + element = element # type: Spin + value = element.Widget.get_value() + elif element.Type == ELEM_TYPE_INPUT_SLIDER: + element = element # type: Slider + value = element.Widget.get_value() + elif element.Type == ELEM_TYPE_INPUT_MULTILINE: + element = element # type: Multiline + if element.WriteOnly: + continue + value = element.Widget.get_value() + 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: + element = element # type:Table + value = [element.SelectedRow,] + elif element.Type == ELEM_TYPE_TREE: + value = element.SelectedRows + elif element.Type == ELEM_TYPE_GRAPH: + value = element.ClickPosition + elif element.Type == ELEM_TYPE_MENUBAR: + value = element.MenuItemChosen + else: + value = None + + # if an input type element, update the results + if element.Type != ELEM_TYPE_BUTTON and \ + element.Type != ELEM_TYPE_TEXT and \ + element.Type != ELEM_TYPE_IMAGE and \ + element.Type != ELEM_TYPE_OUTPUT and \ + element.Type != ELEM_TYPE_PROGRESS_BAR and \ + element.Type != ELEM_TYPE_COLUMN and \ + element.Type != ELEM_TYPE_FRAME \ + and element.Type != ELEM_TYPE_TAB: + AddToReturnList(form, value) + AddToReturnDictionary(top_level_form, element, value) + elif (element.Type == ELEM_TYPE_BUTTON and + element.BType == BUTTON_TYPE_CALENDAR_CHOOSER and + element.Target == (None, None)) or \ + (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))): + AddToReturnList(form, value) + AddToReturnDictionary(top_level_form, element, value) + + # if this is a column, then will fail so need to wrap with tr + try: + if form.ReturnKeyboardEvents and form.LastKeyboardEvent is not None: + button_pressed_text = form.LastKeyboardEvent + form.LastKeyboardEvent = None + except: + pass + + try: + form.ReturnValuesDictionary.pop(None, None) # clean up dictionary include None was included + except: + pass + + if not form.UseDictionary: + form.ReturnValues = button_pressed_text, form.ReturnValuesList + else: + form.ReturnValues = button_pressed_text, form.ReturnValuesDictionary + + return form.ReturnValues + + +def _FillFormWithValues(form, values_dict): + _FillSubformWithValues(form, values_dict) + + +def _FillSubformWithValues(form, values_dict): + for row_num, row in enumerate(form.Rows): + for col_num, element in enumerate(row): + value = None + if element.Type == ELEM_TYPE_COLUMN: + _FillSubformWithValues(element, values_dict) + if element.Type == ELEM_TYPE_FRAME: + _FillSubformWithValues(element, values_dict) + if element.Type == ELEM_TYPE_TAB_GROUP: + _FillSubformWithValues(element, values_dict) + if element.Type == ELEM_TYPE_TAB: + _FillSubformWithValues(element, values_dict) + try: + value = values_dict[element.Key] + except: + continue + if element.Type == ELEM_TYPE_INPUT_TEXT: + element.Update(value) + elif element.Type == ELEM_TYPE_INPUT_CHECKBOX: + element.Update(value) + elif element.Type == ELEM_TYPE_INPUT_RADIO: + element.Update(value) + elif element.Type == ELEM_TYPE_INPUT_COMBO: + element.Update(value) + elif element.Type == ELEM_TYPE_INPUT_OPTION_MENU: + element.Update(value) + elif element.Type == ELEM_TYPE_INPUT_LISTBOX: + element.SetValue(value) + elif element.Type == ELEM_TYPE_INPUT_SLIDER: + element.Update(value) + elif element.Type == ELEM_TYPE_INPUT_MULTILINE: + element.Update(value) + elif element.Type == ELEM_TYPE_INPUT_SPIN: + element.Update(value) + elif element.Type == ELEM_TYPE_BUTTON: + element.Update(value) + + +def _FindElementFromKeyInSubForm(form, key): + for row_num, row in enumerate(form.Rows): + for col_num, element in enumerate(row): + if element.Type == ELEM_TYPE_COLUMN: + matching_elem = _FindElementFromKeyInSubForm(element, key) + if matching_elem is not None: + return matching_elem + if element.Type == ELEM_TYPE_FRAME: + matching_elem = _FindElementFromKeyInSubForm(element, key) + if matching_elem is not None: + return matching_elem + if element.Type == ELEM_TYPE_TAB_GROUP: + matching_elem = _FindElementFromKeyInSubForm(element, key) + if matching_elem is not None: + return matching_elem + if element.Type == ELEM_TYPE_TAB: + matching_elem = _FindElementFromKeyInSubForm(element, key) + if matching_elem is not None: + return matching_elem + if element.Key == key: + return element + + +def _FindElementWithFocusInSubForm(form): + 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 + if element.Type == ELEM_TYPE_FRAME: + matching_elem = _FindElementWithFocusInSubForm(element) + if matching_elem is not None: + return matching_elem + if element.Type == ELEM_TYPE_TAB_GROUP: + matching_elem = _FindElementWithFocusInSubForm(element) + if matching_elem is not None: + return matching_elem + if element.Type == ELEM_TYPE_TAB: + matching_elem = _FindElementWithFocusInSubForm(element) + if matching_elem is not None: + return matching_elem + if element.Type == ELEM_TYPE_INPUT_TEXT: + if element.TKEntry is not None: + if element.TKEntry is element.TKEntry.focus_get(): + return element + if element.Type == ELEM_TYPE_INPUT_MULTILINE: + if element.TKText is not None: + if element.TKText is element.TKText.focus_get(): + return element + + +def AddMenuItem(top_menu, sub_menu_info, element, is_sub_menu=False, skip=False): + # m3 = gui.MenuItem('Dialog', width=100, height=30) + # m3.onclick.connect(self.menu_dialog_clicked) + # menu.append([m1, m2, m3]) + + return_val = None + if type(sub_menu_info) is str: + if not is_sub_menu and not skip: + # print(f'Adding command {sub_menu_info}') + pos = sub_menu_info.find('&') + if pos != -1: + if pos == 0 or sub_menu_info[pos - 1] != "\\": + sub_menu_info = sub_menu_info[:pos] + sub_menu_info[pos + 1:] + if sub_menu_info == '---': + # top_menu.add('separator') + pass + 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: + menu_item = remi.gui.MenuItem(item_without_key[1:], width=100, height=30) + menu_item.set_enabled(False) + top_menu.append([menu_item,]) + + # TODO add callback here! + # TODO disable entry + else: + menu_item = remi.gui.MenuItem(item_without_key, width=100, height=30) + top_menu.append([menu_item,]) + # menu_item.set_on_click_listener(element._ChangedCallbackMenu, sub_menu_info) + menu_item.onclick.connect(element._ChangedCallbackMenu, 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: + pos = sub_menu_info[i].find('&') + if pos != -1: + if pos == 0 or sub_menu_info[i][pos - 1] != "\\": + sub_menu_info[i] = sub_menu_info[i][:pos] + sub_menu_info[i][pos + 1:] + if sub_menu_info[i][0] == MENU_DISABLED_CHARACTER: + new_menu = remi.gui.MenuItem(sub_menu_info[i][len(MENU_DISABLED_CHARACTER):], width=100, height=30) + new_menu.set_enabled(False) + + # TODO Disable Entry + else: + new_menu = remi.gui.MenuItem(sub_menu_info[i], width=100, height=30) + + top_menu.append([new_menu,]) + return_val = new_menu + AddMenuItem(new_menu, sub_menu_info[i + 1], element, is_sub_menu=True) + i += 1 # skip the next one + else: + AddMenuItem(top_menu, item, element) + else: + AddMenuItem(top_menu, item, element) + i += 1 + return return_val + +""" + ::::::::: :::::::::: ::: ::: ::::::::::: + :+: :+: :+: :+:+: :+:+: :+: + +:+ +:+ +:+ +:+ +:+:+ +:+ +:+ + +#++:++#: +#++:++# +#+ +:+ +#+ +#+ + +#+ +#+ +#+ +#+ +#+ +#+ + #+# #+# #+# #+# #+# #+# + ### ### ########## ### ### ########### +""" +# ------------------------------------------------------------------------------------------------------------ # +# ===================================== REMI CODE STARTS HERE ================================================ # +# ------------------------------------------------------------------------------------------------------------ # + + + + +def PackFormIntoFrame(form, containing_frame, toplevel_form): + def CharWidthInPixels(): + return tkinter.font.Font().measure('A') # single character width + + def pad_widget(widget): + lrsizer = wx.BoxSizer(wx.HORIZONTAL) + if full_element_pad[1] == full_element_pad[3]: # if right = left + lrsizer.Add(widget, 0, wx.LEFT | wx.RIGHT, border=full_element_pad[1]) + else: + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(widget, 0, wx.LEFT, border=full_element_pad[3]) + lrsizer.Add(sizer, 0, wx.RIGHT, border=full_element_pad[1]) + + top_bottom_sizer = wx.BoxSizer(wx.HORIZONTAL) + if full_element_pad[0] == full_element_pad[2]: # if top = bottom + top_bottom_sizer.Add(lrsizer, 0, wx.TOP | wx.BOTTOM, border=full_element_pad[0]) + else: + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(lrsizer, 0, wx.TOP, border=full_element_pad[0]) + top_bottom_sizer.Add(sizer, 0, wx.BOTTOM, border=full_element_pad[2]) + return top_bottom_sizer + + # + # font, text color, background color, size, disabled, visible, tooltip + # + def do_font_and_color(widget): + font_info = font_parse_string(font) # family, point size, other + widget.style['font-family'] = font_info[0] + if element.BackgroundColor not in (None, COLOR_SYSTEM_DEFAULT): + widget.style['background-color'] = element.BackgroundColor + if element.TextColor not in (None, COLOR_SYSTEM_DEFAULT): + widget.style['color'] = element.TextColor + widget.style['font-size'] = '{}px'.format(font_info[1]) + if element_size[0]: # if size is zero, don't set any sizes + size = convert_tkinter_size_to_Wx(element_size) + widget.style['height'] = '{}px'.format(size[1]) + widget.style['width'] = '{}px'.format(size[0]) + widget.style['margin'] = '{}px {}px {}px {}px'.format(*full_element_pad) + if element.Disabled: + widget.set_enabled(False) + if not element.Visible: + widget.attributes['hidden'] = 'true' + if element.Tooltip is not None: + widget.attributes['title'] = element.Tooltip + + border_depth = toplevel_form.BorderDepth if toplevel_form.BorderDepth is not None else DEFAULT_BORDER_WIDTH + # --------------------------------------------------------------------------- # + # **************** Use FlexForm to build the tkinter window ********** ----- # + # Building is done row by row. # + # --------------------------------------------------------------------------- # + focus_set = False + ######################### 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 = remi.gui.HBox() + tk_row_frame.style['align-items'] = 'flex-start' + if form.ElementJustification.startswith('c'): + tk_row_frame.style['margin-left'] = 'auto' + tk_row_frame.style['margin-right'] = 'auto' + # tk_row_frame.style['justify-content'] = 'center' + elif form.ElementJustification.startswith('r'): + # tk_row_frame.style['justify-content'] = 'flex-end' + tk_row_frame.style['margin-left'] = 'auto' + else: # everything else is left justified + # tk_row_frame.style['justify-content'] = 'flex-flexstart' + tk_row_frame.style['margin-right'] = 'auto' + + if form.BackgroundColor not in(None, COLOR_SYSTEM_DEFAULT): + tk_row_frame.style['background-color'] = form.BackgroundColor + + for col_num, element in enumerate(flex_row): + element.ParentForm = toplevel_form # save the button's parent form object + if toplevel_form.Font and (element.Font == DEFAULT_FONT or not element.Font): + 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 + # Determine Element size + element_size = element.Size + if (element_size == (None, None) and element_type != ELEM_TYPE_BUTTON): # user did not specify a size + element_size = toplevel_form.DefaultElementSize + elif (element_size == (None, None) and element_type == ELEM_TYPE_BUTTON): + element_size = toplevel_form.DefaultButtonElementSize + else: + auto_size_text = False # if user has specified a size then it shouldn't autosize + + full_element_pad = [0, 0, 0, 0] # Top, Right, Bottom, Left + elementpad = element.Pad if element.Pad is not None else toplevel_form.ElementPadding + if type(elementpad[0]) != tuple: # left and right + full_element_pad[1] = full_element_pad[3] = elementpad[0] + else: + full_element_pad[3], full_element_pad[1] = elementpad[0] + if type(elementpad[1]) != tuple: # top and bottom + full_element_pad[0] = full_element_pad[2] = elementpad[1] + else: + full_element_pad[0], full_element_pad[2] = elementpad[1] + + # ------------------------- COLUMN element ------------------------- # + if element_type == ELEM_TYPE_COLUMN: + element = element # type: Column + element.Widget = column_widget = remi.gui.VBox() + if element.BackgroundColor not in (None, COLOR_SYSTEM_DEFAULT): + column_widget.style['background-color'] = element.BackgroundColor + PackFormIntoFrame(element, column_widget, toplevel_form) + tk_row_frame.append(element.Widget) + + # ------------------------- TEXT element ------------------------- # + elif element_type == ELEM_TYPE_TEXT: + element = element # type: Text + element.Widget = remi.gui.Label(element.DisplayText) + do_font_and_color(element.Widget) + if auto_size_text and element.Size == (None, None): + del(element.Widget.style['width']) + if element.Justification: + if element.Justification.startswith('c'): + element.Widget.style['text-align'] = 'center' + elif element.Justification.startswith('r'): + element.Widget.style['text-align'] = 'right' + if element.ClickSubmits: + element.Widget.onclick.connect(element._ChangedCallback) + tk_row_frame.append(element.Widget) + + # ------------------------- BUTTON element ------------------------- # + elif element_type == ELEM_TYPE_BUTTON: + element = element # type: Button + size = convert_tkinter_size_to_Wx(element_size) + element.Widget = remi.gui.Button(element.ButtonText, width=size[0], height=size[1], margin='10px') + element.Widget.onclick.connect(element._ButtonCallBack) + do_font_and_color(element.Widget) + if element.AutoSizeButton or (toplevel_form.AutoSizeButtons and element.AutoSizeButton is not False) and element.Size == (None, None): + del (element.Widget.style['width']) + if element.ImageFilename: + element.ImageWidget = SuperImage(element.ImageFilename if element.ImageFilename is not None else element.ImageData) + element.Widget.append(element.ImageWidget) + tk_row_frame.append(element.Widget) + + # 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 + # border_depth = element.BorderWidth + # if btype != BUTTON_TYPE_REALTIME: + # tkbutton = tk.Button(tk_row_frame, text=btext, width=width, height=height, + # command=element.ButtonCallBack, justify=tk.LEFT, bd=border_depth, font=font) + # else: + # tkbutton = tk.Button(tk_row_frame, text=btext, width=width, height=height, justify=tk.LEFT, + # bd=border_depth, font=font) + # tkbutton.bind('', element.ButtonReleaseCallBack) + # tkbutton.bind('', element.ButtonPressCallBack) + # if bc != (None, None) and bc != COLOR_SYSTEM_DEFAULT and bc[1] != COLOR_SYSTEM_DEFAULT: + # tkbutton.config(foreground=bc[0], background=bc[1], activebackground=bc[1]) + # elif bc[1] == COLOR_SYSTEM_DEFAULT: + # tkbutton.config(foreground=bc[0]) + # + # 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 + # tkbutton.config(highlightthickness=0) + # 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 + # tkbutton.config(highlightthickness=0) + # 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 + # tkbutton.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1]) + # if element.BindReturnKey: + # element.TKButton.bind('', element.ReturnKeyHandler) + # if element.Focus is True or (toplevel_form.UseDefaultFocus and not focus_set): + # focus_set = True + # element.TKButton.bind('', element.ReturnKeyHandler) + # element.TKButton.focus_set() + # toplevel_form.TKroot.focus_force() + # 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 element ------------------------- # + elif element_type == ELEM_TYPE_INPUT_TEXT: + element = element # type: InputText + element.Widget = InputText.TextInput_raw_onkeyup(hint=element.DefaultText) + # element.Widget = remi.gui.TextInput(hint=element.DefaultText) + do_font_and_color(element.Widget) + if element.ChangeSubmits: + element.Widget.onkeyup.connect(element._InputTextCallback) + # element.Widget.onkeydown.connect(element._InputTextCallback) + tk_row_frame.append(element.Widget) + + # show = element.PasswordCharacter if element.PasswordCharacter else "" + # if element.Justification is not None: + # justification = element.Justification + # else: + # justification = DEFAULT_TEXT_JUSTIFICATION + # justify = tk.LEFT if justification == 'left' else tk.CENTER if justification == 'center' else tk.RIGHT + # # anchor = tk.NW if justification == 'left' else tk.N if justification == 'center' else tk.NE + # element.TKEntry = tk.Entry(tk_row_frame, width=element_size[0], textvariable=element.TKStringVar, + # bd=border_depth, font=font, show=show, justify=justify) + # if element.ChangeSubmits: + # element.TKEntry.bind('', element.KeyboardHandler) + # element.TKEntry.bind('', 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) + # element.TKEntry.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1], expand=True, fill='x') + # if element.Focus is True or (toplevel_form.UseDefaultFocus and not focus_set): + # focus_set = True + # element.TKEntry.focus_set() + # if element.Disabled: + # element.TKEntry['state'] = 'disabled' + # if element.Tooltip is not None: + # element.TooltipObject = ToolTip(element.TKEntry, text=element.Tooltip, timeout=DEFAULT_TOOLTIP_TIME) + # ------------------------- COMBO element ------------------------- # + elif element_type == ELEM_TYPE_INPUT_COMBO: + element = element # type: Combo + element.Widget = remi.gui.DropDown.new_from_list(element.Values) + if element.DefaultValue is not None: + element.Widget.select_by_value(element.DefaultValue) + do_font_and_color(element.Widget) + if element.ChangeSubmits: + element.Widget.onchange.connect(element._ChangedCallback) + tk_row_frame.append(element.Widget) + + # ------------------------- OPTION MENU (Like ComboBox but different) element ------------------------- # + elif element_type == ELEM_TYPE_INPUT_OPTION_MENU: + element.Widget = remi.gui.FileUploader('./', width=200, height=30, margin='10px') + + # element.Widget = remi.gui.FileFolderNavigator(False, r'a:\TEMP', True, False) + tk_row_frame.append(element.Widget) + pass + # ------------------------- LISTBOX element ------------------------- # + elif element_type == ELEM_TYPE_INPUT_LISTBOX: + element = element # type: Listbox + element.Widget = remi.gui.ListView.new_from_list(element.Values) + do_font_and_color(element.Widget) + if element.ChangeSubmits: + element.Widget.onselection.connect(element._ChangedCallback) + tk_row_frame.append(element.Widget) + # max_line_len = max([len(str(l)) for l in element.Values]) if len(element.Values) != 0 else 0 + # if auto_size_text is False: + # width = element_size[0] + # else: + # width = max_line_len + # listbox_frame = tk.Frame(tk_row_frame) + # element.TKStringVar = tk.StringVar() + # element.TKListbox = tk.Listbox(listbox_frame, height=element_size[1], width=width, + # selectmode=element.SelectMode, font=font) + # 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 text_color is not None and text_color != COLOR_SYSTEM_DEFAULT: + # element.TKListbox.configure(fg=text_color) + # if element.ChangeSubmits: + # element.TKListbox.bind('<>', element.ListboxSelectHandler) + # vsb = tk.Scrollbar(listbox_frame, orient="vertical", command=element.TKListbox.yview) + # element.TKListbox.configure(yscrollcommand=vsb.set) + # element.TKListbox.pack(side=tk.LEFT) + # vsb.pack(side=tk.LEFT, fill='y') + # listbox_frame.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1]) + # if element.BindReturnKey: + # element.TKListbox.bind('', element.ListboxSelectHandler) + # element.TKListbox.bind('', element.ListboxSelectHandler) + # if element.Disabled == True: + # element.TKListbox['state'] = 'disabled' + # if element.Tooltip is not None: + # element.TooltipObject = ToolTip(element.TKListbox, text=element.Tooltip, + # timeout=DEFAULT_TOOLTIP_TIME) + # ------------------------- INPUT MULTILINE element ------------------------- # + elif element_type == ELEM_TYPE_INPUT_MULTILINE: + element = element # type: Multiline + element.Widget = remi.gui.TextInput(single_line=False, hint=element.DefaultText) + do_font_and_color(element.Widget) + if element.ChangeSubmits: + element.Widget.onkeydown.connect(element._InputTextCallback) + tk_row_frame.append(element.Widget) + # default_text = element.DefaultText + # width, height = element_size + # element.TKText = tk.scrolledtext.ScrolledText(tk_row_frame, width=width, height=height, wrap='word', + # bd=border_depth, font=font) + # element.TKText.insert(1.0, default_text) # set the default text + # if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT: + # element.TKText.configure(background=element.BackgroundColor) + # element.TKText.vbar.config(troughcolor=DEFAULT_SCROLLBAR_COLOR) + # element.TKText.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1], expand=True, fill='both') + # if element.ChangeSubmits: + # element.TKText.bind('', element.KeyboardHandler) + # if element.EnterSubmits: + # element.TKText.bind('', element.ReturnKeyHandler) + # if element.Focus is True or (toplevel_form.UseDefaultFocus and not focus_set): + # focus_set = 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 == True: + # element.TKText['state'] = 'disabled' + # if element.Tooltip is not None: + # element.TooltipObject = ToolTip(element.TKText, text=element.Tooltip, timeout=DEFAULT_TOOLTIP_TIME) + + # ------------------------- INPUT CHECKBOX element ------------------------- # + elif element_type == ELEM_TYPE_INPUT_CHECKBOX: + element = element # type: Checkbox + element.Widget = remi.gui.CheckBoxLabel(element.Text) + if element.InitialState: + element.Widget.set_value(element.InitialState) + if element.ChangeSubmits: + element.Widget.onchange.connect(element._ChangedCallback) + do_font_and_color(element.Widget) + tk_row_frame.append(element.Widget) + + # 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 = 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 = tk.Checkbutton(tk_row_frame, anchor=tk.NW, text=element.Text, width=width, + # variable=element.TKIntVar, bd=border_depth, font=font) + # if default_value is None or 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.BackgroundColor) + # 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.TKCheckbutton.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1]) + # if element.Tooltip is not None: + # element.TooltipObject = ToolTip(element.TKCheckbutton, text=element.Tooltip, + # timeout=DEFAULT_TOOLTIP_TIME) + # # ------------------------- PROGRESS BAR element ------------------------- # + elif element_type == ELEM_TYPE_PROGRESS_BAR: + pass + # # save this form because it must be 'updated' (refreshed) solely for the purpose of updating bar + # 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 + # element.TKProgressBar = TKProgressBar(tk_row_frame, element.MaxValue, progress_length, progress_width, + # orientation=direction, BarColor=bar_color, + # border_width=element.BorderWidth, relief=element.Relief, + # style=element.BarStyle, key=element.Key) + # element.TKProgressBar.TKProgressBarForReal.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1]) + # ------------------------- INPUT RADIO BUTTON element ------------------------- # + elif element_type == ELEM_TYPE_INPUT_RADIO: + pass + # 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(row_num, col_num) # value to set intvar to if this radio is selected + # 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 = 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 = 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.BackgroundColor) + # if text_color is not None and text_color != COLOR_SYSTEM_DEFAULT: + # element.TKRadio.configure(fg=text_color) + # if element.Disabled: + # element.TKRadio['state'] = 'disabled' + # element.TKRadio.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1]) + # if element.Tooltip is not None: + # element.TooltipObject = ToolTip(element.TKRadio, text=element.Tooltip, timeout=DEFAULT_TOOLTIP_TIME) + # ------------------------- INPUT SPIN element ------------------------- # + elif element_type == ELEM_TYPE_INPUT_SPIN: + element = element # type: Spin + element.Widget = remi.gui.SpinBox(50, 0, 100) + if element.DefaultValue is not None: + element.Widget.set_value(element.DefaultValue) + do_font_and_color(element.Widget) + if element.ChangeSubmits: + element.Widget.onchange.connect(element._ChangedCallback) + tk_row_frame.append(element.Widget) + # width, height = element_size + # width = 0 if auto_size_text else element_size[0] + # element.TKStringVar = tk.StringVar() + # element.TKSpinBox = tk.Spinbox(tk_row_frame, values=element.Values, textvariable=element.TKStringVar, + # width=width, bd=border_depth) + # 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.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1]) + # if text_color is not None and text_color != COLOR_SYSTEM_DEFAULT: + # element.TKSpinBox.configure(fg=text_color) + # if element.ChangeSubmits: + # element.TKSpinBox.bind('', element.SpinChangedHandler) + # if element.Disabled == True: + # element.TKSpinBox['state'] = 'disabled' + # if element.Tooltip is not None: + # element.TooltipObject = ToolTip(element.TKSpinBox, text=element.Tooltip, + # timeout=DEFAULT_TOOLTIP_TIME) + # ------------------------- OUTPUT element ------------------------- # + elif element_type == ELEM_TYPE_OUTPUT: + element=element # type: Output + element.Widget = remi.gui.TextInput(single_line=False) + element.Disabled = True + do_font_and_color(element.Widget) + tk_row_frame.append(element.Widget) + toplevel_form.OutputElementForStdOut = element + Window.stdout_is_rerouted = True + Window.stdout_string_io = StringIO() + sys.stdout = Window.stdout_string_io + + # width, height = element_size + # element._TKOut = TKOutput(tk_row_frame, width=width, height=height, bd=border_depth, + # background_color=element.BackgroundColor, text_color=text_color, font=font, + # pad=element.Pad) + # element._TKOut.pack(side=tk.LEFT, expand=True, fill='both') + # if element.Tooltip is not None: + # element.TooltipObject = ToolTip(element._TKOut, text=element.Tooltip, timeout=DEFAULT_TOOLTIP_TIME) + # ------------------------- OUTPUT MULTILINE element ------------------------- # + elif element_type == ELEM_TYPE_MULTILINE_OUTPUT: + element = element # type: MultilineOutput + element.Widget = remi.gui.TextInput(single_line=False) + element.Disabled = True + do_font_and_color(element.Widget) + tk_row_frame.append(element.Widget) + if element.DefaultText: + element.Widget.set_value(element.DefaultText) + # ------------------------- IMAGE element ------------------------- # + elif element_type == ELEM_TYPE_IMAGE: + element = element # type: Image + # element.Widget = remi.gui.Image(element.Filename) + element.Widget = SuperImage(element.Filename if element.Filename is not None else element.Data) + if element.Filename is not None: + # print(f'loading image filename in pack frame {element.Filename}') + element.Widget.load(element.Filename) + do_font_and_color(element.Widget) + if element.EnableEvents: + element.Widget.onclick.connect(element._ChangedCallback) + tk_row_frame.append(element.Widget) + # 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 + # print('*ERROR laying out form.... Image Element has no image specified*') + # + # if photo is not None: + # if element_size == ( + # None, None) or element_size == None or element_size == toplevel_form.DefaultElementSize: + # width, height = photo.width(), photo.height() + # else: + # width, height = element_size + # if photo is not None: + # element.tktext_label = tk.Label(tk_row_frame, image=photo, width=width, height=height, + # bd=border_depth) + # else: + # element.tktext_label = tk.Label(tk_row_frame, width=width, height=height, bd=border_depth) + # if element.BackgroundColor is not None: + # element.tktext_label.config(background=element.BackgroundColor); + # + # element.tktext_label.image = photo + # # tktext_label.configure(anchor=tk.NW, image=photo) + # element.tktext_label.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1]) + # if element.Tooltip is not None: + # element.TooltipObject = ToolTip(element.tktext_label, text=element.Tooltip, + # timeout=DEFAULT_TOOLTIP_TIME) + # ------------------------- Canvas element ------------------------- # + elif element_type == ELEM_TYPE_CANVAS: + pass + # 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) + # element._TKCanvas.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1]) + # if element.Tooltip is not None: + # element.TooltipObject = ToolTip(element._TKCanvas, text=element.Tooltip, + # timeout=DEFAULT_TOOLTIP_TIME) + + # ------------------------- Graph element ------------------------- # + elif element_type == ELEM_TYPE_GRAPH: + element = element # type: Graph + element.Widget = remi.gui.Svg(width=element.CanvasSize[0], height=element.CanvasSize[1]) + element.SvgGroup = remi.gui.SvgSubcontainer(0,0, "100%", "100%") + element.Widget.append([element.SvgGroup,]) + do_font_and_color(element.Widget) + if element.ChangeSubmits: + element.Widget.onmouseup.connect(element._MouseUpCallback) + # element.Widget.onclick.connect(element.ClickCallback) + if element.DragSubmits: + element.Widget.onmousedown.connect(element._MouseDownCallback) + element.Widget.onmouseup.connect(element._MouseUpCallback) + element.Widget.onmousemove.connect(element._DragCallback) + + tk_row_frame.append(element.Widget) + # 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 + # element._TKCanvas2 = tk.Canvas(element._TKCanvas, width=width, height=height, bd=border_depth) + # element._TKCanvas2.pack(side=tk.LEFT) + # 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._TKCanvas.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1]) + # if element.Tooltip is not None: + # element.TooltipObject = ToolTip(element._TKCanvas, text=element.Tooltip, + # timeout=DEFAULT_TOOLTIP_TIME) + # if element.ChangeSubmits: + # element._TKCanvas2.bind('', element.ButtonReleaseCallBack) + # element._TKCanvas2.bind('', element.ButtonPressCallBack) + # if element.DragSubmits: + # element._TKCanvas2.bind('', element.MotionCallBack) + # ------------------------- MENUBAR element ------------------------- # + elif element_type == ELEM_TYPE_MENUBAR: + element = element # type: Menu + menu = remi.gui.Menu(width='100%', height=str(element_size[1])) + element_size = (0,0) # makes the menu span across the top + do_font_and_color(menu) + + menu_def = element.MenuDefinition + for menu_entry in menu_def: + # print(f'Adding a Menubar ENTRY {menu_entry}') + pos = menu_entry[0].find('&') + # print(pos) + if pos != -1: + if pos == 0 or menu_entry[0][pos - 1] != "\\": + menu_entry[0] = menu_entry[0][:pos] + menu_entry[0][pos + 1:] + if menu_entry[0][0] == MENU_DISABLED_CHARACTER: + item = remi.gui.MenuItem(menu_entry[0][1:], width=100, height=element_size[1]) + item.set_enabled(False) + else: + item = remi.gui.MenuItem(menu_entry[0], width=100, height=element_size[1]) + do_font_and_color(item) + menu.append([item,]) + if len(menu_entry) > 1: + AddMenuItem(item, menu_entry[1], element) + + element.Widget = menubar = remi.gui.MenuBar(width='100%', height='30px') + element.Widget.style['z-index'] = '1' + menubar.append(menu) + # tk_row_frame.append(element.Widget) + containing_frame.append(element.Widget) + + # ------------------------- Frame element ------------------------- # + elif element_type == ELEM_TYPE_FRAME: + element = element # type: Frame + # element.Widget = column_widget = remi.gui.VBox() + element.Widget = column_widget = CLASSframe(element.Title) + if element.BackgroundColor not in (None, COLOR_SYSTEM_DEFAULT): + column_widget.style['background-color'] = element.BackgroundColor + PackFormIntoFrame(element, column_widget, toplevel_form) + tk_row_frame.append(element.Widget) + + # + # element = element # type: Frame + # element.Widget = column_widget = remi.gui.VBox() + # if element.Justification.startswith('c'): + # column_widget.style['align-items'] = 'center' + # column_widget.style['justify-content'] = 'center' + # else: + # column_widget.style['justify-content'] = 'flex-start' + # column_widget.style['align-items'] = 'baseline' + # if element.BackgroundColor not in (None, COLOR_SYSTEM_DEFAULT): + # column_widget.style['background-color'] = element.BackgroundColor + # PackFormIntoFrame(element, column_widget, toplevel_form) + # tk_row_frame.append(element.Widget) + + # labeled_frame = tk.LabelFrame(tk_row_frame, text=element.Title, relief=element.Relief) + # PackFormIntoFrame(element, labeled_frame, toplevel_form) + # labeled_frame.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1]) + # 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) + # ------------------------- Tab element ------------------------- # + elif element_type == ELEM_TYPE_TAB: + element = element # type: Tab + element.Widget = remi.gui.VBox() + if element.Justification.startswith('c'): + # print('CENTERING') + element.Widget.style['align-items'] = 'center' + element.Widget.style['justify-content'] = 'center' + else: + element.Widget.style['justify-content'] = 'flex-start' + element.Widget.style['align-items'] = 'baseline' + if element.BackgroundColor not in (None, COLOR_SYSTEM_DEFAULT): + element.Widget.style['background-color'] = element.BackgroundColor + if element.BackgroundColor not in (None, COLOR_SYSTEM_DEFAULT): + element.Widget.style['background-color'] = element.BackgroundColor + PackFormIntoFrame(element, element.Widget, toplevel_form) + # tk_row_frame.append(element.Widget) + containing_frame.add_tab(element.Widget, element.Title, None) + + # element.TKFrame = tk.Frame(form.TKNotebook) + # PackFormIntoFrame(element, element.TKFrame, toplevel_form) + # if element.Disabled: + # form.TKNotebook.add(element.TKFrame, text=element.Title, state='disabled') + # else: + # form.TKNotebook.add(element.TKFrame, text=element.Title) + # form.TKNotebook.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1]) + # 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.TextColor != COLOR_SYSTEM_DEFAULT and element.TextColor is not None: + # # element.TKFrame.configure(foreground=element.TextColor) + # + # # ttk.Style().configure("TNotebook", background='red') + # # ttk.Style().map("TNotebook.Tab", background=[("selected", 'orange')], + # # foreground=[("selected", 'green')]) + # # ttk.Style().configure("TNotebook.Tab", background='blue', foreground='yellow') + # + # 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) + # ------------------------- TabGroup element ------------------------- # + elif element_type == ELEM_TYPE_TAB_GROUP: + element = element # type: TabGroup + element.Widget = remi.gui.TabBox() + # do_font_and_color(element.Widget) + PackFormIntoFrame(element ,element.Widget, toplevel_form) + tk_row_frame.append(element.Widget) + + # custom_style = str(element.Key) + 'customtab.TNotebook' + # style = ttk.Style(tk_row_frame) + # if element.Theme is not None: + # style.theme_use(element.Theme) + # 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, foreground='purple') + # + # # style.theme_create("yummy", parent="alt", settings={ + # # "TNotebook": {"configure": {"tabmargins": [2, 5, 2, 0]}}, + # # "TNotebook.Tab": { + # # "configure": {"padding": [5, 1], "background": mygreen}, + # # "map": {"background": [("selected", myred)], + # # "expand": [("selected", [1, 1, 1, 0])]}}}) + # + # # style.configure(custom_style+'.Tab', background='red') + # if element.SelectedTitleColor != None: + # style.map(custom_style + '.Tab', foreground=[("selected", element.SelectedTitleColor)]) + # if element.TextColor is not None and element.TextColor != COLOR_SYSTEM_DEFAULT: + # style.configure(custom_style + '.Tab', foreground=element.TextColor) + # # style.configure(custom_style, background='blue', foreground='yellow') + # + # element.TKNotebook = ttk.Notebook(tk_row_frame, style=custom_style) + # + # PackFormIntoFrame(element, toplevel_form.TKroot, toplevel_form) + # + # if element.ChangeSubmits: + # element.TKNotebook.bind('<>', element.TabGroupSelectHandler) + # if element.BorderWidth is not None: + # element.TKNotebook.configure(borderwidth=element.BorderWidth) + # if element.Tooltip is not None: + # element.TooltipObject = ToolTip(element.TKNotebook, text=element.Tooltip, + # timeout=DEFAULT_TOOLTIP_TIME) + # ------------------------- SLIDER element ------------------------- # + elif element_type == ELEM_TYPE_INPUT_SLIDER: + element = element # type: Slider + orient = remi.gui.Container.LAYOUT_HORIZONTAL if element.Orientation.lower().startswith('h') else remi.gui.Container.LAYOUT_VERTICAL + # print(f'slider orient = {orient}') + element.Widget = remi.gui.Slider(layout_orientation=orient, default_value=element.DefaultValue, min=element.Range[0], max=element.Range[1],step=element.Resolution) + if element.DefaultValue: + element.Widget.set_value(element.DefaultValue) + # if element.Orientation.startswith('v'): + # element.Container.LAYOUT_orientation = remi.gui.Container.LAYOUT_VERTICAL + do_font_and_color(element.Widget) + if element.ChangeSubmits: + element.Widget.onchange.connect(element._SliderCallback) + element.Widget.style['orientation'] = 'vertical' + element.Widget.attributes['orientation'] = 'vertical' + # print(f'slider = {element.Widget.style, element.Widget.attributes}') + tk_row_frame.append(element.Widget) # slider_length = element_size[0] * CharWidthInPixels() + + # ------------------------- TABLE element ------------------------- # + elif element_type == ELEM_TYPE_TABLE: + element = element # type: Table + new_table = [] + for row_num, row in enumerate(element.Values): # convert entire table to strings + new_row= [str(item) for item in row] + if element.DisplayRowNumbers: + new_row = [element.RowHeaderText if row_num == 0 else str(row_num+element.StartingRowNumber) ,] + new_row + new_table.append(new_row) + element.Widget = remi.gui.Table.new_from_list(new_table) + do_font_and_color(element.Widget) + tk_row_frame.append(element.Widget) + element.Widget.on_table_row_click.connect(element._on_table_row_click) + # frame = tk.Frame(tk_row_frame) + # + # height = element.NumRows + # if element.Justification == 'left': + # anchor = tk.W + # elif element.Justification == 'right': + # anchor = tk.E + # else: + # anchor = tk.CENTER + # column_widths = {} + # 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: + # displaycolumns.append(element.ColumnHeadings[i]) + # column_headings = element.ColumnHeadings + # if element.DisplayRowNumbers: # if display row number, tack on the numbers to front of columns + # displaycolumns = [element.RowHeaderText, ] + displaycolumns + # column_headings = [element.RowHeaderText, ] + element.ColumnHeadings + # element.TKTreeview = 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 + # treeview.column(element.RowHeaderText, width=50, anchor=anchor) + # + # 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: + # width = max(column_widths[i], len(heading)) + # else: + # try: + # width = element.ColumnWidths[i] + # except: + # width = element.DefaultColumnWidth + # treeview.column(heading, width=width * CharWidthInPixels(), anchor=anchor) + # + # # 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) + # 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]) + # + # if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT: + # ttk.Style().configure("Treeview", background=element.BackgroundColor, + # fieldbackground=element.BackgroundColor) + # if element.TextColor is not None and element.TextColor != COLOR_SYSTEM_DEFAULT: + # ttk.Style().configure("Treeview", foreground=element.TextColor) + # if element.RowHeight is not None: + # ttk.Style().configure("Treeview", rowheight=element.RowHeight) + # ttk.Style().configure("Treeview", font=font) + # # scrollable_frame.pack(side=tk.LEFT, padx=elementpad[0], pady=elementpad[1], expand=True, fill='both') + # treeview.bind("<>", element.treeview_selected) + # if element.BindReturnKey: + # treeview.bind('', element.treeview_double_click) + # treeview.bind('', element.treeview_double_click) + # + # scrollbar = tk.Scrollbar(frame) + # scrollbar.pack(side=tk.RIGHT, fill='y') + # scrollbar.config(command=treeview.yview) + # + # 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) + # + # treeview.configure(yscrollcommand=scrollbar.set) + # + # element.TKTreeview.pack(side=tk.LEFT, expand=True, padx=0, pady=0, fill='both') + # if element.Visible is False: + # element.TKTreeview.pack_forget() + # frame.pack(side=tk.LEFT, expand=True, padx=0, pady=0) + # if element.Tooltip is not None: + # element.TooltipObject = ToolTip(element.TKTreeview, text=element.Tooltip, + # timeout=DEFAULT_TOOLTIP_TIME) + # if element.RightClickMenu or toplevel_form.RightClickMenu: + # menu = element.RightClickMenu or toplevel_form.RightClickMenu + # top_menu = tk.Menu(toplevel_form.TKroot, tearoff=False) + # AddMenuItem(top_menu, menu[1], element) + # element.TKRightClickMenu = top_menu + # element.TKTreeview.bind('', element.RightClickMenuCallback) + pass + # frame = tk.Frame(tk_row_frame) + # + # height = element.NumRows + # if element.Justification == 'left': + # anchor = tk.W + # elif element.Justification == 'right': + # anchor = tk.E + # else: + # anchor = tk.CENTER + # column_widths = {} + # 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 + # else: + # displaycolumns = [] + # for i, should_display in enumerate(element.ColumnsToDisplay): + # if should_display: + # displaycolumns.append(element.ColumnHeadings[i]) + # column_headings = element.ColumnHeadings + # if element.DisplayRowNumbers: # if display row number, tack on the numbers to front of columns + # displaycolumns = [element.RowHeaderText, ] + displaycolumns + # column_headings = [element.RowHeaderText, ] + element.ColumnHeadings + # element.TKTreeview = 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 + # treeview.column(element.RowHeaderText, width=50, anchor=anchor) + # for i, heading in enumerate(element.ColumnHeadings): + # treeview.heading(heading, text=heading) + # if element.AutoSizeColumns: + # width = max(column_widths[i], len(heading)) + # else: + # try: + # width = element.ColumnWidths[i] + # except: + # width = element.DefaultColumnWidth + # + # treeview.column(heading, width=width * CharWidthInPixels(), anchor=anchor) + # # 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 % 2) + # if element.AlternatingRowColor is not None: + # treeview.tag_configure(1, background=element.AlternatingRowColor) + # if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT: + # ttk.Style().configure("Treeview", background=element.BackgroundColor, + # fieldbackground=element.BackgroundColor) + # if element.TextColor is not None and element.TextColor != COLOR_SYSTEM_DEFAULT: + # ttk.Style().configure("Treeview", foreground=element.TextColor) + # # scrollable_frame.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1], expand=True, fill='both') + # treeview.bind("<>", element.treeview_selected) + # if element.BindReturnKey: + # treeview.bind('', element.treeview_double_click) + # treeview.bind('', element.treeview_double_click) + # scrollbar = tk.Scrollbar(frame) + # scrollbar.pack(side=tk.RIGHT, fill='y') + # scrollbar.config(command=treeview.yview) + # treeview.configure(yscrollcommand=scrollbar.set) + # + # element.TKTreeview.pack(side=tk.LEFT, expand=True, padx=0, pady=0, fill='both') + # frame.pack(side=tk.LEFT, expand=True, padx=0, pady=0) + # if element.Tooltip is not None: + # element.TooltipObject = ToolTip(element.TKTreeview, text=element.Tooltip, + # timeout=DEFAULT_TOOLTIP_TIME) + # ------------------------- Tree element ------------------------- # + elif element_type == ELEM_TYPE_TREE: + pass + # frame = tk.Frame(tk_row_frame) + # + # height = element.NumRows + # if element.Justification == 'left': # justification + # anchor = tk.W + # elif element.Justification == 'right': + # 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 = ttk.Treeview(frame, columns=column_headings, + # displaycolumns=displaycolumns, show='tree headings', height=height, + # selectmode=element.SelectMode, ) + # treeview = element.TKTreeview + # for i, heading in enumerate(element.ColumnHeadings): # Configure cols + headings + # treeview.heading(heading, text=heading) + # if element.AutoSizeColumns: + # width = min(element.MaxColumnWidth, len(heading) + 1) + # else: + # try: + # width = element.ColumnWidths[i] + # except: + # width = element.DefaultColumnWidth + # treeview.column(heading, width=width * CharWidthInPixels(), anchor=anchor) + # + # def add_treeview_data(node): + # # print(f'Inserting {node.key} under parent {node.parent}') + # if node.key != '': + # treeview.insert(node.parent, 'end', node.key, text=node.text, values=node.values, + # open=element.ShowExpanded) + # for node in node.children: + # add_treeview_data(node) + # + # add_treeview_data(element.TreeData.root_node) + # treeview.column('#0', width=element.Col0Width * CharWidthInPixels(), anchor=anchor) + # # ----- configure colors ----- + # if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT: + # ttk.Style().configure("Treeview", background=element.BackgroundColor, + # fieldbackground=element.BackgroundColor) + # if element.TextColor is not None and element.TextColor != COLOR_SYSTEM_DEFAULT: + # ttk.Style().configure("Treeview", foreground=element.TextColor) + # + # scrollbar = tk.Scrollbar(frame) + # scrollbar.pack(side=tk.RIGHT, fill='y') + # scrollbar.config(command=treeview.yview) + # treeview.configure(yscrollcommand=scrollbar.set) + # element.TKTreeview.pack(side=tk.LEFT, expand=True, padx=0, pady=0, fill='both') + # frame.pack(side=tk.LEFT, expand=True, padx=0, pady=0) + # treeview.bind("<>", element.treeview_selected) + # if element.Tooltip is not None: # tooltip + # element.TooltipObject = ToolTip(element.TKTreeview, text=element.Tooltip, + # timeout=DEFAULT_TOOLTIP_TIME) + # ------------------------- Separator element ------------------------- # + elif element_type == ELEM_TYPE_SEPARATOR: + pass + # separator = ttk.Separator(tk_row_frame, orient=element.Orientation, ) + # separator.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1], fill='both', expand=True) + # + # # ............................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]) + # tk_row_frame.pack(side=tk.TOP, anchor='nw', padx=DEFAULT_MARGINS[0], expand=False) + # if form.BackgroundColor is not None and form.BackgroundColor != COLOR_SYSTEM_DEFAULT: + # tk_row_frame.configure(background=form.BackgroundColor) + # toplevel_form.TKroot.configure(padx=DEFAULT_MARGINS[0], pady=DEFAULT_MARGINS[1]) + if not type(containing_frame) == remi.gui.TabBox: + containing_frame.append(tk_row_frame) + return + + +def setup_remi_window(app:Window.MyApp, window:Window): + master_widget = remi.gui.VBox() + master_widget.style['justify-content'] = 'flex-start' + master_widget.style['align-items'] = 'baseline' + if window.BackgroundColor not in (None, COLOR_SYSTEM_DEFAULT): + master_widget.style['background-color'] = window.BackgroundColor + try: + PackFormIntoFrame(window, master_widget, window) + except: + print('* ERROR PACKING FORM *') + print(traceback.format_exc()) + + + if window.BackgroundImage: + master_widget.style['background-image'] = "url('{}')".format('/' + window.BackgroundImage) + # print(f'background info',self.master_widget.attributes['background-image'] ) + + if not window.DisableClose: + # add the following 3 lines to your app and the on_window_close method to make the console close automatically + tag = remi.gui.Tag(_type='script') + tag.add_child("javascript", """window.onunload=function(e){sendCallback('%s','%s');return "close?";};""" % ( + str(id(app)), "on_window_close")) + master_widget.add_child("onunloadevent", tag) + + if window.ReturnKeyboardEvents: + app.page.children['body'].onkeyup.connect(window.on_key_up) + if window.ReturnKeyDownEvents: + app.page.children['body'].onkeydown.connect(window.on_key_down) + + + # if window.WindowIcon: + # if type(window.WindowIcon) is bytes or len(window.WindowIcon) > 200: + # app.page.children['head'].set_icon_data( base64_data=str(window.WindowIcon), mimetype="image/gif" ) + # else: + # app.page.children['head'].set_icon_file("/res:{}".format(window.WindowIcon)) + # pass + # mimetype, encoding = mimetypes.guess_type(image_source) + # with open(image_source, 'rb') as f: + # data = f.read() + # b64 = base64.b64encode(data) + # b64_str = b64.decode("utf-8") + # image_string = "data:image/svg;base64,%s"%b64_str + # rpoint.set_image(image_string) + + + return master_widget + +# ----====----====----====----====----==== STARTUP TK ====----====----====----====----====----# +def StartupTK(window:Window): + global _my_windows + + + # print('Starting TK open Windows = {}'.format(ow)) + + _my_windows.Increment() + + # if not my_flex_form.Resizable: + # root.resizable(False, False) + + # if my_flex_form.KeepOnTop: + # root.wm_attributes("-topmost", 1) + # master = window.TKroot + # Set Title + # master.title(MyFlexForm.Title) + # master = 00000 + + InitializeResults(window) + + # Does all of the window setup, starting up Remi + # if no windows exist, start Remi thread which will call same setup_remi_window call as shown below + if len(Window.active_windows) == 0: + window.thread_id = threading.Thread(target=window.remi_thread, daemon=True) + window.thread_id.daemon = True + window.thread_id.start() + item = window.MessageQueue.get() # Get the layout complete message + Window.active_windows.append(window) + Window.App = window.App + else: + # print('Starting second page') + # margin 0px auto allows to center the app to the screen + # master_widget = remi.gui.VBox() + # master_widget.style['justify-content'] = 'flex-start' + # master_widget.style['align-items'] = 'baseline' + # if window.BackgroundColor not in (None, COLOR_SYSTEM_DEFAULT): + # master_widget.style['background-color'] = window.BackgroundColor + # PackFormIntoFrame(window, master_widget, window) + master_widget = setup_remi_window(Window.App, window) + window.master_widget = master_widget + Window.active_windows.append(window) + Window.App.set_root_widget(master_widget) + + return + +# ==============================_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 ConvertArgsToSingleString(*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 + + +# ============================== ProgressMeter =====# +# ===================================================# +def _ProgressMeter(title, max_value, *args, orientation=None, bar_color=(None, None), button_color=None, + size=DEFAULT_PROGRESS_BAR_SIZE, border_width=None, grab_anywhere=False): + ''' + Create and show a form on tbe caller's behalf. + :param title: + :param max_value: + :param args: ANY number of arguments the caller wants to display + :param orientation: + :param bar_color: + :param size: + :param Style: + :param StyleOffset: + :return: ProgressBar object that is in the form + ''' + local_orientation = DEFAULT_METER_ORIENTATION if orientation is None else orientation + local_border_width = DEFAULT_PROGRESS_BAR_BORDER_WIDTH if border_width is None else border_width + bar2 = ProgressBar(max_value, orientation=local_orientation, size=size, bar_color=bar_color, + border_width=local_border_width, relief=DEFAULT_PROGRESS_BAR_RELIEF) + form = Window(title, auto_size_text=True, grab_anywhere=grab_anywhere) + + # Form using a horizontal bar + if local_orientation[0].lower() == 'h': + single_line_message, width, height = ConvertArgsToSingleString(*args) + bar2.TextToDisplay = single_line_message + bar2.TextToDisplay = single_line_message + bar2.MaxValue = max_value + bar2.CurrentValue = 0 + bar_text = Text(single_line_message, size=(width, height + 3), auto_size_text=True) + form.AddRow(bar_text) + form.AddRow((bar2)) + form.AddRow((CloseButton('Cancel', button_color=button_color))) + else: + single_line_message, width, height = ConvertArgsToSingleString(*args) + bar2.TextToDisplay = single_line_message + bar2.MaxValue = max_value + bar2.CurrentValue = 0 + bar_text = Text(single_line_message, size=(width, height + 3), auto_size_text=True) + form.AddRow(bar2, bar_text) + form.AddRow((CloseButton('Cancel', button_color=button_color))) + + form.NonBlocking = True + form.Show(non_blocking=True) + return bar2, bar_text + + +# ============================== ProgressMeterUpdate =====# +def _ProgressMeterUpdate(bar, value, text_elem, *args): + ''' + Update the progress meter for a form + :param form: class ProgressBar + :param value: int + :return: True if not cancelled, OK....False if Error + ''' + global _my_windows + if bar == None: return False + if bar.BarExpired: return False + message, w, h = ConvertArgsToSingleString(*args) + text_elem.Update(message) + # bar.TextToDisplay = message + bar.CurrentValue = value + rc = bar.UpdateBar(value) + if value >= bar.MaxValue or not rc: + bar.BarExpired = True + bar.ParentForm._Close() + if rc: # if update was OK but bar expired, decrement num windows + _my_windows.Decrement() + if bar.ParentForm.RootNeedsDestroying: + try: + bar.ParentForm.TKroot.destroy() + # there is a bug with progress meters not decrementing the number of windows + # correctly when the X is used to close the window + # uncommenting this line fixes that problem, but causes a double-decrement when + # the cancel button is used... damned if you do, damned if you don't, so I'm choosing + # don't, as in don't decrement too many times. It's OK now to have a mismatch in + # number of windows because of the "hidden" master window. This ensures all windows + # will be toplevel. Sorry about the bug, but the user never sees any problems as a result + # _my_windows.Decrement() + except: + pass + bar.ParentForm.RootNeedsDestroying = False + return False + + return rc + + +# ============================== EASY PROGRESS METER ========================================== # +# class to hold the easy meter info (a global variable essentialy) +class EasyProgressMeterDataClass(): + def __init__(self, title='', current_value=1, max_value=10, start_time=None, stat_messages=()): + self.Title = title + self.CurrentValue = current_value + self.MaxValue = max_value + self.StartTime = start_time + self.StatMessages = stat_messages + self.ParentForm = None + self.MeterID = None + self.MeterText = None + + # =========================== COMPUTE PROGRESS STATS ======================# + def ComputeProgressStats(self): + utc = datetime.datetime.utcnow() + time_delta = utc - self.StartTime + total_seconds = time_delta.total_seconds() + if not total_seconds: + total_seconds = 1 + try: + time_per_item = total_seconds / self.CurrentValue + except: + time_per_item = 1 + seconds_remaining = (self.MaxValue - self.CurrentValue) * 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.StatMessages = [ + '{} of {}'.format(self.CurrentValue, self.MaxValue), + '{} %'.format(100 * self.CurrentValue // self.MaxValue), + '', + ' {:6.2f} Iterations per Second'.format(self.CurrentValue / total_seconds), + ' {:6.2f} Seconds per Iteration'.format(total_seconds / (self.CurrentValue if self.CurrentValue else 1)), + '', + '{} Elapsed Time'.format(time_delta_short), + '{} Time Remaining'.format(time_remaining_short), + '{} Estimated Total Time'.format(total_time_short)] + return + + +# ============================== EasyProgressMeter =====# +def EasyProgressMeter(title, current_value, max_value, *args, orientation=None, bar_color=(None, None), + button_color=None, size=DEFAULT_PROGRESS_BAR_SIZE, border_width=None): + ''' + A ONE-LINE progress meter. Add to your code where ever you need a meter. No need for a second + function call before your loop. You've got enough code to write! + :param title: Title will be shown on the window + :param current_value: Current count of your items + :param max_value: Max value your count will ever reach. This indicates it should be closed + :param args: VARIABLE number of arguements... you request it, we'll print it no matter what the item! + :param orientation: + :param bar_color: + :param size: + :param Style: + :param StyleOffset: + :return: False if should stop the meter + ''' + local_border_width = DEFAULT_PROGRESS_BAR_BORDER_WIDTH if not border_width else border_width + # STATIC VARIABLE! + # This is a very clever form of static variable using a function attribute + # If the variable doesn't yet exist, then it will create it and initialize with the 3rd parameter + EasyProgressMeter.Data = getattr(EasyProgressMeter, 'Data', EasyProgressMeterDataClass()) + # if no meter currently running + if EasyProgressMeter.Data.MeterID is None: # Starting a new meter + print( + "Please change your call of EasyProgressMeter to use OneLineProgressMeter. EasyProgressMeter will be removed soon") + if int(current_value) >= int(max_value): + return False + del (EasyProgressMeter.Data) + EasyProgressMeter.Data = EasyProgressMeterDataClass(title, 1, int(max_value), datetime.datetime.utcnow(), []) + EasyProgressMeter.Data.ComputeProgressStats() + message = "\n".join([line for line in EasyProgressMeter.Data.StatMessages]) + EasyProgressMeter.Data.MeterID, EasyProgressMeter.Data.MeterText = _ProgressMeter(title, int(max_value), + message, *args, + orientation=orientation, + bar_color=bar_color, + size=size, + button_color=button_color, + border_width=local_border_width) + EasyProgressMeter.Data.ParentForm = EasyProgressMeter.Data.MeterID.ParentForm + return True + # if exactly the same values as before, then ignore. + if EasyProgressMeter.Data.MaxValue == max_value and EasyProgressMeter.Data.CurrentValue == current_value: + return True + if EasyProgressMeter.Data.MaxValue != int(max_value): + EasyProgressMeter.Data.MeterID = None + EasyProgressMeter.Data.ParentForm = None + del (EasyProgressMeter.Data) + EasyProgressMeter.Data = EasyProgressMeterDataClass() # setup a new progress meter + return True # HAVE to return TRUE or else the new meter will thing IT is failing when it hasn't + EasyProgressMeter.Data.CurrentValue = int(current_value) + EasyProgressMeter.Data.MaxValue = int(max_value) + EasyProgressMeter.Data.ComputeProgressStats() + message = '' + for line in EasyProgressMeter.Data.StatMessages: + message = message + str(line) + '\n' + message = "\n".join(EasyProgressMeter.Data.StatMessages) + args = args + (message,) + rc = _ProgressMeterUpdate(EasyProgressMeter.Data.MeterID, current_value, + EasyProgressMeter.Data.MeterText, *args) + # if counter >= max then the progress meter is all done. Indicate none running + if current_value >= EasyProgressMeter.Data.MaxValue or not rc: + EasyProgressMeter.Data.MeterID = None + del (EasyProgressMeter.Data) + EasyProgressMeter.Data = EasyProgressMeterDataClass() # setup a new progress meter + return False # even though at the end, return True so don't cause error with the app + return rc # return whatever the update told us + + +def EasyProgressMeterCancel(title, *args): + EasyProgressMeter.EasyProgressMeterData = getattr(EasyProgressMeter, 'EasyProgressMeterData', + EasyProgressMeterDataClass()) + if EasyProgressMeter.EasyProgressMeterData.MeterID is not None: + # tell the normal meter update that we're at max value which will close the meter + rc = EasyProgressMeter(title, EasyProgressMeter.EasyProgressMeterData.MaxValue, + EasyProgressMeter.EasyProgressMeterData.MaxValue, ' *** CANCELLING ***', + 'Caller requested a cancel', *args) + return rc + return True + + +# global variable containing dictionary will all currently running one-line progress meters. +_one_line_progress_meters = {} + + +# ============================== OneLineProgressMeter =====# +def OneLineProgressMeter(title, current_value, max_value, key='OK for 1 meter', *args, orientation=None, bar_color=(None, None), + button_color=None, size=DEFAULT_PROGRESS_BAR_SIZE, border_width=None, grab_anywhere=False): + global _one_line_progress_meters + + local_border_width = DEFAULT_PROGRESS_BAR_BORDER_WIDTH if border_width is not None else border_width + try: + meter_data = _one_line_progress_meters[key] + except: # a new meater is starting + if int(current_value) >= int(max_value): # if already expired then it's an old meter, ignore + return False + meter_data = EasyProgressMeterDataClass(title, 1, int(max_value), datetime.datetime.utcnow(), []) + _one_line_progress_meters[key] = meter_data + meter_data.ComputeProgressStats() + message = "\n".join([line for line in meter_data.StatMessages]) + meter_data.MeterID, meter_data.MeterText = _ProgressMeter(title, int(max_value), message, *args, + orientation=orientation, bar_color=bar_color, + size=size, button_color=button_color, + border_width=local_border_width, + grab_anywhere=grab_anywhere) + meter_data.ParentForm = meter_data.MeterID.ParentForm + return True + + # if exactly the same values as before, then ignore, return success. + if meter_data.MaxValue == max_value and meter_data.CurrentValue == current_value: + return True + meter_data.CurrentValue = int(current_value) + meter_data.MaxValue = int(max_value) + meter_data.ComputeProgressStats() + message = '' + for line in meter_data.StatMessages: + message = message + str(line) + '\n' + message = "\n".join(meter_data.StatMessages) + args = args + (message,) + rc = _ProgressMeterUpdate(meter_data.MeterID, current_value, + meter_data.MeterText, *args) + # if counter >= max then the progress meter is all done. Indicate none running + if current_value >= meter_data.MaxValue or not rc: + del _one_line_progress_meters[key] + return False + return rc # return whatever the update told us + + +def OneLineProgressMeterCancel(key='OK for 1 meter'): + global _one_line_progress_meters + + try: + meter_data = _one_line_progress_meters[key] + except: # meter is already deleted + return + OneLineProgressMeter('', meter_data.MaxValue, meter_data.MaxValue, key=key) + + +# input is #RRGGBB +# output is #RRGGBB +def GetComplimentaryHex(color): + # 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 =====# +# ===================================================# +_easy_print_data = None # global variable... I'm cheating + + +class DebugWin(): + def __init__(self, size=(None, None), location=(None, None), font=None, no_titlebar=False, no_button=False, + grab_anywhere=False, keep_on_top=False): + # Show a form that's a running counter + win_size = size if size != (None, None) else DEFAULT_DEBUG_WINDOW_SIZE + self.window = Window('Debug Window', no_titlebar=no_titlebar, auto_size_text=True, location=location, + font=font or ('Courier New', 10), grab_anywhere=grab_anywhere, keep_on_top=keep_on_top) + self.output_element = Output(size=win_size) + if no_button: + self.layout = [[self.output_element]] + else: + self.layout = [ + [self.output_element], + [DummyButton('Quit')] + ] + self.window.AddRows(self.layout) + self.window.Read(timeout=0) # Show a non-blocking form, returns immediately + return + + def Print(self, *args, end=None, sep=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, just print + print(*args, sep=sepchar, end=endchar) + return + + event, values = self.window.Read(timeout=0) + if event == 'Quit' or event is None: + self.Close() + print(*args, sep=sepchar, end=endchar) + # Add extra check to see if the window was closed... if closed by X sometimes am not told + try: + state = self.window.TKroot.state() + except: + self.Close() + + def Close(self): + self.window.Close() + self.window = None + + +def PrintClose(): + EasyPrintClose() + + +def EasyPrint(*args, size=(None, None), end=None, sep=None, location=(None, None), font=None, no_titlebar=False, + no_button=False, grab_anywhere=False, keep_on_top=False): + global _easy_print_data + + if _easy_print_data is None: + _easy_print_data = DebugWin(size=size, location=location, font=font, no_titlebar=no_titlebar, + no_button=no_button, grab_anywhere=grab_anywhere, keep_on_top=keep_on_top) + _easy_print_data.Print(*args, end=end, sep=sep) + + +Print = EasyPrint +eprint = EasyPrint + + +def EasyPrintClose(): + global _easy_print_data + if _easy_print_data is not None: + _easy_print_data.Close() + _easy_print_data = 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, **kwargs): +def cprint(*args, end=None, sep=' ', text_color=None, t=None, background_color=None, b=None, colors=None, c=None, window=None, key=None): + """ + 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 - Tuple[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 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 + :type colors: (str) or Tuple[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 + :type c: (str) or Tuple[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) + :return: None + :rtype: None + """ + + 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): + kw_text_color = dual_color.split(' on ')[0] + kw_background_color = dual_color.split(' on ')[1] + 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) + mline.print('') + else: + mline.print(*args,text_color=kw_text_color, background_color=kw_background_color, end=end, sep=sep) + 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=True): + """ + Print like Python normally prints except route the output to a multline element and also add colors if desired + + :param multiline_element: The multiline element to be output to + :type multiline_element: Multiline or MultilineOutput + :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 + """ + 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=text_color, background_color=background_color, autoscroll=autoscroll) + + + +# ======================== Scrolled Text Box =====# +# ===================================================# +def PopupScrolled(*args, button_color=None, yes_no=False, auto_close=False, auto_close_duration=None, + size=(None, None)): + if not args: return + width, height = size + width = width if width else MESSAGE_BOX_LINE_WIDTH + form = Window(args[0], auto_size_text=True, button_color=button_color, auto_close=auto_close, + auto_close_duration=auto_close_duration) + 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 + 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 + form.AddRow(Multiline(complete_output, size=(max_line_width, height_computed))) + pad = max_line_total - 15 if max_line_total > 15 else 1 + # show either an OK or Yes/No depending on paramater + if yes_no: + form.AddRow(Text('', size=(pad, 1), auto_size_text=False), Yes(), No()) + button, values = form.Read() + return button + else: + form.AddRow(Text('', size=(pad, 1), auto_size_text=False), Button('OK', size=(5, 1), button_color=button_color)) + button, values = form.Read() + form.Close() + return button + + +ScrolledTextBox = PopupScrolled + + +# ============================== SetGlobalIcon ======# +# Sets the icon to be used by default # +# ===================================================# +def SetGlobalIcon(icon): + global _my_windows + + try: + with open(icon, 'r') as icon_file: + pass + except: + raise FileNotFoundError + _my_windows.user_defined_icon = icon + return True + + +# ============================== SetOptions =========# +# Sets the icon to be used by default # +# ===================================================# +def SetOptions(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), + tooltip_time=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 _my_windows + + if icon: + try: + with open(icon, 'r') as icon_file: + pass + except: + raise FileNotFoundError + _my_windows.user_defined_icon = icon + + if button_color != None: + 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: + 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 + + 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_COLOR, + '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'), # based on Reddit color + 'PROGRESS': DEFAULT_PROGRESS_BAR_COLOR, + '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_COLOR, + '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_COLOR, + 'BORDER': 1, + 'SLIDER_DEPTH': 0, + 'PROGRESS_DEPTH': 0, + 'ACCENT1': '#c15226', + 'ACCENT2': '#7a4d5f', + 'ACCENT3': '#889743'}, + + 'GreenTan': {'BACKGROUND': '#9FB8AD', + 'TEXT': COLOR_SYSTEM_DEFAULT, + 'INPUT': '#F7F3EC', 'TEXT_INPUT': '#000000', + 'SCROLL': '#F7F3EC', + 'BUTTON': ('#FFFFFF', '#475841'), + 'PROGRESS': DEFAULT_PROGRESS_BAR_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + 'BORDER': 1, + 'SLIDER_DEPTH': 0, + 'PROGRESS_DEPTH': 0}, + ################################## Renamed Original Themes ################################## + 'Default': # plain gray but blue buttons + {'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': # everything is gray + {'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': # a duplicate of "Default" for users that are tired of the nag screen + {'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}, + + 'LightBlue': {'BACKGROUND': '#E3F2FD', + 'TEXT': '#000000', + 'INPUT': '#86A8FF', + 'TEXT_INPUT': '#000000', + 'SCROLL': '#86A8FF', + 'BUTTON': ('#FFFFFF', '#5079D3'), + 'PROGRESS': DEFAULT_PROGRESS_BAR_COLOR, + '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'), # based on Reddit color + 'PROGRESS': DEFAULT_PROGRESS_BAR_COLOR, + '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_COLOR, + '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_COLOR, + 'BORDER': 1, + 'SLIDER_DEPTH': 0, + 'PROGRESS_DEPTH': 0, + 'ACCENT1': '#c15226', + 'ACCENT2': '#7a4d5f', + 'ACCENT3': '#889743'}, + + 'LightGreen1': {'BACKGROUND': '#9FB8AD', + 'TEXT': COLOR_SYSTEM_DEFAULT, + 'INPUT': '#F7F3EC', 'TEXT_INPUT': '#000000', + 'SCROLL': '#F7F3EC', + 'BUTTON': ('#FFFFFF', '#475841'), + 'PROGRESS': DEFAULT_PROGRESS_BAR_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + '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_COLOR, + 'BORDER': 1, + 'SLIDER_DEPTH': 0, + 'PROGRESS_DEPTH': 0}, + + ################################## End Renamed Original Themes ################################## + + # + 'LightBrown4': {'BACKGROUND': '#d7c79e', 'TEXT': '#a35638', 'INPUT': '#9dab86', 'TEXT_INPUT': '#000000', 'SCROLL': '#a35638', + 'BUTTON': ('#FFFFFF', '#a35638'), + 'PROGRESS': DEFAULT_PROGRESS_BAR_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, 'BORDER': 1, 'SLIDER_DEPTH': 0, + 'PROGRESS_DEPTH': 0, 'COLOR_LIST': ['#070739', '#521477', '#c327ab', '#e1e099'], + 'DESCRIPTION': ['#000000', 'Purple', 'Yellow', 'Dark']}, + 'DarkBlue13': {'BACKGROUND': '#203562', 'TEXT': '#e3e8f8', 'INPUT': '#c0c5cd', 'TEXT_INPUT': '#000000', 'SCROLL': '#c0c5cd', + 'BUTTON': ('#FFFFFF', '#3e588f'), 'PROGRESS': DEFAULT_PROGRESS_BAR_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, 'BORDER': 1, 'SLIDER_DEPTH': 0, + 'PROGRESS_DEPTH': 0, 'COLOR_LIST': ['#5c715e', '#b6cdbd', '#ddeedf', '#f2f9f1'], + 'DESCRIPTION': ['Grey', 'Green', 'Vintage']}, + 'DarkGrey7': {'BACKGROUND': '#4b586e', 'TEXT': '#dddddd', 'INPUT': '#574e6d', 'TEXT_INPUT': '#dddddd', 'SCROLL': '#574e6d', + 'BUTTON': ('#dddddd', '#43405d'), 'PROGRESS': DEFAULT_PROGRESS_BAR_COLOR, '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_COLOR, '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_COLOR, '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_COLOR, 'BORDER': 1, 'SLIDER_DEPTH': 0, + 'PROGRESS_DEPTH': 0, + }, + } + + +def ListOfLookAndFeelValues(): + """ + Get a list of the valid values to pass into your call to change_look_and_feel + :return: List[str] - list of valid string values + """ + 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: (str) the new theme name to use + :return: (str) the currently selected theme + """ + if new_theme is not None: + change_look_and_feel(new_theme) + 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 + + :return: (str) - color string of the background color currently in use + """ + if color is not None: + set_options(background_color=color) + return DEFAULT_BACKGROUND_COLOR + + +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 + """ + 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 + """ + 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 + """ + 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 + """ + 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 + """ + 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: Tuple[str, str] - TUPLE with color strings of the button color currently in use (button text color, button background color) + """ + if color is not None: + set_options(button_color=color) + return DEFAULT_BUTTON_COLOR + + +def theme_progress_bar_color(color=None): + """ + Sets/Returns the progress bar colors by the current color theme + + :return: Tuple[str, str] - TUPLE with color strings of the ProgressBar color currently in use(button text color, button background color) + """ + 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: (str) - color string of the slider color currently in use + """ + 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: (int) - border width currently in use + """ + 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: (int) - border width currently in use + """ + 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: (int) - border width currently in use + """ + 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: (str) - color string currently in use + """ + 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: List[str] - A sorted list of the currently available color themes + """ + 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_previewer(columns=12): + """ + Show a window with all of the color themes - takes a while so be patient + + :param columns: (int) number of themes in a single row + """ + preview_all_look_and_feel_themes(columns) + +def ChangeLookAndFeel(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: (str) the name of the index into the Look and Feel table (does not have to be exact, can be "fuzzy") + :param force: (bool) no longer used + """ + + global CURRENT_LOOK_AND_FEEL + + # if sys.platform.startswith('darwin') and not force: + # print('*** Changing look and feel is not supported on Mac platform ***') + # return + + theme = index + # normalize available l&f values + lf_values = [item.lower() for item in list_of_look_and_feel_values()] + + # option 1 + opt1 = theme.replace(' ', '').lower() + + # option 2 (reverse lookup) + optx = theme.lower().split(' ') + optx.reverse() + opt2 = ''.join(optx) + + # search for valid l&f name + if opt1 in lf_values: + ix = lf_values.index(opt1) + elif opt2 in lf_values: + ix = lf_values.index(opt2) + else: + ix = randint(0, len(lf_values) - 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 = list_of_look_and_feel_values()[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['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 sys.platform.startswith('darwin') 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()) + + +def preview_all_look_and_feel_themes(columns=12): + """ + 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: (int) The number of themes to display per row + """ + + # 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....', 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('Cancel'), Slider((1, 10), orientation='h', size=(5, 15))]] + + layout = [[Text('Here is a complete list of themes', font='Default 18', background_color=win_bg)]] + + names = list_of_look_and_feel_values() + names.sort() + row = [] + for count, theme in enumerate(names): + change_look_and_feel(theme) + if not count % columns: + layout += [row] + row = [] + row += [Frame(theme, sample_layout() if not web else [[T(theme)]] + sample_layout())] + if row: + layout += [row] + + window = Window('Preview of all Look and Feel choices', layout, background_color=win_bg) + window.read() + window.close() + + +# ============================== sprint ======# +# Is identical to the Scrolled Text Box # +# Provides a crude 'print' mechanism but in a # +# GUI environment # +# ============================================# +sprint = ScrolledTextBox + + +# Converts an object's contents into a nice printable string. Great for dumping debug data +def ObjToStringSingleObj(obj): + 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 ObjToString(obj, extra=' '): + 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__))) + + +# ------------------------------------------------------------------------------------------------------------------ # +# ===================================== Upper PySimpleGUI ======================================================== # +# Pre-built dialog boxes for all your needs These are the "high level API calls # +# ------------------------------------------------------------------------------------------------------------------ # + +# ----------------------------------- The mighty Popup! ------------------------------------------------------------ # + +def Popup(*args, 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=DEFAULT_WINDOW_ICON, line_width=None, + font=None, no_titlebar=False, grab_anywhere=False, keep_on_top=False, location=(None, None)): + """ + Popup - Display a popup box with as many parms as you wish to include + :param args: + :param button_color: + :param background_color: + :param text_color: + :param button_type: + :param auto_close: + :param auto_close_duration: + :param non_blocking: + :param icon: + :param line_width: + :param font: + :param no_titlebar: + :param grab_anywhere: + :param keep_on_top: + :param location: + :return: + """ + 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 = args_to_print[0] if args_to_print[0] is not None else 'None' + window = Window(title, 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) + max_line_total, total_lines = 0, 0 + 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 + window.AddRow( + 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: + window.AddRow(PopupButton(custom_text, size=(len(custom_text), 1), button_color=button_color, focus=True, + bind_return_key=True)) + elif custom_text[1] is None: + window.AddRow( + PopupButton(custom_text[0], size=(len(custom_text[0]), 1), button_color=button_color, focus=True, + bind_return_key=True)) + else: + window.AddRow(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[0]), 1))) + elif button_type is POPUP_BUTTONS_YES_NO: + window.AddRow(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: + window.AddRow( + PopupButton('Cancelled', button_color=button_color, focus=True, bind_return_key=True, pad=((20, 0), 3))) + elif button_type is POPUP_BUTTONS_ERROR: + window.AddRow(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: + window.AddRow(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: + window.AddRow(PopupButton('OK', size=(5, 1), button_color=button_color, focus=True, bind_return_key=True, + pad=((20, 0), 3))) + + if non_blocking: + button, values = window.Read(timeout=0) + else: + button, values = window.Read() + window.Close() + 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): + raise DeprecationWarning('MsgBox is no longer supported... change your call to Popup') + + +# --------------------------- PopupNoButtons --------------------------- +def PopupNoButtons(*args, 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=False, location=(None, None)): + """ + Show a Popup but without any buttons + :param args: + :param button_color: + :param background_color: + :param text_color: + :param auto_close: + :param auto_close_duration: + :param non_blocking: + :param icon: + :param line_width: + :param font: + :param no_titlebar: + :param grab_anywhere: + :param keep_on_top: + :param location: + :return: + """ + Popup(*args, button_color=button_color, 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) + + +# --------------------------- PopupNonBlocking --------------------------- +def PopupNonBlocking(*args, 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=DEFAULT_WINDOW_ICON, + line_width=None, font=None, no_titlebar=False, grab_anywhere=False, keep_on_top=False, + location=(None, None)): + """ + Show Popup box and immediately return (does not block) + :param args: + :param button_type: + :param button_color: + :param background_color: + :param text_color: + :param auto_close: + :param auto_close_duration: + :param non_blocking: + :param icon: + :param line_width: + :param font: + :param no_titlebar: + :param grab_anywhere: + :param keep_on_top: + :param location: + :return: + """ + Popup(*args, 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) + + +PopupNoWait = PopupNonBlocking + + +# --------------------------- PopupQuick - a NonBlocking, Self-closing Popup --------------------------- +def PopupQuick(*args, 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=DEFAULT_WINDOW_ICON, line_width=None, + font=None, no_titlebar=False, grab_anywhere=False, keep_on_top=False, location=(None, None)): + """ + Show Popup box that doesn't block and closes itself + :param args: + :param button_type: + :param button_color: + :param background_color: + :param text_color: + :param auto_close: + :param auto_close_duration: + :param non_blocking: + :param icon: + :param line_width: + :param font: + :param no_titlebar: + :param grab_anywhere: + :param keep_on_top: + :param location: + :return: + """ + Popup(*args, 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) + + +# --------------------------- PopupQuick - a NonBlocking, Self-closing Popup with no titlebar and no buttons --------------------------- +def PopupQuickMessage(*args, 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=DEFAULT_WINDOW_ICON, + line_width=None, + font=None, no_titlebar=True, grab_anywhere=False, keep_on_top=False, location=(None, None)): + """ + Show Popup box that doesn't block and closes itself + :param args: + :param button_type: + :param button_color: + :param background_color: + :param text_color: + :param auto_close: + :param auto_close_duration: + :param non_blocking: + :param icon: + :param line_width: + :param font: + :param no_titlebar: + :param grab_anywhere: + :param keep_on_top: + :param location: + :return: + """ + Popup(*args, 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) + + +# --------------------------- PopupNoTitlebar --------------------------- +def PopupNoTitlebar(*args, 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=DEFAULT_WINDOW_ICON, + line_width=None, font=None, grab_anywhere=True, keep_on_top=False, location=(None, None)): + """ + Display a Popup without a titlebar. Enables grab anywhere so you can move it + :param args: + :param button_type: + :param button_color: + :param background_color: + :param text_color: + :param auto_close: + :param auto_close_duration: + :param non_blocking: + :param icon: + :param line_width: + :param font: + :param grab_anywhere: + :param keep_on_top: + :param location: + :return: + """ + Popup(*args, 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) + + +PopupNoFrame = PopupNoTitlebar +PopupNoBorder = PopupNoTitlebar +PopupAnnoying = PopupNoTitlebar + + +# --------------------------- PopupAutoClose --------------------------- +def PopupAutoClose(*args, 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=DEFAULT_WINDOW_ICON, + line_width=None, font=None, no_titlebar=False, grab_anywhere=False, keep_on_top=False, + location=(None, None)): + """ + Popup that closes itself after some time period + :param args: + :param button_type: + :param button_color: + :param background_color: + :param text_color: + :param auto_close: + :param auto_close_duration: + :param non_blocking: + :param icon: + :param line_width: + :param font: + :param no_titlebar: + :param grab_anywhere: + :param keep_on_top: + :param location: + :return: + """ + Popup(*args, 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) + + +PopupTimed = PopupAutoClose + + +# --------------------------- PopupError --------------------------- +def PopupError(*args, button_color=DEFAULT_ERROR_BUTTON_COLOR, 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=False, location=(None, None)): + """ + Popup with colored button and 'Error' as button text + :param args: + :param button_color: + :param background_color: + :param text_color: + :param auto_close: + :param auto_close_duration: + :param non_blocking: + :param icon: + :param line_width: + :param font: + :param no_titlebar: + :param grab_anywhere: + :param keep_on_top: + :param location: + :return: + """ + Popup(*args, 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=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) + + +# --------------------------- PopupCancel --------------------------- +def PopupCancel(*args, 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=False, location=(None, None)): + """ + Display Popup with "cancelled" button text + :param args: + :param button_color: + :param background_color: + :param text_color: + :param auto_close: + :param auto_close_duration: + :param non_blocking: + :param icon: + :param line_width: + :param font: + :param no_titlebar: + :param grab_anywhere: + :param keep_on_top: + :param location: + :return: + """ + Popup(*args, 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) + + +# --------------------------- PopupOK --------------------------- +def PopupOK(*args, 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=False, location=(None, None)): + """ + Display Popup with OK button only + :param args: + :param button_color: + :param background_color: + :param text_color: + :param auto_close: + :param auto_close_duration: + :param non_blocking: + :param icon: + :param line_width: + :param font: + :param no_titlebar: + :param grab_anywhere: + :param keep_on_top: + :param location: + :return: + """ + Popup(*args, 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) + + +# --------------------------- PopupOKCancel --------------------------- +def PopupOKCancel(*args, 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=False, location=(None, None)): + """ + Display popup with OK and Cancel buttons + :param args: + :param button_color: + :param background_color: + :param text_color: + :param auto_close: + :param auto_close_duration: + :param non_blocking: + :param icon: + :param line_width: + :param font: + :param no_titlebar: + :param grab_anywhere: + :param keep_on_top: + :param location: + :return: OK, Cancel or None + """ + return Popup(*args, 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) + + +# --------------------------- PopupYesNo --------------------------- +def PopupYesNo(*args, 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=False, location=(None, None)): + """ + Display Popup with Yes and No buttons + :param args: + :param button_color: + :param background_color: + :param text_color: + :param auto_close: + :param auto_close_duration: + :param non_blocking: + :param icon: + :param line_width: + :param font: + :param no_titlebar: + :param grab_anywhere: + :param keep_on_top: + :param location: + :return: Yes, No or None + """ + return Popup(*args, 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) + + +############################################################################## +# The PopupGet_____ functions - Will return user input # +############################################################################## + +# --------------------------- PopupGetFolder --------------------------- + + +def PopupGetFolder(message, default_path='', no_window=False, size=(None, None), button_color=None, + background_color=None, text_color=None, icon=DEFAULT_WINDOW_ICON, font=None, no_titlebar=False, + grab_anywhere=False, keep_on_top=False, location=(None, None), initial_folder=None): + """ + Display popup with text entry field and browse button. Browse for folder + :param message: + :param default_path: + :param no_window: + :param size: + :param button_color: + :param background_color: + :param text_color: + :param icon: + :param font: + :param no_titlebar: + :param grab_anywhere: + :param keep_on_top: + :param location: + :return: Contents of text field. None if closed using X or cancelled + """ + + global _my_windows + + if no_window: + if _my_windows._NumOpenWindows: + root = tk.Toplevel() + else: + root = tk.Tk() + try: + root.attributes('-alpha', 0) # hide window while building it. makes for smoother 'paint' + except: + pass + folder_name = tk.filedialog.askdirectory() # show the 'get folder' dialog box + root.destroy() + return folder_name + + layout = [[Text(message, auto_size_text=True, text_color=text_color, background_color=background_color)], + [InputText(default_text=default_path, size=size, key='_INPUT_'), FolderBrowse(initial_folder=initial_folder)], + [Button('Ok', size=(5, 1), bind_return_key=True), Button('Cancel', size=(5, 1))]] + + window = Window(title=message, layout=layout, icon=icon, auto_size_text=True, button_color=button_color, + background_color=background_color, + font=font, no_titlebar=no_titlebar, grab_anywhere=grab_anywhere, keep_on_top=keep_on_top, + location=location) + + button, values = window.Read() + window.Close() + if button != 'Ok': + return None + else: + path = values['_INPUT_'] + return path + + +# --------------------------- PopupGetFile --------------------------- + +def PopupGetFile(message, default_path='', default_extension='', save_as=False, file_types=(("ALL Files", "*.*"),), + no_window=False, size=(None, None), button_color=None, background_color=None, text_color=None, + icon=DEFAULT_WINDOW_ICON, font=None, no_titlebar=False, grab_anywhere=False, keep_on_top=False, + location=(None, None), initial_folder=None): + """ + Display popup with text entry field and browse button. Browse for file + :param message: + :param default_path: + :param default_extension: + :param save_as: + :param file_types: + :param no_window: + :param size: + :param button_color: + :param background_color: + :param text_color: + :param icon: + :param font: + :param no_titlebar: + :param grab_anywhere: + :param keep_on_top: + :param location: + :return: string representing the path chosen, None if cancelled or window closed with X + """ + + global _my_windows + + if no_window: + if _my_windows._NumOpenWindows: + root = tk.Toplevel() + else: + root = tk.Tk() + try: + root.attributes('-alpha', 0) # hide window while building it. makes for smoother 'paint' + except: + pass + if save_as: + filename = tk.filedialog.asksaveasfilename(filetypes=file_types, + defaultextension=default_extension) # show the 'get file' dialog box + else: + filename = tk.filedialog.askopenfilename(filetypes=file_types, + defaultextension=default_extension) # show the 'get file' dialog box + root.destroy() + return filename + + browse_button = SaveAs(file_types=file_types, initial_folder=initial_folder) if save_as else FileBrowse( + file_types=file_types, initial_folder=initial_folder) + + layout = [[Text(message, auto_size_text=True, text_color=text_color, background_color=background_color)], + [InputText(default_text=default_path, size=size, key='_INPUT_'), browse_button], + [Button('Ok', size=(6, 1), bind_return_key=True), Button('Cancel', size=(6, 1))]] + + window = Window(title=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) + + button, values = window.Read() + window.Close() + if button != 'Ok': + return None + else: + path = values['_INPUT_'] + return path + + + +# --------------------------- PopupGetText --------------------------- + +def PopupGetText(message, default_text='', password_char='', size=(None, None), button_color=None, + background_color=None, text_color=None, icon=DEFAULT_WINDOW_ICON, font=None, no_titlebar=False, + grab_anywhere=False, keep_on_top=False, location=(None, None)): + """ + Display Popup with text entry field + :param message: + :param default_text: + :param password_char: + :param size: + :param button_color: + :param background_color: + :param text_color: + :param icon: + :param font: + :param no_titlebar: + :param grab_anywhere: + :param keep_on_top: + :param location: + :return: Text entered or None if window was closed + """ + + layout = [[Text(message, auto_size_text=True, text_color=text_color, background_color=background_color, font=font)], + [InputText(default_text=default_text, size=size, key='_INPUT_', password_char=password_char)], + [Button('Ok', size=(5, 1), bind_return_key=True), Button('Cancel', size=(5, 1))]] + + window = Window(title=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) + + button, values = window.Read() + window.Close() + if button != 'Ok': + return None + else: + path = values['_INPUT_'] + return path + + +change_look_and_feel = ChangeLookAndFeel +easy_print = EasyPrint +easy_print_close = EasyPrintClose +get_complimentary_hex = GetComplimentaryHex +list_of_look_and_feel_values = ListOfLookAndFeelValues +obj_to_string = ObjToString +obj_to_string_single_obj = ObjToStringSingleObj +one_line_progress_meter = OneLineProgressMeter +one_line_progress_meter_cancel = OneLineProgressMeterCancel +popup = Popup +popup_annoying = PopupAnnoying +popup_auto_close = PopupAutoClose +popup_cancel = PopupCancel +popup_error = PopupError +popup_get_file = PopupGetFile +popup_get_folder = PopupGetFolder +popup_get_text = PopupGetText +popup_no_border = PopupNoBorder +popup_no_buttons = PopupNoButtons +popup_no_frame = PopupNoFrame +popup_no_titlebar = PopupNoTitlebar +popup_no_wait = PopupNoWait +popup_non_blocking = PopupNonBlocking +popup_ok = PopupOK +popup_ok_cancel = PopupOKCancel +popup_quick = PopupQuick +popup_quick_message = PopupQuickMessage +popup_scrolled = PopupScrolled +popup_timed = PopupTimed +popup_yes_no = PopupYesNo +print_close = PrintClose +rgb = RGB +scrolled_text_box = ScrolledTextBox +set_global_icon = SetGlobalIcon +set_options = SetOptions +timer_start = TimerStart +timer_stop = TimerStop +sprint = sprint + +#------------------------ Set the "Official PySimpleGUI Theme Colors" ------------------------ +theme(CURRENT_LOOK_AND_FEEL) +# theme_previewer() +# -------------------------------- ENTRY POINT IF RUN STANDALONE -------------------------------- # + + + + +def main(): + # ChangeLookAndFeel('light green 6' ) + + # Popup('Popup Test') + + # SetOptions(background_color='blue', text_element_background_color='blue', text_color='white') + # layout = [[Text('You are running the PySimpleGUI.py file itself', font='Any 25', size=(60,1), tooltip='My tooltip!')], + # [Text('You should be importing it rather than running it', size=(60, 1))], + # [Text('Here is your sample window....')], + # [Text('Source Folder', justification='right', size=(40,1)), InputText('Source', focus=True, disabled=True), + # FolderBrowse()], + # [Text('Destination Folder', justification='right', size=(40,1)), InputText('Dest'), FolderBrowse()], + # [Ok(), Cancel(disabled=True), Exit(tooltip='Exit button'), Button('Hidden Button', visible=False)]] + + ver = version.split('\n')[0] + + + def VerLine(version, description, size=(30,1)): + return [Column([[T(description, font='Courier 18', text_color='yellow')], [T(version, font='Courier 18', size=size)]])] + + + menu_def = [['&File', ['&Open', '&Save', 'E&xit', 'Properties']], + ['&Edit', ['Paste', ['Special', 'Normal', ], '!Undo'], ], + ['!&Disabled', ['Paste', ['Special', 'Normal', ], '!Undo'], ], + ['&Help', '&About...'], ] + + + menu_def = [['File', ['&Open::mykey', '&Save', 'E&xit', 'Properties']], + ['Edit', ['!Paste', ['Special', 'Normal', ], '!Undo'], ], + ['!Disabled', ['Has Sub', ['Item1', 'Item2', ], 'No Sub'], ], + ['Help', 'About...'], ] + + col1 = [[Text('Column 1 line 1', background_color='red')], [Text('Column 1 line 2')]] + + layout = [ + [Menu(menu_def, key='_MENU_', text_color='yellow', background_color='#475841', font='Courier 14')], + # [T('123435', size=(1,8))], + [Text('PySimpleGUIWeb Welcomes You...', tooltip='text', font=('Comic sans ms', 20),size=(40,1), text_color='yellow', enable_events=False, key='_PySimpleGUIWeb_')], + # [OptionMenu([])], + [T('System platform = %s'%sys.platform)], + [Image(data=DEFAULT_BASE64_ICON, enable_events=False)], + # [Image(filename=r'C:\Python\PycharmProjects\PSG\logo500.png', key='-IMAGE-')], + VerLine(ver, 'PySimpleGUI Version'), + VerLine(os.path.dirname(os.path.abspath(__file__)), 'PySimpleGUI Location'), + VerLine(sys.version, 'Python Version', size=(60,2)), + VerLine(pkg_resources.get_distribution("remi").version, 'Remi Version'), + # [Text('VERSION {}'.format(version), text_color='red', font='Courier 24')], + [T('Current Time '), Text('Text', key='_TEXT_', font='Arial 18', text_color='black', size=(30,1)), Column(col1, background_color='red')], + [T('Up Time'), Text('Text', key='_TEXT_UPTIME_', font='Arial 18', text_color='black', size=(30,1))], + [Input('Single Line Input', do_not_clear=True, enable_events=False, size=(30, 1), text_color='red', key='_IN_')], + [Multiline('Multiline Input', do_not_clear=True, size=(40, 4), enable_events=False, key='_MULTI_IN_')], + # [Output(size=(60,10))], + [MultilineOutput('Multiline Output', size=(80, 8), text_color='blue', font='Courier 12', key='_MULTIOUT_', autoscroll=True)], + [Checkbox('Checkbox 1', enable_events=True, key='_CB1_'), Checkbox('Checkbox 2', default=True, key='_CB2_', enable_events=True)], + [Combo(values=['Combo 1', 'Combo 2', 'Combo 3'], default_value='Combo 2', key='_COMBO_', enable_events=True, + readonly=False, tooltip='Combo box', disabled=False, size=(12, 1))], + [Listbox(values=('Listbox 1', 'Listbox 2', 'Listbox 3'), enable_events =True, size=(10, 3), key='_LIST_')], + # [Image(filename=r'C:\Python\PycharmProjects\PSG\logo200.png', enable_events=False)], + [Slider((1, 100), default_value=80, key='_SLIDER_', visible=True, enable_events=True, orientation='v')], + [Spin(values=(1, 2, 3), initial_value='2', size=(4, 1), key='_SPIN_', enable_events=True)], + [OK(), Button('Hidden', visible=False, key='_HIDDEN_'), Button('Values'), Button('Exit', button_color=('white', 'red')), Button('UnHide'), B('Popup')] + ] + + window = Window('PySimpleGUIWeb Test Harness Window', layout, + font='Arial 18', + icon=DEFAULT_BASE64_ICON, + default_element_size=(12,1), + auto_size_buttons=False) + + start_time = datetime.datetime.now() + while True: + event, values = window.Read(timeout=100) + window.Element('_TEXT_').Update(str(datetime.datetime.now())) + window.Element('_TEXT_UPTIME_').Update(str(datetime.datetime.now()-start_time)) + print(event, values) if event != TIMEOUT_KEY else None + if event in (None, 'Exit'): + break + elif event == 'OK': + + window.Element('_MULTIOUT_').print('You clicked the OK button') + # window.Element('_MULTIOUT_').Update('You clicked the OK button', append=True, autoscroll=True) + window.Element('_PySimpleGUIWeb_').Widget.style['background-image'] = "url('/my_resources:mine.png')" + + elif event == 'Values': + window.Element('_MULTIOUT_').Update(str(values)+'\n', append=True) + # nav = remi.gui.FileFolderNavigator(False,r'a:\TEMP', True, False) + # here is returned the Input Dialog widget, and it will be shown + # fileselectionDialog.show(window.Element('_IN_').Widget) + + elif event != TIMEOUT_KEY: + window.Element('_MULTIOUT_').print('EVENT: ' + str(event)) + if event == 'Popup': + Popup('This is a popup!') + if event == 'UnHide': + print('Unhiding...') + window.Element('_HIDDEN_').Update(visible=True) + + window.Close() + + +if __name__ == '__main__': + main() + exit(0) diff --git a/README.md b/README.md new file mode 100644 index 0000000..d77a194 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Shannon-for-Dummies + +Educational application + +Exploration from Claude's Shannon initial theory to its practical application to satellite communications. + +The application is using PysimpleGUI / PySimpleGUIWeb and runs either in local windows or in web pages. + +The Web version via localhost is fully functional although not as convenient as the windowed version (only 1 plot open). +The look of the web version differs significantly from the local version (PySimpleGUIWeb is still beta). + +The Web version in remote does work for a single user (all users connected can send commands but see the same page). +This mode is experimental and doesn't behave as a web server : users are not managed, the app closes when the user closes the window ... + +The value of the application is essentially in the background information accessed by clicking labels of all inputs / outputs. + +A Knowledge DB is coupled to the application for collaborative contributions on the subject opening technical discussions. At this stage the DB is local. + diff --git a/Satellite.png b/Satellite.png new file mode 100644 index 0000000000000000000000000000000000000000..111ad3a55ef291e794ac3877d490128fc6b72a68 GIT binary patch literal 26620 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 literal 0 HcmV?d00001 diff --git a/Shannon.png b/Shannon.png new file mode 100644 index 0000000000000000000000000000000000000000..b37da4b7caba941988fe4dc663ae99128e1adaad GIT binary patch literal 125262 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

%N_6P6yQ|hAwPb7r28As(@;>T{EtGgOfh_ z#*8ID!V|fMGw|uKV{B!DuN0jp(H*ESB=p+e#xg) zZlBq2KkW$G`*L=E%`rUW1(=RCay*N!Mw(22QFz^v_9}0?U2PmS%CV%S(&m~z*JGi3 zC85E{Dk|o8v|Mfh<(0>EN`DZfOzDCo8klT%Tg+d>gBkuDwUr(yen$WyVWLQJ7D>N3 z;?M+|5C?-w#mZ=9gq%vn8CV){nl<85P1;N91Vk`I;6{+1e}#e>Mk#@{6XjFN!2@&U zv6>>A!arwOOJ7KI(2))yf_)TFi_Ju@#NIstxxK(VnZnH7WqaIHO8&j)wXK6fC#Rqp zN(#R%YHmW34g6TC=7c^@)2Ks~4y<$|PXh12Bq$AW? zK=wlE1;m=NtX)tR#L|dqxM~7+$lW0{4TaTbp@+qjMBGKJCo0f31a3YXS?E8jk!l3* zR~bx%cs9OIm;fH&?o*melw%$4&R}n$_HhBSGSg8&0d9RlAO0B|&JFpS>YE>)plYpC z-F2WI5+K?{gJ0iDbZ>^=3Eg-CJs|xxr(vubT2m` zwVyzo{DI!|_*}Rz%&PN!)ga&hyy6{ZqT?jbrY;>sQ}ApqsP25 zj;tlhoH)rGdYShTw8l7uFi&I?k+LO?AKWBsh6yv5)`1orGe9`7+BNAk3%ZysU*bfL zQmSZTr5=a1Pg#GqB;*{g){X_#Uk4M z4-S(%jnuig?7k58yl&yUeIi$olEof1%|wVx0Q-G#Z$KOcLGE?7w+4wjKcEJP;L9dL zV71h_P&2Shitl=lQA&MQromuobGf2-FLo*crdiZ_Ec1C=%NmVwd+r`j?VNgR`u-_D z5+xEy6Rd_;u-o9his77lQ}>&rCohD<^A+(}dJySm7*U?8+iCPf`YfIZFV4@Cs({#F z0mGO~=L<-mHepLs8HrGWla9Vn7%owRSg+Q{P*)HpW=&K%BuPep0v+Bh9lN7RkXm5O z*%fhuZh+4>Oi+VoDU%kPL+{I%)K)hWziwDzOm`Bbo+G=)Czh{pUy2+ zF>i&F`Y$3R12%1$ArqC4RY_T${FHLmRR@5DE}0XxftRd(u0TFga-y`hYy?QQN9-Xb z`FwuD|4K42x?WOnrDRb1yqg!Io6@SEcNIdP;jbwvpdrH|Q07MY`?5TyWFDtp(;?NF z85+`6IYmiPjgd~mv5VXqf_izvn;|9+}@oasg8E~V6V>jFsf;ok~_Qn<%o+x8u=fG^wZwwnpc=}HCtg2_wlYfbC zztOp5K(0gcV%O!|wbSde(D8TM=SbUcyHChvk+$?_SXEsf`v+K%D@;^GAWUB%w?q{F zH3m(w^^^$=eA35gRAYNSFogPvLDFZ7BOr&m)f=!i^L=@4Bm-3w{9^SQU;DS$ez~?n zF7SAnMBL>r`!9Bnlnfle*ZE9W znHl(GtEp>@BdMukrFV7`RMga@or|Y(^6;F#emzc3Qy}CyyJ=P3c3e_-KBuQe z{w3~s(BBf;sPy+`-CMOtEi-%GvwoRfFMFb0s%*t4x_+Bo{-gX zFbHMb{LZ^ap2N$Xbf6Rg7AgJkeoR{3RsP(jvcyg!xp~??MVtA7qpIVx2#*{<7ISiO zp4(Jifj9&H_zK~SRjOAVZUU0jjBPmuS!+Q!ijiwTJs-`B*El8 z%#9k$|D^c+r;YlrlgbInzRUJ87(xthkjr*RO!jMJlH+&G6guB{8zp+8Y8ypOIviNZ z+n_+X_~;WTz0GLjVEAHwc_d{;6kEpyRGt*7&>R~SYb1E7#jmKID0Z3}CmJw0+Fa$p###|e17;O4XOxM7L z%JQe;v{Wr&)M2>C2Z;yT_EC>Om7hO+rUu7gtC1**w9KhtGuKS3BuB_un`(U)I5On} zUu^8`&~`(=mr3K3yN^1mz`el7Y@VK7r}p#+Bi}|9##bqna1?o^5LPtWJhAwYxNMwE zP_uT4r?DEcP>Q_+_XA~xj{i-XgDKA_CSpu7K(vr_*0ZzCk~hLZTB7(&u2PaJ3c;lU6cBAXOv6Ur4-o+-tlZRUx3vAQkhqRVMAFtF6DDASMLM_sX zzIf!H_5Z)^&hS8J{9C;3FY4g{DLaIOP^A(<%AQC7;S3q#+E~onYy8!uASIYGV8G1r zJk~h)o7!7)Jp-&LO?7i1%pOrCmYzzE^8o(OVMuS{0kBN2lki+iDMUXgSC6pcF%N-G zi;hwj>)P!|aK6uh?d@_4)BY|55>lFWVA-j0Xjrggw;-*YjPB7!B4uAPJS_&P`V1 zSaok}s?sIV?H)-%dU)WPpWmbdriF>_T<4m3&W(14dLN@@TvVHwa(E1Jei#;n1#}NX z%9Kf#1QaNG`}ow@W57ugS=MfC>CZRR4XWY)%kSWO+Po{kJT$Olg&O`4|CORp4-smm zQDCM#=Jb;^>P4bFLC}0dn`%UOu=Q~ym(9_?b7S3!?x{e`&RacuDAV!ZG5x){z!? zP5SP|+u+}oaR<)W=~?Flk+hYe`f(AdMe&Ls)G;gSWTZ{>W0@=+lKGml%sy|}+AYvn zS=r9^hTv#7I|4?9FBLid>*I~a&xp<$JIe^LB-(z{gy_lwcNfKOPRhB3%HLxUTs_nVa0>~R5q~!>x_d?8fK(^_sY+i z1QkFW@n1@wZO>xje|wC!o9%NwBJ+?Py9n48oH)>cpmYCU;{`pqQ?xdP0%G7$ir#IY zkV4-5>z3~Th@1oS`9MkjC&&>yS@er~l#iQ{2gOWc9HrFQLWdlm8*~9`E;)%k);Dtm zLjnApe*fu~mGuiXFg`gonGwR*i}ULUBKY(_;tWO5kPlK7J0I*+n8V)iK>RJ}szz-tqTpBy+?)ZHw zH3qqXDiv*w)|bi;?r@@f z?7AD>3L6h!&1iwU;@c$sx18#cmk(BIA=~cfKre26onO`Wa-mi6{JY80S=HfwGz)OY zi2^tQv&Wf(`PK_9vm=8<`0W=P!|NceU3n@Ms}(U@(9EnsxnenT*I!%jW9{h7+m|W> z3nx9}gCJEY$)xpsdyXSmr+)y$;hMsa)23CLf3jHrhl`!D!FWMqakzdB$~jpXDr1!@ zGoXa2@Sbq0`LdF0lx^g0A#16fm8IJv--1)!lEZP)GAG+Dbtj>3{7Z|0Q66^i>6d3} znR4L~+Gcq~+dwT}3s*}S2E_>Sp@N-V%~o_$*j3iW{i`>#VpD5r%n>yOpH)Tlcy*Yy zRc%eunu5Z-mZk%{S>?rr{P=*z>`;Z({LOK5+c=Y7SOW9(bnLuvox(A2vH>Xf<|2#B z$xJGMnF9lryXxsOXvP03tcWy?Qor@~8fsjW99$Uw?ex8N$xjY=UkOMJ_;V7F_Rk@i z`Gt1#yV^lcY;DOw~P2tV&3*FJ-7jh$k@S4PpWAm^`!EIK^nzj#+iF~Jol7L~J_j8jP}OEm zwZKy_>G9|}-V0N=}PU^ z?KxvziYdR@k@1BgHmSD43H3=1WxAek;`i&Nx`e8opE_Qz8-K*NZ;yE<=ovM&^aDF2 z)Fopa2Wn^|?nH@_n&~BYMFa&+kAe>)B~8{N7Da8pVuT(U3Ev`c3a2~>7_6W9Z$c;Kn5+*IZ*{$r$lh3u(&TaO+)#SJ! z-5DA0z0AxkE&KG+tUqM+l&OEY(y~7%nuqA{1#i$YOxvojJ+uzq{ zgbe^81$)!(4SL8vAeU;uOq1WAOBTfzT3?`{(-?s>28p~R~m?l~w>7Xz#qguFii=_Tq%7DPh<6Lrw<$P5Do z1L9qdgtmiO!_=_ir3i6}6C{tVFT9yJl*T@ZI|wX-ZD-DmR-Uknd;$3!UpM)%x*K&C{W z?Dm9wWzdib1qkuQ^khw@^rjhJ1ss+f+Pjk$x!0UENn<4-F@A_<2I-dy#Cf8x!DQKv*o@;(p%t^H z%YCu&;M*dmAjcqIV+$g`59>chOFPSScJ>Icxzg#@$KB_Z4>^CtkYJ~2ai@g1@2;(V zmJJ15J+@US^s4&0tcjIhQ?OhlM-i{hXv@~M^s*kh%3k26b@My6`I2B6T3hC7n|97XGr9ql81EPS~qmI*vWaDe1a24c8( zfqRe8@r*MX43BZ00}0I^B0%hwv6s(4A7YN(70RYSjFdEfb zU$79ViqQ0=0=ZGz6|%X<Z6=~O+7aTOPbFdbriAjHJmt<}E+>AyK z+UZC4t2Nr*=jocVESu~t3g%erW+_VNKVBy?ayC^J5=t84OFzp328Q8hJy7qTWcUZU zyS2~GFVlpGyYBOF_#RSJ&6q?OvfJ!iwp^D_^{6ow$1V(fN-?=a-sTj|HsW$`*EM@Z zKa0F`QVQ?7I~Vhjry-ldm{DJkWw|L%p!@&+gij)~mNSwlo`9G@obdoeouqqo-(B6_ z<-hB>UvE;|lE(0x9z|gtE!DNlTUQ>epJ)KcK0}7Dy;@^O@WWsk#GHfUbYX5IpF59oeWepy7w$sxgD;&gUz)>F~mtQ@0NT3GG& zhB=hixQW-P#zjaH|1LW*(Ne?JjpXOAo`p7n2ijei=Vn%(M&}rVf{({EZ6BB~jJ<`q zm^QtC&B%)%OAyYVu#}ga9NGHTthGp9)Ur@Hg@ee#hPpQ`^}Up zaI~<$LY*l425t3J1!l6aZ~SiHNBUF3r@e@ifAIGwNVv(@o8Fy>6#Xvu@q>5~`&vkx z?@TbpMWB?;8p*z3SmZ-y2e0T@0hDbt+ffgImROCdqE!Yy8=R+YpE}}%6jt?vbY?c< zNGL7LubbjH0nKzOaZ(^XkS|fxhpY^e`=$hrDT!Qf3kEVnX>1&fXc=q-oQY$ZlmlEk zI!6hxnaapxw-VgJwLlO7`Reqj`+0xP$wi~{#s`ndv>NMCSDDmDxh9L2tY5vBdlVH_ zRc*qEEig#Q`T`#KYrzAlq4>w7=)%9tH~m&ZoL}vFc|xZnX=@WcY2?!T`MnX~WTx1Rz`R*24LYC%J;h23LV9AAHt1Ay()e9@Ut9%58cV?$tj#Vo7i>mL5- zzPbv2w5+TM?I(KXa(`0#WXtOL{a*1(hWo6DvfkLeuh0uKJ88$o&GMXs; zX21R*8F zY+RzLR=LVrmyPoaasuPQ@3N$=E-RnCE*nN4Jmp_o!jJyEUN_Z5JY8-xDkw}lkC&!P z%gIgtA$D8kgU2nGK*r6I!+74Chw_5%2km^5Xt+&KW*OD!FP5YM*{P{XnvKSF-`dowVG*u^O*n>_D1 zt24K>aN)?o1=D6C8LNS(FnGdl3m*gTcVFk`a!TeYMfE;F{X#1o!^h(^L$Mb*Q(Hb4 zH19^H91yiXephnz3Joyv*C}_#PxzT3>5{gIDH;?y&-=&l~pOl1m(I zKu@8y_c{1Z@P_w}H(f3=^)Wo;!WFI0^T!)RajryWUah^VK)oDFi3|{HkH2 zT;5Q_9;8k#>qy&=T551Yh#K4CybWtHeqr#JVA?_Ly8FrB>u^uXtya14F3E-YC)2e= zIUfNbfn7h`2U^i;^w43NxyFnnNp$1jf~3{&W>uTc8&|ixl<~6s*~yJj&YG|x6;~nZMtl5nv;O*J-K=FPrdH^1WPY#K|65Y+)+>SzN7(x#}dgdR+N%+6z9(S~43 zguY9_r?l7w z1Q+g-#H=Q*c~hTzLp(0~!$`;7&Go{!LHBNXNAj}^A+g7=`H3R9ES5M+is1^fO=YCd zkj`u?iT-%<^?c|XS1AZYU)o)+$@JlUIle4ICW(2(Lf^<$Fl@SABYZ=Y=6UmT>OhAb zqX!5y#41I-?DkzlmV)K}_S5$7Q6T}L8=bsY^%P%Ikv$aJV?7P71*IxL7Ntt(JYc`U zokUv4opf)1;O1au!SjuD37+h0>wUM=t=ZLE<$`F0Z6l>&xVpOHM(_a?9c)IyGdt9` zj(@l}*4G8UoB=z;S_SS6_+uOzEVdQVKFAw*QWWYRDiGYS$ua0%kxM`YP?)O8uI}z~ zoFvM{U54JTuWq$$a*vm5Cbj&zul#17j^f^3ugq{rBRodmh%d|kNuAvv*LqCCH$DD_ z{A>0YH?#wT`b-XwKYI9|%&^J9*k=hqi4gOt{^X_Sce)ow!Hq^~dC^;^b$({&xwF}P^*SmcQlQY^b(uvguNjQ;?rz z%StTItBci?l@E_v?)=KBp()c{wz0ZtBye5QY&ZXsBgI)$eH^bsn@^g`UbC<}7I8I| zyu>|jioCAz;xqfmWZ7jp%5blfXv@M_A+Msc)=Y2U_?*cSr$sRg%jjmyB{FLajSsyp z1GVMW9{&~9Sm~VK*j*l*zMI!!7a`O!26LWLe;%IgumpL)OW~`Em2>apj^B@EmusK4 zrL4(2C9uKp#z^R%UXq?3;hXZap#B=wQD_QTZasFg#l&)8t^XqF6X}y)PBtb&TQVbS z3A)>^@Dp)QL-U5|u5eLzm(L}TLjcBxoIfG*g7Sz(PrKN2It&VI91;MV4Y*fcj)jQA z9!&RZ=&0}~DjAJ=tq`Nfe8{M$@tQ_9|X$qb|dWAapnIfE%u zl?4=iG2K%kKZj1(09ifj>t-s&BQHPMN*p$&VK${JtCj*CY*U`^PGnr?i!F`eeomf-0QihXB@i|9Ze@)VKzK1((KFS{I8}d_Yjl@p1QafC zZC_C;0L?|N(n!W+(g3FHrLDLw42xLMMZ#pbO~kP1T0-J#9RIr60nO0U=IG-r7nhqm zv3P7=l6cNl5u$K*dP!PWk={k!4j0dooR{`z>z4NqFMi;=sB=HJB`%ekBkSen$_H%; z&e$w*`F&;P3(L@Z)NiF0bktq^K4uOc`K@yW9k5`!xmL-XGe%R?N=kQ$eSrxDVuh`w zY3Z2VCY?T}^Az6#Y~X;=qJRw%V8Zvk;9AKjBCi(j3mln6m4nJHoZNmxE3F+=qCt;& zWjllV-H%H+bvbF^n|Dt}zKU~Yv^#E!P?S9gzUMs~Jb42Xm(P_Z^6P|V$ z8J+N+xQhs^g%u|C6mSQ(NN0AMFqL0*J#W97#uV??lVp0Q}grd z9zV(a!Cr_Of+yL#z=t&P5x|GyLtw*%OoT=_A+;1zjYothqV;z9jj8%hdA-=o-{)t4 zCNO1eutXpKNDf;ksHmg7maHd4o%rD12|(CsUU$b61EI*aJq^Ckz($gD>Ea^QA<$et z9Dh)hVaQAsFOtMzF(8Y6s1K~?uzp&=H69b`de){CS88=!T;a)TP$a^XqDxm-8*3%r zD6MMKj8GWJ3A=SxLYMmdDZfy7PI^%m$ z>-Kn};Tg=oJk{#ruh7Yme?fw=F__B@80<(Sh!H-^EvJZNPZwMyR*Ki8XiA!Se(h_} zneoL)z-gI2r4zUZshxhBy;!IyQA>-JsVURPikHdD4RVG(BPB?St2H<~B}}&Mg!4|ebSh~PnK>Ho!pP#RgNFkZ?whN<1%Hs(&1vy>I45x6ahLF#%B;W#K>yK z%&aqz$?>&H5x3TVK|*V(Kgby_>A;{_T^! zyt;FO*OVjry~FCYM$1$enuF+W17Y_cT=BZYz*@7I3wiVcNn_G&9Q>5}>YS`!oV zy$I}9>XVztIDzYWUg$5o-cK7lf0i|5|G|l9ICP{DzxG<8M_|j8@RAcv>sweR&}*Hj zWHe5Y?xip=+lkYvybN;7xw)WM>L>AB5F#Ma#*oHRfOdndj}J!eE+8{bwQ#flVmi#* z&u(}VQch-v&C;Iqa|uwVU3lEp#FPswrDNnnL+2yYeOie-YsuB$;^5?E8lo($Zol3g z38};>LTJN3W^FICY5|C}X5DXR3>&Ev^($45m;vnrr}f`69B=xAsOaivHe$N1}2 z+?_k=QGsnrrZ;UerF#J1(n7Lc_&HoC;!$C&kM~PSRh866y3Z$^KzCD5>Bb7o@fZ{v zLYr4E;A){TmQD3k*SlLjB(pz1r!vrF<>Gqq?vWh1r5Fm1m9U{sp0MG6dE-bh;+t+G z<7z1X470|dBVwcNv~_)X&f$i11-`_R`m^gl%#|RSAm4vuyttVdAqK51;rE|7zf6Q- zNfs)Q)|YX@XB=x?pAtU?jHH z?KBLP(3axz-|ays9d|<$5+ukSHDNS`5`gm9&?u{XryXnCj6%n}ipImzzGZ}i^}<;b z<_&D`qoBo(rPXYhTAGT(y}X*$w<*4Odn-hOV-m?$tz`KZd(O@;Qq^c^&vK3h(o#p3 z;W7n!w>G6#L*eF4Egk!UE9XgR77KP(*680u-Z?Z3X5Umfs-I&{bG{OLtq$-IRHW>g z>P!k*!wLLSBU7Qk;6FN-zu)zK0vzndLPptpH}3kUYS8n{nan>3HhyCbdPg3Up9 ztKHwk+^?<^`XQ$ssHi)b8pNHG*AGLzq0Qn?A=#C6RjbTmc9ZE% zbmGK;ZA3ys`Msd+dilw$2u(Q(Wkz*xKNBGqDFt9eXL>Dyk2C1~(jutyWGcx(>1~)- z&=S;z2DT-)4-1#N7edO3XtjtvGK9-i)2EEg^0ugp}`GX$NhAw&yd#+&I-#Q z9)6zTBpP?+^;mGa7Vy~g@9O5~ou}06J*#vb_@z8BYWhoFX?GU~yC;1o33#v0LBq+t zu%aze8P53!jK&e0SChA(V$nmi1-JD))ZAvRd>^TOy|fTv{CltXm&)+ZZTb-)*lZk! zlFS4Cz1Qz`l1qEz^~2{7K!sDX9eT9?We`uS^gMF?XXYTw{>Oa{@t=|GzzDM{Myp`i znd_DuYW3qc1_Di!j`d^I1bhtG{^j0&G|qCY)%P$L1eVM0JW6|6MUS=$Sx14d;(Olv zNiYH%1!H8(((=bMR>`f}%p7%oam~}_Qu!Hb=iiG88!nc!c^x=%>aI^#TWhsUj8BIgPCLCRfJW3L>jvTe7@7AHmJ^{OAl`LCcY z6^~6ZNNuqca@QC#LpdTa7HQV>cB4$}`Pa)5#32i@hg`t1145~ZyU!{bNIb^6%;?(s zR-}1yk{dh2bJ=KdIuyghL%d4W2>{&JxT3PM<-cL~cwU={q8hsakT%Dr=g6AJfPre+ z03DZTTg|!PU-ZY z;rwzbcZBf?>*W0Iw7${7DYk7TCRtc-R#47${h>efmJ`@wSZp~S&YENQ(^ zw~(;niXQP5pS>UaBvCz39AcbMyir_Zd{2tT-k@%w2fkQZYIXW}n*p_ zwpO@bpE*&l2TM(ttJie$WGC}ONm|*%EXhbGVkQx-R@7Ebg+4a=XojO$zqHd8F7}1O z4g^AfK@hkF6=9Dc9aoCyU`wOU9|(zyG2|)>)GgQw?RNpVL;UySu3wo7jde}BlaaE; zI+a)tS$~;oOpMS)k1bWa73+mKj|y0VM9mhZ(bq#gsfH2?1HRenp|3%)gZ~UxG~Zjk z-lNczdeBk(bi$iDD8{1{W`NH=MB`_IN#Ygfy>mos{i%MWs7f9 zHs?b2cW0z~VkUya5@a6f7bhmnO|gL!B7F9b2YH;}-~J^>_8t_tv@}G6M@Fs0c_<0^ z&Cqt7+5#yIldevg1U!A1TBYkzk4zFQb50+zw6E8Sn4C&tpzXALSy0>`5l_Ur(w(oI ztaHcb`L`iV6qG)%U#?l?b3H{|ZM5lPJDxCrEspk_+WRY!F!x!%{}k07?>S78V>)Gm z+{II(h1%&oSFcO*=()=^{|>GO4LR1mWmjT#-Zb9N|h7VXBNMLPMC|Ee`k zKEVuGf>HmbbUcb86sYy92J*20+==t*9{dMr+`O`5b1Mf29%pL4q?GL*|FurDa9eNl zCfkk9K?FGCZ_TG}(-;(7TxC!Vd<{5|Zb(t6dY){5a?eBe!8w5w6XlEN-{HhaS8((! zBHZTryVTEH1Yx{uqSR%Q3Eo>;mR^@|f}p4)5|hRcmXRPBhjKrI#W_o93btj%6F#NsPB}C-Mo!KU3T7ZAEB9y8bMJLP|!G@ z5nR&MCe`WPS*kcWvI$8oV+aT|M88atX`lpZk#8eDkO_@oAG?(dKh7Rn{IaKbVPcrP z*Dfj_m#a-!@F}a4re7ElEXPMq)(E~OMqx|K)s<+VfCyQ#QzzPyj(9RW=+gCbcSOavyUGC%#=@;6%!UzuqSZdf)A3sF zI_sxj>F%awl=7wlRw?`*^cj@w(#Ds@6XbqK@=!)px>^y2L{&whrE8Cs!2QR3?4 z_vpQe{XU^mf1d=+IM>Y6HgZJ(Q#oeK-$SA132j-_vuAEjhT`>C+f@%I1B`rnUN`@;1j-*H&X z5Fs~i0w9Bbc=%x9lnL-%#_8GaIJViD=xKvNi#d;zL5X>?sFne-b0l;ON-71CYwf@A zT};N0kR`7VjzvW}9s31yCuyohhChZ2zn3Iy$oi|j8gMN>xjl@DRgt1YnO1T6J(uE+ z)6>?~_uw{?*IKEGb2{8M78l&rvwQ6phL`vmbj>VXA*` zH8tlPIfp+g66S4}KYujRVcJG2fu;(&;xB;>It|>cXU=o6Js=!{teEYD-cKz^0T@Q( zmb%Z0x(@He5?sLuAHFPCYpC|Csf$;~B#6YuDr9M<&S2S;HuTIK$EvG4QL-wX%$Bt( zxR{m3f-&TpsqMtO>BDjQZJ~~HhfFJ?Min3jgv!-dW#HQgq57Rse`y9YM~%uKiIE6W z&Yx)4m$C=>n(&XLo^ds^f26k{smixXKF_l5BIO8o2Cc6wEo&2Cm?tO+`s_~~-r;wL z<>nS8t+XCIHQH!~`@WeNHoIpt7+?dhk^|3CAb6B==XLw((g7jlv6R)X!VL-!hi# zIM`^n;eNKcVkVB%wS13`a@x*G7K@5n#s+SH*h`W}%N{Vf=^=@%N)*nJUjy?`waf?} zQFYw!M^7n?rVvz=bbJR%8qI!^1Xj8Zm)XeL0dvR3yg&-Eg&>FMv~>;n0ktEjm}^zH9jp<@H+ zJxKhYJ{CDN-%j=Z{2bIhO7HDdctl0den3hiPryD#Gi4FHFE$U-^h+7g zEwBr*fjbOF&ZBSJyp`Lq4G|9-GK$se_0JU_#H1wW7wDX-3~>UF+?-8lcO;NtaGSzV z*g4V>aRd)G{TX&Hxg9cx!l8mGHpBW+8-zXAq!prs>}l}^X-A%&^o<$j7y*Oz8i@*% zK8xHDp(wgkmLWMYWuhU0(yTkKLvWg%$^70|n)OAFOz$O#@#7T(HnIk{o$QbahcLVS z6J5ck>#XPihIc6&7H}C6X7R5ulEZ>k*D$g{x5uF#R$m|T@iFG z0e3tBl$wTqEyP`CW!X&8kTAo0G!z+uO2z;V!9ofCCrHoaBk4*r>@=S&h zyG}PPHnmo3+#P>5Bt09If{f&;k~s=QiMlirApWhk;P#w`ay0v&)(h!Qp^NoPE z<7HCHt8t6~qjJ2?tlPT!nbh98y}F?a@stq>qNLw?XYc@j^Bb%GujtQlZjQW=>_V*z zDUOY+Z1C{V2{6n-Hb1K|zZ;c&$HGW6`gQ=5q+TkFjVP<}@y?Ns?%=>A%?^$cm0239 zQ!-60bBBk6rL>XZwsO}+XqlMqRLlncC?%N(KM`siv>7YgLDk|cAzcx3F$O$WSq6&_ zID|gYXR4{p2;VUwhK9_qWj4tRH9MnCV`kH(;f-Sbfo30r zwZOHLW}nPsJs;OBbpDdlIwHzaV;$F~Oq!`l;^;DJy^aMBdiUdj02)eM56?Js(RGR} z5l|s1#K}{snnMme-(v>DOk$29ji4q)I%lZj+ZZGWBv5Eot`e00#dHYmZ9wSH3ccu% z(v#y8MjZ4L#n~HXA4y!C97EoOM!>s9G|^>9XXcJFFdh0#ajo6`^K)+d`_!R@peDH; zroE6!F{QWu_{)N=XOQd4)6+y&ZB7XmCf`}J#s6XHE2G-%ns#vu?oM#G;OELt|ARQM#!SjMKr5E+E>1Ix!{f4eJt@HJ!QZ)jCD~_Gs$7L0!tB`TNBL6R9 zhqyw#>E_>dvq}|N%!g+sqciWdr)~YU$2mNaiLgMko!5I4sRFU%!1t`4)VS}@TP47$ z*#0g|p_0tHI6(ySAz(VonR$hjso{sOE@60NtQlB3Xu;{Lxs*|#XC(f7kzRM%jfPHr z^-&Y>%AVZnT*fBD4VvVvt_TRAKw1Q1IL7W|>W+UFvRx&92nG|b_w4Cf6y)S;D*0Sy z^;7nB+5w?GK%K}}<2Y_wvy(2;;2n6$So%?Qiq9D(cVZme!{&Mnj6c|Kk>_!j>t!sM zCI+YJvXqp=I$B&Q0xw2qx06SPFaRECroGF{OEav^_G&CFt~hGI%1B=t$`oxHX0o|2 z$xv^1{ff3{m*&mLQ47g=X#FHvy_~Y*-&oV0>9#7hA7(2=#(HU6XCPucV0Be|hBMyGMj)7o+gw;gn7(ui zsao1ePZmj&c&kQ6s7Phvs68_ygFlK?vdegMjOUv%3fl8}5Z1z34|K6C!-gM!`qkjk z^+T(rkQ3|gpx%!#(v@fWXMhcVGZhtt6|+rURFZ1)eBee&Z=c+5()-w_BanxMfFHK8 zVM?nFQ}Xa_&9`y+J$JpS?J}j(pxS%f__^QspW10bn+6?bii(iVX6rWQ*B2OX(F2*dt$a2Xx`S#boSg)n1BzuN-*DanwOdb8SyncS`7BFKReLEIaHX2# zl$dZ6Gayp(gBkOT7Sem#a1DvWp<@DIr)?LC#1B^Eq=l?GiAE^}kKQfC`fIJN!t^;G zM)v;&AFs4dmdP)6`zJ6rWz*d}m*}WS{^gkzRpoaePSpAu1MEQ74GdKX`&6-7>G}{% z2_V+x785H5xq-=+lS_x47aG4brnc)3Sk2eeuo42A?M_e?73Y7-uPDPBl+GTLheuh~avaD?KVgF)!F<6O zadmUhctiswa=alXaT#+JHPywNBB~0TjPtwUubPt)`(gpJmAdizn@uX##?6${Xqr)= zD!l-~jNA-`8NuqDR2{jk> zTOXsi>F!Ze^dC$(PEdEpIwB|6XDWBuxLwTsyw`&~z0MDn&A6IY#dpYMw_d)mu00QLw_n~Yo$+3wZ*vC3;Or_0HJGdP`vH8l!!Eo<^h!i=UDg`% ziTp?XV;@33LN|98a!d-*q-8sI2h91}`q>8)zVBPf_21Tv9whP}BvR}5^4@mxb`qmF zg=nsjMI4*f;+qdtzj)0oPZe)~GiZNCS!F)JUWW4aprm0h$5+OS_9>;EW8N8G zl^7TYE$%2c$1Q?qTgokENcD6Iw5$G6|9D+xzKp)&2XJtPWV9g$vws?~TnQ-Nd@Q(e znUYKUna$6EC3+uUU#Z8cfBsLep1L+fm&AaF3Hq6#>&hTh3_?(`(kHz|M-(iBFTWOV zRT%90`sbjO;e_gswT0XQ>9W)O6Hp$?M=?f=8u&Smqpz#0#V7w-+_Dg=(<2m$9V&#_ zBZ497@p$H_hh+OnF)}hPi={u5AhC+3?9yuHyrlrmP`)7)tepSGeItd8-$Ix39hSos zc{%WsX83n^T|RyJsS+T92eR&(C@>l#S$1>cE9B_1n#;*urEH}nsW%GRr>5skRWRD6 zH}EFLTHMBwQeBdz3wKhT94tqz`7u@YdwH>RkJsX@!&lxZ!B2=FPEmE#bF?0+&Pqd5 z_+R~2;XP(d`V34XcODs@CHg*cNmL|};r)fqN+vc2IN3yjA3}4$pj|dONx5~|1!$%Y z#DwwcdzhVN_qn;nn#WcQp>--MDgZZ`ZL?6a%!Jm~}l)(mJ0l zX!TPImkVYMy$*_XULg)9>$Bk_;au^;~-0~HC)|tjY zhG~Z7$7N?X>UIuUv=Mj3HR_LwzTTspj!DR8Yb<-&o|)NoHj1Y&H|_kLiMNdgr5V$C zDw{R9DPpa6qht09;qaTkv~Rp>hS%jaIa0@d1$%as=OwZH;9krB_wDXhQO)8<$Ep#@ zss!89ZJf0}oUp`?vi854s!$R4PTN(k;}J4~YVomdR3GF+Y0@Tm6t<5yK~hnoG1`eF zFEd#w23TcUyZ^7=HElRNezJ}tBm0Js=t2Rg8J?3kJoM-mG`%!Kf8iwEBHU_U(O*bA zBXvHPs*NUhaBy1f>~8>xp{-(L4)xVVC`5%IDoY$T{-a+MpG$7(kU5roKF?77%%%gZ zCYE5c2C9KsJvLYnfS;{7ZAz4zm=4;5&$~8@G?*6BL6&ttbSgzR4kv`5Y&lAt|JIU_ zGQ}-wg<6rJPC^VAzSLlLbK+E~g|G6KS3E8!OB4y4>rP`^ti}(K%;iiRO!CK+Ak+5^ zncl2s7cz7p2+Quow@5>c`jDJ6(hs8_t&F+g$mPa}O{;_$`51uU1E)&cv_*OO`TNah z{slAG?u++Pw*@BK>H_(EN+PpqBb01A=gj8^Ga(cEXt&ZC7Wl*)AywIRA*^`a3J8_*YN zio1^R4vQkB8-tidR!zD_Vz(r7D`=FcxZhzStgWC(xz%?G7U1WjlmF7kM8m(ry&D;> z#>8Z#Y*@Q9qgDGFz+D(nTK?D3YYzKwX7SX`iW17eqBY+()C@X7#IaLBZJ|yo_i0(# z{HsS_lxN|*{aT0C-bl;4v#^4K&h6;}W3kIPN{C)NwOh_i&_+B$F*`<%7B#GsO+;`H zzg59CUagtsoN{$^6o3S>IrS6q*9q0JfF^Rk-_@?`P7%ft+uv&biJ|BZ4hp5RS@o2k z9%W3YaRzU|3v&*0D4UJiH($a5Lo{rhY>E{Kb!vg9UfWaS5)Y-K4Q4-ZFq&=+e5OKN z7U+qTRi9*bsX#01EqZny2x{e;P&XvlM&dGY#h7Ss9A`DBjEkX&&|o)m8#{wNn?;xp z()<*2@NBj~2(Svyp-e@|o#IN^cfI2t)@q@s*rCWse!^UA2JeKpG|d^uc2@sU$le@t zUe-+bR*&CV8?3pozqNS4%&CLMt^G3&T|AoA9PI?6O6`uJ!-fq?z8jG6tGuJtqalB< zL;Q12=!&A`xaRlWZDZ#Og4*&&kDn3TJ&wBpT}9o&Rrp*V9@x&L&-Zy?T`?U@zY)-9 z9pgc1iq-WN+&_z*(K`0A_hvnV&H`}-*o z>yZcS9U3m+Nc{hwx_)PIUu$gjcXNOve!op4zqhVr*Zu@n$bZ;$J!NJWyA$YK;CQ=f zr&9X(5(l32r1=vQ?4iBT@G%MVgmkf9zAOG~gkHP}*itXdpT{x$2@ zlqPHcvY^$h@OlLll^(%3%3Y7A9aV9d=g>4jHxJ+}?7RMHvBCd#BW2N3EtQW&hVW`0 zC7CXIic2fZuw(n`%H7Dm%wazcr2kPj_`&AE1{Fp5U)lsAM*qFyS%|iHyeiiK8v1iq zLQLVHLcYnq``~^t>Z?Q|Z9(&35;KJosyX2iy5Bh5x=msBC|ec5A-hMfd#z<-#XZFm z?oNVeT|01Zaz1q*Z8KE<>Gsk140Bb%57nWBkLaDACA6kqmhpSiU`4W*zM=w4hU$D zV~PcA{f?ho#IkM$>+f}13po6zs{FBwJeC8R1GMMI@&8X$3ZK!l<|oQ02cAlM;zh2z zlMTdRRs?w25s}r4Sc`Gp!4dLqQMG77HkgQlKGE*xF+hA}QOYjm+xy68Io?IyX?13#kM(iYS*G;wFNQlySKU$K8Cx81J%fYI2bEFu zUxKrLS1g>>V9b#a#4a_3lWKrfO(Rg1q14`POybc{MhI0_VjbW!5UT#vgJ(@h_CwZ% z`&jV1q`Ci`B9#KWw~jGq_UM}YP_bBR*QHN%kQ6{I^PSqXLE<(&K@mLqN0?tsq=ux_ zwi8HLwQU;vTyFnifXcw_m%O`)FD!GwB)$#^T(FMT$Csk?cQL45oqwyIWEyUs@)U8N zrvvP_eO2pD>04rNkgC~&WK~y5AQr~`LuKTA*n8Ot7*_?7j{R$k(ce>clj5FSUv3T# zd^RoT1AT~_sR+ys>jx0>#B(%Zn7YY$$3vRqvIHcSSieR%$wmhC;vhvcN(Y4GvkTTk5wEg$x%$JxZ>GW%nfqOCdPd*io3Bhe+5| z^v)bvF^k;cI778SW)Stp?}@(2HKsj@`);f<^>k&KswznejC}1sXW#F`zNd>^>i_wY z^)C-0%;kHR?Fuc`=tn2myGU=Tl|GpB6wy<>yKEx5F^e?A+ckV1{9k9^-<}A~c1$>j z2oFmz)JyV3$iyB>&UJ=x)ZbN{d0#-dMW_YC$#>dZbhM92iDz`N2S09?89Vq~Yxel9h2 z94|NPE9Qv&S)={W>_3n&$d*oCG$A>X<;g1nQIMSw&?4SXpQ5CwXAG{F%wN}(@r@@2 zf2#`Rp6A-jzbEsAr_Mi;6HaMB7nG4X8h=67EHae>%P@T5>6AgypnNlX&1K)VfIu`y zfmdphPXThFx4-|^d9YDCz;7eS0(@)TwC7)bj@V*TmKnxx)P^KB{|ly-(yeqA$_OKd zhSSVh3}GFH@D{!_m?A}RD>!K&7XiL|6LLF~*|JFv{R-j>Pas}Iw#(~~@xxvVzI3AM zNf~~fyAsvxqcv+1Xu_<{rD?2bsM(cV1!C1&>Q*gTk5%mW5h%;ieuNo}cVi+;V>mOy zH|Ni5^m4x1SnfG4iy^aPXo+rSB~ZRBvodttVI!ER-{XQSILD~;&M~;koFB@A`dYav ze8%h)rOJEhe495Q?0BuO93Fgq$U7l!)m+nno!t0`a2LOaq>M`Gq8Yt8A1fVI4+HxM z%dF0;{ zV}D$1=-cRJH=OdHv10#_$OohlTl%}jjdOTuygS6e0?OmYxTsOhy`aDMF|>cq^_93Z*FRIT~!<4?tn9CfnXG`s3(F2ukg4)4Mil% zaPIIx0B4)VDl|Pjy{rmsxNKFXgB^}FN7%8~Jh&LQ)^~x9;Jz#Qf5)}Q{lr%LQ1^Bt(P7dk*l}=zyfdYFQod{B;Y#yIGfcwoQTtD#k5Hf@k!tI3i7QG@1^E5o08yg1|BO|Jv zO9@AJHTyhm*zp4M%jUTSsvG_^Nd9J5{X5N`6muH|V)@&mqB;&X?n$V+t#owcTu>Q- zWH^bXveuD(11e_4=OPExx!yTG^mBe^rZr3A=TC6bsPLKs?C_=#2x9#vt3mx}R34zO zsoK+WOM7It(_?vu%Cbh9!zl^3ft9JtO*kGeqgjlkDB|{{r{Ghs(J&~R7ZJPwH3UVCn`-K)sEU)bk z{gr-v-}+4JyI*>OAE6p!{(x(eNsmw*ER+qDQYL&RWR;&xT?i-!Ou;-0y{jCEtFkw1 zzDJ(N8QDNBrnr`jbL?5}tE@_1*ux;y0o;IyqUEZ)S2Go4LoEDD4|<;3vfX(o>pZJp zsV2|U`WT;b$g!XtBtO$m!%hk0rxUzC4y9MHg0pLayA2iu*4#XlB$WKh$Rp;j>mdZ_ z6?VTdhRDK^%Xm|j3;oS(V*Q!k<*dw|(|lR^i$iG3R>=s#x8E3~a}WoN*s@spaf__-4Q{U$kmHn#{62{M7&nd2XLH zKBZ=&od?18Z%um8Jo@!$>5n{1DIKAPgCbRses)Ur6n8{O&n^o>f^-A9(*T6a0B+ z$HgYdL!eKjqtr8wBE_=3_2*)}+UxN$vTS7yFq^9=Za*!k?P;UmxZ4Fu2qqB;5?N0O z$p4iq!w-aX^g%w>V$xKgb34V`n0*v8K1xpgmpKOY`Jt$O%2q0NQ$g{{;_X4xWWc=* z9FsogC2b;0+2&k(z#*$F-VDcOj$*!JI*W&y>^O$%kr^>lm5R8K)`u12^446U+V)n? zUcUqn-{jo~`+Q6(c?bqL$&OqAD18`AoC=vs!vIZJJ_pLiAiJ2}7+T-&ssF7@yOe=(k)2qo37*u=7#JkM!xUnJA`x?(ds5K4H$4y>#hTdT3x+ouf%l-Eb?>#- zwxQ?I?u1O*D%OkEAMg}&1sOxqgqSDLxE6&4(Y9leM^UYt*)B+5%$&yFSQnn8z%A<~>L5lKDS0B%i|@sXDLBPa)CKCrujinPY=U%HAMg#TM@ zcgPLuwt~fkS7?k!0%>AYm%k9}jm@B|1(%}dVWS-}A&?NVj9 zn*oyI@5aHnBRjwBJ@;WB`pX+Lo-ln$_K#9nE8CBxejBDyNZV}PTAl7- zUneDvf__n+VmEn`K>LI5?TV&zUpW5S%XaU);kU5_24osqOag@@MQ11I3rvb_P^Epp z!};At7C(|73YS$!p`YRNl}J?6yF3f$1l@!~9!B zpP@vW00%3^&rdC|_BCo4p0n)`O=v56EyWLsiII%tjgAL$5T`O#%Nn4d zD0h7-6Dwh0C<0S==o+xxw5aS^)k#mCY2FqZlLO5#&4bi6B#WN1to?R#l!_Vd#66&? z>VJ9(Ym1D^M@nL3QtN-LSd*AS=7gd@1hNU3E;apUz~&{56Bo~E>xnIpL6=Hr)M5^4 zkmQxpG1&o|N4)B~tRxI2l>;ZJZMLv~OJ3$x??qR|m_hdB4rFrYgsK#BEdu1Zg& z3G7OD6g>h>HuK52TBOy9A4!RU)JRT!!a zat#-dB3tP0d_4TUv+O!q-~a3cLmjxnxH)BlDC2{@OY+^g_bvCqx_Arv;Mx4w_$F6W z1y<;wn~Jz&lB@2soHOSnnmGVxuGTI9`TY+Glq_L-Krrin`ETy$!Efe%mrqJNnYu1t z-8_Sl&4YAJ_ua(A3|*#6F!%*dJbmJ3ZH><7Pb)ZFYr8iCZah~OrG(y|$EW90Qr!Qe z;THQrJ8zmwgTTMiZdD1zn~WK(N-RqJe=83K7OMR|<|*~kF9#=^({OOJ*S7vQ3cAu( zvdGY0C{EA&dxx4qn#W5LoL`46hK(w76lOj%oK^IrQGaQXdBI+j?hgn+rda6oZ`28X zU%g!~$%D?&6%+wdSNhDH1RuY}!)R;c;SQOu1@{>M3jp;~X>yUqY&F;2uLdX;$8s1hU~^C; z00pADE?6OPopr`y!Fh^qj(UbIj;&f31*}`9Qhq992g;B9`}gx^s&TsLtwU~f_Bwj_ zGvTq$)3E0bME6O9f7+^95F$IWu$OIi=BF_W4|{}O##Xi4CD#AlcilHCYtiZT`KroC z7yW8-BP1lu7@3d)<_Jd|uNJ{6_B+Sm{Zb<(Y+Z!e>u9HAy;RO?)5Yn0|5My2E?B>% zmsi(u$jQahVds7p)C+h;t{ zta`$8w)2;}jyth;e~fLaw+kxvqlBe4XloOs`?(n_&F=82jI2+BTF^#2E{uIwQJ>E{ z*>71>qo&%niv|Jl`*V>SW+&^cmrW01`BI!W{^nqI*nRd zgoD@1t^a}*z)qhHC#hY4f>nKr#fgbic4U8l)w!t-YQo_bPL)Bgl=4gZwM{hfG)#|7 z_Xz&Q4)KEE07;tjLW~VX#lnm5;2yn>4(plO*&+w+ghP@*6b%JYoW?SlAclQ_;^*V4C4)AseUmz#cTz3E~f?Xmdi|{3i_xad|{W^4k*)4}GwBGDmge6vg^X zEWYIh?-aLj#VB#Z{B#VTMjAM9k#pu3q=_ZO+o=LKKpB26gs;KKA?E+BGp22ZbG^~G zV`z^S==YU)u=7Jsegw|yauN)v>S`?9N@hsjoz3DFbnsBgG2!Q+s^&wDjDc5}8AznsG02t^R)@oVG%2vz@Xok0ob znGiMfdi1Je;0%|r*dmn~AVHxHN8MFdOdO8*K#N4_b50F46Amjf`1PScBlV)%xqa33 zra&B$8vGv)s*V0|SFP(F#}T6=C_7~oSCAj*eS2K*qPjZunt%v3X7&lqruOBUtLD*~ z(PYD?Bfk!ek@;GbXv1u}qXi)k`ei)6zh+TuZ%WCC$kT_+twa3nQLN;oucl9VMFrbr zvuKQT7-|K`zGzPKsQ5J073707wKZy<^C+;5OUQ=JQkUkQREh$~TVWB>k;uC~A0}PvL3nqEZ@hcRa1{I^Bn2+@HjT!s$Wzz5cW8U-c-oC+!xHc2eY-I? zpK+;9bk6r%yuqS+*GVAlraC zkE!0y!*2bG(ZGOi_(w%Pl#FD+wLgr|S=*KQ%j3;x4U!adZy-E<%nI#cK1Wxq zz$A|=k%cidgCuo0xMjE^FbYd#9ULxcBzgr!e5I`~zJ1lfLjlH*sn6d8w(vVHTMzoc z!g3MbU*i5iY(&{DgU9K@#2f@Yuc)q4D?2X*onPnq#?r2Xkh9&OX65$en95KZTi6dw zAk6bIR{?gnF&?3Mk+eLKp>4!NcL|b|M#h)to3wf&)}V)b+~JGj)VC&FE`^z4)Gud{MkA(h#%QxQT@ojtil**2VQGBzb2R^2Fz!W**>Q&UsLdx&+*k-F(7%>e*_ zIVK3(fev$c)%R&^nvfNp&DrT2sR57m!KHKD1_d0dOJH)}b)(U5F;fb~WqcH5*VZXe z)vOI4ydLHpi`yt1FE@9oF#e^E;SyCNKee}%5w-#%EB3&=F_zQChWALnq4i1&-}L;F z8KH2r*mjx#CZ$l1$2o-D8p}dD^kk`Z{;R+#e1$T@%#KxFcqgtp9la2v*`+s1qL=O< zj&eF!cJR{@&Xl*fm@v)BA-8?PCj2FAB0>~`KcqdL%Re5B;?Cw@nJfclbEk~rIw!b* z&uJt#@ardV{Z2uSjm!#ra?8&9JfDR4(~v_4glUdeUpP|R+N(I7o%WRt~?D1LQp(Y^f2TG$R))@&0Ce5;N{%B1%=t_ z0zMJ+aFF40Le~~2caybP1wFMW_hfL%$@7lwZqQR)TT%~yRIWRu_~YPs@E8X@GrrRS zlVZ7Es=sYlfJWD{6_6o4&Izkq)2w%Zdf3f6u>xE^aWlzG=ypSxp~v>Zbx_3 zir_+1idvl&6q@Cvk`l%uFpC~P9b05uI+tl&^34KfO~+)7>|IS3Q`JDMVXa6o!od}AA^x;5NjiD}@W#Z%WQFf2 zJ;s%EIVZnF->i^Er;#v%Te~XTKnnJ+Ca*H<;}9r9XM8KB12s(2?f^Tag{N2V@zO9{vl~Z9x$8zjniGJUZWMf4SegcEGY+;H z9?N@1BWMBIaL~28k9j6vj9LiZs%}A}z zEwL5bcC!&?ZttUVosWGLav8tW#xFCn&hcqlJVdk*4VRg8?yOgnJQVKL_5zR!%imeTg`FI+%Q;8uZ@#ifyp1P714LyZ$iJb_@cLwf+_!faMb{X*5(0r@+-=cwB}H@+$-y5}zA2jX`<3uFDg z+;K9a->OP4j(*EL%dI6RKla;N?G*x7-KR1mN51sHqb~#!pi0qT-AX(6-Z@fhXm})? zLY+}OV^p1>SimG3OcCd?kf2~0`msyZQ8Xxo07-`ih49^Y6qOu>i*1|jUxza0B*py3 zbdLx_eiRNW`Bpa6x-=bIaxCOQ7Tw}9RS%vx~%5#R~b;(8CuWpPpr% zs-Yn_^PB9dR6eM^Ik&9S!bWRCHO|7#J%>mdky~tR>`?kd@mTduXj%v|z)+F0OvuBE zvV^09RwrW5ajO|wO86oJrlE;WY8ANU;p#f|7}u8UPdr>iM{IrcZA;jQM_7$ubbrQn zu81}#!TL6w*wfZFtinZX`8?j2?;q4Z> zl%v!G7n#r>QJ`OyIZyIX8OOJ)D_z1^;RI>mOb9|BB$WcNqJpB)f)BtRF>+AGS(6JP z1`YzC=jU-S4a|e1W?1BpJnxEjiTu0E;q_MDm7+kxROTqejGOLW_4Sc2-=CHUwJ%LL zgBwEv$TGr#8BQ3IRjp7xDEmGg92JCTpe%L^r#_El6AC0 zYj59t{Be`0lj&WNwF6bfOrL=($B-vE0mAbpx`B!AKqpL*jWR~7T+foWYp7^PI$k>B z>Lrpn%q%KEC`>50fofmw!5kq_MF?NAh>Ib)cUjh$f;^V-{8*;2F!@~HvNq5vZZGRz z0wK?GkB~liO=ME=BC_LohjXd+?R`GBq(-aWT!&sM@$>(A+qj9j1n-a^q0fDD4T$Jx zqeRWCRthno=0{=nJ0Z`B0usMF-LVD<9V93;WE3N%LX+q36E(10;9ZF?5w|%P?17Xl zDVYh(2;V}gB;C(HPz1W9UDCFjyQv-8?yD2M?verVi&}EXmdmS>{;k=@6>eDc zrvd2{Q+qQg^Q(<^4tyh9bUACk_2atN;l4tJ-E1r4H?7SCj|j2_t6M+cygeGjiQT4}ZeM z&AS;FsI3CK$A~{8F#LRQYVE=-7JD@{RDYm!G$s$OHB;iF6gUbA5OHT_?rnUw0POXy+eUwYQ2+npL z$N>HM&#N*P)mazHQF{S?D=9W$QKX?HICepsNz!BCgmPIuqcl*ccuh#5tUuV8{P%3|lF2>kKx}*Me@`3IKTwib` zsVVGY=w+x$#iQDm~wLsnD}K8|JX4X%ftd*bIlbg-Hw_a z6C%ys4C65bGrCekdZbCDxoIl8yN#@}okN-Jt^x`ncyS#XvEs#@RE7FUH3>PjA+nKp zyX54l$SQXWLZ;u~izBQsqXag|R(!6;p=O(J&k=j^6jX{R2!x92n=DXHL=I(O$HM72 zoD(}C9Wo6miZoyt_G9GR`8!u%=;^q&oMXf=wiwwJ^Q5m3NuW5SGD%H%c$W^YR_dCP zWgpK?8vDWzs7!(!W`>hqe05ygJEAwj)9tDr?~(iT-py~x7=II8A&`lJe{LZ4F`!-{ z*l{>E#Ir%Kx0uNABd!1OT_YRjTZh zGLSW)`K100v?Os0--=%8f3j4tBk1oe4o5&07*=u7aj`=~uP-%L^J*D7{hbC(BfMAn zw=piDg}Sd_&5IjbM+3JTNHIzeMM>v6q+ad4&9ihXfD*y=7&3NVJ2qCiEbwAEmauTl zW?KZ*Q5d?jH13p+0`b6y3=&%XD*pm3_LLO1U1c`g8Utt;8DY1@?>T|P>M|)|$~G+Y z>m>$Va2#@=LpuWtwF6m^R>gl>5=jwEkkZVALE_KGb5a#i^qZRH^~K95Bj?*?qT-^| zapE1u4Wid%3oSPvGl;2@>Kb%OKQWJ~C}|9GQ=;1DMQK(N(veJwhXmoyy1+#gbxbD? z?MAna?0eIZCVBAE_%;`9Z<-!_7<3R#g>O%bQp{0?QDOkB+rld&hlf0vhxck>p=e=? z#dBO~@l#=*qi}6V?sJubm?5j7CXHOHa3*kI48-B?jZ&E0%6A6S<=;xbzthQA2C%u! z(9A%Q%h$WkwK2tNV()i(;pEc0;kd4(5+pVMtHVM~zkBIIKLe1rbyghP2q+F^7BuP! zZ$OvS?G0U7>qTxdnlgnZ^l%ZEmua2LtEG9oW8l&Js*5pB0%b#fEO{@<&36nxe$r=` zM}FN1%75?VS>u|wUupRy4#i|Lk9s9uW+XD=H`bj*{jILv_u*fBw#YiKj%7k2B)6yE zI+yTCH}Tpx2`ag?ymEcRwaXMdDLoo8M4Cd)z_n$Bm}%rkP^jmt^IBzx!yGl!`Cr?F zez1f>QTbaBHKzy5g0YD%5N_yFM#&^_5Yi4eEd7iXTf4jI?)sGT?ZbZyscCKda+lf4 zoH`cmB}#sOAs*5Lb;ZR=lr|~){FNUufhO!yy3PR?aQ*!3w3rUGD_k}{sIq_qEshkr zpM%bO7Y3=L?v6nG*%sTyIe*@IOoOC48C-!vhJul{5x`A}b{z)@?&Ac-w-t7Ms)dq0 zfqDz}4+H$S#kh4{hRx7plax-@;|TR2E^DQP%a_vN|U?yZqRpCvY34$9MWSx3(B1vTdW zg&V;fVN7?(v+9YTlZLdhwb@WQH>I;;U)|DAK@J1{NJP;`jh1V8_4t=k2xlmV%tAw2uZIZkk-(Y z0NbaHU=M3g1Ehan%IodKR2V=xMs7>opq}hT&)SKPs_(?Uoq#9GD$r(i(+Jgyw4TWj zj3QpLFN%NF2uxEW9pvMuhNaZUCfc-=)m%rhk{Z=k8y(hZ+x@%6Sji>v#%6x}KdwB# z&Ka+p=I~JUTQ=3&DZfz?V2&LI`8R_L5VXr^Lbwj%j#4u1efLclTD>c8FnSnD2CI)L zNEey96}2<<%mnf09&JG&Q5tXevjmEz#?he;UluD>n+w&^>>xf=H!htak?iNW~OuAqaWLqi+oqwE=R>Mib0S9X#l zFrMNIxw^^0OlnE2oywAO&XA{@SYyGCtd_0~BJH#_qIrY11wK7~#0M*dG~OjxPoJ;S zP%oL!?k9`ve2D3)d>nsYFw}!#vyIOKVn*NP+ z1P(=$;*0?3>LQi{2#Z4ZQ2xbNxdSTR*Yno_Sv|-i1Gou|JKvrf!#!4c zU7?~+eabyMSigDKq1iv*kDkXb`g48X@#B!6rVmrLiuF1=x-djkIU-+`s5MwB=yIpm zbo$#M9Fl$10SG`RW=GcJg3G|Jp{fKt)RCD9!B4q(FZw8c2Z?H?jCx>Ih zlHc>t_p{}X@oEb-im3qUvZ4&#*klSM)JQ1X2kTmLzkc1lTcN)oT9wL`RG{bW0I846om)gi}+{?L(UQ zx(cg1Bf|7BOwwLr&JAH094yjKzZrnh{84-|cMFk{&Q5@WBP3)b1dw4x5ffjN<#=VF zJ)H2Qy1J@e`zKVJ&OqS?aO`%AaPb$DuE)`33|*PSEz8=|gb$01ka|gQzZnkdf!ma< zHmEtdh)qjFuOS2YH@&iCHz)HnC~sj7 zQ5j%hxM>VyI#|XpBcD~${f3o|g}?P}lp-yAb=-$=rt43-=H0b5{|CLq4ANH7 zQRfbY5E)r>t83(gqhZgEWot!zS3UPKY$Q@iia@&*E(ojyWqL#BQo8B4PfBT6gm&({ zPVoLk=kN=cJu%4hIeVgeLaAN^cT}r3ScCVNc5$>eHuBy!WYs$RSQ$OmL>G-3Y%i}; zRg$-oyv~@?OFoD*-5X%UPAzxl2i3{%w;z#UCdu-sQ65fYig48tH{-!f24SC=TJxK3 zrHV|t8!}UM!<31fV7h{RPhmig^Uf+mw@bU`w>#EoDyFn2U6Z^#n7K zGOl|5I$mE&6ICvP(gLmxHsXJt^i9Zz*OTtGuHE)v+me43;c^N!rW_AusG>KP6m zc>g2kNq{|n<9~YNuLJ#IUeM;C}_`w|0p$d#4$P~5OkF}RTCt`SmFr}251gcNvakt#JeKy^=p%*kAfH|H;Vp>Hu zch&zmilm|Ld$VhSL$ml9*zKUCgF5r;)zBUs&$)iis#|t~K?A6|{{7qggF~aR3K$^i zF{-}`Q6g+z==%V)>>>DUU>d->7i1BQcs)S&g?FA}juF3+_CqXms~3FQcBQ68bfn0i7A*)cz#*S1m>^HQ2NUz9&_8n}hEyTf`@~uL$$ZZM{a!GvrrlJskL!K>Yls6=H@t6u*vTuzA@&aaZw< zWJJ%&9b;|=hwHkmkAQfx1n$!ESsrd|XNJ(=_HYFd;k7j?;x*(kje+sV<(gB-R(xq&s<^O4O z=sq;zO3~p^5+??O%Ej~;I1~)SF9gdwZXX446MKB=Oe-Kj}35V=v;tR0<~_ z)uZ9Ib?T&H#hzmWKS@)%eJ!FVz&g60+qs!HvU)LMVOr>Ze{H10=rA8^)=F-Y2GfLu zpaI0W3H}M`qM#sca$xVFuo6xvFzauKcYj2dCYL5o6=d=@7j&kJ?2W)IOYpp{HaE2* zNCO(c;cQmx^#+c<|3VW<*f6$ru?!VOy2yHkAvRhdzM@F<5{eE%JnMqWK$Jt_i1-=z zr~6^sAVfePESk7Q!Lu2K%Vmk`^!z+)h>wwD>j^&~TY{U9-d9**N4VwPZqm#9=FA`r zhpmaU7FM=|%A8~fH)^ub%``@OU8~F#sHWI2$|q!_(;Sa4Z?64IlL;C;wryK>#xk~%ecuNeL}VKb2E`=CAdIn7mM3erNldoJHpp6b4b3oW zsFXs4ENNt^WJyd3St_M(>UrPqect!@=f3}&`Ca$*JFoLRuk$#LjMz8vcO4v8Kj}gk zN@|UsalPLl6yHh=M9r>V2xSmrW`R*q))jb|zP%Qx${7ibsU)R8c9xL9aeCIiD!LgC z6vvz9$ZU&PiTItlO;eQ4EjK=;DB002TTFtJzfdj}`tGnfV^{cg7YV%Ij?6&Um#d!h z+0plN%=~fz0)0ID_~@%8h{;I#sxghU{-AXY=XDTXkq-&4S95FOJd9jyI0HB;Lr}&Ja@8f5zLS0&5 zU*fpo$phZx)aU{5tR@(1JrxC>do?6yC4}EK34Klz-$}y#>VS=qw)Z#K#4T5 z-F2>W^bf62xxq&IY`pB68eQGq9JR0kG4^n(?|GNcffwVQPZCFfM_Ly1Mwzv=&!I5r zV)L8kaQwC?RFqB1Olt6W6Kp@ zsAwku(x9>)&#$JFYc{bIn1W7+i<(;qC_4Q!pdKp#0dh0kKPTtrRMx^^(sP~m$&vKd zc1!yEOWUk$lvd2`_<BkMG0J)IGq@(h- zB##He)>%rqY^AY9?MwNo=1snCSNCMDS>Z1~f%kTd@u`rVozQXYaeHJJk8n4WKcoMp zugEJ0$GwkLmuBbmtgDNbW^cjQ;l2t{q`_HyFVS{ZZi44O689cmGs~H15W&FvdL%wsQT0Ldt{TGB9!K4dzdsS*? zK*90;2$cBkJ}x^$1fU4Cz;txwC@w~pX^x1?jlX;yu4A`%EoVsk!ac`3CpT-{vm(O9 z(3biMwne(9&AezlL~`iWA?z&&2%DUMjs~A_%Di_(XqqDQR7h6fiOV0;?>T|FuL=>q zdeUy7TG2~PWOR7tvpJ+?GJcDbnI-l*ygyouoAoT{h0bNgtTjNa7Oz5SUx zqk@lLXQeKlYgt(g>&1x=iY@oVLDa0n4U+1^XlxRv!&OoWo12ho_N53V}`0@aDa#4_V7wg!xt*<|Q!ML+M1^zS+0 zbux*^sr}214Ge?(itB`{Lv}L{5^v|^T`S*Z>C;Zz88fAWPpsAD!j3bl<1XsfINxCY z%#2vL3P`)kLtx98v^R)}N~mgT|J=2(X(9oq@`oRc)<%6MFhnPo66=sH0%2Bz-7LW! z$)8Z}cLQ=U5t(*MisD}u?~NNEc+c?lYcDBI1Yx&eO_)Eov+OT*+cZWd+L;FaG`q>4-XIOBxq2GsBLBY;-h;l z@HFcr(@iEfLPD)tamXlmC&r}5esANo%3SP6{Lm0Xv&Oza+jU2LFuSJfO_)(|ZP@%V zOmnfCwj`fzn}T0b!>>{0Bj+QBx3H9`Q@$~@L2l0+)DbD6Pd3NxTi#RmI@UYMj+YAW zbS;>R`D}$Q+t>R0JDZhdJbT%mcPr338*ustay;}Vs6@GDNxyKq`(~9A*NXY%SBqFn zAOvMokAP$g*ekU9KCgw`TOcw-vDE75?P|+!q3?gh2D<#A!x$*8}e4gVn%*^o84u>0$NnCgIp1Mpgt;eJxc`I`up6FhDBNmchyVH=b;9{@(2jg_Gp5eQRvf-R}s0 znG(UnoMvVz1e>v}X7EdimYl@3Oggpm!#xN0uha@+S4!{6zj&{un@ zyv)0tMGHNIAcI&H%ZE|G*Kan?WzRr2&k`+2cjT-)++&M^&8BITo*J5{4*D{k$&Wd| z*Vqc~cKRV+y_?nqS1;DP#XG$1*T=q`Yol1EpCzqug=D4$RN1nc$1Y784D!y`;KY`c zq|e){HchWRNR5%zf;%3hd+^hYfv%X(&7lV;G7k%{a%^AOhRSUqJ+kVvV$@VyVJ~ok z?MEUXs&-6-!0TN*r$%zDn|Zisq!_>4+7kCm6%IyfWc=aDb$@TO0N8eZra-kd3!JOKW zUT6Vy0c9kK%Ru(~TD}F!wK;58D23fkI`50g({Be0@C#tX}xy1*EdN7*1BNOU`iU?Pk-cX)X&^I70&WA~>?e zY;VG9HSNMV_Sx+vnSb1oP2TB7bYa?{m0p#!Hxv23-()TvrnKhTUr|6XnoIvdUSinW zs9GO1TIV>5`aX^z1){omA7foX+l>XgFF1Q9+m4003Q@l|p-+9x`%O+f&g6L+JnVQB zD;w@A9-*CXbmikCdA4CLTMh&tT_nb2xBsTj&={c(b<4IHQIK@7KioG}SH#V@9e~>w zCxSKLNY9|Mi=KWv(Td_a(6kW)yh#30EvOiPkiCoRKYtb0sor|pd%_19C3WIzzbtGv zz@7PwZ<(ei&Qkb6%(6Im_abmO9 z#^Agxf}tu4RvGjoIr(aYU`=;h*lC5K+I>-V0Z?ndOq z_=H1@j;j}*(Vfj}>A%QWR)}KTlKscRvL89Q?y#p#EvrJuN=;ED?{G7k^;8KiL#Yx+ zj}s>HLB20Avo-Ia0~zuX|G=ip6WZKEHWeS5G&S1iO4@FQ+C+=FUlJCZ$NV--zlt>b z=sL^eKExMwC*p8%yV{=Qwf;rd^UPSN*)%6YJnz*$o~KUuj^2< z1-hpQ%7xdXir1Re)W$#~O3IP|`)1E^h&a?~KA}FuRYUcVX{tPuh)_NmY97k@zYHcUxemZCl_OiEm1{_>*?Cj4LEYcJmk?MV08V< zu=wyPdSKvo0bDWMvt{>szA;c4?}QhBUnRV~NG_s0r{o1{mA6CWC#aZCubs*RbFY5+ zV1i$O2=syHQVRbNR>>JsJPA>7WnXLLdT*amjk{Bx+OgAm_tw)5FUvWn&?%*cPoL;? zT|`Et0*Gh-QpBjyPt795n|`2dr=F_YZ&d`U3H?N5?!T1M;8(Ryv zd)l(%28jq#K&ovomNlD;sdqt$yVGbUc(x%TTFX@($UPKM7xhACWwE&l0U z=Zbe5l9gk);AYcjKpV!YJIKrC;00!uAtx=j&Nn1?8zzM)aYuy!aYx8-&F0c6jCTS} zxwon7-LjF9V!nfI7m92sCxXjDxHF?nU8V8-$UA-V&e4`PFHoK?<^`F_#(iLMJ<&5P zG;KLM9@C?6{OaeHC%dd~`a`bBc1vbAE(UNhuQ0=b4&V8>{1t7;VXT}qUF6qedA^+4 zL4%$V5!3Gyjd&t0C4zK6<4@d|J!x(`XZ1tK?9m0;qvX#&>E;!2tk{yp(lP3#O#@aezwp=l>T7u5n{4xXOi8aMCxHF=*?zC$ z^Nf)}N7d>m5=iH+bZeo5DPFV>BPY$d%=Ol?%7=VJmT3p)a4Jt?=14yFBFD3&nCH;T z5lvf|$KW$Tr)!-%zeh@JzoBT8oAOzKGRQhidzBLRCw-{(aD%sO`6#Le1$hGmWb{n% z4YD6*I4mU2+B10#DuPjQFFhPP%;jF{TztX*R`Bu?DkoQI!-Ztw$s)HL$lLx(_xm={ z`=oiurdT^}EB)hdH9&hI+%cSlt=e~+5wSs$0leu1A2E$|YzTvx%zsj^7LXPzkx)I@ z(%1E#xo%~;Qwn$%h@ghPYbbt>*_G6W@g!wNoH!^e8S6YBya}WIJH`6@XflK`ZCe%l zVe9WizIN7>5tT4w;qUc~TuQlHaR@lB6qC{#&z01@FenD<-@Kyz}MOH<#cuscff=%JY^g# z>2zHVdEcB8z2&fD*c(WGzG=4f{<1%S!qEp%v_cktZR#NzZh^%gPZH^|kX2@3*%zl@ z5uHfmslZ3-%%akqN5F1D{P2{M1V%igJk<=hdG&118^}?Sd6DD1HuAB#m5;>{9Y%tZ zyCb(5W++(fG7oqjvSFRuoxH>reuXn_XVZJ9oaf|y%LF|V=Y$dSYy_t&PAu$#tTR#4 zWC;6WT^~8>Q}!g5wOl_|_1XBLpB%FQ^x)_!MiO!PtGci8sb_&iQ|>DT^(A{=O%08W zL@z4PqPy;edvK5$B_8BivXnEkyj^6dvu+(%F4->9Hs>h44oP?%&lI<0-kX>bL>HJ}L@5J21MU zXg*L?qjm@rrd)>+^>$hFEaEo#B2&w`;K!dCU|6@{jg)uf76#*<}ljYd6*@S6{mRYNhM~I#WRlmk&J6+;TqW=5L$xFLv18`M1non5Z zTk6WOy`_n)h&$qJA}Ujw-k|<+7v?DPqXlSxh0*KE_UVVtqJ_8l&OR>ODoVF>=K65) z?AgI`lr#osTsEf6>b?DWH(tejMPv4VtK3s8u^kq^iuD8D4&^Qz%1>~so z@`AAfJ6IqYKJ5f9n>hzC&@*}QYTaMUMhp|KEf{VII#Lu>ulQ|7jLv`F0@-*pm?gh< zvlS+*VJJQ6hCFbk?TsuN)m87BDVWNJB0xe)A-f}jtT7?8o~0y4yR$`*ha-1hOiDt3 zzXINR)34^|B)zdw**`AMimpEQN@%_~mArzmo?FdFVGhfNR{b%5xvU$(=6MDLOH>dt zFW5UOCJv9~Rk*2_jKDMZg!D=d$8SH+tH{-KRl}c-mwKTVF0@|~r#tV4tHSjJP{IqB zmS#(?Pf>U;YM!xyAtJSDBT)c+P Wy%T#>f~6%B19sM@tlFSH*#83tTk1Lh literal 0 HcmV?d00001 diff --git a/Shannon.py b/Shannon.py new file mode 100644 index 0000000..b46244b --- /dev/null +++ b/Shannon.py @@ -0,0 +1,1028 @@ +''' ---------------------------------------------------------------------------------------------------------------- + +Shannon Equation for Dummies - JPC Feb 2021 + +Educational Application + +Exploration from Claude's Shannon initial theory to its practical application to satellite communications + +The application Runs either in local windows or in web pages + +The Web version via localhost is fully functional although not as convenient as the windowed version (only 1 plot open) + +The Web version in remote does work for a single user (all users connected can send command but see the same page) + +--------------------------------------------------------------------------------------------------------------------''' + +''' ------------------------------------------ Imports --------------------------------------------------- + +The GUI has been designed to be compatible with both PySimpleGUIWeb and PySimpleGUI + +The PySimpleGUi version takes full benefit of the matplotlib windowing whereas the Web version is constrained to use +a web compatible method with only one graph at a time + +''' +from math import * +import matplotlib.pyplot as plt +import numpy as np +import webbrowser +import Shannon_Dict as Shd +import sqlite3 +import os +import binascii +import itur +from astropy.units import imperial +# Python Program to Get IP Address +import socket +hostname = socket.gethostname() +IPAddr = socket.gethostbyname(hostname) + +Web_Version=True # +Web_Remote=True # +imperial.enable() + +if Web_Version : + import PySimpleGUIWeb as sg + from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, FigureCanvasAgg + import io + def draw_matfig(fig, element): + canv = FigureCanvasAgg(fig) + buf = io.BytesIO() + canv.print_figure(buf, format='png') + if buf is None: + return None + buf.seek(0) + data = buf.read() + element.update(data=data) + def window_matfig(fig, title_fig, title_win): + matlayout = [[sg.T(title_fig, font='Any 20')], + [sg.Image(key='-IMAGE-')], + [sg.B('Exit')]] + winmat = sg.Window(title_win, matlayout, finalize=True) + draw_matfig(fig, winmat['-IMAGE-']) + while True: + event, values = winmat.read() + if event == 'Exit' or event == sg.WIN_CLOSED: + break + plt.close() + winmat.close() +else: + import PySimpleGUI as sg + +if Web_Version and Web_Remote : + web_cfg={"web_ip":IPAddr,"web_port":8080,"web_start_browser":False} +else: + web_cfg={} + +''' ------------------------------------------ Core Functions --------------------------------------------------- + +These functions are mainly application of formulas given in the first panel and formatting functions + +''' + +def Combine_CNR(*CNR): + ''' Combination of Carrier to Noise Ratio ''' + NCR_l = 0 + for CNR_dB in CNR: + NCR_l += 10 ** (-CNR_dB / 10) # Summation of normalized noise variances + return -10 * log(NCR_l, 10) + +def Shannon(BW=36.0, CNR=10.0, Penalty=0.0): + ''' Shannon Limit, returns Bit Rate ''' + CNR_l = 10 ** ((CNR - Penalty) / 10) + return BW * log(1 + CNR_l, 2) + +def BR_Multiplier(BW_mul=1.0, P_mul=2.0, CNR=10.0): + ''' Returns BR multiplier ''' + CNR_l = 10 ** (CNR / 10) + return BW_mul * log(1 + CNR_l * P_mul / BW_mul, 2) / log(1 + CNR_l, 2) + +def Shannon_Points(BW=36.0, CNR=10.0): + ''' Returns CNR_l, BR_inf, C/N0 and BR(BW,CNR) ''' + CNR_l = 10 ** (CNR / 10) + C_N0_l = CNR_l * BW + BR_infinity = C_N0_l / log(2) + BR_constrained = Shannon(BW, CNR) + return CNR_l, BR_infinity, C_N0_l, BR_constrained + +def Shannon_Sp_Eff(Sp_Eff=0.5, BW=36.0, CNR=10.0): + ''' Returns values at required Spe : CNR, BW, BR ''' + C_N0_l = 10 ** (CNR / 10) * BW + BW_Spe_1 = C_N0_l + BW_Spe = C_N0_l / (2 ** Sp_Eff - 1) + BR_Spe = BW_Spe * Sp_Eff + CNR_Spe = 10 * log(BW_Spe_1 / BW_Spe, 10) + return CNR_Spe, BW_Spe, BR_Spe + +def BR_Format(BR=100): + return "{:.1f}".format(BR) + ' Mbps' + +def Power_Format(Pow=100): + Pow_dB=10*log(Pow,10) + if Pow > 1 and Pow < 1e4 : + return "{:.1f}".format(Pow) + ' W .. ' + "{:.1f}".format(Pow_dB) + ' dBW' + elif Pow <= 1 and Pow > 1e-3 : + return "{:.4f}".format(Pow) + ' W .. ' + "{:.1f}".format(Pow_dB) + ' dBW' + else : + return "{:.1e}".format(Pow) + ' W .. ' + "{:.1f}".format(Pow_dB) + ' dBW' + +def PFD_Format(Pow=1): # PSD in W per m2 + Pow_dB=10*log(Pow,10) + return "{:.1e}".format(Pow) + ' W/m\N{SUPERSCRIPT TWO} .. ' + "{:.1f}".format(Pow_dB) + ' dBW/m\N{SUPERSCRIPT TWO}' + +def PSD_Format(Pow=1): # PSD in W per MHz + Pow_dB=10*log(Pow,10) + return "{:.1e}".format(Pow) + ' W/MHz .. ' + "{:.1f}".format(Pow_dB) + ' dBW/MHz' + + +def Gain_Format(Gain=1000): + Gain_dB=10*log(Gain,10) + return "{:.1f}".format(Gain) + ' .. ' + "{:.1f}".format(Gain_dB) + ' dBi' + +def PLoss_Format(Loss=10): + Loss_dB=10*log(Loss,10) + return "{:.2}".format(Loss) + ' m\N{SUPERSCRIPT TWO} .. ' + "{:.1f}".format(Loss_dB) + ' dBm\N{SUPERSCRIPT TWO}' + + +''' ------------------------------------------ Database Functions --------------------------------------------------- + +Functions for management of users' contributions + +''' + +def Contribution_Write (DB_File) : + + Sh_DB=sqlite3.connect(DB_File) + Sh_DB_c=Sh_DB.cursor() + + Sh_DB_c.execute("CREATE TABLE IF NOT EXISTS contributions " + "(num INTEGER, name TEXT, title TEXT, keywords TEXT, text TEXT, date TEXT, password TEXT)") + Sh_DB.commit() + + Sh_DB_c.execute("SELECT MAX(num) FROM contributions") + ID_Contrib=Sh_DB_c.fetchall()[0][0] + + if ID_Contrib == None: + ID_Contrib = 0 + + print (ID_Contrib) + + layout3 = [ [sg.Text('Initials / name',size=(20,1), justification='center'), + sg.Input('',size=(45,1), key='-Name-')], + [sg.Text('Title ', size=(20, 1), justification='center'), + sg.Input('', size=(45, 1), key='-Title-')], + [sg.Text('Keywords ',size=(20,1), justification='center'), + sg.Input('', size=(45,1), key='-Keywords-')], + [sg.Text('Password ', size=(20, 1), justification='center'), + sg.Input('', size=(45, 1), key='-Password-')], + [sg.Frame('Write your text here', + [[sg.Multiline('', size=(80, 15), key='-Text-')], + [sg.Button('Validate'), + sg.Button('Exit'), + sg.Button('Help')]], + element_justification='center')] + ] + window3 = sg.Window('Write Contribution', layout3, finalize=True) + + while True: + event3, values = window3.read() + print(event3) + + if event3 == sg.WIN_CLOSED or event3 == 'Exit' : # if user closes window + break + + elif event3 == 'Validate' : + + ID_Contrib += 1 + + Sh_DB_c.execute("INSERT INTO contributions VALUES ( " + str (ID_Contrib) + ", '" + values['-Name-'] + + "', '" + values['-Title-'] + "', '" + values['-Keywords-'] + "', '" + values['-Text-'] + + "', " + "Date('now')" + ", '" + values['-Password-'] + "' )") + Sh_DB.commit() + + window3['-Text-'].Update('Thank you, your contribution has been successfully stored, ID #'+str(ID_Contrib)) + + elif event3 == 'Help': + window3['-Text-'].Update('Write your contribution here as a free text. Edit it in the text window until you' + ' are happy with the result and then Validate. \n\nContributions should be candid observations' + ' about the technical subject, references to relevant material (ideally as web pointers), open ' + ' discussion items about adjacent subjects, suggestion for improvement... \n\nYou can retrieve' + ' your contribution via a search from the Read Panel and delete them there with the relevant ' + 'password. Keeping the "no password" default will allow anyone to delete.') + + Sh_DB.close() + + window3.close() + + return ID_Contrib + +def Contribution_Read (DB_File) : + + layout4 = [ [sg.Text('Filter on Name',size=(20,1), justification='center'), + sg.Input('',size=(45,1), key='-Name-')], + [sg.Text('Filter on Title ', size=(20, 1), justification='center'), + sg.Input('', size=(45, 1), key='-Title-')], + [sg.Text('Filter on Keywords ',size=(20,1), justification='center'), + sg.Input('', size=(45,1), key='-Keywords-')], + [sg.Text('Filter on Content ', size=(20, 1), justification='center'), + sg.Input('', size=(45, 1), key='-Content-')], + [sg.Frame('Contribution', + [[sg.Multiline('', size=(80, 15), key='-Text-')], + [sg.Button('Load / Filter', key='-Filter-'), + sg.Button('Read Next', key='-Next-'), + sg.Button('Delete', key='-Del-'), + sg.Button('Exit'), + sg.Button('Help')]], + element_justification='center')] + ] + + if os.path.isfile(DB_File): + Sh_DB = sqlite3.connect(DB_File) + Sh_DB_c = Sh_DB.cursor() + else: + sg.popup('No Database Found', keep_on_top=True) + return 0 + + window4 = sg.Window('Read Contributions', layout4) + DB_ind = 0 + + while True: + + event4, values = window4.read() + + print(event4) + + if event4 == sg.WIN_CLOSED or event4 == 'Exit': # if user closes window + break + + elif event4 == '-Filter-': + + Name = "'%" + values['-Name-'] + "%'" + Title = "'%" + values['-Title-'] + "%'" + Keywords = "'%" + values['-Keywords-'] + "%'" + Content = "'%" + values['-Content-'] + "%'" + Sh_DB_c.execute("SELECT num, name, title, keywords, text, date, password FROM contributions WHERE " + + "name LIKE " + Name + + " AND title LIKE " + Title + + " AND keywords LIKE " + Keywords + + " AND text LIKE " + Content + + " ORDER BY num DESC " + " LIMIT 50 ") + DB_Extract = Sh_DB_c.fetchall() + + print(len(DB_Extract)) + + if len(DB_Extract) > 0 : # First Contribution read automatically + + window4['-Text-'].Update( DB_Extract[0][2] + ' , ' + DB_Extract[0][1] + + '\n\n' + DB_Extract[0][4] + '\n\n Keywords : ' + DB_Extract[0][3] + + '\n\n ID #' + str(DB_Extract[0][0]) + ' - ' + str(DB_Extract[0][5]) ) + DB_ind = 1 + + elif event4 == '-Next-' and DB_ind>0 : + + if DB_ind < len(DB_Extract): + window4['-Text-'].Update(DB_Extract[DB_ind][2] + ' , ' + DB_Extract[DB_ind][1] + + '\n\n' + DB_Extract[DB_ind][4] + '\n\n Keywords : ' + DB_Extract[DB_ind][3] + + '\n\n ID #' + str(DB_Extract[DB_ind][0]) + ' - ' + str(DB_Extract[DB_ind][5])) + DB_ind +=1 + + elif event4 == '-Del-': + Password = sg.popup_get_text('Enter Item\'s Password') + print (Password) + DB_ind -= 1 + if DB_Extract[DB_ind][6] == Password : + Item_num=DB_Extract[DB_ind][0] + Sh_DB_c.execute("DELETE FROM contributions WHERE num = " + str(Item_num)) + window4['-Text-'].Update('Item ID# '+ str(Item_num) + ' deleted') + else: + window4['-Text-'].Update('Incorrect password, item unaffected') + + elif event4 == 'Help': + window4['-Text-'].Update('To read contributions you have to load them first. If the search fields are empty' + ', all contributions will be loaded. You should only enter 1 item per search field.' + '\n\n You can delete items if you know the associated password. Items are not encrypted' + ' in the Database and can be removed by the admin.') + + Sh_DB.commit() + Sh_DB.close() + window4.close() + + return DB_ind + + +''' ------------------------------------------ GUI Functions--------------------------------------------------- + +LEDs from PySimpleGUI demos + +''' + + +def LEDIndicator(key=None, radius=30): + return sg.Graph(canvas_size=(radius, radius), + graph_bottom_left=(-radius, -radius), + graph_top_right=(radius, radius), + pad=(0, 0), key=key) + +def SetLED(window, key, color): + graph = window[key] + graph.erase() + graph.draw_circle((0, 0), 12, fill_color=color, line_color='black') + + +''' ------------------------------------------ Main Program --------------------------------------------------- + +The program has 2 main panels associated with events collection loops + +As the loops are build, when the second panel is open, events are only collected for this panel + +In the windowed version, matplotlib plots don't interfere with the event loops : as many as desired can be open + +''' + +# ---------------- First Panel : Shannon's Equation ------------------- + +form_i = {"size":(22,1),"justification":'left',"enable_events":True} # input label format +form_iv = {"size":(8,1),"justification":'center'} # imput value format +form_ov = {"size":(20,1),"justification":'center'} # output value format +form_o = {"size":(65,1),"justification":'right',"enable_events":True} # output label format, also using input elements +form_CRC = {"size":(8,1),"justification":'center',"enable_events":True} # CRC Format + +sg.set_options(auto_size_buttons=False, button_element_size=(14,1)) + +col1=sg.Column([[sg.Frame('Theoretical Exploration', + [[sg.Text(' Reference C/N [dB] ', **form_i,key='-iCNR-'),sg.Input('12', **form_iv, key='-CNR-')], + [sg.Text(' Reference BW [MHz] ', **form_i,key='-iBW-'),sg.Input('36', **form_iv, key='-BW-'), + sg.Text('',size=(34,1)),sg.Text('', **form_CRC, key='-CRC-'), LEDIndicator('-OK-')], + [sg.Text('Carrier Power to Noise Power Density Ratio : C/N\N{SUBSCRIPT ZERO}', **form_o,key='-iC_N0-'), + sg.Input(**form_ov,key='-C_N0-')], + [sg.Text('Theoretical BR at infinite BW : 1.44 C/N\N{SUBSCRIPT ZERO}', **form_o,key='-iBRinf-'), + sg.Input(**form_ov,key='-BRinf-')], + [sg.Text('Theoretical BR at Spectral Efficiency = 1 : C/N\N{SUBSCRIPT ZERO}', **form_o,key='-iBRunit-'), + sg.Input(**form_ov,key='-BRunit-')], + [sg.Text('Theoretical BR at Reference (BW,C/N)', **form_o,key='-iBRbw-'), + sg.Input(**form_ov, key='-BRbw-')], + [sg.Text('Carrier to Noise Ratio : C / N = C / (N\N{SUBSCRIPT ZERO}.B)', **form_o,key='-iCNRlin-'), + sg.Input(**form_ov, key='-CNRlin-')], + [sg.Text(' BW Increase Factor ',**form_i,key='-iBWmul-'), sg.Input('1', **form_iv, key='-BWmul-')], + [sg.Text(' Power Increase Factor ',**form_i,key='-iCmul-'), sg.Input('2', **form_iv, key='-Cmul-')], + [sg.Text('Bit Rate Increase Factor', **form_o,key='-iBRmul-'), sg.Input(**form_ov, key='-BRmul-')], + [sg.Button('Evaluation', visible=False, key='-Evaluation-', bind_return_key = True), + sg.Button('BW Sensitivity', pad=((60,5),(2,2)), key='-BW_Graph-'), + sg.Button('Power Sensitivity',key='-Pow_Graph-'), + sg.Button('BR Factor Map', key='-Map-'), + sg.Button('Go to Real World', key='-Real-')]])]]) + +col2=sg.Column([[sg.Frame('Background Information (click on items)', + [[sg.Multiline('Click on parameter\'s label to get information',size=(80,15),key='-Dialog-')], + [sg.Button('Advanced'), sg.Button('Write Contribution', key='-Write_Ct-'), + sg.Button('Read Contributions',key='-Read_Ct-'),sg.Button('Help')]], element_justification='center')]]) + +layout=[ + [sg.Button('Wiki : Claude_Shannon',size=(25,1), key='-Wiki-')], + [sg.Image(filename='Shannon.png',key='-iShannon-',enable_events=True, background_color='black')], + [col1,col2] + ] + +window = sg.Window('Shannon\'s Equation for Dummies', layout, disable_close=True, finalize=True, element_justification='center', **web_cfg) + +# Needed for the Web version (on a finalized window) +window['-CNR-'].Update('12') +window['-BW-'].Update('36') +window['-BWmul-'].Update('1') +window['-Cmul-'].Update('2') + +File_CRC=hex(binascii.crc32(open('Shannon.py', 'rb').read()) & 0xFFFFFFFF)[2:].upper() +print("File's CRC : ", File_CRC) + +Win_time_out = 500 # Time out approach required for enter capture in the Web version +First_Pass = True +Err_msg = False + +while True: + + event, values = window.read(timeout=Win_time_out, timeout_key='__TIMEOUT__') + + if event != '__TIMEOUT__' : print (event) + + try: + + if event == sg.WIN_CLOSED: # if user closes window + break + + elif event == '-Evaluation-' or event == '__TIMEOUT__' : + + if First_Pass : + window['-Dialog-'].Update(Shd.Help['-iShannon-']) + First_Pass = False + + CNR_List=values['-CNR-'] # CNR can be a combination of comma separated CNRs + CNR_Nyq=Combine_CNR(*[float(val) for val in CNR_List.split(',')]) + + BW_Nyq=float(values['-BW-']) + + CNR_l, BRinf, C_N0_l, BRbw = Shannon_Points(BW_Nyq,CNR_Nyq) + + BRunit = C_N0_l # Mbps / MHz : Sp Eff =1 + + window['-C_N0-'].Update("{:.1f}".format(C_N0_l) + ' MHz') + window['-BRinf-'].Update(BR_Format(BRinf)) + window['-BRunit-'].Update(BR_Format(BRunit)) + window['-BRbw-'].Update(BR_Format(BRbw)) + window['-CNRlin-'].Update("{:.1f}".format(CNR_Nyq)+" [dB] .. "+"{:.1f}".format(CNR_l)+' [Linear]') + + BWmul=float(values['-BWmul-']) + Cmul=float(values['-Cmul-']) + + BRmul=BR_Multiplier(BWmul,Cmul,CNR_Nyq) + window['-BRmul-'].Update("{:.2f}".format(BRmul)) + + Params = str(CNR_Nyq)+','+str(BW_Nyq)+','+str(BWmul)+','+str(Cmul) + Params_CRC = (hex(binascii.crc32(Params.encode('ascii')) & 0xFFFFFFFF)[2:].upper()) # Params' CRC + + elif event == '-Wiki-': + webbrowser.open('https://en.wikipedia.org/wiki/Claude_Shannon') + + elif event in ('-iC_N0-','-iCNR-','-iBRinf-','-iBRunit-','-iBRbw-','-iCNRlin-','-iBRmul-','-iCmul-', + '-iBWmul-','-iBW-','-iShannon-', '-CRC-', 'Advanced','Help' ): + window['-Dialog-'].Update(Shd.Help[event]) + + elif event == '-BW_Graph-': + BW = np.zeros(20) + BR = np.zeros(20) + CNR = np.zeros(20) + CNR[0] = CNR_Nyq + 10 * log(8, 10) + BW[0] = BW_Nyq / 8 + BR[0] = Shannon(BW[0], CNR[0]) + for i in range(1, 20): + BW[i] = BW[i - 1] * 2 ** (1 / 3) + CNR[i] = CNR[i - 1] - 10 * log(BW[i] / BW[i - 1], 10) + BR[i] = Shannon(BW[i], CNR[i]) + fig = plt.figure(figsize=(6, 4)) + ax = fig.add_subplot(111) + plt.plot(BW, BR, 'b') + Mark = ('D', 's', 'p', 'h', 'x') + for i in range(5): + ind = 3 * (i + 1) + BR_norm = BR[ind] / BR[9] + plt.plot(BW[ind], BR[ind], Mark[i] + 'b', label="{:.1f}".format(BW[ind]) + " MHz" + + " , {:.1f}".format(BR[ind]) + " Mbps" + " : {:.0%}".format(BR_norm)) + plt.title('Theoretical Bit Rate at Constant Power\nC/N\N{SUBSCRIPT ZERO} = ' + + "{:.1f}".format(C_N0_l) + " MHz" ) + plt.xlabel('Bandwidth [MHz]') + plt.ylabel('Bit Rate [Mbps]') + plt.grid(True) + plt.legend(loc='lower right') + + if Web_Version: + window_matfig(fig, title_fig='Bandwidth Sensitivity', title_win='Shannon for Dummies') + else: + plt.show(block=False) + + elif event == '-Pow_Graph-': + P_mul = np.zeros(20) + BR = np.zeros(20) + CNR = np.zeros(20) + P_mul[0] = 1 / 8 + CNR[0] = CNR_Nyq - 10 * log(8, 10) + BR[0] = Shannon(BW_Nyq, CNR[0]) + for i in range(1, 20): + P_mul[i] = P_mul[i-1] * 2 ** ( 1 / 3 ) + CNR[i] = CNR[i - 1] + 10 * log( 2 ** ( 1 / 3 ), 10 ) + BR[i] = Shannon(BW_Nyq, CNR[i]) + fig = plt.figure(figsize=(6, 4)) + ax = fig.add_subplot(111) + plt.plot(P_mul, BR, 'b') + Mark = ('D', 's', 'p', 'h', 'x') + for i in range(5): + ind = 3 * (i + 1) + BR_norm = BR[ind] / BR[9] + plt.plot(P_mul[ind], BR[ind], Mark[i] + 'b', label='{:.2f}'.format(P_mul[ind]) + + 'x , {:.1f}'.format(BR[ind]) + ' Mbps' + ' : {:.0%}'.format(BR_norm)) + plt.title('Theoretical Bit Rate at Constant Bandwidth : ' + '{:.1f}'.format(BW_Nyq) + ' MHz \n' + 'Reference : C/N = {:.1f}'.format(CNR_l) + ' [Linear Format]') + plt.xlabel('Power Multiplying Factor') + plt.ylabel('Bit Rate [Mbps]') + plt.grid(True) + plt.legend(loc='lower right') + + if Web_Version: + window_matfig(fig, title_fig='Power Sensitivity', title_win='Shannon for Dummies') + else: + plt.show(block=False) + + elif event == '-Map-' : + BR_mul=np.zeros((21,21)) + BW_mul=np.zeros((21,21)) + P_mul=np.zeros((21,21)) + for i in range(21): + for j in range(21): + BW_mul[i, j] = (i + 1)/4 + P_mul[i, j] = (j + 1)/4 + BR_mul[i, j] = BR_Multiplier(BW_mul[i, j], P_mul[i, j], CNR_Nyq) + fig = plt.figure(figsize=(6, 4)) + ax = fig.add_subplot(111) + Map = plt.contour(BW_mul,P_mul,BR_mul, 20) + plt.clabel(Map, inline=1, fontsize=8,fmt='%.2f') + plt.title('Bit Rate Multiplying Factor, \n Reference : C/N = {:.1f}'.format(CNR_Nyq) + ' dB, BW = ' + + '{:.1f}'.format(BW_Nyq) + ' MHz , C/N\N{SUBSCRIPT ZERO} = ' + '{:.1f}'.format(C_N0_l) + + ' MHz, BR = {:.1f}'.format(BRbw) + ' Mbps', fontsize=10) + plt.xlabel('Bandwidth Multiplying Factor') + plt.ylabel('Power Multiplying Factor') + plt.grid(True) + + if Web_Version: + window_matfig(fig, title_fig='Multiplying Factors Map', title_win='Shannon for Dummies') + else: + plt.show(block=False) + + elif event == '-Write_Ct-' : + Contribution_Write('Shannon_Theory.db') + + elif event == '-Read_Ct-' : + Contribution_Read('Shannon_Theory.db') + + elif event == '-Real-': # ---------------- Second Panel : Real World ------------------- + + P_i1, P_i2, P_i3 = '', '', '' + + fr1 = sg.Frame('Satellite Link',[ + [sg.Text('Satellite Altitude [km] ​', **form_i, key='-iSatAlt-'), + sg.Input('35786', **form_iv, key='-SatAlt-'), + sg.Text('Satellite Lat, Long ​[\N{DEGREE SIGN}]', **form_i, key='-iSatLatLong-'), + sg.Input('0.0, 19.2', **form_iv, key='-SatLatLong-'), + sg.Text('Ground Station Lat, Long ​[\N{DEGREE SIGN}]', **form_i, key='-iGSLatLong-'), + sg.Input('49.7, 6.3', **form_iv, key='-GSLatLong-')], + [sg.Text('HPA Output Power [W]',**form_i,key='-iHPA-'), + sg.Input('120', **form_iv, key='-HPA_P-'), + sg.Text('Output Losses [dB]',**form_i,key='-iLoss-'), + sg.Input('2', **form_iv, key='-Losses-'), + sg.Text('TX Impairments, C/I [dB]', **form_i, key='-iSCIR-'), + sg.Input('25, 25', **form_iv, key='-Sat_CIR-')], + [sg.Text('Beam Diameter [\N{DEGREE SIGN}]', **form_i, key='-iSBeam-'), + sg.Input('3', **form_iv, key='-Sat_Beam-'), + sg.Text('Offset from Peak [dB] ', **form_i, key='-iGOff-'), + sg.Input('0', **form_iv, key='-Gain_Offset-'), sg.Text('', size=(7, 1)), + sg.Text('', **form_CRC, key='-CRC1-'), LEDIndicator('-OK1-')], + [sg.Text('Frequency [GHz]', **form_i, key='-iFreq-'), + sg.Input('12', **form_iv, key='-Freq-'), + sg.Text('Link Availability [%]', **form_i, key='-iAvail-'), + sg.Input('99.9', **form_iv, key='-Avail-')], + [sg.Text('Output Power', **form_o,key='-iOPow-'), + sg.Input('', size=(35, 1), key='-Feed_P-', justification='center')], + [sg.Text('Satellite Antenna Gain​', **form_o,key='-iSGain-'), + sg.Input('', size=(35, 1), key='-Sat_G-', justification='center')], + [sg.Text('Equivalent Isotropic Radiated Power​', **form_o,key='-iEIRP-'), + sg.Input('', size=(35, 1), key='-EIRP-', justification='center')], + [sg.Text('Path Length @ Elevation', **form_o, key='-iPathLength-'), + sg.Input('', size=(35, 1), key='-PathLength-', justification='center')], + [sg.Text('Path Dispersion Loss', **form_o, key='-iPLoss-'), + sg.Input('', size=(35, 1), key='-PLoss-', justification='center')], + [sg.Text('Atmospheric Attenuation', **form_o, key='-iAtmLoss-'), + sg.Input('', size=(35, 1), key='-AtmLoss-', justification='center')], + [sg.Text('Power Flux Density', **form_o,key='-iPFD-'), + sg.Input('', size=(35, 1), key='-PFD-', justification='center')], + ], key='-SatLink-') + + fr2=sg.Frame('Radio Front End ',[ + [sg.Text('Receive Antenna Size [m]​',**form_i,key='-iCPE-'), + sg.Input('0.6', **form_iv, key='-CPE_Ant-'), + sg.Text('Noise Temperature [K] ​', **form_i, key='-iCPE_T-'), + sg.Input('140', **form_iv, key='-CPE_T-'), sg.Text('',size=(7, 1)), + sg.Text('', **form_CRC, key='-CRC2-'),LEDIndicator('-OK2-')], + [sg.Text('Customer Antenna Effective Area and G/T', **form_o,key='-iCGain-'), + sg.Input('', size=(35, 1), key='-CPE_G-', justification='center')], + [sg.Text('RX Power at Antenna Output', **form_o,key='-iRXPow-'), + sg.Input('', size=(35, 1), key='-RX_P-', justification='center')], + [sg.Text('Noise Power Density Antenna Output', **form_o, key='-iN0-'), + sg.Input('', size=(35, 1), key='-N0-', justification='center')], + [sg.Text('Bit Rate at infinite Bandwidth', **form_o,key='-iBRinf-'), + sg.Input('', size=(35, 1), key='-BRinf-', justification='center')], + [sg.Text('Bit Rate at Spectral Efficiency=1', **form_o,key='-iBRUnit-'), + sg.Input('', size=(35, 1), key='-BRUnit-', justification='center')], + [sg.Text('Bit Rate at Spectral Efficiency=2', **form_o,key='-iBRdouble-'), + sg.Input('', size=(35, 1), key='-BRdouble-', justification='center')] + ], key='-iRadio-') + + fr3=sg.Frame('Baseband Unit',[ + [sg.Text('Occupied Bandwidth [MHz]',**form_i,key='-iBW-'), + sg.Input('36', **form_iv, key='-BW-'), + sg.Text('Nyquist Filter Rolloff [%]',**form_i,key='-iRO-'), + sg.Input('5', **form_iv, key='-RO-'), + sg.Text('Higher Layers Overhead [%]', **form_i, key='-iOH-'), + sg.Input('5', **form_iv, key='-OH-')], + [sg.Text('RX Impairments, C/I [dB]',**form_i,key='-iCIR-'), + sg.Input('20', **form_iv, key='-CIR-'), + sg.Text('Implementation Penalty [dB]​',**form_i,key='-iPenalty-'), + sg.Input('1.5', **form_iv, key='-Penalty-'), sg.Text('',size=(7, 1)), + sg.Text('', **form_CRC, key='-CRC3-'), LEDIndicator('-OK3-')], + [sg.Text('Signal to Noise Ratio in Available BW', **form_o,key='-iCNRbw-'), + sg.Input('', size=(35, 1), key='-CNRbw-', justification='center')], + [sg.Text('Signal to Noise Ratio in Nyquist BW', **form_o,key='-iCNRnyq-'), + sg.Input('', size=(35, 1), key='-CNRnyq-', justification='center')], + [sg.Text('Signal to Noise Ratio at Receiver Output', **form_o,key='-iCNRrcv-'), + sg.Input('', size=(35, 1), key='-CNRrcv-', justification='center')], + [sg.Text('Theoretical Bit Rate', **form_o,key='-iBRnyq-'), + sg.Input('', size=(35, 1), key='-BRnyq-', justification='center')], + [sg.Text('Practical Bit Rate', **form_o,key='-iBRrcv-'), + sg.Input('', size=(35, 1), key='-BRrcv-', justification='center')], + [sg.Text('Practical Higher Layers Bit Rate', **form_o,key='-iBRhigh-'), + sg.Input('', size=(35, 1), key='-BRhigh-', justification='center')], + [sg.Button('Evaluation', key='-Evaluation-',visible=False, bind_return_key = True), + sg.Button('BW Sensitivity', pad=((100,5),(2,2)), key='-BW_Graph-'), + sg.Button('Power Sensitivity',key='-Pow_Graph-'), + sg.Button('BR Factor Map', key='-Map-'), + sg.Button('Back to Theory', key='-Back-')], + ], key='-Baseband-') + + fr4 = sg.Frame('Background Information (click on items)', + [[sg.Multiline('Click on parameter\'s label to get information', size=(80, 15),key='-Dialog-')], + [sg.Button('Advanced'), sg.Button('Write Contribution', key='-Write_Ct-'), + sg.Button('Read Contributions',key='-Read_Ct-'), sg.Button('Help')]],element_justification='center') + + layout2 =[ + [sg.Column([[fr1],[fr2],[fr3]]), + sg.Column([[sg.Text('', size=(55, 1), justification='center', key='-FCRC-')], + [sg.Button('Wiki : Harry Nyquist​', size=(25,1), key='-W_Nyquist-'), + sg.Button('Wiki : Richard Hamming​', size=(25,1), key='-W_Hamming-')], + [sg.Button('Wiki : Andrew Viterbi​', size=(25,1), key='-W_Viterbi-'), + sg.Button('Wiki : Claude Berrou​', size=(25,1), key='-W_Berrou-')], + [sg.Image(filename='Satellite.png', key='-Satellite-',background_color='black', enable_events=True)], + [fr4]], element_justification='center')]] + + window2 = sg.Window('Shannon and Friends in the Real World', layout2, finalize=True) + + window2['-FCRC-'].Update('Program\'s CRC: ' + File_CRC) + + # Needed for the Web version (on a finalized window) + window2['-Freq-'].Update('12') + window2['-Losses-'].Update('2') + window2['-SatAlt-'].Update('35786') + window2['-SatLatLong-'].Update('0.0, 19.2') + window2['-Avail-'].Update('99.9') + window2['-GSLatLong-'].Update('49.7, 6.3') + window2['-HPA_P-'].Update('120') + window2['-Sat_CIR-'].Update('25, 25') + window2['-Sat_Beam-'].Update('3') + window2['-Gain_Offset-'].Update('0') + window2['-CPE_Ant-'].Update('0.6') + window2['-CPE_T-'].Update('120') + window2['-Penalty-'].Update('1.5') + window2['-BW-'].Update('36') + window2['-CIR-'].Update('25') + window2['-RO-'].Update('5') + window2['-OH-'].Update('5') + window2['-AtmLoss-'].Update(' ... LOADING ...') + + First_Pass = True + while True: + + event2, values = window2.read(timeout=Win_time_out,timeout_key = '__TIMEOUT__') + + if event2 != '__TIMEOUT__' : print(event2) + + P_err = 0 # Error ID + + try: + + if event2 == sg.WIN_CLOSED or event2 == '-Back-': # closes window + break + + elif event2 == '-Evaluation-'or event2 == '__TIMEOUT__' : + + if First_Pass : + window2['-Dialog-'].Update(Shd.Help2['-Satellite-']) + First_Pass = False + + P_err = 1 # Error ID + Freq = float(values['-Freq-']) # GHz + HPA_Power = float(values['-HPA_P-']) # Watts + Sat_Loss = float(values['-Losses-']) # dB + Sat_CIR_List = values['-Sat_CIR-'] # list if comma separated CIR contributions in dB + Sat_CIR = Combine_CNR(*[float(val) for val in Sat_CIR_List.split(',')]) + Sig_Power = HPA_Power * 10 ** (-Sat_Loss / 10) # Watts + Sat_Beam = float(values['-Sat_Beam-']) #dB + Gain_Offset = float(values['-Gain_Offset-']) #dB + Sat_Ant_eff = 0.65 # Factor + + window2['-Feed_P-'].Update(Power_Format(Sig_Power)) + + Lambda = 300e6 / Freq / 1e9 # meter + Sat_Gain_l = Sat_Ant_eff * ( pi * 70 / Sat_Beam ) ** 2 + Sat_Gain_l = Sat_Gain_l * 10**(-Gain_Offset/10) + Sat_Ant_d = 70 * Lambda / Sat_Beam + Sat_Gain_dB = 10 * log(Sat_Gain_l, 10) + + window2['-Sat_G-'].Update(Gain_Format(Sat_Gain_l)) + + EIRP_l = Sig_Power * Sat_Gain_l + EIRP_dB = 10 * log(EIRP_l, 10) + window2['-EIRP-'].Update(Power_Format(EIRP_l)) + + Avail = float(values['-Avail-']) + + R_earth=6378 + + [lat_GS, lon_GS] = [float(val) for val in values['-GSLatLong-'].split(',')] + [lat_sat, lon_sat] = [float(val) for val in values['-SatLatLong-'].split(',')] + + h_sat = float(values['-SatAlt-']) + + Path_Length = sqrt (h_sat**2 + 2 * R_earth * (R_earth + h_sat ) * + ( 1 - cos(np.radians(lat_sat-lat_GS)) * cos(np.radians(lon_sat-lon_GS)))) + + # elevation = itur.utils.elevation_angle(h_sat, lat_sat, lon_sat, lat_GS, lon_GS) # non signed + + Phi = acos(cos(np.radians(lat_sat-lat_GS)) * cos(np.radians(lon_sat-lon_GS))) + + if Phi > 0 : + elevation = np.degrees(atan((cos(Phi)-R_earth/(R_earth+h_sat))/sqrt(1-cos(Phi)**2))) + else : + elevation = 90 + + if elevation <= 0 : + Atm_Loss = 999 + else: + Atm_Loss = itur.atmospheric_attenuation_slant_path(lat_GS, lon_GS, Freq, + elevation, 100-Avail, 1).value + + window2['-AtmLoss-'].Update("{:.1f}".format(10 ** (Atm_Loss/10)) + ' [Linear] .. ' + + "{:.1f}".format(Atm_Loss) + " [dB]") + window2['-PathLength-'].Update("{:.1f}".format(Path_Length) + " [km] @ " + + "{:.1f}".format(elevation) + " [\N{DEGREE SIGN}]") + + Free_Space_Loss_l = (4 * pi * Path_Length * 1000 / Lambda) ** 2 + Free_Space_Loss_dB = 10 * log(Free_Space_Loss_l, 10) + + Path_Loss_l = 4 * pi * (Path_Length * 1000) ** 2 + Path_Loss_dB = 10 * log(Path_Loss_l, 10) + window2['-PLoss-'].Update(PLoss_Format(Path_Loss_l)) + + PFD_l = EIRP_l / Path_Loss_l * 10 ** (-Atm_Loss/ 10) + PFD_dB = 10 * log(PFD_l, 10) + window2['-PFD-'].Update(PFD_Format(PFD_l)) + + P_err = 2 # Error ID + CPE_Ant_d = float(values['-CPE_Ant-']) # meter + CPE_T_Clear= float(values['-CPE_T-']) # K + CPE_Ant_eff = 0.6 + CPE_T_Att = (CPE_T_Clear - 40) + 40 * 10 ** (-Atm_Loss/10) + 290 * (1 - 10 ** (-Atm_Loss/10)) + k_Boltz = 1.38e-23 # J/K + + P_err = 3 # Error ID + Penalties = float(values['-Penalty-']) # dB, overall implementation penalty + Bandwidth = float(values['-BW-']) # MHz + CNR_Imp_List = values['-CIR-'] # List of comma separated CNR impairments in dB + CNR_Imp = Combine_CNR(*[float(val) for val in CNR_Imp_List.split(',')]) + Rolloff = float(values['-RO-']) # percent + Overheads = float(values['-OH-']) # percent + + CPE_Ae = pi * CPE_Ant_d ** 2 / 4 * CPE_Ant_eff + CPE_Gain_l = (pi * CPE_Ant_d / Lambda) ** 2 * CPE_Ant_eff + CPE_Gain_dB = 10 * log(CPE_Gain_l, 10) + CPE_Beam = 70 * Lambda / CPE_Ant_d # diameter in degrees + CPE_G_T = 10 * log(CPE_Gain_l / CPE_T_Att, 10) + window2['-CPE_G-'].Update( + "{:.1f}".format(CPE_Ae)+" m\N{SUPERSCRIPT TWO} .. {:.1f}".format(CPE_G_T)+" dB/K") + + RX_Power_l = PFD_l * CPE_Ae + # Alternative : RX_Power_l=EIRP_l/Free_Space_Loss_l*CPE_Gain_l + RX_Power_dB = 10 * log(RX_Power_l,10) + N0 = k_Boltz * CPE_T_Att # W/Hz + C_N0_l = RX_Power_l / N0 # Hz + C_N0_dB = 10 * log(C_N0_l, 10) # dBHz + window2['-RX_P-'].Update('C : ' + Power_Format(RX_Power_l)) + window2['-N0-'].Update('N\N{SUBSCRIPT ZERO} : ' + PSD_Format(N0*1e6)) + + BW_Spe_1 = C_N0_l / 1e6 # C_N0 without Penalty in MHz + BW_Spe_1half = BW_Spe_1 / (2 ** 0.5 - 1) # MHz + BW_Spe_1quarter = BW_Spe_1 / (2 ** 0.25 - 1) + BW_Spe_double = BW_Spe_1 / (2 ** 2 - 1) + + BR_Spe_1 = BW_Spe_1 # Mbps + BR_Spe_1half = BW_Spe_1half / 2 + BR_Spe_1quarter = BW_Spe_1quarter / 4 + BR_Spe_double= BW_Spe_double * 2 + BR_infinity = BW_Spe_1 / log(2) + BR_Spe_1_Norm = BR_Spe_1 / BR_infinity + BR_Spe_1half_Norm = BR_Spe_1half / BR_infinity + BR_Spe_1quarter_Norm = BR_Spe_1quarter / BR_infinity + BR_Spe_double_Norm = BR_Spe_double / BR_infinity + window2['-BRinf-'].Update('1.443 C/N\N{SUBSCRIPT ZERO} : ' + + BR_Format(BR_infinity)+" .. {:.0%}".format(1)) + window2['-BRUnit-'].Update('C/N\N{SUBSCRIPT ZERO} : ' + + BR_Format(BR_Spe_1)+" .. {:.0%}".format(BR_Spe_1_Norm)) + window2['-BRdouble-'].Update('0.667 C/N\N{SUBSCRIPT ZERO} : ' + + BR_Format(BR_Spe_double)+" .. {:.0%}".format(BR_Spe_double_Norm)) + + CNR_Spe_1 = 0 # dB + CNR_Spe_1half = -10 * log(BW_Spe_1half / BW_Spe_1, 10) + CNR_Spe_1quarter = -10 * log(BW_Spe_1quarter / BW_Spe_1, 10) + CNR_Spe_double = -10 * log(BW_Spe_double/ BW_Spe_1, 10) + + CNR_BW = CNR_Spe_1 + 10 * log(BW_Spe_1 / Bandwidth, 10) # dB + BW_Nyq = Bandwidth / (1 + Rolloff / 100) # MHz + CNR_Nyq = CNR_Spe_1 + 10 * log(BW_Spe_1 / BW_Nyq, 10) # dBB + CNR_Rcv = Combine_CNR (CNR_Nyq, CNR_Imp, Sat_CIR) # + BR_BW = Shannon (Bandwidth, CNR_BW) # Mbps + BR_Nyq = Shannon (BW_Nyq, CNR_Nyq) # Mbps + BR_Rcv = Shannon (BW_Nyq, CNR_Rcv,Penalties) # Mbps + BR_Rcv_Higher = BR_Rcv / (1 + Overheads / 100) # Mbps + BR_BW_Norm = BR_BW / BR_infinity + BR_Nyq_Norm = BR_Nyq / BR_infinity + BR_Rcv_Norm = BR_Rcv / BR_infinity + BR_Rcv_H_Norm = BR_Rcv_Higher / BR_infinity + Spe_BW = BR_BW / Bandwidth + Spe_Nyq = BR_Nyq / Bandwidth # Efficiency in available bandwidth + Bits_per_Symbol = BR_Nyq / BW_Nyq # Efficiency in Nyquist bandwidth + Spe_Rcv = BR_Rcv / Bandwidth + Spe_Higher = BR_Rcv_Higher / Bandwidth + + window2['-CNRbw-'].Update("{:.1f}".format(CNR_BW)+" dB in "+"{:.1f}".format(Bandwidth)+" MHz") + window2['-CNRnyq-'].Update("{:.1f}".format(CNR_Nyq)+" dB in "+"{:.1f}".format(BW_Nyq)+" MHz") + window2['-CNRrcv-'].Update("{:.1f}".format(CNR_Rcv)+" dB") + window2['-BRnyq-'].Update(BR_Format(BR_Nyq) + " .. {:.0%}".format(BR_Nyq_Norm)+ + " .. {:.1f}".format(Spe_Nyq)+" bps/Hz .. {:.1f}".format(Bits_per_Symbol)+" b/S") + window2['-BRrcv-'].Update(BR_Format(BR_Rcv) + " .. {:.0%}".format(BR_Rcv_Norm)+ + " .. {:.1f}".format(Spe_Rcv)+" bps/Hz") + window2['-BRhigh-'].Update(BR_Format(BR_Rcv_Higher)+" .. {:.0%}".format(BR_Rcv_H_Norm)+ + " .. {:.1f}".format(Spe_Higher)+" bps/Hz") + + Params1 = str(Freq) + ',' + str(Path_Length) + ',' + str(Atm_Loss) + ',' + \ + str(HPA_Power) + ',' + str(Sat_Loss) + ',' + str(Sat_CIR) + ',' + \ + str(Sat_Beam) + ',' + str(Gain_Offset) + Params2 = str(CPE_Ant_d) + ',' + str(CPE_T_Clear) + Params3 = str(Bandwidth) + ',' + str(Rolloff) + ',' + str(Overheads) + ',' + \ + str(CNR_Imp) + ',' + str(Penalties) + + Params1_CRC=hex(binascii.crc32(Params1.encode('ascii')) & 0xFFFFFFFF)[2:].upper() + Params2_CRC = hex(binascii.crc32(Params2.encode('ascii')) & 0xFFFFFFFF)[2:].upper() + Params3_CRC = hex(binascii.crc32(Params3.encode('ascii')) & 0xFFFFFFFF)[2:].upper() + + elif event2 in ('-iFreq-','-iHPA-','-iSBeam-','-iLoss-', '-iGOff-','-iFade-', '-iSCIR-', + '-iOPow-','-iSGain-', '-iEIRP-', '-iPFD-', '-iCPE-','-iCGain-','-iRXPow-', + '-iBRinf-','-iBRhalf-', '-iBRUnit-','-iBRdouble-','-iBW-','-iRO-','-iCIR-', + '-iPenalty-', '-iOH-','-iNBW-', '-iCNRbw-','-iCNRnyq-','-iCNRrcv-','-iBRbw-', + '-iBRnyq-','-iBRrcv-','-iBRhigh-','-Satellite-','-iPLoss-','Advanced','Help', + '-CRC1-', '-CRC2-', '-CRC3-', '-iN0-','-iCPE_T-', '-iSatAlt-','-iSatLatLong-', + '-iGSLatLong-', '-iAvail-', '-iPathLength-','-iAtmLoss-'): + window2['-Dialog-'].Update(Shd.Help2[event2]) + + elif event2 == '-BW_Graph-' : + BW = np.zeros(20) + BR = np.zeros(20) + CNR = np.zeros(20) + CNR[0] = CNR_Nyq+10*log(8,10) + BW[0] = Bandwidth/8 + BR[0] = Shannon(BW[0]/(1+Rolloff/100), CNR[0], Penalties) / (1 + Overheads / 100) + for i in range(1, 20): + BW[i] = BW[i - 1] * 2 ** (1 / 3) + CNR[i] = CNR[i - 1] - 10 * log(BW[i] / BW[i - 1], 10) + CNR_Rcv_i = Combine_CNR(CNR[i], CNR_Imp, Sat_CIR) + BR[i] = Shannon(BW[i]/(1+Rolloff/100), CNR_Rcv_i, Penalties) / (1 + Overheads / 100) + + fig = plt.figure(figsize=(6, 4)) + ax = fig.add_subplot(111) + plt.plot(BW, BR, 'b') + Mark = ('D', 's', 'p', 'h', 'x') + for i in range(5): + ind = 3 * (i + 1) + BR_norm = BR[ind] / BR[9] + plt.plot(BW[ind], BR[ind], Mark[i] + 'b',label="{:.1f}".format(BW[ind]) + + " MHz" + " , {:.1f}".format(BR[ind]) + " Mbps" + " : {:.0%}".format(BR_norm)) + plt.title('Higher Layers Bit Rate at Constant HPA Output Power : ' + + "{:.1f}".format(HPA_Power) + " W") + plt.xlabel('Occupied Bandwidth [MHz]') + plt.ylabel('Bit Rate [Mbps]') + plt.grid(True) + plt.legend(loc='lower right') + + if Web_Version: + window_matfig(fig, title_fig='Bandwidth Sensitivity', + title_win='Shannon and Friends in the Real World') + else: + plt.show(block=False) + + elif event2 == '-Pow_Graph-' : + Power = np.zeros(20) + BR = np.zeros(20) + CNR = np.zeros(20) + Power[0] = HPA_Power / 8 + CNR[0] = CNR_Nyq-10*log(8,10) + CNR_Rcv_i = Combine_CNR(CNR[0], CNR_Imp, Sat_CIR) + BR[0] = Shannon(BW_Nyq, CNR_Rcv_i, Penalties) / (1 + Overheads / 100) + for i in range(1, 20): + Power[i] = Power[i-1] * 2 ** (1 / 3) + CNR[i] = CNR[i - 1] + 10 * log( 2 ** (1 / 3) , 10 ) + CNR_Rcv_i = Combine_CNR(CNR[i], CNR_Imp, Sat_CIR) + BR[i] = Shannon(BW_Nyq, CNR_Rcv_i, Penalties) / (1 + Overheads / 100) + fig = plt.figure(figsize=(6, 4)) + ax = fig.add_subplot(111) + plt.plot(Power, BR, 'b') + Mark = ('D', 's', 'p', 'h', 'x') + for i in range(5): + ind = 3 * (i + 1) + BR_norm=BR[ind]/BR[9] + plt.plot(Power[ind], BR[ind], Mark[i] + 'b', label="{:.1f}".format(Power[ind]) + " W" + + " , {:.1f}".format(BR[ind]) + " Mbps"+" : {:.0%}".format(BR_norm)) + plt.title('Higher Layers Bit Rate at Constant Occupied Bandwidth : ' + + "{:.1f}".format(Bandwidth) + " MHz") + plt.xlabel('HPA Output Power @ Operating Point [Watts]') + plt.ylabel('Bit Rate [Mbps]') + plt.grid(True) + plt.legend(loc='lower right') + + if Web_Version: + window_matfig(fig, title_fig='Power Sensitivity', + title_win='Shannon and Friends in the Real World') + else: + plt.show(block=False) + + elif event2 == '-Map-': + BR_mul = np.zeros((21, 21)) + BW_mul = np.zeros((21, 21)) + P_mul = np.zeros((21, 21)) + BR_00 = BR_Rcv_Higher + for i in range(21): + for j in range(21): + BW_mul[i, j] = (i + 1) / 4 + P_mul[i, j] = (j + 1) / 4 + CNR_ij = CNR_Nyq + 10 * log( P_mul[i, j] / BW_mul[i, j] , 10 ) + CNR_Rcv_ij = Combine_CNR (CNR_ij, CNR_Imp, Sat_CIR) + BW_ij = BW_Nyq * BW_mul[i, j] + BR_ij = Shannon( BW_ij / (1 + Rolloff / 100), CNR_Rcv_ij, Penalties) / (1 + Overheads / 100) + BR_mul[i, j] = BR_ij / BR_00 + fig = plt.figure(figsize=(6, 4)) + ax = fig.add_subplot(111) + Map = plt.contour(BW_mul, P_mul, BR_mul, 20) + plt.clabel(Map, inline=1, fontsize=8, fmt='%.2f') + plt.title('Bit Rate Multiplying Factor, \n Reference : Power = {:.1f}'.format(HPA_Power) + + ' W , BW = ' + '{:.1f}'.format(Bandwidth) + + ' MHz , BR = {:.1f}'.format(BR_Rcv_Higher) + ' Mbps') + plt.xlabel('Bandwidth Multiplying Factor') + plt.ylabel('Power Multiplying Factor') + plt.grid(True) + + if Web_Version: + window_matfig(fig, title_fig='Multiplying Factors Map', + title_win='Shannon and Friends in the Real World') + else: + plt.show(block=False) + + elif event2 == '-W_Nyquist-': + webbrowser.open('https://en.wikipedia.org/wiki/Harry_Nyquist') + elif event2 == '-W_Hamming-': + webbrowser.open('https://en.wikipedia.org/wiki/Richard_Hamming') + elif event2 == '-W_Viterbi-': + webbrowser.open('https://en.wikipedia.org/wiki/Andrew_Viterbi') + elif event2 == '-W_Berrou-': + webbrowser.open('https://en.wikipedia.org/wiki/Claude_Berrou') + + elif event2 == '-Write_Ct-': + Contribution_Write('Shannon_Real.db') + + elif event2 == '-Read_Ct-': + Contribution_Read('Shannon_Real.db') + + else: + print("Untrapped event : "+event2) + + + except : + window2['-Dialog-'].Update('Invalid input fields') + if P_err == 1 : + window2['-CRC1-'].Update('-') + SetLED(window2, '-OK1-', 'red') + if P_err == 2 : + window2['-CRC2-'].Update('-') + SetLED(window2, '-OK2-', 'red') + if P_err == 3 : + window2['-CRC3-'].Update('-') + SetLED(window2, '-OK3-', 'red') + Err_msg=True + else: + window2['-CRC1-'].Update(Params1_CRC) + window2['-CRC2-'].Update(Params2_CRC) + window2['-CRC3-'].Update(Params3_CRC) + SetLED(window2, '-OK1-', 'green') + SetLED(window2, '-OK2-', 'green') + SetLED(window2, '-OK3-', 'green') + if Err_msg : + window2['-Dialog-'].Update('Input Validated') + Err_msg = False + + window2.close() + + except : + window['-Dialog-'].Update('Invalid input fields') + window['-CRC-'].Update('-') + SetLED(window, '-OK-', 'red') + Err_msg = True + else: + window['-CRC-'].Update(Params_CRC) + SetLED(window, '-OK-', 'green') + if Err_msg: + window['-Dialog-'].Update('Input Validated') + Err_msg = False + +window.close() diff --git a/Shannon.py.save b/Shannon.py.save new file mode 100644 index 0000000..3c43740 --- /dev/null +++ b/Shannon.py.save @@ -0,0 +1,1024 @@ +''' ---------------------------------------------------------------------------------------------------------------- + +Shannon Equation for Dummies - JPC Feb 2021 + +Educational Application + +Exploration from Claude's Shannon initial theory to its practical application to satellite communications + +The application Runs either in local windows or in web pages + +The Web version via localhost is fully functional although not as convenient as the windowed version (only 1 plot open) + +The Web version in remote does work for a single user (all users connected can send command but see the same page) + +--------------------------------------------------------------------------------------------------------------------''' + +''' ------------------------------------------ Imports --------------------------------------------------- + +The GUI has been designed to be compatible with both PySimpleGUIWeb and PySimpleGUI + +The PySimpleGUi version takes full benefit of the matplotlib windowing whereas the Web version is constrained to use +a web compatible method with only one graph at a time + +''' +from math import * +import matplotlib.pyplot as plt +import numpy as np +import webbrowser +import Shannon_Dict as Shd +import sqlite3 +import os +import binascii +import itur +from astropy.units import imperial + +Web_Version=True # +Web_Remote=True # +imperial.enable() + +if Web_Version : + import PySimpleGUIWeb as sg + from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, FigureCanvasAgg + import io + def draw_matfig(fig, element): + canv = FigureCanvasAgg(fig) + buf = io.BytesIO() + canv.print_figure(buf, format='png') + if buf is None: + return None + buf.seek(0) + data = buf.read() + element.update(data=data) + def window_matfig(fig, title_fig, title_win): + matlayout = [[sg.T(title_fig, font='Any 20')], + [sg.Image(key='-IMAGE-')], + [sg.B('Exit')]] + winmat = sg.Window(title_win, matlayout, finalize=True) + draw_matfig(fig, winmat['-IMAGE-']) + while True: + event, values = winmat.read() + if event == 'Exit' or event == sg.WIN_CLOSED: + break + plt.close() + winmat.close() +else: + import PySimpleGUI as sg + +if Web_Version and Web_Remote : + web_cfg={"web_ip":'127.0.0.1',"web_port":8080,"web_start_browser":False} +else: + web_cfg={} + +''' ------------------------------------------ Core Functions --------------------------------------------------- + +These functions are mainly application of formulas given in the first panel and formatting functions + +''' + +def Combine_CNR(*CNR): + ''' Combination of Carrier to Noise Ratio ''' + NCR_l = 0 + for CNR_dB in CNR: + NCR_l += 10 ** (-CNR_dB / 10) # Summation of normalized noise variances + return -10 * log(NCR_l, 10) + +def Shannon(BW=36.0, CNR=10.0, Penalty=0.0): + ''' Shannon Limit, returns Bit Rate ''' + CNR_l = 10 ** ((CNR - Penalty) / 10) + return BW * log(1 + CNR_l, 2) + +def BR_Multiplier(BW_mul=1.0, P_mul=2.0, CNR=10.0): + ''' Returns BR multiplier ''' + CNR_l = 10 ** (CNR / 10) + return BW_mul * log(1 + CNR_l * P_mul / BW_mul, 2) / log(1 + CNR_l, 2) + +def Shannon_Points(BW=36.0, CNR=10.0): + ''' Returns CNR_l, BR_inf, C/N0 and BR(BW,CNR) ''' + CNR_l = 10 ** (CNR / 10) + C_N0_l = CNR_l * BW + BR_infinity = C_N0_l / log(2) + BR_constrained = Shannon(BW, CNR) + return CNR_l, BR_infinity, C_N0_l, BR_constrained + +def Shannon_Sp_Eff(Sp_Eff=0.5, BW=36.0, CNR=10.0): + ''' Returns values at required Spe : CNR, BW, BR ''' + C_N0_l = 10 ** (CNR / 10) * BW + BW_Spe_1 = C_N0_l + BW_Spe = C_N0_l / (2 ** Sp_Eff - 1) + BR_Spe = BW_Spe * Sp_Eff + CNR_Spe = 10 * log(BW_Spe_1 / BW_Spe, 10) + return CNR_Spe, BW_Spe, BR_Spe + +def BR_Format(BR=100): + return "{:.1f}".format(BR) + ' Mbps' + +def Power_Format(Pow=100): + Pow_dB=10*log(Pow,10) + if Pow > 1 and Pow < 1e4 : + return "{:.1f}".format(Pow) + ' W .. ' + "{:.1f}".format(Pow_dB) + ' dBW' + elif Pow <= 1 and Pow > 1e-3 : + return "{:.4f}".format(Pow) + ' W .. ' + "{:.1f}".format(Pow_dB) + ' dBW' + else : + return "{:.1e}".format(Pow) + ' W .. ' + "{:.1f}".format(Pow_dB) + ' dBW' + +def PFD_Format(Pow=1): # PSD in W per m2 + Pow_dB=10*log(Pow,10) + return "{:.1e}".format(Pow) + ' W/m\N{SUPERSCRIPT TWO} .. ' + "{:.1f}".format(Pow_dB) + ' dBW/m\N{SUPERSCRIPT TWO}' + +def PSD_Format(Pow=1): # PSD in W per MHz + Pow_dB=10*log(Pow,10) + return "{:.1e}".format(Pow) + ' W/MHz .. ' + "{:.1f}".format(Pow_dB) + ' dBW/MHz' + + +def Gain_Format(Gain=1000): + Gain_dB=10*log(Gain,10) + return "{:.1f}".format(Gain) + ' .. ' + "{:.1f}".format(Gain_dB) + ' dBi' + +def PLoss_Format(Loss=10): + Loss_dB=10*log(Loss,10) + return "{:.2}".format(Loss) + ' m\N{SUPERSCRIPT TWO} .. ' + "{:.1f}".format(Loss_dB) + ' dBm\N{SUPERSCRIPT TWO}' + + +''' ------------------------------------------ Database Functions --------------------------------------------------- + +Functions for management of users' contributions + +''' + +def Contribution_Write (DB_File) : + + Sh_DB=sqlite3.connect(DB_File) + Sh_DB_c=Sh_DB.cursor() + + Sh_DB_c.execute("CREATE TABLE IF NOT EXISTS contributions " + "(num INTEGER, name TEXT, title TEXT, keywords TEXT, text TEXT, date TEXT, password TEXT)") + Sh_DB.commit() + + Sh_DB_c.execute("SELECT MAX(num) FROM contributions") + ID_Contrib=Sh_DB_c.fetchall()[0][0] + + if ID_Contrib == None: + ID_Contrib = 0 + + print (ID_Contrib) + + layout3 = [ [sg.Text('Initials / name',size=(20,1), justification='center'), + sg.Input('',size=(45,1), key='-Name-')], + [sg.Text('Title ', size=(20, 1), justification='center'), + sg.Input('', size=(45, 1), key='-Title-')], + [sg.Text('Keywords ',size=(20,1), justification='center'), + sg.Input('', size=(45,1), key='-Keywords-')], + [sg.Text('Password ', size=(20, 1), justification='center'), + sg.Input('', size=(45, 1), key='-Password-')], + [sg.Frame('Write your text here', + [[sg.Multiline('', size=(80, 15), key='-Text-')], + [sg.Button('Validate'), + sg.Button('Exit'), + sg.Button('Help')]], + element_justification='center')] + ] + window3 = sg.Window('Write Contribution', layout3, finalize=True) + + while True: + event3, values = window3.read() + print(event3) + + if event3 == sg.WIN_CLOSED or event3 == 'Exit' : # if user closes window + break + + elif event3 == 'Validate' : + + ID_Contrib += 1 + + Sh_DB_c.execute("INSERT INTO contributions VALUES ( " + str (ID_Contrib) + ", '" + values['-Name-'] + + "', '" + values['-Title-'] + "', '" + values['-Keywords-'] + "', '" + values['-Text-'] + + "', " + "Date('now')" + ", '" + values['-Password-'] + "' )") + Sh_DB.commit() + + window3['-Text-'].Update('Thank you, your contribution has been successfully stored, ID #'+str(ID_Contrib)) + + elif event3 == 'Help': + window3['-Text-'].Update('Write your contribution here as a free text. Edit it in the text window until you' + ' are happy with the result and then Validate. \n\nContributions should be candid observations' + ' about the technical subject, references to relevant material (ideally as web pointers), open ' + ' discussion items about adjacent subjects, suggestion for improvement... \n\nYou can retrieve' + ' your contribution via a search from the Read Panel and delete them there with the relevant ' + 'password. Keeping the "no password" default will allow anyone to delete.') + + Sh_DB.close() + + window3.close() + + return ID_Contrib + +def Contribution_Read (DB_File) : + + layout4 = [ [sg.Text('Filter on Name',size=(20,1), justification='center'), + sg.Input('',size=(45,1), key='-Name-')], + [sg.Text('Filter on Title ', size=(20, 1), justification='center'), + sg.Input('', size=(45, 1), key='-Title-')], + [sg.Text('Filter on Keywords ',size=(20,1), justification='center'), + sg.Input('', size=(45,1), key='-Keywords-')], + [sg.Text('Filter on Content ', size=(20, 1), justification='center'), + sg.Input('', size=(45, 1), key='-Content-')], + [sg.Frame('Contribution', + [[sg.Multiline('', size=(80, 15), key='-Text-')], + [sg.Button('Load / Filter', key='-Filter-'), + sg.Button('Read Next', key='-Next-'), + sg.Button('Delete', key='-Del-'), + sg.Button('Exit'), + sg.Button('Help')]], + element_justification='center')] + ] + + if os.path.isfile(DB_File): + Sh_DB = sqlite3.connect(DB_File) + Sh_DB_c = Sh_DB.cursor() + else: + sg.popup('No Database Found', keep_on_top=True) + return 0 + + window4 = sg.Window('Read Contributions', layout4) + DB_ind = 0 + + while True: + + event4, values = window4.read() + + print(event4) + + if event4 == sg.WIN_CLOSED or event4 == 'Exit': # if user closes window + break + + elif event4 == '-Filter-': + + Name = "'%" + values['-Name-'] + "%'" + Title = "'%" + values['-Title-'] + "%'" + Keywords = "'%" + values['-Keywords-'] + "%'" + Content = "'%" + values['-Content-'] + "%'" + Sh_DB_c.execute("SELECT num, name, title, keywords, text, date, password FROM contributions WHERE " + + "name LIKE " + Name + + " AND title LIKE " + Title + + " AND keywords LIKE " + Keywords + + " AND text LIKE " + Content + + " ORDER BY num DESC " + " LIMIT 50 ") + DB_Extract = Sh_DB_c.fetchall() + + print(len(DB_Extract)) + + if len(DB_Extract) > 0 : # First Contribution read automatically + + window4['-Text-'].Update( DB_Extract[0][2] + ' , ' + DB_Extract[0][1] + + '\n\n' + DB_Extract[0][4] + '\n\n Keywords : ' + DB_Extract[0][3] + + '\n\n ID #' + str(DB_Extract[0][0]) + ' - ' + str(DB_Extract[0][5]) ) + DB_ind = 1 + + elif event4 == '-Next-' and DB_ind>0 : + + if DB_ind < len(DB_Extract): + window4['-Text-'].Update(DB_Extract[DB_ind][2] + ' , ' + DB_Extract[DB_ind][1] + + '\n\n' + DB_Extract[DB_ind][4] + '\n\n Keywords : ' + DB_Extract[DB_ind][3] + + '\n\n ID #' + str(DB_Extract[DB_ind][0]) + ' - ' + str(DB_Extract[DB_ind][5])) + DB_ind +=1 + + elif event4 == '-Del-': + Password = sg.popup_get_text('Enter Item\'s Password') + print (Password) + DB_ind -= 1 + if DB_Extract[DB_ind][6] == Password : + Item_num=DB_Extract[DB_ind][0] + Sh_DB_c.execute("DELETE FROM contributions WHERE num = " + str(Item_num)) + window4['-Text-'].Update('Item ID# '+ str(Item_num) + ' deleted') + else: + window4['-Text-'].Update('Incorrect password, item unaffected') + + elif event4 == 'Help': + window4['-Text-'].Update('To read contributions you have to load them first. If the search fields are empty' + ', all contributions will be loaded. You should only enter 1 item per search field.' + '\n\n You can delete items if you know the associated password. Items are not encrypted' + ' in the Database and can be removed by the admin.') + + Sh_DB.commit() + Sh_DB.close() + window4.close() + + return DB_ind + + +''' ------------------------------------------ GUI Functions--------------------------------------------------- + +LEDs from PySimpleGUI demos + +''' + + +def LEDIndicator(key=None, radius=30): + return sg.Graph(canvas_size=(radius, radius), + graph_bottom_left=(-radius, -radius), + graph_top_right=(radius, radius), + pad=(0, 0), key=key) + +def SetLED(window, key, color): + graph = window[key] + graph.erase() + graph.draw_circle((0, 0), 12, fill_color=color, line_color='black') + + +''' ------------------------------------------ Main Program --------------------------------------------------- + +The program has 2 main panels associated with events collection loops + +As the loops are build, when the second panel is open, events are only collected for this panel + +In the windowed version, matplotlib plots don't interfere with the event loops : as many as desired can be open + +''' + +# ---------------- First Panel : Shannon's Equation ------------------- + +form_i = {"size":(22,1),"justification":'left',"enable_events":True} # input label format +form_iv = {"size":(8,1),"justification":'center'} # imput value format +form_ov = {"size":(20,1),"justification":'center'} # output value format +form_o = {"size":(65,1),"justification":'right',"enable_events":True} # output label format, also using input elements +form_CRC = {"size":(8,1),"justification":'center',"enable_events":True} # CRC Format + +sg.set_options(auto_size_buttons=False, button_element_size=(14,1)) + +col1=sg.Column([[sg.Frame('Theoretical Exploration', + [[sg.Text(' Reference C/N [dB] ', **form_i,key='-iCNR-'),sg.Input('12', **form_iv, key='-CNR-')], + [sg.Text(' Reference BW [MHz] ', **form_i,key='-iBW-'),sg.Input('36', **form_iv, key='-BW-'), + sg.Text('',size=(34,1)),sg.Text('', **form_CRC, key='-CRC-'), LEDIndicator('-OK-')], + [sg.Text('Carrier Power to Noise Power Density Ratio : C/N\N{SUBSCRIPT ZERO}', **form_o,key='-iC_N0-'), + sg.Input(**form_ov,key='-C_N0-')], + [sg.Text('Theoretical BR at infinite BW : 1.44 C/N\N{SUBSCRIPT ZERO}', **form_o,key='-iBRinf-'), + sg.Input(**form_ov,key='-BRinf-')], + [sg.Text('Theoretical BR at Spectral Efficiency = 1 : C/N\N{SUBSCRIPT ZERO}', **form_o,key='-iBRunit-'), + sg.Input(**form_ov,key='-BRunit-')], + [sg.Text('Theoretical BR at Reference (BW,C/N)', **form_o,key='-iBRbw-'), + sg.Input(**form_ov, key='-BRbw-')], + [sg.Text('Carrier to Noise Ratio : C / N = C / (N\N{SUBSCRIPT ZERO}.B)', **form_o,key='-iCNRlin-'), + sg.Input(**form_ov, key='-CNRlin-')], + [sg.Text(' BW Increase Factor ',**form_i,key='-iBWmul-'), sg.Input('1', **form_iv, key='-BWmul-')], + [sg.Text(' Power Increase Factor ',**form_i,key='-iCmul-'), sg.Input('2', **form_iv, key='-Cmul-')], + [sg.Text('Bit Rate Increase Factor', **form_o,key='-iBRmul-'), sg.Input(**form_ov, key='-BRmul-')], + [sg.Button('Evaluation', visible=False, key='-Evaluation-', bind_return_key = True), + sg.Button('BW Sensitivity', pad=((60,5),(2,2)), key='-BW_Graph-'), + sg.Button('Power Sensitivity',key='-Pow_Graph-'), + sg.Button('BR Factor Map', key='-Map-'), + sg.Button('Go to Real World', key='-Real-')]])]]) + +col2=sg.Column([[sg.Frame('Background Information (click on items)', + [[sg.Multiline('Click on parameter\'s label to get information',size=(80,15),key='-Dialog-')], + [sg.Button('Advanced'), sg.Button('Write Contribution', key='-Write_Ct-'), + sg.Button('Read Contributions',key='-Read_Ct-'),sg.Button('Help')]], element_justification='center')]]) + +layout=[ + [sg.Button('Wiki : Claude_Shannon',size=(25,1), key='-Wiki-')], + [sg.Image(filename='Shannon.png',key='-iShannon-',enable_events=True, background_color='black')], + [col1,col2] + ] + +window = sg.Window('Shannon\'s Equation for Dummies', layout , finalize=True, element_justification='center', **web_cfg) + +# Needed for the Web version (on a finalized window) +window['-CNR-'].Update('12') +window['-BW-'].Update('36') +window['-BWmul-'].Update('1') +window['-Cmul-'].Update('2') + +File_CRC=hex(binascii.crc32(open('Shannon.py', 'rb').read()) & 0xFFFFFFFF)[2:].upper() +print("File's CRC : ", File_CRC) + +Win_time_out = 500 # Time out approach required for enter capture in the Web version +First_Pass = True +Err_msg = False + +while True: + + event, values = window.read(timeout=Win_time_out, timeout_key='__TIMEOUT__') + + if event != '__TIMEOUT__' : print (event) + + try: + + if event == sg.WIN_CLOSED: # if user closes window + break + + elif event == '-Evaluation-' or event == '__TIMEOUT__' : + + if First_Pass : + window['-Dialog-'].Update(Shd.Help['-iShannon-']) + First_Pass = False + + CNR_List=values['-CNR-'] # CNR can be a combination of comma separated CNRs + CNR_Nyq=Combine_CNR(*[float(val) for val in CNR_List.split(',')]) + + BW_Nyq=float(values['-BW-']) + + CNR_l, BRinf, C_N0_l, BRbw = Shannon_Points(BW_Nyq,CNR_Nyq) + + BRunit = C_N0_l # Mbps / MHz : Sp Eff =1 + + window['-C_N0-'].Update("{:.1f}".format(C_N0_l) + ' MHz') + window['-BRinf-'].Update(BR_Format(BRinf)) + window['-BRunit-'].Update(BR_Format(BRunit)) + window['-BRbw-'].Update(BR_Format(BRbw)) + window['-CNRlin-'].Update("{:.1f}".format(CNR_Nyq)+" [dB] .. "+"{:.1f}".format(CNR_l)+' [Linear]') + + BWmul=float(values['-BWmul-']) + Cmul=float(values['-Cmul-']) + + BRmul=BR_Multiplier(BWmul,Cmul,CNR_Nyq) + window['-BRmul-'].Update("{:.2f}".format(BRmul)) + + Params = str(CNR_Nyq)+','+str(BW_Nyq)+','+str(BWmul)+','+str(Cmul) + Params_CRC = (hex(binascii.crc32(Params.encode('ascii')) & 0xFFFFFFFF)[2:].upper()) # Params' CRC + + elif event == '-Wiki-': + webbrowser.open('https://en.wikipedia.org/wiki/Claude_Shannon') + + elif event in ('-iC_N0-','-iCNR-','-iBRinf-','-iBRunit-','-iBRbw-','-iCNRlin-','-iBRmul-','-iCmul-', + '-iBWmul-','-iBW-','-iShannon-', '-CRC-', 'Advanced','Help' ): + window['-Dialog-'].Update(Shd.Help[event]) + + elif event == '-BW_Graph-': + BW = np.zeros(20) + BR = np.zeros(20) + CNR = np.zeros(20) + CNR[0] = CNR_Nyq + 10 * log(8, 10) + BW[0] = BW_Nyq / 8 + BR[0] = Shannon(BW[0], CNR[0]) + for i in range(1, 20): + BW[i] = BW[i - 1] * 2 ** (1 / 3) + CNR[i] = CNR[i - 1] - 10 * log(BW[i] / BW[i - 1], 10) + BR[i] = Shannon(BW[i], CNR[i]) + fig = plt.figure(figsize=(6, 4)) + ax = fig.add_subplot(111) + plt.plot(BW, BR, 'b') + Mark = ('D', 's', 'p', 'h', 'x') + for i in range(5): + ind = 3 * (i + 1) + BR_norm = BR[ind] / BR[9] + plt.plot(BW[ind], BR[ind], Mark[i] + 'b', label="{:.1f}".format(BW[ind]) + " MHz" + + " , {:.1f}".format(BR[ind]) + " Mbps" + " : {:.0%}".format(BR_norm)) + plt.title('Theoretical Bit Rate at Constant Power\nC/N\N{SUBSCRIPT ZERO} = ' + + "{:.1f}".format(C_N0_l) + " MHz" ) + plt.xlabel('Bandwidth [MHz]') + plt.ylabel('Bit Rate [Mbps]') + plt.grid(True) + plt.legend(loc='lower right') + + if Web_Version: + window_matfig(fig, title_fig='Bandwidth Sensitivity', title_win='Shannon for Dummies') + else: + plt.show(block=False) + + elif event == '-Pow_Graph-': + P_mul = np.zeros(20) + BR = np.zeros(20) + CNR = np.zeros(20) + P_mul[0] = 1 / 8 + CNR[0] = CNR_Nyq - 10 * log(8, 10) + BR[0] = Shannon(BW_Nyq, CNR[0]) + for i in range(1, 20): + P_mul[i] = P_mul[i-1] * 2 ** ( 1 / 3 ) + CNR[i] = CNR[i - 1] + 10 * log( 2 ** ( 1 / 3 ), 10 ) + BR[i] = Shannon(BW_Nyq, CNR[i]) + fig = plt.figure(figsize=(6, 4)) + ax = fig.add_subplot(111) + plt.plot(P_mul, BR, 'b') + Mark = ('D', 's', 'p', 'h', 'x') + for i in range(5): + ind = 3 * (i + 1) + BR_norm = BR[ind] / BR[9] + plt.plot(P_mul[ind], BR[ind], Mark[i] + 'b', label='{:.2f}'.format(P_mul[ind]) + + 'x , {:.1f}'.format(BR[ind]) + ' Mbps' + ' : {:.0%}'.format(BR_norm)) + plt.title('Theoretical Bit Rate at Constant Bandwidth : ' + '{:.1f}'.format(BW_Nyq) + ' MHz \n' + 'Reference : C/N = {:.1f}'.format(CNR_l) + ' [Linear Format]') + plt.xlabel('Power Multiplying Factor') + plt.ylabel('Bit Rate [Mbps]') + plt.grid(True) + plt.legend(loc='lower right') + + if Web_Version: + window_matfig(fig, title_fig='Power Sensitivity', title_win='Shannon for Dummies') + else: + plt.show(block=False) + + elif event == '-Map-' : + BR_mul=np.zeros((21,21)) + BW_mul=np.zeros((21,21)) + P_mul=np.zeros((21,21)) + for i in range(21): + for j in range(21): + BW_mul[i, j] = (i + 1)/4 + P_mul[i, j] = (j + 1)/4 + BR_mul[i, j] = BR_Multiplier(BW_mul[i, j], P_mul[i, j], CNR_Nyq) + fig = plt.figure(figsize=(6, 4)) + ax = fig.add_subplot(111) + Map = plt.contour(BW_mul,P_mul,BR_mul, 20) + plt.clabel(Map, inline=1, fontsize=8,fmt='%.2f') + plt.title('Bit Rate Multiplying Factor, \n Reference : C/N = {:.1f}'.format(CNR_Nyq) + ' dB, BW = ' + + '{:.1f}'.format(BW_Nyq) + ' MHz , C/N\N{SUBSCRIPT ZERO} = ' + '{:.1f}'.format(C_N0_l) + + ' MHz, BR = {:.1f}'.format(BRbw) + ' Mbps', fontsize=10) + plt.xlabel('Bandwidth Multiplying Factor') + plt.ylabel('Power Multiplying Factor') + plt.grid(True) + + if Web_Version: + window_matfig(fig, title_fig='Multiplying Factors Map', title_win='Shannon for Dummies') + else: + plt.show(block=False) + + elif event == '-Write_Ct-' : + Contribution_Write('Shannon_Theory.db') + + elif event == '-Read_Ct-' : + Contribution_Read('Shannon_Theory.db') + + elif event == '-Real-': # ---------------- Second Panel : Real World ------------------- + + P_i1, P_i2, P_i3 = '', '', '' + + fr1 = sg.Frame('Satellite Link',[ + [sg.Text('Satellite Altitude [km] ​', **form_i, key='-iSatAlt-'), + sg.Input('35786', **form_iv, key='-SatAlt-'), + sg.Text('Satellite Lat, Long ​[\N{DEGREE SIGN}]', **form_i, key='-iSatLatLong-'), + sg.Input('0.0, 19.2', **form_iv, key='-SatLatLong-'), + sg.Text('Ground Station Lat, Long ​[\N{DEGREE SIGN}]', **form_i, key='-iGSLatLong-'), + sg.Input('49.7, 6.3', **form_iv, key='-GSLatLong-')], + [sg.Text('HPA Output Power [W]',**form_i,key='-iHPA-'), + sg.Input('120', **form_iv, key='-HPA_P-'), + sg.Text('Output Losses [dB]',**form_i,key='-iLoss-'), + sg.Input('2', **form_iv, key='-Losses-'), + sg.Text('TX Impairments, C/I [dB]', **form_i, key='-iSCIR-'), + sg.Input('25, 25', **form_iv, key='-Sat_CIR-')], + [sg.Text('Beam Diameter [\N{DEGREE SIGN}]', **form_i, key='-iSBeam-'), + sg.Input('3', **form_iv, key='-Sat_Beam-'), + sg.Text('Offset from Peak [dB] ', **form_i, key='-iGOff-'), + sg.Input('0', **form_iv, key='-Gain_Offset-'), sg.Text('', size=(7, 1)), + sg.Text('', **form_CRC, key='-CRC1-'), LEDIndicator('-OK1-')], + [sg.Text('Frequency [GHz]', **form_i, key='-iFreq-'), + sg.Input('12', **form_iv, key='-Freq-'), + sg.Text('Link Availability [%]', **form_i, key='-iAvail-'), + sg.Input('99.9', **form_iv, key='-Avail-')], + [sg.Text('Output Power', **form_o,key='-iOPow-'), + sg.Input('', size=(35, 1), key='-Feed_P-', justification='center')], + [sg.Text('Satellite Antenna Gain​', **form_o,key='-iSGain-'), + sg.Input('', size=(35, 1), key='-Sat_G-', justification='center')], + [sg.Text('Equivalent Isotropic Radiated Power​', **form_o,key='-iEIRP-'), + sg.Input('', size=(35, 1), key='-EIRP-', justification='center')], + [sg.Text('Path Length @ Elevation', **form_o, key='-iPathLength-'), + sg.Input('', size=(35, 1), key='-PathLength-', justification='center')], + [sg.Text('Path Dispersion Loss', **form_o, key='-iPLoss-'), + sg.Input('', size=(35, 1), key='-PLoss-', justification='center')], + [sg.Text('Atmospheric Attenuation', **form_o, key='-iAtmLoss-'), + sg.Input('', size=(35, 1), key='-AtmLoss-', justification='center')], + [sg.Text('Power Flux Density', **form_o,key='-iPFD-'), + sg.Input('', size=(35, 1), key='-PFD-', justification='center')], + ], key='-SatLink-') + + fr2=sg.Frame('Radio Front End ',[ + [sg.Text('Receive Antenna Size [m]​',**form_i,key='-iCPE-'), + sg.Input('0.6', **form_iv, key='-CPE_Ant-'), + sg.Text('Noise Temperature [K] ​', **form_i, key='-iCPE_T-'), + sg.Input('140', **form_iv, key='-CPE_T-'), sg.Text('',size=(7, 1)), + sg.Text('', **form_CRC, key='-CRC2-'),LEDIndicator('-OK2-')], + [sg.Text('Customer Antenna Effective Area and G/T', **form_o,key='-iCGain-'), + sg.Input('', size=(35, 1), key='-CPE_G-', justification='center')], + [sg.Text('RX Power at Antenna Output', **form_o,key='-iRXPow-'), + sg.Input('', size=(35, 1), key='-RX_P-', justification='center')], + [sg.Text('Noise Power Density Antenna Output', **form_o, key='-iN0-'), + sg.Input('', size=(35, 1), key='-N0-', justification='center')], + [sg.Text('Bit Rate at infinite Bandwidth', **form_o,key='-iBRinf-'), + sg.Input('', size=(35, 1), key='-BRinf-', justification='center')], + [sg.Text('Bit Rate at Spectral Efficiency=1', **form_o,key='-iBRUnit-'), + sg.Input('', size=(35, 1), key='-BRUnit-', justification='center')], + [sg.Text('Bit Rate at Spectral Efficiency=2', **form_o,key='-iBRdouble-'), + sg.Input('', size=(35, 1), key='-BRdouble-', justification='center')] + ], key='-iRadio-') + + fr3=sg.Frame('Baseband Unit',[ + [sg.Text('Occupied Bandwidth [MHz]',**form_i,key='-iBW-'), + sg.Input('36', **form_iv, key='-BW-'), + sg.Text('Nyquist Filter Rolloff [%]',**form_i,key='-iRO-'), + sg.Input('5', **form_iv, key='-RO-'), + sg.Text('Higher Layers Overhead [%]', **form_i, key='-iOH-'), + sg.Input('5', **form_iv, key='-OH-')], + [sg.Text('RX Impairments, C/I [dB]',**form_i,key='-iCIR-'), + sg.Input('20', **form_iv, key='-CIR-'), + sg.Text('Implementation Penalty [dB]​',**form_i,key='-iPenalty-'), + sg.Input('1.5', **form_iv, key='-Penalty-'), sg.Text('',size=(7, 1)), + sg.Text('', **form_CRC, key='-CRC3-'), LEDIndicator('-OK3-')], + [sg.Text('Signal to Noise Ratio in Available BW', **form_o,key='-iCNRbw-'), + sg.Input('', size=(35, 1), key='-CNRbw-', justification='center')], + [sg.Text('Signal to Noise Ratio in Nyquist BW', **form_o,key='-iCNRnyq-'), + sg.Input('', size=(35, 1), key='-CNRnyq-', justification='center')], + [sg.Text('Signal to Noise Ratio at Receiver Output', **form_o,key='-iCNRrcv-'), + sg.Input('', size=(35, 1), key='-CNRrcv-', justification='center')], + [sg.Text('Theoretical Bit Rate', **form_o,key='-iBRnyq-'), + sg.Input('', size=(35, 1), key='-BRnyq-', justification='center')], + [sg.Text('Practical Bit Rate', **form_o,key='-iBRrcv-'), + sg.Input('', size=(35, 1), key='-BRrcv-', justification='center')], + [sg.Text('Practical Higher Layers Bit Rate', **form_o,key='-iBRhigh-'), + sg.Input('', size=(35, 1), key='-BRhigh-', justification='center')], + [sg.Button('Evaluation', key='-Evaluation-',visible=False, bind_return_key = True), + sg.Button('BW Sensitivity', pad=((100,5),(2,2)), key='-BW_Graph-'), + sg.Button('Power Sensitivity',key='-Pow_Graph-'), + sg.Button('BR Factor Map', key='-Map-'), + sg.Button('Back to Theory', key='-Back-')], + ], key='-Baseband-') + + fr4 = sg.Frame('Background Information (click on items)', + [[sg.Multiline('Click on parameter\'s label to get information', size=(80, 15),key='-Dialog-')], + [sg.Button('Advanced'), sg.Button('Write Contribution', key='-Write_Ct-'), + sg.Button('Read Contributions',key='-Read_Ct-'), sg.Button('Help')]],element_justification='center') + + layout2 =[ + [sg.Column([[fr1],[fr2],[fr3]]), + sg.Column([[sg.Text('', size=(55, 1), justification='center', key='-FCRC-')], + [sg.Button('Wiki : Harry Nyquist​', size=(25,1), key='-W_Nyquist-'), + sg.Button('Wiki : Richard Hamming​', size=(25,1), key='-W_Hamming-')], + [sg.Button('Wiki : Andrew Viterbi​', size=(25,1), key='-W_Viterbi-'), + sg.Button('Wiki : Claude Berrou​', size=(25,1), key='-W_Berrou-')], + [sg.Image(filename='Satellite.png', key='-Satellite-',background_color='black', enable_events=True)], + [fr4]], element_justification='center')]] + + window2 = sg.Window('Shannon and Friends in the Real World', layout2, finalize=True) + + window2['-FCRC-'].Update('Program\'s CRC: ' + File_CRC) + + # Needed for the Web version (on a finalized window) + window2['-Freq-'].Update('12') + window2['-SatAlt-'].Update('35786') + window2['-SatLatLong-'].Update('0.0, 19.2') + window2['-Avail-'].Update('99.9') + window2['-GSLatLong-'].Update('49.7, 6.3') + window2['-HPA_P-'].Update('120') + window2['-Sat_CIR-'].Update('25, 25') + window2['-Sat_Beam-'].Update('3') + window2['-Gain_Offset-'].Update('0') + window2['-CPE_Ant-'].Update('0.6') + window2['-CPE_T-'].Update('120') + window2['-Penalty-'].Update('1.5') + window2['-BW-'].Update('36') + window2['-CIR-'].Update('25') + window2['-RO-'].Update('5') + window2['-OH-'].Update('5') + window2['-AtmLoss-'].Update(' ... LOADING ...') + + First_Pass = True + + while True: + + event2, values = window2.read(timeout=Win_time_out,timeout_key = '__TIMEOUT__') + + if event2 != '__TIMEOUT__' : print(event2) + + P_err = 0 # Error ID + + try: + + if event2 == sg.WIN_CLOSED or event2 == '-Back-': # closes window + break + + elif event2 == '-Evaluation-'or event2 == '__TIMEOUT__' : + + if First_Pass : + window2['-Dialog-'].Update(Shd.Help2['-Satellite-']) + First_Pass = False + + P_err = 1 # Error ID + Freq = float(values['-Freq-']) # GHz + HPA_Power = float(values['-HPA_P-']) # Watts + Sat_Loss = float(values['-Losses-']) # dB + Sat_CIR_List = values['-Sat_CIR-'] # list if comma separated CIR contributions in dB + Sat_CIR = Combine_CNR(*[float(val) for val in Sat_CIR_List.split(',')]) + Sig_Power = HPA_Power * 10 ** (-Sat_Loss / 10) # Watts + Sat_Beam = float(values['-Sat_Beam-']) #dB + Gain_Offset = float(values['-Gain_Offset-']) #dB + Sat_Ant_eff = 0.65 # Factor + + window2['-Feed_P-'].Update(Power_Format(Sig_Power)) + + Lambda = 300e6 / Freq / 1e9 # meter + Sat_Gain_l = Sat_Ant_eff * ( pi * 70 / Sat_Beam ) ** 2 + Sat_Gain_l = Sat_Gain_l * 10**(-Gain_Offset/10) + Sat_Ant_d = 70 * Lambda / Sat_Beam + Sat_Gain_dB = 10 * log(Sat_Gain_l, 10) + + window2['-Sat_G-'].Update(Gain_Format(Sat_Gain_l)) + + EIRP_l = Sig_Power * Sat_Gain_l + EIRP_dB = 10 * log(EIRP_l, 10) + window2['-EIRP-'].Update(Power_Format(EIRP_l)) + + Avail = float(values['-Avail-']) + + R_earth=6378 + + [lat_GS, lon_GS] = [float(val) for val in values['-GSLatLong-'].split(',')] + [lat_sat, lon_sat] = [float(val) for val in values['-SatLatLong-'].split(',')] + + h_sat = float(values['-SatAlt-']) + + Path_Length = sqrt (h_sat**2 + 2 * R_earth * (R_earth + h_sat ) * + ( 1 - cos(np.radians(lat_sat-lat_GS)) * cos(np.radians(lon_sat-lon_GS)))) + + # elevation = itur.utils.elevation_angle(h_sat, lat_sat, lon_sat, lat_GS, lon_GS) # non signed + + Phi = acos(cos(np.radians(lat_sat-lat_GS)) * cos(np.radians(lon_sat-lon_GS))) + + if Phi > 0 : + elevation = np.degrees(atan((cos(Phi)-R_earth/(R_earth+h_sat))/sqrt(1-cos(Phi)**2))) + else : + elevation = 90 + + if elevation <= 0 : + Atm_Loss = 999 + else: + Atm_Loss = itur.atmospheric_attenuation_slant_path(lat_GS, lon_GS, Freq, + elevation, 100-Avail, 1).value + + window2['-AtmLoss-'].Update("{:.1f}".format(10 ** (Atm_Loss/10)) + ' [Linear] .. ' + + "{:.1f}".format(Atm_Loss) + " [dB]") + window2['-PathLength-'].Update("{:.1f}".format(Path_Length) + " [km] @ " + + "{:.1f}".format(elevation) + " [\N{DEGREE SIGN}]") + + Free_Space_Loss_l = (4 * pi * Path_Length * 1000 / Lambda) ** 2 + Free_Space_Loss_dB = 10 * log(Free_Space_Loss_l, 10) + + Path_Loss_l = 4 * pi * (Path_Length * 1000) ** 2 + Path_Loss_dB = 10 * log(Path_Loss_l, 10) + window2['-PLoss-'].Update(PLoss_Format(Path_Loss_l)) + + PFD_l = EIRP_l / Path_Loss_l * 10 ** (-Atm_Loss/ 10) + PFD_dB = 10 * log(PFD_l, 10) + window2['-PFD-'].Update(PFD_Format(PFD_l)) + + P_err = 2 # Error ID + CPE_Ant_d = float(values['-CPE_Ant-']) # meter + CPE_T_Clear= float(values['-CPE_T-']) # K + CPE_Ant_eff = 0.6 + CPE_T_Att = (CPE_T_Clear - 40) + 40 * 10 ** (-Atm_Loss/10) + 290 * (1 - 10 ** (-Atm_Loss/10)) + k_Boltz = 1.38e-23 # J/K + + P_err = 3 # Error ID + Penalties = float(values['-Penalty-']) # dB, overall implementation penalty + Bandwidth = float(values['-BW-']) # MHz + CNR_Imp_List = values['-CIR-'] # List of comma separated CNR impairments in dB + CNR_Imp = Combine_CNR(*[float(val) for val in CNR_Imp_List.split(',')]) + Rolloff = float(values['-RO-']) # percent + Overheads = float(values['-OH-']) # percent + + CPE_Ae = pi * CPE_Ant_d ** 2 / 4 * CPE_Ant_eff + CPE_Gain_l = (pi * CPE_Ant_d / Lambda) ** 2 * CPE_Ant_eff + CPE_Gain_dB = 10 * log(CPE_Gain_l, 10) + CPE_Beam = 70 * Lambda / CPE_Ant_d # diameter in degrees + CPE_G_T = 10 * log(CPE_Gain_l / CPE_T_Att, 10) + window2['-CPE_G-'].Update( + "{:.1f}".format(CPE_Ae)+" m\N{SUPERSCRIPT TWO} .. {:.1f}".format(CPE_G_T)+" dB/K") + + RX_Power_l = PFD_l * CPE_Ae + # Alternative : RX_Power_l=EIRP_l/Free_Space_Loss_l*CPE_Gain_l + RX_Power_dB = 10 * log(RX_Power_l,10) + N0 = k_Boltz * CPE_T_Att # W/Hz + C_N0_l = RX_Power_l / N0 # Hz + C_N0_dB = 10 * log(C_N0_l, 10) # dBHz + window2['-RX_P-'].Update('C : ' + Power_Format(RX_Power_l)) + window2['-N0-'].Update('N\N{SUBSCRIPT ZERO} : ' + PSD_Format(N0*1e6)) + + BW_Spe_1 = C_N0_l / 1e6 # C_N0 without Penalty in MHz + BW_Spe_1half = BW_Spe_1 / (2 ** 0.5 - 1) # MHz + BW_Spe_1quarter = BW_Spe_1 / (2 ** 0.25 - 1) + BW_Spe_double = BW_Spe_1 / (2 ** 2 - 1) + + BR_Spe_1 = BW_Spe_1 # Mbps + BR_Spe_1half = BW_Spe_1half / 2 + BR_Spe_1quarter = BW_Spe_1quarter / 4 + BR_Spe_double= BW_Spe_double * 2 + BR_infinity = BW_Spe_1 / log(2) + BR_Spe_1_Norm = BR_Spe_1 / BR_infinity + BR_Spe_1half_Norm = BR_Spe_1half / BR_infinity + BR_Spe_1quarter_Norm = BR_Spe_1quarter / BR_infinity + BR_Spe_double_Norm = BR_Spe_double / BR_infinity + window2['-BRinf-'].Update('1.443 C/N\N{SUBSCRIPT ZERO} : ' + + BR_Format(BR_infinity)+" .. {:.0%}".format(1)) + window2['-BRUnit-'].Update('C/N\N{SUBSCRIPT ZERO} : ' + + BR_Format(BR_Spe_1)+" .. {:.0%}".format(BR_Spe_1_Norm)) + window2['-BRdouble-'].Update('0.667 C/N\N{SUBSCRIPT ZERO} : ' + + BR_Format(BR_Spe_double)+" .. {:.0%}".format(BR_Spe_double_Norm)) + + CNR_Spe_1 = 0 # dB + CNR_Spe_1half = -10 * log(BW_Spe_1half / BW_Spe_1, 10) + CNR_Spe_1quarter = -10 * log(BW_Spe_1quarter / BW_Spe_1, 10) + CNR_Spe_double = -10 * log(BW_Spe_double/ BW_Spe_1, 10) + + CNR_BW = CNR_Spe_1 + 10 * log(BW_Spe_1 / Bandwidth, 10) # dB + BW_Nyq = Bandwidth / (1 + Rolloff / 100) # MHz + CNR_Nyq = CNR_Spe_1 + 10 * log(BW_Spe_1 / BW_Nyq, 10) # dBB + CNR_Rcv = Combine_CNR (CNR_Nyq, CNR_Imp, Sat_CIR) # + BR_BW = Shannon (Bandwidth, CNR_BW) # Mbps + BR_Nyq = Shannon (BW_Nyq, CNR_Nyq) # Mbps + BR_Rcv = Shannon (BW_Nyq, CNR_Rcv,Penalties) # Mbps + BR_Rcv_Higher = BR_Rcv / (1 + Overheads / 100) # Mbps + BR_BW_Norm = BR_BW / BR_infinity + BR_Nyq_Norm = BR_Nyq / BR_infinity + BR_Rcv_Norm = BR_Rcv / BR_infinity + BR_Rcv_H_Norm = BR_Rcv_Higher / BR_infinity + Spe_BW = BR_BW / Bandwidth + Spe_Nyq = BR_Nyq / Bandwidth # Efficiency in available bandwidth + Bits_per_Symbol = BR_Nyq / BW_Nyq # Efficiency in Nyquist bandwidth + Spe_Rcv = BR_Rcv / Bandwidth + Spe_Higher = BR_Rcv_Higher / Bandwidth + + window2['-CNRbw-'].Update("{:.1f}".format(CNR_BW)+" dB in "+"{:.1f}".format(Bandwidth)+" MHz") + window2['-CNRnyq-'].Update("{:.1f}".format(CNR_Nyq)+" dB in "+"{:.1f}".format(BW_Nyq)+" MHz") + window2['-CNRrcv-'].Update("{:.1f}".format(CNR_Rcv)+" dB") + window2['-BRnyq-'].Update(BR_Format(BR_Nyq) + " .. {:.0%}".format(BR_Nyq_Norm)+ + " .. {:.1f}".format(Spe_Nyq)+" bps/Hz .. {:.1f}".format(Bits_per_Symbol)+" b/S") + window2['-BRrcv-'].Update(BR_Format(BR_Rcv) + " .. {:.0%}".format(BR_Rcv_Norm)+ + " .. {:.1f}".format(Spe_Rcv)+" bps/Hz") + window2['-BRhigh-'].Update(BR_Format(BR_Rcv_Higher)+" .. {:.0%}".format(BR_Rcv_H_Norm)+ + " .. {:.1f}".format(Spe_Higher)+" bps/Hz") + + Params1 = str(Freq) + ',' + str(Path_Length) + ',' + str(Atm_Loss) + ',' + \ + str(HPA_Power) + ',' + str(Sat_Loss) + ',' + str(Sat_CIR) + ',' + \ + str(Sat_Beam) + ',' + str(Gain_Offset) + Params2 = str(CPE_Ant_d) + ',' + str(CPE_T_Clear) + Params3 = str(Bandwidth) + ',' + str(Rolloff) + ',' + str(Overheads) + ',' + \ + str(CNR_Imp) + ',' + str(Penalties) + + Params1_CRC=hex(binascii.crc32(Params1.encode('ascii')) & 0xFFFFFFFF)[2:].upper() + Params2_CRC = hex(binascii.crc32(Params2.encode('ascii')) & 0xFFFFFFFF)[2:].upper() + Params3_CRC = hex(binascii.crc32(Params3.encode('ascii')) & 0xFFFFFFFF)[2:].upper() + + elif event2 in ('-iFreq-','-iHPA-','-iSBeam-','-iLoss-', '-iGOff-','-iFade-', '-iSCIR-', + '-iOPow-','-iSGain-', '-iEIRP-', '-iPFD-', '-iCPE-','-iCGain-','-iRXPow-', + '-iBRinf-','-iBRhalf-', '-iBRUnit-','-iBRdouble-','-iBW-','-iRO-','-iCIR-', + '-iPenalty-', '-iOH-','-iNBW-', '-iCNRbw-','-iCNRnyq-','-iCNRrcv-','-iBRbw-', + '-iBRnyq-','-iBRrcv-','-iBRhigh-','-Satellite-','-iPLoss-','Advanced','Help', + '-CRC1-', '-CRC2-', '-CRC3-', '-iN0-','-iCPE_T-', '-iSatAlt-','-iSatLatLong-', + '-iGSLatLong-', '-iAvail-', '-iPathLength-','-iAtmLoss-'): + window2['-Dialog-'].Update(Shd.Help2[event2]) + + elif event2 == '-BW_Graph-' : + BW = np.zeros(20) + BR = np.zeros(20) + CNR = np.zeros(20) + CNR[0] = CNR_Nyq+10*log(8,10) + BW[0] = Bandwidth/8 + BR[0] = Shannon(BW[0]/(1+Rolloff/100), CNR[0], Penalties) / (1 + Overheads / 100) + for i in range(1, 20): + BW[i] = BW[i - 1] * 2 ** (1 / 3) + CNR[i] = CNR[i - 1] - 10 * log(BW[i] / BW[i - 1], 10) + CNR_Rcv_i = Combine_CNR(CNR[i], CNR_Imp, Sat_CIR) + BR[i] = Shannon(BW[i]/(1+Rolloff/100), CNR_Rcv_i, Penalties) / (1 + Overheads / 100) + + fig = plt.figure(figsize=(6, 4)) + ax = fig.add_subplot(111) + plt.plot(BW, BR, 'b') + Mark = ('D', 's', 'p', 'h', 'x') + for i in range(5): + ind = 3 * (i + 1) + BR_norm = BR[ind] / BR[9] + plt.plot(BW[ind], BR[ind], Mark[i] + 'b',label="{:.1f}".format(BW[ind]) + + " MHz" + " , {:.1f}".format(BR[ind]) + " Mbps" + " : {:.0%}".format(BR_norm)) + plt.title('Higher Layers Bit Rate at Constant HPA Output Power : ' + + "{:.1f}".format(HPA_Power) + " W") + plt.xlabel('Occupied Bandwidth [MHz]') + plt.ylabel('Bit Rate [Mbps]') + plt.grid(True) + plt.legend(loc='lower right') + + if Web_Version: + window_matfig(fig, title_fig='Bandwidth Sensitivity', + title_win='Shannon and Friends in the Real World') + else: + plt.show(block=False) + + elif event2 == '-Pow_Graph-' : + Power = np.zeros(20) + BR = np.zeros(20) + CNR = np.zeros(20) + Power[0] = HPA_Power / 8 + CNR[0] = CNR_Nyq-10*log(8,10) + CNR_Rcv_i = Combine_CNR(CNR[0], CNR_Imp, Sat_CIR) + BR[0] = Shannon(BW_Nyq, CNR_Rcv_i, Penalties) / (1 + Overheads / 100) + for i in range(1, 20): + Power[i] = Power[i-1] * 2 ** (1 / 3) + CNR[i] = CNR[i - 1] + 10 * log( 2 ** (1 / 3) , 10 ) + CNR_Rcv_i = Combine_CNR(CNR[i], CNR_Imp, Sat_CIR) + BR[i] = Shannon(BW_Nyq, CNR_Rcv_i, Penalties) / (1 + Overheads / 100) + fig = plt.figure(figsize=(6, 4)) + ax = fig.add_subplot(111) + plt.plot(Power, BR, 'b') + Mark = ('D', 's', 'p', 'h', 'x') + for i in range(5): + ind = 3 * (i + 1) + BR_norm=BR[ind]/BR[9] + plt.plot(Power[ind], BR[ind], Mark[i] + 'b', label="{:.1f}".format(Power[ind]) + " W" + + " , {:.1f}".format(BR[ind]) + " Mbps"+" : {:.0%}".format(BR_norm)) + plt.title('Higher Layers Bit Rate at Constant Occupied Bandwidth : ' + + "{:.1f}".format(Bandwidth) + " MHz") + plt.xlabel('HPA Output Power @ Operating Point [Watts]') + plt.ylabel('Bit Rate [Mbps]') + plt.grid(True) + plt.legend(loc='lower right') + + if Web_Version: + window_matfig(fig, title_fig='Power Sensitivity', + title_win='Shannon and Friends in the Real World') + else: + plt.show(block=False) + + elif event2 == '-Map-': + BR_mul = np.zeros((21, 21)) + BW_mul = np.zeros((21, 21)) + P_mul = np.zeros((21, 21)) + BR_00 = BR_Rcv_Higher + for i in range(21): + for j in range(21): + BW_mul[i, j] = (i + 1) / 4 + P_mul[i, j] = (j + 1) / 4 + CNR_ij = CNR_Nyq + 10 * log( P_mul[i, j] / BW_mul[i, j] , 10 ) + CNR_Rcv_ij = Combine_CNR (CNR_ij, CNR_Imp, Sat_CIR) + BW_ij = BW_Nyq * BW_mul[i, j] + BR_ij = Shannon( BW_ij / (1 + Rolloff / 100), CNR_Rcv_ij, Penalties) / (1 + Overheads / 100) + BR_mul[i, j] = BR_ij / BR_00 + fig = plt.figure(figsize=(6, 4)) + ax = fig.add_subplot(111) + Map = plt.contour(BW_mul, P_mul, BR_mul, 20) + plt.clabel(Map, inline=1, fontsize=8, fmt='%.2f') + plt.title('Bit Rate Multiplying Factor, \n Reference : Power = {:.1f}'.format(HPA_Power) + + ' W , BW = ' + '{:.1f}'.format(Bandwidth) + + ' MHz , BR = {:.1f}'.format(BR_Rcv_Higher) + ' Mbps') + plt.xlabel('Bandwidth Multiplying Factor') + plt.ylabel('Power Multiplying Factor') + plt.grid(True) + + if Web_Version: + window_matfig(fig, title_fig='Multiplying Factors Map', + title_win='Shannon and Friends in the Real World') + else: + plt.show(block=False) + + elif event2 == '-W_Nyquist-': + webbrowser.open('https://en.wikipedia.org/wiki/Harry_Nyquist') + elif event2 == '-W_Hamming-': + webbrowser.open('https://en.wikipedia.org/wiki/Richard_Hamming') + elif event2 == '-W_Viterbi-': + webbrowser.open('https://en.wikipedia.org/wiki/Andrew_Viterbi') + elif event2 == '-W_Berrou-': + webbrowser.open('https://en.wikipedia.org/wiki/Claude_Berrou') + + elif event2 == '-Write_Ct-': + Contribution_Write('Shannon_Real.db') + + elif event2 == '-Read_Ct-': + Contribution_Read('Shannon_Real.db') + + else: + print("Untrapped event : "+event2) + + + except : + window2['-Dialog-'].Update('Invalid input fields') + if P_err == 1 : + window2['-CRC1-'].Update('-') + SetLED(window2, '-OK1-', 'red') + if P_err == 2 : + window2['-CRC2-'].Update('-') + SetLED(window2, '-OK2-', 'red') + if P_err == 3 : + window2['-CRC3-'].Update('-') + SetLED(window2, '-OK3-', 'red') + Err_msg=True + else: + window2['-CRC1-'].Update(Params1_CRC) + window2['-CRC2-'].Update(Params2_CRC) + window2['-CRC3-'].Update(Params3_CRC) + SetLED(window2, '-OK1-', 'green') + SetLED(window2, '-OK2-', 'green') + SetLED(window2, '-OK3-', 'green') + if Err_msg : + window2['-Dialog-'].Update('Input Validated') + Err_msg = False + + window2.close() + + except : + window['-Dialog-'].Update('Invalid input fields') + window['-CRC-'].Update('-') + SetLED(window, '-OK-', 'red') + Err_msg = True + else: + window['-CRC-'].Update(Params_CRC) + SetLED(window, '-OK-', 'green') + if Err_msg: + window['-Dialog-'].Update('Input Validated') + Err_msg = False + +window.close() diff --git a/Shannon_Dict.py b/Shannon_Dict.py new file mode 100644 index 0000000..d8f83d9 --- /dev/null +++ b/Shannon_Dict.py @@ -0,0 +1,330 @@ + +Help={'-iCNR-':'Reference C/N [dB]\n\nReference Carrier to Noise Ratio in decibels : 10 log (C/N), where C is the ' + 'Carrier\'s power and N is the Noise\'s Power, both are measured in the reference ' + 'Channel Bandwidth.\n\nThe Carrier\'s power if often named Signal\'s Power and the Carrier to Noise Ratio' + ' is often named Signal to Noise Ratio.', + '-iBW-':'Reference BW [MHz]\n\nReference Channel Bandwidth, this is a key parameter of the communication channel.' + '\n\nThis bandwidth is usually a degree of freedom of the system design, eventually constrained by technological' + ' constraints and various kind of frequency usage regulations.', + '-iC_N0-':'Carrier Power to Noise Power Density Ratio : C/N\N{SUBSCRIPT ZERO}\n\nCarrier\'s power (in Watts) ' + 'divided by the Noise Spectral Power Density (in Watts per MHz), the result\'s units are MHz.', + '-iBRinf-':'Theoretical BR at infinite BW : 1.44 C/N\N{SUBSCRIPT ZERO}\n\nBit Rate theoretically achievable when ' + 'the signal occupies an infinite Bandwidth, this value is a useful asympotical limit.', + '-iBRunit-':'Theoretical BR at Spectral Efficiency = 1 : C/N\N{SUBSCRIPT ZERO}\n\nBit Rate theoretically ' + 'achievable at a Spectral Efficiency = 1 : Bit Rate in Mbps = Bandwith in MHz.' + '\n\nThe corresponding value, deduced from the Shannon\'s formula is given by C/N\N{SUBSCRIPT ZERO}.', + '-iBRbw-':'Theoretical BR at Reference (BW,C/N)\n\nBit Rate theoretically achievable when the Bandwidth is ' + 'constrained to the given reference value.', + '-iCNRlin-':'C / N = C / (N\N{SUBSCRIPT ZERO}.B)\n\nReference Carrier to Noise Ratio (or Signal to Noise Ratio).' + ' The C/N Ratio or CNR is usually given in dBs or 10 log (C/N). ' + '\n\nAlthough the logarithm is convenient for many evaluations (multiplications become additions), ' + 'it\'s also good to consider the ratio itself (named here Linear ' + 'Format) to get some physical sense of the power ratio.' + '\n\nThe Carrier to Noise Ratio in linear format, is the value used in the Shannon\'s formula.', + '-iBRmul-':'Bit Rate Increase Factor\n\nBit Rate multiplying factor achieved when the Bandwidth and the Power ' + 'and multiplied by a given set of values.', + '-iCmul-':'Power Increase Factor\n\nArbitrary multiplying factor applied to the Carrier\'s Power, for ' + 'sensitivity analysis.', + '-iBWmul-':'BW Increase Factor\n\nArbitrary multiplying factor applied to the Carrier\'s Bandwidth, for ' + 'sensitivity analysis.', + '-CRC-':'Cyclic Redundancy Check of the input parameters, for changes identification purposes.\n\n' + 'https://en.wikipedia.org/wiki/Cyclic_redundancy_check', + 'Advanced': 'The model assumes that the communication channel is \"AWGN\", just Adding White Gaussian Noise to ' + 'the signal. This noise is supposed to be random and white which means that noise at a given time is ' + 'independent of noise at any other time, this implies that the noise has a flat and infinite spectrum.' + 'This noise is also supposed to be Gaussian which means that its probability density function follows a ' + 'Gaussian law, with a variance associated to the Noise\'s power.\n\n' + 'Although these assumptions seem very strong, they are quite accurately matching the cases of interest. ' + 'Many impairments are actually non linear and/or non additive, but just combining equivalent C/N of all ' + 'impairments as if they were fully AWGN is in most of the cases very accurate.' + 'The reason for that is that the sum of random variables of unknown laws always tend to Gaussian and that ' + 'in most systems, thermal noise is dominating, is actually white and gaussian and is whitening the rest.\n\n' + 'The tool accepts lists of comma separated CNRs which will be combined in that way. \n\n' + 'In satellite systems, the noise is mainly coming from the electronics of the radio front end, the ground ' + 'seen by the antenna, the stars and the atmospheric attenuator. In case of rain, the signal is punished twice ' + ': the attenuation makes it weaker and the rain attenuator generates noise added to the overall noise.\n\n' + 'Overall the Shannon Limit is a pretty convenient tool to predict the real performances of communication ' + 'systems and even more importantly to get a sense of the role of the key design parameters.', + '-iShannon-':'The Shannon Limit allows to evaluate the theoretical capacity achievable over a communication ' + 'channel.\n\nAs a true genius, Claude Shannon has funded the communication theory, the information theory ' + 'and more (click the Wikipedia button for more info).\n\nThis equation is fundamental for the evaluation of ' + 'communication systems. It is an apparently simple but extremely powerful tool to guide communication systems\'' + ' designs.\n\nThis equation tells us what is achievable, not how to achieve it. It took almost 50 years to ' + 'approach this limit with the invention of Turbo codes.\n\nIn the satellite domain, DVB-S2x, using LDPC codes ' + 'iteratively decoded (Turbo-Like), is only 1 dB away from this limit.', + 'Help': 'Recommendations for using the tool\n\nThe first purpose of the tool is educational, allowing people to ' + 'better understand the physics of communications and the role of key parameters.\n\n' + 'The user should try multiple values in all the fields one per one, explore the graphs and try to ' + 'understand the underlying physics.\n\n' + 'The units for the different figures are as explicit as possible to facilitate the exploration.\n\n' + 'All labels can be \"clicked\" to get information about associated item. All values (including this text)' + ' can be copy/pasted for further usage.' + } + +Help2={'-iFreq-':'Frequency [GHz]\n\nFrequency of the electromagnetic wave supporting the communication in GHz ' + '(billions of cycles per second).\n\nFor satellite downlink (satellite to terminal), frequency bands ' + 'and frequencies are typically : L : 1.5 GHz , S : 2.2 GHz , C : 4 GHz , Ku : 12 GHz, Ka : 19 GHz, ' + 'Q : 40 GHz', + '-iSatAlt-':'Satellite Altitude [km]\n\nThe position of the satellite is expressed in latitude, longitude, ' + 'altitude. The program doesnt simulate the orbit, any satellite coordinates can be used. ' + 'A GEO satellite has a latitude of zero degrees and an altitude of 35786 km. LEO satellites ' + 'have an altitude lower than 2000 km. MEO\'s altitudes are between LEO and GEO : the O3B ' + 'constellation\'s altitude is 8063 km', + '-iSatLatLong-':'Satellite Latitude and Longitude [\N{DEGREE SIGN}]\n\nThe position of the satellite is ' + 'expressed in latitude, longitude, altitude. The program doesnt simulate the orbit, any ' + 'satellite coordinates can be used. A GEO satellite has a latitude of zero degrees and an ' + 'altitude of 35786 km. LEO satellites have an altitude lower than 2000 km. MEO\'s altitudes are' + ' between LEO and GEO : the O3B constellation\'s altitude is 8063 km', + '-iGSLatLong-':'Ground Station Latitude and Longitude [\N{DEGREE SIGN}]\n\nThe position of the ground station ' + 'is expressed in latitude, longitude (the ground station is assumed to be at the surface of ' + 'the earth).\n\nThe position of the ground station is affecting the link availability due to' + ' the differences in weather statistics at different locations on earth (tropical regions have ' + 'very heavy rains attenuating dramatically signals at high frequencies). It is also impacting ' + 'the elevation angle at which the satellite is seen and thus the length of the path in the rain.' + '\n\nThe position of the ground station is also impacting the overall path length and thus the ' + 'path dispersion loss.\n\nUseful link to find coordinates of interest : ' + 'https://www.gps-coordinates.net', + '-iAvail-':'Desired Link Availability [%]\n\nThe link availability in percentage of the time is a key ' + 'performance indicator for satellite communications.\n\nIn this program the only cause of ' + 'unavailability modelled in a probabilistic way is the attenuation caused by the atmosphere. A high ' + 'desired link availability corresponds to a high signal attenuation : only rare and severe weather ' + 'events exceeding this attenuation can interrupt the link.\n\nFor example for an availability of' + '99.9%, the attenuation considered in the link sizing is only exceeded for 0.1% of the time.', + '-iPathLength-':'Path Length [km] @ Elevation [\N{DEGREE SIGN}]\n\nDistance in kilometers from the satellite ' + 'to the ground station and elevation angle at which the satellite is seen. The actual distance' + ' depends on the satellite\'s altitude and on the relative positions of the satellite and the ' + 'ground station.\n\nThe minimum path length is the satellite altitude, achieved when the ground' + ' station is under the satellite (elevation = 90\N{DEGREE SIGN}).\n\nA negative elevation ' + 'implies that the satellite is not visible (beyond the horizon).', + '-iAtmLoss-':'Overall Atmospheric Attenuation [dB]\n\nThe Atmosphere is affecting radio wave propagation ' + 'with a signal attenuation caused by rain precipitations and clouds, by scintillation and ' + 'multi path effects, by sand and dust storms and also by atmospheric gases. \n\n' + 'Simply speaking, the attenuation is increasing with the rain intensity and with the signal ' + 'frequency. C band is almost unaffected, Ku band is significantly affected, Ka band is severely ' + 'affected, Q band is dramatically affected\n\nThe overall attenuation depends on the actual ' + 'geographical location and on actual weather events. By nature, it is is thus statistical ' + '(considering the past) or probabilistic (considering the future).\n\nAll effects included, ' + 'here are typical attenuation figures exceeded for 0.1% of the time in Europe from the GEO orbit ' + ': Ku: 2.5 dB, Ka: 6.9 dB, 22 dB \n\n' + 'The program uses ITU-Rpy, python implementation of the ITU-R P Recommendations: ' + 'https://itu-rpy.readthedocs.io/en/latest/index.html', + '-iHPA-':'HPA Power at operating point [W]\n\nPower of the High Power Amplifier used as a last stage of ' + 'amplification in the satellite payload.' + 'The value in watts is the value at operating point and for the carrier of interest.\n\n' + 'Some satellites operate their HPAs at saturation in single carrier mode (typical DTH case).' + 'Other satellites operate in multicarrier mode and reduced power (3dB Output Back Off is typical ' + 'for satellites serving VSATs)', + '-iSBeam-':'Satellite Half Power Beam Diameter [\N{DEGREE SIGN}]\n\nBeam diameter expressed as an angle at ' + 'satellite level. The power radiated at the edge of this beam is half of the power radiated at ' + 'the peak of the beam (on-axis value).\n\n' + 'The beam evaluated is a basic one with simple illumination of a parabolic reflector\n\n' + 'Typical beam size : 0.4-1.4 degrees for GEO HTS satellites, 3..6 degrees for GEO DTH satellites.', + '-iGOff-': 'Gain Offset from Peak [dB]\n\nThis offset allows to simulate terminals which are not all at ' + 'the beam peak. A 3 dB value would simulate a worst case position in a 3dB beam, typical approach ' + 'used in DTH. In single feed per beam HTS, a 1 dB value would give a typical median performance.' + 'If you know the EIRP you have, the best is to iterate this value to get this EIRP ' + '(the process will allow you to get a feeling of the tradeoff power / footprint size / EIRP. ', + '-iLoss-':'Output Section Losses [dB]\n\nLoss of signal\'s power in the path connecting the HPA to the ' + 'antenna. This loss is associated with filters, waveguide sections, switches ...\n\n' + 'Typical value : 2.5 dB for large classical satellites, 1 dB for active antennas with HPAs close to ' + 'the feeds. If the power value is given at antenna level, the value should just be set to zero.', + '-iSCIR-':'Satellite C/I [dB]\n\nEffect of signal impairments associated with satellite implementation,' + 'expressed as a signal to impairment noise ratio to be combined with the intrinsic Signal to Noise ' + 'Ratio affecting the link. Typical impairments are : intermodulation in the HPA, filtering effects, ' + 'oscillator\'s phase noise ...\n\n' + 'The tool supports comma separated lists of C/I or C/N values expressed in dB. In addition to ' + 'satellites impairments, one can use this feature to also simulate infrastructure C/N, uplink C/N, ' + 'uplink interferences ...', + '-iOPow-':'Output Power [W]\n\nThe output power in watts at antenna output is associated with the useful ' + 'signal carrying user\'s information. It is also common to express this value in dBs (dBs transform ' + 'multiplications in additions, easier for human computation. Nevertheless, reasoning in watts tells ' + 'more about the physics.', + '-iSGain-':'Satellite Antenna Gain \n\nAn antenna concentrating the signal in the direction of the users is ' + 'almost always required to compensate for the path loss associated with the distance from the ' + 'satellite to the terminal.\n\nThe antenna gain is the ratio between the signal radiated ' + 'on the axis of the antenna (direction of maximum radiation) and the signal radiated by an ' + 'antenna radiating equally in all directions (for the same input power).\n\n' + 'Antenna gains are without units but can be expressed in dB for convenience : dBi = dB relative to' + ' isotropic antenna (antenna radiating equally in all directions)', + '-iEIRP-':'Equivalent Isotropic Radiated Power\n\nThe product Power x Gain expressed in Watts is a convenient ' + 'characterisation of the satellite radiation capability. It does correspond to the power which would ' + 'be required for an isotropic antenna radiating in the same way in the direction of the antenna ' + 'considered.\n\nThere is no "power creation" of course : for the directive antenna, the integral of ' + 'the radiated signal over a sphere centered on the antenna is at best equal to the input power ' + '(lossless antenna).\n\n' + 'As the value in watts is usually pretty big, a value in dBW is more convenient ' + 'for practical human computations.', + '-iPLoss-':'Path Dispersion Loss\n\nAssuming communication in free space (thus also in the vacuum), ' + 'this figure characterises the effect' + ' of the distance from the satellite to the terminal. It gives an attenuation equivalent to the ' + 'inverse ratio of the power reaching one square meter at the terminal side and the equivalent ' + 'isotropic radiated power at satellite level.\n\n' + 'This simply equals the surface in square meters of a sphere with a radius equal to the path length.' + 'This attenuation is pretty big and is thus more humanly manageable in dB m\N{SUPERSCRIPT TWO}.\n\n' + 'As the the vacuum is lossless, this "attenuation" is simply associated with the fact that only ' + 'a marginal fraction of the power radiated is captured in one square meter at destination, ' + 'the rest is going somewhere else.', + '-iPFD-':'Power Flux Density\n\nSignal power per square meter at the terminal side. ' + 'The actual power captured by the terminal is given by this value multiplied by the effective surface ' + 'of the terminal\'s antenna.\n\nNote that if the surface of antenna is not perpendicular to the ' + 'propagation direction of the radio wave, the effective surface presented to the wave is reduced ' + 'and less power is captured.', + '-iCPE-':'Customer Antenna Size [m]\n\nSize of the terminal antenna. A basic parabolic antenna with state of ' + 'the art efficiency is assumed.\n\n' + 'The main source of noise is in general the terminal\'s radio front end' + ' attached to the antenna. A state of the art Noise Temperature of 80K is assumed for this front end.', + '-iCPE_T-':'Noise Temperature [K]\n\nTotal Receiver\'s Clear Sky Noise Temperature. It includes all noise ' + 'temperature\'s contributors : receiver, sky, ground seen by the antenna... Antenna catalogs often ' + 'provide this value, the proposed default of 120K is a reasonable typical value. The computation ' + 'under rain fade conditions assumes 40K is affected by rain attenuation and the rest is not. ', + '-iCGain-':'Customer Antenna Effective Area and G/T\n\nThe effective area in square meters is expressing the ' + 'capability of the terminal to capture the Power Flux Density ' + '(the multiplication of both give the power captured). The effective area is typically 60% of the ' + 'physical surface of the antenna\'s aperture.' + 'This capability can also be equivalently expressed as a gain as it\'s the case for the satellite ' + 'antenna.\n\nThe figure of merit of a receive antenna is best expressed as the G/T ratio, ' + 'ratio between antenna gain and the total Noise temperature in Kelvins. The noise is mainly coming ' + 'from the electronics of the radio front end, the ground seen by the antenna, the stars and the ' + 'atmospheric attenuator.\n\nIn case of rain, the signal is punished twice : the ' + 'attenuation makes it weaker and the rain attenuator generates noise added to the overall noise.\n\n' + 'The noise power density N\N{SUBSCRIPT ZERO} is derived from the noise temperature with a very ' + 'simple formula : N\N{SUBSCRIPT ZERO}=kTB (k being the Boltzmann constant), ' + 'the G/T leads easily to the key overall link figure of merit C/N\N{SUBSCRIPT ZERO}.', + '-iRXPow-':'RX Power at Antenna Output\n\nPower at receiver\'s antenna output before amplification. ' + 'This power is extremely small and can only be exploited after strong amplification.\n\n' + 'As the main source of noise is in general coming from this amplification, the first amplification ' + 'stage has to be a Low Noise Amplifier.\n\nThis power is "C" in the Shannon\'s equation.', + '-iN0-' : 'Noise Power Density Antenna Output\n\nNoise Spectral Power Density of the radio front end under ' + 'actual link conditions (in Watts per MHz). ' + 'This PSD is N\N{SUBSCRIPT ZERO} in the Shannon\'s equation', + '-iBRinf-':'Bit Rate at infinite Bandwidth\n\nBit Rate theoretically achievable when the signal occupies an ' + 'infinite Bandwidth, this value is a useful asymptotic limit. The corresponding value, deduced ' + 'from the Shannon\'s formula is given by 1.443 C/N\N{SUBSCRIPT ZERO}\n\nThis bit rate is an ' + 'asymptotic value and is thus never achieved in practice.', + '-iBRhalf-':'Bit Rate at Spectral Efficiency=1/2\n\nBit Rate theoretically achievable at a Spectral Efficiency ' + '= 1/2. The corresponding value, deduced from the Shannon\'s formula is given by 1.207 ' + 'C/N\N{SUBSCRIPT ZERO}\n\nThis operating point is bandwidth intensive ( bandwidth = 2 x bit rate). ' + 'Practical systems allow this operating point ( DVB-S2\'s QPSK 1/4 )', + '-iBRUnit-':'Bit Rate at Spectral Efficiency=1\n\nBit Rate theoretically achievable at a Spectral Efficiency ' + '= 1. The corresponding value, deduced from the Shannon\'s formula is given by ' + 'C/N\N{SUBSCRIPT ZERO}.\n\nThis data point has remarkable attributes : bandwidth = bit rate and ' + 'C/N = 1 (equivalent to 0 dB), which means Noise Power = Signal Power.', + '-iBRdouble-':'Bit Rate at Spectral Efficiency=2\n\nBit Rate theoretically achievable at a Spectral Efficiency ' + '= 1. The corresponding value, deduced from the Shannon\'s formula is given by ' + '0.667 C/N\N{SUBSCRIPT ZERO}.\n\nThis operating point is relatively bandwidth efficient ' + '( bandwidth = 0.5 x bit rate) and is often considered as a typical setting.', + '-iBW-':'Available Bandwidth [MHz]\n\nBandwith occupied by the communication channel. This bandwidth is usually' + ' a degree of freedom of the system design, eventually constrained by technological constraints and ' + 'various kind of frequency usage regulations. Interestingly this parameter is also often mentally ' + 'constrained by past usages which were driven by technological constraints at that time.', + '-iRO-':'Nyquist Filter Rolloff [%]\n\n' + 'To pass a limited bandwidth channel symbol have to be mapped on pulses, "filtered" to limit the ' + 'Bandwidth occupied. Theoretically, filtering can be "brickwall", one symbol per second passing in ' + '1 Hertz. Practically, an excess of bandwidth is required, also called "Roll-Off of the filter.\n\n' + 'The filter used is designed to respect the symmetry condition expressed in the Nyquist Criterion ' + 'avoiding inter-symbol interferences. Such a filter is called a Nyquist Filter. ' + 'and the mimimum theoretical bandwidth (Roll-Off = zero) is called Nyquist Bandwidth.\n\n' + 'The Roll-Off or Excess of Bandwidth is usually expressed as a percentage of the Nyquist Bandwidth.', + '-iCIR-':'Receiver\'s C/I [dB]\n\nEffect of signal impairment associated with terminal implementation, ' + 'expressed as a signal to noise ratio to be combined with the intrinsic Signal to Noise Ratio affecting' + ' the link.\n\nImpairments are multiple : Phase Noise of the radio front end, Quantization Noise of the' + ' receiver\'s Analog to Digital Conversion, effect of imperfect synchronisation ...\n\n' + 'The tool supports comma separated lists of C/I or C/N values expressed in dB. In addition to the ' + 'overall receiver\'s impairments, one can use this feature to simulate more details : downlink ' + 'interferences, LNB\'s phase noise, impairment of signal distribution ...\n\nNote that signal ' + 'impairments associated with the satellite and the receiver are combined together with the link ' + 'noise to evaluate the practical bit rate.', + '-iPenalty-':'Implementation Penalty vs theory [dB]\n\nTurbo and Turbo-like Codes are known for getting ' + '"almost Shannon" performances. There are however still some implementation taxes ' + ': codes always have a residual bit error rate, making it very low requires some CNR margin.\n\n' + 'Other practical aspects also cost signal\'s energy like time and frequency synchronisation, ' + 'physical layer framing...\n\nDVB-S2x, using LDPC codes and modern modulation related features ' + 'is typically 1 dB away of the Shannon Limit in Quasi Error Free operation. Real systems also have' + ' to take margins, considering a reasonable value of 0.5 dB, a total penalty of 1.5 dB can be ' + 'considered as typical.\n\n' + 'Original Turbo codes designed with higher residual bit error rates can get much closer ' + 'to the Shannon Limit. ', + '-iOH-':'Higher Layers Overhead [%]\n\nThe practical usage of information bits is based on a breakdown ' + 'in multiple communications layers, all spending bits for the logistics of carrying user bits.' + 'For example, the process of encapsulation of IP datagrams on a DVB-S2x physical layer using' + ' the GSE standard costs a few percents of net bit rate, spent in framing structures, integrity ' + 'control bits ...\n\n' + 'In a modern efficient satellite forward communication system the overhead to IP costs typically 5%', + '-iNBW-':'Nyquist Bandwidth\n\nThe modulated carrier is passing bits in groups mapped on modulation symbols.' + 'Satellite modulation schemes typically map from 1 to 8 bits on each symbol passing though the channel.' + 'The Bit Rate is directly linked to the symbol rate, the number of symbols per second passing ' + 'the channel ( BR = SR . Number of Bits per Symbol ).\n\n' + 'To pass a bandwidth limited channel, symbols have to be mapped on pulses "filtered" to limit the ' + 'bandwidth. Theoretically, filtering can be "brickwall", one symbol per second passing in 1 Hertz.' + 'Practically, an excess of bandwidth is required, also called "Roll-Off of the filter. ' + 'The filter used is also designed to respect the symmetry condition expressed in the Nyquist Criterion ' + 'avoiding inter-symbol interferences. Such a filter is thus called Nyquist Filter ' + 'and the minimum theoretical bandwidth (Roll-Off = zero) is called Nyquist Bandwidth.', + '-iCNRbw-':'Signal to Noise Ratio in Available BW\n\n Ratio of the Signal Power and the Noise Power Captured ' + ' in the available bandwidth.', + '-iCNRnyq-':'Signal to Noise Ratio in Nyquist BW\n\nRatio of the Signal Power and the Noise Power Captured ' + ' in the Nyquist Bandwidth = Available Bandwidth / ( 1 + Roll-Off).', + '-iCNRrcv-':'Signal to Noise Ratio at Receiver Output\n\nRatio of the Signal Power and the total Noise Power' + ' captured along the complete communication chain (at receiver ouptut). This ratio is the relevant one' + ' for real-life performance evaluation. It is computed by combining the Signal to Noise in the Nyquist ' + 'Bandwidth, the Receiver\'s C/I and the Satellite\'s C/I. Note that these 2 items are themselves ' + 'resulting of many items which can be detailed as comma separated lists.', + '-iBRbw-':'Theoretical Bit Rate in Available BW\n\nBit Rate theoretically achieved with zero Roll-Off in ' + 'the available bandwidth. This bit rate is given by a direct application of the Shannon Limit. ' + 'The normalized bit rate expressed as a percentage of the bit rate at infinite bandwidth is also given ' + 'as well as the spectral efficiency of the available bandwidth.', + '-iBRnyq-':'Theoretical Bit Rate in Nyquist BW\n\nBit Rate theoretically achieved in ' + 'the Nyquist bandwidth (after having removed the Nyquist Roll-Off from the available Bandwidth).' + 'This bit rate is given by a direct application of the Shannon Limit.\n\nThe normalized bit rate ' + 'expressed as a percentage of the bit rate at infinite bandwidth is also given as well as the spectral ' + 'efficiency of the available bandwidth.\n\nThe efficiency in bit per symbol is also given and does ' + 'correspond to the classical spectral efficiency in the Nyquist bandwidth.', + '-iBRrcv-':'Practical Physcial Layer Bit Rate\n\n Practical Bit Rate achieved using real-world conditions. ' + 'This bit rate is evaluated by using the "all degradations included" signal to noise ratio' + 'in the Shannon\'s formula.' + 'This bit rate does correspond to the user bits of the Physical Layer Frames.', + '-iBRhigh-':'Practical Higher Layers Bit Rate\n\nPractical Bit Rate achieved using real-world modulation ' + 'and coding and modern encapsulation methods of higher layers strcutures.\n\nThis Bit Rate does ' + 'typically correspond to the user bits of the IP datagrams', + '-Satellite-':'The evaluation is decomposed in 3 sections:\n\n' + '1. The satellite link : satellite transmitter and path to the receiver\'s location with ' + 'associated key characteristics \n\n' + '2. The radio front end : antenna and amplification unit capturing as much signal as possible ' + 'and as little noise as possible\n\n' + '3. The base-band processing unit : unit extracting from a modulated carrier the useful ' + 'information bits.' + ' All key functions are usually performed via digital signal processing : Nyquist filtering, ' + 'synchronisation, demodulation, error correction, higher layer "decapsulation"...\n\n' + 'All fields are initially filled with meaningful values, you should start the exploration by ' + 'changing the straightforward parameters and keep the intimidating figures unchanged. ' + 'All parameters are "clickable" for getting associated background information.', + '-CRC1-': 'Cyclic Redundancy Check of the input parameters, for changes identification purposes.', + '-CRC2-': 'Cyclic Redundancy Check of the input parameters, for changes identification purposes.', + '-CRC3-': 'Cyclic Redundancy Check of the input parameters, for changes identification purposes.', + 'Advanced': 'The Shannon Limit is a very powerful tool to analyse communication systems\' design, trade offs.' + 'All capacity evaluations in this tool are based on direct application of this formula taking ' + 'into account real world impairments via signal to noise combinations. With this approach, ' + 'using the overall C/N evaluated for a practical communication link gives a good estimate of the ' + 'capacity achievable.\n\nApplying in addition the known average penalty of real modulation and ' + 'coding schemes makes it accurate enough for initial systems evaluations.\n\nThe analytic formulas ' + 'derived from the Shannon Limit for given spectral efficiencies are also of great help to drive ' + 'the thinking in practical trade-offs.\n\n' + 'Additional useful links for people interested in a theoretical immersion :' + 'https://en.wikipedia.org/wiki/Nyquist_ISI_criterion\n' + 'https://en.wikipedia.org/wiki/Error_correction_code#Forward_error_correction\n' + 'https://en.wikipedia.org/wiki/Viterbi_decoder\n' + 'https://en.wikipedia.org/wiki/Turbo_code\n' + 'https://en.wikipedia.org/wiki/DVB-S2\n' + 'https://en.wikipedia.org/wiki/OSI_model\n', + 'Help':'Recommendations for using the tool\n\nThe first purpose of the tool is educational, allowing people to ' + 'better understand the physics of communications and the role of key parameters\n\n' + 'All labels can be \"clicked\" to get information about associated item. All values ' + '(including this text) can be copy/pasted for further usage.\n\n' + 'The user should try multiple values in the fields one per one (starting from the least intimidating), ' + 'explore the graphs and try to understand the underlying physics.\n\n' + 'The units for the different figures are as explicit as possible to facilitate the exploration.\n\n' + 'Despite the simplicity of the approach, the tool can also be useful to do a quick analysis of a ' + 'communication link with a first order approach, avoiding the trap of the illusion of precision.\n\n' + + } + diff --git a/Shannon_Real.db b/Shannon_Real.db new file mode 100644 index 0000000000000000000000000000000000000000..6bcfaee37e4ba758a0e0d8ee8a0acd33f054c5da GIT binary patch literal 8192 zcmeI1-D}i95Wtf|kRJE}J_!0UK2`CWtEo?YYAvyxR;e|JuXD-V-Ey1Vba&GP876JP>NfC(@GCcp%k025#WOn?b6f&Z1j)r;`q^X={M$~v4& z%CvGu%qu6fvV+6u=qx!*6G#s~oFp(X!E03&F#DP&$I00Q6cz+&@-v-)6HeahYieq3 za(io{OSe&T^iBTFg%Yi8y}IL@t~Y-?3;beX0!)AjFaajO1egF5U;<2l2`~XBa6f_T z_uCsHxjFf$o3pm%61M6E*DgCh7el&Dmk;)o4m8k3&R1MhH084Q} zP+3}35>VxEUbVyNoE!>CIf#D*?fl(}gz*7zNw7a2w?+Jw9kLY~%p4hEoye?r1Xwv& z+WqO&mY7jw*NxHHnp~9A^lp?nVPU&WU5>#`be%QIbTPQI=wDm5ukF+MY`v+o^n4o09hFaLy literal 0 HcmV?d00001 diff --git a/Shannon_Theory.db b/Shannon_Theory.db new file mode 100644 index 0000000000000000000000000000000000000000..9e29d3caa403eec559cfe9cd60e96d750b3ac83d GIT binary patch literal 8192 zcmeI1OK;Oa5PZGK%aG?zb3N0;-g}AQmNxanFb$8co1E=!1 z^AGte%sNTaMirbvyNctTnVs>>w|g0F|NR~}2m`4KW?;*E;Q7Ay9KiFu1$r9vOovaC zTT_AFR{q;q@LF$w-lbLk{luf68;*b@;0QPZj({WJ2si?cfFs}tI0BBq|4QKEp?`aM zZOy;XhQ&E1QW(YK%5W+4HKEZv3b&&WqV1i%5U!cvv8W2zeHVqV!lMlktU!pukI@Dg zZu05;7)NJPrFv?@b2FKyjGFwhL&>zZt7>uG_U4T{zr1^W`g6k(a0DCyN5Bzq1RMcJ zz!7i+9D#qAz}J(;qQCTP`TLjEmByRH-foIa+uKeMwmHI*Ynh_1)vr+@FnYA4?uy^M z5{!fx;Y@L3P+Y#_5tt0AXDfDfFaLm{|Lq zd4G5RU{19y=yPI{`z+87vclfoaxt89=u^j+=k{*5&p-dke4t`(#!O~$XWl*PQf;Ig z>u9xF&CX`$X|UM|I$Ix57+IZvd*UxHEdmZvLs}BEGHyKHL?^z z4hSFDP%1f8tN@y_Q^s@KF9R2F)$wp-G9qUEfv&7;KbEEnwT3Qa#+16-ZV$Q1s<=fP z(*9H?nH&rh8@2lx6GDoBkOk9Xn=cV_7$`)$w+aKOG) zrmdi0lp&}6p%lzzDRq9fj^Wr`N2JzUCcH)oXOv;Bj9GF#RI(B&MHxs7;gn9Fl_qIW l>~mAG95T$y7DN`6P3nsFIx;#Z3?`sbULP^R)aZuI`VBC?RPg`+ literal 0 HcmV?d00001 diff --git a/command.txt b/command.txt new file mode 100644 index 0000000..6d4b06b --- /dev/null +++ b/command.txt @@ -0,0 +1,2 @@ +docker build -t my-python-app . +docker run -it --rm --name my-running-app --network=nginx-proxy --env VIRTUAL_HOST=shannon.antopoid.com --env LETSENCRYPT_HOST=shannon.antopoid.com --env LETSENCRYPT_EMAIL=poidevin.freeboxos.fr@gmail.com my-python-app diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c1fef2b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3.1' +services: + python: + image: python.slim:latest + container_name: python + restart: always + environment: + - VIRTUAL_HOST=shannon.antopoid.com + - LETSENCRYPT_HOST=shannon.antopoid.com + - LETSENCRYPT_EMAIL=poidevin.freeboxos.fr@gmail.com + ports: + - 8888:8080 + +networks: + default: + external: + name: nginx-proxy + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7b2feaa --- /dev/null +++ b/requirements.txt @@ -0,0 +1,25 @@ +HTMLParser +tk-tools==0.16.0 +Flask==1.1.2 +Jinja2==3.0.3 +MarkupSafe==2.0.1 +astropy==5.3 +certifi==2021.10.8 +cycler==0.11.0 +fonttools==4.28.5 +itur==0.3.3 +kiwisolver==1.4.4 +matplotlib==3.7.2 +numpy==1.25 +packaging==21.3 +Pillow==10.0.0 +pyerfa==2.0.0.3 +pyparsing==3.0.6 +pyproj==3.6.0 +remi==2021.3.2 +PySimpleGUI==4.60.5 +python-dateutil==2.8.2 +PyYAML==6.0 +scipy==1.11.1 +six==1.16.0 + diff --git a/slim.report.json b/slim.report.json new file mode 100644 index 0000000..00ef60b --- /dev/null +++ b/slim.report.json @@ -0,0 +1,505 @@ +{ + "version": "1.1", + "engine": "linux|Transformer|1.40.4|d310b07567dc90763f5f27f94c618f057295b55d|2023-08-26_01:39:22AM", + "containerized": false, + "host_distro": { + "name": "Ubuntu", + "version": "22.04", + "display_name": "Ubuntu 22.04.3 LTS" + }, + "type": "build", + "state": "done", + "target_reference": "python:latest", + "system": { + "type": "", + "release": "", + "distro": { + "name": "", + "version": "", + "display_name": "" + } + }, + "source_image": { + "identity": { + "id": "sha256:e13fe79153bbf089fb6bac4fc8710eae318c6aa124a1ba4aa609e1e136496543", + "tags": [ + "latest" + ], + "names": [ + "python:latest" + ] + }, + "size": 1020536111, + "size_human": "1.0 GB", + "create_time": "2023-07-16T18:27:16Z", + "docker_version": "20.10.21", + "architecture": "amd64", + "os": "linux", + "env_vars": [ + "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "LANG=C.UTF-8", + "GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D", + "PYTHON_VERSION=3.11.4", + "PYTHON_PIP_VERSION=23.1.2", + "PYTHON_SETUPTOOLS_VERSION=65.5.1", + "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/0d8570dc44796f4369b652222cf176b3db6ac70e/public/get-pip.py", + "PYTHON_GET_PIP_SHA256=96461deced5c2a487ddc65207ec5a9cffeca0d34e7af7ea1afc470ff0d746207" + ], + "container_entry": { + "exe_path": "" + } + }, + "minified_image_size": 267206999, + "minified_image_size_human": "267 MB", + "minified_image": "python.slim", + "minified_image_has_data": true, + "minified_by": 3.8192716314290855, + "artifact_location": "/tmp/slim-state/.slim-state/images/e13fe79153bbf089fb6bac4fc8710eae318c6aa124a1ba4aa609e1e136496543/artifacts", + "container_report_name": "creport.json", + "seccomp_profile_name": "python-seccomp.json", + "apparmor_profile_name": "python-apparmor-profile", + "image_stack": [ + { + "is_top_image": true, + "id": "sha256:e13fe79153bbf089fb6bac4fc8710eae318c6aa124a1ba4aa609e1e136496543", + "full_name": "python:latest", + "repo_name": "python", + "version_tag": "latest", + "raw_tags": [ + "python:latest" + ], + "create_time": "2023-07-16T18:27:16Z", + "new_size": 1020536111, + "new_size_human": "1.0 GB", + "instructions": [ + { + "type": "ADD", + "time": "2023-07-04T01:19:58Z", + "is_nop": true, + "local_image_exists": false, + "layer_index": 0, + "size": 74759018, + "size_human": "75 MB", + "params": "file:bd80a4461150784e5f2f5a1faa720cc347ad3e30ee0969adbfad574c316f5aef in /", + "command_snippet": "ADD file:bd80a4461150784e5f2f5a1faa720cc347a...", + "command_all": "ADD file:bd80a4461150784e5f2f5a1faa720cc347ad3e30ee0969adbfad574c316f5aef /", + "target": "/", + "source_type": "file", + "inst_set_time_bucket": "2023-07-04T01:15:00Z", + "inst_set_time_index": 1, + "inst_set_time_reverse_index": 3 + }, + { + "type": "CMD", + "time": "2023-07-04T01:19:58Z", + "is_nop": true, + "is_exec_form": true, + "local_image_exists": false, + "layer_index": 0, + "size": 0, + "params": "[\"bash\"]\n", + "command_snippet": "CMD [\"bash\"]\n", + "command_all": "CMD [\"bash\"]\n", + "inst_set_time_bucket": "2023-07-04T01:15:00Z", + "inst_set_time_index": 1, + "inst_set_time_reverse_index": 3 + }, + { + "type": "ENV", + "time": "2023-06-13T17:45:16Z", + "is_nop": false, + "local_image_exists": false, + "layer_index": 0, + "size": 0, + "params": "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "command_snippet": "ENV PATH=/usr/local/bin:/usr/local/sbin:/usr...", + "command_all": "ENV PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "comment": "buildkit.dockerfile.v0", + "is_buildkit_instruction": true, + "inst_set_time_bucket": "2023-06-13T17:45:00Z", + "inst_set_time_index": 0, + "inst_set_time_reverse_index": 4 + }, + { + "type": "ENV", + "time": "2023-06-13T17:45:16Z", + "is_nop": false, + "local_image_exists": false, + "layer_index": 0, + "size": 0, + "params": "LANG=C.UTF-8", + "command_snippet": "ENV LANG=C.UTF-8", + "command_all": "ENV LANG=C.UTF-8", + "comment": "buildkit.dockerfile.v0", + "is_buildkit_instruction": true, + "inst_set_time_bucket": "2023-06-13T17:45:00Z", + "inst_set_time_index": 0, + "inst_set_time_reverse_index": 4 + }, + { + "type": "RUN", + "time": "2023-06-13T17:45:16Z", + "is_nop": false, + "local_image_exists": false, + "layer_index": 0, + "size": 9214160, + "size_human": "9.2 MB", + "command_snippet": "RUN set -eux; \tapt-get update; \tapt-get inst...", + "command_all": "RUN set -eux; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tca-certificates \t\tnetbase \t\ttzdata \t; \trm -rf /var/lib/apt/lists/*", + "system_commands": [ + "set -eux", + "apt-get update", + "apt-get install -y --no-install-recommends ca-certificates netbase tzdata", + "rm -rf /var/lib/apt/lists/*" + ], + "comment": "buildkit.dockerfile.v0", + "is_buildkit_instruction": true, + "inst_set_time_bucket": "2023-06-13T17:45:00Z", + "inst_set_time_index": 0, + "inst_set_time_reverse_index": 4 + }, + { + "type": "ENV", + "time": "2023-06-13T17:45:16Z", + "is_nop": false, + "local_image_exists": false, + "layer_index": 0, + "size": 0, + "params": "GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D", + "command_snippet": "ENV GPG_KEY=A035C8C19219BA821ECEA86B64E628F8...", + "command_all": "ENV GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D", + "comment": "buildkit.dockerfile.v0", + "is_buildkit_instruction": true, + "inst_set_time_bucket": "2023-06-13T17:45:00Z", + "inst_set_time_index": 0, + "inst_set_time_reverse_index": 4 + }, + { + "type": "ENV", + "time": "2023-06-13T17:45:16Z", + "is_nop": false, + "local_image_exists": false, + "layer_index": 0, + "size": 0, + "params": "PYTHON_VERSION=3.11.4", + "command_snippet": "ENV PYTHON_VERSION=3.11.4", + "command_all": "ENV PYTHON_VERSION=3.11.4", + "comment": "buildkit.dockerfile.v0", + "is_buildkit_instruction": true, + "inst_set_time_bucket": "2023-06-13T17:45:00Z", + "inst_set_time_index": 0, + "inst_set_time_reverse_index": 4 + }, + { + "type": "RUN", + "time": "2023-06-13T17:45:16Z", + "is_nop": false, + "local_image_exists": false, + "layer_index": 0, + "size": 53218187, + "size_human": "53 MB", + "command_snippet": "RUN set -eux; \t\tsavedAptMark=\"$(apt-mark sho...", + "command_all": "RUN set -eux; \t\tsavedAptMark=\"$(apt-mark showmanual)\"; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tdpkg-dev \t\tgcc \t\tgnupg \t\tlibbluetooth-dev \t\tlibbz2-dev \t\tlibc6-dev \t\tlibdb-dev \t\tlibexpat1-dev \t\tlibffi-dev \t\tlibgdbm-dev \t\tliblzma-dev \t\tlibncursesw5-dev \t\tlibreadline-dev \t\tlibsqlite3-dev \t\tlibssl-dev \t\tmake \t\ttk-dev \t\tuuid-dev \t\twget \t\txz-utils \t\tzlib1g-dev \t; \t\twget -O python.tar.xz \"https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz\"; \twget -O python.tar.xz.asc \"https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc\"; \tGNUPGHOME=\"$(mktemp -d)\"; export GNUPGHOME; \tgpg --batch --keyserver hkps://keys.openpgp.org --recv-keys \"$GPG_KEY\"; \tgpg --batch --verify python.tar.xz.asc python.tar.xz; \tgpgconf --kill all; \trm -rf \"$GNUPGHOME\" python.tar.xz.asc; \tmkdir -p /usr/src/python; \ttar --extract --directory /usr/src/python --strip-components=1 --file python.tar.xz; \trm python.tar.xz; \t\tcd /usr/src/python; \tgnuArch=\"$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)\"; \t./configure \t\t--build=\"$gnuArch\" \t\t--enable-loadable-sqlite-extensions \t\t--enable-optimizations \t\t--enable-option-checking=fatal \t\t--enable-shared \t\t--with-lto \t\t--with-system-expat \t\t--without-ensurepip \t; \tnproc=\"$(nproc)\"; \tEXTRA_CFLAGS=\"$(dpkg-buildflags --get CFLAGS)\"; \tLDFLAGS=\"$(dpkg-buildflags --get LDFLAGS)\"; \tLDFLAGS=\"${LDFLAGS:--Wl},--strip-all\"; \tmake -j \"$nproc\" \t\t\"EXTRA_CFLAGS=${EXTRA_CFLAGS:-}\" \t\t\"LDFLAGS=${LDFLAGS:-}\" \t\t\"PROFILE_TASK=${PROFILE_TASK:-}\" \t; \trm python; \tmake -j \"$nproc\" \t\t\"EXTRA_CFLAGS=${EXTRA_CFLAGS:-}\" \t\t\"LDFLAGS=${LDFLAGS:--Wl},-rpath='\\$\\$ORIGIN/../lib'\" \t\t\"PROFILE_TASK=${PROFILE_TASK:-}\" \t\tpython \t; \tmake install; \t\tcd /; \trm -rf /usr/src/python; \t\tfind /usr/local -depth \t\t\\( \t\t\t\\( -type d -a \\( -name test -o -name tests -o -name idle_test \\) \\) \t\t\t-o \\( -type f -a \\( -name '*.pyc' -o -name '*.pyo' -o -name 'libpython*.a' \\) \\) \t\t\\) -exec rm -rf '{}' + \t; \t\tldconfig; \t\tapt-mark auto '.*' > /dev/null; \tapt-mark manual $savedAptMark; \tfind /usr/local -type f -executable -not \\( -name '*tkinter*' \\) -exec ldd '{}' ';' \t\t| awk '/=>/ { so = $(NF-1); if (index(so, \"/usr/local/\") == 1) { next }; gsub(\"^/(usr/)?\", \"\", so); print so }' \t\t| sort -u \t\t| xargs -r dpkg-query --search \t\t| cut -d: -f1 \t\t| sort -u \t\t| xargs -r apt-mark manual \t; \tapt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \trm -rf /var/lib/apt/lists/*; \t\tpython3 --version", + "system_commands": [ + "set -eux", + "savedAptMark=\"$(apt-mark showmanual)\"", + "apt-get update", + "apt-get install -y --no-install-recommends dpkg-dev gcc gnupg libbluetooth-dev libbz2-dev libc6-dev libdb-dev libexpat1-dev libffi-dev libgdbm-dev liblzma-dev libncursesw5-dev libreadline-dev libsqlite3-dev libssl-dev make tk-dev uuid-dev wget xz-utils zlib1g-dev", + "wget -O python.tar.xz \"https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz\"", + "wget -O python.tar.xz.asc \"https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc\"", + "GNUPGHOME=\"$(mktemp -d)\"", + "export GNUPGHOME", + "gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys \"$GPG_KEY\"", + "gpg --batch --verify python.tar.xz.asc python.tar.xz", + "gpgconf --kill all", + "rm -rf \"$GNUPGHOME\" python.tar.xz.asc", + "mkdir -p /usr/src/python", + "tar --extract --directory /usr/src/python --strip-components=1 --file python.tar.xz", + "rm python.tar.xz", + "cd /usr/src/python", + "gnuArch=\"$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)\"", + "./configure --build=\"$gnuArch\" --enable-loadable-sqlite-extensions --enable-optimizations --enable-option-checking=fatal --enable-shared --with-lto --with-system-expat --without-ensurepip", + "nproc=\"$(nproc)\"", + "EXTRA_CFLAGS=\"$(dpkg-buildflags --get CFLAGS)\"", + "LDFLAGS=\"$(dpkg-buildflags --get LDFLAGS)\"", + "LDFLAGS=\"${LDFLAGS:--Wl},--strip-all\"", + "make -j \"$nproc\" \"EXTRA_CFLAGS=${EXTRA_CFLAGS:-}\" \"LDFLAGS=${LDFLAGS:-}\" \"PROFILE_TASK=${PROFILE_TASK:-}\"", + "rm python", + "make -j \"$nproc\" \"EXTRA_CFLAGS=${EXTRA_CFLAGS:-}\" \"LDFLAGS=${LDFLAGS:--Wl},-rpath='$$ORIGIN/../lib'\" \"PROFILE_TASK=${PROFILE_TASK:-}\" python", + "make install", + "cd /", + "rm -rf /usr/src/python", + "find /usr/local -depth ( ( -type d -a ( -name test -o -name tests -o -name idle_test ) ) -o ( -type f -a ( -name '*.pyc' -o -name '*.pyo' -o -name 'libpython*.a' ) ) ) -exec rm -rf '{}' +", + "ldconfig", + "apt-mark auto '.*' > /dev/null", + "apt-mark manual $savedAptMark", + "find /usr/local -type f -executable -not ( -name '*tkinter*' ) -exec ldd '{}' '", + "' | awk '/=>/ { so = $(NF-1)", + "if (index(so, \"/usr/local/\") == 1) { next }", + "gsub(\"^/(usr/)?\", \"\", so)", + "print so }' | sort -u | xargs -r dpkg-query --search | cut -d: -f1 | sort -u | xargs -r apt-mark manual", + "apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false", + "rm -rf /var/lib/apt/lists/*", + "python3 --version" + ], + "comment": "buildkit.dockerfile.v0", + "is_buildkit_instruction": true, + "inst_set_time_bucket": "2023-06-13T17:45:00Z", + "inst_set_time_index": 0, + "inst_set_time_reverse_index": 4 + }, + { + "type": "RUN", + "time": "2023-06-13T17:45:16Z", + "is_nop": false, + "local_image_exists": false, + "layer_index": 0, + "size": 32, + "size_human": "32 B", + "command_snippet": "RUN set -eux; \tfor src in idle3 pydoc3 pytho...", + "command_all": "RUN set -eux; \tfor src in idle3 pydoc3 python3 python3-config; do \t\tdst=\"$(echo \"$src\" | tr -d 3)\"; \t\t[ -s \"/usr/local/bin/$src\" ]; \t\t[ ! -e \"/usr/local/bin/$dst\" ]; \t\tln -svT \"$src\" \"/usr/local/bin/$dst\"; \tdone", + "system_commands": [ + "set -eux", + "for src in idle3 pydoc3 python3 python3-config", + "do dst=\"$(echo \"$src\" | tr -d 3)\"", + "[ -s \"/usr/local/bin/$src\" ]", + "[ ! -e \"/usr/local/bin/$dst\" ]", + "ln -svT \"$src\" \"/usr/local/bin/$dst\"", + "done" + ], + "comment": "buildkit.dockerfile.v0", + "is_buildkit_instruction": true, + "inst_set_time_bucket": "2023-06-13T17:45:00Z", + "inst_set_time_index": 0, + "inst_set_time_reverse_index": 4 + }, + { + "type": "ENV", + "time": "2023-06-13T17:45:16Z", + "is_nop": false, + "local_image_exists": false, + "layer_index": 0, + "size": 0, + "params": "PYTHON_PIP_VERSION=23.1.2", + "command_snippet": "ENV PYTHON_PIP_VERSION=23.1.2", + "command_all": "ENV PYTHON_PIP_VERSION=23.1.2", + "comment": "buildkit.dockerfile.v0", + "is_buildkit_instruction": true, + "inst_set_time_bucket": "2023-06-13T17:45:00Z", + "inst_set_time_index": 0, + "inst_set_time_reverse_index": 4 + }, + { + "type": "ENV", + "time": "2023-06-13T17:45:16Z", + "is_nop": false, + "local_image_exists": false, + "layer_index": 0, + "size": 0, + "params": "PYTHON_SETUPTOOLS_VERSION=65.5.1", + "command_snippet": "ENV PYTHON_SETUPTOOLS_VERSION=65.5.1", + "command_all": "ENV PYTHON_SETUPTOOLS_VERSION=65.5.1", + "comment": "buildkit.dockerfile.v0", + "is_buildkit_instruction": true, + "inst_set_time_bucket": "2023-06-13T17:45:00Z", + "inst_set_time_index": 0, + "inst_set_time_reverse_index": 4 + }, + { + "type": "ENV", + "time": "2023-06-13T17:45:16Z", + "is_nop": false, + "local_image_exists": false, + "layer_index": 0, + "size": 0, + "params": "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/0d8570dc44796f4369b652222cf176b3db6ac70e/public/get-pip.py", + "command_snippet": "ENV PYTHON_GET_PIP_URL=https://github.com/py...", + "command_all": "ENV PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/0d8570dc44796f4369b652222cf176b3db6ac70e/public/get-pip.py", + "comment": "buildkit.dockerfile.v0", + "is_buildkit_instruction": true, + "inst_set_time_bucket": "2023-06-13T17:45:00Z", + "inst_set_time_index": 0, + "inst_set_time_reverse_index": 4 + }, + { + "type": "ENV", + "time": "2023-06-13T17:45:16Z", + "is_nop": false, + "local_image_exists": false, + "layer_index": 0, + "size": 0, + "params": "PYTHON_GET_PIP_SHA256=96461deced5c2a487ddc65207ec5a9cffeca0d34e7af7ea1afc470ff0d746207", + "command_snippet": "ENV PYTHON_GET_PIP_SHA256=96461deced5c2a487d...", + "command_all": "ENV PYTHON_GET_PIP_SHA256=96461deced5c2a487ddc65207ec5a9cffeca0d34e7af7ea1afc470ff0d746207", + "comment": "buildkit.dockerfile.v0", + "is_buildkit_instruction": true, + "inst_set_time_bucket": "2023-06-13T17:45:00Z", + "inst_set_time_index": 0, + "inst_set_time_reverse_index": 4 + }, + { + "type": "RUN", + "time": "2023-06-13T17:45:16Z", + "is_nop": false, + "local_image_exists": false, + "layer_index": 0, + "size": 12240418, + "size_human": "12 MB", + "command_snippet": "RUN set -eux; \t\tsavedAptMark=\"$(apt-mark sho...", + "command_all": "RUN set -eux; \t\tsavedAptMark=\"$(apt-mark showmanual)\"; \tapt-get update; \tapt-get install -y --no-install-recommends wget; \t\twget -O get-pip.py \"$PYTHON_GET_PIP_URL\"; \techo \"$PYTHON_GET_PIP_SHA256 *get-pip.py\" | sha256sum -c -; \t\tapt-mark auto '.*' > /dev/null; \t[ -z \"$savedAptMark\" ] || apt-mark manual $savedAptMark > /dev/null; \tapt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \trm -rf /var/lib/apt/lists/*; \t\texport PYTHONDONTWRITEBYTECODE=1; \t\tpython get-pip.py \t\t--disable-pip-version-check \t\t--no-cache-dir \t\t--no-compile \t\t\"pip==$PYTHON_PIP_VERSION\" \t\t\"setuptools==$PYTHON_SETUPTOOLS_VERSION\" \t; \trm -f get-pip.py; \t\tpip --version", + "system_commands": [ + "set -eux", + "savedAptMark=\"$(apt-mark showmanual)\"", + "apt-get update", + "apt-get install -y --no-install-recommends wget", + "wget -O get-pip.py \"$PYTHON_GET_PIP_URL\"", + "echo \"$PYTHON_GET_PIP_SHA256 *get-pip.py\" | sha256sum -c -", + "apt-mark auto '.*' > /dev/null", + "[ -z \"$savedAptMark\" ] || apt-mark manual $savedAptMark > /dev/null", + "apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false", + "rm -rf /var/lib/apt/lists/*", + "export PYTHONDONTWRITEBYTECODE=1", + "python get-pip.py --disable-pip-version-check --no-cache-dir --no-compile \"pip==$PYTHON_PIP_VERSION\" \"setuptools==$PYTHON_SETUPTOOLS_VERSION\"", + "rm -f get-pip.py", + "pip --version" + ], + "comment": "buildkit.dockerfile.v0", + "is_buildkit_instruction": true, + "inst_set_time_bucket": "2023-06-13T17:45:00Z", + "inst_set_time_index": 0, + "inst_set_time_reverse_index": 4 + }, + { + "type": "CMD", + "time": "2023-06-13T17:45:16Z", + "is_nop": false, + "is_exec_form": true, + "local_image_exists": true, + "intermediate_image_id": "sha256:be2470db10f711ec941d24bc9a489dd457b6000b624ee251e19a445ad9f38839", + "layer_index": 0, + "size": 0, + "params": "[\"python3\"]\n", + "command_snippet": "CMD [\"python3\"]\n", + "command_all": "CMD [\"python3\"]\n", + "comment": "buildkit.dockerfile.v0", + "is_buildkit_instruction": true, + "inst_set_time_bucket": "2023-06-13T17:45:00Z", + "inst_set_time_index": 0, + "inst_set_time_reverse_index": 4 + }, + { + "type": "WORKDIR", + "time": "2023-07-06T15:08:21Z", + "is_nop": true, + "local_image_exists": true, + "intermediate_image_id": "sha256:ad187361307c8e838bb1f0a48b91e97d83d7aea811827b4d2bab393284f739e0", + "layer_index": 0, + "size": 0, + "params": "/usr/src/app", + "command_snippet": "WORKDIR /usr/src/app", + "command_all": "WORKDIR /usr/src/app", + "system_commands": [ + "mkdir -p /usr/src/app" + ], + "inst_set_time_bucket": "2023-07-06T15:00:00Z", + "inst_set_time_index": 2, + "inst_set_time_reverse_index": 2 + }, + { + "type": "COPY", + "time": "2023-07-12T20:36:09Z", + "is_nop": true, + "local_image_exists": true, + "intermediate_image_id": "sha256:a1b245cb980c1ac2c038a60bbe84fb8c65a1be6aedce32e15f29a53e4ea8e364", + "layer_index": 0, + "size": 398, + "size_human": "398 B", + "params": "file:1383fead6d13fe1a3d2822aaafeadc3c38b2cfeea627e71b14c63805820e09a2 in ./", + "command_snippet": "COPY file:1383fead6d13fe1a3d2822aaafeadc3c38...", + "command_all": "COPY file:1383fead6d13fe1a3d2822aaafeadc3c38b2cfeea627e71b14c63805820e09a2 ./", + "target": "./", + "source_type": "file", + "inst_set_time_bucket": "2023-07-12T20:30:00Z", + "inst_set_time_index": 3, + "inst_set_time_reverse_index": 1 + }, + { + "type": "RUN", + "time": "2023-07-16T18:25:45Z", + "is_nop": false, + "local_image_exists": true, + "intermediate_image_id": "sha256:0bffdc8daa291358931fff5ca204342be123ed040aa8352657c0a97dc0da7a1b", + "layer_index": 0, + "size": 36428885, + "size_human": "36 MB", + "command_snippet": "RUN apt-get update -y && \\\n\tapt-get install ...", + "command_all": "RUN apt-get update -y && \\\n\tapt-get install -y tk tcl", + "system_commands": [ + "apt-get update -y", + "apt-get install -y tk tcl" + ], + "inst_set_time_bucket": "2023-07-16T18:15:00Z", + "inst_set_time_index": 4, + "inst_set_time_reverse_index": 0 + }, + { + "type": "RUN", + "time": "2023-07-16T18:27:15Z", + "is_nop": false, + "local_image_exists": true, + "intermediate_image_id": "sha256:1087081f44d63fc4d50ed96db8ee05d9ec956c1dbbcd8b125511ced95b4c1d7e", + "layer_index": 0, + "size": 833949399, + "size_human": "834 MB", + "command_snippet": "RUN pip install --force-reinstall -r require...", + "command_all": "RUN pip install --force-reinstall -r requirements.txt", + "system_commands": [ + "pip install --force-reinstall -r requirements.txt" + ], + "inst_set_time_bucket": "2023-07-16T18:15:00Z", + "inst_set_time_index": 4, + "inst_set_time_reverse_index": 0 + }, + { + "type": "COPY", + "time": "2023-07-16T18:27:16Z", + "is_nop": true, + "local_image_exists": true, + "intermediate_image_id": "sha256:7ae91476369b98ce3f0ec518e591385fdde2a0631944b93cb5672850c27086d5", + "layer_index": 0, + "size": 725614, + "size_human": "726 kB", + "params": "dir:c64af32925ea67cdf709617fb045107117c1bc58e9add8805e4e31a29cdfbc91 in .", + "command_snippet": "COPY dir:c64af32925ea67cdf709617fb045107117c...", + "command_all": "COPY dir:c64af32925ea67cdf709617fb045107117c1bc58e9add8805e4e31a29cdfbc91 .", + "target": ".", + "source_type": "dir", + "inst_set_time_bucket": "2023-07-16T18:15:00Z", + "inst_set_time_index": 4, + "inst_set_time_reverse_index": 0 + }, + { + "type": "CMD", + "time": "2023-07-16T18:27:16Z", + "is_last_instruction": true, + "is_nop": true, + "is_exec_form": true, + "local_image_exists": true, + "layer_index": 0, + "size": 0, + "params": "[\"python\",\"./Shannon.py\"]\n", + "command_snippet": "CMD [\"python\",\"./Shannon.py\"]\n", + "command_all": "CMD [\"python\",\"./Shannon.py\"]\n", + "raw_tags": [ + "python:latest" + ], + "inst_set_time_bucket": "2023-07-16T18:15:00Z", + "inst_set_time_index": 4, + "inst_set_time_reverse_index": 0 + } + ] + } + ], + "image_created": true, + "image_build_engine": "internal" +}