diff --git a/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/INSTALLER b/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/LICENSE b/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/LICENSE new file mode 100644 index 0000000..2c9bc28 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Tom Schimansky + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/METADATA b/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/METADATA new file mode 100644 index 0000000..d7fc60b --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/METADATA @@ -0,0 +1,18 @@ +Metadata-Version: 2.1 +Name: customtkinter +Version: 5.2.2 +Summary: Create modern looking GUIs with Python +Home-page: https://customtkinter.tomschimansky.com +Author: Tom Schimansky +License: Creative Commons Zero v1.0 Universal +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.7 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: darkdetect +Requires-Dist: packaging +Requires-Dist: typing-extensions ; python_version <= "3.7" + +A modern and customizable python UI-library based on Tkinter: https://customtkinter.tomschimansky.com diff --git a/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/RECORD b/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/RECORD new file mode 100644 index 0000000..5f00a54 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/RECORD @@ -0,0 +1,102 @@ +customtkinter-5.2.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +customtkinter-5.2.2.dist-info/LICENSE,sha256=yXO4YA5iAVnLBHPrmwh4MUPg2e1TM_NQ_JMzaNlNng0,1071 +customtkinter-5.2.2.dist-info/METADATA,sha256=Ju0LVS8oBVdkzfvV7D4lrmRd_LASEAF5p8PeVYw6DsM,677 +customtkinter-5.2.2.dist-info/RECORD,, +customtkinter-5.2.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +customtkinter-5.2.2.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92 +customtkinter-5.2.2.dist-info/top_level.txt,sha256=wLXrY2KS8Vj2MnAIH53xH5gHMNCLCKQEmn2QjU9nDH0,14 +customtkinter/__init__.py,sha256=_eLjpybhQB49OORUMIU-op8w0_W4Gxus5AQi3yzQcjg,2861 +customtkinter/__pycache__/__init__.cpython-311.pyc,, +customtkinter/assets/.DS_Store,sha256=V62jh68Vv_RIJCoF5ONdK3V3mLCALLiUyBtNxORzAC8,6148 +customtkinter/assets/fonts/CustomTkinter_shapes_font.otf,sha256=-tZ-KwYMMYtshkbQh_vTrdk4tmdiQ_FLDFJiMXlkEnQ,3528 +customtkinter/assets/fonts/Roboto/Roboto-Medium.ttf,sha256=SsjgNgb_pMN_YaZRCiCA8fN6cFT0cmwhSIfTsj9y42k,168644 +customtkinter/assets/fonts/Roboto/Roboto-Regular.ttf,sha256=MZz_bnox8PKkHEddykKJCqXRn-FgF-IpD4wdThT3ZIE,168260 +customtkinter/assets/icons/.DS_Store,sha256=5T77LKT94iGaPcXe1CLsRu7MegVHtmY7msnhYZasbSU,6148 +customtkinter/assets/icons/CustomTkinter_icon_Windows.ico,sha256=EjTAF8hx6y4g029mj5PgZs3Lk9tGTVzvnXpb-DUG0ow,13238 +customtkinter/assets/themes/blue.json,sha256=yUF0cMFs7XpD1sSo4Cevpu3GLCTVrufEwtzRE4WWTTs,4520 +customtkinter/assets/themes/dark-blue.json,sha256=alf6b4-4lhowzmQpUisYDXbjr5uODarCWQWYQThqa9M,4514 +customtkinter/assets/themes/green.json,sha256=ea2Gv-p_BVesHiCAKJKrtEqWevFbkxWwA5zXXItyp3Y,4515 +customtkinter/windows/__init__.py,sha256=eSoo0x_4XkSok_BaQbtM-AtwMRu1za6MPhd6YfWz6sI,107 +customtkinter/windows/__pycache__/__init__.cpython-311.pyc,, +customtkinter/windows/__pycache__/ctk_input_dialog.cpython-311.pyc,, +customtkinter/windows/__pycache__/ctk_tk.cpython-311.pyc,, +customtkinter/windows/__pycache__/ctk_toplevel.cpython-311.pyc,, +customtkinter/windows/ctk_input_dialog.py,sha256=_mAWVEBSPiuBgISbXaWE8LG5Fs7hp0KOM-BmVfBwnp0,5897 +customtkinter/windows/ctk_tk.py,sha256=pvYD7mP9SXdVKIomRbNKt0Zu2pFsIOf3246DqPFz1Pg,15646 +customtkinter/windows/ctk_toplevel.py,sha256=jEaJpXnAXW6wsdeh3Tr9oABk39GSuk0Z_7OgG8F9Nf8,14750 +customtkinter/windows/widgets/__init__.py,sha256=DNohOpF5R2Ae1iP4wNpsDYkOnMRiVrcNdwCPWIxNWGI,622 +customtkinter/windows/widgets/__pycache__/__init__.cpython-311.pyc,, +customtkinter/windows/widgets/__pycache__/ctk_button.cpython-311.pyc,, +customtkinter/windows/widgets/__pycache__/ctk_checkbox.cpython-311.pyc,, +customtkinter/windows/widgets/__pycache__/ctk_combobox.cpython-311.pyc,, +customtkinter/windows/widgets/__pycache__/ctk_entry.cpython-311.pyc,, +customtkinter/windows/widgets/__pycache__/ctk_frame.cpython-311.pyc,, +customtkinter/windows/widgets/__pycache__/ctk_label.cpython-311.pyc,, +customtkinter/windows/widgets/__pycache__/ctk_optionmenu.cpython-311.pyc,, +customtkinter/windows/widgets/__pycache__/ctk_progressbar.cpython-311.pyc,, +customtkinter/windows/widgets/__pycache__/ctk_radiobutton.cpython-311.pyc,, +customtkinter/windows/widgets/__pycache__/ctk_scrollable_frame.cpython-311.pyc,, +customtkinter/windows/widgets/__pycache__/ctk_scrollbar.cpython-311.pyc,, +customtkinter/windows/widgets/__pycache__/ctk_segmented_button.cpython-311.pyc,, +customtkinter/windows/widgets/__pycache__/ctk_slider.cpython-311.pyc,, +customtkinter/windows/widgets/__pycache__/ctk_switch.cpython-311.pyc,, +customtkinter/windows/widgets/__pycache__/ctk_tabview.cpython-311.pyc,, +customtkinter/windows/widgets/__pycache__/ctk_textbox.cpython-311.pyc,, +customtkinter/windows/widgets/appearance_mode/__init__.py,sha256=fKKK0fVUHFiqmSZw0GZFY0GKgJGDs8A5OXvSBEC3qSg,172 +customtkinter/windows/widgets/appearance_mode/__pycache__/__init__.cpython-311.pyc,, +customtkinter/windows/widgets/appearance_mode/__pycache__/appearance_mode_base_class.cpython-311.pyc,, +customtkinter/windows/widgets/appearance_mode/__pycache__/appearance_mode_tracker.cpython-311.pyc,, +customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py,sha256=nTXJC4vpwjrgCiaOv3V20xVvtkxKR9pVJykIkhWKFz8,2602 +customtkinter/windows/widgets/appearance_mode/appearance_mode_tracker.py,sha256=L-X-eLxWFhenWtHr7Z_Mp5msaBTMEIxc-hhKrIVn7bc,3630 +customtkinter/windows/widgets/core_rendering/__init__.py,sha256=212WNYQ90_4YY_QB6x0E-xCqqmUXsWegNPQ4VeXSKO0,324 +customtkinter/windows/widgets/core_rendering/__pycache__/__init__.cpython-311.pyc,, +customtkinter/windows/widgets/core_rendering/__pycache__/ctk_canvas.cpython-311.pyc,, +customtkinter/windows/widgets/core_rendering/__pycache__/draw_engine.cpython-311.pyc,, +customtkinter/windows/widgets/core_rendering/ctk_canvas.py,sha256=uXHYqdTns1LAp2GYadyBDcKt5PpNUHka_7l4aV1jqqg,5894 +customtkinter/windows/widgets/core_rendering/draw_engine.py,sha256=eeXpsYiCDOqUQo5Pz5nBHDQqg99drt4QDjODmrdee8w,91030 +customtkinter/windows/widgets/core_widget_classes/__init__.py,sha256=DsG3zo4hzAUmdFbt3aXgen0Vs0Y9-ppycKTIfrHTYMA,81 +customtkinter/windows/widgets/core_widget_classes/__pycache__/__init__.cpython-311.pyc,, +customtkinter/windows/widgets/core_widget_classes/__pycache__/ctk_base_class.cpython-311.pyc,, +customtkinter/windows/widgets/core_widget_classes/__pycache__/dropdown_menu.cpython-311.pyc,, +customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py,sha256=PTxPeqvfnct-wuXBqodbGDxpfXJ67Cal201qJxCt3EE,15793 +customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py,sha256=dC_SEWk_XQbX47NjLDyhsKh8YKNyPFndoIIxqAnOGRE,8516 +customtkinter/windows/widgets/ctk_button.py,sha256=TE_6T88cL8ae1p8ZOy_WE4inpxjP0iaXej0qmjcjbCU,27699 +customtkinter/windows/widgets/ctk_checkbox.py,sha256=h8w7SG_9oWE4x4_iBQ-qXvxTq1J_o0Dj4C6JZAqXANU,22420 +customtkinter/windows/widgets/ctk_combobox.py,sha256=p1dYS2r9nq7BFfidIbt9JCaZRB8XtjV4XHUV8nGpdGo,20602 +customtkinter/windows/widgets/ctk_entry.py,sha256=bxhcZAQt81S59UA5v9fJtsXLLHf9NuUy4qDh4K2m-Ec,17898 +customtkinter/windows/widgets/ctk_frame.py,sha256=hcyGh8OzB2CzwCb9c4DMC29QqJS9awzNzEZZmzv5Usc,9515 +customtkinter/windows/widgets/ctk_label.py,sha256=4WDRL_gbctV9t-SIKp1oowbvXB-x3CN5o5yajrOogxE,13106 +customtkinter/windows/widgets/ctk_optionmenu.py,sha256=5g2OzI6mexzGZxmrUStsyuG-MiihAr-UhyzVS5y5vko,19637 +customtkinter/windows/widgets/ctk_progressbar.py,sha256=k8aa_RUIVb-gNUsOUxIHAzBkw8yDgZuU0ar5JeRp4EA,14120 +customtkinter/windows/widgets/ctk_radiobutton.py,sha256=hEY0uDuStmkizlpkP8QsfLXRgRNexaj_JPM8_2Iyf3Q,20215 +customtkinter/windows/widgets/ctk_scrollable_frame.py,sha256=wKJI1U36m5FhDjIcuDoYPWbTQbLDtusBXu3c1UYQOzk,15121 +customtkinter/windows/widgets/ctk_scrollbar.py,sha256=cO6NwExutdSXUBSRdbVve9b0dvV9djUs9Pxetqfxfn8,13860 +customtkinter/windows/widgets/ctk_segmented_button.py,sha256=zKPj1J_ZbEXbt5en7vHdZhooqNIrDkr3DKo-QEQ3rvc,20878 +customtkinter/windows/widgets/ctk_slider.py,sha256=1xY9vnha-djmym0iLpBkVK30aCDw92lqTMOgZiJ4Hhk,18867 +customtkinter/windows/widgets/ctk_switch.py,sha256=aOscR68hoHVkmugnWMqTlsKlLZgGCiO1rfXkDOQSC0Y,23420 +customtkinter/windows/widgets/ctk_tabview.py,sha256=ieGuEQqUH7miPQWIkSyon7KVWdbqFFKW5k7oBv53jRg,21472 +customtkinter/windows/widgets/ctk_textbox.py,sha256=l-XEQQYVKMB7cTEk4h150b2s-o2HpRh7zHROVc9H3bA,24571 +customtkinter/windows/widgets/font/__init__.py,sha256=zYlSsJH-8rL77t7dkz0vXOofHh8NdiIuMQK4V8mfINM,1302 +customtkinter/windows/widgets/font/__pycache__/__init__.cpython-311.pyc,, +customtkinter/windows/widgets/font/__pycache__/ctk_font.cpython-311.pyc,, +customtkinter/windows/widgets/font/__pycache__/font_manager.cpython-311.pyc,, +customtkinter/windows/widgets/font/ctk_font.py,sha256=5cdHvujB_tfDlsw9ZRMiGBASvmRSg_WIbSF99x_21vA,3825 +customtkinter/windows/widgets/font/font_manager.py,sha256=jwpu61Z00noq0UWcFaHftAMRvXgmKQHEmQiDq-CrqNw,2268 +customtkinter/windows/widgets/image/__init__.py,sha256=pzbwlIAV4OE-CkdxJIrl1ELcRg7vbQKkC6HaHLGDNI8,32 +customtkinter/windows/widgets/image/__pycache__/__init__.cpython-311.pyc,, +customtkinter/windows/widgets/image/__pycache__/ctk_image.cpython-311.pyc,, +customtkinter/windows/widgets/image/ctk_image.py,sha256=uL_oJrlq8Z134qjiii44Lg03utETSf7rBJ6kgPHeYzk,5306 +customtkinter/windows/widgets/scaling/__init__.py,sha256=2GjeG2_A4gq8g7pROKPw6QR2mIH7b6iRDQvT26qz8R4,287 +customtkinter/windows/widgets/scaling/__pycache__/__init__.cpython-311.pyc,, +customtkinter/windows/widgets/scaling/__pycache__/scaling_base_class.cpython-311.pyc,, +customtkinter/windows/widgets/scaling/__pycache__/scaling_tracker.cpython-311.pyc,, +customtkinter/windows/widgets/scaling/scaling_base_class.py,sha256=VbzzA9GPujOYbEJjDsQRnvoY6v7RSitZwr37lZAkk_8,7060 +customtkinter/windows/widgets/scaling/scaling_tracker.py,sha256=cuilXEeSTOylgDRNf2kh-eDQShup-i2Nc7oOcLo3Vtw,8745 +customtkinter/windows/widgets/theme/__init__.py,sha256=iZ6T6O-v39hOLfTLuNbTLt0XA_raCYtxwGnmqNB2Lmk,471 +customtkinter/windows/widgets/theme/__pycache__/__init__.cpython-311.pyc,, +customtkinter/windows/widgets/theme/__pycache__/theme_manager.cpython-311.pyc,, +customtkinter/windows/widgets/theme/theme_manager.py,sha256=E-VtzWKZ2IXrc_hvfGUdj50GNr8eII7S_fWBk58xix0,2179 +customtkinter/windows/widgets/utility/__init__.py,sha256=STsgOQvThAoUarV9ovM6M40araQUDNU_QmmHbyMaRx0,72 +customtkinter/windows/widgets/utility/__pycache__/__init__.cpython-311.pyc,, +customtkinter/windows/widgets/utility/__pycache__/utility_functions.cpython-311.pyc,, +customtkinter/windows/widgets/utility/utility_functions.py,sha256=d0Wj5Aioxhb3qtC0ITuNZHjwfFHegUfK-bfkDXrrufk,766 diff --git a/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/REQUESTED b/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/WHEEL b/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/WHEEL new file mode 100644 index 0000000..98c0d20 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.42.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/top_level.txt b/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/top_level.txt new file mode 100644 index 0000000..3477701 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter-5.2.2.dist-info/top_level.txt @@ -0,0 +1 @@ +customtkinter diff --git a/.venv/Lib/site-packages/customtkinter/__init__.py b/.venv/Lib/site-packages/customtkinter/__init__.py new file mode 100644 index 0000000..d9144ce --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/__init__.py @@ -0,0 +1,88 @@ +__version__ = "5.2.2" + +import os +import sys +from tkinter import Variable, StringVar, IntVar, DoubleVar, BooleanVar +from tkinter.constants import * +import tkinter.filedialog as filedialog + +# import manager classes +from .windows.widgets.appearance_mode import AppearanceModeTracker +from .windows.widgets.font import FontManager +from .windows.widgets.scaling import ScalingTracker +from .windows.widgets.theme import ThemeManager +from .windows.widgets.core_rendering import DrawEngine + +# import base widgets +from .windows.widgets.core_rendering import CTkCanvas +from .windows.widgets.core_widget_classes import CTkBaseClass + +# import widgets +from .windows.widgets import CTkButton +from .windows.widgets import CTkCheckBox +from .windows.widgets import CTkComboBox +from .windows.widgets import CTkEntry +from .windows.widgets import CTkFrame +from .windows.widgets import CTkLabel +from .windows.widgets import CTkOptionMenu +from .windows.widgets import CTkProgressBar +from .windows.widgets import CTkRadioButton +from .windows.widgets import CTkScrollbar +from .windows.widgets import CTkSegmentedButton +from .windows.widgets import CTkSlider +from .windows.widgets import CTkSwitch +from .windows.widgets import CTkTabview +from .windows.widgets import CTkTextbox +from .windows.widgets import CTkScrollableFrame + +# import windows +from .windows import CTk +from .windows import CTkToplevel +from .windows import CTkInputDialog + +# import font classes +from .windows.widgets.font import CTkFont + +# import image classes +from .windows.widgets.image import CTkImage + +from .windows import ctk_tk + +_ = Variable, StringVar, IntVar, DoubleVar, BooleanVar, CENTER, filedialog # prevent IDE from removing unused imports + + +def set_appearance_mode(mode_string: str): + """ possible values: light, dark, system """ + AppearanceModeTracker.set_appearance_mode(mode_string) + + +def get_appearance_mode() -> str: + """ get current state of the appearance mode (light or dark) """ + if AppearanceModeTracker.appearance_mode == 0: + return "Light" + elif AppearanceModeTracker.appearance_mode == 1: + return "Dark" + + +def set_default_color_theme(color_string: str): + """ set color theme or load custom theme file by passing the path """ + ThemeManager.load_theme(color_string) + + +def set_widget_scaling(scaling_value: float): + """ set scaling for the widget dimensions """ + ScalingTracker.set_widget_scaling(scaling_value) + + +def set_window_scaling(scaling_value: float): + """ set scaling for window dimensions """ + ScalingTracker.set_window_scaling(scaling_value) + + +def deactivate_automatic_dpi_awareness(): + """ deactivate DPI awareness of current process (windll.shcore.SetProcessDpiAwareness(0)) """ + ScalingTracker.deactivate_automatic_dpi_awareness = True + + +def set_ctk_parent_class(ctk_parent_class): + ctk_tk.CTK_PARENT_CLASS = ctk_parent_class diff --git a/.venv/Lib/site-packages/customtkinter/__pycache__/__init__.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..ca64e16 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/__pycache__/__init__.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/assets/.DS_Store b/.venv/Lib/site-packages/customtkinter/assets/.DS_Store new file mode 100644 index 0000000..2bf4a8e Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/assets/.DS_Store differ diff --git a/.venv/Lib/site-packages/customtkinter/assets/fonts/CustomTkinter_shapes_font.otf b/.venv/Lib/site-packages/customtkinter/assets/fonts/CustomTkinter_shapes_font.otf new file mode 100644 index 0000000..a891053 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/assets/fonts/CustomTkinter_shapes_font.otf differ diff --git a/.venv/Lib/site-packages/customtkinter/assets/fonts/Roboto/Roboto-Medium.ttf b/.venv/Lib/site-packages/customtkinter/assets/fonts/Roboto/Roboto-Medium.ttf new file mode 100644 index 0000000..e89b0b7 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/assets/fonts/Roboto/Roboto-Medium.ttf differ diff --git a/.venv/Lib/site-packages/customtkinter/assets/fonts/Roboto/Roboto-Regular.ttf b/.venv/Lib/site-packages/customtkinter/assets/fonts/Roboto/Roboto-Regular.ttf new file mode 100644 index 0000000..3d6861b Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/assets/fonts/Roboto/Roboto-Regular.ttf differ diff --git a/.venv/Lib/site-packages/customtkinter/assets/icons/.DS_Store b/.venv/Lib/site-packages/customtkinter/assets/icons/.DS_Store new file mode 100644 index 0000000..57d1d87 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/assets/icons/.DS_Store differ diff --git a/.venv/Lib/site-packages/customtkinter/assets/icons/CustomTkinter_icon_Windows.ico b/.venv/Lib/site-packages/customtkinter/assets/icons/CustomTkinter_icon_Windows.ico new file mode 100644 index 0000000..fe8eeaf Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/assets/icons/CustomTkinter_icon_Windows.ico differ diff --git a/.venv/Lib/site-packages/customtkinter/assets/themes/blue.json b/.venv/Lib/site-packages/customtkinter/assets/themes/blue.json new file mode 100644 index 0000000..192c136 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/assets/themes/blue.json @@ -0,0 +1,155 @@ +{ + "CTk": { + "fg_color": ["gray92", "gray14"] + }, + "CTkToplevel": { + "fg_color": ["gray92", "gray14"] + }, + "CTkFrame": { + "corner_radius": 6, + "border_width": 0, + "fg_color": ["gray86", "gray17"], + "top_fg_color": ["gray81", "gray20"], + "border_color": ["gray65", "gray28"] + }, + "CTkButton": { + "corner_radius": 6, + "border_width": 0, + "fg_color": ["#3B8ED0", "#1F6AA5"], + "hover_color": ["#36719F", "#144870"], + "border_color": ["#3E454A", "#949A9F"], + "text_color": ["#DCE4EE", "#DCE4EE"], + "text_color_disabled": ["gray74", "gray60"] + }, + "CTkLabel": { + "corner_radius": 0, + "fg_color": "transparent", + "text_color": ["gray10", "#DCE4EE"] + }, + "CTkEntry": { + "corner_radius": 6, + "border_width": 2, + "fg_color": ["#F9F9FA", "#343638"], + "border_color": ["#979DA2", "#565B5E"], + "text_color":["gray10", "#DCE4EE"], + "placeholder_text_color": ["gray52", "gray62"] + }, + "CTkCheckBox": { + "corner_radius": 6, + "border_width": 3, + "fg_color": ["#3B8ED0", "#1F6AA5"], + "border_color": ["#3E454A", "#949A9F"], + "hover_color": ["#3B8ED0", "#1F6AA5"], + "checkmark_color": ["#DCE4EE", "gray90"], + "text_color": ["gray10", "#DCE4EE"], + "text_color_disabled": ["gray60", "gray45"] + }, + "CTkSwitch": { + "corner_radius": 1000, + "border_width": 3, + "button_length": 0, + "fg_color": ["#939BA2", "#4A4D50"], + "progress_color": ["#3B8ED0", "#1F6AA5"], + "button_color": ["gray36", "#D5D9DE"], + "button_hover_color": ["gray20", "gray100"], + "text_color": ["gray10", "#DCE4EE"], + "text_color_disabled": ["gray60", "gray45"] + }, + "CTkRadioButton": { + "corner_radius": 1000, + "border_width_checked": 6, + "border_width_unchecked": 3, + "fg_color": ["#3B8ED0", "#1F6AA5"], + "border_color": ["#3E454A", "#949A9F"], + "hover_color": ["#36719F", "#144870"], + "text_color": ["gray10", "#DCE4EE"], + "text_color_disabled": ["gray60", "gray45"] + }, + "CTkProgressBar": { + "corner_radius": 1000, + "border_width": 0, + "fg_color": ["#939BA2", "#4A4D50"], + "progress_color": ["#3B8ED0", "#1F6AA5"], + "border_color": ["gray", "gray"] + }, + "CTkSlider": { + "corner_radius": 1000, + "button_corner_radius": 1000, + "border_width": 6, + "button_length": 0, + "fg_color": ["#939BA2", "#4A4D50"], + "progress_color": ["gray40", "#AAB0B5"], + "button_color": ["#3B8ED0", "#1F6AA5"], + "button_hover_color": ["#36719F", "#144870"] + }, + "CTkOptionMenu": { + "corner_radius": 6, + "fg_color": ["#3B8ED0", "#1F6AA5"], + "button_color": ["#36719F", "#144870"], + "button_hover_color": ["#27577D", "#203A4F"], + "text_color": ["#DCE4EE", "#DCE4EE"], + "text_color_disabled": ["gray74", "gray60"] + }, + "CTkComboBox": { + "corner_radius": 6, + "border_width": 2, + "fg_color": ["#F9F9FA", "#343638"], + "border_color": ["#979DA2", "#565B5E"], + "button_color": ["#979DA2", "#565B5E"], + "button_hover_color": ["#6E7174", "#7A848D"], + "text_color": ["gray10", "#DCE4EE"], + "text_color_disabled": ["gray50", "gray45"] + }, + "CTkScrollbar": { + "corner_radius": 1000, + "border_spacing": 4, + "fg_color": "transparent", + "button_color": ["gray55", "gray41"], + "button_hover_color": ["gray40", "gray53"] + }, + "CTkSegmentedButton": { + "corner_radius": 6, + "border_width": 2, + "fg_color": ["#979DA2", "gray29"], + "selected_color": ["#3B8ED0", "#1F6AA5"], + "selected_hover_color": ["#36719F", "#144870"], + "unselected_color": ["#979DA2", "gray29"], + "unselected_hover_color": ["gray70", "gray41"], + "text_color": ["#DCE4EE", "#DCE4EE"], + "text_color_disabled": ["gray74", "gray60"] + }, + "CTkTextbox": { + "corner_radius": 6, + "border_width": 0, + "fg_color": ["#F9F9FA", "#1D1E1E"], + "border_color": ["#979DA2", "#565B5E"], + "text_color":["gray10", "#DCE4EE"], + "scrollbar_button_color": ["gray55", "gray41"], + "scrollbar_button_hover_color": ["gray40", "gray53"] + }, + "CTkScrollableFrame": { + "label_fg_color": ["gray78", "gray23"] + }, + "DropdownMenu": { + "fg_color": ["gray90", "gray20"], + "hover_color": ["gray75", "gray28"], + "text_color": ["gray10", "gray90"] + }, + "CTkFont": { + "macOS": { + "family": "SF Display", + "size": 13, + "weight": "normal" + }, + "Windows": { + "family": "Roboto", + "size": 13, + "weight": "normal" + }, + "Linux": { + "family": "Roboto", + "size": 13, + "weight": "normal" + } + } +} diff --git a/.venv/Lib/site-packages/customtkinter/assets/themes/dark-blue.json b/.venv/Lib/site-packages/customtkinter/assets/themes/dark-blue.json new file mode 100644 index 0000000..54ff211 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/assets/themes/dark-blue.json @@ -0,0 +1,155 @@ +{ + "CTk": { + "fg_color": ["gray95", "gray10"] + }, + "CTkToplevel": { + "fg_color": ["gray95", "gray10"] + }, + "CTkFrame": { + "corner_radius": 6, + "border_width": 0, + "fg_color": ["gray90", "gray13"], + "top_fg_color": ["gray85", "gray16"], + "border_color": ["gray65", "gray28"] + }, + "CTkButton": { + "corner_radius": 6, + "border_width": 0, + "fg_color": ["#3a7ebf", "#1f538d"], + "hover_color": ["#325882", "#14375e"], + "border_color": ["#3E454A", "#949A9F"], + "text_color": ["#DCE4EE", "#DCE4EE"], + "text_color_disabled": ["gray74", "gray60"] + }, + "CTkLabel": { + "corner_radius": 0, + "fg_color": "transparent", + "text_color": ["gray14", "gray84"] + }, + "CTkEntry": { + "corner_radius": 6, + "border_width": 2, + "fg_color": ["#F9F9FA", "#343638"], + "border_color": ["#979DA2", "#565B5E"], + "text_color": ["gray14", "gray84"], + "placeholder_text_color": ["gray52", "gray62"] + }, + "CTkCheckBox": { + "corner_radius": 6, + "border_width": 3, + "fg_color": ["#3a7ebf", "#1f538d"], + "border_color": ["#3E454A", "#949A9F"], + "hover_color": ["#325882", "#14375e"], + "checkmark_color": ["#DCE4EE", "gray90"], + "text_color": ["gray14", "gray84"], + "text_color_disabled": ["gray60", "gray45"] + }, + "CTkSwitch": { + "corner_radius": 1000, + "border_width": 3, + "button_length": 0, + "fg_color": ["#939BA2", "#4A4D50"], + "progress_color": ["#3a7ebf", "#1f538d"], + "button_color": ["gray36", "#D5D9DE"], + "button_hover_color": ["gray20", "gray100"], + "text_color": ["gray14", "gray84"], + "text_color_disabled": ["gray60", "gray45"] + }, + "CTkRadioButton": { + "corner_radius": 1000, + "border_width_checked": 6, + "border_width_unchecked": 3, + "fg_color": ["#3a7ebf", "#1f538d"], + "border_color": ["#3E454A", "#949A9F"], + "hover_color": ["#325882", "#14375e"], + "text_color": ["gray14", "gray84"], + "text_color_disabled": ["gray60", "gray45"] + }, + "CTkProgressBar": { + "corner_radius": 1000, + "border_width": 0, + "fg_color": ["#939BA2", "#4A4D50"], + "progress_color": ["#3a7ebf", "#1f538d"], + "border_color": ["gray", "gray"] + }, + "CTkSlider": { + "corner_radius": 1000, + "button_corner_radius": 1000, + "border_width": 6, + "button_length": 0, + "fg_color": ["#939BA2", "#4A4D50"], + "progress_color": ["gray40", "#AAB0B5"], + "button_color": ["#3a7ebf", "#1f538d"], + "button_hover_color": ["#325882", "#14375e"] + }, + "CTkOptionMenu": { + "corner_radius": 6, + "fg_color": ["#3a7ebf", "#1f538d"], + "button_color": ["#325882", "#14375e"], + "button_hover_color": ["#234567", "#1e2c40"], + "text_color": ["#DCE4EE", "#DCE4EE"], + "text_color_disabled": ["gray74", "gray60"] + }, + "CTkComboBox": { + "corner_radius": 6, + "border_width": 2, + "fg_color": ["#F9F9FA", "#343638"], + "border_color": ["#979DA2", "#565B5E"], + "button_color": ["#979DA2", "#565B5E"], + "button_hover_color": ["#6E7174", "#7A848D"], + "text_color": ["gray14", "gray84"], + "text_color_disabled": ["gray50", "gray45"] + }, + "CTkScrollbar": { + "corner_radius": 1000, + "border_spacing": 4, + "fg_color": "transparent", + "button_color": ["gray55", "gray41"], + "button_hover_color": ["gray40", "gray53"] + }, + "CTkSegmentedButton": { + "corner_radius": 6, + "border_width": 2, + "fg_color": ["#979DA2", "gray29"], + "selected_color": ["#3a7ebf", "#1f538d"], + "selected_hover_color": ["#325882", "#14375e"], + "unselected_color": ["#979DA2", "gray29"], + "unselected_hover_color": ["gray70", "gray41"], + "text_color": ["#DCE4EE", "#DCE4EE"], + "text_color_disabled": ["gray74", "gray60"] + }, + "CTkTextbox": { + "corner_radius": 6, + "border_width": 0, + "fg_color": ["gray100", "gray20"], + "border_color": ["#979DA2", "#565B5E"], + "text_color": ["gray14", "gray84"], + "scrollbar_button_color": ["gray55", "gray41"], + "scrollbar_button_hover_color": ["gray40", "gray53"] + }, + "CTkScrollableFrame": { + "label_fg_color": ["gray80", "gray21"] + }, + "DropdownMenu": { + "fg_color": ["gray90", "gray20"], + "hover_color": ["gray75", "gray28"], + "text_color": ["gray14", "gray84"] + }, + "CTkFont": { + "macOS": { + "family": "SF Display", + "size": 13, + "weight": "normal" + }, + "Windows": { + "family": "Roboto", + "size": 13, + "weight": "normal" + }, + "Linux": { + "family": "Roboto", + "size": 13, + "weight": "normal" + } + } +} diff --git a/.venv/Lib/site-packages/customtkinter/assets/themes/green.json b/.venv/Lib/site-packages/customtkinter/assets/themes/green.json new file mode 100644 index 0000000..200012f --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/assets/themes/green.json @@ -0,0 +1,155 @@ +{ + "CTk": { + "fg_color": ["gray92", "gray14"] + }, + "CTkToplevel": { + "fg_color": ["gray92", "gray14"] + }, + "CTkFrame": { + "corner_radius": 6, + "border_width": 0, + "fg_color": ["gray86", "gray17"], + "top_fg_color": ["gray81", "gray20"], + "border_color": ["gray65", "gray28"] + }, + "CTkButton": { + "corner_radius": 6, + "border_width": 0, + "fg_color": ["#2CC985", "#2FA572"], + "hover_color": ["#0C955A", "#106A43"], + "border_color": ["#3E454A", "#949A9F"], + "text_color": ["gray98", "#DCE4EE"], + "text_color_disabled": ["gray78", "gray68"] + }, + "CTkLabel": { + "corner_radius": 0, + "fg_color": "transparent", + "text_color": ["gray10", "#DCE4EE"] + }, + "CTkEntry": { + "corner_radius": 6, + "border_width": 2, + "fg_color": ["#F9F9FA", "#343638"], + "border_color": ["#979DA2", "#565B5E"], + "text_color":["gray10", "#DCE4EE"], + "placeholder_text_color": ["gray52", "gray62"] + }, + "CTkCheckBox": { + "corner_radius": 6, + "border_width": 3, + "fg_color": ["#2CC985", "#2FA572"], + "border_color": ["#3E454A", "#949A9F"], + "hover_color": ["#0C955A", "#106A43"], + "checkmark_color": ["#DCE4EE", "gray90"], + "text_color": ["gray10", "#DCE4EE"], + "text_color_disabled": ["gray60", "gray45"] + }, + "CTkSwitch": { + "corner_radius": 1000, + "border_width": 3, + "button_length": 0, + "fg_color": ["#939BA2", "#4A4D50"], + "progress_color": ["#2CC985", "#2FA572"], + "button_color": ["gray36", "#D5D9DE"], + "button_hover_color": ["gray20", "gray100"], + "text_color": ["gray10", "#DCE4EE"], + "text_color_disabled": ["gray60", "gray45"] + }, + "CTkRadioButton": { + "corner_radius": 1000, + "border_width_checked": 6, + "border_width_unchecked": 3, + "fg_color": ["#2CC985", "#2FA572"], + "border_color": ["#3E454A", "#949A9F"], + "hover_color":["#0C955A", "#106A43"], + "text_color": ["gray10", "#DCE4EE"], + "text_color_disabled": ["gray60", "gray45"] + }, + "CTkProgressBar": { + "corner_radius": 1000, + "border_width": 0, + "fg_color": ["#939BA2", "#4A4D50"], + "progress_color": ["#2CC985", "#2FA572"], + "border_color": ["gray", "gray"] + }, + "CTkSlider": { + "corner_radius": 1000, + "button_corner_radius": 1000, + "border_width": 6, + "button_length": 0, + "fg_color": ["#939BA2", "#4A4D50"], + "progress_color": ["gray40", "#AAB0B5"], + "button_color": ["#2CC985", "#2FA572"], + "button_hover_color": ["#0C955A", "#106A43"] + }, + "CTkOptionMenu": { + "corner_radius": 6, + "fg_color": ["#2cbe79", "#2FA572"], + "button_color": ["#0C955A", "#106A43"], + "button_hover_color": ["#0b6e3d", "#17472e"], + "text_color": ["gray98", "#DCE4EE"], + "text_color_disabled": ["gray78", "gray68"] + }, + "CTkComboBox": { + "corner_radius": 6, + "border_width": 2, + "fg_color": ["#F9F9FA", "#343638"], + "border_color": ["#979DA2", "#565B5E"], + "button_color": ["#979DA2", "#565B5E"], + "button_hover_color": ["#6E7174", "#7A848D"], + "text_color": ["gray10", "#DCE4EE"], + "text_color_disabled": ["gray50", "gray45"] + }, + "CTkScrollbar": { + "corner_radius": 1000, + "border_spacing": 4, + "fg_color": "transparent", + "button_color": ["gray55", "gray41"], + "button_hover_color": ["gray40", "gray53"] + }, + "CTkSegmentedButton": { + "corner_radius": 6, + "border_width": 2, + "fg_color": ["#979DA2", "gray29"], + "selected_color": ["#2CC985", "#2FA572"], + "selected_hover_color": ["#0C955A", "#106A43"], + "unselected_color": ["#979DA2", "gray29"], + "unselected_hover_color": ["gray70", "gray41"], + "text_color": ["gray98", "#DCE4EE"], + "text_color_disabled": ["gray78", "gray68"] + }, + "CTkTextbox": { + "corner_radius": 6, + "border_width": 0, + "fg_color": ["#F9F9FA", "gray23"], + "border_color": ["#979DA2", "#565B5E"], + "text_color":["gray10", "#DCE4EE"], + "scrollbar_button_color": ["gray55", "gray41"], + "scrollbar_button_hover_color": ["gray40", "gray53"] + }, + "CTkScrollableFrame": { + "label_fg_color": ["gray78", "gray23"] + }, + "DropdownMenu": { + "fg_color": ["gray90", "gray20"], + "hover_color": ["gray75", "gray28"], + "text_color": ["gray10", "gray90"] + }, + "CTkFont": { + "macOS": { + "family": "SF Display", + "size": 13, + "weight": "normal" + }, + "Windows": { + "family": "Roboto", + "size": 13, + "weight": "normal" + }, + "Linux": { + "family": "Roboto", + "size": 13, + "weight": "normal" + } + } +} diff --git a/.venv/Lib/site-packages/customtkinter/windows/__init__.py b/.venv/Lib/site-packages/customtkinter/windows/__init__.py new file mode 100644 index 0000000..ca681b7 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/__init__.py @@ -0,0 +1,3 @@ +from .ctk_tk import CTk +from .ctk_toplevel import CTkToplevel +from .ctk_input_dialog import CTkInputDialog diff --git a/.venv/Lib/site-packages/customtkinter/windows/__pycache__/__init__.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..1819885 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/__pycache__/__init__.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/__pycache__/ctk_input_dialog.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/__pycache__/ctk_input_dialog.cpython-311.pyc new file mode 100644 index 0000000..05c68c3 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/__pycache__/ctk_input_dialog.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/__pycache__/ctk_tk.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/__pycache__/ctk_tk.cpython-311.pyc new file mode 100644 index 0000000..a95d3ba Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/__pycache__/ctk_tk.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/__pycache__/ctk_toplevel.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/__pycache__/ctk_toplevel.cpython-311.pyc new file mode 100644 index 0000000..d063179 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/__pycache__/ctk_toplevel.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/ctk_input_dialog.py b/.venv/Lib/site-packages/customtkinter/windows/ctk_input_dialog.py new file mode 100644 index 0000000..6c4669a --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/ctk_input_dialog.py @@ -0,0 +1,117 @@ +from typing import Union, Tuple, Optional + +from .widgets import CTkLabel +from .widgets import CTkEntry +from .widgets import CTkButton +from .widgets.theme import ThemeManager +from .ctk_toplevel import CTkToplevel +from .widgets.font import CTkFont + + +class CTkInputDialog(CTkToplevel): + """ + Dialog with extra window, message, entry widget, cancel and ok button. + For detailed information check out the documentation. + """ + + def __init__(self, + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color: Optional[Union[str, Tuple[str, str]]] = None, + button_fg_color: Optional[Union[str, Tuple[str, str]]] = None, + button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + button_text_color: Optional[Union[str, Tuple[str, str]]] = None, + entry_fg_color: Optional[Union[str, Tuple[str, str]]] = None, + entry_border_color: Optional[Union[str, Tuple[str, str]]] = None, + entry_text_color: Optional[Union[str, Tuple[str, str]]] = None, + + title: str = "CTkDialog", + font: Optional[Union[tuple, CTkFont]] = None, + text: str = "CTkDialog"): + + super().__init__(fg_color=fg_color) + + self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) + self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(button_hover_color) + self._button_fg_color = ThemeManager.theme["CTkButton"]["fg_color"] if button_fg_color is None else self._check_color_type(button_fg_color) + self._button_hover_color = ThemeManager.theme["CTkButton"]["hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color) + self._button_text_color = ThemeManager.theme["CTkButton"]["text_color"] if button_text_color is None else self._check_color_type(button_text_color) + self._entry_fg_color = ThemeManager.theme["CTkEntry"]["fg_color"] if entry_fg_color is None else self._check_color_type(entry_fg_color) + self._entry_border_color = ThemeManager.theme["CTkEntry"]["border_color"] if entry_border_color is None else self._check_color_type(entry_border_color) + self._entry_text_color = ThemeManager.theme["CTkEntry"]["text_color"] if entry_text_color is None else self._check_color_type(entry_text_color) + + self._user_input: Union[str, None] = None + self._running: bool = False + self._title = title + self._text = text + self._font = font + + self.title(self._title) + self.lift() # lift window on top + self.attributes("-topmost", True) # stay on top + self.protocol("WM_DELETE_WINDOW", self._on_closing) + self.after(10, self._create_widgets) # create widgets with slight delay, to avoid white flickering of background + self.resizable(False, False) + self.grab_set() # make other windows not clickable + + def _create_widgets(self): + self.grid_columnconfigure((0, 1), weight=1) + self.rowconfigure(0, weight=1) + + self._label = CTkLabel(master=self, + width=300, + wraplength=300, + fg_color="transparent", + text_color=self._text_color, + text=self._text, + font=self._font) + self._label.grid(row=0, column=0, columnspan=2, padx=20, pady=20, sticky="ew") + + self._entry = CTkEntry(master=self, + width=230, + fg_color=self._entry_fg_color, + border_color=self._entry_border_color, + text_color=self._entry_text_color, + font=self._font) + self._entry.grid(row=1, column=0, columnspan=2, padx=20, pady=(0, 20), sticky="ew") + + self._ok_button = CTkButton(master=self, + width=100, + border_width=0, + fg_color=self._button_fg_color, + hover_color=self._button_hover_color, + text_color=self._button_text_color, + text='Ok', + font=self._font, + command=self._ok_event) + self._ok_button.grid(row=2, column=0, columnspan=1, padx=(20, 10), pady=(0, 20), sticky="ew") + + self._cancel_button = CTkButton(master=self, + width=100, + border_width=0, + fg_color=self._button_fg_color, + hover_color=self._button_hover_color, + text_color=self._button_text_color, + text='Cancel', + font=self._font, + command=self._cancel_event) + self._cancel_button.grid(row=2, column=1, columnspan=1, padx=(10, 20), pady=(0, 20), sticky="ew") + + self.after(150, lambda: self._entry.focus()) # set focus to entry with slight delay, otherwise it won't work + self._entry.bind("", self._ok_event) + + def _ok_event(self, event=None): + self._user_input = self._entry.get() + self.grab_release() + self.destroy() + + def _on_closing(self): + self.grab_release() + self.destroy() + + def _cancel_event(self): + self.grab_release() + self.destroy() + + def get_input(self): + self.master.wait_window(self) + return self._user_input diff --git a/.venv/Lib/site-packages/customtkinter/windows/ctk_tk.py b/.venv/Lib/site-packages/customtkinter/windows/ctk_tk.py new file mode 100644 index 0000000..e137dc3 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/ctk_tk.py @@ -0,0 +1,333 @@ +import tkinter +import sys +import os +import platform +import ctypes +from typing import Union, Tuple, Optional +from packaging import version + +from .widgets.theme import ThemeManager +from .widgets.scaling import CTkScalingBaseClass +from .widgets.appearance_mode import CTkAppearanceModeBaseClass + +from customtkinter.windows.widgets.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty + +CTK_PARENT_CLASS = tkinter.Tk + + +class CTk(CTK_PARENT_CLASS, CTkAppearanceModeBaseClass, CTkScalingBaseClass): + """ + Main app window with dark titlebar on Windows and macOS. + For detailed information check out the documentation. + """ + + _valid_tk_constructor_arguments: set = {"screenName", "baseName", "className", "useTk", "sync", "use"} + + _valid_tk_configure_arguments: set = {'bd', 'borderwidth', 'class', 'menu', 'relief', 'screen', + 'use', 'container', 'cursor', 'height', + 'highlightthickness', 'padx', 'pady', 'takefocus', 'visual', 'width'} + + _deactivate_macos_window_header_manipulation: bool = False + _deactivate_windows_window_header_manipulation: bool = False + + def __init__(self, + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + **kwargs): + + self._enable_macos_dark_title_bar() + + # call init methods of super classes + CTK_PARENT_CLASS.__init__(self, **pop_from_dict_by_set(kwargs, self._valid_tk_constructor_arguments)) + CTkAppearanceModeBaseClass.__init__(self) + CTkScalingBaseClass.__init__(self, scaling_type="window") + check_kwargs_empty(kwargs, raise_error=True) + + self._current_width = 600 # initial window size, independent of scaling + self._current_height = 500 + self._min_width: int = 0 + self._min_height: int = 0 + self._max_width: int = 1_000_000 + self._max_height: int = 1_000_000 + self._last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs) + + self._fg_color = ThemeManager.theme["CTk"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) + + # set bg of tkinter.Tk + super().configure(bg=self._apply_appearance_mode(self._fg_color)) + + # set title + self.title("CTk") + + # indicator variables + self._iconbitmap_method_called = False # indicates if wm_iconbitmap method got called + self._state_before_windows_set_titlebar_color = None + self._window_exists = False # indicates if the window is already shown through update() or mainloop() after init + self._withdraw_called_before_window_exists = False # indicates if withdraw() was called before window is first shown through update() or mainloop() + self._iconify_called_before_window_exists = False # indicates if iconify() was called before window is first shown through update() or mainloop() + self._block_update_dimensions_event = False + + # save focus before calling withdraw + self.focused_widget_before_widthdraw = None + + # set CustomTkinter titlebar icon (Windows only) + if sys.platform.startswith("win"): + self.after(200, self._windows_set_titlebar_icon) + + # set titlebar color (Windows only) + if sys.platform.startswith("win"): + self._windows_set_titlebar_color(self._get_appearance_mode()) + + self.bind('', self._update_dimensions_event) + self.bind('', self._focus_in_event) + + def destroy(self): + self._disable_macos_dark_title_bar() + + # call destroy methods of super classes + tkinter.Tk.destroy(self) + CTkAppearanceModeBaseClass.destroy(self) + CTkScalingBaseClass.destroy(self) + + def _focus_in_event(self, event): + # sometimes window looses jumps back on macOS if window is selected from Mission Control, so has to be lifted again + if sys.platform == "darwin": + self.lift() + + def _update_dimensions_event(self, event=None): + if not self._block_update_dimensions_event: + + detected_width = super().winfo_width() # detect current window size + detected_height = super().winfo_height() + + # detected_width = event.width + # detected_height = event.height + + if self._current_width != self._reverse_window_scaling(detected_width) or self._current_height != self._reverse_window_scaling(detected_height): + self._current_width = self._reverse_window_scaling(detected_width) # adjust current size according to new size given by event + self._current_height = self._reverse_window_scaling(detected_height) # _current_width and _current_height are independent of the scale + + def _set_scaling(self, new_widget_scaling, new_window_scaling): + super()._set_scaling(new_widget_scaling, new_window_scaling) + + # Force new dimensions on window by using min, max, and geometry. Without min, max it won't work. + super().minsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height)) + super().maxsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height)) + + super().geometry(f"{self._apply_window_scaling(self._current_width)}x{self._apply_window_scaling(self._current_height)}") + + # set new scaled min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window) + self.after(1000, self._set_scaled_min_max) # Why 1000ms delay? Experience! (Everything tested on Windows 11) + + def block_update_dimensions_event(self): + self._block_update_dimensions_event = False + + def unblock_update_dimensions_event(self): + self._block_update_dimensions_event = False + + def _set_scaled_min_max(self): + if self._min_width is not None or self._min_height is not None: + super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height)) + if self._max_width is not None or self._max_height is not None: + super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height)) + + def withdraw(self): + if self._window_exists is False: + self._withdraw_called_before_window_exists = True + super().withdraw() + + def iconify(self): + if self._window_exists is False: + self._iconify_called_before_window_exists = True + super().iconify() + + def update(self): + if self._window_exists is False: + if sys.platform.startswith("win"): + if not self._withdraw_called_before_window_exists and not self._iconify_called_before_window_exists: + # print("window dont exists -> deiconify in update") + self.deiconify() + + self._window_exists = True + + super().update() + + def mainloop(self, *args, **kwargs): + if not self._window_exists: + if sys.platform.startswith("win"): + self._windows_set_titlebar_color(self._get_appearance_mode()) + + if not self._withdraw_called_before_window_exists and not self._iconify_called_before_window_exists: + # print("window dont exists -> deiconify in mainloop") + self.deiconify() + + self._window_exists = True + + super().mainloop(*args, **kwargs) + + def resizable(self, width: bool = None, height: bool = None): + current_resizable_values = super().resizable(width, height) + self._last_resizable_args = ([], {"width": width, "height": height}) + + if sys.platform.startswith("win"): + self._windows_set_titlebar_color(self._get_appearance_mode()) + + return current_resizable_values + + def minsize(self, width: int = None, height: int = None): + self._min_width = width + self._min_height = height + if self._current_width < width: + self._current_width = width + if self._current_height < height: + self._current_height = height + super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height)) + + def maxsize(self, width: int = None, height: int = None): + self._max_width = width + self._max_height = height + if self._current_width > width: + self._current_width = width + if self._current_height > height: + self._current_height = height + super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height)) + + def geometry(self, geometry_string: str = None): + if geometry_string is not None: + super().geometry(self._apply_geometry_scaling(geometry_string)) + + # update width and height attributes + width, height, x, y = self._parse_geometry_string(geometry_string) + if width is not None and height is not None: + self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between min and max + self._current_height = max(self._min_height, min(height, self._max_height)) + else: + return self._reverse_geometry_scaling(super().geometry()) + + def configure(self, **kwargs): + if "fg_color" in kwargs: + self._fg_color = self._check_color_type(kwargs.pop("fg_color")) + super().configure(bg=self._apply_appearance_mode(self._fg_color)) + + for child in self.winfo_children(): + try: + child.configure(bg_color=self._fg_color) + except Exception: + pass + + super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_configure_arguments)) + check_kwargs_empty(kwargs) + + def cget(self, attribute_name: str) -> any: + if attribute_name == "fg_color": + return self._fg_color + else: + return super().cget(attribute_name) + + def wm_iconbitmap(self, bitmap=None, default=None): + self._iconbitmap_method_called = True + super().wm_iconbitmap(bitmap, default) + + def iconbitmap(self, bitmap=None, default=None): + self._iconbitmap_method_called = True + super().wm_iconbitmap(bitmap, default) + + def _windows_set_titlebar_icon(self): + try: + # if not the user already called iconbitmap method, set icon + if not self._iconbitmap_method_called: + customtkinter_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.iconbitmap(os.path.join(customtkinter_directory, "assets", "icons", "CustomTkinter_icon_Windows.ico")) + except Exception: + pass + + @classmethod + def _enable_macos_dark_title_bar(cls): + if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS + if version.parse(platform.python_version()) < version.parse("3.10"): + if version.parse(tkinter.Tcl().call("info", "patchlevel")) >= version.parse("8.6.9"): # Tcl/Tk >= 8.6.9 + os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No") + # This command allows dark-mode for all programs + + @classmethod + def _disable_macos_dark_title_bar(cls): + if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS + if version.parse(platform.python_version()) < version.parse("3.10"): + if version.parse(tkinter.Tcl().call("info", "patchlevel")) >= version.parse("8.6.9"): # Tcl/Tk >= 8.6.9 + os.system("defaults delete -g NSRequiresAquaSystemAppearance") + # This command reverts the dark-mode setting for all programs. + + def _windows_set_titlebar_color(self, color_mode: str): + """ + Set the titlebar color of the window to light or dark theme on Microsoft Windows. + + Credits for this function: + https://stackoverflow.com/questions/23836000/can-i-change-the-title-bar-in-tkinter/70724666#70724666 + + MORE INFO: + https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute + """ + + if sys.platform.startswith("win") and not self._deactivate_windows_window_header_manipulation: + + if self._window_exists: + self._state_before_windows_set_titlebar_color = self.state() + # print("window_exists -> state_before_windows_set_titlebar_color: ", self.state_before_windows_set_titlebar_color) + + if self._state_before_windows_set_titlebar_color != "iconic" or self._state_before_windows_set_titlebar_color != "withdrawn": + self.focused_widget_before_widthdraw = self.focus_get() + super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible + else: + # print("window dont exists -> withdraw and update") + self.focused_widget_before_widthdraw = self.focus_get() + super().withdraw() + super().update() + + if color_mode.lower() == "dark": + value = 1 + elif color_mode.lower() == "light": + value = 0 + else: + return + + try: + hwnd = ctypes.windll.user32.GetParent(self.winfo_id()) + DWMWA_USE_IMMERSIVE_DARK_MODE = 20 + DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19 + + # try with DWMWA_USE_IMMERSIVE_DARK_MODE + if ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, + ctypes.byref(ctypes.c_int(value)), + ctypes.sizeof(ctypes.c_int(value))) != 0: + + # try with DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20h1 + ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1, + ctypes.byref(ctypes.c_int(value)), + ctypes.sizeof(ctypes.c_int(value))) + + except Exception as err: + print(err) + + if self._window_exists or True: + # print("window_exists -> return to original state: ", self.state_before_windows_set_titlebar_color) + if self._state_before_windows_set_titlebar_color == "normal": + self.deiconify() + elif self._state_before_windows_set_titlebar_color == "iconic": + self.iconify() + elif self._state_before_windows_set_titlebar_color == "zoomed": + self.state("zoomed") + else: + self.state(self._state_before_windows_set_titlebar_color) # other states + else: + pass # wait for update or mainloop to be called + + if self.focused_widget_before_widthdraw is not None: + self.after(1, self.focused_widget_before_widthdraw.focus) + self.focused_widget_before_widthdraw = None + + def _set_appearance_mode(self, mode_string: str): + super()._set_appearance_mode(mode_string) + + if sys.platform.startswith("win"): + self._windows_set_titlebar_color(mode_string) + + super().configure(bg=self._apply_appearance_mode(self._fg_color)) diff --git a/.venv/Lib/site-packages/customtkinter/windows/ctk_toplevel.py b/.venv/Lib/site-packages/customtkinter/windows/ctk_toplevel.py new file mode 100644 index 0000000..9780380 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/ctk_toplevel.py @@ -0,0 +1,307 @@ +import tkinter +from packaging import version +import sys +import os +import platform +import ctypes +from typing import Union, Tuple, Optional + +from .widgets.theme import ThemeManager +from .widgets.scaling import CTkScalingBaseClass +from .widgets.appearance_mode import CTkAppearanceModeBaseClass + +from customtkinter.windows.widgets.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty + + +class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseClass): + """ + Toplevel window with dark titlebar on Windows and macOS. + For detailed information check out the documentation. + """ + + _valid_tk_toplevel_arguments: set = {"master", "bd", "borderwidth", "class", "container", "cursor", "height", + "highlightbackground", "highlightthickness", "menu", "relief", + "screen", "takefocus", "use", "visual", "width"} + + _deactivate_macos_window_header_manipulation: bool = False + _deactivate_windows_window_header_manipulation: bool = False + + def __init__(self, *args, + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + **kwargs): + + self._enable_macos_dark_title_bar() + + # call init methods of super classes + super().__init__(*args, **pop_from_dict_by_set(kwargs, self._valid_tk_toplevel_arguments)) + CTkAppearanceModeBaseClass.__init__(self) + CTkScalingBaseClass.__init__(self, scaling_type="window") + check_kwargs_empty(kwargs, raise_error=True) + + try: + # Set Windows titlebar icon + if sys.platform.startswith("win"): + customtkinter_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.after(200, lambda: self.iconbitmap(os.path.join(customtkinter_directory, "assets", "icons", "CustomTkinter_icon_Windows.ico"))) + except Exception: + pass + + self._current_width = 200 # initial window size, always without scaling + self._current_height = 200 + self._min_width: int = 0 + self._min_height: int = 0 + self._max_width: int = 1_000_000 + self._max_height: int = 1_000_000 + self._last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs) + + self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) + + # set bg color of tkinter.Toplevel + super().configure(bg=self._apply_appearance_mode(self._fg_color)) + + # set title of tkinter.Toplevel + super().title("CTkToplevel") + + # indicator variables + self._iconbitmap_method_called = True + self._state_before_windows_set_titlebar_color = None + self._windows_set_titlebar_color_called = False # indicates if windows_set_titlebar_color was called, stays True until revert_withdraw_after_windows_set_titlebar_color is called + self._withdraw_called_after_windows_set_titlebar_color = False # indicates if withdraw() was called after windows_set_titlebar_color + self._iconify_called_after_windows_set_titlebar_color = False # indicates if iconify() was called after windows_set_titlebar_color + self._block_update_dimensions_event = False + + # save focus before calling withdraw + self.focused_widget_before_widthdraw = None + + # set CustomTkinter titlebar icon (Windows only) + if sys.platform.startswith("win"): + self.after(200, self._windows_set_titlebar_icon) + + # set titlebar color (Windows only) + if sys.platform.startswith("win"): + self._windows_set_titlebar_color(self._get_appearance_mode()) + + self.bind('', self._update_dimensions_event) + self.bind('', self._focus_in_event) + + def destroy(self): + self._disable_macos_dark_title_bar() + + # call destroy methods of super classes + tkinter.Toplevel.destroy(self) + CTkAppearanceModeBaseClass.destroy(self) + CTkScalingBaseClass.destroy(self) + + def _focus_in_event(self, event): + # sometimes window looses jumps back on macOS if window is selected from Mission Control, so has to be lifted again + if sys.platform == "darwin": + self.lift() + + def _update_dimensions_event(self, event=None): + if not self._block_update_dimensions_event: + detected_width = self.winfo_width() # detect current window size + detected_height = self.winfo_height() + + if self._current_width != self._reverse_window_scaling(detected_width) or self._current_height != self._reverse_window_scaling(detected_height): + self._current_width = self._reverse_window_scaling(detected_width) # adjust current size according to new size given by event + self._current_height = self._reverse_window_scaling(detected_height) # _current_width and _current_height are independent of the scale + + def _set_scaling(self, new_widget_scaling, new_window_scaling): + super()._set_scaling(new_widget_scaling, new_window_scaling) + + # Force new dimensions on window by using min, max, and geometry. Without min, max it won't work. + super().minsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height)) + super().maxsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height)) + + super().geometry(f"{self._apply_window_scaling(self._current_width)}x{self._apply_window_scaling(self._current_height)}") + + # set new scaled min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window) + self.after(1000, self._set_scaled_min_max) # Why 1000ms delay? Experience! (Everything tested on Windows 11) + + def block_update_dimensions_event(self): + self._block_update_dimensions_event = False + + def unblock_update_dimensions_event(self): + self._block_update_dimensions_event = False + + def _set_scaled_min_max(self): + if self._min_width is not None or self._min_height is not None: + super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height)) + if self._max_width is not None or self._max_height is not None: + super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height)) + + def geometry(self, geometry_string: str = None): + if geometry_string is not None: + super().geometry(self._apply_geometry_scaling(geometry_string)) + + # update width and height attributes + width, height, x, y = self._parse_geometry_string(geometry_string) + if width is not None and height is not None: + self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between min and max + self._current_height = max(self._min_height, min(height, self._max_height)) + else: + return self._reverse_geometry_scaling(super().geometry()) + + def withdraw(self): + if self._windows_set_titlebar_color_called: + self._withdraw_called_after_windows_set_titlebar_color = True + super().withdraw() + + def iconify(self): + if self._windows_set_titlebar_color_called: + self._iconify_called_after_windows_set_titlebar_color = True + super().iconify() + + def resizable(self, width: bool = None, height: bool = None): + current_resizable_values = super().resizable(width, height) + self._last_resizable_args = ([], {"width": width, "height": height}) + + if sys.platform.startswith("win"): + self.after(10, lambda: self._windows_set_titlebar_color(self._get_appearance_mode())) + + return current_resizable_values + + def minsize(self, width=None, height=None): + self._min_width = width + self._min_height = height + if self._current_width < width: + self._current_width = width + if self._current_height < height: + self._current_height = height + super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height)) + + def maxsize(self, width=None, height=None): + self._max_width = width + self._max_height = height + if self._current_width > width: + self._current_width = width + if self._current_height > height: + self._current_height = height + super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height)) + + def configure(self, **kwargs): + if "fg_color" in kwargs: + self._fg_color = self._check_color_type(kwargs.pop("fg_color")) + super().configure(bg=self._apply_appearance_mode(self._fg_color)) + + for child in self.winfo_children(): + try: + child.configure(bg_color=self._fg_color) + except Exception: + pass + + super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_toplevel_arguments)) + check_kwargs_empty(kwargs) + + def cget(self, attribute_name: str) -> any: + if attribute_name == "fg_color": + return self._fg_color + else: + return super().cget(attribute_name) + + def wm_iconbitmap(self, bitmap=None, default=None): + self._iconbitmap_method_called = True + super().wm_iconbitmap(bitmap, default) + + def _windows_set_titlebar_icon(self): + try: + # if not the user already called iconbitmap method, set icon + if not self._iconbitmap_method_called: + customtkinter_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.iconbitmap(os.path.join(customtkinter_directory, "assets", "icons", "CustomTkinter_icon_Windows.ico")) + except Exception: + pass + + @classmethod + def _enable_macos_dark_title_bar(cls): + if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS + if version.parse(platform.python_version()) < version.parse("3.10"): + if version.parse(tkinter.Tcl().call("info", "patchlevel")) >= version.parse("8.6.9"): # Tcl/Tk >= 8.6.9 + os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No") + + @classmethod + def _disable_macos_dark_title_bar(cls): + if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS + if version.parse(platform.python_version()) < version.parse("3.10"): + if version.parse(tkinter.Tcl().call("info", "patchlevel")) >= version.parse("8.6.9"): # Tcl/Tk >= 8.6.9 + os.system("defaults delete -g NSRequiresAquaSystemAppearance") + # This command reverts the dark-mode setting for all programs. + + def _windows_set_titlebar_color(self, color_mode: str): + """ + Set the titlebar color of the window to light or dark theme on Microsoft Windows. + + Credits for this function: + https://stackoverflow.com/questions/23836000/can-i-change-the-title-bar-in-tkinter/70724666#70724666 + + MORE INFO: + https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute + """ + + if sys.platform.startswith("win") and not self._deactivate_windows_window_header_manipulation: + + self._state_before_windows_set_titlebar_color = self.state() + self.focused_widget_before_widthdraw = self.focus_get() + super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible + super().update() + + if color_mode.lower() == "dark": + value = 1 + elif color_mode.lower() == "light": + value = 0 + else: + return + + try: + hwnd = ctypes.windll.user32.GetParent(self.winfo_id()) + DWMWA_USE_IMMERSIVE_DARK_MODE = 20 + DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19 + + # try with DWMWA_USE_IMMERSIVE_DARK_MODE + if ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, + ctypes.byref(ctypes.c_int(value)), + ctypes.sizeof(ctypes.c_int(value))) != 0: + # try with DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20h1 + ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1, + ctypes.byref(ctypes.c_int(value)), + ctypes.sizeof(ctypes.c_int(value))) + + except Exception as err: + print(err) + + self._windows_set_titlebar_color_called = True + self.after(5, self._revert_withdraw_after_windows_set_titlebar_color) + + if self.focused_widget_before_widthdraw is not None: + self.after(10, self.focused_widget_before_widthdraw.focus) + self.focused_widget_before_widthdraw = None + + def _revert_withdraw_after_windows_set_titlebar_color(self): + """ if in a short time (5ms) after """ + if self._windows_set_titlebar_color_called: + + if self._withdraw_called_after_windows_set_titlebar_color: + pass # leave it withdrawed + elif self._iconify_called_after_windows_set_titlebar_color: + super().iconify() + else: + if self._state_before_windows_set_titlebar_color == "normal": + self.deiconify() + elif self._state_before_windows_set_titlebar_color == "iconic": + self.iconify() + elif self._state_before_windows_set_titlebar_color == "zoomed": + self.state("zoomed") + else: + self.state(self._state_before_windows_set_titlebar_color) # other states + + self._windows_set_titlebar_color_called = False + self._withdraw_called_after_windows_set_titlebar_color = False + self._iconify_called_after_windows_set_titlebar_color = False + + def _set_appearance_mode(self, mode_string): + super()._set_appearance_mode(mode_string) + + if sys.platform.startswith("win"): + self._windows_set_titlebar_color(mode_string) + + super().configure(bg=self._apply_appearance_mode(self._fg_color)) diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/__init__.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/__init__.py new file mode 100644 index 0000000..a75c63d --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/__init__.py @@ -0,0 +1,16 @@ +from .ctk_button import CTkButton +from .ctk_checkbox import CTkCheckBox +from .ctk_combobox import CTkComboBox +from .ctk_entry import CTkEntry +from .ctk_frame import CTkFrame +from .ctk_label import CTkLabel +from .ctk_optionmenu import CTkOptionMenu +from .ctk_progressbar import CTkProgressBar +from .ctk_radiobutton import CTkRadioButton +from .ctk_scrollbar import CTkScrollbar +from .ctk_segmented_button import CTkSegmentedButton +from .ctk_slider import CTkSlider +from .ctk_switch import CTkSwitch +from .ctk_tabview import CTkTabview +from .ctk_textbox import CTkTextbox +from .ctk_scrollable_frame import CTkScrollableFrame diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/__init__.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..4796eaa Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/__init__.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_button.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_button.cpython-311.pyc new file mode 100644 index 0000000..b059779 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_button.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_checkbox.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_checkbox.cpython-311.pyc new file mode 100644 index 0000000..0966935 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_checkbox.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_combobox.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_combobox.cpython-311.pyc new file mode 100644 index 0000000..6f08cff Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_combobox.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_entry.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_entry.cpython-311.pyc new file mode 100644 index 0000000..021a63a Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_entry.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_frame.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_frame.cpython-311.pyc new file mode 100644 index 0000000..11c2e02 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_frame.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_label.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_label.cpython-311.pyc new file mode 100644 index 0000000..7b20fd0 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_label.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_optionmenu.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_optionmenu.cpython-311.pyc new file mode 100644 index 0000000..7b8409e Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_optionmenu.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_progressbar.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_progressbar.cpython-311.pyc new file mode 100644 index 0000000..3c85941 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_progressbar.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_radiobutton.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_radiobutton.cpython-311.pyc new file mode 100644 index 0000000..f457374 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_radiobutton.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_scrollable_frame.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_scrollable_frame.cpython-311.pyc new file mode 100644 index 0000000..a07a345 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_scrollable_frame.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_scrollbar.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_scrollbar.cpython-311.pyc new file mode 100644 index 0000000..5112937 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_scrollbar.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_segmented_button.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_segmented_button.cpython-311.pyc new file mode 100644 index 0000000..5b044a0 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_segmented_button.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_slider.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_slider.cpython-311.pyc new file mode 100644 index 0000000..99d269b Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_slider.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_switch.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_switch.cpython-311.pyc new file mode 100644 index 0000000..700f35d Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_switch.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_tabview.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_tabview.cpython-311.pyc new file mode 100644 index 0000000..e58ae82 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_tabview.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_textbox.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_textbox.cpython-311.pyc new file mode 100644 index 0000000..ede7318 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/__pycache__/ctk_textbox.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/appearance_mode/__init__.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/appearance_mode/__init__.py new file mode 100644 index 0000000..e979ca8 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/appearance_mode/__init__.py @@ -0,0 +1,4 @@ +from .appearance_mode_base_class import CTkAppearanceModeBaseClass +from .appearance_mode_tracker import AppearanceModeTracker + +AppearanceModeTracker.init_appearance_mode() diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/appearance_mode/__pycache__/__init__.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/appearance_mode/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..1bdfb9b Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/appearance_mode/__pycache__/__init__.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/appearance_mode/__pycache__/appearance_mode_base_class.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/appearance_mode/__pycache__/appearance_mode_base_class.cpython-311.pyc new file mode 100644 index 0000000..9f0907e Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/appearance_mode/__pycache__/appearance_mode_base_class.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/appearance_mode/__pycache__/appearance_mode_tracker.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/appearance_mode/__pycache__/appearance_mode_tracker.cpython-311.pyc new file mode 100644 index 0000000..fb38aba Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/appearance_mode/__pycache__/appearance_mode_tracker.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py new file mode 100644 index 0000000..b7f757a --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py @@ -0,0 +1,61 @@ +from typing import Union, Tuple, List + +from .appearance_mode_tracker import AppearanceModeTracker + + +class CTkAppearanceModeBaseClass: + """ + Super-class that manages the appearance mode. Methods: + + - destroy() must be called when sub-class is destroyed + - _set_appearance_mode() abstractmethod, gets called when appearance mode changes, must be overridden + - _apply_appearance_mode() to convert tuple color + + """ + def __init__(self): + AppearanceModeTracker.add(self._set_appearance_mode, self) + self.__appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" + + def destroy(self): + AppearanceModeTracker.remove(self._set_appearance_mode) + + def _set_appearance_mode(self, mode_string: str): + """ can be overridden but super method must be called at the beginning """ + if mode_string.lower() == "dark": + self.__appearance_mode = 1 + elif mode_string.lower() == "light": + self.__appearance_mode = 0 + + def _get_appearance_mode(self) -> str: + """ get appearance mode as a string, 'light' or 'dark' """ + if self.__appearance_mode == 0: + return "light" + else: + return "dark" + + def _apply_appearance_mode(self, color: Union[str, Tuple[str, str], List[str]]) -> str: + """ + color can be either a single hex color string or a color name or it can be a + tuple color with (light_color, dark_color). The functions returns + always a single color string + """ + + if isinstance(color, (tuple, list)): + return color[self.__appearance_mode] + else: + return color + + @staticmethod + def _check_color_type(color: any, transparency: bool = False): + if color is None: + raise ValueError(f"color is None, for transparency set color='transparent'") + elif isinstance(color, (tuple, list)) and (color[0] == "transparent" or color[1] == "transparent"): + raise ValueError(f"transparency is not allowed in tuple color {color}, use 'transparent'") + elif color == "transparent" and transparency is False: + raise ValueError(f"transparency is not allowed for this attribute") + elif isinstance(color, str): + return color + elif isinstance(color, (tuple, list)) and len(color) == 2 and isinstance(color[0], str) and isinstance(color[1], str): + return color + else: + raise ValueError(f"color {color} must be string ('transparent' or 'color-name' or 'hex-color') or tuple of two strings, not {type(color)}") diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/appearance_mode/appearance_mode_tracker.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/appearance_mode/appearance_mode_tracker.py new file mode 100644 index 0000000..eb20a73 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/appearance_mode/appearance_mode_tracker.py @@ -0,0 +1,122 @@ +import tkinter +from typing import Callable +import darkdetect + + +class AppearanceModeTracker: + + callback_list = [] + app_list = [] + update_loop_running = False + update_loop_interval = 30 # milliseconds + + appearance_mode_set_by = "system" + appearance_mode = 0 # Light (standard) + + @classmethod + def init_appearance_mode(cls): + if cls.appearance_mode_set_by == "system": + new_appearance_mode = cls.detect_appearance_mode() + + if new_appearance_mode != cls.appearance_mode: + cls.appearance_mode = new_appearance_mode + cls.update_callbacks() + + @classmethod + def add(cls, callback: Callable, widget=None): + cls.callback_list.append(callback) + + if widget is not None: + app = cls.get_tk_root_of_widget(widget) + if app not in cls.app_list: + cls.app_list.append(app) + + if not cls.update_loop_running: + app.after(cls.update_loop_interval, cls.update) + cls.update_loop_running = True + + @classmethod + def remove(cls, callback: Callable): + try: + cls.callback_list.remove(callback) + except ValueError: + return + + @staticmethod + def detect_appearance_mode() -> int: + try: + if darkdetect.theme() == "Dark": + return 1 # Dark + else: + return 0 # Light + except NameError: + return 0 # Light + + @classmethod + def get_tk_root_of_widget(cls, widget): + current_widget = widget + + while isinstance(current_widget, tkinter.Tk) is False: + current_widget = current_widget.master + + return current_widget + + @classmethod + def update_callbacks(cls): + if cls.appearance_mode == 0: + for callback in cls.callback_list: + try: + callback("Light") + except Exception: + continue + + elif cls.appearance_mode == 1: + for callback in cls.callback_list: + try: + callback("Dark") + except Exception: + continue + + @classmethod + def update(cls): + if cls.appearance_mode_set_by == "system": + new_appearance_mode = cls.detect_appearance_mode() + + if new_appearance_mode != cls.appearance_mode: + cls.appearance_mode = new_appearance_mode + cls.update_callbacks() + + # find an existing tkinter.Tk object for the next call of .after() + for app in cls.app_list: + try: + app.after(cls.update_loop_interval, cls.update) + return + except Exception: + continue + + cls.update_loop_running = False + + @classmethod + def get_mode(cls) -> int: + return cls.appearance_mode + + @classmethod + def set_appearance_mode(cls, mode_string: str): + if mode_string.lower() == "dark": + cls.appearance_mode_set_by = "user" + new_appearance_mode = 1 + + if new_appearance_mode != cls.appearance_mode: + cls.appearance_mode = new_appearance_mode + cls.update_callbacks() + + elif mode_string.lower() == "light": + cls.appearance_mode_set_by = "user" + new_appearance_mode = 0 + + if new_appearance_mode != cls.appearance_mode: + cls.appearance_mode = new_appearance_mode + cls.update_callbacks() + + elif mode_string.lower() == "system": + cls.appearance_mode_set_by = "system" diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/core_rendering/__init__.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_rendering/__init__.py new file mode 100644 index 0000000..ccadbc7 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_rendering/__init__.py @@ -0,0 +1,12 @@ +import sys + +from .ctk_canvas import CTkCanvas +from .draw_engine import DrawEngine + +CTkCanvas.init_font_character_mapping() + +# determine draw method based on current platform +if sys.platform == "darwin": + DrawEngine.preferred_drawing_method = "polygon_shapes" +else: + DrawEngine.preferred_drawing_method = "font_shapes" diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/core_rendering/__pycache__/__init__.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_rendering/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..64064e8 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_rendering/__pycache__/__init__.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/core_rendering/__pycache__/ctk_canvas.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_rendering/__pycache__/ctk_canvas.cpython-311.pyc new file mode 100644 index 0000000..f888284 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_rendering/__pycache__/ctk_canvas.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/core_rendering/__pycache__/draw_engine.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_rendering/__pycache__/draw_engine.cpython-311.pyc new file mode 100644 index 0000000..05cde0a Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_rendering/__pycache__/draw_engine.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/core_rendering/ctk_canvas.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_rendering/ctk_canvas.py new file mode 100644 index 0000000..f291e2c --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_rendering/ctk_canvas.py @@ -0,0 +1,117 @@ +import tkinter +import sys +from typing import Union, Tuple + + +class CTkCanvas(tkinter.Canvas): + """ + Canvas with additional functionality to draw antialiased circles on Windows/Linux. + + Call .init_font_character_mapping() at program start to load the correct character + dictionary according to the operating system. Characters (circle sizes) are optimised + to look best for rendering CustomTkinter shapes on the different operating systems. + + - .create_aa_circle() creates antialiased circle and returns int identifier. + - .coords() is modified to support the aa-circle shapes correctly like you would expect. + - .itemconfig() is also modified to support aa-cricle shapes. + + The aa-circles are created by choosing a character from the custom created and loaded + font 'CustomTkinter_shapes_font'. It contains circle shapes with different sizes filling + either the whole character space or just pert of it (characters A to R). Circles with a smaller + radius need a smaller circle character to look correct when rendered on the canvas. + + For an optimal result, the draw-engine creates two aa-circles on top of each other, while + one is rotated by 90 degrees. This helps to make the circle look more symetric, which is + not can be a problem when using only a single circle character. + """ + + radius_to_char_fine: dict = None # dict to map radius to font circle character + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._aa_circle_canvas_ids = set() + + @classmethod + def init_font_character_mapping(cls): + """ optimizations made for Windows 10, 11 only """ + + radius_to_char_warped = {19: 'B', 18: 'B', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'B', 12: 'B', 11: 'B', + 10: 'B', + 9: 'C', 8: 'D', 7: 'C', 6: 'E', 5: 'F', 4: 'G', 3: 'H', 2: 'H', 1: 'H', 0: 'A'} + + radius_to_char_fine_windows_10 = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'C', 12: 'C', + 11: 'C', 10: 'C', + 9: 'D', 8: 'D', 7: 'D', 6: 'C', 5: 'D', 4: 'G', 3: 'G', 2: 'H', 1: 'H', + 0: 'A'} + + radius_to_char_fine_windows_11 = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'C', 12: 'C', + 11: 'D', 10: 'D', + 9: 'E', 8: 'F', 7: 'C', 6: 'I', 5: 'E', 4: 'G', 3: 'P', 2: 'R', 1: 'R', + 0: 'A'} + + radius_to_char_fine_linux = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'F', 12: 'C', + 11: 'F', 10: 'C', + 9: 'D', 8: 'G', 7: 'D', 6: 'F', 5: 'D', 4: 'G', 3: 'M', 2: 'H', 1: 'H', + 0: 'A'} + + if sys.platform.startswith("win"): + if sys.getwindowsversion().build > 20000: # Windows 11 + cls.radius_to_char_fine = radius_to_char_fine_windows_11 + else: # < Windows 11 + cls.radius_to_char_fine = radius_to_char_fine_windows_10 + elif sys.platform.startswith("linux"): # Optimized on Kali Linux + cls.radius_to_char_fine = radius_to_char_fine_linux + else: + cls.radius_to_char_fine = radius_to_char_fine_windows_10 + + def _get_char_from_radius(self, radius: int) -> str: + if radius >= 20: + return "A" + else: + return self.radius_to_char_fine[radius] + + def create_aa_circle(self, x_pos: int, y_pos: int, radius: int, angle: int = 0, fill: str = "white", + tags: Union[str, Tuple[str, ...]] = "", anchor: str = tkinter.CENTER) -> int: + # create a circle with a font element + circle_1 = self.create_text(x_pos, y_pos, text=self._get_char_from_radius(radius), anchor=anchor, fill=fill, + font=("CustomTkinter_shapes_font", -radius * 2), tags=tags, angle=angle) + self.addtag_withtag("ctk_aa_circle_font_element", circle_1) + self._aa_circle_canvas_ids.add(circle_1) + + return circle_1 + + def coords(self, tag_or_id, *args): + + if type(tag_or_id) == str and "ctk_aa_circle_font_element" in self.gettags(tag_or_id): + coords_id = self.find_withtag(tag_or_id)[0] # take the lowest id for the given tag + super().coords(coords_id, *args[:2]) + + if len(args) == 3: + super().itemconfigure(coords_id, font=("CustomTkinter_shapes_font", -int(args[2]) * 2), text=self._get_char_from_radius(args[2])) + + elif type(tag_or_id) == int and tag_or_id in self._aa_circle_canvas_ids: + super().coords(tag_or_id, *args[:2]) + + if len(args) == 3: + super().itemconfigure(tag_or_id, font=("CustomTkinter_shapes_font", -args[2] * 2), text=self._get_char_from_radius(args[2])) + + else: + super().coords(tag_or_id, *args) + + def itemconfig(self, tag_or_id, *args, **kwargs): + kwargs_except_outline = kwargs.copy() + if "outline" in kwargs_except_outline: + del kwargs_except_outline["outline"] + + if type(tag_or_id) == int: + if tag_or_id in self._aa_circle_canvas_ids: + super().itemconfigure(tag_or_id, *args, **kwargs_except_outline) + else: + super().itemconfigure(tag_or_id, *args, **kwargs) + else: + configure_ids = self.find_withtag(tag_or_id) + for configure_id in configure_ids: + if configure_id in self._aa_circle_canvas_ids: + super().itemconfigure(configure_id, *args, **kwargs_except_outline) + else: + super().itemconfigure(configure_id, *args, **kwargs) diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/core_rendering/draw_engine.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_rendering/draw_engine.py new file mode 100644 index 0000000..5acea56 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_rendering/draw_engine.py @@ -0,0 +1,1235 @@ +from __future__ import annotations +import sys +import math +import tkinter +from typing import Union, TYPE_CHECKING + +if TYPE_CHECKING: + from ..core_rendering import CTkCanvas + + +class DrawEngine: + """ + This is the core of the CustomTkinter library where all the drawing on the tkinter.Canvas happens. + A year of experimenting and trying out different drawing methods have led to the current state of this + class, and I don't think there's much I can do to make the rendering look better than this with the + limited capabilities the tkinter.Canvas offers. + + Functions: + - draw_rounded_rect_with_border() + - draw_rounded_rect_with_border_vertical_split() + - draw_rounded_progress_bar_with_border() + - draw_rounded_slider_with_border_and_button() + - draw_rounded_scrollbar() + - draw_checkmark() + - draw_dropdown_arrow() + + """ + + preferred_drawing_method: str = None # 'polygon_shapes', 'font_shapes', 'circle_shapes' + + def __init__(self, canvas: CTkCanvas): + self._canvas = canvas + self._round_width_to_even_numbers: bool = True + self._round_height_to_even_numbers: bool = True + + def set_round_to_even_numbers(self, round_width_to_even_numbers: bool = True, round_height_to_even_numbers: bool = True): + self._round_width_to_even_numbers: bool = round_width_to_even_numbers + self._round_height_to_even_numbers: bool = round_height_to_even_numbers + + def __calc_optimal_corner_radius(self, user_corner_radius: Union[float, int]) -> Union[float, int]: + # optimize for drawing with polygon shapes + if self.preferred_drawing_method == "polygon_shapes": + if sys.platform == "darwin": + return user_corner_radius + else: + return round(user_corner_radius) + + # optimize for drawing with antialiased font shapes + elif self.preferred_drawing_method == "font_shapes": + return round(user_corner_radius) + + # optimize for drawing with circles and rects + elif self.preferred_drawing_method == "circle_shapes": + user_corner_radius = 0.5 * round(user_corner_radius / 0.5) # round to 0.5 steps + + # make sure the value is always with .5 at the end for smoother corners + if user_corner_radius == 0: + return 0 + elif user_corner_radius % 1 == 0: + return user_corner_radius + 0.5 + else: + return user_corner_radius + + def draw_background_corners(self, width: Union[float, int], height: Union[float, int], ): + if self._round_width_to_even_numbers: + width = math.floor(width / 2) * 2 # round (floor) _current_width and _current_height and restrict them to even values only + if self._round_height_to_even_numbers: + height = math.floor(height / 2) * 2 + + requires_recoloring = False + + if not self._canvas.find_withtag("background_corner_top_left"): + self._canvas.create_rectangle((0, 0, 0, 0), tags=("background_parts", "background_corner_top_left"), width=0) + requires_recoloring = True + if not self._canvas.find_withtag("background_corner_top_right"): + self._canvas.create_rectangle((0, 0, 0, 0), tags=("background_parts", "background_corner_top_right"), width=0) + requires_recoloring = True + if not self._canvas.find_withtag("background_corner_bottom_right"): + self._canvas.create_rectangle((0, 0, 0, 0), tags=("background_parts", "background_corner_bottom_right"), width=0) + requires_recoloring = True + if not self._canvas.find_withtag("background_corner_bottom_left"): + self._canvas.create_rectangle((0, 0, 0, 0), tags=("background_parts", "background_corner_bottom_left"), width=0) + requires_recoloring = True + + mid_width, mid_height = round(width / 2), round(height / 2) + self._canvas.coords("background_corner_top_left", (0, 0, mid_width, mid_height)) + self._canvas.coords("background_corner_top_right", (mid_width, 0, width, mid_height)) + self._canvas.coords("background_corner_bottom_right", (mid_width, mid_height, width, height)) + self._canvas.coords("background_corner_bottom_left", (0, mid_height, mid_width, height)) + + if requires_recoloring: # new parts were added -> manage z-order + self._canvas.tag_lower("background_parts") + + return requires_recoloring + + def draw_rounded_rect_with_border(self, width: Union[float, int], height: Union[float, int], corner_radius: Union[float, int], + border_width: Union[float, int], overwrite_preferred_drawing_method: str = None) -> bool: + """ Draws a rounded rectangle with a corner_radius and border_width on the canvas. The border elements have a 'border_parts' tag, + the main foreground elements have an 'inner_parts' tag to color the elements accordingly. + + returns bool if recoloring is necessary """ + + if self._round_width_to_even_numbers: + width = math.floor(width / 2) * 2 # round (floor) _current_width and _current_height and restrict them to even values only + if self._round_height_to_even_numbers: + height = math.floor(height / 2) * 2 + corner_radius = round(corner_radius) + + if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too large + corner_radius = min(width / 2, height / 2) + + border_width = round(border_width) + corner_radius = self.__calc_optimal_corner_radius(corner_radius) # optimize corner_radius for different drawing methods (different rounding) + + if corner_radius >= border_width: + inner_corner_radius = corner_radius - border_width + else: + inner_corner_radius = 0 + + if overwrite_preferred_drawing_method is not None: + preferred_drawing_method = overwrite_preferred_drawing_method + else: + preferred_drawing_method = self.preferred_drawing_method + + if preferred_drawing_method == "polygon_shapes": + return self.__draw_rounded_rect_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius) + elif preferred_drawing_method == "font_shapes": + return self.__draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, ()) + elif preferred_drawing_method == "circle_shapes": + return self.__draw_rounded_rect_with_border_circle_shapes(width, height, corner_radius, border_width, inner_corner_radius) + + def __draw_rounded_rect_with_border_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int) -> bool: + requires_recoloring = False + + # create border button parts (only if border exists) + if border_width > 0: + if not self._canvas.find_withtag("border_parts"): + self._canvas.create_polygon((0, 0, 0, 0), tags=("border_line_1", "border_parts")) + requires_recoloring = True + + self._canvas.coords("border_line_1", + (corner_radius, + corner_radius, + width - corner_radius, + corner_radius, + width - corner_radius, + height - corner_radius, + corner_radius, + height - corner_radius)) + self._canvas.itemconfig("border_line_1", + joinstyle=tkinter.ROUND, + width=corner_radius * 2) + + else: + self._canvas.delete("border_parts") + + # create inner button parts + if not self._canvas.find_withtag("inner_parts"): + self._canvas.create_polygon((0, 0, 0, 0), tags=("inner_line_1", "inner_parts"), joinstyle=tkinter.ROUND) + requires_recoloring = True + + if corner_radius <= border_width: + bottom_right_shift = -1 # weird canvas rendering inaccuracy that has to be corrected in some cases + else: + bottom_right_shift = 0 + + self._canvas.coords("inner_line_1", + border_width + inner_corner_radius, + border_width + inner_corner_radius, + width - (border_width + inner_corner_radius) + bottom_right_shift, + border_width + inner_corner_radius, + width - (border_width + inner_corner_radius) + bottom_right_shift, + height - (border_width + inner_corner_radius) + bottom_right_shift, + border_width + inner_corner_radius, + height - (border_width + inner_corner_radius) + bottom_right_shift) + self._canvas.itemconfig("inner_line_1", + width=inner_corner_radius * 2) + + if requires_recoloring: # new parts were added -> manage z-order + self._canvas.tag_lower("inner_parts") + self._canvas.tag_lower("border_parts") + self._canvas.tag_lower("background_parts") + + return requires_recoloring + + def __draw_rounded_rect_with_border_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int, + exclude_parts: tuple) -> bool: + requires_recoloring = False + + # create border button parts + if border_width > 0: + if corner_radius > 0: + # create canvas border corner parts if not already created, but only if needed, and delete if not needed + if not self._canvas.find_withtag("border_oval_1_a") and "border_oval_1" not in exclude_parts: + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_1_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_1_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("border_oval_1_a") and "border_oval_1" in exclude_parts: + self._canvas.delete("border_oval_1_a", "border_oval_1_b") + + if not self._canvas.find_withtag("border_oval_2_a") and width > 2 * corner_radius and "border_oval_2" not in exclude_parts: + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_2_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_2_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("border_oval_2_a") and (not width > 2 * corner_radius or "border_oval_2" in exclude_parts): + self._canvas.delete("border_oval_2_a", "border_oval_2_b") + + if not self._canvas.find_withtag("border_oval_3_a") and height > 2 * corner_radius \ + and width > 2 * corner_radius and "border_oval_3" not in exclude_parts: + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("border_oval_3_a") and (not (height > 2 * corner_radius + and width > 2 * corner_radius) or "border_oval_3" in exclude_parts): + self._canvas.delete("border_oval_3_a", "border_oval_3_b") + + if not self._canvas.find_withtag("border_oval_4_a") and height > 2 * corner_radius and "border_oval_4" not in exclude_parts: + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_4_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_4_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("border_oval_4_a") and (not height > 2 * corner_radius or "border_oval_4" in exclude_parts): + self._canvas.delete("border_oval_4_a", "border_oval_4_b") + + # change position of border corner parts + self._canvas.coords("border_oval_1_a", corner_radius, corner_radius, corner_radius) + self._canvas.coords("border_oval_1_b", corner_radius, corner_radius, corner_radius) + self._canvas.coords("border_oval_2_a", width - corner_radius, corner_radius, corner_radius) + self._canvas.coords("border_oval_2_b", width - corner_radius, corner_radius, corner_radius) + self._canvas.coords("border_oval_3_a", width - corner_radius, height - corner_radius, corner_radius) + self._canvas.coords("border_oval_3_b", width - corner_radius, height - corner_radius, corner_radius) + self._canvas.coords("border_oval_4_a", corner_radius, height - corner_radius, corner_radius) + self._canvas.coords("border_oval_4_b", corner_radius, height - corner_radius, corner_radius) + + else: + self._canvas.delete("border_corner_part") # delete border corner parts if not needed + + # create canvas border rectangle parts if not already created + if not self._canvas.find_withtag("border_rectangle_1"): + self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_1", "border_rectangle_part", "border_parts"), width=0) + self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_2", "border_rectangle_part", "border_parts"), width=0) + requires_recoloring = True + + # change position of border rectangle parts + self._canvas.coords("border_rectangle_1", (0, corner_radius, width, height - corner_radius)) + self._canvas.coords("border_rectangle_2", (corner_radius, 0, width - corner_radius, height)) + + else: + self._canvas.delete("border_parts") + + # create inner button parts + if inner_corner_radius > 0: + + # create canvas border corner parts if not already created, but only if they're needed and delete if not needed + if not self._canvas.find_withtag("inner_oval_1_a") and "inner_oval_1" not in exclude_parts: + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_1_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_1_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("inner_oval_1_a") and "inner_oval_1" in exclude_parts: + self._canvas.delete("inner_oval_1_a", "inner_oval_1_b") + + if not self._canvas.find_withtag("inner_oval_2_a") and width - (2 * border_width) > 2 * inner_corner_radius and "inner_oval_2" not in exclude_parts: + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_2_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_2_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("inner_oval_2_a") and (not width - (2 * border_width) > 2 * inner_corner_radius or "inner_oval_2" in exclude_parts): + self._canvas.delete("inner_oval_2_a", "inner_oval_2_b") + + if not self._canvas.find_withtag("inner_oval_3_a") and height - (2 * border_width) > 2 * inner_corner_radius \ + and width - (2 * border_width) > 2 * inner_corner_radius and "inner_oval_3" not in exclude_parts: + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_3_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_3_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("inner_oval_3_a") and (not (height - (2 * border_width) > 2 * inner_corner_radius + and width - (2 * border_width) > 2 * inner_corner_radius) or "inner_oval_3" in exclude_parts): + self._canvas.delete("inner_oval_3_a", "inner_oval_3_b") + + if not self._canvas.find_withtag("inner_oval_4_a") and height - (2 * border_width) > 2 * inner_corner_radius and "inner_oval_4" not in exclude_parts: + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_4_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_4_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("inner_oval_4_a") and (not height - (2 * border_width) > 2 * inner_corner_radius or "inner_oval_4" in exclude_parts): + self._canvas.delete("inner_oval_4_a", "inner_oval_4_b") + + # change position of border corner parts + self._canvas.coords("inner_oval_1_a", border_width + inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius) + self._canvas.coords("inner_oval_1_b", border_width + inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius) + self._canvas.coords("inner_oval_2_a", width - border_width - inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius) + self._canvas.coords("inner_oval_2_b", width - border_width - inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius) + self._canvas.coords("inner_oval_3_a", width - border_width - inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius) + self._canvas.coords("inner_oval_3_b", width - border_width - inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius) + self._canvas.coords("inner_oval_4_a", border_width + inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius) + self._canvas.coords("inner_oval_4_b", border_width + inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius) + else: + self._canvas.delete("inner_corner_part") # delete inner corner parts if not needed + + # create canvas inner rectangle parts if not already created + if not self._canvas.find_withtag("inner_rectangle_1"): + self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_1", "inner_rectangle_part", "inner_parts"), width=0) + requires_recoloring = True + + if not self._canvas.find_withtag("inner_rectangle_2") and inner_corner_radius * 2 < height - (border_width * 2): + self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_2", "inner_rectangle_part", "inner_parts"), width=0) + requires_recoloring = True + + elif self._canvas.find_withtag("inner_rectangle_2") and not inner_corner_radius * 2 < height - (border_width * 2): + self._canvas.delete("inner_rectangle_2") + + # change position of inner rectangle parts + self._canvas.coords("inner_rectangle_1", (border_width + inner_corner_radius, + border_width, + width - border_width - inner_corner_radius, + height - border_width)) + self._canvas.coords("inner_rectangle_2", (border_width, + border_width + inner_corner_radius, + width - border_width, + height - inner_corner_radius - border_width)) + + if requires_recoloring: # new parts were added -> manage z-order + self._canvas.tag_lower("inner_parts") + self._canvas.tag_lower("border_parts") + self._canvas.tag_lower("background_parts") + + return requires_recoloring + + def __draw_rounded_rect_with_border_circle_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int) -> bool: + requires_recoloring = False + + # border button parts + if border_width > 0: + if corner_radius > 0: + + if not self._canvas.find_withtag("border_oval_1"): + self._canvas.create_oval(0, 0, 0, 0, tags=("border_oval_1", "border_corner_part", "border_parts"), width=0) + self._canvas.create_oval(0, 0, 0, 0, tags=("border_oval_2", "border_corner_part", "border_parts"), width=0) + self._canvas.create_oval(0, 0, 0, 0, tags=("border_oval_3", "border_corner_part", "border_parts"), width=0) + self._canvas.create_oval(0, 0, 0, 0, tags=("border_oval_4", "border_corner_part", "border_parts"), width=0) + self._canvas.tag_lower("border_parts") + requires_recoloring = True + + self._canvas.coords("border_oval_1", 0, 0, corner_radius * 2 - 1, corner_radius * 2 - 1) + self._canvas.coords("border_oval_2", width - corner_radius * 2, 0, width - 1, corner_radius * 2 - 1) + self._canvas.coords("border_oval_3", 0, height - corner_radius * 2, corner_radius * 2 - 1, height - 1) + self._canvas.coords("border_oval_4", width - corner_radius * 2, height - corner_radius * 2, width - 1, height - 1) + + else: + self._canvas.delete("border_corner_part") + + if not self._canvas.find_withtag("border_rectangle_1"): + self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_1", "border_rectangle_part", "border_parts"), width=0) + self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_2", "border_rectangle_part", "border_parts"), width=0) + self._canvas.tag_lower("border_parts") + requires_recoloring = True + + self._canvas.coords("border_rectangle_1", (0, corner_radius, width, height - corner_radius)) + self._canvas.coords("border_rectangle_2", (corner_radius, 0, width - corner_radius, height)) + + else: + self._canvas.delete("border_parts") + + # inner button parts + if inner_corner_radius > 0: + + if not self._canvas.find_withtag("inner_oval_1"): + self._canvas.create_oval(0, 0, 0, 0, tags=("inner_oval_1", "inner_corner_part", "inner_parts"), width=0) + self._canvas.create_oval(0, 0, 0, 0, tags=("inner_oval_2", "inner_corner_part", "inner_parts"), width=0) + self._canvas.create_oval(0, 0, 0, 0, tags=("inner_oval_3", "inner_corner_part", "inner_parts"), width=0) + self._canvas.create_oval(0, 0, 0, 0, tags=("inner_oval_4", "inner_corner_part", "inner_parts"), width=0) + self._canvas.tag_raise("inner_parts") + requires_recoloring = True + + self._canvas.coords("inner_oval_1", (border_width, border_width, + border_width + inner_corner_radius * 2 - 1, border_width + inner_corner_radius * 2 - 1)) + self._canvas.coords("inner_oval_2", (width - border_width - inner_corner_radius * 2, border_width, + width - border_width - 1, border_width + inner_corner_radius * 2 - 1)) + self._canvas.coords("inner_oval_3", (border_width, height - border_width - inner_corner_radius * 2, + border_width + inner_corner_radius * 2 - 1, height - border_width - 1)) + self._canvas.coords("inner_oval_4", (width - border_width - inner_corner_radius * 2, height - border_width - inner_corner_radius * 2, + width - border_width - 1, height - border_width - 1)) + else: + self._canvas.delete("inner_corner_part") # delete inner corner parts if not needed + + if not self._canvas.find_withtag("inner_rectangle_1"): + self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_1", "inner_rectangle_part", "inner_parts"), width=0) + self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_2", "inner_rectangle_part", "inner_parts"), width=0) + self._canvas.tag_raise("inner_parts") + requires_recoloring = True + + self._canvas.coords("inner_rectangle_1", (border_width + inner_corner_radius, + border_width, + width - border_width - inner_corner_radius, + height - border_width)) + self._canvas.coords("inner_rectangle_2", (border_width, + border_width + inner_corner_radius, + width - border_width, + height - inner_corner_radius - border_width)) + + return requires_recoloring + + def draw_rounded_rect_with_border_vertical_split(self, width: Union[float, int], height: Union[float, int], corner_radius: Union[float, int], + border_width: Union[float, int], left_section_width: Union[float, int]) -> bool: + """ Draws a rounded rectangle with a corner_radius and border_width on the canvas which is split at left_section_width. + The border elements have the tags 'border_parts_left', 'border_parts_lright', + the main foreground elements have an 'inner_parts_left' and inner_parts_right' tag, + to color the elements accordingly. + + returns bool if recoloring is necessary """ + + left_section_width = round(left_section_width) + if self._round_width_to_even_numbers: + width = math.floor(width / 2) * 2 # round (floor) _current_width and _current_height and restrict them to even values only + if self._round_height_to_even_numbers: + height = math.floor(height / 2) * 2 + corner_radius = round(corner_radius) + + if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger + corner_radius = min(width / 2, height / 2) + + border_width = round(border_width) + corner_radius = self.__calc_optimal_corner_radius(corner_radius) # optimize corner_radius for different drawing methods (different rounding) + + if corner_radius >= border_width: + inner_corner_radius = corner_radius - border_width + else: + inner_corner_radius = 0 + + if left_section_width > width - corner_radius * 2: + left_section_width = width - corner_radius * 2 + elif left_section_width < corner_radius * 2: + left_section_width = corner_radius * 2 + + if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes": + return self.__draw_rounded_rect_with_border_vertical_split_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius, left_section_width) + elif self.preferred_drawing_method == "font_shapes": + return self.__draw_rounded_rect_with_border_vertical_split_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, left_section_width, ()) + + def __draw_rounded_rect_with_border_vertical_split_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int, + left_section_width: int) -> bool: + requires_recoloring = False + + # create border button parts (only if border exists) + if border_width > 0: + if not self._canvas.find_withtag("border_parts"): + self._canvas.create_polygon((0, 0, 0, 0), tags=("border_line_left_1", "border_parts_left", "border_parts", "left_parts")) + self._canvas.create_polygon((0, 0, 0, 0), tags=("border_line_right_1", "border_parts_right", "border_parts", "right_parts")) + self._canvas.create_rectangle((0, 0, 0, 0), tags=("border_rect_left_1", "border_parts_left", "border_parts", "left_parts"), width=0) + self._canvas.create_rectangle((0, 0, 0, 0), tags=("border_rect_right_1", "border_parts_right", "border_parts", "right_parts"), width=0) + requires_recoloring = True + + self._canvas.coords("border_line_left_1", + (corner_radius, + corner_radius, + left_section_width - corner_radius, + corner_radius, + left_section_width - corner_radius, + height - corner_radius, + corner_radius, + height - corner_radius)) + self._canvas.coords("border_line_right_1", + (left_section_width + corner_radius, + corner_radius, + width - corner_radius, + corner_radius, + width - corner_radius, + height - corner_radius, + left_section_width + corner_radius, + height - corner_radius)) + self._canvas.coords("border_rect_left_1", + (left_section_width - corner_radius, + 0, + left_section_width, + height)) + self._canvas.coords("border_rect_right_1", + (left_section_width, + 0, + left_section_width + corner_radius, + height)) + self._canvas.itemconfig("border_line_left_1", joinstyle=tkinter.ROUND, width=corner_radius * 2) + self._canvas.itemconfig("border_line_right_1", joinstyle=tkinter.ROUND, width=corner_radius * 2) + + else: + self._canvas.delete("border_parts") + + # create inner button parts + if not self._canvas.find_withtag("inner_parts"): + self._canvas.create_polygon((0, 0, 0, 0), tags=("inner_line_left_1", "inner_parts_left", "inner_parts", "left_parts"), joinstyle=tkinter.ROUND) + self._canvas.create_polygon((0, 0, 0, 0), tags=("inner_line_right_1", "inner_parts_right", "inner_parts", "right_parts"), joinstyle=tkinter.ROUND) + self._canvas.create_rectangle((0, 0, 0, 0), tags=("inner_rect_left_1", "inner_parts_left", "inner_parts", "left_parts"), width=0) + self._canvas.create_rectangle((0, 0, 0, 0), tags=("inner_rect_right_1", "inner_parts_right", "inner_parts", "right_parts"), width=0) + requires_recoloring = True + + self._canvas.coords("inner_line_left_1", + corner_radius, + corner_radius, + left_section_width - inner_corner_radius, + corner_radius, + left_section_width - inner_corner_radius, + height - corner_radius, + corner_radius, + height - corner_radius) + self._canvas.coords("inner_line_right_1", + left_section_width + inner_corner_radius, + corner_radius, + width - corner_radius, + corner_radius, + width - corner_radius, + height - corner_radius, + left_section_width + inner_corner_radius, + height - corner_radius) + self._canvas.coords("inner_rect_left_1", + (left_section_width - inner_corner_radius, + border_width, + left_section_width, + height - border_width)) + self._canvas.coords("inner_rect_right_1", + (left_section_width, + border_width, + left_section_width + inner_corner_radius, + height - border_width)) + self._canvas.itemconfig("inner_line_left_1", width=inner_corner_radius * 2) + self._canvas.itemconfig("inner_line_right_1", width=inner_corner_radius * 2) + + if requires_recoloring: # new parts were added -> manage z-order + self._canvas.tag_lower("inner_parts") + self._canvas.tag_lower("border_parts") + self._canvas.tag_lower("background_parts") + + return requires_recoloring + + def __draw_rounded_rect_with_border_vertical_split_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int, + left_section_width: int, exclude_parts: tuple) -> bool: + requires_recoloring = False + + # create border button parts + if border_width > 0: + if corner_radius > 0: + # create canvas border corner parts if not already created, but only if needed, and delete if not needed + if not self._canvas.find_withtag("border_oval_1_a") and "border_oval_1" not in exclude_parts: + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_1_a", "border_corner_part", "border_parts_left", "border_parts", "left_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_1_b", "border_corner_part", "border_parts_left", "border_parts", "left_parts"), anchor=tkinter.CENTER, + angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("border_oval_1_a") and "border_oval_1" in exclude_parts: + self._canvas.delete("border_oval_1_a", "border_oval_1_b") + + if not self._canvas.find_withtag("border_oval_2_a") and width > 2 * corner_radius and "border_oval_2" not in exclude_parts: + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_2_a", "border_corner_part", "border_parts_right", "border_parts", "right_parts"), + anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_2_b", "border_corner_part", "border_parts_right", "border_parts", "right_parts"), + anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("border_oval_2_a") and (not width > 2 * corner_radius or "border_oval_2" in exclude_parts): + self._canvas.delete("border_oval_2_a", "border_oval_2_b") + + if not self._canvas.find_withtag("border_oval_3_a") and height > 2 * corner_radius \ + and width > 2 * corner_radius and "border_oval_3" not in exclude_parts: + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_a", "border_corner_part", "border_parts_right", "border_parts", "right_parts"), + anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_b", "border_corner_part", "border_parts_right", "border_parts", "right_parts"), + anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("border_oval_3_a") and (not (height > 2 * corner_radius + and width > 2 * corner_radius) or "border_oval_3" in exclude_parts): + self._canvas.delete("border_oval_3_a", "border_oval_3_b") + + if not self._canvas.find_withtag("border_oval_4_a") and height > 2 * corner_radius and "border_oval_4" not in exclude_parts: + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_4_a", "border_corner_part", "border_parts_left", "border_parts", "left_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_4_b", "border_corner_part", "border_parts_left", "border_parts", "left_parts"), anchor=tkinter.CENTER, + angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("border_oval_4_a") and (not height > 2 * corner_radius or "border_oval_4" in exclude_parts): + self._canvas.delete("border_oval_4_a", "border_oval_4_b") + + # change position of border corner parts + self._canvas.coords("border_oval_1_a", corner_radius, corner_radius, corner_radius) + self._canvas.coords("border_oval_1_b", corner_radius, corner_radius, corner_radius) + self._canvas.coords("border_oval_2_a", width - corner_radius, corner_radius, corner_radius) + self._canvas.coords("border_oval_2_b", width - corner_radius, corner_radius, corner_radius) + self._canvas.coords("border_oval_3_a", width - corner_radius, height - corner_radius, corner_radius) + self._canvas.coords("border_oval_3_b", width - corner_radius, height - corner_radius, corner_radius) + self._canvas.coords("border_oval_4_a", corner_radius, height - corner_radius, corner_radius) + self._canvas.coords("border_oval_4_b", corner_radius, height - corner_radius, corner_radius) + + else: + self._canvas.delete("border_corner_part") # delete border corner parts if not needed + + # create canvas border rectangle parts if not already created + if not self._canvas.find_withtag("border_rectangle_1"): + self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_left_1", "border_rectangle_part", "border_parts_left", "border_parts", "left_parts"), width=0) + self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_left_2", "border_rectangle_part", "border_parts_left", "border_parts", "left_parts"), width=0) + self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_right_1", "border_rectangle_part", "border_parts_right", "border_parts", "right_parts"), width=0) + self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_right_2", "border_rectangle_part", "border_parts_right", "border_parts", "right_parts"), width=0) + requires_recoloring = True + + # change position of border rectangle parts + self._canvas.coords("border_rectangle_left_1", (0, corner_radius, left_section_width, height - corner_radius)) + self._canvas.coords("border_rectangle_left_2", (corner_radius, 0, left_section_width, height)) + self._canvas.coords("border_rectangle_right_1", (left_section_width, corner_radius, width, height - corner_radius)) + self._canvas.coords("border_rectangle_right_2", (left_section_width, 0, width - corner_radius, height)) + + else: + self._canvas.delete("border_parts") + + # create inner button parts + if inner_corner_radius > 0: + + # create canvas border corner parts if not already created, but only if they're needed and delete if not needed + if not self._canvas.find_withtag("inner_oval_1_a") and "inner_oval_1" not in exclude_parts: + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_1_a", "inner_corner_part", "inner_parts_left", "inner_parts", "left_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_1_b", "inner_corner_part", "inner_parts_left", "inner_parts", "left_parts"), anchor=tkinter.CENTER, + angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("inner_oval_1_a") and "inner_oval_1" in exclude_parts: + self._canvas.delete("inner_oval_1_a", "inner_oval_1_b") + + if not self._canvas.find_withtag("inner_oval_2_a") and width - (2 * border_width) > 2 * inner_corner_radius and "inner_oval_2" not in exclude_parts: + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_2_a", "inner_corner_part", "inner_parts_right", "inner_parts", "right_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_2_b", "inner_corner_part", "inner_parts_right", "inner_parts", "right_parts"), anchor=tkinter.CENTER, + angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("inner_oval_2_a") and (not width - (2 * border_width) > 2 * inner_corner_radius or "inner_oval_2" in exclude_parts): + self._canvas.delete("inner_oval_2_a", "inner_oval_2_b") + + if not self._canvas.find_withtag("inner_oval_3_a") and height - (2 * border_width) > 2 * inner_corner_radius \ + and width - (2 * border_width) > 2 * inner_corner_radius and "inner_oval_3" not in exclude_parts: + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_3_a", "inner_corner_part", "inner_parts_right", "inner_parts", "right_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_3_b", "inner_corner_part", "inner_parts_right", "inner_parts", "right_parts"), anchor=tkinter.CENTER, + angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("inner_oval_3_a") and (not (height - (2 * border_width) > 2 * inner_corner_radius + and width - (2 * border_width) > 2 * inner_corner_radius) or "inner_oval_3" in exclude_parts): + self._canvas.delete("inner_oval_3_a", "inner_oval_3_b") + + if not self._canvas.find_withtag("inner_oval_4_a") and height - (2 * border_width) > 2 * inner_corner_radius and "inner_oval_4" not in exclude_parts: + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_4_a", "inner_corner_part", "inner_parts_left", "inner_parts", "left_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_4_b", "inner_corner_part", "inner_parts_left", "inner_parts", "left_parts"), anchor=tkinter.CENTER, + angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("inner_oval_4_a") and (not height - (2 * border_width) > 2 * inner_corner_radius or "inner_oval_4" in exclude_parts): + self._canvas.delete("inner_oval_4_a", "inner_oval_4_b") + + # change position of border corner parts + self._canvas.coords("inner_oval_1_a", border_width + inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius) + self._canvas.coords("inner_oval_1_b", border_width + inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius) + self._canvas.coords("inner_oval_2_a", width - border_width - inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius) + self._canvas.coords("inner_oval_2_b", width - border_width - inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius) + self._canvas.coords("inner_oval_3_a", width - border_width - inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius) + self._canvas.coords("inner_oval_3_b", width - border_width - inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius) + self._canvas.coords("inner_oval_4_a", border_width + inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius) + self._canvas.coords("inner_oval_4_b", border_width + inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius) + else: + self._canvas.delete("inner_corner_part") # delete inner corner parts if not needed + + # create canvas inner rectangle parts if not already created + if not self._canvas.find_withtag("inner_rectangle_1"): + self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_left_1", "inner_rectangle_part", "inner_parts_left", "inner_parts", "left_parts"), width=0) + self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_right_1", "inner_rectangle_part", "inner_parts_right", "inner_parts", "right_parts"), width=0) + requires_recoloring = True + + if not self._canvas.find_withtag("inner_rectangle_2") and inner_corner_radius * 2 < height - (border_width * 2): + self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_left_2", "inner_rectangle_part", "inner_parts_left", "inner_parts", "left_parts"), width=0) + self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_right_2", "inner_rectangle_part", "inner_parts_right", "inner_parts", "right_parts"), width=0) + requires_recoloring = True + + elif self._canvas.find_withtag("inner_rectangle_2") and not inner_corner_radius * 2 < height - (border_width * 2): + self._canvas.delete("inner_rectangle_left_2") + self._canvas.delete("inner_rectangle_right_2") + + # change position of inner rectangle parts + self._canvas.coords("inner_rectangle_left_1", (border_width + inner_corner_radius, + border_width, + left_section_width, + height - border_width)) + self._canvas.coords("inner_rectangle_left_2", (border_width, + border_width + inner_corner_radius, + left_section_width, + height - inner_corner_radius - border_width)) + self._canvas.coords("inner_rectangle_right_1", (left_section_width, + border_width, + width - border_width - inner_corner_radius, + height - border_width)) + self._canvas.coords("inner_rectangle_right_2", (left_section_width, + border_width + inner_corner_radius, + width - border_width, + height - inner_corner_radius - border_width)) + + if requires_recoloring: # new parts were added -> manage z-order + self._canvas.tag_lower("inner_parts") + self._canvas.tag_lower("border_parts") + self._canvas.tag_lower("background_parts") + + return requires_recoloring + + def draw_rounded_progress_bar_with_border(self, width: Union[float, int], height: Union[float, int], corner_radius: Union[float, int], + border_width: Union[float, int], progress_value_1: float, progress_value_2: float, orientation: str) -> bool: + """ Draws a rounded bar on the canvas, and onntop sits a progress bar from value 1 to value 2 (range 0-1, left to right, bottom to top). + The border elements get the 'border_parts' tag", the main elements get the 'inner_parts' tag and + the progress elements get the 'progress_parts' tag. The 'orientation' argument defines from which direction the progress starts (n, w, s, e). + + returns bool if recoloring is necessary """ + + if self._round_width_to_even_numbers: + width = math.floor(width / 2) * 2 # round _current_width and _current_height and restrict them to even values only + if self._round_height_to_even_numbers: + height = math.floor(height / 2) * 2 + + if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger + corner_radius = min(width / 2, height / 2) + + border_width = round(border_width) + corner_radius = self.__calc_optimal_corner_radius(corner_radius) # optimize corner_radius for different drawing methods (different rounding) + + if corner_radius >= border_width: + inner_corner_radius = corner_radius - border_width + else: + inner_corner_radius = 0 + + if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes": + return self.__draw_rounded_progress_bar_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius, + progress_value_1, progress_value_2, orientation) + elif self.preferred_drawing_method == "font_shapes": + return self.__draw_rounded_progress_bar_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, + progress_value_1, progress_value_2, orientation) + + def __draw_rounded_progress_bar_with_border_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int, + progress_value_1: float, progress_value_2: float, orientation: str) -> bool: + + requires_recoloring = self.__draw_rounded_rect_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius) + + if corner_radius <= border_width: + bottom_right_shift = 0 # weird canvas rendering inaccuracy that has to be corrected in some cases + else: + bottom_right_shift = 0 + + # create progress parts + if not self._canvas.find_withtag("progress_parts"): + self._canvas.create_polygon((0, 0, 0, 0), tags=("progress_line_1", "progress_parts"), joinstyle=tkinter.ROUND) + self._canvas.tag_raise("progress_parts", "inner_parts") + requires_recoloring = True + + if orientation == "w": + self._canvas.coords("progress_line_1", + border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, + border_width + inner_corner_radius, + border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, + border_width + inner_corner_radius, + border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, + height - (border_width + inner_corner_radius) + bottom_right_shift, + border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, + height - (border_width + inner_corner_radius) + bottom_right_shift) + + elif orientation == "s": + self._canvas.coords("progress_line_1", + border_width + inner_corner_radius, + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), + width - (border_width + inner_corner_radius), + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), + width - (border_width + inner_corner_radius), + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1), + border_width + inner_corner_radius, + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1)) + + self._canvas.itemconfig("progress_line_1", width=inner_corner_radius * 2) + + return requires_recoloring + + def __draw_rounded_progress_bar_with_border_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int, + progress_value_1: float, progress_value_2: float, orientation: str) -> bool: + + requires_recoloring, requires_recoloring_2 = False, False + + if inner_corner_radius > 0: + # create canvas border corner parts if not already created + if not self._canvas.find_withtag("progress_oval_1_a"): + self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_1_a", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_1_b", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER, angle=180) + self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_2_a", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_2_b", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + + if not self._canvas.find_withtag("progress_oval_3_a") and round(inner_corner_radius) * 2 < height - 2 * border_width: + self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_3_a", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_3_b", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER, angle=180) + self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_4_a", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_4_b", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("progress_oval_3_a") and not round(inner_corner_radius) * 2 < height - 2 * border_width: + self._canvas.delete("progress_oval_3_a", "progress_oval_3_b", "progress_oval_4_a", "progress_oval_4_b") + + if not self._canvas.find_withtag("progress_rectangle_1"): + self._canvas.create_rectangle(0, 0, 0, 0, tags=("progress_rectangle_1", "progress_rectangle_part", "progress_parts"), width=0) + requires_recoloring = True + + if not self._canvas.find_withtag("progress_rectangle_2") and inner_corner_radius * 2 < height - (border_width * 2): + self._canvas.create_rectangle(0, 0, 0, 0, tags=("progress_rectangle_2", "progress_rectangle_part", "progress_parts"), width=0) + requires_recoloring = True + elif self._canvas.find_withtag("progress_rectangle_2") and not inner_corner_radius * 2 < height - (border_width * 2): + self._canvas.delete("progress_rectangle_2") + + # horizontal orientation from the bottom + if orientation == "w": + requires_recoloring_2 = self.__draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, + ()) + + # set positions of progress corner parts + self._canvas.coords("progress_oval_1_a", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, + border_width + inner_corner_radius, inner_corner_radius) + self._canvas.coords("progress_oval_1_b", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, + border_width + inner_corner_radius, inner_corner_radius) + self._canvas.coords("progress_oval_2_a", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, + border_width + inner_corner_radius, inner_corner_radius) + self._canvas.coords("progress_oval_2_b", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, + border_width + inner_corner_radius, inner_corner_radius) + self._canvas.coords("progress_oval_3_a", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, + height - border_width - inner_corner_radius, inner_corner_radius) + self._canvas.coords("progress_oval_3_b", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, + height - border_width - inner_corner_radius, inner_corner_radius) + self._canvas.coords("progress_oval_4_a", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, + height - border_width - inner_corner_radius, inner_corner_radius) + self._canvas.coords("progress_oval_4_b", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, + height - border_width - inner_corner_radius, inner_corner_radius) + + # set positions of progress rect parts + self._canvas.coords("progress_rectangle_1", + border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, + border_width, + border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, + height - border_width) + self._canvas.coords("progress_rectangle_2", + border_width + 2 * inner_corner_radius + (width - 2 * inner_corner_radius - 2 * border_width) * progress_value_1, + border_width + inner_corner_radius, + border_width + 2 * inner_corner_radius + (width - 2 * inner_corner_radius - 2 * border_width) * progress_value_2, + height - inner_corner_radius - border_width) + + # vertical orientation from the bottom + if orientation == "s": + requires_recoloring_2 = self.__draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, + ()) + + # set positions of progress corner parts + self._canvas.coords("progress_oval_1_a", border_width + inner_corner_radius, + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), inner_corner_radius) + self._canvas.coords("progress_oval_1_b", border_width + inner_corner_radius, + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), inner_corner_radius) + self._canvas.coords("progress_oval_2_a", width - border_width - inner_corner_radius, + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), inner_corner_radius) + self._canvas.coords("progress_oval_2_b", width - border_width - inner_corner_radius, + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), inner_corner_radius) + self._canvas.coords("progress_oval_3_a", width - border_width - inner_corner_radius, + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1), inner_corner_radius) + self._canvas.coords("progress_oval_3_b", width - border_width - inner_corner_radius, + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1), inner_corner_radius) + self._canvas.coords("progress_oval_4_a", border_width + inner_corner_radius, + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1), inner_corner_radius) + self._canvas.coords("progress_oval_4_b", border_width + inner_corner_radius, + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1), inner_corner_radius) + + # set positions of progress rect parts + self._canvas.coords("progress_rectangle_1", + border_width + inner_corner_radius, + border_width + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), + width - border_width - inner_corner_radius, + border_width + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1)) + self._canvas.coords("progress_rectangle_2", + border_width, + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), + width - border_width, + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1)) + + return requires_recoloring or requires_recoloring_2 + + def draw_rounded_slider_with_border_and_button(self, width: Union[float, int], height: Union[float, int], corner_radius: Union[float, int], + border_width: Union[float, int], button_length: Union[float, int], button_corner_radius: Union[float, int], + slider_value: float, orientation: str) -> bool: + + if self._round_width_to_even_numbers: + width = math.floor(width / 2) * 2 # round _current_width and _current_height and restrict them to even values only + if self._round_height_to_even_numbers: + height = math.floor(height / 2) * 2 + + if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger + corner_radius = min(width / 2, height / 2) + + if button_corner_radius > width / 2 or button_corner_radius > height / 2: # restrict button_corner_radius if it's too larger + button_corner_radius = min(width / 2, height / 2) + + button_length = round(button_length) + border_width = round(border_width) + button_corner_radius = round(button_corner_radius) + corner_radius = self.__calc_optimal_corner_radius(corner_radius) # optimize corner_radius for different drawing methods (different rounding) + + if corner_radius >= border_width: + inner_corner_radius = corner_radius - border_width + else: + inner_corner_radius = 0 + + if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes": + return self.__draw_rounded_slider_with_border_and_button_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius, + button_length, button_corner_radius, slider_value, orientation) + elif self.preferred_drawing_method == "font_shapes": + return self.__draw_rounded_slider_with_border_and_button_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, + button_length, button_corner_radius, slider_value, orientation) + + def __draw_rounded_slider_with_border_and_button_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int, + button_length: int, button_corner_radius: int, slider_value: float, orientation: str) -> bool: + + # draw normal progressbar + requires_recoloring = self.__draw_rounded_progress_bar_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius, + 0, slider_value, orientation) + + # create slider button part + if not self._canvas.find_withtag("slider_parts"): + self._canvas.create_polygon((0, 0, 0, 0), tags=("slider_line_1", "slider_parts"), joinstyle=tkinter.ROUND) + self._canvas.tag_raise("slider_parts") # manage z-order + requires_recoloring = True + + if corner_radius <= border_width: + bottom_right_shift = -1 # weird canvas rendering inaccuracy that has to be corrected in some cases + else: + bottom_right_shift = 0 + + if orientation == "w": + slider_x_position = corner_radius + (button_length / 2) + (width - 2 * corner_radius - button_length) * slider_value + self._canvas.coords("slider_line_1", + slider_x_position - (button_length / 2), button_corner_radius, + slider_x_position + (button_length / 2), button_corner_radius, + slider_x_position + (button_length / 2), height - button_corner_radius, + slider_x_position - (button_length / 2), height - button_corner_radius) + self._canvas.itemconfig("slider_line_1", + width=button_corner_radius * 2) + elif orientation == "s": + slider_y_position = corner_radius + (button_length / 2) + (height - 2 * corner_radius - button_length) * (1 - slider_value) + self._canvas.coords("slider_line_1", + button_corner_radius, slider_y_position - (button_length / 2), + button_corner_radius, slider_y_position + (button_length / 2), + width - button_corner_radius, slider_y_position + (button_length / 2), + width - button_corner_radius, slider_y_position - (button_length / 2)) + self._canvas.itemconfig("slider_line_1", + width=button_corner_radius * 2) + + return requires_recoloring + + def __draw_rounded_slider_with_border_and_button_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int, + button_length: int, button_corner_radius: int, slider_value: float, orientation: str) -> bool: + + # draw normal progressbar + requires_recoloring = self.__draw_rounded_progress_bar_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, + 0, slider_value, orientation) + + # create 4 circles (if not needed, then less) + if not self._canvas.find_withtag("slider_oval_1_a"): + self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_1_a", "slider_corner_part", "slider_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_1_b", "slider_corner_part", "slider_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + + if not self._canvas.find_withtag("slider_oval_2_a") and button_length > 0: + self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_2_a", "slider_corner_part", "slider_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_2_b", "slider_corner_part", "slider_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("slider_oval_2_a") and not button_length > 0: + self._canvas.delete("slider_oval_2_a", "slider_oval_2_b") + + if not self._canvas.find_withtag("slider_oval_4_a") and height > 2 * button_corner_radius: + self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_4_a", "slider_corner_part", "slider_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_4_b", "slider_corner_part", "slider_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("slider_oval_4_a") and not height > 2 * button_corner_radius: + self._canvas.delete("slider_oval_4_a", "slider_oval_4_b") + + if not self._canvas.find_withtag("slider_oval_3_a") and button_length > 0 and height > 2 * button_corner_radius: + self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_3_a", "slider_corner_part", "slider_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_3_b", "slider_corner_part", "slider_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("border_oval_3_a") and not (button_length > 0 and height > 2 * button_corner_radius): + self._canvas.delete("slider_oval_3_a", "slider_oval_3_b") + + # create the 2 rectangles (if needed) + if not self._canvas.find_withtag("slider_rectangle_1") and button_length > 0: + self._canvas.create_rectangle(0, 0, 0, 0, tags=("slider_rectangle_1", "slider_rectangle_part", "slider_parts"), width=0) + requires_recoloring = True + elif self._canvas.find_withtag("slider_rectangle_1") and not button_length > 0: + self._canvas.delete("slider_rectangle_1") + + if not self._canvas.find_withtag("slider_rectangle_2") and height > 2 * button_corner_radius: + self._canvas.create_rectangle(0, 0, 0, 0, tags=("slider_rectangle_2", "slider_rectangle_part", "slider_parts"), width=0) + requires_recoloring = True + elif self._canvas.find_withtag("slider_rectangle_2") and not height > 2 * button_corner_radius: + self._canvas.delete("slider_rectangle_2") + + # set positions of circles and rectangles + if orientation == "w": + slider_x_position = corner_radius + (button_length / 2) + (width - 2 * corner_radius - button_length) * slider_value + self._canvas.coords("slider_oval_1_a", slider_x_position - (button_length / 2), button_corner_radius, button_corner_radius) + self._canvas.coords("slider_oval_1_b", slider_x_position - (button_length / 2), button_corner_radius, button_corner_radius) + self._canvas.coords("slider_oval_2_a", slider_x_position + (button_length / 2), button_corner_radius, button_corner_radius) + self._canvas.coords("slider_oval_2_b", slider_x_position + (button_length / 2), button_corner_radius, button_corner_radius) + self._canvas.coords("slider_oval_3_a", slider_x_position + (button_length / 2), height - button_corner_radius, button_corner_radius) + self._canvas.coords("slider_oval_3_b", slider_x_position + (button_length / 2), height - button_corner_radius, button_corner_radius) + self._canvas.coords("slider_oval_4_a", slider_x_position - (button_length / 2), height - button_corner_radius, button_corner_radius) + self._canvas.coords("slider_oval_4_b", slider_x_position - (button_length / 2), height - button_corner_radius, button_corner_radius) + + self._canvas.coords("slider_rectangle_1", + slider_x_position - (button_length / 2), 0, + slider_x_position + (button_length / 2), height) + self._canvas.coords("slider_rectangle_2", + slider_x_position - (button_length / 2) - button_corner_radius, button_corner_radius, + slider_x_position + (button_length / 2) + button_corner_radius, height - button_corner_radius) + + elif orientation == "s": + slider_y_position = corner_radius + (button_length / 2) + (height - 2 * corner_radius - button_length) * (1 - slider_value) + self._canvas.coords("slider_oval_1_a", button_corner_radius, slider_y_position - (button_length / 2), button_corner_radius) + self._canvas.coords("slider_oval_1_b", button_corner_radius, slider_y_position - (button_length / 2), button_corner_radius) + self._canvas.coords("slider_oval_2_a", button_corner_radius, slider_y_position + (button_length / 2), button_corner_radius) + self._canvas.coords("slider_oval_2_b", button_corner_radius, slider_y_position + (button_length / 2), button_corner_radius) + self._canvas.coords("slider_oval_3_a", width - button_corner_radius, slider_y_position + (button_length / 2), button_corner_radius) + self._canvas.coords("slider_oval_3_b", width - button_corner_radius, slider_y_position + (button_length / 2), button_corner_radius) + self._canvas.coords("slider_oval_4_a", width - button_corner_radius, slider_y_position - (button_length / 2), button_corner_radius) + self._canvas.coords("slider_oval_4_b", width - button_corner_radius, slider_y_position - (button_length / 2), button_corner_radius) + + self._canvas.coords("slider_rectangle_1", + 0, slider_y_position - (button_length / 2), + width, slider_y_position + (button_length / 2)) + self._canvas.coords("slider_rectangle_2", + button_corner_radius, slider_y_position - (button_length / 2) - button_corner_radius, + width - button_corner_radius, slider_y_position + (button_length / 2) + button_corner_radius) + + if requires_recoloring: # new parts were added -> manage z-order + self._canvas.tag_raise("slider_parts") + + return requires_recoloring + + def draw_rounded_scrollbar(self, width: Union[float, int], height: Union[float, int], corner_radius: Union[float, int], + border_spacing: Union[float, int], start_value: float, end_value: float, orientation: str) -> bool: + + if self._round_width_to_even_numbers: + width = math.floor(width / 2) * 2 # round _current_width and _current_height and restrict them to even values only + if self._round_height_to_even_numbers: + height = math.floor(height / 2) * 2 + + if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger + corner_radius = min(width / 2, height / 2) + + border_spacing = round(border_spacing) + corner_radius = self.__calc_optimal_corner_radius(corner_radius) # optimize corner_radius for different drawing methods (different rounding) + + if corner_radius >= border_spacing: + inner_corner_radius = corner_radius - border_spacing + else: + inner_corner_radius = 0 + + if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes": + return self.__draw_rounded_scrollbar_polygon_shapes(width, height, corner_radius, inner_corner_radius, + start_value, end_value, orientation) + elif self.preferred_drawing_method == "font_shapes": + return self.__draw_rounded_scrollbar_font_shapes(width, height, corner_radius, inner_corner_radius, + start_value, end_value, orientation) + + def __draw_rounded_scrollbar_polygon_shapes(self, width: int, height: int, corner_radius: int, inner_corner_radius: int, + start_value: float, end_value: float, orientation: str) -> bool: + requires_recoloring = False + + if not self._canvas.find_withtag("border_parts"): + self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_1", "border_parts"), width=0) + requires_recoloring = True + self._canvas.coords("border_rectangle_1", 0, 0, width, height) + + if not self._canvas.find_withtag("scrollbar_parts"): + self._canvas.create_polygon((0, 0, 0, 0), tags=("scrollbar_polygon_1", "scrollbar_parts"), joinstyle=tkinter.ROUND) + self._canvas.tag_raise("scrollbar_parts", "border_parts") + requires_recoloring = True + + if orientation == "vertical": + self._canvas.coords("scrollbar_polygon_1", + corner_radius, corner_radius + (height - 2 * corner_radius) * start_value, + width - corner_radius, corner_radius + (height - 2 * corner_radius) * start_value, + width - corner_radius, corner_radius + (height - 2 * corner_radius) * end_value, + corner_radius, corner_radius + (height - 2 * corner_radius) * end_value) + elif orientation == "horizontal": + self._canvas.coords("scrollbar_polygon_1", + corner_radius + (width - 2 * corner_radius) * start_value, corner_radius, + corner_radius + (width - 2 * corner_radius) * end_value, corner_radius, + corner_radius + (width - 2 * corner_radius) * end_value, height - corner_radius, + corner_radius + (width - 2 * corner_radius) * start_value, height - corner_radius,) + + self._canvas.itemconfig("scrollbar_polygon_1", width=inner_corner_radius * 2) + + return requires_recoloring + + def __draw_rounded_scrollbar_font_shapes(self, width: int, height: int, corner_radius: int, inner_corner_radius: int, + start_value: float, end_value: float, orientation: str) -> bool: + requires_recoloring = False + + if not self._canvas.find_withtag("border_parts"): + self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_1", "border_parts"), width=0) + requires_recoloring = True + self._canvas.coords("border_rectangle_1", 0, 0, width, height) + + if inner_corner_radius > 0: + if not self._canvas.find_withtag("scrollbar_oval_1_a"): + self._canvas.create_aa_circle(0, 0, 0, tags=("scrollbar_oval_1_a", "scrollbar_corner_part", "scrollbar_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("scrollbar_oval_1_b", "scrollbar_corner_part", "scrollbar_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + + if not self._canvas.find_withtag("scrollbar_oval_2_a") and width > 2 * corner_radius: + self._canvas.create_aa_circle(0, 0, 0, tags=("scrollbar_oval_2_a", "scrollbar_corner_part", "scrollbar_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("scrollbar_oval_2_b", "scrollbar_corner_part", "scrollbar_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("scrollbar_oval_2_a") and not width > 2 * corner_radius: + self._canvas.delete("scrollbar_oval_2_a", "scrollbar_oval_2_b") + + if not self._canvas.find_withtag("scrollbar_oval_3_a") and height > 2 * corner_radius and width > 2 * corner_radius: + self._canvas.create_aa_circle(0, 0, 0, tags=("scrollbar_oval_3_a", "scrollbar_corner_part", "scrollbar_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("scrollbar_oval_3_b", "scrollbar_corner_part", "scrollbar_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("scrollbar_oval_3_a") and not (height > 2 * corner_radius and width > 2 * corner_radius): + self._canvas.delete("scrollbar_oval_3_a", "scrollbar_oval_3_b") + + if not self._canvas.find_withtag("scrollbar_oval_4_a") and height > 2 * corner_radius: + self._canvas.create_aa_circle(0, 0, 0, tags=("scrollbar_oval_4_a", "scrollbar_corner_part", "scrollbar_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("scrollbar_oval_4_b", "scrollbar_corner_part", "scrollbar_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("scrollbar_oval_4_a") and not height > 2 * corner_radius: + self._canvas.delete("scrollbar_oval_4_a", "scrollbar_oval_4_b") + else: + self._canvas.delete("scrollbar_corner_part") + + if not self._canvas.find_withtag("scrollbar_rectangle_1") and height > 2 * corner_radius: + self._canvas.create_rectangle(0, 0, 0, 0, tags=("scrollbar_rectangle_1", "scrollbar_rectangle_part", "scrollbar_parts"), width=0) + requires_recoloring = True + elif self._canvas.find_withtag("scrollbar_rectangle_1") and not height > 2 * corner_radius: + self._canvas.delete("scrollbar_rectangle_1") + + if not self._canvas.find_withtag("scrollbar_rectangle_2") and width > 2 * corner_radius: + self._canvas.create_rectangle(0, 0, 0, 0, tags=("scrollbar_rectangle_2", "scrollbar_rectangle_part", "scrollbar_parts"), width=0) + requires_recoloring = True + elif self._canvas.find_withtag("scrollbar_rectangle_2") and not width > 2 * corner_radius: + self._canvas.delete("scrollbar_rectangle_2") + + if orientation == "vertical": + self._canvas.coords("scrollbar_rectangle_1", + corner_radius - inner_corner_radius, corner_radius + (height - 2 * corner_radius) * start_value, + width - (corner_radius - inner_corner_radius), corner_radius + (height - 2 * corner_radius) * end_value) + self._canvas.coords("scrollbar_rectangle_2", + corner_radius, corner_radius - inner_corner_radius + (height - 2 * corner_radius) * start_value, + width - (corner_radius), corner_radius + inner_corner_radius + (height - 2 * corner_radius) * end_value) + + self._canvas.coords("scrollbar_oval_1_a", corner_radius, corner_radius + (height - 2 * corner_radius) * start_value, inner_corner_radius) + self._canvas.coords("scrollbar_oval_1_b", corner_radius, corner_radius + (height - 2 * corner_radius) * start_value, inner_corner_radius) + self._canvas.coords("scrollbar_oval_2_a", width - corner_radius, corner_radius + (height - 2 * corner_radius) * start_value, inner_corner_radius) + self._canvas.coords("scrollbar_oval_2_b", width - corner_radius, corner_radius + (height - 2 * corner_radius) * start_value, inner_corner_radius) + self._canvas.coords("scrollbar_oval_3_a", width - corner_radius, corner_radius + (height - 2 * corner_radius) * end_value, inner_corner_radius) + self._canvas.coords("scrollbar_oval_3_b", width - corner_radius, corner_radius + (height - 2 * corner_radius) * end_value, inner_corner_radius) + self._canvas.coords("scrollbar_oval_4_a", corner_radius, corner_radius + (height - 2 * corner_radius) * end_value, inner_corner_radius) + self._canvas.coords("scrollbar_oval_4_b", corner_radius, corner_radius + (height - 2 * corner_radius) * end_value, inner_corner_radius) + + if orientation == "horizontal": + self._canvas.coords("scrollbar_rectangle_1", + corner_radius - inner_corner_radius + (width - 2 * corner_radius) * start_value, corner_radius, + corner_radius + inner_corner_radius + (width - 2 * corner_radius) * end_value, height - corner_radius) + self._canvas.coords("scrollbar_rectangle_2", + corner_radius + (width - 2 * corner_radius) * start_value, corner_radius - inner_corner_radius, + corner_radius + (width - 2 * corner_radius) * end_value, height - (corner_radius - inner_corner_radius)) + + self._canvas.coords("scrollbar_oval_1_a", corner_radius + (width - 2 * corner_radius) * start_value, corner_radius, inner_corner_radius) + self._canvas.coords("scrollbar_oval_1_b", corner_radius + (width - 2 * corner_radius) * start_value, corner_radius, inner_corner_radius) + self._canvas.coords("scrollbar_oval_2_a", corner_radius + (width - 2 * corner_radius) * end_value, corner_radius, inner_corner_radius) + self._canvas.coords("scrollbar_oval_2_b", corner_radius + (width - 2 * corner_radius) * end_value, corner_radius, inner_corner_radius) + self._canvas.coords("scrollbar_oval_3_a", corner_radius + (width - 2 * corner_radius) * end_value, height - corner_radius, inner_corner_radius) + self._canvas.coords("scrollbar_oval_3_b", corner_radius + (width - 2 * corner_radius) * end_value, height - corner_radius, inner_corner_radius) + self._canvas.coords("scrollbar_oval_4_a", corner_radius + (width - 2 * corner_radius) * start_value, height - corner_radius, inner_corner_radius) + self._canvas.coords("scrollbar_oval_4_b", corner_radius + (width - 2 * corner_radius) * start_value, height - corner_radius, inner_corner_radius) + + return requires_recoloring + + def draw_checkmark(self, width: Union[float, int], height: Union[float, int], size: Union[int, float]) -> bool: + """ Draws a rounded rectangle with a corner_radius and border_width on the canvas. The border elements have a 'border_parts' tag, + the main foreground elements have an 'inner_parts' tag to color the elements accordingly. + + returns bool if recoloring is necessary """ + + size = round(size) + requires_recoloring = False + + if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes": + x, y, radius = width / 2, height / 2, size / 2.8 + if not self._canvas.find_withtag("checkmark"): + self._canvas.create_line(0, 0, 0, 0, tags=("checkmark", "create_line"), width=round(height / 8), joinstyle=tkinter.MITER, capstyle=tkinter.ROUND) + self._canvas.tag_raise("checkmark") + requires_recoloring = True + + self._canvas.coords("checkmark", + x + radius, y - radius, + x - radius / 4, y + radius * 0.8, + x - radius, y + radius / 6) + elif self.preferred_drawing_method == "font_shapes": + if not self._canvas.find_withtag("checkmark"): + self._canvas.create_text(0, 0, text="Z", font=("CustomTkinter_shapes_font", -size), tags=("checkmark", "create_text"), anchor=tkinter.CENTER) + self._canvas.tag_raise("checkmark") + requires_recoloring = True + + self._canvas.coords("checkmark", round(width / 2), round(height / 2)) + + return requires_recoloring + + def draw_dropdown_arrow(self, x_position: Union[int, float], y_position: Union[int, float], size: Union[int, float]) -> bool: + """ Draws a dropdown bottom facing arrow at (x_position, y_position) in a given size + + returns bool if recoloring is necessary """ + + x_position, y_position, size = round(x_position), round(y_position), round(size) + requires_recoloring = False + + if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes": + if not self._canvas.find_withtag("dropdown_arrow"): + self._canvas.create_line(0, 0, 0, 0, tags="dropdown_arrow", width=round(size / 3), joinstyle=tkinter.ROUND, capstyle=tkinter.ROUND) + self._canvas.tag_raise("dropdown_arrow") + requires_recoloring = True + + self._canvas.coords("dropdown_arrow", + x_position - (size / 2), + y_position - (size / 5), + x_position, + y_position + (size / 5), + x_position + (size / 2), + y_position - (size / 5)) + + elif self.preferred_drawing_method == "font_shapes": + if not self._canvas.find_withtag("dropdown_arrow"): + self._canvas.create_text(0, 0, text="Y", font=("CustomTkinter_shapes_font", -size), tags="dropdown_arrow", anchor=tkinter.CENTER) + self._canvas.tag_raise("dropdown_arrow") + requires_recoloring = True + + self._canvas.itemconfigure("dropdown_arrow", font=("CustomTkinter_shapes_font", -size)) + self._canvas.coords("dropdown_arrow", x_position, y_position) + + return requires_recoloring diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/core_widget_classes/__init__.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_widget_classes/__init__.py new file mode 100644 index 0000000..75e2d84 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_widget_classes/__init__.py @@ -0,0 +1,2 @@ +from .dropdown_menu import DropdownMenu +from .ctk_base_class import CTkBaseClass diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/core_widget_classes/__pycache__/__init__.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_widget_classes/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..7845b7a Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_widget_classes/__pycache__/__init__.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/core_widget_classes/__pycache__/ctk_base_class.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_widget_classes/__pycache__/ctk_base_class.cpython-311.pyc new file mode 100644 index 0000000..1649c0f Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_widget_classes/__pycache__/ctk_base_class.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/core_widget_classes/__pycache__/dropdown_menu.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_widget_classes/__pycache__/dropdown_menu.cpython-311.pyc new file mode 100644 index 0000000..9a80361 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_widget_classes/__pycache__/dropdown_menu.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py new file mode 100644 index 0000000..afd9431 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py @@ -0,0 +1,326 @@ +import sys +import warnings +import tkinter +import tkinter.ttk as ttk +from typing import Union, Callable, Tuple, Any + +try: + from typing import TypedDict +except ImportError: + from typing_extensions import TypedDict + +from .... import windows # import windows for isinstance checks + +from ..theme import ThemeManager +from ..font import CTkFont +from ..image import CTkImage +from ..appearance_mode import CTkAppearanceModeBaseClass +from ..scaling import CTkScalingBaseClass + +from ..utility import pop_from_dict_by_set, check_kwargs_empty + + +class CTkBaseClass(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClass): + """ Base class of every CTk widget, handles the dimensions, bg_color, + appearance_mode changes, scaling, bg changes of master if master is not a CTk widget """ + + # attributes that are passed to and managed by the tkinter frame only: + _valid_tk_frame_attributes: set = {"cursor"} + + _cursor_manipulation_enabled: bool = True + + def __init__(self, + master: Any, + width: int = 0, + height: int = 0, + + bg_color: Union[str, Tuple[str, str]] = "transparent", + **kwargs): + + # call init methods of super classes + tkinter.Frame.__init__(self, master=master, width=width, height=height, **pop_from_dict_by_set(kwargs, self._valid_tk_frame_attributes)) + CTkAppearanceModeBaseClass.__init__(self) + CTkScalingBaseClass.__init__(self, scaling_type="widget") + + # check if kwargs is empty, if not raise error for unsupported arguments + check_kwargs_empty(kwargs, raise_error=True) + + # dimensions independent of scaling + self._current_width = width # _current_width and _current_height in pixel, represent current size of the widget + self._current_height = height # _current_width and _current_height are independent of the scale + self._desired_width = width # _desired_width and _desired_height, represent desired size set by width and height + self._desired_height = height + + # set width and height of tkinter.Frame + super().configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + + # save latest geometry function and kwargs + class GeometryCallDict(TypedDict): + function: Callable + kwargs: dict + self._last_geometry_manager_call: Union[GeometryCallDict, None] = None + + # background color + self._bg_color: Union[str, Tuple[str, str]] = self._detect_color_of_master() if bg_color == "transparent" else self._check_color_type(bg_color, transparency=True) + + # set bg color of tkinter.Frame + super().configure(bg=self._apply_appearance_mode(self._bg_color)) + + # add configure callback to tkinter.Frame + super().bind('', self._update_dimensions_event) + + # overwrite configure methods of master when master is tkinter widget, so that bg changes get applied on child CTk widget as well + if isinstance(self.master, (tkinter.Tk, tkinter.Toplevel, tkinter.Frame, tkinter.LabelFrame, ttk.Frame, ttk.LabelFrame, ttk.Notebook)) and not isinstance(self.master, (CTkBaseClass, CTkAppearanceModeBaseClass)): + master_old_configure = self.master.config + + def new_configure(*args, **kwargs): + if "bg" in kwargs: + self.configure(bg_color=kwargs["bg"]) + elif "background" in kwargs: + self.configure(bg_color=kwargs["background"]) + + # args[0] is dict when attribute gets changed by widget[] syntax + elif len(args) > 0 and type(args[0]) == dict: + if "bg" in args[0]: + self.configure(bg_color=args[0]["bg"]) + elif "background" in args[0]: + self.configure(bg_color=args[0]["background"]) + master_old_configure(*args, **kwargs) + + self.master.config = new_configure + self.master.configure = new_configure + + def destroy(self): + """ Destroy this and all descendants widgets. """ + + # call destroy methods of super classes + tkinter.Frame.destroy(self) + CTkAppearanceModeBaseClass.destroy(self) + CTkScalingBaseClass.destroy(self) + + def _draw(self, no_color_updates: bool = False): + """ can be overridden but super method must be called """ + if no_color_updates is False: + # Configuring color of tkinter.Frame not necessary at the moment? + # Causes flickering on Windows and Linux for segmented button for some reason! + # super().configure(bg=self._apply_appearance_mode(self._bg_color)) + pass + + def config(self, *args, **kwargs): + raise AttributeError("'config' is not implemented for CTk widgets. For consistency, always use 'configure' instead.") + + def configure(self, require_redraw=False, **kwargs): + """ basic configure with bg_color, width, height support, calls configure of tkinter.Frame, updates in the end """ + + if "width" in kwargs: + self._set_dimensions(width=kwargs.pop("width")) + + if "height" in kwargs: + self._set_dimensions(height=kwargs.pop("height")) + + if "bg_color" in kwargs: + new_bg_color = self._check_color_type(kwargs.pop("bg_color"), transparency=True) + if new_bg_color == "transparent": + self._bg_color = self._detect_color_of_master() + else: + self._bg_color = self._check_color_type(new_bg_color) + require_redraw = True + + super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_frame_attributes)) # configure tkinter.Frame + + # if there are still items in the kwargs dict, raise ValueError + check_kwargs_empty(kwargs, raise_error=True) + + if require_redraw: + self._draw() + + def cget(self, attribute_name: str): + """ basic cget with bg_color, width, height support, calls cget of tkinter.Frame """ + + if attribute_name == "bg_color": + return self._bg_color + elif attribute_name == "width": + return self._desired_width + elif attribute_name == "height": + return self._desired_height + + elif attribute_name in self._valid_tk_frame_attributes: + return super().cget(attribute_name) # cget of tkinter.Frame + else: + raise ValueError(f"'{attribute_name}' is not a supported argument. Look at the documentation for supported arguments.") + + def _check_font_type(self, font: any): + """ check font type when passed to widget """ + if isinstance(font, CTkFont): + return font + + elif type(font) == tuple and len(font) == 1: + warnings.warn(f"{type(self).__name__} Warning: font {font} given without size, will be extended with default text size of current theme\n") + return font[0], ThemeManager.theme["text"]["size"] + + elif type(font) == tuple and 2 <= len(font) <= 6: + return font + + else: + raise ValueError(f"Wrong font type {type(font)}\n" + + f"For consistency, Customtkinter requires the font argument to be a tuple of len 2 to 6 or an instance of CTkFont.\n" + + f"\nUsage example:\n" + + f"font=customtkinter.CTkFont(family='', size=)\n" + + f"font=('', )\n") + + def _check_image_type(self, image: any): + """ check image type when passed to widget """ + if image is None: + return image + elif isinstance(image, CTkImage): + return image + else: + warnings.warn(f"{type(self).__name__} Warning: Given image is not CTkImage but {type(image)}. Image can not be scaled on HighDPI displays, use CTkImage instead.\n") + return image + + def _update_dimensions_event(self, event): + # only redraw if dimensions changed (for performance), independent of scaling + if round(self._current_width) != round(self._reverse_widget_scaling(event.width)) or round(self._current_height) != round(self._reverse_widget_scaling(event.height)): + self._current_width = self._reverse_widget_scaling(event.width) # adjust current size according to new size given by event + self._current_height = self._reverse_widget_scaling(event.height) # _current_width and _current_height are independent of the scale + + self._draw(no_color_updates=True) # faster drawing without color changes + + def _detect_color_of_master(self, master_widget=None) -> Union[str, Tuple[str, str]]: + """ detect foreground color of master widget for bg_color and transparent color """ + + if master_widget is None: + master_widget = self.master + + if isinstance(master_widget, (windows.widgets.core_widget_classes.CTkBaseClass, windows.CTk, windows.CTkToplevel, windows.widgets.ctk_scrollable_frame.CTkScrollableFrame)): + if master_widget.cget("fg_color") is not None and master_widget.cget("fg_color") != "transparent": + return master_widget.cget("fg_color") + + elif isinstance(master_widget, windows.widgets.ctk_scrollable_frame.CTkScrollableFrame): + return self._detect_color_of_master(master_widget.master.master.master) + + # if fg_color of master is None, try to retrieve fg_color from master of master + elif hasattr(master_widget, "master"): + return self._detect_color_of_master(master_widget.master) + + elif isinstance(master_widget, (ttk.Frame, ttk.LabelFrame, ttk.Notebook, ttk.Label)): # master is ttk widget + try: + ttk_style = ttk.Style() + return ttk_style.lookup(master_widget.winfo_class(), 'background') + except Exception: + return "#FFFFFF", "#000000" + + else: # master is normal tkinter widget + try: + return master_widget.cget("bg") # try to get bg color by .cget() method + except Exception: + return "#FFFFFF", "#000000" + + def _set_appearance_mode(self, mode_string): + super()._set_appearance_mode(mode_string) + self._draw() + super().update_idletasks() + + def _set_scaling(self, new_widget_scaling, new_window_scaling): + super()._set_scaling(new_widget_scaling, new_window_scaling) + + super().configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + + if self._last_geometry_manager_call is not None: + self._last_geometry_manager_call["function"](**self._apply_argument_scaling(self._last_geometry_manager_call["kwargs"])) + + def _set_dimensions(self, width=None, height=None): + if width is not None: + self._desired_width = width + if height is not None: + self._desired_height = height + + super().configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + + def bind(self, sequence=None, command=None, add=None): + raise NotImplementedError + + def unbind(self, sequence=None, funcid=None): + raise NotImplementedError + + def unbind_all(self, sequence): + raise AttributeError("'unbind_all' is not allowed, because it would delete necessary internal callbacks for all widgets") + + def bind_all(self, sequence=None, func=None, add=None): + raise AttributeError("'bind_all' is not allowed, could result in undefined behavior") + + def place(self, **kwargs): + """ + Place a widget in the parent widget. Use as options: + in=master - master relative to which the widget is placed + in_=master - see 'in' option description + x=amount - locate anchor of this widget at position x of master + y=amount - locate anchor of this widget at position y of master + relx=amount - locate anchor of this widget between 0.0 and 1.0 relative to width of master (1.0 is right edge) + rely=amount - locate anchor of this widget between 0.0 and 1.0 relative to height of master (1.0 is bottom edge) + anchor=NSEW (or subset) - position anchor according to given direction + width=amount - width of this widget in pixel + height=amount - height of this widget in pixel + relwidth=amount - width of this widget between 0.0 and 1.0 relative to width of master (1.0 is the same width as the master) + relheight=amount - height of this widget between 0.0 and 1.0 relative to height of master (1.0 is the same height as the master) + bordermode="inside" or "outside" - whether to take border width of master widget into account + """ + if "width" in kwargs or "height" in kwargs: + raise ValueError("'width' and 'height' arguments must be passed to the constructor of the widget, not the place method") + self._last_geometry_manager_call = {"function": super().place, "kwargs": kwargs} + return super().place(**self._apply_argument_scaling(kwargs)) + + def place_forget(self): + """ Unmap this widget. """ + self._last_geometry_manager_call = None + return super().place_forget() + + def pack(self, **kwargs): + """ + Pack a widget in the parent widget. Use as options: + after=widget - pack it after you have packed widget + anchor=NSEW (or subset) - position widget according to given direction + before=widget - pack it before you will pack widget + expand=bool - expand widget if parent size grows + fill=NONE or X or Y or BOTH - fill widget if widget grows + in=master - use master to contain this widget + in_=master - see 'in' option description + ipadx=amount - add internal padding in x direction + ipady=amount - add internal padding in y direction + padx=amount - add padding in x direction + pady=amount - add padding in y direction + side=TOP or BOTTOM or LEFT or RIGHT - where to add this widget. + """ + self._last_geometry_manager_call = {"function": super().pack, "kwargs": kwargs} + return super().pack(**self._apply_argument_scaling(kwargs)) + + def pack_forget(self): + """ Unmap this widget and do not use it for the packing order. """ + self._last_geometry_manager_call = None + return super().pack_forget() + + def grid(self, **kwargs): + """ + Position a widget in the parent widget in a grid. Use as options: + column=number - use cell identified with given column (starting with 0) + columnspan=number - this widget will span several columns + in=master - use master to contain this widget + in_=master - see 'in' option description + ipadx=amount - add internal padding in x direction + ipady=amount - add internal padding in y direction + padx=amount - add padding in x direction + pady=amount - add padding in y direction + row=number - use cell identified with given row (starting with 0) + rowspan=number - this widget will span several rows + sticky=NSEW - if cell is larger on which sides will this widget stick to the cell boundary + """ + self._last_geometry_manager_call = {"function": super().grid, "kwargs": kwargs} + return super().grid(**self._apply_argument_scaling(kwargs)) + + def grid_forget(self): + """ Unmap this widget. """ + self._last_geometry_manager_call = None + return super().grid_forget() diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py new file mode 100644 index 0000000..a6b8186 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py @@ -0,0 +1,198 @@ +import tkinter +import sys +from typing import Union, Tuple, Callable, List, Optional + +from ..theme import ThemeManager +from ..font import CTkFont +from ..appearance_mode import CTkAppearanceModeBaseClass +from ..scaling import CTkScalingBaseClass + + +class DropdownMenu(tkinter.Menu, CTkAppearanceModeBaseClass, CTkScalingBaseClass): + def __init__(self, *args, + min_character_width: int = 18, + + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + hover_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color: Optional[Union[str, Tuple[str, str]]] = None, + + font: Optional[Union[tuple, CTkFont]] = None, + command: Union[Callable, None] = None, + values: Optional[List[str]] = None, + **kwargs): + + # call init methods of super classes + tkinter.Menu.__init__(self, *args, **kwargs) + CTkAppearanceModeBaseClass.__init__(self) + CTkScalingBaseClass.__init__(self, scaling_type="widget") + + self._min_character_width = min_character_width + self._fg_color = ThemeManager.theme["DropdownMenu"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) + self._hover_color = ThemeManager.theme["DropdownMenu"]["hover_color"] if hover_color is None else self._check_color_type(hover_color) + self._text_color = ThemeManager.theme["DropdownMenu"]["text_color"] if text_color is None else self._check_color_type(text_color) + + # font + self._font = CTkFont() if font is None else self._check_font_type(font) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + self._configure_menu_for_platforms() + + self._values = values + self._command = command + + self._add_menu_commands() + + def destroy(self): + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + + # call destroy methods of super classes + tkinter.Menu.destroy(self) + CTkAppearanceModeBaseClass.destroy(self) + + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling """ + super().configure(font=self._apply_font_scaling(self._font)) + + def _configure_menu_for_platforms(self): + """ apply platform specific appearance attributes, configure all colors """ + + if sys.platform == "darwin": + super().configure(tearoff=False, + font=self._apply_font_scaling(self._font)) + + elif sys.platform.startswith("win"): + super().configure(tearoff=False, + relief="flat", + activebackground=self._apply_appearance_mode(self._hover_color), + borderwidth=self._apply_widget_scaling(4), + activeborderwidth=self._apply_widget_scaling(4), + bg=self._apply_appearance_mode(self._fg_color), + fg=self._apply_appearance_mode(self._text_color), + activeforeground=self._apply_appearance_mode(self._text_color), + font=self._apply_font_scaling(self._font), + cursor="hand2") + + else: + super().configure(tearoff=False, + relief="flat", + activebackground=self._apply_appearance_mode(self._hover_color), + borderwidth=0, + activeborderwidth=0, + bg=self._apply_appearance_mode(self._fg_color), + fg=self._apply_appearance_mode(self._text_color), + activeforeground=self._apply_appearance_mode(self._text_color), + font=self._apply_font_scaling(self._font)) + + def _add_menu_commands(self): + """ delete existing menu labels and createe new labels with command according to values list """ + + self.delete(0, "end") # delete all old commands + + if sys.platform.startswith("linux"): + for value in self._values: + self.add_command(label=" " + value.ljust(self._min_character_width) + " ", + command=lambda v=value: self._button_callback(v), + compound="left") + else: + for value in self._values: + self.add_command(label=value.ljust(self._min_character_width), + command=lambda v=value: self._button_callback(v), + compound="left") + + def _button_callback(self, value): + if self._command is not None: + self._command(value) + + def open(self, x: Union[int, float], y: Union[int, float]): + + if sys.platform == "darwin": + y += self._apply_widget_scaling(8) + else: + y += self._apply_widget_scaling(3) + + if sys.platform == "darwin" or sys.platform.startswith("win"): + self.post(int(x), int(y)) + else: # Linux + self.tk_popup(int(x), int(y)) + + def configure(self, **kwargs): + if "fg_color" in kwargs: + self._fg_color = self._check_color_type(kwargs.pop("fg_color")) + super().configure(bg=self._apply_appearance_mode(self._fg_color)) + + if "hover_color" in kwargs: + self._hover_color = self._check_color_type(kwargs.pop("hover_color")) + super().configure(activebackground=self._apply_appearance_mode(self._hover_color)) + + if "text_color" in kwargs: + self._text_color = self._check_color_type(kwargs.pop("text_color")) + super().configure(fg=self._apply_appearance_mode(self._text_color)) + + if "font" in kwargs: + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + self._font = self._check_font_type(kwargs.pop("font")) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + self._update_font() + + if "command" in kwargs: + self._command = kwargs.pop("command") + + if "values" in kwargs: + self._values = kwargs.pop("values") + self._add_menu_commands() + + super().configure(**kwargs) + + def cget(self, attribute_name: str) -> any: + if attribute_name == "min_character_width": + return self._min_character_width + + elif attribute_name == "fg_color": + return self._fg_color + elif attribute_name == "hover_color": + return self._hover_color + elif attribute_name == "text_color": + return self._text_color + + elif attribute_name == "font": + return self._font + elif attribute_name == "command": + return self._command + elif attribute_name == "values": + return self._values + + else: + return super().cget(attribute_name) + + @staticmethod + def _check_font_type(font: any): + if isinstance(font, CTkFont): + return font + + elif type(font) == tuple and len(font) == 1: + sys.stderr.write(f"Warning: font {font} given without size, will be extended with default text size of current theme\n") + return font[0], ThemeManager.theme["text"]["size"] + + elif type(font) == tuple and 2 <= len(font) <= 3: + return font + + else: + raise ValueError(f"Wrong font type {type(font)} for font '{font}'\n" + + f"For consistency, Customtkinter requires the font argument to be a tuple of len 2 or 3 or an instance of CTkFont.\n" + + f"\nUsage example:\n" + + f"font=customtkinter.CTkFont(family='', size=)\n" + + f"font=('', )\n") + + def _set_scaling(self, new_widget_scaling, new_window_scaling): + super()._set_scaling(new_widget_scaling, new_window_scaling) + self._configure_menu_for_platforms() + + def _set_appearance_mode(self, mode_string): + """ colors won't update on appearance mode change when dropdown is open, because it's not necessary """ + super()._set_appearance_mode(mode_string) + self._configure_menu_for_platforms() diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_button.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_button.py new file mode 100644 index 0000000..e9f7839 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_button.py @@ -0,0 +1,594 @@ +import tkinter +import sys +from typing import Union, Tuple, Callable, Optional, Any + +from .core_rendering import CTkCanvas +from .theme import ThemeManager +from .core_rendering import DrawEngine +from .core_widget_classes import CTkBaseClass +from .font import CTkFont +from .image import CTkImage + + +class CTkButton(CTkBaseClass): + """ + Button with rounded corners, border, hover effect, image support, click command and textvariable. + For detailed information check out the documentation. + """ + + _image_label_spacing: int = 6 + + def __init__(self, + master: Any, + width: int = 140, + height: int = 28, + corner_radius: Optional[int] = None, + border_width: Optional[int] = None, + border_spacing: int = 2, + + bg_color: Union[str, Tuple[str, str]] = "transparent", + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + hover_color: Optional[Union[str, Tuple[str, str]]] = None, + border_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None, + + background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = None, + round_width_to_even_numbers: bool = True, + round_height_to_even_numbers: bool = True, + + text: str = "CTkButton", + font: Optional[Union[tuple, CTkFont]] = None, + textvariable: Union[tkinter.Variable, None] = None, + image: Union[CTkImage, "ImageTk.PhotoImage", None] = None, + state: str = "normal", + hover: bool = True, + command: Union[Callable[[], Any], None] = None, + compound: str = "left", + anchor: str = "center", + **kwargs): + + # transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass + super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) + + # shape + self._corner_radius: int = ThemeManager.theme["CTkButton"]["corner_radius"] if corner_radius is None else corner_radius + self._corner_radius = min(self._corner_radius, round(self._current_height / 2)) + self._border_width: int = ThemeManager.theme["CTkButton"]["border_width"] if border_width is None else border_width + self._border_spacing: int = border_spacing + + # color + self._fg_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True) + self._hover_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["hover_color"] if hover_color is None else self._check_color_type(hover_color) + self._border_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["border_color"] if border_color is None else self._check_color_type(border_color) + self._text_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["text_color"] if text_color is None else self._check_color_type(text_color) + self._text_color_disabled: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled) + + # rendering options + self._background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = background_corner_colors # rendering options for DrawEngine + self._round_width_to_even_numbers: bool = round_width_to_even_numbers # rendering options for DrawEngine + self._round_height_to_even_numbers: bool = round_height_to_even_numbers # rendering options for DrawEngine + + # text, font + self._text = text + self._text_label: Union[tkinter.Label, None] = None + self._textvariable: tkinter.Variable = textvariable + self._font: Union[tuple, CTkFont] = CTkFont() if font is None else self._check_font_type(font) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + # image + self._image = self._check_image_type(image) + self._image_label: Union[tkinter.Label, None] = None + if isinstance(self._image, CTkImage): + self._image.add_configure_callback(self._update_image) + + # other + self._state: str = state + self._hover: bool = hover + self._command: Callable = command + self._compound: str = compound + self._anchor: str = anchor + self._click_animation_running: bool = False + + # canvas and draw engine + self._canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._canvas.grid(row=0, column=0, rowspan=5, columnspan=5, sticky="nsew") + self._draw_engine = DrawEngine(self._canvas) + self._draw_engine.set_round_to_even_numbers(self._round_width_to_even_numbers, self._round_height_to_even_numbers) # rendering options + + # configure cursor and initial draw + self._create_bindings() + self._set_cursor() + self._draw() + + def _create_bindings(self, sequence: Optional[str] = None): + """ set necessary bindings for functionality of widget, will overwrite other bindings """ + + if sequence is None or sequence == "": + self._canvas.bind("", self._on_enter) + + if self._text_label is not None: + self._text_label.bind("", self._on_enter) + if self._image_label is not None: + self._image_label.bind("", self._on_enter) + + if sequence is None or sequence == "": + self._canvas.bind("", self._on_leave) + + if self._text_label is not None: + self._text_label.bind("", self._on_leave) + if self._image_label is not None: + self._image_label.bind("", self._on_leave) + + if sequence is None or sequence == "": + self._canvas.bind("", self._clicked) + + if self._text_label is not None: + self._text_label.bind("", self._clicked) + if self._image_label is not None: + self._image_label.bind("", self._clicked) + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self._create_grid() + + if self._text_label is not None: + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + self._update_image() + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw(no_color_updates=True) + + def _set_appearance_mode(self, mode_string): + super()._set_appearance_mode(mode_string) + self._update_image() + + def _set_dimensions(self, width: int = None, height: int = None): + super()._set_dimensions(width, height) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw() + + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ + if self._text_label is not None: + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + # Workaround to force grid to be resized when text changes size. + # Otherwise grid will lag and only resizes if other mouse action occurs. + self._canvas.grid_forget() + self._canvas.grid(row=0, column=0, rowspan=5, columnspan=5, sticky="nsew") + + def _update_image(self): + if self._image_label is not None: + if isinstance(self._image, CTkImage): + self._image_label.configure(image=self._image.create_scaled_photo_image(self._get_widget_scaling(), + self._get_appearance_mode())) + elif self._image is not None: + self._image_label.configure(image=self._image) + + def destroy(self): + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + super().destroy() + + def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + + if self._background_corner_colors is not None: + self._draw_engine.draw_background_corners(self._apply_widget_scaling(self._current_width), + self._apply_widget_scaling(self._current_height)) + self._canvas.itemconfig("background_corner_top_left", fill=self._apply_appearance_mode(self._background_corner_colors[0])) + self._canvas.itemconfig("background_corner_top_right", fill=self._apply_appearance_mode(self._background_corner_colors[1])) + self._canvas.itemconfig("background_corner_bottom_right", fill=self._apply_appearance_mode(self._background_corner_colors[2])) + self._canvas.itemconfig("background_corner_bottom_left", fill=self._apply_appearance_mode(self._background_corner_colors[3])) + else: + self._canvas.delete("background_parts") + + requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width), + self._apply_widget_scaling(self._current_height), + self._apply_widget_scaling(self._corner_radius), + self._apply_widget_scaling(self._border_width)) + + if no_color_updates is False or requires_recoloring: + + self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + + # set color for the button border parts (outline) + self._canvas.itemconfig("border_parts", + outline=self._apply_appearance_mode(self._border_color), + fill=self._apply_appearance_mode(self._border_color)) + + # set color for inner button parts + if self._fg_color == "transparent": + self._canvas.itemconfig("inner_parts", + outline=self._apply_appearance_mode(self._bg_color), + fill=self._apply_appearance_mode(self._bg_color)) + else: + self._canvas.itemconfig("inner_parts", + outline=self._apply_appearance_mode(self._fg_color), + fill=self._apply_appearance_mode(self._fg_color)) + + # create text label if text given + if self._text is not None and self._text != "": + + if self._text_label is None: + self._text_label = tkinter.Label(master=self, + font=self._apply_font_scaling(self._font), + text=self._text, + padx=0, + pady=0, + borderwidth=1, + textvariable=self._textvariable) + self._create_grid() + + self._text_label.bind("", self._on_enter) + self._text_label.bind("", self._on_leave) + self._text_label.bind("", self._clicked) + self._text_label.bind("", self._clicked) + + if no_color_updates is False: + # set text_label fg color (text color) + self._text_label.configure(fg=self._apply_appearance_mode(self._text_color)) + + if self._state == tkinter.DISABLED: + self._text_label.configure(fg=(self._apply_appearance_mode(self._text_color_disabled))) + else: + self._text_label.configure(fg=self._apply_appearance_mode(self._text_color)) + + if self._apply_appearance_mode(self._fg_color) == "transparent": + self._text_label.configure(bg=self._apply_appearance_mode(self._bg_color)) + else: + self._text_label.configure(bg=self._apply_appearance_mode(self._fg_color)) + + else: + # delete text_label if no text given + if self._text_label is not None: + self._text_label.destroy() + self._text_label = None + self._create_grid() + + # create image label if image given + if self._image is not None: + + if self._image_label is None: + self._image_label = tkinter.Label(master=self) + self._update_image() # set image + self._create_grid() + + self._image_label.bind("", self._on_enter) + self._image_label.bind("", self._on_leave) + self._image_label.bind("", self._clicked) + self._image_label.bind("", self._clicked) + + if no_color_updates is False: + # set image_label bg color (background color of label) + if self._apply_appearance_mode(self._fg_color) == "transparent": + self._image_label.configure(bg=self._apply_appearance_mode(self._bg_color)) + else: + self._image_label.configure(bg=self._apply_appearance_mode(self._fg_color)) + + else: + # delete text_label if no text given + if self._image_label is not None: + self._image_label.destroy() + self._image_label = None + self._create_grid() + + def _create_grid(self): + """ configure grid system (5x5) """ + + # Outer rows and columns have weight of 1000 to overpower the rows and columns of the label and image with weight 1. + # Rows and columns of image and label need weight of 1 to collapse in case of missing space on the button, + # so image and label need sticky option to stick together in the center, and therefore outer rows and columns + # need weight of 100 in case of other anchor than center. + n_padding_weight, s_padding_weight, e_padding_weight, w_padding_weight = 1000, 1000, 1000, 1000 + if self._anchor != "center": + if "n" in self._anchor: + n_padding_weight, s_padding_weight = 0, 1000 + if "s" in self._anchor: + n_padding_weight, s_padding_weight = 1000, 0 + if "e" in self._anchor: + e_padding_weight, w_padding_weight = 1000, 0 + if "w" in self._anchor: + e_padding_weight, w_padding_weight = 0, 1000 + + scaled_minsize_rows = self._apply_widget_scaling(max(self._border_width + 1, self._border_spacing)) + scaled_minsize_columns = self._apply_widget_scaling(max(self._corner_radius, self._border_width + 1, self._border_spacing)) + + self.grid_rowconfigure(0, weight=n_padding_weight, minsize=scaled_minsize_rows) + self.grid_rowconfigure(4, weight=s_padding_weight, minsize=scaled_minsize_rows) + self.grid_columnconfigure(0, weight=e_padding_weight, minsize=scaled_minsize_columns) + self.grid_columnconfigure(4, weight=w_padding_weight, minsize=scaled_minsize_columns) + + if self._compound in ("right", "left"): + self.grid_rowconfigure(2, weight=1) + if self._image_label is not None and self._text_label is not None: + self.grid_columnconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._image_label_spacing)) + else: + self.grid_columnconfigure(2, weight=0) + + self.grid_rowconfigure((1, 3), weight=0) + self.grid_columnconfigure((1, 3), weight=1) + else: + self.grid_columnconfigure(2, weight=1) + if self._image_label is not None and self._text_label is not None: + self.grid_rowconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._image_label_spacing)) + else: + self.grid_rowconfigure(2, weight=0) + + self.grid_columnconfigure((1, 3), weight=0) + self.grid_rowconfigure((1, 3), weight=1) + + if self._compound == "right": + if self._image_label is not None: + self._image_label.grid(row=2, column=3, sticky="w") + if self._text_label is not None: + self._text_label.grid(row=2, column=1, sticky="e") + elif self._compound == "left": + if self._image_label is not None: + self._image_label.grid(row=2, column=1, sticky="e") + if self._text_label is not None: + self._text_label.grid(row=2, column=3, sticky="w") + elif self._compound == "top": + if self._image_label is not None: + self._image_label.grid(row=1, column=2, sticky="s") + if self._text_label is not None: + self._text_label.grid(row=3, column=2, sticky="n") + elif self._compound == "bottom": + if self._image_label is not None: + self._image_label.grid(row=3, column=2, sticky="n") + if self._text_label is not None: + self._text_label.grid(row=1, column=2, sticky="s") + + def configure(self, require_redraw=False, **kwargs): + if "corner_radius" in kwargs: + self._corner_radius = kwargs.pop("corner_radius") + self._create_grid() + require_redraw = True + + if "border_width" in kwargs: + self._border_width = kwargs.pop("border_width") + self._create_grid() + require_redraw = True + + if "border_spacing" in kwargs: + self._border_spacing = kwargs.pop("border_spacing") + self._create_grid() + require_redraw = True + + if "fg_color" in kwargs: + self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True) + require_redraw = True + + if "hover_color" in kwargs: + self._hover_color = self._check_color_type(kwargs.pop("hover_color")) + require_redraw = True + + if "border_color" in kwargs: + self._border_color = self._check_color_type(kwargs.pop("border_color")) + require_redraw = True + + if "text_color" in kwargs: + self._text_color = self._check_color_type(kwargs.pop("text_color")) + require_redraw = True + + if "text_color_disabled" in kwargs: + self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled")) + require_redraw = True + + if "background_corner_colors" in kwargs: + self._background_corner_colors = kwargs.pop("background_corner_colors") + require_redraw = True + + if "text" in kwargs: + self._text = kwargs.pop("text") + if self._text_label is None: + require_redraw = True # text_label will be created in .draw() + else: + self._text_label.configure(text=self._text) + + if "font" in kwargs: + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + self._font = self._check_font_type(kwargs.pop("font")) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + self._update_font() + + if "textvariable" in kwargs: + self._textvariable = kwargs.pop("textvariable") + if self._text_label is not None: + self._text_label.configure(textvariable=self._textvariable) + + if "image" in kwargs: + if isinstance(self._image, CTkImage): + self._image.remove_configure_callback(self._update_image) + self._image = self._check_image_type(kwargs.pop("image")) + if isinstance(self._image, CTkImage): + self._image.add_configure_callback(self._update_image) + self._update_image() + + if "state" in kwargs: + self._state = kwargs.pop("state") + self._set_cursor() + require_redraw = True + + if "hover" in kwargs: + self._hover = kwargs.pop("hover") + + if "command" in kwargs: + self._command = kwargs.pop("command") + self._set_cursor() + + if "compound" in kwargs: + self._compound = kwargs.pop("compound") + require_redraw = True + + if "anchor" in kwargs: + self._anchor = kwargs.pop("anchor") + self._create_grid() + require_redraw = True + + super().configure(require_redraw=require_redraw, **kwargs) + + def cget(self, attribute_name: str) -> any: + if attribute_name == "corner_radius": + return self._corner_radius + elif attribute_name == "border_width": + return self._border_width + elif attribute_name == "border_spacing": + return self._border_spacing + + elif attribute_name == "fg_color": + return self._fg_color + elif attribute_name == "hover_color": + return self._hover_color + elif attribute_name == "border_color": + return self._border_color + elif attribute_name == "text_color": + return self._text_color + elif attribute_name == "text_color_disabled": + return self._text_color_disabled + elif attribute_name == "background_corner_colors": + return self._background_corner_colors + + elif attribute_name == "text": + return self._text + elif attribute_name == "font": + return self._font + elif attribute_name == "textvariable": + return self._textvariable + elif attribute_name == "image": + return self._image + elif attribute_name == "state": + return self._state + elif attribute_name == "hover": + return self._hover + elif attribute_name == "command": + return self._command + elif attribute_name == "compound": + return self._compound + elif attribute_name == "anchor": + return self._anchor + else: + return super().cget(attribute_name) + + def _set_cursor(self): + if self._cursor_manipulation_enabled: + if self._state == tkinter.DISABLED: + if sys.platform == "darwin" and self._command is not None: + self.configure(cursor="arrow") + elif sys.platform.startswith("win") and self._command is not None: + self.configure(cursor="arrow") + + elif self._state == tkinter.NORMAL: + if sys.platform == "darwin" and self._command is not None: + self.configure(cursor="pointinghand") + elif sys.platform.startswith("win") and self._command is not None: + self.configure(cursor="hand2") + + def _on_enter(self, event=None): + if self._hover is True and self._state == "normal": + if self._hover_color is None: + inner_parts_color = self._fg_color + else: + inner_parts_color = self._hover_color + + # set color of inner button parts to hover color + self._canvas.itemconfig("inner_parts", + outline=self._apply_appearance_mode(inner_parts_color), + fill=self._apply_appearance_mode(inner_parts_color)) + + # set text_label bg color to button hover color + if self._text_label is not None: + self._text_label.configure(bg=self._apply_appearance_mode(inner_parts_color)) + + # set image_label bg color to button hover color + if self._image_label is not None: + self._image_label.configure(bg=self._apply_appearance_mode(inner_parts_color)) + + def _on_leave(self, event=None): + self._click_animation_running = False + + if self._fg_color == "transparent": + inner_parts_color = self._bg_color + else: + inner_parts_color = self._fg_color + + # set color of inner button parts + self._canvas.itemconfig("inner_parts", + outline=self._apply_appearance_mode(inner_parts_color), + fill=self._apply_appearance_mode(inner_parts_color)) + + # set text_label bg color (label color) + if self._text_label is not None: + self._text_label.configure(bg=self._apply_appearance_mode(inner_parts_color)) + + # set image_label bg color (image bg color) + if self._image_label is not None: + self._image_label.configure(bg=self._apply_appearance_mode(inner_parts_color)) + + def _click_animation(self): + if self._click_animation_running: + self._on_enter() + + def _clicked(self, event=None): + if self._state != tkinter.DISABLED: + + # click animation: change color with .on_leave() and back to normal after 100ms with click_animation() + self._on_leave() + self._click_animation_running = True + self.after(100, self._click_animation) + + if self._command is not None: + self._command() + + def invoke(self): + """ calls command function if button is not disabled """ + if self._state != tkinter.DISABLED: + if self._command is not None: + return self._command() + + def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True): + """ called on the tkinter.Canvas """ + if not (add == "+" or add is True): + raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") + self._canvas.bind(sequence, command, add=True) + + if self._text_label is not None: + self._text_label.bind(sequence, command, add=True) + if self._image_label is not None: + self._image_label.bind(sequence, command, add=True) + + def unbind(self, sequence: str = None, funcid: str = None): + """ called on the tkinter.Label and tkinter.Canvas """ + if funcid is not None: + raise ValueError("'funcid' argument can only be None, because there is a bug in" + + " tkinter and its not clear whether the internal callbacks will be unbinded or not") + self._canvas.unbind(sequence, None) + + if self._text_label is not None: + self._text_label.unbind(sequence, None) + if self._image_label is not None: + self._image_label.unbind(sequence, None) + + self._create_bindings(sequence=sequence) # restore internal callbacks for sequence + + def focus(self): + return self._text_label.focus() + + def focus_set(self): + return self._text_label.focus_set() + + def focus_force(self): + return self._text_label.focus_force() diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_checkbox.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_checkbox.py new file mode 100644 index 0000000..42f04f5 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_checkbox.py @@ -0,0 +1,469 @@ +import tkinter +import sys +from typing import Union, Tuple, Callable, Optional, Any + +from .core_rendering import CTkCanvas +from .theme import ThemeManager +from .core_rendering import DrawEngine +from .core_widget_classes import CTkBaseClass +from .font import CTkFont + + +class CTkCheckBox(CTkBaseClass): + """ + Checkbox with rounded corners, border, variable support and hover effect. + For detailed information check out the documentation. + """ + + def __init__(self, + master: Any, + width: int = 100, + height: int = 24, + checkbox_width: int = 24, + checkbox_height: int = 24, + corner_radius: Optional[int] = None, + border_width: Optional[int] = None, + + bg_color: Union[str, Tuple[str, str]] = "transparent", + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + hover_color: Optional[Union[str, Tuple[str, str]]] = None, + border_color: Optional[Union[str, Tuple[str, str]]] = None, + checkmark_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None, + + text: str = "CTkCheckBox", + font: Optional[Union[tuple, CTkFont]] = None, + textvariable: Union[tkinter.Variable, None] = None, + state: str = tkinter.NORMAL, + hover: bool = True, + command: Union[Callable[[], Any], None] = None, + onvalue: Union[int, str] = 1, + offvalue: Union[int, str] = 0, + variable: Union[tkinter.Variable, None] = None, + **kwargs): + + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass + super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) + + # dimensions + self._checkbox_width = checkbox_width + self._checkbox_height = checkbox_height + + # color + self._fg_color = ThemeManager.theme["CTkCheckBox"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) + self._hover_color = ThemeManager.theme["CTkCheckBox"]["hover_color"] if hover_color is None else self._check_color_type(hover_color) + self._border_color = ThemeManager.theme["CTkCheckBox"]["border_color"] if border_color is None else self._check_color_type(border_color) + self._checkmark_color = ThemeManager.theme["CTkCheckBox"]["checkmark_color"] if checkmark_color is None else self._check_color_type(checkmark_color) + + # shape + self._corner_radius = ThemeManager.theme["CTkCheckBox"]["corner_radius"] if corner_radius is None else corner_radius + self._border_width = ThemeManager.theme["CTkCheckBox"]["border_width"] if border_width is None else border_width + + # text + self._text = text + self._text_label: Union[tkinter.Label, None] = None + self._text_color = ThemeManager.theme["CTkCheckBox"]["text_color"] if text_color is None else self._check_color_type(text_color) + self._text_color_disabled = ThemeManager.theme["CTkCheckBox"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled) + + # font + self._font = CTkFont() if font is None else self._check_font_type(font) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + # callback and hover functionality + self._command = command + self._state = state + self._hover = hover + self._check_state = False + + self._onvalue = onvalue + self._offvalue = offvalue + self._variable: tkinter.Variable = variable + self._variable_callback_blocked = False + self._textvariable: tkinter.Variable = textvariable + self._variable_callback_name = None + + # configure grid system (1x3) + self.grid_columnconfigure(0, weight=0) + self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6)) + self.grid_columnconfigure(2, weight=1) + self.grid_rowconfigure(0, weight=1) + + self._bg_canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe") + + self._canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._checkbox_width), + height=self._apply_widget_scaling(self._checkbox_height)) + self._canvas.grid(row=0, column=0, sticky="e") + self._draw_engine = DrawEngine(self._canvas) + + self._text_label = tkinter.Label(master=self, + bd=0, + padx=0, + pady=0, + text=self._text, + justify=tkinter.LEFT, + font=self._apply_font_scaling(self._font), + textvariable=self._textvariable) + self._text_label.grid(row=0, column=2, sticky="w") + self._text_label["anchor"] = "w" + + # register variable callback and set state according to variable + if self._variable is not None and self._variable != "": + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self._check_state = True if self._variable.get() == self._onvalue else False + + self._create_bindings() + self._set_cursor() + self._draw() + + def _create_bindings(self, sequence: Optional[str] = None): + """ set necessary bindings for functionality of widget, will overwrite other bindings """ + if sequence is None or sequence == "": + self._canvas.bind("", self._on_enter) + self._text_label.bind("", self._on_enter) + if sequence is None or sequence == "": + self._canvas.bind("", self._on_leave) + self._text_label.bind("", self._on_leave) + if sequence is None or sequence == "": + self._canvas.bind("", self.toggle) + self._text_label.bind("", self.toggle) + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6)) + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + self._canvas.delete("checkmark") + self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._canvas.configure(width=self._apply_widget_scaling(self._checkbox_width), + height=self._apply_widget_scaling(self._checkbox_height)) + self._draw(no_color_updates=True) + + def _set_dimensions(self, width: int = None, height: int = None): + super()._set_dimensions(width, height) + + self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ + if self._text_label is not None: + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + # Workaround to force grid to be resized when text changes size. + # Otherwise grid will lag and only resizes if other mouse action occurs. + self._bg_canvas.grid_forget() + self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe") + + def destroy(self): + if self._variable is not None: + self._variable.trace_remove("write", self._variable_callback_name) + + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + + super().destroy() + + def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + + requires_recoloring_1 = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._checkbox_width), + self._apply_widget_scaling(self._checkbox_height), + self._apply_widget_scaling(self._corner_radius), + self._apply_widget_scaling(self._border_width)) + + if self._check_state is True: + requires_recoloring_2 = self._draw_engine.draw_checkmark(self._apply_widget_scaling(self._checkbox_width), + self._apply_widget_scaling(self._checkbox_height), + self._apply_widget_scaling(self._checkbox_height * 0.58)) + else: + requires_recoloring_2 = False + self._canvas.delete("checkmark") + + if no_color_updates is False or requires_recoloring_1 or requires_recoloring_2: + self._bg_canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + + if self._check_state is True: + self._canvas.itemconfig("inner_parts", + outline=self._apply_appearance_mode(self._fg_color), + fill=self._apply_appearance_mode(self._fg_color)) + self._canvas.itemconfig("border_parts", + outline=self._apply_appearance_mode(self._fg_color), + fill=self._apply_appearance_mode(self._fg_color)) + + if "create_line" in self._canvas.gettags("checkmark"): + self._canvas.itemconfig("checkmark", fill=self._apply_appearance_mode(self._checkmark_color)) + else: + self._canvas.itemconfig("checkmark", fill=self._apply_appearance_mode(self._checkmark_color)) + else: + self._canvas.itemconfig("inner_parts", + outline=self._apply_appearance_mode(self._bg_color), + fill=self._apply_appearance_mode(self._bg_color)) + self._canvas.itemconfig("border_parts", + outline=self._apply_appearance_mode(self._border_color), + fill=self._apply_appearance_mode(self._border_color)) + + if self._state == tkinter.DISABLED: + self._text_label.configure(fg=(self._apply_appearance_mode(self._text_color_disabled))) + else: + self._text_label.configure(fg=self._apply_appearance_mode(self._text_color)) + + self._text_label.configure(bg=self._apply_appearance_mode(self._bg_color)) + + def configure(self, require_redraw=False, **kwargs): + if "corner_radius" in kwargs: + self._corner_radius = kwargs.pop("corner_radius") + require_redraw = True + + if "border_width" in kwargs: + self._border_width = kwargs.pop("border_width") + require_redraw = True + + if "checkbox_width" in kwargs: + self._checkbox_width = kwargs.pop("checkbox_width") + self._canvas.configure(width=self._apply_widget_scaling(self._checkbox_width)) + require_redraw = True + + if "checkbox_height" in kwargs: + self._checkbox_height = kwargs.pop("checkbox_height") + self._canvas.configure(height=self._apply_widget_scaling(self._checkbox_height)) + require_redraw = True + + if "text" in kwargs: + self._text = kwargs.pop("text") + self._text_label.configure(text=self._text) + + if "font" in kwargs: + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + self._font = self._check_font_type(kwargs.pop("font")) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + self._update_font() + + if "state" in kwargs: + self._state = kwargs.pop("state") + self._set_cursor() + require_redraw = True + + if "fg_color" in kwargs: + self._fg_color = self._check_color_type(kwargs.pop("fg_color")) + require_redraw = True + + if "hover_color" in kwargs: + self._hover_color = self._check_color_type(kwargs.pop("hover_color")) + require_redraw = True + + if "border_color" in kwargs: + self._border_color = self._check_color_type(kwargs.pop("border_color")) + require_redraw = True + + if "checkmark_color" in kwargs: + self._checkmark_color = self._check_color_type(kwargs.pop("checkmark_color")) + require_redraw = True + + if "text_color" in kwargs: + self._text_color = self._check_color_type(kwargs.pop("text_color")) + require_redraw = True + + if "text_color_disabled" in kwargs: + self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled")) + require_redraw = True + + if "hover" in kwargs: + self._hover = kwargs.pop("hover") + + if "command" in kwargs: + self._command = kwargs.pop("command") + + if "textvariable" in kwargs: + self._textvariable = kwargs.pop("textvariable") + self._text_label.configure(textvariable=self._textvariable) + + if "variable" in kwargs: + if self._variable is not None and self._variable != "": + self._variable.trace_remove("write", self._variable_callback_name) # remove old variable callback + + self._variable = kwargs.pop("variable") + + if self._variable is not None and self._variable != "": + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self._check_state = True if self._variable.get() == self._onvalue else False + require_redraw = True + + super().configure(require_redraw=require_redraw, **kwargs) + + def cget(self, attribute_name: str) -> any: + if attribute_name == "corner_radius": + return self._corner_radius + elif attribute_name == "border_width": + return self._border_width + elif attribute_name == "checkbox_width": + return self._checkbox_width + elif attribute_name == "checkbox_height": + return self._checkbox_height + + elif attribute_name == "fg_color": + return self._fg_color + elif attribute_name == "hover_color": + return self._hover_color + elif attribute_name == "border_color": + return self._border_color + elif attribute_name == "checkmark_color": + return self._checkmark_color + elif attribute_name == "text_color": + return self._text_color + elif attribute_name == "text_color_disabled": + return self._text_color_disabled + + elif attribute_name == "text": + return self._text + elif attribute_name == "font": + return self._font + elif attribute_name == "textvariable": + return self._textvariable + elif attribute_name == "state": + return self._state + elif attribute_name == "hover": + return self._hover + elif attribute_name == "onvalue": + return self._onvalue + elif attribute_name == "offvalue": + return self._offvalue + elif attribute_name == "variable": + return self._variable + else: + return super().cget(attribute_name) + + def _set_cursor(self): + if self._cursor_manipulation_enabled: + if self._state == tkinter.DISABLED: + if sys.platform == "darwin": + self._canvas.configure(cursor="arrow") + if self._text_label is not None: + self._text_label.configure(cursor="arrow") + elif sys.platform.startswith("win"): + self._canvas.configure(cursor="arrow") + if self._text_label is not None: + self._text_label.configure(cursor="arrow") + + elif self._state == tkinter.NORMAL: + if sys.platform == "darwin": + self._canvas.configure(cursor="pointinghand") + if self._text_label is not None: + self._text_label.configure(cursor="pointinghand") + elif sys.platform.startswith("win"): + self._canvas.configure(cursor="hand2") + if self._text_label is not None: + self._text_label.configure(cursor="hand2") + + def _on_enter(self, event=0): + if self._hover is True and self._state == tkinter.NORMAL: + if self._check_state is True: + self._canvas.itemconfig("inner_parts", + fill=self._apply_appearance_mode(self._hover_color), + outline=self._apply_appearance_mode(self._hover_color)) + self._canvas.itemconfig("border_parts", + fill=self._apply_appearance_mode(self._hover_color), + outline=self._apply_appearance_mode(self._hover_color)) + else: + self._canvas.itemconfig("inner_parts", + fill=self._apply_appearance_mode(self._hover_color), + outline=self._apply_appearance_mode(self._hover_color)) + + def _on_leave(self, event=0): + if self._check_state is True: + self._canvas.itemconfig("inner_parts", + fill=self._apply_appearance_mode(self._fg_color), + outline=self._apply_appearance_mode(self._fg_color)) + self._canvas.itemconfig("border_parts", + fill=self._apply_appearance_mode(self._fg_color), + outline=self._apply_appearance_mode(self._fg_color)) + else: + self._canvas.itemconfig("inner_parts", + fill=self._apply_appearance_mode(self._bg_color), + outline=self._apply_appearance_mode(self._bg_color)) + self._canvas.itemconfig("border_parts", + fill=self._apply_appearance_mode(self._border_color), + outline=self._apply_appearance_mode(self._border_color)) + + def _variable_callback(self, var_name, index, mode): + if not self._variable_callback_blocked: + if self._variable.get() == self._onvalue: + self.select(from_variable_callback=True) + elif self._variable.get() == self._offvalue: + self.deselect(from_variable_callback=True) + + def toggle(self, event=0): + if self._state == tkinter.NORMAL: + if self._check_state is True: + self._check_state = False + self._draw() + else: + self._check_state = True + self._draw() + + if self._variable is not None: + self._variable_callback_blocked = True + self._variable.set(self._onvalue if self._check_state is True else self._offvalue) + self._variable_callback_blocked = False + + if self._command is not None: + self._command() + + def select(self, from_variable_callback=False): + self._check_state = True + self._draw() + + if self._variable is not None and not from_variable_callback: + self._variable_callback_blocked = True + self._variable.set(self._onvalue) + self._variable_callback_blocked = False + + def deselect(self, from_variable_callback=False): + self._check_state = False + self._draw() + + if self._variable is not None and not from_variable_callback: + self._variable_callback_blocked = True + self._variable.set(self._offvalue) + self._variable_callback_blocked = False + + def get(self) -> Union[int, str]: + return self._onvalue if self._check_state is True else self._offvalue + + def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True): + """ called on the tkinter.Canvas """ + if not (add == "+" or add is True): + raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") + self._canvas.bind(sequence, command, add=True) + self._text_label.bind(sequence, command, add=True) + + def unbind(self, sequence: str = None, funcid: str = None): + """ called on the tkinter.Label and tkinter.Canvas """ + if funcid is not None: + raise ValueError("'funcid' argument can only be None, because there is a bug in" + + " tkinter and its not clear whether the internal callbacks will be unbinded or not") + self._canvas.unbind(sequence, None) + self._text_label.unbind(sequence, None) + self._create_bindings(sequence=sequence) # restore internal callbacks for sequence + + def focus(self): + return self._text_label.focus() + + def focus_set(self): + return self._text_label.focus_set() + + def focus_force(self): + return self._text_label.focus_force() diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_combobox.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_combobox.py new file mode 100644 index 0000000..9949564 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_combobox.py @@ -0,0 +1,424 @@ +import tkinter +import sys +import copy +from typing import Union, Tuple, Callable, List, Optional, Any + +from .core_widget_classes import DropdownMenu +from .core_rendering import CTkCanvas +from .theme import ThemeManager +from .core_rendering import DrawEngine +from .core_widget_classes import CTkBaseClass +from .font import CTkFont + + +class CTkComboBox(CTkBaseClass): + """ + Combobox with dropdown menu, rounded corners, border, variable support. + For detailed information check out the documentation. + """ + + def __init__(self, + master: Any, + width: int = 140, + height: int = 28, + corner_radius: Optional[int] = None, + border_width: Optional[int] = None, + + bg_color: Union[str, Tuple[str, str]] = "transparent", + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + border_color: Optional[Union[str, Tuple[str, str]]] = None, + button_color: Optional[Union[str, Tuple[str, str]]] = None, + button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + dropdown_fg_color: Optional[Union[str, Tuple[str, str]]] = None, + dropdown_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + dropdown_text_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None, + + font: Optional[Union[tuple, CTkFont]] = None, + dropdown_font: Optional[Union[tuple, CTkFont]] = None, + values: Optional[List[str]] = None, + state: str = tkinter.NORMAL, + hover: bool = True, + variable: Union[tkinter.Variable, None] = None, + command: Union[Callable[[str], Any], None] = None, + justify: str = "left", + **kwargs): + + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass + super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) + + # shape + self._corner_radius = ThemeManager.theme["CTkComboBox"]["corner_radius"] if corner_radius is None else corner_radius + self._border_width = ThemeManager.theme["CTkComboBox"]["border_width"] if border_width is None else border_width + + # color + self._fg_color = ThemeManager.theme["CTkComboBox"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) + self._border_color = ThemeManager.theme["CTkComboBox"]["border_color"] if border_color is None else self._check_color_type(border_color) + self._button_color = ThemeManager.theme["CTkComboBox"]["button_color"] if button_color is None else self._check_color_type(button_color) + self._button_hover_color = ThemeManager.theme["CTkComboBox"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color) + self._text_color = ThemeManager.theme["CTkComboBox"]["text_color"] if text_color is None else self._check_color_type(text_color) + self._text_color_disabled = ThemeManager.theme["CTkComboBox"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled) + + # font + self._font = CTkFont() if font is None else self._check_font_type(font) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + # callback and hover functionality + self._command = command + self._variable = variable + self._state = state + self._hover = hover + + if values is None: + self._values = ["CTkComboBox"] + else: + self._values = values + + self._dropdown_menu = DropdownMenu(master=self, + values=self._values, + command=self._dropdown_callback, + fg_color=dropdown_fg_color, + hover_color=dropdown_hover_color, + text_color=dropdown_text_color, + font=dropdown_font) + + # configure grid system (1x1) + self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) + + self._canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self.draw_engine = DrawEngine(self._canvas) + + self._entry = tkinter.Entry(master=self, + state=self._state, + width=1, + bd=0, + justify=justify, + highlightthickness=0, + font=self._apply_font_scaling(self._font)) + + self._create_grid() + self._create_bindings() + self._draw() # initial draw + + if self._variable is not None: + self._entry.configure(textvariable=self._variable) + + # insert default value + if self._variable is None: + if len(self._values) > 0: + self._entry.insert(0, self._values[0]) + else: + self._entry.insert(0, "CTkComboBox") + + def _create_bindings(self, sequence: Optional[str] = None): + """ set necessary bindings for functionality of widget, will overwrite other bindings """ + if sequence is None: + self._canvas.tag_bind("right_parts", "", self._on_enter) + self._canvas.tag_bind("dropdown_arrow", "", self._on_enter) + self._canvas.tag_bind("right_parts", "", self._on_leave) + self._canvas.tag_bind("dropdown_arrow", "", self._on_leave) + self._canvas.tag_bind("right_parts", "", self._clicked) + self._canvas.tag_bind("dropdown_arrow", "", self._clicked) + + def _create_grid(self): + self._canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew") + + left_section_width = self._current_width - self._current_height + self._entry.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="ew", + padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(3)), + max(self._apply_widget_scaling(self._current_width - left_section_width + 3), self._apply_widget_scaling(3))), + pady=self._apply_widget_scaling(self._border_width)) + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + # change entry font size and grid padding + self._entry.configure(font=self._apply_font_scaling(self._font)) + self._create_grid() + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw(no_color_updates=True) + + def _set_dimensions(self, width: int = None, height: int = None): + super()._set_dimensions(width, height) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw() + + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ + self._entry.configure(font=self._apply_font_scaling(self._font)) + + # Workaround to force grid to be resized when text changes size. + # Otherwise grid will lag and only resizes if other mouse action occurs. + self._canvas.grid_forget() + self._canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew") + + def destroy(self): + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + + super().destroy() + + def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + + left_section_width = self._current_width - self._current_height + requires_recoloring = self.draw_engine.draw_rounded_rect_with_border_vertical_split(self._apply_widget_scaling(self._current_width), + self._apply_widget_scaling(self._current_height), + self._apply_widget_scaling(self._corner_radius), + self._apply_widget_scaling(self._border_width), + self._apply_widget_scaling(left_section_width)) + + requires_recoloring_2 = self.draw_engine.draw_dropdown_arrow(self._apply_widget_scaling(self._current_width - (self._current_height / 2)), + self._apply_widget_scaling(self._current_height / 2), + self._apply_widget_scaling(self._current_height / 3)) + + if no_color_updates is False or requires_recoloring or requires_recoloring_2: + + self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + + self._canvas.itemconfig("inner_parts_left", + outline=self._apply_appearance_mode(self._fg_color), + fill=self._apply_appearance_mode(self._fg_color)) + self._canvas.itemconfig("border_parts_left", + outline=self._apply_appearance_mode(self._border_color), + fill=self._apply_appearance_mode(self._border_color)) + self._canvas.itemconfig("inner_parts_right", + outline=self._apply_appearance_mode(self._button_color), + fill=self._apply_appearance_mode(self._button_color)) + self._canvas.itemconfig("border_parts_right", + outline=self._apply_appearance_mode(self._button_color), + fill=self._apply_appearance_mode(self._button_color)) + + self._entry.configure(bg=self._apply_appearance_mode(self._fg_color), + fg=self._apply_appearance_mode(self._text_color), + readonlybackground=self._apply_appearance_mode(self._fg_color), + disabledbackground=self._apply_appearance_mode(self._fg_color), + disabledforeground=self._apply_appearance_mode(self._text_color_disabled), + highlightcolor=self._apply_appearance_mode(self._fg_color), + insertbackground=self._apply_appearance_mode(self._text_color)) + + if self._state == tkinter.DISABLED: + self._canvas.itemconfig("dropdown_arrow", + fill=self._apply_appearance_mode(self._text_color_disabled)) + else: + self._canvas.itemconfig("dropdown_arrow", + fill=self._apply_appearance_mode(self._text_color)) + + def _open_dropdown_menu(self): + self._dropdown_menu.open(self.winfo_rootx(), + self.winfo_rooty() + self._apply_widget_scaling(self._current_height + 0)) + + def configure(self, require_redraw=False, **kwargs): + if "corner_radius" in kwargs: + self._corner_radius = kwargs.pop("corner_radius") + require_redraw = True + + if "border_width" in kwargs: + self._border_width = kwargs.pop("border_width") + self._create_grid() + require_redraw = True + + if "fg_color" in kwargs: + self._fg_color = self._check_color_type(kwargs.pop("fg_color")) + require_redraw = True + + if "border_color" in kwargs: + self._border_color = self._check_color_type(kwargs.pop("border_color")) + require_redraw = True + + if "button_color" in kwargs: + self._button_color = self._check_color_type(kwargs.pop("button_color")) + require_redraw = True + + if "button_hover_color" in kwargs: + self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color")) + require_redraw = True + + if "dropdown_fg_color" in kwargs: + self._dropdown_menu.configure(fg_color=kwargs.pop("dropdown_fg_color")) + + if "dropdown_hover_color" in kwargs: + self._dropdown_menu.configure(hover_color=kwargs.pop("dropdown_hover_color")) + + if "dropdown_text_color" in kwargs: + self._dropdown_menu.configure(text_color=kwargs.pop("dropdown_text_color")) + + if "text_color" in kwargs: + self._text_color = self._check_color_type(kwargs.pop("text_color")) + require_redraw = True + + if "text_color_disabled" in kwargs: + self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled")) + require_redraw = True + + if "font" in kwargs: + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + self._font = self._check_font_type(kwargs.pop("font")) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + self._update_font() + + if "dropdown_font" in kwargs: + self._dropdown_menu.configure(font=kwargs.pop("dropdown_font")) + + if "values" in kwargs: + self._values = kwargs.pop("values") + self._dropdown_menu.configure(values=self._values) + + if "state" in kwargs: + self._state = kwargs.pop("state") + self._entry.configure(state=self._state) + require_redraw = True + + if "hover" in kwargs: + self._hover = kwargs.pop("hover") + + if "variable" in kwargs: + self._variable = kwargs.pop("variable") + self._entry.configure(textvariable=self._variable) + + if "command" in kwargs: + self._command = kwargs.pop("command") + + if "justify" in kwargs: + self._entry.configure(justify=kwargs.pop("justify")) + + super().configure(require_redraw=require_redraw, **kwargs) + + def cget(self, attribute_name: str) -> any: + if attribute_name == "corner_radius": + return self._corner_radius + elif attribute_name == "border_width": + return self._border_width + + elif attribute_name == "fg_color": + return self._fg_color + elif attribute_name == "border_color": + return self._border_color + elif attribute_name == "button_color": + return self._button_color + elif attribute_name == "button_hover_color": + return self._button_hover_color + elif attribute_name == "dropdown_fg_color": + return self._dropdown_menu.cget("fg_color") + elif attribute_name == "dropdown_hover_color": + return self._dropdown_menu.cget("hover_color") + elif attribute_name == "dropdown_text_color": + return self._dropdown_menu.cget("text_color") + elif attribute_name == "text_color": + return self._text_color + elif attribute_name == "text_color_disabled": + return self._text_color_disabled + + elif attribute_name == "font": + return self._font + elif attribute_name == "dropdown_font": + return self._dropdown_menu.cget("font") + elif attribute_name == "values": + return copy.copy(self._values) + elif attribute_name == "state": + return self._state + elif attribute_name == "hover": + return self._hover + elif attribute_name == "variable": + return self._variable + elif attribute_name == "command": + return self._command + elif attribute_name == "justify": + return self._entry.cget("justify") + else: + return super().cget(attribute_name) + + def _on_enter(self, event=0): + if self._hover is True and self._state == tkinter.NORMAL and len(self._values) > 0: + if sys.platform == "darwin" and len(self._values) > 0 and self._cursor_manipulation_enabled: + self._canvas.configure(cursor="pointinghand") + elif sys.platform.startswith("win") and len(self._values) > 0 and self._cursor_manipulation_enabled: + self._canvas.configure(cursor="hand2") + + # set color of inner button parts to hover color + self._canvas.itemconfig("inner_parts_right", + outline=self._apply_appearance_mode(self._button_hover_color), + fill=self._apply_appearance_mode(self._button_hover_color)) + self._canvas.itemconfig("border_parts_right", + outline=self._apply_appearance_mode(self._button_hover_color), + fill=self._apply_appearance_mode(self._button_hover_color)) + + def _on_leave(self, event=0): + if sys.platform == "darwin" and len(self._values) > 0 and self._cursor_manipulation_enabled: + self._canvas.configure(cursor="arrow") + elif sys.platform.startswith("win") and len(self._values) > 0 and self._cursor_manipulation_enabled: + self._canvas.configure(cursor="arrow") + + # set color of inner button parts + self._canvas.itemconfig("inner_parts_right", + outline=self._apply_appearance_mode(self._button_color), + fill=self._apply_appearance_mode(self._button_color)) + self._canvas.itemconfig("border_parts_right", + outline=self._apply_appearance_mode(self._button_color), + fill=self._apply_appearance_mode(self._button_color)) + + def _dropdown_callback(self, value: str): + if self._state == "readonly": + self._entry.configure(state="normal") + self._entry.delete(0, tkinter.END) + self._entry.insert(0, value) + self._entry.configure(state="readonly") + else: + self._entry.delete(0, tkinter.END) + self._entry.insert(0, value) + + if self._command is not None: + self._command(value) + + def set(self, value: str): + if self._state == "readonly": + self._entry.configure(state="normal") + self._entry.delete(0, tkinter.END) + self._entry.insert(0, value) + self._entry.configure(state="readonly") + else: + self._entry.delete(0, tkinter.END) + self._entry.insert(0, value) + + def get(self) -> str: + return self._entry.get() + + def _clicked(self, event=None): + if self._state is not tkinter.DISABLED and len(self._values) > 0: + self._open_dropdown_menu() + + def bind(self, sequence=None, command=None, add=True): + """ called on the tkinter.Entry """ + if not (add == "+" or add is True): + raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") + self._entry.bind(sequence, command, add=True) + + def unbind(self, sequence=None, funcid=None): + """ called on the tkinter.Entry """ + if funcid is not None: + raise ValueError("'funcid' argument can only be None, because there is a bug in" + + " tkinter and its not clear whether the internal callbacks will be unbinded or not") + self._entry.unbind(sequence, None) # unbind all callbacks for sequence + self._create_bindings(sequence=sequence) # restore internal callbacks for sequence + + def focus(self): + return self._entry.focus() + + def focus_set(self): + return self._entry.focus_set() + + def focus_force(self): + return self._entry.focus_force() diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_entry.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_entry.py new file mode 100644 index 0000000..cdc0220 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_entry.py @@ -0,0 +1,384 @@ +import tkinter +from typing import Union, Tuple, Optional, Any + +from .core_rendering import CTkCanvas +from .theme import ThemeManager +from .core_rendering import DrawEngine +from .core_widget_classes import CTkBaseClass +from .font import CTkFont +from .utility import pop_from_dict_by_set, check_kwargs_empty + + +class CTkEntry(CTkBaseClass): + """ + Entry with rounded corners, border, textvariable support, focus and placeholder. + For detailed information check out the documentation. + """ + + _minimum_x_padding = 6 # minimum padding between tkinter entry and frame border + + # attributes that are passed to and managed by the tkinter entry only: + _valid_tk_entry_attributes = {"exportselection", "insertborderwidth", "insertofftime", + "insertontime", "insertwidth", "justify", "selectborderwidth", + "show", "takefocus", "validate", "validatecommand", "xscrollcommand"} + + def __init__(self, + master: Any, + width: int = 140, + height: int = 28, + corner_radius: Optional[int] = None, + border_width: Optional[int] = None, + + bg_color: Union[str, Tuple[str, str]] = "transparent", + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + border_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color: Optional[Union[str, Tuple[str, str]]] = None, + placeholder_text_color: Optional[Union[str, Tuple[str, str]]] = None, + + textvariable: Union[tkinter.Variable, None] = None, + placeholder_text: Union[str, None] = None, + font: Optional[Union[tuple, CTkFont]] = None, + state: str = tkinter.NORMAL, + **kwargs): + + # transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass + super().__init__(master=master, bg_color=bg_color, width=width, height=height) + + # configure grid system (1x1) + self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) + + # color + self._fg_color = ThemeManager.theme["CTkEntry"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True) + self._text_color = ThemeManager.theme["CTkEntry"]["text_color"] if text_color is None else self._check_color_type(text_color) + self._placeholder_text_color = ThemeManager.theme["CTkEntry"]["placeholder_text_color"] if placeholder_text_color is None else self._check_color_type(placeholder_text_color) + self._border_color = ThemeManager.theme["CTkEntry"]["border_color"] if border_color is None else self._check_color_type(border_color) + + # shape + self._corner_radius = ThemeManager.theme["CTkEntry"]["corner_radius"] if corner_radius is None else corner_radius + self._border_width = ThemeManager.theme["CTkEntry"]["border_width"] if border_width is None else border_width + + # text and state + self._is_focused: bool = True + self._placeholder_text = placeholder_text + self._placeholder_text_active = False + self._pre_placeholder_arguments = {} # some set arguments of the entry will be changed for placeholder and then set back + self._textvariable = textvariable + self._state = state + self._textvariable_callback_name: str = "" + + # font + self._font = CTkFont() if font is None else self._check_font_type(font) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + if not (self._textvariable is None or self._textvariable == ""): + self._textvariable_callback_name = self._textvariable.trace_add("write", self._textvariable_callback) + + self._canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._current_width), + height=self._apply_widget_scaling(self._current_height)) + self._draw_engine = DrawEngine(self._canvas) + + self._entry = tkinter.Entry(master=self, + bd=0, + width=1, + highlightthickness=0, + font=self._apply_font_scaling(self._font), + state=self._state, + textvariable=self._textvariable, + **pop_from_dict_by_set(kwargs, self._valid_tk_entry_attributes)) + + check_kwargs_empty(kwargs, raise_error=True) + + self._create_grid() + self._activate_placeholder() + self._create_bindings() + self._draw() + + def _create_bindings(self, sequence: Optional[str] = None): + """ set necessary bindings for functionality of widget, will overwrite other bindings """ + if sequence is None or sequence == "": + self._entry.bind("", self._entry_focus_in) + if sequence is None or sequence == "": + self._entry.bind("", self._entry_focus_out) + + def _create_grid(self): + self._canvas.grid(column=0, row=0, sticky="nswe") + + if self._corner_radius >= self._minimum_x_padding: + self._entry.grid(column=0, row=0, sticky="nswe", + padx=min(self._apply_widget_scaling(self._corner_radius), round(self._apply_widget_scaling(self._current_height/2))), + pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width + 1))) + else: + self._entry.grid(column=0, row=0, sticky="nswe", + padx=self._apply_widget_scaling(self._minimum_x_padding), + pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width + 1))) + + def _textvariable_callback(self, var_name, index, mode): + if self._textvariable.get() == "": + self._activate_placeholder() + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self._entry.configure(font=self._apply_font_scaling(self._font)) + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height)) + self._create_grid() + self._draw(no_color_updates=True) + + def _set_dimensions(self, width=None, height=None): + super()._set_dimensions(width, height) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw(no_color_updates=True) + + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ + self._entry.configure(font=self._apply_font_scaling(self._font)) + + # Workaround to force grid to be resized when text changes size. + # Otherwise grid will lag and only resizes if other mouse action occurs. + self._canvas.grid_forget() + self._canvas.grid(column=0, row=0, sticky="nswe") + + def destroy(self): + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + + super().destroy() + + def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + + requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width), + self._apply_widget_scaling(self._current_height), + self._apply_widget_scaling(self._corner_radius), + self._apply_widget_scaling(self._border_width)) + + if requires_recoloring or no_color_updates is False: + self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + + if self._apply_appearance_mode(self._fg_color) == "transparent": + self._canvas.itemconfig("inner_parts", + fill=self._apply_appearance_mode(self._bg_color), + outline=self._apply_appearance_mode(self._bg_color)) + self._entry.configure(bg=self._apply_appearance_mode(self._bg_color), + disabledbackground=self._apply_appearance_mode(self._bg_color), + readonlybackground=self._apply_appearance_mode(self._bg_color), + highlightcolor=self._apply_appearance_mode(self._bg_color)) + else: + self._canvas.itemconfig("inner_parts", + fill=self._apply_appearance_mode(self._fg_color), + outline=self._apply_appearance_mode(self._fg_color)) + self._entry.configure(bg=self._apply_appearance_mode(self._fg_color), + disabledbackground=self._apply_appearance_mode(self._fg_color), + readonlybackground=self._apply_appearance_mode(self._fg_color), + highlightcolor=self._apply_appearance_mode(self._fg_color)) + + self._canvas.itemconfig("border_parts", + fill=self._apply_appearance_mode(self._border_color), + outline=self._apply_appearance_mode(self._border_color)) + + if self._placeholder_text_active: + self._entry.config(fg=self._apply_appearance_mode(self._placeholder_text_color), + disabledforeground=self._apply_appearance_mode(self._placeholder_text_color), + insertbackground=self._apply_appearance_mode(self._placeholder_text_color)) + else: + self._entry.config(fg=self._apply_appearance_mode(self._text_color), + disabledforeground=self._apply_appearance_mode(self._text_color), + insertbackground=self._apply_appearance_mode(self._text_color)) + + def configure(self, require_redraw=False, **kwargs): + if "state" in kwargs: + self._state = kwargs.pop("state") + self._entry.configure(state=self._state) + + if "fg_color" in kwargs: + self._fg_color = self._check_color_type(kwargs.pop("fg_color")) + require_redraw = True + + if "text_color" in kwargs: + self._text_color = self._check_color_type(kwargs.pop("text_color")) + require_redraw = True + + if "placeholder_text_color" in kwargs: + self._placeholder_text_color = self._check_color_type(kwargs.pop("placeholder_text_color")) + require_redraw = True + + if "border_color" in kwargs: + self._border_color = self._check_color_type(kwargs.pop("border_color")) + require_redraw = True + + if "border_width" in kwargs: + self._border_width = kwargs.pop("border_width") + self._create_grid() + require_redraw = True + + if "corner_radius" in kwargs: + self._corner_radius = kwargs.pop("corner_radius") + self._create_grid() + require_redraw = True + + if "placeholder_text" in kwargs: + self._placeholder_text = kwargs.pop("placeholder_text") + if self._placeholder_text_active: + self._entry.delete(0, tkinter.END) + self._entry.insert(0, self._placeholder_text) + else: + self._activate_placeholder() + + if "textvariable" in kwargs: + self._textvariable = kwargs.pop("textvariable") + self._entry.configure(textvariable=self._textvariable) + + if "font" in kwargs: + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + self._font = self._check_font_type(kwargs.pop("font")) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + self._update_font() + + if "show" in kwargs: + if self._placeholder_text_active: + self._pre_placeholder_arguments["show"] = kwargs.pop("show") # remember show argument for when placeholder gets deactivated + else: + self._entry.configure(show=kwargs.pop("show")) + + self._entry.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_entry_attributes)) # configure Tkinter.Entry + super().configure(require_redraw=require_redraw, **kwargs) # configure CTkBaseClass + + def cget(self, attribute_name: str) -> any: + if attribute_name == "corner_radius": + return self._corner_radius + elif attribute_name == "border_width": + return self._border_width + + elif attribute_name == "fg_color": + return self._fg_color + elif attribute_name == "border_color": + return self._border_color + elif attribute_name == "text_color": + return self._text_color + elif attribute_name == "placeholder_text_color": + return self._placeholder_text_color + + elif attribute_name == "textvariable": + return self._textvariable + elif attribute_name == "placeholder_text": + return self._placeholder_text + elif attribute_name == "font": + return self._font + elif attribute_name == "state": + return self._state + + elif attribute_name in self._valid_tk_entry_attributes: + return self._entry.cget(attribute_name) # cget of tkinter.Entry + else: + return super().cget(attribute_name) # cget of CTkBaseClass + + def bind(self, sequence=None, command=None, add=True): + """ called on the tkinter.Entry """ + if not (add == "+" or add is True): + raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") + self._entry.bind(sequence, command, add=True) + + def unbind(self, sequence=None, funcid=None): + """ called on the tkinter.Entry """ + if funcid is not None: + raise ValueError("'funcid' argument can only be None, because there is a bug in" + + " tkinter and its not clear whether the internal callbacks will be unbinded or not") + self._entry.unbind(sequence, None) # unbind all callbacks for sequence + self._create_bindings(sequence=sequence) # restore internal callbacks for sequence + + def _activate_placeholder(self): + if self._entry.get() == "" and self._placeholder_text is not None and (self._textvariable is None or self._textvariable == ""): + self._placeholder_text_active = True + + self._pre_placeholder_arguments = {"show": self._entry.cget("show")} + self._entry.config(fg=self._apply_appearance_mode(self._placeholder_text_color), + disabledforeground=self._apply_appearance_mode(self._placeholder_text_color), + show="") + self._entry.delete(0, tkinter.END) + self._entry.insert(0, self._placeholder_text) + + def _deactivate_placeholder(self): + if self._placeholder_text_active and self._entry.cget("state") != "readonly": + self._placeholder_text_active = False + + self._entry.config(fg=self._apply_appearance_mode(self._text_color), + disabledforeground=self._apply_appearance_mode(self._text_color),) + self._entry.delete(0, tkinter.END) + for argument, value in self._pre_placeholder_arguments.items(): + self._entry[argument] = value + + def _entry_focus_out(self, event=None): + self._activate_placeholder() + self._is_focused = False + + def _entry_focus_in(self, event=None): + self._deactivate_placeholder() + self._is_focused = True + + def delete(self, first_index, last_index=None): + self._entry.delete(first_index, last_index) + + if not self._is_focused and self._entry.get() == "": + self._activate_placeholder() + + def insert(self, index, string): + self._deactivate_placeholder() + + return self._entry.insert(index, string) + + def get(self): + if self._placeholder_text_active: + return "" + else: + return self._entry.get() + + def focus(self): + self._entry.focus() + + def focus_set(self): + self._entry.focus_set() + + def focus_force(self): + self._entry.focus_force() + + def index(self, index): + return self._entry.index(index) + + def icursor(self, index): + return self._entry.icursor(index) + + def select_adjust(self, index): + return self._entry.select_adjust(index) + + def select_from(self, index): + return self._entry.icursor(index) + + def select_clear(self): + return self._entry.select_clear() + + def select_present(self): + return self._entry.select_present() + + def select_range(self, start_index, end_index): + return self._entry.select_range(start_index, end_index) + + def select_to(self, index): + return self._entry.select_to(index) + + def xview(self, index): + return self._entry.xview(index) + + def xview_moveto(self, f): + return self._entry.xview_moveto(f) + + def xview_scroll(self, number, what): + return self._entry.xview_scroll(number, what) diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_frame.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_frame.py new file mode 100644 index 0000000..7bddf3c --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_frame.py @@ -0,0 +1,196 @@ +from typing import Union, Tuple, List, Optional, Any + +from .core_rendering import CTkCanvas +from .theme import ThemeManager +from .core_rendering import DrawEngine +from .core_widget_classes import CTkBaseClass + + +class CTkFrame(CTkBaseClass): + """ + Frame with rounded corners and border. + Default foreground colors are set according to theme. + To make the frame transparent set fg_color=None. + For detailed information check out the documentation. + """ + + def __init__(self, + master: Any, + width: int = 200, + height: int = 200, + corner_radius: Optional[Union[int, str]] = None, + border_width: Optional[Union[int, str]] = None, + + bg_color: Union[str, Tuple[str, str]] = "transparent", + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + border_color: Optional[Union[str, Tuple[str, str]]] = None, + + background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = None, + overwrite_preferred_drawing_method: Union[str, None] = None, + **kwargs): + + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass + super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) + + # color + self._border_color = ThemeManager.theme["CTkFrame"]["border_color"] if border_color is None else self._check_color_type(border_color) + + # determine fg_color of frame + if fg_color is None: + if isinstance(self.master, CTkFrame): + if self.master._fg_color == ThemeManager.theme["CTkFrame"]["fg_color"]: + self._fg_color = ThemeManager.theme["CTkFrame"]["top_fg_color"] + else: + self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"] + else: + self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"] + else: + self._fg_color = self._check_color_type(fg_color, transparency=True) + + self._background_corner_colors = background_corner_colors # rendering options for DrawEngine + + # shape + self._corner_radius = ThemeManager.theme["CTkFrame"]["corner_radius"] if corner_radius is None else corner_radius + self._border_width = ThemeManager.theme["CTkFrame"]["border_width"] if border_width is None else border_width + + self._canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._current_width), + height=self._apply_widget_scaling(self._current_height)) + self._canvas.place(x=0, y=0, relwidth=1, relheight=1) + self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + self._draw_engine = DrawEngine(self._canvas) + self._overwrite_preferred_drawing_method = overwrite_preferred_drawing_method + + self._draw(no_color_updates=True) + + def winfo_children(self) -> List[any]: + """ + winfo_children of CTkFrame without self.canvas widget, + because it's not a child but part of the CTkFrame itself + """ + + child_widgets = super().winfo_children() + try: + child_widgets.remove(self._canvas) + return child_widgets + except ValueError: + return child_widgets + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw() + + def _set_dimensions(self, width=None, height=None): + super()._set_dimensions(width, height) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw() + + def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + + if not self._canvas.winfo_exists(): + return + + if self._background_corner_colors is not None: + self._draw_engine.draw_background_corners(self._apply_widget_scaling(self._current_width), + self._apply_widget_scaling(self._current_height)) + self._canvas.itemconfig("background_corner_top_left", fill=self._apply_appearance_mode(self._background_corner_colors[0])) + self._canvas.itemconfig("background_corner_top_right", fill=self._apply_appearance_mode(self._background_corner_colors[1])) + self._canvas.itemconfig("background_corner_bottom_right", fill=self._apply_appearance_mode(self._background_corner_colors[2])) + self._canvas.itemconfig("background_corner_bottom_left", fill=self._apply_appearance_mode(self._background_corner_colors[3])) + else: + self._canvas.delete("background_parts") + + requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width), + self._apply_widget_scaling(self._current_height), + self._apply_widget_scaling(self._corner_radius), + self._apply_widget_scaling(self._border_width), + overwrite_preferred_drawing_method=self._overwrite_preferred_drawing_method) + + if no_color_updates is False or requires_recoloring: + if self._fg_color == "transparent": + self._canvas.itemconfig("inner_parts", + fill=self._apply_appearance_mode(self._bg_color), + outline=self._apply_appearance_mode(self._bg_color)) + else: + self._canvas.itemconfig("inner_parts", + fill=self._apply_appearance_mode(self._fg_color), + outline=self._apply_appearance_mode(self._fg_color)) + + self._canvas.itemconfig("border_parts", + fill=self._apply_appearance_mode(self._border_color), + outline=self._apply_appearance_mode(self._border_color)) + self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + + # self._canvas.tag_lower("inner_parts") # maybe unnecessary, I don't know ??? + # self._canvas.tag_lower("border_parts") + + def configure(self, require_redraw=False, **kwargs): + if "fg_color" in kwargs: + self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True) + require_redraw = True + + # check if CTk widgets are children of the frame and change their bg_color to new frame fg_color + for child in self.winfo_children(): + if isinstance(child, CTkBaseClass): + child.configure(bg_color=self._fg_color) + + if "bg_color" in kwargs: + # pass bg_color change to children if fg_color is "transparent" + if self._fg_color == "transparent": + for child in self.winfo_children(): + if isinstance(child, CTkBaseClass): + child.configure(bg_color=self._fg_color) + + if "border_color" in kwargs: + self._border_color = self._check_color_type(kwargs.pop("border_color")) + require_redraw = True + + if "background_corner_colors" in kwargs: + self._background_corner_colors = kwargs.pop("background_corner_colors") + require_redraw = True + + if "corner_radius" in kwargs: + self._corner_radius = kwargs.pop("corner_radius") + require_redraw = True + + if "border_width" in kwargs: + self._border_width = kwargs.pop("border_width") + require_redraw = True + + super().configure(require_redraw=require_redraw, **kwargs) + + def cget(self, attribute_name: str) -> any: + if attribute_name == "corner_radius": + return self._corner_radius + elif attribute_name == "border_width": + return self._border_width + + elif attribute_name == "fg_color": + return self._fg_color + elif attribute_name == "border_color": + return self._border_color + elif attribute_name == "background_corner_colors": + return self._background_corner_colors + + else: + return super().cget(attribute_name) + + def bind(self, sequence=None, command=None, add=True): + """ called on the tkinter.Canvas """ + if not (add == "+" or add is True): + raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") + self._canvas.bind(sequence, command, add=True) + + def unbind(self, sequence=None, funcid=None): + """ called on the tkinter.Canvas """ + if funcid is not None: + raise ValueError("'funcid' argument can only be None, because there is a bug in" + + " tkinter and its not clear whether the internal callbacks will be unbinded or not") + self._canvas.unbind(sequence, None) diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_label.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_label.py new file mode 100644 index 0000000..7e59ad7 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_label.py @@ -0,0 +1,291 @@ +import tkinter +from typing import Union, Tuple, Callable, Optional, Any + +from .core_rendering import CTkCanvas +from .theme import ThemeManager +from .core_rendering import DrawEngine +from .core_widget_classes import CTkBaseClass +from .font import CTkFont +from .image import CTkImage +from .utility import pop_from_dict_by_set, check_kwargs_empty + + +class CTkLabel(CTkBaseClass): + """ + Label with rounded corners. Default is fg_color=None (transparent fg_color). + For detailed information check out the documentation. + + state argument will probably be removed because it has no effect + """ + + # attributes that are passed to and managed by the tkinter entry only: + _valid_tk_label_attributes = {"cursor", "justify", "padx", "pady", + "textvariable", "state", "takefocus", "underline"} + + def __init__(self, + master: Any, + width: int = 0, + height: int = 28, + corner_radius: Optional[int] = None, + + bg_color: Union[str, Tuple[str, str]] = "transparent", + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None, + + text: str = "CTkLabel", + font: Optional[Union[tuple, CTkFont]] = None, + image: Union[CTkImage, None] = None, + compound: str = "center", + anchor: str = "center", # label anchor: center, n, e, s, w + wraplength: int = 0, + **kwargs): + + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass + super().__init__(master=master, bg_color=bg_color, width=width, height=height) + + # color + self._fg_color = ThemeManager.theme["CTkLabel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True) + self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(text_color) + + if text_color_disabled is None: + if "text_color_disabled" in ThemeManager.theme["CTkLabel"]: + self._text_color_disabled = ThemeManager.theme["CTkLabel"]["text_color"] + else: + self._text_color_disabled = self._text_color + else: + self._text_color_disabled = self._check_color_type(text_color_disabled) + + # shape + self._corner_radius = ThemeManager.theme["CTkLabel"]["corner_radius"] if corner_radius is None else corner_radius + + # text + self._anchor = anchor + self._text = text + self._wraplength = wraplength + + # image + self._image = self._check_image_type(image) + self._compound = compound + if isinstance(self._image, CTkImage): + self._image.add_configure_callback(self._update_image) + + # font + self._font = CTkFont() if font is None else self._check_font_type(font) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + # configure grid system (1x1) + self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) + + self._canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._canvas.grid(row=0, column=0, sticky="nswe") + self._draw_engine = DrawEngine(self._canvas) + + self._label = tkinter.Label(master=self, + highlightthickness=0, + padx=0, + pady=0, + borderwidth=0, + anchor=self._anchor, + compound=self._compound, + wraplength=self._apply_widget_scaling(self._wraplength), + text=self._text, + font=self._apply_font_scaling(self._font)) + self._label.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_label_attributes)) + + check_kwargs_empty(kwargs, raise_error=True) + + self._create_grid() + self._update_image() + self._draw() + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height)) + self._label.configure(font=self._apply_font_scaling(self._font)) + self._label.configure(wraplength=self._apply_widget_scaling(self._wraplength)) + + self._create_grid() + self._update_image() + self._draw(no_color_updates=True) + + def _set_appearance_mode(self, mode_string): + super()._set_appearance_mode(mode_string) + self._update_image() + + def _set_dimensions(self, width=None, height=None): + super()._set_dimensions(width, height) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._create_grid() + self._draw() + + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ + self._label.configure(font=self._apply_font_scaling(self._font)) + + # Workaround to force grid to be resized when text changes size. + # Otherwise grid will lag and only resizes if other mouse action occurs. + self._canvas.grid_forget() + self._canvas.grid(row=0, column=0, sticky="nswe") + + def _update_image(self): + if isinstance(self._image, CTkImage): + self._label.configure(image=self._image.create_scaled_photo_image(self._get_widget_scaling(), + self._get_appearance_mode())) + elif self._image is not None: + self._label.configure(image=self._image) + + def destroy(self): + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + super().destroy() + + def _create_grid(self): + """ configure grid system (1x1) """ + + text_label_grid_sticky = self._anchor if self._anchor != "center" else "" + self._label.grid(row=0, column=0, sticky=text_label_grid_sticky, + padx=self._apply_widget_scaling(min(self._corner_radius, round(self._current_height / 2)))) + + def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + + requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width), + self._apply_widget_scaling(self._current_height), + self._apply_widget_scaling(self._corner_radius), + 0) + + if no_color_updates is False or requires_recoloring: + if self._apply_appearance_mode(self._fg_color) == "transparent": + self._canvas.itemconfig("inner_parts", + fill=self._apply_appearance_mode(self._bg_color), + outline=self._apply_appearance_mode(self._bg_color)) + + self._label.configure(fg=self._apply_appearance_mode(self._text_color), + disabledforeground=self._apply_appearance_mode(self._text_color_disabled), + bg=self._apply_appearance_mode(self._bg_color)) + else: + self._canvas.itemconfig("inner_parts", + fill=self._apply_appearance_mode(self._fg_color), + outline=self._apply_appearance_mode(self._fg_color)) + + self._label.configure(fg=self._apply_appearance_mode(self._text_color), + disabledforeground=self._apply_appearance_mode(self._text_color_disabled), + bg=self._apply_appearance_mode(self._fg_color)) + + self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + + def configure(self, require_redraw=False, **kwargs): + if "corner_radius" in kwargs: + self._corner_radius = kwargs.pop("corner_radius") + self._create_grid() + require_redraw = True + + if "fg_color" in kwargs: + self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True) + require_redraw = True + + if "text_color" in kwargs: + self._text_color = self._check_color_type(kwargs.pop("text_color")) + require_redraw = True + + if "text_color_disabled" in kwargs: + self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled")) + require_redraw = True + + if "text" in kwargs: + self._text = kwargs.pop("text") + self._label.configure(text=self._text) + + if "font" in kwargs: + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + self._font = self._check_font_type(kwargs.pop("font")) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + self._update_font() + + if "image" in kwargs: + if isinstance(self._image, CTkImage): + self._image.remove_configure_callback(self._update_image) + self._image = self._check_image_type(kwargs.pop("image")) + if isinstance(self._image, CTkImage): + self._image.add_configure_callback(self._update_image) + self._update_image() + + if "compound" in kwargs: + self._compound = kwargs.pop("compound") + self._label.configure(compound=self._compound) + + if "anchor" in kwargs: + self._anchor = kwargs.pop("anchor") + self._label.configure(anchor=self._anchor) + self._create_grid() + + if "wraplength" in kwargs: + self._wraplength = kwargs.pop("wraplength") + self._label.configure(wraplength=self._apply_widget_scaling(self._wraplength)) + + self._label.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_label_attributes)) # configure tkinter.Label + super().configure(require_redraw=require_redraw, **kwargs) # configure CTkBaseClass + + def cget(self, attribute_name: str) -> any: + if attribute_name == "corner_radius": + return self._corner_radius + + elif attribute_name == "fg_color": + return self._fg_color + elif attribute_name == "text_color": + return self._text_color + elif attribute_name == "text_color_disabled": + return self._text_color_disabled + + elif attribute_name == "text": + return self._text + elif attribute_name == "font": + return self._font + elif attribute_name == "image": + return self._image + elif attribute_name == "compound": + return self._compound + elif attribute_name == "anchor": + return self._anchor + elif attribute_name == "wraplength": + return self._wraplength + + elif attribute_name in self._valid_tk_label_attributes: + return self._label.cget(attribute_name) # cget of tkinter.Label + else: + return super().cget(attribute_name) # cget of CTkBaseClass + + def bind(self, sequence: str = None, command: Callable = None, add: str = True): + """ called on the tkinter.Label and tkinter.Canvas """ + if not (add == "+" or add is True): + raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") + self._canvas.bind(sequence, command, add=True) + self._label.bind(sequence, command, add=True) + + def unbind(self, sequence: str = None, funcid: Optional[str] = None): + """ called on the tkinter.Label and tkinter.Canvas """ + if funcid is not None: + raise ValueError("'funcid' argument can only be None, because there is a bug in" + + " tkinter and its not clear whether the internal callbacks will be unbinded or not") + self._canvas.unbind(sequence, None) + self._label.unbind(sequence, None) + + def focus(self): + return self._label.focus() + + def focus_set(self): + return self._label.focus_set() + + def focus_force(self): + return self._label.focus_force() diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_optionmenu.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_optionmenu.py new file mode 100644 index 0000000..491027b --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_optionmenu.py @@ -0,0 +1,426 @@ +import tkinter +import copy +import sys +from typing import Union, Tuple, Callable, Optional, Any + +from .core_rendering import CTkCanvas +from .theme import ThemeManager +from .core_rendering import DrawEngine +from .core_widget_classes import CTkBaseClass +from .core_widget_classes import DropdownMenu +from .font import CTkFont + + +class CTkOptionMenu(CTkBaseClass): + """ + Optionmenu with rounded corners, dropdown menu, variable support, command. + For detailed information check out the documentation. + """ + + def __init__(self, + master: Any, + width: int = 140, + height: int = 28, + corner_radius: Optional[Union[int]] = None, + + bg_color: Union[str, Tuple[str, str]] = "transparent", + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + button_color: Optional[Union[str, Tuple[str, str]]] = None, + button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None, + dropdown_fg_color: Optional[Union[str, Tuple[str, str]]] = None, + dropdown_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + dropdown_text_color: Optional[Union[str, Tuple[str, str]]] = None, + + font: Optional[Union[tuple, CTkFont]] = None, + dropdown_font: Optional[Union[tuple, CTkFont]] = None, + values: Optional[list] = None, + variable: Union[tkinter.Variable, None] = None, + state: str = tkinter.NORMAL, + hover: bool = True, + command: Union[Callable[[str], Any], None] = None, + dynamic_resizing: bool = True, + anchor: str = "w", + **kwargs): + + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass + super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) + + # color variables + self._fg_color = ThemeManager.theme["CTkOptionMenu"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) + self._button_color = ThemeManager.theme["CTkOptionMenu"]["button_color"] if button_color is None else self._check_color_type(button_color) + self._button_hover_color = ThemeManager.theme["CTkOptionMenu"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color) + + # shape + self._corner_radius = ThemeManager.theme["CTkOptionMenu"]["corner_radius"] if corner_radius is None else corner_radius + + # text and font + self._text_color = ThemeManager.theme["CTkOptionMenu"]["text_color"] if text_color is None else self._check_color_type(text_color) + self._text_color_disabled = ThemeManager.theme["CTkOptionMenu"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled) + + # font + self._font = CTkFont() if font is None else self._check_font_type(font) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + # callback and hover functionality + self._command = command + self._variable = variable + self._variable_callback_blocked: bool = False + self._variable_callback_name: Union[str, None] = None + self._state = state + self._hover = hover + self._dynamic_resizing = dynamic_resizing + + if values is None: + self._values = ["CTkOptionMenu"] + else: + self._values = values + + if len(self._values) > 0: + self._current_value = self._values[0] + else: + self._current_value = "CTkOptionMenu" + + self._dropdown_menu = DropdownMenu(master=self, + values=self._values, + command=self._dropdown_callback, + fg_color=dropdown_fg_color, + hover_color=dropdown_hover_color, + text_color=dropdown_text_color, + font=dropdown_font) + + # configure grid system (1x1) + self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) + + self._canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw_engine = DrawEngine(self._canvas) + + self._text_label = tkinter.Label(master=self, + font=self._apply_font_scaling(self._font), + anchor=anchor, + padx=0, + pady=0, + borderwidth=1, + text=self._current_value) + + if self._cursor_manipulation_enabled: + if sys.platform == "darwin": + self.configure(cursor="pointinghand") + elif sys.platform.startswith("win"): + self.configure(cursor="hand2") + + self._create_grid() + if not self._dynamic_resizing: + self.grid_propagate(0) + + self._create_bindings() + self._draw() # initial draw + + if self._variable is not None: + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self._current_value = self._variable.get() + self._text_label.configure(text=self._current_value) + + def _create_bindings(self, sequence: Optional[str] = None): + """ set necessary bindings for functionality of widget, will overwrite other bindings """ + if sequence is None or sequence == "": + self._canvas.bind("", self._on_enter) + self._text_label.bind("", self._on_enter) + if sequence is None or sequence == "": + self._canvas.bind("", self._on_leave) + self._text_label.bind("", self._on_leave) + if sequence is None or sequence == "": + self._canvas.bind("", self._clicked) + self._text_label.bind("", self._clicked) + + def _create_grid(self): + self._canvas.grid(row=0, column=0, sticky="nsew") + + left_section_width = self._current_width - self._current_height + self._text_label.grid(row=0, column=0, sticky="ew", + padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(3)), + max(self._apply_widget_scaling(self._current_width - left_section_width + 3), self._apply_widget_scaling(3)))) + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + # change label font size and grid padding + self._text_label.configure(font=self._apply_font_scaling(self._font)) + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._create_grid() + self._draw(no_color_updates=True) + + def _set_dimensions(self, width: int = None, height: int = None): + super()._set_dimensions(width, height) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw() + + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + # Workaround to force grid to be resized when text changes size. + # Otherwise grid will lag and only resizes if other mouse action occurs. + self._canvas.grid_forget() + self._canvas.grid(row=0, column=0, sticky="nsew") + + def destroy(self): + if self._variable is not None: # remove old callback + self._variable.trace_remove("write", self._variable_callback_name) + + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + + super().destroy() + + def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + + left_section_width = self._current_width - self._current_height + requires_recoloring = self._draw_engine.draw_rounded_rect_with_border_vertical_split(self._apply_widget_scaling(self._current_width), + self._apply_widget_scaling(self._current_height), + self._apply_widget_scaling(self._corner_radius), + 0, + self._apply_widget_scaling(left_section_width)) + + requires_recoloring_2 = self._draw_engine.draw_dropdown_arrow(self._apply_widget_scaling(self._current_width - (self._current_height / 2)), + self._apply_widget_scaling(self._current_height / 2), + self._apply_widget_scaling(self._current_height / 3)) + + if no_color_updates is False or requires_recoloring or requires_recoloring_2: + self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + + self._canvas.itemconfig("inner_parts_left", + outline=self._apply_appearance_mode(self._fg_color), + fill=self._apply_appearance_mode(self._fg_color)) + self._canvas.itemconfig("inner_parts_right", + outline=self._apply_appearance_mode(self._button_color), + fill=self._apply_appearance_mode(self._button_color)) + + self._text_label.configure(fg=self._apply_appearance_mode(self._text_color)) + + if self._state == tkinter.DISABLED: + self._text_label.configure(fg=(self._apply_appearance_mode(self._text_color_disabled))) + self._canvas.itemconfig("dropdown_arrow", + fill=self._apply_appearance_mode(self._text_color_disabled)) + else: + self._text_label.configure(fg=self._apply_appearance_mode(self._text_color)) + self._canvas.itemconfig("dropdown_arrow", + fill=self._apply_appearance_mode(self._text_color)) + + self._text_label.configure(bg=self._apply_appearance_mode(self._fg_color)) + + self._canvas.update_idletasks() + + def configure(self, require_redraw=False, **kwargs): + if "corner_radius" in kwargs: + self._corner_radius = kwargs.pop("corner_radius") + self._create_grid() + require_redraw = True + + if "fg_color" in kwargs: + self._fg_color = self._check_color_type(kwargs.pop("fg_color")) + require_redraw = True + + if "button_color" in kwargs: + self._button_color = self._check_color_type(kwargs.pop("button_color")) + require_redraw = True + + if "button_hover_color" in kwargs: + self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color")) + require_redraw = True + + if "text_color" in kwargs: + self._text_color = self._check_color_type(kwargs.pop("text_color")) + require_redraw = True + + if "text_color_disabled" in kwargs: + self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled")) + require_redraw = True + + if "dropdown_fg_color" in kwargs: + self._dropdown_menu.configure(fg_color=kwargs.pop("dropdown_fg_color")) + + if "dropdown_hover_color" in kwargs: + self._dropdown_menu.configure(hover_color=kwargs.pop("dropdown_hover_color")) + + if "dropdown_text_color" in kwargs: + self._dropdown_menu.configure(text_color=kwargs.pop("dropdown_text_color")) + + if "font" in kwargs: + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + self._font = self._check_font_type(kwargs.pop("font")) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + self._update_font() + + if "dropdown_font" in kwargs: + self._dropdown_menu.configure(font=kwargs.pop("dropdown_font")) + + if "values" in kwargs: + self._values = kwargs.pop("values") + self._dropdown_menu.configure(values=self._values) + + if "variable" in kwargs: + if self._variable is not None: # remove old callback + self._variable.trace_remove("write", self._variable_callback_name) + + self._variable = kwargs.pop("variable") + + if self._variable is not None and self._variable != "": + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self._current_value = self._variable.get() + self._text_label.configure(text=self._current_value) + else: + self._variable = None + + if "state" in kwargs: + self._state = kwargs.pop("state") + require_redraw = True + + if "hover" in kwargs: + self._hover = kwargs.pop("hover") + + if "command" in kwargs: + self._command = kwargs.pop("command") + + if "dynamic_resizing" in kwargs: + self._dynamic_resizing = kwargs.pop("dynamic_resizing") + if not self._dynamic_resizing: + self.grid_propagate(0) + else: + self.grid_propagate(1) + + if "anchor" in kwargs: + self._text_label.configure(anchor=kwargs.pop("anchor")) + + super().configure(require_redraw=require_redraw, **kwargs) + + def cget(self, attribute_name: str) -> any: + if attribute_name == "corner_radius": + return self._corner_radius + + elif attribute_name == "fg_color": + return self._fg_color + elif attribute_name == "button_color": + return self._button_color + elif attribute_name == "button_hover_color": + return self._button_hover_color + elif attribute_name == "text_color": + return self._text_color + elif attribute_name == "text_color_disabled": + return self._text_color_disabled + elif attribute_name == "dropdown_fg_color": + return self._dropdown_menu.cget("fg_color") + elif attribute_name == "dropdown_hover_color": + return self._dropdown_menu.cget("hover_color") + elif attribute_name == "dropdown_text_color": + return self._dropdown_menu.cget("text_color") + + elif attribute_name == "font": + return self._font + elif attribute_name == "dropdown_font": + return self._dropdown_menu.cget("font") + elif attribute_name == "values": + return copy.copy(self._values) + elif attribute_name == "variable": + return self._variable + elif attribute_name == "state": + return self._state + elif attribute_name == "hover": + return self._hover + elif attribute_name == "command": + return self._command + elif attribute_name == "dynamic_resizing": + return self._dynamic_resizing + elif attribute_name == "anchor": + return self._text_label.cget("anchor") + + else: + return super().cget(attribute_name) + + def _open_dropdown_menu(self): + self._dropdown_menu.open(self.winfo_rootx(), + self.winfo_rooty() + self._apply_widget_scaling(self._current_height + 0)) + + def _on_enter(self, event=0): + if self._hover is True and self._state == tkinter.NORMAL and len(self._values) > 0: + # set color of inner button parts to hover color + self._canvas.itemconfig("inner_parts_right", + outline=self._apply_appearance_mode(self._button_hover_color), + fill=self._apply_appearance_mode(self._button_hover_color)) + + def _on_leave(self, event=0): + # set color of inner button parts + self._canvas.itemconfig("inner_parts_right", + outline=self._apply_appearance_mode(self._button_color), + fill=self._apply_appearance_mode(self._button_color)) + + def _variable_callback(self, var_name, index, mode): + if not self._variable_callback_blocked: + self._current_value = self._variable.get() + self._text_label.configure(text=self._current_value) + + def _dropdown_callback(self, value: str): + self._current_value = value + self._text_label.configure(text=self._current_value) + + if self._variable is not None: + self._variable_callback_blocked = True + self._variable.set(self._current_value) + self._variable_callback_blocked = False + + if self._command is not None: + self._command(self._current_value) + + def set(self, value: str): + self._current_value = value + self._text_label.configure(text=self._current_value) + + if self._variable is not None: + self._variable_callback_blocked = True + self._variable.set(self._current_value) + self._variable_callback_blocked = False + + def get(self) -> str: + return self._current_value + + def _clicked(self, event=0): + if self._state is not tkinter.DISABLED and len(self._values) > 0: + self._open_dropdown_menu() + + def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True): + """ called on the tkinter.Canvas """ + if not (add == "+" or add is True): + raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") + self._canvas.bind(sequence, command, add=True) + self._text_label.bind(sequence, command, add=True) + + def unbind(self, sequence: str = None, funcid: str = None): + """ called on the tkinter.Label and tkinter.Canvas """ + if funcid is not None: + raise ValueError("'funcid' argument can only be None, because there is a bug in" + + " tkinter and its not clear whether the internal callbacks will be unbinded or not") + self._canvas.unbind(sequence, None) + self._text_label.unbind(sequence, None) + self._create_bindings(sequence=sequence) # restore internal callbacks for sequence + + def focus(self): + return self._text_label.focus() + + def focus_set(self): + return self._text_label.focus_set() + + def focus_force(self): + return self._text_label.focus_force() diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_progressbar.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_progressbar.py new file mode 100644 index 0000000..2d6ce59 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_progressbar.py @@ -0,0 +1,312 @@ +import tkinter +import math +from typing import Union, Tuple, Optional, Callable, Any +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + +from .core_rendering import CTkCanvas +from .theme import ThemeManager +from .core_rendering import DrawEngine +from .core_widget_classes import CTkBaseClass + + +class CTkProgressBar(CTkBaseClass): + """ + Progressbar with rounded corners, border, variable support, + indeterminate mode, vertical orientation. + For detailed information check out the documentation. + """ + + def __init__(self, + master: Any, + width: Optional[int] = None, + height: Optional[int] = None, + corner_radius: Optional[int] = None, + border_width: Optional[int] = None, + + bg_color: Union[str, Tuple[str, str]] = "transparent", + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + border_color: Optional[Union[str, Tuple[str, str]]] = None, + progress_color: Optional[Union[str, Tuple[str, str]]] = None, + + variable: Union[tkinter.Variable, None] = None, + orientation: str = "horizontal", + mode: Literal["determinate", "indeterminate"] = "determinate", + determinate_speed: float = 1, + indeterminate_speed: float = 1, + **kwargs): + + # set default dimensions according to orientation + if width is None: + if orientation.lower() == "vertical": + width = 8 + else: + width = 200 + if height is None: + if orientation.lower() == "vertical": + height = 200 + else: + height = 8 + + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass + super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) + + # color + self._border_color = ThemeManager.theme["CTkProgressBar"]["border_color"] if border_color is None else self._check_color_type(border_color) + self._fg_color = ThemeManager.theme["CTkProgressBar"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) + self._progress_color = ThemeManager.theme["CTkProgressBar"]["progress_color"] if progress_color is None else self._check_color_type(progress_color) + + # control variable + self._variable = variable + self._variable_callback_blocked = False + self._variable_callback_name = None + self._loop_after_id = None + + # shape + self._corner_radius = ThemeManager.theme["CTkProgressBar"]["corner_radius"] if corner_radius is None else corner_radius + self._border_width = ThemeManager.theme["CTkProgressBar"]["border_width"] if border_width is None else border_width + self._determinate_value: float = 0.5 # range 0-1 + self._determinate_speed = determinate_speed # range 0-1 + self._indeterminate_value: float = 0 # range 0-inf + self._indeterminate_width: float = 0.4 # range 0-1 + self._indeterminate_speed = indeterminate_speed # range 0-1 to travel in 50ms + self._loop_running: bool = False + self._orientation = orientation + self._mode = mode # "determinate" or "indeterminate" + + self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) + + self._canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nswe") + self._draw_engine = DrawEngine(self._canvas) + + self._draw() # initial draw + + if self._variable is not None: + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self._variable_callback_blocked = True + self.set(self._variable.get(), from_variable_callback=True) + self._variable_callback_blocked = False + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw(no_color_updates=True) + + def _set_dimensions(self, width=None, height=None): + super()._set_dimensions(width, height) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw() + + def destroy(self): + if self._variable is not None: + self._variable.trace_remove("write", self._variable_callback_name) + + super().destroy() + + def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + + if self._orientation.lower() == "horizontal": + orientation = "w" + elif self._orientation.lower() == "vertical": + orientation = "s" + else: + orientation = "w" + + if self._mode == "determinate": + requires_recoloring = self._draw_engine.draw_rounded_progress_bar_with_border(self._apply_widget_scaling(self._current_width), + self._apply_widget_scaling(self._current_height), + self._apply_widget_scaling(self._corner_radius), + self._apply_widget_scaling(self._border_width), + 0, + self._determinate_value, + orientation) + else: # indeterminate mode + progress_value = (math.sin(self._indeterminate_value * math.pi / 40) + 1) / 2 + progress_value_1 = min(1.0, progress_value + (self._indeterminate_width / 2)) + progress_value_2 = max(0.0, progress_value - (self._indeterminate_width / 2)) + + requires_recoloring = self._draw_engine.draw_rounded_progress_bar_with_border(self._apply_widget_scaling(self._current_width), + self._apply_widget_scaling(self._current_height), + self._apply_widget_scaling(self._corner_radius), + self._apply_widget_scaling(self._border_width), + progress_value_1, + progress_value_2, + orientation) + + if no_color_updates is False or requires_recoloring: + self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + self._canvas.itemconfig("border_parts", + fill=self._apply_appearance_mode(self._border_color), + outline=self._apply_appearance_mode(self._border_color)) + self._canvas.itemconfig("inner_parts", + fill=self._apply_appearance_mode(self._fg_color), + outline=self._apply_appearance_mode(self._fg_color)) + self._canvas.itemconfig("progress_parts", + fill=self._apply_appearance_mode(self._progress_color), + outline=self._apply_appearance_mode(self._progress_color)) + + def configure(self, require_redraw=False, **kwargs): + if "corner_radius" in kwargs: + self._corner_radius = kwargs.pop("corner_radius") + require_redraw = True + + if "border_width" in kwargs: + self._border_width = kwargs.pop("border_width") + require_redraw = True + + if "fg_color" in kwargs: + self._fg_color = self._check_color_type(kwargs.pop("fg_color")) + require_redraw = True + + if "border_color" in kwargs: + self._border_color = self._check_color_type(kwargs.pop("border_color")) + require_redraw = True + + if "progress_color" in kwargs: + self._progress_color = self._check_color_type(kwargs.pop("progress_color")) + require_redraw = True + + if "variable" in kwargs: + if self._variable is not None: + self._variable.trace_remove("write", self._variable_callback_name) + + self._variable = kwargs.pop("variable") + + if self._variable is not None and self._variable != "": + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self.set(self._variable.get(), from_variable_callback=True) + else: + self._variable = None + + if "mode" in kwargs: + self._mode = kwargs.pop("mode") + require_redraw = True + + if "determinate_speed" in kwargs: + self._determinate_speed = kwargs.pop("determinate_speed") + + if "indeterminate_speed" in kwargs: + self._indeterminate_speed = kwargs.pop("indeterminate_speed") + + super().configure(require_redraw=require_redraw, **kwargs) + + def cget(self, attribute_name: str) -> any: + if attribute_name == "corner_radius": + return self._corner_radius + elif attribute_name == "border_width": + return self._border_width + + elif attribute_name == "fg_color": + return self._fg_color + elif attribute_name == "border_color": + return self._border_color + elif attribute_name == "progress_color": + return self._progress_color + + elif attribute_name == "variable": + return self._variable + elif attribute_name == "orientation": + return self._orientation + elif attribute_name == "mode": + return self._mode + elif attribute_name == "determinate_speed": + return self._determinate_speed + elif attribute_name == "indeterminate_speed": + return self._indeterminate_speed + + else: + return super().cget(attribute_name) + + def _variable_callback(self, var_name, index, mode): + if not self._variable_callback_blocked: + self.set(self._variable.get(), from_variable_callback=True) + + def set(self, value, from_variable_callback=False): + """ set determinate value """ + self._determinate_value = value + + if self._determinate_value > 1: + self._determinate_value = 1 + elif self._determinate_value < 0: + self._determinate_value = 0 + + self._draw(no_color_updates=True) + + if self._variable is not None and not from_variable_callback: + self._variable_callback_blocked = True + self._variable.set(round(self._determinate_value) if isinstance(self._variable, tkinter.IntVar) else self._determinate_value) + self._variable_callback_blocked = False + + def get(self) -> float: + """ get determinate value """ + return self._determinate_value + + def start(self): + """ start automatic mode """ + if not self._loop_running: + self._loop_running = True + self._internal_loop() + + def stop(self): + """ stop automatic mode """ + if self._loop_after_id is not None: + self.after_cancel(self._loop_after_id) + self._loop_running = False + + def _internal_loop(self): + if self._loop_running: + if self._mode == "determinate": + self._determinate_value += self._determinate_speed / 50 + if self._determinate_value > 1: + self._determinate_value -= 1 + self._draw() + self._loop_after_id = self.after(20, self._internal_loop) + else: + self._indeterminate_value += self._indeterminate_speed + self._draw() + self._loop_after_id = self.after(20, self._internal_loop) + + def step(self): + """ increase progress """ + if self._mode == "determinate": + self._determinate_value += self._determinate_speed / 50 + if self._determinate_value > 1: + self._determinate_value -= 1 + self._draw() + else: + self._indeterminate_value += self._indeterminate_speed + self._draw() + + def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True): + """ called on the tkinter.Canvas """ + if not (add == "+" or add is True): + raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") + self._canvas.bind(sequence, command, add=True) + + def unbind(self, sequence: str = None, funcid: str = None): + """ called on the tkinter.Label and tkinter.Canvas """ + if funcid is not None: + raise ValueError("'funcid' argument can only be None, because there is a bug in" + + " tkinter and its not clear whether the internal callbacks will be unbinded or not") + self._canvas.unbind(sequence, None) + + def focus(self): + return self._canvas.focus() + + def focus_set(self): + return self._canvas.focus_set() + + def focus_force(self): + return self._canvas.focus_force() diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_radiobutton.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_radiobutton.py new file mode 100644 index 0000000..c07cd1f --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_radiobutton.py @@ -0,0 +1,430 @@ +import tkinter +import sys +from typing import Union, Tuple, Callable, Optional, Any + +from .core_rendering import CTkCanvas +from .theme import ThemeManager +from .core_rendering import DrawEngine +from .core_widget_classes import CTkBaseClass +from .font import CTkFont + + +class CTkRadioButton(CTkBaseClass): + """ + Radiobutton with rounded corners, border, label, variable support, command. + For detailed information check out the documentation. + """ + + def __init__(self, + master: Any, + width: int = 100, + height: int = 22, + radiobutton_width: int = 22, + radiobutton_height: int = 22, + corner_radius: Optional[int] = None, + border_width_unchecked: Optional[int] = None, + border_width_checked: Optional[int] = None, + + bg_color: Union[str, Tuple[str, str]] = "transparent", + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + hover_color: Optional[Union[str, Tuple[str, str]]] = None, + border_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None, + + text: str = "CTkRadioButton", + font: Optional[Union[tuple, CTkFont]] = None, + textvariable: Union[tkinter.Variable, None] = None, + variable: Union[tkinter.Variable, None] = None, + value: Union[int, str] = 0, + state: str = tkinter.NORMAL, + hover: bool = True, + command: Union[Callable, Any] = None, + **kwargs): + + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass + super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) + + # dimensions + self._radiobutton_width = radiobutton_width + self._radiobutton_height = radiobutton_height + + # color + self._fg_color = ThemeManager.theme["CTkRadioButton"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) + self._hover_color = ThemeManager.theme["CTkRadioButton"]["hover_color"] if hover_color is None else self._check_color_type(hover_color) + self._border_color = ThemeManager.theme["CTkRadioButton"]["border_color"] if border_color is None else self._check_color_type(border_color) + + # shape + self._corner_radius = ThemeManager.theme["CTkRadioButton"]["corner_radius"] if corner_radius is None else corner_radius + self._border_width_unchecked = ThemeManager.theme["CTkRadioButton"]["border_width_unchecked"] if border_width_unchecked is None else border_width_unchecked + self._border_width_checked = ThemeManager.theme["CTkRadioButton"]["border_width_checked"] if border_width_checked is None else border_width_checked + + # text + self._text = text + self._text_label: Union[tkinter.Label, None] = None + self._text_color = ThemeManager.theme["CTkRadioButton"]["text_color"] if text_color is None else self._check_color_type(text_color) + self._text_color_disabled = ThemeManager.theme["CTkRadioButton"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled) + + # font + self._font = CTkFont() if font is None else self._check_font_type(font) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + # callback and control variables + self._command = command + self._state = state + self._hover = hover + self._check_state: bool = False + self._value = value + self._variable: tkinter.Variable = variable + self._variable_callback_blocked: bool = False + self._textvariable = textvariable + self._variable_callback_name: Union[str, None] = None + + # configure grid system (3x1) + self.grid_columnconfigure(0, weight=0) + self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6)) + self.grid_columnconfigure(2, weight=1) + self.grid_rowconfigure(0, weight=1) + + self._bg_canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._current_width), + height=self._apply_widget_scaling(self._current_height)) + self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe") + + self._canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._radiobutton_width), + height=self._apply_widget_scaling(self._radiobutton_height)) + self._canvas.grid(row=0, column=0) + self._draw_engine = DrawEngine(self._canvas) + + self._text_label = tkinter.Label(master=self, + bd=0, + padx=0, + pady=0, + text=self._text, + justify=tkinter.LEFT, + font=self._apply_font_scaling(self._font), + textvariable=self._textvariable) + self._text_label.grid(row=0, column=2, sticky="w") + self._text_label["anchor"] = "w" + + if self._variable is not None: + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self._check_state = True if self._variable.get() == self._value else False + + self._create_bindings() + self._set_cursor() + self._draw() + + def _create_bindings(self, sequence: Optional[str] = None): + """ set necessary bindings for functionality of widget, will overwrite other bindings """ + if sequence is None or sequence == "": + self._canvas.bind("", self._on_enter) + self._text_label.bind("", self._on_enter) + if sequence is None or sequence == "": + self._canvas.bind("", self._on_leave) + self._text_label.bind("", self._on_leave) + if sequence is None or sequence == "": + self._canvas.bind("", self.invoke) + self._text_label.bind("", self.invoke) + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6)) + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._canvas.configure(width=self._apply_widget_scaling(self._radiobutton_width), + height=self._apply_widget_scaling(self._radiobutton_height)) + self._draw(no_color_updates=True) + + def _set_dimensions(self, width: int = None, height: int = None): + super()._set_dimensions(width, height) + + self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + # Workaround to force grid to be resized when text changes size. + # Otherwise grid will lag and only resizes if other mouse action occurs. + self._bg_canvas.grid_forget() + self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe") + + def destroy(self): + if self._variable is not None: + self._variable.trace_remove("write", self._variable_callback_name) + + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + + super().destroy() + + def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + + if self._check_state is True: + requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._radiobutton_width), + self._apply_widget_scaling(self._radiobutton_height), + self._apply_widget_scaling(self._corner_radius), + self._apply_widget_scaling(self._border_width_checked)) + else: + requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._radiobutton_width), + self._apply_widget_scaling(self._radiobutton_height), + self._apply_widget_scaling(self._corner_radius), + self._apply_widget_scaling(self._border_width_unchecked)) + + if no_color_updates is False or requires_recoloring: + self._bg_canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + + if self._check_state is False: + self._canvas.itemconfig("border_parts", + outline=self._apply_appearance_mode(self._border_color), + fill=self._apply_appearance_mode(self._border_color)) + else: + self._canvas.itemconfig("border_parts", + outline=self._apply_appearance_mode(self._fg_color), + fill=self._apply_appearance_mode(self._fg_color)) + + self._canvas.itemconfig("inner_parts", + outline=self._apply_appearance_mode(self._bg_color), + fill=self._apply_appearance_mode(self._bg_color)) + + if self._state == tkinter.DISABLED: + self._text_label.configure(fg=self._apply_appearance_mode(self._text_color_disabled)) + else: + self._text_label.configure(fg=self._apply_appearance_mode(self._text_color)) + + self._text_label.configure(bg=self._apply_appearance_mode(self._bg_color)) + + def configure(self, require_redraw=False, **kwargs): + if "corner_radius" in kwargs: + self._corner_radius = kwargs.pop("corner_radius") + require_redraw = True + + if "border_width_unchecked" in kwargs: + self._border_width_unchecked = kwargs.pop("border_width_unchecked") + require_redraw = True + + if "border_width_checked" in kwargs: + self._border_width_checked = kwargs.pop("border_width_checked") + require_redraw = True + + if "radiobutton_width" in kwargs: + self._radiobutton_width = kwargs.pop("radiobutton_width") + self._canvas.configure(width=self._apply_widget_scaling(self._radiobutton_width)) + require_redraw = True + + if "radiobutton_height" in kwargs: + self._radiobutton_height = kwargs.pop("radiobutton_height") + self._canvas.configure(height=self._apply_widget_scaling(self._radiobutton_height)) + require_redraw = True + + if "text" in kwargs: + self._text = kwargs.pop("text") + self._text_label.configure(text=self._text) + + if "font" in kwargs: + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + self._font = self._check_font_type(kwargs.pop("font")) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + self._update_font() + + if "state" in kwargs: + self._state = kwargs.pop("state") + self._set_cursor() + require_redraw = True + + if "fg_color" in kwargs: + self._fg_color = self._check_color_type(kwargs.pop("fg_color")) + require_redraw = True + + if "hover_color" in kwargs: + self._hover_color = self._check_color_type(kwargs.pop("hover_color")) + require_redraw = True + + if "text_color" in kwargs: + self._text_color = self._check_color_type(kwargs.pop("text_color")) + require_redraw = True + + if "text_color_disabled" in kwargs: + self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled")) + require_redraw = True + + if "border_color" in kwargs: + self._border_color = self._check_color_type(kwargs.pop("border_color")) + require_redraw = True + + if "hover" in kwargs: + self._hover = kwargs.pop("hover") + + if "command" in kwargs: + self._command = kwargs.pop("command") + + if "textvariable" in kwargs: + self._textvariable = kwargs.pop("textvariable") + self._text_label.configure(textvariable=self._textvariable) + + if "variable" in kwargs: + if self._variable is not None: + self._variable.trace_remove("write", self._variable_callback_name) + + self._variable = kwargs.pop("variable") + + if self._variable is not None and self._variable != "": + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self._check_state = True if self._variable.get() == self._value else False + require_redraw = True + + super().configure(require_redraw=require_redraw, **kwargs) + + def cget(self, attribute_name: str) -> any: + if attribute_name == "corner_radius": + return self._corner_radius + elif attribute_name == "border_width_unchecked": + return self._border_width_unchecked + elif attribute_name == "border_width_checked": + return self._border_width_checked + elif attribute_name == "radiobutton_width": + return self._radiobutton_width + elif attribute_name == "radiobutton_height": + return self._radiobutton_height + + elif attribute_name == "fg_color": + return self._fg_color + elif attribute_name == "hover_color": + return self._hover_color + elif attribute_name == "border_color": + return self._border_color + elif attribute_name == "text_color": + return self._text_color + elif attribute_name == "text_color_disabled": + return self._text_color_disabled + + elif attribute_name == "text": + return self._text + elif attribute_name == "font": + return self._font + elif attribute_name == "textvariable": + return self._textvariable + elif attribute_name == "variable": + return self._variable + elif attribute_name == "value": + return self._value + elif attribute_name == "state": + return self._state + elif attribute_name == "hover": + return self._hover + elif attribute_name == "command": + return self._command + + else: + return super().cget(attribute_name) + + def _set_cursor(self): + if self._cursor_manipulation_enabled: + if self._state == tkinter.DISABLED: + if sys.platform == "darwin": + self._canvas.configure(cursor="arrow") + if self._text_label is not None: + self._text_label.configure(cursor="arrow") + elif sys.platform.startswith("win"): + self._canvas.configure(cursor="arrow") + if self._text_label is not None: + self._text_label.configure(cursor="arrow") + + elif self._state == tkinter.NORMAL: + if sys.platform == "darwin": + self._canvas.configure(cursor="pointinghand") + if self._text_label is not None: + self._text_label.configure(cursor="pointinghand") + elif sys.platform.startswith("win"): + self._canvas.configure(cursor="hand2") + if self._text_label is not None: + self._text_label.configure(cursor="hand2") + + def _on_enter(self, event=0): + if self._hover is True and self._state == tkinter.NORMAL: + self._canvas.itemconfig("border_parts", + fill=self._apply_appearance_mode(self._hover_color), + outline=self._apply_appearance_mode(self._hover_color)) + + def _on_leave(self, event=0): + if self._check_state is True: + self._canvas.itemconfig("border_parts", + fill=self._apply_appearance_mode(self._fg_color), + outline=self._apply_appearance_mode(self._fg_color)) + else: + self._canvas.itemconfig("border_parts", + fill=self._apply_appearance_mode(self._border_color), + outline=self._apply_appearance_mode(self._border_color)) + + def _variable_callback(self, var_name, index, mode): + if not self._variable_callback_blocked: + if self._variable.get() == self._value: + self.select(from_variable_callback=True) + else: + self.deselect(from_variable_callback=True) + + def invoke(self, event=0): + if self._state == tkinter.NORMAL: + if self._check_state is False: + self._check_state = True + self.select() + + if self._command is not None: + self._command() + + def select(self, from_variable_callback=False): + self._check_state = True + self._draw() + + if self._variable is not None and not from_variable_callback: + self._variable_callback_blocked = True + self._variable.set(self._value) + self._variable_callback_blocked = False + + def deselect(self, from_variable_callback=False): + self._check_state = False + self._draw() + + if self._variable is not None and not from_variable_callback: + self._variable_callback_blocked = True + self._variable.set("") + self._variable_callback_blocked = False + + def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True): + """ called on the tkinter.Canvas """ + if not (add == "+" or add is True): + raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") + self._canvas.bind(sequence, command, add=True) + self._text_label.bind(sequence, command, add=True) + + def unbind(self, sequence: str = None, funcid: str = None): + """ called on the tkinter.Label and tkinter.Canvas """ + if funcid is not None: + raise ValueError("'funcid' argument can only be None, because there is a bug in" + + " tkinter and its not clear whether the internal callbacks will be unbinded or not") + self._canvas.unbind(sequence, None) + self._text_label.unbind(sequence, None) + self._create_bindings(sequence=sequence) # restore internal callbacks for sequence + + def focus(self): + return self._text_label.focus() + + def focus_set(self): + return self._text_label.focus_set() + + def focus_force(self): + return self._text_label.focus_force() diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_scrollable_frame.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_scrollable_frame.py new file mode 100644 index 0000000..eede091 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_scrollable_frame.py @@ -0,0 +1,316 @@ +from typing import Union, Tuple, Optional, Any +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal +import tkinter +import sys + +from .ctk_frame import CTkFrame +from .ctk_scrollbar import CTkScrollbar +from .appearance_mode import CTkAppearanceModeBaseClass +from .scaling import CTkScalingBaseClass +from .core_widget_classes import CTkBaseClass +from .ctk_label import CTkLabel +from .font import CTkFont +from .theme import ThemeManager + + +class CTkScrollableFrame(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClass): + def __init__(self, + master: Any, + width: int = 200, + height: int = 200, + corner_radius: Optional[Union[int, str]] = None, + border_width: Optional[Union[int, str]] = None, + + bg_color: Union[str, Tuple[str, str]] = "transparent", + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + border_color: Optional[Union[str, Tuple[str, str]]] = None, + scrollbar_fg_color: Optional[Union[str, Tuple[str, str]]] = None, + scrollbar_button_color: Optional[Union[str, Tuple[str, str]]] = None, + scrollbar_button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + label_fg_color: Optional[Union[str, Tuple[str, str]]] = None, + label_text_color: Optional[Union[str, Tuple[str, str]]] = None, + + label_text: str = "", + label_font: Optional[Union[tuple, CTkFont]] = None, + label_anchor: str = "center", + orientation: Literal["vertical", "horizontal"] = "vertical"): + + self._orientation = orientation + + # dimensions independent of scaling + self._desired_width = width # _desired_width and _desired_height, represent desired size set by width and height + self._desired_height = height + + self._parent_frame = CTkFrame(master=master, width=0, height=0, corner_radius=corner_radius, + border_width=border_width, bg_color=bg_color, fg_color=fg_color, border_color=border_color) + self._parent_canvas = tkinter.Canvas(master=self._parent_frame, highlightthickness=0) + self._set_scroll_increments() + + if self._orientation == "horizontal": + self._scrollbar = CTkScrollbar(master=self._parent_frame, orientation="horizontal", command=self._parent_canvas.xview, + fg_color=scrollbar_fg_color, button_color=scrollbar_button_color, button_hover_color=scrollbar_button_hover_color) + self._parent_canvas.configure(xscrollcommand=self._scrollbar.set) + elif self._orientation == "vertical": + self._scrollbar = CTkScrollbar(master=self._parent_frame, orientation="vertical", command=self._parent_canvas.yview, + fg_color=scrollbar_fg_color, button_color=scrollbar_button_color, button_hover_color=scrollbar_button_hover_color) + self._parent_canvas.configure(yscrollcommand=self._scrollbar.set) + + self._label_text = label_text + self._label = CTkLabel(self._parent_frame, text=label_text, anchor=label_anchor, font=label_font, + corner_radius=self._parent_frame.cget("corner_radius"), text_color=label_text_color, + fg_color=ThemeManager.theme["CTkScrollableFrame"]["label_fg_color"] if label_fg_color is None else label_fg_color) + + tkinter.Frame.__init__(self, master=self._parent_canvas, highlightthickness=0) + CTkAppearanceModeBaseClass.__init__(self) + CTkScalingBaseClass.__init__(self, scaling_type="widget") + + self._create_grid() + + self._parent_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + + self.bind("", lambda e: self._parent_canvas.configure(scrollregion=self._parent_canvas.bbox("all"))) + self._parent_canvas.bind("", self._fit_frame_dimensions_to_canvas) + self.bind_all("", self._mouse_wheel_all, add="+") + self.bind_all("", self._keyboard_shift_press_all, add="+") + self.bind_all("", self._keyboard_shift_press_all, add="+") + self.bind_all("", self._keyboard_shift_release_all, add="+") + self.bind_all("", self._keyboard_shift_release_all, add="+") + self._create_window_id = self._parent_canvas.create_window(0, 0, window=self, anchor="nw") + + if self._parent_frame.cget("fg_color") == "transparent": + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color"))) + self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color"))) + else: + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color"))) + self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color"))) + + self._shift_pressed = False + + def destroy(self): + tkinter.Frame.destroy(self) + CTkAppearanceModeBaseClass.destroy(self) + CTkScalingBaseClass.destroy(self) + + def _create_grid(self): + border_spacing = self._apply_widget_scaling(self._parent_frame.cget("corner_radius") + self._parent_frame.cget("border_width")) + + if self._orientation == "horizontal": + self._parent_frame.grid_columnconfigure(0, weight=1) + self._parent_frame.grid_rowconfigure(1, weight=1) + self._parent_canvas.grid(row=1, column=0, sticky="nsew", padx=border_spacing, pady=(border_spacing, 0)) + self._scrollbar.grid(row=2, column=0, sticky="nsew", padx=border_spacing) + + if self._label_text is not None and self._label_text != "": + self._label.grid(row=0, column=0, sticky="ew", padx=border_spacing, pady=border_spacing) + else: + self._label.grid_forget() + + elif self._orientation == "vertical": + self._parent_frame.grid_columnconfigure(0, weight=1) + self._parent_frame.grid_rowconfigure(1, weight=1) + self._parent_canvas.grid(row=1, column=0, sticky="nsew", padx=(border_spacing, 0), pady=border_spacing) + self._scrollbar.grid(row=1, column=1, sticky="nsew", pady=border_spacing) + + if self._label_text is not None and self._label_text != "": + self._label.grid(row=0, column=0, columnspan=2, sticky="ew", padx=border_spacing, pady=border_spacing) + else: + self._label.grid_forget() + + def _set_appearance_mode(self, mode_string): + super()._set_appearance_mode(mode_string) + + if self._parent_frame.cget("fg_color") == "transparent": + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color"))) + self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color"))) + else: + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color"))) + self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color"))) + + def _set_scaling(self, new_widget_scaling, new_window_scaling): + super()._set_scaling(new_widget_scaling, new_window_scaling) + + self._parent_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + + def _set_dimensions(self, width=None, height=None): + if width is not None: + self._desired_width = width + if height is not None: + self._desired_height = height + + self._parent_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + + def configure(self, **kwargs): + if "width" in kwargs: + self._set_dimensions(width=kwargs.pop("width")) + + if "height" in kwargs: + self._set_dimensions(height=kwargs.pop("height")) + + if "corner_radius" in kwargs: + new_corner_radius = kwargs.pop("corner_radius") + self._parent_frame.configure(corner_radius=new_corner_radius) + if self._label is not None: + self._label.configure(corner_radius=new_corner_radius) + self._create_grid() + + if "border_width" in kwargs: + self._parent_frame.configure(border_width=kwargs.pop("border_width")) + self._create_grid() + + if "fg_color" in kwargs: + self._parent_frame.configure(fg_color=kwargs.pop("fg_color")) + + if self._parent_frame.cget("fg_color") == "transparent": + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color"))) + self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color"))) + else: + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color"))) + self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color"))) + + for child in self.winfo_children(): + if isinstance(child, CTkBaseClass): + child.configure(bg_color=self._parent_frame.cget("fg_color")) + + if "scrollbar_fg_color" in kwargs: + self._scrollbar.configure(fg_color=kwargs.pop("scrollbar_fg_color")) + + if "scrollbar_button_color" in kwargs: + self._scrollbar.configure(button_color=kwargs.pop("scrollbar_button_color")) + + if "scrollbar_button_hover_color" in kwargs: + self._scrollbar.configure(button_hover_color=kwargs.pop("scrollbar_button_hover_color")) + + if "label_text" in kwargs: + self._label_text = kwargs.pop("label_text") + self._label.configure(text=self._label_text) + self._create_grid() + + if "label_font" in kwargs: + self._label.configure(font=kwargs.pop("label_font")) + + if "label_text_color" in kwargs: + self._label.configure(text_color=kwargs.pop("label_text_color")) + + if "label_fg_color" in kwargs: + self._label.configure(fg_color=kwargs.pop("label_fg_color")) + + if "label_anchor" in kwargs: + self._label.configure(anchor=kwargs.pop("label_anchor")) + + self._parent_frame.configure(**kwargs) + + def cget(self, attribute_name: str): + if attribute_name == "width": + return self._desired_width + elif attribute_name == "height": + return self._desired_height + + elif attribute_name == "label_text": + return self._label_text + elif attribute_name == "label_font": + return self._label.cget("font") + elif attribute_name == "label_text_color": + return self._label.cget("_text_color") + elif attribute_name == "label_fg_color": + return self._label.cget("fg_color") + elif attribute_name == "label_anchor": + return self._label.cget("anchor") + + elif attribute_name.startswith("scrollbar_fg_color"): + return self._scrollbar.cget("fg_color") + elif attribute_name.startswith("scrollbar_button_color"): + return self._scrollbar.cget("button_color") + elif attribute_name.startswith("scrollbar_button_hover_color"): + return self._scrollbar.cget("button_hover_color") + + else: + return self._parent_frame.cget(attribute_name) + + def _fit_frame_dimensions_to_canvas(self, event): + if self._orientation == "horizontal": + self._parent_canvas.itemconfigure(self._create_window_id, height=self._parent_canvas.winfo_height()) + elif self._orientation == "vertical": + self._parent_canvas.itemconfigure(self._create_window_id, width=self._parent_canvas.winfo_width()) + + def _set_scroll_increments(self): + if sys.platform.startswith("win"): + self._parent_canvas.configure(xscrollincrement=1, yscrollincrement=1) + elif sys.platform == "darwin": + self._parent_canvas.configure(xscrollincrement=4, yscrollincrement=8) + + def _mouse_wheel_all(self, event): + if self.check_if_master_is_canvas(event.widget): + if sys.platform.startswith("win"): + if self._shift_pressed: + if self._parent_canvas.xview() != (0.0, 1.0): + self._parent_canvas.xview("scroll", -int(event.delta / 6), "units") + else: + if self._parent_canvas.yview() != (0.0, 1.0): + self._parent_canvas.yview("scroll", -int(event.delta / 6), "units") + elif sys.platform == "darwin": + if self._shift_pressed: + if self._parent_canvas.xview() != (0.0, 1.0): + self._parent_canvas.xview("scroll", -event.delta, "units") + else: + if self._parent_canvas.yview() != (0.0, 1.0): + self._parent_canvas.yview("scroll", -event.delta, "units") + else: + if self._shift_pressed: + if self._parent_canvas.xview() != (0.0, 1.0): + self._parent_canvas.xview("scroll", -event.delta, "units") + else: + if self._parent_canvas.yview() != (0.0, 1.0): + self._parent_canvas.yview("scroll", -event.delta, "units") + + def _keyboard_shift_press_all(self, event): + self._shift_pressed = True + + def _keyboard_shift_release_all(self, event): + self._shift_pressed = False + + def check_if_master_is_canvas(self, widget): + if widget == self._parent_canvas: + return True + elif widget.master is not None: + return self.check_if_master_is_canvas(widget.master) + else: + return False + + def pack(self, **kwargs): + self._parent_frame.pack(**kwargs) + + def place(self, **kwargs): + self._parent_frame.place(**kwargs) + + def grid(self, **kwargs): + self._parent_frame.grid(**kwargs) + + def pack_forget(self): + self._parent_frame.pack_forget() + + def place_forget(self, **kwargs): + self._parent_frame.place_forget() + + def grid_forget(self, **kwargs): + self._parent_frame.grid_forget() + + def grid_remove(self, **kwargs): + self._parent_frame.grid_remove() + + def grid_propagate(self, **kwargs): + self._parent_frame.grid_propagate() + + def grid_info(self, **kwargs): + return self._parent_frame.grid_info() + + def lift(self, aboveThis=None): + self._parent_frame.lift(aboveThis) + + def lower(self, belowThis=None): + self._parent_frame.lower(belowThis) diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_scrollbar.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_scrollbar.py new file mode 100644 index 0000000..8e96221 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_scrollbar.py @@ -0,0 +1,281 @@ +import sys +from typing import Union, Tuple, Callable, Optional, Any + +from .core_rendering import CTkCanvas +from .theme import ThemeManager +from .core_rendering import DrawEngine +from .core_widget_classes import CTkBaseClass + + +class CTkScrollbar(CTkBaseClass): + """ + Scrollbar with rounded corners, configurable spacing. + Connect to scrollable widget by passing .set() method and set command attribute. + For detailed information check out the documentation. + """ + + def __init__(self, + master: Any, + width: Optional[Union[int, str]] = None, + height: Optional[Union[int, str]] = None, + corner_radius: Optional[int] = None, + border_spacing: Optional[int] = None, + minimum_pixel_length: int = 20, + + bg_color: Union[str, Tuple[str, str]] = "transparent", + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + button_color: Optional[Union[str, Tuple[str, str]]] = None, + button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + + hover: bool = True, + command: Union[Callable, Any] = None, + orientation: str = "vertical", + **kwargs): + + # set default dimensions according to orientation + if width is None: + if orientation.lower() == "vertical": + width = 16 + else: + width = 200 + if height is None: + if orientation.lower() == "horizontal": + height = 16 + else: + height = 200 + + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass + super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) + + # color + self._fg_color = ThemeManager.theme["CTkScrollbar"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True) + self._button_color = ThemeManager.theme["CTkScrollbar"]["button_color"] if button_color is None else self._check_color_type(button_color) + self._button_hover_color = ThemeManager.theme["CTkScrollbar"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color) + + # shape + self._corner_radius = ThemeManager.theme["CTkScrollbar"]["corner_radius"] if corner_radius is None else corner_radius + self._border_spacing = ThemeManager.theme["CTkScrollbar"]["border_spacing"] if border_spacing is None else border_spacing + + self._hover = hover + self._hover_state: bool = False + self._command = command + self._orientation = orientation + self._start_value: float = 0 # 0 to 1 + self._end_value: float = 1 # 0 to 1 + self._minimum_pixel_length = minimum_pixel_length + + self._canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._current_width), + height=self._apply_widget_scaling(self._current_height)) + self._canvas.place(x=0, y=0, relwidth=1, relheight=1) + self._draw_engine = DrawEngine(self._canvas) + + self._create_bindings() + self._draw() + + def _create_bindings(self, sequence: Optional[str] = None): + """ set necessary bindings for functionality of widget, will overwrite other bindings """ + if sequence is None: + self._canvas.tag_bind("border_parts", "", self._clicked) + if sequence is None or sequence == "": + self._canvas.bind("", self._on_enter) + if sequence is None or sequence == "": + self._canvas.bind("", self._on_leave) + if sequence is None or sequence == "": + self._canvas.bind("", self._clicked) + if sequence is None or sequence == "": + self._canvas.bind("", self._mouse_scroll_event) + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw(no_color_updates=True) + + def _set_dimensions(self, width=None, height=None): + super()._set_dimensions(width, height) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw(no_color_updates=True) + + def _get_scrollbar_values_for_minimum_pixel_size(self): + # correct scrollbar float values if scrollbar is too small + if self._orientation == "vertical": + scrollbar_pixel_length = (self._end_value - self._start_value) * self._current_height + if scrollbar_pixel_length < self._minimum_pixel_length and -scrollbar_pixel_length + self._current_height != 0: + # calculate how much to increase the float interval values so that the scrollbar width is self.minimum_pixel_length + interval_extend_factor = (-scrollbar_pixel_length + self._minimum_pixel_length) / (-scrollbar_pixel_length + self._current_height) + corrected_end_value = self._end_value + (1 - self._end_value) * interval_extend_factor + corrected_start_value = self._start_value - self._start_value * interval_extend_factor + return corrected_start_value, corrected_end_value + else: + return self._start_value, self._end_value + + else: + scrollbar_pixel_length = (self._end_value - self._start_value) * self._current_width + if scrollbar_pixel_length < self._minimum_pixel_length and -scrollbar_pixel_length + self._current_width != 0: + # calculate how much to increase the float interval values so that the scrollbar width is self.minimum_pixel_length + interval_extend_factor = (-scrollbar_pixel_length + self._minimum_pixel_length) / (-scrollbar_pixel_length + self._current_width) + corrected_end_value = self._end_value + (1 - self._end_value) * interval_extend_factor + corrected_start_value = self._start_value - self._start_value * interval_extend_factor + return corrected_start_value, corrected_end_value + else: + return self._start_value, self._end_value + + def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + + corrected_start_value, corrected_end_value = self._get_scrollbar_values_for_minimum_pixel_size() + requires_recoloring = self._draw_engine.draw_rounded_scrollbar(self._apply_widget_scaling(self._current_width), + self._apply_widget_scaling(self._current_height), + self._apply_widget_scaling(self._corner_radius), + self._apply_widget_scaling(self._border_spacing), + corrected_start_value, + corrected_end_value, + self._orientation) + + if no_color_updates is False or requires_recoloring: + if self._hover_state is True: + self._canvas.itemconfig("scrollbar_parts", + fill=self._apply_appearance_mode(self._button_hover_color), + outline=self._apply_appearance_mode(self._button_hover_color)) + else: + self._canvas.itemconfig("scrollbar_parts", + fill=self._apply_appearance_mode(self._button_color), + outline=self._apply_appearance_mode(self._button_color)) + + if self._fg_color == "transparent": + self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + self._canvas.itemconfig("border_parts", + fill=self._apply_appearance_mode(self._bg_color), + outline=self._apply_appearance_mode(self._bg_color)) + else: + self._canvas.configure(bg=self._apply_appearance_mode(self._fg_color)) + self._canvas.itemconfig("border_parts", + fill=self._apply_appearance_mode(self._fg_color), + outline=self._apply_appearance_mode(self._fg_color)) + + self._canvas.update_idletasks() + + def configure(self, require_redraw=False, **kwargs): + if "fg_color" in kwargs: + self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True) + require_redraw = True + + if "button_color" in kwargs: + self._button_color = self._check_color_type(kwargs.pop("button_color")) + require_redraw = True + + if "button_hover_color" in kwargs: + self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color")) + require_redraw = True + + if "hover" in kwargs: + self._hover = kwargs.pop("hover") + + if "command" in kwargs: + self._command = kwargs.pop("command") + + if "corner_radius" in kwargs: + self._corner_radius = kwargs.pop("corner_radius") + require_redraw = True + + if "border_spacing" in kwargs: + self._border_spacing = kwargs.pop("border_spacing") + require_redraw = True + + super().configure(require_redraw=require_redraw, **kwargs) + + def cget(self, attribute_name: str) -> any: + if attribute_name == "corner_radius": + return self._corner_radius + elif attribute_name == "border_spacing": + return self._border_spacing + elif attribute_name == "minimum_pixel_length": + return self._minimum_pixel_length + + elif attribute_name == "fg_color": + return self._fg_color + elif attribute_name == "scrollbar_color": + return self._button_color + elif attribute_name == "scrollbar_hover_color": + return self._button_hover_color + + elif attribute_name == "hover": + return self._hover + elif attribute_name == "command": + return self._command + elif attribute_name == "orientation": + return self._orientation + + else: + return super().cget(attribute_name) + + def _on_enter(self, event=0): + if self._hover is True: + self._hover_state = True + self._canvas.itemconfig("scrollbar_parts", + outline=self._apply_appearance_mode(self._button_hover_color), + fill=self._apply_appearance_mode(self._button_hover_color)) + + def _on_leave(self, event=0): + self._hover_state = False + self._canvas.itemconfig("scrollbar_parts", + outline=self._apply_appearance_mode(self._button_color), + fill=self._apply_appearance_mode(self._button_color)) + + def _clicked(self, event): + if self._orientation == "vertical": + value = self._reverse_widget_scaling(((event.y - self._border_spacing) / (self._current_height - 2 * self._border_spacing))) + else: + value = self._reverse_widget_scaling(((event.x - self._border_spacing) / (self._current_width - 2 * self._border_spacing))) + + current_scrollbar_length = self._end_value - self._start_value + value = max(current_scrollbar_length / 2, min(value, 1 - (current_scrollbar_length / 2))) + self._start_value = value - (current_scrollbar_length / 2) + self._end_value = value + (current_scrollbar_length / 2) + self._draw() + + if self._command is not None: + self._command('moveto', self._start_value) + + def _mouse_scroll_event(self, event=None): + if self._command is not None: + if sys.platform.startswith("win"): + self._command('scroll', -int(event.delta/40), 'units') + else: + self._command('scroll', -event.delta, 'units') + + def set(self, start_value: float, end_value: float): + self._start_value = float(start_value) + self._end_value = float(end_value) + self._draw() + + def get(self): + return self._start_value, self._end_value + + def bind(self, sequence=None, command=None, add=True): + """ called on the tkinter.Canvas """ + if not (add == "+" or add is True): + raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") + self._canvas.bind(sequence, command, add=True) + + def unbind(self, sequence=None, funcid=None): + """ called on the tkinter.Canvas, restores internal callbacks """ + if funcid is not None: + raise ValueError("'funcid' argument can only be None, because there is a bug in" + + " tkinter and its not clear whether the internal callbacks will be unbinded or not") + self._canvas.unbind(sequence, None) # unbind all callbacks for sequence + self._create_bindings(sequence=sequence) # restore internal callbacks for sequence + + def focus(self): + return self._canvas.focus() + + def focus_set(self): + return self._canvas.focus_set() + + def focus_force(self): + return self._canvas.focus_force() diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_segmented_button.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_segmented_button.py new file mode 100644 index 0000000..b8de1e7 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_segmented_button.py @@ -0,0 +1,447 @@ +import tkinter +import copy +from typing import Union, Tuple, List, Dict, Callable, Optional, Any +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + +from .theme import ThemeManager +from .font import CTkFont +from .ctk_button import CTkButton +from .ctk_frame import CTkFrame +from .utility import check_kwargs_empty + + +class CTkSegmentedButton(CTkFrame): + """ + Segmented button with corner radius, border width, variable support. + For detailed information check out the documentation. + """ + + def __init__(self, + master: Any, + width: int = 140, + height: int = 28, + corner_radius: Optional[int] = None, + border_width: int = 3, + + bg_color: Union[str, Tuple[str, str]] = "transparent", + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + selected_color: Optional[Union[str, Tuple[str, str]]] = None, + selected_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + unselected_color: Optional[Union[str, Tuple[str, str]]] = None, + unselected_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None, + background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = None, + + font: Optional[Union[tuple, CTkFont]] = None, + values: Optional[list] = None, + variable: Union[tkinter.Variable, None] = None, + dynamic_resizing: bool = True, + command: Union[Callable[[str], Any], None] = None, + state: str = "normal"): + + super().__init__(master=master, bg_color=bg_color, width=width, height=height) + + self._sb_fg_color = ThemeManager.theme["CTkSegmentedButton"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) + + self._sb_selected_color = ThemeManager.theme["CTkSegmentedButton"]["selected_color"] if selected_color is None else self._check_color_type(selected_color) + self._sb_selected_hover_color = ThemeManager.theme["CTkSegmentedButton"]["selected_hover_color"] if selected_hover_color is None else self._check_color_type(selected_hover_color) + + self._sb_unselected_color = ThemeManager.theme["CTkSegmentedButton"]["unselected_color"] if unselected_color is None else self._check_color_type(unselected_color) + self._sb_unselected_hover_color = ThemeManager.theme["CTkSegmentedButton"]["unselected_hover_color"] if unselected_hover_color is None else self._check_color_type(unselected_hover_color) + + self._sb_text_color = ThemeManager.theme["CTkSegmentedButton"]["text_color"] if text_color is None else self._check_color_type(text_color) + self._sb_text_color_disabled = ThemeManager.theme["CTkSegmentedButton"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled) + + self._sb_corner_radius = ThemeManager.theme["CTkSegmentedButton"]["corner_radius"] if corner_radius is None else corner_radius + self._sb_border_width = ThemeManager.theme["CTkSegmentedButton"]["border_width"] if border_width is None else border_width + + self._background_corner_colors = background_corner_colors # rendering options for DrawEngine + + self._command: Callable[[str], None] = command + self._font = CTkFont() if font is None else font + self._state = state + + self._buttons_dict: Dict[str, CTkButton] = {} # mapped from value to button object + if values is None: + self._value_list: List[str] = ["CTkSegmentedButton"] + else: + self._value_list: List[str] = values # Values ordered like buttons rendered on widget + + self._dynamic_resizing = dynamic_resizing + if not self._dynamic_resizing: + self.grid_propagate(False) + + self._check_unique_values(self._value_list) + self._current_value: str = "" + if len(self._value_list) > 0: + self._create_buttons_from_values() + self._create_button_grid() + + self._variable = variable + self._variable_callback_blocked: bool = False + self._variable_callback_name: Union[str, None] = None + + if self._variable is not None: + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self.set(self._variable.get(), from_variable_callback=True) + + super().configure(corner_radius=self._sb_corner_radius, fg_color="transparent") + + def destroy(self): + if self._variable is not None: # remove old callback + self._variable.trace_remove("write", self._variable_callback_name) + + super().destroy() + + def _set_dimensions(self, width: int = None, height: int = None): + super()._set_dimensions(width, height) + + for button in self._buttons_dict.values(): + button.configure(height=height) + + def _variable_callback(self, var_name, index, mode): + if not self._variable_callback_blocked: + self.set(self._variable.get(), from_variable_callback=True) + + def _get_index_by_value(self, value: str): + for index, value_from_list in enumerate(self._value_list): + if value_from_list == value: + return index + + raise ValueError(f"CTkSegmentedButton does not contain value '{value}'") + + def _configure_button_corners_for_index(self, index: int): + if index == 0 and len(self._value_list) == 1: + if self._background_corner_colors is None: + self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._bg_color, self._bg_color, self._bg_color, self._bg_color)) + else: + self._buttons_dict[self._value_list[index]].configure(background_corner_colors=self._background_corner_colors) + + elif index == 0: + if self._background_corner_colors is None: + self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._bg_color, self._sb_fg_color, self._sb_fg_color, self._bg_color)) + else: + self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._background_corner_colors[0], self._sb_fg_color, self._sb_fg_color, self._background_corner_colors[3])) + + elif index == len(self._value_list) - 1: + if self._background_corner_colors is None: + self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._bg_color, self._bg_color, self._sb_fg_color)) + else: + self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._background_corner_colors[1], self._background_corner_colors[2], self._sb_fg_color)) + + else: + self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._sb_fg_color, self._sb_fg_color, self._sb_fg_color)) + + def _unselect_button_by_value(self, value: str): + if value in self._buttons_dict: + self._buttons_dict[value].configure(fg_color=self._sb_unselected_color, + hover_color=self._sb_unselected_hover_color) + + def _select_button_by_value(self, value: str): + if self._current_value is not None and self._current_value != "": + self._unselect_button_by_value(self._current_value) + + self._current_value = value + + self._buttons_dict[value].configure(fg_color=self._sb_selected_color, + hover_color=self._sb_selected_hover_color) + + def _create_button(self, index: int, value: str) -> CTkButton: + new_button = CTkButton(self, + width=0, + height=self._current_height, + corner_radius=self._sb_corner_radius, + border_width=self._sb_border_width, + fg_color=self._sb_unselected_color, + border_color=self._sb_fg_color, + hover_color=self._sb_unselected_hover_color, + text_color=self._sb_text_color, + text_color_disabled=self._sb_text_color_disabled, + text=value, + font=self._font, + state=self._state, + command=lambda v=value: self.set(v, from_button_callback=True), + background_corner_colors=None, + round_width_to_even_numbers=False, + round_height_to_even_numbers=False) # DrawEngine rendering option (so that theres no gap between buttons) + + return new_button + + @staticmethod + def _check_unique_values(values: List[str]): + """ raises exception if values are not unique """ + if len(values) != len(set(values)): + raise ValueError("CTkSegmentedButton values are not unique") + + def _create_button_grid(self): + # remove minsize from every grid cell in the first row + number_of_columns, _ = self.grid_size() + for n in range(number_of_columns): + self.grid_columnconfigure(n, weight=1, minsize=0) + self.grid_rowconfigure(0, weight=1) + + for index, value in enumerate(self._value_list): + self.grid_columnconfigure(index, weight=1, minsize=self._current_height) + self._buttons_dict[value].grid(row=0, column=index, sticky="nsew") + + def _create_buttons_from_values(self): + assert len(self._buttons_dict) == 0 + assert len(self._value_list) > 0 + + for index, value in enumerate(self._value_list): + self._buttons_dict[value] = self._create_button(index, value) + self._configure_button_corners_for_index(index) + + def configure(self, **kwargs): + if "width" in kwargs: + super().configure(width=kwargs.pop("width")) + + if "height" in kwargs: + super().configure(height=kwargs.pop("height")) + + if "corner_radius" in kwargs: + self._sb_corner_radius = kwargs.pop("corner_radius") + super().configure(corner_radius=self._sb_corner_radius) + for button in self._buttons_dict.values(): + button.configure(corner_radius=self._sb_corner_radius) + + if "border_width" in kwargs: + self._sb_border_width = kwargs.pop("border_width") + for button in self._buttons_dict.values(): + button.configure(border_width=self._sb_border_width) + + if "bg_color" in kwargs: + super().configure(bg_color=kwargs.pop("bg_color")) + + if len(self._buttons_dict) > 0: + self._configure_button_corners_for_index(0) + if len(self._buttons_dict) > 1: + max_index = len(self._buttons_dict) - 1 + self._configure_button_corners_for_index(max_index) + + if "fg_color" in kwargs: + self._sb_fg_color = self._check_color_type(kwargs.pop("fg_color")) + for index, button in enumerate(self._buttons_dict.values()): + button.configure(border_color=self._sb_fg_color) + self._configure_button_corners_for_index(index) + + if "selected_color" in kwargs: + self._sb_selected_color = self._check_color_type(kwargs.pop("selected_color")) + if self._current_value in self._buttons_dict: + self._buttons_dict[self._current_value].configure(fg_color=self._sb_selected_color) + + if "selected_hover_color" in kwargs: + self._sb_selected_hover_color = self._check_color_type(kwargs.pop("selected_hover_color")) + if self._current_value in self._buttons_dict: + self._buttons_dict[self._current_value].configure(hover_color=self._sb_selected_hover_color) + + if "unselected_color" in kwargs: + self._sb_unselected_color = self._check_color_type(kwargs.pop("unselected_color")) + for value, button in self._buttons_dict.items(): + if value != self._current_value: + button.configure(fg_color=self._sb_unselected_color) + + if "unselected_hover_color" in kwargs: + self._sb_unselected_hover_color = self._check_color_type(kwargs.pop("unselected_hover_color")) + for value, button in self._buttons_dict.items(): + if value != self._current_value: + button.configure(hover_color=self._sb_unselected_hover_color) + + if "text_color" in kwargs: + self._sb_text_color = self._check_color_type(kwargs.pop("text_color")) + for button in self._buttons_dict.values(): + button.configure(text_color=self._sb_text_color) + + if "text_color_disabled" in kwargs: + self._sb_text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled")) + for button in self._buttons_dict.values(): + button.configure(text_color_disabled=self._sb_text_color_disabled) + + if "background_corner_colors" in kwargs: + self._background_corner_colors = kwargs.pop("background_corner_colors") + for i in range(len(self._buttons_dict)): + self._configure_button_corners_for_index(i) + + if "font" in kwargs: + self._font = kwargs.pop("font") + for button in self._buttons_dict.values(): + button.configure(font=self._font) + + if "values" in kwargs: + for button in self._buttons_dict.values(): + button.destroy() + self._buttons_dict.clear() + self._value_list = kwargs.pop("values") + + self._check_unique_values(self._value_list) + + if len(self._value_list) > 0: + self._create_buttons_from_values() + self._create_button_grid() + + if self._current_value in self._value_list: + self._select_button_by_value(self._current_value) + + if "variable" in kwargs: + if self._variable is not None: # remove old callback + self._variable.trace_remove("write", self._variable_callback_name) + + self._variable = kwargs.pop("variable") + + if self._variable is not None and self._variable != "": + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self.set(self._variable.get(), from_variable_callback=True) + else: + self._variable = None + + if "dynamic_resizing" in kwargs: + self._dynamic_resizing = kwargs.pop("dynamic_resizing") + if not self._dynamic_resizing: + self.grid_propagate(False) + else: + self.grid_propagate(True) + + if "command" in kwargs: + self._command = kwargs.pop("command") + + if "state" in kwargs: + self._state = kwargs.pop("state") + for button in self._buttons_dict.values(): + button.configure(state=self._state) + + check_kwargs_empty(kwargs, raise_error=True) + + def cget(self, attribute_name: str) -> any: + if attribute_name == "width": + return super().cget(attribute_name) + elif attribute_name == "height": + return super().cget(attribute_name) + elif attribute_name == "corner_radius": + return self._sb_corner_radius + elif attribute_name == "border_width": + return self._sb_border_width + + elif attribute_name == "bg_color": + return super().cget(attribute_name) + elif attribute_name == "fg_color": + return self._sb_fg_color + elif attribute_name == "selected_color": + return self._sb_selected_color + elif attribute_name == "selected_hover_color": + return self._sb_selected_hover_color + elif attribute_name == "unselected_color": + return self._sb_unselected_color + elif attribute_name == "unselected_hover_color": + return self._sb_unselected_hover_color + elif attribute_name == "text_color": + return self._sb_text_color + elif attribute_name == "text_color_disabled": + return self._sb_text_color_disabled + + elif attribute_name == "font": + return self._font + elif attribute_name == "values": + return copy.copy(self._value_list) + elif attribute_name == "variable": + return self._variable + elif attribute_name == "dynamic_resizing": + return self._dynamic_resizing + elif attribute_name == "command": + return self._command + + else: + raise ValueError(f"'{attribute_name}' is not a supported argument. Look at the documentation for supported arguments.") + + def set(self, value: str, from_variable_callback: bool = False, from_button_callback: bool = False): + if value == self._current_value: + return + elif value in self._buttons_dict: + self._select_button_by_value(value) + + if self._variable is not None and not from_variable_callback: + self._variable_callback_blocked = True + self._variable.set(value) + self._variable_callback_blocked = False + else: + if self._current_value in self._buttons_dict: + self._unselect_button_by_value(self._current_value) + self._current_value = value + + if self._variable is not None and not from_variable_callback: + self._variable_callback_blocked = True + self._variable.set(value) + self._variable_callback_blocked = False + + if from_button_callback: + if self._command is not None: + self._command(self._current_value) + + def get(self) -> str: + return self._current_value + + def index(self, value: str) -> int: + return self._value_list.index(value) + + def insert(self, index: int, value: str): + if value not in self._buttons_dict: + if value != "": + self._value_list.insert(index, value) + self._buttons_dict[value] = self._create_button(index, value) + + self._configure_button_corners_for_index(index) + if index > 0: + self._configure_button_corners_for_index(index - 1) + if index < len(self._buttons_dict) - 1: + self._configure_button_corners_for_index(index + 1) + + self._create_button_grid() + + if value == self._current_value: + self._select_button_by_value(self._current_value) + else: + raise ValueError(f"CTkSegmentedButton can not insert value ''") + else: + raise ValueError(f"CTkSegmentedButton can not insert value '{value}', already part of the values") + + def move(self, new_index: int, value: str): + if 0 <= new_index < len(self._value_list): + if value in self._buttons_dict: + self.delete(value) + self.insert(new_index, value) + else: + raise ValueError(f"CTkSegmentedButton has no value named '{value}'") + else: + raise ValueError(f"CTkSegmentedButton new_index {new_index} not in range of value list with len {len(self._value_list)}") + + def delete(self, value: str): + if value in self._buttons_dict: + self._buttons_dict[value].destroy() + self._buttons_dict.pop(value) + index_to_remove = self._get_index_by_value(value) + self._value_list.pop(index_to_remove) + + # removed index was outer right element + if index_to_remove == len(self._buttons_dict) and len(self._buttons_dict) > 0: + self._configure_button_corners_for_index(index_to_remove - 1) + + # removed index was outer left element + if index_to_remove == 0 and len(self._buttons_dict) > 0: + self._configure_button_corners_for_index(0) + + #if index_to_remove <= len(self._buttons_dict) - 1: + # self._configure_button_corners_for_index(index_to_remove) + + self._create_button_grid() + else: + raise ValueError(f"CTkSegmentedButton does not contain value '{value}'") + + def bind(self, sequence=None, command=None, add=None): + raise NotImplementedError + + def unbind(self, sequence=None, funcid=None): + raise NotImplementedError + diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_slider.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_slider.py new file mode 100644 index 0000000..7aa03ee --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_slider.py @@ -0,0 +1,413 @@ +import tkinter +import sys +from typing import Union, Tuple, Callable, Optional, Any + +from .core_rendering import CTkCanvas +from .theme import ThemeManager +from .core_rendering import DrawEngine +from .core_widget_classes import CTkBaseClass + + +class CTkSlider(CTkBaseClass): + """ + Slider with rounded corners, border, number of steps, variable support, vertical orientation. + For detailed information check out the documentation. + """ + + def __init__(self, + master: Any, + width: Optional[int] = None, + height: Optional[int] = None, + corner_radius: Optional[int] = None, + button_corner_radius: Optional[int] = None, + border_width: Optional[int] = None, + button_length: Optional[int] = None, + + bg_color: Union[str, Tuple[str, str]] = "transparent", + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + border_color: Union[str, Tuple[str, str]] = "transparent", + progress_color: Optional[Union[str, Tuple[str, str]]] = None, + button_color: Optional[Union[str, Tuple[str, str]]] = None, + button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + + from_: int = 0, + to: int = 1, + state: str = "normal", + number_of_steps: Union[int, None] = None, + hover: bool = True, + command: Union[Callable[[float], Any], None] = None, + variable: Union[tkinter.Variable, None] = None, + orientation: str = "horizontal", + **kwargs): + + # set default dimensions according to orientation + if width is None: + if orientation.lower() == "vertical": + width = 16 + else: + width = 200 + if height is None: + if orientation.lower() == "vertical": + height = 200 + else: + height = 16 + + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass + super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) + + # color + self._border_color = self._check_color_type(border_color, transparency=True) + self._fg_color = ThemeManager.theme["CTkSlider"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) + self._progress_color = ThemeManager.theme["CTkSlider"]["progress_color"] if progress_color is None else self._check_color_type(progress_color, transparency=True) + self._button_color = ThemeManager.theme["CTkSlider"]["button_color"] if button_color is None else self._check_color_type(button_color) + self._button_hover_color = ThemeManager.theme["CTkSlider"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color) + + # shape + self._corner_radius = ThemeManager.theme["CTkSlider"]["corner_radius"] if corner_radius is None else corner_radius + self._button_corner_radius = ThemeManager.theme["CTkSlider"]["button_corner_radius"] if button_corner_radius is None else button_corner_radius + self._border_width = ThemeManager.theme["CTkSlider"]["border_width"] if border_width is None else border_width + self._button_length = ThemeManager.theme["CTkSlider"]["button_length"] if button_length is None else button_length + self._value: float = 0.5 # initial value of slider in percent + self._orientation = orientation + self._hover_state: bool = False + self._hover = hover + self._from_ = from_ + self._to = to + self._number_of_steps = number_of_steps + self._output_value = self._from_ + (self._value * (self._to - self._from_)) + + if self._corner_radius < self._button_corner_radius: + self._corner_radius = self._button_corner_radius + + # callback and control variables + self._command = command + self._variable: tkinter.Variable = variable + self._variable_callback_blocked: bool = False + self._variable_callback_name: Union[bool, None] = None + self._state = state + + self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) + + self._canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._canvas.grid(column=0, row=0, rowspan=1, columnspan=1, sticky="nswe") + self._draw_engine = DrawEngine(self._canvas) + + self._create_bindings() + self._set_cursor() + self._draw() # initial draw + + if self._variable is not None: + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self._variable_callback_blocked = True + self.set(self._variable.get(), from_variable_callback=True) + self._variable_callback_blocked = False + + def _create_bindings(self, sequence: Optional[str] = None): + """ set necessary bindings for functionality of widget, will overwrite other bindings """ + if sequence is None or sequence == "": + self._canvas.bind("", self._on_enter) + if sequence is None or sequence == "": + self._canvas.bind("", self._on_leave) + if sequence is None or sequence == "": + self._canvas.bind("", self._clicked) + if sequence is None or sequence == "": + self._canvas.bind("", self._clicked) + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw(no_color_updates=True) + + def _set_dimensions(self, width=None, height=None): + super()._set_dimensions(width, height) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw() + + def destroy(self): + # remove variable_callback from variable callbacks if variable exists + if self._variable is not None: + self._variable.trace_remove("write", self._variable_callback_name) + + super().destroy() + + def _set_cursor(self): + if self._state == "normal" and self._cursor_manipulation_enabled: + if sys.platform == "darwin": + self.configure(cursor="pointinghand") + elif sys.platform.startswith("win"): + self.configure(cursor="hand2") + + elif self._state == "disabled" and self._cursor_manipulation_enabled: + if sys.platform == "darwin": + self.configure(cursor="arrow") + elif sys.platform.startswith("win"): + self.configure(cursor="arrow") + + def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + + if self._orientation.lower() == "horizontal": + orientation = "w" + elif self._orientation.lower() == "vertical": + orientation = "s" + else: + orientation = "w" + + requires_recoloring = self._draw_engine.draw_rounded_slider_with_border_and_button(self._apply_widget_scaling(self._current_width), + self._apply_widget_scaling(self._current_height), + self._apply_widget_scaling(self._corner_radius), + self._apply_widget_scaling(self._border_width), + self._apply_widget_scaling(self._button_length), + self._apply_widget_scaling(self._button_corner_radius), + self._value, orientation) + + if no_color_updates is False or requires_recoloring: + self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + + if self._border_color == "transparent": + self._canvas.itemconfig("border_parts", fill=self._apply_appearance_mode(self._bg_color), + outline=self._apply_appearance_mode(self._bg_color)) + else: + self._canvas.itemconfig("border_parts", fill=self._apply_appearance_mode(self._border_color), + outline=self._apply_appearance_mode(self._border_color)) + + self._canvas.itemconfig("inner_parts", fill=self._apply_appearance_mode(self._fg_color), + outline=self._apply_appearance_mode(self._fg_color)) + + if self._progress_color == "transparent": + self._canvas.itemconfig("progress_parts", fill=self._apply_appearance_mode(self._fg_color), + outline=self._apply_appearance_mode(self._fg_color)) + else: + self._canvas.itemconfig("progress_parts", fill=self._apply_appearance_mode(self._progress_color), + outline=self._apply_appearance_mode(self._progress_color)) + + if self._hover_state is True: + self._canvas.itemconfig("slider_parts", + fill=self._apply_appearance_mode(self._button_hover_color), + outline=self._apply_appearance_mode(self._button_hover_color)) + else: + self._canvas.itemconfig("slider_parts", + fill=self._apply_appearance_mode(self._button_color), + outline=self._apply_appearance_mode(self._button_color)) + + def configure(self, require_redraw=False, **kwargs): + if "corner_radius" in kwargs: + self._corner_radius = kwargs.pop("corner_radius") + require_redraw = True + + if "button_corner_radius" in kwargs: + self._button_corner_radius = kwargs.pop("button_corner_radius") + require_redraw = True + + if "border_width" in kwargs: + self._border_width = kwargs.pop("border_width") + require_redraw = True + + if "button_length" in kwargs: + self._button_length = kwargs.pop("button_length") + require_redraw = True + + if "fg_color" in kwargs: + self._fg_color = self._check_color_type(kwargs.pop("fg_color")) + require_redraw = True + + if "border_color" in kwargs: + self._border_color = self._check_color_type(kwargs.pop("border_color"), transparency=True) + require_redraw = True + + if "progress_color" in kwargs: + self._progress_color = self._check_color_type(kwargs.pop("progress_color"), transparency=True) + require_redraw = True + + if "button_color" in kwargs: + self._button_color = self._check_color_type(kwargs.pop("button_color")) + require_redraw = True + + if "button_hover_color" in kwargs: + self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color")) + require_redraw = True + + if "from_" in kwargs: + self._from_ = kwargs.pop("from_") + + if "to" in kwargs: + self._to = kwargs.pop("to") + + if "state" in kwargs: + self._state = kwargs.pop("state") + self._set_cursor() + require_redraw = True + + if "number_of_steps" in kwargs: + self._number_of_steps = kwargs.pop("number_of_steps") + + if "hover" in kwargs: + self._hover = kwargs.pop("hover") + + if "command" in kwargs: + self._command = kwargs.pop("command") + + if "variable" in kwargs: + if self._variable is not None: + self._variable.trace_remove("write", self._variable_callback_name) + + self._variable = kwargs.pop("variable") + + if self._variable is not None and self._variable != "": + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self.set(self._variable.get(), from_variable_callback=True) + else: + self._variable = None + + if "orientation" in kwargs: + self._orientation = kwargs.pop("orientation") + require_redraw = True + + super().configure(require_redraw=require_redraw, **kwargs) + + def cget(self, attribute_name: str) -> any: + if attribute_name == "corner_radius": + return self._corner_radius + elif attribute_name == "button_corner_radius": + return self._button_corner_radius + elif attribute_name == "border_width": + return self._border_width + elif attribute_name == "button_length": + return self._button_length + + elif attribute_name == "fg_color": + return self._fg_color + elif attribute_name == "border_color": + return self._border_color + elif attribute_name == "progress_color": + return self._progress_color + elif attribute_name == "button_color": + return self._button_color + elif attribute_name == "button_hover_color": + return self._button_hover_color + + elif attribute_name == "from_": + return self._from_ + elif attribute_name == "to": + return self._to + elif attribute_name == "state": + return self._state + elif attribute_name == "number_of_steps": + return self._number_of_steps + elif attribute_name == "hover": + return self._hover + elif attribute_name == "command": + return self._command + elif attribute_name == "variable": + return self._variable + elif attribute_name == "orientation": + return self._orientation + + else: + return super().cget(attribute_name) + + def _clicked(self, event=None): + if self._state == "normal": + if self._orientation.lower() == "horizontal": + self._value = self._reverse_widget_scaling(event.x / self._current_width) + else: + self._value = 1 - self._reverse_widget_scaling(event.y / self._current_height) + + if self._value > 1: + self._value = 1 + if self._value < 0: + self._value = 0 + + self._output_value = self._round_to_step_size(self._from_ + (self._value * (self._to - self._from_))) + self._value = (self._output_value - self._from_) / (self._to - self._from_) + + self._draw(no_color_updates=False) + + if self._variable is not None: + self._variable_callback_blocked = True + self._variable.set(round(self._output_value) if isinstance(self._variable, tkinter.IntVar) else self._output_value) + self._variable_callback_blocked = False + + if self._command is not None: + self._command(self._output_value) + + def _on_enter(self, event=0): + if self._hover is True and self._state == "normal": + self._hover_state = True + self._canvas.itemconfig("slider_parts", + fill=self._apply_appearance_mode(self._button_hover_color), + outline=self._apply_appearance_mode(self._button_hover_color)) + + def _on_leave(self, event=0): + self._hover_state = False + self._canvas.itemconfig("slider_parts", + fill=self._apply_appearance_mode(self._button_color), + outline=self._apply_appearance_mode(self._button_color)) + + def _round_to_step_size(self, value) -> float: + if self._number_of_steps is not None: + step_size = (self._to - self._from_) / self._number_of_steps + value = self._to - (round((self._to - value) / step_size) * step_size) + return value + else: + return value + + def get(self) -> float: + return self._output_value + + def set(self, output_value, from_variable_callback=False): + if self._from_ < self._to: + if output_value > self._to: + output_value = self._to + elif output_value < self._from_: + output_value = self._from_ + else: + if output_value < self._to: + output_value = self._to + elif output_value > self._from_: + output_value = self._from_ + + self._output_value = self._round_to_step_size(output_value) + self._value = (self._output_value - self._from_) / (self._to - self._from_) + + self._draw(no_color_updates=False) + + if self._variable is not None and not from_variable_callback: + self._variable_callback_blocked = True + self._variable.set(round(self._output_value) if isinstance(self._variable, tkinter.IntVar) else self._output_value) + self._variable_callback_blocked = False + + def _variable_callback(self, var_name, index, mode): + if not self._variable_callback_blocked: + self.set(self._variable.get(), from_variable_callback=True) + + def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True): + """ called on the tkinter.Canvas """ + if not (add == "+" or add is True): + raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") + self._canvas.bind(sequence, command, add=True) + + def unbind(self, sequence: str = None, funcid: str = None): + """ called on the tkinter.Label and tkinter.Canvas """ + if funcid is not None: + raise ValueError("'funcid' argument can only be None, because there is a bug in" + + " tkinter and its not clear whether the internal callbacks will be unbinded or not") + self._canvas.unbind(sequence, None) + self._create_bindings(sequence=sequence) # restore internal callbacks for sequence + + def focus(self): + return self._canvas.focus() + + def focus_set(self): + return self._canvas.focus_set() + + def focus_force(self): + return self._canvas.focus_force() diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_switch.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_switch.py new file mode 100644 index 0000000..155c174 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_switch.py @@ -0,0 +1,483 @@ +import tkinter +import sys +from typing import Union, Tuple, Callable, Optional, Any + +from .core_rendering import CTkCanvas +from .theme import ThemeManager +from .core_rendering import DrawEngine +from .core_widget_classes import CTkBaseClass +from .font import CTkFont + + +class CTkSwitch(CTkBaseClass): + """ + Switch with rounded corners, border, label, command, variable support. + For detailed information check out the documentation. + """ + + def __init__(self, + master: Any, + width: int = 100, + height: int = 24, + switch_width: int = 36, + switch_height: int = 18, + corner_radius: Optional[int] = None, + border_width: Optional[int] = None, + button_length: Optional[int] = None, + + bg_color: Union[str, Tuple[str, str]] = "transparent", + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + border_color: Union[str, Tuple[str, str]] = "transparent", + progress_color: Optional[Union[str, Tuple[str, str]]] = None, + button_color: Optional[Union[str, Tuple[str, str]]] = None, + button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None, + + text: str = "CTkSwitch", + font: Optional[Union[tuple, CTkFont]] = None, + textvariable: Union[tkinter.Variable, None] = None, + onvalue: Union[int, str] = 1, + offvalue: Union[int, str] = 0, + variable: Union[tkinter.Variable, None] = None, + hover: bool = True, + command: Union[Callable, Any] = None, + state: str = tkinter.NORMAL, + **kwargs): + + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass + super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) + + # dimensions + self._switch_width = switch_width + self._switch_height = switch_height + + # color + self._border_color = self._check_color_type(border_color, transparency=True) + self._fg_color = ThemeManager.theme["CTkSwitch"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) + self._progress_color = ThemeManager.theme["CTkSwitch"]["progress_color"] if progress_color is None else self._check_color_type(progress_color, transparency=True) + self._button_color = ThemeManager.theme["CTkSwitch"]["button_color"] if button_color is None else self._check_color_type(button_color) + self._button_hover_color = ThemeManager.theme["CTkSwitch"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color) + self._text_color = ThemeManager.theme["CTkSwitch"]["text_color"] if text_color is None else self._check_color_type(text_color) + self._text_color_disabled = ThemeManager.theme["CTkSwitch"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled) + + # text + self._text = text + self._text_label = None + + # font + self._font = CTkFont() if font is None else self._check_font_type(font) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + # shape + self._corner_radius = ThemeManager.theme["CTkSwitch"]["corner_radius"] if corner_radius is None else corner_radius + self._border_width = ThemeManager.theme["CTkSwitch"]["border_width"] if border_width is None else border_width + self._button_length = ThemeManager.theme["CTkSwitch"]["button_length"] if button_length is None else button_length + self._hover_state: bool = False + self._check_state: bool = False # True if switch is activated + self._hover = hover + self._state = state + self._onvalue = onvalue + self._offvalue = offvalue + + # callback and control variables + self._command = command + self._variable = variable + self._variable_callback_blocked = False + self._variable_callback_name = None + self._textvariable = textvariable + + # configure grid system (3x1) + self.grid_columnconfigure(0, weight=0) + self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6)) + self.grid_columnconfigure(2, weight=1) + self.grid_rowconfigure(0, weight=1) + + self._bg_canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._current_width), + height=self._apply_widget_scaling(self._current_height)) + self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe") + + self._canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._switch_width), + height=self._apply_widget_scaling(self._switch_height)) + self._canvas.grid(row=0, column=0, sticky="") + self._draw_engine = DrawEngine(self._canvas) + + self._text_label = tkinter.Label(master=self, + bd=0, + padx=0, + pady=0, + text=self._text, + justify=tkinter.LEFT, + font=self._apply_font_scaling(self._font), + textvariable=self._textvariable) + self._text_label.grid(row=0, column=2, sticky="w") + self._text_label["anchor"] = "w" + + if self._variable is not None and self._variable != "": + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self._check_state = True if self._variable.get() == self._onvalue else False + + self._create_bindings() + self._set_cursor() + self._draw() # initial draw + + def _create_bindings(self, sequence: Optional[str] = None): + """ set necessary bindings for functionality of widget, will overwrite other bindings """ + if sequence is None or sequence == "": + self._canvas.bind("", self._on_enter) + self._text_label.bind("", self._on_enter) + if sequence is None or sequence == "": + self._canvas.bind("", self._on_leave) + self._text_label.bind("", self._on_leave) + if sequence is None or sequence == "": + self._canvas.bind("", self.toggle) + self._text_label.bind("", self.toggle) + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6)) + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._canvas.configure(width=self._apply_widget_scaling(self._switch_width), + height=self._apply_widget_scaling(self._switch_height)) + self._draw(no_color_updates=True) + + def _set_dimensions(self, width: int = None, height: int = None): + super()._set_dimensions(width, height) + + self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + # Workaround to force grid to be resized when text changes size. + # Otherwise grid will lag and only resizes if other mouse action occurs. + self._bg_canvas.grid_forget() + self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe") + + def destroy(self): + # remove variable_callback from variable callbacks if variable exists + if self._variable is not None: + self._variable.trace_remove("write", self._variable_callback_name) + + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + + super().destroy() + + def _set_cursor(self): + if self._cursor_manipulation_enabled: + if self._state == tkinter.DISABLED: + if sys.platform == "darwin": + self._canvas.configure(cursor="arrow") + if self._text_label is not None: + self._text_label.configure(cursor="arrow") + elif sys.platform.startswith("win"): + self._canvas.configure(cursor="arrow") + if self._text_label is not None: + self._text_label.configure(cursor="arrow") + + elif self._state == tkinter.NORMAL: + if sys.platform == "darwin": + self._canvas.configure(cursor="pointinghand") + if self._text_label is not None: + self._text_label.configure(cursor="pointinghand") + elif sys.platform.startswith("win"): + self._canvas.configure(cursor="hand2") + if self._text_label is not None: + self._text_label.configure(cursor="hand2") + + def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + + if self._check_state is True: + requires_recoloring = self._draw_engine.draw_rounded_slider_with_border_and_button(self._apply_widget_scaling(self._switch_width), + self._apply_widget_scaling(self._switch_height), + self._apply_widget_scaling(self._corner_radius), + self._apply_widget_scaling(self._border_width), + self._apply_widget_scaling(self._button_length), + self._apply_widget_scaling(self._corner_radius), + 1, "w") + else: + requires_recoloring = self._draw_engine.draw_rounded_slider_with_border_and_button(self._apply_widget_scaling(self._switch_width), + self._apply_widget_scaling(self._switch_height), + self._apply_widget_scaling(self._corner_radius), + self._apply_widget_scaling(self._border_width), + self._apply_widget_scaling(self._button_length), + self._apply_widget_scaling(self._corner_radius), + 0, "w") + + if no_color_updates is False or requires_recoloring: + self._bg_canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + + if self._border_color == "transparent": + self._canvas.itemconfig("border_parts", + fill=self._apply_appearance_mode(self._bg_color), + outline=self._apply_appearance_mode(self._bg_color)) + else: + self._canvas.itemconfig("border_parts", + fill=self._apply_appearance_mode(self._border_color), + outline=self._apply_appearance_mode(self._border_color)) + + self._canvas.itemconfig("inner_parts", + fill=self._apply_appearance_mode(self._fg_color), + outline=self._apply_appearance_mode(self._fg_color)) + + if self._progress_color == "transparent": + self._canvas.itemconfig("progress_parts", + fill=self._apply_appearance_mode(self._fg_color), + outline=self._apply_appearance_mode(self._fg_color)) + else: + self._canvas.itemconfig("progress_parts", + fill=self._apply_appearance_mode(self._progress_color), + outline=self._apply_appearance_mode(self._progress_color)) + + self._canvas.itemconfig("slider_parts", + fill=self._apply_appearance_mode(self._button_color), + outline=self._apply_appearance_mode(self._button_color)) + + if self._state == tkinter.DISABLED: + self._text_label.configure(fg=(self._apply_appearance_mode(self._text_color_disabled))) + else: + self._text_label.configure(fg=self._apply_appearance_mode(self._text_color)) + + self._text_label.configure(bg=self._apply_appearance_mode(self._bg_color)) + + def configure(self, require_redraw=False, **kwargs): + if "corner_radius" in kwargs: + self._corner_radius = kwargs.pop("corner_radius") + require_redraw = True + + if "border_width" in kwargs: + self._border_width = kwargs.pop("border_width") + require_redraw = True + + if "button_length" in kwargs: + self._button_length = kwargs.pop("button_length") + require_redraw = True + + if "switch_width" in kwargs: + self._switch_width = kwargs.pop("switch_width") + self._canvas.configure(width=self._apply_widget_scaling(self._switch_width)) + require_redraw = True + + if "switch_height" in kwargs: + self._switch_height = kwargs.pop("switch_height") + self._canvas.configure(height=self._apply_widget_scaling(self._switch_height)) + require_redraw = True + + if "text" in kwargs: + self._text = kwargs.pop("text") + self._text_label.configure(text=self._text) + + if "font" in kwargs: + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + self._font = self._check_font_type(kwargs.pop("font")) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + self._update_font() + + if "state" in kwargs: + self._state = kwargs.pop("state") + self._set_cursor() + require_redraw = True + + if "fg_color" in kwargs: + self._fg_color = self._check_color_type(kwargs.pop("fg_color")) + require_redraw = True + + if "border_color" in kwargs: + self._border_color = self._check_color_type(kwargs.pop("border_color"), transparency=True) + require_redraw = True + + if "progress_color" in kwargs: + self._progress_color = self._check_color_type(kwargs.pop("progress_color"), transparency=True) + require_redraw = True + + if "button_color" in kwargs: + self._button_color = self._check_color_type(kwargs.pop("button_color")) + require_redraw = True + + if "button_hover_color" in kwargs: + self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color")) + require_redraw = True + + if "text_color" in kwargs: + self._text_color = self._check_color_type(kwargs.pop("text_color")) + require_redraw = True + + if "text_color_disabled" in kwargs: + self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled")) + require_redraw = True + + if "hover" in kwargs: + self._hover = kwargs.pop("hover") + + if "command" in kwargs: + self._command = kwargs.pop("command") + + if "textvariable" in kwargs: + self._textvariable = kwargs.pop("textvariable") + self._text_label.configure(textvariable=self._textvariable) + + if "variable" in kwargs: + if self._variable is not None and self._variable != "": + self._variable.trace_remove("write", self._variable_callback_name) + + self._variable = kwargs.pop("variable") + + if self._variable is not None and self._variable != "": + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self._check_state = True if self._variable.get() == self._onvalue else False + require_redraw = True + + super().configure(require_redraw=require_redraw, **kwargs) + + def cget(self, attribute_name: str) -> any: + if attribute_name == "corner_radius": + return self._corner_radius + elif attribute_name == "border_width": + return self._border_width + elif attribute_name == "button_length": + return self._button_length + elif attribute_name == "switch_width": + return self._switch_width + elif attribute_name == "switch_height": + return self._switch_height + + elif attribute_name == "fg_color": + return self._fg_color + elif attribute_name == "border_color": + return self._border_color + elif attribute_name == "progress_color": + return self._progress_color + elif attribute_name == "button_color": + return self._button_color + elif attribute_name == "button_hover_color": + return self._button_hover_color + elif attribute_name == "text_color": + return self._text_color + elif attribute_name == "text_color_disabled": + return self._text_color_disabled + + elif attribute_name == "text": + return self._text + elif attribute_name == "font": + return self._font + elif attribute_name == "textvariable": + return self._textvariable + elif attribute_name == "onvalue": + return self._onvalue + elif attribute_name == "offvalue": + return self._offvalue + elif attribute_name == "variable": + return self._variable + elif attribute_name == "hover": + return self._hover + elif attribute_name == "command": + return self._command + elif attribute_name == "state": + return self._state + + else: + return super().cget(attribute_name) + + def toggle(self, event=None): + if self._state is not tkinter.DISABLED: + if self._check_state is True: + self._check_state = False + else: + self._check_state = True + + self._draw(no_color_updates=True) + + if self._variable is not None: + self._variable_callback_blocked = True + self._variable.set(self._onvalue if self._check_state is True else self._offvalue) + self._variable_callback_blocked = False + + if self._command is not None: + self._command() + + def select(self, from_variable_callback=False): + if self._state is not tkinter.DISABLED or from_variable_callback: + self._check_state = True + + self._draw(no_color_updates=True) + + if self._variable is not None and not from_variable_callback: + self._variable_callback_blocked = True + self._variable.set(self._onvalue) + self._variable_callback_blocked = False + + def deselect(self, from_variable_callback=False): + if self._state is not tkinter.DISABLED or from_variable_callback: + self._check_state = False + + self._draw(no_color_updates=True) + + if self._variable is not None and not from_variable_callback: + self._variable_callback_blocked = True + self._variable.set(self._offvalue) + self._variable_callback_blocked = False + + def get(self) -> Union[int, str]: + return self._onvalue if self._check_state is True else self._offvalue + + def _on_enter(self, event=0): + if self._hover is True and self._state == "normal": + self._hover_state = True + self._canvas.itemconfig("slider_parts", + fill=self._apply_appearance_mode(self._button_hover_color), + outline=self._apply_appearance_mode(self._button_hover_color)) + + def _on_leave(self, event=0): + self._hover_state = False + self._canvas.itemconfig("slider_parts", + fill=self._apply_appearance_mode(self._button_color), + outline=self._apply_appearance_mode(self._button_color)) + + def _variable_callback(self, var_name, index, mode): + if not self._variable_callback_blocked: + if self._variable.get() == self._onvalue: + self.select(from_variable_callback=True) + elif self._variable.get() == self._offvalue: + self.deselect(from_variable_callback=True) + + def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True): + """ called on the tkinter.Canvas """ + if not (add == "+" or add is True): + raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") + self._canvas.bind(sequence, command, add=True) + self._text_label.bind(sequence, command, add=True) + + def unbind(self, sequence: str = None, funcid: str = None): + """ called on the tkinter.Label and tkinter.Canvas """ + if funcid is not None: + raise ValueError("'funcid' argument can only be None, because there is a bug in" + + " tkinter and its not clear whether the internal callbacks will be unbinded or not") + self._canvas.unbind(sequence, None) + self._text_label.unbind(sequence, None) + self._create_bindings(sequence=sequence) # restore internal callbacks for sequence + + def focus(self): + return self._text_label.focus() + + def focus_set(self): + return self._text_label.focus_set() + + def focus_force(self): + return self._text_label.focus_force() diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_tabview.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_tabview.py new file mode 100644 index 0000000..3b2ea5b --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_tabview.py @@ -0,0 +1,433 @@ +import tkinter +from typing import Union, Tuple, Dict, List, Callable, Optional, Any + +from .theme import ThemeManager +from .ctk_frame import CTkFrame +from .core_rendering import CTkCanvas +from .core_rendering import DrawEngine +from .core_widget_classes import CTkBaseClass +from .ctk_segmented_button import CTkSegmentedButton + + +class CTkTabview(CTkBaseClass): + """ + Tabview... + For detailed information check out the documentation. + """ + + _outer_spacing: int = 10 # px on top or below the button + _outer_button_overhang: int = 8 # px + _button_height: int = 26 + _segmented_button_border_width: int = 3 + + def __init__(self, + master: Any, + width: int = 300, + height: int = 250, + corner_radius: Optional[int] = None, + border_width: Optional[int] = None, + + bg_color: Union[str, Tuple[str, str]] = "transparent", + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + border_color: Optional[Union[str, Tuple[str, str]]] = None, + + segmented_button_fg_color: Optional[Union[str, Tuple[str, str]]] = None, + segmented_button_selected_color: Optional[Union[str, Tuple[str, str]]] = None, + segmented_button_selected_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + segmented_button_unselected_color: Optional[Union[str, Tuple[str, str]]] = None, + segmented_button_unselected_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + + text_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None, + + command: Union[Callable, Any] = None, + anchor: str = "center", + state: str = "normal", + **kwargs): + + # transfer some functionality to CTkFrame + super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) + + # color + self._border_color = ThemeManager.theme["CTkFrame"]["border_color"] if border_color is None else self._check_color_type(border_color) + + # determine fg_color of frame + if fg_color is None: + if isinstance(self.master, (CTkFrame, CTkTabview)): + if self.master.cget("fg_color") == ThemeManager.theme["CTkFrame"]["fg_color"]: + self._fg_color = ThemeManager.theme["CTkFrame"]["top_fg_color"] + else: + self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"] + else: + self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"] + else: + self._fg_color = self._check_color_type(fg_color, transparency=True) + + # shape + self._corner_radius = ThemeManager.theme["CTkFrame"]["corner_radius"] if corner_radius is None else corner_radius + self._border_width = ThemeManager.theme["CTkFrame"]["border_width"] if border_width is None else border_width + self._anchor = anchor + + self._canvas = CTkCanvas(master=self, + bg=self._apply_appearance_mode(self._bg_color), + highlightthickness=0, + width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height - self._outer_spacing - self._outer_button_overhang)) + self._draw_engine = DrawEngine(self._canvas) + + self._segmented_button = CTkSegmentedButton(self, + values=[], + height=self._button_height, + fg_color=segmented_button_fg_color, + selected_color=segmented_button_selected_color, + selected_hover_color=segmented_button_selected_hover_color, + unselected_color=segmented_button_unselected_color, + unselected_hover_color=segmented_button_unselected_hover_color, + text_color=text_color, + text_color_disabled=text_color_disabled, + corner_radius=corner_radius, + border_width=self._segmented_button_border_width, + command=self._segmented_button_callback, + state=state) + self._configure_segmented_button_background_corners() + self._configure_grid() + self._set_grid_canvas() + + self._tab_dict: Dict[str, CTkFrame] = {} + self._name_list: List[str] = [] # list of unique tab names in order of tabs + self._current_name: str = "" + self._command = command + + self._draw() + + def _segmented_button_callback(self, selected_name): + self._tab_dict[self._current_name].grid_forget() + self._current_name = selected_name + self._set_grid_current_tab() + + if self._command is not None: + self._command() + + def winfo_children(self) -> List[any]: + """ + winfo_children of CTkTabview without canvas and segmented button widgets, + because it's not a child but part of the CTkTabview itself + """ + + child_widgets = super().winfo_children() + try: + child_widgets.remove(self._canvas) + child_widgets.remove(self._segmented_button) + return child_widgets + except ValueError: + return child_widgets + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height - self._outer_spacing - self._outer_button_overhang)) + self._configure_grid() + self._draw(no_color_updates=True) + + def _set_dimensions(self, width=None, height=None): + super()._set_dimensions(width, height) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height - self._outer_spacing - self._outer_button_overhang)) + self._draw() + + def _configure_segmented_button_background_corners(self): + """ needs to be called for changes in fg_color, bg_color """ + + if self._fg_color == "transparent": + self._segmented_button.configure(background_corner_colors=(self._bg_color, self._bg_color, self._bg_color, self._bg_color)) + else: + if self._anchor.lower() in ("center", "w", "nw", "n", "ne", "e", "e"): + self._segmented_button.configure(background_corner_colors=(self._bg_color, self._bg_color, self._fg_color, self._fg_color)) + else: + self._segmented_button.configure(background_corner_colors=(self._fg_color, self._fg_color, self._bg_color, self._bg_color)) + + def _configure_grid(self): + """ create 3 x 4 grid system """ + + if self._anchor.lower() in ("center", "w", "nw", "n", "ne", "e", "e"): + self.grid_rowconfigure(0, weight=0, minsize=self._apply_widget_scaling(self._outer_spacing)) + self.grid_rowconfigure(1, weight=0, minsize=self._apply_widget_scaling(self._outer_button_overhang)) + self.grid_rowconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._button_height - self._outer_button_overhang)) + self.grid_rowconfigure(3, weight=1) + else: + self.grid_rowconfigure(0, weight=1) + self.grid_rowconfigure(1, weight=0, minsize=self._apply_widget_scaling(self._button_height - self._outer_button_overhang)) + self.grid_rowconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._outer_button_overhang)) + self.grid_rowconfigure(3, weight=0, minsize=self._apply_widget_scaling(self._outer_spacing)) + + self.grid_columnconfigure(0, weight=1) + + def _set_grid_canvas(self): + if self._anchor.lower() in ("center", "w", "nw", "n", "ne", "e", "e"): + self._canvas.grid(row=2, rowspan=2, column=0, columnspan=1, sticky="nsew") + else: + self._canvas.grid(row=0, rowspan=2, column=0, columnspan=1, sticky="nsew") + + def _set_grid_segmented_button(self): + """ needs to be called for changes in corner_radius, anchor """ + + if self._anchor.lower() in ("center", "n", "s"): + self._segmented_button.grid(row=1, rowspan=2, column=0, columnspan=1, padx=self._apply_widget_scaling(self._corner_radius), sticky="ns") + elif self._anchor.lower() in ("nw", "w", "sw"): + self._segmented_button.grid(row=1, rowspan=2, column=0, columnspan=1, padx=self._apply_widget_scaling(self._corner_radius), sticky="nsw") + elif self._anchor.lower() in ("ne", "e", "se"): + self._segmented_button.grid(row=1, rowspan=2, column=0, columnspan=1, padx=self._apply_widget_scaling(self._corner_radius), sticky="nse") + + def _set_grid_current_tab(self): + """ needs to be called for changes in corner_radius, border_width """ + if self._anchor.lower() in ("center", "w", "nw", "n", "ne", "e", "e"): + self._tab_dict[self._current_name].grid(row=3, column=0, sticky="nsew", + padx=self._apply_widget_scaling(max(self._corner_radius, self._border_width)), + pady=self._apply_widget_scaling(max(self._corner_radius, self._border_width))) + else: + self._tab_dict[self._current_name].grid(row=0, column=0, sticky="nsew", + padx=self._apply_widget_scaling(max(self._corner_radius, self._border_width)), + pady=self._apply_widget_scaling(max(self._corner_radius, self._border_width))) + + def _grid_forget_all_tabs(self, exclude_name=None): + for name, frame in self._tab_dict.items(): + if name != exclude_name: + frame.grid_forget() + + def _create_tab(self) -> CTkFrame: + new_tab = CTkFrame(self, + height=0, + width=0, + border_width=0, + corner_radius=0) + + if self._fg_color == "transparent": + new_tab.configure(fg_color=self._apply_appearance_mode(self._bg_color), + bg_color=self._apply_appearance_mode(self._bg_color)) + else: + new_tab.configure(fg_color=self._apply_appearance_mode(self._fg_color), + bg_color=self._apply_appearance_mode(self._fg_color)) + + return new_tab + + def _draw(self, no_color_updates: bool = False): + super()._draw(no_color_updates) + + if not self._canvas.winfo_exists(): + return + + requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width), + self._apply_widget_scaling(self._current_height - self._outer_spacing - self._outer_button_overhang), + self._apply_widget_scaling(self._corner_radius), + self._apply_widget_scaling(self._border_width)) + + if no_color_updates is False or requires_recoloring: + if self._fg_color == "transparent": + self._canvas.itemconfig("inner_parts", + fill=self._apply_appearance_mode(self._bg_color), + outline=self._apply_appearance_mode(self._bg_color)) + for tab in self._tab_dict.values(): + tab.configure(fg_color=self._apply_appearance_mode(self._bg_color), + bg_color=self._apply_appearance_mode(self._bg_color)) + else: + self._canvas.itemconfig("inner_parts", + fill=self._apply_appearance_mode(self._fg_color), + outline=self._apply_appearance_mode(self._fg_color)) + for tab in self._tab_dict.values(): + tab.configure(fg_color=self._apply_appearance_mode(self._fg_color), + bg_color=self._apply_appearance_mode(self._fg_color)) + + self._canvas.itemconfig("border_parts", + fill=self._apply_appearance_mode(self._border_color), + outline=self._apply_appearance_mode(self._border_color)) + self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._bg_color)) # configure bg color of tkinter.Frame, cause canvas does not fill frame + + def configure(self, require_redraw=False, **kwargs): + if "corner_radius" in kwargs: + self._corner_radius = kwargs.pop("corner_radius") + self._set_grid_segmented_button() + self._set_grid_current_tab() + self._set_grid_canvas() + self._configure_segmented_button_background_corners() + self._segmented_button.configure(corner_radius=self._corner_radius) + if "border_width" in kwargs: + self._border_width = kwargs.pop("border_width") + require_redraw = True + if "fg_color" in kwargs: + self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True) + self._configure_segmented_button_background_corners() + require_redraw = True + if "border_color" in kwargs: + self._border_color = self._check_color_type(kwargs.pop("border_color")) + require_redraw = True + if "segmented_button_fg_color" in kwargs: + self._segmented_button.configure(fg_color=kwargs.pop("segmented_button_fg_color")) + if "segmented_button_selected_color" in kwargs: + self._segmented_button.configure(selected_color=kwargs.pop("segmented_button_selected_color")) + if "segmented_button_selected_hover_color" in kwargs: + self._segmented_button.configure(selected_hover_color=kwargs.pop("segmented_button_selected_hover_color")) + if "segmented_button_unselected_color" in kwargs: + self._segmented_button.configure(unselected_color=kwargs.pop("segmented_button_unselected_color")) + if "segmented_button_unselected_hover_color" in kwargs: + self._segmented_button.configure(unselected_hover_color=kwargs.pop("segmented_button_unselected_hover_color")) + if "text_color" in kwargs: + self._segmented_button.configure(text_color=kwargs.pop("text_color")) + if "text_color_disabled" in kwargs: + self._segmented_button.configure(text_color_disabled=kwargs.pop("text_color_disabled")) + + if "command" in kwargs: + self._command = kwargs.pop("command") + if "anchor" in kwargs: + self._anchor = kwargs.pop("anchor") + self._configure_grid() + self._set_grid_segmented_button() + if "state" in kwargs: + self._segmented_button.configure(state=kwargs.pop("state")) + + super().configure(require_redraw=require_redraw, **kwargs) + + def cget(self, attribute_name: str): + if attribute_name == "corner_radius": + return self._corner_radius + elif attribute_name == "border_width": + return self._border_width + + elif attribute_name == "fg_color": + return self._fg_color + elif attribute_name == "border_color": + return self._border_color + elif attribute_name == "segmented_button_fg_color": + return self._segmented_button.cget(attribute_name) + elif attribute_name == "segmented_button_selected_color": + return self._segmented_button.cget(attribute_name) + elif attribute_name == "segmented_button_selected_hover_color": + return self._segmented_button.cget(attribute_name) + elif attribute_name == "segmented_button_unselected_color": + return self._segmented_button.cget(attribute_name) + elif attribute_name == "segmented_button_unselected_hover_color": + return self._segmented_button.cget(attribute_name) + elif attribute_name == "text_color": + return self._segmented_button.cget(attribute_name) + elif attribute_name == "text_color_disabled": + return self._segmented_button.cget(attribute_name) + + elif attribute_name == "command": + return self._command + elif attribute_name == "anchor": + return self._anchor + elif attribute_name == "state": + return self._segmented_button.cget(attribute_name) + + else: + return super().cget(attribute_name) + + def tab(self, name: str) -> CTkFrame: + """ returns reference to the tab with given name """ + + if name in self._tab_dict: + return self._tab_dict[name] + else: + raise ValueError(f"CTkTabview has no tab named '{name}'") + + def insert(self, index: int, name: str) -> CTkFrame: + """ creates new tab with given name at position index """ + + if name not in self._tab_dict: + # if no tab exists, set grid for segmented button + if len(self._tab_dict) == 0: + self._set_grid_segmented_button() + + self._name_list.append(name) + self._tab_dict[name] = self._create_tab() + self._segmented_button.insert(index, name) + + # if created tab is only tab select this tab + if len(self._tab_dict) == 1: + self._current_name = name + self._segmented_button.set(self._current_name) + self._grid_forget_all_tabs() + self._set_grid_current_tab() + + return self._tab_dict[name] + else: + raise ValueError(f"CTkTabview already has tab named '{name}'") + + def add(self, name: str) -> CTkFrame: + """ appends new tab with given name """ + return self.insert(len(self._tab_dict), name) + + def index(self, name) -> int: + """ get index of tab with given name """ + return self._segmented_button.index(name) + + def move(self, new_index: int, name: str): + if 0 <= new_index < len(self._name_list): + if name in self._tab_dict: + self._segmented_button.move(new_index, name) + else: + raise ValueError(f"CTkTabview has no name '{name}'") + else: + raise ValueError(f"CTkTabview new_index {new_index} not in range of name list with len {len(self._name_list)}") + + def rename(self, old_name: str, new_name: str): + if new_name in self._name_list: + raise ValueError(f"new_name '{new_name}' already exists") + + # segmented button + old_index = self._segmented_button.index(old_name) + self._segmented_button.delete(old_name) + self._segmented_button.insert(old_index, new_name) + + # name list + self._name_list.remove(old_name) + self._name_list.append(new_name) + + # tab dictionary + self._tab_dict[new_name] = self._tab_dict.pop(old_name) + + def delete(self, name: str): + """ delete tab by name """ + + if name in self._tab_dict: + self._name_list.remove(name) + self._tab_dict[name].grid_forget() + self._tab_dict.pop(name) + self._segmented_button.delete(name) + + # set current_name to '' and remove segmented button if no tab is left + if len(self._name_list) == 0: + self._current_name = "" + self._segmented_button.grid_forget() + + # if only one tab left, select this tab + elif len(self._name_list) == 1: + self._current_name = self._name_list[0] + self._segmented_button.set(self._current_name) + self._grid_forget_all_tabs() + self._set_grid_current_tab() + + # more tabs are left + else: + # if current_name is deleted tab, select first tab at position 0 + if self._current_name == name: + self.set(self._name_list[0]) + else: + raise ValueError(f"CTkTabview has no tab named '{name}'") + + def set(self, name: str): + """ select tab by name """ + + if name in self._tab_dict: + self._current_name = name + self._segmented_button.set(name) + self._set_grid_current_tab() + self.after(100, lambda: self._grid_forget_all_tabs(exclude_name=name)) + else: + raise ValueError(f"CTkTabview has no tab named '{name}'") + + def get(self) -> str: + """ returns name of selected tab, returns empty string if no tab selected """ + return self._current_name diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_textbox.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_textbox.py new file mode 100644 index 0000000..4b3a165 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/ctk_textbox.py @@ -0,0 +1,500 @@ +import tkinter +from typing import Union, Tuple, Optional, Callable, Any + +from .core_rendering import CTkCanvas +from .ctk_scrollbar import CTkScrollbar +from .theme import ThemeManager +from .core_rendering import DrawEngine +from .core_widget_classes import CTkBaseClass +from .font import CTkFont +from .utility import pop_from_dict_by_set, check_kwargs_empty + + +class CTkTextbox(CTkBaseClass): + """ + Textbox with x and y scrollbars, rounded corners, and all text features of tkinter.Text widget. + Scrollbars only appear when they are needed. Text is wrapped on line end by default, + set wrap='none' to disable automatic line wrapping. + For detailed information check out the documentation. + + Detailed methods and parameters of the underlaying tkinter.Text widget can be found here: + https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/text.html + (most of them are implemented here too) + """ + + _scrollbar_update_time = 200 # interval in ms, to check if scrollbars are needed + + # attributes that are passed to and managed by the tkinter textbox only: + _valid_tk_text_attributes = {"autoseparators", "cursor", "exportselection", + "insertborderwidth", "insertofftime", "insertontime", "insertwidth", + "maxundo", "padx", "pady", "selectborderwidth", "spacing1", + "spacing2", "spacing3", "state", "tabs", "takefocus", "undo", "wrap", + "xscrollcommand", "yscrollcommand"} + + def __init__(self, + master: any, + width: int = 200, + height: int = 200, + corner_radius: Optional[int] = None, + border_width: Optional[int] = None, + border_spacing: int = 3, + + bg_color: Union[str, Tuple[str, str]] = "transparent", + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + border_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color: Optional[Union[str, str]] = None, + scrollbar_button_color: Optional[Union[str, Tuple[str, str]]] = None, + scrollbar_button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + + font: Optional[Union[tuple, CTkFont]] = None, + activate_scrollbars: bool = True, + **kwargs): + + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass + super().__init__(master=master, bg_color=bg_color, width=width, height=height) + + # color + self._fg_color = ThemeManager.theme["CTkTextbox"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True) + self._border_color = ThemeManager.theme["CTkTextbox"]["border_color"] if border_color is None else self._check_color_type(border_color) + self._text_color = ThemeManager.theme["CTkTextbox"]["text_color"] if text_color is None else self._check_color_type(text_color) + self._scrollbar_button_color = ThemeManager.theme["CTkTextbox"]["scrollbar_button_color"] if scrollbar_button_color is None else self._check_color_type(scrollbar_button_color) + self._scrollbar_button_hover_color = ThemeManager.theme["CTkTextbox"]["scrollbar_button_hover_color"] if scrollbar_button_hover_color is None else self._check_color_type(scrollbar_button_hover_color) + + # shape + self._corner_radius = ThemeManager.theme["CTkTextbox"]["corner_radius"] if corner_radius is None else corner_radius + self._border_width = ThemeManager.theme["CTkTextbox"]["border_width"] if border_width is None else border_width + self._border_spacing = border_spacing + + # font + self._font = CTkFont() if font is None else self._check_font_type(font) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + self._canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew") + self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + self._draw_engine = DrawEngine(self._canvas) + + self._textbox = tkinter.Text(self, + fg=self._apply_appearance_mode(self._text_color), + width=0, + height=0, + font=self._apply_font_scaling(self._font), + highlightthickness=0, + relief="flat", + insertbackground=self._apply_appearance_mode(self._text_color), + **pop_from_dict_by_set(kwargs, self._valid_tk_text_attributes)) + + check_kwargs_empty(kwargs, raise_error=True) + + # scrollbars + self._scrollbars_activated = activate_scrollbars + self._hide_x_scrollbar = True + self._hide_y_scrollbar = True + + self._y_scrollbar = CTkScrollbar(self, + width=8, + height=0, + border_spacing=0, + fg_color=self._fg_color, + button_color=self._scrollbar_button_color, + button_hover_color=self._scrollbar_button_hover_color, + orientation="vertical", + command=self._textbox.yview) + self._textbox.configure(yscrollcommand=self._y_scrollbar.set) + + self._x_scrollbar = CTkScrollbar(self, + height=8, + width=0, + border_spacing=0, + fg_color=self._fg_color, + button_color=self._scrollbar_button_color, + button_hover_color=self._scrollbar_button_hover_color, + orientation="horizontal", + command=self._textbox.xview) + self._textbox.configure(xscrollcommand=self._x_scrollbar.set) + + self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True) + + self.after(50, self._check_if_scrollbars_needed, None, True) + self._draw() + + def _create_grid_for_text_and_scrollbars(self, re_grid_textbox=False, re_grid_x_scrollbar=False, re_grid_y_scrollbar=False): + + # configure 2x2 grid + self.grid_rowconfigure(0, weight=1) + self.grid_rowconfigure(1, weight=0, minsize=self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing))) + self.grid_columnconfigure(0, weight=1) + self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing))) + + if re_grid_textbox: + self._textbox.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew", + padx=(self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)), 0), + pady=(self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)), 0)) + + if re_grid_x_scrollbar: + if not self._hide_x_scrollbar and self._scrollbars_activated: + self._x_scrollbar.grid(row=1, column=0, rowspan=1, columnspan=1, sticky="ewn", + pady=(3, self._border_spacing + self._border_width), + padx=(max(self._corner_radius, self._border_width + self._border_spacing), 0)) # scrollbar grid method without scaling + else: + self._x_scrollbar.grid_forget() + + if re_grid_y_scrollbar: + if not self._hide_y_scrollbar and self._scrollbars_activated: + self._y_scrollbar.grid(row=0, column=1, rowspan=1, columnspan=1, sticky="nsw", + padx=(3, self._border_spacing + self._border_width), + pady=(max(self._corner_radius, self._border_width + self._border_spacing), 0)) # scrollbar grid method without scaling + else: + self._y_scrollbar.grid_forget() + + def _check_if_scrollbars_needed(self, event=None, continue_loop: bool = False): + """ Method hides or places the scrollbars if they are needed on key release event of tkinter.text widget """ + + if self._scrollbars_activated: + if self._textbox.xview() != (0.0, 1.0) and not self._x_scrollbar.winfo_ismapped(): # x scrollbar needed + self._hide_x_scrollbar = False + self._create_grid_for_text_and_scrollbars(re_grid_x_scrollbar=True) + elif self._textbox.xview() == (0.0, 1.0) and self._x_scrollbar.winfo_ismapped(): # x scrollbar not needed + self._hide_x_scrollbar = True + self._create_grid_for_text_and_scrollbars(re_grid_x_scrollbar=True) + + if self._textbox.yview() != (0.0, 1.0) and not self._y_scrollbar.winfo_ismapped(): # y scrollbar needed + self._hide_y_scrollbar = False + self._create_grid_for_text_and_scrollbars(re_grid_y_scrollbar=True) + elif self._textbox.yview() == (0.0, 1.0) and self._y_scrollbar.winfo_ismapped(): # y scrollbar not needed + self._hide_y_scrollbar = True + self._create_grid_for_text_and_scrollbars(re_grid_y_scrollbar=True) + else: + self._hide_x_scrollbar = False + self._hide_x_scrollbar = False + self._create_grid_for_text_and_scrollbars(re_grid_y_scrollbar=True) + + if self._textbox.winfo_exists() and continue_loop is True: + self.after(self._scrollbar_update_time, lambda: self._check_if_scrollbars_needed(continue_loop=True)) + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self._textbox.configure(font=self._apply_font_scaling(self._font)) + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True) + self._draw(no_color_updates=True) + + def _set_dimensions(self, width=None, height=None): + super()._set_dimensions(width, height) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw() + + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ + self._textbox.configure(font=self._apply_font_scaling(self._font)) + + # Workaround to force grid to be resized when text changes size. + # Otherwise grid will lag and only resizes if other mouse action occurs. + self._canvas.grid_forget() + self._canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew") + + def destroy(self): + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + + super().destroy() + + def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + + if not self._canvas.winfo_exists(): + return + + requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width), + self._apply_widget_scaling(self._current_height), + self._apply_widget_scaling(self._corner_radius), + self._apply_widget_scaling(self._border_width)) + + if no_color_updates is False or requires_recoloring: + if self._fg_color == "transparent": + self._canvas.itemconfig("inner_parts", + fill=self._apply_appearance_mode(self._bg_color), + outline=self._apply_appearance_mode(self._bg_color)) + self._textbox.configure(fg=self._apply_appearance_mode(self._text_color), + bg=self._apply_appearance_mode(self._bg_color), + insertbackground=self._apply_appearance_mode(self._text_color)) + self._x_scrollbar.configure(fg_color=self._bg_color, button_color=self._scrollbar_button_color, + button_hover_color=self._scrollbar_button_hover_color) + self._y_scrollbar.configure(fg_color=self._bg_color, button_color=self._scrollbar_button_color, + button_hover_color=self._scrollbar_button_hover_color) + else: + self._canvas.itemconfig("inner_parts", + fill=self._apply_appearance_mode(self._fg_color), + outline=self._apply_appearance_mode(self._fg_color)) + self._textbox.configure(fg=self._apply_appearance_mode(self._text_color), + bg=self._apply_appearance_mode(self._fg_color), + insertbackground=self._apply_appearance_mode(self._text_color)) + self._x_scrollbar.configure(fg_color=self._fg_color, button_color=self._scrollbar_button_color, + button_hover_color=self._scrollbar_button_hover_color) + self._y_scrollbar.configure(fg_color=self._fg_color, button_color=self._scrollbar_button_color, + button_hover_color=self._scrollbar_button_hover_color) + + self._canvas.itemconfig("border_parts", + fill=self._apply_appearance_mode(self._border_color), + outline=self._apply_appearance_mode(self._border_color)) + self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + + self._canvas.tag_lower("inner_parts") + self._canvas.tag_lower("border_parts") + + def configure(self, require_redraw=False, **kwargs): + if "fg_color" in kwargs: + self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True) + require_redraw = True + + # check if CTk widgets are children of the frame and change their _bg_color to new frame fg_color + for child in self.winfo_children(): + if isinstance(child, CTkBaseClass) and hasattr(child, "_fg_color"): + child.configure(bg_color=self._fg_color) + + if "border_color" in kwargs: + self._border_color = self._check_color_type(kwargs.pop("border_color")) + require_redraw = True + + if "text_color" in kwargs: + self._text_color = self._check_color_type(kwargs.pop("text_color")) + require_redraw = True + + if "scrollbar_button_color" in kwargs: + self._scrollbar_button_color = self._check_color_type(kwargs.pop("scrollbar_button_color")) + self._x_scrollbar.configure(button_color=self._scrollbar_button_color) + self._y_scrollbar.configure(button_color=self._scrollbar_button_color) + + if "scrollbar_button_hover_color" in kwargs: + self._scrollbar_button_hover_color = self._check_color_type(kwargs.pop("scrollbar_button_hover_color")) + self._x_scrollbar.configure(button_hover_color=self._scrollbar_button_hover_color) + self._y_scrollbar.configure(button_hover_color=self._scrollbar_button_hover_color) + + if "corner_radius" in kwargs: + self._corner_radius = kwargs.pop("corner_radius") + self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True) + require_redraw = True + + if "border_width" in kwargs: + self._border_width = kwargs.pop("border_width") + self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True) + require_redraw = True + + if "border_spacing" in kwargs: + self._border_spacing = kwargs.pop("border_spacing") + self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True) + require_redraw = True + + if "font" in kwargs: + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + self._font = self._check_font_type(kwargs.pop("font")) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + self._update_font() + + self._textbox.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_text_attributes)) + super().configure(require_redraw=require_redraw, **kwargs) + + def cget(self, attribute_name: str) -> any: + if attribute_name == "corner_radius": + return self._corner_radius + elif attribute_name == "border_width": + return self._border_width + elif attribute_name == "border_spacing": + return self._border_spacing + + elif attribute_name == "fg_color": + return self._fg_color + elif attribute_name == "border_color": + return self._border_color + elif attribute_name == "text_color": + return self._text_color + + elif attribute_name == "font": + return self._font + + else: + return super().cget(attribute_name) + + def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True): + """ called on the tkinter.Canvas """ + if not (add == "+" or add is True): + raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") + self._textbox.bind(sequence, command, add=True) + + def unbind(self, sequence: str = None, funcid: str = None): + """ called on the tkinter.Label and tkinter.Canvas """ + if funcid is not None: + raise ValueError("'funcid' argument can only be None, because there is a bug in" + + " tkinter and its not clear whether the internal callbacks will be unbinded or not") + self._textbox.unbind(sequence, None) + + def focus(self): + return self._textbox.focus() + + def focus_set(self): + return self._textbox.focus_set() + + def focus_force(self): + return self._textbox.focus_force() + + def insert(self, index, text, tags=None): + return self._textbox.insert(index, text, tags) + + def get(self, index1, index2=None): + return self._textbox.get(index1, index2) + + def bbox(self, index): + return self._textbox.bbox(index) + + def compare(self, index, op, index2): + return self._textbox.compare(index, op, index2) + + def delete(self, index1, index2=None): + return self._textbox.delete(index1, index2) + + def dlineinfo(self, index): + return self._textbox.dlineinfo(index) + + def edit_modified(self, arg=None): + return self._textbox.edit_modified(arg) + + def edit_redo(self): + self._check_if_scrollbars_needed() + return self._textbox.edit_redo() + + def edit_reset(self): + return self._textbox.edit_reset() + + def edit_separator(self): + return self._textbox.edit_separator() + + def edit_undo(self): + self._check_if_scrollbars_needed() + return self._textbox.edit_undo() + + def image_create(self, index, **kwargs): + raise AttributeError("embedding images is forbidden, because would be incompatible with scaling") + + def image_cget(self, index, option): + raise AttributeError("embedding images is forbidden, because would be incompatible with scaling") + + def image_configure(self, index): + raise AttributeError("embedding images is forbidden, because would be incompatible with scaling") + + def image_names(self): + raise AttributeError("embedding images is forbidden, because would be incompatible with scaling") + + def index(self, i): + return self._textbox.index(i) + + def mark_gravity(self, mark, gravity=None): + return self._textbox.mark_gravity(mark, gravity) + + def mark_names(self): + return self._textbox.mark_names() + + def mark_next(self, index): + return self._textbox.mark_next(index) + + def mark_previous(self, index): + return self._textbox.mark_previous(index) + + def mark_set(self, mark, index): + return self._textbox.mark_set(mark, index) + + def mark_unset(self, mark): + return self._textbox.mark_unset(mark) + + def scan_dragto(self, x, y): + return self._textbox.scan_dragto(x, y) + + def scan_mark(self, x, y): + return self._textbox.scan_mark(x, y) + + def search(self, pattern, index, *args, **kwargs): + return self._textbox.search(pattern, index, *args, **kwargs) + + def see(self, index): + return self._textbox.see(index) + + def tag_add(self, tagName, index1, index2=None): + return self._textbox.tag_add(tagName, index1, index2) + + def tag_bind(self, tagName, sequence, func, add=None): + return self._textbox.tag_bind(tagName, sequence, func, add) + + def tag_cget(self, tagName, option): + return self._textbox.tag_cget(tagName, option) + + def tag_config(self, tagName, **kwargs): + if "font" in kwargs: + raise AttributeError("'font' option forbidden, because would be incompatible with scaling") + return self._textbox.tag_config(tagName, **kwargs) + + def tag_delete(self, *tagName): + return self._textbox.tag_delete(*tagName) + + def tag_lower(self, tagName, belowThis=None): + return self._textbox.tag_lower(tagName, belowThis) + + def tag_names(self, index=None): + return self._textbox.tag_names(index) + + def tag_nextrange(self, tagName, index1, index2=None): + return self._textbox.tag_nextrange(tagName, index1, index2) + + def tag_prevrange(self, tagName, index1, index2=None): + return self._textbox.tag_prevrange(tagName, index1, index2) + + def tag_raise(self, tagName, aboveThis=None): + return self._textbox.tag_raise(tagName, aboveThis) + + def tag_ranges(self, tagName): + return self._textbox.tag_ranges(tagName) + + def tag_remove(self, tagName, index1, index2=None): + return self._textbox.tag_remove(tagName, index1, index2) + + def tag_unbind(self, tagName, sequence, funcid=None): + return self._textbox.tag_unbind(tagName, sequence, funcid) + + def window_cget(self, index, option): + raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)") + + def window_configure(self, index, option): + raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)") + + def window_create(self, index, **kwargs): + raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)") + + def window_names(self): + raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)") + + def xview(self, *args): + return self._textbox.xview(*args) + + def xview_moveto(self, fraction): + return self._textbox.xview_moveto(fraction) + + def xview_scroll(self, n, what): + return self._textbox.xview_scroll(n, what) + + def yview(self, *args): + return self._textbox.yview(*args) + + def yview_moveto(self, fraction): + return self._textbox.yview_moveto(fraction) + + def yview_scroll(self, n, what): + return self._textbox.yview_scroll(n, what) diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/font/__init__.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/font/__init__.py new file mode 100644 index 0000000..64a49f1 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/font/__init__.py @@ -0,0 +1,24 @@ +import os +import sys + +from .ctk_font import CTkFont +from .font_manager import FontManager + +# import DrawEngine to set preferred_drawing_method if loading shapes font fails +from ..core_rendering import DrawEngine + +FontManager.init_font_manager() + +# load Roboto fonts (used on Windows/Linux) +customtkinter_directory = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +FontManager.load_font(os.path.join(customtkinter_directory, "assets", "fonts", "Roboto", "Roboto-Regular.ttf")) +FontManager.load_font(os.path.join(customtkinter_directory, "assets", "fonts", "Roboto", "Roboto-Medium.ttf")) + +# load font necessary for rendering the widgets (used on Windows/Linux) +if FontManager.load_font(os.path.join(customtkinter_directory, "assets", "fonts", "CustomTkinter_shapes_font.otf")) is False: + # change draw method if font loading failed + if DrawEngine.preferred_drawing_method == "font_shapes": + sys.stderr.write("customtkinter.windows.widgets.font warning: " + + "Preferred drawing method 'font_shapes' can not be used because the font file could not be loaded.\n" + + "Using 'circle_shapes' instead. The rendering quality will be bad!\n") + DrawEngine.preferred_drawing_method = "circle_shapes" diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/font/__pycache__/__init__.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/font/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..95bf8c1 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/font/__pycache__/__init__.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/font/__pycache__/ctk_font.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/font/__pycache__/ctk_font.cpython-311.pyc new file mode 100644 index 0000000..06d54e2 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/font/__pycache__/ctk_font.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/font/__pycache__/font_manager.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/font/__pycache__/font_manager.cpython-311.pyc new file mode 100644 index 0000000..2c81bb1 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/font/__pycache__/font_manager.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/font/ctk_font.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/font/ctk_font.py new file mode 100644 index 0000000..e0eca0d --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/font/ctk_font.py @@ -0,0 +1,94 @@ +from tkinter.font import Font +import copy +from typing import List, Callable, Tuple, Optional +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + +from ..theme import ThemeManager + + +class CTkFont(Font): + """ + Font object with size in pixel, independent of scaling. + To get scaled tuple representation use create_scaled_tuple() method. + + family The font family name as a string. + size The font height as an integer in pixel. + weight 'bold' for boldface, 'normal' for regular weight. + slant 'italic' for italic, 'roman' for unslanted. + underline 1 for underlined text, 0 for normal. + overstrike 1 for overstruck text, 0 for normal. + + Tkinter Font: https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/fonts.html + """ + + def __init__(self, + family: Optional[str] = None, + size: Optional[int] = None, + weight: Literal["normal", "bold"] = None, + slant: Literal["italic", "roman"] = "roman", + underline: bool = False, + overstrike: bool = False): + + self._size_configure_callback_list: List[Callable] = [] + + self._size = ThemeManager.theme["CTkFont"]["size"] if size is None else size + + super().__init__(family=ThemeManager.theme["CTkFont"]["family"] if family is None else family, + size=-abs(self._size), + weight=ThemeManager.theme["CTkFont"]["weight"] if weight is None else weight, + slant=slant, + underline=underline, + overstrike=overstrike) + + self._family = super().cget("family") + self._tuple_style_string = f"{super().cget('weight')} {slant} {'underline' if underline else ''} {'overstrike' if overstrike else ''}" + + def add_size_configure_callback(self, callback: Callable): + """ add function, that gets called when font got configured """ + self._size_configure_callback_list.append(callback) + + def remove_size_configure_callback(self, callback: Callable): + """ remove function, that gets called when font got configured """ + try: + self._size_configure_callback_list.remove(callback) + except ValueError: + pass + + def create_scaled_tuple(self, font_scaling: float) -> Tuple[str, int, str]: + """ return scaled tuple representation of font in the form (family: str, size: int, style: str)""" + return self._family, round(-abs(self._size) * font_scaling), self._tuple_style_string + + def config(self, *args, **kwargs): + raise AttributeError("'config' is not implemented for CTk widgets. For consistency, always use 'configure' instead.") + + def configure(self, **kwargs): + if "size" in kwargs: + self._size = kwargs.pop("size") + super().configure(size=-abs(self._size)) + + if "family" in kwargs: + super().configure(family=kwargs.pop("family")) + self._family = super().cget("family") + + super().configure(**kwargs) + + # update style string for create_scaled_tuple() method + self._tuple_style_string = f"{super().cget('weight')} {super().cget('slant')} {'underline' if super().cget('underline') else ''} {'overstrike' if super().cget('overstrike') else ''}" + + # call all functions registered with add_size_configure_callback() + for callback in self._size_configure_callback_list: + callback() + + def cget(self, attribute_name: str) -> any: + if attribute_name == "size": + return self._size + if attribute_name == "family": + return self._family + else: + return super().cget(attribute_name) + + def copy(self) -> "CTkFont": + return copy.deepcopy(self) diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/font/font_manager.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/font/font_manager.py new file mode 100644 index 0000000..b3ef369 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/font/font_manager.py @@ -0,0 +1,66 @@ +import sys +import os +import shutil +from typing import Union + + +class FontManager: + + linux_font_path = "~/.fonts/" + + @classmethod + def init_font_manager(cls): + # Linux + if sys.platform.startswith("linux"): + try: + if not os.path.isdir(os.path.expanduser(cls.linux_font_path)): + os.mkdir(os.path.expanduser(cls.linux_font_path)) + return True + except Exception as err: + sys.stderr.write("FontManager error: " + str(err) + "\n") + return False + + # other platforms + else: + return True + + @classmethod + def windows_load_font(cls, font_path: Union[str, bytes], private: bool = True, enumerable: bool = False) -> bool: + """ Function taken from: https://stackoverflow.com/questions/11993290/truly-custom-font-in-tkinter/30631309#30631309 """ + + from ctypes import windll, byref, create_unicode_buffer, create_string_buffer + + FR_PRIVATE = 0x10 + FR_NOT_ENUM = 0x20 + + if isinstance(font_path, bytes): + path_buffer = create_string_buffer(font_path) + add_font_resource_ex = windll.gdi32.AddFontResourceExA + elif isinstance(font_path, str): + path_buffer = create_unicode_buffer(font_path) + add_font_resource_ex = windll.gdi32.AddFontResourceExW + else: + raise TypeError('font_path must be of type bytes or str') + + flags = (FR_PRIVATE if private else 0) | (FR_NOT_ENUM if not enumerable else 0) + num_fonts_added = add_font_resource_ex(byref(path_buffer), flags, 0) + return bool(min(num_fonts_added, 1)) + + @classmethod + def load_font(cls, font_path: str) -> bool: + # Windows + if sys.platform.startswith("win"): + return cls.windows_load_font(font_path, private=True, enumerable=False) + + # Linux + elif sys.platform.startswith("linux"): + try: + shutil.copy(font_path, os.path.expanduser(cls.linux_font_path)) + return True + except Exception as err: + sys.stderr.write("FontManager error: " + str(err) + "\n") + return False + + # macOS and others + else: + return False diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/image/__init__.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/image/__init__.py new file mode 100644 index 0000000..b712c89 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/image/__init__.py @@ -0,0 +1 @@ +from .ctk_image import CTkImage diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/image/__pycache__/__init__.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/image/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..3b7d776 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/image/__pycache__/__init__.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/image/__pycache__/ctk_image.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/image/__pycache__/ctk_image.cpython-311.pyc new file mode 100644 index 0000000..38dcde0 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/image/__pycache__/ctk_image.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/image/ctk_image.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/image/ctk_image.py new file mode 100644 index 0000000..0247cdd --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/image/ctk_image.py @@ -0,0 +1,122 @@ +from typing import Tuple, Dict, Callable, List +try: + from PIL import Image, ImageTk +except ImportError: + pass + + +class CTkImage: + """ + Class to store one or two PIl.Image.Image objects and display size independent of scaling: + + light_image: PIL.Image.Image for light mode + dark_image: PIL.Image.Image for dark mode + size: tuple (, ) with display size for both images + + One of the two images can be None and will be replaced by the other image. + """ + + _checked_PIL_import = False + + def __init__(self, + light_image: "Image.Image" = None, + dark_image: "Image.Image" = None, + size: Tuple[int, int] = (20, 20)): + + if not self._checked_PIL_import: + self._check_pil_import() + + self._light_image = light_image + self._dark_image = dark_image + self._check_images() + self._size = size + + self._configure_callback_list: List[Callable] = [] + self._scaled_light_photo_images: Dict[Tuple[int, int], ImageTk.PhotoImage] = {} + self._scaled_dark_photo_images: Dict[Tuple[int, int], ImageTk.PhotoImage] = {} + + @classmethod + def _check_pil_import(cls): + try: + _, _ = Image, ImageTk + except NameError: + raise ImportError("PIL.Image and PIL.ImageTk couldn't be imported") + + def add_configure_callback(self, callback: Callable): + """ add function, that gets called when image got configured """ + self._configure_callback_list.append(callback) + + def remove_configure_callback(self, callback: Callable): + """ remove function, that gets called when image got configured """ + self._configure_callback_list.remove(callback) + + def configure(self, **kwargs): + if "light_image" in kwargs: + self._light_image = kwargs.pop("light_image") + self._scaled_light_photo_images = {} + self._check_images() + if "dark_image" in kwargs: + self._dark_image = kwargs.pop("dark_image") + self._scaled_dark_photo_images = {} + self._check_images() + if "size" in kwargs: + self._size = kwargs.pop("size") + + # call all functions registered with add_configure_callback() + for callback in self._configure_callback_list: + callback() + + def cget(self, attribute_name: str) -> any: + if attribute_name == "light_image": + return self._light_image + if attribute_name == "dark_image": + return self._dark_image + if attribute_name == "size": + return self._size + + def _check_images(self): + # check types + if self._light_image is not None and not isinstance(self._light_image, Image.Image): + raise ValueError(f"CTkImage: light_image must be instance if PIL.Image.Image, not {type(self._light_image)}") + if self._dark_image is not None and not isinstance(self._dark_image, Image.Image): + raise ValueError(f"CTkImage: dark_image must be instance if PIL.Image.Image, not {type(self._dark_image)}") + + # check values + if self._light_image is None and self._dark_image is None: + raise ValueError("CTkImage: No image given, light_image is None and dark_image is None.") + + # check sizes + if self._light_image is not None and self._dark_image is not None and self._light_image.size != self._dark_image.size: + raise ValueError(f"CTkImage: light_image size {self._light_image.size} must be the same as dark_image size {self._dark_image.size}.") + + def _get_scaled_size(self, widget_scaling: float) -> Tuple[int, int]: + return round(self._size[0] * widget_scaling), round(self._size[1] * widget_scaling) + + def _get_scaled_light_photo_image(self, scaled_size: Tuple[int, int]) -> "ImageTk.PhotoImage": + if scaled_size in self._scaled_light_photo_images: + return self._scaled_light_photo_images[scaled_size] + else: + self._scaled_light_photo_images[scaled_size] = ImageTk.PhotoImage(self._light_image.resize(scaled_size)) + return self._scaled_light_photo_images[scaled_size] + + def _get_scaled_dark_photo_image(self, scaled_size: Tuple[int, int]) -> "ImageTk.PhotoImage": + if scaled_size in self._scaled_dark_photo_images: + return self._scaled_dark_photo_images[scaled_size] + else: + self._scaled_dark_photo_images[scaled_size] = ImageTk.PhotoImage(self._dark_image.resize(scaled_size)) + return self._scaled_dark_photo_images[scaled_size] + + def create_scaled_photo_image(self, widget_scaling: float, appearance_mode: str) -> "ImageTk.PhotoImage": + scaled_size = self._get_scaled_size(widget_scaling) + + if appearance_mode == "light" and self._light_image is not None: + return self._get_scaled_light_photo_image(scaled_size) + elif appearance_mode == "light" and self._light_image is None: + return self._get_scaled_dark_photo_image(scaled_size) + + elif appearance_mode == "dark" and self._dark_image is not None: + return self._get_scaled_dark_photo_image(scaled_size) + elif appearance_mode == "dark" and self._dark_image is None: + return self._get_scaled_light_photo_image(scaled_size) + + diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/scaling/__init__.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/scaling/__init__.py new file mode 100644 index 0000000..8fc0db8 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/scaling/__init__.py @@ -0,0 +1,7 @@ +import sys + +from .scaling_base_class import CTkScalingBaseClass +from .scaling_tracker import ScalingTracker + +if sys.platform.startswith("win") and sys.getwindowsversion().build < 9000: # No automatic scaling on Windows < 8.1 + ScalingTracker.deactivate_automatic_dpi_awareness = True diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/scaling/__pycache__/__init__.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/scaling/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..c1e1e9a Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/scaling/__pycache__/__init__.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/scaling/__pycache__/scaling_base_class.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/scaling/__pycache__/scaling_base_class.cpython-311.pyc new file mode 100644 index 0000000..dc65400 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/scaling/__pycache__/scaling_base_class.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/scaling/__pycache__/scaling_tracker.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/scaling/__pycache__/scaling_tracker.cpython-311.pyc new file mode 100644 index 0000000..66a5a65 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/scaling/__pycache__/scaling_tracker.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/scaling/scaling_base_class.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/scaling/scaling_base_class.py new file mode 100644 index 0000000..0d7b29b --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/scaling/scaling_base_class.py @@ -0,0 +1,159 @@ +from typing import Union, Tuple +import copy +import re +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + +from .scaling_tracker import ScalingTracker +from ..font import CTkFont + + +class CTkScalingBaseClass: + """ + Super-class that manages the scaling values and callbacks. + Works for widgets and windows, type must be set in init method with + scaling_type attribute. Methods: + + - _set_scaling() abstractmethod, gets called when scaling changes, must be overridden + - destroy() must be called when sub-class is destroyed + - _apply_widget_scaling() + - _reverse_widget_scaling() + - _apply_window_scaling() + - _reverse_window_scaling() + - _apply_font_scaling() + - _apply_argument_scaling() + - _apply_geometry_scaling() + - _reverse_geometry_scaling() + - _parse_geometry_string() + + """ + def __init__(self, scaling_type: Literal["widget", "window"] = "widget"): + self.__scaling_type = scaling_type + + if self.__scaling_type == "widget": + ScalingTracker.add_widget(self._set_scaling, self) # add callback for automatic scaling changes + self.__widget_scaling = ScalingTracker.get_widget_scaling(self) + elif self.__scaling_type == "window": + ScalingTracker.activate_high_dpi_awareness() # make process DPI aware + ScalingTracker.add_window(self._set_scaling, self) # add callback for automatic scaling changes + self.__window_scaling = ScalingTracker.get_window_scaling(self) + + def destroy(self): + if self.__scaling_type == "widget": + ScalingTracker.remove_widget(self._set_scaling, self) + elif self.__scaling_type == "window": + ScalingTracker.remove_window(self._set_scaling, self) + + def _set_scaling(self, new_widget_scaling, new_window_scaling): + """ can be overridden, but super method must be called at the beginning """ + self.__widget_scaling = new_widget_scaling + self.__window_scaling = new_window_scaling + + def _get_widget_scaling(self) -> float: + return self.__widget_scaling + + def _get_window_scaling(self) -> float: + return self.__window_scaling + + def _apply_widget_scaling(self, value: Union[int, float]) -> Union[float]: + assert self.__scaling_type == "widget" + return value * self.__widget_scaling + + def _reverse_widget_scaling(self, value: Union[int, float]) -> Union[float]: + assert self.__scaling_type == "widget" + return value / self.__widget_scaling + + def _apply_window_scaling(self, value: Union[int, float]) -> int: + assert self.__scaling_type == "window" + return int(value * self.__window_scaling) + + def _reverse_window_scaling(self, scaled_value: Union[int, float]) -> int: + assert self.__scaling_type == "window" + return int(scaled_value / self.__window_scaling) + + def _apply_font_scaling(self, font: Union[Tuple, CTkFont]) -> tuple: + """ Takes CTkFont object and returns tuple font with scaled size, has to be called again for every change of font object """ + assert self.__scaling_type == "widget" + + if type(font) == tuple: + if len(font) == 1: + return font + elif len(font) == 2: + return font[0], -abs(round(font[1] * self.__widget_scaling)) + elif 3 <= len(font) <= 6: + return font[0], -abs(round(font[1] * self.__widget_scaling)), font[2:] + else: + raise ValueError(f"Can not scale font {font}. font needs to be tuple of len 1, 2 or 3") + + elif isinstance(font, CTkFont): + return font.create_scaled_tuple(self.__widget_scaling) + else: + raise ValueError(f"Can not scale font '{font}' of type {type(font)}. font needs to be tuple or instance of CTkFont") + + def _apply_argument_scaling(self, kwargs: dict) -> dict: + assert self.__scaling_type == "widget" + + scaled_kwargs = copy.copy(kwargs) + + # scale padding values + if "pady" in scaled_kwargs: + if isinstance(scaled_kwargs["pady"], (int, float)): + scaled_kwargs["pady"] = self._apply_widget_scaling(scaled_kwargs["pady"]) + elif isinstance(scaled_kwargs["pady"], tuple): + scaled_kwargs["pady"] = tuple([self._apply_widget_scaling(v) for v in scaled_kwargs["pady"]]) + if "padx" in kwargs: + if isinstance(scaled_kwargs["padx"], (int, float)): + scaled_kwargs["padx"] = self._apply_widget_scaling(scaled_kwargs["padx"]) + elif isinstance(scaled_kwargs["padx"], tuple): + scaled_kwargs["padx"] = tuple([self._apply_widget_scaling(v) for v in scaled_kwargs["padx"]]) + + # scaled x, y values for place geometry manager + if "x" in scaled_kwargs: + scaled_kwargs["x"] = self._apply_widget_scaling(scaled_kwargs["x"]) + if "y" in scaled_kwargs: + scaled_kwargs["y"] = self._apply_widget_scaling(scaled_kwargs["y"]) + + return scaled_kwargs + + @staticmethod + def _parse_geometry_string(geometry_string: str) -> tuple: + # index: 1 2 3 4 5 6 + # regex group structure: ('x', '', '', '+-+-', '-', '-') + result = re.search(r"((\d+)x(\d+)){0,1}(\+{0,1}([+-]{0,1}\d+)\+{0,1}([+-]{0,1}\d+)){0,1}", geometry_string) + + width = int(result.group(2)) if result.group(2) is not None else None + height = int(result.group(3)) if result.group(3) is not None else None + x = int(result.group(5)) if result.group(5) is not None else None + y = int(result.group(6)) if result.group(6) is not None else None + + return width, height, x, y + + def _apply_geometry_scaling(self, geometry_string: str) -> str: + assert self.__scaling_type == "window" + + width, height, x, y = self._parse_geometry_string(geometry_string) + + if x is None and y is None: # no and in geometry_string + return f"{round(width * self.__window_scaling)}x{round(height * self.__window_scaling)}" + + elif width is None and height is None: # no and in geometry_string + return f"+{x}+{y}" + + else: + return f"{round(width * self.__window_scaling)}x{round(height * self.__window_scaling)}+{x}+{y}" + + def _reverse_geometry_scaling(self, scaled_geometry_string: str) -> str: + assert self.__scaling_type == "window" + + width, height, x, y = self._parse_geometry_string(scaled_geometry_string) + + if x is None and y is None: # no and in geometry_string + return f"{round(width / self.__window_scaling)}x{round(height / self.__window_scaling)}" + + elif width is None and height is None: # no and in geometry_string + return f"+{x}+{y}" + + else: + return f"{round(width / self.__window_scaling)}x{round(height / self.__window_scaling)}+{x}+{y}" diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/scaling/scaling_tracker.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/scaling/scaling_tracker.py new file mode 100644 index 0000000..d3627c2 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/scaling/scaling_tracker.py @@ -0,0 +1,206 @@ +import tkinter +import sys +from typing import Callable + + +class ScalingTracker: + deactivate_automatic_dpi_awareness = False + + window_widgets_dict = {} # contains window objects as keys with list of widget callbacks as elements + window_dpi_scaling_dict = {} # contains window objects as keys and corresponding scaling factors + + widget_scaling = 1 # user values which multiply to detected window scaling factor + window_scaling = 1 + + update_loop_running = False + update_loop_interval = 100 # ms + loop_pause_after_new_scaling = 1500 # ms + + @classmethod + def get_widget_scaling(cls, widget) -> float: + window_root = cls.get_window_root_of_widget(widget) + return cls.window_dpi_scaling_dict[window_root] * cls.widget_scaling + + @classmethod + def get_window_scaling(cls, window) -> float: + window_root = cls.get_window_root_of_widget(window) + return cls.window_dpi_scaling_dict[window_root] * cls.window_scaling + + @classmethod + def set_widget_scaling(cls, widget_scaling_factor: float): + cls.widget_scaling = max(widget_scaling_factor, 0.4) + cls.update_scaling_callbacks_all() + + @classmethod + def set_window_scaling(cls, window_scaling_factor: float): + cls.window_scaling = max(window_scaling_factor, 0.4) + cls.update_scaling_callbacks_all() + + @classmethod + def get_window_root_of_widget(cls, widget): + current_widget = widget + + while isinstance(current_widget, tkinter.Tk) is False and\ + isinstance(current_widget, tkinter.Toplevel) is False: + current_widget = current_widget.master + + return current_widget + + @classmethod + def update_scaling_callbacks_all(cls): + for window, callback_list in cls.window_widgets_dict.items(): + for set_scaling_callback in callback_list: + if not cls.deactivate_automatic_dpi_awareness: + set_scaling_callback(cls.window_dpi_scaling_dict[window] * cls.widget_scaling, + cls.window_dpi_scaling_dict[window] * cls.window_scaling) + else: + set_scaling_callback(cls.widget_scaling, + cls.window_scaling) + + @classmethod + def update_scaling_callbacks_for_window(cls, window): + for set_scaling_callback in cls.window_widgets_dict[window]: + if not cls.deactivate_automatic_dpi_awareness: + set_scaling_callback(cls.window_dpi_scaling_dict[window] * cls.widget_scaling, + cls.window_dpi_scaling_dict[window] * cls.window_scaling) + else: + set_scaling_callback(cls.widget_scaling, + cls.window_scaling) + + @classmethod + def add_widget(cls, widget_callback: Callable, widget): + window_root = cls.get_window_root_of_widget(widget) + + if window_root not in cls.window_widgets_dict: + cls.window_widgets_dict[window_root] = [widget_callback] + else: + cls.window_widgets_dict[window_root].append(widget_callback) + + if window_root not in cls.window_dpi_scaling_dict: + cls.window_dpi_scaling_dict[window_root] = cls.get_window_dpi_scaling(window_root) + + if not cls.update_loop_running: + window_root.after(100, cls.check_dpi_scaling) + cls.update_loop_running = True + + @classmethod + def remove_widget(cls, widget_callback, widget): + window_root = cls.get_window_root_of_widget(widget) + try: + cls.window_widgets_dict[window_root].remove(widget_callback) + except: + pass + + @classmethod + def remove_window(cls, window_callback, window): + try: + del cls.window_widgets_dict[window] + except: + pass + + @classmethod + def add_window(cls, window_callback, window): + if window not in cls.window_widgets_dict: + cls.window_widgets_dict[window] = [window_callback] + else: + cls.window_widgets_dict[window].append(window_callback) + + if window not in cls.window_dpi_scaling_dict: + cls.window_dpi_scaling_dict[window] = cls.get_window_dpi_scaling(window) + + @classmethod + def activate_high_dpi_awareness(cls): + """ make process DPI aware, customtkinter elements will get scaled automatically, + only gets activated when CTk object is created """ + + if not cls.deactivate_automatic_dpi_awareness: + if sys.platform == "darwin": + pass # high DPI scaling works automatically on macOS + + elif sys.platform.startswith("win"): + import ctypes + + # Values for SetProcessDpiAwareness and SetProcessDpiAwarenessContext: + # internal enum PROCESS_DPI_AWARENESS + # { + # Process_DPI_Unaware = 0, + # Process_System_DPI_Aware = 1, + # Process_Per_Monitor_DPI_Aware = 2 + # } + # + # internal enum DPI_AWARENESS_CONTEXT + # { + # DPI_AWARENESS_CONTEXT_UNAWARE = 16, + # DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = 17, + # DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = 18, + # DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = 34 + # } + + # ctypes.windll.user32.SetProcessDpiAwarenessContext(34) # Non client area scaling at runtime (titlebar) + # does not work with resizable(False, False), window starts growing on monitor with different scaling (weird tkinter bug...) + # ctypes.windll.user32.EnableNonClientDpiScaling(hwnd) does not work for some reason (tested on Windows 11) + + # It's too bad, that these Windows API methods don't work properly with tkinter. But I tested days with multiple monitor setups, + # and I don't think there is anything left to do. So this is the best option at the moment: + + ctypes.windll.shcore.SetProcessDpiAwareness(2) # Titlebar does not scale at runtime + else: + pass # DPI awareness on Linux not implemented + + @classmethod + def get_window_dpi_scaling(cls, window) -> float: + if not cls.deactivate_automatic_dpi_awareness: + if sys.platform == "darwin": + return 1 # scaling works automatically on macOS + + elif sys.platform.startswith("win"): + from ctypes import windll, pointer, wintypes + + DPI100pc = 96 # DPI 96 is 100% scaling + DPI_type = 0 # MDT_EFFECTIVE_DPI = 0, MDT_ANGULAR_DPI = 1, MDT_RAW_DPI = 2 + window_hwnd = wintypes.HWND(window.winfo_id()) + monitor_handle = windll.user32.MonitorFromWindow(window_hwnd, wintypes.DWORD(2)) # MONITOR_DEFAULTTONEAREST = 2 + x_dpi, y_dpi = wintypes.UINT(), wintypes.UINT() + windll.shcore.GetDpiForMonitor(monitor_handle, DPI_type, pointer(x_dpi), pointer(y_dpi)) + return (x_dpi.value + y_dpi.value) / (2 * DPI100pc) + + else: + return 1 # DPI awareness on Linux not implemented + else: + return 1 + + @classmethod + def check_dpi_scaling(cls): + new_scaling_detected = False + + # check for every window if scaling value changed + for window in cls.window_widgets_dict: + if window.winfo_exists() and not window.state() == "iconic": + current_dpi_scaling_value = cls.get_window_dpi_scaling(window) + if current_dpi_scaling_value != cls.window_dpi_scaling_dict[window]: + cls.window_dpi_scaling_dict[window] = current_dpi_scaling_value + + if sys.platform.startswith("win"): + window.attributes("-alpha", 0.15) + + window.block_update_dimensions_event() + cls.update_scaling_callbacks_for_window(window) + window.unblock_update_dimensions_event() + + if sys.platform.startswith("win"): + window.attributes("-alpha", 1) + + new_scaling_detected = True + + # find an existing tkinter object for the next call of .after() + for app in cls.window_widgets_dict.keys(): + try: + if new_scaling_detected: + app.after(cls.loop_pause_after_new_scaling, cls.check_dpi_scaling) + else: + app.after(cls.update_loop_interval, cls.check_dpi_scaling) + return + except Exception: + continue + + cls.update_loop_running = False diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/theme/__init__.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/theme/__init__.py new file mode 100644 index 0000000..8931f35 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/theme/__init__.py @@ -0,0 +1,9 @@ +from .theme_manager import ThemeManager + +# load default blue theme +try: + ThemeManager.load_theme("blue") +except FileNotFoundError as err: + raise FileNotFoundError(f"{err}\nThe .json theme file for CustomTkinter could not be found.\n" + + f"If packaging with pyinstaller was used, have a look at the wiki:\n" + + f"https://github.com/TomSchimansky/CustomTkinter/wiki/Packaging#windows-pyinstaller-auto-py-to-exe") diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/theme/__pycache__/__init__.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/theme/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..762e64c Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/theme/__pycache__/__init__.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/theme/__pycache__/theme_manager.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/theme/__pycache__/theme_manager.cpython-311.pyc new file mode 100644 index 0000000..0b90c10 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/theme/__pycache__/theme_manager.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/theme/theme_manager.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/theme/theme_manager.py new file mode 100644 index 0000000..cf22858 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/theme/theme_manager.py @@ -0,0 +1,55 @@ +import sys +import os +import pathlib +import json +from typing import List, Union + + +class ThemeManager: + + theme: dict = {} # contains all the theme data + _built_in_themes: List[str] = ["blue", "green", "dark-blue", "sweetkind"] + _currently_loaded_theme: Union[str, None] = None + + @classmethod + def load_theme(cls, theme_name_or_path: str): + script_directory = os.path.dirname(os.path.abspath(__file__)) + + if theme_name_or_path in cls._built_in_themes: + customtkinter_path = pathlib.Path(script_directory).parent.parent.parent + with open(os.path.join(customtkinter_path, "assets", "themes", f"{theme_name_or_path}.json"), "r") as f: + cls.theme = json.load(f) + else: + with open(theme_name_or_path, "r") as f: + cls.theme = json.load(f) + + # store theme path for saving + cls._currently_loaded_theme = theme_name_or_path + + # filter theme values for platform + for key in cls.theme.keys(): + # check if values for key differ on platforms + if "macOS" in cls.theme[key].keys(): + if sys.platform == "darwin": + cls.theme[key] = cls.theme[key]["macOS"] + elif sys.platform.startswith("win"): + cls.theme[key] = cls.theme[key]["Windows"] + else: + cls.theme[key] = cls.theme[key]["Linux"] + + # fix name inconsistencies + if "CTkCheckbox" in cls.theme.keys(): + cls.theme["CTkCheckBox"] = cls.theme.pop("CTkCheckbox") + if "CTkRadiobutton" in cls.theme.keys(): + cls.theme["CTkRadioButton"] = cls.theme.pop("CTkRadiobutton") + + @classmethod + def save_theme(cls): + if cls._currently_loaded_theme is not None: + if cls._currently_loaded_theme not in cls._built_in_themes: + with open(cls._currently_loaded_theme, "r") as f: + json.dump(cls.theme, f, indent=2) + else: + raise ValueError(f"cannot modify builtin theme '{cls._currently_loaded_theme}'") + else: + raise ValueError(f"cannot save theme, no theme is loaded") diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/utility/__init__.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/utility/__init__.py new file mode 100644 index 0000000..c4b6fe8 --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/utility/__init__.py @@ -0,0 +1 @@ +from .utility_functions import pop_from_dict_by_set, check_kwargs_empty diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/utility/__pycache__/__init__.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/utility/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..640a097 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/utility/__pycache__/__init__.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/utility/__pycache__/utility_functions.cpython-311.pyc b/.venv/Lib/site-packages/customtkinter/windows/widgets/utility/__pycache__/utility_functions.cpython-311.pyc new file mode 100644 index 0000000..6bc4f68 Binary files /dev/null and b/.venv/Lib/site-packages/customtkinter/windows/widgets/utility/__pycache__/utility_functions.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/customtkinter/windows/widgets/utility/utility_functions.py b/.venv/Lib/site-packages/customtkinter/windows/widgets/utility/utility_functions.py new file mode 100644 index 0000000..a9968bb --- /dev/null +++ b/.venv/Lib/site-packages/customtkinter/windows/widgets/utility/utility_functions.py @@ -0,0 +1,22 @@ + +def pop_from_dict_by_set(dictionary: dict, valid_keys: set) -> dict: + """ remove and create new dict with key value pairs of dictionary, where key is in valid_keys """ + new_dictionary = {} + + for key in list(dictionary.keys()): + if key in valid_keys: + new_dictionary[key] = dictionary.pop(key) + + return new_dictionary + + +def check_kwargs_empty(kwargs_dict, raise_error=False) -> bool: + """ returns True if kwargs are empty, False otherwise, raises error if not empty """ + + if len(kwargs_dict) > 0: + if raise_error: + raise ValueError(f"{list(kwargs_dict.keys())} are not supported arguments. Look at the documentation for supported arguments.") + else: + return True + else: + return False diff --git a/.venv/Lib/site-packages/darkdetect-0.8.0.dist-info/INSTALLER b/.venv/Lib/site-packages/darkdetect-0.8.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/.venv/Lib/site-packages/darkdetect-0.8.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/.venv/Lib/site-packages/darkdetect-0.8.0.dist-info/LICENSE b/.venv/Lib/site-packages/darkdetect-0.8.0.dist-info/LICENSE new file mode 100644 index 0000000..5d17f5e --- /dev/null +++ b/.venv/Lib/site-packages/darkdetect-0.8.0.dist-info/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2019, Alberto Sottile +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of "darkdetect" nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL "Alberto Sottile" BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/.venv/Lib/site-packages/darkdetect-0.8.0.dist-info/METADATA b/.venv/Lib/site-packages/darkdetect-0.8.0.dist-info/METADATA new file mode 100644 index 0000000..53842e6 --- /dev/null +++ b/.venv/Lib/site-packages/darkdetect-0.8.0.dist-info/METADATA @@ -0,0 +1,87 @@ +Metadata-Version: 2.1 +Name: darkdetect +Version: 0.8.0 +Summary: Detect OS Dark Mode from Python +Author-email: Alberto Sottile +License: BSD-3-Clause +Project-URL: homepage, http://github.com/albertosottile/darkdetect +Project-URL: download, http://github.com/albertosottile/darkdetect/releases +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: Microsoft :: Windows :: Windows 10 +Classifier: Operating System :: POSIX :: Linux +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Requires-Python: >=3.6 +Description-Content-Type: text/markdown +License-File: LICENSE +Provides-Extra: macos-listener +Requires-Dist: pyobjc-framework-Cocoa ; (platform_system == "Darwin") and extra == 'macos-listener' + +# Darkdetect + +This package allows to detect if the user is using Dark Mode on: + +- [macOS 10.14+](https://support.apple.com/en-us/HT208976) +- [Windows 10 1607+](https://blogs.windows.com/windowsexperience/2016/08/08/windows-10-tip-personalize-your-pc-by-enabling-the-dark-theme/) +- Linux with [a dark GTK theme](https://www.gnome-look.org/browse/cat/135/ord/rating/?tag=dark). + +The main application of this package is to detect the Dark mode from your GUI Python application (Tkinter/wx/pyqt/qt for python (pyside)/...) and apply the needed adjustments to your interface. Darkdetect is particularly useful if your GUI library **does not** provide a public API for this detection (I am looking at you, Qt). In addition, this package does not depend on other modules or packages that are not already included in standard Python distributions. + + +## Usage + +``` +import darkdetect + +>>> darkdetect.theme() +'Dark' + +>>> darkdetect.isDark() +True + +>>> darkdetect.isLight() +False +``` +It's that easy. + +You can create a dark mode switch listener daemon thread with `darkdetect.listener` and pass a callback function. The function will be called with string "Dark" or "Light" when the OS switches the dark mode setting. + +``` python +import threading +import darkdetect + +# def listener(callback: typing.Callable[[str], None]) -> None: ... + +t = threading.Thread(target=darkdetect.listener, args=(print,)) +t.daemon = True +t.start() +``` + +## Install + +The preferred channel is PyPI: +``` +pip install darkdetect +``` + +Alternatively, you are free to vendor directly a copy of Darkdetect in your app. Further information on vendoring can be found [here](https://medium.com/underdog-io-engineering/vendoring-python-dependencies-with-pip-b9eb6078b9c0). + +## Optional Installs + +To enable the macOS listener, additional components are required, these can be installed via: +```bash +pip install darkdetect[macos-listener] +``` + +## Notes + +- This software is licensed under the terms of the 3-clause BSD License. +- This package can be installed on any operative system, but it will always return `None` unless executed on a OS that supports Dark Mode, including older versions of macOS and Windows. +- On macOS, detection of the dark menu bar and dock option (available from macOS 10.10) is not supported. +- [Details](https://stackoverflow.com/questions/25207077/how-to-detect-if-os-x-is-in-dark-mode) on the detection method used on macOS. +- [Details](https://askubuntu.com/questions/1261366/detecting-dark-mode#comment2132694_1261366) on the experimental detection method used on Linux. diff --git a/.venv/Lib/site-packages/darkdetect-0.8.0.dist-info/RECORD b/.venv/Lib/site-packages/darkdetect-0.8.0.dist-info/RECORD new file mode 100644 index 0000000..39b20a4 --- /dev/null +++ b/.venv/Lib/site-packages/darkdetect-0.8.0.dist-info/RECORD @@ -0,0 +1,18 @@ +darkdetect-0.8.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +darkdetect-0.8.0.dist-info/LICENSE,sha256=abuEZk7_kzrdyycchm1nYi2cLFOlvaMbGnUjVpwlCv4,1495 +darkdetect-0.8.0.dist-info/METADATA,sha256=NkgDlZffAhLrmov8qk_CcvNff5Rg-chv3fQ6sJAk4UA,3590 +darkdetect-0.8.0.dist-info/RECORD,, +darkdetect-0.8.0.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92 +darkdetect-0.8.0.dist-info/top_level.txt,sha256=lIG2Q43uQniIc4b-ukidPntGpjQzuFfQCYrgxeM4RVQ,11 +darkdetect/__init__.py,sha256=q2BLQUpYqbfFzsXHVoQL38IKzwXH50aEFAMy4rBuh50,1412 +darkdetect/__main__.py,sha256=H1iyyQbJFbpOtKpSEmmV9H5ZjcrAI3zPWcCv1min5Ww,332 +darkdetect/__pycache__/__init__.cpython-311.pyc,, +darkdetect/__pycache__/__main__.cpython-311.pyc,, +darkdetect/__pycache__/_dummy.cpython-311.pyc,, +darkdetect/__pycache__/_linux_detect.cpython-311.pyc,, +darkdetect/__pycache__/_mac_detect.cpython-311.pyc,, +darkdetect/__pycache__/_windows_detect.cpython-311.pyc,, +darkdetect/_dummy.py,sha256=vKv_XK2dpvIGXZAjlNRuXiCH-vrRy7DBVCQKpbflrsM,473 +darkdetect/_linux_detect.py,sha256=nfyc7rpVNqMfbfGoXLsB7bPPSMG2cP1AauON73dCiKE,1584 +darkdetect/_mac_detect.py,sha256=XQfJ9gcbEFWoZxFrrnrsh-AVwIWHV-_oMpk3UCVBMWA,3745 +darkdetect/_windows_detect.py,sha256=8CCrPeTqNno1zVDXU5CP8D0AzOfqtcYOViHFnK3_WfA,4172 diff --git a/.venv/Lib/site-packages/darkdetect-0.8.0.dist-info/WHEEL b/.venv/Lib/site-packages/darkdetect-0.8.0.dist-info/WHEEL new file mode 100644 index 0000000..57e3d84 --- /dev/null +++ b/.venv/Lib/site-packages/darkdetect-0.8.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.38.4) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/.venv/Lib/site-packages/darkdetect-0.8.0.dist-info/top_level.txt b/.venv/Lib/site-packages/darkdetect-0.8.0.dist-info/top_level.txt new file mode 100644 index 0000000..70fb6ee --- /dev/null +++ b/.venv/Lib/site-packages/darkdetect-0.8.0.dist-info/top_level.txt @@ -0,0 +1 @@ +darkdetect diff --git a/.venv/Lib/site-packages/darkdetect/__init__.py b/.venv/Lib/site-packages/darkdetect/__init__.py new file mode 100644 index 0000000..73d92de --- /dev/null +++ b/.venv/Lib/site-packages/darkdetect/__init__.py @@ -0,0 +1,44 @@ +#----------------------------------------------------------------------------- +# Copyright (C) 2019 Alberto Sottile +# +# Distributed under the terms of the 3-clause BSD License. +#----------------------------------------------------------------------------- + +__version__ = '0.8.0' + +import sys +import platform + +def macos_supported_version(): + sysver = platform.mac_ver()[0] #typically 10.14.2 or 12.3 + major = int(sysver.split('.')[0]) + if major < 10: + return False + elif major >= 11: + return True + else: + minor = int(sysver.split('.')[1]) + if minor < 14: + return False + else: + return True + +if sys.platform == "darwin": + if macos_supported_version(): + from ._mac_detect import * + else: + from ._dummy import * +elif sys.platform == "win32" and platform.release().isdigit() and int(platform.release()) >= 10: + # Checks if running Windows 10 version 10.0.14393 (Anniversary Update) OR HIGHER. The getwindowsversion method returns a tuple. + # The third item is the build number that we can use to check if the user has a new enough version of Windows. + winver = int(platform.version().split('.')[2]) + if winver >= 14393: + from ._windows_detect import * + else: + from ._dummy import * +elif sys.platform == "linux": + from ._linux_detect import * +else: + from ._dummy import * + +del sys, platform diff --git a/.venv/Lib/site-packages/darkdetect/__main__.py b/.venv/Lib/site-packages/darkdetect/__main__.py new file mode 100644 index 0000000..1cb260b --- /dev/null +++ b/.venv/Lib/site-packages/darkdetect/__main__.py @@ -0,0 +1,9 @@ +#----------------------------------------------------------------------------- +# Copyright (C) 2019 Alberto Sottile +# +# Distributed under the terms of the 3-clause BSD License. +#----------------------------------------------------------------------------- + +import darkdetect + +print('Current theme: {}'.format(darkdetect.theme())) diff --git a/.venv/Lib/site-packages/darkdetect/__pycache__/__init__.cpython-311.pyc b/.venv/Lib/site-packages/darkdetect/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..8657541 Binary files /dev/null and b/.venv/Lib/site-packages/darkdetect/__pycache__/__init__.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/darkdetect/__pycache__/__main__.cpython-311.pyc b/.venv/Lib/site-packages/darkdetect/__pycache__/__main__.cpython-311.pyc new file mode 100644 index 0000000..2a718df Binary files /dev/null and b/.venv/Lib/site-packages/darkdetect/__pycache__/__main__.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/darkdetect/__pycache__/_dummy.cpython-311.pyc b/.venv/Lib/site-packages/darkdetect/__pycache__/_dummy.cpython-311.pyc new file mode 100644 index 0000000..4cf2c8e Binary files /dev/null and b/.venv/Lib/site-packages/darkdetect/__pycache__/_dummy.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/darkdetect/__pycache__/_linux_detect.cpython-311.pyc b/.venv/Lib/site-packages/darkdetect/__pycache__/_linux_detect.cpython-311.pyc new file mode 100644 index 0000000..8c269f9 Binary files /dev/null and b/.venv/Lib/site-packages/darkdetect/__pycache__/_linux_detect.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/darkdetect/__pycache__/_mac_detect.cpython-311.pyc b/.venv/Lib/site-packages/darkdetect/__pycache__/_mac_detect.cpython-311.pyc new file mode 100644 index 0000000..d591551 Binary files /dev/null and b/.venv/Lib/site-packages/darkdetect/__pycache__/_mac_detect.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/darkdetect/__pycache__/_windows_detect.cpython-311.pyc b/.venv/Lib/site-packages/darkdetect/__pycache__/_windows_detect.cpython-311.pyc new file mode 100644 index 0000000..4143c46 Binary files /dev/null and b/.venv/Lib/site-packages/darkdetect/__pycache__/_windows_detect.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/darkdetect/_dummy.py b/.venv/Lib/site-packages/darkdetect/_dummy.py new file mode 100644 index 0000000..1e82117 --- /dev/null +++ b/.venv/Lib/site-packages/darkdetect/_dummy.py @@ -0,0 +1,19 @@ +#----------------------------------------------------------------------------- +# Copyright (C) 2019 Alberto Sottile +# +# Distributed under the terms of the 3-clause BSD License. +#----------------------------------------------------------------------------- + +import typing + +def theme(): + return None + +def isDark(): + return None + +def isLight(): + return None + +def listener(callback: typing.Callable[[str], None]) -> None: + raise NotImplementedError() diff --git a/.venv/Lib/site-packages/darkdetect/_linux_detect.py b/.venv/Lib/site-packages/darkdetect/_linux_detect.py new file mode 100644 index 0000000..0570e6a --- /dev/null +++ b/.venv/Lib/site-packages/darkdetect/_linux_detect.py @@ -0,0 +1,45 @@ +#----------------------------------------------------------------------------- +# Copyright (C) 2019 Alberto Sottile, Eric Larson +# +# Distributed under the terms of the 3-clause BSD License. +#----------------------------------------------------------------------------- + +import subprocess + +def theme(): + try: + #Using the freedesktop specifications for checking dark mode + out = subprocess.run( + ['gsettings', 'get', 'org.gnome.desktop.interface', 'color-scheme'], + capture_output=True) + stdout = out.stdout.decode() + #If not found then trying older gtk-theme method + if len(stdout)<1: + out = subprocess.run( + ['gsettings', 'get', 'org.gnome.desktop.interface', 'gtk-theme'], + capture_output=True) + stdout = out.stdout.decode() + except Exception: + return 'Light' + # we have a string, now remove start and end quote + theme = stdout.lower().strip()[1:-1] + if '-dark' in theme.lower(): + return 'Dark' + else: + return 'Light' + +def isDark(): + return theme() == 'Dark' + +def isLight(): + return theme() == 'Light' + +# def listener(callback: typing.Callable[[str], None]) -> None: +def listener(callback): + with subprocess.Popen( + ('gsettings', 'monitor', 'org.gnome.desktop.interface', 'gtk-theme'), + stdout=subprocess.PIPE, + universal_newlines=True, + ) as p: + for line in p.stdout: + callback('Dark' if '-dark' in line.strip().removeprefix("gtk-theme: '").removesuffix("'").lower() else 'Light') diff --git a/.venv/Lib/site-packages/darkdetect/_mac_detect.py b/.venv/Lib/site-packages/darkdetect/_mac_detect.py new file mode 100644 index 0000000..8d44bc7 --- /dev/null +++ b/.venv/Lib/site-packages/darkdetect/_mac_detect.py @@ -0,0 +1,124 @@ +#----------------------------------------------------------------------------- +# Copyright (C) 2019 Alberto Sottile +# +# Distributed under the terms of the 3-clause BSD License. +#----------------------------------------------------------------------------- + +import ctypes +import ctypes.util +import subprocess +import sys +import os +from pathlib import Path +from typing import Callable + +try: + from Foundation import NSObject, NSKeyValueObservingOptionNew, NSKeyValueChangeNewKey, NSUserDefaults + from PyObjCTools import AppHelper + _can_listen = True +except ModuleNotFoundError: + _can_listen = False + + +try: + # macOS Big Sur+ use "a built-in dynamic linker cache of all system-provided libraries" + appkit = ctypes.cdll.LoadLibrary('AppKit.framework/AppKit') + objc = ctypes.cdll.LoadLibrary('libobjc.dylib') +except OSError: + # revert to full path for older OS versions and hardened programs + appkit = ctypes.cdll.LoadLibrary(ctypes.util.find_library('AppKit')) + objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('objc')) + +void_p = ctypes.c_void_p +ull = ctypes.c_uint64 + +objc.objc_getClass.restype = void_p +objc.sel_registerName.restype = void_p + +# See https://docs.python.org/3/library/ctypes.html#function-prototypes for arguments description +MSGPROTOTYPE = ctypes.CFUNCTYPE(void_p, void_p, void_p, void_p) +msg = MSGPROTOTYPE(('objc_msgSend', objc), ((1 ,'', None), (1, '', None), (1, '', None))) + +def _utf8(s): + if not isinstance(s, bytes): + s = s.encode('utf8') + return s + +def n(name): + return objc.sel_registerName(_utf8(name)) + +def C(classname): + return objc.objc_getClass(_utf8(classname)) + +def theme(): + NSAutoreleasePool = objc.objc_getClass('NSAutoreleasePool') + pool = msg(NSAutoreleasePool, n('alloc')) + pool = msg(pool, n('init')) + + NSUserDefaults = C('NSUserDefaults') + stdUserDef = msg(NSUserDefaults, n('standardUserDefaults')) + + NSString = C('NSString') + + key = msg(NSString, n("stringWithUTF8String:"), _utf8('AppleInterfaceStyle')) + appearanceNS = msg(stdUserDef, n('stringForKey:'), void_p(key)) + appearanceC = msg(appearanceNS, n('UTF8String')) + + if appearanceC is not None: + out = ctypes.string_at(appearanceC) + else: + out = None + + msg(pool, n('release')) + + if out is not None: + return out.decode('utf-8') + else: + return 'Light' + +def isDark(): + return theme() == 'Dark' + +def isLight(): + return theme() == 'Light' + + +def _listen_child(): + """ + Run by a child process, install an observer and print theme on change + """ + import signal + signal.signal(signal.SIGINT, signal.SIG_IGN) + + OBSERVED_KEY = "AppleInterfaceStyle" + + class Observer(NSObject): + def observeValueForKeyPath_ofObject_change_context_( + self, path, object, changeDescription, context + ): + result = changeDescription[NSKeyValueChangeNewKey] + try: + print(f"{'Light' if result is None else result}", flush=True) + except IOError: + os._exit(1) + + observer = Observer.new() # Keep a reference alive after installing + defaults = NSUserDefaults.standardUserDefaults() + defaults.addObserver_forKeyPath_options_context_( + observer, OBSERVED_KEY, NSKeyValueObservingOptionNew, 0 + ) + + AppHelper.runConsoleEventLoop() + + +def listener(callback: Callable[[str], None]) -> None: + if not _can_listen: + raise NotImplementedError() + with subprocess.Popen( + (sys.executable, "-c", "import _mac_detect as m; m._listen_child()"), + stdout=subprocess.PIPE, + universal_newlines=True, + cwd=Path(__file__).parent, + ) as p: + for line in p.stdout: + callback(line.strip()) diff --git a/.venv/Lib/site-packages/darkdetect/_windows_detect.py b/.venv/Lib/site-packages/darkdetect/_windows_detect.py new file mode 100644 index 0000000..2363f18 --- /dev/null +++ b/.venv/Lib/site-packages/darkdetect/_windows_detect.py @@ -0,0 +1,122 @@ +from winreg import HKEY_CURRENT_USER as hkey, QueryValueEx as getSubkeyValue, OpenKey as getKey + +import ctypes +import ctypes.wintypes + +advapi32 = ctypes.windll.advapi32 + +# LSTATUS RegOpenKeyExA( +# HKEY hKey, +# LPCSTR lpSubKey, +# DWORD ulOptions, +# REGSAM samDesired, +# PHKEY phkResult +# ); +advapi32.RegOpenKeyExA.argtypes = ( + ctypes.wintypes.HKEY, + ctypes.wintypes.LPCSTR, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ctypes.POINTER(ctypes.wintypes.HKEY), +) +advapi32.RegOpenKeyExA.restype = ctypes.wintypes.LONG + +# LSTATUS RegQueryValueExA( +# HKEY hKey, +# LPCSTR lpValueName, +# LPDWORD lpReserved, +# LPDWORD lpType, +# LPBYTE lpData, +# LPDWORD lpcbData +# ); +advapi32.RegQueryValueExA.argtypes = ( + ctypes.wintypes.HKEY, + ctypes.wintypes.LPCSTR, + ctypes.wintypes.LPDWORD, + ctypes.wintypes.LPDWORD, + ctypes.wintypes.LPBYTE, + ctypes.wintypes.LPDWORD, +) +advapi32.RegQueryValueExA.restype = ctypes.wintypes.LONG + +# LSTATUS RegNotifyChangeKeyValue( +# HKEY hKey, +# WINBOOL bWatchSubtree, +# DWORD dwNotifyFilter, +# HANDLE hEvent, +# WINBOOL fAsynchronous +# ); +advapi32.RegNotifyChangeKeyValue.argtypes = ( + ctypes.wintypes.HKEY, + ctypes.wintypes.BOOL, + ctypes.wintypes.DWORD, + ctypes.wintypes.HANDLE, + ctypes.wintypes.BOOL, +) +advapi32.RegNotifyChangeKeyValue.restype = ctypes.wintypes.LONG + +def theme(): + """ Uses the Windows Registry to detect if the user is using Dark Mode """ + # Registry will return 0 if Windows is in Dark Mode and 1 if Windows is in Light Mode. This dictionary converts that output into the text that the program is expecting. + valueMeaning = {0: "Dark", 1: "Light"} + # In HKEY_CURRENT_USER, get the Personalisation Key. + try: + key = getKey(hkey, "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize") + # In the Personalisation Key, get the AppsUseLightTheme subkey. This returns a tuple. + # The first item in the tuple is the result we want (0 or 1 indicating Dark Mode or Light Mode); the other value is the type of subkey e.g. DWORD, QWORD, String, etc. + subkey = getSubkeyValue(key, "AppsUseLightTheme")[0] + except FileNotFoundError: + # some headless Windows instances (e.g. GitHub Actions or Docker images) do not have this key + return None + return valueMeaning[subkey] + +def isDark(): + if theme() is not None: + return theme() == 'Dark' + +def isLight(): + if theme() is not None: + return theme() == 'Light' + +#def listener(callback: typing.Callable[[str], None]) -> None: +def listener(callback): + hKey = ctypes.wintypes.HKEY() + advapi32.RegOpenKeyExA( + ctypes.wintypes.HKEY(0x80000001), # HKEY_CURRENT_USER + ctypes.wintypes.LPCSTR(b'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize'), + ctypes.wintypes.DWORD(), + ctypes.wintypes.DWORD(0x00020019), # KEY_READ + ctypes.byref(hKey), + ) + + dwSize = ctypes.wintypes.DWORD(ctypes.sizeof(ctypes.wintypes.DWORD)) + queryValueLast = ctypes.wintypes.DWORD() + queryValue = ctypes.wintypes.DWORD() + advapi32.RegQueryValueExA( + hKey, + ctypes.wintypes.LPCSTR(b'AppsUseLightTheme'), + ctypes.wintypes.LPDWORD(), + ctypes.wintypes.LPDWORD(), + ctypes.cast(ctypes.byref(queryValueLast), ctypes.wintypes.LPBYTE), + ctypes.byref(dwSize), + ) + + while True: + advapi32.RegNotifyChangeKeyValue( + hKey, + ctypes.wintypes.BOOL(True), + ctypes.wintypes.DWORD(0x00000004), # REG_NOTIFY_CHANGE_LAST_SET + ctypes.wintypes.HANDLE(None), + ctypes.wintypes.BOOL(False), + ) + advapi32.RegQueryValueExA( + hKey, + ctypes.wintypes.LPCSTR(b'AppsUseLightTheme'), + ctypes.wintypes.LPDWORD(), + ctypes.wintypes.LPDWORD(), + ctypes.cast(ctypes.byref(queryValue), ctypes.wintypes.LPBYTE), + ctypes.byref(dwSize), + ) + if queryValueLast.value != queryValue.value: + queryValueLast.value = queryValue.value + callback('Light' if queryValue.value else 'Dark') diff --git a/conf.txt b/conf.txt index 1d72e79..101c628 100644 --- a/conf.txt +++ b/conf.txt @@ -1,3 +1,50 @@ -Kp=3.00 -Ki=0.02 -Kd=0.05 \ No newline at end of file +0,1{ + Kp:0.0 + Ki:0.1 + Kd:0.0 +} +0,09{ + Kp:0.0 + Ki:0.09 + Kd:0.0 +} +0,08{ + Kp:0.0 + Ki:0.08 + Kd:0.0 +} +0,07{ + Kp:0.0 + Ki:0.07 + Kd:0.0 +} +0,06{ + Kp:0.0 + Ki:0.06 + Kd:0.0 +} +0,05{ + Kp:0.0 + Ki:0.05 + Kd:0.0 +} +0,04{ + Kp:0.0 + Ki:0.04 + Kd:0.0 +} +0,03{ + Kp:0.0 + Ki:0.03 + Kd:0.0 +} +0,02{ + Kp:0.0 + Ki:0.02 + Kd:0.0 +} +0,01{ + Kp:0.0 + Ki:0.01 + Kd:0.0 +} \ No newline at end of file diff --git a/main_v3.py b/main_v3.py index 2a9b842..eee6727 100644 --- a/main_v3.py +++ b/main_v3.py @@ -1,5 +1,8 @@ import tkinter as tk from tkinter import ttk, messagebox +import customtkinter +customtkinter.set_appearance_mode("light") +customtkinter.set_default_color_theme("blue") import serial import serial.tools.list_ports import matplotlib.pyplot as plt @@ -10,8 +13,9 @@ import numpy as np class ArduinoGUI: def __init__(self, root): + self.my_font = customtkinter.CTkFont(family="Calibri", size=15) self.root = root - self.root.title("Arduino GUI") + self.root.title("PRG 342 GUI") self.arduino = None self.ports = list(serial.tools.list_ports.comports()) @@ -24,55 +28,87 @@ class ArduinoGUI: self.multi_setpoints_flag = False self.tare_flag = False self.demag_flag = False + self.input_source_flag = False + self.pid_flag = False # Flag for sending PID parameters + + self.pid_params = {} + self.selected_pid = None self.create_widgets() + self.load_configurations() + # Start the communication thread self.communication_thread = threading.Thread(target=self.communication_loop) self.communication_thread.start() - + def create_widgets(self): - self.com_label = tk.Label(self.root, text="Select COM Port:") - self.com_label.grid(row=0, column=0) - self.combobox_value = tk.StringVar() - self.combobox = ttk.Combobox(self.root, textvariable=self.combobox_value) - self.combobox['values'] = [port.device for port in self.ports] - self.combobox.grid(row=0, column=1) + # Labels and ComboBox for Stellantriebstyp + self.stellantriebstyp_label = customtkinter.CTkLabel(self.root, text="Stellantriebstyp:", font=self.my_font) + self.stellantriebstyp_label.grid(row=0, column=0, padx=(10, 5), pady=(10, 0), sticky="w") - self.connect_button = tk.Button(self.root, text="Connect", command=self.connect_arduino) - self.connect_button.grid(row=0, column=2) + self.config_combobox_value = customtkinter.StringVar() + self.config_combobox = customtkinter.CTkComboBox(self.root, variable=self.config_combobox_value, state="readonly", font=self.my_font) + self.config_combobox.grid(row=0, column=1, columnspan=1, padx=(0, 10), pady=(10, 0), sticky="ew") + #self.config_combobox.bind("<>", self.on_config_selected) - self.setpoint_label = tk.Label(self.root, text="Setpoint (Nm):") - self.setpoint_label.grid(row=1, column=0) + # Setzen Button für Stellantriebstyp + self.set_pid_button = customtkinter.CTkButton(self.root, text="Setzen", command=self.set_pid_parameters, font=self.my_font) + self.set_pid_button.grid(row=0, column=2, padx=(0, 10), pady=(10, 0), sticky="ew") - self.setpoint_entry = tk.Entry(self.root) - self.setpoint_entry.grid(row=1, column=1) + # Labels and ComboBox for COM Port + self.com_label = customtkinter.CTkLabel(self.root, text="COM Port:", font=self.my_font) + self.com_label.grid(row=1, column=0, padx=(10, 5), pady=(10, 0), sticky="w") - self.set_setpoint_button = tk.Button(self.root, text="Set", command=self.set_setpoint) - self.set_setpoint_button.grid(row=1, column=2) + self.combobox_value = customtkinter.StringVar() + self.combobox = customtkinter.CTkComboBox(self.root, variable=self.combobox_value, font=self.my_font) + self.combobox.configure(values=[port.device for port in self.ports]) + self.combobox.grid(row=1, column=1, padx=(0, 10), pady=(10, 0), sticky="ew") - self.multi_setpoints_label = tk.Label(self.root, text="Setpoints (angle;torque):") - self.multi_setpoints_label.grid(row=2, column=0) + self.connect_button = customtkinter.CTkButton(self.root, text="Verbinden", command=self.connect_arduino, font=self.my_font) + self.connect_button.grid(row=1, column=2, padx=(0, 10), pady=(10, 0), sticky="ew") - self.multi_setpoints_text = tk.Text(self.root, height=5, width=20) - self.multi_setpoints_text.grid(row=2, column=1) + # Setpoint entry + self.setpoint_label = customtkinter.CTkLabel(self.root, text="Sollwerteingabe in Nm:", font=self.my_font) + self.setpoint_label.grid(row=2, column=0, padx=(10, 5), pady=(10, 0), sticky="w") - self.send_multi_setpoints_button = tk.Button(self.root, text="Send Setpoints", command=self.send_multi_setpoints) - self.send_multi_setpoints_button.grid(row=2, column=2) + self.setpoint_entry = customtkinter.CTkEntry(self.root, font=self.my_font) + self.setpoint_entry.grid(row=2, column=1, padx=(0, 10), pady=(10, 0), sticky="ew") - self.tare_button = tk.Button(self.root, text="Tare Angle", command=self.tare_angle) - self.tare_button.grid(row=3, column=2) + self.set_setpoint_button = customtkinter.CTkButton(self.root, text="Setzen", command=self.set_setpoint, font=self.my_font) + self.set_setpoint_button.grid(row=2, column=2, padx=(0, 10), pady=(10, 0), sticky="ew") - self.demag_button = tk.Button(self.root, text="Demagnetize", command=self.demagnetize) - self.demag_button.grid(row=4, column=2) + # Multi Setpoints Textbox and Button + self.multi_setpoints_label = customtkinter.CTkLabel(self.root, text="Sollwerteeingabe\n(Winkel;Drehmoment):", font=self.my_font) + self.multi_setpoints_label.grid(row=3, column=0, padx=(10, 5), pady=(10, 0), sticky="w") - self.exit_button = tk.Button(self.root, text="Exit", command=self.on_closing) - self.exit_button.grid(row=5, column=2) + self.multi_setpoints_text = customtkinter.CTkTextbox(self.root, font=self.my_font) + self.multi_setpoints_text.grid(row=3, column=1, padx=(0, 10), pady=(10, 0), sticky="ew") + self.send_multi_setpoints_button = customtkinter.CTkButton(self.root, text="Sollwerte senden", command=self.send_multi_setpoints, font=self.my_font) + self.send_multi_setpoints_button.grid(row=3, column=2, padx=(0, 10), pady=(10, 0), sticky="ew") + + # Analog Control CheckBox + self.checkbox_var = customtkinter.IntVar() + self.checkbox = customtkinter.CTkCheckBox(self.root, text="Analogsteuerung", variable=self.checkbox_var, command=self.input_source_Switch, font=self.my_font) + self.checkbox.grid(row=4, column=0, columnspan=3, padx=(10, 10), pady=(10, 0), sticky="w") + + # Tare and Demagnetize Buttons + self.tare_button = customtkinter.CTkButton(self.root, text="Drehwinkel tarieren", command=self.tare_angle, font=self.my_font) + self.tare_button.grid(row=5, column=0, padx=(10, 5), pady=(10, 0), sticky="sew") + + self.demag_button = customtkinter.CTkButton(self.root, text="Manuelle Entmagnetisierung", command=self.demagnetize, font=self.my_font) + self.demag_button.grid(row=5, column=1, padx=(0, 5), pady=(10, 0), sticky="sew") + + # Exit Button + self.exit_button = customtkinter.CTkButton(self.root, text="Beenden", command=self.on_closing, font=self.my_font) + self.exit_button.grid(row=5, column=2, padx=(0, 10), pady=(10, 0), sticky="sew") + + # Plot self.figure, self.ax = plt.subplots() self.canvas = FigureCanvasTkAgg(self.figure, self.root) - self.canvas.get_tk_widget().grid(row=6, column=0, columnspan=3) + self.canvas.get_tk_widget().grid(row=0, column=3, rowspan=6, columnspan=4, padx=(10, 10), pady=(10, 0), sticky="nsew") self.ax.set_title("Drehmoment-Drehwinkel-Kennlinie") self.ax.set_xlabel("Winkel (°)") @@ -80,18 +116,82 @@ class ArduinoGUI: self.ax.set_xlim(0, 90) self.ax.set_ylim(0, 50) + # Current readings labels self.current_angle = 0 self.current_torque = 0 + self.analogInput = 0 - self.angle_label_var = tk.StringVar() - self.angle_label_var.set("Current Angle: 0") - self.angle_label = tk.Label(self.root, textvariable=self.angle_label_var) - self.angle_label.grid(row=7, column=0) + self.angle_label_var = customtkinter.StringVar() + self.angle_label_var.set("Drehwinkel: 0") + self.angle_label = customtkinter.CTkLabel(self.root, textvariable=self.angle_label_var, font=self.my_font) + self.angle_label.grid(row=6, column=3, padx=(10, 5), pady=(10, 0), sticky="sw") - self.torque_label_var = tk.StringVar() - self.torque_label_var.set("Current Torque: 0") - self.torque_label = tk.Label(self.root, textvariable=self.torque_label_var) - self.torque_label.grid(row=7, column=1) + self.torque_label_var = customtkinter.StringVar() + self.torque_label_var.set("Istwert: 0") + self.torque_label = customtkinter.CTkLabel(self.root, textvariable=self.torque_label_var, font=self.my_font) + self.torque_label.grid(row=6, column=4, padx=(10, 5), pady=(10, 0), sticky="sw") + + self.analogread_label_var = customtkinter.StringVar() + self.analogread_label_var.set("Analogeingang: 0") + self.analogread_label = customtkinter.CTkLabel(self.root, textvariable=self.analogread_label_var, font=self.my_font) + self.analogread_label.grid(row=6, column=5, padx=(10, 5), pady=(10, 0), sticky="sw") + + self.currentSetpoint_label_var = customtkinter.StringVar() + self.currentSetpoint_label_var.set("Aktueller Sollwert: 0") + self.currentSetpoint_label = customtkinter.CTkLabel(self.root, textvariable=self.currentSetpoint_label_var, font=self.my_font) + self.currentSetpoint_label.grid(row=6, column=6, padx=(10, 5), pady=(10, 0), sticky="sw") + + self.root.grid_columnconfigure(0, weight=0) + self.root.grid_columnconfigure(1, weight=0) + self.root.grid_columnconfigure(2, weight=0) + self.root.grid_columnconfigure(3, weight=1) + self.root.grid_columnconfigure(4, weight=1) + self.root.grid_columnconfigure(5, weight=1) + self.root.grid_columnconfigure(6, weight=1) + self.root.grid_rowconfigure(0, weight=0) + self.root.grid_rowconfigure(1, weight=0) + self.root.grid_rowconfigure(2, weight=0) + self.root.grid_rowconfigure(3, weight=0) + self.root.grid_rowconfigure(4, weight=0) + self.root.grid_rowconfigure(5, weight=1) + self.root.grid_rowconfigure(6, weight=0) + + def set_pid_parameters(self): + if self.arduino: + self.selected_pid = self.config_combobox.get() + if self.selected_pid in self.pid_params: + self.pid_flag = True + else: + messagebox.showerror("Error", "Selected PID configuration not found.") + else: + messagebox.showwarning("Warning", "Arduino not connected") + + def load_configurations(self): + try: + with open("conf.txt", "r") as file: + config_name = None + for line in file: + line = line.strip() + if line.startswith("#") or not line: + continue + if "{" in line: + config_name = line.split("{")[0].strip() + self.pid_params[config_name] = {"Kp": 0.0, "Ki": 0.0, "Kd": 0.0} + elif "}" in line: + config_name = None + else: + if config_name: + key, value = line.split(":") + key = key.strip() + value = round(float(value.strip()), 3) + self.pid_params[config_name][key] = value + + self.config_combobox.configure(values=list(self.pid_params.keys())) + if self.pid_params: + self.config_combobox.set(list(self.pid_params.keys())[0]) + self.selected_pid = list(self.pid_params.keys())[0] + except Exception as e: + messagebox.showerror("Error", f"Error loading configurations: {e}") def connect_arduino(self): com_port = self.combobox_value.get() @@ -193,6 +293,12 @@ class ArduinoGUI: else: messagebox.showwarning("Warning", "Arduino not connected") + def input_source_Switch(self): + if self.arduino: + self.input_source_flag = True + else: + messagebox.showwarning("Warning", "Arduino not connected") + def communication_loop(self): while self.running: if self.arduino: @@ -213,6 +319,13 @@ class ArduinoGUI: self.arduino.write(b'e\n') self.demag_flag = False time.sleep(0.1) + if self.input_source_flag: + self.arduino.write(b'S\n') + self.input_source_flag = False + time.sleep(0.1) + if self.pid_flag: + self.send_pid_parameters() + self.pid_flag = False self.arduino.write(b'a\n') timeout_ctr = time.time() @@ -223,10 +336,13 @@ class ArduinoGUI: data = self.arduino.readline().decode(errors='ignore').strip().split(";") self.current_angle = float(data[0]) / 1000 self.current_torque = float(data[1]) / 1000 - print(data[2]) + self.analogInput = float(data[2]) / 1000 + self.currentSetpoint = float(data[3]) / 1000 - self.angle_label_var.set(f"Current Angle: {self.current_angle}") - self.torque_label_var.set(f"Current Torque: {self.current_torque}") + self.angle_label_var.set(f"Drehwinkel: {self.current_angle:.1f} °") + self.torque_label_var.set(f"Istwert: {self.current_torque:.1f} Nm") + self.analogread_label_var.set(f"Analogeingang: {self.analogInput:.1f} V") + self.currentSetpoint_label_var.set(f"Aktueller Sollwert: {self.currentSetpoint:.1f} Nm") if self.current_point is not None: self.current_point.remove() @@ -234,9 +350,24 @@ class ArduinoGUI: self.canvas.draw() except Exception as e: print(e) - + time.sleep(0.1) + def send_pid_parameters(self): + if self.selected_pid in self.pid_params: + pid_values = self.pid_params[self.selected_pid] + kp = f"{pid_values['Kp']:.3f}" + ki = f"{pid_values['Ki']:.3f}" + kd = f"{pid_values['Kd']:.3f}" + self.arduino.write(f"p{kp}\n".encode()) + time.sleep(0.1) + self.arduino.write(f"i{ki}\n".encode()) + time.sleep(0.1) + self.arduino.write(f"d{kd}\n".encode()) + messagebox.showinfo("Info", f"PID parameters sent: Kp={kp}, Ki={ki}, Kd={kd}") + else: + messagebox.showerror("Error", "Selected PID configuration not found.") + def on_closing(self): self.running = False if self.arduino: @@ -245,7 +376,7 @@ class ArduinoGUI: self.root.destroy() if __name__ == "__main__": - root = tk.Tk() + root = customtkinter.CTk() app = ArduinoGUI(root) root.protocol("WM_DELETE_WINDOW", app.on_closing) - root.mainloop() \ No newline at end of file + root.mainloop() diff --git a/tmp.py b/tmp.py new file mode 100644 index 0000000..eee6727 --- /dev/null +++ b/tmp.py @@ -0,0 +1,382 @@ +import tkinter as tk +from tkinter import ttk, messagebox +import customtkinter +customtkinter.set_appearance_mode("light") +customtkinter.set_default_color_theme("blue") +import serial +import serial.tools.list_ports +import matplotlib.pyplot as plt +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +import threading +import time +import numpy as np + +class ArduinoGUI: + def __init__(self, root): + self.my_font = customtkinter.CTkFont(family="Calibri", size=15) + self.root = root + self.root.title("PRG 342 GUI") + + self.arduino = None + self.ports = list(serial.tools.list_ports.comports()) + self.running = True + self.current_point = None + self.setpoints = [] + + # Flags for user actions + self.setpoint_flag = False + self.multi_setpoints_flag = False + self.tare_flag = False + self.demag_flag = False + self.input_source_flag = False + self.pid_flag = False # Flag for sending PID parameters + + self.pid_params = {} + self.selected_pid = None + + self.create_widgets() + + self.load_configurations() + + # Start the communication thread + self.communication_thread = threading.Thread(target=self.communication_loop) + self.communication_thread.start() + + def create_widgets(self): + + # Labels and ComboBox for Stellantriebstyp + self.stellantriebstyp_label = customtkinter.CTkLabel(self.root, text="Stellantriebstyp:", font=self.my_font) + self.stellantriebstyp_label.grid(row=0, column=0, padx=(10, 5), pady=(10, 0), sticky="w") + + self.config_combobox_value = customtkinter.StringVar() + self.config_combobox = customtkinter.CTkComboBox(self.root, variable=self.config_combobox_value, state="readonly", font=self.my_font) + self.config_combobox.grid(row=0, column=1, columnspan=1, padx=(0, 10), pady=(10, 0), sticky="ew") + #self.config_combobox.bind("<>", self.on_config_selected) + + # Setzen Button für Stellantriebstyp + self.set_pid_button = customtkinter.CTkButton(self.root, text="Setzen", command=self.set_pid_parameters, font=self.my_font) + self.set_pid_button.grid(row=0, column=2, padx=(0, 10), pady=(10, 0), sticky="ew") + + # Labels and ComboBox for COM Port + self.com_label = customtkinter.CTkLabel(self.root, text="COM Port:", font=self.my_font) + self.com_label.grid(row=1, column=0, padx=(10, 5), pady=(10, 0), sticky="w") + + self.combobox_value = customtkinter.StringVar() + self.combobox = customtkinter.CTkComboBox(self.root, variable=self.combobox_value, font=self.my_font) + self.combobox.configure(values=[port.device for port in self.ports]) + self.combobox.grid(row=1, column=1, padx=(0, 10), pady=(10, 0), sticky="ew") + + self.connect_button = customtkinter.CTkButton(self.root, text="Verbinden", command=self.connect_arduino, font=self.my_font) + self.connect_button.grid(row=1, column=2, padx=(0, 10), pady=(10, 0), sticky="ew") + + # Setpoint entry + self.setpoint_label = customtkinter.CTkLabel(self.root, text="Sollwerteingabe in Nm:", font=self.my_font) + self.setpoint_label.grid(row=2, column=0, padx=(10, 5), pady=(10, 0), sticky="w") + + self.setpoint_entry = customtkinter.CTkEntry(self.root, font=self.my_font) + self.setpoint_entry.grid(row=2, column=1, padx=(0, 10), pady=(10, 0), sticky="ew") + + self.set_setpoint_button = customtkinter.CTkButton(self.root, text="Setzen", command=self.set_setpoint, font=self.my_font) + self.set_setpoint_button.grid(row=2, column=2, padx=(0, 10), pady=(10, 0), sticky="ew") + + # Multi Setpoints Textbox and Button + self.multi_setpoints_label = customtkinter.CTkLabel(self.root, text="Sollwerteeingabe\n(Winkel;Drehmoment):", font=self.my_font) + self.multi_setpoints_label.grid(row=3, column=0, padx=(10, 5), pady=(10, 0), sticky="w") + + self.multi_setpoints_text = customtkinter.CTkTextbox(self.root, font=self.my_font) + self.multi_setpoints_text.grid(row=3, column=1, padx=(0, 10), pady=(10, 0), sticky="ew") + + self.send_multi_setpoints_button = customtkinter.CTkButton(self.root, text="Sollwerte senden", command=self.send_multi_setpoints, font=self.my_font) + self.send_multi_setpoints_button.grid(row=3, column=2, padx=(0, 10), pady=(10, 0), sticky="ew") + + # Analog Control CheckBox + self.checkbox_var = customtkinter.IntVar() + self.checkbox = customtkinter.CTkCheckBox(self.root, text="Analogsteuerung", variable=self.checkbox_var, command=self.input_source_Switch, font=self.my_font) + self.checkbox.grid(row=4, column=0, columnspan=3, padx=(10, 10), pady=(10, 0), sticky="w") + + # Tare and Demagnetize Buttons + self.tare_button = customtkinter.CTkButton(self.root, text="Drehwinkel tarieren", command=self.tare_angle, font=self.my_font) + self.tare_button.grid(row=5, column=0, padx=(10, 5), pady=(10, 0), sticky="sew") + + self.demag_button = customtkinter.CTkButton(self.root, text="Manuelle Entmagnetisierung", command=self.demagnetize, font=self.my_font) + self.demag_button.grid(row=5, column=1, padx=(0, 5), pady=(10, 0), sticky="sew") + + # Exit Button + self.exit_button = customtkinter.CTkButton(self.root, text="Beenden", command=self.on_closing, font=self.my_font) + self.exit_button.grid(row=5, column=2, padx=(0, 10), pady=(10, 0), sticky="sew") + + # Plot + self.figure, self.ax = plt.subplots() + self.canvas = FigureCanvasTkAgg(self.figure, self.root) + self.canvas.get_tk_widget().grid(row=0, column=3, rowspan=6, columnspan=4, padx=(10, 10), pady=(10, 0), sticky="nsew") + + self.ax.set_title("Drehmoment-Drehwinkel-Kennlinie") + self.ax.set_xlabel("Winkel (°)") + self.ax.set_ylabel("Drehmoment (Nm)") + self.ax.set_xlim(0, 90) + self.ax.set_ylim(0, 50) + + # Current readings labels + self.current_angle = 0 + self.current_torque = 0 + self.analogInput = 0 + + self.angle_label_var = customtkinter.StringVar() + self.angle_label_var.set("Drehwinkel: 0") + self.angle_label = customtkinter.CTkLabel(self.root, textvariable=self.angle_label_var, font=self.my_font) + self.angle_label.grid(row=6, column=3, padx=(10, 5), pady=(10, 0), sticky="sw") + + self.torque_label_var = customtkinter.StringVar() + self.torque_label_var.set("Istwert: 0") + self.torque_label = customtkinter.CTkLabel(self.root, textvariable=self.torque_label_var, font=self.my_font) + self.torque_label.grid(row=6, column=4, padx=(10, 5), pady=(10, 0), sticky="sw") + + self.analogread_label_var = customtkinter.StringVar() + self.analogread_label_var.set("Analogeingang: 0") + self.analogread_label = customtkinter.CTkLabel(self.root, textvariable=self.analogread_label_var, font=self.my_font) + self.analogread_label.grid(row=6, column=5, padx=(10, 5), pady=(10, 0), sticky="sw") + + self.currentSetpoint_label_var = customtkinter.StringVar() + self.currentSetpoint_label_var.set("Aktueller Sollwert: 0") + self.currentSetpoint_label = customtkinter.CTkLabel(self.root, textvariable=self.currentSetpoint_label_var, font=self.my_font) + self.currentSetpoint_label.grid(row=6, column=6, padx=(10, 5), pady=(10, 0), sticky="sw") + + self.root.grid_columnconfigure(0, weight=0) + self.root.grid_columnconfigure(1, weight=0) + self.root.grid_columnconfigure(2, weight=0) + self.root.grid_columnconfigure(3, weight=1) + self.root.grid_columnconfigure(4, weight=1) + self.root.grid_columnconfigure(5, weight=1) + self.root.grid_columnconfigure(6, weight=1) + self.root.grid_rowconfigure(0, weight=0) + self.root.grid_rowconfigure(1, weight=0) + self.root.grid_rowconfigure(2, weight=0) + self.root.grid_rowconfigure(3, weight=0) + self.root.grid_rowconfigure(4, weight=0) + self.root.grid_rowconfigure(5, weight=1) + self.root.grid_rowconfigure(6, weight=0) + + def set_pid_parameters(self): + if self.arduino: + self.selected_pid = self.config_combobox.get() + if self.selected_pid in self.pid_params: + self.pid_flag = True + else: + messagebox.showerror("Error", "Selected PID configuration not found.") + else: + messagebox.showwarning("Warning", "Arduino not connected") + + def load_configurations(self): + try: + with open("conf.txt", "r") as file: + config_name = None + for line in file: + line = line.strip() + if line.startswith("#") or not line: + continue + if "{" in line: + config_name = line.split("{")[0].strip() + self.pid_params[config_name] = {"Kp": 0.0, "Ki": 0.0, "Kd": 0.0} + elif "}" in line: + config_name = None + else: + if config_name: + key, value = line.split(":") + key = key.strip() + value = round(float(value.strip()), 3) + self.pid_params[config_name][key] = value + + self.config_combobox.configure(values=list(self.pid_params.keys())) + if self.pid_params: + self.config_combobox.set(list(self.pid_params.keys())[0]) + self.selected_pid = list(self.pid_params.keys())[0] + except Exception as e: + messagebox.showerror("Error", f"Error loading configurations: {e}") + + def connect_arduino(self): + com_port = self.combobox_value.get() + if com_port: + try: + self.arduino = serial.Serial(com_port, 115200, timeout=1) + messagebox.showinfo("Info", "Connected to Arduino on " + com_port) + except Exception as e: + messagebox.showerror("Error", str(e)) + else: + messagebox.showwarning("Warning", "Please select a COM port") + + def set_setpoint(self): + if self.arduino: + try: + self.setpoint = float(self.setpoint_entry.get()) * 1000 + self.setpoint_flag = True + except ValueError: + messagebox.showerror("Error", "Invalid setpoint value.") + else: + messagebox.showwarning("Warning", "Arduino not connected") + + def send_multi_setpoints(self): + if self.arduino: + setpoints = self.multi_setpoints_text.get("1.0", tk.END).strip().split("\n") + coordinates = [] + for point in setpoints: + try: + angle, torque = map(float, point.split(";")) + coordinates.append((angle, torque)) + except ValueError: + messagebox.showerror("Error", f"Invalid input: {point}") + return + + coordinates.sort() + self.setpoints = coordinates + self.plot_coordinates(coordinates) + self.multi_setpoints_flag = True + else: + messagebox.showwarning("Warning", "Arduino not connected") + + def plot_coordinates(self, coordinates): + angles, torques = zip(*coordinates) + self.ax.clear() + self.ax.plot(angles, torques, 'bo-', label="Setpoints") + self.ax.set_title("Drehmoment-Drehwinkel-Kennlinie") + self.ax.set_xlabel("Winkel (°)") + self.ax.set_ylabel("Drehmoment (Nm)") + self.ax.set_xlim(0, 90) + self.ax.set_ylim(0, 50) + self.ax.legend() + self.canvas.draw() + + def interpolate_coordinates(self, coordinates): + interpolated_points = [] + for i in range(len(coordinates) - 1): + start_angle, start_torque = coordinates[i] + end_angle, end_torque = coordinates[i + 1] + num_points = int((end_angle - start_angle) * 2) + angles = np.linspace(start_angle, end_angle, num_points + 1) + torques = np.linspace(start_torque, end_torque, num_points + 1) + interpolated_points.extend(zip(angles, torques)) + + full_interpolated_points = [(angle, 0) for angle in np.arange(0, 90.5, 0.5)] + for angle, torque in interpolated_points: + index = int(angle * 2) + full_interpolated_points[index] = (angle, torque) + + return full_interpolated_points + + def send_interpolated_points(self): + if self.arduino: + setpoints_command = "u" + for angle, torque in self.setpoints: + torque_mnm = torque * 1000 + setpoints_command += f"{int(angle * 2)},{torque_mnm:.1f};" + setpoints_command = setpoints_command.rstrip(';') + "u\n" + self.arduino.write(setpoints_command.encode()) + + timeout_ctr = time.time() + while(self.arduino.in_waiting == 0 and time.time() - timeout_ctr < 2): + pass + + try: + print(self.arduino.readline()) + + except Exception as e: + print(e) + + def tare_angle(self): + if self.arduino: + self.tare_flag = True + else: + messagebox.showwarning("Warning", "Arduino not connected") + + def demagnetize(self): + if self.arduino: + self.demag_flag = True + else: + messagebox.showwarning("Warning", "Arduino not connected") + + def input_source_Switch(self): + if self.arduino: + self.input_source_flag = True + else: + messagebox.showwarning("Warning", "Arduino not connected") + + def communication_loop(self): + while self.running: + if self.arduino: + self.arduino.read_all() + if self.setpoint_flag: + self.arduino.write(f"s{self.setpoint:.0f},0\n".encode()) + self.setpoint_flag = False + time.sleep(0.1) + if self.multi_setpoints_flag: + self.send_interpolated_points() + self.multi_setpoints_flag = False + time.sleep(0.1) + if self.tare_flag: + self.arduino.write(b'w\n') + self.tare_flag = False + time.sleep(0.1) + if self.demag_flag: + self.arduino.write(b'e\n') + self.demag_flag = False + time.sleep(0.1) + if self.input_source_flag: + self.arduino.write(b'S\n') + self.input_source_flag = False + time.sleep(0.1) + if self.pid_flag: + self.send_pid_parameters() + self.pid_flag = False + + self.arduino.write(b'a\n') + timeout_ctr = time.time() + while(self.arduino.in_waiting == 0 and time.time() - timeout_ctr < 2): + pass + + try: + data = self.arduino.readline().decode(errors='ignore').strip().split(";") + self.current_angle = float(data[0]) / 1000 + self.current_torque = float(data[1]) / 1000 + self.analogInput = float(data[2]) / 1000 + self.currentSetpoint = float(data[3]) / 1000 + + self.angle_label_var.set(f"Drehwinkel: {self.current_angle:.1f} °") + self.torque_label_var.set(f"Istwert: {self.current_torque:.1f} Nm") + self.analogread_label_var.set(f"Analogeingang: {self.analogInput:.1f} V") + self.currentSetpoint_label_var.set(f"Aktueller Sollwert: {self.currentSetpoint:.1f} Nm") + + if self.current_point is not None: + self.current_point.remove() + self.current_point, = self.ax.plot([self.current_angle], [self.current_torque], 'ro') + self.canvas.draw() + except Exception as e: + print(e) + + time.sleep(0.1) + + def send_pid_parameters(self): + if self.selected_pid in self.pid_params: + pid_values = self.pid_params[self.selected_pid] + kp = f"{pid_values['Kp']:.3f}" + ki = f"{pid_values['Ki']:.3f}" + kd = f"{pid_values['Kd']:.3f}" + self.arduino.write(f"p{kp}\n".encode()) + time.sleep(0.1) + self.arduino.write(f"i{ki}\n".encode()) + time.sleep(0.1) + self.arduino.write(f"d{kd}\n".encode()) + messagebox.showinfo("Info", f"PID parameters sent: Kp={kp}, Ki={ki}, Kd={kd}") + else: + messagebox.showerror("Error", "Selected PID configuration not found.") + + def on_closing(self): + self.running = False + if self.arduino: + self.arduino.close() + self.root.quit() + self.root.destroy() + +if __name__ == "__main__": + root = customtkinter.CTk() + app = ArduinoGUI(root) + root.protocol("WM_DELETE_WINDOW", app.on_closing) + root.mainloop()