2022-02-21 17:35:08 +03:00
import tkinter
from distutils . version import StrictVersion as Version
import sys
import os
import platform
import ctypes
2022-05-02 00:29:14 +03:00
import re
2022-05-16 17:51:19 +03:00
from typing import Union , Tuple
2022-02-21 17:35:08 +03:00
2022-04-09 22:45:57 +03:00
from . . appearance_mode_tracker import AppearanceModeTracker
2022-05-17 22:44:59 +03:00
from . . theme_manager import ThemeManager
from . . settings import Settings
2022-05-02 00:29:14 +03:00
from . . scaling_tracker import ScalingTracker
2022-02-21 17:35:08 +03:00
class CTkToplevel ( tkinter . Toplevel ) :
2022-10-02 04:23:10 +03:00
"""
Toplevel window with dark titlebar on Windows and macOS .
For detailed information check out the documentation .
"""
2022-02-21 17:35:08 +03:00
def __init__ ( self , * args ,
2022-10-02 04:23:10 +03:00
fg_color : Union [ str , Tuple [ str , str ] ] = " default_theme " ,
2022-02-21 17:35:08 +03:00
* * kwargs ) :
2022-10-02 04:23:10 +03:00
self . _enable_macos_dark_title_bar ( )
2022-02-21 17:35:08 +03:00
super ( ) . __init__ ( * args , * * kwargs )
2022-10-02 04:23:10 +03:00
self . _appearance_mode = AppearanceModeTracker . get_mode ( ) # 0: "Light" 1: "Dark"
2022-02-21 17:35:08 +03:00
2022-05-02 00:29:14 +03:00
# add set_scaling method to callback list of ScalingTracker for automatic scaling changes
2022-10-02 04:23:10 +03:00
ScalingTracker . add_widget ( self . _set_scaling , self )
self . _window_scaling = ScalingTracker . get_window_scaling ( self )
2022-05-02 00:29:14 +03:00
2022-10-02 04:23:10 +03:00
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)
2022-05-02 00:29:14 +03:00
2022-10-02 04:23:10 +03:00
self . _fg_color = ThemeManager . theme [ " color " ] [ " window_bg_color " ] if fg_color == " default_theme " else fg_color
2022-02-21 17:35:08 +03:00
if " bg " in kwargs :
2022-10-02 04:23:10 +03:00
self . _fg_color = kwargs . pop ( " bg " )
2022-02-21 17:35:08 +03:00
elif " background " in kwargs :
2022-10-02 04:23:10 +03:00
self . fg_color = kwargs . pop ( " background " )
2022-02-21 17:35:08 +03:00
2022-04-07 00:09:54 +03:00
# add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes
2022-10-02 04:23:10 +03:00
AppearanceModeTracker . add ( self . _set_appearance_mode , self )
super ( ) . configure ( bg = ThemeManager . single_color ( self . _fg_color , self . _appearance_mode ) )
2022-02-23 00:38:40 +03:00
super ( ) . title ( " CTkToplevel " )
2022-02-21 17:35:08 +03:00
2022-10-02 04:23:10 +03:00
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
2022-08-19 01:13:00 +03:00
2022-02-21 17:35:08 +03:00
if sys . platform . startswith ( " win " ) :
2022-10-02 04:23:10 +03:00
if self . _appearance_mode == 1 :
self . _windows_set_titlebar_color ( " dark " )
2022-02-21 17:35:08 +03:00
else :
2022-10-02 04:23:10 +03:00
self . _windows_set_titlebar_color ( " light " )
2022-02-21 17:35:08 +03:00
2022-10-02 04:23:10 +03:00
self . bind ( ' <Configure> ' , self . _update_dimensions_event )
2022-05-22 02:55:58 +03:00
2022-10-02 04:23:10 +03:00
def _update_dimensions_event ( self , event = None ) :
2022-05-02 00:29:14 +03:00
detected_width = self . winfo_width ( ) # detect current window size
detected_height = self . winfo_height ( )
2022-10-02 04:23:10 +03:00
if self . _current_width != round ( detected_width / self . _window_scaling ) or self . _current_height != round ( detected_height / self . _window_scaling ) :
self . _current_width = round ( detected_width / self . _window_scaling ) # adjust current size according to new size given by event
self . _current_height = round ( detected_height / self . _window_scaling ) # _current_width and _current_height are independent of the scale
2022-05-02 00:29:14 +03:00
2022-10-02 04:23:10 +03:00
def _set_scaling ( self , new_widget_scaling , new_spacing_scaling , new_window_scaling ) :
self . _window_scaling = new_window_scaling
2022-05-02 00:29:14 +03:00
2022-05-22 02:55:58 +03:00
# force new dimensions on window by using min, max, and geometry
2022-10-02 04:23:10 +03:00
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 ) )
2022-05-22 02:55:58 +03:00
super ( ) . geometry (
2022-10-02 04:23:10 +03:00
f " { self . _apply_window_scaling ( self . _current_width ) } x " + f " { self . _apply_window_scaling ( self . _current_height ) } " )
2022-05-16 17:51:19 +03:00
2022-05-22 02:55:58 +03:00
# set new scaled min and max with 400ms delay (otherwise it won't work for some reason)
2022-10-02 04:23:10 +03:00
self . after ( 400 , self . _set_scaled_min_max )
2022-05-02 00:29:14 +03:00
2022-10-02 04:23:10 +03:00
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 ) )
2022-05-16 17:51:19 +03:00
2022-07-17 22:41:13 +03:00
def geometry ( self , geometry_string : str = None ) :
if geometry_string is not None :
2022-10-02 04:23:10 +03:00
super ( ) . geometry ( self . _apply_geometry_scaling ( geometry_string ) )
2022-05-02 00:29:14 +03:00
2022-07-17 22:41:13 +03:00
# update width and height attributes
2022-10-02 04:23:10 +03:00
width , height , x , y = self . _parse_geometry_string ( geometry_string )
2022-08-05 21:38:05 +03:00
if width is not None and height is not None :
2022-10-02 04:23:10 +03:00
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 ) )
2022-07-17 22:41:13 +03:00
else :
2022-10-02 04:23:10 +03:00
return self . _reverse_geometry_scaling ( super ( ) . geometry ( ) )
2022-05-02 00:29:14 +03:00
2022-08-05 21:38:05 +03:00
@staticmethod
2022-10-02 04:23:10 +03:00
def _parse_geometry_string ( geometry_string : str ) - > tuple :
2022-08-05 21:38:05 +03:00
# index: 1 2 3 4 5 6
# regex group structure: ('<width>x<height>', '<width>', '<height>', '+-<x>+-<y>', '-<x>', '-<y>')
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
2022-10-02 04:23:10 +03:00
def _apply_geometry_scaling ( self , geometry_string : str ) - > str :
width , height , x , y = self . _parse_geometry_string ( geometry_string )
2022-08-05 21:38:05 +03:00
if x is None and y is None : # no <x> and <y> in geometry_string
2022-10-02 04:23:10 +03:00
return f " { round ( width * self . _window_scaling ) } x { round ( height * self . _window_scaling ) } "
2022-08-05 21:38:05 +03:00
elif width is None and height is None : # no <width> and <height> in geometry_string
return f " + { x } + { y } "
else :
2022-10-02 04:23:10 +03:00
return f " { round ( width * self . _window_scaling ) } x { round ( height * self . _window_scaling ) } + { x } + { y } "
2022-08-05 21:38:05 +03:00
2022-10-02 04:23:10 +03:00
def _reverse_geometry_scaling ( self , scaled_geometry_string : str ) - > str :
width , height , x , y = self . _parse_geometry_string ( scaled_geometry_string )
2022-08-05 21:38:05 +03:00
if x is None and y is None : # no <x> and <y> in geometry_string
2022-10-02 04:23:10 +03:00
return f " { round ( width / self . _window_scaling ) } x { round ( height / self . _window_scaling ) } "
2022-08-05 21:38:05 +03:00
elif width is None and height is None : # no <width> and <height> in geometry_string
return f " + { x } + { y } "
else :
2022-10-02 04:23:10 +03:00
return f " { round ( width / self . _window_scaling ) } x { round ( height / self . _window_scaling ) } + { x } + { y } "
2022-08-05 21:38:05 +03:00
2022-10-02 04:23:10 +03:00
def _apply_window_scaling ( self , value ) :
2022-08-05 21:38:05 +03:00
if isinstance ( value , ( int , float ) ) :
2022-10-02 04:23:10 +03:00
return int ( value * self . _window_scaling )
2022-08-05 21:38:05 +03:00
else :
return value
2022-02-21 17:35:08 +03:00
def destroy ( self ) :
2022-10-02 04:23:10 +03:00
AppearanceModeTracker . remove ( self . _set_appearance_mode )
ScalingTracker . remove_window ( self . _set_scaling , self )
self . _disable_macos_dark_title_bar ( )
2022-02-21 17:35:08 +03:00
super ( ) . destroy ( )
2022-08-19 01:13:00 +03:00
def withdraw ( self ) :
2022-10-02 04:23:10 +03:00
if self . _windows_set_titlebar_color_called :
self . _withdraw_called_after_windows_set_titlebar_color = True
2022-08-19 01:13:00 +03:00
super ( ) . withdraw ( )
def iconify ( self ) :
2022-10-02 04:23:10 +03:00
if self . _windows_set_titlebar_color_called :
self . _iconify_called_after_windows_set_titlebar_color = True
2022-08-19 01:13:00 +03:00
super ( ) . iconify ( )
2022-10-04 00:50:59 +03:00
def resizable ( self , width : bool = None , height : bool = None ) :
super ( ) . resizable ( width , height )
self . _last_resizable_args = ( [ ] , { " width " : width , " height " : height } )
2022-02-21 17:35:08 +03:00
if sys . platform . startswith ( " win " ) :
2022-10-02 04:23:10 +03:00
if self . _appearance_mode == 1 :
self . after ( 10 , lambda : self . _windows_set_titlebar_color ( " dark " ) )
2022-02-21 17:35:08 +03:00
else :
2022-10-02 04:23:10 +03:00
self . after ( 10 , lambda : self . _windows_set_titlebar_color ( " light " ) )
2022-02-21 17:35:08 +03:00
2022-05-16 17:51:19 +03:00
def minsize ( self , width = None , height = None ) :
2022-10-02 04:23:10 +03:00
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 ) )
2022-05-16 17:51:19 +03:00
def maxsize ( self , width = None , height = None ) :
2022-10-02 04:23:10 +03:00
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 ) )
2022-05-16 17:51:19 +03:00
2022-02-21 17:35:08 +03:00
def configure ( self , * args , * * kwargs ) :
bg_changed = False
if " bg " in kwargs :
2022-10-02 04:23:10 +03:00
self . _fg_color = kwargs [ " bg " ]
2022-02-21 17:35:08 +03:00
bg_changed = True
2022-10-02 04:23:10 +03:00
kwargs [ " bg " ] = ThemeManager . single_color ( self . _fg_color , self . _appearance_mode )
2022-02-21 17:35:08 +03:00
elif " background " in kwargs :
2022-10-02 04:23:10 +03:00
self . _fg_color = kwargs [ " background " ]
2022-02-21 17:35:08 +03:00
bg_changed = True
2022-10-02 04:23:10 +03:00
kwargs [ " background " ] = ThemeManager . single_color ( self . _fg_color , self . _appearance_mode )
2022-02-21 17:35:08 +03:00
elif " fg_color " in kwargs :
2022-10-02 04:23:10 +03:00
self . _fg_color = kwargs . pop ( " fg_color " )
kwargs [ " bg " ] = ThemeManager . single_color ( self . _fg_color , self . _appearance_mode )
2022-02-21 17:35:08 +03:00
bg_changed = True
elif len ( args ) > 0 and type ( args [ 0 ] ) == dict :
if " bg " in args [ 0 ] :
2022-10-02 04:23:10 +03:00
self . _fg_color = args [ 0 ] [ " bg " ]
2022-02-21 17:35:08 +03:00
bg_changed = True
2022-10-02 04:23:10 +03:00
args [ 0 ] [ " bg " ] = ThemeManager . single_color ( self . _fg_color , self . _appearance_mode )
2022-02-21 17:35:08 +03:00
elif " background " in args [ 0 ] :
2022-10-02 04:23:10 +03:00
self . _fg_color = args [ 0 ] [ " background " ]
2022-02-21 17:35:08 +03:00
bg_changed = True
2022-10-02 04:23:10 +03:00
args [ 0 ] [ " background " ] = ThemeManager . single_color ( self . _fg_color , self . _appearance_mode )
2022-02-21 17:35:08 +03:00
if bg_changed :
2022-05-06 15:33:38 +03:00
from . . widgets . widget_base_class import CTkBaseClass
2022-02-21 17:35:08 +03:00
for child in self . winfo_children ( ) :
2022-10-02 04:23:10 +03:00
if isinstance ( child , CTkBaseClass ) and hasattr ( child , " _fg_color " ) :
child . configure ( bg_color = self . _fg_color )
2022-02-21 17:35:08 +03:00
super ( ) . configure ( * args , * * kwargs )
2022-10-03 01:33:06 +03:00
def cget ( self , attribute_name : str ) - > any :
if attribute_name == " fg_color " :
return self . _fg_color
else :
return super ( ) . cget ( attribute_name )
2022-02-21 17:35:08 +03:00
@staticmethod
2022-10-02 04:23:10 +03:00
def _enable_macos_dark_title_bar ( ) :
2022-05-17 22:44:59 +03:00
if sys . platform == " darwin " and not Settings . deactivate_macos_window_header_manipulation : # macOS
2022-02-21 17:35:08 +03:00
if Version ( platform . python_version ( ) ) < Version ( " 3.10 " ) :
if Version ( tkinter . Tcl ( ) . call ( " info " , " patchlevel " ) ) > = Version ( " 8.6.9 " ) : # Tcl/Tk >= 8.6.9
os . system ( " defaults write -g NSRequiresAquaSystemAppearance -bool No " )
@staticmethod
2022-10-02 04:23:10 +03:00
def _disable_macos_dark_title_bar ( ) :
2022-05-17 22:44:59 +03:00
if sys . platform == " darwin " and not Settings . deactivate_macos_window_header_manipulation : # macOS
2022-02-21 17:35:08 +03:00
if Version ( platform . python_version ( ) ) < Version ( " 3.10 " ) :
if Version ( tkinter . Tcl ( ) . call ( " info " , " patchlevel " ) ) > = Version ( " 8.6.9 " ) : # Tcl/Tk >= 8.6.9
os . system ( " defaults delete -g NSRequiresAquaSystemAppearance " )
# This command reverts the dark-mode setting for all programs.
2022-10-02 04:23:10 +03:00
def _windows_set_titlebar_color ( self , color_mode : str ) :
2022-02-21 17:35:08 +03:00
"""
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
"""
2022-05-17 22:44:59 +03:00
if sys . platform . startswith ( " win " ) and not Settings . deactivate_windows_window_header_manipulation :
2022-02-21 17:35:08 +03:00
2022-10-02 04:23:10 +03:00
self . _state_before_windows_set_titlebar_color = self . state ( )
2022-02-21 17:35:08 +03:00
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 )
2022-10-02 04:23:10 +03:00
self . _windows_set_titlebar_color_called = True
self . after ( 5 , self . _revert_withdraw_after_windows_set_titlebar_color )
2022-08-19 01:13:00 +03:00
2022-10-02 04:23:10 +03:00
def _revert_withdraw_after_windows_set_titlebar_color ( self ) :
2022-08-19 01:13:00 +03:00
""" if in a short time (5ms) after """
2022-10-02 04:23:10 +03:00
if self . _windows_set_titlebar_color_called :
2022-08-19 01:13:00 +03:00
2022-10-02 04:23:10 +03:00
if self . _withdraw_called_after_windows_set_titlebar_color :
2022-08-19 01:13:00 +03:00
pass # leave it withdrawed
2022-10-02 04:23:10 +03:00
elif self . _iconify_called_after_windows_set_titlebar_color :
2022-08-19 01:13:00 +03:00
super ( ) . iconify ( )
else :
2022-10-02 04:23:10 +03:00
if self . _state_before_windows_set_titlebar_color == " normal " :
2022-08-19 01:13:00 +03:00
self . deiconify ( )
2022-10-02 04:23:10 +03:00
elif self . _state_before_windows_set_titlebar_color == " iconic " :
2022-08-19 01:13:00 +03:00
self . iconify ( )
2022-10-02 04:23:10 +03:00
elif self . _state_before_windows_set_titlebar_color == " zoomed " :
2022-08-19 01:13:00 +03:00
self . state ( " zoomed " )
else :
2022-10-02 04:23:10 +03:00
self . state ( self . _state_before_windows_set_titlebar_color ) # other states
2022-08-19 01:13:00 +03:00
2022-10-02 04:23:10 +03:00
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
2022-02-21 17:35:08 +03:00
2022-10-02 04:23:10 +03:00
def _set_appearance_mode ( self , mode_string ) :
2022-02-21 17:35:08 +03:00
if mode_string . lower ( ) == " dark " :
2022-10-02 04:23:10 +03:00
self . _appearance_mode = 1
2022-02-21 17:35:08 +03:00
elif mode_string . lower ( ) == " light " :
2022-10-02 04:23:10 +03:00
self . _appearance_mode = 0
2022-02-21 17:35:08 +03:00
if sys . platform . startswith ( " win " ) :
2022-10-02 04:23:10 +03:00
if self . _appearance_mode == 1 :
self . _windows_set_titlebar_color ( " dark " )
2022-02-21 17:35:08 +03:00
else :
2022-10-02 04:23:10 +03:00
self . _windows_set_titlebar_color ( " light " )
2022-02-21 17:35:08 +03:00
2022-10-02 04:23:10 +03:00
super ( ) . configure ( bg = ThemeManager . single_color ( self . _fg_color , self . _appearance_mode ) )