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())
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())
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"))
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))
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))
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)
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"))
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', {})])
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)
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 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)
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"))
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))
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))
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)
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)
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
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"))
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)
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)
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 _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 __init__(self): self._dispatcher = NotificationCenter()
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)
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"]))
def test_dispatcher(self): notificationCenter = NotificationCenter() obj = BaseObject() obj._dispatcher = weakref.ref(notificationCenter) self.assertEqual(obj.dispatcher, notificationCenter)
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))
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))
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
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)