diff --git a/fontchooser.py b/fontchooser.py index ad2839f..537d4a9 100644 --- a/fontchooser.py +++ b/fontchooser.py @@ -75,6 +75,9 @@ import tkinter from tkinter.font import Font +compatible = tkinter.TkVersion >= 8.6 + + class Chooser: """ @@ -119,6 +122,7 @@ class Chooser: 5. Workaround for bug on macOS where fontchooser could start open based on saved application state. 6. Workaround for bug on macOS where fontchooser needs an initial font set before will invoke callbacks. 7. Workaround for bug on X11 where using configure with a subset of option would reset others to default. + 8. We always bind to <> and it will call the command arg Still to be fixed: @@ -132,70 +136,134 @@ class Chooser: def __init__(self, **kw): self._command = None - self.w = kw.get('parent') if kw.get('parent') else tkinter._default_root - self.w.tk.call('tk', 'fontchooser', 'configure', '-command', self.w.register(self._font_changed)) + self._visibility_command = None + self.w = None + self._lastfont = None self.configure(**kw) - if self.w._windowingsystem == 'aqua': - self.w.after(1, self.hide) # workaround startup bug on macOS - self.w.tk.call('tk', 'fontchooser', 'configure', '-font', 'TkDefaultFont') def hide(self): """Hide the font selection dialog if visible.""" + self._initial_parent() self.w.tk.call("tk", "fontchooser", "hide") - def show(self): + def show(self, **kw): """Show the font selection dialog. This method does not return a value. On platforms where the font chooser is modal, this method won't return until the font chooser is dismissed. On other platforms, this method returns immediately.""" + self._initial_parent() + self.configure(**kw) self.w.tk.call("tk", "fontchooser", "show") def configure(self, **options): """Set the values of one or more options.""" + self._initial_parent(options.get('parent')) for k in options: self[k] = options[k] config = configure + def cget(self, key): + self._initial_parent() + if key == 'command': + return self._command + elif key == 'visibilitycommand': + return self._visibility_command + v = self.w.tk.call('tk', 'fontchooser', 'configure', '-'+key) + if key == 'font': + return Font(font=v) + elif key == 'visible': + return bool(v) + elif key == 'parent': + return self.w.nametowidget(v) + return v + + def _initial_parent(self, parent=None): + if self.w is None: + self.w = parent.winfo_toplevel() if parent else tkinter._default_root + if self.w is not None: + self.w.tk.call('tk', 'fontchooser', 'configure', '-command', self.w.register(self._font_changed), '-parent', self.w) + self._bind(self.w) + if self.w._windowingsystem == 'aqua': + self.w.after(1, self.hide) # workaround startup bug on macOS + self.w.tk.call('tk', 'fontchooser', 'configure', '-font', 'TkDefaultFont') + + def _bind(self, parent): + parent.bind('<>', self._visibility_changed_event) + parent.bind('<>', self._font_changed_event) + + def _visibility_changed_event(self, event): + """Callback from fontchooser binding when visibility is changed""" + if self._visibility_command: + self._visibility_command(self.isvisible()) + + def _font_equal(self, font1, font2): + return font1['family'] == font2['family'] and \ + font1['size'] == font2['size'] and \ + font1['weight'] == font2['weight'] and \ + font1['slant'] == font2['slant'] and \ + font1['underline'] == font2['underline'] and \ + font1['overstrike'] == font2['overstrike'] + + def _font_changed_event(self, event): + """Callback from fontchooser binding when font is changed""" + newfont = self.cget('font') + # Only call on platforms where -font is changed by user + if not self._font_equal(newfont, self._lastfont): + self._font_changed(newfont) + def _font_changed(self, fontspec): """Callback from fontchooser when font is changed""" - # On Windows, update -font if OK pressed in modal dialog, but not if Apply pressed - if self.ismodal() and not self.isvisible(): + # On Windows, update -font if OK pressed in modal dialog, but not if Apply pressed + self._lastfont = self['font'] + if self.ismodal and not self.isvisible(): self['font'] = fontspec - if self._command: + if self._command is not None: self._command(Font(font=fontspec)) - + + @property def ismodal(self): """True if font chooser is modal on this platform""" + self._initial_parent() return self.w._windowingsystem == "win32" def mayhavefocus(self): """True if font chooser might have focus on this platform""" + self._initial_parent() return (self.w._windowingsystem in ["x11", "win32"]) and self.isvisible() def isvisible(self): - return self.w.tk.call('tk', 'fontchooser', 'configure', '-visible') == 1 + return self.cget('visible') def __setitem__(self, key, value): if key == "command": self._command = value + elif key == 'visibilitycommand': + self._visibility_command = value else: + if key == 'parent': + self._initial_parent(value) # workaround bug on X11 where all options need to be specified, or those not specified are reset to defaults command = self.w.tk.call('tk', 'fontchooser', 'configure', '-command') - parent = value if key=='parent' else self.w.tk.call('tk', 'fontchooser', 'configure', '-parent') - title = value if key=='title' else self.w.tk.call('tk', 'fontchooser', 'configure', '-title') - font = value if key=='font' else self.w.tk.call('tk', 'fontchooser', 'configure', '-font') + parent = value.winfo_toplevel() if key=='parent' else self.cget('parent') + title = value if key=='title' else self.cget('title') + font = value if key=='font' else self.cget('font') self.w.tk.call("tk", "fontchooser", "configure", "-command", command, "-parent", parent, "-title", title, "-font", font) def __getitem__(self, key): - return self._command if key=="command" else self.w.tk.call("tk", "fontchooser", "configure", "-" + key) + return self.cget(key) + +chooser = Chooser() if __name__ == "__main__": + if not compatible: + raise RuntimeError("Arg! Incompatible Tk version!") + class FontChooserSimpleDemo(): def __init__(self, w): self.w = w - self.chooser = Chooser(command=self.myfont_changed, parent=self.w) + chooser.config(command=self.myfont_changed, parent=self.w) self.t = tkinter.Text(self.w, width=20, height=4, borderwidth=1, relief='solid') self.t.insert('end', 'testing') self.t.grid() @@ -203,7 +271,7 @@ if __name__ == "__main__": self.b.grid() def toggle(self): - self.chooser.hide() if self.chooser.isvisible() else self.chooser.show() + chooser.hide() if chooser.isvisible() else chooser.show() def myfont_changed(self, font): self.t['font'] = font @@ -213,7 +281,7 @@ if __name__ == "__main__": def __init__(self, w): self.w = w self.w.title("Font Chooser Demo") - self.chooser = Chooser(command=self.font_changed, parent=self.w) + chooser.config(command=self.font_changed, parent=self.w, visibilitycommand=self.visibility_changed) self.target = None # Widget we manage currently holding the keyboard focus # Button to Show/Hide font dialog; label is updated as chooser opens/closes; @@ -221,9 +289,7 @@ if __name__ == "__main__": # doesn't make sense (you'd never be able to use it). In that case, the button always says "Font...". self.fc_btn = tkinter.Button(w, takefocus=0, command=self.toggle) self.fc_btn.grid() - self.w.bind('<>', self.visibility_changed) - self.w.bind('<>', self.font_changed_event) - if self.chooser.ismodal(): + if chooser.ismodal: self.fc_btn['text'] = 'Font...' self.fc_btn['state'] = 'disabled' else: @@ -240,12 +306,12 @@ if __name__ == "__main__": def visibility_changed(self, *args): """Respond to font dialog being shown or hidden.""" - if not self.chooser.ismodal(): - self.fc_btn['text'] = 'Hide Fonts' if self.chooser.isvisible() else 'Show Fonts' + if not chooser.ismodal: + self.fc_btn['text'] = 'Hide Fonts' if chooser.isvisible() else 'Show Fonts' def toggle(self): """Show/Hide font dialog""" - self.chooser.hide() if self.chooser.isvisible() else self.chooser.show() + chooser.hide() if chooser.isvisible() else chooser.show() def create_fontable_text(self, font): """Simple text widget that allows font changes via font chooser.""" @@ -257,18 +323,18 @@ if __name__ == "__main__": def gained_focus(self, w): """One of our text widgets gained focus. Update the font chooser to match its current font""" - self.chooser['font'] = w['font'] + chooser['font'] = w['font'] self.target = w self.fc_btn['state'] = 'normal' def lost_focus(self, w): """One of our text widgets lost focus. It will no longer be the target of future font changes.""" - if not self.chooser.mayhavefocus(): + if not chooser.mayhavefocus(): # Ideally, if we move the keyboard focus elsewhere, the font dialog shouldn't update one # of our fontable_text widgets. That doesn't work so well if the font dialog will steal # the focus... self.target = None - if self.chooser.ismodal(): + if chooser.ismodal: self.fc_btn['state'] = 'disabled' def font_changed(self, font): @@ -276,10 +342,6 @@ if __name__ == "__main__": if self.target: self.target['font'] = font - def font_changed_event(self, *args): - """<> event generated; not used.""" - pass - root = tkinter.Tk() # FontChooserSimpleDemo(root) FontChooserDemo(root)