Skip to content

Instantly share code, notes, and snippets.

@frankrolf
Last active September 23, 2026 12:49
Show Gist options
  • Select an option

  • Save frankrolf/07d09be24d22413e55ba2a9b4d12a2c8 to your computer and use it in GitHub Desktop.

Select an option

Save frankrolf/07d09be24d22413e55ba2a9b4d12a2c8 to your computer and use it in GitHub Desktop.
Contextual menu items for RoboFont’s font overview window
'''
Contextual menu items for RoboFont’s font overview window
'''
from AppKit import NSPredicate
from mojo import smartSet
from mojo.roboFont import CurrentFont
from mojo.subscriber import (
getRegisteredSubscriberEvents,
registerRoboFontSubscriber,
registerSubscriberEvent,
Subscriber)
from mojo.UI import CurrentFontWindow
from contextual_tools import filter_dependent, make_copy
SKIP_KEY = 'public.skipExportGlyphs'
class FontContextualMenu(Subscriber):
def fontOverviewAdditionalContextualMenuItems(self, notification):
additionContextualMenuItems = notification[
'lowLevelEvents'][0]['additionContextualMenuItems']
font = CurrentFont()
select_section = [
# basic selections
('Select …', [
('pure composite glyphs', self.add_font(
self.select_pure_composites, font)),
('mixed composite glyphs', self.add_font(
self.select_mixed_composites, font)),
('glyphs with Unicode', self.add_attr(
self.select_Unicode, font, True)),
('glyphs without Unicode', self.add_attr(
self.select_Unicode, font, False)),
# mark color RGBA or None
('same mark color(s)', self.add_font(
self.select_same_mark_colors, font)),
# select three when three.alt is selected
('unsuffixed base glyph(s)', self.add_font(
self.select_baseglyphs, font)),
])]
# basic selections when no glyph is selected to derive attributes from
item_title, item_list = select_section[0]
simple_item_list = [i for i in item_list if 'glyphs' in i[0]]
simple_select_section = [(item_title, simple_item_list)]
filter_section = [
# filter glyph overview based on selected glyph(s)
('Filter …', [
# composite glyphs
('dependent glyphs', self.add_font(
self.dependent_glyphs, font)),
# glyphs sharing the same L kerning group
('same L kerning group', self.add_attr(
self.kerning_glyph_group, font, 'L')),
# glyphs sharing the same R kerning group
('same R kerning group', self.add_attr(
self.kerning_glyph_group, font, 'R')),
])]
filter_same_section = [
# filters based on a single selected glyph
('Filter same …', [
('mark color', self.add_attr(
self.filter_glyph_attr, font, 'markColor')),
('width', self.add_attr(
self.filter_glyph_attr, font, 'width')),
('anchor count', self.add_attr(
self.filter_glyph_attr, font, 'anchors')),
('anchor names', self.add_attr(
self.filter_glyph_attr, font, 'anchor_names')),
('component count', self.add_attr(
self.filter_glyph_attr, font, 'components')),
('contour count', self.add_attr(
self.filter_glyph_attr, font, 'contours')),
])]
selection = font.templateSelectedGlyphNames
skipped_glyphs = font.lib.get(SKIP_KEY, [])
if len(selection) > 0:
menu_items = []
if len(selection) == 1:
# single glyph selected
menu_items.extend(filter_same_section)
menu_items.extend(filter_section)
menu_items.extend(select_section)
menu_items.append(
('Add alternate(s)', self.add_font(self.add_alternates, font)))
menu_items.append(
('Initialize', self.add_font(self.initialize, font)))
if set(selection) <= set(skipped_glyphs):
menu_items.append(
('Unskip', self.add_font(self.unskip, font)))
else:
menu_items.append(
('Skip', self.add_font(self.skip, font)))
menu_items.append(
('Invert selection', self.add_font(self.invert_selection, font)))
menu_items.append(
('Isolate selection', self.add_font(self.isolate, font)))
else:
menu_items = []
menu_items.extend(simple_select_section)
# a font winddow query is active
if CurrentFontWindow().getGlyphCollection().getQuery() is not None:
menu_items.append(
('Un-Isolate', self.add_font(self.unisolate, font))
)
additionContextualMenuItems.extend(menu_items)
def make_query(self, glyph_list):
query_text = 'Name in {"%s"}' % '", "'.join(glyph_list)
query = NSPredicate.predicateWithFormat_(query_text)
CurrentFontWindow().getGlyphCollection().setQuery(query)
def add_attr(self, func, font, attribute):
def wrapper(sender):
func(sender, font, attribute)
return wrapper
def add_font(self, func, font):
def wrapper(sender):
func(sender, font)
return wrapper
def filter_glyph_attr(self, sender, font, attribute):
def anchor_names(glyph):
return set([a.name for a in glyph.anchors])
if len(font.selectedGlyphNames):
repr_glyph_name = font.selectedGlyphNames[0]
repr_glyph = font[repr_glyph_name]
if attribute in ['anchors', 'contours', 'components']:
repr_attr = getattr(repr_glyph, attribute)
filter_list = [g.name for g in font if (
len(getattr(g, attribute)) == len(repr_attr))]
elif attribute == 'anchor_names':
filter_list = [
g.name for g in font if
anchor_names(g) == anchor_names(repr_glyph)]
else:
filter_list = [g.name for g in font if (
getattr(g, attribute) == getattr(repr_glyph, attribute))]
self.make_query(filter_list)
def invert_selection(self, sender, font):
combined_selection = (
list(font.selectedGlyphNames) + list(font.templateSelectedGlyphNames))
all_glyphs = set(
font.glyphOrder) | set([g.name for g in font.templateGlyphs])
invertedSelection = all_glyphs - set(combined_selection)
font.selectedGlyphNames = list(invertedSelection)
def select_pure_composites(self, sender, font):
font.selectedGlyphNames = [
g.name for g in font if g.components and not g.contours]
def select_mixed_composites(self, sender, font):
font.selectedGlyphNames = [
g.name for g in font if g.components and g.contours]
def isolate(self, sender, font):
self.make_query(font.templateSelectedGlyphNames)
def unisolate(self, sender, font):
CurrentFontWindow().getGlyphCollection().setQuery(None)
def add_alternates(self, sender, font):
# record selected sets
selected_sets = smartSet.selectedSmartSets()
for gname in font.selectedGlyphNames:
make_copy(font[gname])
# select the sets again
smartSet.selectSmartSets(selected_sets)
def initialize(self, sender, font):
'''
initialize template glyph
'''
empty = set(font.templateSelectedGlyphNames) - set(font.selectedGlyphNames)
for gname in empty:
font.newGlyph(gname)
def skip(self, sender, font):
'''
add selected glyph(s) to public.skipExportGlyphs lib key
'''
skipped_glyphs = font.lib.get(SKIP_KEY, []) + list(font.selectedGlyphNames)
font.lib[SKIP_KEY] = skipped_glyphs
font.changed()
def unskip(self, sender, font):
'''
remove selected glyph(s) from public.skipExportGlyphs lib key
'''
skipped_glyphs_before = font.lib.get(SKIP_KEY)
skipped_glyphs_after = [
gn for gn in skipped_glyphs_before
if gn not in list(font.selectedGlyphNames)]
if skipped_glyphs_after:
font.lib[SKIP_KEY] = skipped_glyphs_after
else:
# all glyphs unskipped
del font.lib[SKIP_KEY]
font.changed()
def select_baseglyphs(self, sender, font):
font.selectedGlyphNames = [
gname.split('.')[0] for gname in font.selectedGlyphNames if
font.selectedGlyphNames]
def select_same_mark_colors(self, sender, font):
sel_colors = [font[gn].markColor for gn in font.selectedGlyphNames]
font.selectedGlyphNames = [
gname for gname in font.keys() if
font[gname].markColor in sel_colors]
def select_Unicode(self, sender, font, has_Unicode):
if has_Unicode:
filter_list = [g.name for g in font if g.unicode]
else:
filter_list = [g.name for g in font if not g.unicode]
font.selectedGlyphNames = filter_list
def dependent_glyphs(self, sender, font):
all_dependent_glyphs = []
for gname in font.selectedGlyphNames:
all_dependent_glyphs.extend(filter_dependent(font[gname]))
self.make_query(all_dependent_glyphs)
def kerning_glyph_group(self, sender, font, side):
if font.selectedGlyphNames:
gname = font.selectedGlyphNames[0]
print('using first selected glyph:', gname)
containing_groups = font.groups.findGlyph(gname)
if containing_groups:
if side == 'L':
kern_group = next((
gr_name for gr_name in containing_groups if
gr_name.startswith('public.kern1.')), None)
if kern_group:
self.make_query(font.groups.get(kern_group))
else:
self.make_query([gname])
elif side == 'R':
kern_group = next((
gr_name for gr_name in containing_groups if
gr_name.startswith('public.kern2.')), None)
if kern_group:
self.make_query(font.groups.get(kern_group))
else:
self.make_query([gname])
eventName = 'fontOverviewAdditionalContextualMenuItems'
allEvents = getRegisteredSubscriberEvents()
if eventName not in allEvents:
registerSubscriberEvent(
subscriberEventName=eventName,
methodName='fontOverviewAdditionalContextualMenuItems',
lowLevelEventNames='fontOverviewAdditionContextualMenuItems',
dispatcher='roboFont',
delay=0,
documentation=('')
)
registerRoboFontSubscriber(FontContextualMenu)
'''
tools used by contextual_filters.py, needs to live in the same folder
'''
def filter_dependent(glyph):
component_users = [g for g in glyph.font if g.components]
components_used = {}
for user in component_users:
for component in user.components:
components_used.setdefault(
component.baseGlyph, []).append(user.name)
# the glyph itself
dependent_glyphs = [glyph.name]
# the glyph is used as a component in others
dependent_glyphs.extend(components_used.get(glyph.name, []))
for dep_gname in dependent_glyphs:
dependent_glyphs.extend(components_used.get(dep_gname, []))
return dependent_glyphs
def make_new_name(gname, alt_index=1):
return gname.split('.')[0] + '.' + '{:0>2d}'.format(alt_index)
def make_copy(glyph):
# XXX this could potentially be simplified with g.copy()
font = glyph.font
alt_index = 1
new_gname = make_new_name(glyph.name, alt_index)
while new_gname in font.keys():
alt_index += 1
new_gname = make_new_name(glyph.name, alt_index)
new_glyph = font.newGlyph(new_gname, clear=True)
for layer in [
lname for lname in font.layerOrder if lname != 'background'
]:
layer_glyph = glyph.getLayer(layer)
new_layer_glyph = new_glyph.getLayer(layer)
new_layer_glyph.appendGlyph(layer_glyph)
new_layer_glyph.width = layer_glyph.width
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment