├── .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 |
--------------------------------------------------------------------------------