#!/usr/bin/python # fontasia 0.6: # List all fonts on the system and let the user group them into categories. # # 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__() # 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() 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. # Toggle whether XXX # #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 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) selection = self.treeView.get_selection() model, item = selection.get_selected_rows() if not item : return size = int(self.sizespin.get_value()) * pango.SCALE # Only render the first font. fontname = model[item[0][0]][0] 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() FontApp() gtk.main()