├── build.py ├── build └── Space Station.roboFontExt │ ├── info.plist │ ├── lib │ ├── main.py │ ├── menu_fontEditorSpaceStation.py │ ├── menu_glyphEditorSpaceStation.py │ └── spaceStation │ │ ├── __init__.py │ │ ├── auto.py │ │ ├── controller.py │ │ ├── fontEditorWindow.py │ │ ├── formulas.py │ │ ├── glyphEditorWindow.py │ │ ├── tools.py │ │ └── window.py │ └── license ├── develop.py ├── license.txt ├── readme.md ├── requirements.txt ├── source └── code │ ├── main.py │ ├── menu_fontEditorSpaceStation.py │ ├── menu_glyphEditorSpaceStation.py │ └── spaceStation │ ├── __init__.py │ ├── auto.py │ ├── controller.py │ ├── fontEditorWindow.py │ ├── formulas.py │ ├── glyphEditorWindow.py │ ├── tools.py │ └── window.py └── to do.txt /build.py: -------------------------------------------------------------------------------- 1 | # ----------------- 2 | # Extension Details 3 | # ----------------- 4 | 5 | name = "Space Station" 6 | version = "0.1" 7 | developer = "Type Supply" 8 | developerURL = "http://typesupply.com" 9 | roboFontVersion = "3.2" 10 | pycOnly = False 11 | menuItems = [ 12 | dict( 13 | path="menu_glyphEditorSpaceStation.py", 14 | preferredName="Glyph Editor", 15 | shortKey=("command", "/") 16 | ), 17 | dict( 18 | path="menu_fontEditorSpaceStation.py", 19 | preferredName="Font Editor", 20 | shortKey="" 21 | ) 22 | ] 23 | 24 | installAfterBuild = True 25 | 26 | # ---------------------- 27 | # Don't edit below here. 28 | # ---------------------- 29 | 30 | from AppKit import * 31 | import os 32 | import shutil 33 | from mojo.extensions import ExtensionBundle 34 | 35 | # Convert short key modifiers. 36 | 37 | modifierMap = { 38 | "command": NSCommandKeyMask, 39 | "control": NSAlternateKeyMask, 40 | "option": NSAlternateKeyMask, 41 | "shift": NSShiftKeyMask, 42 | "capslock": NSAlphaShiftKeyMask, 43 | } 44 | 45 | for menuItem in menuItems: 46 | shortKey = menuItem.get("shortKey") 47 | if isinstance(shortKey, tuple): 48 | shortKey = list(shortKey) 49 | character = shortKey.pop(-1) 50 | modifiers = [modifierMap.get(modifier, modifier) for modifier in shortKey] 51 | if len(modifiers) == 1: 52 | modifiers = modifiers[0] 53 | else: 54 | m = None 55 | for modifier in modifiers: 56 | if m is None: 57 | m = modifier 58 | else: 59 | m |= modifier 60 | modifiers = m 61 | converted = (modifiers, character) 62 | menuItem["shortKey"] = tuple(converted) 63 | 64 | # Make the various paths. 65 | 66 | basePath = os.path.dirname(__file__) 67 | sourcePath = os.path.join(basePath, "source") 68 | libPath = os.path.join(sourcePath, "code") 69 | licensePath = os.path.join(basePath, "license.txt") 70 | requirementsPath = os.path.join(basePath, "requirements.txt") 71 | extensionFile = "%s.roboFontExt" % name 72 | buildPath = os.path.join(basePath, "build") 73 | extensionPath = os.path.join(buildPath, extensionFile) 74 | 75 | # Build the extension. 76 | 77 | B = ExtensionBundle() 78 | B.name = name 79 | B.developer = developer 80 | B.developerURL = developerURL 81 | B.version = version 82 | B.launchAtStartUp = True 83 | B.mainScript = "main.py" 84 | B.html = os.path.exists(os.path.join(sourcePath, "documentation", "index.html")) 85 | B.requiresVersionMajor = roboFontVersion.split(".")[0] 86 | B.requiresVersionMinor = roboFontVersion.split(".")[1] 87 | B.addToMenu = menuItems 88 | with open(licensePath) as license: 89 | B.license = license.read() 90 | with open(requirementsPath) as requirements: 91 | B.requirements = requirements.read() 92 | print("Building extension...", end=" ") 93 | v = B.save(extensionPath, libPath=libPath, pycOnly=pycOnly) 94 | print("done!") 95 | errors = B.validationErrors() 96 | if errors: 97 | print("Uh oh! There were errors:") 98 | print(errors) 99 | 100 | # Install the extension. 101 | 102 | if installAfterBuild: 103 | print("Installing extension...", end=" ") 104 | installDirectory = os.path.expanduser("~/Library/Application Support/RoboFont/plugins") 105 | installPath = os.path.join(installDirectory, extensionFile) 106 | if os.path.exists(installPath): 107 | shutil.rmtree(installPath) 108 | shutil.copytree(extensionPath, installPath) 109 | print("done!") 110 | print("RoboFont must now be restarted.") 111 | -------------------------------------------------------------------------------- /build/Space Station.roboFontExt/info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | addToMenu 6 | 7 | 8 | path 9 | menu_glyphEditorSpaceStation.py 10 | preferredName 11 | Glyph Editor 12 | shortKey 13 | 14 | 1048576 15 | / 16 | 17 | 18 | 19 | path 20 | menu_fontEditorSpaceStation.py 21 | preferredName 22 | Font Editor 23 | shortKey 24 | 25 | 26 | 27 | developer 28 | Type Supply 29 | developerURL 30 | http://typesupply.com 31 | html 32 | 33 | launchAtStartUp 34 | 35 | mainScript 36 | main.py 37 | name 38 | Space Station 39 | requiresVersionMajor 40 | 3 41 | requiresVersionMinor 42 | 2 43 | timeStamp 44 | 1598383298.38304 45 | version 46 | 0.1 47 | 48 | 49 | -------------------------------------------------------------------------------- /build/Space Station.roboFontExt/lib/main.py: -------------------------------------------------------------------------------- 1 | from AppKit import NSApp 2 | from spaceStation.controller import _SpaceStationController 3 | 4 | if __name__ == "__main__": 5 | NSApp().SpaceStationController = _SpaceStationController() 6 | -------------------------------------------------------------------------------- /build/Space Station.roboFontExt/lib/menu_fontEditorSpaceStation.py: -------------------------------------------------------------------------------- 1 | from spaceStation import SpaceStationController 2 | 3 | SpaceStationController().showFontEditorSpaceStation() -------------------------------------------------------------------------------- /build/Space Station.roboFontExt/lib/menu_glyphEditorSpaceStation.py: -------------------------------------------------------------------------------- 1 | from spaceStation import SpaceStationController 2 | 3 | SpaceStationController().showGlyphEditorSpaceStation() -------------------------------------------------------------------------------- /build/Space Station.roboFontExt/lib/spaceStation/__init__.py: -------------------------------------------------------------------------------- 1 | from AppKit import NSApp 2 | 3 | class SpaceStationError(Exception): pass 4 | 5 | def SpaceStationController(): 6 | return NSApp().SpaceStationController -------------------------------------------------------------------------------- /build/Space Station.roboFontExt/lib/spaceStation/auto.py: -------------------------------------------------------------------------------- 1 | from spaceStation.formulas import getFormula, calculateFormula,\ 2 | getReferencesInFormula, splitReference,\ 3 | setMetricValue 4 | from spaceStation import SpaceStationError 5 | 6 | # ----------- 7 | # Application 8 | # ----------- 9 | 10 | def applyFormulasInLayer(layer, onlyGlyphNames=None, onlyAttr=None): 11 | if onlyGlyphNames is None: 12 | onlyGlyphNames = layers.keys() 13 | sequence = getResolutionSequence(layer) 14 | for group in sequence: 15 | for reference in group: 16 | glyphName, attr = splitReference(reference) 17 | if glyphName not in onlyGlyphNames: 18 | continue 19 | if onlyAttr is not None and attr != onlyAttr: 20 | continue 21 | glyph = layer[glyphName] 22 | formula = getFormula(glyph, attr) 23 | if formula: 24 | value = calculateFormula(glyph, formula, attr) 25 | setMetricValue(glyph, attr, value) 26 | 27 | # --------------------- 28 | # Resolution Sequencing 29 | # --------------------- 30 | 31 | maximumReferenceDepth = 20 32 | 33 | def getResolutionSequence(layer): 34 | glyphNames = set() 35 | for glyphName in layer.keys(): 36 | glyph = layer[glyphName] 37 | for attr in "leftMargin rightMargin width".split(" "): 38 | formula = getFormula(glyph, attr) 39 | if formula: 40 | if attr == "leftMargin": 41 | a = "@left" 42 | elif attr == "rightMargin": 43 | a = "@right" 44 | else: 45 | a = "@width" 46 | glyphNames.add(glyphName + a) 47 | glyphNames |= getReferencesInFormula(formula, attr) 48 | sequence = [glyphNames] 49 | for i in range(maximumReferenceDepth): 50 | glyphNames = getGlyphsNeedingCalculation(layer, glyphNames) 51 | if not glyphNames: 52 | break 53 | else: 54 | sequence.append(glyphNames) 55 | if glyphNames: 56 | error = "Maximum reference depth exceeded. Could there be a circular reference among these glyphs?: %s" % repr(glyphNames) 57 | raise SpaceStationError(error) 58 | compressed = [] 59 | previous = None 60 | for glyphNames in reversed(sequence): 61 | if previous is not None: 62 | glyphNames = [glyphName for glyphName in glyphNames if glyphName not in previous] 63 | compressed.append(glyphNames) 64 | previous = glyphNames 65 | return compressed 66 | 67 | def getGlyphsNeedingCalculation(layer, glyphNames): 68 | found = set() 69 | for reference in glyphNames: 70 | glyphName, attr = splitReference(reference) 71 | if glyphName not in layer: 72 | continue 73 | glyph = layer[glyphName] 74 | formula = getFormula(glyph, attr) 75 | if formula: 76 | references = getReferencesInFormula(formula, attr) 77 | if references: 78 | found |= references 79 | return found 80 | 81 | def getDependenciesForGlyph(glyph, attr): 82 | formula = getFormula(glyph, attr) 83 | references = getReferencesInFormula(formula, attr) 84 | return references 85 | -------------------------------------------------------------------------------- /build/Space Station.roboFontExt/lib/spaceStation/controller.py: -------------------------------------------------------------------------------- 1 | from AppKit import NSBeep 2 | from fontParts.world import CurrentGlyph, CurrentFont 3 | from .glyphEditorWindow import GlyphEditorSpaceStationController 4 | from .fontEditorWindow import FontEditorSpaceStationController 5 | 6 | controllerIdentifier = "com.typesupply.SpaceStation" 7 | 8 | 9 | class _SpaceStationController(object): 10 | 11 | identifier = controllerIdentifier 12 | 13 | def showGlyphEditorSpaceStation(self): 14 | glyph = CurrentGlyph() 15 | if glyph is not None: 16 | GlyphEditorSpaceStationController(glyph) 17 | else: 18 | NSBeep() 19 | 20 | def showFontEditorSpaceStation(self): 21 | font = CurrentFont() 22 | if font is not None: 23 | FontEditorSpaceStationController(font.defaultLayer) 24 | else: 25 | NSBeep() 26 | -------------------------------------------------------------------------------- /build/Space Station.roboFontExt/lib/spaceStation/fontEditorWindow.py: -------------------------------------------------------------------------------- 1 | import os 2 | import fnmatch 3 | import AppKit 4 | import vanilla 5 | from mojo.UI import CurrentFontWindow 6 | from spaceStation import formulas 7 | from spaceStation import auto 8 | from spaceStation import tools 9 | 10 | class FontEditorSpaceStationController(object): 11 | 12 | def __init__(self, layer): 13 | self.layer = layer 14 | self.font = layer.font 15 | 16 | self.w = vanilla.Sheet( 17 | (946, 500), 18 | minSize=(946, 500), 19 | maxSize=(946, 50000), 20 | parentWindow=CurrentFontWindow().w 21 | ) 22 | 23 | columnDescriptions = [ 24 | dict( 25 | key="name", 26 | title="Name", 27 | editable=False 28 | ), 29 | dict( 30 | key="left", 31 | title="Left", 32 | editable=True 33 | ), 34 | dict( 35 | key="right", 36 | title="Right", 37 | editable=True 38 | ), 39 | dict( 40 | key="width", 41 | title="Width", 42 | editable=True 43 | ) 44 | ] 45 | self.w.list = vanilla.List( 46 | "auto", 47 | [], 48 | columnDescriptions=columnDescriptions, 49 | drawFocusRing=False, 50 | editCallback=self.listEditCallback 51 | ) 52 | 53 | self.w.filterSearchBox = vanilla.SearchBox( 54 | "auto", 55 | callback=self.populateList 56 | ) 57 | self.w.prioritizeProblemsCheckBox = vanilla.CheckBox( 58 | "auto", 59 | "Prioritize Problems", 60 | value=True, 61 | callback=self.populateList 62 | ) 63 | 64 | self.w.line = vanilla.HorizontalLine("auto") 65 | 66 | self.w.updateAllButton = vanilla.ImageButton( 67 | "auto", 68 | imageNamed=AppKit.NSImageNameRefreshTemplate, 69 | bordered=False, 70 | callback=self.updateButtonCallback 71 | ) 72 | self.w.clearAllButton = vanilla.ImageButton( 73 | "auto", 74 | imageNamed=AppKit.NSImageNameStopProgressTemplate, 75 | bordered=False, 76 | callback=self.clearButtonCallback 77 | ) 78 | 79 | self.w.updateLeftButton = vanilla.ImageButton( 80 | "auto", 81 | imageNamed=AppKit.NSImageNameRefreshTemplate, 82 | bordered=False, 83 | callback=self.updateButtonCallback 84 | ) 85 | self.w.clearLeftButton = vanilla.ImageButton( 86 | "auto", 87 | imageNamed=AppKit.NSImageNameStopProgressTemplate, 88 | bordered=False, 89 | callback=self.clearButtonCallback 90 | ) 91 | 92 | self.w.updateRightButton = vanilla.ImageButton( 93 | "auto", 94 | imageNamed=AppKit.NSImageNameRefreshTemplate, 95 | bordered=False, 96 | callback=self.updateButtonCallback 97 | ) 98 | self.w.clearRightButton = vanilla.ImageButton( 99 | "auto", 100 | imageNamed=AppKit.NSImageNameStopProgressTemplate, 101 | bordered=False, 102 | callback=self.clearButtonCallback 103 | ) 104 | 105 | self.w.updateWidthButton = vanilla.ImageButton( 106 | "auto", 107 | imageNamed=AppKit.NSImageNameRefreshTemplate, 108 | bordered=False, 109 | callback=self.updateButtonCallback 110 | ) 111 | self.w.clearWidthButton = vanilla.ImageButton( 112 | "auto", 113 | imageNamed=AppKit.NSImageNameStopProgressTemplate, 114 | bordered=False, 115 | callback=self.clearButtonCallback 116 | ) 117 | 118 | self.w.importButton = vanilla.Button( 119 | "auto", 120 | "Import", 121 | callback=self.importButtonCallback 122 | ) 123 | self.w.exportButton = vanilla.Button( 124 | "auto", 125 | "Export", 126 | callback=self.exportButtonCallback 127 | ) 128 | self.w.closeButton = vanilla.Button( 129 | "auto", 130 | "Close", 131 | callback=self.closeButtonCallback 132 | ) 133 | 134 | metrics = dict( 135 | margin=15, 136 | spacing=10, 137 | padding=5, 138 | imageButton=20, 139 | button=100, 140 | column=225, 141 | column1=15, 142 | column2=240, 143 | column3=465, 144 | column4=690 145 | ) 146 | rules = [ 147 | "H:|-margin-[filterSearchBox(==215)]-spacing-[prioritizeProblemsCheckBox]", 148 | "H:|-margin-[line]-margin-|", 149 | "H:|-column1-[updateAllButton(==imageButton)]-padding-[clearAllButton(==imageButton)]", 150 | "H:|-column2-[updateLeftButton(==imageButton)]-padding-[clearLeftButton(==imageButton)]", 151 | "H:|-column3-[updateRightButton(==imageButton)]-padding-[clearRightButton(==imageButton)]", 152 | "H:|-column4-[updateWidthButton(==imageButton)]-padding-[clearWidthButton(==imageButton)]", 153 | "H:|-margin-[list]-margin-|", 154 | "H:|-margin-[importButton(==button)]-spacing-[exportButton(==button)]", 155 | "H:[closeButton(==button)]-margin-|", 156 | "V:|" 157 | "-margin-" 158 | "[filterSearchBox]" 159 | "-spacing-" 160 | "[line]" 161 | "-spacing-" 162 | "[updateAllButton(==imageButton)]" 163 | "-padding-" 164 | "[list]" 165 | "-spacing-" 166 | "[importButton]" 167 | "-margin-" 168 | "|", 169 | "V:|" 170 | "-margin-" 171 | "[prioritizeProblemsCheckBox(==filterSearchBox)]", 172 | "V:" 173 | "[line]" 174 | "-spacing-" 175 | "[clearAllButton(==imageButton)]", 176 | "V:" 177 | "[line]" 178 | "-spacing-" 179 | "[updateLeftButton(==imageButton)]", 180 | "V:" 181 | "[line]" 182 | "-spacing-" 183 | "[clearLeftButton(==imageButton)]", 184 | "V:" 185 | "[line]" 186 | "-spacing-" 187 | "[updateRightButton(==imageButton)]", 188 | "V:" 189 | "[line]" 190 | "-spacing-" 191 | "[clearRightButton(==imageButton)]", 192 | "V:" 193 | "[line]" 194 | "-spacing-" 195 | "[updateWidthButton(==imageButton)]", 196 | "V:" 197 | "[line]" 198 | "-spacing-" 199 | "[clearWidthButton(==imageButton)]", 200 | "V:" 201 | "[list]" 202 | "-spacing-" 203 | "[exportButton]", 204 | "V:" 205 | "[list]" 206 | "-spacing-" 207 | "[closeButton]", 208 | ] 209 | 210 | self.w.addAutoPosSizeRules(rules, metrics) 211 | self.populateList() 212 | self.w.open() 213 | 214 | def closeButtonCallback(self, sender): 215 | self.w.close() 216 | 217 | def populateList(self, sender=None): 218 | searchPattern = self.w.filterSearchBox.get() 219 | glyphNames = [name for name in self.font.glyphOrder if name in self.layer] 220 | for glyphName in sorted(self.layer.keys()): 221 | if glyphName not in glyphNames: 222 | glyphNames.append(gyphName) 223 | if searchPattern: 224 | glyphNames = [ 225 | glyphName 226 | for glyphName in glyphNames 227 | if fnmatch.fnmatchcase(glyphName, searchPattern) 228 | ] 229 | if self.w.prioritizeProblemsCheckBox.get(): 230 | problems = [] 231 | noProblems = [] 232 | for glyphName in glyphNames: 233 | data = self.getDataForGlyphName(glyphName) 234 | if data["leftHasProblem"] or data["rightHasProblem"] or data["widthHasProblem"]: 235 | problems.append(data) 236 | else: 237 | noProblems.append(data) 238 | data = problems + noProblems 239 | else: 240 | data = [ 241 | self.getDataForGlyphName(glyphName) 242 | for glyphName in glyphNames 243 | ] 244 | self._inInternalDataUpdate = True 245 | self.w.list.set(data) 246 | self._inInternalDataUpdate = False 247 | 248 | def updateAllData(self): 249 | self._inInternalDataUpdate = True 250 | for container in self.w.list: 251 | self.getDataForGlyphName(container["name"], container) 252 | self._inInternalDataUpdate = False 253 | 254 | def getDataForGlyphName(self, glyphName, container=None): 255 | if container is None: 256 | container = dict( 257 | name=glyphName, 258 | left=None, 259 | leftHasProblem=False, 260 | right=None, 261 | rightHasProblem=False, 262 | width=None, 263 | widthHasProblem=False 264 | ) 265 | glyph = self.layer[glyphName] 266 | left, leftHasProblem = visualizeFormula( 267 | glyph, 268 | "leftMargin", 269 | formulas.getFormula(glyph, "leftMargin") 270 | ) 271 | container["left"] = left 272 | container["leftHasProblem"] = leftHasProblem 273 | right, rightHasProblem = visualizeFormula( 274 | glyph, 275 | "rightMargin", 276 | formulas.getFormula(glyph, "rightMargin") 277 | ) 278 | container["right"] = right 279 | container["rightHasProblem"] = rightHasProblem 280 | width, widthHasProblem = visualizeFormula( 281 | glyph, 282 | "width", 283 | formulas.getFormula(glyph, "width") 284 | ) 285 | container["width"] = width 286 | container["widthHasProblem"] = widthHasProblem 287 | for k, v in container.items(): 288 | if v is None: 289 | container[k] = "" 290 | return container 291 | 292 | # ---- 293 | # Edit 294 | # ---- 295 | 296 | _inInternalDataUpdate = False 297 | 298 | def listEditCallback(self, sender): 299 | if self._inInternalDataUpdate: 300 | return 301 | selection = sender.getSelection()[0] 302 | container = sender[selection] 303 | glyphName = container["name"] 304 | glyph = self.layer[glyphName] 305 | left = container.get("left", "") 306 | if left: 307 | left = str(left) 308 | formulas.setFormula(glyph, "leftMargin", left) 309 | else: 310 | formulas.clearFormula(glyph, "leftMargin") 311 | right = container.get("right", "") 312 | if right: 313 | right = str(right) 314 | formulas.setFormula(glyph, "rightMargin", right) 315 | else: 316 | formulas.clearFormula(glyph, "rightMargin") 317 | width = container.get("width", "") 318 | if width: 319 | width = str(width) 320 | formulas.setFormula(glyph, "width", width) 321 | else: 322 | formulas.clearFormula(glyph, "width") 323 | self.updateAllData() 324 | 325 | # ------------ 326 | # Update/Clear 327 | # ------------ 328 | 329 | def updateButtonCallback(self, sender): 330 | attrMap = { 331 | self.w.updateAllButton : None, 332 | self.w.updateLeftButton : "leftMargin", 333 | self.w.updateRightButton : "rightMargin", 334 | self.w.updateWidthButton : "width" 335 | } 336 | attr = attrMap[sender] 337 | glyphNames = [self.w.list[i]["name"] for i in self.w.list.getSelection()] 338 | auto.applyFormulasInLayer( 339 | self.layer, 340 | onlyGlyphNames=glyphNames, 341 | onlyAttr=attr 342 | ) 343 | self.updateAllData() 344 | 345 | def clearButtonCallback(self, sender): 346 | attrMap = { 347 | self.w.clearAllButton : None, 348 | self.w.clearLeftButton : "leftMargin", 349 | self.w.clearRightButton : "rightMargin", 350 | self.w.clearWidthButton : "width" 351 | } 352 | attr = attrMap[sender] 353 | glyphNames = [self.w.list[i]["name"] for i in self.w.list.getSelection()] 354 | for glyphName in glyphNames: 355 | glyph = self.layer[glyphName] 356 | if attr is None: 357 | formulas.clearFormula(glyph, "leftMargin") 358 | formulas.clearFormula(glyph, "rightMargin") 359 | formulas.clearFormula(glyph, "width") 360 | else: 361 | formulas.clearFormula(glyph, attr) 362 | self.updateAllData() 363 | 364 | # ------------- 365 | # Import/Export 366 | # ------------- 367 | 368 | def importButtonCallback(self, sender): 369 | vanilla.dialogs.getFile( 370 | fileTypes=["spacestation"], 371 | resultCallback=self._importCallback, 372 | parentWindow=self.w 373 | ) 374 | 375 | def _importCallback(self, path): 376 | if not path: 377 | return 378 | for glyph in self.layer: 379 | formulas.clearFormula(glyph, "leftMargin") 380 | formulas.clearFormula(glyph, "rightMargin") 381 | formulas.clearFormula(glyph, "width") 382 | path = path[0] 383 | f = open(path, "r") 384 | text = f.read() 385 | f.close() 386 | formulas.layerFromString(self.layer, text) 387 | self.updateAllData() 388 | 389 | def exportButtonCallback(self, sender): 390 | directory = os.path.dirname(self.font.path) 391 | fileName = os.path.splitext(os.path.basename(self.font.path))[0] + ".spacestation" 392 | vanilla.dialogs.putFile( 393 | resultCallback=self._exportCallback, 394 | parentWindow=self.w, 395 | directory=directory, 396 | fileName=fileName 397 | ) 398 | 399 | def _exportCallback(self, path): 400 | if not path: 401 | return 402 | glyphOrder = list(self.font.glyphOrder) 403 | for name in sorted(self.layer.keys()): 404 | if name not in glyphOrder: 405 | glyphOrder.append(name) 406 | text = formulas.layerToString(self.layer, glyphOrder) 407 | f = open(path, "w") 408 | f.write(text) 409 | f.close() 410 | 411 | 412 | red = AppKit.NSColor.redColor() 413 | 414 | def visualizeFormula(glyph, attr, formula): 415 | if not formula: 416 | return formula, False 417 | calculatedValue = formulas.calculateFormula( 418 | glyph, 419 | formula, 420 | formulas.getAngledAttrIfNecessary(glyph.font, attr) 421 | ) 422 | needColor = False 423 | if calculatedValue is None: 424 | needColor = True 425 | else: 426 | value = formulas.getMetricValue(glyph, attr) 427 | if tools.roundint(value) != tools.roundint(calculatedValue): 428 | needColor = True 429 | if needColor: 430 | formula = AppKit.NSAttributedString.alloc().initWithString_attributes_( 431 | formula, 432 | {AppKit.NSForegroundColorAttributeName : red} 433 | ) 434 | return formula, needColor 435 | -------------------------------------------------------------------------------- /build/Space Station.roboFontExt/lib/spaceStation/formulas.py: -------------------------------------------------------------------------------- 1 | from .tools import roundint 2 | 3 | # ------------- 4 | # Lib Shortcuts 5 | # ------------- 6 | 7 | formulaLibKeyStub = "com.typesupply.SpaceStation.formula." 8 | 9 | def getFormula(glyph, attr): 10 | """ 11 | Get the formula set in the glyph for the attr. 12 | """ 13 | return glyph.lib.get(formulaLibKeyStub + attr) 14 | 15 | def setFormula(glyph, attr, formula): 16 | """ 17 | Set the formula in the glyph for the attr. 18 | """ 19 | key = formulaLibKeyStub + attr 20 | if glyph.lib.get(key) == formula: 21 | return 22 | if formula is None: 23 | if key in glyph.lib: 24 | del glyph.lib[key] 25 | else: 26 | glyph.lib[key] = formula 27 | 28 | def clearFormula(glyph, attr=None): 29 | """ 30 | Clear the formula for the attr from the glyph. 31 | If no attr is given, all attrs will be cleared. 32 | """ 33 | if attr is not None: 34 | attrs = [attr] 35 | else: 36 | attrs = symbolToAttribute.values() 37 | for attr in attrs: 38 | key = formulaLibKeyStub + attr 39 | if key in glyph.lib: 40 | del glyph.lib[key] 41 | 42 | # -------- 43 | # Formulas 44 | # -------- 45 | 46 | mathSymbols = """ 47 | + 48 | - 49 | * 50 | / 51 | ( 52 | ) 53 | """.strip().splitlines() 54 | 55 | symbolToAttribute = { 56 | "@left" : "leftMargin", 57 | "@right" : "rightMargin", 58 | "@width" : "width" 59 | } 60 | 61 | attributeToSymbol = {} 62 | for symbol, attr in symbolToAttribute.items(): 63 | attributeToSymbol[attr] = symbol 64 | 65 | def splitFormula(formula): 66 | """ 67 | Split a formula into parts. 68 | """ 69 | formula = formula.strip() 70 | formula = formula.split("#", 1)[0] 71 | if not formula: 72 | return [] 73 | for symbol in mathSymbols: 74 | formula = formula.replace(symbol, " " + symbol + " ") 75 | formula = [i for i in formula.split(" ") if i] 76 | return formula 77 | 78 | def calculateFormula(glyph, formula, impliedAttr): 79 | """ 80 | Calculate the value of a formula. 81 | """ 82 | formula = splitFormula(formula) 83 | formula = _convertReferencesToNumbers(glyph, glyph.layer, formula, impliedAttr) 84 | if formula is None: 85 | return None 86 | value = _evaluateFormula(formula) 87 | return value 88 | 89 | def _evaluateFormula(formula): 90 | text = " ".join(formula) 91 | value = eval(text) 92 | return value 93 | 94 | # ---------- 95 | # References 96 | # ---------- 97 | 98 | def _convertReferencesToNumbers(glyph, layer, formula, impliedAttr="leftMargin"): 99 | expanded = [] 100 | for part in formula: 101 | if part in mathSymbols: 102 | expanded.append(part) 103 | else: 104 | value = 0 105 | try: 106 | v = float(part) 107 | value = v 108 | except ValueError: 109 | attr = impliedAttr 110 | for symbol, a in symbolToAttribute.items(): 111 | if part.endswith(symbol): 112 | attr = a 113 | part = part[:-len(symbol)] 114 | break 115 | if not part: 116 | part = glyph 117 | elif part not in layer: 118 | return None 119 | else: 120 | part = layer[part] 121 | value = getMetricValue(part, attr) 122 | expanded.append(str(value)) 123 | return expanded 124 | 125 | def getReferencesInFormula(formula, impliedAttr): 126 | """ 127 | Get glyphs referenced by a formula. 128 | """ 129 | formula = splitFormula(formula) 130 | references = set() 131 | for i in formula: 132 | if i in mathSymbols: 133 | continue 134 | if i in symbolToAttribute: 135 | continue 136 | try: 137 | float(i) 138 | continue 139 | except ValueError: 140 | pass 141 | foundSymbol = False 142 | for symbol in symbolToAttribute.keys(): 143 | if i.endswith(symbol): 144 | foundSymbol = True 145 | break 146 | if not foundSymbol: 147 | i += attributeToSymbol[impliedAttr] 148 | references.add(i) 149 | return references 150 | 151 | def splitReference(reference): 152 | """ 153 | Split a reference into a glyph name and attribute. 154 | """ 155 | for symbol, attr in symbolToAttribute.items(): 156 | if reference.endswith(symbol): 157 | reference = reference[:-len(symbol)] 158 | return reference, attr 159 | return reference, None 160 | 161 | # ----- 162 | # Tools 163 | # ----- 164 | 165 | def getMetricValue(glyph, attr): 166 | """ 167 | Get the metric value for an attribute. 168 | """ 169 | attr = getAngledAttrIfNecessary(glyph.font, attr) 170 | return getattr(glyph, attr) 171 | 172 | def setMetricValue(glyph, attr, value): 173 | """ 174 | Set the metric value for an attribute. 175 | """ 176 | attr = getAngledAttrIfNecessary(glyph.font, attr) 177 | value = roundint(value) 178 | setattr(glyph, attr, value) 179 | 180 | def getAngledAttrIfNecessary(font, attr): 181 | """ 182 | Coerce "leftMargin" or "rightMargin" to 183 | "angledLeftMargin" or "angledRightMargin" 184 | if the font is italic. 185 | """ 186 | useAngledMargins = font.info.italicAngle != 0 187 | if useAngledMargins: 188 | if attr == "leftMargin": 189 | attr = "angledLeftMargin" 190 | elif attr == "rightMargin": 191 | attr = "angledRightMargin" 192 | return attr 193 | 194 | # --- 195 | # I/O 196 | # --- 197 | 198 | syntax = """ 199 | # SYNTAX 200 | # ------ 201 | # # = comment. Anything after # will be ignored. 202 | # > = glyph name 203 | # L = left margin 204 | # R = right margin 205 | # W = width 206 | # 207 | # Empty lines have no meaning. 208 | """.strip() 209 | 210 | def layerToString(layer, glyphOrder=None): 211 | """ 212 | Write the formulas for all glyph in the layer to a string. 213 | """ 214 | if glyphOrder is None: 215 | glyphOrder = layer.font.glyphOrder 216 | text = [syntax + "\n\n\n"] 217 | for glyphName in glyphOrder: 218 | if glyphName not in layer: 219 | continue 220 | glyph = layer[glyphName] 221 | text.append(glyphToString(glyph)) 222 | return ("\n\n".join(text)) 223 | 224 | tokenToAttr = dict(L="leftMargin", R="rightMargin", W="width") 225 | tokenOrder = list("LRW") 226 | 227 | def glyphToString(glyph): 228 | """ 229 | Write the formulas defined for the glyph to a string. 230 | """ 231 | text = [ 232 | "> " + glyph.name 233 | ] 234 | for token in tokenOrder: 235 | attr = tokenToAttr[token] 236 | formula = getFormula(glyph, attr) 237 | line = token + " = " 238 | if not formula: 239 | line = "#" + line 240 | else: 241 | line += formula 242 | value = roundint(getMetricValue(glyph, attr)) 243 | calculated = calculateFormula(glyph, formula, attr) 244 | if calculated is not None: 245 | calculated = roundint(calculated) 246 | if value != calculated: 247 | line += " # value: {value} expected: {calculated}".format(value=value, calculated=calculated) 248 | text.append(line) 249 | return "\n".join(text) 250 | 251 | def layerFromString(layer, text): 252 | """ 253 | Load the formulas for all glyphs in the layer from the text. 254 | This does not apply the calculated formulas. 255 | """ 256 | glyphs = {} 257 | currentGlyph = None 258 | for line in text.splitlines(): 259 | line = line.split("#", 1)[0] 260 | line = line.strip() 261 | if not line: 262 | continue 263 | if line.startswith(">"): 264 | currentGlyph = line[1:].strip() 265 | glyphs[currentGlyph] = [] 266 | elif currentGlyph is not None: 267 | glyphs[currentGlyph].append(line) 268 | for glyphName, text in glyphs.items(): 269 | if glyphName not in layer: 270 | continue 271 | text = "\n".join(text) 272 | glyphFromString(layer[glyphName], text) 273 | 274 | def glyphFromString(glyph, text): 275 | """ 276 | Load the formulas for the glyph from the text. 277 | This does not apply the calculated formulas. 278 | """ 279 | clearFormula(glyph) 280 | for line in text.splitlines(): 281 | line = line.split("#", 1)[0] 282 | line = line.strip() 283 | if not line: 284 | continue 285 | if line.startswith(">"): 286 | continue 287 | if "=" not in line: 288 | continue 289 | token, formula = line.split("=", 1) 290 | token = token.strip() 291 | formula = formula.strip() 292 | if token not in tokenToAttr: 293 | continue 294 | attr = tokenToAttr[token] 295 | setFormula(glyph, attr, formula) 296 | -------------------------------------------------------------------------------- /build/Space Station.roboFontExt/lib/spaceStation/glyphEditorWindow.py: -------------------------------------------------------------------------------- 1 | from AppKit import * 2 | import vanilla 3 | from mojo.roboFont import CurrentGlyph 4 | from mojo.UI import StatusInteractivePopUpWindow, CurrentGlyphWindow 5 | from .formulas import getFormula, setFormula, clearFormula, calculateFormula, getMetricValue, setMetricValue 6 | from .tools import roundint 7 | 8 | 9 | inSyncButtonColor = NSColor.blackColor() 10 | outSyncButtonColor = NSColor.redColor() 11 | 12 | escapeCharacter = "\x1B" 13 | 14 | 15 | class GlyphEditorSpaceStationController(object): 16 | 17 | def __init__(self, glyph): 18 | self.w = StatusInteractivePopUpWindow((250, 0), centerInView=CurrentGlyphWindow().getGlyphView()) 19 | 20 | metrics = dict( 21 | border=15, 22 | padding1=10, 23 | padding2=5, 24 | titleWidth=45, 25 | inputSpace=70, # border + title + padding 26 | killButtonWidth=20, 27 | navigateButtonWidth=30, 28 | fieldHeight=22, 29 | ) 30 | rules = [ 31 | # Left 32 | "H:|-border-[leftTitle(==titleWidth)]-padding1-[leftField]-border-|", 33 | "H:|-inputSpace-[leftButton]-padding2-[leftKillButton(==killButtonWidth)]-border-|", 34 | "V:|-border-[leftTitle(==fieldHeight)]", 35 | "V:|-border-[leftField(==fieldHeight)]", 36 | "V:[leftField]-padding2-[leftButton]", 37 | "V:[leftField]-padding2-[leftKillButton(==leftButton)]", 38 | 39 | # Right 40 | "H:|-border-[rightTitle(==titleWidth)]-padding1-[rightField]-border-|", 41 | "H:|-inputSpace-[rightButton]-padding2-[rightKillButton(==killButtonWidth)]-border-|", 42 | "V:[leftButton]-padding1-[rightTitle(==fieldHeight)]", 43 | "V:[leftButton]-padding1-[rightField(==fieldHeight)]", 44 | "V:[rightField]-padding2-[rightButton]", 45 | "V:[rightField]-padding2-[rightKillButton(==rightButton)]", 46 | 47 | # Width 48 | "H:|-border-[widthTitle(==titleWidth)]-padding1-[widthField]-border-|", 49 | "H:|-inputSpace-[widthButton]-padding2-[widthKillButton(==killButtonWidth)]-border-|", 50 | "V:[rightButton]-padding1-[widthTitle(==fieldHeight)]", 51 | "V:[rightButton]-padding1-[widthField(==fieldHeight)]", 52 | "V:[widthField]-padding2-[widthButton]", 53 | "V:[widthField]-padding2-[widthKillButton(==rightButton)]", 54 | 55 | # Bottom 56 | "H:|-inputSpace-[line]-border-|", 57 | "H:|-inputSpace-[previousGlyphButton(==navigateButtonWidth)]-padding2-[nextGlyphButton(==navigateButtonWidth)]-padding1-[doneButton(>=0)]-border-|", 58 | "V:[widthButton]-padding1-[line]", 59 | "V:[line]-padding1-[previousGlyphButton]-border-|", 60 | "V:[line]-padding1-[nextGlyphButton]-border-|", 61 | "V:[line]-padding1-[doneButton]-border-|", 62 | ] 63 | 64 | self.w.leftTitle = vanilla.TextBox("auto", "Left:", alignment="right") 65 | self.w.leftField = vanilla.EditText("auto", "", continuous=False, callback=self.fieldCallback) 66 | self.w.leftButton = vanilla.Button("auto", "", callback=self.buttonCallback) 67 | self.w.leftKillButton = vanilla.ImageButton("auto", imageNamed=NSImageNameStopProgressFreestandingTemplate, bordered=False, callback=self.buttonCallback) 68 | 69 | self.w.rightTitle = vanilla.TextBox("auto", "Right:", alignment="right") 70 | self.w.rightField = vanilla.EditText("auto", "", continuous=False, callback=self.fieldCallback) 71 | self.w.rightButton = vanilla.Button("auto", "", callback=self.buttonCallback) 72 | self.w.rightKillButton = vanilla.ImageButton("auto", imageNamed=NSImageNameStopProgressFreestandingTemplate, bordered=False, callback=self.buttonCallback) 73 | 74 | self.w.widthTitle = vanilla.TextBox("auto", "Width:", alignment="right") 75 | self.w.widthField = vanilla.EditText("auto", "", continuous=False, callback=self.fieldCallback) 76 | self.w.widthButton = vanilla.Button("auto", "", callback=self.buttonCallback) 77 | self.w.widthKillButton = vanilla.ImageButton("auto", imageNamed=NSImageNameStopProgressFreestandingTemplate, bordered=False, callback=self.buttonCallback) 78 | 79 | self.controlGroups = [ 80 | dict(attr="leftMargin", field=self.w.leftField, button=self.w.leftButton, kill=self.w.leftKillButton), 81 | dict(attr="rightMargin", field=self.w.rightField, button=self.w.rightButton, kill=self.w.rightKillButton), 82 | dict(attr="width", field=self.w.widthField, button=self.w.widthButton, kill=self.w.widthKillButton), 83 | ] 84 | for group in self.controlGroups: 85 | field = group["field"] 86 | button = group["button"] 87 | button.getNSButton().setAlignment_(NSLeftTextAlignment) 88 | 89 | self.w.line = vanilla.HorizontalLine("auto") 90 | self.w.doneButton = vanilla.Button("auto", "Close", callback=self.doneCallback) 91 | self.w.doneButton.bind(escapeCharacter, []) 92 | self.w.previousGlyphButton = vanilla.Button("auto", "←", callback=self.previousGlyphCallback) 93 | self.w.previousGlyphButton.bind("[", ["command"]) 94 | self.w.nextGlyphButton = vanilla.Button("auto", "→", callback=self.nextGlyphCallback) 95 | self.w.nextGlyphButton.bind("]", ["command"]) 96 | 97 | self.w.addAutoPosSizeRules(rules, metrics) 98 | 99 | self.loadGlyph() 100 | 101 | self.w.open() 102 | 103 | def setFirstResponder(self, control): 104 | self.w.getNSWindow().makeFirstResponder_(control.getNSTextField()) 105 | 106 | def _getControlGroup(self, sender): 107 | for group in self.controlGroups: 108 | field = group["field"] 109 | button = group["button"] 110 | kill = group["kill"] 111 | if sender == field: 112 | return group 113 | if sender == button: 114 | return group 115 | if sender == kill: 116 | return group 117 | 118 | def doneCallback(self, sender): 119 | self.w.close() 120 | 121 | # -------- 122 | # Updaters 123 | # -------- 124 | 125 | def loadGlyph(self): 126 | self._inGlyphLoad = True 127 | self.glyph = CurrentGlyph() 128 | if self.glyph.bounds is None: 129 | self.setFirstResponder(self.w.widthField) 130 | else: 131 | self.setFirstResponder(self.w.leftField) 132 | leftField = self.w.leftField.getNSTextField() 133 | rightField = self.w.rightField.getNSTextField() 134 | leftField.setNextKeyView_(rightField) 135 | rightField.setNextKeyView_(leftField) 136 | self._updateFields() 137 | self._updateButtons() 138 | self._inGlyphLoad = False 139 | 140 | def _updateFields(self): 141 | for group in self.controlGroups: 142 | attr = group["attr"] 143 | field = group["field"] 144 | if attr in ("leftMargin", "rightMargin") and self.glyph.bounds is None: 145 | value = "" 146 | else: 147 | value = getMetricValue(self.glyph, attr) 148 | value = roundint(value) 149 | field.set(value) 150 | 151 | def _updateButtons(self): 152 | for group in self.controlGroups: 153 | attr = group["attr"] 154 | button = group["button"] 155 | formula = getFormula(self.glyph, attr) 156 | if not formula: 157 | button.setTitle("") 158 | button.enable(False) 159 | continue 160 | calculatedValue = calculateFormula(self.glyph, formula, attr) 161 | value = getMetricValue(self.glyph, attr) 162 | if roundint(value) != roundint(calculatedValue): 163 | color = outSyncButtonColor 164 | else: 165 | color = inSyncButtonColor 166 | string = NSAttributedString.alloc().initWithString_attributes_(formula, {NSForegroundColorAttributeName : color}) 167 | button.setTitle(string) 168 | button.enable(True) 169 | 170 | # --------- 171 | # Callbacks 172 | # --------- 173 | 174 | def fieldCallback(self, sender): 175 | if self._inGlyphLoad: 176 | return 177 | group = self._getControlGroup(sender) 178 | attr = group["attr"] 179 | field = group["field"] 180 | button = group["button"] 181 | value = field.get().strip() 182 | if value.startswith("="): 183 | formula = value[1:] 184 | if not formula: 185 | NSBeep() 186 | return 187 | value = calculateFormula(self.glyph, formula, attr) 188 | if value is None: 189 | NSBeep() 190 | return 191 | field.set(str(roundint(value))) 192 | setFormula(self.glyph, attr, formula) 193 | else: 194 | try: 195 | value = int(value) 196 | except: 197 | NSBeep() 198 | return 199 | self.glyph.prepareUndo("Spacing Change") 200 | setMetricValue(self.glyph, attr, value) 201 | self.glyph.performUndo() 202 | self._updateFields() 203 | self._updateButtons() 204 | 205 | def buttonCallback(self, sender): 206 | group = self._getControlGroup(sender) 207 | attr = group["attr"] 208 | field = group["field"] 209 | button = group["button"] 210 | kill = group["kill"] 211 | if sender == kill: 212 | clearFormula(self.glyph, attr) 213 | else: 214 | formula = button.getTitle() 215 | value = calculateFormula(self.glyph, formula, attr) 216 | if value is None: 217 | NSBeep() 218 | return 219 | self.glyph.prepareUndo("Spacing Change") 220 | setMetricValue(self.glyph, attr, value) 221 | self.glyph.performUndo() 222 | self._updateFields() 223 | self._updateButtons() 224 | 225 | def previousGlyphCallback(self, sender): 226 | CurrentGlyphWindow().getGlyphView().previousGlyph_() 227 | self.loadGlyph() 228 | 229 | def nextGlyphCallback(self, sender): 230 | CurrentGlyphWindow().getGlyphView().nextGlyph_() 231 | self.loadGlyph() 232 | 233 | -------------------------------------------------------------------------------- /build/Space Station.roboFontExt/lib/spaceStation/tools.py: -------------------------------------------------------------------------------- 1 | def roundint(number): 2 | return int(round(number)) 3 | -------------------------------------------------------------------------------- /build/Space Station.roboFontExt/lib/spaceStation/window.py: -------------------------------------------------------------------------------- 1 | from AppKit import * 2 | import vanilla 3 | 4 | escapeCharacter = "$" 5 | 6 | class GlyphEditorSpaceStationController(object): 7 | 8 | def __init__(self): 9 | self.glyph = CurrentGlyph() 10 | 11 | self.w = vanilla.Window((300, 0), minSize=(100, 100)) 12 | 13 | metrics = dict( 14 | border=15, 15 | padding1=10, 16 | padding2=5, 17 | titleWidth=45, 18 | inputSpace=70, # border + title + padding 19 | killButtonWidth=20, 20 | fieldHeight=22, 21 | ) 22 | rules = [ 23 | # Left 24 | "H:|-border-[leftTitle(==titleWidth)]-padding1-[leftField]-border-|", 25 | "H:|-inputSpace-[leftButton]-padding2-[leftKillButton(==killButtonWidth)]-border-|", 26 | "V:|-border-[leftTitle(==fieldHeight)]", 27 | "V:|-border-[leftField(==fieldHeight)]", 28 | "V:[leftField]-padding2-[leftButton]", 29 | "V:[leftField]-padding2-[leftKillButton(==leftButton)]", 30 | 31 | # Right 32 | "H:|-border-[rightTitle(==titleWidth)]-padding1-[rightField]-border-|", 33 | "H:|-inputSpace-[rightButton]-padding2-[rightKillButton(==killButtonWidth)]-border-|", 34 | "V:[leftButton]-padding1-[rightTitle(==fieldHeight)]", 35 | "V:[leftButton]-padding1-[rightField(==fieldHeight)]", 36 | "V:[rightField]-padding2-[rightButton]", 37 | "V:[rightField]-padding2-[rightKillButton(==rightButton)]", 38 | 39 | # Width 40 | "H:|-border-[widthTitle(==titleWidth)]-padding1-[widthField]-border-|", 41 | "H:|-inputSpace-[widthButton]-padding2-[widthKillButton(==killButtonWidth)]-border-|", 42 | "V:[rightButton]-padding1-[widthTitle(==fieldHeight)]", 43 | "V:[rightButton]-padding1-[widthField(==fieldHeight)]", 44 | "V:[widthField]-padding2-[widthButton]", 45 | "V:[widthField]-padding2-[widthKillButton(==rightButton)]", 46 | 47 | # Bottom 48 | "H:|-inputSpace-[line]-border-|", 49 | "H:|-inputSpace-[doneButton]-border-|", 50 | "V:[widthButton]-padding1-[line]-padding1-[doneButton]-border-|", 51 | ] 52 | 53 | self.w.leftTitle = vanilla.TextBox("auto", "Left:", alignment="right") 54 | self.w.leftField = vanilla.EditText("auto", "", continuous=False, callback=self.fieldCallback) 55 | self.w.leftButton = vanilla.Button("auto", "", callback=self.buttonCallback) 56 | self.w.leftKillButton = vanilla.ImageButton("auto", imageNamed=NSImageNameStopProgressFreestandingTemplate, bordered=False, callback=self.buttonCallback) 57 | 58 | self.w.rightTitle = vanilla.TextBox("auto", "Right:", alignment="right") 59 | self.w.rightField = vanilla.EditText("auto", "", continuous=False, callback=self.fieldCallback) 60 | self.w.rightButton = vanilla.Button("auto", "", callback=self.buttonCallback) 61 | self.w.rightKillButton = vanilla.ImageButton("auto", imageNamed=NSImageNameStopProgressFreestandingTemplate, bordered=False, callback=self.buttonCallback) 62 | 63 | self.w.widthTitle = vanilla.TextBox("auto", "Width:", alignment="right") 64 | self.w.widthField = vanilla.EditText("auto", "", continuous=False, callback=self.fieldCallback) 65 | self.w.widthButton = vanilla.Button("auto", "", callback=self.buttonCallback) 66 | self.w.widthKillButton = vanilla.ImageButton("auto", imageNamed=NSImageNameStopProgressFreestandingTemplate, bordered=False, callback=self.buttonCallback) 67 | 68 | self.controlGroups = [ 69 | dict(attr="leftMargin", field=self.w.leftField, button=self.w.leftButton), 70 | dict(attr="rightMargin", field=self.w.rightField, button=self.w.rightButton), 71 | dict(attr="width", field=self.w.widthField, button=self.w.widthButton), 72 | ] 73 | for group in self.controlGroups: 74 | field = group["field"] 75 | button = group["button"] 76 | button.getNSButton().setAlignment_(NSLeftTextAlignment) 77 | 78 | # if self.glyph.bounds is None: 79 | # self.setFirstResponder(self.w.widthField) 80 | # else: 81 | # self.setFirstResponder(self.w.leftField) 82 | # leftField = self.w.leftField.getNSTextField() 83 | # rightField = self.w.rightField.getNSTextField() 84 | # leftField.setNextKeyView_(rightField) 85 | # rightField.setNextKeyView_(leftField) 86 | 87 | self.w.line = vanilla.HorizontalLine("auto") 88 | self.w.doneButton = vanilla.Button("auto", "Close", callback=self.doneCallback) 89 | self.w.doneButton.bind(escapeCharacter, []) 90 | 91 | self.w.addAutoPosSizeRules(rules, metrics) 92 | 93 | self.w.open() 94 | 95 | def fieldCallback(self, sender): 96 | pass 97 | 98 | def buttonCallback(self, sender): 99 | pass 100 | 101 | def doneCallback(self, sender): 102 | pass 103 | 104 | GlyphEditorSpaceStationController() -------------------------------------------------------------------------------- /build/Space Station.roboFontExt/license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Type Supply LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /develop.py: -------------------------------------------------------------------------------- 1 | from AppKit import * 2 | import os 3 | import subprocess 4 | import time 5 | import shutil 6 | 7 | robofont = "/usr/local/bin/robofont" 8 | workspace = NSWorkspace.sharedWorkspace() 9 | directory = os.path.dirname(__file__) 10 | buildDirectory = os.path.join(directory, "build") 11 | buildCompletedMarkerPath = os.path.join(buildDirectory, "build completed") 12 | openDocumentsRecordPath = os.path.join(buildDirectory, "open documents") 13 | 14 | if os.path.exists(buildCompletedMarkerPath): 15 | os.remove(buildCompletedMarkerPath) 16 | if os.path.exists(openDocumentsRecordPath): 17 | os.remove(openDocumentsRecordPath) 18 | 19 | # Make sure RoboFont is open. 20 | 21 | haveRoboFont = NSRunningApplication.runningApplicationsWithBundleIdentifier_("com.typemytype.robofont3") 22 | 23 | if not haveRoboFont: 24 | print("Launching RoboFont...") 25 | 26 | workspace = NSWorkspace.sharedWorkspace() 27 | workspace.launchApplication_("RoboFont") 28 | time.sleep(5) 29 | 30 | # Build the extension. 31 | 32 | print("Building extension...") 33 | 34 | buildScriptPath = os.path.join(directory, "build.py") 35 | subprocess.call([robofont, "-p", buildScriptPath]) 36 | 37 | buildCompletedMarkerCode = """ 38 | f = open("{buildCompletedMarkerPath}", "w") 39 | f.write("") 40 | f.close() 41 | """ 42 | 43 | buildCompletedMarkerCode = buildCompletedMarkerCode.format( 44 | buildCompletedMarkerPath=buildCompletedMarkerPath 45 | ) 46 | 47 | success = False 48 | subprocess.call([robofont, "-c", buildCompletedMarkerCode]) 49 | 50 | maxWaitTime = 5 51 | startTime = time.time() 52 | while 1: 53 | if os.path.exists(buildCompletedMarkerPath): 54 | os.remove(buildCompletedMarkerPath) 55 | success = True 56 | break 57 | if time.time() - startTime > maxWaitTime: 58 | print("Build execution timed out! Check output in RoboFont to see if there were errors.") 59 | break 60 | 61 | if success: 62 | print("Built and installed.") 63 | 64 | # Get open documents. 65 | 66 | openDocumentsRecordCode = """ 67 | from AppKit import * 68 | openDocumentPaths = [] 69 | for document in NSApp().orderedDocuments(): 70 | url = document.fileURL() 71 | if url is not None: 72 | openDocumentPaths.append(url.path()) 73 | openDocumentPaths = "\\n".join(openDocumentPaths) 74 | f = open("{openDocumentsRecordPath}", "w") 75 | f.write(openDocumentPaths) 76 | f.close() 77 | """ 78 | 79 | if success: 80 | print("Gathering open paths...") 81 | 82 | openDocumentsRecordCode = openDocumentsRecordCode.format( 83 | openDocumentsRecordPath=openDocumentsRecordPath 84 | ) 85 | 86 | success = False 87 | subprocess.call([robofont, "-c", openDocumentsRecordCode]) 88 | 89 | maxWaitTime = 5 90 | startTime = time.time() 91 | while 1: 92 | if os.path.exists(openDocumentsRecordPath): 93 | success = True 94 | break 95 | if time.time() - startTime > maxWaitTime: 96 | print("Failed gathering open documents.") 97 | break 98 | 99 | # Reboot RoboFont. 100 | 101 | if success: 102 | print("Rebooting RoboFont...") 103 | 104 | f = open(openDocumentsRecordPath, "r") 105 | openDocumentPaths = f.read().splitlines() 106 | os.remove(openDocumentsRecordPath) 107 | 108 | # Shut down. 109 | NSRunningApplication.runningApplicationsWithBundleIdentifier_("com.typemytype.robofont3")[0].forceTerminate() 110 | 111 | # Launch. 112 | maxWaitTime = 5 113 | startTime = time.time() 114 | while 1: 115 | workspace = NSWorkspace.sharedWorkspace() 116 | success = workspace.launchApplication_("RoboFont") 117 | if not success: 118 | time.sleep(0.5) 119 | else: 120 | break 121 | if time.time() - startTime > maxWaitTime: 122 | print("Failed to open RoboFont.") 123 | break 124 | 125 | # Reopen the documents. 126 | if openDocumentPaths: 127 | for path in openDocumentPaths: 128 | print("Opening " + path + "...") 129 | workspace.openFile_withApplication_(path, "RoboFont") 130 | 131 | print("RoboFont has been restored.") 132 | 133 | print("Done.") -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Type Supply LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Space Station 2 | 3 | A system for specifying glyph spacing values as formulas containing references to other glyphs. It is a work in progress. 4 | 5 | ## Formulas 6 | 7 | A formula is a line of text defingin how a spacing value should be calculated. The formulas may contain the following parts: 8 | 9 | - static numbers 10 | - references to glyphs 11 | - math symbols 12 | 13 | The formula will be converted to a Python expression and evaluated to calculate the final value. 14 | 15 | ### Static Numbers 16 | 17 | Numbers are numbers. 18 | 19 | - `10` 20 | - `10.01` 21 | - `-10` 22 | - `-10.01` 23 | 24 | ### References to Glyphs 25 | 26 | Glyphs are referenced by name. The name will reference the glyph with the given name in the same layer as the target glyph. 27 | 28 | - `S` 29 | - `hyphen` 30 | - `space` 31 | 32 | A glyph name may have a symbol attached to it to indicate which metric value should be used. These are mapped to the following fontParts attributes: 33 | 34 | Symbol | fontParts Attribute 35 | -------- | -------------------------------------------------------- 36 | `@left` | `glyph.leftMargin` or `glyph.angledLeftMargin` 37 | `@right` | `glyph.rightMargin` or `glyph.angledRightMargin` 38 | `@width` | `glyph.width` 39 | 40 | - `parenleft@right` 41 | - `space.tab@width` 42 | 43 | If no symbol is attached to a glyp name, the metric being set is implied. 44 | 45 | Metric | Implied Symbol 46 | ------------ | -------------- 47 | left margin | `@left` 48 | right margin | `@right` 49 | width | `@width` 50 | 51 | If you want a glyph to reference itself, for example to make the right margin equal the left margin, enter the symbols without a glyph name. 52 | 53 | - `@left` 54 | 55 | ### Math Symbols 56 | 57 | The basic Python math symbols are allowed in formulas: 58 | 59 | - `+` 60 | - `-` 61 | - `*` 62 | - `/` 63 | - `(` 64 | - `)` 65 | 66 | ## Glyph Editor 67 | 68 | 69 | ## Apply To Font 70 | 71 | 72 | ## Import/Export 73 | 74 | 75 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typesupply/spacestation/a05d9833907525c89d9ce9db140dbbf84739bfeb/requirements.txt -------------------------------------------------------------------------------- /source/code/main.py: -------------------------------------------------------------------------------- 1 | from AppKit import NSApp 2 | from spaceStation.controller import _SpaceStationController 3 | 4 | if __name__ == "__main__": 5 | NSApp().SpaceStationController = _SpaceStationController() 6 | -------------------------------------------------------------------------------- /source/code/menu_fontEditorSpaceStation.py: -------------------------------------------------------------------------------- 1 | from spaceStation import SpaceStationController 2 | 3 | SpaceStationController().showFontEditorSpaceStation() -------------------------------------------------------------------------------- /source/code/menu_glyphEditorSpaceStation.py: -------------------------------------------------------------------------------- 1 | from spaceStation import SpaceStationController 2 | 3 | SpaceStationController().showGlyphEditorSpaceStation() -------------------------------------------------------------------------------- /source/code/spaceStation/__init__.py: -------------------------------------------------------------------------------- 1 | from AppKit import NSApp 2 | 3 | class SpaceStationError(Exception): pass 4 | 5 | def SpaceStationController(): 6 | return NSApp().SpaceStationController -------------------------------------------------------------------------------- /source/code/spaceStation/auto.py: -------------------------------------------------------------------------------- 1 | from spaceStation.formulas import getFormula, calculateFormula,\ 2 | getReferencesInFormula, splitReference,\ 3 | setMetricValue 4 | from spaceStation import SpaceStationError 5 | 6 | # ----------- 7 | # Application 8 | # ----------- 9 | 10 | def applyFormulasInLayer(layer, onlyGlyphNames=None, onlyAttr=None): 11 | if onlyGlyphNames is None: 12 | onlyGlyphNames = layers.keys() 13 | sequence = getResolutionSequence(layer) 14 | for group in sequence: 15 | for reference in group: 16 | glyphName, attr = splitReference(reference) 17 | if glyphName not in onlyGlyphNames: 18 | continue 19 | if onlyAttr is not None and attr != onlyAttr: 20 | continue 21 | glyph = layer[glyphName] 22 | formula = getFormula(glyph, attr) 23 | if formula: 24 | value = calculateFormula(glyph, formula, attr) 25 | setMetricValue(glyph, attr, value) 26 | 27 | # --------------------- 28 | # Resolution Sequencing 29 | # --------------------- 30 | 31 | maximumReferenceDepth = 20 32 | 33 | def getResolutionSequence(layer): 34 | glyphNames = set() 35 | for glyphName in layer.keys(): 36 | glyph = layer[glyphName] 37 | for attr in "leftMargin rightMargin width".split(" "): 38 | formula = getFormula(glyph, attr) 39 | if formula: 40 | if attr == "leftMargin": 41 | a = "@left" 42 | elif attr == "rightMargin": 43 | a = "@right" 44 | else: 45 | a = "@width" 46 | glyphNames.add(glyphName + a) 47 | glyphNames |= getReferencesInFormula(formula, attr) 48 | sequence = [glyphNames] 49 | for i in range(maximumReferenceDepth): 50 | glyphNames = getGlyphsNeedingCalculation(layer, glyphNames) 51 | if not glyphNames: 52 | break 53 | else: 54 | sequence.append(glyphNames) 55 | if glyphNames: 56 | error = "Maximum reference depth exceeded. Could there be a circular reference among these glyphs?: %s" % repr(glyphNames) 57 | raise SpaceStationError(error) 58 | compressed = [] 59 | previous = None 60 | for glyphNames in reversed(sequence): 61 | if previous is not None: 62 | glyphNames = [glyphName for glyphName in glyphNames if glyphName not in previous] 63 | compressed.append(glyphNames) 64 | previous = glyphNames 65 | return compressed 66 | 67 | def getGlyphsNeedingCalculation(layer, glyphNames): 68 | found = set() 69 | for reference in glyphNames: 70 | glyphName, attr = splitReference(reference) 71 | if glyphName not in layer: 72 | continue 73 | glyph = layer[glyphName] 74 | formula = getFormula(glyph, attr) 75 | if formula: 76 | references = getReferencesInFormula(formula, attr) 77 | if references: 78 | found |= references 79 | return found 80 | 81 | def getDependenciesForGlyph(glyph, attr): 82 | formula = getFormula(glyph, attr) 83 | references = getReferencesInFormula(formula, attr) 84 | return references 85 | -------------------------------------------------------------------------------- /source/code/spaceStation/controller.py: -------------------------------------------------------------------------------- 1 | from AppKit import NSBeep 2 | from fontParts.world import CurrentGlyph, CurrentFont 3 | from .glyphEditorWindow import GlyphEditorSpaceStationController 4 | from .fontEditorWindow import FontEditorSpaceStationController 5 | 6 | controllerIdentifier = "com.typesupply.SpaceStation" 7 | 8 | 9 | class _SpaceStationController(object): 10 | 11 | identifier = controllerIdentifier 12 | 13 | def showGlyphEditorSpaceStation(self): 14 | glyph = CurrentGlyph() 15 | if glyph is not None: 16 | GlyphEditorSpaceStationController(glyph) 17 | else: 18 | NSBeep() 19 | 20 | def showFontEditorSpaceStation(self): 21 | font = CurrentFont() 22 | if font is not None: 23 | FontEditorSpaceStationController(font.defaultLayer) 24 | else: 25 | NSBeep() 26 | -------------------------------------------------------------------------------- /source/code/spaceStation/fontEditorWindow.py: -------------------------------------------------------------------------------- 1 | import os 2 | import fnmatch 3 | import AppKit 4 | import vanilla 5 | from mojo.UI import CurrentFontWindow 6 | from spaceStation import formulas 7 | from spaceStation import auto 8 | from spaceStation import tools 9 | 10 | class FontEditorSpaceStationController(object): 11 | 12 | def __init__(self, layer): 13 | self.layer = layer 14 | self.font = layer.font 15 | 16 | self.w = vanilla.Sheet( 17 | (946, 500), 18 | minSize=(946, 500), 19 | maxSize=(946, 50000), 20 | parentWindow=CurrentFontWindow().w 21 | ) 22 | 23 | columnDescriptions = [ 24 | dict( 25 | key="name", 26 | title="Name", 27 | editable=False 28 | ), 29 | dict( 30 | key="left", 31 | title="Left", 32 | editable=True 33 | ), 34 | dict( 35 | key="right", 36 | title="Right", 37 | editable=True 38 | ), 39 | dict( 40 | key="width", 41 | title="Width", 42 | editable=True 43 | ) 44 | ] 45 | self.w.list = vanilla.List( 46 | "auto", 47 | [], 48 | columnDescriptions=columnDescriptions, 49 | drawFocusRing=False, 50 | editCallback=self.listEditCallback 51 | ) 52 | 53 | self.w.filterSearchBox = vanilla.SearchBox( 54 | "auto", 55 | callback=self.populateList 56 | ) 57 | self.w.prioritizeProblemsCheckBox = vanilla.CheckBox( 58 | "auto", 59 | "Prioritize Problems", 60 | value=True, 61 | callback=self.populateList 62 | ) 63 | 64 | self.w.line = vanilla.HorizontalLine("auto") 65 | 66 | self.w.updateAllButton = vanilla.ImageButton( 67 | "auto", 68 | imageNamed=AppKit.NSImageNameRefreshTemplate, 69 | bordered=False, 70 | callback=self.updateButtonCallback 71 | ) 72 | self.w.clearAllButton = vanilla.ImageButton( 73 | "auto", 74 | imageNamed=AppKit.NSImageNameStopProgressTemplate, 75 | bordered=False, 76 | callback=self.clearButtonCallback 77 | ) 78 | 79 | self.w.updateLeftButton = vanilla.ImageButton( 80 | "auto", 81 | imageNamed=AppKit.NSImageNameRefreshTemplate, 82 | bordered=False, 83 | callback=self.updateButtonCallback 84 | ) 85 | self.w.clearLeftButton = vanilla.ImageButton( 86 | "auto", 87 | imageNamed=AppKit.NSImageNameStopProgressTemplate, 88 | bordered=False, 89 | callback=self.clearButtonCallback 90 | ) 91 | 92 | self.w.updateRightButton = vanilla.ImageButton( 93 | "auto", 94 | imageNamed=AppKit.NSImageNameRefreshTemplate, 95 | bordered=False, 96 | callback=self.updateButtonCallback 97 | ) 98 | self.w.clearRightButton = vanilla.ImageButton( 99 | "auto", 100 | imageNamed=AppKit.NSImageNameStopProgressTemplate, 101 | bordered=False, 102 | callback=self.clearButtonCallback 103 | ) 104 | 105 | self.w.updateWidthButton = vanilla.ImageButton( 106 | "auto", 107 | imageNamed=AppKit.NSImageNameRefreshTemplate, 108 | bordered=False, 109 | callback=self.updateButtonCallback 110 | ) 111 | self.w.clearWidthButton = vanilla.ImageButton( 112 | "auto", 113 | imageNamed=AppKit.NSImageNameStopProgressTemplate, 114 | bordered=False, 115 | callback=self.clearButtonCallback 116 | ) 117 | 118 | self.w.importButton = vanilla.Button( 119 | "auto", 120 | "Import", 121 | callback=self.importButtonCallback 122 | ) 123 | self.w.exportButton = vanilla.Button( 124 | "auto", 125 | "Export", 126 | callback=self.exportButtonCallback 127 | ) 128 | self.w.closeButton = vanilla.Button( 129 | "auto", 130 | "Close", 131 | callback=self.closeButtonCallback 132 | ) 133 | 134 | metrics = dict( 135 | margin=15, 136 | spacing=10, 137 | padding=5, 138 | imageButton=20, 139 | button=100, 140 | column=225, 141 | column1=15, 142 | column2=240, 143 | column3=465, 144 | column4=690 145 | ) 146 | rules = [ 147 | "H:|-margin-[filterSearchBox(==215)]-spacing-[prioritizeProblemsCheckBox]", 148 | "H:|-margin-[line]-margin-|", 149 | "H:|-column1-[updateAllButton(==imageButton)]-padding-[clearAllButton(==imageButton)]", 150 | "H:|-column2-[updateLeftButton(==imageButton)]-padding-[clearLeftButton(==imageButton)]", 151 | "H:|-column3-[updateRightButton(==imageButton)]-padding-[clearRightButton(==imageButton)]", 152 | "H:|-column4-[updateWidthButton(==imageButton)]-padding-[clearWidthButton(==imageButton)]", 153 | "H:|-margin-[list]-margin-|", 154 | "H:|-margin-[importButton(==button)]-spacing-[exportButton(==button)]", 155 | "H:[closeButton(==button)]-margin-|", 156 | "V:|" 157 | "-margin-" 158 | "[filterSearchBox]" 159 | "-spacing-" 160 | "[line]" 161 | "-spacing-" 162 | "[updateAllButton(==imageButton)]" 163 | "-padding-" 164 | "[list]" 165 | "-spacing-" 166 | "[importButton]" 167 | "-margin-" 168 | "|", 169 | "V:|" 170 | "-margin-" 171 | "[prioritizeProblemsCheckBox(==filterSearchBox)]", 172 | "V:" 173 | "[line]" 174 | "-spacing-" 175 | "[clearAllButton(==imageButton)]", 176 | "V:" 177 | "[line]" 178 | "-spacing-" 179 | "[updateLeftButton(==imageButton)]", 180 | "V:" 181 | "[line]" 182 | "-spacing-" 183 | "[clearLeftButton(==imageButton)]", 184 | "V:" 185 | "[line]" 186 | "-spacing-" 187 | "[updateRightButton(==imageButton)]", 188 | "V:" 189 | "[line]" 190 | "-spacing-" 191 | "[clearRightButton(==imageButton)]", 192 | "V:" 193 | "[line]" 194 | "-spacing-" 195 | "[updateWidthButton(==imageButton)]", 196 | "V:" 197 | "[line]" 198 | "-spacing-" 199 | "[clearWidthButton(==imageButton)]", 200 | "V:" 201 | "[list]" 202 | "-spacing-" 203 | "[exportButton]", 204 | "V:" 205 | "[list]" 206 | "-spacing-" 207 | "[closeButton]", 208 | ] 209 | 210 | self.w.addAutoPosSizeRules(rules, metrics) 211 | self.populateList() 212 | self.w.open() 213 | 214 | def closeButtonCallback(self, sender): 215 | self.w.close() 216 | 217 | def populateList(self, sender=None): 218 | searchPattern = self.w.filterSearchBox.get() 219 | glyphNames = [name for name in self.font.glyphOrder if name in self.layer] 220 | for glyphName in sorted(self.layer.keys()): 221 | if glyphName not in glyphNames: 222 | glyphNames.append(gyphName) 223 | if searchPattern: 224 | glyphNames = [ 225 | glyphName 226 | for glyphName in glyphNames 227 | if fnmatch.fnmatchcase(glyphName, searchPattern) 228 | ] 229 | if self.w.prioritizeProblemsCheckBox.get(): 230 | problems = [] 231 | noProblems = [] 232 | for glyphName in glyphNames: 233 | data = self.getDataForGlyphName(glyphName) 234 | if data["leftHasProblem"] or data["rightHasProblem"] or data["widthHasProblem"]: 235 | problems.append(data) 236 | else: 237 | noProblems.append(data) 238 | data = problems + noProblems 239 | else: 240 | data = [ 241 | self.getDataForGlyphName(glyphName) 242 | for glyphName in glyphNames 243 | ] 244 | self._inInternalDataUpdate = True 245 | self.w.list.set(data) 246 | self._inInternalDataUpdate = False 247 | 248 | def updateAllData(self): 249 | self._inInternalDataUpdate = True 250 | for container in self.w.list: 251 | self.getDataForGlyphName(container["name"], container) 252 | self._inInternalDataUpdate = False 253 | 254 | def getDataForGlyphName(self, glyphName, container=None): 255 | if container is None: 256 | container = dict( 257 | name=glyphName, 258 | left=None, 259 | leftHasProblem=False, 260 | right=None, 261 | rightHasProblem=False, 262 | width=None, 263 | widthHasProblem=False 264 | ) 265 | glyph = self.layer[glyphName] 266 | left, leftHasProblem = visualizeFormula( 267 | glyph, 268 | "leftMargin", 269 | formulas.getFormula(glyph, "leftMargin") 270 | ) 271 | container["left"] = left 272 | container["leftHasProblem"] = leftHasProblem 273 | right, rightHasProblem = visualizeFormula( 274 | glyph, 275 | "rightMargin", 276 | formulas.getFormula(glyph, "rightMargin") 277 | ) 278 | container["right"] = right 279 | container["rightHasProblem"] = rightHasProblem 280 | width, widthHasProblem = visualizeFormula( 281 | glyph, 282 | "width", 283 | formulas.getFormula(glyph, "width") 284 | ) 285 | container["width"] = width 286 | container["widthHasProblem"] = widthHasProblem 287 | for k, v in container.items(): 288 | if v is None: 289 | container[k] = "" 290 | return container 291 | 292 | # ---- 293 | # Edit 294 | # ---- 295 | 296 | _inInternalDataUpdate = False 297 | 298 | def listEditCallback(self, sender): 299 | if self._inInternalDataUpdate: 300 | return 301 | selection = sender.getSelection()[0] 302 | container = sender[selection] 303 | glyphName = container["name"] 304 | glyph = self.layer[glyphName] 305 | left = container.get("left", "") 306 | if left: 307 | left = str(left) 308 | formulas.setFormula(glyph, "leftMargin", left) 309 | else: 310 | formulas.clearFormula(glyph, "leftMargin") 311 | right = container.get("right", "") 312 | if right: 313 | right = str(right) 314 | formulas.setFormula(glyph, "rightMargin", right) 315 | else: 316 | formulas.clearFormula(glyph, "rightMargin") 317 | width = container.get("width", "") 318 | if width: 319 | width = str(width) 320 | formulas.setFormula(glyph, "width", width) 321 | else: 322 | formulas.clearFormula(glyph, "width") 323 | self.updateAllData() 324 | 325 | # ------------ 326 | # Update/Clear 327 | # ------------ 328 | 329 | def updateButtonCallback(self, sender): 330 | attrMap = { 331 | self.w.updateAllButton : None, 332 | self.w.updateLeftButton : "leftMargin", 333 | self.w.updateRightButton : "rightMargin", 334 | self.w.updateWidthButton : "width" 335 | } 336 | attr = attrMap[sender] 337 | glyphNames = [self.w.list[i]["name"] for i in self.w.list.getSelection()] 338 | auto.applyFormulasInLayer( 339 | self.layer, 340 | onlyGlyphNames=glyphNames, 341 | onlyAttr=attr 342 | ) 343 | self.updateAllData() 344 | 345 | def clearButtonCallback(self, sender): 346 | attrMap = { 347 | self.w.clearAllButton : None, 348 | self.w.clearLeftButton : "leftMargin", 349 | self.w.clearRightButton : "rightMargin", 350 | self.w.clearWidthButton : "width" 351 | } 352 | attr = attrMap[sender] 353 | glyphNames = [self.w.list[i]["name"] for i in self.w.list.getSelection()] 354 | for glyphName in glyphNames: 355 | glyph = self.layer[glyphName] 356 | if attr is None: 357 | formulas.clearFormula(glyph, "leftMargin") 358 | formulas.clearFormula(glyph, "rightMargin") 359 | formulas.clearFormula(glyph, "width") 360 | else: 361 | formulas.clearFormula(glyph, attr) 362 | self.updateAllData() 363 | 364 | # ------------- 365 | # Import/Export 366 | # ------------- 367 | 368 | def importButtonCallback(self, sender): 369 | vanilla.dialogs.getFile( 370 | fileTypes=["spacestation"], 371 | resultCallback=self._importCallback, 372 | parentWindow=self.w 373 | ) 374 | 375 | def _importCallback(self, path): 376 | if not path: 377 | return 378 | for glyph in self.layer: 379 | formulas.clearFormula(glyph, "leftMargin") 380 | formulas.clearFormula(glyph, "rightMargin") 381 | formulas.clearFormula(glyph, "width") 382 | path = path[0] 383 | f = open(path, "r") 384 | text = f.read() 385 | f.close() 386 | formulas.layerFromString(self.layer, text) 387 | self.updateAllData() 388 | 389 | def exportButtonCallback(self, sender): 390 | directory = os.path.dirname(self.font.path) 391 | fileName = os.path.splitext(os.path.basename(self.font.path))[0] + ".spacestation" 392 | vanilla.dialogs.putFile( 393 | resultCallback=self._exportCallback, 394 | parentWindow=self.w, 395 | directory=directory, 396 | fileName=fileName 397 | ) 398 | 399 | def _exportCallback(self, path): 400 | if not path: 401 | return 402 | glyphOrder = list(self.font.glyphOrder) 403 | for name in sorted(self.layer.keys()): 404 | if name not in glyphOrder: 405 | glyphOrder.append(name) 406 | text = formulas.layerToString(self.layer, glyphOrder) 407 | f = open(path, "w") 408 | f.write(text) 409 | f.close() 410 | 411 | 412 | red = AppKit.NSColor.redColor() 413 | 414 | def visualizeFormula(glyph, attr, formula): 415 | if not formula: 416 | return formula, False 417 | calculatedValue = formulas.calculateFormula( 418 | glyph, 419 | formula, 420 | formulas.getAngledAttrIfNecessary(glyph.font, attr) 421 | ) 422 | needColor = False 423 | if calculatedValue is None: 424 | needColor = True 425 | else: 426 | value = formulas.getMetricValue(glyph, attr) 427 | if tools.roundint(value) != tools.roundint(calculatedValue): 428 | needColor = True 429 | if needColor: 430 | formula = AppKit.NSAttributedString.alloc().initWithString_attributes_( 431 | formula, 432 | {AppKit.NSForegroundColorAttributeName : red} 433 | ) 434 | return formula, needColor 435 | -------------------------------------------------------------------------------- /source/code/spaceStation/formulas.py: -------------------------------------------------------------------------------- 1 | from .tools import roundint 2 | 3 | # ------------- 4 | # Lib Shortcuts 5 | # ------------- 6 | 7 | formulaLibKeyStub = "com.typesupply.SpaceStation.formula." 8 | 9 | def getFormula(glyph, attr): 10 | """ 11 | Get the formula set in the glyph for the attr. 12 | """ 13 | return glyph.lib.get(formulaLibKeyStub + attr) 14 | 15 | def setFormula(glyph, attr, formula): 16 | """ 17 | Set the formula in the glyph for the attr. 18 | """ 19 | key = formulaLibKeyStub + attr 20 | if glyph.lib.get(key) == formula: 21 | return 22 | if formula is None: 23 | if key in glyph.lib: 24 | del glyph.lib[key] 25 | else: 26 | glyph.lib[key] = formula 27 | 28 | def clearFormula(glyph, attr=None): 29 | """ 30 | Clear the formula for the attr from the glyph. 31 | If no attr is given, all attrs will be cleared. 32 | """ 33 | if attr is not None: 34 | attrs = [attr] 35 | else: 36 | attrs = symbolToAttribute.values() 37 | for attr in attrs: 38 | key = formulaLibKeyStub + attr 39 | if key in glyph.lib: 40 | del glyph.lib[key] 41 | 42 | # -------- 43 | # Formulas 44 | # -------- 45 | 46 | mathSymbols = """ 47 | + 48 | - 49 | * 50 | / 51 | ( 52 | ) 53 | """.strip().splitlines() 54 | 55 | symbolToAttribute = { 56 | "@left" : "leftMargin", 57 | "@right" : "rightMargin", 58 | "@width" : "width" 59 | } 60 | 61 | attributeToSymbol = {} 62 | for symbol, attr in symbolToAttribute.items(): 63 | attributeToSymbol[attr] = symbol 64 | 65 | def splitFormula(formula): 66 | """ 67 | Split a formula into parts. 68 | """ 69 | formula = formula.strip() 70 | formula = formula.split("#", 1)[0] 71 | if not formula: 72 | return [] 73 | for symbol in mathSymbols: 74 | formula = formula.replace(symbol, " " + symbol + " ") 75 | formula = [i for i in formula.split(" ") if i] 76 | return formula 77 | 78 | def calculateFormula(glyph, formula, impliedAttr): 79 | """ 80 | Calculate the value of a formula. 81 | """ 82 | formula = splitFormula(formula) 83 | formula = _convertReferencesToNumbers(glyph, glyph.layer, formula, impliedAttr) 84 | if formula is None: 85 | return None 86 | value = _evaluateFormula(formula) 87 | return value 88 | 89 | def _evaluateFormula(formula): 90 | text = " ".join(formula) 91 | value = eval(text) 92 | return value 93 | 94 | # ---------- 95 | # References 96 | # ---------- 97 | 98 | def _convertReferencesToNumbers(glyph, layer, formula, impliedAttr="leftMargin"): 99 | expanded = [] 100 | for part in formula: 101 | if part in mathSymbols: 102 | expanded.append(part) 103 | else: 104 | value = 0 105 | try: 106 | v = float(part) 107 | value = v 108 | except ValueError: 109 | attr = impliedAttr 110 | for symbol, a in symbolToAttribute.items(): 111 | if part.endswith(symbol): 112 | attr = a 113 | part = part[:-len(symbol)] 114 | break 115 | if not part: 116 | part = glyph 117 | elif part not in layer: 118 | return None 119 | else: 120 | part = layer[part] 121 | value = getMetricValue(part, attr) 122 | expanded.append(str(value)) 123 | return expanded 124 | 125 | def getReferencesInFormula(formula, impliedAttr): 126 | """ 127 | Get glyphs referenced by a formula. 128 | """ 129 | formula = splitFormula(formula) 130 | references = set() 131 | for i in formula: 132 | if i in mathSymbols: 133 | continue 134 | if i in symbolToAttribute: 135 | continue 136 | try: 137 | float(i) 138 | continue 139 | except ValueError: 140 | pass 141 | foundSymbol = False 142 | for symbol in symbolToAttribute.keys(): 143 | if i.endswith(symbol): 144 | foundSymbol = True 145 | break 146 | if not foundSymbol: 147 | i += attributeToSymbol[impliedAttr] 148 | references.add(i) 149 | return references 150 | 151 | def splitReference(reference): 152 | """ 153 | Split a reference into a glyph name and attribute. 154 | """ 155 | for symbol, attr in symbolToAttribute.items(): 156 | if reference.endswith(symbol): 157 | reference = reference[:-len(symbol)] 158 | return reference, attr 159 | return reference, None 160 | 161 | # ----- 162 | # Tools 163 | # ----- 164 | 165 | def getMetricValue(glyph, attr): 166 | """ 167 | Get the metric value for an attribute. 168 | """ 169 | attr = getAngledAttrIfNecessary(glyph.font, attr) 170 | return getattr(glyph, attr) 171 | 172 | def setMetricValue(glyph, attr, value): 173 | """ 174 | Set the metric value for an attribute. 175 | """ 176 | attr = getAngledAttrIfNecessary(glyph.font, attr) 177 | value = roundint(value) 178 | setattr(glyph, attr, value) 179 | 180 | def getAngledAttrIfNecessary(font, attr): 181 | """ 182 | Coerce "leftMargin" or "rightMargin" to 183 | "angledLeftMargin" or "angledRightMargin" 184 | if the font is italic. 185 | """ 186 | useAngledMargins = font.info.italicAngle != 0 187 | if useAngledMargins: 188 | if attr == "leftMargin": 189 | attr = "angledLeftMargin" 190 | elif attr == "rightMargin": 191 | attr = "angledRightMargin" 192 | return attr 193 | 194 | # --- 195 | # I/O 196 | # --- 197 | 198 | syntax = """ 199 | # SYNTAX 200 | # ------ 201 | # # = comment. Anything after # will be ignored. 202 | # > = glyph name 203 | # L = left margin 204 | # R = right margin 205 | # W = width 206 | # 207 | # Empty lines have no meaning. 208 | """.strip() 209 | 210 | def layerToString(layer, glyphOrder=None): 211 | """ 212 | Write the formulas for all glyph in the layer to a string. 213 | """ 214 | if glyphOrder is None: 215 | glyphOrder = layer.font.glyphOrder 216 | text = [syntax + "\n\n\n"] 217 | for glyphName in glyphOrder: 218 | if glyphName not in layer: 219 | continue 220 | glyph = layer[glyphName] 221 | text.append(glyphToString(glyph)) 222 | return ("\n\n".join(text)) 223 | 224 | tokenToAttr = dict(L="leftMargin", R="rightMargin", W="width") 225 | tokenOrder = list("LRW") 226 | 227 | def glyphToString(glyph): 228 | """ 229 | Write the formulas defined for the glyph to a string. 230 | """ 231 | text = [ 232 | "> " + glyph.name 233 | ] 234 | for token in tokenOrder: 235 | attr = tokenToAttr[token] 236 | formula = getFormula(glyph, attr) 237 | line = token + " = " 238 | if not formula: 239 | line = "#" + line 240 | else: 241 | line += formula 242 | value = roundint(getMetricValue(glyph, attr)) 243 | calculated = calculateFormula(glyph, formula, attr) 244 | if calculated is not None: 245 | calculated = roundint(calculated) 246 | if value != calculated: 247 | line += " # value: {value} expected: {calculated}".format(value=value, calculated=calculated) 248 | text.append(line) 249 | return "\n".join(text) 250 | 251 | def layerFromString(layer, text): 252 | """ 253 | Load the formulas for all glyphs in the layer from the text. 254 | This does not apply the calculated formulas. 255 | """ 256 | glyphs = {} 257 | currentGlyph = None 258 | for line in text.splitlines(): 259 | line = line.split("#", 1)[0] 260 | line = line.strip() 261 | if not line: 262 | continue 263 | if line.startswith(">"): 264 | currentGlyph = line[1:].strip() 265 | glyphs[currentGlyph] = [] 266 | elif currentGlyph is not None: 267 | glyphs[currentGlyph].append(line) 268 | for glyphName, text in glyphs.items(): 269 | if glyphName not in layer: 270 | continue 271 | text = "\n".join(text) 272 | glyphFromString(layer[glyphName], text) 273 | 274 | def glyphFromString(glyph, text): 275 | """ 276 | Load the formulas for the glyph from the text. 277 | This does not apply the calculated formulas. 278 | """ 279 | clearFormula(glyph) 280 | for line in text.splitlines(): 281 | line = line.split("#", 1)[0] 282 | line = line.strip() 283 | if not line: 284 | continue 285 | if line.startswith(">"): 286 | continue 287 | if "=" not in line: 288 | continue 289 | token, formula = line.split("=", 1) 290 | token = token.strip() 291 | formula = formula.strip() 292 | if token not in tokenToAttr: 293 | continue 294 | attr = tokenToAttr[token] 295 | setFormula(glyph, attr, formula) 296 | -------------------------------------------------------------------------------- /source/code/spaceStation/glyphEditorWindow.py: -------------------------------------------------------------------------------- 1 | from AppKit import * 2 | import vanilla 3 | from mojo.roboFont import CurrentGlyph 4 | from mojo.UI import StatusInteractivePopUpWindow, CurrentGlyphWindow 5 | from .formulas import getFormula, setFormula, clearFormula, calculateFormula, getMetricValue, setMetricValue 6 | from .tools import roundint 7 | 8 | 9 | inSyncButtonColor = NSColor.blackColor() 10 | outSyncButtonColor = NSColor.redColor() 11 | 12 | escapeCharacter = "\x1B" 13 | 14 | 15 | class GlyphEditorSpaceStationController(object): 16 | 17 | def __init__(self, glyph): 18 | self.w = StatusInteractivePopUpWindow((250, 0), centerInView=CurrentGlyphWindow().getGlyphView()) 19 | 20 | metrics = dict( 21 | border=15, 22 | padding1=10, 23 | padding2=5, 24 | titleWidth=45, 25 | inputSpace=70, # border + title + padding 26 | killButtonWidth=20, 27 | navigateButtonWidth=30, 28 | fieldHeight=22, 29 | ) 30 | rules = [ 31 | # Left 32 | "H:|-border-[leftTitle(==titleWidth)]-padding1-[leftField]-border-|", 33 | "H:|-inputSpace-[leftButton]-padding2-[leftKillButton(==killButtonWidth)]-border-|", 34 | "V:|-border-[leftTitle(==fieldHeight)]", 35 | "V:|-border-[leftField(==fieldHeight)]", 36 | "V:[leftField]-padding2-[leftButton]", 37 | "V:[leftField]-padding2-[leftKillButton(==leftButton)]", 38 | 39 | # Right 40 | "H:|-border-[rightTitle(==titleWidth)]-padding1-[rightField]-border-|", 41 | "H:|-inputSpace-[rightButton]-padding2-[rightKillButton(==killButtonWidth)]-border-|", 42 | "V:[leftButton]-padding1-[rightTitle(==fieldHeight)]", 43 | "V:[leftButton]-padding1-[rightField(==fieldHeight)]", 44 | "V:[rightField]-padding2-[rightButton]", 45 | "V:[rightField]-padding2-[rightKillButton(==rightButton)]", 46 | 47 | # Width 48 | "H:|-border-[widthTitle(==titleWidth)]-padding1-[widthField]-border-|", 49 | "H:|-inputSpace-[widthButton]-padding2-[widthKillButton(==killButtonWidth)]-border-|", 50 | "V:[rightButton]-padding1-[widthTitle(==fieldHeight)]", 51 | "V:[rightButton]-padding1-[widthField(==fieldHeight)]", 52 | "V:[widthField]-padding2-[widthButton]", 53 | "V:[widthField]-padding2-[widthKillButton(==rightButton)]", 54 | 55 | # Bottom 56 | "H:|-inputSpace-[line]-border-|", 57 | "H:|-inputSpace-[previousGlyphButton(==navigateButtonWidth)]-padding2-[nextGlyphButton(==navigateButtonWidth)]-padding1-[doneButton(>=0)]-border-|", 58 | "V:[widthButton]-padding1-[line]", 59 | "V:[line]-padding1-[previousGlyphButton]-border-|", 60 | "V:[line]-padding1-[nextGlyphButton]-border-|", 61 | "V:[line]-padding1-[doneButton]-border-|", 62 | ] 63 | 64 | self.w.leftTitle = vanilla.TextBox("auto", "Left:", alignment="right") 65 | self.w.leftField = vanilla.EditText("auto", "", continuous=False, callback=self.fieldCallback) 66 | self.w.leftButton = vanilla.Button("auto", "", callback=self.buttonCallback) 67 | self.w.leftKillButton = vanilla.ImageButton("auto", imageNamed=NSImageNameStopProgressFreestandingTemplate, bordered=False, callback=self.buttonCallback) 68 | 69 | self.w.rightTitle = vanilla.TextBox("auto", "Right:", alignment="right") 70 | self.w.rightField = vanilla.EditText("auto", "", continuous=False, callback=self.fieldCallback) 71 | self.w.rightButton = vanilla.Button("auto", "", callback=self.buttonCallback) 72 | self.w.rightKillButton = vanilla.ImageButton("auto", imageNamed=NSImageNameStopProgressFreestandingTemplate, bordered=False, callback=self.buttonCallback) 73 | 74 | self.w.widthTitle = vanilla.TextBox("auto", "Width:", alignment="right") 75 | self.w.widthField = vanilla.EditText("auto", "", continuous=False, callback=self.fieldCallback) 76 | self.w.widthButton = vanilla.Button("auto", "", callback=self.buttonCallback) 77 | self.w.widthKillButton = vanilla.ImageButton("auto", imageNamed=NSImageNameStopProgressFreestandingTemplate, bordered=False, callback=self.buttonCallback) 78 | 79 | self.controlGroups = [ 80 | dict(attr="leftMargin", field=self.w.leftField, button=self.w.leftButton, kill=self.w.leftKillButton), 81 | dict(attr="rightMargin", field=self.w.rightField, button=self.w.rightButton, kill=self.w.rightKillButton), 82 | dict(attr="width", field=self.w.widthField, button=self.w.widthButton, kill=self.w.widthKillButton), 83 | ] 84 | for group in self.controlGroups: 85 | field = group["field"] 86 | button = group["button"] 87 | button.getNSButton().setAlignment_(NSLeftTextAlignment) 88 | 89 | self.w.line = vanilla.HorizontalLine("auto") 90 | self.w.doneButton = vanilla.Button("auto", "Close", callback=self.doneCallback) 91 | self.w.doneButton.bind(escapeCharacter, []) 92 | self.w.previousGlyphButton = vanilla.Button("auto", "←", callback=self.previousGlyphCallback) 93 | self.w.previousGlyphButton.bind("[", ["command"]) 94 | self.w.nextGlyphButton = vanilla.Button("auto", "→", callback=self.nextGlyphCallback) 95 | self.w.nextGlyphButton.bind("]", ["command"]) 96 | 97 | self.w.addAutoPosSizeRules(rules, metrics) 98 | 99 | self.loadGlyph() 100 | 101 | self.w.open() 102 | 103 | def setFirstResponder(self, control): 104 | self.w.getNSWindow().makeFirstResponder_(control.getNSTextField()) 105 | 106 | def _getControlGroup(self, sender): 107 | for group in self.controlGroups: 108 | field = group["field"] 109 | button = group["button"] 110 | kill = group["kill"] 111 | if sender == field: 112 | return group 113 | if sender == button: 114 | return group 115 | if sender == kill: 116 | return group 117 | 118 | def doneCallback(self, sender): 119 | self.w.close() 120 | 121 | # -------- 122 | # Updaters 123 | # -------- 124 | 125 | def loadGlyph(self): 126 | self._inGlyphLoad = True 127 | self.glyph = CurrentGlyph() 128 | if self.glyph.bounds is None: 129 | self.setFirstResponder(self.w.widthField) 130 | else: 131 | self.setFirstResponder(self.w.leftField) 132 | leftField = self.w.leftField.getNSTextField() 133 | rightField = self.w.rightField.getNSTextField() 134 | leftField.setNextKeyView_(rightField) 135 | rightField.setNextKeyView_(leftField) 136 | self._updateFields() 137 | self._updateButtons() 138 | self._inGlyphLoad = False 139 | 140 | def _updateFields(self): 141 | for group in self.controlGroups: 142 | attr = group["attr"] 143 | field = group["field"] 144 | if attr in ("leftMargin", "rightMargin") and self.glyph.bounds is None: 145 | value = "" 146 | else: 147 | value = getMetricValue(self.glyph, attr) 148 | value = roundint(value) 149 | field.set(value) 150 | 151 | def _updateButtons(self): 152 | for group in self.controlGroups: 153 | attr = group["attr"] 154 | button = group["button"] 155 | formula = getFormula(self.glyph, attr) 156 | if not formula: 157 | button.setTitle("") 158 | button.enable(False) 159 | continue 160 | calculatedValue = calculateFormula(self.glyph, formula, attr) 161 | value = getMetricValue(self.glyph, attr) 162 | if roundint(value) != roundint(calculatedValue): 163 | color = outSyncButtonColor 164 | else: 165 | color = inSyncButtonColor 166 | string = NSAttributedString.alloc().initWithString_attributes_(formula, {NSForegroundColorAttributeName : color}) 167 | button.setTitle(string) 168 | button.enable(True) 169 | 170 | # --------- 171 | # Callbacks 172 | # --------- 173 | 174 | def fieldCallback(self, sender): 175 | if self._inGlyphLoad: 176 | return 177 | group = self._getControlGroup(sender) 178 | attr = group["attr"] 179 | field = group["field"] 180 | button = group["button"] 181 | value = field.get().strip() 182 | if value.startswith("="): 183 | formula = value[1:] 184 | if not formula: 185 | NSBeep() 186 | return 187 | value = calculateFormula(self.glyph, formula, attr) 188 | if value is None: 189 | NSBeep() 190 | return 191 | field.set(str(roundint(value))) 192 | setFormula(self.glyph, attr, formula) 193 | else: 194 | try: 195 | value = int(value) 196 | except: 197 | NSBeep() 198 | return 199 | self.glyph.prepareUndo("Spacing Change") 200 | setMetricValue(self.glyph, attr, value) 201 | self.glyph.performUndo() 202 | self._updateFields() 203 | self._updateButtons() 204 | 205 | def buttonCallback(self, sender): 206 | group = self._getControlGroup(sender) 207 | attr = group["attr"] 208 | field = group["field"] 209 | button = group["button"] 210 | kill = group["kill"] 211 | if sender == kill: 212 | clearFormula(self.glyph, attr) 213 | else: 214 | formula = button.getTitle() 215 | value = calculateFormula(self.glyph, formula, attr) 216 | if value is None: 217 | NSBeep() 218 | return 219 | self.glyph.prepareUndo("Spacing Change") 220 | setMetricValue(self.glyph, attr, value) 221 | self.glyph.performUndo() 222 | self._updateFields() 223 | self._updateButtons() 224 | 225 | def previousGlyphCallback(self, sender): 226 | CurrentGlyphWindow().getGlyphView().previousGlyph_() 227 | self.loadGlyph() 228 | 229 | def nextGlyphCallback(self, sender): 230 | CurrentGlyphWindow().getGlyphView().nextGlyph_() 231 | self.loadGlyph() 232 | 233 | -------------------------------------------------------------------------------- /source/code/spaceStation/tools.py: -------------------------------------------------------------------------------- 1 | def roundint(number): 2 | return int(round(number)) 3 | -------------------------------------------------------------------------------- /source/code/spaceStation/window.py: -------------------------------------------------------------------------------- 1 | from AppKit import * 2 | import vanilla 3 | 4 | escapeCharacter = "$" 5 | 6 | class GlyphEditorSpaceStationController(object): 7 | 8 | def __init__(self): 9 | self.glyph = CurrentGlyph() 10 | 11 | self.w = vanilla.Window((300, 0), minSize=(100, 100)) 12 | 13 | metrics = dict( 14 | border=15, 15 | padding1=10, 16 | padding2=5, 17 | titleWidth=45, 18 | inputSpace=70, # border + title + padding 19 | killButtonWidth=20, 20 | fieldHeight=22, 21 | ) 22 | rules = [ 23 | # Left 24 | "H:|-border-[leftTitle(==titleWidth)]-padding1-[leftField]-border-|", 25 | "H:|-inputSpace-[leftButton]-padding2-[leftKillButton(==killButtonWidth)]-border-|", 26 | "V:|-border-[leftTitle(==fieldHeight)]", 27 | "V:|-border-[leftField(==fieldHeight)]", 28 | "V:[leftField]-padding2-[leftButton]", 29 | "V:[leftField]-padding2-[leftKillButton(==leftButton)]", 30 | 31 | # Right 32 | "H:|-border-[rightTitle(==titleWidth)]-padding1-[rightField]-border-|", 33 | "H:|-inputSpace-[rightButton]-padding2-[rightKillButton(==killButtonWidth)]-border-|", 34 | "V:[leftButton]-padding1-[rightTitle(==fieldHeight)]", 35 | "V:[leftButton]-padding1-[rightField(==fieldHeight)]", 36 | "V:[rightField]-padding2-[rightButton]", 37 | "V:[rightField]-padding2-[rightKillButton(==rightButton)]", 38 | 39 | # Width 40 | "H:|-border-[widthTitle(==titleWidth)]-padding1-[widthField]-border-|", 41 | "H:|-inputSpace-[widthButton]-padding2-[widthKillButton(==killButtonWidth)]-border-|", 42 | "V:[rightButton]-padding1-[widthTitle(==fieldHeight)]", 43 | "V:[rightButton]-padding1-[widthField(==fieldHeight)]", 44 | "V:[widthField]-padding2-[widthButton]", 45 | "V:[widthField]-padding2-[widthKillButton(==rightButton)]", 46 | 47 | # Bottom 48 | "H:|-inputSpace-[line]-border-|", 49 | "H:|-inputSpace-[doneButton]-border-|", 50 | "V:[widthButton]-padding1-[line]-padding1-[doneButton]-border-|", 51 | ] 52 | 53 | self.w.leftTitle = vanilla.TextBox("auto", "Left:", alignment="right") 54 | self.w.leftField = vanilla.EditText("auto", "", continuous=False, callback=self.fieldCallback) 55 | self.w.leftButton = vanilla.Button("auto", "", callback=self.buttonCallback) 56 | self.w.leftKillButton = vanilla.ImageButton("auto", imageNamed=NSImageNameStopProgressFreestandingTemplate, bordered=False, callback=self.buttonCallback) 57 | 58 | self.w.rightTitle = vanilla.TextBox("auto", "Right:", alignment="right") 59 | self.w.rightField = vanilla.EditText("auto", "", continuous=False, callback=self.fieldCallback) 60 | self.w.rightButton = vanilla.Button("auto", "", callback=self.buttonCallback) 61 | self.w.rightKillButton = vanilla.ImageButton("auto", imageNamed=NSImageNameStopProgressFreestandingTemplate, bordered=False, callback=self.buttonCallback) 62 | 63 | self.w.widthTitle = vanilla.TextBox("auto", "Width:", alignment="right") 64 | self.w.widthField = vanilla.EditText("auto", "", continuous=False, callback=self.fieldCallback) 65 | self.w.widthButton = vanilla.Button("auto", "", callback=self.buttonCallback) 66 | self.w.widthKillButton = vanilla.ImageButton("auto", imageNamed=NSImageNameStopProgressFreestandingTemplate, bordered=False, callback=self.buttonCallback) 67 | 68 | self.controlGroups = [ 69 | dict(attr="leftMargin", field=self.w.leftField, button=self.w.leftButton), 70 | dict(attr="rightMargin", field=self.w.rightField, button=self.w.rightButton), 71 | dict(attr="width", field=self.w.widthField, button=self.w.widthButton), 72 | ] 73 | for group in self.controlGroups: 74 | field = group["field"] 75 | button = group["button"] 76 | button.getNSButton().setAlignment_(NSLeftTextAlignment) 77 | 78 | # if self.glyph.bounds is None: 79 | # self.setFirstResponder(self.w.widthField) 80 | # else: 81 | # self.setFirstResponder(self.w.leftField) 82 | # leftField = self.w.leftField.getNSTextField() 83 | # rightField = self.w.rightField.getNSTextField() 84 | # leftField.setNextKeyView_(rightField) 85 | # rightField.setNextKeyView_(leftField) 86 | 87 | self.w.line = vanilla.HorizontalLine("auto") 88 | self.w.doneButton = vanilla.Button("auto", "Close", callback=self.doneCallback) 89 | self.w.doneButton.bind(escapeCharacter, []) 90 | 91 | self.w.addAutoPosSizeRules(rules, metrics) 92 | 93 | self.w.open() 94 | 95 | def fieldCallback(self, sender): 96 | pass 97 | 98 | def buttonCallback(self, sender): 99 | pass 100 | 101 | def doneCallback(self, sender): 102 | pass 103 | 104 | GlyphEditorSpaceStationController() -------------------------------------------------------------------------------- /to do.txt: -------------------------------------------------------------------------------- 1 | - selective import/export 2 | - use cmd, shift and option + arrow to make quick adjustments 3 | - add update dependents button (or maybe one each for L R W) 4 | - add indicator to glyph editor that shows if there is a link and if it is up to date 5 | - add indicator to glyph cells to indicate if there is a link and if it is up to date 6 | - HUD control with quick shortcuts to apply formula --------------------------------------------------------------------------------