Esempio n. 1
0
 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()
Esempio n. 2
0
class Application(QApplication):

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

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

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

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

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

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

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

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

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

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

    def currentMainWindow(self):
        return self._currentMainWindow

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

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

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

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

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

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

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

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

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

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

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

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

    def currentGlyph(self):
        return self._currentGlyph

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

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

    # directory getters

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

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

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

        settings.setValue(key, subFolder)
        return subFolder

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

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

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

    def drawingTools(self):
        return self._drawingTools

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

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

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

    def extensions(self):
        return self._extensions

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

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

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

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

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

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

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

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

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

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

    # Window

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

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

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

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

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

    # Help

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

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

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

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

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

        menu.setEnabled(len(recentFiles))