├── .gitignore ├── img └── AdoptFromOtherFont.png ├── AnchorsPalette ├── AnchorsPalette.png └── Anchors.glyphsPalette │ └── Contents │ ├── MacOS │ └── plugin │ ├── Resources │ ├── AnchorsPaletteView.nib │ ├── plugin.py │ └── AnchorsPaletteView.xib │ └── Info.plist ├── CapsAndCorners ├── CapsAndCorners.png └── CapsAndCorners.glyphsPlugin │ └── Contents │ ├── MacOS │ └── plugin │ ├── Info.plist │ └── Resources │ └── plugin.py ├── HandleRelations ├── HandleRelations.png ├── HandleRelations.glyphsReporter │ └── Contents │ │ ├── MacOS │ │ └── plugin │ │ ├── Info.plist │ │ └── Resources │ │ └── plugin.py └── README.md ├── SuffixesPalette ├── SuffixesPalette.png └── Suffixes.glyphsPalette │ └── Contents │ ├── MacOS │ └── plugin │ ├── Info.plist │ └── Resources │ └── plugin.py ├── AlignmentPalette ├── AlignmentPalette.png └── Alignment.glyphsPalette │ └── Contents │ ├── MacOS │ └── plugin │ ├── Info.plist │ └── Resources │ └── plugin.py ├── ComponentsPalette └── Components.glyphsPalette │ └── Contents │ ├── MacOS │ └── plugin │ ├── Resources │ ├── ComponentsPaletteView.nib │ │ ├── keyedobjects.nib │ │ └── keyedobjects-101300.nib │ └── plugin.py │ └── Info.plist ├── Delete All Anchors.py ├── Remove Backup Layers.py ├── Show Components.py ├── Edit Previous Glyph.py ├── Make Quadratic Curve.py ├── Edit Next Glyph.py ├── Paste Background.py ├── Make Backup Layer.py ├── Glyphset Diff.py ├── Round Kerning.py ├── Delete Zero-Thickness Hints.py ├── Sort by Vertical Center.py ├── Make Cubic Curve.py ├── Select Inaccessible Glyphs.py ├── Print Coeffs.py ├── Delete BCP.py ├── Toggle Backup Layer.py ├── Expand Kerning.py ├── Adopt Background.py ├── Adopt from Other Font.py ├── Insert Glyph.py ├── Jump to Alternate.py ├── Symmetrify Terminal.py ├── README.md ├── Insert Glyph to Background.py ├── Symmetrify.py └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /img/AdoptFromOtherFont.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/HEAD/img/AdoptFromOtherFont.png -------------------------------------------------------------------------------- /AnchorsPalette/AnchorsPalette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/HEAD/AnchorsPalette/AnchorsPalette.png -------------------------------------------------------------------------------- /CapsAndCorners/CapsAndCorners.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/HEAD/CapsAndCorners/CapsAndCorners.png -------------------------------------------------------------------------------- /HandleRelations/HandleRelations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/HEAD/HandleRelations/HandleRelations.png -------------------------------------------------------------------------------- /SuffixesPalette/SuffixesPalette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/HEAD/SuffixesPalette/SuffixesPalette.png -------------------------------------------------------------------------------- /AlignmentPalette/AlignmentPalette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/HEAD/AlignmentPalette/AlignmentPalette.png -------------------------------------------------------------------------------- /AnchorsPalette/Anchors.glyphsPalette/Contents/MacOS/plugin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/HEAD/AnchorsPalette/Anchors.glyphsPalette/Contents/MacOS/plugin -------------------------------------------------------------------------------- /SuffixesPalette/Suffixes.glyphsPalette/Contents/MacOS/plugin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/HEAD/SuffixesPalette/Suffixes.glyphsPalette/Contents/MacOS/plugin -------------------------------------------------------------------------------- /AlignmentPalette/Alignment.glyphsPalette/Contents/MacOS/plugin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/HEAD/AlignmentPalette/Alignment.glyphsPalette/Contents/MacOS/plugin -------------------------------------------------------------------------------- /CapsAndCorners/CapsAndCorners.glyphsPlugin/Contents/MacOS/plugin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/HEAD/CapsAndCorners/CapsAndCorners.glyphsPlugin/Contents/MacOS/plugin -------------------------------------------------------------------------------- /ComponentsPalette/Components.glyphsPalette/Contents/MacOS/plugin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/HEAD/ComponentsPalette/Components.glyphsPalette/Contents/MacOS/plugin -------------------------------------------------------------------------------- /HandleRelations/HandleRelations.glyphsReporter/Contents/MacOS/plugin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/HEAD/HandleRelations/HandleRelations.glyphsReporter/Contents/MacOS/plugin -------------------------------------------------------------------------------- /AnchorsPalette/Anchors.glyphsPalette/Contents/Resources/AnchorsPaletteView.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/HEAD/AnchorsPalette/Anchors.glyphsPalette/Contents/Resources/AnchorsPaletteView.nib -------------------------------------------------------------------------------- /ComponentsPalette/Components.glyphsPalette/Contents/Resources/ComponentsPaletteView.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/HEAD/ComponentsPalette/Components.glyphsPalette/Contents/Resources/ComponentsPaletteView.nib/keyedobjects.nib -------------------------------------------------------------------------------- /ComponentsPalette/Components.glyphsPalette/Contents/Resources/ComponentsPaletteView.nib/keyedobjects-101300.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/HEAD/ComponentsPalette/Components.glyphsPalette/Contents/Resources/ComponentsPaletteView.nib/keyedobjects-101300.nib -------------------------------------------------------------------------------- /Delete All Anchors.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Delete All Anchors 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | Removes all anchors from the selected glyphs. 9 | ''' 10 | 11 | from GlyphsApp import * 12 | 13 | doc = Glyphs.currentDocument 14 | font = doc.font 15 | 16 | for selectedLayer in doc.selectedLayers(): 17 | glyph = selectedLayer.parent 18 | for layer in glyph.layers: 19 | layer.anchors = [] 20 | -------------------------------------------------------------------------------- /Remove Backup Layers.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Remove Backup Layers 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | Removes all backup layers (i.e. those created using the "Copy" button) from the selected glyphs. 9 | ''' 10 | 11 | font = Glyphs.currentDocument.font 12 | selected_glyphs = set( [ layer.parent for layer in font.selectedLayers ] ) 13 | 14 | for glyph in selected_glyphs: 15 | for i in range(len(glyph.layers) - 1, -1, -1): 16 | if not glyph.layers[i].isSpecialLayer and not glyph.layers[i].isMasterLayer: 17 | del glyph.layers[i] 18 | -------------------------------------------------------------------------------- /Show Components.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Show Components 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | Shows the glyphs contained within the selected glyph(s). 9 | Useful for “dissolving” ligatures, or to determine glyph set dependencies 10 | (i.e. glyphs that cannot be deleted if the selected ones are to be retained). 11 | ''' 12 | 13 | tabLayers = Glyphs.font.currentTab.layers 14 | for layer in Glyphs.font.selectedLayers: 15 | masterId = layer.master.id 16 | for component in layer.components: 17 | glyph = Glyphs.font.glyphs[component.name] 18 | newLayer = glyph.layers[masterId] 19 | tabLayers.append(newLayer) 20 | -------------------------------------------------------------------------------- /Edit Previous Glyph.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Edit Previous Glyph 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | Activates the previous glyph in the tab for editing. You can give it a keyboard shortcut in the macOS system preferences. 9 | ''' 10 | 11 | font = Glyphs.font 12 | if font: 13 | tab = font.currentTab 14 | if tab: 15 | # move cursor: 16 | newPosition = (tab.layersCursor - 1 + len(tab.layers)) % (len(tab.layers)) 17 | tab.layersCursor = newPosition 18 | # re-center glyph: 19 | vp = tab.viewPort 20 | vp.origin.x = tab.selectedLayerOrigin.x + 0.5 * ( font.selectedLayers[0].width * tab.scale - vp.size.width ) 21 | if newPosition == len(tab.layers) - 1: 22 | print() 23 | # ^ very strange: if we don’t do this 24 | # then the glyph is not centred correctly 25 | # if the text cursor is active 26 | # explanation from Georg: 27 | # https://forum.glyphsapp.com/t/centering-the-current-glyph-in-tab/29408/18 28 | tab.viewPort = vp 29 | # TODO: in case the new glyph is on a different line, also adjust y 30 | -------------------------------------------------------------------------------- /Make Quadratic Curve.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Make Quadratic Curve 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | Converts cubic curves to quadratic curves (if a BCP is selected). 9 | 10 | This does not try to retain the shape, it does not add or remove nodes, it simply re-defines the cuve type. 11 | ''' 12 | 13 | def samePosition(node1, node2): 14 | return node1.position.x == node2.position.x and node1.position.y == node2.position.y 15 | 16 | for selectedLayer in Glyphs.currentDocument.selectedLayers(): 17 | glyph = selectedLayer.parent 18 | glyph.beginUndo() 19 | for layer in glyph.layers: 20 | for path in layer.paths: 21 | for node in path.nodes: 22 | if not node.selected: 23 | continue 24 | if node.type != OFFCURVE: 25 | continue 26 | if node.nextNode.type == CURVE: 27 | # node is the second cubic BCP 28 | node.nextNode.type = QCURVE 29 | elif node.nextNode.nextNode.type == CURVE: 30 | # node is the first cubic BCP 31 | node.nextNode.nextNode.type = QCURVE 32 | glyph.endUndo() 33 | -------------------------------------------------------------------------------- /Edit Next Glyph.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Edit Next Glyph 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | Activates the next glyph in the tab for editing. You can give it a keyboard shortcut in the macOS system preferences. 9 | ''' 10 | 11 | font = Glyphs.font 12 | if font: 13 | tab = font.currentTab 14 | if tab: 15 | # move cursor: 16 | # (adopted from https://glyphsapp.com/news/glyphs-3-2-released) 17 | newPosition = (tab.layersCursor + 1) % (len(tab.layers)) 18 | tab.layersCursor = newPosition 19 | # re-center glyph: 20 | vp = tab.viewPort 21 | vp.origin.x = tab.selectedLayerOrigin.x + 0.5 * ( font.selectedLayers[0].width * tab.scale - vp.size.width ) 22 | if newPosition == 0: 23 | print() 24 | # ^ very strange: if we don’t do this 25 | # then the glyph is not centred correctly 26 | # if the text cursor is active 27 | # explanation from Georg: 28 | # https://forum.glyphsapp.com/t/centering-the-current-glyph-in-tab/29408/18 29 | tab.viewPort = vp 30 | # TODO: in case the new glyph is on a different line, also adjust y 31 | -------------------------------------------------------------------------------- /Paste Background.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Paste Background 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | Pastes the background into the current layer. 9 | 10 | Components are pasted as paths (i.e. decomposed). 11 | ''' 12 | 13 | from GlyphsApp import * 14 | 15 | doc = Glyphs.currentDocument 16 | font = doc.font 17 | layers = doc.selectedLayers() 18 | glyph = layers[0].parent 19 | 20 | glyph.beginUndo() 21 | 22 | for layer in layers: 23 | # deselect all in the foreground 24 | for path in layer.paths: 25 | for node in path.nodes: 26 | layer.removeObjectFromSelection_( node ) 27 | # layer.removeObjectsFromSelection_( path.pyobjc_instanceMethods.nodes() ) 28 | # insert the background contents and select them 29 | for path in layer.background.copyDecomposedLayer().paths: 30 | layer.paths.append( path.copy() ) 31 | # select path 32 | try: 33 | # Glyphs 2 34 | for node in layer.paths[-1].nodes: 35 | layer.addSelection_( node ) 36 | except: 37 | # Glyphs 3 38 | for node in layer.shapes[-1].nodes: 39 | layer.addSelection_( node ) 40 | 41 | glyph.endUndo() 42 | -------------------------------------------------------------------------------- /Make Backup Layer.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Make Backup Layer 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | Same as the “+” button on the Layers palette. 9 | 10 | I wrote this script mostly because it allows me to assign a keyboard shortcut. Also, it correctly copies brace layers, unlike Glyphs’ built-in function. 11 | ''' 12 | 13 | import datetime 14 | import Foundation 15 | 16 | formatter = Foundation.NSDateFormatter.alloc().init() 17 | formatter.setDateStyle_(Foundation.NSDateFormatterMediumStyle) 18 | formatter.setTimeStyle_(Foundation.NSDateFormatterShortStyle) 19 | now = Foundation.NSDate.date() 20 | nowString = formatter.stringFromDate_(now).replace(' 202', ' 2') 21 | # ^ seems to be the best way to create the same as Glyphs, as of now 22 | 23 | for layer in Glyphs.font.selectedLayers: 24 | glyph = layer.parent 25 | newLayer = layer.copy() 26 | if newLayer.isSpecialLayer: 27 | newLayer.name = '{.} ' + nowString 28 | del newLayer.attributes["axisRules"] 29 | del newLayer.attributes["coordinates"] 30 | else: 31 | newLayer.name = nowString 32 | glyph.layers.append( newLayer ) 33 | -------------------------------------------------------------------------------- /Glyphset Diff.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Glyphset Diff 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | Shows the glyphs that are not present in the other font. 9 | ''' 10 | 11 | from GlyphsApp import Glyphs, Message 12 | from Cocoa import NSPredicate 13 | 14 | def setFilter(font, glyphNames): 15 | if not glyphNames: 16 | return 17 | fontView = font.fontView 18 | glyphsArrayController = fontView.glyphsArrayController() 19 | predicate = NSPredicate.predicateWithFormat_("name IN %@", glyphNames) 20 | glyphsArrayController.setFilterPredicate_(predicate) 21 | 22 | if len(Glyphs.documents) == 2: 23 | thisFont = Glyphs.documents[0].font 24 | otherFont = Glyphs.documents[1].font 25 | diff = list(set(thisFont.glyphNames()) - set(otherFont.glyphNames())) 26 | if diff: 27 | setFilter(thisFont, diff) 28 | else: 29 | diffReverse = set(otherFont.glyphNames()) - set(thisFont.glyphNames()) 30 | if diffReverse: 31 | Message('The other font has additional glyphs.', '') 32 | else: 33 | Message('The glyph sets are identical', '') 34 | else: 35 | Message('Please make sure that\nexactly two fonts are open', '') 36 | -------------------------------------------------------------------------------- /Round Kerning.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Round Kerning 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | Rounds the kerning values to a chosen number. 9 | 10 | In addition, values smaller than MIN_VALUE are erased. 11 | ''' 12 | 13 | MIN_VALUE = 4 14 | QUANTISATION = 1 15 | from GlyphsApp import * 16 | from GlyphsApp import MGOrderedDictionary 17 | import time 18 | font = Glyphs.font 19 | 20 | def filterKern(): 21 | kerning = font.kerning 22 | for master in Font.masters: 23 | masterDict = kerning[master.id] 24 | newMasterDict = MGOrderedDictionary.new() 25 | firstKeys = masterDict.keys() 26 | for firstKey in firstKeys: 27 | rightDict = masterDict[firstKey] 28 | secondKeys = rightDict.keys() 29 | newRightDict = MGOrderedDictionary.new() 30 | for secondKey in secondKeys: 31 | value = rightDict[secondKey] 32 | value = round(value / QUANTISATION) * QUANTISATION 33 | if abs(value) >= MIN_VALUE: 34 | newRightDict[secondKey] = value 35 | if len(newRightDict) > 0: 36 | newMasterDict[firstKey] = newRightDict 37 | kerning[master.id] = newMasterDict 38 | font.kerning = kerning 39 | 40 | start = time.time() 41 | 42 | filterKern() 43 | 44 | end = time.time() 45 | print("time", end - start) -------------------------------------------------------------------------------- /Delete Zero-Thickness Hints.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Delete Zero-Thickness Hints 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | Removes all zero-thickness, 9 | or otherwise invalid hints 10 | from all glyphs in the font. 11 | ''' 12 | 13 | doc = Glyphs.currentDocument 14 | font = doc.font 15 | 16 | deletions_count = 0 17 | for glyph in font.glyphs: 18 | for layer in glyph.layers: 19 | for indx in range( len( layer.hints ) - 1, -1, -1 ): 20 | hint = layer.hints[indx] 21 | if hint.originNode is None: 22 | # this is an invalid hint that was probably set by Glyphs’ auto-instructing 23 | print ( "deleting invalid hint from", layer.parent.name ) 24 | del( layer.hints[indx] ) 25 | deletions_count += 1 26 | continue 27 | if hint.targetNode: 28 | if hint.horizontal: 29 | if hint.originNode.y == hint.targetNode.y: 30 | del( layer.hints[indx] ) 31 | deletions_count += 1 32 | print( 'deleted zero-width hint from', glyph.name ) 33 | else: 34 | if hint.originNode.x == hint.targetNode.x: 35 | del( layer.hints[indx] ) 36 | deletions_count += 1 37 | print( 'deleted zero-width hint from', glyph.name ) 38 | 39 | Message( '', 'Deleted %i hints.' % deletions_count ) 40 | -------------------------------------------------------------------------------- /ComponentsPalette/Components.glyphsPalette/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | plugin 9 | CFBundleIdentifier 10 | com.remixtools.ComponentsPalette 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Components Palette 15 | CFBundleVersion 16 | 1 17 | CFBundleShortVersionString 18 | 1.0 19 | UpdateFeedURL 20 | https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/master/ComponentsPalette/Components.glyphsPalette/Contents/Info.plist 21 | productPageURL 22 | https://github.com/justanotherfoundry/freemix-glyphsapp 23 | productReleaseNotes 24 | Initial release. 25 | NSHumanReadableCopyright 26 | Copyright, Tim Ahrens, 2025 27 | NSPrincipalClass 28 | ComponentsPalette 29 | PyMainFileNames 30 | 31 | plugin.py 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /AnchorsPalette/Anchors.glyphsPalette/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | plugin 9 | CFBundleIdentifier 10 | com.remixtools.AnchorsPalette 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Anchors Palette 15 | CFBundleVersion 16 | 11 17 | CFBundleShortVersionString 18 | 1.10 19 | UpdateFeedURL 20 | https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/master/AnchorsPalette/Anchors.glyphsPalette/Contents/Info.plist 21 | productPageURL 22 | https://github.com/justanotherfoundry/freemix-glyphsapp 23 | productReleaseNotes 24 | The display order is now sorted by y position 25 | NSHumanReadableCopyright 26 | Copyright, Tim Ahrens, 2022 27 | NSPrincipalClass 28 | AnchorsPalette 29 | PyMainFileNames 30 | 31 | plugin.py 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /AlignmentPalette/Alignment.glyphsPalette/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | plugin 9 | CFBundleIdentifier 10 | com.remixtools.AlignmentPalette 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Alignment Palette 15 | CFBundleVersion 16 | 122 17 | CFBundleShortVersionString 18 | 1.21 19 | UpdateFeedURL 20 | https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/master/AlignmentPalette/Alignment.glyphsPalette/Contents/Info.plist 21 | productPageURL 22 | https://github.com/justanotherfoundry/freemix-glyphsapp 23 | productReleaseNotes 24 | allow glyph/name or character; all masters at once 25 | NSHumanReadableCopyright 26 | Copyright Tim Ahrens, 2025 27 | NSPrincipalClass 28 | AlignmentPalette 29 | PyMainFileNames 30 | 31 | plugin.py 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /SuffixesPalette/Suffixes.glyphsPalette/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | plugin 9 | CFBundleIdentifier 10 | com.remixtools.SuffixesPalette 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Suffixes Palette 15 | CFBundleVersion 16 | 10 17 | CFBundleShortVersionString 18 | 1.08 19 | UpdateFeedURL 20 | https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/master/SuffixesPalette/Suffixes.glyphsPalette/Contents/Info.plist 21 | productPageURL 22 | https://github.com/justanotherfoundry/freemix-glyphsapp 23 | productReleaseNotes 24 | Update Python binary for M1 and macOS Big Sur compatibility 25 | NSHumanReadableCopyright 26 | Copyright, Tim Ahrens, 2020 27 | NSPrincipalClass 28 | SuffixesPalette 29 | PyMainFileNames 30 | 31 | plugin.py 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /CapsAndCorners/CapsAndCorners.glyphsPlugin/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | plugin 9 | CFBundleIdentifier 10 | com.remixtools.CapsAndCorners 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Caps and Corners 15 | CFBundleVersion 16 | 1 17 | CFBundleShortVersionString 18 | 1.0 19 | UpdateFeedURL 20 | https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/master/CapsAndCorners/CapsAndCorners.glyphsPlugin/Contents/Info.plist 21 | productPageURL 22 | https://github.com/justanotherfoundry/freemix-glyphsapp 23 | productReleaseNotes 24 | initial version 25 | NSHumanReadableCopyright 26 | Copyright, Tim Ahrens, 2022 27 | NSPrincipalClass 28 | CapsAndCorners 29 | MinGlyphsVersion 30 | 3.0 31 | PyMainFileNames 32 | 33 | plugin.py 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /HandleRelations/HandleRelations.glyphsReporter/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | plugin 9 | CFBundleIdentifier 10 | com.RMX.HandleRelations 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | HandleRelations 15 | CFBundleShortVersionString 16 | 1.04 17 | CFBundleVersion 18 | 5 19 | UpdateFeedURL 20 | https://raw.githubusercontent.com/justanotherfoundry/freemix-glyphsapp/master/HandleRelations/HandleRelations.glyphsReporter/Contents/Info.plist 21 | productPageURL 22 | https://github.com/justanotherfoundry/freemix-glyphsapp 23 | productReleaseNotes 24 | For shallow curves, show handle lengths relative to chord 25 | NSHumanReadableCopyright 26 | Copyright, Tim Ahrens, 2023 27 | NSPrincipalClass 28 | HandleRelations 29 | PyMainFileNames 30 | 31 | plugin.py 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Sort by Vertical Center.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Sort by Vertical Center 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | Outputs the selected glyphs (from Font or Edit view) in a new tab, sorted by vertical center. 9 | ''' 10 | 11 | import GlyphsApp 12 | 13 | font = Glyphs.font 14 | 15 | # returns a list of GSLayer objects for the current selection 16 | def get_selected_layers(f): 17 | if f.currentTab: 18 | # Edit View: selected layers 19 | return [l for l in f.currentTab.selectedLayers if not isinstance(l, GSControlLayer)] 20 | elif f.selection: 21 | # Font View: selected glyphs 22 | layers = [] 23 | master_id = f.selectedFontMaster.id 24 | return [g.layers[master_id] for g in f.selection] 25 | return [] 26 | 27 | # computes bounding box center Y 28 | def center_y(layer): 29 | bbox = layer.bounds 30 | if bbox.size.height == 0: 31 | return 0 32 | return bbox.origin.y + bbox.size.height / 2.0 33 | 34 | # sort layers by their center y 35 | layers_without_duplicates = list(dict.fromkeys(get_selected_layers(font))) 36 | sorted_layers = sorted(layers_without_duplicates, key=center_y) 37 | 38 | tab_text = "" 39 | current_center = None 40 | 41 | for layer in sorted_layers: 42 | cy = center_y(layer) 43 | if current_center is None: 44 | current_center = cy 45 | elif abs(cy - current_center) > 0.6: 46 | tab_text += "\n" 47 | current_center = cy 48 | tab_text += "/" + layer.parent.name 49 | 50 | # Open in new tab 51 | font.newTab(tab_text) 52 | -------------------------------------------------------------------------------- /Make Cubic Curve.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Make Cubic Curve 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | Same as Path > Other > Convert to cubic 9 | but it can be applied to individual curve segments 10 | (i.e. respects the selection). 11 | 12 | Careful: This script is always applied to all all layers. 13 | ''' 14 | 15 | def samePosition(node1, node2): 16 | return node1.position.x == node2.position.x and node1.position.y == node2.position.y 17 | 18 | for selectedLayer in Glyphs.currentDocument.selectedLayers(): 19 | glyph = selectedLayer.parent 20 | glyph.beginUndo() 21 | for layer in glyph.layers: 22 | layer.saveHints() 23 | for path in layer.paths: 24 | for i in range(len(path.nodes)): 25 | node = path.nodes[i] 26 | if not node.selected: 27 | continue 28 | if node.type != OFFCURVE: 29 | continue 30 | if node.nextNode.type == QCURVE: 31 | # node is a quad BCP 32 | bcp1pos = node.position 33 | bcp2pos = node.position 34 | bcp1pos.x += (node.nextNode.position.x - bcp1pos.x) / 3 35 | bcp1pos.y += (node.nextNode.position.y - bcp1pos.y) / 3 36 | bcp2pos.x += (node.prevNode.position.x - bcp2pos.x) / 3 37 | bcp2pos.y += (node.prevNode.position.y - bcp2pos.y) / 3 38 | newNode = node.copy() 39 | node.position = bcp1pos 40 | newNode.position = bcp2pos 41 | path.nodes.insert(i, newNode) 42 | node.nextNode.type = CURVE 43 | break 44 | layer.restoreHints() 45 | glyph.endUndo() 46 | -------------------------------------------------------------------------------- /Select Inaccessible Glyphs.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Select Inaccessible Glyphs 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | Run this macro while in the Font View. 9 | 10 | The macro selects all glyphs that 11 | - export 12 | - do not have a Unicode value and 13 | - are not covered by any OT feature 14 | 15 | i.e. are not accessible in the final font. 16 | 17 | These glyphs can usually be excluded from the final exported OTF font. 18 | ''' 19 | 20 | from GlyphsApp import * 21 | import re 22 | 23 | doc = Glyphs.currentDocument 24 | font = doc.font 25 | 26 | # find all glyphs that are substitutions in the OT features, i.e. after the "sub ... by" 27 | 28 | # collect all features and featurePrefixes 29 | all_features = [ f.code for f in font.features ] 30 | all_features.extend( [ f.code for f in font.featurePrefixes ] ) 31 | 32 | # put all into one string, without linebreaks, without brackets 33 | features_text = ' '.join( ' '.join( all_features ).splitlines() ).replace( ']', ' ' ).replace( '[', ' ' ) 34 | # find substitutions via regex 35 | substitutions = ' '.join( re.findall( r" by ([^;]*);", features_text ) ).split() 36 | 37 | glyphs_inaccessible = [ glyph for glyph in font.glyphs if glyph.export and not glyph.name in substitutions and not glyph.name.startswith('.') and ( not glyph.glyphInfo or not glyph.glyphInfo.unicode ) ] 38 | 39 | # select inaccessible glyphs (and deselect all others) 40 | for glyph in font.glyphs: 41 | glyph.selected = glyph in glyphs_inaccessible 42 | -------------------------------------------------------------------------------- /Print Coeffs.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Print Coeffs 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | Prints the interpolation coefficients for each master in all instances (make sure the Macro Panel is open). 9 | ''' 10 | 11 | import re 12 | 13 | abbreviations = { 14 | 'Light': 'Lt', 15 | 'Bold': 'Bd', 16 | 'Text': 'Tx', 17 | 'Banner': 'Bn', 18 | 'Tall': 'Ta', 19 | 'Condensed': 'Cnd', 20 | 'Compressed': 'Cmp', 21 | } 22 | abbreviations = dict((re.escape(k), v) for k, v in abbreviations.items()) 23 | pattern = re.compile("|".join(abbreviations.keys())) 24 | master_names = [ pattern.sub(lambda m: abbreviations[re.escape(m.group(0))], master.name).replace(' ','') for master in Glyphs.font.masters ] 25 | longest_master_name = max( master_names, key = len ) 26 | master_column_width = max( 11, len( longest_master_name ) + 1 ) 27 | 28 | instance_names = [ instance.fullName for instance in Glyphs.font.instances ] 29 | longest_instance_name = max( instance_names, key = len ) 30 | first_column_width = len( longest_instance_name ) + 5 31 | 32 | print() 33 | print( ''.ljust( first_column_width ), ''.join( [ n.rjust( master_column_width ) for n in master_names] ) ) 34 | 35 | for instance in Glyphs.font.instances: 36 | print( ' ' if instance.active else '*', end='' ) 37 | print( instance.fullName.ljust( first_column_width ), end='' ) 38 | for master in Glyphs.font.masters: 39 | coeff_str = '' 40 | try: 41 | coeff = instance.instanceInterpolations[master.id] 42 | if coeff: 43 | coeff_str = '%10.2f%%' % ( coeff * 100 ) 44 | except (KeyError, TypeError): 45 | pass 46 | print( coeff_str.rjust( master_column_width ), end='' ) 47 | print() 48 | -------------------------------------------------------------------------------- /Delete BCP.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Delete BCP 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | This literally deletes the selected BCP(s): 9 | If you delete one of the two BCPs in a cubic curve then it becomes quadratic. 10 | ''' 11 | 12 | for selectedLayer in Glyphs.currentDocument.selectedLayers(): 13 | glyph = selectedLayer.parent 14 | glyph.beginUndo() 15 | for layer in glyph.layers: 16 | for path in layer.paths: 17 | for i in range(len(path.nodes) - 1, -1, -1): 18 | node = path.nodes[i] 19 | if node.selected and node.type == OFFCURVE: 20 | if node.nextNode.type == OFFCURVE: 21 | # node is the first cubic BCP 22 | node.nextNode.nextNode.type = QCURVE 23 | if node.position == node.prevNode.position: 24 | # to-be-deleted BCP is retracted 25 | bcp = node.nextNode.position 26 | h2_x = node.nextNode.nextNode.position.x - bcp.x 27 | h2_y = node.nextNode.nextNode.position.y - bcp.y 28 | bcp.x = round( node.nextNode.nextNode.position.x - h2_x * 0.9 ) 29 | bcp.y = round( node.nextNode.nextNode.position.y - h2_y * 0.9 ) 30 | node.nextNode.position = bcp 31 | elif node.prevNode.type == OFFCURVE: 32 | # node is the second cubic BCP 33 | node.nextNode.type = QCURVE 34 | if node.position == node.nextNode.position: 35 | bcp = node.prevNode.position 36 | h1_x = bcp.x - node.prevNode.prevNode.position.x 37 | h1_y = bcp.y - node.prevNode.prevNode.position.y 38 | bcp.x = round( node.prevNode.prevNode.position.x + h1_x * 0.9 ) 39 | bcp.y = round( node.prevNode.prevNode.position.y + h1_y * 0.9 ) 40 | node.prevNode.position = bcp 41 | else: 42 | # curve is quadratic and is becoming a straight line 43 | node.nextNode.type = LINE 44 | node.nextNode.smooth = False 45 | node.prevNode.smooth = False 46 | del path.nodes[i] 47 | glyph.endUndo() 48 | -------------------------------------------------------------------------------- /Toggle Backup Layer.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Toggle Backup Layer 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | This script toggles between the currently selected layer and the master layer (alternatively, between the master and the last backup layer in the list). 9 | 10 | If given a keyboard shortcut, this is very useful for comparing two versions of a glyph. 11 | ''' 12 | 13 | font = Glyphs.font 14 | currentTab = font.currentTab 15 | currentLayer = font.selectedLayers[0] 16 | currentGlyph = currentLayer.parent 17 | 18 | textStorage = currentTab.graphicView().textStorage() 19 | text = textStorage.text() 20 | 21 | selectedRange = currentTab.graphicView().selectedRange() 22 | selectedRange.length = 1 23 | try: 24 | layerIdAttribute = text.attribute_atIndex_effectiveRange_("GSLayerIdAttrib", selectedRange.location, None)[0] 25 | except KeyError: 26 | layerIdAttribute = None 27 | 28 | # determine last layer: 29 | for layer in currentGlyph.layers: 30 | if layer.associatedMasterId == currentLayer.associatedMasterId: 31 | lastLayerId = layer.layerId 32 | 33 | textStorage.willChangeValueForKey_("text") 34 | if layerIdAttribute == currentLayer.layerId: 35 | # currently on backup layer. switch to master layer: 36 | text.removeAttribute_range_("GSLayerIdAttrib", selectedRange) 37 | backupLayerId = layerIdAttribute 38 | else: 39 | # currently on master layer. 40 | try: 41 | backupLayer = currentGlyph.layers[backupLayerId] 42 | except (TypeError, NameError): 43 | # uninitialized backupLayerId 44 | backupLayer = None 45 | if backupLayer and backupLayer.associatedMasterId != currentLayer.layerId: 46 | # remembered layer is from a different master. let’s not switch to that one. 47 | backupLayer = None 48 | if not backupLayer: 49 | # backup layer not specified (remembered). use the last layer: 50 | backupLayerId = lastLayerId 51 | text.addAttribute_value_range_("GSLayerIdAttrib", backupLayerId, selectedRange) 52 | 53 | if backupLayerId == lastLayerId: 54 | # “forget” the used backup layer: 55 | backupLayerId = None 56 | # the effect is that when new backup layers are added, 57 | # it will switch to the last (added) layer instead of the current one 58 | 59 | # trigger UI update: 60 | textStorage.didChangeValueForKey_("text") 61 | currentTab.textCursor = currentTab.textCursor 62 | -------------------------------------------------------------------------------- /HandleRelations/README.md: -------------------------------------------------------------------------------- 1 | # Show Handle Relations 2 | 3 | Interpolations can have unintentional kinks in smooth connections between curves, 4 | or nodes that connect a straight line and a curve (so-called tangent points). 5 | 6 | There is no risk of kinks if the angle is the same in all masters 7 | (which is always given for horizontal or vertical extrema). However, the design often makes this impossible. 8 | In that case, kinks can be avoided if the relative length of the handles is the same in all masters. 9 | 10 | This add-on for Glyphs helps you achieve this by showing the relative handle lengths 11 | for connections that may develop kinks after interpolation. 12 | 13 | Furthermore, the label give you a quick overview of the nodes that need to be fixed: 14 | * Red labels mean that the handle relation is wrong, and you should correct them as necessary. 15 | * Black labels are close enough to the other masters. 16 | * Light grey labels mean the position is perfect. 17 | 18 | https://github.com/justanotherfoundry/freemix-glyphsapp/assets/1331354/38b19f29-538a-4533-9d61-2e14bd39421e 19 | 20 | Note: If any nodes are selected then only these will be shown. 21 | 22 | To do: 23 | * Calculate and display the intra-curve coefficients. For shallow curves, this may be non-trivial. 24 | * Refine the definition of “close enough”. 25 | * How about intermediate special layers? 26 | * Look at the interpolated exports and ignore masters that are not used in interpolations 27 | (to be precise, there may even be independent groups of masters that are only interpolated among themselves). 28 | * Detect if all masters have the same (or similar enough) angle. 29 | What if some masters have the same angle, and some have the same relative handle lengths? 30 | Need to think about that carefully, and respect the interpolated exports. 31 | * Auto-fix the problems (oh wait, that will be a nice feature for [RMX](https://remix-tools.com) 2.0 some day) 32 | 33 | Fun fact: I already implemented automatically establishing coefficients consitency 34 | in what was essentially the [very first version](https://forum.fontlab.com/archive-old-fontlab-forum/fl-macro-general-outline-optimiser/msg6314) 35 | of the RMX Harmonizer in 2004. Haha, seems like no-one could be bothered back then, at least there are no replies. 36 | See [Optimiser.py](Optimiser.py) for details (in the comments). 37 | “In other words, the macro will remove dents and bumps and allow you to inter- and extrapolate like crazy.” Now that’s quite something! 38 | -------------------------------------------------------------------------------- /Expand Kerning.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Expand Kerning 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | Converts the class kerning to glyph-glyph pairs 9 | ''' 10 | 11 | doc = Glyphs.currentDocument 12 | font = doc.font 13 | master_id = font.selectedFontMaster.id 14 | 15 | right_groups = set( [ '@MMK_R_' + glyph.leftKerningGroup for glyph in font.glyphs if glyph.leftKerningGroup ] ) 16 | left_groups = set( [ '@MMK_L_' + glyph.rightKerningGroup for glyph in font.glyphs if glyph.rightKerningGroup ] ) 17 | glyph_names = set( [ glyph.name for glyph in font.glyphs ] ) 18 | right_sides = right_groups.union( glyph_names ) 19 | 20 | # expand 21 | flatKerning = { master_id: {} } 22 | kerningDict = flatKerning[master_id] 23 | for leftGlyph in font.glyphs: 24 | leftLayer = leftGlyph.layers[master_id] 25 | for rightGlyph in font.glyphs: 26 | rightLayer = rightGlyph.layers[master_id] 27 | kernValue = font.kerningFirstLayer_secondLayer_(leftLayer, rightLayer) 28 | 29 | if right_glyph.leftKerningGroup: 30 | right_name = '@MMK_R_' + right_glyph.leftKerningGroup 31 | else: 32 | right_name = right_glyph.name 33 | value = font.kerningForPair( master_id, left_name, right_name ) 34 | if value is not None and value != 0 and value < 77000: 35 | existing_value = font.kerningForPair( master_id, left_glyph.name, right_glyph.name ) 36 | if existing_value is not None and existing_value < 77000: 37 | continue 38 | exception_value = font.kerningForPair( master_id, left_glyph.name, right_name ) 39 | if exception_value is not None and exception_value < 77000: 40 | value = exception_value 41 | exception_value = font.kerningForPair( master_id, left_name, right_glyph.name ) 42 | if exception_value is not None and exception_value < 77000: 43 | value = exception_value 44 | try: 45 | glyphKerningDict = kerningDict[left_glyph.id] 46 | except KeyError: 47 | kerningDict[left_glyph.id] = {} 48 | glyphKerningDict = kerningDict[left_glyph.id] 49 | # glyphKerningDict[right_glyph.id] = value 50 | # font.setKerningForPair( master_id, left_glyph.name, right_glyph.name, value ) 51 | flatKerning.append((left_glyph.name, right_glyph.name, value)) 52 | 53 | font.kerning = {} 54 | print(len(flatKerning), 'flat kerning pairs') 55 | for left_glyph_name, right_glyph_name, value in flatKerning: 56 | font.setKerningForPair( master_id, left_glyph_name, right_glyph_name, value ) 57 | 58 | # remove kerning groups 59 | # (TODO) 60 | -------------------------------------------------------------------------------- /Adopt Background.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Adopt Background 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | The selected nodes will adopt the position of the corresponding nodes in the background. 9 | In combination with Insert Glyph to Background, you can easily 10 | transfer parts of the outlines between glyphs. 11 | ''' 12 | 13 | import sys 14 | from AppKit import NSBeep 15 | 16 | def decomposedBackground(background): 17 | if background.components: 18 | # we cannot use background.copyDecomposedLayer() here 19 | # as this would also decompose caps and corners. 20 | # so, we need to manually create a decomposed copy: 21 | foregroundLayer = background.foreground() 22 | glyph = foregroundLayer.parent 23 | glyph_copy = glyph.copy() 24 | glyph_copy.parent = glyph.parent 25 | background = glyph_copy.layers[foregroundLayer.layerId].background 26 | background.decomposeComponents() 27 | return background 28 | 29 | def counterparts( selection, background ): 30 | best_point_range = [] 31 | best_deviation = sys.maxsize 32 | # search in each path 33 | for bg_path in background.paths: 34 | bg_nodes = bg_path.nodes 35 | # try each starting point 36 | for start in range( len( bg_nodes ) ): 37 | indx = start 38 | deviation = 0 39 | point_range = [] 40 | # calculate deviation 41 | for node in selection: 42 | bg_node = bg_nodes[indx] 43 | if ( node.type == OFFCURVE ) != ( bg_node.type == OFFCURVE ): 44 | break 45 | deviation += abs( node.x - bg_node.x ) 46 | deviation += abs( node.y - bg_node.y ) 47 | if deviation > best_deviation: 48 | break 49 | point_range.append( bg_node ) 50 | # increase index looping around 51 | indx = ( indx + 1 ) % len( bg_nodes ) 52 | else: 53 | best_deviation = deviation 54 | best_point_range = point_range 55 | return zip( selection, best_point_range ) 56 | 57 | def subpaths( selection ): 58 | selection.append( GSNode() ) 59 | # build subpaths 60 | subpaths = [] 61 | tail = [] 62 | nextNode = None 63 | current_subpath = [] 64 | current_subpath_is_tail = False 65 | for node in selection: 66 | if node == nextNode: 67 | current_subpath.append( node ) 68 | else: 69 | if current_subpath_is_tail: 70 | if current_subpath[0] == nextNode: 71 | subpaths.append( current_subpath ) 72 | else: 73 | tail = current_subpath 74 | else: 75 | if tail and tail[0] == nextNode: 76 | current_subpath.extend( tail ) 77 | tail = [] 78 | if current_subpath: 79 | subpaths.append( current_subpath ) 80 | # start new subpath 81 | current_subpath = [ node ] 82 | # starting a new tail? 83 | if node.prevNode in selection: 84 | current_subpath_is_tail = True 85 | tail = [ node ] 86 | else: 87 | current_subpath_is_tail = False 88 | nextNode = node.nextNode 89 | return subpaths 90 | 91 | layer = Glyphs.font.selectedLayers[0] 92 | glyph = layer.parent 93 | if layer.selection: 94 | selection = [node for path in layer.paths for node in path.nodes if node in layer.selection] 95 | else: 96 | selection = [node for path in layer.paths for node in path.nodes] 97 | any_changes = False 98 | background = decomposedBackground(layer.background) 99 | subpaths = subpaths( selection ) 100 | for subpath in subpaths: 101 | for node, bg_node in counterparts( subpath, background ): 102 | if not any_changes: 103 | if node.position == bg_node.position: 104 | continue 105 | else: 106 | any_changes = True 107 | glyph.beginUndo() 108 | layer.beginChanges() 109 | node.position = bg_node.position 110 | for anchor in layer.anchors: 111 | if anchor.selected or not layer.selection: 112 | closestBackgroundAnchor = None 113 | closestDist = -1 114 | for backgroundAnchor in background.anchors: 115 | dist = abs(backgroundAnchor.position.x - anchor.position.x) + abs(backgroundAnchor.position.y - anchor.position.y) 116 | if closestDist == -1 or dist < closestDist: 117 | closestDist = dist 118 | closestBackgroundAnchor = backgroundAnchor 119 | if closestDist != 0 and closestBackgroundAnchor: 120 | anchor.position = closestBackgroundAnchor.position 121 | any_changes = True 122 | if any_changes: 123 | layer.syncMetrics() 124 | layer.endChanges() 125 | glyph.endUndo() 126 | else: 127 | pass 128 | # NSBeep() 129 | -------------------------------------------------------------------------------- /Adopt from Other Font.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Adopt from Other Font 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | Adopts glyph properties from the same-named glyph in a different font. 9 | ''' 10 | 11 | import vanilla 12 | 13 | try: 14 | props 15 | otherFontName 16 | except NameError: 17 | props = {'LSB': 0, 'RSB': 0, 'Width': 0, 'Metrics Keys': 0, 'Glyph Color': 0, 'Add Anchors': 0, 'Replace Components': 0, '... in all masters': 0} 18 | otherFontName = '' 19 | 20 | class AdoptDialog(object): 21 | 22 | def __init__(self): 23 | global otherFontName 24 | global props 25 | windowWidth = 300 26 | windowHeight = 284 27 | lineHeight = 22 28 | posX = 14 29 | posY = 15 30 | elementHeight = 20 31 | rightMargin = -10 32 | try: 33 | self.layer = doc.selectedLayers()[0] 34 | except TypeError: 35 | self.layer = None 36 | return 37 | self.w = vanilla.FloatingWindow((windowWidth, windowHeight), 'Adopt from Other Font', autosaveName='com.freemix.adopt_from.mainwindow') 38 | openedFonts = [font.filepath.split('/')[-1] for font in Glyphs.fonts if not font is Glyphs.font] 39 | if not openedFonts: 40 | self.w.descriptionText = vanilla.TextBox((posX, posY, rightMargin, 14), 'Please open another font.', selectable=True) 41 | return 42 | self.w.popUpButton = vanilla.PopUpButton((posX, posY, rightMargin, elementHeight), openedFonts, callback=self.popUpButtonCallback) 43 | if otherFontName in openedFonts: 44 | self.w.popUpButton.setItem(otherFontName) 45 | else: 46 | otherFontName = self.w.popUpButton.getItem() 47 | posY += 10 48 | for label, value in props.items(): 49 | posY += lineHeight 50 | posXlocal = posX 51 | if label == '... in all masters': 52 | posY += lineHeight // 2 53 | posX += 20 54 | checkbox = vanilla.CheckBox((posX, posY, rightMargin, elementHeight), label, callback=self.checkBoxCallback, value=value) 55 | attrName = label.replace(' ', '_').replace('.', '') +'Checkbox' 56 | setattr(self.w, attrName, checkbox) 57 | self.w.adoptButton = vanilla.Button((-100, -40, -10, -20), 'Adopt values', callback=self.buttonCallback) 58 | self.w.setDefaultButton(self.w.adoptButton) 59 | 60 | def popUpButtonCallback(self, sender): 61 | global otherFontName 62 | otherFontName = self.w.popUpButton.getItem() 63 | 64 | def checkBoxCallback(self, sender): 65 | global props 66 | props[sender.getTitle()] = sender.get() 67 | 68 | def buttonCallback(self, sender): 69 | global props 70 | global otherFontName 71 | for font in Glyphs.fonts: 72 | if font.filepath.split('/')[-1] == otherFontName: 73 | otherFont = font 74 | break 75 | else: 76 | return 77 | anySuccessful = False 78 | if props['... in all masters']: 79 | layers = [] 80 | for layer in doc.selectedLayers(): 81 | if not layer: 82 | continue 83 | glyph = layer.parent 84 | for master in glyph.parent.masters: 85 | layers.append(glyph.layers[master.id]) 86 | else: 87 | layers = doc.selectedLayers() 88 | for layer in layers: 89 | glyph = layer.parent 90 | otherGlyph = otherFont.glyphs[glyph.name] 91 | if not otherGlyph: 92 | continue 93 | if len(otherFont.masters) == 1: 94 | otherLayer = otherGlyph.layers[0] 95 | else: 96 | for master in otherFont.masters: 97 | if master.name == layer.master.name: 98 | otherLayer = otherGlyph.layers[master.id] 99 | assert(otherLayer) 100 | break 101 | else: 102 | continue 103 | anySuccessful = True 104 | if props['LSB']: 105 | layer.LSB = otherLayer.LSB 106 | if props['RSB']: 107 | layer.RSB = otherLayer.RSB 108 | if props['Width']: 109 | layer.width = otherLayer.width 110 | if props['Metrics Keys']: 111 | glyph.leftMetricsKey = otherGlyph.leftMetricsKey 112 | glyph.rightMetricsKey = otherGlyph.rightMetricsKey 113 | glyph.widthMetricsKey = otherGlyph.widthMetricsKey 114 | if props['Glyph Color']: 115 | glyph.color = otherGlyph.color 116 | if props['Add Anchors']: 117 | for anchor in otherLayer.anchors: 118 | if not anchor.name in layer.anchors.keys(): 119 | layer.anchors[anchor.name] = anchor 120 | print(otherLayer.parent.name, 'adding anchor', anchor.name) 121 | if props['Replace Components']: 122 | layer.shapes.clear() 123 | layer.shapes.extend(list(otherLayer.components)) 124 | # for component in otherLayer.components: 125 | # layer.anchors[anchor.name] =anchor 126 | if not anySuccessful: 127 | return 128 | self.w.close() 129 | 130 | dialog = AdoptDialog() 131 | if dialog.layer is not None: 132 | dialog.w.open() 133 | dialog.w.makeKey() 134 | -------------------------------------------------------------------------------- /Insert Glyph.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Insert Glyph 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | 1. Enter a glyph name. 9 | 2. Press the left align or right align button. 10 | 3. This script will clear the mask, then insert the specified glyph into the mask. 11 | 12 | - With right align selected, the contours will be pasted as if the advance widths were aligned. 13 | - The keyboard shortcuts for left and right aligned are Enter and Esc. 14 | - It is sufficient to enter the beginning of the glyph name, e.g. "deg" for "degree". 15 | ''' 16 | 17 | from vanilla import Window, EditText, Button 18 | 19 | LEFT = '<' 20 | RIGHT = '>' 21 | 22 | font = Glyphs.font 23 | 24 | def insert_paths( to_layer, from_layer, alignment = LEFT ): 25 | # insert all paths 26 | for path in from_layer.copyDecomposedLayer().paths: 27 | if alignment == RIGHT: 28 | shift = to_layer.width - from_layer.width 29 | for node in path.nodes: 30 | node.x = node.x + shift 31 | to_layer.paths.append( path ) 32 | # select path (makes is quicker to move around the shape later) 33 | to_layer.paths[-1].selected = True 34 | 35 | class GlyphnameDialog( object): 36 | 37 | def __init__( self ): 38 | x = 10 39 | y = 10 40 | height = 20 41 | button_width = 30 42 | glyphname_width = 180 43 | gap = 6 44 | self.w = Window( ( x + button_width + gap + glyphname_width + gap + button_width + x, y + height + y ), "insert glyph" ) 45 | self.w.center() 46 | self.w.glyphname = EditText( ( x, y, glyphname_width, height ), '') 47 | x += glyphname_width + gap 48 | self.w.alignleft = Button( ( x, y, button_width, height ), LEFT, callback = self.buttonCallback ) 49 | x += button_width + gap 50 | self.w.alignright = Button( ( x, y, button_width, height ), RIGHT, callback = self.buttonCallback ) 51 | self.w.setDefaultButton( self.w.alignleft ) 52 | self.w.alignright.bind( "\x1b", [] ) 53 | self.w.open() 54 | 55 | def buttonCallback( self, sender ): 56 | title = sender.getTitle() 57 | glyphname = self.w.glyphname.get() 58 | if not glyphname: 59 | self.w.close() 60 | return 61 | if len( glyphname ) == 1: 62 | uni = ord(glyphname) 63 | g = font.glyphForUnicode_("%.4X" % uni) 64 | if g: 65 | glyphname = g.name 66 | other_glyph = font.glyphs[ glyphname ] 67 | if not other_glyph: 68 | for glyph in font.glyphs: 69 | if glyph.name.startswith( glyphname ): 70 | other_glyph = glyph 71 | break 72 | else: 73 | self.w.close() 74 | return 75 | 76 | for layer in font.selectedLayers: 77 | glyph = layer.parent 78 | glyph.beginUndo() 79 | # deselect all 80 | for path in layer.paths: 81 | for node in path.nodes: 82 | layer.removeObjectFromSelection_( node ) 83 | # find other layer 84 | for other_layer in other_glyph.layers: 85 | if other_layer.name == layer.name: 86 | # insert paths 87 | for path in other_layer.copyDecomposedLayer().paths: 88 | if title == RIGHT: 89 | shift = layer.width - other_layer.width 90 | for node in path.nodes: 91 | node.x = node.x + shift 92 | layer.paths.append( path ) 93 | # select path 94 | layer.paths[-1].selected = True 95 | break 96 | glyph.endUndo() 97 | self.w.close() 98 | 99 | GSSelectGlyphsDialogController = objc.lookUpClass("GSSelectGlyphsDialogController") 100 | selectGlyphPanel = GSSelectGlyphsDialogController.alloc().init() 101 | selectGlyphPanel.setTitle_("Find Glyphs") 102 | 103 | master = font.masters[0] # Pick with master you are interested in, e.g., currentTab.masterIndex 104 | selectGlyphPanel.setMasterID_(master.id) 105 | selectGlyphPanel.setContent_(list(font.glyphs)) 106 | PreviousSearch = Glyphs.defaults["PickGlyphsSearch"] 107 | if PreviousSearch and len(PreviousSearch) > 0: 108 | selectGlyphPanel.setSearch_(PreviousSearch) 109 | 110 | if selectGlyphPanel.runModal(): 111 | alignment = LEFT 112 | Glyphs.defaults["PickGlyphsSearch"] = selectGlyphPanel.glyphsSelectSearchField().stringValue() 113 | other_glyph = selectGlyphPanel.selectedGlyphs()[0] 114 | else: 115 | alignment = RIGHT 116 | glyphname = selectGlyphPanel.glyphsSelectSearchField().stringValue() 117 | other_glyph = font.glyphs[ glyphname ] 118 | for layer in font.selectedLayers: 119 | glyph = layer.parent 120 | glyph.beginUndo() 121 | # deselect all 122 | for path in layer.paths: 123 | for node in path.nodes: 124 | layer.removeObjectFromSelection_( node ) 125 | # find other layer 126 | for other_layer in other_glyph.layers: 127 | if other_layer.name == layer.name: 128 | insert_paths( layer, other_layer, alignment ) 129 | ''' 130 | # insert paths 131 | for path in other_layer.copyDecomposedLayer().paths: 132 | if alignment == RIGHT: 133 | shift = layer.width - other_layer.width 134 | for node in path.nodes: 135 | node.x = node.x + shift 136 | layer.paths.append( path ) 137 | # select path 138 | for node in layer.paths[-1].nodes: 139 | layer.addSelection_( node ) 140 | ''' 141 | break 142 | else: 143 | insert_paths( layer, other_glyph.layers[layer.associatedMasterId], alignment ) 144 | glyph.endUndo() 145 | 146 | 147 | # GlyphnameDialog() 148 | -------------------------------------------------------------------------------- /ComponentsPalette/Components.glyphsPalette/Contents/Resources/plugin.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import objc 4 | from GlyphsApp import * 5 | from GlyphsApp.plugins import * 6 | import operator 7 | from AppKit import NSPoint 8 | 9 | MIN_NUMBER_OF_LINES = 4 10 | MAX_NUMBER_OF_LINES = 10 11 | VERTICAL_MARGIN = 6 12 | 13 | class ComponentsPalette (PalettePlugin): 14 | 15 | dialog = objc.IBOutlet() 16 | label0 = objc.IBOutlet() 17 | label1 = objc.IBOutlet() 18 | label2 = objc.IBOutlet() 19 | label3 = objc.IBOutlet() 20 | label4 = objc.IBOutlet() 21 | label5 = objc.IBOutlet() 22 | label6 = objc.IBOutlet() 23 | label7 = objc.IBOutlet() 24 | label8 = objc.IBOutlet() 25 | label9 = objc.IBOutlet() 26 | posx0 = objc.IBOutlet() 27 | posx1 = objc.IBOutlet() 28 | posx2 = objc.IBOutlet() 29 | posx3 = objc.IBOutlet() 30 | posx4 = objc.IBOutlet() 31 | posx5 = objc.IBOutlet() 32 | posx6 = objc.IBOutlet() 33 | posx7 = objc.IBOutlet() 34 | posx8 = objc.IBOutlet() 35 | posx9 = objc.IBOutlet() 36 | posy0 = objc.IBOutlet() 37 | posy1 = objc.IBOutlet() 38 | posy2 = objc.IBOutlet() 39 | posy3 = objc.IBOutlet() 40 | posy4 = objc.IBOutlet() 41 | posy5 = objc.IBOutlet() 42 | posy6 = objc.IBOutlet() 43 | posy7 = objc.IBOutlet() 44 | posy8 = objc.IBOutlet() 45 | posy9 = objc.IBOutlet() 46 | heightConstrains = objc.IBOutlet() 47 | allFieldsHidden = False 48 | font = None 49 | 50 | # seems to be called whenever a new font is opened 51 | # careful! not called when the user switches to a different, already opened font 52 | @objc.python_method 53 | def settings(self): 54 | self.name = Glyphs.localize({'en': u'Components'}) 55 | self.loadNib('ComponentsPaletteView', __file__) 56 | self.lineheight = self.posx0.frame().origin.y - self.posx1.frame().origin.y 57 | self.posxFieldsOriginX = self.posx0.frame().origin.x 58 | 59 | @objc.IBAction 60 | def editTextCallback_(self, textField): 61 | try: 62 | newValue = float(textField.stringValue()) 63 | except ValueError: 64 | self.update() 65 | return 66 | for layer in self.font.selectedLayers: 67 | try: 68 | component = layer.components[textField.tag()] 69 | except IndexError: 70 | continue 71 | if textField.frame().origin.x == self.posxFieldsOriginX: 72 | component.position = NSPoint(newValue, component.position.y) 73 | else: 74 | component.position = NSPoint(component.position.x, newValue) 75 | 76 | @objc.python_method 77 | def update(self, sender=None): 78 | collapsed = (self.dialog.frame().origin.y != 0) 79 | if collapsed: 80 | # do not update in case the palette is collapsed: 81 | return 82 | if sender: 83 | self.font = sender.object() 84 | if not self.font: 85 | return 86 | self.allFieldsHidden = False 87 | visibleLinesCount = 0 88 | for i in range(MAX_NUMBER_OF_LINES): 89 | x = None 90 | y = None 91 | if not self.font.selectedLayers: 92 | # this also catches None, which Glyphs may return 93 | pass 94 | elif (len(self.font.selectedLayers)) == 1: 95 | layer = self.font.selectedLayers[0] 96 | try: 97 | component = layer.components[i] 98 | x = component.position.x 99 | y = component.position.y 100 | getattr(self, 'label' + str(i)).setStringValue_(component.name) 101 | except IndexError: 102 | pass 103 | else: 104 | getattr(self, 'label' + str(i)).setStringValue_(str(i + 1)) 105 | for layer in self.font.selectedLayers: 106 | try: 107 | component = layer.components[i] 108 | except IndexError: 109 | continue 110 | if x == None: 111 | x = component.position.x 112 | elif x != round(component.position.x, 3): 113 | x = '' 114 | if y == None: 115 | y = component.position.y 116 | elif y != round(component.position.y, 3): 117 | y = '' 118 | assert((x is None) == (y is None)) 119 | if x is None: 120 | getattr(self, 'label' + str(i)).setStringValue_('') 121 | getattr(self, 'posx' + str(i)).setHidden_(True) 122 | getattr(self, 'posy' + str(i)).setHidden_(True) 123 | continue 124 | getattr(self, 'posx' + str(i)).setHidden_(False) 125 | getattr(self, 'posy' + str(i)).setHidden_(False) 126 | if x == '': 127 | getattr(self, 'posx' + str(i)).setStringValue_('') 128 | else: 129 | getattr(self, 'posx' + str(i)).setIntValue_(int(x)) 130 | if y == '': 131 | getattr(self, 'posy' + str(i)).setStringValue_('') 132 | else: 133 | getattr(self, 'posy' + str(i)).setIntValue_(int(y)) 134 | visibleLinesCount = i + 1 135 | height = VERTICAL_MARGIN + visibleLinesCount * self.lineheight 136 | # we are never reducing the height of the palette 137 | # so as to minimize the changes (frequent height change 138 | # would be very distracting as we step through glyphs 139 | # in edit view) 140 | if height > self.heightConstrains.constant(): 141 | self.heightConstrains.setConstant_(height) 142 | 143 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 144 | # the following methods are adopted from the SDK without any changes 145 | 146 | @objc.python_method 147 | def start(self): 148 | # Adding a callback for the 'GSUpdateInterface' event 149 | Glyphs.addCallback(self.update, UPDATEINTERFACE) 150 | 151 | @objc.python_method 152 | def __del__(self): 153 | Glyphs.removeCallback(self.update) 154 | 155 | @objc.python_method 156 | def __file__(self): 157 | """Please leave this method unchanged""" 158 | return __file__ 159 | 160 | # Temporary Fix 161 | # Sort ID for compatibility with v919: 162 | _sortID = 0 163 | @objc.python_method 164 | def setSortID_(self, id): 165 | try: 166 | self._sortID = id 167 | except Exception as e: 168 | self.logToConsole("setSortID_: %s" % str(e)) 169 | 170 | @objc.python_method 171 | def sortID(self): 172 | return self._sortID 173 | -------------------------------------------------------------------------------- /Jump to Alternate.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Jump to Alternate 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | In the edit view, use this script to “jump” back and forth (or to circle) 9 | between alternate glyphs such as one, one.lf and one.tosf. 10 | 11 | Tip: Give it a keyboard shortcut! 12 | 13 | If several glyphs are selected you can choose to add or remove suffixes. 14 | ''' 15 | 16 | from builtins import chr 17 | import vanilla 18 | from Foundation import NSString, NSMakeRange 19 | font = Glyphs.font 20 | 21 | # returns the suffix or '' 22 | def getSuffix(glyph): 23 | nameSplit = glyph.name.split('.', 1) 24 | if len(nameSplit) == 1: 25 | return '' 26 | return glyph.name.split('.')[-1] 27 | 28 | # returns the full suffix or '' or None 29 | def fullSuffix(glyph, baseName): 30 | nameSplit = glyph.name.split('.', 1) 31 | if nameSplit[0] == baseName: 32 | try: 33 | return nameSplit[1] 34 | except IndexError: 35 | return '' 36 | return None 37 | 38 | # returns the shared suffix or '' or None 39 | def sharedSuffix(layers): 40 | suffix = None 41 | for layer in layers: 42 | glyphSuffix = getSuffix(layer.parent) 43 | if suffix is None: 44 | suffix = glyphSuffix 45 | elif glyphSuffix != suffix: 46 | return None 47 | return suffix 48 | 49 | def sharedAlternateSuffixes(font): 50 | suffixes = None 51 | for layer in font.selectedLayers: 52 | currentSuffixes = [] 53 | currentBaseName = layer.parent.name.split('.', 1)[0] 54 | for glyph in font.glyphs: 55 | suffix = fullSuffix(glyph, currentBaseName) 56 | if not suffix is None: 57 | currentSuffixes.append(suffix) 58 | currentSuffixes.sort() 59 | if suffixes is None: 60 | suffixes = set(currentSuffixes) 61 | else: 62 | suffixes.intersection_update(currentSuffixes) 63 | return suffixes 64 | 65 | def replaceInDisplayString(newString): 66 | newString = NSString.stringWithString_(newString) 67 | graphicView = font.currentTab.graphicView() 68 | selectedRange = graphicView.selectedRange() 69 | textSelectionLength = selectedRange.length 70 | if textSelectionLength == 0: 71 | # no text selection but we want to replace the current glyph, 72 | # i.e. to the right of the text cursor (which corresponds to font.selectedLayers) 73 | selectedRange.length = 1 74 | graphicView.replaceCharactersInRange_withString_(selectedRange, newString) 75 | if textSelectionLength != 0: 76 | graphicView.setSelectedRange_(NSMakeRange(selectedRange.location, newString.length())) 77 | 78 | class JumpDialog(object): 79 | 80 | def __init__(self): 81 | windowWidth = 300 82 | windowHeight = 125 83 | try: 84 | self.layer = doc.selectedLayers()[0] 85 | except TypeError: 86 | self.layer = None 87 | return 88 | self.w = vanilla.FloatingWindow((windowWidth, windowHeight), 'Jump to Alternates', autosaveName='com.freemix.jump_to_alternate.mainwindow') 89 | leftMargin = 20 90 | rightMargin = -20 91 | elementHeight = 20 92 | posY = 12 93 | self.w.removeSuffixLabel = vanilla.TextBox((leftMargin, posY + 1, 115, elementHeight), "Remove suffix") 94 | self.w.removeSuffix = vanilla.EditText((120, posY, rightMargin, elementHeight)) 95 | posY += elementHeight + 10 96 | self.w.addSuffixLabel = vanilla.TextBox((leftMargin, posY + 1, 115, elementHeight), "Add suffix") 97 | self.w.addSuffix = vanilla.EditText((120, posY, rightMargin, elementHeight)) 98 | self.w.jumpButton = vanilla.Button((leftMargin, -40, -10, rightMargin), 'Replace with alternates', callback=self.buttonCallback) 99 | self.w.setDefaultButton(self.w.jumpButton) 100 | 101 | def buttonCallback(self, sender): 102 | newText = '' 103 | for layer in font.selectedLayers: 104 | glyph = layer.parent 105 | glyphName = glyph.name.replace(self.w.removeSuffix.get(), '') 106 | glyphName += self.w.addSuffix.get() 107 | nextGlyph = font.glyphs[glyphName] 108 | if nextGlyph: 109 | glyph = nextGlyph 110 | nextChar = chr(font.characterForGlyph(glyph)) 111 | newText += nextChar 112 | replaceInDisplayString(newText) 113 | self.w.close() 114 | 115 | def jumpToAlternate(): 116 | if len(font.selectedLayers) > 1: 117 | alternateSuffixes = sharedAlternateSuffixes(font) 118 | currentSuffix = sharedSuffix(font.selectedLayers) 119 | if currentSuffix in alternateSuffixes: 120 | alternateSuffixes = sorted(list(alternateSuffixes)) 121 | i = alternateSuffixes.index(currentSuffix) 122 | nextSuffix = alternateSuffixes[(i + 1) % len(alternateSuffixes)] 123 | if nextSuffix: 124 | nextSuffix = '.' + nextSuffix 125 | if currentSuffix: 126 | currentSuffix = '.' + currentSuffix 127 | newText = '' 128 | for layer in font.selectedLayers: 129 | glyph = layer.parent 130 | glyphName = glyph.name 131 | if currentSuffix == '': 132 | glyphName += nextSuffix 133 | elif glyphName.endswith(currentSuffix): 134 | glyphName = glyphName[:-len(currentSuffix)] + nextSuffix 135 | nextGlyph = font.glyphs[glyphName] 136 | if nextGlyph: 137 | glyph = nextGlyph 138 | nextChar = chr(font.characterForGlyph(glyph)) 139 | newText += nextChar 140 | replaceInDisplayString(newText) 141 | else: 142 | dialog = JumpDialog() 143 | dialog.w.open() 144 | dialog.w.makeKey() 145 | if currentSuffix is not None: 146 | dialog.w.removeSuffix.set('.' + currentSuffix) 147 | return 148 | # find new glyph: 149 | try: 150 | currentLayer = font.selectedLayers[0] 151 | except IndexError: 152 | return 153 | currentGlyphName = currentLayer.parent.name 154 | currentBaseName = currentGlyphName.split('.', 1)[0] 155 | if not currentBaseName: 156 | # for example .notdef 157 | return 158 | alternates = [] 159 | dontJumpToSC = not currentGlyphName.endswith('.sc') 160 | # ^ let’s not jump from non-SC to SC 161 | for glyph in font.glyphs: 162 | suffix = fullSuffix(glyph, currentBaseName) 163 | if suffix is None: 164 | continue 165 | if dontJumpToSC and glyph.name.endswith('.sc'): 166 | continue 167 | alternates.append(glyph) 168 | if len(alternates) == 1: 169 | # no others found 170 | return 171 | for a in range(len(alternates)): 172 | if alternates[a].name == currentGlyphName: 173 | try: 174 | nextGlyph = alternates[a+1] 175 | except IndexError: 176 | nextGlyph = alternates[0] 177 | nextChar = chr(font.characterForGlyph(nextGlyph)) 178 | replaceInDisplayString(nextChar) 179 | 180 | jumpToAlternate() 181 | -------------------------------------------------------------------------------- /Symmetrify Terminal.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Symmetrify Terminal 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | Select a stroke end (i.e. the two nodes). This script makes the terminal perpendicular (symmetrical). 9 | ''' 10 | 11 | import math 12 | from Cocoa import NSPoint, NSMakePoint 13 | 14 | class FMXpoint: 15 | def __init__(self, x, y=None): 16 | if isinstance(x, GSNode): 17 | x, y = x.position 18 | elif isinstance(x, NSPoint): 19 | x, y = x 20 | self.x = x 21 | self.y = y 22 | 23 | def assignToNode(self, glyphsNode): 24 | pos = glyphsNode.position 25 | pos.x = self.x 26 | pos.y = self.y 27 | glyphsNode.position = pos 28 | # note: Glyphs will do the rounding as applicable 29 | 30 | def __repr__(self): 31 | return f"FMXpoint(x={self.x}, y={self.y})" 32 | 33 | def __add__(self, other): 34 | return FMXpoint(self.x + other.x, self.y + other.y) 35 | 36 | def __iadd__(self, other): 37 | self.x += other.x 38 | self.y += other.y 39 | return self 40 | 41 | def __sub__(self, other): 42 | return FMXpoint(self.x - other.x, self.y - other.y) 43 | 44 | def __isub__(self, other): 45 | self.x -= other.x 46 | self.y -= other.y 47 | return self 48 | 49 | def __mul__(self, scalar): 50 | return FMXpoint(self.x * scalar, self.y * scalar) 51 | 52 | def __imul__(self, scalar): 53 | self.x *= scalar 54 | self.y *= scalar 55 | 56 | def lengthSquared(self): 57 | return self.x ** 2 + self.y ** 2 58 | 59 | def normal(self): 60 | return FMXpoint(self.y, self.x) 61 | 62 | def length(self): 63 | return math.sqrt(self.lengthSquared()) 64 | 65 | def atLength(self, newLength): 66 | if isinstance(newLength, FMXpoint): 67 | return self * math.sqrt(newLength.lengthSquared() / self.lengthSquared()) 68 | else: 69 | return self * (newLength / self.length()) 70 | 71 | def isShorterThan(self, other): 72 | if isinstance(other, FMXpoint): 73 | return self.lengthSquared() < other.lengthSquared() 74 | else: 75 | return self.lengthSquared() < other ** 2 76 | 77 | def rounded(self): 78 | return FMXpoint(round(self.x), round(self.y)) 79 | 80 | # returns the rounding error 81 | def roundInPlace(self): 82 | unrounded = self 83 | self.x = round(self.x) 84 | self.y = round(self.y) 85 | return self - unrounded 86 | 87 | # returns the dor product 88 | def dot(self, other): 89 | return self.x * other.x + self.y * other.y 90 | 91 | # projects the vector onto another 92 | # • the returned vector is parallel to other 93 | # • self and the returned vector form a rectangular triangle 94 | def alignedTo(self, other): 95 | if other.x == 0: 96 | return (0, y) 97 | elif other.y == 0: 98 | return (x, 0) 99 | else: 100 | return other * (self.dot(other) / other.lengthSquared()) 101 | 102 | @classmethod 103 | # this seems more sensible as a class method 104 | # so as to reflect the conceptual symmetry 105 | def dist(cls, p1, p2): 106 | return (p1 - p2).length() 107 | 108 | def intersection(p1, p2, p3, p4): 109 | A1 = p2.y - p1.y 110 | B1 = p1.x - p2.x 111 | C1 = A1 * p1.x + B1 * p1.y 112 | A2 = p4.y - p3.y 113 | B2 = p3.x - p4.x 114 | C2 = A2 * p3.x + B2 * p3.y 115 | det = A1 * B2 - A2 * B1 116 | if det == 0: 117 | # lines are parallel 118 | return None 119 | x = (B2 * C1 - B1 * C2) / det 120 | y = (A1 * C2 - A2 * C1) / det 121 | return FMXpoint(x, y) 122 | 123 | # if the vertex is further away that this 124 | # the sides of the stroke will be considered parallel: 125 | # (this is mostly because of float imprecision) 126 | MAX_VERTEX_DIST = 3000 127 | 128 | for selectedLayer in Font.selectedLayers: 129 | glyph = selectedLayer.parent 130 | for path in selectedLayer.paths: 131 | for node in path.nodes: 132 | if node.selected and node.nextNode.selected: 133 | n0 = node.prevNode 134 | n1 = node 135 | n2 = n1.nextNode 136 | n3 = n2.nextNode 137 | if n1.position == n2.position: 138 | # pointed terminal, perpendicularity not applicable 139 | continue 140 | if n0.position == n1.position: 141 | # retracted BCP 142 | n0 = n0.prevNode 143 | if n2.position == n3.position: 144 | # retracted BCP 145 | n3 = n3.nextNode 146 | p0 = FMXpoint(n0) 147 | p1 = FMXpoint(n1) 148 | p2 = FMXpoint(n2) 149 | p3 = FMXpoint(n3) 150 | vertex = intersection(p0, p1, p2, p3) 151 | if vertex and FMXpoint.dist(p1, vertex) > MAX_VERTEX_DIST: 152 | vertex = None 153 | if vertex: 154 | v1 = p1 - vertex 155 | v2 = p2 - vertex 156 | lenNew = (v1.length() + v2.length()) / 2 157 | p1new = vertex + v1.atLength(lenNew) 158 | p2new = vertex + v2.atLength(lenNew) 159 | if Font.grid != 0: 160 | p1roundingError = p1new.roundInPlace() 161 | p2roundingError = p2new.roundInPlace() 162 | if p1roundingError.isShorterThan(p2roundingError): 163 | # seems better to keep p1new and update p2new. 164 | # let’s tweak p2 so as to adopt the rounding error: 165 | # (we prefer to shift the stroke rather than changing its weight) 166 | p2 += p1roundingError 167 | # updating the vertex may be overly perfectionist 168 | # but let’s do all we can to end up with perpendicularity: 169 | vertex = intersection(p0, p1new, p2, p3) 170 | v1 = p1new - vertex 171 | v2 = p2 - vertex 172 | # we prioritise perpendicularity over stroke length 173 | # so let’s adopt the given v1 length: 174 | # (this means p1p2 does not generally rotate around its middle) 175 | p2new = vertex + v2.atLength(v1) 176 | else: 177 | # same process as above: 178 | p1 += p2roundingError 179 | vertex = intersection(p0, p1, p2new, p3) 180 | v1 = p1 - vertex 181 | v2 = p2new - vertex 182 | p1new = vertex + v1.atLength(v2) 183 | else: 184 | # parallel sides 185 | shift = (p2 - p1).alignedTo(p1 - p0) * 0.5 186 | p1new = p1 + shift 187 | if Font.grid != 0: 188 | # note: because of symmetry, the rounding error 189 | # would be the same for p1 and p2 190 | p1roundingError = p1new.roundInPlace() 191 | # similar to the above, adopt the rounding error: 192 | p2 += p1roundingError 193 | # update the shift for p2new: 194 | shift = (p2 - p1new).alignedTo(p1new - p0) 195 | p2new = p2 - shift 196 | p1new.assignToNode(n1) 197 | p2new.assignToNode(n2) 198 | -------------------------------------------------------------------------------- /AnchorsPalette/Anchors.glyphsPalette/Contents/Resources/plugin.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import objc 4 | from GlyphsApp import * 5 | from GlyphsApp.plugins import * 6 | import operator 7 | from AppKit import NSPoint 8 | 9 | MIN_NUMBER_OF_LINES = 4 10 | MAX_NUMBER_OF_LINES = 10 11 | VERTICAL_MARGIN = 6 12 | 13 | class AnchorsPalette (PalettePlugin): 14 | 15 | dialog = objc.IBOutlet() 16 | label0 = objc.IBOutlet() 17 | label1 = objc.IBOutlet() 18 | label2 = objc.IBOutlet() 19 | label3 = objc.IBOutlet() 20 | label4 = objc.IBOutlet() 21 | label5 = objc.IBOutlet() 22 | label6 = objc.IBOutlet() 23 | label7 = objc.IBOutlet() 24 | label8 = objc.IBOutlet() 25 | label9 = objc.IBOutlet() 26 | posx0 = objc.IBOutlet() 27 | posx1 = objc.IBOutlet() 28 | posx2 = objc.IBOutlet() 29 | posx3 = objc.IBOutlet() 30 | posx4 = objc.IBOutlet() 31 | posx5 = objc.IBOutlet() 32 | posx6 = objc.IBOutlet() 33 | posx7 = objc.IBOutlet() 34 | posx8 = objc.IBOutlet() 35 | posx9 = objc.IBOutlet() 36 | posy0 = objc.IBOutlet() 37 | posy1 = objc.IBOutlet() 38 | posy2 = objc.IBOutlet() 39 | posy3 = objc.IBOutlet() 40 | posy4 = objc.IBOutlet() 41 | posy5 = objc.IBOutlet() 42 | posy6 = objc.IBOutlet() 43 | posy7 = objc.IBOutlet() 44 | posy8 = objc.IBOutlet() 45 | posy9 = objc.IBOutlet() 46 | heightConstrains = objc.IBOutlet() 47 | allFieldsHidden = False 48 | 49 | # seems to be called whenever a new font is opened 50 | # careful! not called when the user switches to a different, already opened font 51 | @objc.python_method 52 | def settings(self): 53 | self.name = Glyphs.localize({'en': u'Anchors'}) 54 | self.loadNib( 'AnchorsPaletteView', __file__ ) 55 | self.lineheight = self.posx0.frame().origin.y - self.posx1.frame().origin.y 56 | self.posxFieldsOriginX = self.posx0.frame().origin.x 57 | 58 | @objc.IBAction 59 | def editTextCallback_(self, textField): 60 | try: 61 | newValue = float( textField.stringValue() ) 62 | except ValueError: 63 | self.update() 64 | return 65 | anchorName = self.anchorNames[textField.tag()] 66 | for layer in self.font.selectedLayers: 67 | for anchor in layer.anchors: 68 | if anchor.name == anchorName: 69 | if textField.frame().origin.x == self.posxFieldsOriginX: 70 | anchor.position = NSPoint( newValue, anchor.position.y ) 71 | else: 72 | anchor.position = NSPoint( anchor.position.x, newValue ) 73 | break 74 | 75 | @objc.python_method 76 | def update( self, sender=None ): 77 | collapsed = ( self.dialog.frame().origin.y != 0 ) 78 | if collapsed and self.allFieldsHidden: 79 | # do not update in case the palette is collapsed: 80 | return 81 | if sender: 82 | self.font = sender.object() 83 | if not self.font: 84 | return 85 | if not collapsed: 86 | anchorStats = {} 87 | if self.font.selectedLayers: 88 | for layer in self.font.selectedLayers: 89 | for anchor in layer.anchors: 90 | if anchor.name not in anchorStats: 91 | anchorStats[anchor.name] = [0, 0] 92 | anchorStats[anchor.name][0] += 1 93 | anchorStats[anchor.name][1] += anchor.position.y 94 | # convert to a list, sorted by number of anchors: 95 | anchorStats = sorted( anchorStats.items(), key=operator.itemgetter(1), reverse=True ) 96 | # trim to max MAX_NUMBER_OF_LINES elements: 97 | del anchorStats[MAX_NUMBER_OF_LINES:] 98 | # sort by average y position: 99 | anchorStats = sorted( [(name, stat[1]/stat[0]) for name, stat in anchorStats], key=operator.itemgetter(1), reverse=True ) 100 | self.anchorNames = [] 101 | self.allFieldsHidden = False 102 | else: 103 | anchorStats = [] 104 | for i in range( MAX_NUMBER_OF_LINES ): 105 | try: 106 | anchorName = anchorStats[i][0] 107 | except IndexError: 108 | getattr( self, 'label' + str( i ) ).setStringValue_( '' ) 109 | getattr( self, 'posx' + str( i ) ).setHidden_( True ) 110 | getattr( self, 'posy' + str( i ) ).setHidden_( True ) 111 | continue 112 | getattr( self, 'posx' + str( i ) ).setHidden_( False ) 113 | getattr( self, 'posy' + str( i ) ).setHidden_( False ) 114 | getattr( self, 'label' + str( i ) ).setStringValue_( anchorName ) 115 | x = None 116 | y = None 117 | for layer in self.font.selectedLayers: 118 | for anchor in layer.anchors: 119 | if anchor.name == anchorName: 120 | if x == None: 121 | x = anchor.position.x 122 | if x == round( x, 3 ): 123 | x = int( x ) 124 | elif x != round( anchor.position.x, 3 ): 125 | x = '' 126 | if y == None: 127 | y = anchor.position.y 128 | if y == round( y, 3 ): 129 | y = int( y ) 130 | elif y != round( anchor.position.y, 3 ): 131 | y = '' 132 | if x == '': 133 | getattr( self, 'posx' + str( i ) ).setStringValue_( '' ) 134 | else: 135 | getattr( self, 'posx' + str( i ) ).setIntValue_( x ) 136 | if y == '': 137 | getattr( self, 'posy' + str( i ) ).setStringValue_( '' ) 138 | else: 139 | getattr( self, 'posy' + str( i ) ).setIntValue_( y ) 140 | self.anchorNames.append( anchorName ) 141 | if collapsed: 142 | height = VERTICAL_MARGIN + MIN_NUMBER_OF_LINES * self.lineheight 143 | self.heightConstrains.setConstant_( height ) 144 | self.allFieldsHidden = True 145 | return 146 | lines = max( MIN_NUMBER_OF_LINES, len( anchorStats ) ) 147 | height = VERTICAL_MARGIN + lines * self.lineheight 148 | # we are never reducing the height of the palette 149 | # so as to minimize the changes (frequent height change 150 | # would be very distracting as we step through glyphs 151 | # in edit view) 152 | if height > self.heightConstrains.constant(): 153 | self.heightConstrains.setConstant_( height ) 154 | 155 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 156 | # the following methods are adopted from the SDK without any changes 157 | 158 | @objc.python_method 159 | def start(self): 160 | # Adding a callback for the 'GSUpdateInterface' event 161 | Glyphs.addCallback(self.update, UPDATEINTERFACE) 162 | 163 | @objc.python_method 164 | def __del__(self): 165 | Glyphs.removeCallback(self.update) 166 | 167 | @objc.python_method 168 | def __file__(self): 169 | """Please leave this method unchanged""" 170 | return __file__ 171 | 172 | # Temporary Fix 173 | # Sort ID for compatibility with v919: 174 | _sortID = 0 175 | @objc.python_method 176 | def setSortID_(self, id): 177 | try: 178 | self._sortID = id 179 | except Exception as e: 180 | self.logToConsole( "setSortID_: %s" % str(e) ) 181 | 182 | @objc.python_method 183 | def sortID(self): 184 | return self._sortID 185 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Freemix for Glyphs 2 | ================= 3 | 4 | Some Python scripts to be used with the [Glyphs](http://www.glyphsapp.com/) font editor, written by Tim Ahrens. 5 | 6 | To install Freemix in Glyphs, 7 | 8 | * download the whole freemix package using “Clone or download”, “Download ZIP”. 9 | * in Glyphs, press Cmd+Shift+Y, which opens a Finder window 10 | * move the `.py` files from the freemix package into the `Scripts` subfolder 11 | * move the `.glyphsReporter` and `.glyphsPalette` files into the `Plugins` subfolder 12 | * in Glyphs, open Preferences (Cmd+,) / Addons / Modules and press “Install Modules” 13 | * restart Glyphs 14 | 15 | See also: [Font Remix Tools for Glyphs](http://remix-tools.com/glyphsapp), [Kern On for Glyphs](http://kern-on.com), [Just Another Foundry](http://justanotherfoundry.com/) 16 | 17 | 18 | ### Adopt Background 19 | 20 | The selected nodes will adopt the position of the corresponding nodes in the background. 21 | 22 | This is one of the simplest but probably the most powerful of my scripts. In combination with Insert Glyph to Background, you can easily transfer parts of the outlines between glyphs. 23 | 24 | Tip: Give it a keyboard shortcut if you use it a lot! 25 | 26 | 27 | ### Adopt from Other Font 28 | 29 | Adopts glyph properties from the same-named glyph(s) in a different font. 30 | 31 | 32 | 33 | 34 | ### Alignment Palette 35 | 36 | The palette shows the position of the center of the glphs’s bounding box. This works with components, with multiple glyphs selected and is also editable. Useful for centering all case-sensitive punctuation vertically, or to check whether mathematical operands are on the same x position (use Glyphs’ built-in glyph info to check whether they have the same advance width). 37 | Note that the bounding box center may be .5 even if your font has a grid of 1 without subdivisions (i.e. integer coordinates). The node/path selection is intentionally ignored. 38 | 39 | The Overshoots section displays the overshoot of the selected glyph(s) relative to each alignment zone. 40 | 41 | 42 | 43 | 44 | ### Anchors Palette 45 | 46 | The palette shows the position of anchors in the selected glyphs. This helps you check for consistent positioning of anchors if multiple glyphs are selected. For example, select A–Z to see whether all `top` anchors are on the same height, and adjust their position. If the position of the anchors is not identical in all selected glyphs then a gray x or y is shown. 47 | 48 | The palette shows the four most frequently used anchors. Setting the x or y position of a particular anchor via the palette only affects glyphs that have an anchor with the respective name (anchors are never inserted or removed). 49 | 50 | 51 | 52 | 53 | ### Caps and Corners Window 54 | 55 | To show this, use Window -> Caps and Corners. This window shows the cap and corner components in the selected glyphs. This helps you check for consistency: just select multiple glyphs. Note that the fields are editable, so you can adjust the scaling of caps and corners for multiple glyphs at once. 56 | 57 | 58 | 59 | 60 | ### Components Palette 61 | 62 | The palette shows the position of components in the selected glyphs. This helps you check for consistent positioning if multiple glyphs are selected. 63 | 64 | 65 | ### Delete All Anchors 66 | 67 | Removes all anchors from the selected glyphs. 68 | 69 | 70 | ### Delete Zero-Thickness Hints 71 | 72 | Removes all zero-thickness hints from all glyphs in the font. 73 | 74 | 75 | ### Delete BCP 76 | 77 | This literally deletes individual BCPs: If you delete one of the two BCPs in a cubic curve then it becomes quadratic. 78 | 79 | If the deleted BCP was retracted (i.e. on the node) then the other handle length is adjusted to better retain the shape. 80 | 81 | 82 | ### Edit Next Glyph/ Previous Glyph 83 | 84 | Activates the next/ previous glyph in the tab for editing. Makes most sense if you give it a keyboard shortcut in the macOS system preferences. 85 | 86 | 87 | ### Expand Kerning 88 | 89 | Expand Kerning like we know it from FontLab. 90 | 91 | 92 | ### Font Book Checker 93 | 94 | Outputs information on the supported languages as per Font Book on macOS (make sure the Macro Panel is open). 95 | 96 | 97 | ### Glyphset Diff 98 | 99 | Shows the glyphs that are not present in the other font (exactly two fonts need to be open). 100 | 101 | 102 | ### Insert Glyph to Background 103 | 104 | This is one of the most powerful scripts in this collection. I highly recommend to give it a keyboard shortcut. 105 | 106 | 1. Enter a glyph name. 107 | 2. Press the left align or right align button. 108 | 3. This script will clear the mask, then insert the specified glyph into the mask. 109 | 110 | - With right align selected, the contours will be pasted as if the advance widths were aligned. For example, inserting the X into the K right-aligned will instantly give you a visual feedback whether the spacing is consistent. 111 | - The keyboard shortcuts for left and right aligned are Enter and Esc. If you really have to cancel the dialog, use the little button on the dialog’s title bar. 112 | - It is sufficient to enter the beginning of the glyph name, e.g. "deg" for "degree". Be careful if there are several glyphs in the font starting with your entry. The script will simply enter one it finds. 113 | – For non-master layers, the script will try to find matching (by name) non-master layers from the inserted glyph. 114 | – For brace layers, if no corresponding brace layer is found in the inserted glyph, an interpolation is generated on-the-fly. A typical use case would be to set up a brace layer for the E, then insert the L so as to determine the standard (not visually corrected) interpolated stems. 115 | 116 | 117 | ### Insert Glyph 118 | 119 | Same as “Insert Glyph to Background” but the glyph is inserted into the active (foreground) layer, not in the background 120 | 121 | 122 | ### Jump to Alternate 123 | 124 | In the edit view, use this script to “jump” back and forth (or to circle) between alternate glyphs such as one, one.lf and one.tosf. 125 | If several glyphs are selected you can choose to add or remove suffixes. 126 | 127 | ### Make Backup Layer 128 | 129 | Same as the “Copy” button on the Layers palette but as a script. Because I really want a keyboard shortcut for this. Without a keyboard shortcut this script is completely useless. Sorry. 130 | 131 | 132 | ### Paste Background 133 | 134 | Pastes the background contours into the current layer. 135 | 136 | Former FontLab users can give it the familiar Cmd+L shortcut via App Shortcuts 137 | in the Mac OS System Preferences. 138 | 139 | 140 | ### Print Coeffs 141 | 142 | Prints the interpolation coefficients for each master in all instances (make sure the Macro Panel is open). 143 | 144 | 145 | ### Remove Backup Layers 146 | 147 | Removes all backup layers (i.e. those created using the "Copy" button) from the selected glyphs. 148 | 149 | 150 | ### Round Kerning 151 | 152 | Rounds the kerning values to full integer numbers. 153 | 154 | In addition, values smaller than MIN_VALUE are erased. 155 | 156 | 157 | ### Select Inaccessible Glyphs 158 | 159 | Run this macro while in the Font View. 160 | 161 | The macro selects all glyphs that 162 | - export 163 | - do not have a Unicode value and 164 | - are not covered by any OT feature 165 | 166 | i.e. are not accessible in the final font. 167 | 168 | These glyphs can usually be excluded from the final exported OTF font. 169 | 170 | 171 | ### Suffixes Palette 172 | 173 | The palette shows the name(s) of the selected glyph(s), split by suffix. The fields are editable. 174 | 175 | This is useful for quickly changing the suffix of multiple glyphs at once. 176 | 177 | 178 | 179 | 180 | ### Symmetrify 181 | 182 | Symmetrifies the glyph shape. 183 | 184 | S - creates point reflection (rotational symmetry) 185 | 186 | T - creates horizontal reflection symmetry 187 | 188 | C - creates vertical reflection symmetry 189 | 190 | H - creates 2-axis symmetry (ie. all the above) 191 | 192 | * - creates 5-fold rotational symmetry, useful for asterisks (note that this automatically also applies horizontal reflection symmetry) 193 | 194 | The buttons are available only as far as the node structure allows. 195 | 196 | 197 | ### Toggle Backup Layer 198 | 199 | - This script toggles between the master layer and the last backup layer in the list. 200 | 201 | - Given a keyboard shortcut, this is useful for quickly comparing two versions of a glyph. 202 | -------------------------------------------------------------------------------- /Insert Glyph to Background.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Insert Glyph to Background 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | 1. Enter a glyph name. 9 | 2. Press the left align or right align button. 10 | 3. This script will clear the mask, then insert the specified glyph into the mask. 11 | 12 | • With right align selected, the contours will be pasted as if the advance widths were aligned. 13 | 14 | • The keyboard shortcuts for left and right aligned are Enter and Esc. 15 | 16 | • It is sufficient to enter the beginning of the glyph name, e.g. “deg” for “degree”. 17 | ''' 18 | 19 | from vanilla import Window, EditText, Button, CheckBox 20 | from AppKit import NSPoint, NSTextField, NSButton, NSBeep 21 | 22 | LEFT = '<' 23 | RIGHT = '>' 24 | 25 | font = Glyphs.font 26 | active_layerId = Glyphs.font.selectedLayers[0].layerId 27 | 28 | def insert_paths( to_layer, from_layer, alignment, as_component, clear_contents ): 29 | # clear layer 30 | if clear_contents: 31 | to_layer.background.clear() 32 | if as_component: 33 | # insert as component 34 | shift = ( to_layer.width - from_layer.width ) if alignment == RIGHT else 0 35 | from_glyph_name = from_layer.parent.name 36 | to_layer.background.components.append( GSComponent( from_glyph_name, NSPoint( shift, 0 ) ) ) 37 | # select component (makes is quicker to move around the shape later) 38 | try: 39 | # Glyphs 3 40 | to_layer.background.shapes[-1].selected = True 41 | except: 42 | # Glyphs 2 43 | if to_layer.background.components: 44 | to_layer.background.components[-1].selected = True 45 | else: 46 | # insert all paths 47 | for path in from_layer.copyDecomposedLayer().paths: 48 | if alignment == RIGHT: 49 | shift = to_layer.width - from_layer.width 50 | for node in path.nodes: 51 | node.x = node.x + shift 52 | try: 53 | # Glyphs 3 54 | to_layer.background.shapes.append( path ) 55 | to_layer.background.shapes[-1].selected = True 56 | except: 57 | # Glyphs 2 58 | to_layer.background.paths.append( path ) 59 | to_layer.background.paths[-1].selected = True 60 | 61 | class GlyphnameDialog( object): 62 | 63 | def __init__( self ): 64 | self.selected_glyphs = set( [ layer.parent for layer in font.selectedLayers ] ) 65 | hori_margin = 10 66 | verti_margin = hori_margin 67 | button_width = 30 68 | glyphname_width = 180 69 | line_height = 20 70 | gap = 9 71 | dialog_height = line_height + gap + line_height + gap + line_height 72 | dialog_width = button_width + gap + glyphname_width + gap + button_width 73 | self.w = Window( ( hori_margin + dialog_width + hori_margin, verti_margin + dialog_height + verti_margin ), "insert glyph" ) 74 | self.w.center() 75 | x = hori_margin 76 | y = verti_margin 77 | # glyph name 78 | self.w.glyphname = EditText( ( x, y, glyphname_width, line_height ), '') 79 | self.w.glyphname.getNSTextField().setToolTip_( u'Enter the name of the glyph to be inserted. It is sufficient to enter the beginning of the glyph name, e.g. “deg” for “degree”.' ) 80 | # buttons 81 | x += glyphname_width + gap 82 | self.w.alignleft = Button( ( x, y, button_width, line_height ), LEFT, callback = self.buttonCallback ) 83 | self.w.alignleft.getNSButton().setToolTip_( 'Insert the other glyph left-aligned, i.e. at its original same position. Keyboard shortcut: Enter' ) 84 | x += button_width + gap 85 | self.w.alignright = Button( ( x, y, button_width, line_height ), RIGHT, callback = self.buttonCallback ) 86 | self.w.alignright.getNSButton().setToolTip_( 'Insert the other glyph right-aligned with respect to the advance widths. Keyboard shortcut: Esc' ) 87 | self.w.setDefaultButton( self.w.alignleft ) 88 | self.w.alignright.bind( "\x1b", [] ) 89 | # quick-insert 90 | self.find_metrics_keys() 91 | self.find_suffixless() 92 | if self.lmk or self.rmk: 93 | y += line_height + gap 94 | quick_button_width = dialog_width / 2 - hori_margin / 2 95 | if self.lmk: 96 | x = hori_margin 97 | self.w.quickleft = Button( ( x, y, quick_button_width, line_height ), LEFT + ' ' + self.lmk, callback = self.buttonCallback ) 98 | if self.rmk: 99 | x = hori_margin + ( dialog_width + hori_margin ) / 2 100 | self.w.quickright = Button( ( x, y, quick_button_width, line_height ), self.rmk + ' ' + RIGHT, callback = self.buttonCallback ) 101 | # insert as component 102 | as_component_is_checked = True 103 | if Glyphs.defaults["com.FMX.InsertGlyphToBackground.AsCompoment"] is not None: 104 | as_component_is_checked = Glyphs.defaults["com.FMX.InsertGlyphToBackground.AsCompoment"] 105 | y += line_height + gap 106 | x = hori_margin 107 | self.w.as_component = CheckBox( ( x, y, dialog_width, line_height ), 'Insert as component', callback=None, value=as_component_is_checked ) 108 | self.w.as_component.getNSButton().setToolTip_( 'If checked, the other glyph is inserted to the background as a component. Otherwise, it is inserted as paths (even if the other glyph is made of components).' ) 109 | # clear current contents 110 | y += line_height + gap 111 | clear_contents_is_checked = True 112 | if Glyphs.defaults["com.FMX.InsertGlyphToBackground.ClearContents"] is not None: 113 | clear_contents_is_checked = Glyphs.defaults["com.FMX.InsertGlyphToBackground.ClearContents"] 114 | self.w.clear_contents = CheckBox( ( x, y, dialog_width, line_height ), 'Clear current contents', callback=None, value=clear_contents_is_checked ) 115 | self.w.clear_contents.getNSButton().setToolTip_( 'Check this to clear the background before inserting the other glyph. Uncheck to keep the current contents of the background.' ) 116 | self.w.open() 117 | 118 | def find_metrics_keys( self ): 119 | self.lmk = None 120 | self.rmk = None 121 | for glyph in self.selected_glyphs: 122 | try: 123 | if font.glyphs[glyph.leftMetricsKey]: 124 | self.lmk = glyph.leftMetricsKey 125 | except TypeError: 126 | pass 127 | try: 128 | if font.glyphs[glyph.rightMetricsKey]: 129 | self.rmk = glyph.rightMetricsKey 130 | except TypeError: 131 | pass 132 | 133 | def find_suffixless( self ): 134 | if self.lmk or self.rmk: 135 | return 136 | for glyph in self.selected_glyphs: 137 | name_components = glyph.name.split('.') 138 | if len(name_components) > 1: 139 | basename = name_components[0] 140 | try: 141 | if font.glyphs[basename]: 142 | self.lmk = basename 143 | self.rmk = basename 144 | except TypeError: 145 | pass 146 | break 147 | 148 | def buttonCallback( self, sender ): 149 | alignment = sender.getTitle() 150 | glyphname = self.w.glyphname.get().strip(" /") 151 | if len( alignment ) != 1: 152 | # this implies that a quick-insert button was pressed 153 | title_split = alignment.split() 154 | assert len( title_split ) == 2 155 | if title_split[0] == LEFT: 156 | alignment = LEFT 157 | glyphname = title_split[1] 158 | else: 159 | assert title_split[1] == RIGHT 160 | alignment = RIGHT 161 | glyphname = title_split[0] 162 | as_component_is_checked = self.w.as_component.get() 163 | clear_contents_is_checked = self.w.clear_contents.get() 164 | if not glyphname: 165 | self.w.close() 166 | return 167 | if len( glyphname ) == 1: 168 | uni = ord(glyphname) 169 | g = font.glyphForUnicode_("%.4X" % uni) 170 | if g: 171 | glyphname = g.name 172 | try: 173 | this_glyph = font.selectedLayers[0].parent 174 | except IndexError: 175 | return 176 | other_glyph = font.glyphs[ glyphname ] 177 | if not other_glyph: 178 | # this means the user typed only the beginning of the intended glyph’s name 179 | for glyph in font.glyphs: 180 | if glyph.name.startswith( glyphname ) and not glyph.name == this_glyph.name: 181 | other_glyph = glyph 182 | break 183 | else: 184 | NSBeep() 185 | self.w.close() 186 | return 187 | for glyph in self.selected_glyphs: 188 | glyph.beginUndo() 189 | for layer in glyph.layers: 190 | # find other layer 191 | for other_layer in other_glyph.layers: 192 | if other_layer.name == layer.name: 193 | insert_paths( layer, other_layer, alignment, as_component_is_checked, clear_contents_is_checked ) 194 | break 195 | else: 196 | if layer.isBraceLayer(): 197 | # the corresponding brace layer was not found in other_glyph. 198 | # let’s interpolate it on-the-fly: 199 | other_glyph_copy = other_glyph.copy() 200 | other_glyph_copy.parent = font 201 | # ^ Glyphs needs the font’s master coordinates for the re-interpolation 202 | interpolatedLayer = layer.copy() 203 | # ^ in Glyphs 3, it seems starting with a blank GSLayer() does not work. 204 | interpolatedLayer.name = layer.name 205 | # ^ necessary for the re-interpolation 206 | other_glyph_copy.layers.append( interpolatedLayer ) 207 | interpolatedLayer.reinterpolate() 208 | insert_paths( layer, interpolatedLayer, alignment, as_component = False, clear_contents = clear_contents_is_checked ) 209 | elif active_layerId == layer.layerId: 210 | insert_paths( layer, other_glyph.layers[layer.associatedMasterId], alignment, as_component_is_checked, clear_contents_is_checked ) 211 | glyph.endUndo() 212 | Glyphs.defaults["com.FMX.InsertGlyphToBackground.AsCompoment"] = as_component_is_checked 213 | Glyphs.defaults["com.FMX.InsertGlyphToBackground.ClearContents"] = clear_contents_is_checked 214 | self.w.close() 215 | 216 | GlyphnameDialog() 217 | -------------------------------------------------------------------------------- /SuffixesPalette/Suffixes.glyphsPalette/Contents/Resources/plugin.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import objc 4 | from GlyphsApp import * 5 | from GlyphsApp.plugins import * 6 | from vanilla import * 7 | from AppKit import NSView 8 | 9 | NUMBER_OF_FIELDS = 20 10 | MINIMUM_NON_DOT_SUFFIX_LENGTH = 4 11 | 12 | class SuffixesPalette( PalettePlugin ): 13 | 14 | # seems to be called whenever a new font is opened 15 | # careful! not called when the user switches to a different, already opened font 16 | @objc.python_method 17 | def settings(self): 18 | self.name = Glyphs.localize({'en': u'Suffixes'}) 19 | # Create Vanilla window and group with controls 20 | self.width = 150 21 | self.margin = 5 22 | self.gutter = 3 23 | self.textFieldHeight = 15 24 | self.height = 2 * self.margin + self.textFieldHeight + self.gutter * 2 25 | self.paletteView = Window( (self.width, self.height), minSize=(self.width, self.height - 10), maxSize=(self.width, self.height + 200 ) ) 26 | self.paletteView.group = Group( (0, 0, self.width, self.height ) ) 27 | posx = self.margin 28 | for i in range( NUMBER_OF_FIELDS ): 29 | setattr( self.paletteView.group, 'txt' + str( i ), EditText( ( 10+28*i, self.margin, 25, self.textFieldHeight ), callback=self.editTextCallback, continuous=False, readOnly=False, formatter=None, placeholder='multiple', sizeStyle='mini' ) ) 30 | # Set dialog to NSView 31 | self.dialog = self.paletteView.group.getNSView() 32 | 33 | # splits a glyph nname into its base name and the dot suffixes, retaining the dots 34 | @objc.python_method 35 | def dotSplit( self, glyphName ): 36 | if not glyphName: 37 | # this happens when line breaks are selected 38 | return [] 39 | ds = [ '.' + n if i != 0 else n for i, n in enumerate( glyphName.split('.') ) ] 40 | if not ds[0]: 41 | # this happens for .notdef 42 | del ds[0] 43 | return ds 44 | 45 | # changes the index'th suffix of all selected glyphs. 46 | # in simulationMode this returns ( oldName, newName ) if newName already exists 47 | @objc.python_method 48 | def changeDotSuffix( self, newSuffix, index, simulationMode = False ): 49 | newSuffix = newSuffix.strip() 50 | for glyph in self.selectedGlyphs: 51 | split = self.dotSplit( glyph.name ) 52 | try: 53 | split[index] = newSuffix 54 | newName = ''.join( split ) 55 | except IndexError: 56 | newName = glyph.name + newSuffix 57 | if glyph.name != newName: 58 | if simulationMode: 59 | if self.font.glyphs[newName]: 60 | return ( glyph.name, newName ) 61 | else: 62 | glyph.name = newName 63 | glyph.updateGlyphInfo() 64 | if not simulationMode: 65 | self.update() 66 | return ( None, None ) 67 | 68 | # removes the last n characters and appends newSuffix 69 | # for all selected glyphs 70 | @objc.python_method 71 | def changeNameEnding( self, newSuffix, n ): 72 | newSuffix = newSuffix.strip() 73 | for glyph in self.selectedGlyphs: 74 | glyph.name = glyph.name[:-n] + newSuffix 75 | 76 | # captures changes to the text fields 77 | @objc.python_method 78 | def editTextCallback( self, editText ): 79 | try: 80 | for i in range( len( self.nameSplit ) ): 81 | if editText.getPosSize() == getattr( self.paletteView.group, 'txt' + str( i ) ).getPosSize(): 82 | # we have found the right text field 83 | if editText.get() != self.nameSplit[i]: 84 | # text was changed 85 | if self.suffixLength == 0: 86 | errorNameBefore, errorNameAfter = self.changeDotSuffix( editText.get(), i, simulationMode = True ) 87 | if errorNameBefore: 88 | editText.enable( False ) 89 | Message( 'Existing name', 'This would change ' + errorNameBefore + ' to ' + errorNameAfter + ', which already exists.\nNo glyphs were renamed.' ) 90 | editText.enable( True ) 91 | else: 92 | self.changeDotSuffix( editText.get(), i ) 93 | else: 94 | self.changeNameEnding( editText.get(), self.suffixLength ) 95 | return 96 | except AttributeError: 97 | pass 98 | 99 | # returns a list of dot split for all glyphs, replacing inconsistent suffixes with '.' 100 | @objc.python_method 101 | def determineSharedDotSplit( self, selectedNames ): 102 | sharedNames = [] 103 | for selectedName in selectedNames: 104 | glyphNameSplit = self.dotSplit( selectedName ) 105 | # for the first selected glyph, this is triggered: 106 | if not sharedNames: 107 | sharedNames = glyphNameSplit 108 | continue 109 | lengthDiff = len( glyphNameSplit ) - len( sharedNames ) 110 | # glyph longer than shared: compare then pad 111 | if lengthDiff > 0: 112 | for i in range( ( len( sharedNames ) ) ): 113 | if sharedNames[i] != glyphNameSplit[i]: 114 | sharedNames[i] = '.' 115 | sharedNames += ['.'] * lengthDiff 116 | # shared longer than glyph: compare then set rest to '.' 117 | if lengthDiff < 0: 118 | for i in range( ( len( glyphNameSplit ) ) ): 119 | if sharedNames[i] != glyphNameSplit[i]: 120 | sharedNames[i] = '.' 121 | sharedNames[lengthDiff:] = ['.'] * (-lengthDiff) 122 | # same length: compare 123 | else: 124 | for i in range( ( len( glyphNameSplit ) ) ): 125 | if sharedNames[i] != glyphNameSplit[i]: 126 | sharedNames[i] = '.' 127 | return sharedNames 128 | 129 | # tries to find shared string ending, i.e. non-dot suffix 130 | # returns the shared ending or ' ' (note: this is a space) 131 | # if no sufficiently long suffix was found. 132 | @objc.python_method 133 | def determineSharedSuffix( self, selectedNames, minimumLength = MINIMUM_NON_DOT_SUFFIX_LENGTH ): 134 | self.suffixLength = 0 135 | # shortcut if we have only one name 136 | if len( selectedNames ) == 1: 137 | return ' ' 138 | name0 = selectedNames[0] 139 | self.suffixLength = len( name0 ) 140 | for selectedName in selectedNames[1:]: 141 | if self.suffixLength > len( selectedName ): 142 | self.suffixLength = len( selectedName ) 143 | for i in range( self.suffixLength ): 144 | if name0[-i-1] != selectedName[-i-1]: 145 | if i < minimumLength: 146 | self.suffixLength = 0 147 | return ' ' 148 | self.suffixLength = i 149 | break 150 | return name0[-self.suffixLength:] 151 | 152 | @objc.python_method 153 | def updateTextFields( self ): 154 | for i in range( ( self.fieldCount ) ): 155 | try: 156 | editText = getattr( self.paletteView.group, 'txt' + str( i ) ) 157 | except IndexError: 158 | continue 159 | if self.nameSplit[i] == '.': 160 | editText.set( '' ) 161 | else: 162 | editText.set( self.nameSplit[i] ) 163 | 164 | # re-sizes and re-positions the fields 165 | # according to the number of suffixes 166 | @objc.python_method 167 | def updateLayout( self ): 168 | suffixFieldWidth = 1.0 * ( self.width - self.margin - ( self.fieldCount - 1 ) * self.gutter ) / ( self.fieldCount + 1 ) 169 | x = self.margin 170 | if self.fieldCount == 0: 171 | x = self.width + 1 172 | w = round( suffixFieldWidth * 2 + self.gutter ) 173 | for i in range( NUMBER_OF_FIELDS ): 174 | editText = getattr( self.paletteView.group, 'txt' + str( i ) ) 175 | if x > self.width: 176 | editText.show( False ) 177 | else: 178 | editText.show( True ) 179 | editText.setPosSize( ( x, self.margin, w, self.textFieldHeight ) ) 180 | x += w + self.gutter 181 | # set the width to suffixFieldWidth (this only has an effect in the first iteration) 182 | w = suffixFieldWidth 183 | 184 | @objc.python_method 185 | def update( self, sender=None ): 186 | # do not update in case the palette is collapsed 187 | if self.dialog.frame().origin.y != 0: 188 | return 189 | if sender: 190 | if Glyphs.buildNumber >= 3004: 191 | editView = sender.object() 192 | if editView.windowController() != self.windowController(): 193 | return 194 | self.font = editView.representedObject() 195 | else: 196 | self.font = sender.object() 197 | if not self.font: 198 | return 199 | sharedNames = [] 200 | # self.suffixLength is used to store whether the second field 201 | # represents the last suffixLength characters in the glyph name 202 | self.suffixLength = 0 203 | if self.font.selectedLayers: 204 | self.selectedGlyphs = [ layer.parent for layer in self.font.selectedLayers if layer.parent ] 205 | else: 206 | self.selectedGlyphs = [] 207 | selectedNames = [ selectedGlyph.name for selectedGlyph in self.selectedGlyphs if ( selectedGlyph and selectedGlyph.name ) ] 208 | # ^ we ensure that the list does not contain any None 209 | self.nameSplit = self.determineSharedDotSplit( selectedNames ) 210 | self.fieldCount = len( self.nameSplit ) 211 | if self.fieldCount == 1: 212 | # no suffixes found yet: append another element 213 | # that may be a space if no non-dot suffix was found 214 | # so the user can add a suffix 215 | self.nameSplit.append( self.determineSharedSuffix( selectedNames ) ) 216 | self.fieldCount = 2 217 | self.updateTextFields() 218 | self.updateLayout() 219 | 220 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 221 | # the following methods are adopted from the SDK without any changes 222 | 223 | @objc.python_method 224 | def start(self): 225 | # Adding a callback for the 'GSUpdateInterface' event 226 | Glyphs.addCallback(self.update, UPDATEINTERFACE) 227 | 228 | @objc.python_method 229 | def __del__(self): 230 | Glyphs.removeCallback(self.update) 231 | 232 | @objc.python_method 233 | def __file__(self): 234 | """Please leave this method unchanged""" 235 | return __file__ 236 | 237 | # Temporary Fix 238 | # Sort ID for compatibility with v919: 239 | _sortID = 0 240 | @objc.python_method 241 | def setSortID_(self, id): 242 | try: 243 | self._sortID = id 244 | except Exception as e: 245 | self.logToConsole( "setSortID_: %s" % str(e) ) 246 | 247 | @objc.python_method 248 | def sortID(self): 249 | return self._sortID 250 | -------------------------------------------------------------------------------- /CapsAndCorners/CapsAndCorners.glyphsPlugin/Contents/Resources/plugin.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, unicode_literals 2 | import objc 3 | from GlyphsApp import * 4 | from GlyphsApp.plugins import * 5 | import vanilla 6 | import AppKit 7 | import traceback 8 | 9 | NUMBER_OF_FIELDS = 12 10 | MULTIPLE_VALUES = -1024 11 | 12 | # from https://forum.glyphsapp.com/t/vanilla-make-edittext-arrow-savvy/5894/2 13 | GSSteppingTextField = objc.lookUpClass("GSSteppingTextField") 14 | class ArrowEditText(vanilla.EditText): 15 | nsTextFieldClass = GSSteppingTextField 16 | def _setCallback(self, callback): 17 | super(ArrowEditText, self)._setCallback(callback) 18 | if callback is not None and self._continuous: 19 | self._nsObject.setContinuous_(True) 20 | self._nsObject.setAction_(self._target.action_) 21 | self._nsObject.setTarget_(self._target) 22 | 23 | class CapsAndCorners(GeneralPlugin): 24 | 25 | @objc.python_method 26 | def settings(self): 27 | self.name = 'Caps and Corners'; 28 | 29 | @objc.python_method 30 | def start(self): 31 | newMenuItem = NSMenuItem(self.name, self.showWindow_) 32 | Glyphs.menu[WINDOW_MENU].append(newMenuItem) 33 | 34 | def showWindow_(self, sender): 35 | try: 36 | self.margin = 13 37 | gutter = 6 38 | widthName = 128 39 | widthFitBox = 20 40 | widthDimensionBox = 58 41 | self.textFieldHeight = 23 42 | self.lineToLine = self.textFieldHeight + 5 43 | self.w = vanilla.HUDFloatingWindow((100, 100), title = self.name, autosaveName = 'FMXCapsAndCorners') 44 | posy = self.margin 45 | posx = self.margin 46 | posx += widthName 47 | width = widthFitBox 48 | self.w.headerFit = vanilla.TextBox((posx, posy, width, self.textFieldHeight), text = 'fit') 49 | posx += width 50 | width = widthDimensionBox 51 | self.w.headerWidth = vanilla.TextBox((posx, posy, width, self.textFieldHeight), text = 'width') 52 | posx += width + gutter 53 | width = self.textFieldHeight 54 | posx += width + gutter 55 | width = widthDimensionBox 56 | self.w.headerDepth = vanilla.TextBox((posx, posy, width, self.textFieldHeight), text = 'depth') 57 | posx += width 58 | dialogWidth = posx + self.margin 59 | posy += self.lineToLine 60 | for i in range(NUMBER_OF_FIELDS): 61 | posx = self.margin 62 | width = widthName 63 | setattr(self.w, 'name'+str(i), vanilla.TextBox((posx, posy, width, self.textFieldHeight), text = '_cap.something')) 64 | posx += width 65 | width = widthFitBox 66 | setattr(self.w, 'fit_'+str(i), vanilla.CheckBox((posx, posy, width, self.textFieldHeight), callback=self.fitCallback, title = '', sizeStyle='small')) 67 | posx += width 68 | width = widthDimensionBox 69 | setattr(self.w, 'widt' +str(i), ArrowEditText((posx, posy, width, self.textFieldHeight), callback=self.editTextCallback, continuous=True, readOnly=False, formatter=None, placeholder='multiple')) 70 | posx += width + gutter 71 | width = self.textFieldHeight - 2 72 | setattr(self.w, 'lock'+str(i), vanilla.ImageButton((posx, posy + 1, width, self.textFieldHeight - 2), callback=self.lockWidthDepthCallback, sizeStyle='small')) 73 | posx += width + gutter 74 | width = widthDimensionBox 75 | setattr(self.w, 'dept' +str(i), ArrowEditText((posx, posy, width, self.textFieldHeight), callback=self.editTextCallback, continuous=True, readOnly=False, formatter=None, placeholder='multiple')) 76 | posy += self.lineToLine 77 | posSize = self.w.getPosSize() 78 | self.w.setPosSize((posSize[0], posSize[1], dialogWidth, posSize[3])) 79 | self.updateDocument(None) 80 | self.w.open() 81 | self.w.bind('close', self.windowClose_) 82 | Glyphs.addCallback(self.updateDocument, DOCUMENTACTIVATED) 83 | Glyphs.addCallback(self.updateDocument, DOCUMENTWILLCLOSE) 84 | except: 85 | print(traceback.format_exc()) 86 | 87 | @objc.python_method 88 | def updateDocument(self, sender): 89 | for i in range(NUMBER_OF_FIELDS): 90 | for prefix in ['name', 'fit_', 'widt', 'lock', 'dept']: 91 | getattr(self.w, prefix+str(i)).show(False) 92 | Glyphs.removeCallback(self.update) 93 | if not Glyphs.currentDocument: 94 | self.font = None 95 | return 96 | self.font = Glyphs.currentDocument.font 97 | if not self.font: 98 | return 99 | Glyphs.addCallback(self.update, UPDATEINTERFACE) 100 | corners = set() 101 | caps = set() 102 | for glyph in self.font.glyphs: 103 | for layer in glyph.layers: 104 | for hint in layer.hints: 105 | if hint.isCorner: 106 | if hint.type == CORNER: 107 | corners.add(hint.name) 108 | else: 109 | assert(hint.type == CAP) 110 | caps.add(hint.name) 111 | caps = sorted(list(caps)) 112 | corners = sorted(list(corners)) 113 | self.cc = [(c,CAP) for c in caps] 114 | self.cc += [(c,CORNER) for c in corners] 115 | i = 0 116 | for cname, ctype in self.cc: 117 | if ctype == CAP: 118 | getattr(self.w, 'fit_'+str(i)).show(True) 119 | nameBox = getattr(self.w, 'name'+str(i)) 120 | nameBox.set(cname) 121 | nameBox.show(True) 122 | getattr(self.w, 'widt'+str(i)).show(True) 123 | getattr(self.w, 'lock'+str(i)).show(True) 124 | getattr(self.w, 'dept'+str(i)).show(True) 125 | i += 1 126 | if i == NUMBER_OF_FIELDS: 127 | break 128 | newHeight = self.lineToLine + len(self.cc) * self.lineToLine + 2 * self.margin - 4 129 | posSize = self.w.getPosSize() 130 | self.w.setPosSize((posSize[0], posSize[1], posSize[2], newHeight)) 131 | self.isLocked = [False] * NUMBER_OF_FIELDS 132 | self.update(None) 133 | 134 | @objc.python_method 135 | def updateLockButtonImage(self, lockButton, i): 136 | if self.isLocked[i]: 137 | lockButton.setImage(imageNamed=AppKit.NSImageNameLockLockedTemplate) 138 | else: 139 | lockButton.setImage(imageNamed=AppKit.NSImageNameLockUnlockedTemplate) 140 | 141 | @objc.python_method 142 | def update(self, sender): 143 | try: 144 | currentDocument = Glyphs.currentDocument 145 | if not currentDocument or not self.font or not self.font.selectedLayers: 146 | return 147 | self.details = {} 148 | for layer in self.font.selectedLayers: 149 | for hint in layer.hints: 150 | if hint.isCorner: 151 | scale = hint.pyobjc_instanceMethods.scale() 152 | depth = abs(scale.y) 153 | width = abs(scale.x) 154 | isFit = hint.options & 8 155 | if hint.name in self.details: 156 | if self.details[hint.name]['widt'] != width: 157 | self.details[hint.name]['widt'] = MULTIPLE_VALUES 158 | if self.details[hint.name]['dept'] != depth: 159 | self.details[hint.name]['dept'] = MULTIPLE_VALUES 160 | if hint.type == CAP and self.details[hint.name]['fit'] != isFit: 161 | self.details[hint.name]['fit'] = MULTIPLE_VALUES 162 | else: 163 | hintDetails = {'type': hint.type, 'widt': width, 'dept': depth} 164 | if hint.type == CAP: 165 | hintDetails['fit'] = isFit 166 | self.details[hint.name] = hintDetails 167 | i = 0 168 | for cname, ctype in self.cc: 169 | anyDetails = cname in self.details 170 | for dimension in ['widt','dept']: 171 | scaleField = getattr(self.w, dimension+str(i)) 172 | if anyDetails: 173 | if self.details[cname][dimension] == MULTIPLE_VALUES: 174 | scaleField.set('') 175 | else: 176 | scaleField.set('{0:g}'.format(self.details[cname][dimension] * 100.0)) 177 | scaleField.show(anyDetails) 178 | lockButton = getattr(self.w, 'lock'+str(i)) 179 | if anyDetails: 180 | self.isLocked[i] = self.details[cname]['widt'] == self.details[cname]['dept'] 181 | self.updateLockButtonImage(lockButton, i) 182 | lockButton.show(anyDetails) 183 | if ctype == CAP: 184 | fitBox = getattr(self.w, 'fit_'+str(i)) 185 | if anyDetails: 186 | fitBox.set(self.details[cname]['fit'] != 0) 187 | getattr(self.w, 'widt'+str(i)).show(not fitBox.get()) 188 | # ^ for now, let’s hide this as Glyphs 3 does not report a sensible figure 189 | getattr(self.w, 'widt'+str(i)).enable(not fitBox.get()) 190 | getattr(self.w, 'lock'+str(i)).show(not fitBox.get()) 191 | fitBox.show(anyDetails) 192 | i += 1 193 | if i == NUMBER_OF_FIELDS: 194 | break 195 | except: 196 | print(traceback.format_exc()) 197 | 198 | @objc.python_method 199 | def updateHint(self, cname, ctype, dimension, newValue): 200 | self.font.disableUpdateInterface() 201 | for layer in self.font.selectedLayers: 202 | undoHasBegun = False 203 | for hint in layer.hints: 204 | if hint.type == ctype and hint.name == cname: 205 | scale = hint.pyobjc_instanceMethods.scale() 206 | if dimension == 'widt': 207 | if abs(scale.x - newValue) < 0.00001: 208 | # no change. let’s skip this hint in order to avoid “empty” undo steps 209 | continue 210 | if scale.x > 0: 211 | scale.x = newValue 212 | else: 213 | scale.x = -newValue 214 | else: 215 | if abs(scale.y - newValue) < 0.00001: 216 | continue 217 | if scale.y > 0: 218 | scale.y = newValue 219 | else: 220 | scale.y = -newValue 221 | if not undoHasBegun: 222 | layer.parent.beginUndo() 223 | undoHasBegun = True 224 | hint.setScale_(scale) 225 | if undoHasBegun: 226 | layer.parent.endUndo() 227 | self.font.enableUpdateInterface() 228 | Glyphs.redraw() 229 | 230 | @objc.python_method 231 | def editTextCallback(self, editText): 232 | try: 233 | i = 0 234 | for cname, ctype in self.cc: 235 | for dimension in ['widt','dept']: 236 | if editText == getattr(self.w, dimension+str(i)): 237 | try: 238 | newValue = 0.01 * float(editText.get().strip('%')) 239 | except: 240 | return 241 | if self.isLocked[i]: 242 | self.updateHint(cname, ctype, 'widt', newValue) 243 | self.updateHint(cname, ctype, 'dept', newValue) 244 | else: 245 | self.updateHint(cname, ctype, dimension, newValue) 246 | return 247 | i += 1 248 | if i == NUMBER_OF_FIELDS: 249 | break 250 | except AttributeError: 251 | pass 252 | 253 | @objc.python_method 254 | def fitCallback(self, fitBox): 255 | try: 256 | i = 0 257 | for cname, ctype in self.cc: 258 | if fitBox == getattr(self.w, 'fit_'+str(i)): 259 | for layer in self.font.selectedLayers: 260 | for hint in layer.hints: 261 | if hint.type == ctype and hint.name == cname: 262 | hint.options = hint.options ^ 8 263 | return 264 | i += 1 265 | if i == NUMBER_OF_FIELDS: 266 | break 267 | except AttributeError: 268 | pass 269 | 270 | @objc.python_method 271 | def lockWidthDepthCallback(self, lockButton): 272 | try: 273 | i = 0 274 | for cname, ctype in self.cc: 275 | if lockButton == getattr(self.w, 'lock'+str(i)): 276 | self.isLocked[i] = not self.isLocked[i] 277 | lockButton = getattr(self.w, 'lock'+str(i)) 278 | self.updateLockButtonImage(lockButton, i) 279 | if self.isLocked[i]: 280 | cname, ctype = self.cc[i] 281 | details = self.details[cname] 282 | if details['widt'] != details['dept']: 283 | details['widt'] = (details['widt'] + details['dept']) / 2 284 | details['dept'] = details['widt'] 285 | self.updateHint(cname, ctype, 'widt', details['widt']) 286 | self.updateHint(cname, ctype, 'dept', details['widt']) 287 | return 288 | i += 1 289 | if i == NUMBER_OF_FIELDS: 290 | break 291 | except AttributeError: 292 | pass 293 | 294 | @objc.python_method 295 | def __del__(self): 296 | Glyphs.removeCallback(self.update) 297 | Glyphs.removeCallback(self.updateDocument) 298 | 299 | @objc.python_method 300 | def __file__(self): 301 | """Please leave this method unchanged""" 302 | return __file__ 303 | 304 | def windowClose_(self, window): 305 | try: 306 | Glyphs.removeCallback(self.update) 307 | Glyphs.removeCallback(self.updateDocument) 308 | return True 309 | except: 310 | print(traceback.format_exc()) 311 | -------------------------------------------------------------------------------- /HandleRelations/HandleRelations.glyphsReporter/Contents/Resources/plugin.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import objc 4 | from GlyphsApp import * 5 | from GlyphsApp.plugins import * 6 | import math, statistics 7 | 8 | TAU = 6.283185307179586 9 | TEXT_OFFSET = 15 10 | TEXT_HUE = 0.0 11 | DEVIATION_STRICTNESS = 16.0 12 | DEVIATION_GREEN_MAX = 0.85 13 | DEVIATION_GREEN_FACTOR = 16.0 14 | HORIZONTAL_OFFSET_FACTOR = 0.4 15 | TEXT_SIZE_SMALL = 10.0 16 | TEXT_SIZE_DEVIATION_FACTOR = 8.0 17 | OTHER_DIRECTION_PARALLELITY_TOLERANCE = 0.25 18 | OTHER_DIRECTION_PARALLELITY_TOLERANCE_FACTOR = 1.0 / 128 19 | OTHER_DIRECTION_DISPLAY_LENGTH = 0.75 20 | SHALLOW_CURVE_THRESHOLD = 0.4 21 | # ^ in radians 22 | 23 | ignoredMasters = [] 24 | # usage example: 25 | # ignoredMasters = ['Light Compressed', 'Extrabold Compressed', 'Light Compressed Italic'] 26 | 27 | def samePosition(node1, node2): 28 | return node1.position.x == node2.position.x and node1.position.y == node2.position.y 29 | 30 | def isHoriVerti(node1, node2): 31 | return node1.position.x == node2.position.x or node1.position.y == node2.position.y 32 | 33 | def isHoriVertiAllLayers(node1, node2, pathIndex, otherLayers): 34 | if not isHoriVerti(node1, node2): 35 | return False 36 | for otherLayer in otherLayers: 37 | try: 38 | otherPath = otherLayer.paths[pathIndex] 39 | otherNode1 = otherPath.nodes[node1.index] 40 | otherNode2 = otherPath.nodes[node2.index] 41 | except IndexError: 42 | continue 43 | if not isHoriVerti(otherNode1, otherNode2): 44 | return False 45 | return True 46 | 47 | def pointDiff(node1, node2): 48 | return node1.position.x - node2.position.x, node1.position.y - node2.position.y 49 | 50 | def vectorLength(dx, dy): 51 | return math.sqrt(dx * dx + dy * dy) 52 | 53 | def dist(node1, node2): 54 | dx, dy = pointDiff(node1, node2) 55 | return vectorLength(dx, dy) 56 | 57 | # p1 is the vertex (return value is positive or negative) 58 | def angle(p0, p1, p2): 59 | v1x, v1y = pointDiff(p0, p1) 60 | v2x, v2y = pointDiff(p2, p1) 61 | determinant = v1x * v2y - v1y * v2x 62 | dot_product = v1x * v2x + v1y * v2y 63 | return math.atan2(determinant, dot_product) 64 | 65 | def angle_to_x_axis(p0, p1): 66 | dx, dy = pointDiff(p0, p1) 67 | return math.atan2(dx, dy) 68 | 69 | def relativePosition(node1, node2, node3): 70 | outerLength = dist(node3, node1) 71 | firstLength = dist(node2, node1) 72 | return firstLength / outerLength 73 | 74 | def relPositionDeviation(prevNode, node, nextNode, pathIndex, relPosition, layer, otherLayers): 75 | thisLengthSqrt = math.sqrt(min(dist(prevNode, node), dist(nextNode, node))) 76 | thisAngle = angle_to_x_axis(prevNode, nextNode) 77 | relPositions = [relPosition] 78 | errorSum = 0 79 | for otherLayer in otherLayers: 80 | try: 81 | otherPath = otherLayer.paths[pathIndex] 82 | otherNode = otherPath.nodes[node.index] 83 | otherPrevNode = otherPath.nodes[prevNode.index] 84 | otherNextNode = otherPath.nodes[nextNode.index] 85 | except IndexError: 86 | continue 87 | otherRelPosition = relativePosition(otherPrevNode, otherNode, otherNextNode) 88 | relPositions.append(otherRelPosition) 89 | otherAngle = angle_to_x_axis(otherPrevNode, otherNextNode) 90 | angleDiff = thisAngle - otherAngle 91 | angleDiff = min(abs(angleDiff), abs(angleDiff + TAU), abs(angleDiff - TAU)) 92 | errorSum += angleDiff * abs(relPosition - otherRelPosition) * thisLengthSqrt 93 | # ^ this is an approximation of the kink we can expect in interpolations 94 | medianRelPos = statistics.median(relPositions) 95 | if medianRelPos == relPosition: 96 | return 0.0 97 | else: 98 | try: 99 | deviationRel = max(relPosition / medianRelPos, medianRelPos / relPosition, (1.0-relPosition) / (1.0-medianRelPos), (1.0-medianRelPos) / (1.0-relPosition)) 100 | deviation = DEVIATION_STRICTNESS * (deviationRel - 1.0) * errorSum / len(otherLayers) 101 | return min(1.0, deviation) 102 | except ZeroDivisionError: 103 | return 1.0 104 | 105 | class HandleRelations(ReporterPlugin): 106 | 107 | @objc.python_method 108 | def settings(self): 109 | self.menuName = "Handle Relations" 110 | 111 | def conditionsAreMetForDrawing(self): 112 | # copied from https://github.com/schriftgestalt/GlyphsSDK/tree/master/Python%20Templates/Reporter 113 | currentController = self.controller.view().window().windowController() 114 | if currentController: 115 | tool = currentController.toolDrawDelegate() 116 | textToolIsActive = tool.isKindOfClass_(NSClassFromString("GlyphsToolText")) 117 | handToolIsActive = tool.isKindOfClass_(NSClassFromString("GlyphsToolHand")) 118 | if not textToolIsActive and not handToolIsActive: 119 | return True 120 | return False 121 | 122 | @objc.python_method 123 | def drawTextNearNode(self, prevNode, node, nextNode, text, fontColor, fontSize): 124 | textAlignment = 'center' 125 | offsetLength = TEXT_OFFSET * self.getScale()**-0.9 126 | bothHandlesX, bothHandlesY = pointDiff(node.prevNode, node.nextNode) 127 | if bothHandlesY < - abs(bothHandlesX): 128 | textAlignment = 'left' 129 | offsetLength *= HORIZONTAL_OFFSET_FACTOR 130 | elif bothHandlesY > abs(bothHandlesX): 131 | textAlignment = 'right' 132 | offsetLength *= HORIZONTAL_OFFSET_FACTOR 133 | bothHandlesLength = vectorLength(bothHandlesX, bothHandlesY) 134 | offsetX = - bothHandlesY / bothHandlesLength * offsetLength 135 | offsetY = bothHandlesX / bothHandlesLength * offsetLength 136 | self.drawTextAtPoint(text, NSPoint(node.position.x + offsetX, node.position.y + offsetY), align = textAlignment, fontColor = fontColor, fontSize = fontSize) 137 | 138 | @objc.python_method 139 | def drawRelativePosition(self, prevNode, node, nextNode, pathIndex, layer, otherLayers): 140 | relPosition = relativePosition(prevNode, node, nextNode) 141 | textColor = NSColor.blackColor() 142 | textSize = TEXT_SIZE_SMALL 143 | if otherLayers: 144 | deviation = relPositionDeviation(prevNode, node, nextNode, pathIndex, relPosition, layer, otherLayers) 145 | red = deviation 146 | green = DEVIATION_GREEN_MAX - deviation * DEVIATION_GREEN_FACTOR 147 | green = max(0.0, green) 148 | textColor = NSColor.colorWithRed_green_blue_alpha_(red, green, 0.0, 1.0) 149 | textSize += deviation * TEXT_SIZE_DEVIATION_FACTOR; 150 | self.drawTextNearNode(prevNode, node, nextNode, text = "{:.2f}".format(relPosition).lstrip('0'), fontColor = textColor, fontSize = textSize) 151 | 152 | @objc.python_method 153 | def drawLineFromNodeToPoint(self, node, line): 154 | myPath = NSBezierPath.alloc().init() 155 | myPath.moveToPoint_((node.position.x, node.position.y)) 156 | myPath.relativeLineToPoint_(line) 157 | myPath.setLineWidth_(0.375 * self.getScale()**-0.9) 158 | myPath.stroke() 159 | 160 | @objc.python_method 161 | def lineWithDirection(self, node, handleLengthSq, otherNode, otherBCP): 162 | dx, dy = pointDiff(otherBCP, otherNode) 163 | otherHandleLengthSq = dx * dx + dy * dy 164 | if (otherHandleLengthSq == 0): 165 | return 166 | handleFactor = math.sqrt(handleLengthSq / otherHandleLengthSq) 167 | dx *= handleFactor 168 | dy *= handleFactor 169 | return dx, dy 170 | 171 | # returns True if the node is to be ignored 172 | @objc.python_method 173 | def drawOtherDirections(self, node, pathIndex, layer, otherLayers): 174 | NSColor.colorWithRed_green_blue_alpha_(0.0, 0.3, 1.0, 1.0).set() 175 | inHandleX, inHandleY = pointDiff(node.prevNode, node) 176 | outHandleX, outHandleY = pointDiff(node.nextNode, node) 177 | inHandleLengthSq = inHandleX**2 + inHandleY**2 178 | outHandleLengthSq = outHandleX**2 + outHandleY**2 179 | inHandleParallelityToleranceSq = (OTHER_DIRECTION_PARALLELITY_TOLERANCE + math.sqrt(inHandleLengthSq) * OTHER_DIRECTION_PARALLELITY_TOLERANCE_FACTOR)**2 180 | outHandleParallelityToleranceSq = (OTHER_DIRECTION_PARALLELITY_TOLERANCE + math.sqrt(outHandleLengthSq) * OTHER_DIRECTION_PARALLELITY_TOLERANCE_FACTOR)**2 181 | endpoints = [] 182 | allParallel = True 183 | for otherLayer in otherLayers: 184 | try: 185 | otherPath = otherLayer.paths[pathIndex] 186 | otherNode = otherPath.nodes[node.index] 187 | except IndexError: 188 | continue 189 | # inhandle 190 | lineX, lineY = self.lineWithDirection(node, inHandleLengthSq, otherNode, otherNode.prevNode) 191 | endpoints.append((lineX * OTHER_DIRECTION_DISPLAY_LENGTH, lineY * OTHER_DIRECTION_DISPLAY_LENGTH)) 192 | deviationSq = (inHandleX - lineX)**2 + (inHandleY - lineY)**2 193 | if deviationSq > inHandleParallelityToleranceSq: 194 | allParallel = False 195 | # outhandle 196 | lineX, lineY = self.lineWithDirection(node, outHandleLengthSq, otherNode, otherNode.nextNode) 197 | endpoints.append((lineX * OTHER_DIRECTION_DISPLAY_LENGTH, lineY * OTHER_DIRECTION_DISPLAY_LENGTH)) 198 | deviationSq = (outHandleX - lineX)**2 + (outHandleY - lineY)**2 199 | if deviationSq > outHandleParallelityToleranceSq: 200 | allParallel = False 201 | if allParallel and not node.selected and not node.prevNode.selected and not node.nextNode.selected: 202 | return True 203 | # draw the directions (but only for curve-curve connections): 204 | if node.prevNode.type == OFFCURVE and node.nextNode.type == OFFCURVE: 205 | for endpoint in endpoints: 206 | self.drawLineFromNodeToPoint(node, endpoint) 207 | 208 | @objc.python_method 209 | def foreground(self, layer): 210 | if not self.conditionsAreMetForDrawing(): 211 | return 212 | if not layer.parent.mastersCompatible: 213 | return 214 | otherLayers = [otherLayer for otherLayer in layer.parent.layers if not otherLayer is layer and (otherLayer.isMasterLayer or otherLayer.isSpecialLayer) and not otherLayer.name in ignoredMasters] 215 | pathIndex = 0 216 | for path in layer.paths: 217 | # smooth nodes: 218 | for node in path.nodes: 219 | if not node.smooth: 220 | continue 221 | if node.prevNode.type != OFFCURVE and node.nextNode.type != OFFCURVE: 222 | continue 223 | if layer.selection and not node.selected and (node.prevNode.type != OFFCURVE or not node.prevNode.selected) and (node.nextNode.type != OFFCURVE or not node.nextNode.selected): 224 | continue 225 | if isHoriVertiAllLayers(node.prevNode, node.nextNode, pathIndex, otherLayers): 226 | continue 227 | ignoreNode = self.drawOtherDirections(node, pathIndex, layer, otherLayers) 228 | if ignoreNode: 229 | continue 230 | self.drawRelativePosition(node.prevNode, node, node.nextNode, pathIndex, layer, otherLayers) 231 | # shallow curves: 232 | for bcp1 in path.nodes: 233 | node1 = bcp1.prevNode 234 | if node1.type == OFFCURVE: 235 | continue 236 | if bcp1.type != OFFCURVE: 237 | continue 238 | bcp2 = bcp1.nextNode 239 | if bcp2.type == OFFCURVE: 240 | node2 = bcp2.nextNode 241 | else: 242 | bcp2 = None 243 | node2 = bcp1.nextNode 244 | if layer.selection and not bcp1.selected and (bcp2 and not bcp2.selected) and not node1.selected and not node2.selected: 245 | continue 246 | angle1 = angle(node1, bcp1, node2) 247 | if bcp2: 248 | angle2 = angle(node1, bcp2, node2) 249 | threshold1 = SHALLOW_CURVE_THRESHOLD 250 | threshold2 = SHALLOW_CURVE_THRESHOLD 251 | if bcp2 and angle1 * angle2 < 0: 252 | # the curve has an s-shape so the BCPs are less likely to have redundancy 253 | threshold1 *= 0.5 254 | threshold2 *= 0.5 255 | if isHoriVerti(node1, bcp1): 256 | threshold1 = 0 257 | if isHoriVerti(node2, bcp2): 258 | threshold2 = 0 259 | if node1.smooth: 260 | # bcp1 is likely to be restricted 261 | threshold1 *= 0.5 262 | if isHoriVerti(node1, bcp1): 263 | threshold1 *= 0.25 264 | if node2.smooth: 265 | # bcp2 is likely to be restricted 266 | threshold2 *= 0.5 267 | if bcp2 and isHoriVerti(node2, bcp2): 268 | threshold2 *= 0.25 269 | if bcp1.selected: 270 | threshold1 += 1.0 271 | # this almost guarantees that the relation is displayed 272 | if bcp2 and bcp2.selected: 273 | threshold2 += 1.0 274 | if abs(angle1) > math.pi - threshold1 and not samePosition(bcp1, node1): 275 | if bcp2: 276 | self.drawRelativePosition(node1, bcp1, bcp2, pathIndex, layer, otherLayers) 277 | else: 278 | self.drawRelativePosition(node1, bcp1, node2, pathIndex, layer, otherLayers) 279 | if bcp2 and abs(angle2) > math.pi - threshold2 and not samePosition(bcp2, node2) and not samePosition(bcp1, bcp2): 280 | self.drawRelativePosition(node2, bcp2, bcp1, pathIndex, layer, otherLayers) 281 | pathIndex += 1 282 | 283 | @objc.python_method 284 | def __file__(self): 285 | """Please leave this method unchanged""" 286 | return __file__ 287 | -------------------------------------------------------------------------------- /Symmetrify.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Symmetrify 2 | 3 | # by Tim Ahrens 4 | # http://justanotherfoundry.com 5 | # https://github.com/justanotherfoundry/freemix-glyphsapp 6 | 7 | __doc__ = ''' 8 | Symmetrifies the glyph shape. 9 | 10 | S - creates point reflection (rotational symmetry) 11 | T - creates horizontal reflection symmetry 12 | C - creates vertical reflection symmetry 13 | H - creates 2-axis symmetry (ie. all the above) 14 | * - creates 5-fold rotational symmetry 15 | 16 | The buttons are available only as far as the node structure allows. 17 | ''' 18 | 19 | import math 20 | from AppKit import NSPoint 21 | 22 | doc = Glyphs.currentDocument 23 | font = doc.font 24 | 25 | PERFECT_SYMMETRY = None 26 | # • with PERFECT_SYMMETRY, the result is guaranteeed to be symmetrical 27 | # • without PERFECT_SYMMETRY, the bounding box is guaranteed to be retained 28 | # This affects 'T', 'C' and 'H' symmetrification when a node is in the centre 29 | # and the bounding box has uneven dimensions. 30 | 31 | INSTANT_SYMMETRIFICATION = None 32 | # set INSTANT_SYMMETRIFICATION to 'S', 'T', 'C', 'H' or '*' 33 | # to perform the symmetrification without showing the dialogue 34 | 35 | SMALL_NEG = -1.0 / 4096 36 | SMALL_POS = 1.0 / 4096 37 | 38 | try: 39 | from vanilla import Window, SquareButton 40 | except: 41 | if Glyphs.versionNumber >= 3: 42 | Message("This script requires the Vanilla module. You can install it from the Modules tab of the Plugin Manager.", "Missing module") 43 | else: 44 | Message("This script requires the Vanilla module. To install it, go to Glyphs > Preferences > Addons > Modules and click the Install Modules button.", "Missing module") 45 | 46 | def swapped_if(t, do_swap): 47 | if do_swap: 48 | return (t[1], t[0]) 49 | return t 50 | 51 | def apply_grid(v): 52 | if font.grid > 0: 53 | return round(v / font.grid) * font.grid 54 | else: 55 | return v 56 | 57 | def apply_half_grid(v): 58 | return 0.5 * apply_grid(v * 2) 59 | 60 | class SymmetrifyDialog(object): 61 | 62 | def __init__(self): 63 | try: 64 | self.layer = doc.selectedLayers()[0] 65 | except TypeError: 66 | self.layer = None 67 | return 68 | self.init_contours() 69 | self.init_center() 70 | # determine the buttons (i.e. possible symmetry types): 71 | button_titles = [] 72 | if self.can_rotate(): 73 | button_titles.append('S') 74 | if self.can_rotate5(): 75 | button_titles.append('*') 76 | if all(self.get_flip_partner(contour, is_horizontal=False) is not None for contour in self.contours): 77 | # ^ note: here, it does not matter whether we is_horizontal is Ture or False 78 | button_titles.extend(['T', 'C', 'H']) 79 | if not button_titles: 80 | self.layer = None 81 | return 82 | # dialog layout: 83 | margin = 10 84 | size = 40 85 | self.w = Window((len(button_titles) * (margin + size) + margin, 2 * margin + size), "Symmetrify") 86 | top = margin 87 | left = margin 88 | for title in button_titles: 89 | button = SquareButton((left, top, size, size), title, callback = self.buttonCallback) 90 | setattr(self.w, title, button) 91 | left += size + margin 92 | 93 | def init_contours(self): 94 | self.contours = [[node for node in path.nodes if node.selected] for path in self.layer.paths] 95 | self.contours = [contour for contour in self.contours if len(contour) >= 2] 96 | if not self.contours: 97 | self.contours = [[node for node in path.nodes] for path in self.layer.paths] 98 | 99 | def init_center(self): 100 | max_x = max([node.position.x for contour in self.contours for node in contour if node.type != OFFCURVE]) 101 | min_x = min([node.position.x for contour in self.contours for node in contour if node.type != OFFCURVE]) 102 | max_y = max([node.position.y for contour in self.contours for node in contour if node.type != OFFCURVE]) 103 | min_y = min([node.position.y for contour in self.contours for node in contour if node.type != OFFCURVE]) 104 | self.cx = 0.5 * (max_x + min_x) 105 | self.cy = 0.5 * (max_y + min_y) 106 | 107 | def run(self): 108 | self.w.open() 109 | 110 | # returns the best partner index for point 0 111 | def get_flip_partner(self, contour, is_horizontal): 112 | min_sum = float('inf') 113 | best_partner_index_for_0 = None 114 | for tested_partner_index_for_0 in range(len(contour)): 115 | current_sum = 0 116 | point_index = tested_partner_index_for_0 117 | for other_point_index in range(len(contour)): 118 | point = contour[point_index] 119 | other_point = contour[other_point_index] 120 | if (point.type == OFFCURVE) != (other_point.type == OFFCURVE): 121 | break 122 | if is_horizontal: 123 | current_sum += abs(other_point.x + point.x - 2 * self.cx) 124 | current_sum += abs(other_point.y - point.y) * 2 125 | else: 126 | current_sum += abs(other_point.y + point.y - 2 * self.cy) 127 | current_sum += abs(other_point.x - point.x) * 2 128 | if point_index == 0: 129 | point_index = len(contour) - 1 130 | else: 131 | point_index -= 1 132 | else: 133 | # all compared points have the same type 134 | if current_sum < min_sum: 135 | min_sum = current_sum 136 | best_partner_index_for_0 = tested_partner_index_for_0 137 | return best_partner_index_for_0 138 | 139 | def flip(self, flip_horizontal, flip_vertical): 140 | flips = [True] * flip_horizontal + [False] * flip_vertical 141 | for contour in self.contours: 142 | if PERFECT_SYMMETRY and self.get_flip_partner(contour, is_horizontal=False) % 2 == 0: 143 | # we have points on the line of symmetry 144 | self.cy = apply_grid(self.cy) 145 | else: 146 | # this is relevant if we have fractional input 147 | self.cy = apply_half_grid(self.cy) 148 | if PERFECT_SYMMETRY and self.get_flip_partner(contour, is_horizontal=True) % 2 == 0: 149 | self.cx = apply_grid(self.cx) 150 | else: 151 | self.cx = apply_half_grid(self.cx) 152 | for contour in self.contours: 153 | xy = [(p.x, p.y) for p in contour] 154 | for current_is_horizontal in flips: 155 | is_last_round = current_is_horizontal == flips[-1] 156 | almost_one = 1023 / 1024 if font.grid > 0 else 1 157 | if is_last_round: 158 | almost_one *= almost_one 159 | partner_index = self.get_flip_partner(contour, current_is_horizontal) 160 | assert partner_index is not None 161 | for point_index in range(len(contour)): 162 | if point_index <= partner_index: 163 | # ^ this check is for performance only, 164 | # to avoid treating point pairs twice 165 | x, y = swapped_if(xy[point_index], current_is_horizontal) 166 | partner_x, partner_y = swapped_if(xy[partner_index], current_is_horizontal) 167 | c_x, c_y = swapped_if((self.cx, self.cy), current_is_horizontal) 168 | # at this point, we are assuming a horizontal axis (vertical flipping). 169 | # the x values will be (nearly) the same. 170 | correction_x = 0.5 * (partner_x - x) 171 | correction_y = (c_y - 0.5 * (partner_y + y)) 172 | # ensure we don’t end up with .5 coordinates: 173 | correction_x *= almost_one if partner_x >= x else 1.0/almost_one 174 | correction_y *= almost_one if partner_y >= y else 1.0/almost_one 175 | x += correction_x 176 | y += correction_y 177 | xy[point_index] = swapped_if((x, y), current_is_horizontal) 178 | if point_index != partner_index: 179 | partner_y = 2 * c_y - y 180 | if is_last_round: 181 | # we can use the perfectly mirrored x here: 182 | xy[partner_index] = swapped_if((x, partner_y), current_is_horizontal) 183 | else: 184 | partner_x -= correction_x 185 | xy[partner_index] = swapped_if((partner_x, partner_y), current_is_horizontal) 186 | if partner_index == 0: 187 | partner_index = len(contour) - 1 188 | else: 189 | partner_index -= 1 190 | for point_index in range(len(contour)): 191 | point = contour[point_index] 192 | new_x, new_y = xy[point_index] 193 | point.x, point.y = apply_grid(new_x), apply_grid(new_y) 194 | 195 | def can_rotate(self): 196 | for contour in self.contours: 197 | if len(contour) % 2 != 0: 198 | return False 199 | other_point_index = len(contour)//2 200 | for point_index in range(other_point_index): 201 | point = contour[point_index] 202 | other_point = contour[other_point_index] 203 | if (point.type == OFFCURVE) != (other_point.type == OFFCURVE): 204 | return False 205 | other_point_index = (other_point_index + 1) % len(contour) 206 | return True 207 | 208 | def rotate(self): 209 | for contour in self.contours: 210 | other_point_index = len(contour)//2 211 | for point_index in range(other_point_index): 212 | point = contour[point_index] 213 | other_point = contour[other_point_index] 214 | rx = 0.5 * (point.x - other_point.x) 215 | ry = 0.5 * (point.y - other_point.y) 216 | rx += SMALL_POS if rx > 0 else SMALL_NEG 217 | ry += SMALL_POS if ry > 0 else SMALL_NEG 218 | point.x = apply_grid(self.cx + rx) 219 | point.y = apply_grid(self.cy + ry) 220 | other_point.x = apply_grid(self.cx - rx) 221 | other_point.y = apply_grid(self.cy - ry) 222 | other_point_index = (other_point_index + 1) % len(contour) 223 | 224 | def blend_points(self, p0, p1, p2, p3, p4): 225 | return NSPoint(0.2 * (p0.x + p1.x + p2.x + p3.x + p4.x), 0.2 * (p0.y + p1.y + p2.y + p3.y + p4.y)) 226 | 227 | # returns the vector p-center, rotated around center by angle (given in radians) 228 | def rotated_vector(self, p, angle, center = NSPoint(0, 0)): 229 | v = NSPoint(p.x - center.x, p.y - center.y) 230 | result = NSPoint() 231 | result.x += v.x * math.cos(angle) - v.y * math.sin(angle) 232 | result.y += v.x * math.sin(angle) + v.y * math.cos(angle) 233 | return result 234 | 235 | def can_rotate5(self): 236 | for contour in self.contours: 237 | if len(contour) % 5 != 0: 238 | return False 239 | i1 = len(contour)//5 240 | i2 = 2 * i1 241 | i3 = 3 * i1 242 | i4 = 4 * i1 243 | for i0 in range(i1): 244 | if (contour[i0].type == OFFCURVE) != (contour[i1].type == OFFCURVE): 245 | return False 246 | if (contour[i0].type == OFFCURVE) != (contour[i2].type == OFFCURVE): 247 | return False 248 | if (contour[i0].type == OFFCURVE) != (contour[i3].type == OFFCURVE): 249 | return False 250 | if (contour[i0].type == OFFCURVE) != (contour[i4].type == OFFCURVE): 251 | return False 252 | i1 = (i1 + 1) % len(contour) 253 | i2 = (i2 + 1) % len(contour) 254 | i3 = (i3 + 1) % len(contour) 255 | i4 = (i4 + 1) % len(contour) 256 | return True 257 | 258 | def rotate5(self): 259 | fifth_circle = - math.pi * 2 / 5; 260 | sum_x = sum([p.x for c in self.contours for p in c]) 261 | sum_y = sum([p.y for c in self.contours for p in c]) 262 | num_p = sum([len(c) for c in self.contours]) 263 | cgx = sum_x / num_p 264 | cgy = sum_y / num_p 265 | cg = NSPoint(cgx, cgy) 266 | for contour in self.contours: 267 | i1 = len(contour)//5 268 | i2 = 2 * i1 269 | i3 = 3 * i1 270 | i4 = 4 * i1 271 | for i0 in range(i1): 272 | vector = self.blend_points(subtractPoints(contour[i0].position, cg), 273 | self.rotated_vector(contour[i1].position, fifth_circle, cg), 274 | self.rotated_vector(contour[i2].position, fifth_circle * 2, cg), 275 | self.rotated_vector(contour[i3].position, fifth_circle * 3, cg), 276 | self.rotated_vector(contour[i4].position, fifth_circle * 4, cg)) 277 | contour[i0].position = addPoints(cg, vector) 278 | contour[i1].position = addPoints(cg, self.rotated_vector(vector, -fifth_circle)) 279 | contour[i2].position = addPoints(cg, self.rotated_vector(vector, -fifth_circle * 2)) 280 | contour[i3].position = addPoints(cg, self.rotated_vector(vector, -fifth_circle * 3)) 281 | contour[i4].position = addPoints(cg, self.rotated_vector(vector, -fifth_circle * 4)) 282 | i1 = (i1 + 1) % len(contour) 283 | i2 = (i2 + 1) % len(contour) 284 | i3 = (i3 + 1) % len(contour) 285 | i4 = (i4 + 1) % len(contour) 286 | 287 | def performSymmetrification(self, button): 288 | font.disableUpdateInterface() 289 | glyph = self.layer.parent 290 | glyph.beginUndo() 291 | if button == 'S': 292 | self.rotate() 293 | if button == 'T': 294 | self.flip(flip_horizontal=True, flip_vertical=False) 295 | if button == 'C': 296 | self.flip(flip_horizontal=False, flip_vertical=True) 297 | if button == 'H': 298 | self.flip(flip_horizontal=True, flip_vertical=True) 299 | if button == '*': 300 | self.rotate5() 301 | self.flip(flip_horizontal=True, flip_vertical=False) 302 | self.rotate5() 303 | self.flip(flip_horizontal=True, flip_vertical=False) 304 | self.rotate5() 305 | self.flip(flip_horizontal=True, flip_vertical=False) 306 | self.rotate5() 307 | self.flip(flip_horizontal=True, flip_vertical=False) 308 | self.layer.syncMetrics() 309 | glyph.endUndo() 310 | font.enableUpdateInterface() 311 | 312 | def buttonCallback(self, sender): 313 | self.performSymmetrification(sender.getTitle()) 314 | self.w.close() 315 | 316 | dialog = SymmetrifyDialog() 317 | if dialog.layer is not None: 318 | if INSTANT_SYMMETRIFICATION: 319 | dialog.performSymmetrification(INSTANT_SYMMETRIFICATION) 320 | else: 321 | dialog.run() 322 | -------------------------------------------------------------------------------- /AlignmentPalette/Alignment.glyphsPalette/Contents/Resources/plugin.py: -------------------------------------------------------------------------------- 1 | import objc 2 | from GlyphsApp import * 3 | from GlyphsApp.plugins import * 4 | from vanilla import * 5 | from AppKit import NSFont, NSAttributedString, NSFontAttributeName, NSMidX, NSMidY, NSEvent, NSAlternateKeyMask 6 | 7 | # maximum number of zones to be displayed 8 | # (increase this value if you have more zones in your font) 9 | MAX_ZONES = 8 10 | 11 | # if False, this deactivates the grid, i.e. allows fractional positioning 12 | # to achieve exact centering 13 | STICK_TO_GRID = True 14 | 15 | # maximum number of glyphs 16 | MAX_GLYPHS_COUNT = 100 17 | 18 | # Glyphs constants 19 | COUNTERCLOCKWISE = -1 20 | CLOCKWISE = 1 21 | 22 | def displayStrFromNumbers(value1, value2): 23 | if value1 is None: 24 | return '' 25 | str1 = f"{value1:.1f}".rstrip('0').rstrip('.') 26 | str2 = f'{value2:.1f}'.rstrip('0').rstrip('.') 27 | if str1 == str2: 28 | return str1 29 | return str1 + ' — ' + str2 30 | 31 | # from https://forum.glyphsapp.com/t/vanilla-make-edittext-arrow-savvy/5894/2 32 | GSSteppingTextField = objc.lookUpClass("GSSteppingTextField") 33 | class ArrowEditText (EditText): 34 | nsTextFieldClass = GSSteppingTextField 35 | def _setCallback(self, callback): 36 | super(ArrowEditText, self)._setCallback(callback) 37 | if callback is not None and self._continuous: 38 | self._nsObject.setContinuous_(True) 39 | self._nsObject.setAction_(self._target.action_) 40 | self._nsObject.setTarget_(self._target) 41 | 42 | class AlignmentPalette (PalettePlugin): 43 | 44 | # sets the center of the bounding box of a layer 45 | @objc.python_method 46 | def setCenterOfLayer(self, layer, newCenter, isX): 47 | centerX, centerY = self.centerOfLayer(layer) 48 | oldCenter = centerX if isX else centerY 49 | shift = newCenter - oldCenter + 0.125 50 | if self.font.grid != 0: 51 | shift = self.font.grid * round(shift/self.font.grid) 52 | shiftX = shift if isX else 0 53 | shiftY = shift if not isX else 0 54 | # shift all shapes (but _not_ the anchors): 55 | for shape in layer.shapes: 56 | shape.applyTransform([1.0, 0.0, 0.0, 1.0, shiftX, shiftY]) 57 | layer.syncMetrics() 58 | 59 | # returns the center of the bounding box of a layer 60 | @objc.python_method 61 | def centerOfLayer(self, layer): 62 | if Glyphs.versionNumber >= 3: 63 | if len(layer.shapes) == 0: 64 | return None, None 65 | bounds = layer.bounds 66 | centerX = NSMidX(bounds) 67 | centerY = NSMidY(bounds) 68 | return centerX, centerY 69 | 70 | # returns the center of the layers, 71 | # x or y might be '' if they vary between layers 72 | @objc.python_method 73 | def centerOfLayers(self, layers): 74 | globalCenterX = None 75 | globalCenterY = None 76 | globalCenterXmax = None 77 | globalCenterYmax = None 78 | for layer in layers: 79 | centerX, centerY = self.centerOfLayer(layer) 80 | if centerX is None: 81 | # empty layer (no need to check centerY) 82 | continue 83 | if globalCenterX is None: 84 | globalCenterX = centerX 85 | globalCenterXmax = centerX 86 | globalCenterY = centerY 87 | globalCenterYmax = centerY 88 | else: 89 | globalCenterX = min(globalCenterX, centerX) 90 | globalCenterXmax = max(globalCenterXmax, centerX) 91 | globalCenterY = min(globalCenterY, centerY) 92 | globalCenterYmax = max(globalCenterYmax, centerY) 93 | return displayStrFromNumbers(globalCenterX, globalCenterXmax), displayStrFromNumbers(globalCenterY, globalCenterYmax) 94 | 95 | # returns a list of tuples with zone name, zone and height, 96 | # sorted by height 97 | @objc.python_method 98 | def namedZones(self, layer): 99 | metrics = None 100 | try: 101 | metrics = layer.metrics 102 | # ^ this is a new Glyphs 3 thing. 103 | # not sure how to use it but sometimes it seems to return 104 | # an objc.native_selector object rather than something iterable. 105 | # maybe we need try calling layer.metrics() in addition? 106 | # 107 | # so let’s better keep this inside the try block as well: 108 | if metrics is not None: 109 | zones = [] 110 | for metric in metrics: 111 | zones.append((metric.name, metric, metric.position)) 112 | return zones 113 | except: 114 | pass 115 | 116 | glyph = layer.parent 117 | if not glyph: 118 | return [] 119 | if not self.font: 120 | return [] 121 | masters = [m for m in self.font.masters if m.id == layer.associatedMasterId] 122 | if not masters: 123 | return [] 124 | master = masters[0] 125 | topHeights = [('cap height', master.capHeight), ('ascender', master.ascender), ('x-height', master.xHeight)] 126 | bottomHeights = [('baseline', 0), ('descender', master.descender)] 127 | zones = [] 128 | for zone in master.alignmentZones: 129 | if zone.size > 0: 130 | for name, y in topHeights: 131 | if y >= zone.position and y <= zone.position + zone.size: 132 | zones.append((name, zone, y)) 133 | break 134 | else: 135 | zones.append((str(zone.position), zone, zone.position)) 136 | elif zone.size < 0: 137 | for name, y in bottomHeights: 138 | if y <= zone.position and y >= zone.position + zone.size: 139 | zones.append((name, zone, y)) 140 | break 141 | else: 142 | zones.append((str(zone.position), zone, zone.position)) 143 | # sort by height 144 | zones.sort(key=lambda x: x[2], reverse = True) 145 | return zones 146 | 147 | # returns a list of tuples (zone name, overshoot) for the layer 148 | # overshoot may be None 149 | @objc.python_method 150 | def overshootsOfLayer(self, layer): 151 | zones = self.namedZones(layer) 152 | overshoots = [[name, -1] for name, zone, height in zones] 153 | for path in layer.copyDecomposedLayer().paths: 154 | if path.direction == CLOCKWISE or len(path.nodes) < 2: 155 | continue 156 | node2 = path.nodes[-2] 157 | node3 = path.nodes[-1] 158 | if not node2 or not node3: 159 | # this can happen with open paths 160 | continue 161 | for node in path.nodes: 162 | node1 = node2 163 | node2 = node3 164 | node3 = node 165 | # this means we start with 166 | # path.nodes[-2], path.nodes[-1] and path.nodes[0] 167 | if node2.y == node1.y and node2.y == node3.y: 168 | pass 169 | # top extremum 170 | elif node2.y >= node1.y and node2.y >= node3.y and node1.x > node3.x: 171 | for index, (name, zone, height) in enumerate(zones): 172 | if zone.size > 0 and node2.y >= zone.position and node2.y <= zone.position + zone.size: 173 | overshoot = node2.y - height 174 | existingOvershoot = overshoots[index][1] 175 | if overshoot > existingOvershoot: 176 | overshoots[index][1] = overshoot 177 | # bottom extremum 178 | elif node2.y <= node1.y and node2.y <= node3.y and node1.x < node3.x: 179 | for index, (name, zone, height) in enumerate(zones): 180 | if zone.size < 0 and node2.y <= zone.position and node2.y >= zone.position + zone.size: 181 | overshoot = height - node2.y 182 | existingOvershoot = overshoots[index][1] 183 | if overshoot > existingOvershoot: 184 | overshoots[index][1] = overshoot 185 | return overshoots 186 | 187 | # returns the overshoots for top and bottom zones, 188 | # might be 'multiple' if they vary between layers. 189 | # layers must not be empty. 190 | @objc.python_method 191 | def overshootsOfLayers(self, layers): 192 | globalOvershoots = None 193 | for layer in layers: 194 | if not layer: 195 | continue 196 | if not globalOvershoots: 197 | globalOvershoots = self.overshootsOfLayer(layer) 198 | else: 199 | overshoots = self.overshootsOfLayer(layer) 200 | for index, (name, overshoot) in enumerate(overshoots): 201 | try: 202 | zone = globalOvershoots[index] 203 | except IndexError: 204 | # we have a different number of zones in the layers 205 | # TODO: support this 206 | continue 207 | if zone[0] != name: 208 | # we have differently named zones in the layers 209 | # TODO: support this 210 | continue 211 | if zone[1] == overshoot: 212 | continue 213 | if len(zone) == 2: 214 | # need to add a third value. 215 | # zone[1] is to be interpreted as the min, 216 | # zone[2] is to be interpreted as the max 217 | zone.append(zone[1]) 218 | zone[1] = min(zone[1], overshoot) 219 | zone[2] = max(zone[2], overshoot) 220 | for zone in globalOvershoots: 221 | if len(zone) == 3: 222 | minValue = zone[1] 223 | maxValue = zone.pop() 224 | if minValue == -1: 225 | # some glyphs do not have anything in/on the zone, some do 226 | zone[1] = 'multiple' 227 | else: 228 | zone[1] = displayStrFromNumbers(minValue, maxValue) 229 | return globalOvershoots 230 | 231 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 232 | 233 | # seems to be called whenever a new font is opened 234 | # careful! not called when the user switches to a different, already opened font 235 | @objc.python_method 236 | def settings(self): 237 | self.name = Glyphs.localize({ 238 | 'en': 'Alignment', 239 | 'de': 'Ausrichtung', 240 | 'es': 'Alineación', 241 | 'fr': 'Alignement', 242 | 'pt': 'Alinhamento', 243 | }) 244 | width = 160 245 | self.marginTop = 6 246 | self.marginLeft = 6 247 | self.lineSpacing = 21 248 | smallSize = NSFont.systemFontSizeForControlSize_(NSFont.smallSystemFontSize()) 249 | textFieldHeight = smallSize + 7 250 | textFieldWidth = 86 251 | # lockHeight = textFieldHeight 252 | innerWidth = width - 2 * self.marginLeft 253 | height = (MAX_ZONES + 4) * self.lineSpacing + self.marginTop * 3 254 | self.posx_TextField = width - textFieldWidth - self.marginLeft 255 | 256 | # Create Vanilla window and group with controls 257 | self.paletteView = Window((width, height), minSize=(width, height - 10), maxSize=(width, height + 200)) 258 | self.paletteView.group = Group((0, 0, width, height)) 259 | 260 | posy = self.marginTop 261 | # set up fields for center 262 | headlineBbox = NSAttributedString.alloc().initWithString_attributes_('Bounding box', {NSFontAttributeName: NSFont.boldSystemFontOfSize_(smallSize)}) 263 | self.paletteView.group.headlineBbox = TextBox((10, posy, innerWidth, 18), headlineBbox, sizeStyle='small') 264 | posy += self.lineSpacing 265 | self.paletteView.group.centerXLabel = TextBox((10, posy + 3, innerWidth, 18), 'center x', sizeStyle='small') 266 | self.posy_centerX = posy 267 | self.paletteView.group.centerX = ArrowEditText((self.posx_TextField, posy, textFieldWidth, textFieldHeight), callback=self.editTextCallback, continuous=False, readOnly=False, formatter=None, sizeStyle='small') 268 | posy += self.lineSpacing 269 | self.paletteView.group.centerYLabel = TextBox((10, posy + 3, innerWidth, 18), 'center y', sizeStyle='small') 270 | self.posy_centerY = posy 271 | self.paletteView.group.centerY = ArrowEditText((self.posx_TextField, posy, textFieldWidth, textFieldHeight), callback=self.editTextCallback, continuous=False, readOnly=False, formatter=None, sizeStyle='small') 272 | posy += self.lineSpacing + self.marginTop 273 | # set up fields for overshoot 274 | headlineOvershoot = NSAttributedString.alloc().initWithString_attributes_('Overshoot', {NSFontAttributeName: NSFont.boldSystemFontOfSize_(NSFont.systemFontSizeForControlSize_(smallSize))}) 275 | self.paletteView.group.headlineOvershoot = TextBox((10, posy, innerWidth, 18), headlineOvershoot, sizeStyle='small') 276 | posy += self.lineSpacing 277 | self.paletteView.group, 'lineAbove', HorizontalLine((self.marginLeft, posy - 3, innerWidth, 1)) 278 | for i in range(MAX_ZONES): 279 | setattr(self.paletteView.group, 'name' + str(i), TextBox((10, posy, innerWidth, 18), '', sizeStyle='small')) 280 | setattr(self.paletteView.group, 'value' + str(i), TextBox((self.posx_TextField, posy, textFieldWidth - 3, textFieldHeight), '', sizeStyle='small', alignment='right')) 281 | posy += self.lineSpacing 282 | setattr(self.paletteView.group, 'line' + str(i), HorizontalLine((self.marginLeft, posy - 3, innerWidth, 1))) 283 | # set dialog to NSView 284 | self.dialog = self.paletteView.group.getNSView() 285 | # set self.font 286 | self.font = None 287 | windowController = self.windowController() 288 | if windowController: 289 | self.font = windowController.document().font 290 | 291 | @objc.python_method 292 | def update(self, sender=None): 293 | # do not update in case the palette is collapsed 294 | if self.dialog.frame().origin.y != 0: 295 | return 296 | if sender: 297 | self.font = sender.object() 298 | if isinstance(sender.object(), GSEditViewController): 299 | try: 300 | self.font = sender.object().representedObject() 301 | except: 302 | pass 303 | if isinstance(sender.object(), GSFontViewController): 304 | try: 305 | self.font = sender.object().parent 306 | except: 307 | pass 308 | if not self.font: 309 | return 310 | # do not update when too may glyphs are selected 311 | if not self.font.selectedLayers or len(self.font.selectedLayers) > MAX_GLYPHS_COUNT: 312 | self.paletteView.group.centerX.show(False) 313 | self.paletteView.group.centerY.show(False) 314 | for i in range(MAX_ZONES): 315 | getattr(self.paletteView.group, 'name' + str(i)).set('') 316 | getattr(self.paletteView.group, 'value' + str(i)).show(False) 317 | getattr(self.paletteView.group, 'line' + str(i)).show(False) 318 | return 319 | # update the center x and y 320 | if self.font.selectedLayers: 321 | # determine centers 322 | globalCenterX, globalCenterY = self.centerOfLayers(self.font.selectedLayers) 323 | # update dialog 324 | self.paletteView.group.centerX.show(True) 325 | self.paletteView.group.centerX.set(globalCenterX) 326 | self.paletteView.group.centerY.show(True) 327 | self.paletteView.group.centerY.set(globalCenterY) 328 | else: 329 | self.paletteView.group.centerX.show(False) 330 | self.paletteView.group.centerY.show(False) 331 | # update the overshoots 332 | if self.font.selectedLayers: 333 | # determine overshoots 334 | globalOvershoots = self.overshootsOfLayers(self.font.selectedLayers) 335 | else: 336 | globalOvershoots = [] 337 | for i in range(MAX_ZONES): 338 | try: 339 | zoneName, overshoot = globalOvershoots[i] 340 | getattr(self.paletteView.group, 'name' + str(i)).set(zoneName) 341 | getattr(self.paletteView.group, 'line' + str(i)).show(True) 342 | assert(overshoot is not None) 343 | if overshoot == -1: 344 | # nothing in the zone 345 | overshoot = '' 346 | getattr(self.paletteView.group, 'value' + str(i)).show(True) 347 | getattr(self.paletteView.group, 'value' + str(i)).set(overshoot) 348 | except IndexError: 349 | # this hides the excess fields 350 | getattr(self.paletteView.group, 'name' + str(i)).set('') 351 | getattr(self.paletteView.group, 'value' + str(i)).show(False) 352 | getattr(self.paletteView.group, 'line' + str(i)).show(False) 353 | 354 | # in future, this could be used to "lock" the x or y value, 355 | # i.e. auto-update the glyph in a "set up and forget" fashion 356 | # def lockCallback(self, button): 357 | # posX, posY, w, h = button.getPosSize() 358 | 359 | @objc.python_method 360 | def editTextCallback(self, editText): 361 | if not self.font or not self.font.selectedLayers: 362 | return 363 | isX = editText == self.paletteView.group.centerX 364 | newCenter = editText.get() 365 | sourceGlyph = None 366 | try: 367 | newCenter = float(newCenter) 368 | except ValueError: 369 | # see whether it is a glyph name: 370 | sourceGlyph = self.font.glyphs[newCenter] 371 | if not sourceGlyph: 372 | return 373 | if not STICK_TO_GRID: 374 | # zero subdivisions would not make sense 375 | if self.font.gridSubDivisions == 0: 376 | self.font.gridSubDivisions = 1 377 | # we temporarily double the number of subdivisions to allow for precise centering 378 | self.font.gridSubDivisions *= 2 379 | applyToAllMasters = NSEvent.modifierFlags() & NSShiftKeyMask 380 | if applyToAllMasters: 381 | layers = list({ 382 | l.parent.layers[m.id] 383 | for l in self.font.selectedLayers 384 | for m in self.font.masters 385 | }) 386 | else: 387 | layers = self.font.selectedLayers 388 | # we first treat only layers without components, 389 | # so as to make sure we are not updating the referenced glyph after the component 390 | for hasComponents in [False, True]: 391 | # set the layers' centers 392 | for layer in layers: 393 | if (len(layer.components) > 0) == hasComponents: 394 | try: 395 | layer.parent.beginUndo() 396 | except AttributeError: 397 | # probably a line break 398 | continue 399 | if sourceGlyph: 400 | sourceLayer = sourceGlyph.layers[layer.layerId] 401 | if not sourceLayer: 402 | continue 403 | sourceCenterX, sourceCenterY = self.centerOfLayer(sourceLayer) 404 | newCenter = sourceCenterX if isX else sourceCenterY 405 | self.setCenterOfLayer(layer, newCenter, isX) 406 | layer.parent.endUndo() 407 | # restore the number of subdivisions 408 | if not STICK_TO_GRID: 409 | self.font.gridSubDivisions /= 2 410 | Glyphs.redraw() 411 | self.update() 412 | 413 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 414 | # the following methods are adopted from the SDK without any changes 415 | 416 | @objc.python_method 417 | def start(self): 418 | # Adding a callback for the 'GSUpdateInterface' event 419 | Glyphs.addCallback(self.update, UPDATEINTERFACE) 420 | 421 | @objc.python_method 422 | def __del__(self): 423 | Glyphs.removeCallback(self.update) 424 | 425 | @objc.python_method 426 | def __file__(self): 427 | """Please leave this method unchanged""" 428 | return __file__ 429 | 430 | # Temporary Fix 431 | # Sort ID for compatibility with v919: 432 | _sortID = 0 433 | @objc.python_method 434 | def setSortID_(self, id): 435 | try: 436 | self._sortID = id 437 | except Exception as e: 438 | self.logToConsole("setSortID_: %s" % str(e)) 439 | 440 | @objc.python_method 441 | def sortID(self): 442 | return self._sortID 443 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /AnchorsPalette/Anchors.glyphsPalette/Contents/Resources/AnchorsPaletteView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | --------------------------------------------------------------------------------