Ejemplo n.º 1
0
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFocusPolicy(Qt.StrongFocus)
        self.setAcceptDrops(True)
        self._currentTool = BaseTool()
        self._mouseDown = False
        self._preview = False

        # inbound notification
        app = QApplication.instance()
        app.dispatcher.addObserver(self, "_needsUpdate", "glyphViewUpdate")
Ejemplo n.º 2
0
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFocusPolicy(Qt.StrongFocus)
        self.setAcceptDrops(True)
        self._currentTool = BaseTool()
        self._currentToolActivated = False
        self._eventFilter = KeyEventFilter(self)
        font = self.window().font_()
        self._layoutManager = LayoutManager(font, self)
        self._mouseDown = False
        self._preview = False

        self._defaultDrawingAttributes["showGlyphSelection"] = True

        # inbound notifications
        app = QApplication.instance()
        app.dispatcher.addObserver(self, "_needsUpdate", "glyphViewUpdate")
        app.dispatcher.addObserver(self, "_preferencesChanged",
                                   "preferencesChanged")

        self.readSettings()
Ejemplo n.º 3
0
class GlyphCanvasView(GlyphContextView):
    glyphNamesChanged = pyqtSignal()
    toolModified = pyqtSignal(object)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFocusPolicy(Qt.StrongFocus)
        self.setAcceptDrops(True)
        self._currentTool = BaseTool()
        self._currentToolActivated = False
        self._eventFilter = KeyEventFilter(self)
        font = self.window().font_()
        self._layoutManager = LayoutManager(font, self)
        self._mouseDown = False
        self._preview = False

        self._defaultDrawingAttributes["showGlyphSelection"] = True

        # inbound notifications
        app = QApplication.instance()
        app.dispatcher.addObserver(self, "_needsUpdate", "glyphViewUpdate")
        app.dispatcher.addObserver(self, "_preferencesChanged",
                                   "preferencesChanged")

        self.readSettings()

    def readSettings(self):
        drawingAttributes = settings.drawingAttributes()
        for attr, value in drawingAttributes.items():
            self.setDefaultDrawingAttribute(attr, value)

    # --------------
    # Custom Methods
    # --------------

    def inputNames(self):
        return self._layoutManager.glyphList()

    def setInputNames(self, names):
        self._layoutManager.setGlyphList(names)

    def layoutManager(self):
        return self._layoutManager

    def setGlyphRecords(self, glyphRecords):
        # TODO: should we also NAK in mouseDown here?
        app = QApplication.instance()
        app.postNotification("glyphViewGlyphsWillChange")
        # TODO: secondly stop calling tool disabled when it's just
        # the glyph changing?
        # e.g. def toolGlyphWillChange(self, newGlyph)
        self._setCurrentToolEnabled(False)
        super().setGlyphRecords(glyphRecords)
        self._setCurrentToolEnabled(True)
        app.postNotification("glyphViewGlyphsChanged")
        self.glyphNamesChanged.emit()

    def previewEnabled(self):
        return self._preview

    def setPreviewEnabled(self, value):
        if value and self._mouseDown:
            return
        if value != self._preview:
            self._preview = value
            if value:
                self.setCursor(Qt.OpenHandCursor)
            else:
                self.setCursor(self._currentTool.cursor)
            self.update()

    # -------------
    # Notifications
    # -------------

    def _subscribeToGlyphs(self, glyphRecords):
        super()._subscribeToGlyphs(glyphRecords)
        handledGlyphs = set()
        for glyphRecord in glyphRecords:
            glyph = glyphRecord.glyph
            if glyph in handledGlyphs:
                continue
            handledGlyphs.add(glyph)
            glyph.addObserver(self, "_glyphNameChanged", "Glyph.NameChanged")
            glyph.addObserver(self, "_glyphSelectionChanged",
                              "Glyph.SelectionChanged")

    def _unsubscribeFromGlyphs(self):
        handledGlyphs = set()
        for glyphRecord in self._glyphRecords:
            glyph = glyphRecord.glyph
            if glyph in handledGlyphs:
                continue
            handledGlyphs.add(glyph)
            glyph.removeObserver(self, "Glyph.NameChanged")
            glyph.removeObserver(self, "Glyph.SelectionChanged")
        super()._unsubscribeFromGlyphs()

    def _glyphNameChanged(self, notification):
        self.glyphNamesChanged.emit()

    def _glyphSelectionChanged(self, notification):
        self.update()

    # app

    def _needsUpdate(self, notification):
        self.update()

    def _preferencesChanged(self, notification):
        self.readSettings()

    # ---------------
    # Display Control
    # ---------------

    def drawingAttribute(self, attr, flags):
        toolOverride = self._currentTool.drawingAttribute(attr, flags)
        if toolOverride is not None:
            return toolOverride
        return super().drawingAttribute(attr, flags)

    def drawingColor(self, attr, flags):
        toolOverride = self._currentTool.drawingColor(attr, flags)
        if toolOverride is not None:
            return toolOverride
        return super().drawingColor(attr, flags)

    # defaults

    def showSelection(self):
        return self.defaultDrawingAttribute("showGlyphSelection")

    def setShowSelection(self, value):
        self.setDefaultDrawingAttribute("showGlyphSelection", value)

    # ---------------
    # Drawing helpers
    # ---------------

    def drawGlyphBackground(self, painter, glyph, flags):
        if not self._preview:
            super().drawGlyphBackground(painter, glyph, flags)

    def drawBackground(self, painter, index):
        app = QApplication.instance()
        data = dict(widget=self, painter=painter, index=index)
        app.postNotification("glyphViewDrawBackground", data)
        self._currentTool.paintBackground(painter, index)

    def drawGlyphLayer(self, painter, glyph, flags):
        if self._preview:
            if flags.isActiveLayer:
                self.drawFillAndPoints(painter, glyph, flags)
        else:
            super().drawGlyphLayer(painter, glyph, flags)

    def drawForeground(self, painter, index):
        app = QApplication.instance()
        data = dict(widget=self, painter=painter, index=index)
        app.postNotification("glyphViewDrawForeground", data)
        self._currentTool.paint(painter, index)

    # drawing primitives

    def drawMetrics(self, painter, glyph, flags):
        # TODO: should this have its own parameter?
        if self._impliedPointSize > GlyphViewMinSizeForGrid:
            viewportRect = (self.mapRectToCanvas(self.rect()).adjusted(
                0, 0, 2, 2).getRect())
            drawing.drawGrid(painter, self._inverseScale, viewportRect)
        super().drawMetrics(painter, glyph, flags)

    def drawImage(self, painter, glyph, flags):
        drawing.drawGlyphImage(painter, glyph, self._inverseScale)

    def drawGuidelines(self, painter, glyph, flags):
        drawText = self._impliedPointSize > GlyphViewMinSizeForDetails
        viewportRect = self.mapRectToCanvas(self.rect()).getRect()
        if self.drawingAttribute("showFontGuidelines", flags):
            drawing.drawFontGuidelines(painter,
                                       glyph,
                                       self._inverseScale,
                                       viewportRect,
                                       drawText=drawText)
        if self.drawingAttribute("showGlyphGuidelines", flags):
            drawing.drawGlyphGuidelines(painter,
                                        glyph,
                                        self._inverseScale,
                                        viewportRect,
                                        drawText=drawText)

    def drawFillAndPoints(self, painter, glyph, flags):
        contourFillColor = self.drawingColor("contourFillColor", flags)
        componentFillColor = (contourFillColor if self._preview else
                              self.drawingColor("componentFillColor", flags))
        drawFill = self._preview or self.drawingAttribute(
            "showGlyphFill", flags)
        drawComponentFill = self._preview or self.drawingAttribute(
            "showGlyphComponentFill", flags)
        drawSelection = not self._preview and self.drawingAttribute(
            "showGlyphSelection", flags)
        drawing.drawGlyphFillAndStroke(
            painter,
            glyph,
            self._inverseScale,
            contourFillColor=contourFillColor,
            componentFillColor=componentFillColor,
            drawFill=drawFill,
            drawComponentFill=drawComponentFill,
            drawSelection=drawSelection,
            drawStroke=False,
        )
        if self._preview or not self._impliedPointSize > GlyphViewMinSizeForDetails:
            return
        drawStartPoints = self.drawingAttribute("showGlyphStartPoints", flags)
        drawOnCurves = self.drawingAttribute("showGlyphOnCurvePoints", flags)
        drawOffCurves = self.drawingAttribute("showGlyphOffCurvePoints", flags)
        drawCoordinates = self.drawingAttribute("showGlyphPointCoordinates",
                                                flags)
        drawing.drawGlyphPoints(
            painter,
            glyph,
            self._inverseScale,
            drawOnCurves=drawOnCurves,
            drawOffCurves=drawOffCurves,
            drawStartPoints=drawStartPoints,
            drawCoordinates=drawCoordinates,
            backgroundColor=self._backgroundColor,
        )

    def drawStroke(self, painter, glyph, flags):
        drawDetails = self._impliedPointSize > GlyphViewMinSizeForDetails
        drawStroke = self.drawingAttribute("showGlyphStroke", flags)
        drawComponentStroke = self.drawingAttribute("showGlyphComponentStroke",
                                                    flags)
        drawing.drawGlyphFillAndStroke(
            painter,
            glyph,
            self._inverseScale,
            drawFill=False,
            drawComponentFill=False,
            drawStroke=drawStroke,
            drawComponentStroke=drawComponentStroke,
            drawSelection=False,
            partialAliasing=drawDetails,
        )

    def drawAnchors(self, painter, glyph, flags):
        if not self._impliedPointSize > GlyphViewMinSizeForDetails:
            return
        drawing.drawGlyphAnchors(painter, glyph, self._inverseScale)

    # ---------------
    # QWidget methods
    # ---------------

    def showEvent(self, event):
        super().showEvent(event)
        self._setCurrentToolEnabled(True)

    def hideEvent(self, event):
        super().hideEvent(event)
        self._setCurrentToolEnabled(False)
        self._preview = False

    def closeEvent(self, event):
        super().closeEvent(event)
        if event.isAccepted():
            self._setCurrentToolEnabled(False)
            app = QApplication.instance()
            app.dispatcher.removeObserver(self, "glyphViewUpdate")
            app.dispatcher.removeObserver(self, "parametersChanged")

    def dragEnterEvent(self, event):
        mimeData = event.mimeData()
        if mimeData.hasUrls():
            url = mimeData.urls()[0]
            if url.isLocalFile():
                path = url.toLocalFile()
                ext = os.path.splitext(path)[1][1:]
                formats = QImageReader.supportedImageFormats() + ["glif"]
                if ext.lower() in formats:
                    event.acceptProposedAction()
            return
        super().dragEnterEvent(event)

    def dropEvent(self, event):
        mimeData = event.mimeData()
        if mimeData.hasUrls():
            paths = mimeData.urls()
            # pick just one image
            path = paths[0].toLocalFile()
            fileName = os.path.basename(path)
            with open(path, "rb") as imgFile:
                data = imgFile.read()
            ext = os.path.splitext(path)[1][1:]
            # TODO: make sure we cleanup properly when replacing an image with
            # another
            if ext.lower() == "glif":
                otherGlyph = self._glyph.__class__()
                try:
                    readGlyphFromString(data, otherGlyph,
                                        otherGlyph.getPointPen())
                except Exception as e:
                    errorReports.showCriticalException(e)
                    return
                self._glyph.beginUndoGroup()
                otherGlyph.drawPoints(self._glyph.getPointPen())
                self._glyph.endUndoGroup()
                return
            if ext.lower() == "svg":
                try:
                    svgPath = SVGPath.fromstring(data)
                except Exception as e:
                    errorReports.showCriticalException(e)
                    return
                self._glyph.beginUndoGroup()
                svgPath.draw(self._glyph.getPen())
                self._glyph.endUndoGroup()
                return
            if ext.lower() != "png":
                # convert
                img = QImage(path)
                data = QByteArray()
                buffer = QBuffer(data)
                buffer.open(QIODevice.WriteOnly)
                img.save(buffer, "PNG")
                # format
                data = bytearray(data)
                fileName = "%s.png" % os.path.splitext(fileName)[0]
            imageSet = self._glyph.font.images
            try:
                imageSet[fileName] = data
            except Exception as e:
                errorReports.showCriticalException(e)
                return
            image = self._glyph.instantiateImage()
            image.fileName = fileName
            self._glyph.image = image
            event.setAccepted(True)
        else:
            super().dropEvent(event)

    def contextMenuEvent(self, event):
        self._redirectEvent(event, self._currentTool.contextMenuEvent, True)
        app = QApplication.instance()
        data = dict(event=event, widget=self)
        # TODO: sending an event that doesn't contain the menu isn't
        # extremely useful
        app.postNotification("glyphViewContextMenu", data)

    def keyPressEvent(self, event):
        # TODO: put this in event filter?
        if self._currentTool.grabKeyboard and event.key() == Qt.Key_Escape:
            ok = self.setCurrentTool(self._previousTool)
            if ok:
                self.toolModified.emit(self._previousTool)
        # Note: not needed, only for parity with keyReleaseEvent
        if not self._currentTool.grabKeyboard and event.key() == Qt.Key_Space:
            event.ignore()
            return
        self._redirectEvent(event, self._currentTool.keyPressEvent)
        app = QApplication.instance()
        data = dict(event=event, widget=self)
        app.postNotification("glyphViewKeyPress", data)

    def keyReleaseEvent(self, event):
        # TODO: I don't know why we have to do this for releaseEvent
        if not self._currentTool.grabKeyboard and event.key() == Qt.Key_Space:
            event.ignore()
            return
        self._redirectEvent(event, self._currentTool.keyReleaseEvent)
        app = QApplication.instance()
        data = dict(event=event, widget=self)
        app.postNotification("glyphViewKeyRelease", data)

    def mousePressEvent(self, event):
        self._mouseDown = True
        if self._preview:
            self._panOrigin = event.globalPos()
            self.setCursor(Qt.ClosedHandCursor)
            return
        self._redirectEvent(event, self._currentTool.mousePressEvent, True)
        app = QApplication.instance()
        data = dict(event=event, widget=self)
        app.postNotification("glyphViewMousePress", data)

    def mouseMoveEvent(self, event):
        if self._preview and hasattr(self, "_panOrigin"):
            pos = event.globalPos()
            self.scrollBy(pos - self._panOrigin)
            self._panOrigin = pos
            return
        self._redirectEvent(event, self._currentTool.mouseMoveEvent, True)
        app = QApplication.instance()
        data = dict(event=event, widget=self)
        app.postNotification("glyphViewMouseMove", data)

    def mouseReleaseEvent(self, event):
        self._redirectEvent(event, self._currentTool.mouseReleaseEvent, True)
        app = QApplication.instance()
        data = dict(event=event, widget=self)
        app.postNotification("glyphViewMouseRelease", data)
        if hasattr(self, "_panOrigin"):
            if self._preview:
                self.setCursor(Qt.OpenHandCursor)
            del self._panOrigin
        self._mouseDown = False

    def mouseDoubleClickEvent(self, event):
        self._redirectEvent(event, self._currentTool.mouseDoubleClickEvent,
                            True)
        app = QApplication.instance()
        data = dict(event=event, widget=self)
        app.postNotification("glyphViewMouseDoubleClick", data)

    # ------------
    # Canvas tools
    # ------------

    # current tool

    def _setCurrentToolEnabled(self, value):
        if self._currentToolActivated == value:
            return
        self._currentToolActivated = value
        if value:
            self._currentTool.toolActivated()
            if self._currentTool.grabKeyboard:
                self.window().installEventFilter(self._eventFilter)
        else:
            if self._currentTool.grabKeyboard:
                self.window().removeEventFilter(self._eventFilter)
            self._currentTool.toolDisabled()

    def currentTool(self):
        return self._currentTool

    def setCurrentTool(self, tool):
        if self._mouseDown:
            return False
        self._setCurrentToolEnabled(False)
        if tool.grabKeyboard:
            self._previousTool = self._currentTool
        self._currentTool = tool
        self.setCursor(tool.cursor)
        self._setCurrentToolEnabled(True)
        return True

    def _redirectEvent(self, event, callback, transmute=False):
        if self._preview:
            return
        # construct a new event with pos in canvas coordinates
        if transmute:
            if isinstance(event, QContextMenuEvent):
                canvasPos = self.mapToCanvas(event.pos())
                event = event.__class__(event.reason(), canvasPos,
                                        event.globalPos(), event.modifiers())
                event.localPos = lambda: canvasPos
            elif isinstance(event, QMouseEvent):
                # TODO: not redirect mouse events if there's no glyph?
                # if not self._glyphRecords:
                #     return
                canvasPos = self.mapToCanvas(event.localPos())
                event = event.__class__(
                    event.type(),
                    canvasPos,
                    event.windowPos(),
                    event.screenPos(),
                    event.button(),
                    event.buttons(),
                    event.modifiers(),
                )
            else:
                raise ValueError(f"cannot transmute event: {event}")
        callback(event)

    # items location

    def _itemsAt(self, func, obj, justOne=True):
        """
        Go through all anchors, points and components (in this order) in the
        glyph, construct their canvas path and list items for which
        *func(path, obj)* returns True, or only return the first item if
        *justOne* is set to True.

        An item is a (contour, index) or (element, None) tuple. Point objects
        aren't returned directly because their usefulness is limited barring
        their parent contour.

        Here is a sample *func* function that tests whether item with path
        *path* contains *pos*:

            def myFunction(path, pos):
                return path.contains(pos)

        This is useful to find out whether an item was clicked on canvas.
        """
        scale = self._inverseScale
        # TODO: export this from drawing or use QSettings.
        # XXX: outdated
        # anchor
        anchorSize = 7 * scale
        anchorHalfSize = anchorSize / 2
        # offCurve
        offSize = 5 * scale
        offHalf = offSize / 2
        offStrokeWidth = 2.5 * scale
        # onCurve
        onSize = 6.5 * scale
        onHalf = onSize / 2
        onStrokeWidth = 1.5 * scale
        # onCurve smooth
        smoothSize = 8 * scale
        smoothHalf = smoothSize / 2
        # guideline pt
        guidelineStrokeWidth = 1 * scale

        if not justOne:
            ret = dict(anchors=[],
                       points=[],
                       components=[],
                       guidelines=[],
                       image=None)
        if self._glyph is None:
            if justOne:
                return None
            return ret
        flags = GlyphFlags(True)
        # anchors
        if self.drawingAttribute("showGlyphAnchors", flags):
            for anchor in reversed(self._glyph.anchors):
                path = QPainterPath()
                path.addEllipse(
                    anchor.x - anchorHalfSize,
                    anchor.y - anchorHalfSize,
                    anchorSize,
                    anchorSize,
                )
                if func(path, obj):
                    if justOne:
                        return anchor
                    ret["anchors"].append(anchor)
        # points
        if self.drawingAttribute("showGlyphOnCurvePoints", flags):
            for contour in reversed(self._glyph):
                for index, point in enumerate(contour):
                    path = QPainterPath()
                    if point.segmentType is None:
                        x = point.x - offHalf
                        y = point.y - offHalf
                        path.addEllipse(x, y, offSize, offSize)
                        strokeWidth = offStrokeWidth
                    else:
                        if point.smooth:
                            x = point.x - smoothHalf
                            y = point.y - smoothHalf
                            path.addEllipse(x, y, smoothSize, smoothSize)
                        else:
                            x = point.x - onHalf
                            y = point.y - onHalf
                            path.addRect(x, y, onSize, onSize)
                        strokeWidth = onStrokeWidth
                    path = _shapeFromPath(path, strokeWidth)
                    if func(path, obj):
                        if justOne:
                            return (contour, index)
                        ret["points"].append((contour, index))
        # components
        for component in reversed(self._glyph.components):
            path = component.getRepresentation("TruFont.QPainterPath")
            if func(path, obj):
                if justOne:
                    return component
                ret["components"].append(component)
        # guideline
        # TODO: we should further dispatch with showFontGuidelines,
        # although both are bind in the UI
        if self.drawingAttribute("showGlyphGuidelines", flags):
            for guideline in UIGlyphGuidelines(self._glyph):
                if None not in (guideline.x, guideline.y):
                    # point
                    x = guideline.x - smoothHalf
                    y = guideline.y - smoothHalf
                    path = QPainterPath()
                    path.addEllipse(x, y, smoothSize, smoothSize)
                    path = _shapeFromPath(path, guidelineStrokeWidth)
                    if func(path, obj):
                        if justOne:
                            return guideline
                        ret["guidelines"].append(guideline)
                    # TODO: catch line if selected
        # image
        if self.drawingAttribute("showGlyphImage", flags):
            image = self._glyph.image
            pixmap = image.getRepresentation("defconQt.QPixmap")
            if pixmap is not None:
                path = QPainterPath()
                transform = QTransform(*image.transformation)
                rect = transform.mapRect(QRectF(pixmap.rect()))
                path.addRect(*rect.getCoords())
                if func(path, obj):
                    if justOne:
                        return image
                    ret["image"] = image
        if not justOne:
            return ret
        return None

    def itemAt(self, pos):
        """
        Find one item at *pos*.

        An item is a (point, contour) or (anchor, None) or (component, None)
        tuple.
        """
        return self.itemsAt(pos, True)

    def itemsAt(self, pos, items=False):
        """
        Find items at *pos*.
        """
        return self._itemsAt(lambda path, pos: path.contains(pos), pos, items)

    def items(self, rect):
        """
        Find items that intersect with *rect* (can be any QPainterPath).
        """
        return self._itemsAt(lambda path, rect: path.intersects(rect), rect,
                             False)
Ejemplo n.º 4
0
class GlyphCanvasWidget(GlyphWidget):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFocusPolicy(Qt.StrongFocus)
        self.setAcceptDrops(True)
        self._currentTool = BaseTool()
        self._mouseDown = False
        self._preview = False

        # inbound notification
        app = QApplication.instance()
        app.dispatcher.addObserver(self, "_needsUpdate", "glyphViewUpdate")

    # --------------
    # Custom Methods
    # --------------

    def setGlyph(self, glyph):
        app = QApplication.instance()
        app.postNotification("glyphViewGlyphWillChange")
        self._currentTool.toolDisabled()
        super().setGlyph(glyph)
        self._currentTool.toolActivated()
        app.postNotification("glyphViewGlyphChanged")

    # --------------------
    # Notifications
    # --------------------

    def _needsUpdate(self, notification):
        self.update()

    # ---------------
    # Drawing helpers
    # ---------------

    def drawingAttribute(self, attr, layerName):
        # TODO: work more on a sound model for this
        if attr == "showGlyphStroke":
            return True
        else:
            return super().drawingAttribute(attr, layerName)

    def drawBackground(self, painter):
        app = QApplication.instance()
        data = dict(
            widget=self,
            painter=painter,
        )
        app.postNotification("glyphViewDrawBackground", data)

    def drawGlyphLayer(self, painter, glyph, layerName):
        if self._preview:
            if layerName is None:
                self.drawFillAndStroke(painter, glyph, layerName)
        else:
            super().drawGlyphLayer(painter, glyph, layerName)

    def drawGuidelines(self, painter, glyph, layerName):
        drawText = self._impliedPointSize > GlyphViewMinSizeForDetails
        if self.drawingAttribute("showFontGuidelines", layerName):
            drawing.drawFontGuidelines(
                painter, glyph, self._inverseScale, self._drawingRect,
                drawText=drawText)
        if self.drawingAttribute("showGlyphGuidelines", layerName):
            drawing.drawGlyphGuidelines(
                painter, glyph, self._inverseScale, self._drawingRect,
                drawText=drawText)

    def drawFillAndStroke(self, painter, glyph, layerName):
        if self._preview:
            contourFillColor = componentFillColor = Qt.black
            drawSelection = False
            showFill = True
            showStroke = False
        else:
            contourFillColor = componentFillColor = None
            drawSelection = layerName is None
            showFill = self.drawingAttribute("showGlyphFill", layerName)
            showStroke = self.drawingAttribute("showGlyphStroke", layerName)
        drawing.drawGlyphFillAndStroke(
            painter, glyph, self._inverseScale, self._drawingRect,
            componentFillColor=componentFillColor,
            contourFillColor=contourFillColor, drawFill=showFill,
            drawSelection=drawSelection, drawStroke=showStroke)

    def drawPoints(self, painter, glyph, layerName):
        if not self._impliedPointSize > GlyphViewMinSizeForDetails:
            return
        drawStartPoints = self.drawingAttribute(
            "showGlyphStartPoints", layerName)
        drawOnCurves = self.drawingAttribute(
            "showGlyphOnCurvePoints", layerName)
        drawOffCurves = self.drawingAttribute(
            "showGlyphOffCurvePoints", layerName)
        drawCoordinates = self.drawingAttribute(
            "showGlyphPointCoordinates", layerName)
        drawing.drawGlyphPoints(
            painter, glyph, self._inverseScale, self._drawingRect,
            drawStartPoints=drawStartPoints, drawOnCurves=drawOnCurves,
            drawOffCurves=drawOffCurves, drawCoordinates=drawCoordinates)

    def drawAnchors(self, painter, glyph, layerName):
        if not self._impliedPointSize > GlyphViewMinSizeForDetails:
            return
        drawing.drawGlyphAnchors(
            painter, glyph, self._inverseScale, self._drawingRect)

    def drawForeground(self, painter):
        app = QApplication.instance()
        data = dict(
            widget=self,
            painter=painter,
        )
        app.postNotification("glyphViewDrawForeground", data)
        self._currentTool.paint(painter)

    # ---------------
    # QWidget methods
    # ---------------

    def closeEvent(self, event):
        super().closeEvent(event)
        if event.isAccepted():
            self._currentTool.toolDisabled()
            app = QApplication.instance()
            app.dispatcher.removeObserver(self, "glyphViewUpdate")

    def dragEnterEvent(self, event):
        mimeData = event.mimeData()
        if mimeData.hasUrls():
            url = mimeData.urls()[0]
            if url.isLocalFile():
                path = url.toLocalFile()
                ext = os.path.splitext(path)[1][1:]
                if ext.lower() in QImageReader.supportedImageFormats():
                    event.acceptProposedAction()
            return
        super().dragEnterEvent(event)

    def dropEvent(self, event):
        mimeData = event.mimeData()
        if mimeData.hasUrls():
            paths = mimeData.urls()
            # pick just one image
            path = paths[0].toLocalFile()
            fileName = os.path.basename(path)
            with open(path, "rb") as imgFile:
                data = imgFile.read()
            ext = os.path.splitext(path)[1][1:]
            # TODO: make sure we cleanup properly when replacing an image with
            # another
            if ext.lower() != "png":
                # convert
                img = QImage(path)
                data = QByteArray()
                buffer = QBuffer(data)
                buffer.open(QIODevice.WriteOnly)
                img.save(buffer, 'PNG')
                # format
                data = bytearray(data)
                fileName = "%s.png" % os.path.splitext(fileName)[0]
            imageSet = self._glyph.font.images
            try:
                imageSet[fileName] = data
            except Exception as e:
                errorReports.showCriticalException(e)
                return
            image = self._glyph.instantiateImage()
            image.fileName = fileName
            event.setAccepted(True)
        else:
            super().dropEvent(event)

    def keyPressEvent(self, event):
        if not event.isAutoRepeat() and event.key() == Qt.Key_Space:
            if not self._mouseDown:
                self._preview = True
                self.update()
        self._redirectEvent(event, self._currentTool.keyPressEvent)
        app = QApplication.instance()
        data = dict(
            event=event,
            widget=self,
        )
        app.postNotification("glyphViewKeyPress", data)

    def keyReleaseEvent(self, event):
        if not event.isAutoRepeat() and event.key() == Qt.Key_Space:
            self._preview = False
            self.update()
        self._redirectEvent(event, self._currentTool.keyReleaseEvent)
        app = QApplication.instance()
        data = dict(
            event=event,
            widget=self,
        )
        app.postNotification("glyphViewKeyRelease", data)

    def mousePressEvent(self, event):
        self._mouseDown = True
        self._redirectEvent(event, self._currentTool.mousePressEvent, True)
        app = QApplication.instance()
        data = dict(
            event=event,
            widget=self,
        )
        app.postNotification("glyphViewMousePress", data)

    def mouseMoveEvent(self, event):
        self._redirectEvent(event, self._currentTool.mouseMoveEvent, True)
        app = QApplication.instance()
        data = dict(
            event=event,
            widget=self,
        )
        app.postNotification("glyphViewMouseMove", data)

    def mouseReleaseEvent(self, event):
        self._redirectEvent(event, self._currentTool.mouseReleaseEvent, True)
        app = QApplication.instance()
        data = dict(
            event=event,
            widget=self,
        )
        app.postNotification("glyphViewMouseRelease", data)
        self._mouseDown = False

    def mouseDoubleClickEvent(self, event):
        self._redirectEvent(
            event, self._currentTool.mouseDoubleClickEvent, True)
        app = QApplication.instance()
        data = dict(
            event=event,
            widget=self,
        )
        app.postNotification("glyphViewMouseDoubleClick", data)

    # ------------
    # Canvas tools
    # ------------

    # current tool

    def currentTool(self):
        return self._currentTool

    def setCurrentTool(self, tool):
        if self._mouseDown:
            return False
        self._currentTool.toolDisabled()
        self._currentTool = tool
        self._currentTool.toolActivated()
        return True

    def _redirectEvent(self, event, callback, transmuteMouseEvent=False):
        if self._preview:
            return
        if transmuteMouseEvent:
            # construct a new event with pos in canvas coordinates
            canvasPos = self.mapToCanvas(event.localPos())
            event = QMouseEvent(
                event.type(),
                canvasPos,
                event.windowPos(),
                event.screenPos(),
                event.button(),
                event.buttons(),
                event.modifiers()
            )
        callback(event)

    # items location

    def _itemsAt(self, func, obj, justOne=True):
        """
        Go through all anchors, points and components (in this order) in the
        glyph, construct their canvas path and list items for which
        *func(path, obj)* returns True, or only return the first item if
        *justOne* is set to True.

        An item is a (point, contour) or (anchor, None) or (component, None)
        tuple. The second argument permits accessing parent contour to post
        notifications.

        Here is a sample *func* function that tests whether item with path
        *path* contains *pos*:

            def myFunction(path, pos):
                return path.contains(pos)

        This is useful to find out whether an item was clicked on canvas.
        """
        scale = self._inverseScale
        # TODO: export this from drawing or use QSettings.
        # anchor
        anchorSize = 6 * scale
        anchorHalfSize = anchorSize / 2
        # offCurve
        offWidth = 5 * scale
        offHalf = offWidth / 2
        offStrokeWidth = 3 * scale
        # onCurve
        onWidth = 7 * scale
        onHalf = onWidth / 2
        onStrokeWidth = 1.5 * scale
        # onCurve smooth
        smoothWidth = 8 * scale
        smoothHalf = smoothWidth / 2
        # guideline pt
        guidelineStrokeWidth = 1 * scale

        if not justOne:
            ret = dict(
                anchors=[],
                contours=[],
                points=[],
                components=[],
                guidelines=[],
                image=None,
            )
        # anchors
        for anchor in reversed(self._glyph.anchors):
            path = QPainterPath()
            path.addEllipse(anchor.x - anchorHalfSize,
                            anchor.y - anchorHalfSize, anchorSize, anchorSize)
            if func(path, obj):
                if justOne:
                    return (anchor, None)
                ret["anchors"].append(anchor)
        # points
        for contour in reversed(self._glyph):
            for point in contour:
                path = QPainterPath()
                if point.segmentType is None:
                    x = point.x - offHalf
                    y = point.y - offHalf
                    path.addEllipse(x, y, offWidth, offWidth)
                    strokeWidth = offStrokeWidth
                else:
                    if point.smooth:
                        x = point.x - smoothHalf
                        y = point.y - smoothHalf
                        path.addEllipse(x, y, smoothWidth, smoothWidth)
                    else:
                        x = point.x - onHalf
                        y = point.y - onHalf
                        path.addRect(x, y, onWidth, onWidth)
                    strokeWidth = onStrokeWidth
                path = _shapeFromPath(path, strokeWidth)
                if func(path, obj):
                    if justOne:
                        return (point, contour)
                    ret["contours"].append(contour)
                    ret["points"].append(point)
        # components
        for component in reversed(self._glyph.components):
            path = component.getRepresentation("TruFont.QPainterPath")
            if func(path, obj):
                if justOne:
                    return (component, None)
                ret["components"].append(component)
        # guideline
        for guideline in UIGlyphGuidelines(self._glyph):
            if None not in (guideline.x, guideline.y):
                # point
                x = guideline.x - smoothHalf
                y = guideline.y - smoothHalf
                path = QPainterPath()
                path.addEllipse(x, y, smoothWidth, smoothWidth)
                path = _shapeFromPath(path, guidelineStrokeWidth)
                if func(path, obj):
                    if justOne:
                        return (guideline, None)
                    ret["guidelines"].append(guideline)
                # TODO: catch line if selected
        # image
        image = self._glyph.image
        pixmap = image.getRepresentation("defconQt.QPixmap")
        if pixmap is not None:
            path = QPainterPath()
            transform = QTransform(*image.transformation)
            rect = transform.mapRect(QRectF(pixmap.rect()))
            path.addRect(*rect.getCoords())
            if func(path, obj):
                if justOne:
                    return (image, None)
                ret["image"] = image
        if not justOne:
            return ret
        return None

    def itemAt(self, pos):
        """
        Find one item at *pos*.

        An item is a (point, contour) or (anchor, None) or (component, None)
        tuple.
        """
        return self.itemsAt(pos, True)

    def itemsAt(self, pos, items=False):
        """
        Find items at *pos*.
        """
        return self._itemsAt(lambda path, pos: path.contains(pos), pos, items)

    def items(self, rect):
        """
        Find items that intersect with *rect* (can be any QPainterPath).
        """
        return self._itemsAt(
            lambda path, rect: path.intersects(rect), rect, False)