From 6a4ccc3376bc6999f82474bef66dea4e1c899553 Mon Sep 17 00:00:00 2001 From: "Poidevin, Antoine (ITOP CM) - AF" Date: Fri, 20 Feb 2026 10:33:09 +0100 Subject: [PATCH] feat: add interactive exploration of Shannon's capacity formula with Plotly graphs - Implemented bandwidth sensitivity and power sensitivity plots. - Created a contour map for bit rate multiplying factors. - Added input parameters for C/N and bandwidth with validation. - Displayed computed results and sensitivity analysis metrics. - Integrated interactive graphs for user exploration. - Included background information section for user guidance. --- .dockerignore | 10 + .gitea/workflows/deploy-shannon.yml | 136 + .streamlit/config.toml | 21 + Dockerfile | 47 +- Dockerfile.save | 12 - PySimpleGUIWeb.py | 8199 ----------------- README.md | 113 +- Shannon.py | 1028 --- Shannon.py.save | 1024 -- Shannon_Dict.py | 330 - app.py | 159 + command.txt | 2 - core/__init__.py | 0 core/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 161 bytes core/__pycache__/calculations.cpython-313.pyc | Bin 0 -> 9223 bytes core/__pycache__/database.cpython-313.pyc | Bin 0 -> 5297 bytes core/__pycache__/help_texts.cpython-313.pyc | Bin 0 -> 13154 bytes core/calculations.py | 280 + core/database.py | 107 + core/help_texts.py | 297 + docker-compose.yml | 48 +- docker-stack.yml | 43 + requirements.txt | 44 +- slim.report.json | 505 - views/__init__.py | 0 views/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 162 bytes .../__pycache__/contributions.cpython-313.pyc | Bin 0 -> 6818 bytes .../orbits_animation.cpython-313.pyc | Bin 0 -> 20526 bytes views/__pycache__/real_world.cpython-313.pyc | Bin 0 -> 21123 bytes .../satellite_animation.cpython-313.pyc | Bin 0 -> 26283 bytes .../satellite_types.cpython-313.pyc | Bin 0 -> 39688 bytes views/__pycache__/theory.cpython-313.pyc | Bin 0 -> 12118 bytes views/contributions.py | 130 + views/orbits_animation.py | 585 ++ views/real_world.py | 412 + views/satellite_animation.py | 762 ++ views/satellite_types.py | 917 ++ views/theory.py | 269 + 38 files changed, 4319 insertions(+), 11161 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/deploy-shannon.yml create mode 100644 .streamlit/config.toml delete mode 100644 Dockerfile.save delete mode 100644 PySimpleGUIWeb.py delete mode 100644 Shannon.py delete mode 100644 Shannon.py.save delete mode 100644 Shannon_Dict.py create mode 100644 app.py delete mode 100644 command.txt create mode 100644 core/__init__.py create mode 100644 core/__pycache__/__init__.cpython-313.pyc create mode 100644 core/__pycache__/calculations.cpython-313.pyc create mode 100644 core/__pycache__/database.cpython-313.pyc create mode 100644 core/__pycache__/help_texts.cpython-313.pyc create mode 100644 core/calculations.py create mode 100644 core/database.py create mode 100644 core/help_texts.py create mode 100644 docker-stack.yml delete mode 100644 slim.report.json create mode 100644 views/__init__.py create mode 100644 views/__pycache__/__init__.cpython-313.pyc create mode 100644 views/__pycache__/contributions.cpython-313.pyc create mode 100644 views/__pycache__/orbits_animation.cpython-313.pyc create mode 100644 views/__pycache__/real_world.cpython-313.pyc create mode 100644 views/__pycache__/satellite_animation.cpython-313.pyc create mode 100644 views/__pycache__/satellite_types.cpython-313.pyc create mode 100644 views/__pycache__/theory.cpython-313.pyc create mode 100644 views/contributions.py create mode 100644 views/orbits_animation.py create mode 100644 views/real_world.py create mode 100644 views/satellite_animation.py create mode 100644 views/satellite_types.py create mode 100644 views/theory.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6839098 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +__pycache__ +*.pyc +*.pyo +.git +.gitignore +*.save +*.db +.venv +venv +README.md diff --git a/.gitea/workflows/deploy-shannon.yml b/.gitea/workflows/deploy-shannon.yml new file mode 100644 index 0000000..4fa94ad --- /dev/null +++ b/.gitea/workflows/deploy-shannon.yml @@ -0,0 +1,136 @@ +name: Build & Deploy Shannon + +on: + push: + branches: + - main + - master + paths: + - 'app.py' + - 'core/**' + - 'views/**' + - 'requirements.txt' + - 'Dockerfile' + - 'docker-stack.yml' + - '.streamlit/**' + - '.gitea/workflows/deploy-shannon.yml' + workflow_dispatch: + +env: + IMAGE_NAME: shannon/streamlit + +jobs: + build-and-deploy: + name: ๐Ÿ—๏ธ Build & Deploy Shannon + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ“ฅ Checkout code + uses: https://github.com/actions/checkout@v4 + + - name: ๐Ÿ” Setup SSH + env: + SSH_KEY: ${{ secrets.SWARM_SSH_KEY }} + SSH_HOST: ${{ secrets.SWARM_MANAGER_HOST }} + run: | + mkdir -p ~/.ssh + echo "$SSH_KEY" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts + + - name: ๐Ÿ“ฆ Copy source to server + env: + SSH_USER: ${{ secrets.SWARM_USER }} + SSH_HOST: ${{ secrets.SWARM_MANAGER_HOST }} + run: | + tar -czf /tmp/shannon-source.tar.gz \ + --exclude='.git' \ + --exclude='.venv' \ + --exclude='__pycache__' \ + --exclude='*.db' \ + --exclude='*.pyc' \ + -C . . + scp -i ~/.ssh/deploy_key /tmp/shannon-source.tar.gz "$SSH_USER@$SSH_HOST":/tmp/ + echo "โœ… Source copied" + + - name: ๐Ÿณ Build Docker Image + env: + SSH_USER: ${{ secrets.SWARM_USER }} + SSH_HOST: ${{ secrets.SWARM_MANAGER_HOST }} + run: | + COMMIT_SHA=$(git rev-parse --short HEAD) + echo "๐Ÿ“Œ Commit: $COMMIT_SHA" + + ssh -i ~/.ssh/deploy_key ${SSH_USER}@${SSH_HOST} << ENDSSH + set -e + cd /tmp + rm -rf shannon-build + mkdir -p shannon-build + cd shannon-build + tar -xzf /tmp/shannon-source.tar.gz + + echo "๐Ÿณ Building image with tag: $COMMIT_SHA" + docker build -t shannon/streamlit:$COMMIT_SHA \ + -t shannon/streamlit:latest . + + echo "๐Ÿงน Cleaning old images..." + docker image prune -f + + echo "โœ… Image built: $COMMIT_SHA" + ENDSSH + + - name: ๐Ÿš€ Deploy to Swarm + env: + SSH_USER: ${{ secrets.SWARM_USER }} + SSH_HOST: ${{ secrets.SWARM_MANAGER_HOST }} + run: | + COMMIT_SHA=$(git rev-parse --short HEAD) + + scp -i ~/.ssh/deploy_key ./docker-stack.yml ${SSH_USER}@${SSH_HOST}:/tmp/shannon-docker-stack.yml + + ssh -i ~/.ssh/deploy_key ${SSH_USER}@${SSH_HOST} << ENDSSH + set -e + + echo "๐Ÿš€ Deploying stack..." + docker stack deploy -c /tmp/shannon-docker-stack.yml shannon + + echo "๐Ÿ”„ Forcing service update to use new image..." + docker service update --force --image shannon/streamlit:$COMMIT_SHA shannon_shannon + + echo "โณ Waiting for service to update..." + sleep 15 + + echo "๐Ÿ“Š Service status:" + docker service ls --filter name=shannon_shannon + docker service ps shannon_shannon --no-trunc + + echo "โœ… Deployment complete!" + ENDSSH + + - name: ๐Ÿฅ Health Check + env: + SSH_USER: ${{ secrets.SWARM_USER }} + SSH_HOST: ${{ secrets.SWARM_MANAGER_HOST }} + run: | + ssh -i ~/.ssh/deploy_key ${SSH_USER}@${SSH_HOST} << ENDSSH + echo "๐Ÿฅ Running health check..." + + for i in 1 2 3 4 5 6 7 8 9 10 11 12; do + HTTP_CODE=\$(curl -s -o /dev/null -w "%{http_code}" https://shannon.antopoid.com/_stcore/health) + if [ "\$HTTP_CODE" = "200" ]; then + echo "โœ… Shannon is accessible (HTTP 200)!" + exit 0 + fi + echo "โณ Waiting for Shannon... (\$i/12) - Status: \$HTTP_CODE" + sleep 5 + done + + echo "โŒ Health check failed!" + echo "๐Ÿ“‹ Service logs:" + docker service logs shannon_shannon --tail 50 + exit 1 + ENDSSH + + - name: ๐Ÿงน Cleanup + run: | + rm -f ~/.ssh/deploy_key + echo "โœ… Deployment finished!" diff --git a/.streamlit/config.toml b/.streamlit/config.toml new file mode 100644 index 0000000..521e45e --- /dev/null +++ b/.streamlit/config.toml @@ -0,0 +1,21 @@ +[server] +port = 8080 +address = "0.0.0.0" +headless = true +enableCORS = false +enableXsrfProtection = false +maxUploadSize = 5 +runOnSave = false + +[browser] +gatherUsageStats = false + +[theme] +primaryColor = "#4FC3F7" +backgroundColor = "#0E1117" +secondaryBackgroundColor = "#1a1a2e" +textColor = "#E2E8F0" +font = "sans serif" + +[logger] +level = "info" diff --git a/Dockerfile b/Dockerfile index 6a745fd..87bdac7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,36 @@ -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" ] +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies (gcc for build, curl for healthcheck) +RUN apt-get update && \ + apt-get install -y --no-install-recommends gcc curl && \ + rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application +COPY .streamlit/ .streamlit/ +COPY core/ core/ +COPY views/ views/ +COPY app.py . +COPY Shannon.png . +COPY Satellite.png . + +# Create data directory for SQLite databases +RUN mkdir -p /app/data + +# Expose Streamlit port +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ + CMD curl -f http://localhost:8080/_stcore/health || exit 1 + +# Run Streamlit +ENTRYPOINT ["streamlit", "run", "app.py", \ + "--server.port=8080", \ + "--server.address=0.0.0.0", \ + "--server.headless=true"] diff --git a/Dockerfile.save b/Dockerfile.save deleted file mode 100644 index 04353fc..0000000 --- a/Dockerfile.save +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index e89619b..0000000 --- a/PySimpleGUIWeb.py +++ /dev/null @@ -1,8199 +0,0 @@ -#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 index d77a194..bbcd716 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,111 @@ -# Shannon-for-Dummies +# Shannon's Equation for Dummies -Educational application +**Educational Web Application for Satellite Communications** -Exploration from Claude's Shannon initial theory to its practical application to satellite communications. +Exploration from Claude Shannon's 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. +## ๐Ÿš€ Stack -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). +- **Frontend**: [Streamlit](https://streamlit.io) with [Plotly](https://plotly.com) interactive charts +- **Backend**: Python 3.11+ with scientific libraries (numpy, scipy, itur, astropy) +- **Database**: SQLite for community contributions +- **Deployment**: Docker + docker-compose, designed for 24/7 multi-user operation -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 ... +## ๐Ÿ“ Project Structure -The value of the application is essentially in the background information accessed by clicking labels of all inputs / outputs. +``` +. +โ”œโ”€โ”€ app.py # Main Streamlit entry point +โ”œโ”€โ”€ core/ # Core business logic +โ”‚ โ”œโ”€โ”€ calculations.py # Shannon equations & satellite link budget +โ”‚ โ”œโ”€โ”€ database.py # SQLite contribution management +โ”‚ โ””โ”€โ”€ help_texts.py # Educational help content +โ”œโ”€โ”€ views/ # UI pages +โ”‚ โ”œโ”€โ”€ theory.py # Theoretical exploration (Shannon limit) +โ”‚ โ”œโ”€โ”€ real_world.py # Real-world link budget calculator +โ”‚ โ””โ”€โ”€ contributions.py # Community knowledge database +โ”œโ”€โ”€ .streamlit/config.toml # Streamlit configuration +โ”œโ”€โ”€ Dockerfile # Container image definition +โ”œโ”€โ”€ docker-compose.yml # Orchestration with nginx-proxy +โ””โ”€โ”€ requirements.txt # Python dependencies +``` -A Knowledge DB is coupled to the application for collaborative contributions on the subject opening technical discussions. At this stage the DB is local. +## ๐Ÿ› ๏ธ Local Development + +### Prerequisites +- Python 3.11+ +- pip + +### Setup + +```bash +# Create virtual environment +python3 -m venv .venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate + +# Install dependencies +pip install -r requirements.txt + +# Run the app +streamlit run app.py +``` + +The app will open at `http://localhost:8501` + +## ๐Ÿณ Docker Deployment + +### Build and run + +```bash +docker-compose up -d +``` + +The app will be available at `http://localhost:8080` + +### Environment + +- **Port**: 8080 (configurable in `.streamlit/config.toml`) +- **Health Check**: `/_stcore/health` +- **Data Persistence**: SQLite databases stored in `shannon_data` volume + +### Production Setup + +The `docker-compose.yml` is configured for nginx-proxy: +- Network: `nginx-proxy` +- Virtual host: `shannon.antopoid.com` +- HTTPS ready (with appropriate Let's Encrypt configuration) + +## ๐Ÿ“š Features + +### 1. Theoretical Exploration +- Shannon capacity calculation (C = BW ร— logโ‚‚(1 + C/N)) +- Bandwidth and power sensitivity analysis +- Bit rate factor maps +- Interactive Plotly graphs + +### 2. Real-World Link Budget +- Complete satellite link calculation +- ITU-R atmospheric attenuation models +- Receiver noise and baseband impairments +- Practical Shannon limits with penalties + +### 3. Community Contributions +- Collaborative knowledge database +- Search and filter capabilities +- Read/Write/Delete permissions + +## ๐ŸŽ“ Educational Value + +The application provides extensive background information accessible through help expanders on every input/output, covering: +- Shannon's theorem and its implications +- Satellite communication fundamentals +- Atmospheric propagation effects +- Modulation and coding schemes +- Real-world system design trade-offs + +## ๐Ÿ“ License & Author + +ยฉ 2021-2026 ยท Shannon Equation for Dummies +Created by JPC (February 2021) +Migrated to Streamlit (2026) diff --git a/Shannon.py b/Shannon.py deleted file mode 100644 index b46244b..0000000 --- a/Shannon.py +++ /dev/null @@ -1,1028 +0,0 @@ -''' ---------------------------------------------------------------------------------------------------------------- - -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 deleted file mode 100644 index 3c43740..0000000 --- a/Shannon.py.save +++ /dev/null @@ -1,1024 +0,0 @@ -''' ---------------------------------------------------------------------------------------------------------------- - -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 deleted file mode 100644 index d8f83d9..0000000 --- a/Shannon_Dict.py +++ /dev/null @@ -1,330 +0,0 @@ - -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/app.py b/app.py new file mode 100644 index 0000000..f87e16a --- /dev/null +++ b/app.py @@ -0,0 +1,159 @@ +""" +Shannon Equation for Dummies โ€” Streamlit Application + +Main entry point with sidebar navigation. +Designed for containerized deployment (Docker), multi-user, 24/7 operation. + +Run with: + streamlit run app.py --server.port 8080 --server.address 0.0.0.0 +""" + +import streamlit as st + + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Page Configuration (must be first Streamlit call) +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +st.set_page_config( + page_title="Shannon's Equation for Dummies", + page_icon="๐Ÿ“ก", + layout="wide", + initial_sidebar_state="expanded", +) + + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Custom CSS for a modern, clean look +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +st.markdown(""" + +""", unsafe_allow_html=True) + + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Sidebar Navigation +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +with st.sidebar: + st.markdown("## ๐Ÿ“ก Shannon for Dummies") + st.caption("Educational Application โ€” AP Feb 2021") + st.divider() + + page = st.radio( + "Navigation", + options=["animation", "orbits", "sat_types", "theory", "real_world", "contributions"], + format_func=lambda x: { + "animation": "๐Ÿ“ก Satellite Link Animation", + "orbits": "๐ŸŒ GEO / MEO / LEO Orbits", + "sat_types": "๐Ÿ›ฐ๏ธ Satellite Missions & Types", + "theory": "๐Ÿงฎ Theoretical Exploration", + "real_world": "๐Ÿ›ฐ๏ธ Real World Link Budget", + "contributions": "๐Ÿ’ฌ Community Contributions", + }[x], + label_visibility="collapsed", + ) + + st.divider() + + st.markdown( + "**About**\n\n" + "Exploration from Claude Shannon's initial theory " + "to its practical application to satellite communications.\n\n" + "Built with [Streamlit](https://streamlit.io) ยท " + "[Plotly](https://plotly.com)" + ) + + st.caption("ยฉ 2021-2026 ยท Shannon Equation for Dummies") + + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Page Routing +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +if page == "animation": + from views.satellite_animation import render + render() +elif page == "orbits": + from views.orbits_animation import render + render() +elif page == "sat_types": + from views.satellite_types import render + render() +elif page == "theory": + from views.theory import render + render() +elif page == "real_world": + from views.real_world import render + render() +elif page == "contributions": + from views.contributions import render + render() diff --git a/command.txt b/command.txt deleted file mode 100644 index 6d4b06b..0000000 --- a/command.txt +++ /dev/null @@ -1,2 +0,0 @@ -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/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/__pycache__/__init__.cpython-313.pyc b/core/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0cffbef1656982ed5418b486a2662b6730135223 GIT binary patch literal 161 zcmey&%ge<81i=@lWrFC(AOZ#$p^VQgK*m&tbOudEzm*I{OhDdekkl;){m|mnqGJ8T zypsIPywrmH%#_r!%sl-9pkRJpUSf`ZKxIirex7b{Mq*xGex81Eeo?A^e0*kJW=VX! jUP0w84x8Nkl+v73yCPPgfglTuL5z>gjEsy$%s>_Z=HDo= literal 0 HcmV?d00001 diff --git a/core/__pycache__/calculations.cpython-313.pyc b/core/__pycache__/calculations.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e8aa4f4d0fb871aa88c33b43d89f00c2c6e98f9 GIT binary patch literal 9223 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 literal 0 HcmV?d00001 diff --git a/core/__pycache__/database.cpython-313.pyc b/core/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1247daf5786b5475bb8eebe9089047a78fdd0bb3 GIT binary patch literal 5297 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 float: + """Combine multiple Carrier-to-Noise Ratios (in dB) into one equivalent C/N. + + Uses the summation of normalized noise variances: + 1/CNR_total = sum(1/CNR_i) + """ + ncr_linear = 0.0 + for cnr_db in cnr_values: + ncr_linear += 10 ** (-cnr_db / 10) + return -10 * log(ncr_linear, 10) + + +def shannon_capacity(bw: float = 36.0, cnr: float = 10.0, penalty: float = 0.0) -> float: + """Shannon channel capacity (bit rate in Mbps). + + Args: + bw: Bandwidth in MHz. + cnr: Carrier-to-Noise Ratio in dB. + penalty: Implementation penalty in dB. + """ + cnr_linear = 10 ** ((cnr - penalty) / 10) + return bw * log(1 + cnr_linear, 2) + + +def br_multiplier(bw_mul: float = 1.0, p_mul: float = 2.0, cnr: float = 10.0) -> float: + """Bit Rate multiplying factor when BW and Power are scaled.""" + cnr_linear = 10 ** (cnr / 10) + return bw_mul * log(1 + cnr_linear * p_mul / bw_mul, 2) / log(1 + cnr_linear, 2) + + +def shannon_points(bw: float = 36.0, cnr: float = 10.0): + """Compute key Shannon operating points. + + Returns: + (cnr_linear, br_infinity, c_n0, br_constrained) + """ + cnr_linear = 10 ** (cnr / 10) + c_n0 = cnr_linear * bw + br_infinity = c_n0 / log(2) + br_constrained = shannon_capacity(bw, cnr) + return cnr_linear, br_infinity, c_n0, br_constrained + + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Satellite Link Budget Calculations +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def compute_satellite_link( + freq_ghz: float, + hpa_power_w: float, + sat_loss_db: float, + sat_cir_list: list[float], + sat_beam_deg: float, + gain_offset_db: float, + sat_alt_km: float, + sat_lat: float, + sat_lon: float, + gs_lat: float, + gs_lon: float, + availability_pct: float, +) -> dict: + """Compute all satellite link parameters. + + Returns a dict with all computed values. + """ + import itur + + R_EARTH = 6378 # km + SAT_ANT_EFF = 0.65 + + # Signal power after losses + sig_power = hpa_power_w * 10 ** (-sat_loss_db / 10) + + # Satellite antenna + wavelength = 300e6 / freq_ghz / 1e9 # meters + sat_gain_linear = SAT_ANT_EFF * (pi * 70 / sat_beam_deg) ** 2 + sat_gain_linear *= 10 ** (-gain_offset_db / 10) + + # EIRP + eirp_linear = sig_power * sat_gain_linear + + # Satellite C/I + sat_cir = combine_cnr(*sat_cir_list) + + # Path geometry + path_length = sqrt( + sat_alt_km ** 2 + + 2 * R_EARTH * (R_EARTH + sat_alt_km) + * (1 - cos(np.radians(sat_lat - gs_lat)) * cos(np.radians(sat_lon - gs_lon))) + ) + + phi = acos(cos(np.radians(sat_lat - gs_lat)) * cos(np.radians(sat_lon - gs_lon))) + if phi > 0: + elevation = float(np.degrees( + atan((cos(phi) - R_EARTH / (R_EARTH + sat_alt_km)) / sqrt(1 - cos(phi) ** 2)) + )) + else: + elevation = 90.0 + + # Atmospheric attenuation + if elevation <= 0: + atm_loss_db = 999.0 + else: + atm_loss_db = float( + itur.atmospheric_attenuation_slant_path( + gs_lat, gs_lon, freq_ghz, elevation, 100 - availability_pct, 1 + ).value + ) + + # Path dispersion + path_loss_linear = 4 * pi * (path_length * 1000) ** 2 + free_space_loss_linear = (4 * pi * path_length * 1000 / wavelength) ** 2 + + # PFD + pfd_linear = eirp_linear / path_loss_linear * 10 ** (-atm_loss_db / 10) + + return { + "sig_power": sig_power, + "wavelength": wavelength, + "sat_gain_linear": sat_gain_linear, + "eirp_linear": eirp_linear, + "sat_cir": sat_cir, + "path_length": path_length, + "elevation": elevation, + "atm_loss_db": atm_loss_db, + "path_loss_linear": path_loss_linear, + "free_space_loss_linear": free_space_loss_linear, + "pfd_linear": pfd_linear, + } + + +def compute_receiver( + pfd_linear: float, + atm_loss_db: float, + wavelength: float, + cpe_ant_d: float, + cpe_t_clear: float, +) -> dict: + """Compute receiver-side parameters.""" + CPE_ANT_EFF = 0.6 + K_BOLTZ = 1.38e-23 # J/K + + cpe_t_att = (cpe_t_clear - 40) + 40 * 10 ** (-atm_loss_db / 10) + 290 * (1 - 10 ** (-atm_loss_db / 10)) + + cpe_ae = pi * cpe_ant_d ** 2 / 4 * CPE_ANT_EFF + cpe_gain_linear = (pi * cpe_ant_d / wavelength) ** 2 * CPE_ANT_EFF + cpe_g_t = 10 * log(cpe_gain_linear / cpe_t_att, 10) + + rx_power = pfd_linear * cpe_ae + n0 = K_BOLTZ * cpe_t_att + c_n0_hz = rx_power / n0 + c_n0_mhz = c_n0_hz / 1e6 + + br_infinity = c_n0_mhz / log(2) + + # Spectral efficiency points + bw_spe_1 = c_n0_mhz + bw_spe_double = c_n0_mhz / (2 ** 2 - 1) + + br_spe_1 = bw_spe_1 + br_spe_double = bw_spe_double * 2 + + return { + "cpe_ae": cpe_ae, + "cpe_gain_linear": cpe_gain_linear, + "cpe_g_t": cpe_g_t, + "cpe_t_att": cpe_t_att, + "rx_power": rx_power, + "n0": n0, + "c_n0_hz": c_n0_hz, + "c_n0_mhz": c_n0_mhz, + "br_infinity": br_infinity, + "bw_spe_1": bw_spe_1, + "br_spe_1": br_spe_1, + "br_spe_double": br_spe_double, + } + + +def compute_baseband( + c_n0_mhz: float, + br_infinity: float, + bw_spe_1: float, + sat_cir: float, + bandwidth: float, + rolloff: float, + overheads: float, + cnr_imp_list: list[float], + penalties: float, +) -> dict: + """Compute baseband processing results.""" + cnr_imp = combine_cnr(*cnr_imp_list) + + cnr_spe_1 = 0.0 # dB + cnr_bw = cnr_spe_1 + 10 * log(bw_spe_1 / bandwidth, 10) + bw_nyq = bandwidth / (1 + rolloff / 100) + cnr_nyq = cnr_spe_1 + 10 * log(bw_spe_1 / bw_nyq, 10) + cnr_rcv = combine_cnr(cnr_nyq, cnr_imp, sat_cir) + + br_nyq = shannon_capacity(bw_nyq, cnr_nyq) + br_rcv = shannon_capacity(bw_nyq, cnr_rcv, penalties) + br_rcv_higher = br_rcv / (1 + overheads / 100) + + spe_nyq = br_nyq / bandwidth + bits_per_symbol = br_nyq / bw_nyq + spe_rcv = br_rcv / bandwidth + spe_higher = br_rcv_higher / bandwidth + + return { + "cnr_bw": cnr_bw, + "cnr_nyq": cnr_nyq, + "cnr_rcv": cnr_rcv, + "cnr_imp": cnr_imp, + "bw_nyq": bw_nyq, + "br_nyq": br_nyq, + "br_rcv": br_rcv, + "br_rcv_higher": br_rcv_higher, + "br_nyq_norm": br_nyq / br_infinity, + "br_rcv_norm": br_rcv / br_infinity, + "br_rcv_h_norm": br_rcv_higher / br_infinity, + "spe_nyq": spe_nyq, + "bits_per_symbol": bits_per_symbol, + "spe_rcv": spe_rcv, + "spe_higher": spe_higher, + "bandwidth": bandwidth, + "rolloff": rolloff, + "overheads": overheads, + "penalties": penalties, + } + + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Formatting Helpers +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def fmt_br(br: float) -> str: + return f"{br:.1f} Mbps" + + +def fmt_power(p: float) -> str: + p_db = 10 * log(p, 10) + if 1 < p < 1e4: + return f"{p:.1f} W ยท {p_db:.1f} dBW" + elif 1e-3 < p <= 1: + return f"{p:.4f} W ยท {p_db:.1f} dBW" + else: + return f"{p:.1e} W ยท {p_db:.1f} dBW" + + +def fmt_pfd(p: float) -> str: + p_db = 10 * log(p, 10) + return f"{p:.1e} W/mยฒ ยท {p_db:.1f} dBW/mยฒ" + + +def fmt_psd(p: float) -> str: + p_db = 10 * log(p, 10) + return f"{p:.1e} W/MHz ยท {p_db:.1f} dBW/MHz" + + +def fmt_gain(g: float) -> str: + g_db = 10 * log(g, 10) + return f"{g:.1f} ยท {g_db:.1f} dBi" + + +def fmt_ploss(loss: float) -> str: + loss_db = 10 * log(loss, 10) + return f"{loss:.2e} mยฒ ยท {loss_db:.1f} dBmยฒ" diff --git a/core/database.py b/core/database.py new file mode 100644 index 0000000..a6c5e6c --- /dev/null +++ b/core/database.py @@ -0,0 +1,107 @@ +""" +Database module for managing user contributions. + +Uses parameterized queries (no SQL injection) and context managers. +""" + +import sqlite3 +import os +from datetime import datetime + + +DB_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data") + + +def _get_db_path(db_name: str) -> str: + os.makedirs(DB_DIR, exist_ok=True) + return os.path.join(DB_DIR, db_name) + + +def _init_db(db_path: str): + with sqlite3.connect(db_path) as conn: + conn.execute( + """CREATE TABLE IF NOT EXISTS contributions ( + num INTEGER PRIMARY KEY, + name TEXT NOT NULL, + title TEXT NOT NULL, + keywords TEXT, + text TEXT NOT NULL, + date TEXT NOT NULL, + password TEXT DEFAULT '' + )""" + ) + + +def write_contribution( + db_name: str, name: str, title: str, keywords: str, text: str, password: str = "" +) -> int: + """Write a new contribution. Returns the new contribution ID.""" + db_path = _get_db_path(db_name) + _init_db(db_path) + + with sqlite3.connect(db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT COALESCE(MAX(num), 0) FROM contributions") + next_id = cursor.fetchone()[0] + 1 + + cursor.execute( + "INSERT INTO contributions (num, name, title, keywords, text, date, password) " + "VALUES (?, ?, ?, ?, ?, ?, ?)", + (next_id, name, title, keywords, text, datetime.now().strftime("%Y-%m-%d"), password), + ) + return next_id + + +def search_contributions( + db_name: str, + name_filter: str = "", + title_filter: str = "", + keywords_filter: str = "", + content_filter: str = "", + limit: int = 50, +) -> list[dict]: + """Search contributions with optional filters. Returns list of dicts.""" + db_path = _get_db_path(db_name) + if not os.path.isfile(db_path): + return [] + + _init_db(db_path) + + with sqlite3.connect(db_path) as conn: + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute( + """SELECT num, name, title, keywords, text, date, password + FROM contributions + WHERE name LIKE ? AND title LIKE ? AND keywords LIKE ? AND text LIKE ? + ORDER BY num DESC + LIMIT ?""", + ( + f"%{name_filter}%", + f"%{title_filter}%", + f"%{keywords_filter}%", + f"%{content_filter}%", + limit, + ), + ) + return [dict(row) for row in cursor.fetchall()] + + +def delete_contribution(db_name: str, num: int, password: str) -> bool: + """Delete a contribution if the password matches. Returns True on success.""" + db_path = _get_db_path(db_name) + if not os.path.isfile(db_path): + return False + + with sqlite3.connect(db_path) as conn: + cursor = conn.cursor() + cursor.execute( + "SELECT password FROM contributions WHERE num = ?", (num,) + ) + row = cursor.fetchone() + if row is None: + return False + if row[0] != password: + return False + cursor.execute("DELETE FROM contributions WHERE num = ?", (num,)) + return True diff --git a/core/help_texts.py b/core/help_texts.py new file mode 100644 index 0000000..cf630b2 --- /dev/null +++ b/core/help_texts.py @@ -0,0 +1,297 @@ +""" +Help texts for Shannon application. + +Cleaned-up version of Shannon_Dict.py, organized by section. +Unicode subscripts/superscripts replaced by readable equivalents for web display. +""" + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Panel 1: Theoretical Exploration +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +THEORY_HELP = { + "cnr": ( + "**Reference C/N [dB]**\n\n" + "Reference 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 measured in the reference " + "Channel Bandwidth.\n\n" + "The Carrier's power is often named Signal's Power and the Carrier to Noise Ratio " + "is often named Signal to Noise Ratio." + ), + "bw": ( + "**Reference BW [MHz]**\n\n" + "Reference Channel Bandwidth โ€” a key parameter of the communication channel.\n\n" + "This bandwidth is usually a degree of freedom of the system design, eventually " + "constrained by technological constraints and various kinds of frequency usage regulations." + ), + "c_n0": ( + "**Carrier Power to Noise Power Density Ratio: C/Nโ‚€**\n\n" + "Carrier's power (in Watts) divided by the Noise Spectral Power Density " + "(in Watts per MHz). The result's units are MHz." + ), + "br_inf": ( + "**Theoretical BR at infinite BW: 1.44ยทC/Nโ‚€**\n\n" + "Bit Rate theoretically achievable when the signal occupies an infinite Bandwidth. " + "This value is a useful asymptotic limit." + ), + "br_unit": ( + "**Theoretical BR at Spectral Efficiency = 1: C/Nโ‚€**\n\n" + "Bit Rate theoretically achievable at a Spectral Efficiency = 1: " + "Bit Rate in Mbps = Bandwidth in MHz.\n\n" + "The corresponding value, deduced from Shannon's formula, is given by C/Nโ‚€." + ), + "br_bw": ( + "**Theoretical BR at Reference (BW, C/N)**\n\n" + "Bit Rate theoretically achievable when the Bandwidth is constrained " + "to the given reference value." + ), + "cnr_lin": ( + "**C/N = C / (Nโ‚€ยทB)**\n\n" + "Reference Carrier to Noise Ratio (or Signal to Noise Ratio). " + "The C/N Ratio is usually given in dB: 10ยทlog(C/N).\n\n" + "Although the logarithm is convenient for many evaluations (multiplications become additions), " + "it's also good to consider the ratio itself (Linear Format) to get some physical sense " + "of the power ratio.\n\n" + "The Carrier to Noise Ratio in linear format is the value used in Shannon's formula." + ), + "br_mul": ( + "**Bit Rate Increase Factor**\n\n" + "Bit Rate multiplying factor achieved when the Bandwidth and the Power " + "are multiplied by a given set of values." + ), + "bw_mul": ( + "**BW Increase Factor**\n\n" + "Arbitrary multiplying factor applied to the Carrier's Bandwidth, for sensitivity analysis." + ), + "p_mul": ( + "**Power Increase Factor**\n\n" + "Arbitrary multiplying factor applied to the Carrier's Power, for sensitivity analysis." + ), + "shannon": ( + "**The Shannon Limit** allows to evaluate the theoretical capacity achievable over " + "a communication channel.\n\n" + "As a true genius, Claude Shannon founded communication theory, information theory " + "and more (click the Wikipedia button for more info).\n\n" + "This equation is fundamental for the evaluation of communication systems. " + "It is an apparently simple but extremely powerful tool to guide communication systems' designs.\n\n" + "This 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\n" + "In the satellite domain, DVB-S2x, using LDPC codes iteratively decoded (Turbo-Like), " + "is only 1 dB away from this limit." + ), + "advanced": ( + "**AWGN Channel Model**\n\n" + "The model assumes that the communication channel is **AWGN** (Additive White Gaussian Noise). " + "This noise is supposed to be random and white, meaning noise at a given time is independent " + "of noise at any other time โ€” implying a flat and infinite spectrum. " + "This noise is also supposed to be Gaussian.\n\n" + "Although these assumptions seem very strong, they accurately match the cases of interest. " + "Many impairments are actually non-linear and/or non-additive, but combining equivalent C/N " + "of all impairments as if they were fully AWGN is in most cases very accurate. " + "The reason is that the sum of random variables of unknown laws always tends to Gaussian, " + "and thermal noise is dominating, actually white and gaussian, and whitening the rest.\n\n" + "The tool accepts lists of comma-separated CNRs which will be combined in that way.\n\n" + "Overall, the Shannon Limit is a pretty convenient tool to predict the real performances " + "of communication systems." + ), + "help": ( + "**Recommendations for using the tool**\n\n" + "The first purpose of the tool is educational, allowing people to better understand " + "the physics of communications and the role of key parameters.\n\n" + "- Try multiple values in all the fields one by one\n" + "- Explore the graphs and try to understand the underlying physics\n" + "- The units are as explicit as possible to facilitate the exploration\n" + "- Click on the โ„น๏ธ icons to get information about each parameter" + ), +} + + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Panel 2: Real World (Satellite Link Budget) +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +REAL_WORLD_HELP = { + "freq": ( + "**Frequency [GHz]**\n\n" + "Frequency of the electromagnetic wave supporting the communication.\n\n" + "For satellite downlink, typical bands: L: 1.5 GHz, S: 2.2 GHz, C: 4 GHz, " + "Ku: 12 GHz, Ka: 19 GHz, Q: 40 GHz" + ), + "sat_alt": ( + "**Satellite Altitude [km]**\n\n" + "The position of the satellite is expressed in latitude, longitude, altitude. " + "A GEO satellite has a latitude of 0ยฐ and an altitude of 35786 km. " + "LEO satellites have altitudes lower than 2000 km. " + "MEO altitudes are between LEO and GEO (O3B constellation: 8063 km)." + ), + "sat_latlon": ( + "**Satellite Latitude and Longitude [ยฐ]**\n\n" + "The program doesn't simulate the orbit โ€” any satellite coordinates can be used. " + "A GEO satellite has a latitude of 0ยฐ." + ), + "gs_latlon": ( + "**Ground Station Lat, Lon [ยฐ]**\n\n" + "The position of the ground station affects link availability due to weather statistics " + "(tropical regions have very heavy rains attenuating signals at high frequencies). " + "It also impacts the elevation angle and overall path length.\n\n" + "๐Ÿ”— [Find coordinates](https://www.gps-coordinates.net)" + ), + "availability": ( + "**Link Availability [%]**\n\n" + "A high desired link availability corresponds to high signal attenuation: " + "only rare and severe weather events exceeding this attenuation can interrupt the link.\n\n" + "For example, at 99.9% availability, the attenuation considered is only exceeded 0.1% of the time." + ), + "path_length": ( + "**Path Length [km] @ Elevation [ยฐ]**\n\n" + "Distance from the satellite to the ground station and elevation angle. " + "Minimum path length = satellite altitude (elevation = 90ยฐ). " + "A negative elevation means the satellite is not visible." + ), + "atm_loss": ( + "**Atmospheric Attenuation [dB]**\n\n" + "The atmosphere affects radio wave propagation with attenuation from rain, clouds, " + "scintillation, multi-path, sand/dust storms, and atmospheric gases.\n\n" + "Typical attenuation exceeded 0.1% of the time in Europe from GEO:\n" + "- Ku: 2.5 dB\n- Ka: 6.9 dB\n- Q: 22 dB\n\n" + "Uses [ITU-Rpy](https://itu-rpy.readthedocs.io/en/latest/index.html)." + ), + "hpa": ( + "**HPA Output Power [W]**\n\n" + "Power of the High Power Amplifier at the satellite's last amplification stage. " + "Some satellites operate at saturation (DTH), others in multicarrier mode with " + "reduced power (3 dB Output Back Off is typical for VSAT)." + ), + "sat_beam": ( + "**Satellite Beam Diameter [ยฐ]**\n\n" + "Half-power beam width. Typical values: 0.4โ€“1.4ยฐ for GEO HTS, 3โ€“6ยฐ for GEO DTH." + ), + "gain_offset": ( + "**Gain Offset from Peak [dB]**\n\n" + "Simulates terminals not at beam peak. 3 dB = worst case in 3 dB beam (DTH). " + "1 dB = typical median performance for single feed per beam HTS." + ), + "losses": ( + "**Output Section Losses [dB]**\n\n" + "Signal loss between HPA and antenna (filters, waveguides, switches). " + "Typical: 2.5 dB for classical satellites, 1 dB for active antennas." + ), + "sat_cir": ( + "**Satellite C/I [dB]**\n\n" + "Signal impairments from satellite implementation: intermodulation, filtering, phase noise. " + "Supports comma-separated lists to include uplink C/N, interferences, etc." + ), + "output_power": "**Output Power [W]** โ€” Signal power at antenna output carrying user information.", + "sat_gain": ( + "**Satellite Antenna Gain**\n\n" + "Ratio between signal radiated on-axis vs. isotropic antenna. " + "Expressed in dBi (dB relative to isotropic antenna)." + ), + "eirp": ( + "**Equivalent Isotropic Radiated Power (EIRP)**\n\n" + "Product Power ร— Gain in Watts. Represents the power required for an isotropic antenna " + "to match the directive antenna's radiation in that direction." + ), + "path_loss": ( + "**Path Dispersion Loss**\n\n" + "Free-space propagation loss โ€” simply the surface of a sphere with radius = path length. " + "Not actual absorption, just geometric spreading." + ), + "pfd": ( + "**Power Flux Density**\n\n" + "Signal power per square meter at the terminal side. " + "Actual captured power = PFD ร— antenna effective area." + ), + "cpe_ant": ( + "**Customer Antenna Size [m]**\n\n" + "Parabolic antenna with state-of-the-art efficiency (~60%)." + ), + "cpe_temp": ( + "**Noise Temperature [K]**\n\n" + "Total receiver's clear-sky noise temperature. Includes all contributors: " + "receiver, sky, ground seen by antenna. Default of 120 K is a reasonable typical value." + ), + "cpe_gain": ( + "**Antenna Effective Area and G/T**\n\n" + "G/T is the figure of merit of a receive antenna: Gain / Noise Temperature. " + "In case of rain, the signal is punished twice: attenuation weakens it and " + "the rain attenuator generates additional noise." + ), + "rx_power": "**RX Power** โ€” Extremely small power before amplification. This is 'C' in Shannon's equation.", + "n0": "**Noise Power Density Nโ‚€** โ€” Noise Spectral Power Density of the radio front end.", + "br_inf": ( + "**Bit Rate at infinite BW** โ€” Asymptotic limit: 1.443ยทC/Nโ‚€. Never achieved in practice." + ), + "br_unit": "**Bit Rate at Spectral Efficiency=1** โ€” Bandwidth = Bit Rate and C/N = 0 dB.", + "br_double": ( + "**Bit Rate at Spectral Efficiency=2** โ€” Bandwidth-efficient operating point " + "(BW = 0.5 ร— BR), often considered typical." + ), + "bandwidth": ( + "**Occupied Bandwidth [MHz]**\n\n" + "Bandwidth occupied by the communication channel โ€” a key degree of freedom in system design." + ), + "rolloff": ( + "**Nyquist Filter Rolloff [%]**\n\n" + "Excess bandwidth required beyond the theoretical Nyquist minimum. " + "The Nyquist filter avoids inter-symbol interference." + ), + "cir": ( + "**Receiver C/I [dB]**\n\n" + "Signal impairments from terminal: phase noise, quantization, synchronization errors. " + "Supports comma-separated lists." + ), + "penalty": ( + "**Implementation Penalty [dB]**\n\n" + "DVB-S2x with LDPC codes is typically 1 dB from Shannon Limit. " + "With 0.5 dB margin, a total of 1.5 dB is typical." + ), + "overhead": ( + "**Higher Layers Overhead [%]**\n\n" + "Encapsulation cost (IP over DVB-S2x via GSE). Typically ~5% for modern systems." + ), + "cnr_bw": "**SNR in Available BW** โ€” Signal-to-Noise Ratio in the available bandwidth.", + "cnr_nyq": "**SNR in Nyquist BW** โ€” SNR in Nyquist BW = Available BW / (1 + Roll-Off).", + "cnr_rcv": ( + "**SNR at Receiver Output** โ€” Combining link noise, satellite C/I, and receiver C/I. " + "This is the relevant ratio for real-life performance." + ), + "br_nyq": ( + "**Theoretical Bit Rate** โ€” Direct application of Shannon Limit in Nyquist BW. " + "Efficiency in bits/symbol also shown." + ), + "br_rcv": ( + "**Practical Physical Layer Bit Rate** โ€” Using all-degradations-included SNR " + "in Shannon's formula." + ), + "br_high": ( + "**Practical Higher Layers Bit Rate** โ€” Corresponds to user bits of IP datagrams." + ), + "satellite": ( + "The evaluation is decomposed in 3 sections:\n\n" + "1. **Satellite Link** โ€” transmitter and path to receiver with key characteristics\n" + "2. **Radio Front End** โ€” antenna and amplification capturing signal with minimal noise\n" + "3. **Baseband Unit** โ€” digital signal processing: filtering, synchronization, " + "demodulation, error correction, decapsulation\n\n" + "All fields are initially filled with meaningful values. Start by changing the " + "straightforward parameters. All parameters have help tooltips." + ), + "advanced": ( + "**Advanced Analysis Notes**\n\n" + "All capacity evaluations use direct application of the Shannon formula with real-world " + "impairments via C/N combinations. Useful links:\n\n" + "- [Nyquist ISI Criterion](https://en.wikipedia.org/wiki/Nyquist_ISI_criterion)\n" + "- [Error Correction Codes](https://en.wikipedia.org/wiki/Error_correction_code)\n" + "- [Viterbi Decoder](https://en.wikipedia.org/wiki/Viterbi_decoder)\n" + "- [Turbo Codes](https://en.wikipedia.org/wiki/Turbo_code)\n" + "- [DVB-S2](https://en.wikipedia.org/wiki/DVB-S2)\n" + "- [OSI Model](https://en.wikipedia.org/wiki/OSI_model)" + ), + "help": ( + "**Recommendations**\n\n" + "- Try multiple values one by one, starting from the least intimidating\n" + "- Explore the graphs to understand the physics\n" + "- Units are as explicit as possible\n" + "- The tool can also be used for a quick first-order link analysis" + ), +} diff --git a/docker-compose.yml b/docker-compose.yml index c1fef2b..b1750ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,18 +1,36 @@ -version: '3.1' +version: '3.8' + 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 + shannon: + deploy: + labels: + - traefik.enable=true + - traefik.http.routers.shannon.rule=Host(`shannon.antopoid.com`) + - traefik.http.routers.shannon.entrypoints=websecure + - traefik.http.routers.shannon.tls.certresolver=myhttpchallenge + - traefik.http.services.shannon.loadbalancer.server.port=8080 + build: + context: . + dockerfile: Dockerfile + container_name: shannon-streamlit + restart: always + expose: + - 8080 + volumes: + # Persist SQLite databases across container restarts + - shannon_data:/app/data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/_stcore/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +volumes: + shannon_data: + driver: local networks: - default: - external: - name: nginx-proxy - + default: + external: + name: nginx-proxy diff --git a/docker-stack.yml b/docker-stack.yml new file mode 100644 index 0000000..7e40fd8 --- /dev/null +++ b/docker-stack.yml @@ -0,0 +1,43 @@ +# Docker Stack pour dรฉploiement en Swarm +# Compatible avec CI/CD Gitea Actions +version: '3.8' + +services: + shannon: + image: shannon/streamlit:${IMAGE_TAG:-latest} + expose: + - 8080 + environment: + - STREAMLIT_SERVER_HEADLESS=true + volumes: + - shannon_data:/app/data + networks: + - traefik_traefikfront + deploy: + labels: + - traefik.enable=true + - traefik.http.routers.shannon.rule=Host(`shannon.antopoid.com`) + - traefik.http.routers.shannon.entrypoints=websecure + - traefik.http.routers.shannon.tls.certresolver=myhttpchallenge + - traefik.http.services.shannon.loadbalancer.server.port=8080 + - traefik.http.middlewares.shannon-proxy-headers.headers.customrequestheaders.X-Forwarded-For= + - traefik.http.routers.shannon.middlewares=shannon-proxy-headers + - traefik.http.services.shannon.loadbalancer.passhostheader=true + placement: + constraints: + - "node.hostname==macmini" + replicas: 1 + restart_policy: + condition: on-failure + update_config: + parallelism: 1 + delay: 10s + order: start-first + +volumes: + shannon_data: + driver: local + +networks: + traefik_traefikfront: + external: true diff --git a/requirements.txt b/requirements.txt index 7b2feaa..f4dab0f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,25 +1,19 @@ -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 - +# Core scientific dependencies +numpy>=1.25 +scipy>=1.11 +matplotlib>=3.7 +plotly>=5.18 + +# Streamlit +streamlit>=1.30 + +# Satellite propagation model +itur>=0.3.3 + +# Astronomy utilities +astropy>=5.3 + +# Geospatial +pyproj>=3.6 + +# Database (built-in sqlite3, no extra dep needed) diff --git a/slim.report.json b/slim.report.json deleted file mode 100644 index 00ef60b..0000000 --- a/slim.report.json +++ /dev/null @@ -1,505 +0,0 @@ -{ - "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" -} diff --git a/views/__init__.py b/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/views/__pycache__/__init__.cpython-313.pyc b/views/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9108dabb525d75b60aab90228ea5187ae51b4ff9 GIT binary patch literal 162 zcmey&%ge<81lKQ4%LLJnK?DpiLK&Y~fQ+dO=?t2Tek&P@n1H;`AgNo9`k}?CMaBAw zc_sOod8q~YnJKAdnR)sJK*9XHyu=*+fXb4L{5;*@jKsXWe4s>PdTOzLe0*kJW=VX! jUP0w84x8Nkl+v73yCPPgi69$`L5z>gjEsy$%s>_ZXOk(M literal 0 HcmV?d00001 diff --git a/views/__pycache__/contributions.cpython-313.pyc b/views/__pycache__/contributions.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1cfc105272c4fa1b268931bfb614977e2f92e50b GIT binary patch literal 6818 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 literal 0 HcmV?d00001 diff --git a/views/__pycache__/orbits_animation.cpython-313.pyc b/views/__pycache__/orbits_animation.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5f1459b3058608ef9d1c31d59d6731a1dbaff6b GIT binary patch literal 20526 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 literal 0 HcmV?d00001 diff --git a/views/__pycache__/real_world.cpython-313.pyc b/views/__pycache__/real_world.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16450ec13c5a4482f700548ca7a951fcfedf98e1 GIT binary patch literal 21123 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 literal 0 HcmV?d00001 diff --git a/views/__pycache__/theory.cpython-313.pyc b/views/__pycache__/theory.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ffbbee3060324bbe4307792467c4176babb4cb7d GIT binary patch literal 12118 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 + + + + + + + + +
+

๐Ÿ›ฐ๏ธ Orbit Comparison

+
+

+ Hover over a satellite to see its details. +

+
+
+ +
+ + +
+ SlowFast +
+
+ +
+

๐Ÿ“ Orbit Types

+
+ + LEO + 160 โ€“ 2 000 km +
+
+ + MEO + 2 000 โ€“ 35 786 km +
+
+ + GEO + 35 786 km (fixed) +
+
+ + + + +""" + + +def render(): + """Render the GEO/MEO/LEO orbit comparison animation.""" + st.markdown("## ๐ŸŒ Satellite Orbits โ€” GEO vs MEO vs LEO") + st.markdown( + "Compare the three main satellite orbit types. " + "Hover over satellites or legend items to explore their characteristics." + ) + st.divider() + + components.html(_ORBITS_HTML, height=680, scrolling=False) + + # โ”€โ”€ Educational content below โ”€โ”€ + st.divider() + st.markdown("### ๐Ÿ“Š Orbit Comparison at a Glance") + + col1, col2, col3 = st.columns(3) + + with col1: + st.markdown(""" +#### ๐ŸŸข LEO โ€” Low Earth Orbit +**160 โ€“ 2 000 km** + +- โšก Latency: **4 โ€“ 20 ms** (RTT) +- ๐Ÿ“‰ FSPL: ~155 dB (Ku-band) +- ๐Ÿ”„ Period: ~90 โ€“ 120 min +- ๐Ÿ“ก Small footprint โ†’ **large constellations** needed (hundreds to thousands) +- ๐Ÿค Requires **handover** between satellites +- ๐Ÿ›ฐ๏ธ *Starlink (550 km), OneWeb (1 200 km), Iridium (780 km)* +""") + + with col2: + st.markdown(""" +#### ๐ŸŸก MEO โ€” Medium Earth Orbit +**2 000 โ€“ 35 786 km** + +- โšก Latency: **40 โ€“ 80 ms** (RTT) +- ๐Ÿ“‰ FSPL: ~186 dB (Ku-band) +- ๐Ÿ”„ Period: ~2 โ€“ 12 h +- ๐Ÿ“ก Medium footprint โ†’ **medium constellations** (20 โ€“ 50 sats) +- ๐ŸŒ Good balance between coverage and latency +- ๐Ÿ›ฐ๏ธ *GPS (20 200 km), Galileo, O3b/SES (8 000 km)* +""") + + with col3: + st.markdown(""" +#### ๐Ÿ”ด GEO โ€” Geostationary Orbit +**35 786 km (fixed)** + +- โšก Latency: **240 โ€“ 280 ms** (RTT) +- ๐Ÿ“‰ FSPL: ~205 dB (Ku-band) +- ๐Ÿ”„ Period: 23 h 56 min (= 1 sidereal day) +- ๐Ÿ“ก Huge footprint โ†’ **3 sats = global** coverage +- ๐Ÿ“Œ **Fixed position** in the sky โ€” no tracking needed +- ๐Ÿ›ฐ๏ธ *Intelsat, SES, Eutelsat, ViaSat* +""") + + with st.expander("๐Ÿ”ฌ Key Trade-offs in Detail"): + st.markdown(r""" +| Parameter | LEO | MEO | GEO | +|:---|:---:|:---:|:---:| +| **Altitude** | 160 โ€“ 2 000 km | 2 000 โ€“ 35 786 km | 35 786 km | +| **Round-trip latency** | 4 โ€“ 20 ms | 40 โ€“ 80 ms | 240 โ€“ 280 ms | +| **Free-Space Path Loss** | ~155 dB | ~186 dB | ~205 dB | +| **Orbital period** | 90 โ€“ 120 min | 2 โ€“ 12 h | 23h 56m | +| **Coverage per sat** | ~1 000 km | ~12 000 km | ~15 000 km | +| **Constellation size** | Hundreds โ€“ thousands | 20 โ€“ 50 | 3 | +| **Antenna tracking** | Required (fast) | Required (slow) | Fixed dish | +| **Doppler shift** | High | Moderate | Negligible | +| **Launch cost/sat** | Lower | Medium | Higher | +| **Orbital lifetime** | 5 โ€“ 7 years | 10 โ€“ 15 years | 15+ years | + +**FSPL formula:** $\text{FSPL (dB)} = 20 \log_{10}(d) + 20 \log_{10}(f) + 32.44$ + +Where $d$ is distance in km and $f$ is frequency in MHz. + +**Why it matters for Shannon:** +The higher the orbit, the greater the FSPL, the lower the received C/N. +Since $C = B \log_2(1 + C/N)$, a higher orbit means lower achievable bit rate +for the same bandwidth and transmit power. LEO compensates with lower FSPL +but requires more satellites and complex handover. +""") + + with st.expander("๐Ÿ›ฐ๏ธ Notable Constellations"): + st.markdown(""" +| Constellation | Orbit | Altitude | # Satellites | Use Case | +|:---|:---:|:---:|:---:|:---| +| **Starlink** | LEO | 550 km | ~6 000+ | Broadband Internet | +| **OneWeb** | LEO | 1 200 km | ~650 | Enterprise connectivity | +| **Iridium NEXT** | LEO | 780 km | 66 + spares | Global voice/data | +| **O3b mPOWER** | MEO | 8 000 km | 11+ | Managed connectivity | +| **GPS** | MEO | 20 200 km | 31 | Navigation | +| **Galileo** | MEO | 23 222 km | 30 | Navigation | +| **Intelsat** | GEO | 35 786 km | 50+ | Video & enterprise | +| **SES** | GEO + MEO | Mixed | 70+ | Video & data | +| **Eutelsat** | GEO | 35 786 km | 35+ | Video broadcasting | +""") diff --git a/views/real_world.py b/views/real_world.py new file mode 100644 index 0000000..52465d8 --- /dev/null +++ b/views/real_world.py @@ -0,0 +1,412 @@ +""" +Page 2: Shannon and Friends in the Real World + +Complete satellite link budget with interactive Plotly graphs. +""" + +import streamlit as st +import numpy as np +import plotly.graph_objects as go +from math import log + +from core.calculations import ( + combine_cnr, + shannon_capacity, + compute_satellite_link, + compute_receiver, + compute_baseband, + fmt_br, + fmt_power, + fmt_gain, + fmt_pfd, + fmt_psd, + fmt_ploss, +) +from core.help_texts import REAL_WORLD_HELP + + +def _make_bw_sensitivity_real( + cnr_nyq, bandwidth, rolloff, overheads, penalties, cnr_imp, sat_cir, hpa_power +) -> go.Figure: + n = 40 + bw = np.zeros(n) + br = np.zeros(n) + cnr = np.zeros(n) + cnr[0] = cnr_nyq + 10 * log(8, 10) + bw[0] = bandwidth / 8 + cnr_rcv_0 = combine_cnr(cnr[0], cnr_imp, sat_cir) + br[0] = shannon_capacity(bw[0] / (1 + rolloff / 100), cnr_rcv_0, penalties) / (1 + overheads / 100) + for i in range(1, n): + bw[i] = bw[i - 1] * 2 ** (1 / 6) + 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_capacity(bw[i] / (1 + rolloff / 100), cnr_rcv_i, penalties) / (1 + overheads / 100) + + fig = go.Figure() + fig.add_trace(go.Scatter( + x=bw, y=br, mode="lines", + name="Higher Layers BR", + line=dict(color="#4FC3F7", width=3), + )) + + # Reference point + cnr_rcv_ref = combine_cnr(cnr_nyq, cnr_imp, sat_cir) + br_ref = shannon_capacity(bandwidth / (1 + rolloff / 100), cnr_rcv_ref, penalties) / (1 + overheads / 100) + fig.add_trace(go.Scatter( + x=[bandwidth], y=[br_ref], mode="markers+text", + name=f"Ref: {bandwidth:.0f} MHz, {br_ref:.1f} Mbps", + marker=dict(size=12, color="#FF7043", symbol="diamond"), + text=[f"{br_ref:.1f} Mbps"], + textposition="top center", + )) + + fig.update_layout( + title=f"Higher Layers Bit Rate at Constant HPA Power: {hpa_power:.1f} W", + xaxis_title="Occupied Bandwidth [MHz]", + yaxis_title="Bit Rate [Mbps]", + template="plotly_dark", + height=500, + ) + return fig + + +def _make_power_sensitivity_real( + cnr_nyq, bw_nyq, hpa_power, overheads, penalties, cnr_imp, sat_cir, bandwidth +) -> go.Figure: + n = 40 + power = np.zeros(n) + br = np.zeros(n) + cnr = np.zeros(n) + 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_capacity(bw_nyq, cnr_rcv_i, penalties) / (1 + overheads / 100) + for i in range(1, n): + power[i] = power[i - 1] * 2 ** (1 / 6) + cnr[i] = cnr[i - 1] + 10 * log(2 ** (1 / 6), 10) + cnr_rcv_i = combine_cnr(cnr[i], cnr_imp, sat_cir) + br[i] = shannon_capacity(bw_nyq, cnr_rcv_i, penalties) / (1 + overheads / 100) + + fig = go.Figure() + fig.add_trace(go.Scatter( + x=power, y=br, mode="lines", + name="Higher Layers BR", + line=dict(color="#81C784", width=3), + )) + + cnr_rcv_ref = combine_cnr(cnr_nyq, cnr_imp, sat_cir) + br_ref = shannon_capacity(bw_nyq, cnr_rcv_ref, penalties) / (1 + overheads / 100) + fig.add_trace(go.Scatter( + x=[hpa_power], y=[br_ref], mode="markers+text", + name=f"Ref: {hpa_power:.0f} W, {br_ref:.1f} Mbps", + marker=dict(size=12, color="#FF7043", symbol="diamond"), + text=[f"{br_ref:.1f} Mbps"], + textposition="top center", + )) + + fig.update_layout( + title=f"Higher Layers Bit Rate at Constant BW: {bandwidth:.1f} MHz", + xaxis_title="HPA Output Power [W]", + yaxis_title="Bit Rate [Mbps]", + template="plotly_dark", + height=500, + ) + return fig + + +def _make_br_factor_map_real( + cnr_nyq, bw_nyq, rolloff, overheads, penalties, cnr_imp, sat_cir, + hpa_power, bandwidth, br_rcv_higher, +) -> go.Figure: + n = 41 + bw_mul = np.zeros((n, n)) + p_mul = np.zeros((n, n)) + br_mul = np.zeros((n, n)) + for i in range(n): + for j in range(n): + bw_mul[i, j] = (i + 1) / 8 + p_mul[i, j] = (j + 1) / 8 + 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_capacity(bw_ij / (1 + rolloff / 100), cnr_rcv_ij, penalties) / (1 + overheads / 100) + br_mul[i, j] = br_ij / br_rcv_higher if br_rcv_higher > 0 else 0 + + fig = go.Figure(data=go.Contour( + z=br_mul, + x=bw_mul[:, 0], + y=p_mul[0, :], + colorscale="Viridis", + contours=dict(showlabels=True, labelfont=dict(size=10, color="white")), + colorbar=dict(title="BR Factor"), + )) + fig.update_layout( + title=f"BR Multiplying Factor
Ref: {hpa_power:.0f} W, " + f"{bandwidth:.0f} MHz, {br_rcv_higher:.1f} Mbps", + xaxis_title="Bandwidth Factor", + yaxis_title="Power Factor", + template="plotly_dark", + height=550, + ) + return fig + + +def render(): + """Render the Real World satellite link budget page.""" + + # โ”€โ”€ Header โ”€โ”€ + col_img, col_title = st.columns([1, 3]) + with col_img: + st.image("Satellite.png", width=200) + with col_title: + st.markdown("# ๐Ÿ›ฐ๏ธ Shannon & Friends in the Real World") + st.markdown("From theory to satellite communication link budget.") + + wiki_cols = st.columns(4) + wiki_cols[0].link_button("๐Ÿ“– Harry Nyquist", "https://en.wikipedia.org/wiki/Harry_Nyquist") + wiki_cols[1].link_button("๐Ÿ“– Richard Hamming", "https://en.wikipedia.org/wiki/Richard_Hamming") + wiki_cols[2].link_button("๐Ÿ“– Andrew Viterbi", "https://en.wikipedia.org/wiki/Andrew_Viterbi") + wiki_cols[3].link_button("๐Ÿ“– Claude Berrou", "https://en.wikipedia.org/wiki/Claude_Berrou") + + st.divider() + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # SECTION 1: Satellite Link + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + st.markdown("## ๐Ÿ“ก Satellite Link") + + col1, col2, col3 = st.columns(3) + with col1: + sat_alt = st.number_input("Satellite Altitude [km]", value=35786.0, step=100.0, + help=REAL_WORLD_HELP["sat_alt"]) + sat_lat = st.number_input("Satellite Latitude [ยฐ]", value=0.0, step=0.1, + help=REAL_WORLD_HELP["sat_latlon"]) + sat_lon = st.number_input("Satellite Longitude [ยฐ]", value=19.2, step=0.1, + help=REAL_WORLD_HELP["sat_latlon"]) + with col2: + gs_lat = st.number_input("Ground Station Latitude [ยฐ]", value=49.7, step=0.1, + help=REAL_WORLD_HELP["gs_latlon"]) + gs_lon = st.number_input("Ground Station Longitude [ยฐ]", value=6.3, step=0.1, + help=REAL_WORLD_HELP["gs_latlon"]) + freq = st.number_input("Frequency [GHz]", value=12.0, min_value=0.1, step=0.5, + help=REAL_WORLD_HELP["freq"]) + with col3: + hpa_power = st.number_input("HPA Output Power [W]", value=120.0, min_value=0.1, step=10.0, + help=REAL_WORLD_HELP["hpa"]) + sat_loss = st.number_input("Output Losses [dB]", value=2.0, min_value=0.0, step=0.5, + help=REAL_WORLD_HELP["losses"]) + availability = st.number_input("Link Availability [%]", value=99.9, + min_value=90.0, max_value=99.999, step=0.1, + help=REAL_WORLD_HELP["availability"]) + + col4, col5 = st.columns(2) + with col4: + sat_cir_input = st.text_input("TX Impairments C/I [dB] (comma-sep)", value="25, 25", + help=REAL_WORLD_HELP["sat_cir"]) + sat_beam = st.number_input("Beam Diameter [ยฐ]", value=3.0, min_value=0.1, step=0.1, + help=REAL_WORLD_HELP["sat_beam"]) + with col5: + gain_offset = st.number_input("Offset from Peak [dB]", value=0.0, min_value=0.0, step=0.5, + help=REAL_WORLD_HELP["gain_offset"]) + + # Parse satellite C/I + try: + sat_cir_list = [float(v.strip()) for v in sat_cir_input.split(",")] + except ValueError: + st.error("โŒ Invalid C/I values.") + return + + # Compute satellite link + try: + with st.spinner("Computing atmospheric attenuation (ITU-R model)..."): + sat = compute_satellite_link( + freq, hpa_power, sat_loss, sat_cir_list, sat_beam, gain_offset, + sat_alt, sat_lat, sat_lon, gs_lat, gs_lon, availability, + ) + except Exception as e: + st.error(f"โŒ Satellite link computation error: {e}") + return + + # Display satellite link results + st.markdown("#### ๐Ÿ“Š Satellite Link Results") + r1, r2, r3 = st.columns(3) + r1.metric("Output Power", fmt_power(sat["sig_power"]), help=REAL_WORLD_HELP["output_power"]) + r2.metric("Antenna Gain", fmt_gain(sat["sat_gain_linear"]), help=REAL_WORLD_HELP["sat_gain"]) + r3.metric("EIRP", fmt_power(sat["eirp_linear"]), help=REAL_WORLD_HELP["eirp"]) + + r4, r5, r6 = st.columns(3) + r4.metric("Path Length", f"{sat['path_length']:.1f} km @ {sat['elevation']:.1f}ยฐ", + help=REAL_WORLD_HELP["path_length"]) + r5.metric("Atmospheric Loss", f"{sat['atm_loss_db']:.1f} dB", + help=REAL_WORLD_HELP["atm_loss"]) + r6.metric("Path Dispersion", fmt_ploss(sat["path_loss_linear"]), + help=REAL_WORLD_HELP["path_loss"]) + + st.metric("Power Flux Density", fmt_pfd(sat["pfd_linear"]), help=REAL_WORLD_HELP["pfd"]) + + if sat["elevation"] <= 0: + st.warning("โš ๏ธ Satellite is below the horizon (negative elevation). Results may not be meaningful.") + + st.divider() + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # SECTION 2: Radio Front End + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + st.markdown("## ๐Ÿ“ป Radio Front End") + + col_r1, col_r2 = st.columns(2) + with col_r1: + cpe_ant_d = st.number_input("Receive Antenna Size [m]", value=0.6, min_value=0.1, step=0.1, + help=REAL_WORLD_HELP["cpe_ant"]) + with col_r2: + cpe_t_clear = st.number_input("Noise Temperature [K]", value=120.0, min_value=10.0, step=10.0, + help=REAL_WORLD_HELP["cpe_temp"]) + + try: + rcv = compute_receiver( + sat["pfd_linear"], sat["atm_loss_db"], sat["wavelength"], + cpe_ant_d, cpe_t_clear, + ) + except Exception as e: + st.error(f"โŒ Receiver computation error: {e}") + return + + st.markdown("#### ๐Ÿ“Š Receiver Results") + rx1, rx2 = st.columns(2) + rx1.metric("Antenna Area ยท G/T", f"{rcv['cpe_ae']:.2f} mยฒ ยท {rcv['cpe_g_t']:.1f} dB/K", + help=REAL_WORLD_HELP["cpe_gain"]) + rx2.metric("RX Power (C)", fmt_power(rcv["rx_power"]), help=REAL_WORLD_HELP["rx_power"]) + + rx3, rx4 = st.columns(2) + rx3.metric("Nโ‚€", fmt_psd(rcv["n0"] * 1e6), help=REAL_WORLD_HELP["n0"]) + rx4.metric("BR at โˆž BW", fmt_br(rcv["br_infinity"]), help=REAL_WORLD_HELP["br_inf"]) + + rx5, rx6 = st.columns(2) + rx5.metric("BR at SpEff=1", fmt_br(rcv["br_spe_1"]), help=REAL_WORLD_HELP["br_unit"]) + rx6.metric("BR at SpEff=2", fmt_br(rcv["br_spe_double"]), help=REAL_WORLD_HELP["br_double"]) + + st.divider() + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # SECTION 3: Baseband Unit + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + st.markdown("## ๐Ÿ’ป Baseband Unit") + + col_b1, col_b2, col_b3 = st.columns(3) + with col_b1: + bandwidth = st.number_input("Occupied Bandwidth [MHz]", value=36.0, min_value=0.1, step=1.0, + help=REAL_WORLD_HELP["bandwidth"], key="bw_real") + rolloff = st.number_input("Nyquist Rolloff [%]", value=5.0, min_value=0.0, max_value=100.0, step=1.0, + help=REAL_WORLD_HELP["rolloff"]) + with col_b2: + cir_input = st.text_input("RX Impairments C/I [dB]", value="20", + help=REAL_WORLD_HELP["cir"]) + penalties = st.number_input("Implementation Penalty [dB]", value=1.5, min_value=0.0, step=0.1, + help=REAL_WORLD_HELP["penalty"]) + with col_b3: + overheads = st.number_input("Higher Layers Overhead [%]", value=5.0, min_value=0.0, step=1.0, + help=REAL_WORLD_HELP["overhead"]) + + try: + cnr_imp_list = [float(v.strip()) for v in cir_input.split(",")] + except ValueError: + st.error("โŒ Invalid C/I values.") + return + + try: + bb = compute_baseband( + rcv["c_n0_mhz"], rcv["br_infinity"], rcv["bw_spe_1"], + sat["sat_cir"], bandwidth, rolloff, overheads, cnr_imp_list, penalties, + ) + except Exception as e: + st.error(f"โŒ Baseband computation error: {e}") + return + + st.markdown("#### ๐Ÿ“Š Baseband Results") + + b1, b2, b3 = st.columns(3) + b1.metric("SNR in Available BW", f"{bb['cnr_bw']:.1f} dB in {bandwidth:.1f} MHz", + help=REAL_WORLD_HELP["cnr_bw"]) + b2.metric("SNR in Nyquist BW", f"{bb['cnr_nyq']:.1f} dB in {bb['bw_nyq']:.1f} MHz", + help=REAL_WORLD_HELP["cnr_nyq"]) + b3.metric("SNR at Receiver", f"{bb['cnr_rcv']:.1f} dB", + help=REAL_WORLD_HELP["cnr_rcv"]) + + st.divider() + + b4, b5 = st.columns(2) + with b4: + st.markdown("##### ๐ŸŽฏ Theoretical") + st.metric( + "Theoretical BR", + f"{fmt_br(bb['br_nyq'])} ยท {bb['br_nyq_norm']:.0%}", + help=REAL_WORLD_HELP["br_nyq"], + ) + st.caption(f"Spectral Eff: {bb['spe_nyq']:.2f} bps/Hz ยท {bb['bits_per_symbol']:.2f} b/Symbol") + with b5: + st.markdown("##### ๐Ÿญ Practical") + st.metric( + "Physical Layer BR", + f"{fmt_br(bb['br_rcv'])} ยท {bb['br_rcv_norm']:.0%}", + help=REAL_WORLD_HELP["br_rcv"], + ) + st.metric( + "Higher Layers BR", + f"{fmt_br(bb['br_rcv_higher'])} ยท {bb['br_rcv_h_norm']:.0%}", + delta=f"{bb['spe_higher']:.2f} bps/Hz", + help=REAL_WORLD_HELP["br_high"], + ) + + st.divider() + + # โ”€โ”€ Graphs โ”€โ”€ + st.markdown("### ๐Ÿ“ˆ Interactive Graphs") + + tab_bw, tab_pow, tab_map = st.tabs([ + "๐Ÿ“ถ BW Sensitivity", + "โšก Power Sensitivity", + "๐Ÿ—บ๏ธ BR Factor Map", + ]) + + cnr_imp = combine_cnr(*cnr_imp_list) + + with tab_bw: + st.plotly_chart( + _make_bw_sensitivity_real( + bb["cnr_nyq"], bandwidth, rolloff, overheads, penalties, + cnr_imp, sat["sat_cir"], hpa_power, + ), + width="stretch", + ) + + with tab_pow: + st.plotly_chart( + _make_power_sensitivity_real( + bb["cnr_nyq"], bb["bw_nyq"], hpa_power, overheads, penalties, + cnr_imp, sat["sat_cir"], bandwidth, + ), + width="stretch", + ) + + with tab_map: + st.plotly_chart( + _make_br_factor_map_real( + bb["cnr_nyq"], bb["bw_nyq"], rolloff, overheads, penalties, + cnr_imp, sat["sat_cir"], hpa_power, bandwidth, bb["br_rcv_higher"], + ), + width="stretch", + ) + + # โ”€โ”€ Help โ”€โ”€ + with st.expander("๐Ÿ“˜ Background Information"): + help_topic = st.selectbox( + "Choose a topic:", + options=["satellite", "advanced", "help"], + format_func=lambda x: { + "satellite": "๐Ÿ›ฐ๏ธ Link Budget Overview", + "advanced": "๐Ÿ”ง Advanced Notes", + "help": "โ“ How to use", + }[x], + key="help_real", + ) + st.markdown(REAL_WORLD_HELP[help_topic]) diff --git a/views/satellite_animation.py b/views/satellite_animation.py new file mode 100644 index 0000000..abd57d6 --- /dev/null +++ b/views/satellite_animation.py @@ -0,0 +1,762 @@ +""" +Interactive Satellite Link Animation +===================================== +HTML5 Canvas animation showing a satellite communicating with a ground station, +with toggleable impairment layers (atmosphere, rain, free-space loss, noiseโ€ฆ). +""" + +import streamlit as st +import streamlit.components.v1 as components + + +_ANIMATION_HTML = """ + + + + + + + + + + +
+

โšก Impairments

+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+ + +
+

๐Ÿ“ก Satellite Link Overview

+ + Toggle each impairment to see how it affects the signal. + Cyan = Downlink (sat โ†’ ground). + Green = Uplink (ground โ†’ sat). + The signal strength bar shows the cumulative effect. + +
+ + + + +""" + + +def render(): + """Render the satellite link animation page.""" + st.markdown("## ๐Ÿ›ฐ๏ธ Satellite Link โ€” Interactive Animation") + st.markdown( + "Visualise how a signal travels from a **GEO satellite** to a " + "**ground station** and discover the impairments that degrade it. " + "Toggle each effect on/off to see the impact on signal strength." + ) + st.divider() + + components.html(_ANIMATION_HTML, height=680, scrolling=False) + + # โ”€โ”€ Pedagogical summary below the animation โ”€โ”€ + st.divider() + st.markdown("### ๐Ÿ“š Understanding the Link Budget") + + col1, col2 = st.columns(2) + + with col1: + st.markdown(""" +**Uplink & Downlink path:** + +The signal travels ~36 000 km from a geostationary satellite to Earth. +Along the way it encounters multiple sources of degradation: + +1. **Free-Space Path Loss** โ€” the dominant factor, purely geometric (1/rยฒ) +2. **Atmospheric gases** โ€” Oโ‚‚ and Hโ‚‚O absorption (ITU-R P.676) +3. **Rain** โ€” scattering & absorption, worst at Ka-band (ITU-R P.618) +4. **Ionosphere** โ€” Faraday rotation, scintillation (ITU-R P.531) +""") + + with col2: + st.markdown(""" +**At the receiver:** + +Even after the signal arrives, further degradation occurs: + +5. **Thermal noise** โ€” every component adds noise: $N = k \\cdot T_{sys} \\cdot B$ +6. **Pointing loss** โ€” antenna misalignment reduces gain +7. **Implementation losses** โ€” ADC quantisation, filter roll-off, etc. + +The **Shannon limit** $C = B \\log_2(1 + C/N)$ tells us the maximum +bit rate achievable given the remaining signal-to-noise ratio. +""") + + with st.expander("๐Ÿ”— Key ITU-R Recommendations"): + st.markdown(""" +| Recommendation | Topic | +|:---:|:---| +| **P.618** | Rain attenuation & propagation effects for satellite links | +| **P.676** | Gaseous attenuation on terrestrial and slant paths | +| **P.531** | Ionospheric effects on radiowave propagation | +| **P.837** | Rainfall rate statistics for prediction models | +| **P.839** | Rain height model for prediction methods | +| **S.1428** | Reference satellite link for system design | +""") diff --git a/views/satellite_types.py b/views/satellite_types.py new file mode 100644 index 0000000..7cb3558 --- /dev/null +++ b/views/satellite_types.py @@ -0,0 +1,917 @@ +""" +Satellite Types โ€” Mission Control Dashboard +============================================= +Each satellite category has its own unique animated scene: + - Navigation โ†’ GPS triangulation on a world map + - Communication โ†’ Data-flow network between ground stations + - Earth Observation โ†’ Top-down terrain scan with image strips + - Weather โ†’ Atmospheric cloud/rain/temperature visualisation + - Science โ†’ Deep-space telescope view (nebula zoom) + - Defense โ†’ Tactical radar sweep with blips +""" + +import streamlit as st +import streamlit.components.v1 as components + + +_SATTYPES_HTML = r""" + + + + + + + + + +
+

๐Ÿ›ฐ๏ธ Satellite Missions

Select a mission type above
to explore its animation.

+
Mission Control โ€” choose a category
+ + + + +""" + + +def render(): + """Render the satellite missions / types dashboard page.""" + st.markdown("## ๐Ÿ›ฐ๏ธ Satellite Missions โ€” Types & Applications") + st.markdown( + "Explore **six categories** of satellite missions through unique animated scenes. " + "Click a tab to switch between navigation, communication, observation, weather, " + "science and defense โ€” each with its own visual story." + ) + st.divider() + + components.html(_SATTYPES_HTML, height=720, scrolling=False) + + # โ”€โ”€ Educational content below โ”€โ”€ + st.divider() + st.markdown("### ๐Ÿ“‹ Mission Comparison") + + st.markdown(""" +| Category | Orbit | Altitude | Key Band | Examples | +|:---|:---:|:---:|:---:|:---| +| ๐Ÿงญ **Navigation** | MEO | 20 200 km | L-band | GPS, Galileo, GLONASS, BeiDou | +| ๐Ÿ“ก **Communication** | GEO / LEO | 550 โ€“ 36 000 km | C / Ku / Ka | Starlink, Intelsat, SES | +| ๐Ÿ“ธ **Earth Observation** | LEO (SSO) | 500 โ€“ 800 km | X-band | Sentinel, Landsat, Planet | +| ๐ŸŒฆ๏ธ **Weather** | GEO / LEO | 800 โ€“ 36 000 km | L / Ka | Meteosat, GOES, Himawari | +| ๐Ÿ”ญ **Science** | Various | Variable | S / X / Ka | JWST, Hubble, Gaia | +| ๐Ÿ›ก๏ธ **Defense** | LEO / GEO / HEO | Variable | UHF / EHF | SBIRS, WGS, Syracuse | +""") + + col1, col2 = st.columns(2) + + with col1: + with st.expander("๐Ÿงญ Navigation Satellites"): + st.markdown(""" +**How GNSS works:** + +Each satellite broadcasts its precise position and exact time +(from onboard atomic clocks). A receiver picks up +signals from $\\geq 4$ satellites and solves the position equations: + +$$d_i = c \\cdot (t_{\\text{rx}} - t_{\\text{tx},i})$$ + +Where $d_i$ is the pseudorange to satellite $i$, $c$ is the speed of +light, and $t_{\\text{rx}}$ is the receiver clock time. + +With 4+ satellites the receiver solves for $(x, y, z, \\Delta t)$ โ€” +3D position plus its own clock error. + +**Key systems:** +- **GPS** (USA) โ€” 31 sats, L1 / L2 / L5 +- **Galileo** (EU) โ€” 30 sats, E1 / E5a / E5b / E6 +- **GLONASS** (Russia) โ€” 24 sats +- **BeiDou** (China) โ€” 35+ sats +""") + + with st.expander("๐Ÿ“ก Communication Satellites"): + st.markdown(""" +**Evolution:** + +1. **1960s** โ€” Early Bird: 240 voice channels +2. **1980s** โ€” Large GEO: thousands of transponders +3. **2000s** โ€” HTS: spot beams, frequency reuse +4. **2020s** โ€” Mega-constellations (Starlink 6 000+ LEO) + +**Shannon connection:** + +$$C = B \\cdot \\log_2\\!\\left(1 + \\frac{C}{N}\\right)$$ + +A GEO satellite at 36 000 km suffers ~50 dB more path loss than +LEO at 550 km, but compensates with larger antennas, higher power +and advanced modulation (DVB-S2X, 256-APSK). +""") + + with st.expander("๐ŸŒฆ๏ธ Weather Satellites"): + st.markdown(""" +**Two approaches:** + +1. **GEO** (Meteosat, GOES, Himawari) โ€” continuous monitoring, + full disk every 10โ€“15 min, 16+ spectral bands. +2. **LEO polar** (MetOp, NOAA) โ€” higher resolution but 2 passes/day, + microwave sounders penetrate clouds. + +**Data volume:** GOES-16 generates ~3.6 TB/day of imagery. +""") + + with col2: + with st.expander("๐Ÿ“ธ Earth Observation Satellites"): + st.markdown(""" +**Imaging technologies:** + +| Type | Resolution | Use | +|:---|:---:|:---| +| **Optical** | 0.3 โ€“ 1 m | Urban mapping, defence | +| **Multispectral** | 5 โ€“ 30 m | Agriculture (NDVI) | +| **Hyperspectral** | 30 m, 200+ bands | Mineral detection | +| **SAR** | 1 โ€“ 10 m | All-weather imaging | +| **InSAR** | mm displacement | Ground subsidence | + +**Sun-Synchronous Orbit (SSO):** ~98ยฐ inclination ensures +consistent solar illumination for temporal comparison. +""") + + with st.expander("๐Ÿ”ญ Science & Exploration"): + st.markdown(""" +| Mission | Location | Purpose | +|:---|:---:|:---| +| **JWST** | L2 (1.5 M km) | IR astronomy | +| **Hubble** | LEO (540 km) | Optical / UV | +| **Gaia** | L2 | 3D map of 1.8 B stars | +| **Chandra** | HEO | X-ray astronomy | + +**Deep space challenge:** Voyager 1 at 24 billion km transmits +at ~160 **bits**/s with 23 W through NASA's 70 m DSN antennas. +""") + + with st.expander("๐Ÿ›ก๏ธ Defense & Intelligence"): + st.markdown(""" +**Categories:** + +- **MILCOM** โ€” WGS, AEHF, Syracuse (secure links, EHF) +- **SIGINT** โ€” intercept electromagnetic emissions +- **IMINT** โ€” high-res optical/radar reconnaissance +- **Early Warning** โ€” SBIRS IR missile detection from GEO +- **ELINT** โ€” radar system characterisation + +**Resilience:** frequency hopping, spread spectrum, +radiation hardening, satellite crosslinks. +""") diff --git a/views/theory.py b/views/theory.py new file mode 100644 index 0000000..431d711 --- /dev/null +++ b/views/theory.py @@ -0,0 +1,269 @@ +""" +Page 1: Shannon's Equation โ€” Theoretical Exploration + +Interactive exploration of the Shannon capacity formula with Plotly graphs. +""" + +import streamlit as st +import numpy as np +import plotly.graph_objects as go +from math import log + +from core.calculations import ( + combine_cnr, + shannon_capacity, + shannon_points, + br_multiplier, + fmt_br, +) +from core.help_texts import THEORY_HELP + + +def _make_bw_sensitivity_plot(cnr_nyq: float, bw_nyq: float, c_n0: float) -> go.Figure: + """Bandwidth sensitivity at constant power.""" + n = 40 + bw = np.zeros(n) + br = np.zeros(n) + cnr = np.zeros(n) + cnr[0] = cnr_nyq + 10 * log(8, 10) + bw[0] = bw_nyq / 8 + br[0] = shannon_capacity(bw[0], cnr[0]) + for i in range(1, n): + bw[i] = bw[i - 1] * 2 ** (1 / 6) + cnr[i] = cnr[i - 1] - 10 * log(bw[i] / bw[i - 1], 10) + br[i] = shannon_capacity(bw[i], cnr[i]) + + fig = go.Figure() + fig.add_trace(go.Scatter( + x=bw, y=br, mode="lines", + name="Shannon Capacity", + line=dict(color="#4FC3F7", width=3), + )) + + # Mark reference point + ref_br = shannon_capacity(bw_nyq, cnr_nyq) + fig.add_trace(go.Scatter( + x=[bw_nyq], y=[ref_br], mode="markers+text", + name=f"Reference: {bw_nyq:.1f} MHz, {ref_br:.1f} Mbps", + marker=dict(size=12, color="#FF7043", symbol="diamond"), + text=[f"{ref_br:.1f} Mbps"], + textposition="top center", + )) + + fig.update_layout( + title=f"Theoretical Bit Rate at Constant Power
C/Nโ‚€ = {c_n0:.1f} MHz", + xaxis_title="Bandwidth [MHz]", + yaxis_title="Bit Rate [Mbps]", + template="plotly_dark", + height=500, + showlegend=True, + legend=dict(yanchor="bottom", y=0.02, xanchor="right", x=0.98), + ) + return fig + + +def _make_power_sensitivity_plot(cnr_nyq: float, bw_nyq: float, cnr_linear: float) -> go.Figure: + """Power sensitivity at constant bandwidth.""" + n = 40 + p_mul = np.zeros(n) + br = np.zeros(n) + cnr = np.zeros(n) + p_mul[0] = 1 / 8 + cnr[0] = cnr_nyq - 10 * log(8, 10) + br[0] = shannon_capacity(bw_nyq, cnr[0]) + for i in range(1, n): + p_mul[i] = p_mul[i - 1] * 2 ** (1 / 6) + cnr[i] = cnr[i - 1] + 10 * log(2 ** (1 / 6), 10) + br[i] = shannon_capacity(bw_nyq, cnr[i]) + + fig = go.Figure() + fig.add_trace(go.Scatter( + x=p_mul, y=br, mode="lines", + name="Shannon Capacity", + line=dict(color="#81C784", width=3), + )) + + # Reference point (multiplier = 1) + ref_br = shannon_capacity(bw_nyq, cnr_nyq) + fig.add_trace(go.Scatter( + x=[1.0], y=[ref_br], mode="markers+text", + name=f"Reference: 1x, {ref_br:.1f} Mbps", + marker=dict(size=12, color="#FF7043", symbol="diamond"), + text=[f"{ref_br:.1f} Mbps"], + textposition="top center", + )) + + fig.update_layout( + title=f"Theoretical Bit Rate at Constant Bandwidth: {bw_nyq:.1f} MHz
" + f"Reference: C/N = {cnr_linear:.1f} [Linear]", + xaxis_title="Power Multiplying Factor", + yaxis_title="Bit Rate [Mbps]", + template="plotly_dark", + height=500, + showlegend=True, + legend=dict(yanchor="bottom", y=0.02, xanchor="right", x=0.98), + ) + return fig + + +def _make_br_factor_map(cnr_nyq: float, bw_nyq: float, c_n0: float, br_bw: float) -> go.Figure: + """Contour map of BR multiplying factors.""" + n = 41 + bw_mul = np.zeros((n, n)) + p_mul = np.zeros((n, n)) + br_mul = np.zeros((n, n)) + for i in range(n): + for j in range(n): + bw_mul[i, j] = (i + 1) / 8 + p_mul[i, j] = (j + 1) / 8 + br_mul[i, j] = br_multiplier(bw_mul[i, j], p_mul[i, j], cnr_nyq) + + fig = go.Figure(data=go.Contour( + z=br_mul, + x=bw_mul[:, 0], + y=p_mul[0, :], + colorscale="Viridis", + contours=dict(showlabels=True, labelfont=dict(size=10, color="white")), + colorbar=dict(title="BR Factor"), + )) + fig.update_layout( + title=f"Bit Rate Multiplying Factor
Ref: C/N = {cnr_nyq:.1f} dB, " + f"BW = {bw_nyq:.1f} MHz, C/Nโ‚€ = {c_n0:.1f} MHz, " + f"BR = {br_bw:.1f} Mbps", + xaxis_title="Bandwidth Multiplying Factor", + yaxis_title="Power Multiplying Factor", + template="plotly_dark", + height=550, + ) + return fig + + +def render(): + """Render the Theoretical Exploration page.""" + + # โ”€โ”€ Header โ”€โ”€ + col_img, col_title = st.columns([1, 3]) + with col_img: + st.image("Shannon.png", width=200) + with col_title: + st.markdown("# ๐Ÿ“ก Shannon's Equation for Dummies") + st.markdown( + "Exploration of Claude Shannon's channel capacity theorem โ€” " + "the fundamental limit of digital communications." + ) + st.link_button("๐Ÿ“– Wiki: Claude Shannon", "https://en.wikipedia.org/wiki/Claude_Shannon") + + st.divider() + + # โ”€โ”€ Input Parameters โ”€โ”€ + st.markdown("### โš™๏ธ Input Parameters") + + col_in1, col_in2 = st.columns(2) + with col_in1: + cnr_input = st.text_input( + "Reference C/N [dB]", + value="12", + help=THEORY_HELP["cnr"], + ) + with col_in2: + bw_input = st.number_input( + "Reference BW [MHz]", + value=36.0, min_value=0.1, step=1.0, + help=THEORY_HELP["bw"], + ) + + # Parse CNR (supports comma-separated combinations) + try: + cnr_values = [float(v.strip()) for v in cnr_input.split(",")] + cnr_nyq = combine_cnr(*cnr_values) + except (ValueError, ZeroDivisionError): + st.error("โŒ Invalid C/N values. Use comma-separated numbers (e.g., '12' or '12, 15').") + return + + # โ”€โ”€ Computed Results โ”€โ”€ + cnr_linear, br_inf, c_n0, br_bw = shannon_points(bw_input, cnr_nyq) + br_unit = c_n0 # Spectral efficiency = 1 + + st.markdown("### ๐Ÿ“Š Results") + st.info(THEORY_HELP["c_n0"], icon="โ„น๏ธ") if st.checkbox("Show C/Nโ‚€ explanation", value=False) else None + + m1, m2, m3, m4 = st.columns(4) + m1.metric("C/Nโ‚€", f"{c_n0:.1f} MHz", help=THEORY_HELP["c_n0"]) + m2.metric("BR at โˆž BW", fmt_br(br_inf), help=THEORY_HELP["br_inf"]) + m3.metric("BR at SpEff=1", fmt_br(br_unit), help=THEORY_HELP["br_unit"]) + m4.metric("BR at Ref BW", fmt_br(br_bw), help=THEORY_HELP["br_bw"]) + + st.metric( + "C/N Ratio", + f"{cnr_nyq:.1f} dB ยท {cnr_linear:.1f} linear", + help=THEORY_HELP["cnr_lin"], + ) + + st.divider() + + # โ”€โ”€ Sensitivity Analysis โ”€โ”€ + st.markdown("### ๐Ÿ”ฌ Sensitivity Analysis") + + col_s1, col_s2, col_s3 = st.columns(3) + with col_s1: + bw_mul_val = st.number_input( + "BW Increase Factor", + value=1.0, min_value=0.01, step=0.25, + help=THEORY_HELP["bw_mul"], + ) + with col_s2: + p_mul_val = st.number_input( + "Power Increase Factor", + value=2.0, min_value=0.01, step=0.25, + help=THEORY_HELP["p_mul"], + ) + with col_s3: + br_mul_val = br_multiplier(bw_mul_val, p_mul_val, cnr_nyq) + st.metric( + "Bit Rate Factor", + f"{br_mul_val:.3f}", + delta=f"{(br_mul_val - 1) * 100:+.1f}%", + help=THEORY_HELP["br_mul"], + ) + + st.divider() + + # โ”€โ”€ Graphs โ”€โ”€ + st.markdown("### ๐Ÿ“ˆ Interactive Graphs") + + tab_bw, tab_pow, tab_map = st.tabs([ + "๐Ÿ“ถ Bandwidth Sensitivity", + "โšก Power Sensitivity", + "๐Ÿ—บ๏ธ BR Factor Map", + ]) + + with tab_bw: + st.plotly_chart( + _make_bw_sensitivity_plot(cnr_nyq, bw_input, c_n0), + width="stretch", + ) + + with tab_pow: + st.plotly_chart( + _make_power_sensitivity_plot(cnr_nyq, bw_input, cnr_linear), + width="stretch", + ) + + with tab_map: + st.plotly_chart( + _make_br_factor_map(cnr_nyq, bw_input, c_n0, br_bw), + width="stretch", + ) + + # โ”€โ”€ Help Section โ”€โ”€ + with st.expander("๐Ÿ“˜ Background Information"): + help_topic = st.selectbox( + "Choose a topic:", + options=["shannon", "advanced", "help"], + format_func=lambda x: { + "shannon": "๐Ÿง  Shannon's Equation", + "advanced": "๐Ÿ”ง Advanced (AWGN Model)", + "help": "โ“ How to use this tool", + }[x], + ) + st.markdown(THEORY_HELP[help_topic])