#!/usr/bin/python # fontasia 0.7: # List all fonts on the system and let the user group them into categories. # # Can be run as a standalone script, or as a GIMP plug-in font chooser # (where it shows up as Windows->Fontasia). # # Contributors: Akkana Peck, Mikael Magnusson, Michael Schumacher # # More info on fontasia: http://shallowsky.com/software/fontasia # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # Thanks to jan bodnar's ZetCode.com PyGTK/Pango tutorial. import gtk import pango import os, re # A buttonbox class, which can hold an arbitrary number of buttons. # This is expected to occupy a space which may expand or contract. # May want to experiment with several different layouts. # The base buttonbox class is a simple vbox: class Buttonbox(): def __init__(self): self.widget = gtk.VBox(spacing=8) def addButton(self, btn): self.widget.pack_start(btn, expand=False) # Attempt at different-sized buttons that wrap: # this doesn't work yet, gtk doesn't offer any straightforward way. class ButtonboxWrap(Buttonbox): def __init__(self): self.widget = gtk.HBox(spacing=8) def addButton(self, btn): self.widget.pack_start(btn, expand=False) # A 2-dimensional table: class ButtonboxTable(Buttonbox): def __init__(self): self.rows = 8 self.widget = gtk.Table(homogeneous=False) self.cur_row = 0 self.cur_col = 0 def addButton(self, btn): self.widget.attach(btn, self.cur_col, self.cur_col+1, self.cur_row, self.cur_row+1, xpadding=3, ypadding=2) # fill vertically first self.cur_row += 1 if self.cur_row >= self.rows: self.cur_row = 0 self.cur_col += 1 class FontApp(gtk.Window): def __init__(self): super(FontApp, self).__init__() # When the quit button is pressed we'll set the quit flag, # but won't actually quit -- the caller has to do that # after checking the selected font. self.quit = False # No categories to start with self.category_fontlists = [] self.category_buttons = [] self.oldcolors = None self.highlightcolor = None self.DEFAULT_TEXT = "The quick brown fox jumped over the lazy dog" self.preview_text = self.DEFAULT_TEXT self.DEFAULT_FONT_SIZE = 18 self.fontsize = self.DEFAULT_FONT_SIZE self.fancy_list = True # Default sizes: these can be overridden in .fontasia self.win_width = 760 self.win_height = 400 self.preview_height = 56 self.list_width = 250 # Have we changed anything, so we need to rewrite the config file? self.modified = False self.set_border_width(8) self.connect("destroy", self.save_and_quit) self.set_title("Fontasia: Categorize your fonts") main_vbox = gtk.VBox(spacing = 10) self.add(main_vbox) hbox = gtk.HBox(spacing = 10) main_vbox.pack_start(hbox, expand=False) # # The preview # self.preview = gtk.Entry() self.preview.set_width_chars(45) self.preview.set_text(self.preview_text) # Don't use fallbacks for characters not in the current font. # This apparently doesn't work. al = pango.AttrList() al.insert(pango.AttrFallback(False, 0, -1)) self.preview.get_layout().set_attributes(al) hbox.pack_start(self.preview, expand=True) # Quit button btn = gtk.Button("Quit") hbox.pack_end(btn, expand=False) btn.connect("clicked", self.save_and_quit); main_hbox = gtk.HBox(spacing = 10) main_vbox.add(main_hbox) # # Left pane: the font list # self.sw = gtk.ScrolledWindow() self.sw.set_shadow_type(gtk.SHADOW_ETCHED_IN) self.sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) context = self.create_pango_context() self.all_families = context.list_families() self.store = gtk.ListStore(str) self.treeView = gtk.TreeView(self.store) self.treeView.set_rules_hint(True) self.store.set_sort_column_id(0, gtk.SORT_ASCENDING) selection = self.treeView.get_selection() selection.set_mode(gtk.SELECTION_MULTIPLE) self.treeView.connect("cursor-changed", self.render_font); self.sw.add(self.treeView) self.create_column(self.treeView) main_hbox.pack_start(self.sw, expand=False) # # Right pane: the controls # vbox = gtk.VBox(spacing=15) main_hbox.pack_start(vbox, expand=True) # Box to hold font size, view category etc. hbox = gtk.HBox(spacing=10) vbox.pack_start(hbox, expand=False) # Checkbox for showing fonts in the font list self.fancy_btn = gtk.ToggleButton("Fancy list") self.fancy_btn.set_active(self.fancy_list) self.fancy_btn.connect("toggled", self.toggle_fancy_list_cb); hbox.pack_start(self.fancy_btn, expand=False) # Font size label = gtk.Label("Size:") hbox.pack_start(label, expand=False) adj = gtk.Adjustment(self.fontsize, 1, 1000, 1) self.sizespin = gtk.SpinButton(adj) self.sizespin.set_numeric(True) self.sizespin.connect("value-changed", self.render_font); hbox.pack_start(self.sizespin, expand=False) # Bold and italic buttons self.bold_btn = gtk.ToggleButton("Bold") hbox.pack_start(self.bold_btn, expand=False) self.bold_btn.connect("toggled", self.render_font); self.italic_btn = gtk.ToggleButton("Italic") hbox.pack_start(self.italic_btn, expand=False) self.italic_btn.connect("toggled", self.render_font); # # View category menu # label = gtk.Label("View:") # This is supposed to right-align the label, but alas it does nothing: label.set_alignment(1.0, .5) # This also doesn't right-align, but the docs are clear on that: # label.set_justify(gtk.JUSTIFY_RIGHT) hbox.pack_start(label, expand=False) self.view_cat_combo = gtk.combo_box_new_text() hbox.pack_start(self.view_cat_combo, expand=False) self.combo_connect_id = self.view_cat_combo.connect("changed", self.combochanged) # Now we can get the default button background (I hope): widgcopy = btn.get_style().copy() self.oldcolors = widgcopy.bg self.highlightcolor = gtk.gdk.Color(0, 65535, 0) # Categories header row hbox = gtk.HBox(spacing=8) vbox.pack_start(hbox, expand=False) label = gtk.Label("Categories") hbox.pack_start(label, expand=False) entry = gtk.Entry() entry.set_width_chars(20) hbox.pack_start(entry, expand=False) btn = gtk.Button("Add category") hbox.pack_start(btn, expand=False) btn.set_sensitive(False) btn.connect("clicked", self.add_category, entry); # The entry and button are tied together: entry.connect("changed", self.new_cat_entry_changed, btn); # # The buttonbox: the list of category togglebuttons. # self.buttonbox = ButtonboxTable() vbox.pack_start(self.buttonbox.widget, expand=True) # # Just before showing, update everything: # self.read_category_file() # Set size requests, now that we've read them from the config file: self.set_default_size(self.win_width, self.win_height) self.sw.set_size_request(self.list_width, -1) #self.sw.set_size_request(self.list_width, self.list_height) self.preview.set_size_request(-1, self.preview_height) self.update_font_list() # Done with UI, finish showing window self.show_all() # Setting size_requests earlier set the *minimum* size, # but we don't want to do that. So reset the size request # once the widgets have already been sized. # Alas, gtk has no way to just set the size without # specifying a minimum; size_allocate() is a no-op. #self.sw.set_size_request(self.list_width, 1) #self.preview.set_size_request(-1, -1) def save_and_quit(self, widget=None): self.write_category_file() self.quit = True # gtk.main_quit() def toggle_fancy_list_cb(self, widget): self.fancy_list = widget.get_active() self.toggle_fancy_list(self.fancy_list) #self.update_font_list() def toggle_fancy_list(self, yesno): column = self.treeView.get_column(0) rendererText = column.get_cell_renderers()[0] if self.fancy_list: column.set_cell_data_func(rendererText, self.set_list_font) else: column.set_cell_data_func(rendererText, None) rendererText.set_property('font', None) def set_list_font(self, column, cell, model, iter): fontname = model.get_value(iter, 0) # If the font name ends with a number, we're in trouble -- # pango will try to interpret the number as a HUGE size. # Sample fonts to test: Math2, Math3, # Plain Cred 1978 from ttf-larabie-uncommon. # A few fonts still give width problems, like # Radios in Motion and Radios in Motion Hard. # Scriptina - Alternates and Coca-Cola. fontname = re.sub('[0-9]+$', '', fontname) cell.set_property('font', fontname) def create_column(self, treeView): rendererText = gtk.CellRendererText() column = gtk.TreeViewColumn("Font name", rendererText, text=0) column.set_sort_column_id(0) if self.fancy_list: column.set_cell_data_func(rendererText, self.set_list_font) treeView.set_search_column(0) treeView.append_column(column) def is_font_in_category(self, fontname, catname): for i in range(0, len(self.category_buttons)): if self.category_buttons[i].get_name() == catname: return ( fontname in self.category_fontlists[i] ) return False # # If the font is already in the category, this will remove it; # else add it. # def toggle_font_in_category(self, fontname, catname): for i in range(0, len(self.category_buttons)): if self.category_buttons[i].get_name() == catname: if fontname in self.category_fontlists[i]: self.category_fontlists[i].remove(fontname) else: self.category_fontlists[i].append(fontname) return # # This is the callback from the font category togglebuttons. # It calls toggle_font_in_category. # def toggle_cur_font_in_cat(self, widget, catname): selection = self.treeView.get_selection() model, item = selection.get_selected_rows() # Read all the selected fonts, not just the first one: for x in item: fontname = model[x[0]][0] #print "Adding", fontname, "to", catname self.toggle_font_in_category(fontname, catname) self.update_category_buttons(fontname) self.treeView.grab_focus() # Update the View Categories combobox: def update_cats_menu(self): if self.combo_connect_id: self.view_cat_combo.handler_block(self.combo_connect_id) self.view_cat_combo.get_model().clear() self.view_cat_combo.append_text("All fonts") self.view_cat_combo.append_text("All in categories") self.view_cat_combo.append_text("All uncategorized") for btn in self.category_buttons: catname = btn.get_name() item = gtk.MenuItem(catname) self.view_cat_combo.append_text(catname) self.view_cat_combo.set_active(0) if self.combo_connect_id: self.view_cat_combo.handler_unblock(self.combo_connect_id) # How to get the active text from a combobox: def cur_category(self): model = self.view_cat_combo.get_model() active = self.view_cat_combo.get_active() if active < 0: return None return model[active][0] def combochanged(self, widget): curcat = self.cur_category() if curcat: self.update_font_list(curcat) def update_font_list(self, catname = "All fonts"): # figure out which buttons are toggled: # categories = [] # for btn in self.category_buttons: # if btn.get_active(): # categories.append(btn.get_name()) # show_all = (len(categories) == 0) special = (catname[0:3] == "All") if special: show_all = (catname == "All fonts") show_all_in_cats = (catname == "All in categories") show_all_not_in_cats = (catname == "All uncategorized") # if show_all or show_all_in_cats: # for catbtn in self.category_buttons: # catbtn.set_active(show_all_in_cats) # Create or clear the list store: self.store.clear() for ff in self.all_families: fname = ff.get_name() # Else not a special case. Just check for cat membership: if not special: if self.is_font_in_category(fname, catname): self.store.append([fname]) continue if show_all: self.store.append([fname]) continue if show_all_in_cats: for catbtn in self.category_buttons: if self.is_font_in_category(fname, catbtn.get_name()): self.store.append([fname]) break if show_all_not_in_cats: is_in_cat = False for catbtn in self.category_buttons: if self.is_font_in_category(fname, catbtn.get_name()): is_in_cat = True break if not is_in_cat: self.store.append([fname]) continue # Reset all the button colors, since nothing is selected now # (eventually might be nice to retain the old selection, # if it's still in the list): for btn in self.category_buttons: self.change_button_color(btn, False) # # Lines in the category file look like: # catname: font, font, font ... # def read_category_file(self) : try: from win32com.shell import shellcon, shell homedir = shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA, 0, 0) except ImportError: homedir = os.getenv("HOME") self.cat_file_name = os.path.join(homedir, ".fontasia") filename = self.cat_file_name if not os.access(filename, os.R_OK): filename = "/etc/xdg/fontasia/defaults" try: catfp = open(filename, "r") while 1: line = catfp.readline() if not line: break line = line.strip() if line[0] == '#': # Skip comments continue if line[0:4] == "set ": varval = line[4:].strip().split("=") if varval[0] == "string": self.preview.set_text(varval[1].strip()) continue if varval[0] == "fontsize": self.fontsize = int(varval[1]) self.sizespin.set_value(self.fontsize) continue if varval[0] == "fancy_list": self.fancy_list = (varval[1] == "True") self.fancy_btn.set_active(self.fancy_list) self.toggle_fancy_list(self.fancy_list) continue if varval[0] == "win_width": self.win_width = int(varval[1]) continue if varval[0] == "win_height": self.win_height = int(varval[1]) continue if varval[0] == "preview_height": self.preview_height = int(varval[1]) continue if varval[0] == "list_width": self.list_width = int(varval[1]) continue # Arguably should save any other values # for writing back to the file even though # we don't understand any others else: print "Skipping unknown setting", varval[0] continue colon = line.find(":") if colon < 0: print "Didn't understand line", line, continue catname = line[0:colon].strip() self.add_category(catname) line = line[colon+1:].strip() fonts = line.split(",") for font in fonts: font = font.strip() self.toggle_font_in_category(font, catname) catfp.close() self.update_cats_menu() except IOError: print "No .fontasia file yet -- Welcome to fontasia!" # Set up a couple of default categories with likely fonts self.add_category('monospace') self.toggle_font_in_category('Courier 10 Pitch', 'monospace') self.toggle_font_in_category('DejaVu Sans Mono', 'monospace') self.toggle_font_in_category('FreeMono', 'monospace') self.toggle_font_in_category('Monospace', 'monospace') self.toggle_font_in_category('Liberation Mono', 'monospace') self.toggle_font_in_category('Nimbus Mono L', 'monospace') self.add_category('script') self.toggle_font_in_category('Purisa', 'script') self.toggle_font_in_category('URW Chancery L', 'script') return def write_category_file(self): catfp = open(self.cat_file_name, "w") txt = self.preview.get_text() if txt != self.DEFAULT_TEXT: try: print >>catfp, "set string=" + txt except: print "Problem saving the string -- reverting to default string" fontsize = self.sizespin.get_value() if fontsize != self.DEFAULT_FONT_SIZE: print >>catfp, "set fontsize=" + str(int(fontsize)) # Get the current sizes for the list and the preview alloc = self.get_allocation() print >>catfp, "set win_width=" + str(alloc.width) print >>catfp, "set win_height=" + str(alloc.height) alloc = self.sw.get_allocation() print >>catfp, "set list_width=" + str(alloc.width) alloc = self.preview.get_allocation() print >>catfp, "set preview_height=" + str(alloc.height) if not self.fancy_list: print >>catfp, "set fancy_list=False" for i in range(0, len(self.category_buttons)): catname = self.category_buttons[i].get_name() print >>catfp, catname + ":", ', '.join(self.category_fontlists[i]) catfp.close() print "Saved to", self.cat_file_name def new_cat_entry_changed(self, entry, btn): btn.set_sensitive(entry.get_text_length() > 0) def category_exists(self, catname): for btn in self.category_buttons: if btn.get_name() == catname: return True return False def add_category(self, newcat, entry=None): if entry: newcat = entry.get_text() # Make sure we don't already have this category: if (self.category_exists(newcat)): return btn = gtk.Button(newcat) # was togglebutton btn.set_name(newcat) self.buttonbox.addButton(btn) btn.connect("clicked", self.toggle_cur_font_in_cat, newcat); btn.show() self.category_buttons.append(btn) self.category_fontlists.append([]) self.update_cats_menu() # If any fonts are selected, most likely the user wants them # added to the new category. self.toggle_cur_font_in_cat(None, newcat) if (entry): entry.set_text("") # # Callback for the main toggle buttons down the right half of the window. # #def category_toggle(self, widget, catname): # self.update_font_list() def change_button_color(self, btn, yesno): if self.oldcolors == None: self.oldcolors = btn.get_modifier_style().bg if self.highlightcolor == None: self.highlightcolor = gtk.gdk.Color(0, 65535, 0) if yesno: btn.modify_bg(gtk.STATE_NORMAL, self.highlightcolor) btn.modify_bg(gtk.STATE_ACTIVE, self.highlightcolor) btn.modify_bg(gtk.STATE_PRELIGHT, self.highlightcolor) btn.modify_bg(gtk.STATE_SELECTED, self.highlightcolor) else: btn.modify_bg(gtk.STATE_NORMAL, self.oldcolors[gtk.STATE_NORMAL]) btn.modify_bg(gtk.STATE_ACTIVE, self.oldcolors[gtk.STATE_ACTIVE]) btn.modify_bg(gtk.STATE_PRELIGHT, self.oldcolors[gtk.STATE_PRELIGHT]) btn.modify_bg(gtk.STATE_SELECTED, self.oldcolors[gtk.STATE_SELECTED]) def update_category_buttons(self, fontname): for i in range(0, len(self.category_buttons)): if fontname in self.category_fontlists[i]: self.change_button_color(self.category_buttons[i], True) # Can't call set_active because that invokes the # callback which recursively calls this function. #self.category_buttons[i].set_active(True) else: self.change_button_color(self.category_buttons[i], False) #self.category_buttons[i].set_active(False) def current_font(self): selection = self.treeView.get_selection() model, item = selection.get_selected_rows() if not item: return None # Only render the first font. return model[item[0][0]][0] def render_font(self, widget): # Heroic efforts to keep font changes in the Entry from # making the whole window resize every time. # GTK offers no way to say "make the damn entry stay the same size." #self.allow_grow = False #self.set_property("allow-grow", False) alloc = self.preview.get_allocation() if alloc.width > 1 and alloc.height > 1: self.preview.set_size_request(alloc.width, alloc.height) fontname = self.current_font() size = int(self.sizespin.get_value()) * pango.SCALE fontdesc = pango.FontDescription(fontname) fontdesc.set_size(size) if self.bold_btn.get_active(): fontdesc.set_weight(pango.WEIGHT_BOLD) else: fontdesc.set_weight(pango.WEIGHT_NORMAL) if self.italic_btn.get_active(): fontdesc.set_style(pango.STYLE_ITALIC) else: fontdesc.set_style(pango.STYLE_NORMAL) self.preview.modify_font(fontdesc) self.update_category_buttons(fontname) # Now that we're done changing things, # allow resizing from the user again: # Unfortunately this is still too early: the entry still resizes! # So for now, live with the user not being able to resize smaller. #self.preview.set_size_request(-1, -1) def show_all_categories(self, widget): for btn in self.category_buttons: btn.set_active(False) self.update_font_list() def python_fu_fontasia(): fontwin = FontApp() # fontwin.connect("destroy", gtk.main_quit) # gtk.main() while not fontwin.quit: gtk.main_iteration() pdb.gimp_context_set_font(fontwin.current_font()) if __name__ == '__main__': import sys # Are we being run as a GIMP plug-in? if sys.argv[0].endswith("gimp"): from gimpfu import * register( "python_fu_fontasia", "An alternate font chooser.", "An alternate font chooser.", "Akkana Peck", "Akkana Peck", "2016", "Fontasia...", "", [ # No arguments ], [], python_fu_fontasia, menu = "/Windows/" ) # Call GIMP's main loop, which will call python_fu_fontasia # and handle cleaning up afterward. main() else: fontwin = FontApp() while not fontwin.quit: gtk.main_iteration() # Trust that the FontApp has already saved new categories, etc. sys.exit(0)