This example shows how to control multiple glyph editor subscribers using a single vanilla Window.

First of all, we need to create a custom subscriber event. It is handy to keep custom subscriber events in a separate module, so that the module can be executed only once, when an extension is started.

from mojo.subscriber import registerSubscriberEvent

DEFAULT_KEY = 'com.developerName.SomeTool'

if __name__ == '__main__':
    registerSubscriberEvent(
        subscriberEventName=f"{DEFAULT_KEY}.changed",
        methodName="paletteDidChange",
        lowLevelEventNames=[f"{DEFAULT_KEY}.changed"],
        dispatcher="roboFont",
        documentation="Send when the tool palette did change parameters.",
        delay=0,
        debug=True
    )

If you pack this code into an extension, you should set the events.py module as extension startup script

The rest of the tool is organized in two objects:

  • a WindowController subclass (owning a vanilla window)
  • a Subscriber subclass (taking care of drawing in the glyph editors)

The Subscriber object draws a stroke around any outline in the glyph editors while the WindowController owns a slider that sets the stroke thickness. The drawing is performed using Merz.

#!/usr/bin/env python3

from mojo.subscriber import Subscriber, WindowController
from mojo.subscriber import registerGlyphEditorSubscriber, unregisterGlyphEditorSubscriber
from mojo.roboFont import OpenWindow
from mojo.events import postEvent

from vanilla import FloatingWindow, Slider, TextBox

from events import DEFAULT_KEY


class ToolPalette(WindowController):

    debug = True
    thickness = 5

    def build(self):
        self.w = FloatingWindow((200, 40), "Tool")
        self.w.slider = Slider((10, 10, -30, 23),
                               minValue=0,
                               value=self.thickness,
                               maxValue=25,
                               stopOnTickMarks=True,
                               tickMarkCount=6,
                               continuous=False,
                               callback=self.sliderCallback)
        self.w.textBox = TextBox((-25, 10, 30, 17), f"{self.thickness:.0f}")
        self.w.open()

    def started(self):
        Tool.controller = self
        registerGlyphEditorSubscriber(Tool)

    def destroy(self):
        unregisterGlyphEditorSubscriber(Tool)
        Tool.controller = None

    def sliderCallback(self, sender):
        self.thickness = sender.get()
        self.w.textBox.set(f"{self.thickness:.0f}")
        postEvent(f"{DEFAULT_KEY}.changed")


class Tool(Subscriber):
    """
    Tool can only ask for information to the palette
    """
    debug = True
    controller = None

    def build(self):
        glyphEditor = self.getGlyphEditor()
        container = glyphEditor.extensionContainer(identifier=DEFAULT_KEY,
                                                   location='background',
                                                   clear=True)
        self.path = container.appendPathSublayer(
            fillColor=(0, 0, 0, 0),
            strokeColor=(1, 0, 0, 1),
            strokeWidth=self.controller.thickness if self.controller else 0,
        )
        glyph = self.getGlyphEditor().getGlyph()
        self.path.setPath(glyph.getRepresentation("merz.CGPath"))

    def destroy(self):
        glyphEditor = self.getGlyphEditor()
        container = glyphEditor.extensionContainer(DEFAULT_KEY, location='background')
        container.clearSublayers()

    def glyphEditorGlyphDidChangeOutline(self, info):
        self.path.setPath(info['glyph'].getRepresentation("merz.CGPath"))

    def paletteDidChange(self, info):
        self.path.setStrokeWidth(self.controller.thickness)


if __name__ == '__main__':
    OpenWindow(ToolPalette)

A few aspects worth noting:

  • You have to register and unregister the Subscriber tool inside the started and destroy events of the WindowController subclass. Here, you should set the controller attribute of the Subscriber subclass
  • Remember to fire postEvent inside the user interface callbacks, in this example the sliderCallback
  • You can access the modified value from the user interface inside the custom event callback in the subscriber subclass with self.controller.thickness
  • You can find a specific RoboFont extension boilerplate here
  • A more complex example can be found in the Outliner repository
Last edited on 01/09/2021