Created
June 16, 2025 11:06
-
-
Save frankrolf/07d09be24d22413e55ba2a9b4d12a2c8 to your computer and use it in GitHub Desktop.
Contextual menu items for RoboFont’s font overview window
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
''' | |
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 | |
class FontContextualMenu(Subscriber): | |
def fontOverviewAdditionalContextualMenuItems(self, notification): | |
additionContextualMenuItems = notification[ | |
'lowLevelEvents'][0]['additionContextualMenuItems'] | |
font = CurrentFont() | |
selection = font.templateSelectedGlyphNames | |
if len(selection) > 0: | |
myMenuItems = [] | |
if len(selection) == 1: | |
myMenuItems = [ | |
('Filter same …', [ | |
('mark', self.add_attribute( | |
self.filter_glyph_attribute, font, 'markColor')), | |
('width', self.add_attribute( | |
self.filter_glyph_attribute, font, 'width')), | |
('anchor count', self.add_attribute( | |
self.filter_glyph_attribute, font, 'anchors')), | |
('anchor names', self.add_attribute( | |
self.filter_glyph_attribute, font, 'anchor_names')), | |
('component count', self.add_attribute( | |
self.filter_glyph_attribute, font, 'components')), | |
('contour count', self.add_attribute( | |
self.filter_glyph_attribute, font, 'contours')), | |
])] | |
myMenuItems.extend([ | |
('Filter …', [ | |
('dependent glyphs', self.add_font( | |
self.dependent_glyphs, font)), | |
('L kerning group', self.add_attribute( | |
self.kerning_glyph_group, font, 'L')), | |
('R kerning group', self.add_attribute( | |
self.kerning_glyph_group, font, 'R')), | |
]), | |
('Select …', [ | |
('non-suffixed base glyphs', self.add_font( | |
self.select_baseglyphs, font)), | |
('same mark color(s)', self.add_font( | |
self.select_same_mark_colors, font)), | |
('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_attribute( | |
self.select_Unicode, font, True)), | |
('glyphs without Unicode', self.add_attribute( | |
self.select_Unicode, font, False)), | |
]), | |
('Add alternate(s)', self.add_font( | |
self.add_alternates, font)), | |
('Initialize', self.add_font( | |
self.initialize, font)), | |
('Invert selection', self.add_font( | |
self.invert_selection, font)), | |
('Isolate selection', self.add_font( | |
self.isolate, font)), | |
]) | |
else: | |
myMenuItems = [ | |
('Select …', [ | |
('pure composite glyphs', self.add_font( | |
self.select_pure_composites, font)), | |
('mixed composite glyphs', self.add_font( | |
self.select_mixed_composites, font)), | |
]), | |
] | |
if CurrentFontWindow().getGlyphCollection().getQuery() is not None: | |
myMenuItems.append( | |
('Un-Isolate', self.add_font(self.unisolate, font)) | |
) | |
additionContextualMenuItems.extend(myMenuItems) | |
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_attribute(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_attribute(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): | |
# XXX this set selection stuff is WIP | |
try: | |
selected_set = smartSet.selectedSmartSets()[0] # XXX | |
set_index = smartSet.getSmartSets().index(selected_set) | |
ui_index = set_index + 2 | |
# AllGlyphs = 0, DEFAULT SETS = 1 | |
except Exception: | |
ui_index = 0 | |
for gname in font.selectedGlyphNames: | |
make_copy(font[gname]) | |
CurrentFontWindow().setSmartListSelection(ui_index) | |
def initialize(self, sender, font): | |
''' | |
initialize template glyph | |
''' | |
empty = set(font.templateSelectedGlyphNames) - set(font.selectedGlyphNames) | |
for gname in empty: | |
font.newGlyph(gname) | |
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): | |
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=( | |
'This will be called when a mojo.events ' | |
'`fontOverviewAdditionContextualMenuItems` event is posted. ' | |
'Default delay: 0 seconds.') | |
) | |
registerRoboFontSubscriber(FontContextualMenu) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
''' | |
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