├── media ├── DragToKern.gif ├── screenshot.png ├── DragToKern-Spacing.gif ├── KerningIconTemplate.png ├── DragToKern-Exception.gif ├── KerningIconDisabledTemplate.png └── KerningIconLockedTemplate.png ├── resources └── toolbar.afdesign ├── DragToKern.glyphsTool └── Contents │ ├── MacOS │ └── plugin │ ├── Resources │ ├── toolbar.pdf │ └── plugin.py │ └── Info.plist ├── LICENSE ├── .gitignore └── README.md /media/DragToKern.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/HEAD/media/DragToKern.gif -------------------------------------------------------------------------------- /media/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/HEAD/media/screenshot.png -------------------------------------------------------------------------------- /resources/toolbar.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/HEAD/resources/toolbar.afdesign -------------------------------------------------------------------------------- /media/DragToKern-Spacing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/HEAD/media/DragToKern-Spacing.gif -------------------------------------------------------------------------------- /media/KerningIconTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/HEAD/media/KerningIconTemplate.png -------------------------------------------------------------------------------- /media/DragToKern-Exception.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/HEAD/media/DragToKern-Exception.gif -------------------------------------------------------------------------------- /media/KerningIconDisabledTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/HEAD/media/KerningIconDisabledTemplate.png -------------------------------------------------------------------------------- /media/KerningIconLockedTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/HEAD/media/KerningIconLockedTemplate.png -------------------------------------------------------------------------------- /DragToKern.glyphsTool/Contents/MacOS/plugin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/HEAD/DragToKern.glyphsTool/Contents/MacOS/plugin -------------------------------------------------------------------------------- /DragToKern.glyphsTool/Contents/Resources/toolbar.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/HEAD/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 | -------------------------------------------------------------------------------- /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.1 17 | CFBundleVersion 18 | 71 19 | UpdateFeedURL 20 | 21 | https://raw.githubusercontent.com/LucasFonts/Glyphs-MoveableSidebearings/master/DragToKern.glyphsTool/Contents/Info.plist 22 | productPageURL 23 | https://github.com/LucasFonts/Glyphs-MoveableSidebearings 24 | productReleaseNotes 25 | Check out the Readme file for new features. 26 | NSHumanReadableCopyright 27 | Copyright 2022 by LucasFonts 28 | NSPrincipalClass 29 | DragToKern 30 | PyMainFileNames 31 | 32 | plugin.py 33 | 34 | 35 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /DragToKern.glyphsTool/Contents/Resources/plugin.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | 5 | import objc 6 | from AppKit import ( 7 | NSBezierPath, 8 | NSClassFromString, 9 | NSColor, 10 | NSCursor, 11 | NSFont, 12 | NSFontAttributeName, 13 | NSFontWeightRegular, 14 | NSForegroundColorAttributeName, 15 | NSGradient, 16 | NSPoint, 17 | NSRect, 18 | NSString, 19 | ) 20 | from GlyphsApp import GSLTR, MOUSEMOVED, Glyphs 21 | from GlyphsApp.plugins import SelectTool 22 | 23 | GlyphsToolSelect = NSClassFromString("GlyphsToolSelect") 24 | 25 | SNAP_TOLERANCE = 14 26 | COLOR_R = 0.9 27 | COLOR_G = 0.1 28 | COLOR_B = 0.0 29 | COLOR_ALPHA = 0.5 30 | DRAGGING_HANDLE_HEIGHT = 30 31 | DRAGGING_HANDLE_WIDTH = 1 32 | LABEL_TEXT_SIZE = 11 33 | LABEL_DIST = 6 34 | LABEL_VERT_INNER_BIAS = 0.3 35 | 36 | 37 | def applyKerning(layer1, layer2, delta, step, direction=GSLTR) -> None: 38 | """ 39 | Apply the kerning difference to the given layer pair. 40 | """ 41 | value = layer2.previousKerningForLayer_direction_(layer1, direction) 42 | 43 | # Glyphs 3 returns "no kerning" as None 44 | if value is None or value > 0xFFFF: 45 | # Kern pair didn't exist, set the kerning to the delta value 46 | value = int(round(delta / step) * step) 47 | else: 48 | # Kern pair existed before, add the delta value 49 | value = int(round((value + delta) / step) * step) 50 | 51 | if direction == GSLTR: 52 | layer2.setPreviousKerning_forLayer_direction_(value, layer1, direction) 53 | else: 54 | layer2.setPreviousKerning_forLayer_direction_(value, layer1, direction) 55 | 56 | 57 | def handleException(composedLayers, layerIndex, c, direction=GSLTR) -> None: 58 | """ 59 | Add or remove an exception at the current location 60 | """ 61 | if layerIndex == 0 or layerIndex > 0xFFFF: 62 | return 63 | 64 | # Find out which layers should be get the exception 65 | layer1 = composedLayers[layerIndex - 1] 66 | layer2 = composedLayers[layerIndex] 67 | if layer2.master != layer1.master: 68 | # Can't add kerning between different masters 69 | return 70 | 71 | if c == "d": 72 | # Both layers should get the exception 73 | layer1.setNextKerningExeption_forLayer_direction_(True, layer2, direction) 74 | layer2.setPreviousKerningExeption_forLayer_direction_(True, layer1, direction) 75 | elif c == "a": 76 | # First layer should get exception 77 | layer1.setNextKerningExeption_forLayer_direction_(True, layer2, direction) 78 | elif c == "s": 79 | # First layer should get exception 80 | layer2.setPreviousKerningExeption_forLayer_direction_(True, layer1, direction) 81 | elif c == "D": 82 | # Remove kerning exception for both layers 83 | layer1.setNextKerningExeption_forLayer_direction_(False, layer2, direction) 84 | layer2.setPreviousKerningExeption_forLayer_direction_(False, layer1, direction) 85 | elif c == "A": 86 | # Remove kerning exception for first layer 87 | layer1.setNextKerningExeption_forLayer_direction_(False, layer2, direction) 88 | elif c == "S": 89 | # Remove kerning exception for second layer 90 | layer2.setPreviousKerningExeption_forLayer_direction_(False, layer1, direction) 91 | 92 | 93 | class DragToKern(SelectTool): 94 | @objc.python_method 95 | def settings(self) -> None: 96 | self.name = Glyphs.localize( 97 | { 98 | "en": "Mouse Kerning and Spacing", 99 | "de": "Unterschneidung und Zurichtung per Maus", 100 | } 101 | ) 102 | self.keyboardShortcut = "k" 103 | self.stdCursor = NSCursor.resizeLeftRightCursor() 104 | self.lckCursor = NSCursor.operationNotAllowedCursor() 105 | self.cursor = self.stdCursor 106 | self.colorSBOuter = NSColor.colorWithCalibratedRed_green_blue_alpha_( 107 | COLOR_R, COLOR_G, COLOR_B, COLOR_ALPHA 108 | ) 109 | self.colorSBInner = NSColor.colorWithCalibratedRed_green_blue_alpha_( 110 | COLOR_R, COLOR_G, COLOR_B, 0.0 111 | ) 112 | self.colorLabel = NSColor.textColor() 113 | self.colorBox = NSColor.textBackgroundColor() 114 | 115 | def standardCursor(self): 116 | return self.cursor 117 | 118 | @objc.python_method 119 | def start(self) -> None: 120 | self.mode: str | None = None 121 | self.mouse_position = (0, 0) 122 | self.drag_start = None 123 | self.direction = GSLTR 124 | self.active_metric = None 125 | self.orig_value = None 126 | self.handle_x = None 127 | self.width = None 128 | self.layer1 = None 129 | self.layer2 = None 130 | self.drawMeasurements = Glyphs.defaults[ 131 | "com.lucasfonts.DragToKern.measurements" 132 | ] 133 | if self.drawMeasurements is None: 134 | self.drawMeasurements = False 135 | 136 | @objc.python_method 137 | def activate(self) -> None: 138 | Glyphs.addCallback(self.mouseDidMove, MOUSEMOVED) 139 | self.drawMeasurements = Glyphs.defaults[ 140 | "com.lucasfonts.DragToKern.measurements" 141 | ] 142 | 143 | @objc.python_method 144 | def deactivate(self) -> None: 145 | Glyphs.removeCallback(self.mouseDidMove, MOUSEMOVED) 146 | Glyphs.defaults["com.lucasfonts.DragToKern.measurements"] = ( 147 | self.drawMeasurements 148 | ) 149 | 150 | @objc.python_method 151 | def conditionalContextMenus(self) -> list[dict[str, Any]]: 152 | if self.drawMeasurements: 153 | return [ 154 | { 155 | "name": Glyphs.localize( 156 | { 157 | "en": "Hide Measurements While Spacing", 158 | } 159 | ), 160 | "action": self.toggleMeasurements_, 161 | } 162 | ] 163 | return [ 164 | { 165 | "name": Glyphs.localize( 166 | { 167 | "en": "Show Measurements While Spacing", 168 | } 169 | ), 170 | "action": self.toggleMeasurements_, 171 | } 172 | ] 173 | 174 | def toggleMeasurements_(self, sender=None) -> None: 175 | self.drawMeasurements = not self.drawMeasurements 176 | 177 | @objc.python_method 178 | def doKerning(self, graphicView) -> bool: 179 | return graphicView.doKerning() 180 | 181 | @objc.python_method 182 | def doSpacing(self, graphicView) -> bool: 183 | return not graphicView.doKerning() and graphicView.doSpacing() 184 | 185 | def keyDown_(self, theEvent) -> None: 186 | c = theEvent.characters() 187 | if c in ("a", "s", "d", "A", "S", "D"): 188 | # Get the mouse location and convert it to local coordinates 189 | evc = self.editViewController() 190 | gv = evc.graphicView() 191 | loc = gv.convertPoint_fromView_(theEvent.locationInWindow(), None) 192 | # Which layer is at the mouse click location? 193 | layerIndex = gv.layerIndexForPoint_(loc) 194 | composedLayers = evc.composedLayers 195 | handleException(composedLayers, layerIndex, c, self.direction) 196 | return 197 | 198 | # Other keys are handled by the super class 199 | objc.super().keyDown_(theEvent) 200 | 201 | @objc.python_method 202 | def mouseDidMove(self, notification) -> None: 203 | Glyphs.redraw() 204 | 205 | def mouseDown_(self, theEvent) -> None: 206 | """ 207 | Get the mouse down location to record the start coordinate and dragged 208 | layer. 209 | """ 210 | if theEvent.clickCount() == 2: 211 | wc = self.windowController() 212 | wc.setToolForClass_(GlyphsToolSelect) 213 | toolDelegate = wc.toolEventDelegate() 214 | if toolDelegate.respondsToSelector_("selectGlyph:"): 215 | toolDelegate.selectGlyph_(theEvent) 216 | return 217 | # Get the mouse click location and convert it to local coordinates 218 | evc = self.editViewController() 219 | gv = evc.graphicView() 220 | loc = gv.convertPoint_fromView_(theEvent.locationInWindow(), None) 221 | # Which layer is at the mouse click location? 222 | layerIndex = gv.layerIndexForPoint_(loc) 223 | # Note the start coordinates for later 224 | self.drag_start = loc 225 | # Note the kerning direction 226 | self.direction = evc.direction 227 | 228 | if layerIndex > 0xFFFF: 229 | # No layer (maxint) can't be modified 230 | self.setLockedCursor() 231 | self.cancel_operation() 232 | return 233 | 234 | # Collect some info about the clicked layer 235 | composedLayers = evc.composedLayers 236 | self.layer2 = composedLayers[layerIndex] 237 | layerOrigin = gv.cachedPositionAtIndex_(layerIndex) 238 | 239 | # What should be modified? Kerning, LSB, RSB, or both SBs? 240 | 241 | spacing = self.doSpacing(gv) 242 | kerning = self.doKerning(gv) 243 | if spacing: 244 | # Check if the click was at a sidebearing handle 245 | result = self.checkHandleLocation(loc, gv, self.layer2, layerOrigin) 246 | 247 | if result is None: 248 | self.active_metric = None 249 | else: 250 | self.active_metric = result[0][0] 251 | 252 | if self.windowController().CommandKey(): 253 | self.mode = "move" 254 | elif self.active_metric == "LSB": 255 | self.mode = "LSB" 256 | if self.layer2 is None: 257 | return 258 | self.orig_value = self.layer2.LSB 259 | elif self.active_metric == "RSB": 260 | self.mode = "RSB" 261 | if self.layer2 is None: 262 | return 263 | self.orig_value = self.layer2.RSB 264 | elif kerning: 265 | if not self.setupKerning(composedLayers, layerIndex): 266 | return 267 | else: 268 | self.setLockedCursor() 269 | self.cancel_operation() 270 | return 271 | 272 | elif kerning: 273 | if not self.setupKerning(composedLayers, layerIndex): 274 | return 275 | 276 | if self.layer2 is not None: 277 | self.layer2.parent.beginUndo() 278 | Glyphs.redraw() 279 | 280 | @objc.python_method 281 | def setupKerning(self, composedLayers, layerIndex) -> bool: 282 | if self.layer2 is None: 283 | return False 284 | 285 | # Kerning between two glyphs will be modified 286 | if layerIndex == 0: 287 | # First layer (0) can't be kerned 288 | self.setLockedCursor() 289 | self.cancel_operation() 290 | return False 291 | 292 | # Find out which layers should be kerned 293 | self.layer1 = composedLayers[layerIndex - 1] 294 | # self.layer2 = composedLayers[layerIndex] 295 | if self.layer2.master != self.layer1.master: 296 | # Can't add kerning between different masters 297 | self.setLockedCursor() 298 | self.cancel_operation() 299 | return False 300 | 301 | self.mode = "kern" 302 | return True 303 | 304 | def cancelOperation_(self, sender) -> None: 305 | wc = self.windowController() 306 | wc.setToolForClass_(GlyphsToolSelect) 307 | 308 | @objc.python_method 309 | def cancel_operation(self) -> None: 310 | self.layer1 = None 311 | self.layer2 = None 312 | self.drag_start = None 313 | self.orig_value = None 314 | 315 | @objc.python_method 316 | def setLockedCursor(self) -> None: 317 | # self.editViewController().contentView().enclosingScrollView().setDocumentCursor_(self.lckCursor) 318 | pass 319 | 320 | @objc.python_method 321 | def setStdCursor(self) -> None: 322 | # self.editViewController().contentView().enclosingScrollView().setDocumentCursor_(self.stdCursor) 323 | pass 324 | 325 | def mouseDragged_(self, theEvent) -> None: 326 | """ 327 | Update the kerning when the mouse is dragged and live update is on. 328 | """ 329 | if self.drag_start is None: 330 | return 331 | 332 | needsRedraw = self.handleDrag(theEvent) 333 | if needsRedraw: 334 | self.editViewController().forceRedraw() 335 | 336 | def mouseUp_(self, theEvent) -> None: 337 | """ 338 | End the undo and reset variables when the mouse is released 339 | """ 340 | if self.layer2 is not None: 341 | self.layer2.parent.endUndo() 342 | 343 | self.direction = GSLTR 344 | self.mode = None 345 | self.cancel_operation() 346 | self.setStdCursor() 347 | self.active_metric = None 348 | Glyphs.redraw() 349 | 350 | @objc.python_method 351 | def metricsAreLocked(self, layer) -> bool: 352 | cp1 = "Link Metrics With First Master" 353 | cp2 = "Link Metrics With Master" 354 | if cp1 in layer.master.customParameters or cp2 in layer.master.customParameters: 355 | return True 356 | return False 357 | 358 | @objc.python_method 359 | def handleDrag(self, theEvent) -> bool: 360 | """ 361 | Get the current location while the mouse is dragging. Returns True if 362 | the view needs a redraw, i.e. the kerning or metrics were modified. 363 | """ 364 | if self.layer2 is None: 365 | return False 366 | if self.drag_start is None: 367 | return False 368 | 369 | evc = self.editViewController() 370 | gv = evc.graphicView() 371 | loc = gv.convertPoint_fromView_(theEvent.locationInWindow(), None) 372 | wc = self.windowController() 373 | 374 | # Alt key enables "precision dragging" 375 | if wc.AltKey(): 376 | mouseZoom = 0.1 377 | else: 378 | mouseZoom = 1 379 | 380 | # Shift key rounds to 10 381 | if wc.ShiftKey(): 382 | step = 10 383 | else: 384 | step = 1 385 | 386 | delta = (loc.x - self.drag_start.x) / evc.scale * mouseZoom 387 | 388 | self.drag_start = loc 389 | if delta != 0.0: 390 | # Only "move" can be applied for linked metrics 391 | if self.mode == "move": 392 | self.layer2.LSB += int(round(delta)) 393 | self.layer2.width -= int(round(delta)) 394 | return True 395 | 396 | if self.metricsAreLocked(self.layer2): 397 | return False 398 | 399 | if self.mode == "kern": 400 | applyKerning(self.layer1, self.layer2, delta, step, self.direction) 401 | return False # Kerning changes already trigger a redraw 402 | 403 | if self.mode == "LSB": 404 | self.layer2.LSB += int(round(delta)) 405 | return True 406 | 407 | if self.mode == "RSB": 408 | self.layer2.RSB += int(round(delta)) 409 | return True 410 | 411 | return False 412 | 413 | def drawLayer_atPoint_asActive_attributes_( 414 | self, layer, layerOrigin, active, attributes 415 | ) -> None: 416 | gv = self.editViewController().graphicView() 417 | gv.drawLayer_atPoint_asActive_attributes_( 418 | layer, layerOrigin, active, attributes 419 | ) 420 | if not self.doSpacing(gv): 421 | # Not in spacing mode 422 | return 423 | 424 | if self.drag_start is None: 425 | result = self.checkHandles(gv, layer, layerOrigin) 426 | if result is not None: 427 | metric, handle_x, width = result 428 | self._drawHandle(handle_x, metric) 429 | elif self.drawMeasurements: 430 | self._drawDraggingMeasurements(self.mode, gv, layer, layerOrigin) 431 | 432 | def drawMetricsForLayer_atPoint_asActive_(self, layer, layerOrigin, active) -> None: 433 | pass 434 | 435 | @objc.python_method 436 | def checkHandles( 437 | self, graphicView, layer, layerOrigin 438 | ) -> tuple[tuple[str, float, float, float, float], tuple[float, float], int] | None: 439 | """ 440 | Check if the mouse pointer is at a possible metrics handle location. 441 | Called on MOUSEMOVED via drawLayer_atPoint_asActive_attributes_. 442 | """ 443 | theEvent = Glyphs.currentEvent() 444 | if theEvent is None: 445 | return None 446 | 447 | self.mouse_position = graphicView.convertPoint_fromView_( 448 | theEvent.locationInWindow(), None 449 | ) 450 | return self.checkHandleLocation( 451 | self.mouse_position, graphicView, layer, layerOrigin 452 | ) 453 | 454 | @objc.python_method 455 | def checkHandleLocation( 456 | self, location, graphicView, layer, layerOrigin 457 | ) -> tuple[tuple[str, float, float, float, float], tuple[float, float], int] | None: 458 | """ 459 | Check if the location of an event is at a possible metrics handle 460 | location. 461 | """ 462 | if not self.doSpacing(graphicView): 463 | return None 464 | 465 | try: 466 | master = layer.master 467 | except KeyError: 468 | return None 469 | 470 | x, y = location 471 | scale = graphicView.scale() 472 | desc = master.descender * scale 473 | asc = master.ascender * scale 474 | asc += layerOrigin.y 475 | desc += layerOrigin.y 476 | layerWidth = layer.width * scale 477 | 478 | # Don't draw handles outside ascender/descender 479 | if y < desc or y > asc: 480 | return None 481 | 482 | offsetX = x - layerOrigin.x 483 | 484 | if offsetX < 0 or offsetX > layerWidth: 485 | # Mouse is outside the glyph 486 | return None 487 | 488 | if offsetX > SNAP_TOLERANCE and offsetX < layerWidth - SNAP_TOLERANCE: 489 | # Mouse is too far inside the glyph 490 | return None 491 | 492 | if offsetX < SNAP_TOLERANCE: 493 | handle_x = (layerOrigin.x, SNAP_TOLERANCE) 494 | metric = ( 495 | "LSB", 496 | layer.LSB, 497 | layer, 498 | desc, 499 | asc, 500 | ) 501 | width = layerOrigin.x 502 | else: 503 | handle_x = ( 504 | layerOrigin.x + layerWidth - SNAP_TOLERANCE, 505 | SNAP_TOLERANCE, 506 | ) 507 | metric = ( 508 | "RSB", 509 | layer.RSB, 510 | layer, 511 | desc, 512 | asc, 513 | ) 514 | width = layerOrigin.x + layerWidth 515 | return metric, handle_x, width 516 | 517 | @objc.python_method 518 | def _drawHandle(self, handle_x, metric) -> None: 519 | if handle_x is None: 520 | return 521 | if metric is None: 522 | return 523 | 524 | pos, w = handle_x 525 | metric_name, value, layer, desc, asc = metric 526 | gradient = NSGradient.alloc().initWithStartingColor_endingColor_( 527 | self.colorSBOuter, self.colorSBInner 528 | ) 529 | rect = NSRect( 530 | origin=(pos, desc), 531 | size=(w, asc - desc), 532 | ) 533 | angle = -180 if metric_name == "RSB" else 0 534 | bezierPath = NSBezierPath.bezierPathWithRect_(rect) 535 | gradient.drawInBezierPath_angle_(bezierPath, angle) 536 | 537 | @objc.python_method 538 | def _drawDraggingMeasurements( 539 | self, metric, graphicView, layer, layerOrigin 540 | ) -> None: 541 | if layer != self.layer2 or self.layer2 is None: 542 | # Only draw labels at the layer being modified 543 | return 544 | 545 | try: 546 | master = self.layer2.master 547 | except KeyError: 548 | return 549 | 550 | scale = graphicView.scale() 551 | desc = master.descender * scale 552 | asc = master.ascender * scale 553 | asc += layerOrigin.y 554 | desc += layerOrigin.y 555 | layerWidth = layer.width * scale 556 | locked = self.metricsAreLocked(self.layer2) 557 | 558 | if metric in ("LSB", "RSB", "move"): 559 | # Draw left and right 560 | x1 = layerOrigin.x - DRAGGING_HANDLE_WIDTH * 0.5 561 | x2 = layerOrigin.x + layerWidth - DRAGGING_HANDLE_WIDTH * 0.5 562 | self._drawDraggingTextLabel("LSB", x1, asc, locked) 563 | self._drawDraggingTextLabel("RSB", x2, asc, locked) 564 | pos = [x1, x2] 565 | elif metric == "kern": 566 | # FIXME: This code is never called 567 | # Draw left 568 | x = layerOrigin.x - DRAGGING_HANDLE_WIDTH * 0.5 569 | self._drawDraggingTextLabel("LSB", x, asc, locked) 570 | pos = [x] 571 | else: 572 | return 573 | 574 | self._drawDraggingMeasurement(pos, asc, desc) 575 | 576 | @objc.python_method 577 | def _drawDraggingMeasurement(self, xPositions, asc, desc) -> None: 578 | top = DRAGGING_HANDLE_HEIGHT * LABEL_VERT_INNER_BIAS 579 | bot = DRAGGING_HANDLE_HEIGHT - top 580 | for x in xPositions: 581 | bezierPath = NSBezierPath.bezierPathWithRect_( 582 | NSRect( 583 | origin=(x, desc - bot), 584 | size=(DRAGGING_HANDLE_WIDTH, DRAGGING_HANDLE_HEIGHT), 585 | ) 586 | ) 587 | bezierPath.appendBezierPathWithRect_( 588 | NSRect( 589 | origin=(x, asc - top), 590 | size=(DRAGGING_HANDLE_WIDTH, DRAGGING_HANDLE_HEIGHT), 591 | ) 592 | ) 593 | self.colorSBOuter.set() 594 | bezierPath.fill() 595 | 596 | @objc.python_method 597 | def _drawDraggingTextLabel(self, metric, xPosition, asc, locked) -> None: 598 | if self.layer2 is None: 599 | return 600 | 601 | if locked: 602 | shown_value = "🔒︎" 603 | else: 604 | if metric == "LSB": 605 | shown_value = "%g" % self.layer2.LSB 606 | elif metric == "RSB": 607 | shown_value = "%g" % self.layer2.RSB 608 | else: 609 | return 610 | 611 | attrs = { 612 | NSFontAttributeName: NSFont.monospacedDigitSystemFontOfSize_weight_( 613 | LABEL_TEXT_SIZE, NSFontWeightRegular 614 | ), 615 | NSForegroundColorAttributeName: self.colorLabel, 616 | } 617 | myString = NSString.string().stringByAppendingString_(shown_value) 618 | bbox = myString.sizeWithAttributes_(attrs) 619 | bw = bbox.width 620 | bh = bbox.height 621 | text_pt = NSPoint() 622 | text_pt.y = ( 623 | asc 624 | + DRAGGING_HANDLE_HEIGHT 625 | - DRAGGING_HANDLE_HEIGHT * LABEL_VERT_INNER_BIAS 626 | - bh 627 | ) 628 | if metric == "LSB": 629 | text_pt.x = xPosition + LABEL_DIST 630 | elif metric == "RSB": 631 | text_pt.x = xPosition - LABEL_DIST - bw 632 | else: 633 | return 634 | 635 | rect = NSRect(origin=(text_pt.x, text_pt.y), size=(bw, bh)) 636 | outer = NSRect(origin=(text_pt.x - 2, text_pt.y - 1), size=(bw + 4, bh + 2)) 637 | self.colorBox.set() 638 | NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(outer, 4, 4).fill() 639 | myString.drawInRect_withAttributes_(rect, attrs) 640 | 641 | @objc.python_method 642 | def __file__(self) -> str: 643 | """Please leave this method unchanged""" 644 | return __file__ 645 | --------------------------------------------------------------------------------