Skip to content

Instantly share code, notes, and snippets.

@okay-type
Last active July 15, 2021 18:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save okay-type/86a9d86e1daf1b26f9c9236cdc612d06 to your computer and use it in GitHub Desktop.
Save okay-type/86a9d86e1daf1b26f9c9236cdc612d06 to your computer and use it in GitHub Desktop.
from mojo.UI import GetFolder
from glyphNameFormatter import GlyphName
import metricsMachine as mm
from vanilla import *
from vanilla.dialogs import putFile
from mojo.events import addObserver, removeObserver, postEvent
from AppKit import NSColor, NSFloatingWindowLevel, NSWindowStyleMaskClosable, NSFullSizeContentViewWindowMask, NSTitledWindowMask
from mojo.extensions import registerExtensionDefaults, setExtensionDefault, getExtensionDefault, removeExtensionDefault
from AppKit import NSApp
'''
A simple ui to make small tweaks to a metrics machine pairlist while kerning.
ok@yty.pe
How to use:
1: Open Metrics Machine window
2: Load a pairlist.txt
3: Run this script
4: Use the interface to edit the pairlist. Delete, insert, and reorder pairs
5: Save the edited pairlist when you're done
Known Issues:
- You can have duplicate items in a pairlist, but it can get messy. This tool tries to guess which pair you're targeting (see: closestIndex()) but your mileage may vary.
- The MM API prints the entire pairlist in SetPairList(). This is a bug, right? In the meantime, I just commented out that line out in the mmScripting.py file inside the MM extension.
Improvements To Make:
- shortcuts for bulk deleting pairs
(e.g.: ('H', '*') would delete every pair with an H on the left)
- detect when a new pairlist is loaded
(is this possible? I see a MetricsMachine.currentPairChanged event but that's it. Maybe we should load pairlists through this tool with a [Load] button.)
Version 0.6
- flip pair button is now two buttons:
↔️ flips left-right pairs
🔃 flips and mirrors the glyphs so '(' becomes ')'
-- see the dictionary below for the full list of pairs
both buttons also now set the flipped pair in the MM window (but don't change your list position)
Version 0.5
- Figured out how to throw focus back to the MM window after pressing a button
Version 0.4
- Fixed some big bugs (removeobserver works now, nonexisting pairs dont traceback, etc)
- Switched to normal vanilla window, but still floats
- Save/Load position as preference
Version 0.3
- Smaller title bar sized layout
Version 0.2
- Delete now mantains the selection in the pairlist
- Other little improvements
Version 0.1
- Initial version
'''
class mmPairlistTuner():
def __init__(self):
self.f = CurrentFont()
self.pairList = mm.GetPairList(font=self.f)
self.pairCurrent = mm.GetCurrentPair(font=self.f)
self.indexHistory = [0, 0]
intialindex = self.pairList.index(mm.GetCurrentPair())
if intialindex != None:
self.indexHistory = [intialindex, 0]
self.mmWindow = self.getTheMmUI(self.f)
self.UIpairList = self.mmWindow.delegate().vanillaWrapper().pairList
self.buildUI()
addObserver(self, 'pairChanged', 'MetricsMachine.currentPairChanged')
def getTheMmUI(self, font=None):
if font is None:
font = CurrentFont()
for other in AllFonts():
if other != font:
continue
document = other.document()
for controller in document.windowControllers():
window = controller.window()
if hasattr(window, "windowName") and window.windowName() == "MetricsMachineMainWindow":
return window
raise MetricsMachineScriptingError("A MetricsMachine window is not open for %r." % font)
def buildUI(self):
width = 70
shift = width
self.posSize = (0, 0, width*9+shift+35, 22)
windowname = 'Pairlist Tuner'
# DEBUG - close old versions of window
for window in [w for w in NSApp().orderedWindows() if w.isVisible()]:
if window.title() == windowname:
window.close()
self.w = Window(
self.posSize,
windowname,
fullSizeContentView=True,
titleVisible=False,
)
ns = self.w.getNSWindow()
ns.setLevel_(NSFloatingWindowLevel)
ns.setHidesOnDeactivate_(True)
i = 0
self.w.flip = Button((width*i+shift, 0, width/3, -0), '↔️', sizeStyle='small',callback=self.flipNewPair)
i += .3
self.w.mirror = Button((width*i+shift, 0, width/3, -0), '🔃', sizeStyle='small',callback=self.mirrorPair)
i += .7
self.w.left = EditText((width*i+shift, 5, width*2, 25), self.pairCurrent[0], sizeStyle='small')
i += 2
self.w.right = EditText((width*i+shift, 5, width*2, 25), self.pairCurrent[1], sizeStyle='small')
i += 2
self.w.delete = Button((width*i+shift, 0, width, -0), 'Delete', sizeStyle='small',callback=self.deleteCurrentPair)
i += 1
self.w.insert = Button((width*i+shift, 0, width, -0), 'Insert', sizeStyle='small',callback=self.insertNewPair)
i += 1
self.w.up = Button((width*i+shift, 0, width/2, -0), '⬆︎', sizeStyle='small',callback=self.movePairUp)
i += .5
self.w.down = Button((width*i+shift, 0, width/2, -0), '⬇︎', sizeStyle='small',callback=self.movePairDown)
i += 1
self.w.save = Button((width*i+shift, 0, width, -0), 'Save', sizeStyle='small',callback=self.save)
self.flatText(self.w.left)
self.flatText(self.w.right)
self.flatButton(self.w.flip)
self.flatButton(self.w.mirror)
self.loadPref()
self.w.bind('close', self.closeUI)
self.w.open()
def loadPref(self):
self.prefKey = 'com.okaytype.pairlisttuner'
# defaults
initialDefaults = {
self.prefKey + '.win.posSize': (0, 0, self.posSize[2], self.posSize[3]),
}
# clear saved values
# for v, p in initialDefaults.items():
# removeExtensionDefault(v)
registerExtensionDefaults(initialDefaults)
# load saved values
xywh = getExtensionDefault(self.prefKey + '.win.posSize')
self.w.setPosSize(xywh)
def closeUI(self, sender):
xywh = list(self.w.getPosSize())
setExtensionDefault(self.prefKey + '.win.posSize', xywh)
removeObserver(self, 'MetricsMachine.currentPairChanged')
def pairChanged(self, notification):
self.pairCurrent = notification['pair']
if self.pairCurrent in self.pairList:
self.indexHistory = [self.closestIndex(self.pairCurrent, self.indexHistory[0]), self.indexHistory[0]]
self.w.left.set(self.pairCurrent[0])
self.w.right.set(self.pairCurrent[1])
def deleteCurrentPair(self, sender):
index = self.closestIndex(self.pairCurrent, self.indexHistory[0])
# print('deleteCurrentPair', self.pairCurrent, index, self.indexHistory)
if index != None:
self.pairList.pop(index)
self.pairCurrent = cachepair = self.pairList[index]
self.updatePairlist()
self.UIpairList.setSelection(cachepair)
def insertNewPair(self, sender):
newPair = (self.w.left.get(), self.w.right.get())
index = self.closestIndex(self.pairCurrent, self.indexHistory[0])
if index != None:
self.pairList.insert(index+1, newPair);
self.pairCurrent = newPair
self.updatePairlist()
self.UIpairList.setSelection(newPair)
def movePairDown(self, sender):
index = self.closestIndex(self.pairCurrent, self.indexHistory[0])
if index != None:
self.pairList.insert(index + 1, self.pairList.pop(index))
self.updatePairlist()
def movePairUp(self, sender):
index = self.closestIndex(self.pairCurrent, self.indexHistory[0])
if index != None:
self.pairList.insert(index - 1, self.pairList.pop(index))
self.updatePairlist()
def flipNewPair(self, sender):
flippedPair = [self.w.right.get(), self.w.left.get()]
self.w.left.set(flippedPair[0])
self.w.right.set(flippedPair[1])
mm.SetCurrentPair(flippedPair, font=self.f)
def mirrorPair(self, sender):
flippedPair = [self.w.left.get(), self.w.right.get()]
for openclosepair in openclosepairs.items():
if flippedPair[0] == asGlyphName(openclosepair[0]):
flippedPair[0] = asGlyphName(openclosepair[1])
elif flippedPair[0] == asGlyphName(openclosepair[1]):
flippedPair[0] = asGlyphName(openclosepair[0])
if flippedPair[1] == asGlyphName(openclosepair[0]):
flippedPair[1] = asGlyphName(openclosepair[1])
elif flippedPair[1] == asGlyphName(openclosepair[1]):
flippedPair[1] = asGlyphName(openclosepair[0])
self.w.left.set(flippedPair[1])
self.w.right.set(flippedPair[0])
mm.SetCurrentPair(flippedPair, font=self.f)
def updatePairlist(self):
mm.SetPairList(self.pairList, self.f)
mm.SetCurrentPair(self.pairCurrent, font=self.f)
self.w.left.set(self.pairCurrent[0])
self.w.right.set(self.pairCurrent[1])
self.mmWindow.makeKeyAndOrderFront_(True)
def save(self, sender):
fname = 'kern-' + self.f.info.familyName + '-' + self.f.info.styleName
fname.replace(' ', '')
putFile(messageText="Save Pairlist:",
fileName=fname,
canCreateDirectories=True,
fileTypes=['txt'],
parentWindow=self.w,
resultCallback=self.finalizeSave
)
def closestIndex(self, pair, near):
indexes = [index for index, value in enumerate(self.pairList) if value == self.pairCurrent]
# print('indexes', indexes, pair, near, self.indexHistory)
if indexes != [] and near != None:
closestindex = indexes[min(range(len(indexes)), key = lambda i: abs(indexes[i]-near))]
return closestindex
else:
return None
def finalizeSave(self, path=None):
with open(path, 'w') as newfile:
newfile.write('#KPL:P: My Pair List' + '\n')
for keyglyphpair in self.pairList:
line = str(keyglyphpair[0]) + ' ' +str(keyglyphpair[1]) + '\n'
newfile.write(line)
def flatText(self, this):
ns = this.getNSTextField()
ns.setBezeled_(False)
ns.setBackgroundColor_(NSColor.clearColor())
ns.setFocusRingType_(1)
def flatButton(self, this):
ns = this.getNSButton()
ns.setBordered_(False)
def asGlyphName(character):
if len(character) > 1:
return character
else:
return GlyphName(ord(character)).getName()
openclosepairs = {
# initial/final punctuation (from https://www.compart.com/en/unicode/category/Pi and https://www.compart.com/en/unicode/category/Pf)
"‚": "‘",
"„": "“",
"„": "”",
"‘": "’",
"‛": "’",
"“": "”",
"‟": "”",
"‹": "›",
"›": "‹",
"«": "»",
"»": "«",
"⸂": "⸃",
"⸄": "⸅",
"⸉": "⸊",
"⸌": "⸍",
"⸜": "⸝",
"⸠": "⸡",
"”": "”",
"’": "’",
# Miscellaneous but common open/close pairs
"'": "'",
'"': '"',
# "¡": "!",
# "¿": "?",
"←": "→",
"→": "←",
# opening/closing punctuation (from https://www.compart.com/en/unicode/category/Ps & https://www.compart.com/en/unicode/category/Pe)
"(": ")",
"[": "]",
"{": "}",
"parenleft.sups": "parenright.sups",
"parenleft.subs": "parenright.subs",
"parenleft.uc": "parenright.uc",
"bracketleft.uc": "bracketright.uc",
"braceleft.uc": "braceright.uc",
"bracketangleleft.uc": "bracketangleright.uc",
"<": ">",
">": "<",
"༺": "༻", "༼": "༽", "᚛": "᚜", "‚": "‘", "„": "“", "⁅": "⁆", "⁽": "⁾", "₍": "₎", "⌈": "⌉", "⌊": "⌋", "〈": "〉", "❨": "❩", "❪": "❫", "❬": "❭", "❮": "❯", "❰": "❱", "❲": "❳", "❴": "❵", "⟅": "⟆", "⟦": "⟧", "⟨": "⟩", "⟪": "⟫", "⟬": "⟭", "⟮": "⟯", "⦃": "⦄", "⦅": "⦆", "⦇": "⦈", "⦉": "⦊", "⦋": "⦌", "⦍": "⦎", "⦏": "⦐", "⦑": "⦒", "⦓": "⦔", "⦕": "⦖", "⦗": "⦘", "⧘": "⧙", "⧚": "⧛", "⧼": "⧽", "⸢": "⸣", "⸤": "⸥", "⸦": "⸧", "⸨": "⸩", "〈": "〉", "《": "》", "「": "」", "『": "』", "【": "】", "〔": "〕", "〖": "〗", "〘": "〙", "〚": "〛", "〝": "〞", "⹂": "〟", "﴿": "﴾", "︗": "︘", "︵": "︶", "︷": "︸", "︹": "︺", "︻": "︼", "︽": "︾", "︿": "﹀", "﹁": "﹂", "﹃": "﹄", "﹇": "﹈", "﹙": "﹚", "﹛": "﹜", "﹝": "﹞", "(": ")", "[": "]", "{": "}", "⦅": "⦆", "「": "」",
}
mmPairlistTuner()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment