# # Theme support # # Because of various ttk limitations this app is an unholy mix of Tk and ttk widgets. # So can't use ttk's theme support. So have to change colors manually. # from sys import platform from os.path import join import Tkinter as tk import ttk import tkFont from config import appname, applongname, config if platform == 'win32': import ctypes from ctypes.wintypes import LPCWSTR, DWORD, LPCVOID AddFontResourceEx = ctypes.windll.gdi32.AddFontResourceExW AddFontResourceEx.restypes = [LPCWSTR, DWORD, LPCVOID] FR_PRIVATE = 0x10 FR_NOT_ENUM = 0x20 AddFontResourceEx(join(config.respath, u'EUROCAPS.TTF'), FR_PRIVATE, 0) elif platform == 'linux2': from ctypes import * XID = c_ulong # from X.h: typedef unsigned long XID Window = XID Atom = c_ulong Display = c_void_p # Opaque # Sending ClientMessage to WM using XSendEvent() SubstructureNotifyMask = 1<<19 SubstructureRedirectMask = 1<<20 ClientMessage = 33 _NET_WM_STATE_REMOVE = 0 _NET_WM_STATE_ADD = 1 _NET_WM_STATE_TOGGLE = 2 class XClientMessageEvent_data(Union): _fields_ = [ ('b', c_char * 20), ('s', c_short * 10), ('l', c_long * 5), ] class XClientMessageEvent(Structure): _fields_ = [ ('type', c_int), ('serial', c_ulong), ('send_event', c_int), ('display', POINTER(Display)), ('window', Window), ('message_type', Atom), ('format', c_int), ('data', XClientMessageEvent_data), ] class XEvent(Union): _fields_ = [ ('xclient', XClientMessageEvent), ] xlib = cdll.LoadLibrary('libX11.so.6') XFlush = xlib.XFlush XFlush.argtypes = [POINTER(Display)] XFlush.restype = c_int XInternAtom = xlib.XInternAtom XInternAtom.restype = Atom XInternAtom.argtypes = [POINTER(Display), c_char_p, c_int] XOpenDisplay = xlib.XOpenDisplay XOpenDisplay.argtypes = [c_char_p] XOpenDisplay.restype = POINTER(Display) XQueryTree = xlib.XQueryTree XQueryTree.argtypes = [POINTER(Display), Window, POINTER(Window), POINTER(Window), POINTER(Window), POINTER(c_uint)] XQueryTree.restype = c_int XSendEvent = xlib.XSendEvent XSendEvent.argtypes = [POINTER(Display), Window, c_int, c_long, POINTER(XEvent)] XSendEvent.restype = c_int try: dpy = xlib.XOpenDisplay(None) XA_ATOM = Atom(4) net_wm_state = XInternAtom(dpy, '_NET_WM_STATE', False) net_wm_state_above = XInternAtom(dpy, '_NET_WM_STATE_ABOVE', False) net_wm_state_sticky = XInternAtom(dpy, '_NET_WM_STATE_STICKY', False) net_wm_state_skip_pager = XInternAtom(dpy, '_NET_WM_STATE_SKIP_PAGER', False) net_wm_state_skip_taskbar = XInternAtom(dpy, '_NET_WM_STATE_SKIP_TASKBAR', False) except: dpy = None class _Theme: def __init__(self): self.active = None # Starts out with no theme self.minwidth = None self.widgets = set() self.widgets_pair = [] def register(self, widget): assert isinstance(widget, tk.Widget) or isinstance(widget, tk.BitmapImage), widget if isinstance(widget, tk.Frame) or isinstance(widget, ttk.Frame): for child in widget.winfo_children(): self.register(child) self.widgets.add(widget) def register_alternate(self, pair, gridopts): self.widgets_pair.append((pair, gridopts)) def button_bind(self, widget, command, image=None): widget.bind('', command) widget.bind('', lambda e: self._enter(e, image)) widget.bind('', lambda e: self._leave(e, image)) def _enter(self, event, image): widget = event.widget if widget and widget['state'] != tk.DISABLED: widget.configure(state = tk.ACTIVE) if image: image.configure(foreground = self.current['activeforeground'], background = self.current['activebackground']) def _leave(self, event, image): widget = event.widget if widget and widget['state'] != tk.DISABLED: widget.configure(state = tk.NORMAL) if image: image.configure(foreground = self.current['foreground'], background = self.current['background']) # Set up colors def _colors(self, root, theme): style = ttk.Style() if platform == 'linux2': style.theme_use('clam') # Default dark theme colors if not config.get('dark_text'): config.set('dark_text', '#ff8000') # "Tangerine" in OSX color picker if not config.get('dark_highlight'): config.set('dark_highlight', 'white') if theme: # Dark (r, g, b) = root.winfo_rgb(config.get('dark_text')) self.current = { 'background' : 'grey4', # OSX inactive dark titlebar color 'foreground' : config.get('dark_text'), 'activebackground' : config.get('dark_text'), 'activeforeground' : 'grey4', 'disabledforeground' : '#%02x%02x%02x' % (r/384, g/384, b/384), 'highlight' : config.get('dark_highlight'), 'font' : 'TkDefaultFont', } # Overrides if theme > 1 and not 0x250 < ord(_('Cmdr')[0]) < 0x3000: # Font only supports Latin 1 / Supplement / Extended, and a few General Punctuation and Mathematical Operators self.current['font'] = tkFont.Font(family='Euro Caps', size=9, weight=tkFont.NORMAL) else: # System colors self.current = { 'background' : style.lookup('TLabel', 'background'), 'foreground' : style.lookup('TLabel', 'foreground'), 'activebackground' : style.lookup('TLabel', 'background', ['active']), 'activeforeground' : style.lookup('TLabel', 'foreground', ['active']), 'disabledforeground' : style.lookup('TLabel', 'foreground', ['disabled']), 'highlight' : 'blue', 'font' : 'TkDefaultFont', } # Overrides if platform == 'darwin': self.current['background'] = 'systemMovableModalBackground' elif platform == 'win32': # Menu colors self.current['activebackground'] = 'SystemHighlight' self.current['activeforeground'] = 'SystemHighlightText' # Apply configured theme def apply(self, root): theme = config.getint('theme') self._colors(root, theme) # Apply colors for widget in self.widgets: if isinstance(widget, tk.BitmapImage): # not a widget widget.configure(foreground = self.current['foreground'], background = self.current['background']) elif 'cursor' in widget.keys() and str(widget['cursor']) not in ['', 'arrow']: # Hack - highlight widgets like HyperlinkLabel with a non-default cursor widget.configure(foreground = self.current['highlight'], background = self.current['background'], font = self.current['font']) elif 'activeforeground' in widget.keys(): # e.g. tk.Button, tk.Label, tk.Menu widget.configure(foreground = self.current['foreground'], background = self.current['background'], activeforeground = self.current['activeforeground'], activebackground = self.current['activebackground'], disabledforeground = self.current['disabledforeground'], font = self.current['font'] ) elif 'foreground' in widget.keys(): # e.g. ttk.Label widget.configure(foreground = self.current['foreground'], background = self.current['background'], font = self.current['font']) elif 'background' in widget.keys(): # e.g. Frame widget.configure(background = self.current['background']) widget.configure(highlightbackground = self.current['disabledforeground']) for pair, gridopts in self.widgets_pair: for widget in pair: widget.grid_remove() if isinstance(pair[0], tk.Menu): if theme: root['menu'] = '' pair[theme].grid(**gridopts) else: root['menu'] = pair[0] else: pair[theme].grid(**gridopts) if self.active == theme: return # Don't need to mess with the window manager else: self.active = theme if platform == 'darwin': from AppKit import NSApplication, NSAppearance, NSMiniaturizableWindowMask, NSResizableWindowMask root.update_idletasks() # need main window to be created appearance = NSAppearance.appearanceNamed_(theme and 'NSAppearanceNameVibrantDark' or 'NSAppearanceNameAqua') for window in NSApplication.sharedApplication().windows(): window.setStyleMask_(window.styleMask() & ~(NSMiniaturizableWindowMask | NSResizableWindowMask)) # disable zoom window.setAppearance_(appearance) elif platform == 'win32': GWL_STYLE = -16 WS_MAXIMIZEBOX = 0x00010000 # tk8.5.9/win/tkWinWm.c:342 GWL_EXSTYLE = -20 WS_EX_APPWINDOW = 0x00040000 WS_EX_LAYERED = 0x00080000 GetWindowLongW = ctypes.windll.user32.GetWindowLongW SetWindowLongW = ctypes.windll.user32.SetWindowLongW root.overrideredirect(theme and 1 or 0) root.attributes("-transparentcolor", theme > 1 and 'grey4' or '') root.withdraw() root.update_idletasks() # Size and windows styles get recalculated here hwnd = ctypes.windll.user32.GetParent(root.winfo_id()) SetWindowLongW(hwnd, GWL_STYLE, GetWindowLongW(hwnd, GWL_STYLE) & ~WS_MAXIMIZEBOX) # disable maximize SetWindowLongW(hwnd, GWL_EXSTYLE, theme > 1 and WS_EX_APPWINDOW|WS_EX_LAYERED or WS_EX_APPWINDOW) # Add to taskbar root.deiconify() root.wait_visibility() # need main window to be displayed before returning else: root.withdraw() # https://www.tcl-lang.org/man/tcl/TkCmd/wm.htm#M19 # https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#STACKINGORDER root.attributes('-type', theme and 'splash' or 'normal') root.update_idletasks() # Size gets recalculated here root.deiconify() root.wait_visibility() # need main window to be displayed before returning if dpy and theme: # Try to display in the taskbar xroot = Window() parent = Window() children = Window() nchildren = c_uint() XQueryTree(dpy, root.winfo_id(), byref(xroot), byref(parent), byref(children), byref(nchildren)) # https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm140200472615568 xevent = XEvent(xclient = XClientMessageEvent(ClientMessage, 0, 0, None, parent, net_wm_state, 32, XClientMessageEvent_data(l = (_NET_WM_STATE_REMOVE, net_wm_state_skip_pager, net_wm_state_skip_taskbar, 1, 0)))) XSendEvent(dpy, xroot, False, SubstructureRedirectMask | SubstructureNotifyMask, byref(xevent)) xevent = XEvent(xclient = XClientMessageEvent(ClientMessage, 0, 0, None, parent, net_wm_state, 32, XClientMessageEvent_data(l = (_NET_WM_STATE_REMOVE, net_wm_state_sticky, 0, 1, 0)))) XSendEvent(dpy, xroot, False, SubstructureRedirectMask | SubstructureNotifyMask, byref(xevent)) XFlush(dpy) if not self.minwidth: self.minwidth = root.winfo_width() # Minimum width = width on first creation root.minsize(self.minwidth, -1) # singleton theme = _Theme()