#!/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)