diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 9b1ad36c..407f8cd0 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -43,6 +43,7 @@ import plug from edproxy import edproxy from hotkey import hotkeymgr from monitor import monitor +from theme import theme EDDB = eddb.EDDB() @@ -65,62 +66,65 @@ class AppWindow: self.w.rowconfigure(0, weight=1) self.w.columnconfigure(0, weight=1) + # Special handling for overrideredict + self.w.bind("", self.onmap) + plug.load_plugins() - if platform == 'win32': - self.w.wm_iconbitmap(default='EDMarketConnector.ico') - elif platform == 'linux2': - from PIL import Image, ImageTk - icon = ImageTk.PhotoImage(Image.open("EDMarketConnector.png")) - self.w.tk.call('wm', 'iconphoto', self.w, '-default', icon) - style = ttk.Style() - style.theme_use('clam') - elif platform=='darwin': - # Default ttk font choice looks bad on El Capitan - font = tkFont.Font(family='TkDefaultFont', size=13, weight=tkFont.NORMAL) - style = ttk.Style() - style.configure('TLabel', font=font) - style.configure('TButton', font=font) - style.configure('TLabelframe.Label', font=font) - style.configure('TCheckbutton', font=font) - style.configure('TRadiobutton', font=font) - style.configure('TEntry', font=font) + if platform != 'darwin': + if platform == 'win32': + self.w.wm_iconbitmap(default='EDMarketConnector.ico') + else: + from PIL import Image, ImageTk + self.w.tk.call('wm', 'iconphoto', self.w, '-default', ImageTk.PhotoImage(Image.open("EDMarketConnector.png"))) + self.theme_icon = tk.PhotoImage(data = 'R0lGODlhEAAQAMYAAAAAAAEAAAEBAQICAgQEAwYFBAsHBAoIBgwIBAwIBQ0IBA8JBBAJBBAKBRMMBRkPBhoQBykWCSoWCCoXCTsfCzwfCkAhDEIjDD8kC0AlDEEmC0EmDEcoDk4oDU8pDU4qEFMrD1ktDlotD1ouD1g0EWAyEWU0EV03EmA4EWo2EW03EWQ6Emw4EWo9FGo+E3Y8FH5AFH1IFoBJFo1RGo1SGY1SGpBTGZFTGZJTGZhYG6piHa1kHa5kHbBkHr9uIMt0IgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEKAEAALAAAAAAQABAAAAd7gACCg4SFhoeHGCiIhRs5JwMCkpKGGTIFODaaNjc/D4QaMQMAk5MuEIQOO6OFAiscCIQNPQk8NTO4NDofLwayPi0mIMPDLAcqvoIBDiQWkaUCAykKhAsXAoYCHRKEDDAjIyIiIyEEHhHYhAPr7BQlE+mMABXo8oTx9oWBADs=') + self.theme_minimize = tk.BitmapImage(data = '#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f,\n 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n') + self.theme_close = tk.BitmapImage(data = '#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x0c, 0x30, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,\n 0xe0, 0x07, 0xc0, 0x03, 0xc0, 0x03, 0xe0, 0x07, 0x70, 0x0e, 0x38, 0x1c,\n 0x1c, 0x38, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00 };\n') - frame = ttk.Frame(self.w, name=appname.lower()) + frame = tk.Frame(self.w, name=appname.lower()) frame.grid(sticky=tk.NSEW) frame.columnconfigure(1, weight=1) - ttk.Label(frame, text=_('Cmdr')+':').grid(row=0, column=0, sticky=tk.W) # Main window - ttk.Label(frame, text=_('System')+':').grid(row=1, column=0, sticky=tk.W) # Main window - ttk.Label(frame, text=_('Station')+':').grid(row=2, column=0, sticky=tk.W) # Main window + tk.Label(frame, text=_('Cmdr')+':').grid(row=1, column=0, sticky=tk.W) # Main window + tk.Label(frame, text=_('System')+':').grid(row=2, column=0, sticky=tk.W) # Main window + tk.Label(frame, text=_('Station')+':').grid(row=3, column=0, sticky=tk.W) # Main window - self.cmdr = ttk.Label(frame, width=-21) + self.cmdr = tk.Label(frame, anchor=tk.W) self.system = HyperlinkLabel(frame, compound=tk.RIGHT, url = self.system_url, popup_copy = True) self.station = HyperlinkLabel(frame, url = self.station_url, popup_copy = lambda x: x!=self.STATION_UNDOCKED) - self.cmdr.grid(row=0, column=1, sticky=tk.EW) - self.system.grid(row=1, column=1, sticky=tk.EW) - self.station.grid(row=2, column=1, sticky=tk.EW) + self.cmdr.grid(row=1, column=1, sticky=tk.EW) + self.system.grid(row=2, column=1, sticky=tk.EW) + self.station.grid(row=3, column=1, sticky=tk.EW) for plugname in plug.PLUGINS: appitem = plug.get_plugin_app(plugname, frame) if appitem: appitem.grid(columnspan=2, sticky=tk.W) - self.button = ttk.Button(frame, name='update', text=_('Update'), command=self.getandsend, default=tk.ACTIVE, state=tk.DISABLED) # Update button in main window - self.status = ttk.Label(frame, name='status', width=-25) - self.button.grid(columnspan=2, sticky=tk.NSEW) + minwidth = platform == 'darwin' and 32 or 28 + self.button = ttk.Button(frame, text=_('Update'), width=minwidth, command=self.getandsend, default=tk.ACTIVE, state=tk.DISABLED) # Update button in main window + self.theme_button = tk.Label(frame, text=_('Update'), width=minwidth, state=tk.DISABLED) # Update button in main window + self.status = tk.Label(frame, name='status', anchor=tk.W) + row = frame.grid_size()[1] + self.button.grid(row=row, columnspan=2, sticky=tk.NSEW) + self.theme_button.grid(row=row, columnspan=2, sticky=tk.NSEW) + theme.register_alternate((self.button, self.theme_button), {'row':row, 'columnspan':2, 'sticky':tk.NSEW}) self.status.grid(columnspan=2, sticky=tk.EW) + theme.button_bind(self.theme_button, self.getandsend) self.w.bind('', self.getandsend) self.w.bind('', self.getandsend) for child in frame.winfo_children(): - child.grid_configure(padx=5, pady=(platform=='darwin' and 3 or 2)) + child.grid_configure(padx=5, pady=(platform=='win32' and 1 or 3)) menubar = tk.Menu() if platform=='darwin': - from Foundation import NSBundle + # Can't handle (de)iconify if topmost is set, so suppress iconify button + # http://wiki.tcl.tk/13428 and p15 of https://developer.apple.com/legacy/library/documentation/Carbon/Conceptual/HandlingWindowsControls/windowscontrols.pdf + root.call('tk::unsupported::MacWindowStyle', 'style', root, 'document', 'closeBox horizontalZoom resizable') + # https://www.tcl.tk/man/tcl/TkCmd/menu.htm apple_menu = tk.Menu(menubar, name='apple') apple_menu.add_command(label=_("About {APP}").format(APP=applongname), command=lambda:self.w.call('tk::mac::standardAboutPanel')) # App menu entry on OSX @@ -135,6 +139,7 @@ class AppWindow: menubar.add_cascade(label=_('View'), menu=self.view_menu) # Menu title on OSX window_menu = tk.Menu(menubar, name='window') menubar.add_cascade(label=_('Window'), menu=window_menu) # Menu title on OSX + self.w['menu'] = menubar # https://www.tcl.tk/man/tcl/TkCmd/tk_mac.htm self.w.call('set', 'tk::mac::useCompatibilityMetrics', '0') self.w.createcommand('tkAboutDialog', lambda:self.w.call('tk::mac::standardAboutPanel')) @@ -157,34 +162,53 @@ class AppWindow: self.always_ontop = tk.BooleanVar(value = config.getint('always_ontop')) system_menu = tk.Menu(menubar, name='system', tearoff=tk.FALSE) system_menu.add_separator() - system_menu.add_checkbutton(label=_('Always on top'), variable = self.always_ontop, command=self.ontop_changed) # System menu entry on Windows + system_menu.add_checkbutton(label=_('Always on top'), variable = self.always_ontop, command=self.ontop_changed) # Appearance setting menubar.add_cascade(menu=system_menu) - self.w.wm_attributes('-topmost', self.always_ontop.get()) self.w.bind('', self.copy) self.w.protocol("WM_DELETE_WINDOW", self.onexit) + theme.register(menubar) # menus and children aren't automatically registered + theme.register(file_menu) + theme.register(self.edit_menu) - if platform == 'linux2': - # Fix up menu to use same styling as everything else - (fg, bg, afg, abg) = (style.lookup('TLabel.label', 'foreground'), - style.lookup('TLabel.label', 'background'), - style.lookup('TButton.label', 'foreground', ['active']), - style.lookup('TButton.label', 'background', ['active'])) - menubar.configure( fg = fg, bg = bg, activeforeground = afg, activebackground = abg) - file_menu.configure(fg = fg, bg = bg, activeforeground = afg, activebackground = abg) - self.edit_menu.configure(fg = fg, bg = bg, activeforeground = afg, activebackground = abg) - self.w['menu'] = menubar + # Alternate title bar and menu for dark theme + theme_menubar = tk.Frame(frame) + theme_menubar.columnconfigure(2, weight=1) + theme_titlebar = tk.Label(theme_menubar, text=applongname, image=self.theme_icon, anchor=tk.W, compound=tk.LEFT) + theme_titlebar.grid(columnspan=3, sticky=tk.NSEW) + self.drag_offset = None + theme_titlebar.bind('', self.drag_start) + theme_titlebar.bind('', self.drag_continue) + theme_titlebar.bind('', self.drag_end) + if platform == 'win32': # Can't work out how to deiconify on Linux + theme_minimize = tk.Label(theme_menubar, image=self.theme_minimize) + theme_minimize.grid(row=0, column=3) + theme.button_bind(theme_minimize, self.oniconify, image=self.theme_minimize) + theme_close = tk.Label(theme_menubar, image=self.theme_close) + theme_close.grid(row=0, column=4) + theme.button_bind(theme_close, self.onexit, image=self.theme_close) + theme_file_menu = tk.Label(theme_menubar, text=_('File'), anchor=tk.W) # Menu title on Windows + theme_file_menu.grid(row=1, column=0, padx=5, sticky=tk.W) + theme.button_bind(theme_file_menu, lambda e: file_menu.tk_popup(e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height())) + theme_edit_menu = tk.Label(theme_menubar, text=_('Edit'), anchor=tk.W) # Menu title + theme_edit_menu.grid(row=1, column=1, sticky=tk.W) + theme.button_bind(theme_edit_menu, lambda e: self.edit_menu.tk_popup(e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height())) + theme.register_highlight(theme_titlebar) + theme.register(self.theme_minimize) # images aren't automatically registered + theme.register(self.theme_close) + theme.register_alternate((menubar, theme_menubar), {'row':0, 'columnspan':2, 'sticky':tk.NSEW}) # update geometry if config.get('geometry'): match = re.match('\+([\-\d]+)\+([\-\d]+)', config.get('geometry')) if match and (platform!='darwin' or int(match.group(2))>0): # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 self.w.geometry(config.get('geometry')) - self.w.update_idletasks() - self.w.wait_visibility() - (w, h) = (self.w.winfo_width(), self.w.winfo_height()) - self.w.minsize(w, h) # Minimum size = initial size - if platform != 'linux2': # update_idletasks() doesn't allow for the menubar on Linux - self.w.maxsize(-1, h) # Maximum height = initial height + self.w.attributes('-topmost', config.getint('always_ontop') and 1 or 0) + self.w.resizable(tk.TRUE, tk.FALSE) + + theme.register(frame) + theme.register_highlight(self.system) + theme.register_highlight(self.station) + theme.apply(self.w) # Load updater after UI creation (for WinSparkle) import update @@ -212,7 +236,7 @@ class AppWindow: # call after credentials have changed def login(self): self.status['text'] = _('Logging in...') - self.button['state'] = tk.DISABLED + self.button['state'] = self.theme_button['state'] = tk.DISABLED self.w.update_idletasks() try: self.session.login(config.get('username'), config.get('password')) @@ -250,7 +274,7 @@ class AppWindow: config.save() # Save settings now for use by command-line app except Exception as e: if __debug__: print_exc() - self.button['state'] = tk.NORMAL + self.button['state'] = self.theme_button['state'] = tk.NORMAL self.status['text'] = unicode(e) else: return self.getandsend() # try again @@ -270,7 +294,7 @@ class AppWindow: self.cmdr['text'] = self.system['text'] = self.station['text'] = '' self.system['image'] = '' self.status['text'] = _('Fetching data...') - self.button['state'] = tk.DISABLED + self.theme_button['state'] = tk.DISABLED self.edit_menu.entryconfigure(_('Copy'), state=tk.DISABLED) self.w.update_idletasks() @@ -494,11 +518,11 @@ class AppWindow: def cooldown(self): if time() < self.holdofftime: - self.button['text'] = _('cooldown {SS}s').format(SS = int(self.holdofftime - time())) # Update button in main window + self.button['text'] = self.theme_button['text'] = _('cooldown {SS}s').format(SS = int(self.holdofftime - time())) # Update button in main window self.w.after(1000, self.cooldown) else: - self.button['text'] = _('Update') # Update button in main window - self.button['state'] = tk.NORMAL + self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window + self.button['state'] = self.theme_button['state'] = tk.NORMAL def ontop_changed(self, event=None): config.set('always_ontop', self.always_ontop.get()) @@ -519,6 +543,27 @@ class AppWindow: self.session.close() self.w.destroy() + def drag_start(self, event): + self.drag_offset = (event.x_root - self.w.winfo_rootx(), event.y_root - self.w.winfo_rooty()) + + def drag_continue(self, event): + if self.drag_offset: + self.w.geometry('+%d+%d' % (event.x_root - self.drag_offset[0], event.y_root - self.drag_offset[1])) + + def drag_end(self, event): + self.drag_offset = None + + def oniconify(self, event=None): + self.w.overrideredirect(0) # Can't iconize while overrideredirect + self.w.iconify() + self.w.update_idletasks() # Size and windows styles get recalculated here + self.w.wait_visibility() # Need main window to be re-created before returning + theme.active = None # So theme will be re-applied on map + + def onmap(self, event=None): + if event.widget == self.w: + theme.apply(self.w) + # Run the app if __name__ == "__main__": diff --git a/L10n/cs.strings b/L10n/cs.strings index 183c710b..74754cb0 100644 --- a/L10n/cs.strings +++ b/L10n/cs.strings @@ -10,7 +10,7 @@ /* Explorer rank. [stats.py] */ "Aimless" = "Aimless"; -/* System menu entry on Windows. [EDMarketConnector.py] */ +/* Appearance setting. [EDMarketConnector.py] */ "Always on top" = "Vždy navrchu"; /* CQC rank. [stats.py] */ @@ -19,6 +19,9 @@ /* EDSM setting. [prefs.py] */ "API Key" = "API klíč"; +/* Tab heading in settings. [prefs.py] */ +"Appearance" = "Vzhled"; + /* Output setting. [prefs.py] */ "Automatically make a log entry on entering a system" = "Automaticky vytvořit záznam při vstoupení do systému"; @@ -85,12 +88,18 @@ /* Combat rank. [stats.py] */ "Dangerous" = "Dangerous"; +/* Appearance theme setting. [prefs.py] */ +"Dark" = "Tmavý"; + /* Combat rank. [stats.py] */ "Deadly" = "Deadly"; /* Trade rank. [stats.py] */ "Dealer" = "Dealer"; +/* Appearance theme setting. [prefs.py] */ +"Default" = "Výchozí"; + /* Empire rank. [stats.py] */ "Duke" = "Duke"; @@ -175,6 +184,9 @@ /* CQC rank. [stats.py] */ "Hero" = "Hero"; +/* Dark theme color setting. [prefs.py] */ +"Highlighted text" = "Zvýrazněný text"; + /* Tab heading in settings on Windows. [prefs.py] */ "Hotkey" = "Klávesová zkratka"; @@ -247,6 +259,9 @@ /* No hotkey/shortcut currently defined. [prefs.py] */ "None" = "Žádná"; +/* Dark theme color setting. [prefs.py] */ +"Normal text" = "Normální text"; + /* Combat rank. [stats.py] */ "Novice" = "Novice"; @@ -409,6 +424,9 @@ /* Main window. [EDMarketConnector.py] */ "System" = "Systém"; +/* Appearance setting. [prefs.py] */ +"Theme" = "Schéma"; + /* Ranking. [stats.py] */ "Trade" = "Trade"; diff --git a/L10n/de.strings b/L10n/de.strings index e3c6d0da..26176d25 100644 --- a/L10n/de.strings +++ b/L10n/de.strings @@ -10,7 +10,7 @@ /* Explorer rank. [stats.py] */ "Aimless" = "Total planlos"; -/* System menu entry on Windows. [EDMarketConnector.py] */ +/* Appearance setting. [EDMarketConnector.py] */ "Always on top" = "Immer im Vordergrund"; /* CQC rank. [stats.py] */ @@ -19,9 +19,15 @@ /* EDSM setting. [prefs.py] */ "API Key" = "API-Schlüssel"; +/* Tab heading in settings. [prefs.py] */ +"Appearance" = "Aussehen"; + /* Output setting. [prefs.py] */ "Automatically make a log entry on entering a system" = "Automatisch Logbucheintrag bei Systemeintritt anlegen"; +/* [prefs.py] */ +"Automatically open uncharted systems’ EDSM pages" = "Automatisch die EDSM Seite unerforschter Systeme öffnen"; + /* Cmdr stats. [stats.py] */ "Balance" = "Kontostand"; @@ -61,6 +67,9 @@ /* Combat rank. [stats.py] */ "Competent" = "Kompetent"; +/* Output settings. [prefs.py] */ +"Connected to {EDPROXY} at {ADDR}" = "Verbunden mit {EDPROXY} bei {ADDR}"; + /* Update button in main window. [EDMarketConnector.py] */ "cooldown {SS}s" = "Wartezeit {SS}s"; @@ -79,12 +88,18 @@ /* Combat rank. [stats.py] */ "Dangerous" = "Gefährlich"; +/* Appearance theme setting. [prefs.py] */ +"Dark" = "Dunkel"; + /* Combat rank. [stats.py] */ "Deadly" = "Tödlich"; /* Trade rank. [stats.py] */ "Dealer" = "Kleinhändler"; +/* Appearance theme setting. [prefs.py] */ +"Default" = "Standard"; + /* Empire rank. [stats.py] */ "Duke" = "Herzog"; @@ -130,6 +145,9 @@ /* [companion.py] */ "Error: Server is down" = "Fehler: Server nicht erreichbar"; +/* [companion.py] */ +"Error: Verification failed" = "Fehler: Verifizierung fehlgeschlagen"; + /* Item in the File menu on Windows. [EDMarketConnector.py] */ "Exit" = "Beenden"; @@ -166,6 +184,9 @@ /* CQC rank. [stats.py] */ "Hero" = "Held"; +/* Dark theme color setting. [prefs.py] */ +"Highlighted text" = "Hervorgehobener Text"; + /* Tab heading in settings on Windows. [prefs.py] */ "Hotkey" = "Hotkey"; @@ -238,6 +259,9 @@ /* No hotkey/shortcut currently defined. [prefs.py] */ "None" = "Keine Zuweisung"; +/* Dark theme color setting. [prefs.py] */ +"Normal text" = "Standardmäßiger Text"; + /* Combat rank. [stats.py] */ "Novice" = "Neuling"; @@ -400,6 +424,9 @@ /* Main window. [EDMarketConnector.py] */ "System" = "System"; +/* Appearance setting. [prefs.py] */ +"Theme" = "Theme"; + /* Ranking. [stats.py] */ "Trade" = "Handelsrang"; diff --git a/L10n/en.template b/L10n/en.template index 409ce08e..bb5a5ba4 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -10,7 +10,7 @@ /* Explorer rank. [stats.py] */ "Aimless" = "Aimless"; -/* System menu entry on Windows. [EDMarketConnector.py] */ +/* Appearance setting. [EDMarketConnector.py] */ "Always on top" = "Always on top"; /* CQC rank. [stats.py] */ @@ -19,6 +19,9 @@ /* EDSM setting. [prefs.py] */ "API Key" = "API Key"; +/* Tab heading in settings. [prefs.py] */ +"Appearance" = "Appearance"; + /* Output setting. [prefs.py] */ "Automatically make a log entry on entering a system" = "Automatically make a log entry on entering a system"; @@ -85,12 +88,18 @@ /* Combat rank. [stats.py] */ "Dangerous" = "Dangerous"; +/* Appearance theme setting. [prefs.py] */ +"Dark" = "Dark"; + /* Combat rank. [stats.py] */ "Deadly" = "Deadly"; /* Trade rank. [stats.py] */ "Dealer" = "Dealer"; +/* Appearance theme setting. [prefs.py] */ +"Default" = "Default"; + /* Empire rank. [stats.py] */ "Duke" = "Duke"; @@ -175,6 +184,9 @@ /* CQC rank. [stats.py] */ "Hero" = "Hero"; +/* Dark theme color setting. [prefs.py] */ +"Highlighted text" = "Highlighted text"; + /* Tab heading in settings on Windows. [prefs.py] */ "Hotkey" = "Hotkey"; @@ -247,6 +259,9 @@ /* No hotkey/shortcut currently defined. [prefs.py] */ "None" = "None"; +/* Dark theme color setting. [prefs.py] */ +"Normal text" = "Normal text"; + /* Combat rank. [stats.py] */ "Novice" = "Novice"; @@ -409,6 +424,9 @@ /* Main window. [EDMarketConnector.py] */ "System" = "System"; +/* Appearance setting. [prefs.py] */ +"Theme" = "Theme"; + /* Ranking. [stats.py] */ "Trade" = "Trade"; diff --git a/L10n/es.strings b/L10n/es.strings index dd7deeda..fae90dd4 100644 --- a/L10n/es.strings +++ b/L10n/es.strings @@ -10,7 +10,7 @@ /* Explorer rank. [stats.py] */ "Aimless" = "Perdido"; -/* System menu entry on Windows. [EDMarketConnector.py] */ +/* Appearance setting. [EDMarketConnector.py] */ "Always on top" = "Siempre visible"; /* CQC rank. [stats.py] */ @@ -19,6 +19,9 @@ /* EDSM setting. [prefs.py] */ "API Key" = "API Key"; +/* Tab heading in settings. [prefs.py] */ +"Appearance" = "Apariencia"; + /* Output setting. [prefs.py] */ "Automatically make a log entry on entering a system" = "Crear automáticamente una entrada en el registro al entrar en un sistema"; @@ -64,6 +67,9 @@ /* Combat rank. [stats.py] */ "Competent" = "Competente"; +/* Output settings. [prefs.py] */ +"Connected to {EDPROXY} at {ADDR}" = "Conectado a {EDPROXY} en {ADDR}"; + /* Update button in main window. [EDMarketConnector.py] */ "cooldown {SS}s" = "Espere {SS}s"; @@ -82,12 +88,18 @@ /* Combat rank. [stats.py] */ "Dangerous" = "Peligroso"; +/* Appearance theme setting. [prefs.py] */ +"Dark" = "Oscuro"; + /* Combat rank. [stats.py] */ "Deadly" = "Letal"; /* Trade rank. [stats.py] */ "Dealer" = "Distribuidor"; +/* Appearance theme setting. [prefs.py] */ +"Default" = "Por defecto"; + /* Empire rank. [stats.py] */ "Duke" = "Duque"; @@ -172,6 +184,9 @@ /* CQC rank. [stats.py] */ "Hero" = "Héroe"; +/* Dark theme color setting. [prefs.py] */ +"Highlighted text" = "Texto resaltado"; + /* Tab heading in settings on Windows. [prefs.py] */ "Hotkey" = "Tecla de acceso directo"; @@ -244,6 +259,9 @@ /* No hotkey/shortcut currently defined. [prefs.py] */ "None" = "Ninguna"; +/* Dark theme color setting. [prefs.py] */ +"Normal text" = "Texto normal"; + /* Combat rank. [stats.py] */ "Novice" = "Novato"; @@ -406,6 +424,9 @@ /* Main window. [EDMarketConnector.py] */ "System" = "Sistema"; +/* Appearance setting. [prefs.py] */ +"Theme" = "Tema"; + /* Ranking. [stats.py] */ "Trade" = "Comercio"; diff --git a/L10n/fr.strings b/L10n/fr.strings index 2dd5ea83..4105e632 100644 --- a/L10n/fr.strings +++ b/L10n/fr.strings @@ -10,7 +10,7 @@ /* Explorer rank. [stats.py] */ "Aimless" = "Vagabond"; -/* System menu entry on Windows. [EDMarketConnector.py] */ +/* Appearance setting. [EDMarketConnector.py] */ "Always on top" = "Toujours visible"; /* CQC rank. [stats.py] */ @@ -19,6 +19,9 @@ /* EDSM setting. [prefs.py] */ "API Key" = "API Key"; +/* Tab heading in settings. [prefs.py] */ +"Appearance" = "Apparence"; + /* Output setting. [prefs.py] */ "Automatically make a log entry on entering a system" = "Ajouter automatiquement une entrée au journal en entrant dans un système"; @@ -82,12 +85,18 @@ /* Combat rank. [stats.py] */ "Dangerous" = "Vétéran"; +/* Appearance theme setting. [prefs.py] */ +"Dark" = "Foncé"; + /* Combat rank. [stats.py] */ "Deadly" = "Létal"; /* Trade rank. [stats.py] */ "Dealer" = "Revendeur"; +/* Appearance theme setting. [prefs.py] */ +"Default" = "Par défaut"; + /* Empire rank. [stats.py] */ "Duke" = "Archiduc"; @@ -403,6 +412,9 @@ /* Main window. [EDMarketConnector.py] */ "System" = "Système"; +/* Appearance setting. [prefs.py] */ +"Theme" = "Thème"; + /* Ranking. [stats.py] */ "Trade" = "Commerçant"; diff --git a/L10n/it.strings b/L10n/it.strings index 28b29a41..609b1ffc 100644 --- a/L10n/it.strings +++ b/L10n/it.strings @@ -10,7 +10,7 @@ /* Explorer rank. [stats.py] */ "Aimless" = "Aimless"; -/* System menu entry on Windows. [EDMarketConnector.py] */ +/* Appearance setting. [EDMarketConnector.py] */ "Always on top" = "Sempre in primo piano"; /* CQC rank. [stats.py] */ @@ -19,6 +19,9 @@ /* EDSM setting. [prefs.py] */ "API Key" = "API Key"; +/* Tab heading in settings. [prefs.py] */ +"Appearance" = "Aspetto"; + /* Output setting. [prefs.py] */ "Automatically make a log entry on entering a system" = "Inserisce automaticamente una log entry entrando in un sistema"; @@ -82,12 +85,18 @@ /* Combat rank. [stats.py] */ "Dangerous" = "Dangerous"; +/* Appearance theme setting. [prefs.py] */ +"Dark" = "Scuro"; + /* Combat rank. [stats.py] */ "Deadly" = "Deadly"; /* Trade rank. [stats.py] */ "Dealer" = "Dealer"; +/* Appearance theme setting. [prefs.py] */ +"Default" = "Predefinito"; + /* Empire rank. [stats.py] */ "Duke" = "Duke"; @@ -403,6 +412,9 @@ /* Main window. [EDMarketConnector.py] */ "System" = "Sistema"; +/* Appearance setting. [prefs.py] */ +"Theme" = "Tema"; + /* Ranking. [stats.py] */ "Trade" = "Trade"; diff --git a/L10n/lv.strings b/L10n/lv.strings index 6c74f15b..6c39abf0 100644 --- a/L10n/lv.strings +++ b/L10n/lv.strings @@ -10,7 +10,7 @@ /* Explorer rank. [stats.py] */ "Aimless" = "Aimless"; -/* System menu entry on Windows. [EDMarketConnector.py] */ +/* Appearance setting. [EDMarketConnector.py] */ "Always on top" = "Vienmēr virspusē"; /* CQC rank. [stats.py] */ @@ -19,6 +19,9 @@ /* EDSM setting. [prefs.py] */ "API Key" = "API atslēga"; +/* Tab heading in settings. [prefs.py] */ +"Appearance" = "Izskats"; + /* Output setting. [prefs.py] */ "Automatically make a log entry on entering a system" = "Automātiski veikt ierakstu ierodoties sistēmā"; @@ -82,12 +85,18 @@ /* Combat rank. [stats.py] */ "Dangerous" = "Dangerous"; +/* Appearance theme setting. [prefs.py] */ +"Dark" = "Tumša"; + /* Combat rank. [stats.py] */ "Deadly" = "Deadly"; /* Trade rank. [stats.py] */ "Dealer" = "Dealer"; +/* Appearance theme setting. [prefs.py] */ +"Default" = "Noklusējuma"; + /* Empire rank. [stats.py] */ "Duke" = "Duke"; @@ -403,6 +412,9 @@ /* Main window. [EDMarketConnector.py] */ "System" = "Sistēma"; +/* Appearance setting. [prefs.py] */ +"Theme" = "Dizains"; + /* Ranking. [stats.py] */ "Trade" = "Trade"; diff --git a/L10n/nl.strings b/L10n/nl.strings index 1f3fbeda..f9e323b4 100644 --- a/L10n/nl.strings +++ b/L10n/nl.strings @@ -10,7 +10,7 @@ /* Explorer rank. [stats.py] */ "Aimless" = "Aimless"; -/* System menu entry on Windows. [EDMarketConnector.py] */ +/* Appearance setting. [EDMarketConnector.py] */ "Always on top" = "Altijd op voorgrond"; /* CQC rank. [stats.py] */ @@ -19,6 +19,9 @@ /* EDSM setting. [prefs.py] */ "API Key" = "API Key"; +/* Tab heading in settings. [prefs.py] */ +"Appearance" = "Vormgeving"; + /* Output setting. [prefs.py] */ "Automatically make a log entry on entering a system" = "Automatisch een log regel aanmaken bij het binnengaan van een stelsel"; @@ -85,12 +88,18 @@ /* Combat rank. [stats.py] */ "Dangerous" = "Dangerous"; +/* Appearance theme setting. [prefs.py] */ +"Dark" = "Donker"; + /* Combat rank. [stats.py] */ "Deadly" = "Deadly"; /* Trade rank. [stats.py] */ "Dealer" = "Dealer"; +/* Appearance theme setting. [prefs.py] */ +"Default" = "Standaard"; + /* Empire rank. [stats.py] */ "Duke" = "Duke"; @@ -409,6 +418,9 @@ /* Main window. [EDMarketConnector.py] */ "System" = "Systeem"; +/* Appearance setting. [prefs.py] */ +"Theme" = "Thema"; + /* Ranking. [stats.py] */ "Trade" = "Handels rang"; diff --git a/L10n/pl.strings b/L10n/pl.strings index eaf61101..b09703ab 100644 --- a/L10n/pl.strings +++ b/L10n/pl.strings @@ -10,7 +10,7 @@ /* Explorer rank. [stats.py] */ "Aimless" = "Aimless"; -/* System menu entry on Windows. [EDMarketConnector.py] */ +/* Appearance setting. [EDMarketConnector.py] */ "Always on top" = "Zawsze na wierzchu"; /* CQC rank. [stats.py] */ @@ -19,9 +19,15 @@ /* EDSM setting. [prefs.py] */ "API Key" = "API Key"; +/* Tab heading in settings. [prefs.py] */ +"Appearance" = "Wygląd"; + /* Output setting. [prefs.py] */ "Automatically make a log entry on entering a system" = "Stwórz automatycznie wpis w logu po wejściu do systemu"; +/* [prefs.py] */ +"Automatically open uncharted systems’ EDSM pages" = "Automatycznie otwieraj nie skatalogowane systemy w EDSM"; + /* Cmdr stats. [stats.py] */ "Balance" = "Saldo"; @@ -61,6 +67,9 @@ /* Combat rank. [stats.py] */ "Competent" = "Competent"; +/* Output settings. [prefs.py] */ +"Connected to {EDPROXY} at {ADDR}" = "Połąćzono do {EDPROXY} na {ADDR}"; + /* Update button in main window. [EDMarketConnector.py] */ "cooldown {SS}s" = "Oczekiwanie {SS} sek."; @@ -79,12 +88,18 @@ /* Combat rank. [stats.py] */ "Dangerous" = "Dangerous"; +/* Appearance theme setting. [prefs.py] */ +"Dark" = "Ciemny"; + /* Combat rank. [stats.py] */ "Deadly" = "Deadly"; /* Trade rank. [stats.py] */ "Dealer" = "Dealer"; +/* Appearance theme setting. [prefs.py] */ +"Default" = "Domyślne"; + /* Empire rank. [stats.py] */ "Duke" = "Duke"; @@ -130,6 +145,9 @@ /* [companion.py] */ "Error: Server is down" = "Błąd: Serwer padł"; +/* [companion.py] */ +"Error: Verification failed" = "Błąd: Weryfikacja niepoprawna"; + /* Item in the File menu on Windows. [EDMarketConnector.py] */ "Exit" = "Zakończ"; @@ -166,6 +184,9 @@ /* CQC rank. [stats.py] */ "Hero" = "Hero"; +/* Dark theme color setting. [prefs.py] */ +"Highlighted text" = "Podświetlony tekst"; + /* Tab heading in settings on Windows. [prefs.py] */ "Hotkey" = "Skr. Klaw."; @@ -238,6 +259,9 @@ /* No hotkey/shortcut currently defined. [prefs.py] */ "None" = "Brak"; +/* Dark theme color setting. [prefs.py] */ +"Normal text" = "Zwykły tekst"; + /* Combat rank. [stats.py] */ "Novice" = "Novice"; @@ -400,6 +424,9 @@ /* Main window. [EDMarketConnector.py] */ "System" = "System"; +/* Appearance setting. [prefs.py] */ +"Theme" = "Schemat kolorystyczny"; + /* Ranking. [stats.py] */ "Trade" = "Trade"; diff --git a/L10n/ru.strings b/L10n/ru.strings index da679804..4d901e2b 100644 --- a/L10n/ru.strings +++ b/L10n/ru.strings @@ -10,7 +10,7 @@ /* Explorer rank. [stats.py] */ "Aimless" = "Бесцельный"; -/* System menu entry on Windows. [EDMarketConnector.py] */ +/* Appearance setting. [EDMarketConnector.py] */ "Always on top" = "Поверх остальных окон"; /* CQC rank. [stats.py] */ @@ -19,6 +19,9 @@ /* EDSM setting. [prefs.py] */ "API Key" = "Ключ API"; +/* Tab heading in settings. [prefs.py] */ +"Appearance" = "Внешний вид"; + /* Output setting. [prefs.py] */ "Automatically make a log entry on entering a system" = "Автоматически заносить в журнал полета каждую посещённую систему"; @@ -82,12 +85,18 @@ /* Combat rank. [stats.py] */ "Dangerous" = "Опасный"; +/* Appearance theme setting. [prefs.py] */ +"Dark" = "Темный"; + /* Combat rank. [stats.py] */ "Deadly" = "Смертоносный"; /* Trade rank. [stats.py] */ "Dealer" = "Агент"; +/* Appearance theme setting. [prefs.py] */ +"Default" = "По умолчанию"; + /* Empire rank. [stats.py] */ "Duke" = "Герцог"; @@ -403,6 +412,9 @@ /* Main window. [EDMarketConnector.py] */ "System" = "Система"; +/* Appearance setting. [prefs.py] */ +"Theme" = "Тема"; + /* Ranking. [stats.py] */ "Trade" = "Торговый"; diff --git a/L10n/sl.strings b/L10n/sl.strings index bb130e7f..daf35b08 100644 --- a/L10n/sl.strings +++ b/L10n/sl.strings @@ -10,7 +10,7 @@ /* Explorer rank. [stats.py] */ "Aimless" = "Aimless"; -/* System menu entry on Windows. [EDMarketConnector.py] */ +/* Appearance setting. [EDMarketConnector.py] */ "Always on top" = "Vedno na vrhu"; /* CQC rank. [stats.py] */ @@ -19,6 +19,9 @@ /* EDSM setting. [prefs.py] */ "API Key" = "API ključ"; +/* Tab heading in settings. [prefs.py] */ +"Appearance" = "Videz"; + /* Output setting. [prefs.py] */ "Automatically make a log entry on entering a system" = "Shrani podatke v dnevnik leta, ko prispeš v sistem"; @@ -61,6 +64,9 @@ /* Combat rank. [stats.py] */ "Competent" = "Competent"; +/* Output settings. [prefs.py] */ +"Connected to {EDPROXY} at {ADDR}" = "Connected to {EDPROXY} at {ADDR}"; + /* Update button in main window. [EDMarketConnector.py] */ "cooldown {SS}s" = "Posodobitev v {SS}s"; @@ -79,12 +85,18 @@ /* Combat rank. [stats.py] */ "Dangerous" = "Dangerous"; +/* Appearance theme setting. [prefs.py] */ +"Dark" = "Temno"; + /* Combat rank. [stats.py] */ "Deadly" = "Deadly"; /* Trade rank. [stats.py] */ "Dealer" = "Dealer"; +/* Appearance theme setting. [prefs.py] */ +"Default" = "Privzeta"; + /* Empire rank. [stats.py] */ "Duke" = "Duke"; @@ -400,6 +412,9 @@ /* Main window. [EDMarketConnector.py] */ "System" = "Sistem"; +/* Appearance setting. [prefs.py] */ +"Theme" = "Tema"; + /* Ranking. [stats.py] */ "Trade" = "Trgovanje"; diff --git a/L10n/uk.strings b/L10n/uk.strings index fa00f2a6..29459134 100644 --- a/L10n/uk.strings +++ b/L10n/uk.strings @@ -10,7 +10,7 @@ /* Explorer rank. [stats.py] */ "Aimless" = "Безцільний"; -/* System menu entry on Windows. [EDMarketConnector.py] */ +/* Appearance setting. [EDMarketConnector.py] */ "Always on top" = "Поверх інших вікон"; /* CQC rank. [stats.py] */ @@ -19,6 +19,9 @@ /* EDSM setting. [prefs.py] */ "API Key" = "API Ключ"; +/* Tab heading in settings. [prefs.py] */ +"Appearance" = "Зовнішній вигляд"; + /* Output setting. [prefs.py] */ "Automatically make a log entry on entering a system" = "Автоматично робити запис у журналі на вході в систему"; @@ -85,12 +88,18 @@ /* Combat rank. [stats.py] */ "Dangerous" = "Небезпечний"; +/* Appearance theme setting. [prefs.py] */ +"Dark" = "Темний"; + /* Combat rank. [stats.py] */ "Deadly" = "Убивчий"; /* Trade rank. [stats.py] */ "Dealer" = "Дилер"; +/* Appearance theme setting. [prefs.py] */ +"Default" = "Стандарт"; + /* Empire rank. [stats.py] */ "Duke" = "Герцог"; @@ -175,6 +184,9 @@ /* CQC rank. [stats.py] */ "Hero" = "Герой"; +/* Dark theme color setting. [prefs.py] */ +"Highlighted text" = "Виділений текст"; + /* Tab heading in settings on Windows. [prefs.py] */ "Hotkey" = "Горяча клавіша"; @@ -247,6 +259,9 @@ /* No hotkey/shortcut currently defined. [prefs.py] */ "None" = "Нічого"; +/* Dark theme color setting. [prefs.py] */ +"Normal text" = "Нормальний текст"; + /* Combat rank. [stats.py] */ "Novice" = "Новачок"; @@ -409,6 +424,9 @@ /* Main window. [EDMarketConnector.py] */ "System" = "Система"; +/* Appearance setting. [prefs.py] */ +"Theme" = "Тема"; + /* Ranking. [stats.py] */ "Trade" = "Торговий"; diff --git a/README.md b/README.md index 4fc760dc..1962bc66 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ Click on the station name to go to its [Elite: Dangerous Database](http://eddb.i ![Windows screenshot](img/win.png)   ![Mac screenshot](img/mac.png) +![Windows screenshot](img/win_dark.png)   ![Mac screenshot](img/mac_dark.png) + Installation -------- diff --git a/img/mac_dark.png b/img/mac_dark.png new file mode 100644 index 00000000..0231eaf1 Binary files /dev/null and b/img/mac_dark.png differ diff --git a/img/win_dark.png b/img/win_dark.png new file mode 100644 index 00000000..6e98ef46 Binary files /dev/null and b/img/win_dark.png differ diff --git a/prefs.py b/prefs.py index e4315d02..bba7f4ff 100644 --- a/prefs.py +++ b/prefs.py @@ -6,7 +6,7 @@ from sys import platform import Tkinter as tk import ttk -import tkFileDialog +import tkColorChooser from ttkHyperlinkLabel import HyperlinkLabel import myNotebook as nb @@ -14,6 +14,7 @@ from config import applongname, config from edproxy import edproxy from hotkey import hotkeymgr from monitor import monitor +from theme import theme import plug @@ -62,12 +63,12 @@ class PreferencesDialog(tk.Toplevel): self.geometry("+%d+%d" % (parent.winfo_rootx(), parent.winfo_rooty())) # remove decoration - self.resizable(tk.FALSE, tk.FALSE) if platform=='win32': self.attributes('-toolwindow', tk.TRUE) elif platform=='darwin': # http://wiki.tcl.tk/13428 parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility') + self.resizable(tk.FALSE, tk.FALSE) style = ttk.Style() @@ -217,6 +218,33 @@ class PreferencesDialog(tk.Toplevel): _('Keyboard shortcut') or # Tab heading in settings on OSX _('Hotkey')) # Tab heading in settings on Windows + self.always_ontop = tk.BooleanVar(value = config.getint('always_ontop')) + self.theme = tk.IntVar(value = config.getint('theme') and 1 or 0) + self.theme_colors = [config.get('dark_text'), config.get('dark_highlight')] + self.theme_prompts = [ + _('Normal text'), # Dark theme color setting + _('Highlighted text'), # Dark theme color setting + ] + themeframe = nb.Frame(notebook) + themeframe.columnconfigure(2, weight=1) + nb.Label(themeframe).grid(sticky=tk.W) # big spacer + self.ontop_button = nb.Checkbutton(themeframe, text=_('Always on top'), variable=self.always_ontop, command=self.themevarchanged) + self.ontop_button.grid(columnspan=3, padx=BUTTONX, sticky=tk.W) # Appearance setting + ttk.Separator(themeframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY*8, sticky=tk.EW) + nb.Label(themeframe, text=_('Theme')).grid(columnspan=3, padx=PADX, sticky=tk.W) # Appearance setting + nb.Radiobutton(themeframe, text=_('Default'), variable=self.theme, value=0, command=self.themevarchanged).grid(columnspan=3, padx=BUTTONX, sticky=tk.W) # Appearance theme setting + nb.Radiobutton(themeframe, text=_('Dark'), variable=self.theme, value=1, command=self.themevarchanged).grid(columnspan=3, padx=BUTTONX, sticky=tk.W) # Appearance theme setting + self.theme_label_0 = nb.Label(themeframe, text=self.theme_prompts[0]) + self.theme_label_0.grid(row=10, padx=PADX, sticky=tk.W) + self.theme_button_0 = nb.ColoredButton(themeframe, text=_('Station'), background='black', command=lambda:self.themecolorbrowse(0)) # Main window + self.theme_button_0.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.NSEW) + self.theme_label_1 = nb.Label(themeframe, text=self.theme_prompts[1]) + self.theme_label_1.grid(row=11, padx=PADX, sticky=tk.W) + self.theme_button_1 = nb.ColoredButton(themeframe, text=' Hutton Orbital ', background='black', command=lambda:self.themecolorbrowse(1)) # Do not translate + self.theme_button_1.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.NSEW) + + notebook.add(themeframe, text=_('Appearance')) # Tab heading in settings + # build plugin prefs tabs for plugname in plug.PLUGINS: plugframe = plug.get_plugin_pref(plugname, notebook) @@ -237,14 +265,15 @@ class PreferencesDialog(tk.Toplevel): # Selectively disable buttons depending on output settings self.proxypoll() + self.themevarchanged() # disable hotkey for the duration hotkeymgr.unregister() # wait for window to appear on screen before calling grab_set + self.parent.wm_attributes('-topmost', 0) # needed for dialog to appear ontop of parent on OSX & Linux self.wait_visibility() self.grab_set() - #self.wait_window(self) # causes duplicate events on OSX def proxypoll(self): @@ -296,6 +325,7 @@ class PreferencesDialog(tk.Toplevel): def outbrowse(self): if platform != 'win32': + import tkFileDialog d = tkFileDialog.askdirectory(parent=self, initialdir=expanduser(self.outdir.get()), title=_('File location'), mustexist=tk.TRUE) else: def browsecallback(hwnd, uMsg, lParam, lpData): @@ -328,6 +358,25 @@ class PreferencesDialog(tk.Toplevel): self.outdir.insert(0, d) self.outdir['state'] = 'readonly' + def themecolorbrowse(self, index): + (rgb, color) = tkColorChooser.askcolor(self.theme_colors[index], title=self.theme_prompts[index], parent=self.parent) + if color: + self.theme_colors[index] = color + self.themevarchanged() + + def themevarchanged(self): + self.theme_button_0['foreground'], self.theme_button_1['foreground'] = self.theme_colors + + state = self.theme.get() and tk.NORMAL or tk.DISABLED + self.theme_label_0['state'] = state + self.theme_label_1['state'] = state + self.theme_button_0['state'] = state + self.theme_button_1['state'] = state + + if platform == 'linux2': + # Unmanaged windows are always on top on X + self.ontop_button['state'] = self.theme.get() and tk.DISABLED or tk.NORMAL + def hotkeystart(self, event): event.widget.bind('', self.hotkeylisten) event.widget.bind('', self.hotkeylisten) @@ -396,6 +445,12 @@ class PreferencesDialog(tk.Toplevel): config.set('hotkey_always', int(not self.hotkey_only.get())) config.set('hotkey_mute', int(not self.hotkey_play.get())) + config.set('always_ontop', self.always_ontop.get()) + config.set('theme', self.theme.get()) + config.set('dark_text', self.theme_colors[0]) + config.set('dark_highlight', self.theme_colors[1]) + theme.apply(self.parent) + config.set('anonymous', self.out_anon.get()) self._destroy() @@ -412,6 +467,7 @@ class PreferencesDialog(tk.Toplevel): else: monitor.stop() edproxy.stop() + self.parent.wm_attributes('-topmost', config.getint('always_ontop') and 1 or 0) self.destroy() if platform == 'darwin': diff --git a/theme.py b/theme.py new file mode 100644 index 00000000..e3e3212c --- /dev/null +++ b/theme.py @@ -0,0 +1,221 @@ +# +# 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 + +import Tkinter as tk +import ttk +import tkFont + +from config import appname, applongname, config + + +class _Theme: + + def __init__(self): + self.active = None # Starts out with no theme + self.minwidth = None + self.widgets = set() + self.widgets_highlight = 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_highlight(self, widget): + assert isinstance(widget, tk.Widget) or isinstance(widget, tk.BitmapImage), widget + if isinstance(widget, tk.Frame) or isinstance(widget, ttk.Frame): + self.register_highlight(widget.winfo_children()) + self.widgets_highlight.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') + elif platform == 'darwin': + # Default ttk font spacing looks bad on El Capitan + osxfont = tkFont.Font(family='TkDefaultFont', size=13, weight=tkFont.NORMAL) + style.configure('TLabel', font=osxfont) + style.configure('TButton', font=osxfont) + style.configure('TLabelframe.Label', font=osxfont) + style.configure('TCheckbutton', font=osxfont) + style.configure('TRadiobutton', font=osxfont) + style.configure('TEntry', font=osxfont) + + # 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' : 'black', + 'foreground' : config.get('dark_text'), + 'activebackground' : config.get('dark_text'), + 'activeforeground' : 'black', + 'disabledforeground' : '#%02x%02x%02x' % (r/384, g/384, b/384), + 'highlight' : config.get('dark_highlight'), + 'font' : 'TkDefaultFont', + } + # Overrides + if platform == 'darwin': + self.current['font'] = osxfont + + 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' + self.current['font'] = osxfont + 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 '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']) + + for widget in self.widgets_highlight: + widget.configure(foreground = self.current['highlight'], + background = self.current['background']) + + for pair, gridopts in self.widgets_pair: + (default, dark) = pair + if isinstance(default, tk.Menu): + if theme: + root['menu'] = '' + dark.grid(**gridopts) + else: + root['menu'] = default + dark.grid_remove() + else: + old = theme and default or dark + current = theme and dark or default + old.grid_remove() + current.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, NSColor + root.update_idletasks() # need main window to be created + appearance = NSAppearance.appearanceNamed_(theme and + 'NSAppearanceNameVibrantDark' or + 'NSAppearanceNameAqua') + for window in NSApplication.sharedApplication().windows(): + window.setAppearance_(appearance) + + if not self.minwidth: + self.minwidth = root.winfo_width() # Minimum width = width on first creation + # resizable(0,0) doesn't do anything on OSX + root.minsize(self.minwidth, root.winfo_height()) + root.maxsize(-1, root.winfo_height()) + + elif platform == 'win32': + # tk8.5.9/win/tkWinWm.c:342 + import ctypes + GWL_STYLE = -16 + WS_BORDER = 0x00800000 + WS_OVERLAPPEDWINDOW =0x00CF0000 + GWL_EXSTYLE = -20 + WS_EX_WINDOWEDGE = 0x00000100 + WS_EX_APPWINDOW = 0x00040000 + root.overrideredirect(theme and 1 or 0) # Destroys any top-level window + root.update_idletasks() # Size and windows styles get recalculated here + hwnd = ctypes.windll.user32.GetParent(root.winfo_id()) + ctypes.windll.user32.SetWindowLongW(hwnd, GWL_STYLE, theme and WS_BORDER or WS_OVERLAPPEDWINDOW) + ctypes.windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, theme and WS_EX_APPWINDOW or WS_EX_WINDOWEDGE) + root.deiconify() + root.wait_visibility() # need main window to be displayed before returning + + if not self.minwidth: + self.minwidth = root.winfo_width() # Minimum width = width on first creation + root.minsize(self.minwidth, -1) + + else: + root.overrideredirect(theme and 1 or 0) + root.withdraw() + root.update_idletasks() # Size gets recalculated here + root.deiconify() + root.wait_visibility() # need main window to be displayed before returning + + if not self.minwidth: + self.minwidth = root.winfo_width() # Minimum width = width on first creation + root.minsize(self.minwidth, -1) + +# singleton +theme = _Theme()