├── .gitignore ├── DragToKern.glyphsTool └── Contents │ ├── Info.plist │ ├── MacOS │ └── plugin │ └── Resources │ ├── plugin.py │ └── toolbar.pdf ├── LICENSE ├── README.md ├── media ├── DragToKern-Exception.gif ├── DragToKern-Spacing.gif ├── DragToKern.gif ├── KerningIconDisabledTemplate.png ├── KerningIconLockedTemplate.png ├── KerningIconTemplate.png └── screenshot.png └── resources └── toolbar.afdesign /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /DragToKern.glyphsTool/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | plugin 9 | CFBundleIdentifier 10 | com.lucasfonts.DragToKern 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | DragToKern 15 | CFBundleShortVersionString 16 | 0.7 17 | CFBundleVersion 18 | 7 19 | UpdateFeedURL 20 | https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/master/DragToKern.glyphsTool/Contents/Info.plist 21 | productPageURL 22 | https://github.com/LucasFonts/Glyphs-MoveableSidebearings 23 | productReleaseNotes 24 | Check out the Readme file for new features. 25 | NSHumanReadableCopyright 26 | Copyright 2022 by LucasFonts 27 | NSPrincipalClass 28 | DragToKern 29 | PyMainFileNames 30 | 31 | plugin.py 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /DragToKern.glyphsTool/Contents/MacOS/plugin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/22f2d25c2f0b3452b42e047edc8707abc5b29b3f/DragToKern.glyphsTool/Contents/MacOS/plugin -------------------------------------------------------------------------------- /DragToKern.glyphsTool/Contents/Resources/plugin.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from __future__ import division, print_function, unicode_literals 3 | 4 | import objc 5 | from objc import super 6 | 7 | from AppKit import ( 8 | NSBezierPath, 9 | NSClassFromString, 10 | NSColor, 11 | NSCursor, 12 | NSFont, 13 | NSFontAttributeName, 14 | NSFontWeightRegular, 15 | NSForegroundColorAttributeName, 16 | NSGradient, 17 | NSPoint, 18 | NSRect, 19 | NSString, 20 | ) 21 | from GlyphsApp import Glyphs, MOUSEMOVED 22 | 23 | try: 24 | from GlyphsApp import GSLTR as LTR 25 | except: 26 | from GlyphsApp import LTR 27 | from GlyphsApp.plugins import SelectTool 28 | 29 | GlyphsToolSelect = NSClassFromString("GlyphsToolSelect") 30 | 31 | SNAP_TOLERANCE = 14 32 | COLOR_R = 0.9 33 | COLOR_G = 0.1 34 | COLOR_B = 0.0 35 | COLOR_ALPHA = 0.5 36 | DRAGGING_HANDLE_HEIGHT = 30 37 | DRAGGING_HANDLE_WIDTH = 1 38 | LABEL_TEXT_SIZE = 11 39 | LABEL_DIST = 6 40 | LABEL_VERT_INNER_BIAS = 0.3 41 | 42 | 43 | if Glyphs.versionNumber < 3.0: 44 | 45 | def applyKerning(layer1, layer2, delta, step=1, direction=LTR): 46 | """ 47 | Apply the kerning difference to the given layer pair. 48 | """ 49 | value = layer2.leftKerningForLayer_(layer1) 50 | 51 | # Glyphs 2 returns "no kerning" as maxint 52 | if value is None or value > 0xFFFF: 53 | # Kern pair didn't exist, set the kerning to the delta value 54 | value = int(round(delta / step) * step) 55 | else: 56 | # Kern pair existed before, add the delta value 57 | value = int(round((value + delta) / step) * step) 58 | 59 | if direction == LTR: 60 | layer2.setLeftKerning_forLayer_(value, layer1) 61 | else: 62 | layer2.setLeftKerning_forLayer_(value, layer1) 63 | 64 | def handleException(composedLayers, layerIndex, c, direction=LTR): 65 | """ 66 | Add or remove an exception at the current location 67 | """ 68 | if layerIndex == 0 or layerIndex > 0xFFFF: 69 | return 70 | 71 | # Find out which layers should be get the exception 72 | layer1 = composedLayers[layerIndex - 1] 73 | layer2 = composedLayers[layerIndex] 74 | if layer2.master != layer1.master: 75 | # Can't add kerning between different masters 76 | return False 77 | 78 | if c == "d": 79 | # Both layers should get the exception 80 | layer1.setRightKerningExeption_forLayer_(True, layer2) 81 | layer2.setLeftKerningExeption_forLayer_(True, layer1) 82 | elif c == "a": 83 | # First layer should get exception 84 | layer1.setRightKerningExeption_forLayer_(True, layer2) 85 | elif c == "s": 86 | # First layer should get exception 87 | layer2.setLeftKerningExeption_forLayer_(True, layer1) 88 | elif c == "D": 89 | # Remove kerning exception for both layers 90 | layer1.setRightKerningExeption_forLayer_(False, layer2) 91 | layer2.setLeftKerningExeption_forLayer_(False, layer1) 92 | elif c == "A": 93 | # Remove kerning exception for first layer 94 | layer1.setRightKerningExeption_forLayer_(False, layer2) 95 | elif c == "S": 96 | # Remove kerning exception for second layer 97 | layer2.setLeftKerningExeption_forLayer_(False, layer1) 98 | else: 99 | return False 100 | return True 101 | 102 | else: 103 | 104 | def applyKerning(layer1, layer2, delta, step, direction=LTR): 105 | """ 106 | Apply the kerning difference to the given layer pair. 107 | """ 108 | value = layer2.previousKerningForLayer_direction_(layer1, direction) 109 | 110 | # Glyphs 3 returns "no kerning" as None 111 | if value is None or value > 0xFFFF: 112 | # Kern pair didn't exist, set the kerning to the delta value 113 | value = int(round(delta / step) * step) 114 | else: 115 | # Kern pair existed before, add the delta value 116 | value = int(round((value + delta) / step) * step) 117 | 118 | if direction == LTR: 119 | layer2.setPreviousKerning_forLayer_direction_( 120 | value, layer1, direction 121 | ) 122 | else: 123 | layer2.setPreviousKerning_forLayer_direction_( 124 | value, layer1, direction 125 | ) 126 | 127 | def handleException(composedLayers, layerIndex, c, direction=LTR): 128 | """ 129 | Add or remove an exception at the current location 130 | """ 131 | if layerIndex == 0 or layerIndex > 0xFFFF: 132 | return 133 | 134 | # Find out which layers should be get the exception 135 | layer1 = composedLayers[layerIndex - 1] 136 | layer2 = composedLayers[layerIndex] 137 | if layer2.master != layer1.master: 138 | # Can't add kerning between different masters 139 | return False 140 | 141 | if c == "d": 142 | # Both layers should get the exception 143 | layer1.setNextKerningExeption_forLayer_direction_( 144 | True, layer2, direction 145 | ) 146 | layer2.setPreviousKerningExeption_forLayer_direction_( 147 | True, layer1, direction 148 | ) 149 | elif c == "a": 150 | # First layer should get exception 151 | layer1.setNextKerningExeption_forLayer_direction_( 152 | True, layer2, direction 153 | ) 154 | elif c == "s": 155 | # First layer should get exception 156 | layer2.setPreviousKerningExeption_forLayer_direction_( 157 | True, layer1, direction 158 | ) 159 | elif c == "D": 160 | # Remove kerning exception for both layers 161 | layer1.setNextKerningExeption_forLayer_direction_( 162 | False, layer2, direction 163 | ) 164 | layer2.setPreviousKerningExeption_forLayer_direction_( 165 | False, layer1, direction 166 | ) 167 | elif c == "A": 168 | # Remove kerning exception for first layer 169 | layer1.setNextKerningExeption_forLayer_direction_( 170 | False, layer2, direction 171 | ) 172 | elif c == "S": 173 | # Remove kerning exception for second layer 174 | layer2.setPreviousKerningExeption_forLayer_direction_( 175 | False, layer1, direction 176 | ) 177 | else: 178 | return False 179 | return True 180 | 181 | 182 | class DragToKern(SelectTool): 183 | @objc.python_method 184 | def settings(self): 185 | self.name = Glyphs.localize( 186 | { 187 | "en": "Mouse Kerning and Spacing", 188 | "de": "Unterschneidung und Zurichtung per Maus", 189 | } 190 | ) 191 | self.keyboardShortcut = "k" 192 | self.stdCursor = NSCursor.resizeLeftRightCursor() 193 | self.lckCursor = NSCursor.operationNotAllowedCursor() 194 | self.cursor = self.stdCursor 195 | self.colorSBOuter = NSColor.colorWithCalibratedRed_green_blue_alpha_( 196 | COLOR_R, COLOR_G, COLOR_B, COLOR_ALPHA 197 | ) 198 | self.colorSBInner = NSColor.colorWithCalibratedRed_green_blue_alpha_( 199 | COLOR_R, COLOR_G, COLOR_B, 0.0 200 | ) 201 | self.colorLabel = NSColor.textColor() 202 | self.colorBox = NSColor.textBackgroundColor() 203 | 204 | def standardCursor(self): 205 | return self.cursor 206 | 207 | @objc.python_method 208 | def start(self): 209 | self.mode = None 210 | self.mouse_position = (0, 0) 211 | self.drag_start = None 212 | self.direction = LTR 213 | self.active_metric = None 214 | self.orig_value = None 215 | self.handle_x = None 216 | self.width = None 217 | self.layer1 = None 218 | self.layer2 = None 219 | self.drawMeasurements = Glyphs.defaults[ 220 | "com.lucasfonts.DragToKern.measurements" 221 | ] 222 | if self.drawMeasurements is None: 223 | self.drawMeasurements = False 224 | 225 | @objc.python_method 226 | def activate(self): 227 | Glyphs.addCallback(self.mouseDidMove, MOUSEMOVED) 228 | self.drawMeasurements = Glyphs.defaults[ 229 | "com.lucasfonts.DragToKern.measurements" 230 | ] 231 | 232 | @objc.python_method 233 | def deactivate(self): 234 | Glyphs.removeCallback(self.mouseDidMove, MOUSEMOVED) 235 | Glyphs.defaults[ 236 | "com.lucasfonts.DragToKern.measurements" 237 | ] = self.drawMeasurements 238 | 239 | @objc.python_method 240 | def conditionalContextMenus(self): 241 | if self.drawMeasurements: 242 | return [ 243 | { 244 | "name": Glyphs.localize( 245 | { 246 | "en": "Hide Measurements While Spacing", 247 | } 248 | ), 249 | "action": self.toggleMeasurements_, 250 | } 251 | ] 252 | return [ 253 | { 254 | "name": Glyphs.localize( 255 | { 256 | "en": "Show Measurements While Spacing", 257 | } 258 | ), 259 | "action": self.toggleMeasurements_, 260 | } 261 | ] 262 | 263 | def toggleMeasurements_(self, sender=None): 264 | self.drawMeasurements = not self.drawMeasurements 265 | 266 | @objc.python_method 267 | def doKerning(self, graphicView): 268 | return graphicView.doKerning() 269 | 270 | @objc.python_method 271 | def doSpacing(self, graphicView): 272 | return not graphicView.doKerning() and graphicView.doSpacing() 273 | 274 | def keyDown_(self, theEvent): 275 | c = theEvent.characters() 276 | if c in ("a", "s", "d", "A", "S", "D"): 277 | # Get the mouse location and convert it to local coordinates 278 | evc = self.editViewController() 279 | gv = evc.graphicView() 280 | loc = gv.convertPoint_fromView_(theEvent.locationInWindow(), None) 281 | # Which layer is at the mouse click location? 282 | layerIndex = gv.layerIndexForPoint_(loc) 283 | composedLayers = evc.composedLayers 284 | handleException(composedLayers, layerIndex, c, self.direction) 285 | return 286 | 287 | # Other keys are handled by the super class 288 | super().keyDown_(theEvent) 289 | 290 | @objc.python_method 291 | def mouseDidMove(self, notification): 292 | Glyphs.redraw() 293 | 294 | def mouseDown_(self, theEvent): 295 | """ 296 | Get the mouse down location to record the start coordinate and dragged 297 | layer. 298 | """ 299 | if theEvent.clickCount() == 2: 300 | wc = self.windowController() 301 | wc.setToolForClass_(GlyphsToolSelect) 302 | toolDelegate = wc.toolEventDelegate() 303 | if toolDelegate.respondsToSelector_("selectGlyph:"): 304 | toolDelegate.selectGlyph_(theEvent) 305 | return 306 | # Get the mouse click location and convert it to local coordinates 307 | evc = self.editViewController() 308 | gv = evc.graphicView() 309 | loc = gv.convertPoint_fromView_(theEvent.locationInWindow(), None) 310 | # Which layer is at the mouse click location? 311 | layerIndex = gv.layerIndexForPoint_(loc) 312 | # Note the start coordinates for later 313 | self.drag_start = loc 314 | # Note the kerning direction 315 | self.direction = evc.direction 316 | 317 | if layerIndex > 0xFFFF: 318 | # No layer (maxint) can't be modified 319 | self.setLockedCursor() 320 | self.cancel_operation() 321 | return 322 | 323 | # Collect some info about the clicked layer 324 | composedLayers = evc.composedLayers 325 | self.layer2 = composedLayers[layerIndex] 326 | layerOrigin = gv.cachedPositionAtIndex_(layerIndex) 327 | 328 | # What should be modified? Kerning, LSB, RSB, or both SBs? 329 | 330 | spacing = self.doSpacing(gv) 331 | kerning = self.doKerning(gv) 332 | if spacing: 333 | # Check if the click was at a sidebearing handle 334 | result = self.checkHandleLocation( 335 | loc, gv, self.layer2, layerOrigin 336 | ) 337 | 338 | if result is None: 339 | self.active_metric = None 340 | else: 341 | self.active_metric = result[0][0] 342 | 343 | if self.windowController().CommandKey(): 344 | self.mode = "move" 345 | elif self.active_metric == "LSB": 346 | self.mode = "LSB" 347 | self.orig_value = self.layer2.LSB 348 | elif self.active_metric == "RSB": 349 | self.mode = "RSB" 350 | self.orig_value = self.layer2.RSB 351 | elif kerning: 352 | if not self.setupKerning(composedLayers, layerIndex): 353 | return 354 | else: 355 | self.setLockedCursor() 356 | self.cancel_operation() 357 | return 358 | 359 | elif kerning: 360 | if not self.setupKerning(composedLayers, layerIndex): 361 | return 362 | 363 | if self.layer2 is not None: 364 | self.layer2.parent.beginUndo() 365 | Glyphs.redraw() 366 | 367 | @objc.python_method 368 | def setupKerning(self, composedLayers, layerIndex): 369 | # Kerning between two glyphs will be modified 370 | if layerIndex == 0: 371 | # First layer (0) can't be kerned 372 | self.setLockedCursor() 373 | self.cancel_operation() 374 | return False 375 | 376 | # Find out which layers should be kerned 377 | self.layer1 = composedLayers[layerIndex - 1] 378 | # self.layer2 = composedLayers[layerIndex] 379 | if self.layer2.master != self.layer1.master: 380 | # Can't add kerning between different masters 381 | self.setLockedCursor() 382 | self.cancel_operation() 383 | return False 384 | 385 | self.mode = "kern" 386 | return True 387 | 388 | def cancelOperation_(self, sender): 389 | wc = self.windowController() 390 | wc.setToolForClass_(GlyphsToolSelect) 391 | 392 | @objc.python_method 393 | def cancel_operation(self): 394 | self.layer1 = None 395 | self.layer2 = None 396 | self.drag_start = None 397 | self.orig_value = None 398 | 399 | @objc.python_method 400 | def setLockedCursor(self): 401 | # self.editViewController().contentView().enclosingScrollView().setDocumentCursor_(self.lckCursor) 402 | pass 403 | 404 | @objc.python_method 405 | def setStdCursor(self): 406 | # self.editViewController().contentView().enclosingScrollView().setDocumentCursor_(self.stdCursor) 407 | pass 408 | 409 | def mouseDragged_(self, theEvent): 410 | """ 411 | Update the kerning when the mouse is dragged and live update is on. 412 | """ 413 | if self.drag_start is None: 414 | return 415 | 416 | needsRedraw = self.handleDrag(theEvent) 417 | if needsRedraw: 418 | self.editViewController().forceRedraw() 419 | 420 | def mouseUp_(self, theEvent): 421 | """ 422 | End the undo and reset variables when the mouse is released 423 | """ 424 | if self.layer2 is not None: 425 | self.layer2.parent.endUndo() 426 | 427 | self.direction = LTR 428 | self.mode = None 429 | self.cancel_operation() 430 | self.setStdCursor() 431 | self.active_metric = None 432 | Glyphs.redraw() 433 | 434 | @objc.python_method 435 | def metricsAreLocked(self, layer): 436 | cp1 = "Link Metrics With First Master" 437 | cp2 = "Link Metrics With Master" 438 | if ( 439 | cp1 in layer.master.customParameters 440 | or cp2 in layer.master.customParameters 441 | ): 442 | return True 443 | return False 444 | 445 | @objc.python_method 446 | def handleDrag(self, theEvent): 447 | """ 448 | Get the current location while the mouse is dragging. Returns True if 449 | the view needs a redraw, i.e. the kerning or metrics were modified. 450 | """ 451 | if self.layer2 is None: 452 | return 453 | if self.drag_start is None: 454 | return 455 | 456 | evc = self.editViewController() 457 | gv = evc.graphicView() 458 | loc = gv.convertPoint_fromView_(theEvent.locationInWindow(), None) 459 | wc = self.windowController() 460 | 461 | # Alt key enables "precision dragging" 462 | if wc.AltKey(): 463 | mouseZoom = 0.1 464 | else: 465 | mouseZoom = 1 466 | 467 | # Shift key rounds to 10 468 | if wc.ShiftKey(): 469 | step = 10 470 | else: 471 | step = 1 472 | 473 | delta = (loc.x - self.drag_start.x) / evc.scale * mouseZoom 474 | 475 | self.drag_start = loc 476 | if delta != 0.0: 477 | # Only "move" can be applied for linked metrics 478 | if self.mode == "move": 479 | self.layer2.LSB += int(round(delta)) 480 | self.layer2.width -= int(round(delta)) 481 | return True 482 | 483 | if self.metricsAreLocked(self.layer2): 484 | return False 485 | 486 | if self.mode == "kern": 487 | applyKerning( 488 | self.layer1, self.layer2, delta, step, self.direction 489 | ) 490 | return False # Kerning changes already trigger a redraw 491 | 492 | if self.mode == "LSB": 493 | self.layer2.LSB += int(round(delta)) 494 | return True 495 | 496 | if self.mode == "RSB": 497 | self.layer2.RSB += int(round(delta)) 498 | return True 499 | 500 | return False 501 | 502 | def drawLayer_atPoint_asActive_attributes_( 503 | self, layer, layerOrigin, active, attributes 504 | ): 505 | gv = self.editViewController().graphicView() 506 | gv.drawLayer_atPoint_asActive_attributes_( 507 | layer, layerOrigin, active, attributes 508 | ) 509 | if not self.doSpacing(gv): 510 | # Not in spacing mode 511 | return 512 | 513 | if self.drag_start is None: 514 | result = self.checkHandles(gv, layer, layerOrigin) 515 | if result is not None: 516 | metric, handle_x, width = result 517 | self._drawHandle(handle_x, metric) 518 | elif self.drawMeasurements: 519 | self._drawDraggingMeasurements(self.mode, gv, layer, layerOrigin) 520 | 521 | def drawMetricsForLayer_atPoint_asActive_( 522 | self, layer, layerOrigin, active 523 | ): 524 | pass 525 | 526 | @objc.python_method 527 | def checkHandles(self, graphicView, layer, layerOrigin): 528 | """ 529 | Check if the mouse pointer is at a possible metrics handle location. 530 | Called on MOUSEMOVED via drawLayer_atPoint_asActive_attributes_. 531 | """ 532 | theEvent = Glyphs.currentEvent() 533 | if theEvent is None: 534 | return 535 | 536 | self.mouse_position = graphicView.convertPoint_fromView_( 537 | theEvent.locationInWindow(), None 538 | ) 539 | return self.checkHandleLocation( 540 | self.mouse_position, graphicView, layer, layerOrigin 541 | ) 542 | 543 | @objc.python_method 544 | def checkHandleLocation(self, location, graphicView, layer, layerOrigin): 545 | """ 546 | Check if the location of an event is at a possible metrics handle 547 | location. 548 | """ 549 | if not self.doSpacing(graphicView): 550 | return 551 | 552 | try: 553 | master = layer.master 554 | except KeyError: 555 | return 556 | 557 | x, y = location 558 | scale = graphicView.scale() 559 | desc = master.descender * scale 560 | asc = master.ascender * scale 561 | asc += layerOrigin.y 562 | desc += layerOrigin.y 563 | layerWidth = layer.width * scale 564 | 565 | # Don't draw handles outside ascender/descender 566 | if y < desc or y > asc: 567 | return 568 | 569 | offsetX = x - layerOrigin.x 570 | 571 | if offsetX < 0 or offsetX > layerWidth: 572 | # Mouse is outside the glyph 573 | return 574 | 575 | if offsetX > SNAP_TOLERANCE and offsetX < layerWidth - SNAP_TOLERANCE: 576 | # Mouse is too far inside the glyph 577 | return 578 | 579 | if offsetX < SNAP_TOLERANCE: 580 | handle_x = (layerOrigin.x, SNAP_TOLERANCE) 581 | metric = ( 582 | "LSB", 583 | layer.LSB, 584 | layer, 585 | desc, 586 | asc, 587 | ) 588 | width = layerOrigin.x 589 | else: 590 | handle_x = ( 591 | layerOrigin.x + layerWidth - SNAP_TOLERANCE, 592 | SNAP_TOLERANCE, 593 | ) 594 | metric = ( 595 | "RSB", 596 | layer.RSB, 597 | layer, 598 | desc, 599 | asc, 600 | ) 601 | width = layerOrigin.x + layerWidth 602 | return metric, handle_x, width 603 | 604 | @objc.python_method 605 | def _drawHandle(self, handle_x, metric): 606 | if handle_x is None: 607 | return 608 | if metric is None: 609 | return 610 | 611 | pos, w = handle_x 612 | metric_name, value, layer, desc, asc = metric 613 | gradient = NSGradient.alloc().initWithStartingColor_endingColor_( 614 | self.colorSBOuter, self.colorSBInner 615 | ) 616 | rect = NSRect( 617 | origin=(pos, desc), 618 | size=(w, asc - desc), 619 | ) 620 | angle = -180 if metric_name == "RSB" else 0 621 | bezierPath = NSBezierPath.bezierPathWithRect_(rect) 622 | gradient.drawInBezierPath_angle_(bezierPath, angle) 623 | 624 | @objc.python_method 625 | def _drawDraggingMeasurements( 626 | self, metric, graphicView, layer, layerOrigin 627 | ): 628 | if layer != self.layer2 or self.layer2 is None: 629 | # Only draw labels at the layer being modified 630 | return 631 | 632 | try: 633 | master = self.layer2.master 634 | except KeyError: 635 | return 636 | 637 | scale = graphicView.scale() 638 | desc = master.descender * scale 639 | asc = master.ascender * scale 640 | asc += layerOrigin.y 641 | desc += layerOrigin.y 642 | layerWidth = layer.width * scale 643 | locked = self.metricsAreLocked(self.layer2) 644 | 645 | if metric in ("LSB", "RSB", "move"): 646 | # Draw left and right 647 | x1 = layerOrigin.x - DRAGGING_HANDLE_WIDTH * 0.5 648 | x2 = layerOrigin.x + layerWidth - DRAGGING_HANDLE_WIDTH * 0.5 649 | self._drawDraggingTextLabel("LSB", x1, asc, locked) 650 | self._drawDraggingTextLabel("RSB", x2, asc, locked) 651 | pos = [x1, x2] 652 | elif metric == "kern": 653 | # FIXME: This code is never called 654 | # Draw left 655 | x = layerOrigin.x - DRAGGING_HANDLE_WIDTH * 0.5 656 | self._drawDraggingTextLabel("LSB", x, asc, locked) 657 | pos = [x] 658 | else: 659 | return 660 | 661 | self._drawDraggingMeasurement(pos, asc, desc) 662 | 663 | @objc.python_method 664 | def _drawDraggingMeasurement(self, xPositions, asc, desc): 665 | top = DRAGGING_HANDLE_HEIGHT * LABEL_VERT_INNER_BIAS 666 | bot = DRAGGING_HANDLE_HEIGHT - top 667 | for x in xPositions: 668 | bezierPath = NSBezierPath.bezierPathWithRect_( 669 | NSRect( 670 | origin=(x, desc - bot), 671 | size=(DRAGGING_HANDLE_WIDTH, DRAGGING_HANDLE_HEIGHT), 672 | ) 673 | ) 674 | bezierPath.appendBezierPathWithRect_( 675 | NSRect( 676 | origin=(x, asc - top), 677 | size=(DRAGGING_HANDLE_WIDTH, DRAGGING_HANDLE_HEIGHT), 678 | ) 679 | ) 680 | self.colorSBOuter.set() 681 | bezierPath.fill() 682 | 683 | @objc.python_method 684 | def _drawDraggingTextLabel(self, metric, xPosition, asc, locked): 685 | if locked: 686 | shown_value = "🔒︎" 687 | else: 688 | if metric == "LSB": 689 | shown_value = "%g" % self.layer2.LSB 690 | elif metric == "RSB": 691 | shown_value = "%g" % self.layer2.RSB 692 | else: 693 | return 694 | 695 | attrs = { 696 | NSFontAttributeName: NSFont.monospacedDigitSystemFontOfSize_weight_( 697 | LABEL_TEXT_SIZE, NSFontWeightRegular 698 | ), 699 | NSForegroundColorAttributeName: self.colorLabel, 700 | } 701 | myString = NSString.string().stringByAppendingString_(shown_value) 702 | bbox = myString.sizeWithAttributes_(attrs) 703 | bw = bbox.width 704 | bh = bbox.height 705 | text_pt = NSPoint() 706 | text_pt.y = ( 707 | asc 708 | + DRAGGING_HANDLE_HEIGHT 709 | - DRAGGING_HANDLE_HEIGHT * LABEL_VERT_INNER_BIAS 710 | - bh 711 | ) 712 | if metric == "LSB": 713 | text_pt.x = xPosition + LABEL_DIST 714 | elif metric == "RSB": 715 | text_pt.x = xPosition - LABEL_DIST - bw 716 | else: 717 | return 718 | 719 | rect = NSRect(origin=(text_pt.x, text_pt.y), size=(bw, bh)) 720 | outer = NSRect( 721 | origin=(text_pt.x - 2, text_pt.y - 1), size=(bw + 4, bh + 2) 722 | ) 723 | self.colorBox.set() 724 | NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_( 725 | outer, 4, 4 726 | ).fill() 727 | myString.drawInRect_withAttributes_(rect, attrs) 728 | 729 | @objc.python_method 730 | def __file__(self): 731 | """Please leave this method unchanged""" 732 | return __file__ 733 | -------------------------------------------------------------------------------- /DragToKern.glyphsTool/Contents/Resources/toolbar.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/22f2d25c2f0b3452b42e047edc8707abc5b29b3f/DragToKern.glyphsTool/Contents/Resources/toolbar.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 LucasFonts 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mouse Kerning and Spacing a.k.a. DragToKern 2 | 3 | Apply kerning and edit spacing by dragging glyphs with your mouse. 4 | 5 | _Mouse Kerning and Spacing_ is a tool plugin. Activate it by pressing the 6 | shortcut key _K_ or by clicking the toolbar icon that shows _kTd_. Double-click 7 | a glyph to go back to the _Select_ tool. 8 | 9 | The mode of operation is determined by the kerning/spacing icon in the Edit 10 | view. 11 | 12 | - ![](media/KerningIconTemplate.png) Kerning mode 13 | - ![](media/KerningIconDisabledTemplate.png) Spacing mode 14 | - ![](media/KerningIconLockedTemplate.png) Spacing is locked 15 | 16 | ## Kerning Mode 17 | 18 | Just drag any glyph in your edit view to adjust its kerning. 19 | 20 | ![](media/DragToKern.gif) 21 | 22 | - Hold **Option** to enable _precision mode_ which increases the mouse sensitivity 10-fold. 23 | - Hold **Shift** to round the kerning values to 10 units. 24 | 25 | ### Kerning Exceptions 26 | 27 | To add or remove kerning exceptions, you can use shortcut keys. The kerning 28 | pair on which those shortcuts operate is always the glyph at the current mouse 29 | position and the glyph to the left of it. 30 | 31 | - **A** – Make an exception for the right side of the left glyph 32 | - **S** – Make an exception for the left side of the right glyph 33 | - **D** – Make exceptions for both glyphs 34 | - **Shift+A** – Remove the exception for the right side of the left glyph 35 | - **Shift+S** – Remove the exception for the left side of the right glyph 36 | - **Shift+D** – Remove the exceptions for both glyphs 37 | 38 | This is best illustrated with an example: 39 | 40 | ![](media/DragToKern-Exception.gif) 41 | 42 | Hovering over the **ö**, the shortcuts will: 43 | 44 | - **A** – Make an exception for the **T** with the **o group** 45 | - **S** – Make an exception for the **T group** with the **ö** 46 | - **D** – Make exceptions for **T** with **ö** 47 | - **Shift+A** – Remove the exception for the **T** with the **o group** 48 | - **Shift+S** – Remove the exception for the **T group** with the **ö** 49 | - **Shift+D** – Remove the exceptions for **T** with **ö** 50 | 51 | ## Spacing Mode 52 | 53 | Hover over a glyph’s left or right edge, and red indicators will appear. Click and 54 | drag while the indicators are shown to modify the sidebearings. 55 | 56 | ![](media/DragToKern-Spacing.gif) 57 | 58 | - Click and drag while the **Command** key is pressed to move the glyph’s 59 | outline inside its current width. 60 | - Hold **Option** to enable _precision mode_ which increases the mouse 61 | sensitivity 10-fold. 62 | 63 | - If the current master’s metrics are linked to another master, a lock icon is 64 | shown when trying to drag. You can still move the outline inside its current 65 | width by holding the **Command** key. 66 | 67 | - Hide or show the measurements while dragging via the contextual menu 68 | _(Hide Measurements While Spacing/Show Measurements While Spacing)._ 69 | 70 | ## Known issues 71 | 72 | - Metrics keys are not considered when dragging the spacing. The linked metrics 73 | just go out of sync. 74 | - Undo for metrics and kerning changes only works if you make the affected 75 | glyph the current glyph (e.g. by double-clicking it with the select tool) 76 | 77 | ## Copyright 78 | 79 | © 2022 by [LucasFonts](https://www.lucasfonts.com/). Main programmer: Jens Kutílek. Licensed under the [MIT license](LICENSE). 80 | -------------------------------------------------------------------------------- /media/DragToKern-Exception.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/22f2d25c2f0b3452b42e047edc8707abc5b29b3f/media/DragToKern-Exception.gif -------------------------------------------------------------------------------- /media/DragToKern-Spacing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/22f2d25c2f0b3452b42e047edc8707abc5b29b3f/media/DragToKern-Spacing.gif -------------------------------------------------------------------------------- /media/DragToKern.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/22f2d25c2f0b3452b42e047edc8707abc5b29b3f/media/DragToKern.gif -------------------------------------------------------------------------------- /media/KerningIconDisabledTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/22f2d25c2f0b3452b42e047edc8707abc5b29b3f/media/KerningIconDisabledTemplate.png -------------------------------------------------------------------------------- /media/KerningIconLockedTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/22f2d25c2f0b3452b42e047edc8707abc5b29b3f/media/KerningIconLockedTemplate.png -------------------------------------------------------------------------------- /media/KerningIconTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/22f2d25c2f0b3452b42e047edc8707abc5b29b3f/media/KerningIconTemplate.png -------------------------------------------------------------------------------- /media/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/22f2d25c2f0b3452b42e047edc8707abc5b29b3f/media/screenshot.png -------------------------------------------------------------------------------- /resources/toolbar.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/22f2d25c2f0b3452b42e047edc8707abc5b29b3f/resources/toolbar.afdesign --------------------------------------------------------------------------------