Example #1
0
 def test_addObserver_no_notification_observable(self):
     center = NotificationCenter()
     observable = _TestObservable(center, "Observable")
     observer = NotificationTestObserver()
     center.addObserver(observer, "notificationCallback", None, observable)
     self.assertTrue(center.hasObserver(observer, None, observable))
     self.assertFalse(center.hasObserver(observer, "A", observable))
 def test_areNotificationsHeld_all_held(self):
     center = NotificationCenter()
     observable = _TestObservable(center, "Observable")
     observer = NotificationTestObserver()
     center.addObserver(observer, "notificationCallback", "A", observable)
     center.holdNotifications()
     self.assertTrue(center.areNotificationsHeld())
     center.releaseHeldNotifications()
     self.assertFalse(center.areNotificationsHeld())
Example #3
0
 def test_removeObserver_no_notification_observable(self):
     center = NotificationCenter()
     observable = _TestObservable(center, "Observable")
     observer = NotificationTestObserver()
     center.addObserver(observer, "notificationCallback", None, observable)
     center.addObserver(observer, "notificationCallback", "A", observable)
     center.removeObserver(observer, None, observable)
     self.assertFalse(center.hasObserver(observer, None, observable))
     self.assertTrue(center.hasObserver(observer, "A", observable))
 def test_areNotificationsDisabled_all_off(self):
     center = NotificationCenter()
     observable1 = _TestObservable(center, "Observable1")
     observable2 = _TestObservable(center, "Observable2")
     observer = NotificationTestObserver()
     center.addObserver(observer, "notificationCallback", "A", observable1)
     center.addObserver(observer, "notificationCallback", "B", observable2)
     center.disableNotifications()
     self.assertTrue(center.areNotificationsDisabled())
     center.enableNotifications()
     self.assertFalse(center.areNotificationsDisabled())
Example #5
0
 def test_areNotificationsHeld_notification_off(self):
     center = NotificationCenter()
     observable = _TestObservable(center, "Observable")
     observer = NotificationTestObserver()
     center.addObserver(observer, "notificationCallback", "A", observable)
     center.addObserver(observer, "notificationCallback", "B", observable)
     center.holdNotifications(notification="A")
     self.assertTrue(center.areNotificationsHeld(notification="A"))
     self.assertFalse(center.areNotificationsHeld(notification="B"))
     center.releaseHeldNotifications(notification="A")
     self.assertFalse(center.areNotificationsHeld(notification="A"))
Example #6
0
 def test_areNotificationsHeld_observer_off(self):
     center = NotificationCenter()
     observable = _TestObservable(center, "Observable")
     observer1 = NotificationTestObserver()
     observer2 = NotificationTestObserver()
     center.addObserver(observer1, "notificationCallback", "A", observable)
     center.addObserver(observer2, "notificationCallback", "A", observable)
     center.holdNotifications(observer=observer1)
     self.assertTrue(center.areNotificationsHeld(observer=observer1))
     self.assertFalse(center.areNotificationsHeld(observer=observer2))
     center.releaseHeldNotifications(observer=observer1)
     self.assertFalse(center.areNotificationsHeld(observer=observer1))
Example #7
0
 def test_areNotificationsHeld_observable_off(self):
     center = NotificationCenter()
     observable1 = _TestObservable(center, "Observable1")
     observable2 = _TestObservable(center, "Observable2")
     observer = NotificationTestObserver()
     center.addObserver(observer, "notificationCallback", "A", observable1)
     center.addObserver(observer, "notificationCallback", "B", observable2)
     center.holdNotifications(observable=observable1)
     self.assertTrue(center.areNotificationsHeld(observable=observable1))
     self.assertFalse(center.areNotificationsHeld(observable=observable2))
     center.releaseHeldNotifications(observable=observable1)
     self.assertFalse(center.areNotificationsHeld(observable=observable1))
Example #8
0
 def test_areNotificationsDisabled_observer_off(self):
     center = NotificationCenter()
     observable1 = _TestObservable(center, "Observable1")
     observer1 = NotificationTestObserver()
     observer2 = NotificationTestObserver()
     center.addObserver(observer1, "notificationCallback", "A", observable1)
     center.addObserver(observer2, "notificationCallback", "A", observable1)
     center.disableNotifications(observer=observer1)
     self.assertTrue(center.areNotificationsDisabled(observer=observer1))
     self.assertFalse(center.areNotificationsDisabled(observer=observer2))
     center.enableNotifications(observer=observer1)
     self.assertFalse(center.areNotificationsDisabled(observer=observer1))
Example #9
0
 def test_areNotificationsDisabled_notification_off(self):
     center = NotificationCenter()
     observable1 = _TestObservable(center, "Observable1")
     observable2 = _TestObservable(center, "Observable2")
     observer = NotificationTestObserver()
     center.addObserver(observer, "notificationCallback", "A", observable1)
     center.addObserver(observer, "notificationCallback", "B", observable2)
     center.disableNotifications(notification="A")
     self.assertTrue(center.areNotificationsDisabled(notification="A"))
     self.assertFalse(center.areNotificationsDisabled(notification="B"))
     center.enableNotifications(notification="A")
     self.assertFalse(center.areNotificationsDisabled(notification="A"))
 def test_postNotification_no_notification_observable(self):
     # no notification, observable
     center = NotificationCenter()
     observable1 = _TestObservable(center, "Observable1")
     observable2 = _TestObservable(center, "Observable2")
     observer = NotificationTestObserver()
     center.addObserver(observer, "notificationCallback", None, observable1)
     center.postNotification("A", observable1)
     self.assertEqual(observer.stack[-1], ("A", "Observable1"))
     center.postNotification("A", observable2)
     center.postNotification("B", observable1)
     self.assertEqual(observer.stack[-1], ("B", "Observable1"))
     center.postNotification("B", observable2)
Example #11
0
 def __init__(self, *args, **kwargs):
     super().__init__(*args, **kwargs)
     self._currentGlyph = None
     self._currentFontWindow = None
     self._launched = False
     self._drawingTools = [
         SelectionTool, PenTool, KnifeTool, RulerTool, ShapesTool, TextTool]
     self._extensions = []
     self.dispatcher = NotificationCenter()
     self.dispatcher.addObserver(self, "_fontWindowClosed", "fontWillClose")
     self.focusWindowChanged.connect(self._focusWindowChanged)
     self.GL2UV = None
     self.outputWindow = None
 def test_disable_enableNotifications_specific_observer(self):
     # disable all notifications for a specific observer
     center = NotificationCenter()
     observable1 = _TestObservable(center, "Observable1")
     observable2 = _TestObservable(center, "Observable2")
     observer = NotificationTestObserver()
     center.addObserver(observer, "notificationCallback", "A", observable1)
     center.addObserver(observer, "notificationCallback", "B", observable1)
     center.addObserver(observer, "notificationCallback", "C", observable2)
     center.disableNotifications(observer=observer)
     observable1.postNotification("A")
     observable1.postNotification("B")
     observable2.postNotification("C")
     center.enableNotifications(observer=observer)
     observable1.postNotification("A")
     self.assertEqual(observer.stack[-1], ("A", "Observable1"))
Example #13
0
 def test_object_representations(self):
     notificationCenter = NotificationCenter()
     self.obj._dispatcher = weakref.ref(notificationCenter)
     self.obj.representationFactories = dict(
         test=dict(factory=_representationTestFactory,
                   destructiveNotifications=["BaseObject.Changed"]))
     self.assertEqual(self.obj.getRepresentation("test"), "()")
     self.assertEqual(self.obj.representationKeys(), [('test', {})])
     self.assertEqual(
         self.obj.getRepresentation("test",
                                    attr1="foo",
                                    attr2="bar",
                                    attr3=1),
         "(('attr1', 'foo'), ('attr2', 'bar'), ('attr3', 1))")
     self.assertEqual(
         sortRepresentationKeys(self.obj.representationKeys()),
         sorted([('test', []),
                 ('test', [('attr1', 'foo'), ('attr2', 'bar'),
                           ('attr3', 1)])]))
     self.assertTrue(self.obj.hasCachedRepresentation("test"))
     self.assertTrue(
         self.obj.hasCachedRepresentation("test",
                                          attr1="foo",
                                          attr2="bar",
                                          attr3=1))
     self.assertFalse(
         self.obj.hasCachedRepresentation("test",
                                          attr1="not foo",
                                          attr2="bar",
                                          attr3=1))
     self.obj.destroyAllRepresentations()
     self.assertEqual(self.obj.representationKeys(), [])
     self.obj.representationFactories["foo"] = dict(
         factory=_representationTestFactory,
         destructiveNotifications=["BaseObject.Changed"])
     self.assertEqual(self.obj.getRepresentation("test"), '()')
     self.assertEqual(
         self.obj.getRepresentation("test",
                                    attr1="foo",
                                    attr2="bar",
                                    attr3=1),
         "(('attr1', 'foo'), ('attr2', 'bar'), ('attr3', 1))")
     self.assertEqual(
         self.obj.getRepresentation("test",
                                    attr21="foo",
                                    attr22="bar",
                                    attr23=1),
         "(('attr21', 'foo'), ('attr22', 'bar'), ('attr23', 1))")
     self.assertEqual(self.obj.getRepresentation("foo"), "()")
     self.obj.destroyRepresentation("test",
                                    attr21="foo",
                                    attr22="bar",
                                    attr23=1)
     self.assertEqual(sortRepresentationKeys(self.obj.representationKeys()),
                      [('foo', []), ('test', []),
                       ('test', [('attr1', 'foo'), ('attr2', 'bar'),
                                 ('attr3', 1)])])
     self.obj.destroyRepresentation("test")
     self.assertEqual(self.obj.representationKeys(), [('foo', {})])
Example #14
0
 def test_postNotification_no_notification_observable(self):
     # no notification, observable
     center = NotificationCenter()
     observable1 = _TestObservable(center, "Observable1")
     observable2 = _TestObservable(center, "Observable2")
     observer = NotificationTestObserver()
     center.addObserver(observer, "notificationCallback", None, observable1)
     center.postNotification("A", observable1)
     self.assertEqual(observer.stack[-1], ("A", "Observable1"))
     center.postNotification("A", observable2)
     center.postNotification("B", observable1)
     self.assertEqual(observer.stack[-1], ("B", "Observable1"))
     center.postNotification("B", observable2)
Example #15
0
 def test_addObserver_identifierDuplicate(self):
     center = NotificationCenter()
     observable = _TestObservable(center, "Observable")
     observer = NotificationTestObserver()
     center.addObserver(observer,
                        "notificationCallback",
                        "A",
                        observable,
                        identifier="identifier1")
     center.addObserver(observer,
                        "notificationCallback",
                        "B",
                        observable,
                        identifier="identifier1")
     expected = [
         dict(observer=observer,
              observable=observable,
              notification="A",
              identifier="identifier1"),
         dict(observer=observer,
              observable=observable,
              notification="B",
              identifier="identifier1")
     ]
     self.assertEqual(center.findObservations(), expected)
Example #16
0
 def test_disable_enableNotifications_specific_observer(self):
     # disable all notifications for a specific observer
     center = NotificationCenter()
     observable1 = _TestObservable(center, "Observable1")
     observable2 = _TestObservable(center, "Observable2")
     observer = NotificationTestObserver()
     center.addObserver(observer, "notificationCallback", "A", observable1)
     center.addObserver(observer, "notificationCallback", "B", observable1)
     center.addObserver(observer, "notificationCallback", "C", observable2)
     center.disableNotifications(observer=observer)
     observable1.postNotification("A")
     observable1.postNotification("B")
     observable2.postNotification("C")
     center.enableNotifications(observer=observer)
     observable1.postNotification("A")
     self.assertEqual(observer.stack[-1], ("A", "Observable1"))
Example #17
0
 def __init__(self, *args, **kwargs):
     super().__init__(*args, **kwargs)
     self._currentGlyph = None
     self._currentMainWindow = None
     self._launched = False
     self._drawingTools = [SelectionTool, PenTool, RulerTool, KnifeTool]
     self.dispatcher = NotificationCenter()
     self.dispatcher.addObserver(self, "_mainWindowClosed", "fontWillClose")
     self.focusChanged.connect(self.updateCurrentMainWindow)
     self.GL2UV = None
     self.outputWindow = None
Example #18
0
 def test_hold_and_releaseHeldNotifications_notifications_of_observable(
         self):
     "Hold all notifications of a specific notification"
     center = NotificationCenter()
     observable1 = _TestObservable(center, "Observable1")
     observable2 = _TestObservable(center, "Observable2")
     observer = NotificationTestObserver()
     center.addObserver(observer, "notificationCallback", "A", observable1)
     center.addObserver(observer, "notificationCallback", "B", observable1)
     center.addObserver(observer, "notificationCallback", "C", observable2)
     center.holdNotifications(notification="A")
     observable1.postNotification("A")
     observable1.postNotification("A")
     observable1.postNotification("B")
     self.assertEqual(observer.stack[-1], ("B", "Observable1"))
     observable2.postNotification("C")
     self.assertEqual(observer.stack[-1], ("C", "Observable2"))
     center.releaseHeldNotifications(notification="A")
     self.assertEqual(observer.stack[-1], ("A", "Observable1"))
     self.assertEqual(len(observer.stack), 3)
Example #19
0
 def test_hasObserver(self):
     obj = BaseObject()
     notificationObject = NotificationTestObserver()
     notificationCenter = NotificationCenter()
     obj._dispatcher = weakref.ref(notificationCenter)
     obj.dispatcher.addObserver(observer=notificationObject,
                                methodName="notificationCallback",
                                notification="BaseObject.Changed",
                                observable=obj)
     self.assertTrue(
         obj.hasObserver(observer=notificationObject,
                         notification="BaseObject.Changed"))
Example #20
0
 def test_removeObserver_no_notification_no_observable(self):
     center = NotificationCenter()
     _TestObservable(center, "Observable")
     observer = NotificationTestObserver()
     center.addObserver(observer, "notificationCallback", None, None)
     center.removeObserver(observer, None, None)
     self.assertFalse(center.hasObserver(observer, None, None))
Example #21
0
 def test_removeObserver(self):
     center = NotificationCenter()
     observable = _TestObservable(center, "Observable")
     observer = NotificationTestObserver()
     center.addObserver(observer, "notificationCallback", "A", observable)
     center.removeObserver(observer, "A", observable)
     self.assertFalse(center.hasObserver(observer, "A", observable))
Example #22
0
 def test_removeObserver_identifier(self):
     center = NotificationCenter()
     observable = _TestObservable(center, "Observable")
     observer = NotificationTestObserver()
     center.addObserver(observer,
                        "notificationCallback",
                        "A",
                        observable,
                        identifier="identifier1")
     center.addObserver(observer,
                        "notificationCallback",
                        "B",
                        observable,
                        identifier="identifier2")
     center.removeObserver(observer, "A", observable)
     center.removeObserver(observer, "B", observable)
     expected = []
     self.assertEqual(center.findObservations(), expected)
Example #23
0
    def test_hold_and_releaseHeldNotifications_notifications_of_observable(self):
        "Hold all notifications of a specific observable"
        center = NotificationCenter()
        observable1 = _TestObservable(center, "Observable1")
        observable2 = _TestObservable(center, "Observable2")
        observer = NotificationTestObserver()
        center.addObserver(observer, "notificationCallback", "A", observable1)
        center.addObserver(observer, "notificationCallback", "B", observable1)
        center.addObserver(observer, "notificationCallback", "C", observable2)

        center.holdNotifications(observable=observable1)
        observable1.postNotification("A")
        observable1.postNotification("A")
        observable1.postNotification("B")
        observable2.postNotification("C")
        self.assertEqual(observer.stack[-1], ("C", "Observable2"))
        center.releaseHeldNotifications(observable=observable1)
        self.assertEqual(observer.stack[-2], ("A", "Observable1"))
        self.assertEqual(observer.stack[-1], ("B", "Observable1"))
        self.assertEqual(len(observer.stack), 3)
Example #24
0
 def __init__(self, *args, **kwargs):
     super().__init__(*args, **kwargs)
     self._currentGlyph = None
     self._currentMainWindow = None
     self._launched = False
     self._drawingTools = [SelectionTool, PenTool, RulerTool, KnifeTool]
     self._extensions = []
     self.dispatcher = NotificationCenter()
     self.dispatcher.addObserver(self, "_mainWindowClosed", "fontWillClose")
     # TODO: see about filtering this into windowChanged
     # except if we're going to use tabs with changing menu, then it might
     # be convenient to no filter out (no additional signal has to be sent)
     self.focusChanged.connect(self._focusWidgetChanged)
     self.GL2UV = None
     self.inspectorWindow = None
     self.outputWindow = None
Example #25
0
 def test_dirty_set(self):
     notificationObject = NotificationTestObserver()
     obj = TestBaseObject()
     notificationCenter = NotificationCenter()
     obj._dispatcher = weakref.ref(notificationCenter)
     obj.beginSelfNotificationObservation()
     obj.addObserver(notificationObject, "notificationCallback",
                     "BaseObject.Changed")
     obj.dirty = True
     self.assertEqual(len(notificationObject.stack), 1)
     self.assertEqual(notificationObject.stack[-1],
                      ("BaseObject.Changed", "TestBaseObject"))
     self.assertTrue(obj.dirty)
     obj.dirty = False
     self.assertEqual(notificationObject.stack[-1],
                      ("BaseObject.Changed", "TestBaseObject"))
Example #26
0
 def test_addObserver(self):
     obj = BaseObject()
     notificationObject = NotificationTestObserver()
     notificationCenter = NotificationCenter()
     obj._dispatcher = weakref.ref(notificationCenter)
     obj.addObserver(observer=notificationObject,
                     methodName="notificationCallback",
                     notification="BaseObject.Changed",
                     identifier="test")
     self.assertTrue(
         obj.dispatcher.hasObserver(observer=notificationObject,
                                    notification="BaseObject.Changed",
                                    observable=obj))
     expected = [
         dict(observer=notificationObject,
              notification="BaseObject.Changed",
              observable=obj,
              identifier="test")
     ]
     result = obj.findObservations()
     self.assertEqual(expected, result)
Example #27
0
 def test_findObservations_noIdentifier(self):
     center = NotificationCenter()
     observable = _TestObservable(center, "Observable")
     observer = NotificationTestObserver()
     center.addObserver(observer,
                        "notificationCallback",
                        "A",
                        observable,
                        identifier="identifier1")
     center.addObserver(observer, "notificationCallback", "B", observable)
     expected = [
         dict(observer=observer,
              observable=observable,
              notification="A",
              identifier="identifier1"),
         dict(observer=observer,
              observable=observable,
              notification="B",
              identifier=None)
     ]
     result = center.findObservations()
     self.assertEqual(result, expected)
Example #28
0
class Application(QApplication):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._currentGlyph = None
        self._currentFontWindow = None
        self._launched = False
        self._drawingTools = [
            SelectionTool, PenTool, KnifeTool, RulerTool, ShapesTool, TextTool
        ]
        self._extensions = []
        self.dispatcher = NotificationCenter()
        self.dispatcher.addObserver(self, "_fontWindowClosed", "fontWillClose")
        self.focusWindowChanged.connect(self._focusWindowChanged)
        self.GL2UV = None
        self.outputWindow = None

    # --------------
    # Event handling
    # --------------

    def _focusWindowChanged(self):
        # update menu bar
        self.updateMenuBar()
        # update main window
        window = self.activeWindow()
        if window is None:
            return
        while True:
            parent = window.parent()
            if parent is None:
                break
            window = parent
        if isinstance(window, FontWindow):
            self.setCurrentFontWindow(window)

    def _fontWindowClosed(self, notification):
        font = notification.data["font"]
        # cleanup CurrentFont/CurrentGlyph when closing the corresponding
        # window
        if self._currentFontWindow is not None:
            if self._currentFontWindow.font == font:
                self.setCurrentFontWindow(None)
        if self._currentGlyph is not None:
            if self._currentGlyph.font == font:
                self.setCurrentGlyph(None)

    def event(self, event):
        eventType = event.type()
        # respond to OSX open events
        if eventType == QEvent.FileOpen:
            filePath = event.file()
            self.openFile(filePath)
            return True
        elif eventType == QEvent.ApplicationStateChange:
            applicationState = self.applicationState()
            if applicationState == Qt.ApplicationActive:
                if not self._launched:
                    notification = "applicationLaunched"
                    self.loadGlyphList()
                    self._launched = True
                else:
                    notification = "applicationActivated"
                    # XXX: do it
                    # self.lookupExternalChanges()
                self.postNotification(notification)
            elif applicationState == Qt.ApplicationInactive:
                self.postNotification("applicationWillIdle")
        return super().event(event)

    def postNotification(self, notification, data=None):
        dispatcher = self.dispatcher
        dispatcher.postNotification(notification=notification,
                                    observable=self,
                                    data=data)

    # ---------------
    # File management
    # ---------------

    def loadGlyphList(self):
        glyphListPath = settings.glyphListPath()
        if glyphListPath and os.path.exists(glyphListPath):
            try:
                glyphList_ = glyphList.parseGlyphList(glyphListPath)
            except Exception as e:
                msg = self.tr(
                    "The glyph list at {0} cannot "
                    "be parsed and will be dropped.").format(glyphListPath)
                errorReports.showWarningException(e, msg)
                settings.removeGlyphListPath()
            else:
                self.GL2UV = glyphList_

    def lookupExternalChanges(self):
        for font in self.allFonts():
            if not font.path:
                continue
            changed = font.testForExternalChanges()
            for attr in ("info", "kerning", "groups", "features", "lib"):
                if changed[attr]:
                    data = dict(font=font)
                    self.postNotification("fontChangedExternally", data)
                    return
            # XXX: do more

    # -----------------
    # Window management
    # -----------------

    def currentFontWindow(self):
        return self._currentFontWindow

    def setCurrentFontWindow(self, fontWindow):
        if fontWindow == self._currentFontWindow:
            return
        self._currentFontWindow = fontWindow
        self.postNotification("currentFontChanged")

    # --------
    # Menu Bar
    # --------

    def fetchMenuBar(self, window=None):
        if platformSpecific.useGlobalMenuBar():
            try:
                self._menuBar
            except:
                self._menuBar = globalMenuBar()
            self._menuBar.resetState()
            return self._menuBar
        menuBar = window.menuBar()
        if not isinstance(menuBar, MenuBar):
            menuBar = MenuBar(window)
            window.setMenuBar(menuBar)
        return menuBar

    def setupMenuBar(self, menuBar=None):
        if menuBar is None:
            try:
                menuBar = self._menuBar
            except:
                return
            menuBar.resetState()
        activeWindow = self.activeWindow()
        fileMenu = menuBar.fetchMenu(Entries.File)
        # HACK: scripting window has its own New/Open;
        # figure out how to do this without explicit blacklist.
        if not isinstance(activeWindow, ScriptingWindow):
            fileMenu.fetchAction(Entries.File_New, self.newFile)
            fileMenu.fetchAction(Entries.File_Open, self.openFile)
        recentFilesMenu = fileMenu.fetchMenu(Entries.File_Open_Recent)
        self.updateRecentFiles(recentFilesMenu)
        if not platformSpecific.mergeOpenAndImport():
            fileMenu.fetchAction(Entries.File_Import, self.importFile)
        fileMenu.fetchAction(Entries.File_Save_All, self.saveAll)
        fileMenu.fetchAction(Entries.File_Exit, self.closeAll)

        editMenu = menuBar.fetchMenu(Entries.Edit)
        editMenu.fetchAction(Entries.Edit_Settings, self.settings)

        viewMenu = menuBar.fetchMenu(Entries.View)
        self.updateDrawingAttributes(viewMenu)

        scriptsMenu = menuBar.fetchMenu(Entries.Scripts)
        self.updateExtensions(scriptsMenu)

        windowMenu = menuBar.fetchMenu(Entries.Window)
        if platformSpecific.windowCommandsInMenu(
        ) and activeWindow is not None:
            windowMenu.fetchAction(Entries.Window_Minimize,
                                   activeWindow.showMinimized)
            windowMenu.fetchAction(Entries.Window_Minimize_All,
                                   self.minimizeAll)
            windowMenu.fetchAction(Entries.Window_Zoom,
                                   lambda: self.zoom(activeWindow))
        windowMenu.fetchAction(Entries.Window_Scripting, self.scripting)
        if self.outputWindow is not None:
            windowMenu.fetchAction(Entries.Window_Output, self.output)
        # TODO: add a list of open windows in window menu, check active window
        # maybe add helper function that filters topLevelWidgets into windows
        # bc we need this in a few places

        helpMenu = menuBar.fetchMenu(Entries.Help)
        helpMenu.fetchAction(
            Entries.Help_Documentation,
            lambda: QDesktopServices.openUrl(QUrl("https://elih.blog/")))
        helpMenu.fetchAction(
            Entries.Help_Report_An_Issue, lambda: QDesktopServices.openUrl(
                QUrl("https://github.com/eliheuer/simplefont/issues/new")))
        helpMenu.addSeparator()
        helpMenu.fetchAction(Entries.Help_About, self.about)

    def updateMenuBar(self):
        window = self.activeWindow()
        if window is not None and hasattr(window, "setupMenu"):
            menuBar = self.fetchMenuBar(window)
            window.setupMenu(menuBar)
            menuBar.setSpawnElementsHint(False)
            self.setupMenuBar(menuBar)
            menuBar.setSpawnElementsHint(True)
        else:
            self.setupMenuBar()

    # ---------
    # Scripting
    # ---------

    def allFonts(self):
        fonts = []
        for widget in self.topLevelWidgets():
            if isinstance(widget, FontWindow):
                font = widget.font_()
                fonts.append(font)
        return fonts

    def currentFont(self):
        # might be None when closing all windows with scripting window open
        if self._currentFontWindow is None:
            return None
        return self._currentFontWindow.font_()

    def currentGlyph(self):
        return self._currentGlyph

    def setCurrentGlyph(self, glyph):
        if glyph == self._currentGlyph:
            return
        self._currentGlyph = glyph
        self.postNotification("currentGlyphChanged")

    def globals(self):
        global_vars = {
            "__builtins__": __builtins__,
            "AllFonts": self.allFonts,
            "CurrentFont": self.currentFont,
            "CurrentGlyph": self.currentGlyph,
            "events": self.dispatcher,
            "registerExtension": self.registerExtension,
            "unregisterExtension": self.unregisterExtension,
            "registerTool": self.registerTool,
            "unregisterTool": self.unregisterTool,
            "qApp": self,
        }
        return global_vars

    # directory getters

    def _getLocalDirectory(self, key, name):
        userPath = settings.value(key, type=str)
        if userPath and os.path.isdir(userPath):
            return userPath

        appDataFolder = QStandardPaths.standardLocations(
            QStandardPaths.AppLocalDataLocation)[0]
        subFolder = os.path.normpath(os.path.join(appDataFolder, name))

        if not os.path.exists(subFolder):
            try:
                os.makedirs(subFolder)
            except OSError:
                subFolder = os.path.expanduser("~")

        settings.setValue(key, subFolder)
        return subFolder

    def getExtensionsDirectory(self):
        return self._getLocalDirectory("scripting/extensionsPath",
                                       "Extensions")

    def getScriptsDirectory(self):
        return self._getLocalDirectory("scripting/scriptsPath", "Scripts")

    # -------------
    # Drawing tools
    # -------------

    def drawingTools(self):
        return self._drawingTools

    def registerTool(self, tool):
        self._drawingTools.append(tool)
        data = dict(tool=tool)
        self.postNotification("drawingToolRegistered", data)

    def unregisterTool(self, tool):
        self._drawingTools.remove(tool)
        data = dict(tool=tool)
        self.postNotification("drawingToolUnregistered", data)

    # ----------
    # Extensions
    # ----------

    def extensions(self):
        return self._extensions

    def registerExtension(self, extension):
        self._extensions.append(extension)
        self.updateMenuBar()
        data = dict(extension=extension)
        self.postNotification("extensionRegistered", data)

    def unregisterExtension(self, extension):
        self._extensions.remove(extension)
        self.updateMenuBar()
        data = dict(extension=extension)
        self.postNotification("extensionUnregistered", data)

    def updateExtensions(self, menu):
        def getFunc(ext, path):
            # need a stack frame here to return a unique lambda for each run
            return lambda: ext.run(path)

        menu.clear()
        # also clear submenus
        for child in menu.children():
            if isinstance(child, menu.__class__):
                child.setParent(None)
                child.deleteLater()

        for extension in self._extensions:
            addToMenu = extension.addToMenu
            if addToMenu:
                if isinstance(addToMenu, list):
                    parentMenu = menu.addMenu(extension.name or "")
                else:
                    addToMenu = [addToMenu]
                    parentMenu = menu
                for entry in addToMenu:
                    menuName = entry.get("name")
                    menuPath = entry.get("path")
                    shortcut = entry.get("shortcut")
                    parentMenu.addAction(menuName,
                                         getFunc(extension,
                                                 menuPath), shortcut)
        menu.addSeparator()
        menu.addAction(self.tr(Entries.Scripts_Build_Extension),
                       self.extensionBuilder)

    # ----------------
    # Menu Bar entries
    # ----------------

    def newFile(self):
        font = TFont.new()
        window = FontWindow(font)
        window.show()

    def openFile(self, path=None):
        self._openFile(path, importFile=platformSpecific.mergeOpenAndImport())

    def importFile(self):
        self._openFile(openFile=False, importFile=True)

    def _openFile(self, path=None, openFile=True, importFile=False):
        if not path:
            # formats
            fileFormats = []
            supportedFiles = ""
            if openFile:
                packageAsFile = platformSpecific.treatPackageAsFile()
                if packageAsFile:
                    ufoFormat = "*.ufo"
                    tfExtFormat = "*.tfExt"
                else:
                    ufoFormat = "metainfo.plist"
                    tfExtFormat = "info.plist"
                fileFormats.extend([
                    self.tr("UFO Fonts {}").format("(%s)" % ufoFormat),
                    self.tr("SimpleFont Extension {}").format("(%s)" %
                                                              tfExtFormat)
                ])
                supportedFiles += "{} {} ".format(ufoFormat, tfExtFormat)
            if importFile:
                # TODO: systematize this
                fileFormats.extend([
                    self.tr("OpenType Font file {}").format("(*.otf *.ttf)"),
                    self.tr("Type1 Font file {}").format("(*.pfa *.pfb)"),
                    self.tr("ttx Font file {}").format("(*.ttx)"),
                    self.tr("WOFF Font file {}").format("(*.woff *.woff2)"),
                ])
                supportedFiles += "*.otf *.pfa *.pfb *.ttf *.ttx *.woff"
            fileFormats.extend([
                self.tr("All supported files {}").format(
                    "(%s)" % supportedFiles.rstrip()),
                self.tr("All files {}").format("(*.*)"),
            ])
            # dialog
            importKey = importFile and not openFile
            state = settings.openFileDialogState(
            ) if not importKey else settings.importFileDialogState()
            directory = None if state else QStandardPaths.standardLocations(
                QStandardPaths.DocumentsLocation)[0]
            title = self.tr("Open File") if openFile else self.tr(
                "Import File")
            dialog = QFileDialog(self.activeWindow(), title, directory,
                                 ";;".join(fileFormats))
            if state:
                dialog.restoreState(state)
            dialog.setAcceptMode(QFileDialog.AcceptOpen)
            dialog.setFileMode(QFileDialog.ExistingFile)
            dialog.setNameFilter(fileFormats[-2])
            ret = dialog.exec_()
            # save current directory
            # TODO: should open w/o file chooser also update current dir?
            state = dialog.saveState()
            if importKey:
                settings.setImportFileDialogState(directory)
            else:
                settings.setOpenFileDialogState(directory)
            # cancelled?
            if not ret:
                return
            path = dialog.selectedFiles()[0]
        # sanitize
        path = os.path.normpath(path)
        if ".plist" in os.path.basename(path):
            path = os.path.dirname(path)
        ext = os.path.splitext(path)[1]
        if ext == ".ufo":
            self._loadUFO(path)
        elif ext == ".tfExt":
            self._loadExt(path)
        else:
            self._loadBinary(path)

    def _loadBinary(self, path):
        for widget in self.topLevelWidgets():
            if isinstance(widget, FontWindow):
                font = widget.font_()
                if font is not None and font.binaryPath == path:
                    widget.raise_()
                    return
        font = TFont()
        try:
            font.extract(path)
            self._loadFont(font)
        except Exception as e:
            errorReports.showCriticalException(e)
            return
        self.setCurrentFile(font.binaryPath)

    def _loadExt(self, path):
        # TODO: put version check in place
        e = TExtension(path)
        e.install()

    def _loadUFO(self, path):
        for widget in self.topLevelWidgets():
            if isinstance(widget, FontWindow):
                font = widget.font_()
                if font is not None and font.path == path:
                    widget.raise_()
                    return
        try:
            font = TFont(path)
            self._loadFont(font)
        except Exception as e:
            msg = self.tr("There was an issue opening the font at {}.").format(
                path)
            errorReports.showCriticalException(e, msg)
            return
        self.setCurrentFile(font.path)

    def _loadFont(self, font):
        currentFont = self.currentFont()
        # Open new font in current font window if it contains an unmodified
        # empty font (e.g. after startup).
        if currentFont is not None and currentFont.path is None and \
                currentFont.binaryPath is None and currentFont.dirty is False:
            window = self._currentFontWindow
            window.setFont_(font)
        else:
            window = FontWindow(font)
        window.show()

    def openRecentFile(self):
        fontPath = self.sender().toolTip()
        self.openFile(fontPath)

    def clearRecentFiles(self):
        settings.setRecentFiles([])

    def saveAll(self):
        for widget in self.topLevelWidgets():
            if isinstance(widget, FontWindow):
                widget.saveFile()

    def closeAll(self):
        for widget in self.topLevelWidgets():
            if isinstance(widget, FontWindow):
                widget.close()
        # loop again to see if user kept font windows open
        for widget in self.topLevelWidgets():
            if isinstance(widget, FontWindow):
                return
        self.quit()

    # Edit

    def settings(self):
        if hasattr(self, '_settingsWindow') and \
                self._settingsWindow.isVisible():
            self._settingsWindow.raise_()
        else:
            self._settingsWindow = SettingsWindow()
            self._settingsWindow.show()

    # Scripts

    def extensionBuilder(self):
        # TODO: don't store, spawn window each time instead
        # or have tabs?
        if not hasattr(self, '_extensionBuilderWindow'):
            self._extensionBuilderWindow = ExtensionBuilderWindow()
        if self._extensionBuilderWindow.isVisible():
            self._extensionBuilderWindow.raise_()
        else:
            self._extensionBuilderWindow.show()

    # Window

    def minimizeAll(self):
        for widget in self.topLevelWidgets():
            if widget.isVisible():
                # additional guard, shouldnt be needed
                # if isinstance(widget, (QMenu, QMenuBar)):
                #     continue
                widget.showMinimized()

    def zoom(self, window):
        if window.isMaximized():
            window.showNormal()
        else:
            window.showMaximized()

    def scripting(self):
        # TODO: don't store, spawn window each time instead
        # or have tabs?
        if not hasattr(self, '_scriptingWindow'):
            self._scriptingWindow = ScriptingWindow()
        if self._scriptingWindow.isVisible():
            self._scriptingWindow.raise_()
        else:
            self._scriptingWindow.show()

    def output(self):
        self.outputWindow.setVisible(not self.outputWindow.isVisible())

    # Help

    def about(self):
        AboutDialog(self.activeWindow()).exec_()

    # ------------
    # Recent files
    # ------------

    def setCurrentFile(self, path):
        if path is None:
            return
        path = os.path.abspath(path)
        recentFiles = settings.recentFiles()
        if path in recentFiles:
            recentFiles.remove(path)
        recentFiles.insert(0, path)
        while len(recentFiles) > MAX_RECENT_FILES:
            del recentFiles[-1]
        settings.setRecentFiles(recentFiles)

    def updateRecentFiles(self, menu):
        # bootstrap
        actions = menu.actions()
        for i in range(MAX_RECENT_FILES):
            try:
                action = actions[i]
            except IndexError:
                action = QAction(menu)
                menu.addAction(action)
            action.setVisible(False)
            action.triggered.connect(self.openRecentFile)
        try:
            actions[MAX_RECENT_FILES]
        except IndexError:
            menu.addSeparator()
            action = QAction(menu)
            action.setText(self.tr("Clear Menu"))
            action.triggered.connect(self.clearRecentFiles)
            menu.addAction(action)

        # fill
        actions = menu.actions()
        recentFiles = settings.recentFiles()
        count = min(len(recentFiles), MAX_RECENT_FILES)
        for index, recentFile in enumerate(recentFiles[:count]):
            action = actions[index]
            shortName = os.path.basename(recentFile.rstrip(os.sep))

            action.setText(shortName)
            action.setToolTip(recentFile)
            action.setVisible(True)
        for index in range(count, MAX_RECENT_FILES):
            actions[index].setVisible(False)

        menu.setEnabled(len(recentFiles))
        # TODO: put recent files in dock on OSX
        # import sys
        # if sys.platform == "darwin":
        #     menu.setAsDockMenu()

    # ------------------
    # Drawing attributes
    # ------------------

    def setDrawingAttribute(self):
        sender = self.sender()
        drawingAttributes = settings.drawingAttributes()
        checked = sender.isChecked()
        for attr in sender.data():
            drawingAttributes[attr] = checked
        settings.setDrawingAttributes(drawingAttributes)
        self.postNotification("preferencesChanged")

    def updateDrawingAttributes(self, menu):
        drawingAttributes = settings.drawingAttributes()
        elements = [
            (Entries.View_Show_Points, ("showGlyphOnCurvePoints",
                                        "showGlyphOffCurvePoints")),
            (Entries.View_Show_Metrics,
             ("showGlyphMetrics", "showFontVerticalMetrics",
              "showFontPostscriptBlues")),
            (Entries.View_Show_Images, ("showGlyphImage", )),
            (Entries.View_Show_Guidelines, ("showGlyphGuidelines",
                                            "showFontGuidelines")),
        ]
        for entry, attrs in elements:
            action = menu.fetchAction(entry)
            action.setCheckable(True)
            action.setChecked(drawingAttributes.get(attrs[0], True))
            action.setData(attrs)
            action.triggered.connect(self.setDrawingAttribute)
 def test_areNotificationsDisabled_observer_off(self):
     center = NotificationCenter()
     observable1 = _TestObservable(center, "Observable1")
     observer1 = NotificationTestObserver()
     observer2 = NotificationTestObserver()
     center.addObserver(observer1, "notificationCallback", "A", observable1)
     center.addObserver(observer2, "notificationCallback", "A", observable1)
     center.disableNotifications(observer=observer1)
     self.assertTrue(center.areNotificationsDisabled(observer=observer1))
     self.assertFalse(center.areNotificationsDisabled(observer=observer2))
     center.enableNotifications(observer=observer1)
     self.assertFalse(center.areNotificationsDisabled(observer=observer1))
Example #30
0
 def _buildFindObservationsObjects(self):
     center = NotificationCenter()
     observable1 = _TestObservable(center, "Observable1")
     observable2 = _TestObservable(center, "Observable2")
     observer1 = NotificationTestObserver(name="Observer1")
     observer2 = NotificationTestObserver(name="Observer2")
     center.addObserver(observer1,
                        "notificationCallback",
                        "A",
                        observable1,
                        identifier="identifier1-1-A")
     center.addObserver(observer1,
                        "notificationCallback",
                        "B",
                        observable1,
                        identifier="identifier1-1-B")
     center.addObserver(observer1,
                        "notificationCallback",
                        "A",
                        observable2,
                        identifier="identifier1-2-A")
     center.addObserver(observer1,
                        "notificationCallback",
                        "B",
                        observable2,
                        identifier="identifier1-2-B")
     center.addObserver(observer2,
                        "notificationCallback",
                        "A",
                        observable1,
                        identifier="identifier2-1-A")
     center.addObserver(observer2,
                        "notificationCallback",
                        "B",
                        observable1,
                        identifier="identifier2-1-B")
     center.addObserver(observer2,
                        "notificationCallback",
                        "A",
                        observable2,
                        identifier="identifier2-2-A")
     center.addObserver(observer2,
                        "notificationCallback",
                        "B",
                        observable2,
                        identifier="identifier2-2-B")
     return center, observer1, observer2, observable1, observable2
 def test_areNotificationsHeld_observer_off(self):
     center = NotificationCenter()
     observable = _TestObservable(center, "Observable")
     observer1 = NotificationTestObserver()
     observer2 = NotificationTestObserver()
     center.addObserver(observer1, "notificationCallback", "A", observable)
     center.addObserver(observer2, "notificationCallback", "A", observable)
     center.holdNotifications(observer=observer1)
     self.assertTrue(center.areNotificationsHeld(observer=observer1))
     self.assertFalse(center.areNotificationsHeld(observer=observer2))
     center.releaseHeldNotifications(observer=observer1)
     self.assertFalse(center.areNotificationsHeld(observer=observer1))
 def test_areNotificationsHeld_observable_off(self):
     center = NotificationCenter()
     observable1 = _TestObservable(center, "Observable1")
     observable2 = _TestObservable(center, "Observable2")
     observer = NotificationTestObserver()
     center.addObserver(observer, "notificationCallback", "A", observable1)
     center.addObserver(observer, "notificationCallback", "B", observable2)
     center.holdNotifications(observable=observable1)
     self.assertTrue(center.areNotificationsHeld(observable=observable1))
     self.assertFalse(center.areNotificationsHeld(observable=observable2))
     center.releaseHeldNotifications(observable=observable1)
     self.assertFalse(center.areNotificationsHeld(observable=observable1))
Example #33
0
 def __init__(self):
     self._dispatcher = NotificationCenter()
Example #34
0
class PyREPLSettings(object):
    def __init__(self):
        self._dispatcher = NotificationCenter()

    def __repr__(self):
        return "<Editor Settings Manager. Type \"settings.help\" for documentation.>"

    def _get_help(self):
        # LOL. This is probably very illegal.
        print(settingsManagerDoc)

    help = property(_get_help)

    # Notifications

    def addObserver(self,
                    obj,
                    methodName,
                    notification="PyREPL.SettingsChanged"):
        self._dispatcher.addObserver(obj,
                                     methodName,
                                     notification=notification,
                                     observable=self)

    def removeObserver(self, obj, notification="PyREPL.SettingsChanged"):
        self._dispatcher.removeObserver(obj, notification, self)

    def postNotification(self,
                         notification="PyREPL.SettingsChanged",
                         data=None):
        self._dispatcher.postNotification(notification, self, data)

    # Properties

    windowWidth = settingsProperty("windowWidth", settingsWindowSizeValidator)
    windowHeight = settingsProperty("windowHeight",
                                    settingsWindowSizeValidator)
    fontName = settingsProperty("fontName", settingsStringValidator)
    fontSize = settingsProperty("fontSize", settingsPositiveNumberValidator)
    colorCode = settingsProperty("colorCode", settingsColorValidator)
    colorStdout = settingsProperty("colorStdout", settingsColorValidator)
    colorStderr = settingsProperty("colorStderr", settingsColorValidator)
    colorBackground = settingsProperty("colorBackground",
                                       settingsColorValidator)
    bannerGreeting = settingsProperty("bannerGreeting",
                                      settingsStringValidator)
    startupCode = settingsProperty("startupCode", settingsStringValidator)
    tabString = settingsProperty("tabString", settingsStringValidator)
    showInvisibleCharacters = settingsProperty("showInvisibleCharacters",
                                               settingsBoolValidator)

    def editorItems(self):
        d = dict(fontName=self.fontName,
                 fontSize=self.fontSize,
                 colorCode=self.colorCode,
                 colorStdout=self.colorStdout,
                 colorStderr=self.colorStderr,
                 colorBackground=self.colorBackground,
                 tabString=self.tabString,
                 showInvisibleCharacters=self.showInvisibleCharacters)
        return d.items()

    # Fonts

    def _get_availableFonts(self):
        manager = NSFontManager.sharedFontManager()
        for name in manager.availableFonts():
            font = NSFont.fontWithName_size_(name, 10)
            if font.isFixedPitch():
                print(name)

    availableFonts = property(_get_availableFonts)

    # Startup Code

    def editStartupCode(self):
        self.postNotification(notification="PyREPL.ShowStartupCodeEditor")

    # Themes

    def loadTheme(self, name):
        userThemes = getDefaultValue("userThemes")
        if name in userThemes:
            theme = userThemes[name]
        elif name in defaultThemes:
            theme = defaultThemes[name]
        else:
            raise PyREPLSettingsError("No theme named %r." % name)
        self.colorCode = theme["colorCode"]
        self.colorStdout = theme["colorStdout"]
        self.colorStderr = theme["colorStderr"]
        self.colorBackground = theme["colorBackground"]

    def saveTheme(self, name):
        if not settingsStringValidator(name):
            raise PyREPLSettingsError("Theme names must be strings.")
        theme = dict(colorCode=self.colorCode,
                     colorStderr=self.colorStderr,
                     colorStdout=self.colorStdout,
                     colorBackground=self.colorBackground)
        userThemes = getDefaultValue("userThemes")
        userThemes[name] = theme
        setDefaultValue("userThemes", userThemes)
Example #35
0
class PyREPLSettings(object):
    def __init__(self):
        self._dispatcher = NotificationCenter()

    def __repr__(self):
        return "<Editor Settings Manager. Type \"settings.help\" for documentation.>"

    def _get_help(self):
        # LOL. This is probably very illegal.
        print(settingsManagerDoc)

    help = property(_get_help)

    # Notifications

    def addObserver(self,
                    obj,
                    methodName,
                    notification="PyREPL.SettingsChanged"):
        self._dispatcher.addObserver(obj,
                                     methodName,
                                     notification=notification,
                                     observable=self)

    def removeObserver(self, obj, notification="PyREPL.SettingsChanged"):
        self._dispatcher.removeObserver(obj, notification, self)

    def postNotification(self,
                         notification="PyREPL.SettingsChanged",
                         data=None):
        self._dispatcher.postNotification(notification, self, data)

    # Properties

    windowWidth = settingsProperty("windowWidth", settingsWindowSizeValidator)
    windowHeight = settingsProperty("windowHeight",
                                    settingsWindowSizeValidator)
    fontName = settingsProperty("fontName", settingsStringValidator)
    fontSize = settingsProperty("fontSize", settingsPositiveNumberValidator)
    colorCode = settingsProperty("colorCode", settingsColorValidator)
    colorStdout = settingsProperty("colorStdout", settingsColorValidator)
    colorStderr = settingsProperty("colorStderr", settingsColorValidator)
    colorBackground = settingsProperty("colorBackground",
                                       settingsColorValidator)
    bannerGreeting = settingsProperty("bannerGreeting",
                                      settingsStringValidator)
    startupCode = settingsProperty("startupCode", settingsStringValidator)
    tabString = settingsProperty("tabString", settingsStringValidator)
    showInvisibleCharacters = settingsProperty("showInvisibleCharacters",
                                               settingsBoolValidator)

    def editorItems(self):
        d = dict(fontName=self.fontName,
                 fontSize=self.fontSize,
                 colorCode=self.colorCode,
                 colorStdout=self.colorStdout,
                 colorStderr=self.colorStderr,
                 colorBackground=self.colorBackground,
                 tabString=self.tabString,
                 showInvisibleCharacters=self.showInvisibleCharacters)
        return d.items()

    # Fonts

    def _get_availableFonts(self):
        manager = NSFontManager.sharedFontManager()
        for name in manager.availableFonts():
            font = NSFont.fontWithName_size_(name, 10)
            if font.isFixedPitch():
                print(name)

    availableFonts = property(_get_availableFonts)

    # Startup Code

    def editStartupCode(self):
        self.postNotification(notification="PyREPL.ShowStartupCodeEditor")

    # Themes

    def loadTheme(self, name):
        userThemes = getDefaultValue("userThemes")
        if name in userThemes:
            theme = userThemes[name]
        elif name in defaultThemes:
            theme = defaultThemes[name]
        else:
            raise PyREPLSettingsError("No theme named %r." % name)
        self.colorCode = theme["colorCode"]
        self.colorStdout = theme["colorStdout"]
        self.colorStderr = theme["colorStderr"]
        self.colorBackground = theme["colorBackground"]

    def saveTheme(self, name):
        if not settingsStringValidator(name):
            raise PyREPLSettingsError("Theme names must be strings.")
        theme = dict(colorCode=self.colorCode,
                     colorStderr=self.colorStderr,
                     colorStdout=self.colorStdout,
                     colorBackground=self.colorBackground)
        userThemes = getDefaultValue("userThemes")
        userThemes[name] = theme
        setDefaultValue("userThemes", userThemes)

    def exportSettings(self):
        exportPath = vanilla.dialogs.putFile(
            messageText="Export RoboREPL Settings",
            fileName="Settings.roboREPLSettings")

        if exportPath:
            d = dict(windowWidth=int(self.windowWidth),
                     windowHeight=int(self.windowHeight),
                     fontName=str(self.fontName),
                     fontSize=int(self.fontSize),
                     colorCode=tuple(self.colorCode),
                     colorStdout=tuple(self.colorStdout),
                     colorStderr=tuple(self.colorStderr),
                     colorBackground=tuple(self.colorBackground),
                     bannerGreeting=str(self.bannerGreeting),
                     startupCode=str(self.startupCode),
                     tabString=str(self.tabString),
                     showInvisibleCharacters=bool(
                         self.showInvisibleCharacters),
                     userThemes=dict(getDefaultValue("userThemes")))

            with open(exportPath, 'wb') as f:
                plistlib.dump(d, f)

    def importSettings(self):
        importPath = (vanilla.dialogs.getFile(
            messageText="Import RoboREPL Settings",
            fileTypes=["roboREPLSettings"]))[0]

        if importPath:
            with open(importPath, 'rb') as f:
                try:
                    d = plistlib.load(f)
                except:
                    raise PyREPLSettingsError(
                        "There was an error when loading settings file: %s." %
                        importPath)

            if "windowWidth" in d.keys():
                self.windowWidth = int(d["windowWidth"])
            if "windowHeight" in d.keys():
                self.windowHeight = int(d["windowHeight"])
            if "fontName" in d.keys():
                self.fontName = str(d["fontName"])
            if "fontSize" in d.keys():
                self.fontSize = int(d["fontSize"])
            if "colorCode" in d.keys():
                self.colorCode = tuple(d["colorCode"])
            if "colorStdout" in d.keys():
                self.colorStdout = tuple(d["colorStdout"])
            if "colorStderr" in d.keys():
                self.colorStderr = tuple(d["colorStderr"])
            if "colorBackground" in d.keys():
                self.colorBackground = tuple(d["colorBackground"])
            if "bannerGreeting" in d.keys():
                self.bannerGreeting = str(d["bannerGreeting"])
            if "startupCode" in d.keys():
                self.startupCode = str(d["startupCode"])
            if "tabString" in d.keys():
                self.tabString = str(d["tabString"])
            if "showInvisibleCharacters" in d.keys():
                self.showInvisibleCharacters = bool(
                    d["showInvisibleCharacters"])
            if "userThemes" in d.keys():
                setDefaultValue("userThemes", dict(d["userThemes"]))
Example #36
0
 def test_dispatcher(self):
     notificationCenter = NotificationCenter()
     obj = BaseObject()
     obj._dispatcher = weakref.ref(notificationCenter)
     self.assertEqual(obj.dispatcher, notificationCenter)
Example #37
0
 def __init__(self):
     self._dispatcher = NotificationCenter()
Example #38
0
class Application(QApplication):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._currentGlyph = None
        self._currentMainWindow = None
        self._launched = False
        self._drawingTools = [SelectionTool, PenTool, RulerTool, KnifeTool]
        self._extensions = []
        self.dispatcher = NotificationCenter()
        self.dispatcher.addObserver(self, "_mainWindowClosed", "fontWillClose")
        # TODO: see about filtering this into windowChanged
        # except if we're going to use tabs with changing menu, then it might
        # be convenient to no filter out (no additional signal has to be sent)
        self.focusChanged.connect(self._focusWidgetChanged)
        self.GL2UV = None
        self.inspectorWindow = None
        self.outputWindow = None

    # --------------
    # Event handling
    # --------------

    def _focusWidgetChanged(self):
        # update menu bar
        self.updateMenuBar()
        # update main window
        window = self.activeWindow()
        if window is None:
            return
        while True:
            parent = window.parent()
            if parent is None:
                break
            window = parent
        if isinstance(window, FontWindow):
            self.setCurrentMainWindow(window)

    def _mainWindowClosed(self, notification):
        font = notification.data["font"]
        # cleanup CurrentFont/CurrentGlyph when closing the corresponding
        # window
        if self._currentMainWindow is not None:
            if self._currentMainWindow.font == font:
                self.setCurrentMainWindow(None)
        if self._currentGlyph is not None:
            if self._currentGlyph.font == font:
                self.setCurrentGlyph(None)

    def event(self, event):
        eventType = event.type()
        # respond to OSX open events
        if eventType == QEvent.FileOpen:
            filePath = event.file()
            self.openFile(filePath)
            return True
        elif eventType == QEvent.ApplicationStateChange:
            applicationState = self.applicationState()
            if applicationState == Qt.ApplicationActive:
                if not self._launched:
                    notification = "applicationLaunched"
                    self.loadGlyphList()
                    self._launched = True
                else:
                    notification = "applicationActivated"
                    # XXX: do it
                    # self.lookupExternalChanges()
                self.postNotification(notification)
            elif applicationState == Qt.ApplicationInactive:
                self.postNotification("applicationWillIdle")
        return super().event(event)

    def postNotification(self, notification, data=None):
        dispatcher = self.dispatcher
        dispatcher.postNotification(
            notification=notification, observable=self, data=data)

    # ---------------
    # File management
    # ---------------

    def loadGlyphList(self):
        glyphListPath = settings.glyphListPath()
        if glyphListPath and os.path.exists(glyphListPath):
            try:
                glyphList_ = glyphList.parseGlyphList(glyphListPath)
            except Exception as e:
                msg = self.tr(
                    "The glyph list at {0} cannot "
                    "be parsed and will be dropped.").format(glyphListPath)
                errorReports.showWarningException(e, msg)
                settings.removeGlyphListPath()
            else:
                self.GL2UV = glyphList_

    def lookupExternalChanges(self):
        for font in self.allFonts():
            if not font.path:
                continue
            changed = font.testForExternalChanges()
            for attr in ("info", "kerning", "groups", "features", "lib"):
                if changed[attr]:
                    data = dict(font=font)
                    self.postNotification("fontChangedExternally", data)
                    return
            # XXX: do more

    # -----------------
    # Window management
    # -----------------

    def currentMainWindow(self):
        return self._currentMainWindow

    def setCurrentMainWindow(self, mainWindow):
        if mainWindow == self._currentMainWindow:
            return
        self._currentMainWindow = mainWindow
        self.postNotification("currentFontChanged")

    def openMetricsWindow(self, font):
        # TODO: why are we doing this for metrics window and no other child
        # window?
        for widget in self.topLevelWidgets():
            if isinstance(widget, FontWindow) and widget.font_() == font:
                widget.metrics()
                return widget._metricsWindow
        return None

    # --------
    # Menu Bar
    # --------

    def fetchMenuBar(self, parent=None):
        if platformSpecific.useGlobalMenuBar():
            try:
                self._menuBar
            except:
                self._menuBar = globalMenuBar()
            self._menuBar.resetState()
            return self._menuBar
        return MenuBar(parent)

    def setupMenuBar(self, menuBar=None):
        if menuBar is None:
            try:
                menuBar = self._menuBar
            except:
                return
            menuBar.resetState()
        activeWindow = self.activeWindow()
        # XXX: on local menu bar, entries shouldnt be activated if they werent
        # fetched by local
        # in that case, maybe return a parentless action, that isnt added to
        # the menu
        fileMenu = menuBar.fetchMenu(Entries.File)
        fileMenu.fetchAction(Entries.File_New, self.newFile)
        fileMenu.fetchAction(Entries.File_Open, self.openFile)
        # TODO: maybe move save in there and add save all and close
        recentFilesMenu = fileMenu.fetchMenu(Entries.File_Open_Recent)
        self.updateRecentFiles(recentFilesMenu)
        fileMenu.fetchAction(Entries.File_Exit, self.exit)

        scriptsMenu = menuBar.fetchMenu(Entries.Scripts)
        self.updateExtensions(scriptsMenu)

        windowMenu = menuBar.fetchMenu(Entries.Window)
        if platformSpecific.windowCommandsInMenu(
                ) and activeWindow is not None:
            windowMenu.fetchAction(
                Entries.Window_Minimize, activeWindow.showMinimized)
            windowMenu.fetchAction(
                Entries.Window_Minimize_All, self.minimizeAll)
            windowMenu.fetchAction(
                Entries.Window_Zoom, lambda: self.zoom(activeWindow))
        windowMenu.fetchAction(Entries.Window_Inspector, self.inspector)
        windowMenu.fetchAction(Entries.Window_Scripting, self.scripting)
        if self.outputWindow is not None:
            windowMenu.fetchAction(
                Entries.Window_Output, self.output)
        # TODO: add a list of open windows in window menu, check active window
        # maybe add helper function that filters topLevelWidgets into windows
        # bc we need this in a few places

        helpMenu = menuBar.fetchMenu(Entries.Help)
        helpMenu.fetchAction(
            Entries.Help_Documentation,
            lambda: QDesktopServices.openUrl(
                QUrl("http://trufont.github.io/")))
        helpMenu.fetchAction(
            Entries.Help_Report_An_Issue,
            lambda: QDesktopServices.openUrl(
                QUrl("https://github.com/trufont/trufont/issues/new")))
        helpMenu.addSeparator()
        helpMenu.fetchAction(Entries.Help_About, self.about)

    def updateMenuBar(self):
        window = self.activeWindow()
        if window is None:
            self.setupMenuBar()
            return
        # update menu
        if hasattr(window, "setupMenu"):
            # TODO: convoluted. try to reduce the number of calls
            menuBar = self.fetchMenuBar(window)
            window.setupMenu(menuBar)
            menuBar.setSpawnElementsHint(False)
            self.setupMenuBar(menuBar)
            menuBar.setSpawnElementsHint(True)
            window.setMenuBar(menuBar)
        else:
            self.setupMenuBar()

    # ---------
    # Scripting
    # ---------

    def allFonts(self):
        fonts = []
        for widget in self.topLevelWidgets():
            if isinstance(widget, FontWindow):
                font = widget.font_()
                fonts.append(font)
        return

    def currentFont(self):
        # might be None when closing all windows with scripting window open
        if self._currentMainWindow is None:
            return None
        return self._currentMainWindow.font_()

    def currentGlyph(self):
        return self._currentGlyph

    def setCurrentGlyph(self, glyph):
        if glyph == self._currentGlyph:
            return
        self._currentGlyph = glyph
        self.postNotification("currentGlyphChanged")

    def globals(self):
        global_vars = {
            "__builtins__": __builtins__,
            "AllFonts": self.allFonts,
            "CurrentFont": self.currentFont,
            "CurrentGlyph": self.currentGlyph,
            "events": self.dispatcher,
            "registerTool": self.registerTool,
            "OpenMetricsWindow": self.openMetricsWindow,
            "qApp": self,
        }
        return global_vars

    # directory getters

    def _getLocalDirectory(self, key, name):
        userPath = settings.value(key, type=str)
        if userPath and os.path.isdir(userPath):
            return userPath

        appDataFolder = QStandardPaths.standardLocations(
            QStandardPaths.AppLocalDataLocation)[0]
        subFolder = os.path.normpath(os.path.join(
            appDataFolder, name))

        if not os.path.exists(subFolder):
            try:
                os.makedirs(subFolder)
            except OSError:
                subFolder = os.path.expanduser("~")

        settings.setValue(key, subFolder)
        return subFolder

    def getExtensionsDirectory(self):
        return self._getLocalDirectory(
            "scripting/extensionsPath", "Extensions")

    def getScriptsDirectory(self):
        return self._getLocalDirectory("scripting/scriptsPath", "Scripts")

    # -------------
    # Drawing tools
    # -------------

    def drawingTools(self):
        return self._drawingTools

    def registerTool(self, tool):
        self._drawingTools.append(tool)
        data = dict(tool=tool)
        self.postNotification("drawingToolRegistered", data)

    def unregisterTool(self, tool):
        self._drawingTools.remove(tool)
        data = dict(tool=tool)
        self.postNotification("drawingToolUnregistered", data)

    # ----------
    # Extensions
    # ----------

    def extensions(self):
        return self._extensions

    def registerExtension(self, extension):
        self._extensions.append(extension)
        self.updateMenuBar()
        data = dict(extension=extension)
        self.postNotification("extensionRegistered", data)

    def unregisterExtension(self, extension):
        self._extensions.remove(extension)
        self.updateMenuBar()
        data = dict(extension=extension)
        self.postNotification("extensionUnregistered", data)

    def updateExtensions(self, menu):
        def getFunc(ext, path):
            # need a stack frame here to return a unique lambda for each run
            return lambda: ext.run(path)

        menu.clear()
        # also clear submenus
        for child in menu.children():
            if isinstance(child, menu.__class__):
                child.setParent(None)
                child.deleteLater()

        for extension in self._extensions:
            addToMenu = extension.addToMenu
            if addToMenu:
                if isinstance(addToMenu, list):
                    parentMenu = menu.addMenu(extension.name or "")
                else:
                    addToMenu = [addToMenu]
                    parentMenu = menu
                for entry in addToMenu:
                    menuName = entry.get("name")
                    menuPath = entry.get("path")
                    shortcut = entry.get("shortcut")
                    parentMenu.addAction(
                        menuName, getFunc(extension, menuPath), shortcut)
        menu.addSeparator()
        # TODO
        action = menu.addAction(self.tr(Entries.Scripts_Build_Extension))
        action.setEnabled(False)

    # ----------------
    # Menu Bar entries
    # ----------------

    def newFile(self):
        font = TFont.newStandardFont()
        window = FontWindow(font)
        window.show()

    def openFile(self, path=None):
        if not path:
            fileFormat = self.tr("UFO Fonts {}")
            if platformSpecific.treatPackageAsFile():
                ext = "(*.ufo)"
            else:
                ext = "(metainfo.plist)"

            path, _ = QFileDialog.getOpenFileName(
                self.activeWindow(), self.tr("Open File"), '',
                fileFormat.format(ext)
            )
            if not path:
                return
        if ".plist" in path:
            path = os.path.dirname(path)
        path = os.path.normpath(path)
        for widget in self.topLevelWidgets():
            if isinstance(widget, FontWindow):
                font = widget.font_()
                if font is not None and font.path == path:
                    widget.raise_()
                    return
        try:
            font = TFont(path)
            window = FontWindow(font)
        except Exception as e:
            msg = self.tr(
                "There was an issue when opening the font at {}.".format(
                    path))
            errorReports.showCriticalException(e, msg)
            return
        window.show()
        self.setCurrentFile(font.path)

    def openRecentFile(self):
        fontPath = self.sender().toolTip()
        self.openFile(fontPath)

    # Window

    def minimizeAll(self):
        for widget in self.topLevelWidgets():
            if widget.isVisible():
                # additional guard, shouldnt be needed
                # if isinstance(widget, (QMenu, QMenuBar)):
                #     continue
                widget.showMinimized()

    def zoom(self, window):
        if window.isMaximized():
            window.showNormal()
        else:
            window.showMaximized()

    def inspector(self):
        if self.inspectorWindow is None:
            self.inspectorWindow = InspectorWindow()
        if self.inspectorWindow.isVisible():
            # TODO: do this only if the widget is user-visible, otherwise the
            # key press feels as if it did nothing
            # toggle
            self.inspectorWindow.close()
        else:
            self.inspectorWindow.show()

    def scripting(self):
        # TODO: don't store, spawn window each time instead
        # or have tabs?
        if not hasattr(self, '_scriptingWindow'):
            scriptingWindow = ScriptingWindow()
            scriptingWindow.show()
        elif self._scriptingWindow.isVisible():
            self._scriptingWindow.raise_()
        else:
            self._scriptingWindow.show()

    def output(self):
        self.outputWindow.setVisible(not self.outputWindow.isVisible())

    # Help

    def about(self):
        name = self.applicationName()
        domain = self.organizationDomain()
        caption = self.tr(
            "<h3>About {n}</h3>"
            "<p>{n} is a cross-platform, modular typeface design "
            "application.</p>").format(n=name)
        text = self.tr(
            "<p>{} is built on top of "
            "<a href='http://ts-defcon.readthedocs.org/en/ufo3/'>defcon</a> "
            "and includes scripting support "
            "with a <a href='http://robofab.com/'>robofab</a>-like API.</p>"
            "<p>Running on Qt {} (PyQt {}).</p>"
            "<p>Version {} {} – Python {}.").format(
            name, QT_VERSION_STR, PYQT_VERSION_STR, __version__, gitShortHash,
            platform.python_version())
        if domain:
            text += self.tr("<br>See <a href='http://{d}'>{d}</a> for more "
                            "information.</p>").format(d=domain)
        else:
            text += "</p>"
        # This duplicates much of QMessageBox.about(), but it has no way to
        # setInformativeText()...
        msgBox = QMessageBox(self.activeWindow())
        msgBox.setAttribute(Qt.WA_DeleteOnClose)
        icon = msgBox.windowIcon()
        size = icon.actualSize(QSize(64, 64))
        msgBox.setIconPixmap(icon.pixmap(size))
        msgBox.setWindowTitle(self.tr("About {}").format(name))
        msgBox.setText(caption)
        msgBox.setInformativeText(text)
        if platformSpecific.useCenteredButtons():
            buttonBox = msgBox.findChild(QDialogButtonBox)
            buttonBox.setCenterButtons(True)
        msgBox.show()

    # ------------
    # Recent files
    # ------------

    def setCurrentFile(self, path):
        if path is None:
            return
        path = os.path.abspath(path)
        recentFiles = settings.recentFiles()
        if path in recentFiles:
            recentFiles.remove(path)
        recentFiles.insert(0, path)
        while len(recentFiles) > MAX_RECENT_FILES:
            del recentFiles[-1]
        settings.setRecentFiles(recentFiles)

    def updateRecentFiles(self, menu):
        # bootstrap
        actions = menu.actions()
        for i in range(MAX_RECENT_FILES):
            try:
                action = actions[i]
            except IndexError:
                action = QAction(menu)
                menu.addAction(action)
            action.setVisible(False)
            action.triggered.connect(self.openRecentFile)
        # fill
        actions = menu.actions()
        recentFiles = settings.recentFiles()
        count = min(len(recentFiles), MAX_RECENT_FILES)
        for index, recentFile in enumerate(recentFiles[:count]):
            action = actions[index]
            shortName = os.path.basename(recentFile.rstrip(os.sep))

            action.setText(shortName)
            action.setToolTip(recentFile)
            action.setVisible(True)
        for index in range(count, MAX_RECENT_FILES):
            actions[index].setVisible(False)

        menu.setEnabled(len(recentFiles))
Example #39
0
 def test_removeObserver_allNotifications(self):
     center = NotificationCenter()
     observable1 = _TestObservable(center, "Observable1")
     observable2 = _TestObservable(center, "Observable2")
     observer1 = NotificationTestObserver()
     observer2 = NotificationTestObserver()
     center.addObserver(observer1, "notificationCallback", "A", observable1)
     center.addObserver(observer1, "notificationCallback", "B", observable1)
     center.addObserver(observer2, "notificationCallback", "A", observable1)
     center.addObserver(observer2, "notificationCallback", "B", observable1)
     center.addObserver(observer1, "notificationCallback", "A", observable2)
     center.addObserver(observer1, "notificationCallback", "B", observable2)
     center.addObserver(observer2, "notificationCallback", "A", observable2)
     center.addObserver(observer2, "notificationCallback", "B", observable2)
     center.removeObserver(observer1, "all", observable1)
     self.assertFalse(center.hasObserver(observer1, "A", observable1))
     self.assertFalse(center.hasObserver(observer1, "B", observable1))
     self.assertTrue(center.hasObserver(observer1, "A", observable2))
     self.assertTrue(center.hasObserver(observer1, "B", observable2))
     self.assertTrue(center.hasObserver(observer2, "A", observable1))
     self.assertTrue(center.hasObserver(observer2, "B", observable1))
     self.assertTrue(center.hasObserver(observer2, "A", observable2))
     self.assertTrue(center.hasObserver(observer2, "B", observable2))
Example #40
0
class Application(QApplication):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._currentGlyph = None
        self._currentMainWindow = None
        self._launched = False
        self._drawingTools = [SelectionTool, PenTool, RulerTool, KnifeTool]
        self.dispatcher = NotificationCenter()
        self.dispatcher.addObserver(self, "_mainWindowClosed", "fontWillClose")
        self.focusChanged.connect(self.updateCurrentMainWindow)
        self.GL2UV = None
        self.outputWindow = None

    def event(self, event):
        eventType = event.type()
        # respond to OSX open events
        if eventType == QEvent.FileOpen:
            filePath = event.file()
            self.openFile(filePath)
            return True
        elif eventType == QEvent.ApplicationStateChange:
            applicationState = self.applicationState()
            if applicationState == Qt.ApplicationActive:
                if not self._launched:
                    notification = "applicationLaunched"
                    self.loadGlyphList()
                    self._launched = True
                else:
                    notification = "applicationActivated"
                    # XXX: do it
                    # self.lookupExternalChanges()
                self.postNotification(notification)
                self.updateCurrentMainWindow()
            elif applicationState == Qt.ApplicationInactive:
                self.postNotification("applicationWillIdle")
        return super().event(event)

    def postNotification(self, notification, data=None):
        dispatcher = self.dispatcher
        dispatcher.postNotification(
            notification=notification, observable=self, data=data)

    def loadGlyphList(self):
        glyphListPath = settings.glyphListPath()
        if glyphListPath and os.path.exists(glyphListPath):
            try:
                glyphList_ = glyphList.parseGlyphList(glyphListPath)
            except Exception as e:
                msg = self.tr(
                    "The glyph list at {0} cannot "
                    "be parsed and will be dropped.").format(glyphListPath)
                errorReports.showWarningException(e, msg)
                settings.removeGlyphListPath()
            else:
                self.GL2UV = glyphList_

    def lookupExternalChanges(self):
        for font in self.allFonts():
            if not font.path:
                continue
            changed = font.testForExternalChanges()
            for attr in ("info", "kerning", "groups", "features", "lib"):
                if changed[attr]:
                    data = dict(font=font)
                    self.postNotification("fontChangedExternally", data)
                    return
            # XXX: do more

    def _mainWindowClosed(self, notification):
        font = notification.data["font"]
        # cleanup CurrentFont/CurrentGlyph when closing the corresponding
        # window
        if self._currentMainWindow is not None:
            if self._currentMainWindow.font == font:
                self.setCurrentMainWindow(None)
        if self._currentGlyph is not None:
            if self._currentGlyph.font == font:
                self.setCurrentGlyph(None)

    def newFile(self):
        font = TFont.newStandardFont()
        window = FontWindow(font)
        window.show()

    def openFile(self, path):
        if ".plist" in path:
            path = os.path.dirname(path)
        path = os.path.normpath(path)
        for window in self.topLevelWidgets():
            if isinstance(window, FontWindow):
                font = window.font_()
                if font is not None and font.path == path:
                    window.raise_()
                    return
        try:
            font = TFont(path)
            window = FontWindow(font)
        except Exception as e:
            errorReports.showCriticalException(e)
            return
        window.show()

    def allFonts(self):
        fonts = []
        for window in QApplication.topLevelWidgets():
            if isinstance(window, FontWindow):
                font = window.font_()
                fonts.append(font)
        return fonts

    def currentFont(self):
        # might be None when closing all windows with scripting window open
        if self._currentMainWindow is None:
            return None
        return self._currentMainWindow.font_()

    def currentGlyph(self):
        return self._currentGlyph

    def setCurrentGlyph(self, glyph):
        if glyph == self._currentGlyph:
            return
        self._currentGlyph = glyph
        self.postNotification("currentGlyphChanged")

    def currentMainWindow(self):
        return self._currentMainWindow

    def setCurrentMainWindow(self, mainWindow):
        if mainWindow == self._currentMainWindow:
            return
        self._currentMainWindow = mainWindow
        self.postNotification("currentFontChanged")

    def updateCurrentMainWindow(self):
        window = self.activeWindow()
        if window is None:
            return
        while True:
            parent = window.parent()
            if parent is None:
                break
            window = parent
        if isinstance(window, FontWindow):
            self.setCurrentMainWindow(window)

    def openMetricsWindow(self, font):
        for window in QApplication.topLevelWidgets():
            if isinstance(window, FontWindow) and window.font_() == font:
                window.metrics()
                return window._metricsWindow
        return None

    # -------------
    # Drawing tools
    # -------------

    def drawingTools(self):
        return self._drawingTools

    def installTool(self, tool):
        self._drawingTools.append(tool)
        data = dict(tool=tool)
        self.postNotification("drawingToolInstalled", data)

    def uninstallTool(self, tool):
        pass  # XXX
Example #41
0
class PyREPLSettings(object):

    def __init__(self):
        self._dispatcher = NotificationCenter()

    def __repr__(self):
        return "<Editor Settings Manager. Type \"settings.help\" for documentation.>"

    def _get_help(self):
        # LOL. This is probably very illegal.
        print settingsManagerDoc

    help = property(_get_help)

    # Notifications

    def addObserver(self, obj, methodName, notification="PyREPL.SettingsChanged"):
        self._dispatcher.addObserver(obj, methodName, notification=notification, observable=self)

    def removeObserver(self, obj, notification="PyREPL.SettingsChanged"):
        self._dispatcher.removeObserver(obj, notification, self)

    def postNotification(self, notification="PyREPL.SettingsChanged", data=None):
        self._dispatcher.postNotification(notification, self, data)

    # Properties

    windowWidth = settingsProperty("windowWidth", settingsWindowSizeValidator)
    windowHeight = settingsProperty("windowHeight", settingsWindowSizeValidator)
    fontName = settingsProperty("fontName", settingsStringValidator)
    fontSize = settingsProperty("fontSize", settingsPositiveNumberValidator)
    colorCode = settingsProperty("colorCode", settingsColorValidator)
    colorStdout = settingsProperty("colorStdout", settingsColorValidator)
    colorStderr = settingsProperty("colorStderr", settingsColorValidator)
    colorBackground = settingsProperty("colorBackground", settingsColorValidator)
    bannerGreeting = settingsProperty("bannerGreeting", settingsStringValidator)
    startupCode = settingsProperty("startupCode", settingsStringValidator)
    tabString = settingsProperty("tabString", settingsStringValidator)
    showInvisibleCharacters = settingsProperty("showInvisibleCharacters", settingsBoolValidator)

    def editorItems(self):
        d = dict(
            fontName=self.fontName,
            fontSize=self.fontSize,
            colorCode=self.colorCode,
            colorStdout=self.colorStdout,
            colorStderr=self.colorStderr,
            colorBackground=self.colorBackground,
            tabString=self.tabString,
            showInvisibleCharacters=self.showInvisibleCharacters
        )
        return d.items()

    # Fonts

    def _get_availableFonts(self):
        manager = NSFontManager.sharedFontManager()
        for name in manager.availableFonts():
            font = NSFont.fontWithName_size_(name, 10)
            if font.isFixedPitch():
                print name

    availableFonts = property(_get_availableFonts)

    # Startup Code

    def editStartupCode(self):
        self.postNotification(notification="PyREPL.ShowStartupCodeEditor")

    # Themes

    def loadTheme(self, name):
        userThemes = getDefaultValue("userThemes")
        if name in userThemes:
            theme = userThemes[name]
        elif name in defaultThemes:
            theme = defaultThemes[name]
        else:
            raise PyREPLSettingsError("No theme named %r." % name)
        self.colorCode = theme["colorCode"]
        self.colorStdout = theme["colorStdout"]
        self.colorStderr = theme["colorStderr"]
        self.colorBackground = theme["colorBackground"]

    def saveTheme(self, name):
        if not settingsStringValidator(name):
            raise PyREPLSettingsError("Theme names must be strings.")
        theme = dict(
            colorCode=self.colorCode,
            colorStderr=self.colorStderr,
            colorStdout=self.colorStdout,
            colorBackground=self.colorBackground
        )
        userThemes = getDefaultValue("userThemes")
        userThemes[name] = theme
        setDefaultValue("userThemes", userThemes)