├── .gitignore
├── LICENSE
├── README.md
├── bin
└── v2
│ ├── 2.0.0
│ ├── installer
│ │ ├── themera-v2.0.0-win-x86.exe
│ │ └── themera-v2.0.0-win-x86.exe.sha256sum.txt
│ ├── themera-v2.0.0-win-x86.zip
│ └── themera-v2.0.0-win-x86.zip.sha256sum.txt
│ ├── 2.1.0
│ └── themera-v2.1.0-win-x86
│ │ ├── installer
│ │ ├── themera-v2.1.0-win-x86.exe
│ │ ├── themera-v2.1.0-win-x86.exe.md5sum.txt
│ │ └── themera-v2.1.0-win-x86.exe.sha256sum.txt
│ │ ├── themera-v2.1.0-win-x86.zip
│ │ ├── themera-v2.1.0-win-x86.zip.md5sum.txt
│ │ └── themera-v2.1.0-win-x86.zip.sha256sum.txt
│ └── 2.1.1
│ └── themera-v2.1.1-win-x86
│ ├── installer
│ ├── themera-v2.1.1-win-x86.exe
│ ├── themera-v2.1.1-win-x86.exe.md5sum.txt
│ └── themera-v2.1.1-win-x86.exe.sha256sum.txt
│ ├── themera-v2.1.1-win-x86.zip
│ ├── themera-v2.1.1-win-x86.zip.md5sum.txt
│ └── themera-v2.1.1-win-x86.zip.sha256sum.txt
├── branding
├── raster
│ ├── themera_banner.png
│ ├── themera_installer_logo.icns
│ ├── themera_installer_logo.ico
│ ├── themera_installer_logo.png
│ ├── themera_installer_sidebar.bmp
│ ├── themera_logo.icns
│ ├── themera_logo.ico
│ ├── themera_logo.png
│ ├── themera_sidebar.png
│ ├── themera_small_installer_logo.bmp
│ ├── themera_uninstaller_logo.icns
│ ├── themera_uninstaller_logo.ico
│ └── themera_uninstaller_logo.png
└── vector
│ ├── themera_installer_logo.svg
│ ├── themera_installer_sidebar.svg
│ ├── themera_logo.svg
│ └── themera_uninstaller_logo.svg
├── compile.py
├── help
├── help.odt
└── help.pdf
├── res
├── download.png
└── download.svg
├── screenshots
├── editor_dark.png
├── editor_light.png
├── launcher_dark.png
└── launcher_light.png
├── template.iss
└── themera
├── bytecode.py
├── color_shorthands.py
├── constants.py
├── crash.py
├── custom_preview.py
├── filters.py
├── fonts.py
├── functions.py
├── launcher.py
├── open_docs.py
├── palette_preview.py
├── preview_panel.py
├── requirements.txt
├── settings.py
├── themera.py
├── themes.py
├── version_and_copyright.py
└── window.py
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | .~lock.help.odt#
3 | .history
4 | .idea
5 | .uuid
6 | .vscode
7 | *.build
8 | *.themerasettings
9 | *.tmp
10 | build
11 | temp.iss
12 | themera.dist*
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Themera
2 |
3 | ## Latest Version: v2.1.1
4 |
5 | Themera is currently available in compiled form for:
6 |
7 | #### Windows
8 | [
](https://github.com/definite-d/Themera/releases/latest)
9 |
10 | #### Linux
11 | A Linux version (AppImage) is available [here](https://github.com/blabla-labALT/Themera-for-Linux), thanks to [blabla_lab](https://github.com/blabla-labALT).
12 |
13 | ___________________________________________________________________________________
14 |
15 | ## What's Themera?
16 |
17 | Themera is a theme code generator for PySimpleGUI.
18 |
19 | It enables you to create themes based on any of the existing themes that comes built in with PySimpleGUI, edit any custom existing theme based on the dictionary containing its colors, or create a theme from an image.
20 |
21 | After editing, simply copy your theme code, ready to use in your project without need for alteration.
22 |
23 | It is – of course – built with PySimpleGUI, free and open source under the LGPL v3 license, and comes with a wide range of features from batch color manipulation, to 13 filters that simulate color blindness, auto-contrast, automatic dark and light modes for themes and more.
24 |
25 | All themes generated with Themera are free to be used as you choose; the LGPL license applies only to Themera itself. Same goes for the Sample Themes theme code too.
26 | ______________________________________________________________________________________
27 |
28 | ## Timeline
29 |
30 | * Development began on 29/11/2019, bare minimum (v1.2) got completed on 1/12/2019.
31 |
32 | * The project got renamed from LookyFeely to Themera on 5/1/2021
33 |
34 | * Development of Version 2 began 19/11/2022 completed on 12/04/2023.
35 |
36 | ______________________________________________________________________________________
37 |
38 |
39 |
40 | ## Screenshot Showcase:
41 | 
42 |
43 | 
44 |
45 | 
46 |
47 | 
48 |
--------------------------------------------------------------------------------
/bin/v2/2.0.0/installer/themera-v2.0.0-win-x86.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/bin/v2/2.0.0/installer/themera-v2.0.0-win-x86.exe
--------------------------------------------------------------------------------
/bin/v2/2.0.0/installer/themera-v2.0.0-win-x86.exe.sha256sum.txt:
--------------------------------------------------------------------------------
1 | d541d8acdf5328211c98694f6b6b1633417521ced70278c5b5ba466345fb79da
2 |
--------------------------------------------------------------------------------
/bin/v2/2.0.0/themera-v2.0.0-win-x86.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/bin/v2/2.0.0/themera-v2.0.0-win-x86.zip
--------------------------------------------------------------------------------
/bin/v2/2.0.0/themera-v2.0.0-win-x86.zip.sha256sum.txt:
--------------------------------------------------------------------------------
1 | 21717e31096429decd902a50d0608f17629327763a7079f7fb52c64378783578
2 |
--------------------------------------------------------------------------------
/bin/v2/2.1.0/themera-v2.1.0-win-x86/installer/themera-v2.1.0-win-x86.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/bin/v2/2.1.0/themera-v2.1.0-win-x86/installer/themera-v2.1.0-win-x86.exe
--------------------------------------------------------------------------------
/bin/v2/2.1.0/themera-v2.1.0-win-x86/installer/themera-v2.1.0-win-x86.exe.md5sum.txt:
--------------------------------------------------------------------------------
1 | 78a74c3b451c1c128013dc0aba35eb0b
--------------------------------------------------------------------------------
/bin/v2/2.1.0/themera-v2.1.0-win-x86/installer/themera-v2.1.0-win-x86.exe.sha256sum.txt:
--------------------------------------------------------------------------------
1 | 6fa8741d823a566df74106f5db71c1fb6babd732915e17af86877889d55c27a7
--------------------------------------------------------------------------------
/bin/v2/2.1.0/themera-v2.1.0-win-x86/themera-v2.1.0-win-x86.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/bin/v2/2.1.0/themera-v2.1.0-win-x86/themera-v2.1.0-win-x86.zip
--------------------------------------------------------------------------------
/bin/v2/2.1.0/themera-v2.1.0-win-x86/themera-v2.1.0-win-x86.zip.md5sum.txt:
--------------------------------------------------------------------------------
1 | 3f7464a7f9371d811d6c7ff01cac4b65
--------------------------------------------------------------------------------
/bin/v2/2.1.0/themera-v2.1.0-win-x86/themera-v2.1.0-win-x86.zip.sha256sum.txt:
--------------------------------------------------------------------------------
1 | dcbd873fd42348569231e60d18a3eef0786356b7752524b82410f8a1278faa9a
--------------------------------------------------------------------------------
/bin/v2/2.1.1/themera-v2.1.1-win-x86/installer/themera-v2.1.1-win-x86.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/bin/v2/2.1.1/themera-v2.1.1-win-x86/installer/themera-v2.1.1-win-x86.exe
--------------------------------------------------------------------------------
/bin/v2/2.1.1/themera-v2.1.1-win-x86/installer/themera-v2.1.1-win-x86.exe.md5sum.txt:
--------------------------------------------------------------------------------
1 | 9a621b89dfad6ec8e77f1117e76419d8
--------------------------------------------------------------------------------
/bin/v2/2.1.1/themera-v2.1.1-win-x86/installer/themera-v2.1.1-win-x86.exe.sha256sum.txt:
--------------------------------------------------------------------------------
1 | f1e25c06bad6550d9e9bea1b46da2d5ac919c52b279aac53e75b3cafe0e32727
--------------------------------------------------------------------------------
/bin/v2/2.1.1/themera-v2.1.1-win-x86/themera-v2.1.1-win-x86.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/bin/v2/2.1.1/themera-v2.1.1-win-x86/themera-v2.1.1-win-x86.zip
--------------------------------------------------------------------------------
/bin/v2/2.1.1/themera-v2.1.1-win-x86/themera-v2.1.1-win-x86.zip.md5sum.txt:
--------------------------------------------------------------------------------
1 | e181822765de7cfcefa52bfd35f3a234
--------------------------------------------------------------------------------
/bin/v2/2.1.1/themera-v2.1.1-win-x86/themera-v2.1.1-win-x86.zip.sha256sum.txt:
--------------------------------------------------------------------------------
1 | 82659db6885714484bc283fdc2f009f103520c36f15d37266fa4d7670aa1c031
--------------------------------------------------------------------------------
/branding/raster/themera_banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/branding/raster/themera_banner.png
--------------------------------------------------------------------------------
/branding/raster/themera_installer_logo.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/branding/raster/themera_installer_logo.icns
--------------------------------------------------------------------------------
/branding/raster/themera_installer_logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/branding/raster/themera_installer_logo.ico
--------------------------------------------------------------------------------
/branding/raster/themera_installer_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/branding/raster/themera_installer_logo.png
--------------------------------------------------------------------------------
/branding/raster/themera_installer_sidebar.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/branding/raster/themera_installer_sidebar.bmp
--------------------------------------------------------------------------------
/branding/raster/themera_logo.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/branding/raster/themera_logo.icns
--------------------------------------------------------------------------------
/branding/raster/themera_logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/branding/raster/themera_logo.ico
--------------------------------------------------------------------------------
/branding/raster/themera_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/branding/raster/themera_logo.png
--------------------------------------------------------------------------------
/branding/raster/themera_sidebar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/branding/raster/themera_sidebar.png
--------------------------------------------------------------------------------
/branding/raster/themera_small_installer_logo.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/branding/raster/themera_small_installer_logo.bmp
--------------------------------------------------------------------------------
/branding/raster/themera_uninstaller_logo.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/branding/raster/themera_uninstaller_logo.icns
--------------------------------------------------------------------------------
/branding/raster/themera_uninstaller_logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/branding/raster/themera_uninstaller_logo.ico
--------------------------------------------------------------------------------
/branding/raster/themera_uninstaller_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/branding/raster/themera_uninstaller_logo.png
--------------------------------------------------------------------------------
/branding/vector/themera_installer_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
250 |
--------------------------------------------------------------------------------
/branding/vector/themera_installer_sidebar.svg:
--------------------------------------------------------------------------------
1 |
2 |
18 |
--------------------------------------------------------------------------------
/branding/vector/themera_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
234 |
--------------------------------------------------------------------------------
/branding/vector/themera_uninstaller_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
31 |
--------------------------------------------------------------------------------
/compile.py:
--------------------------------------------------------------------------------
1 | """
2 | Compile Script.
3 | Performs the jobs for compling Themera.
4 |
5 | Since this project was primarily built on Windows, this script is
6 | guaranteed to work for Windows compilation, but it "should" work on
7 | other platforms (Mac and Linux).
8 |
9 | The configuration functions are at the bottom of the script,
10 | if modification is required.
11 |
12 | Compilation in general requires the `black` and `isort` libraries,
13 | as well as `nuitka` (and a suitable compiler) and a working install
14 | of `git`.
15 |
16 | Compiling for Windows requires InnoSetup installed and the
17 | installation directory added to PATH.
18 |
19 | While this script runs with whatever Python version available to it,
20 | you may choose to use a specific version of Python just for compiling
21 | with Nuitka. If you have multiple Python versions installed, modify
22 | the `PYTHON_VERSION` variable below to suit the one you wish to use for
23 | compilation, before running this script. Set it to `None` to use the
24 | default (or only one) available.
25 |
26 | Supporting 32-bit Windows also requires the MSVC compiler, and
27 | a 32-bit version of Python available (3.7.9 has been tested by
28 | me and is thus set as default) to allow cross-compilation for
29 | 32 bit Windows as well. Setting PYTHON_VERSION to a 32-bit
30 | version of Python will automatically compile for 32-bit
31 | compatibility in general. For best compatibility with 32-bit systems,
32 | I advise simply compiling on a 32-bit system itself if possible.
33 | """
34 |
35 | PYTHON_VERSION = "3.7"
36 |
37 | import re
38 | from datetime import datetime
39 | from hashlib import md5, sha256
40 | from os import system as run
41 | from pathlib import Path
42 | from platform import system
43 | from shutil import rmtree
44 | from subprocess import check_output
45 | from sys import path
46 | from zipfile import ZipFile
47 |
48 | from PySimpleGUI import running_linux, running_mac, running_windows
49 |
50 |
51 | print("Starting the Compiler Script...")
52 |
53 | # I'd like to do what I call a "pro-gamer move"...
54 | COMPILER_VERSION_ARCHITECTURE = eval(
55 | check_output(
56 | f'py -{PYTHON_VERSION} -c "from platform import architecture; print(architecture())"'
57 | ).decode("utf-8")
58 | )
59 | COMPILER_PYTHON_VERSION_IS_32_BITS = (
60 | True if COMPILER_VERSION_ARCHITECTURE[0].startswith("32") else False
61 | )
62 |
63 | SYSTEM = system()
64 |
65 | if PYTHON_VERSION:
66 | print(
67 | f"Your chosen compiler Python version is {PYTHON_VERSION} {COMPILER_VERSION_ARCHITECTURE}"
68 | )
69 | if COMPILER_PYTHON_VERSION_IS_32_BITS:
70 | _ = lambda: SYSTEM if SYSTEM != "Darwin" else "MacOS"
71 | print(f"Your {_()} compilation results will have 32-bit compatibility.")
72 |
73 | # SOURCE_FOLDER = Path("./themera copy")
74 | SOURCE_FOLDER = Path("./themera")
75 | path.insert(0, str(SOURCE_FOLDER.resolve()))
76 | from constants import APP_ID, HELP_PATH
77 | from version_and_copyright import COPYRIGHT
78 | from version_and_copyright import __version__ as VERSION
79 |
80 | platforms = {
81 | "Linux": "linux",
82 | "Darwin": "macos",
83 | "Windows": "win",
84 | }
85 |
86 | DESCRIPTION = "PySimpleGUI Theme Code Generator"
87 | APP_NAME = (
88 | f"themera"
89 | f"-v{VERSION}"
90 | f"-{platforms[SYSTEM]}"
91 | f"{('-x86' if COMPILER_PYTHON_VERSION_IS_32_BITS else '-x86_64') if SYSTEM == 'Windows' else ''}"
92 | ).lower()
93 |
94 | if SYSTEM == "Windows":
95 | with open(".uuid", "r") as env:
96 | APP_UUID = env.readline()
97 | if APP_UUID == "":
98 | message = (
99 | "The UUID for compiling the Installer was not found.\n"
100 | "Please request that information from Divine Afam-Ifediogor."
101 | )
102 | raise Exception(message)
103 |
104 | ROOT_PATH = Path(".").resolve()
105 | OUTPUT_PATH = Path(f"bin/v{VERSION.split('.', 1)[0]}/{VERSION}/{APP_NAME}")
106 | NUITKA_OUTPUT_PATH = Path(f"{OUTPUT_PATH}/themera.dist/")
107 | OUTPUT_FILES = tuple(NUITKA_OUTPUT_PATH.rglob("*"))
108 | TOTAL_NUMBER_OF_OUTPUT_FILES = len(OUTPUT_FILES)
109 | INSTALLER_PATH = Path(f"{OUTPUT_PATH}/installer/{APP_NAME}.exe")
110 | YEAR = datetime.now().year
111 | ZIPFILE_PATH = Path(f"{OUTPUT_PATH}/{APP_NAME}.zip")
112 |
113 | COMPANY_NAME, PRODUCT_NAME = (
114 | "Divine Afam-Ifediogor",
115 | "Themera",
116 | ) # This value must match the APP_ID from constants.py.
117 |
118 | ICON_PATH_LINUX = "branding/raster/themera_logo.png"
119 | ICON_PATH_MAC = "branding/raster/themera_logo.icns"
120 | ICON_PATH_WINDOWS = "branding/raster/themera_logo.ico"
121 |
122 | GENERAL_SETTINGS = (
123 | "--follow-imports "
124 | "--remove-output "
125 | f'--output-dir="{OUTPUT_PATH}" '
126 | "--disable-console "
127 | f"--include-data-files={HELP_PATH}={HELP_PATH} "
128 | "--low-memory "
129 | "--standalone "
130 | "--enable-plugin=tk-inter "
131 | # "--run "
132 | )
133 |
134 | WINDOWS_SETTINGS = f'--windows-icon-from-ico="{ICON_PATH_WINDOWS}" '
135 | MAC_SETTINGS = (
136 | f'--macos-app-icon="{ICON_PATH_MAC}"'
137 | f"--macos-signed-app-name={APP_ID}"
138 | f"--macos-app-name={PRODUCT_NAME}"
139 | f"--macos-app-version={VERSION}"
140 | )
141 | LINUX_SETTINGS = f'--linux-icon="{ICON_PATH_LINUX}" '
142 |
143 | OS_SETTINGS = (
144 | WINDOWS_SETTINGS
145 | if running_windows()
146 | else MAC_SETTINGS
147 | if running_mac()
148 | else LINUX_SETTINGS
149 | if running_linux()
150 | else ""
151 | )
152 |
153 | OTHER_SETTINGS = (
154 | f'--company-name="{COMPANY_NAME}" '
155 | f'--product-name="{PRODUCT_NAME}" '
156 | f'--file-version="{VERSION}" '
157 | f'--product-version="{VERSION}" '
158 | f'--file-description="{DESCRIPTION}" '
159 | f'--copyright="{COPYRIGHT}" '
160 | # The source entry
161 | "themera/themera.py"
162 | )
163 |
164 |
165 | def perform_nuitka_compilation():
166 | run(
167 | "py "
168 | f"-{PYTHON_VERSION} "
169 | "-m "
170 | "nuitka "
171 | f"{GENERAL_SETTINGS} "
172 | f"{OS_SETTINGS} "
173 | f"{OTHER_SETTINGS}"
174 | )
175 |
176 |
177 | def update_copyright(filepath):
178 | pattern = re.sub(r"[0-9]{4,4}", "[0-9]{4,4}", COPYRIGHT)
179 | with open(filepath, "r") as current_source_file:
180 | content = current_source_file.read()
181 | content = re.sub(pattern, COPYRIGHT, rf"{content}")
182 | content = content.replace(r"\\n", "\n")
183 | with open(filepath, "w") as current_source_file:
184 | current_source_file.write(content)
185 |
186 |
187 | def run_formatting(filepath):
188 | run(f'isort "{filepath}" --quiet')
189 | run(f'black "{filepath}" --quiet')
190 |
191 |
192 | def update_and_format_source_files():
193 | for file_ in Path(SOURCE_FOLDER).glob("*.py"):
194 | print(f"Updating and formatting {file_}...")
195 | update_copyright(file_)
196 | run_formatting(file_)
197 |
198 |
199 | def write_hashes(filepath: Path):
200 | print(f"Hashing {filepath}...")
201 | hashes_path = filepath.parent
202 | sha256_hash = sha256()
203 | md5_hash = md5()
204 | memview = memoryview(bytearray(128 * 1024))
205 | with open(filepath, "rb") as source:
206 | for n in iter(lambda: source.readinto(memview), 0):
207 | chunk = memview[:n]
208 | sha256_hash.update(chunk)
209 | md5_hash.update(chunk)
210 | with open(
211 | f"{hashes_path}/{filepath.name}.sha256sum.txt",
212 | "w",
213 | ) as sha256_output_file:
214 | sha256_output_file.write(sha256_hash.hexdigest())
215 | with open(
216 | f"{hashes_path}/{filepath.name}.md5sum.txt",
217 | "w",
218 | ) as md5_output_file:
219 | md5_output_file.write(md5_hash.hexdigest())
220 |
221 |
222 | def zip_output_into_archive(remove_output_dir_after=True):
223 | size = len(str(TOTAL_NUMBER_OF_OUTPUT_FILES))
224 | if NUITKA_OUTPUT_PATH.exists():
225 | print(f'Adding output files to archive at "{ZIPFILE_PATH}"')
226 | with ZipFile(ZIPFILE_PATH, "w") as archive:
227 | for index, item in enumerate(OUTPUT_FILES):
228 | print(
229 | f"\r{index+1:4d} of {TOTAL_NUMBER_OF_OUTPUT_FILES} added to archive ({(index/TOTAL_NUMBER_OF_OUTPUT_FILES)*100:.0f}%).",
230 | end="",
231 | )
232 | archive.write(item, item.relative_to(NUITKA_OUTPUT_PATH))
233 | archive.close()
234 | print("", end="")
235 | write_hashes(ZIPFILE_PATH)
236 | print("Done with archiving.")
237 | else:
238 | print("The output path was not found. The archive was not created.")
239 | if remove_output_dir_after:
240 | print("Removing original output files.")
241 | rmtree(NUITKA_OUTPUT_PATH)
242 |
243 |
244 | def prep_innosetup_script():
245 | root_path = str(ROOT_PATH.resolve()).replace("\\", "\\\\")
246 | output_path = str(OUTPUT_PATH.resolve()).replace("\\", "\\\\")
247 |
248 | PATTERNS_TO_REPLACEMENTS = {
249 | r'#define RootPath ".*"': f'#define RootPath "{root_path}"',
250 | r'#define OutputPath ".*"': f'#define OutputPath "{output_path}"',
251 | r'#define ProgramSourcePath ".*"': '#define ProgramSourcePath "'
252 | + output_path
253 | + '\\\\themera.dist"',
254 | r'#define InnoSetupOutputPath ".*"': '#define InnoSetupOutputPath "'
255 | + output_path
256 | + '\\\\installer"',
257 | r'#define MyAppVersion ".*"': f'#define MyAppVersion "{VERSION}"',
258 | r"OutputBaseFilename=.*": f"OutputBaseFilename={APP_NAME}",
259 | r"AppId=\{\{.*\}": "AppId={{" + APP_UUID + "}",
260 | r"AppCopyright=.*": f"AppCopyright={COPYRIGHT}",
261 | }
262 |
263 | with open("template.iss", "r") as script:
264 | content = script.read()
265 | for pattern, replacement in PATTERNS_TO_REPLACEMENTS.items():
266 | content = re.sub(pattern, replacement, content)
267 | with open("temp.iss", "w") as script:
268 | script.write(content)
269 |
270 |
271 | def compile_installer_for_windows():
272 | if SYSTEM == "Windows":
273 | print("Starting compilation of the Windows installer for Themera.")
274 | prep_innosetup_script()
275 | run("iscc /Q temp.iss")
276 | Path("temp.iss").unlink()
277 | if INSTALLER_PATH.is_file():
278 | write_hashes(INSTALLER_PATH)
279 | else:
280 | print("The compilation of the Windows installer failed.")
281 | return
282 | print("The Windows Installer for Themera can only be compiled on Windows.")
283 |
284 |
285 | def update_version_in_readme():
286 | print("Updating version within README.md...")
287 | pattern = r"## Latest Version: v.*"
288 | replacement = f"## Latest Version: v{VERSION}"
289 | with open("README.md", "r") as readme_file:
290 | content = readme_file.read()
291 | content = re.sub(pattern, replacement, content)
292 | with open("README.md", "w") as readme_file:
293 | readme_file.write(content)
294 |
295 |
296 | def git_commit(message: str = f"New Commit at {datetime.now()}"):
297 | print("Staging commit...")
298 | run(f'git commit -m "{message}" -a')
299 | print("Commit completed successfully.")
300 |
301 |
302 | # The following lines are the main controls to this script. Comment and uncomment as desired, but do not change the order.
303 |
304 | update_version_in_readme()
305 | update_and_format_source_files()
306 | perform_nuitka_compilation()
307 | compile_installer_for_windows()
308 | zip_output_into_archive()
309 | git_commit()
310 | print("compile.py has completed execution.")
311 |
--------------------------------------------------------------------------------
/help/help.odt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/help/help.odt
--------------------------------------------------------------------------------
/help/help.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/help/help.pdf
--------------------------------------------------------------------------------
/res/download.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/res/download.png
--------------------------------------------------------------------------------
/res/download.svg:
--------------------------------------------------------------------------------
1 |
2 |
18 |
--------------------------------------------------------------------------------
/screenshots/editor_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/screenshots/editor_dark.png
--------------------------------------------------------------------------------
/screenshots/editor_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/screenshots/editor_light.png
--------------------------------------------------------------------------------
/screenshots/launcher_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/screenshots/launcher_dark.png
--------------------------------------------------------------------------------
/screenshots/launcher_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/definite-d/Themera/38ac6a1f356c7194068136c736570325be4b16dc/screenshots/launcher_light.png
--------------------------------------------------------------------------------
/template.iss:
--------------------------------------------------------------------------------
1 | ; Script generated by the Inno Setup Script Wizard.
2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
3 |
4 | #define RootPath ""
5 | #define OutputPath ""
6 |
7 | #define MyAppName "Themera"
8 | #define MyAppVersion ""
9 | #define MyAppPublisher "Divine Afam-Ifediogor"
10 | #define MyAppURL "https://github.com/definite-d/themera/"
11 | #define MyAppExeName "themera"
12 | #define ProgramSourcePath "{#OutputPath}\themera.dist"
13 | #define InnoSetupOutputPath "{#OutputPath}\installer"
14 |
15 | [Setup]
16 | AppCopyright=
17 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
18 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
19 | AppId={{}
20 | AppName={#MyAppName}
21 | AppVersion={#MyAppVersion}
22 | AppVerName={#MyAppName} v{#MyAppVersion}
23 | AppPublisher={#MyAppPublisher}
24 | AppPublisherURL={#MyAppURL}
25 | AppSupportURL={#MyAppURL}
26 | AppUpdatesURL={#MyAppURL}
27 | DefaultDirName={autopf}\{#MyAppName}
28 | DefaultGroupName={#MyAppName}
29 | AllowNoIcons=yes
30 | LicenseFile={#RootPath}\LICENSE
31 | ; Remove the following line to run in administrative install mode (install for all users.)
32 | PrivilegesRequired=lowest
33 | PrivilegesRequiredOverridesAllowed=dialog
34 | OutputDir={#InnoSetupOutputPath}
35 | OutputBaseFilename=themerasetup
36 | SetupIconFile={#RootPath}\branding\raster\themera_installer_logo.ico
37 | Compression=lzma
38 | SolidCompression=yes
39 | UninstallDisplayName=Uninstall {#MyAppName}
40 | UninstallDisplayIcon={#RootPath}\branding\raster\themera_installer_logo.ico
41 | WizardStyle=modern
42 | WizardImageStretch=yes
43 | WizardSmallImageFile={#RootPath}\branding\raster\themera_small_installer_logo.bmp
44 | WizardImageFile={#RootPath}\branding\raster\themera_installer_sidebar.bmp
45 |
46 | [Languages]
47 | Name: "english"; MessagesFile: "compiler:Default.isl"
48 |
49 | [Tasks]
50 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
51 |
52 | [Files]
53 | Source: "{#ProgramSourcePath}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
54 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
55 |
56 | [Icons]
57 | Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
58 | Name: "{group}\{cm:ProgramOnTheWeb,{#MyAppName}}"; Filename: "{#MyAppURL}"
59 | Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
60 | Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
61 |
62 | [Run]
63 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
64 |
65 |
--------------------------------------------------------------------------------
/themera/color_shorthands.py:
--------------------------------------------------------------------------------
1 | """
2 | d888888P dP
3 | 88 88
4 | 88 88d888b. .d8888b. 88d8b.d8b. .d8888b. 88d888b. .d8888b.
5 | 88 88' `88 88ooood8 88'`88'`88 88ooood8 88' `88 88' `88
6 | 88 88 88 88. ... 88 88 88 88. ... 88 88. .88
7 | dP dP dP `88888P' dP dP dP `88888P' dP `88888P8
8 |
9 | Color Shorthands File
10 | PySimpleGUI Theme Code Generator
11 | Copyright 2023 Divine Afam-Ifediogor
12 | """
13 |
14 | COLOR_SHORTHANDS = {
15 | "affair": "#6f4685",
16 | "africanviolet": "#b284be",
17 | "akakored": "#f07f5e",
18 | "alizarincrimson": "#e32636",
19 | "almond": "#efdecd",
20 | "amethystpurple": "#9966cc",
21 | "applegreen": "#8db600",
22 | "apricot": "#fbceb1",
23 | "asparagus": "#87a96b",
24 | "auburn": "#922724",
25 | "beaver": "#78675d",
26 | "beaverbrown": "#78675d",
27 | "bistre": "#3d2b1f",
28 | "blackbean": "#3d0c02",
29 | "blackolive": "#3b3c36",
30 | "blood": "#8a0707",
31 | "bloodred": "#8a0707",
32 | "bordeaux": "#722f37",
33 | "brickred": "#aa4a44",
34 | "brightred": "#de1738",
35 | "britishracinggreen": "#004225",
36 | "bronze": "#cd7f32",
37 | "brownie": "#2c1608",
38 | "brownishbiege": "#dbb483",
39 | "brownishgreen": "#525025",
40 | "brownishorange": "#b05f03",
41 | "brownishpurple": "#76424e",
42 | "brownishred": "#7b403b",
43 | "brunswickgreen": "#1b4d3e",
44 | "buff": "#f0dc82",
45 | "burgundy": "#800020",
46 | "burntsienna": "#e97451",
47 | "burntumber": "#8a3324",
48 | "byzantium": "#702963",
49 | "byzantiumcolor": "#702963",
50 | "byzantiumcolour": "#702963",
51 | "camel": "#c19a6b",
52 | "candyapplered": "#ff0800",
53 | "caramel": "#ffd59a",
54 | "cardinal": "#bd2031",
55 | "carmine": "#ff0038",
56 | "carnelian": "#b31b1b",
57 | "celadon": "#afe1af",
58 | "cerise": "#de3136",
59 | "cerulean": "#2a52be",
60 | "ceruleanblue": "#2a52be",
61 | "charcoal": "#36454f",
62 | "chestnut": "#954535",
63 | "chineseviolet": "#856088",
64 | "cinnabar": "#e44d2e",
65 | "citron": "#9fa91f",
66 | "claret": "#7f1734",
67 | "cocoa": "#35281e",
68 | "cocoabrown": "#35281e",
69 | "coffee": "#6f4e37",
70 | "copper": "#b87333",
71 | "coquelicot": "#ff3800",
72 | "coral": "#f88379",
73 | "coralpink": "#f88379",
74 | "cordovan": "#893f45",
75 | "cosmiclatte": "#fff8e7",
76 | "darkcherry": "#3b0100",
77 | "darkpurple": "#551a8b",
78 | "dartmouthgreen": "#00693e",
79 | "definite": "#d0ff33",
80 | "desertsand": "#edc9af",
81 | "earlybird": "#cea2fd",
82 | "ebony": "#282c35",
83 | "ecru": "#c2b280",
84 | "eigengrau": "#16161d",
85 | "electricindigo": "#6f00ff",
86 | "electricpurple": "#bf00ff",
87 | "emerald": "#50c878",
88 | "emeraldgreen": "#50c878",
89 | "eminence": "#6c3082",
90 | "englishviolet": "#563c5c",
91 | "evergreen": "#11574a",
92 | "fallow": "#c19578",
93 | "falu": "#7b1818",
94 | "falured": "#7b1818",
95 | "fern": "#4f7942",
96 | "ferngreen": "#4f7942",
97 | "fireengine": "#ce2029",
98 | "fireenginered": "#ce2029",
99 | "flamered": "#cf352e",
100 | "forestgreen": "#228b22",
101 | "fulvous": "#e48400",
102 | "fuschia": "#ca2c92",
103 | "fuschiablue": "#9c51b6",
104 | "goldenbrown": "#bdb76b",
105 | "goodtax": "#c9a0ff",
106 | "grape": "#6f2da8",
107 | "grapecolor": "#6f2da8",
108 | "grapecolour": "#6f2da8",
109 | "greenlake": "#007d69",
110 | "hardwood": "#554545",
111 | "harlequin": "#3fff00",
112 | "harvestgold": "#da9100",
113 | "heliotrope": "#df73ff",
114 | "heliotropepurple": "#df73ff",
115 | "honeydew": "#f0fff0",
116 | "hourbour": "#00ff00",
117 | "huntergreen": "#355e3b",
118 | "iridescentred": "#cc4e5c",
119 | "iris": "#6a5acd",
120 | "irispurple": "#6a5acd",
121 | "islamicgreen": "#009000",
122 | "jade": "#00a86b",
123 | "jadegreen": "#00a86b",
124 | "jet": "#343434",
125 | "junglegreen": "#29ab87",
126 | "kellygreen": "#4cbb17",
127 | "kobicha": "#6b4423",
128 | "lavenderblush": "#fff0f5",
129 | "lawngreen": "#7cfc00",
130 | "licorice": "#1a1110",
131 | "lilac": "#b666d2",
132 | "lion": "#b48648",
133 | "lipstickstain": "#8e4785",
134 | "liver": "#534b4f",
135 | "mahogany": "#c04000",
136 | "mahoganybrown": "#c04000",
137 | "malachite": "#0bda51",
138 | "mardigras": "#880085",
139 | "mauve": "#b784a7",
140 | "midnightgreen": "#004953",
141 | "midnightred": "#360a01",
142 | "mint": "#aaf0d1",
143 | "moss": "#8a9a5b",
144 | "mossgreen": "#8a9a5b",
145 | "mulberry": "#c54b8c",
146 | "myrtle": "#21421e",
147 | "myrtlegreen": "#21421e",
148 | "neongreen": "#39ff14",
149 | "neonred": "#b92e34",
150 | "nightbrown": "#403330",
151 | "northwesternpurple": "#5b3b8c",
152 | "ochre": "#cc7722",
153 | "oldred": "#340d17",
154 | "onyx": "#0f0f0f",
155 | "oxblood": "#4a0000",
156 | "oxfordblue": "#002147",
157 | "palatinate": "#72246c",
158 | "palepurple": "#b19cd9",
159 | "pastelgreen": "#77dd77",
160 | "pastelred": "#ff6961",
161 | "pear": "#d1e231",
162 | "persiangreen": "#00a693",
163 | "persimmon": "#ec5800",
164 | "phthalogreen": "#123524",
165 | "pinegreen": "#01796f",
166 | "pinkishbrown": "#c27e79",
167 | "pinkishred": "#f22952",
168 | "pinklavender": "#fbaed2",
169 | "pizzaedge": "#9a2ca0",
170 | "prussian": "#003153",
171 | "prussianblue": "#003153",
172 | "purpureus": "#9a4eae",
173 | "raisin": "#612302",
174 | "raspberry": "#b3446c",
175 | "rawumber": "#8a3324",
176 | "rebeccapurple": "#663399",
177 | "redleather": "#ab4d50",
178 | "redstone": "#e46b71",
179 | "redviolet": "#c71585",
180 | "redwood": "#5b342e",
181 | "resedagreen": "#587246",
182 | "rikan": "#534a32",
183 | "robineggblue": "#1fcecb",
184 | "rose": "#f5f5dc",
185 | "rosered": "#f5f5dc",
186 | "rosewood": "#65000b",
187 | "rouge": "#a94064",
188 | "royalpurple": "#7851a9",
189 | "ruby": "#9b111e",
190 | "rufous": "#a81c07",
191 | "russet": "#80461b",
192 | "russianviolet": "#32174d",
193 | "rust": "#b7410e",
194 | "sand": "#c2b280",
195 | "sandybrown": "#f4a460",
196 | "sango": "#f8674f",
197 | "sangored": "#f8674f",
198 | "sanguine": "#6d0c0d",
199 | "satoimo": "#654321",
200 | "scarlet": "#ff2400",
201 | "scarletvelvet": "#610706",
202 | "screamingreen": "#76ff7a",
203 | "sealbrown": "#321414",
204 | "sepia": "#56483f",
205 | "sepiabrown": "#56483f",
206 | "shamrockgreen": "#009e60",
207 | "slateblue": "#6a5acd",
208 | "smokyblack": "#100c08",
209 | "studio": "#7851a9",
210 | "sunsetorange": "#fa5f55",
211 | "taupe": "#b38b6d",
212 | "tawny": "#cd5700",
213 | "teal": "#008080",
214 | "terracotta": "#e3735e",
215 | "tetherred": "#f46860",
216 | "thyme": "#5edc1f",
217 | "trueblue": "#0073cf",
218 | "truegarnet": "#680d25",
219 | "turkeyred": "#cd5c5c",
220 | "tyrianpurple": "#66023c",
221 | "ultraviolet": "#645394",
222 | "umber": "#635147",
223 | "verditerblue": "#3d9db3",
224 | "vermillion": "#e34234",
225 | "viridian": "#40826d",
226 | "walnut": "#645452",
227 | "walnutbrown": "#645452",
228 | "wenge": "#645452",
229 | "wine": "#722f37",
230 | "winered": "#9b2242",
231 | "wisteria": "#c9a0dc",
232 | "woodland": "#5f4737",
233 | "yellowgreen": "#9acd32",
234 | "yellowishgreen": "#9acd32",
235 | }
236 |
--------------------------------------------------------------------------------
/themera/constants.py:
--------------------------------------------------------------------------------
1 | """
2 | d888888P dP
3 | 88 88
4 | 88 88d888b. .d8888b. 88d8b.d8b. .d8888b. 88d888b. .d8888b.
5 | 88 88' `88 88ooood8 88'`88'`88 88ooood8 88' `88 88' `88
6 | 88 88 88 88. ... 88 88 88 88. ... 88 88. .88
7 | dP dP dP `88888P' dP dP dP `88888P' dP `88888P8
8 |
9 | Themera Independent Constants File
10 | PySimpleGUI Theme Code Generator
11 | Copyright 2023 Divine Afam-Ifediogor
12 | """
13 |
14 | # IMPORTS ______________________________________________________________________________________________________________
15 | from darkdetect import isLight
16 | from PySimpleGUI import LOOK_AND_FEEL_TABLE, running_mac, running_windows
17 | from version_and_copyright import __version__
18 |
19 | # CONSTANTS ____________________________________________________________________________________________________________
20 | APP_ID = (
21 | f'divineafamifediogor.themera.v{__version__.split(".", 1)[0]}' # Major version only
22 | )
23 |
24 | DEFAULT_SETTINGS_PATH = ".themerasettings"
25 | HELP_PATH = "help/help.pdf"
26 | THEMES = ["Themera Light", "Themera Dark"] + sorted(list(LOOK_AND_FEEL_TABLE.keys()))
27 | DEFAULT_THEME = (
28 | (THEMES[0] if isLight() else THEMES[1]) if isLight() is not None else THEMES[0]
29 | )
30 | EXTERNAL_LINK_ICON = "⇗" # '🔗'
31 | GEAR_ICON = "⚙"
32 | PENCIL_ICON = "✎"
33 | WARNING_ICON = "⚠"
34 | BORDER_UPPER_LIMIT = 100001
35 | BATCH_ACTIONS = [
36 | "-- Choose an action --",
37 | "Select Color",
38 | "Shuffle",
39 | "Interpolate",
40 | "Random Color (All)",
41 | "Random Color (Individual)",
42 | "Brighten Colors",
43 | "Darken Colors",
44 | ]
45 | LAST_USED_BATCH_ACTION = BATCH_ACTIONS[0]
46 | DISPLAY_TO_THEMEDICT = {
47 | "Background": "BACKGROUND",
48 | "Text": "TEXT",
49 | "Input Background": "INPUT",
50 | "Input Text": "TEXT_INPUT",
51 | "Button Background": "BUTTON[1]",
52 | "Button Text": "BUTTON[0]",
53 | "Slider": "SCROLL",
54 | "Progress Bar Trough": "PROGRESS[1]",
55 | "Progress Bar Indicator": "PROGRESS[0]",
56 | "Border": "BORDER",
57 | "Slider Border": "SLIDER_DEPTH",
58 | "Progress Bar Border": "PROGRESS_DEPTH",
59 | "Accent 1": "ACCENT1",
60 | "Accent 2": "ACCENT2",
61 | "Accent 3": "ACCENT3",
62 | "Accent 4": "ACCENT4",
63 | }
64 | SAFE_TO_EXPAND = ["BUTTON", "PROGRESS"]
65 | EXPANSION_FORMAT = "[]"
66 | INDEX_MARKERS = EXPANSION_FORMAT.split("")
67 | THEMEDICT_TO_DISPLAY = {value: key for key, value in DISPLAY_TO_THEMEDICT.items()}
68 | CTRL = "Cmd" if running_mac() else "Ctrl"
69 | CTRL_EVENT = "Command" if running_mac() else "Control"
70 | ALT = "Option" if running_mac() else "Alt"
71 | IMAGE_PREVIEW_SIZE = (
72 | 309,
73 | 229,
74 | ) # Had to hardcode this because Tkinter's methods don't work well with expanded widgets.
75 | CREATE_BUTTON_PADDING = ((3, 2), 3)
76 | BACK_BUTTON_PADDING = ((2, 5), 3)
77 | IMAGE_FILETYPES = [
78 | (
79 | "All Images",
80 | ("*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp", "*.bmp", "*.ico", "*.icns"),
81 | ),
82 | ("PNG Images", ("*.png")),
83 | ("JPEG Images", ("*.jpg", "*.jpeg")),
84 | ("GIF Images", ("*.gif")),
85 | ("WEBP Images", ("*.webp")),
86 | ("Bitmaps", ("*.bmp")),
87 | ("Windows Icons", ("*.ico")) if running_windows() else ("Mac Icons", ("*.icns")),
88 | ]
89 | DEFAULT_COLOR_THEME_FIELDS = [
90 | key
91 | for key in THEMEDICT_TO_DISPLAY.keys()
92 | if "Border" not in THEMEDICT_TO_DISPLAY[key]
93 | ]
94 | DEFAULT_NON_COLOR_THEME_FIELDS = {
95 | "BORDER": 0,
96 | "SLIDER_DEPTH": 0,
97 | "PROGRESS_DEPTH": 0,
98 | }
99 |
100 | IMAGE_INDEX = [0.3, 1, 0.7, 0.1, 0.2, 0, 0.6, 0.4, 0.5, 0.3, 0.7, 0.8, 0.6]
101 | AUTOCONTRAST_INDEX_DARK = [0.99, 0.8, 0.2, 0.1, 0.99, 0.7, 0.8, 0.3, 0.5, 0.6, 0.2, 0.4]
102 | AUTOCONTRAST_INDEX_LIGHT = [0.3, 0.7, 0.3, 0.8, 0.23, 0.3, 0.4, 0.7, 0.2, 0.9, 0.1, 0.6]
103 | DARK_MODE_INDEX = [0.1, 0.9, 0.8, 0.1, 0.3, 0.2, 0.7, 0.8, 0.3, 0.2, 0.4, 0.6, 0.8]
104 | LIGHT_MODE_INDEX = [0.9, 0.1, 0.3, 0.8, 1, 0.9, 0.4, 0.6, 0.2, 0.8, 0.6, 0.4, 0.2]
105 |
106 | DEFAULT_ALIAS = "sg"
107 | DEFAULT_SETTINGS_DICT = {
108 | "psg_alias": DEFAULT_ALIAS,
109 | "theme": DEFAULT_THEME,
110 | "colorboxes": True,
111 | "full_preview_mode": "default",
112 | "image_index": IMAGE_INDEX,
113 | "autocontrast_index_dark": AUTOCONTRAST_INDEX_DARK,
114 | "autocontrast_index_light": AUTOCONTRAST_INDEX_LIGHT,
115 | "dark_mode_index": DARK_MODE_INDEX,
116 | "light_mode_index": LIGHT_MODE_INDEX,
117 | }
118 |
119 | CRASH_REPORT_TITLE_PREFIX = "[Crash]"
120 |
121 | LINK_DEVELOPER = "https://github.com/definite-d"
122 | LINK_GITHUB_REPO = f"{LINK_DEVELOPER}/themera"
123 | LINK_GITHUB_ISSUES = f"{LINK_GITHUB_REPO}/issues"
124 | LINK_NEW_GITHUB_ISSUE = f"{LINK_GITHUB_ISSUES}/new"
125 | LINK_PYSIMPLEGUI_SITE = "https://pysimplegui.org/"
126 | LINK_PYSIMPLEGUI_REPO = "https://github.com/pysimplegui/pysimplegui/"
127 | LINK_DOCS_DL = f"{LINK_GITHUB_REPO}/blob/v2/themera/help/help.rtf"
128 |
129 | DEFAULT_LAYOUT = """# Sample Layout Code
130 | [
131 | [Column([
132 | [Column([
133 | [Column([
134 | [Column([
135 | [
136 | Image(data=EMOJI_BASE64_HAPPY_BIG_SMILE, k=f'tb_icon'),
137 | # Icon and Title
138 | Text('Quick Preview', k=f'tb_title'),
139 | ]
140 | ], pad=(0, 0), k=f'tb_title_and_icon')],
141 | [Column([
142 | [Text('Sample Text; Lorem ipsum dolor sit amet...', k=f'element_text', expand_x=True)],
143 | [Input('Hello world', k=f'element_input', expand_x=True)],
144 | [Button('Button', k=f'element_button')]
145 | ], k=f'element_bg', pad=(0, 0), expand_x=True)]
146 | ], expand_x=True, k=f'tb_container', pad=(2, (0, 2)))],
147 | ], k=f'tb_maincolumn', element_justification='c', pad=(1, 1), expand_x=True)]
148 | ], k=f'visibility_wrap', expand_x=True)]
149 | ]"""
150 |
--------------------------------------------------------------------------------
/themera/crash.py:
--------------------------------------------------------------------------------
1 | """
2 | d888888P dP
3 | 88 88
4 | 88 88d888b. .d8888b. 88d8b.d8b. .d8888b. 88d888b. .d8888b.
5 | 88 88' `88 88ooood8 88'`88'`88 88ooood8 88' `88 88' `88
6 | 88 88 88 88. ... 88 88 88 88. ... 88 88. .88
7 | dP dP dP `88888P' dP dP dP `88888P' dP `88888P8
8 |
9 | Crash Handling File
10 | PySimpleGUI Theme Code Generator
11 | Copyright 2023 Divine Afam-Ifediogor
12 | """
13 | from datetime import datetime
14 | from traceback import format_exception
15 |
16 | from constants import CRASH_REPORT_TITLE_PREFIX, LINK_NEW_GITHUB_ISSUE, WARNING_ICON
17 | from fonts import FONTS
18 | from version_and_copyright import __version__
19 | from window import Window
20 |
21 |
22 | def run_crash_window(error_message, sg):
23 | """
24 | Runs the window that shows the "crashed" error message to the user.
25 | """
26 | main_layout = [
27 | [sg.Text(f"{WARNING_ICON} Fatal Error!", font=FONTS["icon"])],
28 | [sg.Text("Oh no!")],
29 | [sg.Text("It appears Themera has crashed fatally.")],
30 | [sg.Multiline(f"{error_message}", disabled=True, k="error_box", size=(50, 5))],
31 | [sg.HSep()],
32 | [sg.Text("Would you like to open a GitHub issue to report this?")],
33 | [sg.Push(), sg.Button("Yes"), sg.Button("No (and Exit)")],
34 | ]
35 | additional_info_layout = [
36 | [sg.Text(f"{WARNING_ICON} Report on Github", font=FONTS["icon"])],
37 | [
38 | sg.Text(
39 | "Please give any additional feedback on the issue.\n"
40 | "Your system information will be included automatically."
41 | )
42 | ],
43 | [
44 | sg.Multiline(
45 | "-- No additional feedback --", k="additional_info", size=(50, 7)
46 | )
47 | ],
48 | [sg.HSep()],
49 | [sg.Push(), sg.Button("Finish"), sg.Button("Back"), sg.Button("Cancel")],
50 | ]
51 | layout = [
52 | [
53 | sg.Column(main_layout, k="main_panel"),
54 | sg.Column(additional_info_layout, k="additional_info_panel", visible=False),
55 | ],
56 | ]
57 | window = Window("Fatal Error!", layout, element_justification="center")
58 | result = None
59 | while True:
60 | e, v = window()
61 | if e in ("No (and Exit)", None, "Cancel"):
62 | window.close()
63 | break
64 | if e == "Yes":
65 | window["main_panel"](visible=False)
66 | window["additional_info_panel"](visible=True)
67 | if e == "Back":
68 | window["main_panel"](visible=True)
69 | window["additional_info_panel"](visible=False)
70 | if e == "Finish":
71 | result = v["additional_info"]
72 | window.close()
73 | break
74 | return result
75 |
76 |
77 | def handle_crash(exception, sg):
78 | """
79 | Handles the occurrence of a crash given the Exception and the PySimpleGUI Module instance.
80 | """
81 | # First close all windows.
82 | for window in Window.open_windows:
83 | window.close()
84 |
85 | title = f"{CRASH_REPORT_TITLE_PREFIX} {repr(exception)}"
86 | error = "".join(format_exception(exception, exception, exception.__traceback__))
87 | time_of_crash = datetime.now()
88 |
89 | issue_info = run_crash_window(error, sg)
90 |
91 | if issue_info:
92 | from platform import platform, processor
93 | from sys import version as sys_version
94 | from urllib import parse
95 | from webbrowser import open_new_tab as open_link
96 |
97 | from psg_reskinner import __version__ as r_version
98 |
99 | body = (
100 | f"### Error message:\n```shell\n{error}```\n"
101 | f"### User Report/Messages:\n{issue_info}\n"
102 | f"### Time of Crash:\n{time_of_crash}\n"
103 | f"### Platform info:\n{platform()}\n"
104 | f"### Processor:\n{processor()}\n"
105 | f"### Versions:\n"
106 | f"#### Themera Version:\n{__version__}\n"
107 | f"#### PySimpleGUI Version:\n{sg.__version__}\n"
108 | f"#### Reskinner Version:\n{r_version}\n"
109 | f"#### Python Version:\n{sys_version}\n\n"
110 | )
111 |
112 | # Adapted from PySimpleGUI's source.
113 | args = {"title": str(title), "body": str(body)}
114 | link = f"{LINK_NEW_GITHUB_ISSUE}?" + parse.urlencode(args).replace(
115 | "%5Cn", "%0D"
116 | )
117 |
118 | print("\nIt appears Themera has crashed fatally.")
119 | print(body)
120 | print(link)
121 |
122 | open_link(link)
123 |
--------------------------------------------------------------------------------
/themera/custom_preview.py:
--------------------------------------------------------------------------------
1 | """
2 | d888888P dP
3 | 88 88
4 | 88 88d888b. .d8888b. 88d8b.d8b. .d8888b. 88d888b. .d8888b.
5 | 88 88' `88 88ooood8 88'`88'`88 88ooood8 88' `88 88' `88
6 | 88 88 88 88. ... 88 88 88 88. ... 88 88. .88
7 | dP dP dP `88888P' dP dP dP `88888P' dP `88888P8
8 |
9 | Themera Custom Layout Preview Functionality Script
10 | PySimpleGUI Theme Code Generator
11 | Copyright 2023 Divine Afam-Ifediogor
12 | """
13 | from traceback import format_exception
14 | from typing import Dict, List, Optional, Union
15 |
16 | from constants import DEFAULT_LAYOUT
17 | from fonts import FONTS
18 | from pyperclip import paste
19 | from PySimpleGUI import (
20 | EMOJI_BASE64_HAPPY_BIG_SMILE,
21 | LOOK_AND_FEEL_TABLE,
22 | Button,
23 | Element,
24 | Input,
25 | Multiline,
26 | Push,
27 | Text,
28 | theme,
29 | theme_add_new,
30 | )
31 | from window import Window
32 |
33 | ELEMENTS = {element.__name__: element for element in Element.__subclasses__()}
34 | GLOBALS = {"EMOJI_BASE64_HAPPY_BIG_SMILE": EMOJI_BASE64_HAPPY_BIG_SMILE}
35 | GLOBALS.update(ELEMENTS)
36 |
37 |
38 | def custom_layout_preview(
39 | layout: str,
40 | theme_name: str,
41 | themedict: Optional[Dict] = None,
42 | ):
43 | """
44 | Provide your layout without any alias to PySimpleGUI; use the elements themselves only.
45 | The provided layout will be used to create a custom preview window.
46 | Use with caution; this function relies on `eval()`.
47 | :param layout: A string representing the layout code.
48 | :param theme_name: The name of the theme that the preview is working with.
49 | :param themedict: The themedict of the given theme, mainly required if it's not a standard theme.
50 | """
51 | if theme_name not in LOOK_AND_FEEL_TABLE:
52 | if themedict is None:
53 | message = (
54 | f"The theme {theme_name} was not found within the LOOK_AND_FEEL_TABLE, "
55 | f"and its themedict wasn't supplied either."
56 | )
57 | raise KeyError(message)
58 | if themedict:
59 | LOOK_AND_FEEL_TABLE[f"{theme_name}____Themera_temp"] = themedict
60 |
61 | existing_theme: str = theme()
62 | theme(f"{theme_name}____Themera_temp")
63 | try:
64 | layout: List[List[Element]] = eval(layout, GLOBALS)
65 | except Exception as _exception:
66 | theme(existing_theme)
67 | raise (_exception)
68 | window: Window = Window(f"Custom Layout Preview for '{theme_name}' Theme.", layout)
69 | theme(existing_theme)
70 |
71 | while True:
72 | e, v = window.read()
73 | if e in (None, "Exit"):
74 | window.close()
75 | break
76 | if f"{theme_name}____Themera_temp" in LOOK_AND_FEEL_TABLE:
77 | del LOOK_AND_FEEL_TABLE[f"{theme_name}____Themera_temp"]
78 |
79 |
80 | def custom_preview(
81 | present_theme: str,
82 | present_themedict: Dict[str, Union[int, str, tuple, list]],
83 | user_theme_name: str,
84 | user_themedict: Dict[str, Union[int, str, tuple, list]],
85 | ):
86 | """
87 | Gets the custom layout to preview from the user and carries out the preview operation.
88 | """
89 | theme_add_new(present_theme, present_themedict)
90 | theme(present_theme)
91 | layout = [
92 | [Text("Enter your layout", font=FONTS["theme_name"])],
93 | [Text("Please enter the layout code to preview")],
94 | [
95 | Text(
96 | "All PySimpleGUI elements are available; no need for aliases or imports."
97 | )
98 | ],
99 | [Multiline(DEFAULT_LAYOUT, k="user_layout", size=(50, 5), expand_x=True)],
100 | [
101 | Push(),
102 | Button(
103 | "Paste",
104 | tooltip="Paste the contents of the clipboard into the layout code entrybox.",
105 | ),
106 | Button("Preview"),
107 | Button("Cancel"),
108 | ],
109 | ]
110 | window = Window(
111 | "Custom Layout Entry",
112 | layout,
113 | )
114 | while True:
115 | e, v = window.read()
116 |
117 | if e in (None, "Cancel"):
118 | window.close()
119 | break
120 |
121 | elif e == "Paste":
122 | window["user_layout"](paste())
123 |
124 | elif e == "Preview":
125 | try:
126 | custom_layout_preview(v["user_layout"], user_theme_name, user_themedict)
127 | except Exception as exception:
128 | error = format_exception(exception, exception, exception.__traceback__)[
129 | -1
130 | ]
131 | error_window = Window(
132 | "Layout Error",
133 | [
134 | [Text(f"Layout Error", font=FONTS["icon"])],
135 | [
136 | Text(
137 | "There seems to be an issue with the custom layout supplied."
138 | )
139 | ],
140 | [Multiline(error, disabled=True, k="error_box", size=(50, 3))],
141 | [Push(), Button("Close")],
142 | ],
143 | )
144 | while True:
145 | e = error_window.read()[0]
146 | if e in (None, "Close"):
147 | error_window.close()
148 | break
149 |
--------------------------------------------------------------------------------
/themera/filters.py:
--------------------------------------------------------------------------------
1 | """
2 | d888888P dP
3 | 88 88
4 | 88 88d888b. .d8888b. 88d8b.d8b. .d8888b. 88d888b. .d8888b.
5 | 88 88' `88 88ooood8 88'`88'`88 88ooood8 88' `88 88' `88
6 | 88 88 88 88. ... 88 88 88 88. ... 88 88. .88
7 | dP dP dP `88888P' dP dP dP `88888P' dP `88888P8
8 |
9 | Themera Filters File
10 | PySimpleGUI Theme Code Generator
11 | Copyright 2023 Divine Afam-Ifediogor
12 | """
13 |
14 | # IMPORTS ______________________________________________________________________________________________________________
15 | from typing import Callable, Dict, List, Optional, Tuple, Union
16 |
17 | import colour
18 | from functions import (
19 | check_if_color,
20 | flatten_themedict,
21 | invert,
22 | rint,
23 | unflatten_themedict,
24 | )
25 | from settings import SETTINGS
26 |
27 |
28 | # FILTERS ______________________________________________________________________________________________________________
29 | def index_filter(themedict: Dict, index: List) -> Dict:
30 | """
31 | This function essentially rearranges the colors in a given themedict according to positions (each position is a
32 | floating point number ranging from 0 to 1 ) listed in an
33 | index list.
34 |
35 | To give a simple example, given an `index` list `[0.2, 0.8, 0.6, 0.4, 0.5]` and a list of colors
36 | (extracted from the `themedict`) as `['black', 'red', 'yellow', 'cyan', 'white']`, an index filter will rearrange
37 | those colors to give: `['black', 'cyan', 'yellow', 'red', 'yellow']`
38 |
39 | :param themedict: A themedict to work on.
40 | :param index: A list of floating point numbers ranging from 0 to 1.
41 | :return: A re-arranged dict.
42 | """
43 |
44 | flat = flatten_themedict(themedict)
45 | colors = {k: v for k, v in flat.items() if check_if_color(v)}
46 | sorted_colors = sorted(
47 | list(set(colors)), key=lambda x: colour.Color(colors[x]).get_luminance()
48 | )
49 | mapped = []
50 | if len(sorted_colors) <= len(index):
51 | # If we have a list of colors less than the list of indexes:
52 | for _index in index[0 : len(sorted_colors)]:
53 | mapped.append(colors[sorted_colors[rint(_index * len(sorted_colors)) - 1]])
54 | else:
55 | # Otherwise, we need to 'scale' the index list to the colors list.
56 | for n in range(len(colors)):
57 | # Suppose we mentally traverse both the color and index lists simultaneously:
58 | # This is a fraction representing how far we've traversed the color list at the present n:
59 | current_point = n + 1 / len(sorted_colors)
60 | # We can apply the same fraction to the index list and get the corresponding position in the index list.
61 | index_position = rint(current_point * len(index))
62 | # The index list is a list of floating point numbers that indicate which point of the sorted colors should be
63 | # applied at the current index, like a guide on how to "rearrange" the colors by their indexes.
64 | # We'll call that point "mapped_point".
65 | mapped_point = index[
66 | index_position - 1
67 | ] # Minus 1 because indexes start at 0.
68 | # Now we convert the point to a position in the color list.
69 | mapped_position = rint(mapped_point * len(sorted_colors))
70 | # And use that position to get the item that should be at that position.
71 | item = sorted_colors[
72 | mapped_position - 1
73 | ] # Minus 1 because indexes start at 0.
74 | # Get the color for that item.
75 | color = colors[item]
76 | # Finally, add the color to the mapped (result) list.
77 | mapped.append(color)
78 |
79 | register = dict(zip(colors.keys(), mapped))
80 | result = flat.copy()
81 | result.update(register)
82 | return unflatten_themedict(result, themedict)
83 |
84 |
85 | def autocontrast_filter(themedict: Dict):
86 | """
87 | Applies a luminance adjustment filter to tweak the "contrast" of the colors.
88 |
89 | :param themedict: The themedict to apply the filter to.
90 | :return:
91 | """
92 | flat = flatten_themedict(themedict)
93 | luminance = colour.Color(list(flat.values())[0]).get_luminance()
94 | if luminance < 0.5:
95 | lum = [max(round(luminance, 2) * 0.8, 0)] + SETTINGS["autocontrast_index_dark"]
96 | else:
97 | lum = [min(round(luminance, 2) * 1.443, 1)] + SETTINGS[
98 | "autocontrast_index_light"
99 | ]
100 | colors = {k: v for k, v in flat.items() if check_if_color(v)}
101 | # sorted_colors = sorted(list(set(colors)), key=lambda x: colour.Color(colors[x]).get_luminance())
102 | contrasted = list()
103 | for index, color in enumerate(colors.values()):
104 | if len(lum) < len(colors):
105 | colour_object = colour.Color(color)
106 | mapped_index = rint(((index + 1) / len(colors)) * len(lum)) - 1
107 | colour_object.set_luminance(lum[mapped_index])
108 | contrasted.append(colour_object.get_web())
109 | else:
110 | colour_object = colour.Color(color)
111 | colour_object.set_luminance(lum[index])
112 | contrasted.append(colour_object.get_web())
113 | register = dict(zip(colors.keys(), contrasted))
114 | result = flat.copy()
115 | result.update(register)
116 | return unflatten_themedict(result, themedict)
117 |
118 |
119 | def dark_mode_filter(themedict: Dict[str, Union[str, Tuple, List]]):
120 | return index_filter(themedict, index=SETTINGS["dark_mode_index"])
121 |
122 |
123 | def light_mode_filter(themedict: Dict[str, Union[str, Tuple, List]]):
124 | return index_filter(themedict, index=SETTINGS["light_mode_index"])
125 |
126 |
127 | def individual_filter(
128 | action: Callable,
129 | themedict: Dict[str, Union[str, Tuple, List]],
130 | additional_args: Optional[Dict[str, Union[str, Tuple, List]]] = None,
131 | ):
132 | flat = flatten_themedict(themedict)
133 | if additional_args:
134 | colors = {
135 | k: action(v, **additional_args)
136 | for k, v in flat.items()
137 | if check_if_color(v)
138 | }
139 | else:
140 | colors = {k: action(v) for k, v in flat.items() if check_if_color(v)}
141 | result = flat.copy()
142 | result.update(colors)
143 | result = unflatten_themedict(result, themedict)
144 | return result
145 |
146 |
147 | def inverted_filter(themedict: Dict[str, Union[str, Tuple, List]]):
148 | return individual_filter(invert, themedict)
149 |
150 |
151 | def gray_out_filter(themedict: Dict[str, Union[str, Tuple, List]]):
152 | def _action(color):
153 | color = colour.Color(color)
154 | color.set_saturation(0)
155 | return color.get_web()
156 |
157 | return individual_filter(_action, themedict)
158 |
159 |
160 | def transform_filter(color: str, transformation_matrix: List[List[float]]):
161 | color = colour.web2rgb(color)
162 | result = []
163 | for row in transformation_matrix:
164 | sub_result = 0
165 | for index, element in enumerate(row):
166 | sub_result += element * color[index]
167 | result.append(sub_result)
168 | return colour.rgb2web(tuple(result))
169 |
170 |
171 | def protanopia_filter(themedict: Dict[str, Union[str, Tuple, List]]):
172 | matrix = [[0.56667, 0.43333, 0], [0.55833, 0.44167, 0], [0, 0.24167, 0.75833]]
173 | return individual_filter(
174 | transform_filter, themedict, {"transformation_matrix": matrix}
175 | )
176 |
177 |
178 | def protanomaly_filter(themedict: Dict[str, Union[str, Tuple, List]]):
179 | matrix = [[0.81667, 0.18333, 0], [0.33333, 0.66667, 0], [0, 0.125, 0.875]]
180 | return individual_filter(
181 | transform_filter, themedict, {"transformation_matrix": matrix}
182 | )
183 |
184 |
185 | def deuteranopia_filter(themedict: Dict[str, Union[str, Tuple, List]]):
186 | matrix = [[0.625, 0.375, 0], [0.7, 0.3, 0], [0, 0.3, 0.7]]
187 | return individual_filter(
188 | transform_filter, themedict, {"transformation_matrix": matrix}
189 | )
190 |
191 |
192 | def deuteranomaly_filter(themedict: Dict[str, Union[str, Tuple, List]]):
193 | matrix = [[0.8, 0.2, 0], [0, 0.25833, 0.74167], [0, 0.14167, 0.85833]]
194 | return individual_filter(
195 | transform_filter, themedict, {"transformation_matrix": matrix}
196 | )
197 |
198 |
199 | def tritanopia_filter(themedict: Dict[str, Union[str, Tuple, List]]):
200 | matrix = [[0.95, 0.05, 0], [0, 0.43333, 0.56667], [0, 0.475, 0.525]]
201 | return individual_filter(
202 | transform_filter, themedict, {"transformation_matrix": matrix}
203 | )
204 |
205 |
206 | def tritanomaly_filter(themedict: Dict[str, Union[str, Tuple, List]]):
207 | matrix = [[0.96667, 0.03333, 0], [0, 0.73333, 0.26667], [0, 0.18333, 0.81667]]
208 | return individual_filter(
209 | transform_filter, themedict, {"transformation_matrix": matrix}
210 | )
211 |
212 |
213 | def achromatopsia_filter(themedict: Dict[str, Union[str, Tuple, List]]):
214 | matrix = [[0.299, 0.587, 0.114], [0.299, 0.587, 0.114], [0.299, 0.587, 0.114]]
215 | return individual_filter(
216 | transform_filter, themedict, {"transformation_matrix": matrix}
217 | )
218 |
219 |
220 | def achromatomaly_filter(themedict: Dict[str, Union[str, Tuple, List]]):
221 | matrix = [[0.618, 0.32, 0.062], [0.163, 0.775, 0.062], [0.163, 0.32, 0.516]]
222 | return individual_filter(
223 | transform_filter, themedict, {"transformation_matrix": matrix}
224 | )
225 |
226 |
227 | FILTER_MAPPING = {
228 | "-- No Filters --": None,
229 | "Auto Contrast": autocontrast_filter,
230 | "Dark Mode": dark_mode_filter,
231 | "Light Mode": light_mode_filter,
232 | "Gray Out": gray_out_filter,
233 | "Inverted": inverted_filter,
234 | "Achromatomaly": achromatomaly_filter,
235 | "Achromatopsia": achromatopsia_filter,
236 | "Deuteranomaly": deuteranomaly_filter,
237 | "Deuteranopia": deuteranopia_filter,
238 | "Protanomaly": protanomaly_filter,
239 | "Protanopia": protanopia_filter,
240 | "Tritanomaly": tritanomaly_filter,
241 | "Tritanopia": tritanopia_filter,
242 | }
243 |
244 | FILTERS = list(FILTER_MAPPING.keys())
245 |
--------------------------------------------------------------------------------
/themera/fonts.py:
--------------------------------------------------------------------------------
1 | """
2 | d888888P dP
3 | 88 88
4 | 88 88d888b. .d8888b. 88d8b.d8b. .d8888b. 88d888b. .d8888b.
5 | 88 88' `88 88ooood8 88'`88'`88 88ooood8 88' `88 88' `88
6 | 88 88 88 88. ... 88 88 88 88. ... 88 88. .88
7 | dP dP dP `88888P' dP dP dP `88888P' dP `88888P8
8 |
9 | Themera Fonts File
10 | PySimpleGUI Theme Code Generator
11 | Copyright 2023 Divine Afam-Ifediogor
12 | """
13 |
14 | # FONTS ________________________________________________________________________________________________________________
15 | DEFAULT_FONT = "Helvetica"
16 | FONTS = {
17 | "theme_name": (DEFAULT_FONT, 18),
18 | "tagline": (DEFAULT_FONT, 13),
19 | "medium": (DEFAULT_FONT, 11),
20 | "icon": (DEFAULT_FONT, 18),
21 | "regular": (DEFAULT_FONT, 10),
22 | }
23 |
--------------------------------------------------------------------------------
/themera/functions.py:
--------------------------------------------------------------------------------
1 | """
2 | d888888P dP
3 | 88 88
4 | 88 88d888b. .d8888b. 88d8b.d8b. .d8888b. 88d888b. .d8888b.
5 | 88 88' `88 88ooood8 88'`88'`88 88ooood8 88' `88 88' `88
6 | 88 88 88 88. ... 88 88 88 88. ... 88 88. .88
7 | dP dP dP `88888P' dP dP dP `88888P' dP `88888P8
8 |
9 | Themera Independent Functions File
10 | PySimpleGUI Theme Code Generator
11 | Copyright 2023 Divine Afam-Ifediogor
12 | """
13 | # IMPORTS ______________________________________________________________________________________________________________
14 | from random import randint
15 | from typing import Dict, List, Optional, Tuple, Union
16 |
17 | import colour
18 | import PySimpleGUI as sg
19 | from _tkinter import TclError
20 | from constants import (
21 | DISPLAY_TO_THEMEDICT,
22 | EXPANSION_FORMAT,
23 | INDEX_MARKERS,
24 | SAFE_TO_EXPAND,
25 | THEMEDICT_TO_DISPLAY,
26 | )
27 | from PIL import Image
28 | from psg_reskinner import reskin
29 |
30 |
31 | # FUNCTIONS ____________________________________________________________________________________________________________
32 | def rint(value: Union[int, float]) -> int:
33 | """
34 | Stands for "Rounded Integer"; rounds the given value to an integer.
35 |
36 | :param value: An integer or floating point value
37 | :return: An integer
38 | """
39 | return int(round(value, 0))
40 |
41 |
42 | def flatten_themedict(themedict: Dict, targets=SAFE_TO_EXPAND) -> Dict:
43 | """
44 | Reduces themedicts with nested expandable iterable values (e.g. the value for BUTTON in most themedicts) to a new
45 | dict in which each nested value has its own key (e.g. BUTTON[0]: '#ffffff')
46 |
47 | :param themedict: The themedict to work on.
48 | :param targets: This is a list of keys that will be expanded as part of the flattening process. It defaults to the
49 | SAFE_SAFE_TO_EXPAND list.
50 | :return: A flattened Python dictionary.
51 | """
52 | new = {}
53 | for key in themedict.copy():
54 | if key in targets:
55 | for index, item in enumerate(themedict.copy()[key]):
56 | _key = str(key) + EXPANSION_FORMAT.replace("", str(index))
57 | new[_key] = item
58 | continue
59 | new[key] = themedict.copy()[key]
60 | return new
61 |
62 |
63 | def unflatten_themedict(themedict: Dict, original_themedict=Optional[Dict]) -> Dict:
64 | """
65 | Reverses the effects of the `flatten_themedict()` function and returns a Dict similar to a proper PySimpleGUI
66 | themedict.
67 |
68 | :param themedict: The flattened themedict to un-flatten.
69 | :param original_themedict: The original themedict that was fed into the `flatten_themedict()` function. Not
70 | required, but if given, it will be used as a guide to obtain the right iterable type.
71 | :return: An un-flattened themedict.
72 | """
73 | new = {}
74 | for key in themedict:
75 | if INDEX_MARKERS[0] in key and key.endswith(INDEX_MARKERS[1]):
76 | prefix, index = key.rstrip(INDEX_MARKERS[1]).rsplit(INDEX_MARKERS[0], 1)
77 | try:
78 | index = int(index)
79 | except TypeError:
80 | new[key] = themedict[key]
81 | continue
82 | try:
83 | new[prefix].insert(index, themedict[key])
84 | except KeyError:
85 | new[prefix] = [themedict[key]]
86 | continue
87 | new[key] = themedict[key]
88 | if original_themedict:
89 | for key in new:
90 | try:
91 | new[key] = type(original_themedict[key])(new[key])
92 | except (TypeError, ValueError):
93 | pass
94 | return new
95 |
96 |
97 | def get_display_name(themedict_name: str) -> str:
98 | """
99 | "Converts" a given flattened-themedict key to a proper name for displaying in the UI, e.g. BUTTON[0] becomes
100 | 'Button Text'.
101 |
102 | :param themedict_name: A key name from any flattened themedict.
103 | :return: A string.
104 | """
105 | try:
106 | return THEMEDICT_TO_DISPLAY[themedict_name]
107 | except KeyError:
108 | return str(" ").join(
109 | [part.capitalize() for part in (themedict_name).split("_")]
110 | )
111 |
112 |
113 | def get_themedict_name(display_name: str) -> str:
114 | """
115 | "Converts" a given proper name for displaying in the UI to a flattened-themedict key, e.g. 'Button Text' becomes
116 | BUTTON[0].
117 |
118 | :param display_name: Any display name e.g. 'Accent 1'
119 | :return: A string
120 | """
121 | try:
122 | return DISPLAY_TO_THEMEDICT[display_name]
123 | except KeyError:
124 | return str("_").join([part.upper() for part in (display_name).split(" ")])
125 |
126 |
127 | def clamp(value: Union[int, float]):
128 | """
129 | Clamps a given numerical value to between 1 and 0.
130 | """
131 | return min(1, max(0, value))
132 |
133 |
134 | def check_if_color(value: str) -> bool:
135 | """
136 | Checks if the given value is a valid hex-format color or a valid color name.
137 |
138 | :param value: Any string.
139 | :return: True if the value is a valid color, else False.
140 | """
141 | v = str(value)
142 | if (v == "") or (" " in v):
143 | return False
144 | try:
145 | colour.Color(v)
146 | return True
147 | except (ValueError, AttributeError):
148 | return False
149 |
150 |
151 | def random_color() -> str:
152 | """
153 | Generates a random color.
154 |
155 | :return: A hex color string or valid color name.
156 | """
157 | color = "#"
158 | for n in range(3):
159 | color += str(format((randint(0, 255)), "x").zfill(2))
160 | return colour.hex2web(color)
161 |
162 |
163 | def alter_luminance(color: str, factor: float) -> str:
164 | """
165 | This function takes a color, adjusts its luminance value by a given factor (ranging from 0 to 1),
166 | and returns the resulting color.
167 |
168 | :param color: The color to work on.
169 | :param factor: The factor to alter the color's luminance by.
170 | :return: A new color with the luminance alteration applied.
171 | """
172 | color = colour.Color(color)
173 | val = color.get_luminance() * factor if color.get_luminance() else factor
174 | color.set_luminance(
175 | val if 0 <= val <= 1 else (0 if color.get_luminance() <= 0.5 else 1)
176 | )
177 | return color.get_web()
178 |
179 |
180 | def colorbox_text_color(color):
181 | _color = colour.Color(invert(color))
182 | if _color.get_luminance() >= 0.5:
183 | _color.set_luminance(clamp(_color.get_luminance() * 1.5))
184 | else:
185 | _color.set_luminance(clamp(_color.get_luminance() * 0.5))
186 | return _color.get_hex_l()
187 |
188 |
189 | def invert(color: str) -> str:
190 | """
191 | Inverts a given color (e.g. black becomes white)
192 |
193 | :param color: Any valid hex color or color name
194 | :return: An inverted color string
195 | """
196 | color = colour.Color(str(color))
197 | color.set_rgb((1 - color.get_red(), 1 - color.get_green(), 1 - color.get_blue()))
198 | return color.get_web()
199 |
200 |
201 | def get_colors_from_image(image_object: Image.Image, number_of_colors: int) -> List:
202 | """
203 | Extracts the colors in a given image.
204 |
205 | :param image_object: A `PIL.Image.Image` object
206 | :param number_of_colors: The number of colors to extract from the image.
207 | :return: A list of colors of length `number_of_colors`.
208 | """
209 | image = image_object.copy().resize((64, 64)).convert("RGBA")
210 | colors = image.getcolors(maxcolors=image_object.size[0] * image_object.size[1])
211 | result = []
212 | for n in range(number_of_colors):
213 | value = colors[(int((len(colors) * n) / number_of_colors))]
214 | value = (value[1][0] / 255, value[1][1] / 255, value[1][2] / 255)
215 | result.append(colour.rgb2web(value))
216 | return result
217 |
218 |
219 | def reskin_mini_preview_window(
220 | window, key: str, themedict: Dict[str, Union[str, Tuple, List]]
221 | ) -> None:
222 | """
223 | UI utility function.
224 | Made to easily reskin the mini preview window within a given window, along with multiple minor tweaks.
225 |
226 | :param window: The PySimpleGUI window object that has the mini preview.
227 | :param key: The key that was used to instantiate the mini preview's layout.
228 | :param themedict: The current themedict; required to obtain relevant color info.
229 | :return: None
230 | """
231 | targets = [
232 | el.key for el in window.element_list() if f"{key}_element" in str(el.key)
233 | ]
234 | tb_targets = [el.key for el in window.element_list() if f"{key}_tb" in str(el.key)]
235 | sg.theme_add_new("___temp_themera_theme_currently_in_use___", themedict)
236 | reskin(
237 | window,
238 | "___temp_themera_theme_currently_in_use___",
239 | sg.theme,
240 | sg.LOOK_AND_FEEL_TABLE,
241 | target_element_keys=targets,
242 | reskin_background=False,
243 | )
244 | for target in tb_targets:
245 | # Dummy Titlebar elements.
246 | if "maincolumn" in target:
247 | window[target].ParentRowFrame.configure(
248 | background=invert(themedict["BUTTON"][1])
249 | )
250 | else:
251 | window[target].ParentRowFrame.configure(background=themedict["BUTTON"][1])
252 | try:
253 | window[target].widget.configure(
254 | background=themedict["BUTTON"][1]
255 | ) # Should work for all.
256 | window[target].widget.configure(foreground=themedict["BUTTON"][0])
257 | except TclError: # Then the widget doesn't have text.
258 | pass
259 | del sg.LOOK_AND_FEEL_TABLE["___temp_themera_theme_currently_in_use___"]
260 |
261 |
262 | def titlebar_button(
263 | themedict: Dict[str, Union[str, Tuple, List]], symbol: str, key: str
264 | ) -> sg.Text:
265 | """
266 | UI utility function.
267 | Returns a custom titlebar "button" element.
268 |
269 | :param themedict: The current theme dictionary; required to obtain the necessary colors.
270 | :param symbol: The symbol for the button.
271 | :param key: The element's key.
272 | :return: A PySimpleGUI text element.
273 | """
274 | return sg.Text(
275 | symbol,
276 | text_color=themedict["BUTTON"][0],
277 | background_color=themedict["BUTTON"][1],
278 | font=sg.CUSTOM_TITLEBAR_FONT,
279 | k=f"{key}_tb_{symbol}",
280 | )
281 |
282 |
283 | def mini_preview_window_layout(
284 | key: str,
285 | themedict: Dict[str, Union[str, Tuple, List]],
286 | sample_text="Sample Text; Lorem ipsum dolor sit amet...",
287 | input_message="This is a preview of your theme",
288 | ) -> sg.Column:
289 | """
290 | UI utility function.
291 | Generates a layout on demand for the mini preview windows.
292 |
293 | :param key: An string from which all generated elements will derive their key.
294 | :param themedict: The current themedict. Required to obtain the necessary colors.
295 | :param input_message: The message to display within the editable input box.
296 | :return: A PySimpleGUI column containing the entire mini preview window layout.
297 | """
298 | bc = themedict["BUTTON"][1]
299 | return sg.Column(
300 | [ # Mini preview window
301 | [
302 | sg.Column(
303 | [
304 | [
305 | sg.Column(
306 | [
307 | # Dummy Custom Titlebar lifted from PySimpleGUI's source.
308 | [
309 | sg.Column(
310 | [
311 | [
312 | sg.Image(
313 | data=sg.DEFAULT_BASE64_ICON_16_BY_16,
314 | background_color=bc,
315 | k=f"{key}_tb_icon",
316 | ),
317 | # Icon and Title
318 | sg.Text(
319 | "Quick Preview",
320 | text_color=themedict["BUTTON"][
321 | 0
322 | ],
323 | background_color=bc,
324 | k=f"{key}_tb_title",
325 | ),
326 | ]
327 | ],
328 | pad=(0, 0),
329 | background_color=bc,
330 | k=f"{key}_tb_title_and_icon",
331 | ),
332 | sg.Column(
333 | [
334 | [
335 | titlebar_button(
336 | themedict,
337 | sg.SYMBOL_TITLEBAR_MINIMIZE,
338 | key,
339 | ),
340 | titlebar_button(
341 | themedict,
342 | sg.SYMBOL_TITLEBAR_MAXIMIZE,
343 | key,
344 | ),
345 | titlebar_button(
346 | themedict,
347 | sg.SYMBOL_TITLEBAR_CLOSE,
348 | key,
349 | ),
350 | ]
351 | ],
352 | element_justification="r",
353 | expand_x=True,
354 | pad=(0, 0),
355 | background_color=bc,
356 | k=f"{key}_tb_buttons",
357 | ),
358 | ],
359 | [
360 | sg.Column(
361 | [
362 | [
363 | sg.Text(
364 | sample_text,
365 | k=f"{key}_element_text",
366 | expand_x=True,
367 | )
368 | ],
369 | [
370 | sg.Input(
371 | input_message,
372 | k=f"{key}_element_input",
373 | expand_x=True,
374 | )
375 | ],
376 | [
377 | sg.Button(
378 | "Button",
379 | k=f"{key}_element_button",
380 | )
381 | ],
382 | ],
383 | k=f"{key}_element_bg",
384 | pad=(0, 0),
385 | expand_x=True,
386 | )
387 | ],
388 | ],
389 | expand_x=True,
390 | background_color=bc,
391 | k=f"{key}_tb_container",
392 | pad=(2, (0, 2)),
393 | )
394 | ],
395 | ],
396 | k=f"{key}_tb_maincolumn",
397 | element_justification="c",
398 | background_color=bc,
399 | pad=(1, 1),
400 | expand_x=True,
401 | )
402 | ]
403 | ],
404 | k=f"{key}_visibility_wrap",
405 | expand_x=True,
406 | background_color=invert(bc),
407 | )
408 |
--------------------------------------------------------------------------------
/themera/launcher.py:
--------------------------------------------------------------------------------
1 | """
2 | d888888P dP
3 | 88 88
4 | 88 88d888b. .d8888b. 88d8b.d8b. .d8888b. 88d888b. .d8888b.
5 | 88 88' `88 88ooood8 88'`88'`88 88ooood8 88' `88 88' `88
6 | 88 88 88 88. ... 88 88 88 88. ... 88 88. .88
7 | dP dP dP `88888P' dP dP dP `88888P' dP `88888P8
8 |
9 | Themera Launcher Function File
10 | PySimpleGUI Theme Code Generator
11 | Copyright 2023 Divine Afam-Ifediogor
12 | """
13 |
14 | # IMPORTS ______________________________________________________________________________________________________________
15 | from base64 import b64decode, b64encode
16 | from io import BytesIO
17 | from random import getrandbits
18 | from tkinter import StringVar
19 | from typing import Dict, List, Tuple, Union
20 |
21 | import PySimpleGUI as sg
22 | from bytecode import BANNER, SIDEBAR
23 | from colour import Color
24 | from constants import (
25 | BACK_BUTTON_PADDING,
26 | CREATE_BUTTON_PADDING,
27 | DEFAULT_COLOR_THEME_FIELDS,
28 | DEFAULT_NON_COLOR_THEME_FIELDS,
29 | IMAGE_FILETYPES,
30 | IMAGE_INDEX,
31 | IMAGE_PREVIEW_SIZE,
32 | THEMES,
33 | )
34 | from filters import index_filter
35 | from fonts import FONTS
36 | from functions import (
37 | clamp,
38 | get_colors_from_image,
39 | mini_preview_window_layout,
40 | reskin_mini_preview_window,
41 | unflatten_themedict,
42 | )
43 | from open_docs import open_docs
44 | from PIL import Image, ImageTk, UnidentifiedImageError
45 | from preview_panel import PreviewPanel
46 | from version_and_copyright import COPYRIGHT, __version__
47 | from window import Window
48 |
49 |
50 | # FUNCTIONS ____________________________________________________________________________________________________________
51 | def action_button_row(key: str) -> List[sg.Element]:
52 | """
53 | UI utility function.
54 | Generates a row of action buttons for the launcher; `Create` and `Back`.
55 |
56 | :param key: A string from which the action buttons will derive their keys.
57 | :return: A list (row) containing a `Push` element and 2 `Button` elements.
58 | """
59 | return [
60 | sg.Push(),
61 | sg.Button("Create", k=f"{key}_start", pad=CREATE_BUTTON_PADDING),
62 | sg.Button("Back", k=f"{key}_back", pad=BACK_BUTTON_PADDING),
63 | ]
64 |
65 |
66 | def theme_name_row(key: str) -> List[sg.Element]:
67 | """
68 | UI utility function.
69 | Generates a row for the theme name entry box.
70 |
71 | :param key: The element key.
72 | :return: A list containing a text element and the input itself.
73 | """
74 | return [sg.Text("Theme Name", size=(12, 1)), sg.Input(k=f"{key}_themename")]
75 |
76 |
77 | def image_action(image: Image.Image) -> Dict[str, Union[str, Tuple, List]]:
78 | """
79 | This function gets invoked when a new theme is to be created from an image via the launcher.
80 | It gets the colors from the image, makes a themedict out of them and sorts them.
81 |
82 | :param image: A `PIL.Image.Image` object to work on.
83 | :return: A themedict of colors extracted from the image.
84 | """
85 | colors = get_colors_from_image(image, len(DEFAULT_COLOR_THEME_FIELDS))
86 | themedict = unflatten_themedict(dict((zip(DEFAULT_COLOR_THEME_FIELDS, colors))))
87 | themedict.update(DEFAULT_NON_COLOR_THEME_FIELDS)
88 | themedict = index_filter(themedict, IMAGE_INDEX)
89 | return themedict
90 |
91 |
92 | def existing_validation(code: str) -> bool:
93 | """
94 | This function checks if the given theme code is valid or not and returns True or False.
95 |
96 | :param code: A string containing user theme code.
97 | :return: True if the theme code is valid else False.
98 | """
99 | if "{" not in code and "}" not in code:
100 | return False
101 | code = "{" + code.rsplit("{", 1)[1]
102 | code = code.split("}", 1)[0] + "}"
103 | try:
104 | exec(code, {}, {})
105 | except SyntaxError:
106 | return False
107 | return True
108 |
109 |
110 | def existing_action(code):
111 | code = "{" + code.rsplit("{", 1)[1]
112 | code = code.split("}", 1)[0] + "}"
113 | return eval(code, {}, {})
114 |
115 |
116 | def new_action(theme: str) -> Dict[str, Union[str, Tuple, List]]:
117 | """
118 | This function is invoked when a new theme (based on builtin PySimpleGUI themes) is to be created via the launcher.
119 | It simply obtains the appropriate themedict from the `LOOK_LOOK_AND_FEEL_TABLE`.
120 |
121 | :param theme: The name of the builtin PySimpleGUI theme.
122 | :return: The themedict for the given theme.
123 | """
124 | return sg.LOOK_AND_FEEL_TABLE[theme]
125 |
126 |
127 | def get_preview_colors():
128 | col = Color(sg.theme_background_color())
129 | lum = col.get_luminance()
130 | if lum >= 0.5:
131 | col.set_luminance(clamp(lum * 0.8))
132 | preview_bg = col.get_hex()
133 | # col.set_saturation(0.2)
134 | col.set_luminance(lum)
135 | preview_fg = col.get_hex()
136 | else:
137 | col.set_luminance(clamp(lum * 1.8))
138 | preview_bg = col.get_hex()
139 | col.set_saturation(0.2)
140 | col.set_luminance(clamp(lum * 4))
141 | preview_fg = col.get_hex()
142 | del col, lum
143 | return preview_bg, preview_fg
144 |
145 |
146 | def sidebar():
147 | """
148 | Compound element function. Returns the sidebar used in multiple layouts.
149 | """
150 | return sg.Column(
151 | [
152 | [
153 | sg.Image(
154 | data=SIDEBAR,
155 | subsample=3,
156 | pad=(0, 0),
157 | expand_y=True,
158 | )
159 | ]
160 | ],
161 | pad=(0, 0),
162 | expand_y=True,
163 | expand_x=True,
164 | )
165 |
166 |
167 | def Launcher(set_to: str = "main"):
168 | """
169 | This function is responsible for initializing a new launcher upon startup and when requested by the user.
170 |
171 | :return: None
172 | """
173 | themes, default_theme = (
174 | tuple(
175 | theme
176 | for theme in sorted(THEMES)
177 | if sg.COLOR_SYSTEM_DEFAULT not in sg.LOOK_AND_FEEL_TABLE[theme].values()
178 | ),
179 | "Black",
180 | )
181 | image: sg.Optional[Image.Image] = None
182 | preview_bg, preview_fg = get_preview_colors()
183 | layout = [
184 | [
185 | sg.Column(
186 | [
187 | [sg.Image(data=BANNER, subsample=2, pad=(0, 0), expand_x=True)],
188 | [
189 | sg.Push(),
190 | sg.Text(
191 | "The PySimpleGUI Theme Editor",
192 | font=FONTS["tagline"],
193 | ),
194 | sg.Push(),
195 | ],
196 | [
197 | sg.Button(
198 | "New Theme",
199 | font=FONTS["medium"],
200 | k="new_route",
201 | pad=((10, 0), 5),
202 | expand_x=True,
203 | ),
204 | sg.Button(
205 | "Edit Existing Theme",
206 | font=FONTS["medium"],
207 | k="existing_route",
208 | pad=(5, 5),
209 | expand_x=True,
210 | ),
211 | sg.Button(
212 | "Theme from Image",
213 | font=FONTS["medium"],
214 | k="image_route",
215 | pad=((0, 10), 5),
216 | expand_x=True,
217 | ),
218 | ],
219 | [sg.Text(COPYRIGHT, pad=(0, (5, 10)))],
220 | ],
221 | k="main_panel",
222 | visible=set_to == "main",
223 | element_justification="center",
224 | expand_x=True,
225 | expand_y=True,
226 | pad=(0, 0),
227 | ),
228 | sg.Column(
229 | [
230 | [
231 | sidebar(),
232 | sg.Column(
233 | [
234 | [
235 | sg.Text(
236 | "Create New Theme",
237 | font=FONTS["medium"],
238 | expand_x=True,
239 | )
240 | ],
241 | theme_name_row("new"),
242 | [
243 | sg.Text("Based on", size=(9, 1)),
244 | sg.DropDown(
245 | themes,
246 | default_theme,
247 | readonly=True,
248 | expand_x=True,
249 | k="new_theme",
250 | enable_events=True,
251 | ),
252 | ],
253 | [sg.VPush()],
254 | [
255 | mini_preview_window_layout(
256 | "new",
257 | sg.LOOK_AND_FEEL_TABLE[default_theme],
258 | "Some text",
259 | "Mini preview.",
260 | )
261 | ],
262 | [sg.VPush()],
263 | action_button_row("new"),
264 | ],
265 | pad=(5, 5),
266 | expand_x=True,
267 | expand_y=True,
268 | ),
269 | ]
270 | ],
271 | k="new_panel",
272 | visible=set_to == "new",
273 | expand_y=True,
274 | expand_x=True,
275 | pad=(0, 0),
276 | ),
277 | sg.Column(
278 | [
279 | [
280 | sidebar(),
281 | sg.Column(
282 | [
283 | # [sg.VPush()],
284 | [sg.Text("Edit Existing Theme", font=FONTS["medium"])],
285 | theme_name_row("existing"),
286 | [
287 | sg.Multiline(
288 | "~ Your theme's value dict goes here. ~",
289 | k="existing_themecode",
290 | expand_x=True,
291 | expand_y=True,
292 | )
293 | ],
294 | action_button_row("existing"),
295 | # [sg.VPush()],
296 | ],
297 | pad=(5, 5),
298 | expand_x=True,
299 | expand_y=True,
300 | ),
301 | ],
302 | ],
303 | k="existing_panel",
304 | visible=set_to == "existing",
305 | expand_y=True,
306 | expand_x=True,
307 | pad=(0, 0),
308 | ),
309 | sg.Column(
310 | [
311 | [
312 | sidebar(),
313 | sg.Column(
314 | [
315 | # [sg.VPush()],
316 | [
317 | sg.Text(
318 | "Theme from Image",
319 | font=FONTS["medium"],
320 | expand_x=True,
321 | )
322 | ],
323 | theme_name_row("image"),
324 | [
325 | sg.Input(
326 | "~ No image selected. ~",
327 | expand_x=True,
328 | pad=(5, 2),
329 | size=(20, 1),
330 | enable_events=True,
331 | k="image_filepath",
332 | ),
333 | sg.FileBrowse(file_types=IMAGE_FILETYPES),
334 | ],
335 | # [sg.VPush()],
336 | [
337 | # sg.Push(),
338 | sg.Canvas(
339 | expand_x=True,
340 | expand_y=True,
341 | k="image_preview",
342 | pad=(5, (0, 2)),
343 | background_color=preview_bg,
344 | ),
345 | # sg.Push(),
346 | ],
347 | # [sg.VPush()],
348 | action_button_row("image"),
349 | # [sg.VPush()],
350 | ],
351 | pad=(5, 5),
352 | expand_x=True,
353 | expand_y=True,
354 | ),
355 | ],
356 | ],
357 | k="image_panel",
358 | visible=set_to == "image",
359 | expand_y=True,
360 | expand_x=True,
361 | pad=(0, 0),
362 | ),
363 | sg.Column(
364 | [
365 | [
366 | sidebar(),
367 | sg.Column(
368 | [
369 | [sg.VPush()],
370 | [
371 | sg.Text(
372 | "Loading... Please be patient",
373 | font=FONTS["medium"],
374 | expand_x=True,
375 | )
376 | ],
377 | [sg.VPush()],
378 | ],
379 | pad=(5, 5),
380 | expand_x=True,
381 | expand_y=True,
382 | ),
383 | ]
384 | ],
385 | k="loading_panel",
386 | visible=set_to == "loading",
387 | expand_x=True,
388 | expand_y=True,
389 | pad=(0, 0),
390 | ),
391 | ],
392 | ]
393 |
394 | launcher = Window(
395 | f"Themera v{__version__}",
396 | layout,
397 | element_justification="center",
398 | margins=(0, 0),
399 | size=(500, 367),
400 | modal=False,
401 | ).finalize()
402 |
403 | launcher.TKroot.bind(
404 | "",
405 | lambda e: open_docs(),
406 | )
407 |
408 | center_coords = tuple(c / 2 for c in IMAGE_PREVIEW_SIZE)
409 | preview_panel: PreviewPanel = PreviewPanel(launcher["image_preview"], preview_fg)
410 |
411 | name_variable = StringVar(
412 | launcher.TKroot, f"NewTheme{getrandbits(16)}", "theme_name"
413 | )
414 | for element in launcher.element_list():
415 | if str(element.key).endswith("themename"):
416 | element.widget.configure(textvariable=name_variable)
417 | reskin_mini_preview_window(launcher, "new", sg.LOOK_AND_FEEL_TABLE[default_theme])
418 | launcher.set_min_size(launcher.size)
419 | themedict = {}
420 | while True:
421 | e, v = launcher()
422 | if e in (None, "Exit"):
423 | launcher.close()
424 | break
425 |
426 | if e == "new_theme":
427 | reskin_mini_preview_window(
428 | launcher, "new", sg.LOOK_AND_FEEL_TABLE[v["new_theme"]]
429 | )
430 | continue
431 |
432 | if e == "image_filepath":
433 | image = preview_panel.preview(v["image_filepath"])
434 | continue
435 |
436 | if e.endswith("route"):
437 | e = e.split("_", 1)[0]
438 | launcher["main_panel"](visible=False)
439 | launcher[f"{e}_panel"](visible=True)
440 | launcher[f"{e}_start"].BindReturnKey = True
441 | continue
442 |
443 | if e.endswith("back"):
444 | e = e.split("_", 1)[0]
445 | launcher[f"{e}_panel"](visible=False)
446 | launcher[f"main_panel"](visible=True)
447 | launcher[f"{e}_start"].BindReturnKey = False
448 | continue
449 |
450 | if e.endswith("start"):
451 | e = e.split("_", 1)[0]
452 | if e == "existing":
453 | if not existing_validation(v["existing_themecode"]):
454 | sg.PopupError(
455 | "Your themedict is invalid. Please correct it.",
456 | title="Invalid theme dictionary!",
457 | )
458 | continue
459 | themedict = existing_action(v["existing_themecode"])
460 | if e == "new":
461 | themedict = new_action(v["new_theme"])
462 | if e == "image":
463 | if not image:
464 | sg.PopupError("No valid image selected.", title="Invalid image!")
465 | continue
466 | themedict = image_action(image)
467 | launcher["loading_panel"](visible=True)
468 | launcher[f"{e}_panel"](visible=False)
469 | break
470 |
471 | return name_variable.get(), themedict, launcher
472 |
--------------------------------------------------------------------------------
/themera/open_docs.py:
--------------------------------------------------------------------------------
1 | """
2 | d888888P dP
3 | 88 88
4 | 88 88d888b. .d8888b. 88d8b.d8b. .d8888b. 88d888b. .d8888b.
5 | 88 88' `88 88ooood8 88'`88'`88 88ooood8 88' `88 88' `88
6 | 88 88 88 88. ... 88 88 88 88. ... 88 88. .88
7 | dP dP dP `88888P' dP dP dP `88888P' dP `88888P8
8 |
9 | Themera Help Functionality File
10 | PySimpleGUI Theme Code Generator
11 | Copyright 2023 Divine Afam-Ifediogor
12 | """
13 | from os.path import abspath, isfile
14 |
15 | import PySimpleGUI as sg
16 | from constants import HELP_PATH, LINK_DOCS_DL
17 | from window import Window
18 |
19 |
20 | def open_docs():
21 | _path = abspath(HELP_PATH)
22 | if isfile(_path):
23 | if sg.running_windows():
24 | from os import startfile
25 |
26 | startfile(_path)
27 | else:
28 | from os import system
29 |
30 | if sg.running_mac():
31 | _open = "open"
32 | else:
33 | _open = "xdg-open"
34 | system(f"{_open} {_path}")
35 | else:
36 | not_found_window = Window(
37 | "Error Opening Docs!",
38 | [
39 | [sg.Text("The built in help doc could not be opened.")],
40 | [
41 | sg.Push(),
42 | sg.Button("Download the Docs", k="dl"),
43 | sg.Button("Cancel"),
44 | ],
45 | ],
46 | modal=True,
47 | )
48 | choice = not_found_window()[0]
49 | if choice in (None, "Cancel"):
50 | pass
51 | if choice == "dl":
52 | open_link(LINK_DOCS_DL)
53 | not_found_window.close()
54 |
--------------------------------------------------------------------------------
/themera/palette_preview.py:
--------------------------------------------------------------------------------
1 | """
2 | d888888P dP
3 | 88 88
4 | 88 88d888b. .d8888b. 88d8b.d8b. .d8888b. 88d888b. .d8888b.
5 | 88 88' `88 88ooood8 88'`88'`88 88ooood8 88' `88 88' `88
6 | 88 88 88 88. ... 88 88 88 88. ... 88 88. .88
7 | dP dP dP `88888P' dP dP dP `88888P' dP `88888P8
8 |
9 | Themera Palette Preview Functionality Script
10 | PySimpleGUI Theme Code Generator
11 | Copyright 2023 Divine Afam-Ifediogor
12 | """
13 | import PySimpleGUI as sg
14 | from colour import Color
15 | from fonts import FONTS
16 | from functions import (
17 | check_if_color,
18 | clamp,
19 | colorbox_text_color,
20 | flatten_themedict,
21 | invert,
22 | )
23 | from pyperclip import copy
24 | from version_and_copyright import __version__
25 | from window import Window
26 |
27 |
28 | def palette_block(color):
29 | return sg.Button(
30 | color,
31 | key=f"{color}_color",
32 | size=(8, 5),
33 | expand_x=True,
34 | expand_y=True,
35 | button_color=(colorbox_text_color(color), color),
36 | pad=(0, 0),
37 | font=FONTS["medium"],
38 | )
39 |
40 |
41 | def palette_preview(theme_name, themedict):
42 | """
43 | Displays a card type preview of the colors used in the theme.
44 | """
45 | flat = {k: v for k, v in flatten_themedict(themedict).items() if check_if_color(v)}
46 | layout = [
47 | [sg.Text(f"{theme_name} Palette Preview", font=FONTS["icon"])],
48 | [
49 | palette_block(Color(_color).get_web())
50 | for _color in sorted(set(flat.values()), key=lambda x: Color(x).get_hue())
51 | ],
52 | [sg.Text("Click on a button to copy its color.", key="instruction")],
53 | ]
54 | window = Window(
55 | f"Palette Preview of {theme_name} | Themera v{__version__}",
56 | layout,
57 | modal=True,
58 | )
59 | while True:
60 | e, v = window.read()
61 | if e in (None, "Exit"):
62 | window.close()
63 | break
64 | if e.endswith("_color"):
65 | color = e.rsplit("_", 1)[0]
66 | copy(color)
67 | window[e]("Copied!")
68 | window["instruction"](f'Color "{color}" copied to clipboard.')
69 |
70 | def _action():
71 | window[e](color)
72 | window["instruction"]("Click on a button to copy its color.")
73 |
74 | window[e].widget.after(2000, _action)
75 |
--------------------------------------------------------------------------------
/themera/preview_panel.py:
--------------------------------------------------------------------------------
1 | """
2 | d888888P dP
3 | 88 88
4 | 88 88d888b. .d8888b. 88d8b.d8b. .d8888b. 88d888b. .d8888b.
5 | 88 88' `88 88ooood8 88'`88'`88 88ooood8 88' `88 88' `88
6 | 88 88 88 88. ... 88 88 88 88. ... 88 88. .88
7 | dP dP dP `88888P' dP dP dP `88888P' dP `88888P8
8 |
9 | Themera Preview Panel Class File
10 | PySimpleGUI Theme Code Generator
11 | Copyright 2023 Divine Afam-Ifediogor
12 | """
13 |
14 | from tkinter import Canvas
15 | from typing import Optional, Tuple
16 |
17 | import PySimpleGUI as sg
18 | from constants import IMAGE_PREVIEW_SIZE
19 | from fonts import FONTS
20 | from PIL import Image, UnidentifiedImageError
21 | from PIL.ImageTk import PhotoImage
22 |
23 |
24 | class PreviewPanel:
25 | """
26 | This class handles the functionality behind the way the preview panel (for selecting images)
27 | works, and acts as a validator for said images, since if it previews, it'll work for our
28 | purposes.
29 | """
30 |
31 | def __init__(self, element: sg.Canvas, fg: str):
32 | self.canvas_element: sg.Canvas = element
33 | self.canvas: Canvas = self.canvas_element.TKCanvas
34 | self.size: Tuple[int, int] = IMAGE_PREVIEW_SIZE
35 | self.center: Tuple[int, int] = tuple((self.size[0] / 2, self.size[1] / 2))
36 | self.fg: str = fg
37 | self._clear: PhotoImage = PhotoImage(
38 | image=Image.new("RGBA", size=IMAGE_PREVIEW_SIZE, color=(0, 0, 0, 0))
39 | )
40 | self.image: int = self.canvas.create_image(*self.center, image=self._clear)
41 | self.directive: int = self.canvas.create_text(
42 | *self.center,
43 | text='Click "Browse" to select an image.',
44 | font=FONTS["medium"],
45 | fill=self.fg,
46 | )
47 |
48 | def set_directive(self, value: str):
49 | self.canvas.itemconfig(self.directive, text=value)
50 |
51 | def set_image(self, tkimage: PhotoImage):
52 | self.canvas.itemconfig(self.image, image=tkimage)
53 |
54 | def clear_directive(self):
55 | self.set_directive("")
56 |
57 | def clear_image(self):
58 | self.set_image(self._clear)
59 |
60 | def preview(self, filepath: str):
61 | try:
62 | img = Image.open(filepath).convert("RGBA")
63 | except FileNotFoundError:
64 | self.clear_image()
65 | self.set_directive('Click "Browse" to select an image.')
66 | except UnidentifiedImageError:
67 | self.clear_image()
68 | self.set_directive('Invalid Image.\nClick "Browse" to select an image.')
69 | except OverflowError:
70 | self.clear_image()
71 | self.set_directive(
72 | 'There was a problem loading that\nimage. Click "Browse" to select\nan image.'
73 | )
74 | except (ValueError, TypeError):
75 | self.clear_image()
76 | self.set_directive(
77 | "An error occurred while trying\nto process the image. Please try"
78 | '\na different one. Click "Browse" to\nselect an image.'
79 | )
80 | else:
81 | global thumbnail
82 | thumbnail = img.copy()
83 | thumbnail.thumbnail(IMAGE_PREVIEW_SIZE)
84 | thumbnail = PhotoImage(image=thumbnail)
85 | self.set_image(thumbnail)
86 | self.set_directive("")
87 | return img
88 |
--------------------------------------------------------------------------------
/themera/requirements.txt:
--------------------------------------------------------------------------------
1 | colour>=0.1.5
2 | darkdetect>=0.8.0
3 | Pillow
4 | psg_reskinner>=2.3.13
5 | pyperclip>=1.8.2
6 | PySimpleGUI>=4.60.5
7 |
--------------------------------------------------------------------------------
/themera/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | d888888P dP
3 | 88 88
4 | 88 88d888b. .d8888b. 88d8b.d8b. .d8888b. 88d888b. .d8888b.
5 | 88 88' `88 88ooood8 88'`88'`88 88ooood8 88' `88 88' `88
6 | 88 88 88 88. ... 88 88 88 88. ... 88 88. .88
7 | dP dP dP `88888P' dP dP dP `88888P' dP `88888P8
8 |
9 | Themera Settings File
10 | PySimpleGUI Theme Code Generator
11 | Copyright 2023 Divine Afam-Ifediogor
12 | """
13 |
14 | # IMPORTS ______________________________________________________________________________________________________________
15 | from os.path import isfile
16 | from pathlib import Path
17 | from pickle import UnpicklingError, dump, load
18 | from typing import Dict, Optional, Union
19 |
20 | from constants import DEFAULT_SETTINGS_DICT, DEFAULT_SETTINGS_PATH, THEMES
21 | from functions import invert
22 | from psg_reskinner import reskin
23 | from themes import DarkTheme, LightTheme
24 | from window import Window
25 |
26 |
27 | # SETTINGS _____________________________________________________________________________________________________________
28 | class Settings:
29 | """
30 | This class loads Themera settings from a filepath or gets defaults from the `constants` module.
31 | """
32 |
33 | def __init__(self, filepath: Optional[Union[str, Path]] = None) -> None:
34 | filepath = filepath or DEFAULT_SETTINGS_PATH
35 | if isfile(filepath):
36 | print("Getting settings from file.")
37 | self.settings_dict = self.parse_settings_file(filepath)
38 | if not isfile(filepath) or not self.settings_dict:
39 | print("Using default settings.")
40 | self.settings_dict = self.get_defaults()
41 | self.previous_settings = self.settings_dict.copy()
42 |
43 | def __getitem__(self, setting):
44 | return self.settings_dict.get(setting)
45 |
46 | def __setitem__(self, key, value):
47 | self.settings_dict.__setitem__(key, value)
48 |
49 | def save_settings(self, filepath: Optional[Union[str, Path]] = None):
50 | with open(filepath or DEFAULT_SETTINGS_PATH, "wb") as settings_file:
51 | dump(self.settings_dict, settings_file)
52 |
53 | @staticmethod
54 | def parse_settings_file(filepath: Optional[Union[str, Path]]) -> Dict:
55 | with open(filepath or DEFAULT_SETTINGS_PATH, "rb") as settings_file:
56 | settings_dict = None
57 | try:
58 | settings_dict: Dict = load(settings_file)
59 | except (UnpicklingError, EOFError, ValueError):
60 | return None
61 | if type(settings_dict) != dict:
62 | return None
63 | return settings_dict
64 |
65 | @staticmethod
66 | def get_defaults() -> Dict:
67 | settings_dict = DEFAULT_SETTINGS_DICT.copy()
68 | return settings_dict
69 |
70 | def edit(self, editor):
71 | import PySimpleGUI as sg
72 |
73 | layout = [
74 | [
75 | sg.Frame(
76 | "Import Alias",
77 | [
78 | [
79 | sg.Text("import PySimpleGUI as "),
80 | sg.Input(
81 | self["psg_alias"], k="psg_alias_setting", size=(10, 1)
82 | ),
83 | ]
84 | ],
85 | expand_x=True,
86 | ),
87 | sg.Frame(
88 | "UI Theme",
89 | [
90 | [
91 | sg.DropDown(
92 | THEMES,
93 | self["theme"],
94 | k="theme_setting",
95 | enable_events=True,
96 | readonly=True,
97 | )
98 | ],
99 | ],
100 | expand_x=True,
101 | ),
102 | ],
103 | [
104 | sg.Frame(
105 | "Full Preview Mode",
106 | [
107 | [
108 | sg.Radio(
109 | "Default",
110 | group_id="full_preview_mode",
111 | default=True
112 | if self["full_preview_mode"] == "default"
113 | else False,
114 | k="full_preview_mode_default",
115 | ),
116 | sg.Radio(
117 | "Palette",
118 | group_id="full_preview_mode",
119 | default=True
120 | if self["full_preview_mode"] == "palette"
121 | else False,
122 | k="full_preview_mode_palette",
123 | ),
124 | sg.Radio(
125 | "Custom",
126 | group_id="full_preview_mode",
127 | default=True
128 | if self["full_preview_mode"] == "custom"
129 | else False,
130 | k="full_preview_mode_custom",
131 | ),
132 | ]
133 | ],
134 | expand_x=True,
135 | ),
136 | sg.Frame(
137 | "Colorboxes",
138 | [
139 | [
140 | sg.Checkbox(
141 | "Enabled",
142 | default=self["colorboxes"],
143 | k="colorboxes_setting",
144 | ),
145 | ]
146 | ],
147 | expand_x=True,
148 | ),
149 | ],
150 | [
151 | sg.Frame(
152 | "Filter Indexes",
153 | [
154 | [
155 | sg.Text(f"Images", size=(15, 1)),
156 | sg.Input(
157 | str(self["image_index"]),
158 | k="image_index_setting",
159 | expand_x=True,
160 | ),
161 | ],
162 | [
163 | sg.Text("Auto Contrast (Dark)", size=(15, 1)),
164 | sg.Input(
165 | str(self["autocontrast_index_dark"]),
166 | k="autocontrast_index_dark_setting",
167 | expand_x=True,
168 | ),
169 | ],
170 | [
171 | sg.Text("Auto Contrast (Light)", size=(15, 1)),
172 | sg.Input(
173 | str(self["autocontrast_index_light"]),
174 | k="autocontrast_index_light_setting",
175 | expand_x=True,
176 | ),
177 | ],
178 | [
179 | sg.Text("Dark Mode", size=(15, 1)),
180 | sg.Input(
181 | str(self["dark_mode_index"]),
182 | k="dark_mode_index_setting",
183 | expand_x=True,
184 | ),
185 | ],
186 | [
187 | sg.Text("Light Mode", size=(15, 1)),
188 | sg.Input(
189 | str(self["light_mode_index"]),
190 | k="light_mode_index_setting",
191 | expand_x=True,
192 | ),
193 | ],
194 | ],
195 | expand_x=True,
196 | )
197 | ],
198 | [sg.HSep()],
199 | [
200 | sg.Push(),
201 | sg.Button("Save"),
202 | sg.Button("Apply"),
203 | sg.Button("Cancel"),
204 | ],
205 | ]
206 | settings_window = Window("Themera Settings", layout, modal=True)
207 | while True:
208 | e, v = settings_window()
209 |
210 | if e in (None, "Cancel"):
211 | settings_window.close()
212 | break
213 |
214 | if e == "theme_setting":
215 | reskin(
216 | settings_window,
217 | v["theme_setting"],
218 | sg.theme,
219 | sg.LOOK_AND_FEEL_TABLE,
220 | target_element_keys=["theme_setting"],
221 | reskin_background=False,
222 | )
223 | settings_window["theme_setting"].ParentRowFrame.configure(
224 | background=sg.theme_background_color()
225 | )
226 |
227 | if e in ("Apply", "Save"):
228 | if v["theme_setting"] != self["theme"]:
229 | reskin(
230 | settings_window,
231 | self["theme"],
232 | sg.theme,
233 | sg.LOOK_AND_FEEL_TABLE,
234 | True,
235 | honor_previous=False,
236 | )
237 | reskin(
238 | settings_window,
239 | v["theme_setting"],
240 | sg.theme,
241 | sg.LOOK_AND_FEEL_TABLE,
242 | True,
243 | honor_previous=False,
244 | )
245 | settings_window.customize_titlebar()
246 |
247 | for window in settings_window.open_windows:
248 | if v["theme_setting"] != self["theme"]:
249 | reskin(
250 | window,
251 | v["theme_setting"],
252 | sg.theme,
253 | sg.LOOK_AND_FEEL_TABLE,
254 | True,
255 | honor_previous=False,
256 | )
257 | sg.set_options(border_width=0)
258 | window.customize_titlebar()
259 |
260 | if window.editor_object:
261 | if v["theme_setting"] != self["theme"]:
262 | window.editor_object.update_quick_preview(
263 | editor.preview_themedict
264 | )
265 |
266 | if v["psg_alias_setting"] != self["psg_alias"]:
267 | self["psg_alias"] = v["psg_alias_setting"]
268 | window.editor_object.update_theme_code(
269 | window.editor_object.generate_theme_code(
270 | window.editor_object.themedict
271 | )
272 | )
273 |
274 | for k in window.key_dict.keys():
275 | if v["theme_setting"] != self["theme"]:
276 | if str(k).endswith("_warning"):
277 | window[k](text_color=sg.theme_background_color())
278 | if (
279 | v["colorboxes_setting"] != self["colorboxes"]
280 | or v["theme_setting"] != self["theme"]
281 | ):
282 | if (
283 | str(k).endswith("value")
284 | and window[k].metadata == "is_color"
285 | ):
286 | if v["colorboxes_setting"]:
287 | window.editor_object._update_color_value(
288 | k, window[k].widget.get()
289 | )
290 | else:
291 | fg = sg.theme_input_text_color()
292 | bg = sg.theme_input_background_color()
293 | window[k](background_color=bg, text_color=fg)
294 |
295 | errors = False
296 | for _k, _v in v.items():
297 | if _k.endswith("_setting"):
298 | if str(v[_k]).startswith("["):
299 | try:
300 | v[_k] = eval(v[_k])
301 | except (NameError, SyntaxError) as error:
302 | sg.PopupError(
303 | f"Invalid entry:\n{v[_k]}", title="Invalid entry!"
304 | )
305 | errors = True
306 | break
307 | if not errors:
308 | self[_k[:-8]] = v[_k]
309 | self["full_preview_mode"] = {
310 | v["full_preview_mode_default"]: "default",
311 | v["full_preview_mode_palette"]: "palette",
312 | v["full_preview_mode_custom"]: "custom",
313 | }[
314 | True
315 | ] # Backwards-compatible (<3.10) hack for emulating switch statements.
316 | # print(self.settings_dict)
317 |
318 | if e == "Save" and not errors:
319 | if self.settings_dict != self.previous_settings:
320 | self.save_settings()
321 | settings_window.close()
322 | break
323 |
324 |
325 | # SETTINGS OBJECT ______________________________________________________________________________________________________
326 | SETTINGS = Settings()
327 |
--------------------------------------------------------------------------------
/themera/themera.py:
--------------------------------------------------------------------------------
1 | """
2 | d888888P dP
3 | 88 88
4 | 88 88d888b. .d8888b. 88d8b.d8b. .d8888b. 88d888b. .d8888b.
5 | 88 88' `88 88ooood8 88'`88'`88 88ooood8 88' `88 88' `88
6 | 88 88 88 88. ... 88 88 88 88. ... 88 88. .88
7 | dP dP dP `88888P' dP dP dP `88888P' dP `88888P8
8 |
9 | Themera Main File
10 | PySimpleGUI Theme Code Generator
11 | Copyright 2023 Divine Afam-Ifediogor
12 | """
13 | # IMPORTS ______________________________________________________________________________________________________
14 | from os.path import abspath, isfile
15 | from random import shuffle
16 | from tkinter import colorchooser
17 | from typing import Dict, List, Optional, Tuple, Union
18 | from webbrowser import open_new_tab as open_link
19 |
20 | import colour
21 | import PySimpleGUI as sg
22 | from _tkinter import TclError
23 | from bytecode import THEMERA_LOGO
24 | from color_shorthands import COLOR_SHORTHANDS
25 | from constants import (
26 | ALT,
27 | APP_ID,
28 | BATCH_ACTIONS,
29 | BORDER_UPPER_LIMIT,
30 | CTRL,
31 | CTRL_EVENT,
32 | EXTERNAL_LINK_ICON,
33 | GEAR_ICON,
34 | LINK_DEVELOPER,
35 | LINK_GITHUB_REPO,
36 | LINK_NEW_GITHUB_ISSUE,
37 | LINK_PYSIMPLEGUI_SITE,
38 | PENCIL_ICON,
39 | WARNING_ICON,
40 | )
41 | from crash import handle_crash
42 | from custom_preview import custom_preview
43 | from filters import FILTER_MAPPING, FILTERS
44 | from fonts import FONTS
45 | from functions import (
46 | alter_luminance,
47 | check_if_color,
48 | colorbox_text_color,
49 | flatten_themedict,
50 | get_display_name,
51 | invert,
52 | mini_preview_window_layout,
53 | random_color,
54 | reskin_mini_preview_window,
55 | rint,
56 | unflatten_themedict,
57 | )
58 | from launcher import Launcher
59 | from open_docs import open_docs
60 | from palette_preview import palette_preview
61 | from pyperclip import copy
62 | from settings import SETTINGS
63 | from themes import DarkTheme, LightTheme
64 | from version_and_copyright import __version__
65 | from window import Window
66 |
67 | sg.Window = Window
68 | # SETTINGS.save_settings()
69 |
70 | # FUNCTIONS AND UI _____________________________________________________________________________________________________
71 | sg.set_options(
72 | dpi_awareness=True, font=FONTS["regular"], icon=THEMERA_LOGO, border_width=0
73 | )
74 | sg.theme_add_new("Themera Light", LightTheme)
75 | sg.theme_add_new("Themera Dark", DarkTheme)
76 | sg.theme(SETTINGS["theme"])
77 | if sg.running_windows():
78 | import ctypes
79 |
80 | try:
81 | ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
82 | APP_ID
83 | ) # Adapted from PySimpleGUI source.
84 | except Exception as e:
85 | print("Failed to set App ID for Windows.")
86 |
87 |
88 | # This variable is used to tell Themera whether to run again after being exited. It is used for returning to the
89 | # launcher while avoiding event loop nesting.
90 | relaunch_options: Optional[List[Union[Window, str]]] = None
91 |
92 |
93 | def menudef_to_shortcut_router_dict(menudef):
94 | return {
95 | tuple(subsection.split("(")[1].lstrip(" ")[0:-1].split("+")): subsection.split(
96 | " ("
97 | )[0].replace("&", "")
98 | for section in menudef
99 | for subsection in section[1]
100 | if subsection.endswith(")")
101 | }
102 |
103 |
104 | def themedict_entry(name, value, name_size=16, value_size=10):
105 | if check_if_color(value):
106 | text = (
107 | colorbox_text_color(value)
108 | if SETTINGS["colorboxes"]
109 | else sg.theme_input_text_color()
110 | )
111 | bg = value if SETTINGS["colorboxes"] else sg.theme_input_background_color()
112 | return (
113 | sg.Checkbox(
114 | f"{get_display_name(name)}",
115 | k=f"{name}_entryname",
116 | enable_events=True,
117 | pad=(0, 0),
118 | size=(name_size, 1),
119 | tooltip=f"{get_display_name(name)}",
120 | ),
121 | sg.Text(
122 | WARNING_ICON,
123 | k=f"{name}_warning",
124 | font=FONTS["icon"],
125 | pad=(0, 0),
126 | text_color=sg.theme_background_color(),
127 | ),
128 | sg.Input(
129 | value,
130 | k=f"{name}_value",
131 | size=(value_size, 1),
132 | expand_x=True,
133 | metadata="is_color",
134 | text_color=text,
135 | background_color=bg,
136 | ),
137 | sg.Text(
138 | PENCIL_ICON,
139 | k=f"{name}_pickcolor",
140 | font=FONTS["icon"],
141 | enable_events=True,
142 | pad=((0, 3), 0),
143 | ),
144 | )
145 | if type(value) == int:
146 | return (
147 | sg.Text(
148 | f"{get_display_name(name)}",
149 | k=f"{name}_entryname",
150 | pad=((23, 0), 0),
151 | size=(name_size, 1),
152 | tooltip=f"{get_display_name(name)}",
153 | ),
154 | sg.Text(
155 | WARNING_ICON,
156 | k=f"{name}_warning",
157 | font=FONTS["icon"],
158 | pad=(0, 0),
159 | text_color=sg.theme_background_color(),
160 | ),
161 | sg.Spin(
162 | [n for n in range(0, BORDER_UPPER_LIMIT)],
163 | value,
164 | k=f"{name}_value",
165 | expand_x=True,
166 | size=(value_size, 1),
167 | metadata=type(value),
168 | ),
169 | )
170 | if type(value) == float:
171 | return (
172 | sg.Text(
173 | f"{get_display_name(name)}",
174 | k=f"{name}_entryname",
175 | pad=((23, 0), 0),
176 | size=(name_size, 1),
177 | tooltip=f"{get_display_name(name)}",
178 | ),
179 | sg.Text(
180 | WARNING_ICON,
181 | k=f"{name}_warning",
182 | font=FONTS["icon"],
183 | pad=(0, 0),
184 | text_color=sg.theme_background_color(),
185 | ),
186 | sg.Spin(
187 | [n / 10 for n in range(0, BORDER_UPPER_LIMIT)],
188 | value,
189 | k=f"{name}_value",
190 | expand_x=True,
191 | size=(value_size, 1),
192 | metadata=type(value),
193 | ),
194 | )
195 | return (
196 | sg.Text(
197 | f"{get_display_name(name)}",
198 | k=f"{name}_entryname",
199 | pad=((23, 0), 0),
200 | size=(name_size, 1),
201 | tooltip=f"{get_display_name(name)}",
202 | ),
203 | sg.Text(
204 | WARNING_ICON,
205 | k=f"{name}_warning",
206 | font=FONTS["icon"],
207 | pad=(0, 0),
208 | text_color=sg.theme_background_color(),
209 | ),
210 | sg.Input(
211 | str(value),
212 | k=f"{name}_value",
213 | expand_x=True,
214 | size=(value_size, 1),
215 | metadata=type(value),
216 | ),
217 | )
218 |
219 |
220 | def get_editor_window_layout(
221 | theme_name: str,
222 | themedict: Dict[str, Union[str, Tuple, List]],
223 | shortcut_router: Dict[Tuple, Union[str, Tuple, List]],
224 | ):
225 | menu_layout = [
226 | [
227 | "&Theme",
228 | [
229 | f"&Create New Theme ({CTRL}+N)",
230 | f"&Edit Existing Theme ({CTRL}+Shift+N)",
231 | f"&Theme From Image ({CTRL}+{ALT}+N)",
232 | f"Return to &Launcher ({CTRL}+{ALT}+L)",
233 | f"&Settings ({CTRL}+Shift+S)",
234 | f"&Revert to Beginning ({CTRL}+{ALT}+R)",
235 | ],
236 | ],
237 | [
238 | "&Help",
239 | [
240 | "&Themera Help (F1)",
241 | f"&Report Issue on GitHub {EXTERNAL_LINK_ICON}",
242 | f"&PySimpleGUI Docs {EXTERNAL_LINK_ICON}",
243 | "&View valid color names",
244 | ],
245 | ],
246 | [
247 | "&Links",
248 | [
249 | f"&Visit Themera's GitHub Page {EXTERNAL_LINK_ICON}",
250 | f"&Developer's GitHub Profile {EXTERNAL_LINK_ICON}",
251 | ],
252 | ],
253 | ]
254 | shortcut_router.update(menudef_to_shortcut_router_dict(menu_layout))
255 | top_layout = [
256 | [
257 | sg.Column(
258 | [
259 | [
260 | sg.Text(theme_name, k="theme_name", font=FONTS["theme_name"]),
261 | sg.Input(
262 | theme_name,
263 | k="theme_name_value",
264 | font=FONTS["theme_name"],
265 | visible=False,
266 | size=(15, 1),
267 | ),
268 | sg.Text(
269 | PENCIL_ICON,
270 | k="edit_theme_name",
271 | enable_events=True,
272 | visible=False,
273 | font=FONTS["icon"],
274 | ),
275 | ]
276 | ],
277 | pad=(0, 0),
278 | k="theme_name_container",
279 | ),
280 | sg.Push(),
281 | sg.Text(GEAR_ICON, k="Settings", font=FONTS["icon"], enable_events=True),
282 | ],
283 | ]
284 | edit_frame_layout = [
285 | [sg.Checkbox("Select All", k="select_all", enable_events=True, pad=(0, 0))]
286 | ]
287 | themedict_flat = flatten_themedict(themedict)
288 | max_name_size = rint(
289 | max([len(x) for x in [get_display_name(name) for name in themedict_flat]])
290 | * 0.83
291 | )
292 | for name, value in themedict_flat.items():
293 | edit_frame_layout.append(themedict_entry(name, value, max_name_size))
294 | edit_frame_layout.append(
295 | [
296 | sg.Column(
297 | [
298 | [
299 | sg.DropDown(
300 | BATCH_ACTIONS,
301 | BATCH_ACTIONS[0],
302 | expand_x=True,
303 | k="entry_action_dropdown",
304 | readonly=True,
305 | disabled=True,
306 | enable_events=True,
307 | tooltip="Batch Actions",
308 | ),
309 | sg.Button("Execute", k="execute_entry_action", disabled=True),
310 | ]
311 | ],
312 | k="entry_action_container",
313 | pad=(0, 0),
314 | expand_x=True,
315 | )
316 | ]
317 | )
318 | left_layout = [
319 | [
320 | sg.Frame(
321 | "Theme Values",
322 | edit_frame_layout,
323 | k="edit_frame",
324 | element_justification="c",
325 | expand_x=True,
326 | expand_y=True,
327 | )
328 | ]
329 | ]
330 | right_layout = [
331 | [
332 | sg.Frame(
333 | "Theme Preview and Generation",
334 | [
335 | [
336 | sg.DropDown(
337 | FILTERS,
338 | FILTERS[0],
339 | k="filter",
340 | readonly=True,
341 | expand_x=True,
342 | pad=((5, 5), (2, 8)),
343 | enable_events=True,
344 | ),
345 | ],
346 | [mini_preview_window_layout("quickpreview", themedict)],
347 | [
348 | sg.Multiline(
349 | expand_y=True, expand_x=True, k="theme_code", disabled=True
350 | )
351 | ],
352 | [
353 | sg.Button("Copy Theme Code", k="copy", pad=(4, 4)),
354 | sg.Button("Full Preview", k="full_preview", pad=(4, 4)),
355 | sg.Button(
356 | "Apply Filter", k="apply_filter", pad=(4, 4), disabled=True
357 | ),
358 | ],
359 | ],
360 | expand_y=True,
361 | expand_x=True,
362 | element_justification="c",
363 | )
364 | ]
365 | ]
366 | return [
367 | [sg.Menu(menu_layout)],
368 | [sg.Column(top_layout, expand_x=True)],
369 | [
370 | sg.Column(left_layout, expand_y=True, expand_x=True, pad=((2, 1), 2)),
371 | sg.Column(
372 | right_layout,
373 | expand_y=True,
374 | expand_x=True,
375 | element_justification="c",
376 | pad=((1, 2), 2),
377 | ),
378 | ],
379 | ]
380 |
381 |
382 | class Editor:
383 | def __init__(self, theme_name, themedict=None):
384 | self.copy_id = None
385 | self.theme_name = theme_name
386 | self.shortcut_router = {
387 | (CTRL, "Shift", "c"): "copy",
388 | (CTRL, "Shift", "e"): "execute_entry_action",
389 | (CTRL, "p"): "full_preview",
390 | (CTRL, "Shift", "a"): "select_all_kb",
391 | }
392 | self.shortcut_key = None
393 | self.color_shorthands = COLOR_SHORTHANDS.copy()
394 | self.original_themedict = (
395 | themedict.copy() if themedict else sg.LOOK_AND_FEEL_TABLE[theme_name].copy()
396 | )
397 | self.values = {
398 | f"{k}_value": v
399 | for k, v in flatten_themedict(self.original_themedict).items()
400 | }
401 | self.invalids = set()
402 | self.previous_themedict = self.original_themedict.copy()
403 | self.preview_themedict = self.original_themedict.copy()
404 | self.window = Window(
405 | f"'{self.theme_name}' Theme | Themera v{__version__} Editor",
406 | get_editor_window_layout(
407 | self.theme_name, self.original_themedict.copy(), self.shortcut_router
408 | ),
409 | modal=False,
410 | resizable=True,
411 | )
412 | self.window.editor_object = self
413 | self.window.finalize()
414 |
415 | for k, v in self.shortcut_router.items():
416 | k = k[:-1] + tuple(k[-1].upper())
417 | v = v[:-3] if v.endswith("_kb") else v
418 | if v in self.window.key_dict.keys():
419 | self.window[v].set_tooltip("+".join(k))
420 |
421 | self.window["theme_name"].widget.bind(
422 | "", lambda e: self.window["edit_theme_name"](visible=True)
423 | )
424 | self.window["theme_name_container"].widget.bind(
425 | "", lambda e: self.window["edit_theme_name"](visible=False)
426 | )
427 |
428 | self.window["edit_theme_name"].widget.bind(
429 | "", lambda e: self.edit_theme_name_action()
430 | )
431 | self.window["theme_name"].widget.bind(
432 | "", lambda e: self.edit_theme_name_action()
433 | )
434 | self.window["theme_name_container"].widget.bind(
435 | "", lambda e: self.edit_theme_name_action()
436 | )
437 |
438 | self.window["theme_name_value"].widget.bind(
439 | "", lambda e: self.theme_name_value_action()
440 | )
441 | self.window["theme_name_value"].widget.bind(
442 | "", lambda e: self.theme_name_value_action()
443 | )
444 | self.window["theme_name_value"].widget.bind(
445 | "", lambda e: self.theme_name_value_action()
446 | )
447 | for shortcut, key in self.shortcut_router.items():
448 | bindstr = (
449 | f"<"
450 | f'{f"{CTRL_EVENT}-" if CTRL in shortcut else ""}'
451 | f'{"Shift-" if "Shift" in shortcut else ""}'
452 | f'{f"{ALT}-" if ALT in shortcut else ""}'
453 | )
454 | # Bind for both caps and lower because Tk is quirky about case.
455 | self.window.TKroot.bind(
456 | bindstr + f"KeyPress-{shortcut[-1].upper()}>",
457 | lambda e, key=key: setattr(self, "shortcut_key", key),
458 | )
459 | try:
460 | self.window.TKroot.bind(
461 | bindstr + f"KeyPress-{shortcut[-1].lower()}>",
462 | lambda e, key=key: setattr(self, "shortcut_key", key),
463 | )
464 | except TclError:
465 | pass
466 | self.update_quick_preview(self.original_themedict)
467 | self.update_theme_code(self.generate_theme_code(self.original_themedict))
468 | self.window.bring_to_front()
469 | self.window.force_focus()
470 |
471 | def __call__(self, *args, **kwargs):
472 | self.read()
473 |
474 | def _update_color_value(self, key, new_value):
475 | self.window[key](new_value)
476 | if SETTINGS["colorboxes"]:
477 | text_color = colorbox_text_color(new_value)
478 | self.window[key].widget["insertbackground"] = text_color
479 | self.window[key].widget["background"] = new_value
480 | self.window[key].widget["foreground"] = text_color
481 | self.window[key].widget["highlightbackground"] = text_color
482 | self.window[key].widget["highlightcolor"] = new_value
483 | # self.window[key](
484 | # background_color=new_value,
485 | # text_color=text_color,
486 | # )
487 | # self.window[key].widget.configure(**config)
488 | else:
489 | self.window[key](
490 | background_color=sg.theme_input_background_color(),
491 | text_color=sg.theme_input_text_color(),
492 | )
493 |
494 | def color_picker(self, targets: List):
495 | if check_if_color(self.values[f"{targets[0]}_value"]):
496 | display_names = [get_display_name(name) for name in targets]
497 | title = (
498 | f"{display_names[0]} Color"
499 | if len(display_names) == 1
500 | else f'Color Picker ({", ".join(display_names)})'
501 | )
502 | color = colorchooser.askcolor(
503 | parent=self.window.TKroot,
504 | color=self.values[f"{targets[0]}_value"],
505 | title=title,
506 | )
507 | if color[1]:
508 | color = colour.hex2web(color[1])
509 | for target in targets:
510 | self._update_color_value(f"{target}_value", color)
511 |
512 | def edit_theme_name_action(self):
513 | self.window["theme_name"](visible=False)
514 | self.window["edit_theme_name"](visible=False)
515 | self.window["theme_name_value"](visible=True)
516 |
517 | def theme_name_value_action(self):
518 | self.theme_name = self.window["theme_name_value"].TKStringVar.get()
519 | self.window["theme_name"](self.theme_name, visible=True)
520 | # self.window['edit_theme_name'](visible=True)
521 | self.window["theme_name_value"](visible=False)
522 | self.window.set_title(
523 | f"'{self.theme_name}' Theme | Themera v{__version__} Editor"
524 | )
525 | self.update_theme_code(self.generate_theme_code(self.themedict))
526 |
527 | def check_user_colors(self, value):
528 | try:
529 | return self.color_shorthands[str(value).lower()]
530 | except KeyError:
531 | return value
532 |
533 | def invalid_action(self, name):
534 | self.invalids.add(name)
535 | self.window[f"{name}_warning"].widget.configure(
536 | foreground=sg.theme_text_color()
537 | )
538 |
539 | def valid_action(self, name):
540 | try:
541 | self.invalids.remove(name)
542 | except KeyError:
543 | pass
544 | self.window[f"{name}_warning"].widget.configure(
545 | foreground=sg.theme_background_color()
546 | )
547 |
548 | def validate_theme_values(self):
549 | for name in flatten_themedict(self.themedict):
550 | key = f"{name}_value"
551 | value = self.values[key]
552 |
553 | if self.check_user_colors(value) != value:
554 | value = self.check_user_colors(value)
555 | self.values[key] = value
556 | self.window[key](value)
557 |
558 | if self.window[key].metadata == "is_color":
559 | if check_if_color(value):
560 | self.valid_action(name)
561 | self._update_color_value(key, value)
562 | else:
563 | self.invalid_action(name)
564 | continue
565 |
566 | else:
567 | value_type = self.window[key].metadata
568 | try:
569 | if value in (None, "", "-") or (
570 | (issubclass(value_type, int) or issubclass(value_type, float))
571 | and value_type(value) < 0
572 | ):
573 | self.invalid_action(name)
574 | continue
575 | value = eval(f"{value_type.__name__}({value})")
576 | except (ValueError, SyntaxError):
577 | self.invalid_action(name)
578 | continue
579 |
580 | self.valid_action(name)
581 |
582 | def write_themedict(self, themedict):
583 | flat = {f"{k}_value": str(v) for k, v in flatten_themedict(themedict).items()}
584 | for k, v in flatten_themedict(themedict).items():
585 | if self.window[f"{k}_value"].metadata == "is_color":
586 | self._update_color_value(f"{k}_value", v)
587 | else:
588 | self.window[f"{k}_value"](v)
589 |
590 | @property
591 | def themedict(self):
592 | flat = flatten_themedict(self.original_themedict)
593 | for key, value in flat.items():
594 | flat[key] = self.check_user_colors(value)
595 | widget = self.window[f"{key}_value"]
596 | value = self.values[f"{key}_value"]
597 | if widget.metadata != "is_color":
598 | try:
599 | value = eval(f"{widget.metadata.__name__}({value})")
600 | except (ValueError, SyntaxError):
601 | pass
602 | flat[key] = value
603 | result = unflatten_themedict(flat, self.original_themedict)
604 | return result
605 |
606 | def generate_theme_code(self, themedict):
607 | formatted_dict = "{\n"
608 | for k, v in themedict.copy().items():
609 | if isinstance(v, str) and v[0] not in ("{", "[", "("):
610 | # self.window.TKroot.tk.call('eval', f'Tk_GetColor {v}')
611 | v = f"'{self.check_user_colors(v)}'"
612 | if isinstance(k, str):
613 | k = f"'{k}'"
614 | formatted_dict += f" {k}: {v}, \n"
615 | formatted_dict += " }\n"
616 | return (
617 | f'{SETTINGS["psg_alias"]}.theme_add_new(\n \'{self.theme_name}\', '
618 | f'\n {formatted_dict})\n{SETTINGS["psg_alias"]}.theme(\'{self.theme_name}\')'
619 | )
620 |
621 | def update_theme_code(self, value: str):
622 | self.window["theme_code"](value)
623 |
624 | def update_quick_preview(self, themedict):
625 | current_filter = FILTER_MAPPING[FILTERS[self.window["filter"].widget.current()]]
626 | self.preview_themedict = (
627 | themedict.copy() if not current_filter else current_filter(themedict.copy())
628 | )
629 | reskin_mini_preview_window(self.window, "quickpreview", self.preview_themedict)
630 |
631 | def default_full_preview(self):
632 | _theme = sg.theme()
633 | preview_theme = f"{self.theme_name}_____fullpreview"
634 | sg.theme_add_new(preview_theme, self.preview_themedict.copy())
635 | sg.theme(preview_theme)
636 | preview_layout = [
637 | [sg.Text("Theme Preview", font=FONTS["theme_name"])],
638 | [sg.Text("This is how your theme will look when used.")],
639 | [sg.Text("This window serves no other purpose than being a mannequin.")],
640 | [sg.Text("Only the exit button works.")],
641 | [sg.InputText("...just a textbox", size=(60, 8))],
642 | [
643 | sg.Multiline(
644 | f"# This is the code responsible for this window's theme.\n"
645 | f"\n{self.generate_theme_code(self.themedict)}",
646 | size=(58, 5),
647 | )
648 | ],
649 | [
650 | sg.Frame(
651 | title="Progress Bar Preview",
652 | layout=[
653 | # [sg.Text('This bar is static though.')],
654 | [
655 | sg.ProgressBar(
656 | max_value=1000,
657 | orientation="h",
658 | size=(35, 20),
659 | key="p_bar",
660 | )
661 | ]
662 | ],
663 | )
664 | ],
665 | [
666 | sg.Frame(
667 | title="Slider Preview",
668 | layout=[
669 | [sg.Text("This is a useless slider.")],
670 | [
671 | sg.Slider(
672 | range=(0, 1000),
673 | size=(35, 10),
674 | default_value=504,
675 | orientation="h",
676 | )
677 | ],
678 | ],
679 | )
680 | ],
681 | [
682 | sg.Frame(
683 | title="Useless Buttons",
684 | layout=[
685 | [
686 | sg.Button(" Button A ", key="btn_a"),
687 | sg.Button(" Button B ", key="btn_b"),
688 | sg.Button(" Button C ", key="btn_c"),
689 | sg.Button(" Another useless button. ", key="btn_d"),
690 | ]
691 | ],
692 | )
693 | ],
694 | [sg.Button(" Exit ", key="Exit")],
695 | ]
696 | preview_window = Window(
697 | f"'{self.theme_name}' Full Preview | Themera v{__version__} Editor",
698 | preview_layout,
699 | element_justification="center",
700 | modal=True,
701 | )
702 | sg.theme(_theme)
703 | del sg.LOOK_AND_FEEL_TABLE[preview_theme]
704 | Window._move_all_windows = True
705 | while True:
706 | e, v = preview_window(timeout=30)
707 | if e in (None, "Exit"):
708 | preview_window.close()
709 | break
710 | if e == sg.TIMEOUT_EVENT:
711 | progbar_value = preview_window[
712 | "p_bar"
713 | ].TKProgressBar.TKProgressBarForReal["value"]
714 | progbar_max_value = preview_window["p_bar"].MaxValue
715 | if progbar_value <= progbar_max_value:
716 | preview_window["p_bar"](progbar_value + 5)
717 | else:
718 | preview_window["p_bar"](0)
719 | Window._move_all_windows = False
720 |
721 | def read(self):
722 | global relaunch_options
723 | while True:
724 | if self.shortcut_key:
725 | event: str = self.shortcut_key
726 | self.shortcut_key = None
727 | else:
728 | event, self.values = self.window(timeout=350)
729 | if self.values:
730 | self.validate_theme_values()
731 |
732 | if event in (None, "Exit", sg.WIN_CLOSED):
733 | self.window.close()
734 | break
735 |
736 | if (
737 | len(self.invalids) == 0 and self.previous_themedict != self.themedict
738 | ): # No invalid values
739 | themedict = self.themedict.copy()
740 | theme_code = self.generate_theme_code(themedict)
741 | if self.values["theme_code"] != theme_code:
742 | self.values["theme_code"] = theme_code
743 | self.previous_themedict = themedict
744 | self.update_theme_code(theme_code)
745 | self.update_quick_preview(themedict)
746 |
747 | if event.endswith("pickcolor"):
748 | self.color_picker([event.rsplit("_", 1)[0]])
749 |
750 | if event.startswith("full_preview"):
751 | if SETTINGS["full_preview_mode"] == "default":
752 | self.default_full_preview()
753 | elif SETTINGS["full_preview_mode"] == "custom":
754 | current_theme = sg.theme()
755 | custom_preview(
756 | current_theme,
757 | sg.LOOK_AND_FEEL_TABLE[current_theme],
758 | self.theme_name,
759 | self.themedict,
760 | )
761 | elif SETTINGS["full_preview_mode"] == "palette":
762 | palette_preview(self.theme_name, self.themedict)
763 |
764 | if event.startswith("Settings"):
765 | SETTINGS.edit(self)
766 |
767 | if event == "View valid color names":
768 | color_names = []
769 | for color in colour.COLOR_NAME_TO_RGB:
770 | color_names.append(
771 | (
772 | [
773 | sg.Text(
774 | text=color,
775 | size=(20, 1),
776 | text_color=sg.theme_input_text_color(),
777 | background_color=sg.theme_input_background_color(),
778 | ),
779 | sg.Button(
780 | "",
781 | key=f"{color}_col",
782 | tooltip=colour.web2hex(color),
783 | button_color=("#000000", colour.web2hex(color)),
784 | size=(10, 1),
785 | ),
786 | ]
787 | )
788 | )
789 | viewer_layout = [
790 | [
791 | sg.Text(
792 | text=(
793 | f"These are {len(colour.COLOR_NAME_TO_RGB)} valid color names."
794 | )
795 | )
796 | ],
797 | [sg.Text("Ranked from darkest to lightest.")],
798 | [
799 | sg.Column(
800 | layout=color_names,
801 | size=(270, 200),
802 | scrollable=True,
803 | vertical_scroll_only=True,
804 | background_color=sg.theme_input_background_color(),
805 | element_justification="center",
806 | )
807 | ],
808 | [sg.Text("Clicking a color's button copies the color.")],
809 | [sg.Button("Exit")],
810 | ]
811 | viewer = Window(
812 | "Valid Color Name List",
813 | layout=viewer_layout,
814 | element_justification="center",
815 | modal=False,
816 | )
817 | while True:
818 | viewer_e = viewer()[0]
819 | if "col" in str(viewer_e):
820 | copy(viewer_e.rsplit("_", 1)[0])
821 | if viewer_e in (None, "Exit"):
822 | viewer.close()
823 | break
824 |
825 | if event == f"Visit Themera's GitHub Page":
826 | open_link(LINK_GITHUB_REPO)
827 | if event == f"Developer's GitHub Profile":
828 | open_link(LINK_DEVELOPER)
829 | if event == f"Report Issue on GitHub":
830 | open_link(LINK_NEW_GITHUB_ISSUE)
831 | if event == f"PySimpleGUI Docs":
832 | open_link(LINK_PYSIMPLEGUI_SITE)
833 |
834 | if event.startswith("Themera Help"):
835 | open_docs()
836 |
837 | if event == "execute_entry_action":
838 | value = self.values["entry_action_dropdown"]
839 | flat = flatten_themedict(self.themedict).keys()
840 | all_checkboxes = []
841 | for k in flat:
842 | widget = self.window[f"{k}_entryname"]
843 | if isinstance(widget, sg.Checkbox):
844 | all_checkboxes.append(k)
845 | selected = [k for k in all_checkboxes if self.values[f"{k}_entryname"]]
846 | if value == "Select Color":
847 | self.color_picker(selected)
848 | if value == "Interpolate":
849 | begin = self.values[f"{selected[0]}_value"]
850 | end = self.values[f"{selected[-1]}_value"]
851 | colors = [
852 | colour.hsl2hex(c)
853 | for c in colour.color_scale(
854 | colour.web2hsl(begin), colour.web2hsl(end), len(selected)
855 | )
856 | ]
857 | self.window.fill(
858 | dict(zip([f"{k}_value" for k in selected], colors))
859 | )
860 | if value == "Shuffle":
861 | colors = [self.values[f"{k}_value"] for k in selected]
862 | shuffle(colors)
863 | self.window.fill(
864 | dict(zip([f"{k}_value" for k in selected], colors))
865 | )
866 | if value == "Random Color (All)":
867 | color = random_color()
868 | for k in selected:
869 | self._update_color_value(f"{k}_value", color)
870 | self.values[f"{k}_value"] = color
871 | if value == "Random Color (Individual)":
872 | for k in selected:
873 | color = random_color()
874 | self._update_color_value(f"{k}_value", color)
875 | self.values[f"{k}_value"] = color
876 | if value == "Brighten Colors":
877 | for k in selected:
878 | color = alter_luminance(self.values[f"{k}_value"], 1.02)
879 | self._update_color_value(f"{k}_value", color)
880 | self.values[f"{k}_value"] = color
881 | if value == "Darken Colors":
882 | for k in selected:
883 | color = alter_luminance(self.values[f"{k}_value"], 0.98)
884 | self._update_color_value(f"{k}_value", color)
885 | self.values[f"{k}_value"] = color
886 |
887 | if event == "filter":
888 | self.update_quick_preview(self.themedict)
889 | if self.values["filter"] != FILTERS[0]:
890 | self.window["apply_filter"](disabled=False)
891 | else:
892 | self.window["apply_filter"](disabled=True)
893 |
894 | if event == "apply_filter":
895 | self.write_themedict(self.preview_themedict)
896 | self.window["filter"](FILTERS[0])
897 | self.window["apply_filter"](disabled=True)
898 |
899 | if event.startswith("select_all") or event.endswith("entryname"):
900 | # This portion powers the select all behaviour.
901 | flat = flatten_themedict(self.themedict).keys()
902 | all_checkboxes = []
903 | for k in flat:
904 | widget = self.window[f"{k}_entryname"]
905 | if isinstance(widget, sg.Checkbox):
906 | all_checkboxes.append(k)
907 | if event.startswith("select_all"):
908 | if event == "select_all_kb":
909 | # Simulate the action from a click when using the kb shortcut on the select all checkbox.
910 | self.values["select_all"] = not self.values["select_all"]
911 | self.window["select_all"](value=self.values["select_all"])
912 | for k in all_checkboxes:
913 | # Set all checkboxes to the value of the select all checkbox.
914 | self.window[f"{k}_entryname"](self.values["select_all"])
915 | self.values[f"{k}_entryname"] = self.values["select_all"]
916 |
917 | elif not self.values[event]:
918 | # If we're disabling an entry's checkbox, we also turn off the select all checkbox.
919 | self.values["select_all"] = False
920 | self.window["select_all"](False)
921 |
922 | # Count the number of selected entry checkboxes.
923 | selected = [k for k in all_checkboxes if self.values[f"{k}_entryname"]]
924 |
925 | if len(selected) == len(all_checkboxes):
926 | # If all the checkboxes are selected, we tick the select all checkbox.
927 | self.values["select_all"] = True
928 | self.window["select_all"](True)
929 |
930 | if (
931 | len(selected) >= 2
932 | ): # If there's 2 or more selected, enable the batch options.
933 | self.window["entry_action_dropdown"](disabled=False)
934 |
935 | else: # If there's less than 2 selected, disable batch options, because what's getting batched?
936 | self.window["entry_action_dropdown"](disabled=True)
937 |
938 | if event == "entry_action_dropdown":
939 | if self.values["entry_action_dropdown"] != BATCH_ACTIONS[0]:
940 | self.window["execute_entry_action"](disabled=False)
941 | else:
942 | self.window["execute_entry_action"](disabled=True)
943 |
944 | if event.startswith("Revert to Beginning") and (
945 | self.themedict != self.original_themedict
946 | ):
947 | self.write_themedict(self.original_themedict)
948 |
949 | if event == "copy":
950 | if self.copy_id:
951 | self.window["copy"].widget.after_cancel(self.copy_id)
952 | copy(self.values["theme_code"])
953 | self.window["copy"]("Theme Code Copied"),
954 | self.copy_id = self.window["copy"].widget.after(
955 | 2000, lambda: self.window["copy"]("Copy Theme Code")
956 | )
957 |
958 | if event.startswith("Return to Launcher"):
959 | if (
960 | sg.PopupYesNo(
961 | "This action will close the editor. Continue?",
962 | title="Are you sure?",
963 | )
964 | == "Yes"
965 | ):
966 | relaunch_options = (self.window, "main")
967 | break
968 | else:
969 | continue
970 |
971 | if event.startswith("Create New Theme"):
972 | if (
973 | sg.PopupYesNo(
974 | "This action will close the editor. Continue?",
975 | title="Are you sure?",
976 | )
977 | == "Yes"
978 | ):
979 | relaunch_options = (self.window, "new")
980 | break
981 | else:
982 | continue
983 |
984 | if event.startswith("Edit Existing Theme"):
985 | if (
986 | sg.PopupYesNo(
987 | "This action will close the editor. Continue?",
988 | title="Are you sure?",
989 | )
990 | == "Yes"
991 | ):
992 | relaunch_options = (self.window, "existing")
993 | break
994 | else:
995 | continue
996 |
997 | if event.startswith("Theme From Image"):
998 | if (
999 | sg.PopupYesNo(
1000 | "This action will close the editor. Continue?",
1001 | title="Are you sure?",
1002 | )
1003 | == "Yes"
1004 | ):
1005 | relaunch_options = (self.window, "image")
1006 | break
1007 | else:
1008 | continue
1009 |
1010 |
1011 | def main():
1012 | global relaunch_options
1013 | try:
1014 | # raise Exception('Testing the crash reporting system.') # Do not uncomment unless you wish to test crashes.
1015 | while True:
1016 | set_launcher_to = "main"
1017 |
1018 | # Try to close any leftover editor windows in case we're restarting.
1019 | if relaunch_options:
1020 | previous_window, set_launcher_to = relaunch_options
1021 | previous_window.close()
1022 | relaunch_options = None
1023 |
1024 | # Run an instance of the launcher and follow it up with an Editor if successful.
1025 | _name, _themedict, _launcher = Launcher(set_to=set_launcher_to)
1026 | if _name and _themedict: # Launcher completed successfully.
1027 | _editor = eval("Editor(_name, _themedict)")
1028 | _launcher.close()
1029 | _editor()
1030 | else: # Launcher had issues.
1031 | break
1032 |
1033 | # Check whether to restart after closing everything.
1034 | if relaunch_options is None:
1035 | break
1036 |
1037 | except Exception as e:
1038 | handle_crash(e, sg)
1039 |
1040 |
1041 | if __name__ == "__main__":
1042 | # Greetings to whoever can see the console.
1043 | print(f"Themera v{__version__} started successfully.")
1044 | main()
1045 | print(f"Themera v{__version__} closed properly.")
1046 |
--------------------------------------------------------------------------------
/themera/themes.py:
--------------------------------------------------------------------------------
1 | """
2 | d888888P dP
3 | 88 88
4 | 88 88d888b. .d8888b. 88d8b.d8b. .d8888b. 88d888b. .d8888b.
5 | 88 88' `88 88ooood8 88'`88'`88 88ooood8 88' `88 88' `88
6 | 88 88 88 88. ... 88 88 88 88. ... 88 88. .88
7 | dP dP dP `88888P' dP dP dP `88888P' dP `88888P8
8 |
9 | Themera Themes File
10 | PySimpleGUI Theme Code Generator
11 | Copyright 2023 Divine Afam-Ifediogor
12 | """
13 |
14 | # THEMES _______________________________________________________________________________________________________________
15 | LightTheme = {
16 | "BACKGROUND": "white",
17 | "TEXT": "#222",
18 | "INPUT": "#377dc4",
19 | "TEXT_INPUT": "white",
20 | "SCROLL": "#245698",
21 | "BUTTON": ("white", "#377dc4"),
22 | "PROGRESS": ("#5fa1e3", "#245698"),
23 | "BORDER": 0,
24 | "SLIDER_DEPTH": 1,
25 | "PROGRESS_DEPTH": 0,
26 | }
27 |
28 | DarkTheme = {
29 | "BACKGROUND": "#05142e",
30 | "TEXT": "#ccdeff",
31 | "INPUT": "#667",
32 | "TEXT_INPUT": "#fdfdce",
33 | "SCROLL": "#333",
34 | "BUTTON": ("#fdfdce", "#5f5f6d"),
35 | "PROGRESS": ("#fdfdce", "#333"),
36 | "BORDER": 0,
37 | "SLIDER_DEPTH": 1,
38 | "PROGRESS_DEPTH": 0,
39 | }
40 |
--------------------------------------------------------------------------------
/themera/version_and_copyright.py:
--------------------------------------------------------------------------------
1 | """
2 | d888888P dP
3 | 88 88
4 | 88 88d888b. .d8888b. 88d8b.d8b. .d8888b. 88d888b. .d8888b.
5 | 88 88' `88 88ooood8 88'`88'`88 88ooood8 88' `88 88' `88
6 | 88 88 88 88. ... 88 88 88 88. ... 88 88. .88
7 | dP dP dP `88888P' dP dP dP `88888P' dP `88888P8
8 |
9 | Themera Version and Copyright File
10 | PySimpleGUI Theme Code Generator
11 | Copyright 2023 Divine Afam-Ifediogor
12 | """
13 |
14 | # IMPORTS ______________________________________________________________________________________________________________
15 | from datetime import datetime
16 |
17 | # VERSION AND COPYRIGHT ________________________________________________________________________________________________
18 | __version__ = "2.1.1"
19 | YEAR = datetime.now().year
20 | COPYRIGHT = f"Copyright {YEAR} Divine Afam-Ifediogor"
21 |
--------------------------------------------------------------------------------
/themera/window.py:
--------------------------------------------------------------------------------
1 | """
2 | d888888P dP
3 | 88 88
4 | 88 88d888b. .d8888b. 88d8b.d8b. .d8888b. 88d888b. .d8888b.
5 | 88 88' `88 88ooood8 88'`88'`88 88ooood8 88' `88 88' `88
6 | 88 88 88 88. ... 88 88 88 88. ... 88 88. .88
7 | dP dP dP `88888P' dP dP dP `88888P' dP `88888P8
8 |
9 | Themera Custom Window Class File
10 | PySimpleGUI Theme Code Generator
11 | Copyright 2023 Divine Afam-Ifediogor
12 | """
13 | # IMPORTS ______________________________________________________________________________________________________________
14 | import PySimpleGUI as sg
15 | from colour import Color
16 |
17 |
18 | # LOGIC ________________________________________________________________________________________________________________
19 | def hexify(color: str):
20 | color = Color(color).get_hex_l()
21 | color = f"0x00{color[5]}{color[6]}{color[3]}{color[4]}{color[1]}{color[2]}"
22 | color = eval(color)
23 | return color
24 |
25 |
26 | class Window(sg.Window):
27 | open_windows = list()
28 |
29 | def __init__(self, *args, **kwargs):
30 | # Make all windows inherit the last known location.
31 | if len(self.open_windows) > 1:
32 | kwargs.setdefault(
33 | "location", self.open_windows[-1].current_location(more_accurate=True)
34 | )
35 | self.editor_object = None
36 | super().__init__(*args, **kwargs)
37 | self.open_windows.append(self)
38 | self.finalize()
39 |
40 | # Windows-only titlebar customization.
41 | self.customize_titlebar()
42 |
43 | def close(self):
44 | self.open_windows.remove(self)
45 | return super().close()
46 |
47 | def customize_titlebar(self):
48 | # Windows-only titlebar customization.
49 | if not sg.running_windows():
50 | return
51 | from platform import win32_ver
52 |
53 | try:
54 | version = int(win32_ver()[0])
55 | except ValueError:
56 | version = None
57 | if version < 10:
58 | return
59 | import ctypes
60 |
61 | bg = hexify(sg.theme_background_color())
62 | fg = hexify(sg.theme_text_color())
63 | try:
64 | hwnd = ctypes.windll.user32.GetParent(self.TKroot.winfo_id())
65 | ctypes.windll.dwmapi.DwmSetWindowAttribute(
66 | hwnd,
67 | 35,
68 | ctypes.byref(ctypes.c_int(bg)),
69 | ctypes.sizeof(ctypes.c_int),
70 | )
71 | ctypes.windll.dwmapi.DwmSetWindowAttribute(
72 | hwnd,
73 | 36,
74 | ctypes.byref(ctypes.c_int(fg)),
75 | ctypes.sizeof(ctypes.c_int),
76 | )
77 | except Exception as e:
78 | print("Failed to set custom titlebar color.")
79 | raise e
80 | self.TKroot.update()
81 |
--------------------------------------------------------------------------------