def settings(self): if hasattr(self, "_settingsWindow") and self._settingsWindow.isVisible(): self._settingsWindow.raise_() else: self._settingsWindow = SettingsWindow() self._settingsWindow.show()
def settings(self): if self._settingsWindow is not None and \ self._settingsWindow.isVisible(): self._settingsWindow.raise_() else: self._settingsWindow = SettingsWindow(self) self._settingsWindow.show()
def settings(self): if hasattr(self, '_settingsWindow') and \ self._settingsWindow.isVisible(): self._settingsWindow.raise_() else: self._settingsWindow = SettingsWindow() self._settingsWindow.show()
class FontWindow(BaseMainWindow): def __init__(self, font, parent=None): super().__init__(parent) self._font = None self._settingsWindow = None self._infoWindow = None self._featuresWindow = None self._metricsWindow = None self._groupsWindow = None menuBar = self.menuBar() fileMenu = QMenu(self.tr("&File"), self) fileMenu.addAction(self.tr("&New…"), self.newFile, QKeySequence.New) fileMenu.addAction( self.tr("&Open…"), self.openFile, QKeySequence.Open) # recent files self.recentFilesMenu = QMenu(self.tr("Open &Recent"), self) for i in range(MAX_RECENT_FILES): action = QAction(self.recentFilesMenu) action.setVisible(False) action.triggered.connect(self.openRecentFile) self.recentFilesMenu.addAction(action) self.updateRecentFiles() fileMenu.addMenu(self.recentFilesMenu) fileMenu.addAction(self.tr("&Import…"), self.importFile) fileMenu.addSeparator() fileMenu.addAction(self.tr("&Save"), self.saveFile, QKeySequence.Save) fileMenu.addAction( self.tr("Save &As…"), self.saveFileAs, QKeySequence.SaveAs) fileMenu.addAction(self.tr("&Export…"), self.exportFile) fileMenu.addAction(self.tr("&Reload From Disk"), self.reloadFile) fileMenu.addAction(self.tr("E&xit"), self.close, QKeySequence.Quit) menuBar.addMenu(fileMenu) editMenu = QMenu(self.tr("&Edit"), self) self._undoAction = editMenu.addAction( self.tr("&Undo"), self.undo, QKeySequence.Undo) self._redoAction = editMenu.addAction( self.tr("&Redo"), self.redo, QKeySequence.Redo) editMenu.addSeparator() self.markColorMenu = QMenu(self.tr("&Flag Color"), self) self.updateMarkColors() editMenu.addMenu(self.markColorMenu) cut = editMenu.addAction(self.tr("C&ut"), self.cut, QKeySequence.Cut) copy = editMenu.addAction( self.tr("&Copy"), self.copy, QKeySequence.Copy) copyComponent = editMenu.addAction( self.tr("Copy &As Component"), self.copyAsComponent, "Ctrl+Alt+C") paste = editMenu.addAction( self.tr("&Paste"), self.paste, QKeySequence.Paste) self._clipboardActions = (cut, copy, copyComponent, paste) editMenu.addSeparator() editMenu.addAction(self.tr("&Settings…"), self.settings) menuBar.addMenu(editMenu) fontMenu = QMenu(self.tr("&Font"), self) fontMenu.addAction( self.tr("&Add Glyphs…"), self.addGlyphs, "Ctrl+G") fontMenu.addAction( self.tr("Font &Info"), self.fontInfo, "Ctrl+Alt+I") fontMenu.addAction( self.tr("Font &Features"), self.fontFeatures, "Ctrl+Alt+F") fontMenu.addSeparator() fontMenu.addAction(self.tr("&Sort…"), self.sortGlyphs) menuBar.addMenu(fontMenu) pythonMenu = QMenu(self.tr("&Python"), self) pythonMenu.addAction( self.tr("&Scripting Window"), self.scripting, "Ctrl+Alt+R") pythonMenu.addAction( self.tr("&Output Window"), self.outputWindow, "Ctrl+Alt+O") menuBar.addMenu(pythonMenu) windowMenu = QMenu(self.tr("&Windows"), self) action = windowMenu.addAction( self.tr("&Inspector"), self.inspector, "Ctrl+I") # XXX: we're getting duplicate shortcut when we spawn a new window... action.setShortcutContext(Qt.ApplicationShortcut) windowMenu.addAction( self.tr("&Metrics Window"), self.metrics, "Ctrl+Alt+S") windowMenu.addAction( self.tr("&Groups Window"), self.groups, "Ctrl+Alt+G") menuBar.addMenu(windowMenu) helpMenu = QMenu(self.tr("&Help"), self) helpMenu.addAction(self.tr("&About"), self.about) helpMenu.addAction( self.tr("About &Qt"), QApplication.instance().aboutQt) menuBar.addMenu(helpMenu) cellSize = 56 self.glyphCellView = GlyphCellView(self) self.glyphCellView.glyphActivated.connect(self._glyphActivated) self.glyphCellView.glyphsDropped.connect(self._orderChanged) self.glyphCellView.selectionChanged.connect(self._selectionChanged) self.glyphCellView.setAcceptDrops(True) self.glyphCellView.setCellRepresentationName("TruFont.GlyphCell") self.glyphCellView.setCellSize(cellSize) self.glyphCellView.setFocus() self.cellSizeSlider = QSlider(Qt.Horizontal, self) self.cellSizeSlider.setMinimum(32) self.cellSizeSlider.setMaximum(116) self.cellSizeSlider.setFixedWidth(.9 * self.cellSizeSlider.width()) self.cellSizeSlider.setValue(cellSize) self.cellSizeSlider.valueChanged.connect(self._sliderCellSizeChanged) self.selectionLabel = QLabel(self) statusBar = self.statusBar() statusBar.addPermanentWidget(self.cellSizeSlider) statusBar.addWidget(self.selectionLabel) self.setFont_(font) if font is not None: self.setCurrentFile(font.path) app = QApplication.instance() app.dispatcher.addObserver( self, "_preferencesChanged", "preferencesChanged") app.dispatcher.addObserver(self, "_fontSaved", "fontSaved") self._updateGlyphActions() self.setCentralWidget(self.glyphCellView) self.setWindowTitle() self.resize(605, 430) # -------------- # Custom methods # -------------- def font_(self): return self._font def setFont_(self, font): if self._font is not None: self._font.removeObserver(self, "Font.Changed") self._font.removeObserver(self, "Font.GlyphOrderChanged") self._font.removeObserver(self, "Font.SortDescriptorChanged") self._font = font if font is None: return self._updateGlyphsFromGlyphOrder() font.addObserver(self, "_fontChanged", "Font.Changed") font.addObserver( self, "_glyphOrderChanged", "Font.GlyphOrderChanged") font.addObserver( self, "_sortDescriptorChanged", "Font.SortDescriptorChanged") def maybeSaveBeforeExit(self): if self._font.dirty: currentFont = self.windowTitle()[3:] body = self.tr("Do you want to save the changes you made " "to “{}”?").format(currentFont) closeDialog = QMessageBox( QMessageBox.Question, None, body, QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, self) closeDialog.setInformativeText( self.tr("Your changes will be lost if you don’t save them.")) closeDialog.setModal(True) ret = closeDialog.exec_() if ret == QMessageBox.Save: self.saveFile() return True elif ret == QMessageBox.Discard: return True return False return True # ------------- # Notifications # ------------- # app def _fontSaved(self, notification): if notification.data["font"] != self._font: return path = notification.data["path"] self.setCurrentFile(path) self.setWindowModified(False) def _preferencesChanged(self, notification): self.updateMarkColors() # widgets def _sliderCellSizeChanged(self): cellSize = self.cellSizeSlider.value() self.glyphCellView.setCellSize(cellSize) QToolTip.showText(QCursor.pos(), str(cellSize), self) def _glyphActivated(self, glyph): glyphWindow = GlyphWindow(glyph, self) glyphWindow.show() def _orderChanged(self): # TODO: reimplement when we start showing glyph subsets glyphs = self.glyphCellView.glyphs() self._font.glyphOrder = [glyph.name for glyph in glyphs] def _selectionChanged(self): # currentGlyph lastSelectedGlyph = self.glyphCellView.lastSelectedGlyph() app = QApplication.instance() app.setCurrentGlyph(lastSelectedGlyph) # selection text # TODO: this should probably be internal to the label selection = self.glyphCellView.selection() if selection is not None: count = len(selection) if count == 1: glyph = self.glyphCellView.glyphsForIndexes(selection)[0] text = "%s " % glyph.name else: text = "" if count: text = self.tr("{0}(%n selected)".format(text), n=count) else: text = "" self.selectionLabel.setText(text) # actions self._updateGlyphActions() # defcon def _fontChanged(self, notification): font = notification.object self.setWindowModified(font.dirty) def _glyphOrderChanged(self, notification): self._updateGlyphsFromGlyphOrder() def _updateGlyphsFromGlyphOrder(self): font = self._font glyphOrder = font.glyphOrder if glyphOrder: glyphs = [] for glyphName in glyphOrder: if glyphName in font: glyphs.append(font[glyphName]) if len(glyphs) < len(font): # if some glyphs in the font are not present in the glyph # order, loop again to add them at the end for glyph in font: if glyph not in glyphs: glyphs.append(glyph) font.disableNotifications(observer=self) font.glyphOrder = [glyph.name for glyph in glyphs] font.enableNotifications(observer=self) else: glyphs = list(font) font.disableNotifications(observer=self) font.glyphOrder = [glyph.name for glyph in glyphs] font.enableNotifications(observer=self) self.glyphCellView.setGlyphs(glyphs) def _sortDescriptorChanged(self, notification): font = notification.object descriptors = notification.data["newValue"] if descriptors[0]["type"] == "glyphSet": glyphNames = descriptors[0]["glyphs"] else: glyphNames = font.unicodeData.sortGlyphNames( font.keys(), descriptors) font.glyphOrder = glyphNames # ------------ # Menu methods # ------------ # File def newFile(self): QApplication.instance().newFile() def openFile(self): path, _ = QFileDialog.getOpenFileName( self, self.tr("Open File"), '', platformSpecific.fileFormat ) if path: QApplication.instance().openFile(path) def openRecentFile(self): fontPath = self.sender().toolTip() QApplication.instance().openFile(fontPath) def saveFile(self, path=None, ufoFormatVersion=3): if path is None and self._font.path is None: self.saveFileAs() else: if path is None: path = self._font.path self._font.save(path, ufoFormatVersion) def saveFileAs(self): fileFormats = OrderedDict([ (self.tr("UFO Font version 3 {}").format("(*.ufo)"), 3), (self.tr("UFO Font version 2 {}").format("(*.ufo)"), 2), ]) # TODO: switch to directory on platforms that need it dialog = QFileDialog( self, self.tr("Save File"), None, ";;".join(fileFormats.keys())) dialog.setAcceptMode(QFileDialog.AcceptSave) ok = dialog.exec_() if ok: nameFilter = dialog.selectedNameFilter() path = dialog.selectedFiles()[0] self.saveFile(path, fileFormats[nameFilter]) self.setWindowTitle() # return ok def importFile(self): # TODO: systematize this fileFormats = ( 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)"), self.tr("All supported files {}").format( "(*.otf *.pfa *.pfb *.ttf *.ttx *.woff)"), self.tr("All files {}").format("(*.*)"), ) path, _ = QFileDialog.getOpenFileName( self, self.tr("Import File"), None, ";;".join(fileFormats), fileFormats[-2]) if path: font = TFont() try: font.extract(path) except Exception as e: errorReports.showCriticalException(e) return window = FontWindow(font) window.show() def exportFile(self): path, _ = QFileDialog.getSaveFileName( self, self.tr("Export File"), None, self.tr("OpenType PS font {}").format("(*.otf)")) if path: try: self._font.export(path) except Exception as e: errorReports.showCriticalException(e) def reloadFile(self): font = self._font if font.path is None: return font.reloadInfo() font.reloadKerning() font.reloadGroups() font.reloadFeatures() font.reloadLib() font.reloadGlyphs(font.keys()) self.setWindowModified(False) # Edit def undo(self): glyph = self.glyphCellView.lastSelectedGlyph() glyph.undo() def redo(self): glyph = self.glyphCellView.lastSelectedGlyph() glyph.redo() def markColor(self): color = self.sender().data() if color is not None: color = color.getRgbF() glyphs = self.glyphCellView.glyphs() for index in self.glyphCellView.selection(): glyph = glyphs[index] glyph.markColor = color def cut(self): self.copy() glyphs = self.glyphCellView.glyphs() for index in self.glyphCellView.selection(): glyph = glyphs[index] glyph.clear() def copy(self): glyphs = self.glyphCellView.glyphs() pickled = [] for index in sorted(self.glyphCellView.selection()): pickled.append(glyphs[index].serialize( blacklist=("name", "unicode") )) clipboard = QApplication.clipboard() mimeData = QMimeData() mimeData.setData("application/x-trufont-glyph-data", pickle.dumps(pickled)) clipboard.setMimeData(mimeData) def copyAsComponent(self): glyphs = self.glyphCellView.glyphs() pickled = [] for index in self.glyphCellView.selection(): glyph = glyphs[index] componentGlyph = glyph.__class__() componentGlyph.width = glyph.width component = componentGlyph.instantiateComponent() component.baseGlyph = glyph.name pickled.append(componentGlyph.serialize()) clipboard = QApplication.clipboard() mimeData = QMimeData() mimeData.setData("application/x-trufont-glyph-data", pickle.dumps(pickled)) clipboard.setMimeData(mimeData) def paste(self): clipboard = QApplication.clipboard() mimeData = clipboard.mimeData() if mimeData.hasFormat("application/x-trufont-glyph-data"): data = pickle.loads(mimeData.data( "application/x-trufont-glyph-data")) selection = self.glyphCellView.selection() glyphs = self.glyphCellView.glyphsForIndexes(selection) if len(data) == len(glyphs): for pickled, glyph in zip(data, glyphs): # XXX: prune glyph.prepareUndo() glyph.deserialize(pickled) def settings(self): if self._settingsWindow is not None and \ self._settingsWindow.isVisible(): self._settingsWindow.raise_() else: self._settingsWindow = SettingsWindow(self) self._settingsWindow.show() # Font def fontInfo(self): # If a window is already opened, bring it to the front, else spawn one. # TODO: see about using widget.setAttribute(Qt.WA_DeleteOnClose) # otherwise it seems we're just leaking memory after each close... # (both raise_ and show allocate memory instead of using the hidden # widget it seems) if self._infoWindow is not None and self._infoWindow.isVisible(): self._infoWindow.raise_() else: self._infoWindow = FontInfoWindow(self._font, self) self._infoWindow.show() def fontFeatures(self): # TODO: see up here if self._featuresWindow is not None and self._featuresWindow.isVisible( ): self._featuresWindow.raise_() else: self._featuresWindow = FontFeaturesWindow(self._font, self) self._featuresWindow.show() def addGlyphs(self): glyphs = self.glyphCellView.glyphs() newGlyphNames, params, ok = AddGlyphsDialog.getNewGlyphNames( self, glyphs) if ok: sortFont = params.pop("sortFont") for name in newGlyphNames: glyph = self._font.newStandardGlyph(name, **params) if glyph is not None: glyphs.append(glyph) self.glyphCellView.setGlyphs(glyphs) if sortFont: # TODO: when the user add chars from a glyphSet and no others, # should we try to sort according to that glyphSet? # The above would probably warrant some rearchitecturing. # kick-in the sort mechanism self._font.sortDescriptor = self._font.sortDescriptor def sortGlyphs(self): sortDescriptor, ok = SortDialog.getDescriptor( self, self._font.sortDescriptor) if ok: self._font.sortDescriptor = sortDescriptor # Python def scripting(self): app = QApplication.instance() if not hasattr(app, 'scriptingWindow'): app.scriptingWindow = ScriptingWindow() app.scriptingWindow.show() elif app.scriptingWindow.isVisible(): app.scriptingWindow.raise_() else: app.scriptingWindow.show() def outputWindow(self): app = QApplication.instance() if app.outputWindow.isVisible(): app.outputWindow.raise_() else: app.outputWindow.show() # Windows def inspector(self): app = QApplication.instance() if not hasattr(app, 'inspectorWindow'): app.inspectorWindow = InspectorWindow() app.inspectorWindow.show() elif app.inspectorWindow.isVisible(): # TODO: do this only if the widget is user-visible, otherwise the # key press feels as if it did nothing # toggle app.inspectorWindow.close() else: app.inspectorWindow.show() def metrics(self): # TODO: see up here if self._metricsWindow is not None and self._metricsWindow.isVisible(): self._metricsWindow.raise_() else: self._metricsWindow = MetricsWindow(self._font, parent=self) self._metricsWindow.show() # TODO: default string kicks-in on the window before this. Figure out # how to make a clean interface selection = self.glyphCellView.selection() if selection: glyphs = self.glyphCellView.glyphsForIndexes(selection) self._metricsWindow.setGlyphs(glyphs) def groups(self): # TODO: see up here if self._groupsWindow is not None and self._groupsWindow.isVisible(): self._groupsWindow.raise_() else: self._groupsWindow = GroupsWindow(self._font, self) self._groupsWindow.show() # About def about(self): name = QApplication.applicationName() domain = QApplication.organizationDomain() text = self.tr( "<h3>About {n}</h3>" "<p>{n} is a cross-platform, modular typeface design " "application.</p><p>{n} 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>Version {} {} – Python {}.").format( __version__, gitShortHash, platform.python_version(), n=name) if domain: text += self.tr("<br>See <a href='http://{d}'>{d}</a> for more " "information.</p>").format(d=domain) else: text += "</p>" QMessageBox.about(self, self.tr("About {}").format(name), text) # update methods def setCurrentFile(self, path): if path is None: return 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) for window in QApplication.topLevelWidgets(): if isinstance(window, FontWindow): window.updateRecentFiles() def updateRecentFiles(self): recentFiles = settings.recentFiles() count = min(len(recentFiles), MAX_RECENT_FILES) actions = self.recentFilesMenu.actions() 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) self.recentFilesMenu.setEnabled(len(recentFiles)) def updateMarkColors(self): entries = settings.readMarkColors() self.markColorMenu.clear() pixmap = QPixmap(24, 24) none = self.markColorMenu.addAction("None", self.markColor) none.setData(None) for name, color in entries.items(): action = self.markColorMenu.addAction(name, self.markColor) pixmap.fill(color) action.setIcon(QIcon(pixmap)) action.setData(color) def _updateGlyphActions(self): currentGlyph = self.glyphCellView.lastSelectedGlyph() # disconnect eventual signal of previous glyph self._undoAction.disconnect() self._undoAction.triggered.connect(self.undo) self._redoAction.disconnect() self._redoAction.triggered.connect(self.redo) # now update status if currentGlyph is None: self._undoAction.setEnabled(False) self._redoAction.setEnabled(False) else: undoManager = currentGlyph.undoManager self._undoAction.setEnabled(currentGlyph.canUndo()) undoManager.canUndoChanged.connect(self._undoAction.setEnabled) self._redoAction.setEnabled(currentGlyph.canRedo()) undoManager.canRedoChanged.connect(self._redoAction.setEnabled) # and other actions for action in self._clipboardActions: action.setEnabled(currentGlyph is not None) self.markColorMenu.setEnabled(currentGlyph is not None) # ---------- # Qt methods # ---------- def showEvent(self, event): app = QApplication.instance() data = dict( font=self._font, window=self, ) app.postNotification("fontWindowWillOpen", data) super().showEvent(event) app.postNotification("fontWindowOpened", data) def closeEvent(self, event): ok = self.maybeSaveBeforeExit() if ok: app = QApplication.instance() data = dict( font=self._font, window=self, ) app.postNotification("fontWindowWillClose", data) self._font.removeObserver(self, "Font.Changed") app = QApplication.instance() app.dispatcher.removeObserver(self, "preferencesChanged") app.dispatcher.removeObserver(self, "fontSaved") event.accept() else: event.ignore() def event(self, event): if event.type() == QEvent.WindowActivate: app = QApplication.instance() app.setCurrentMainWindow(self) lastSelectedGlyph = self.glyphCellView.lastSelectedGlyph() if lastSelectedGlyph is not None: app.setCurrentGlyph(lastSelectedGlyph) return super().event(event) def setWindowTitle(self, title=None): if title is None: if self._font.path is not None: title = os.path.basename(self._font.path.rstrip(os.sep)) else: title = self.tr("Untitled.ufo") super().setWindowTitle("[*]{}".format(title))
class FontWindow(BaseMainWindow): def __init__(self, font, parent=None): super().__init__(parent) self._font = None self._settingsWindow = None self._infoWindow = None self._featuresWindow = None self._metricsWindow = None self._groupsWindow = None self.glyphCellView = FontCellView(self) self.glyphCellView.glyphActivated.connect(self._glyphActivated) self.glyphCellView.glyphsDropped.connect(self._orderChanged) self.glyphCellView.selectionChanged.connect(self._selectionChanged) self.glyphCellView.setAcceptDrops(True) self.glyphCellView.setCellRepresentationName("TruFont.GlyphCell") self.glyphCellView.setFocus() self.cellSizeSlider = QSlider(Qt.Horizontal, self) self.cellSizeSlider.setMinimum(32) self.cellSizeSlider.setMaximum(116) self.cellSizeSlider.setFixedWidth(.9 * self.cellSizeSlider.width()) self.cellSizeSlider.sliderReleased.connect(self.writeSettings) self.cellSizeSlider.valueChanged.connect(self._sliderCellSizeChanged) self.selectionLabel = QLabel(self) statusBar = self.statusBar() statusBar.addPermanentWidget(self.cellSizeSlider) statusBar.addWidget(self.selectionLabel) statusBar.setSizeGripEnabled(False) if platformSpecific.needsTighterMargins(): margins = (6, -4, 9, -3) else: margins = (2, 0, 8, 0) statusBar.setContentsMargins(*margins) self.setFont_(font) app = QApplication.instance() app.dispatcher.addObserver(self, "_fontSaved", "fontSaved") self.setCentralWidget(self.glyphCellView) self.setWindowTitle() self.readSettings() def readSettings(self): geometry = settings.fontWindowGeometry() if geometry: self.restoreGeometry(geometry) cellSize = settings.glyphCellSize() self.cellSizeSlider.setValue(cellSize) self.cellSizeSlider.valueChanged.emit(cellSize) def writeSettings(self): settings.setFontWindowGeometry(self.saveGeometry()) settings.setGlyphCellSize(self.cellSizeSlider.value()) def setupMenu(self, menuBar): app = QApplication.instance() fileMenu = menuBar.fetchMenu(Entries.File) fileMenu.fetchAction(Entries.File_New) fileMenu.fetchAction(Entries.File_Open) fileMenu.fetchMenu(Entries.File_Open_Recent) # TODO # if not platformSpecific.mergeOpenAndImport(): fileMenu.fetchAction(Entries.File_Import, self.importFile) fileMenu.addSeparator() fileMenu.fetchAction(Entries.File_Save, self.saveFile) fileMenu.fetchAction(Entries.File_Save_As, self.saveFileAs) fileMenu.fetchAction(Entries.File_Reload, self.reloadFile) fileMenu.addSeparator() fileMenu.fetchAction(Entries.File_Export, self.exportFile) fileMenu.fetchAction(Entries.File_Exit) editMenu = menuBar.fetchMenu(Entries.Edit) self._undoAction = editMenu.fetchAction(Entries.Edit_Undo, self.undo) self._redoAction = editMenu.fetchAction(Entries.Edit_Redo, self.redo) editMenu.addSeparator() cut = editMenu.fetchAction(Entries.Edit_Cut, self.cut) copy = editMenu.fetchAction(Entries.Edit_Copy, self.copy) copyComponent = editMenu.fetchAction( Entries.Edit_Copy_As_Component, self.copyAsComponent) paste = editMenu.fetchAction(Entries.Edit_Paste, self.paste) self._clipboardActions = (cut, copy, copyComponent, paste) editMenu.addSeparator() editMenu.fetchAction(Entries.Edit_Settings, self.settings) fontMenu = menuBar.fetchMenu(Entries.Font) fontMenu.fetchAction(Entries.Font_Font_Info, self.fontInfo) fontMenu.fetchAction(Entries.Font_Font_Features, self.fontFeatures) fontMenu.addSeparator() fontMenu.fetchAction(Entries.Font_Add_Glyphs, self.addGlyphs) fontMenu.fetchAction(Entries.Font_Sort, self.sortGlyphs) menuBar.fetchMenu(Entries.Scripts) windowMenu = menuBar.fetchMenu(Entries.Window) windowMenu.fetchAction(Entries.Window_Inspector) windowMenu.addSeparator() windowMenu.fetchAction(Entries.Window_Groups, self.groups) windowMenu.fetchAction(Entries.Window_Metrics, self.metrics) windowMenu.fetchAction(Entries.Window_Scripting) windowMenu.addSeparator() action = windowMenu.fetchAction(Entries.Window_Output) action.setEnabled(app.outputWindow is not None) helpMenu = menuBar.fetchMenu(Entries.Help) helpMenu.fetchAction(Entries.Help_Documentation) helpMenu.fetchAction(Entries.Help_Report_An_Issue) helpMenu.addSeparator() helpMenu.fetchAction(Entries.Help_About) self._updateGlyphActions() # -------------- # Custom methods # -------------- def font_(self): return self._font def setFont_(self, font): if self._font is not None: self._font.removeObserver(self, "Font.Changed") self._font.removeObserver(self, "Font.GlyphOrderChanged") self._font.removeObserver(self, "Font.SortDescriptorChanged") self._font = font if font is None: return self._updateGlyphsFromGlyphOrder() font.addObserver(self, "_fontChanged", "Font.Changed") font.addObserver( self, "_glyphOrderChanged", "Font.GlyphOrderChanged") font.addObserver( self, "_sortDescriptorChanged", "Font.SortDescriptorChanged") def maybeSaveBeforeExit(self): if self._font.dirty: currentFont = self.windowTitle()[3:] body = self.tr("Do you want to save the changes you made " "to “{}”?").format(currentFont) closeDialog = QMessageBox( QMessageBox.Question, None, body, QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, self) closeDialog.setInformativeText( self.tr("Your changes will be lost if you don’t save them.")) closeDialog.setModal(True) ret = closeDialog.exec_() if ret == QMessageBox.Save: self.saveFile() return True elif ret == QMessageBox.Discard: return True return False return True # ------------- # Notifications # ------------- # app def _fontSaved(self, notification): if notification.data["font"] != self._font: return self.setWindowModified(False) # widgets def _sliderCellSizeChanged(self): cellSize = self.cellSizeSlider.value() self.glyphCellView.setCellSize(cellSize) QToolTip.showText(QCursor.pos(), str(cellSize), self) def _glyphActivated(self, glyph): glyphWindow = GlyphWindow(glyph, self) glyphWindow.show() def _orderChanged(self): # TODO: reimplement when we start showing glyph subsets glyphs = self.glyphCellView.glyphs() self._font.glyphOrder = [glyph.name for glyph in glyphs] def _selectionChanged(self): # currentGlyph lastSelectedGlyph = self.glyphCellView.lastSelectedGlyph() app = QApplication.instance() app.setCurrentGlyph(lastSelectedGlyph) # selection text # TODO: this should probably be internal to the label selection = self.glyphCellView.selection() if selection is not None: count = len(selection) if count == 1: glyph = self.glyphCellView.glyphsForIndexes(selection)[0] text = "%s " % glyph.name else: text = "" if count: text = self.tr("{0}(%n selected)".format(text), n=count) else: text = "" self.selectionLabel.setText(text) # actions self._updateGlyphActions() # defcon def _fontChanged(self, notification): font = notification.object self.setWindowModified(font.dirty) def _glyphOrderChanged(self, notification): self._updateGlyphsFromGlyphOrder() def _updateGlyphsFromGlyphOrder(self): font = self._font glyphOrder = font.glyphOrder if glyphOrder: glyphCount = 0 glyphs = [] for glyphName in glyphOrder: if glyphName in font: glyph = font[glyphName] glyphCount += 1 else: glyph = font.newStandardGlyph(glyphName, asTemplate=True) glyphs.append(glyph) if glyphCount < len(font): # if some glyphs in the font are not present in the glyph # order, loop again to add them at the end for glyph in font: if glyph not in glyphs: glyphs.append(glyph) font.disableNotifications(observer=self) font.glyphOrder = [glyph.name for glyph in glyphs] font.enableNotifications(observer=self) else: glyphs = list(font) font.disableNotifications(observer=self) font.glyphOrder = [glyph.name for glyph in glyphs] font.enableNotifications(observer=self) self.glyphCellView.setGlyphs(glyphs) def _sortDescriptorChanged(self, notification): font = notification.object descriptors = notification.data["newValue"] if descriptors[0]["type"] == "glyphSet": glyphNames = descriptors[0]["glyphs"] else: glyphNames = font.unicodeData.sortGlyphNames( font.keys(), descriptors) font.glyphOrder = glyphNames # ------------ # Menu methods # ------------ # File def importFile(self): # TODO: systematize this fileFormats = ( 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)"), self.tr("All supported files {}").format( "(*.otf *.pfa *.pfb *.ttf *.ttx *.woff)"), self.tr("All files {}").format("(*.*)"), ) path, _ = QFileDialog.getOpenFileName( self, self.tr("Import File"), None, ";;".join(fileFormats), fileFormats[-2]) if path: font = TFont() try: font.extract(path) except Exception as e: errorReports.showCriticalException(e) return window = FontWindow(font) window.show() def saveFile(self, path=None, ufoFormatVersion=3): if path is None and self._font.path is None: self.saveFileAs() else: if path is None: path = self._font.path self._font.save(path, ufoFormatVersion) def saveFileAs(self): fileFormats = OrderedDict([ (self.tr("UFO Font version 3 {}").format("(*.ufo)"), 3), (self.tr("UFO Font version 2 {}").format("(*.ufo)"), 2), ]) # TODO: switch to directory on platforms that need it dialog = QFileDialog( self, self.tr("Save File"), None, ";;".join(fileFormats.keys())) dialog.setAcceptMode(QFileDialog.AcceptSave) ok = dialog.exec_() if ok: nameFilter = dialog.selectedNameFilter() path = dialog.selectedFiles()[0] self.saveFile(path, fileFormats[nameFilter]) self.setWindowTitle() # return ok def reloadFile(self): font = self._font if font.path is None: return font.reloadInfo() font.reloadKerning() font.reloadGroups() font.reloadFeatures() font.reloadLib() font.reloadGlyphs(font.keys()) self.setWindowModified(False) def exportFile(self): path, _ = QFileDialog.getSaveFileName( self, self.tr("Export File"), None, self.tr("OpenType PS font {}").format("(*.otf)")) if path: try: self._font.export(path) except Exception as e: errorReports.showCriticalException(e) # Edit def undo(self): glyph = self.glyphCellView.lastSelectedGlyph() glyph.undo() def redo(self): glyph = self.glyphCellView.lastSelectedGlyph() glyph.redo() def cut(self): self.copy() glyphs = self.glyphCellView.glyphs() for index in self.glyphCellView.selection(): glyph = glyphs[index] glyph.clear() def copy(self): glyphs = self.glyphCellView.glyphs() pickled = [] for index in sorted(self.glyphCellView.selection()): pickled.append(glyphs[index].serialize( blacklist=("name", "unicode") )) clipboard = QApplication.clipboard() mimeData = QMimeData() mimeData.setData("application/x-trufont-glyph-data", pickle.dumps(pickled)) clipboard.setMimeData(mimeData) def copyAsComponent(self): glyphs = self.glyphCellView.glyphs() pickled = [] for index in self.glyphCellView.selection(): glyph = glyphs[index] componentGlyph = glyph.__class__() componentGlyph.width = glyph.width component = componentGlyph.instantiateComponent() component.baseGlyph = glyph.name pickled.append(componentGlyph.serialize()) clipboard = QApplication.clipboard() mimeData = QMimeData() mimeData.setData("application/x-trufont-glyph-data", pickle.dumps(pickled)) clipboard.setMimeData(mimeData) def paste(self): clipboard = QApplication.clipboard() mimeData = clipboard.mimeData() if mimeData.hasFormat("application/x-trufont-glyph-data"): data = pickle.loads(mimeData.data( "application/x-trufont-glyph-data")) selection = self.glyphCellView.selection() glyphs = self.glyphCellView.glyphsForIndexes(selection) if len(data) == len(glyphs): for pickled, glyph in zip(data, glyphs): # XXX: prune glyph.prepareUndo() glyph.deserialize(pickled) def settings(self): if self._settingsWindow is not None and \ self._settingsWindow.isVisible(): self._settingsWindow.raise_() else: self._settingsWindow = SettingsWindow(self) self._settingsWindow.show() # Font def fontInfo(self): # If a window is already opened, bring it to the front, else spawn one. # TODO: see about using widget.setAttribute(Qt.WA_DeleteOnClose) # otherwise it seems we're just leaking memory after each close... # (both raise_ and show allocate memory instead of using the hidden # widget it seems) if self._infoWindow is not None and self._infoWindow.isVisible(): self._infoWindow.raise_() else: self._infoWindow = FontInfoWindow(self._font, self) self._infoWindow.show() def fontFeatures(self): # TODO: see up here if self._featuresWindow is not None and self._featuresWindow.isVisible( ): self._featuresWindow.raise_() else: self._featuresWindow = FontFeaturesWindow(self._font, self) self._featuresWindow.show() def addGlyphs(self): glyphs = self.glyphCellView.glyphs() newGlyphNames, params, ok = AddGlyphsDialog.getNewGlyphNames( self, glyphs) if ok: sortFont = params.pop("sortFont") for name in newGlyphNames: glyph = self._font.newStandardGlyph(name, **params) if glyph is not None: glyphs.append(glyph) self.glyphCellView.setGlyphs(glyphs) if sortFont: # TODO: when the user add chars from a glyphSet and no others, # should we try to sort according to that glyphSet? # The above would probably warrant some rearchitecturing. # kick-in the sort mechanism self._font.sortDescriptor = self._font.sortDescriptor def sortGlyphs(self): sortDescriptor, ok = SortDialog.getDescriptor( self, self._font.sortDescriptor) if ok: self._font.sortDescriptor = sortDescriptor # Window def groups(self): # TODO: see up here if self._groupsWindow is not None and self._groupsWindow.isVisible(): self._groupsWindow.raise_() else: self._groupsWindow = GroupsWindow(self._font, self) self._groupsWindow.show() def metrics(self): # TODO: see up here if self._metricsWindow is not None and self._metricsWindow.isVisible(): self._metricsWindow.raise_() else: self._metricsWindow = MetricsWindow(self._font, parent=self) self._metricsWindow.show() # TODO: default string kicks-in on the window before this. Figure out # how to make a clean interface selection = self.glyphCellView.selection() if selection: glyphs = self.glyphCellView.glyphsForIndexes(selection) self._metricsWindow.setGlyphs(glyphs) # update methods def _updateGlyphActions(self): if not hasattr(self, "_undoAction"): return currentGlyph = self.glyphCellView.lastSelectedGlyph() # disconnect eventual signal of previous glyph self._undoAction.disconnect() self._undoAction.triggered.connect(self.undo) self._redoAction.disconnect() self._redoAction.triggered.connect(self.redo) # now update status if currentGlyph is None: self._undoAction.setEnabled(False) self._redoAction.setEnabled(False) else: undoManager = currentGlyph.undoManager self._undoAction.setEnabled(currentGlyph.canUndo()) undoManager.canUndoChanged.connect(self._undoAction.setEnabled) self._redoAction.setEnabled(currentGlyph.canRedo()) undoManager.canRedoChanged.connect(self._redoAction.setEnabled) # and other actions for action in self._clipboardActions: action.setEnabled(currentGlyph is not None) # ---------- # Qt methods # ---------- def sizeHint(self): return QSize(860, 590) def moveEvent(self, event): self.writeSettings() resizeEvent = moveEvent def showEvent(self, event): app = QApplication.instance() data = dict( font=self._font, window=self, ) app.postNotification("fontWindowWillOpen", data) super().showEvent(event) app.postNotification("fontWindowOpened", data) def closeEvent(self, event): ok = self.maybeSaveBeforeExit() if ok: app = QApplication.instance() data = dict( font=self._font, window=self, ) app.postNotification("fontWindowWillClose", data) self._font.removeObserver(self, "Font.Changed") app = QApplication.instance() app.dispatcher.removeObserver(self, "preferencesChanged") app.dispatcher.removeObserver(self, "fontSaved") event.accept() else: event.ignore() def event(self, event): if event.type() == QEvent.WindowActivate: app = QApplication.instance() app.setCurrentMainWindow(self) inspector = app.inspectorWindow if inspector is not None and inspector.isVisible(): inspector.raise_() lastSelectedGlyph = self.glyphCellView.lastSelectedGlyph() if lastSelectedGlyph is not None: app.setCurrentGlyph(lastSelectedGlyph) return super().event(event) def setWindowTitle(self, title=None): if title is None: if self._font.path is not None: title = os.path.basename(self._font.path.rstrip(os.sep)) else: title = self.tr("Untitled.ufo") super().setWindowTitle("[*]{}".format(title))
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 Exception: 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 Exception: 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 fontamentalMenu = menuBar.fetchMenu(Entries.Fontamental) fontamentalMenu.fetchAction( Entries.Fontamental_Documentation, lambda: QDesktopServices. openUrl(QUrl("http://trufont.github.io/"))) fontamentalMenu.addSeparator() 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 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("TruFont Extension {}").format("(%s)" % tfExtFormat), ]) supportedFiles += f"{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)
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") self.focusWindowChanged.connect(self._focusWindowChanged) self.GL2UV = None self.inspectorWindow = None self.outputWindow = None # -------------- # Event handling # -------------- def _focusWindowChanged(self): window = self.activeWindow() # trash unwanted calls if hasattr(self, "_focusWindow") and window == self._focusWindow: return self._focusWindow = window # update menu bar self.updateMenuBar() # update main window 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, 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) # TODO: maybe move save in there and add save all and close 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_Exit, self.exit) editMenu = menuBar.fetchMenu(Entries.Edit) editMenu.fetchAction(Entries.Edit_Settings, self.settings) 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 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._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() menu.addAction(self.tr(Entries.Scripts_Build_Extension), self.extensionBuilder) # ---------------- # Menu Bar entries # ---------------- def newFile(self): font = TFont.newStandardFont() 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: 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("TruFont Extension {}").format("(%s)" % tfExtFormat), ] ) supportedFiles += 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)"), ] ) supportedFiles += "*.otf *.pfa *.pfb *.ttf *.ttx *.woff" fileFormats.extend( [ self.tr("All supported files {}").format("(%s)" % supportedFiles.rstrip()), self.tr("All files {}").format("(*.*)"), ] ) title = self.tr("Open File") if openFile else self.tr("Import File") path, _ = QFileDialog.getOpenFileName( self.activeWindow(), title, None, ";;".join(fileFormats), fileFormats[-2] ) if not path: return # sanitize path = os.path.normpath(path) if ".plist" in 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): font = TFont() try: font.extract(path) except Exception as e: errorReports.showCriticalException(e) return window = FontWindow(font) window.show() 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) window = FontWindow(font) except Exception as e: msg = self.tr("There was an issue 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) def clearRecentFiles(self): settings.setRecentFiles([]) # 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 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"): 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): 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() # TODO: do this more elegantly? we need it with global menu bar self._msgBox = msgBox # ------------ # 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))