Esempio n. 1
0
    def __init__(self, parent=None):
        super().__init__(parent)
        self._currentTool = BaseTool()
        self._mouseDown = False
        self._glyph = None

        # drawing attributes
        self._layerDrawingAttributes = {}
        self._fallbackDrawingAttributes = dict(
            showGlyphFill=False,
            showGlyphStroke=True,
            showGlyphOnCurvePoints=True,
            showGlyphStartPoints=True,
            showGlyphOffCurvePoints=True,
            showGlyphPointCoordinates=False,
            showGlyphAnchors=True,
            showGlyphImage=False,
            showGlyphMargins=True,
            showFontVerticalMetrics=True,
            showFontVerticalMetricsTitles=True,
            showFontPostscriptBlues=False,
            showFontPostscriptFamilyBlues=False,
        )

        # cached vertical metrics
        self._unitsPerEm = 1000
        self._descender = -250
        self._capHeight = 750
        self._ascender = 750

        # drawing data cache
        self._drawingRect = None
        self._scale = 1.0
        self._inverseScale = 0.1
        self._impliedPointSize = 1000

        # drawing calculation
        self._centerVertically = True
        self._centerHorizontally = True
        self._noPointSizePadding = 200
        self._verticalCenterYBuffer = 0

        # insert scrollArea
        self.setFocusPolicy(Qt.ClickFocus)
        self._scrollArea = QScrollArea(parent)
        self._scrollArea.resizeEvent = self.resizeEvent
        self._scrollArea.setWidget(self)
Esempio n. 2
0
    def __init__(self, parent=None):
        super().__init__(parent)
        self._currentTool = BaseTool()
        self._mouseDown = False
        self._glyph = None

        # drawing attributes
        self._layerDrawingAttributes = {}
        self._fallbackDrawingAttributes = dict(
            showGlyphFill=False,
            showGlyphStroke=True,
            showGlyphOnCurvePoints=True,
            showGlyphStartPoints=True,
            showGlyphOffCurvePoints=True,
            showGlyphPointCoordinates=False,
            showGlyphAnchors=True,
            showGlyphImage=False,
            showGlyphMargins=True,
            showFontVerticalMetrics=True,
            showFontVerticalMetricsTitles=True,
            showFontPostscriptBlues=False,
            showFontPostscriptFamilyBlues=False,
        )

        # cached vertical metrics
        self._unitsPerEm = 1000
        self._descender = -250
        self._capHeight = 750
        self._ascender = 750

        # drawing data cache
        self._drawingRect = None
        self._scale = 1.0
        self._inverseScale = 0.1
        self._impliedPointSize = 1000

        # drawing calculation
        self._centerVertically = True
        self._centerHorizontally = True
        self._noPointSizePadding = 200
        self._verticalCenterYBuffer = 0

        # insert scrollArea
        self.setFocusPolicy(Qt.ClickFocus)
        self._scrollArea = QScrollArea(parent)
        self._scrollArea.resizeEvent = self.resizeEvent
        self._scrollArea.setWidget(self)
Esempio n. 3
0
class GlyphView(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._currentTool = BaseTool()
        self._glyph = None

        # drawing attributes
        self._layerDrawingAttributes = {}
        self._fallbackDrawingAttributes = dict(
            showGlyphFill=False,
            showGlyphStroke=True,
            showGlyphOnCurvePoints=True,
            showGlyphStartPoints=True,
            showGlyphOffCurvePoints=True,
            showGlyphPointCoordinates=False,
            showGlyphAnchors=True,
            showGlyphImage=False,
            showGlyphMargins=True,
            showFontVerticalMetrics=True,
            showFontVerticalMetricsTitles=True,
            showFontPostscriptBlues=False,
            showFontPostscriptFamilyBlues=False,
        )

        # cached vertical metrics
        self._unitsPerEm = 1000
        self._descender = -250
        self._capHeight = 750
        self._ascender = 750

        # drawing data cache
        self._drawingRect = None
        self._scale = 1.0
        self._inverseScale = 0.1
        self._impliedPointSize = 1000

        # drawing calculation
        self._centerVertically = True
        self._centerHorizontally = True
        self._noPointSizePadding = 200
        self._verticalCenterYBuffer = 0

        # insert scrollArea
        self.setFocusPolicy(Qt.ClickFocus)
        self._scrollArea = QScrollArea(parent)
        self._scrollArea.resizeEvent = self.resizeEvent
        self._scrollArea.setWidget(self)

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

    def _getGlyphWidthHeight(self):
        if self._glyph is None:
            return 0, 0
        bottom = self._descender
        top = max(self._capHeight, self._ascender,
                  self._unitsPerEm + self._descender)
        width = self._glyph.width
        height = -bottom + top
        return width, height

    def fitScaleMetrics(self):
        fitHeight = self._scrollArea.viewport().height()
        _, glyphHeight = self._getGlyphWidthHeight()
        glyphHeight += self._noPointSizePadding * 2
        self.setScale(fitHeight / glyphHeight)

    def fitScaleBBox(self):
        if self._glyph is None:
            return
        if self._glyph.bounds is None:
            self.fitScaleMetrics()
            return
        _, bottom, _, top = self._glyph.bounds
        fitHeight = self._scrollArea.viewport().height()
        glyphHeight = top - bottom
        glyphHeight += self._noPointSizePadding * 2
        self.setScale(fitHeight / glyphHeight)

    def inverseScale(self):
        return self._inverseScale

    def scale(self):
        return self._scale

    def setScale(self, scale):
        self._scale = scale
        if self._scale <= 0:
            self._scale = .01
        self._inverseScale = 1.0 / self._scale
        self._impliedPointSize = self._unitsPerEm * self._scale

    def glyph(self):
        return self._glyph

    def setGlyph(self, glyph):
        self._glyph = glyph
        self._font = None
        if glyph is not None:
            font = self._font = glyph.font
            if font is not None:
                self._unitsPerEm = font.info.unitsPerEm
                if self._unitsPerEm is None:
                    self._unitsPerEm = 1000
                self._descender = font.info.descender
                if self._descender is None:
                    self._descender = -250
                self._ascender = font.info.ascender
                if self._ascender is None:
                    self._ascender = self._unitsPerEm + self._descender
                self._capHeight = font.info.capHeight
                if self._capHeight is None:
                    self._capHeight = self._ascender
            self.setScale(self._scale)
            self.adjustSize()
        self._currentTool.toolActivated()
        self.update()

    # --------------------
    # Notification Support
    # --------------------

    def glyphChanged(self):
        self.update()

    def fontInfoChanged(self):
        self.setGlyph(self._glyph)

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

    def drawingAttribute(self, attr, layerName):
        if layerName is None:
            return self._fallbackDrawingAttributes.get(attr)
        d = self._layerDrawingAttributes.get(layerName, {})
        return d.get(attr)

    def setDrawingAttribute(self, attr, value, layerName):
        if layerName is None:
            self._fallbackDrawingAttributes[attr] = value
        else:
            if layerName not in self._layerDrawingAttributes:
                self._layerDrawingAttributes[layerName] = {}
            self._layerDrawingAttributes[layerName][attr] = value
        self.update()

    def showFill(self):
        return self.drawingAttribute("showGlyphFill", None)

    def setShowFill(self, value):
        self.setDrawingAttribute("showGlyphFill", value, None)

    def showStroke(self):
        return self.drawingAttribute("showGlyphStroke", None)

    def setShowStroke(self, value):
        self.setDrawingAttribute("showGlyphStroke", value, None)

    def showMetrics(self):
        return self.drawingAttribute("showGlyphMargins", None)

    def setShowMetrics(self, value):
        self.setDrawingAttribute("showGlyphMargins", value, None)
        self.setDrawingAttribute("showFontVerticalMetrics", value, None)

    def showImage(self):
        return self.drawingAttribute("showGlyphImage", None)

    def setShowImage(self, value):
        self.setDrawingAttribute("showGlyphImage", value, None)

    def showMetricsTitles(self):
        return self.drawingAttribute("showFontVerticalMetricsTitles", None)

    def setShowMetricsTitles(self, value):
        self.setDrawingAttribute("showFontVerticalMetricsTitles", value, None)

    def showOnCurvePoints(self):
        return self.drawingAttribute("showGlyphOnCurvePoints", None)

    def setShowOnCurvePoints(self, value):
        self.setDrawingAttribute("showGlyphStartPoints", value, None)
        self.setDrawingAttribute("showGlyphOnCurvePoints", value, None)

    def showOffCurvePoints(self):
        return self.drawingAttribute("showGlyphOffCurvePoints", None)

    def setShowOffCurvePoints(self, value):
        self.setDrawingAttribute("showGlyphOffCurvePoints", value, None)

    def showPointCoordinates(self):
        return self.drawingAttribute("showGlyphPointCoordinates", None)

    def setShowPointCoordinates(self, value):
        self.setDrawingAttribute("showGlyphPointCoordinates", value, None)

    def showAnchors(self):
        return self.drawingAttribute("showGlyphAnchors", None)

    def setShowAnchors(self, value):
        self.setDrawingAttribute("showGlyphAnchors", value, None)

    def showBlues(self):
        return self.drawingAttribute("showFontPostscriptBlues", None)

    def setShowBlues(self, value):
        self.setDrawingAttribute("showFontPostscriptBlues", value, None)

    def showFamilyBlues(self):
        return self.drawingAttribute("showFontPostscriptFamilyBlues", None)

    def setShowFamilyBlues(self, value):
        self.setDrawingAttribute("showFontPostscriptFamilyBlues", value, None)

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

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

    def drawBlues(self, painter, glyph, layerName):
        drawing.drawFontPostscriptBlues(painter, glyph, self._inverseScale,
                                        self._drawingRect)

    def drawFamilyBlues(self, painter, glyph, layerName):
        drawing.drawFontPostscriptFamilyBlues(painter, glyph,
                                              self._inverseScale,
                                              self._drawingRect)

    def drawVerticalMetrics(self, painter, glyph, layerName):
        drawText = self._impliedPointSize > 175
        drawing.drawFontVerticalMetrics(painter,
                                        glyph,
                                        self._inverseScale,
                                        self._drawingRect,
                                        drawText=drawText)

    def drawMargins(self, painter, glyph, layerName):
        drawing.drawGlyphMargins(painter, glyph, self._inverseScale,
                                 self._drawingRect)

    def drawFillAndStroke(self, painter, glyph, layerName):
        partialAliasing = self._impliedPointSize > 175
        showFill = self.drawingAttribute("showGlyphFill", layerName)
        showStroke = self.drawingAttribute("showGlyphStroke", layerName)
        drawing.drawGlyphFillAndStroke(painter,
                                       glyph,
                                       self._inverseScale,
                                       self._drawingRect,
                                       drawFill=showFill,
                                       drawStroke=showStroke,
                                       partialAliasing=partialAliasing)

    def drawPoints(self, painter, glyph, layerName):
        drawStartPoints = self.drawingAttribute(
            "showGlyphStartPoints", layerName) and self._impliedPointSize > 175
        drawOnCurves = self.drawingAttribute(
            "showGlyphOnCurvePoints", layerName) and \
            self._impliedPointSize > 175
        drawOffCurves = self.drawingAttribute(
            "showGlyphOffCurvePoints", layerName) and \
            self._impliedPointSize > 175
        drawCoordinates = self.drawingAttribute(
            "showGlyphPointCoordinates", layerName) and \
            self._impliedPointSize > 250
        drawing.drawGlyphPoints(painter,
                                glyph,
                                self._inverseScale,
                                self._drawingRect,
                                drawStartPoints=drawStartPoints,
                                drawOnCurves=drawOnCurves,
                                drawOffCurves=drawOffCurves,
                                drawCoordinates=drawCoordinates,
                                backgroundColor=Qt.white)

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

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

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setFont(headerFont)
        painter.setRenderHint(QPainter.Antialiasing)
        rect = self.rect()

        # draw the background
        painter.fillRect(rect, Qt.white)
        if self._glyph is None:
            return

        # apply the overall scale
        painter.save()
        # + translate and flip
        painter.translate(0, self.height())
        painter.scale(self._scale, -self._scale)

        # move into position
        widgetWidth = self.width()
        width = self._glyph.width * self._scale
        diff = widgetWidth - width
        xOffset = round((diff / 2) * self._inverseScale)

        yOffset = self._verticalCenterYBuffer * self._inverseScale
        yOffset -= self._descender

        painter.translate(xOffset, yOffset)

        # store the current drawing rect
        w, h = self.width(), self.height()
        w *= self._inverseScale
        h *= self._inverseScale
        self._drawingRect = (-xOffset, -yOffset, w, h)

        # gather the layers
        layerSet = self._glyph.layerSet
        if layerSet is None:
            layers = [(self._glyph, None)]
        else:
            glyphName = self._glyph.name
            layers = []
            for layerName in reversed(layerSet.layerOrder):
                layer = layerSet[layerName]
                if glyphName not in layer:
                    continue
                glyph = layer[glyphName]
                if glyph == self._glyph:
                    layerName = None
                layers.append((glyph, layerName))

        for glyph, layerName in layers:
            # draw the image
            if self.drawingAttribute("showGlyphImage", layerName):
                self.drawImage(painter, glyph, layerName)
            # draw the blues
            if layerName is None and self.drawingAttribute(
                    "showFontPostscriptBlues", None):
                self.drawBlues(painter, glyph, layerName)
            if layerName is None and self.drawingAttribute(
                    "showFontPostscriptFamilyBlues", None):
                self.drawFamilyBlues(painter, glyph, layerName)
            # draw the margins
            if self.drawingAttribute("showGlyphMargins", layerName):
                self.drawMargins(painter, glyph, layerName)
            # draw the vertical metrics
            if layerName is None and self.drawingAttribute(
                    "showFontVerticalMetrics", None):
                self.drawVerticalMetrics(painter, glyph, layerName)
            # draw the glyph
            if self.drawingAttribute("showGlyphFill", layerName) or \
                    self.drawingAttribute("showGlyphStroke", layerName):
                # XXX: trying to debug an assertion failure
                try:
                    self.drawFillAndStroke(painter, glyph, layerName)
                except AssertionError as e:
                    import traceback
                    print("**********")
                    print("Internal error:", str(e))
                    print()
                    print(traceback.print_exc())
                    print()
                    for contour in self._glyph:
                        for point in contour:
                            print(point)
                        print()
                    print("**********")
                    painter.restore()
                    return
            if self.drawingAttribute("showGlyphOnCurvePoints", layerName) or \
                    self.drawingAttribute("showGlyphOffCurvePoints",
                                          layerName):
                self.drawPoints(painter, glyph, layerName)
            if self.drawingAttribute("showGlyphAnchors", layerName):
                self.drawAnchors(painter, glyph, layerName)
        self._currentTool.paint(painter)
        painter.restore()

    def scrollArea(self):
        return self._scrollArea

    def sizeHint(self):
        viewport = self._scrollArea.viewport()
        scrollWidth, scrollHeight = viewport.width(), viewport.height()
        # pick the width and height
        glyphWidth, glyphHeight = self._getGlyphWidthHeight()
        glyphWidth = glyphWidth * self._scale
        glyphHeight = glyphHeight * self._scale
        xOffset = 1000 * 2 * self._scale
        yOffset = xOffset
        width = glyphWidth + xOffset
        height = glyphHeight + yOffset
        if scrollWidth > width:
            width = scrollWidth
        if scrollHeight > height:
            height = scrollHeight
        # calculate and store the vertical centering offset
        self._verticalCenterYBuffer = (height - glyphHeight) / 2.0
        return QSize(width, height)

    def resizeEvent(self, event):
        self.adjustSize()
        event.accept()

    def showEvent(self, event):
        # TODO: use fitScaleBBox and adjust scrollBars accordingly
        self.fitScaleMetrics()
        self.adjustSize()
        hSB = self._scrollArea.horizontalScrollBar()
        vSB = self._scrollArea.verticalScrollBar()
        hSB.setValue((hSB.minimum() + hSB.maximum()) / 2)
        vSB.setValue((vSB.minimum() + vSB.maximum()) / 2)

    def wheelEvent(self, event):
        if event.modifiers() & Qt.ControlModifier:
            factor = pow(1.2, event.angleDelta().y() / 120.0)
            pos = event.pos()
            # compute new scrollbar position
            # http://stackoverflow.com/a/32269574/2037879
            oldScale = self._scale
            newScale = self._scale * factor
            hSB = self._scrollArea.horizontalScrollBar()
            vSB = self._scrollArea.verticalScrollBar()
            scrollBarPos = QPointF(hSB.value(), vSB.value())
            deltaToPos = (self.mapToParent(pos) - self.pos()) / oldScale
            delta = deltaToPos * (newScale - oldScale)
            # TODO: maybe put out a func that does multiply by default
            self.setScale(newScale)
            # TODO: maybe merge this in setScale
            self.adjustSize()
            self.update()
            hSB.setValue(scrollBarPos.x() + delta.x())
            vSB.setValue(scrollBarPos.y() + delta.y())
            event.accept()
        else:
            super().wheelEvent(event)

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

    # current tool

    @property
    def currentTool(self):
        return self._currentTool

    @currentTool.setter
    def currentTool(self, tool):
        self._currentTool = tool
        self._currentTool.toolActivated()

    # event directing

    def mapToCanvas(self, pos):
        """
        Map *pos* from GlyphView widget to canvas coordinates.

        Note that canvas coordinates are scale-independent while widget
        coordinates are not.
        Mouse events sent to tools are in canvas coordinates.
        """
        xOffsetInv, yOffsetInv, _, _ = self._drawingRect
        x = pos.x() * self._inverseScale + xOffsetInv
        y = (pos.y() - self.height()) * (-self._inverseScale) + yOffsetInv
        return QPointF(x, y)

    def mapToWidget(self, pos):
        """
        Map *pos* from canvas to GlyphView widget coordinates.

        Note that canvas coordinates are scale-independent while widget
        coordinates are not.
        Mouse events sent to tools are in canvas coordinates.
        """
        xOffsetInv, yOffsetInv, _, _ = self._drawingRect
        x = (pos.x() - xOffsetInv) / self._inverseScale
        y = (pos.y() - yOffsetInv) / (-self._inverseScale) + self.height()
        return QPointF(x, y)

    def _redirectEvent(self, event, callback, transmuteMouseEvent=False):
        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)

    def keyPressEvent(self, event):
        self._redirectEvent(event, self._currentTool.keyPressEvent)

    def keyReleaseEvent(self, event):
        self._redirectEvent(event, self._currentTool.keyReleaseEvent)

    def mousePressEvent(self, event):
        self._redirectEvent(event, self._currentTool.mousePressEvent, True)

    def mouseMoveEvent(self, event):
        self._redirectEvent(event, self._currentTool.mouseMoveEvent, True)

    def mouseReleaseEvent(self, event):
        self._redirectEvent(event, self._currentTool.mouseReleaseEvent, True)

    def mouseDoubleClickEvent(self, event):
        self._redirectEvent(event, self._currentTool.mouseDoubleClickEvent,
                            True)

    # 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.0
        # onCurve
        onWidth = 7 * scale
        onHalf = onWidth / 2.0
        # onCurve smooth
        smoothWidth = 8 * scale
        smoothHalf = smoothWidth / 2.0

        if not justOne:
            ret = dict(
                anchors=[],
                contours=[],
                points=[],
                components=[],
            )
        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)
        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)
                elif 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)
                if func(path, obj):
                    if justOne:
                        return (point, contour)
                    ret["contours"].append(contour)
                    ret["points"].append(point)
        for component in reversed(self._glyph.components):
            path = component.getRepresentation("defconQt.QPainterPath")
            if func(path, obj):
                if justOne:
                    return (component, None)
                ret["components"].append(component)
        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)
Esempio n. 4
0
class GlyphView(QWidget):

    def __init__(self, parent=None):
        super().__init__(parent)
        self._currentTool = BaseTool()
        self._mouseDown = False
        self._glyph = None

        # drawing attributes
        self._layerDrawingAttributes = {}
        self._fallbackDrawingAttributes = dict(
            showGlyphFill=False,
            showGlyphStroke=True,
            showGlyphOnCurvePoints=True,
            showGlyphStartPoints=True,
            showGlyphOffCurvePoints=True,
            showGlyphPointCoordinates=False,
            showGlyphAnchors=True,
            showGlyphImage=False,
            showGlyphMargins=True,
            showFontVerticalMetrics=True,
            showFontVerticalMetricsTitles=True,
            showFontPostscriptBlues=False,
            showFontPostscriptFamilyBlues=False,
        )

        # cached vertical metrics
        self._unitsPerEm = 1000
        self._descender = -250
        self._capHeight = 750
        self._ascender = 750

        # drawing data cache
        self._drawingRect = None
        self._scale = 1.0
        self._inverseScale = 0.1
        self._impliedPointSize = 1000

        # drawing calculation
        self._centerVertically = True
        self._centerHorizontally = True
        self._noPointSizePadding = 200
        self._verticalCenterYBuffer = 0

        # insert scrollArea
        self.setFocusPolicy(Qt.ClickFocus)
        self._scrollArea = QScrollArea(parent)
        self._scrollArea.resizeEvent = self.resizeEvent
        self._scrollArea.setWidget(self)

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

    def _getGlyphWidthHeight(self):
        if self._glyph is None:
            return 0, 0
        bottom = self._descender
        top = max(self._capHeight, self._ascender,
                  self._unitsPerEm + self._descender)
        width = self._glyph.width
        height = -bottom + top
        return width, height

    def fitScaleMetrics(self):
        fitHeight = self._scrollArea.viewport().height()
        _, glyphHeight = self._getGlyphWidthHeight()
        glyphHeight += self._noPointSizePadding * 2
        self.setScale(fitHeight / glyphHeight)

    def fitScaleBBox(self):
        if self._glyph is None:
            return
        if self._glyph.bounds is None:
            self.fitScaleMetrics()
            return
        _, bottom, _, top = self._glyph.bounds
        fitHeight = self._scrollArea.viewport().height()
        glyphHeight = top - bottom
        glyphHeight += self._noPointSizePadding * 2
        self.setScale(fitHeight / glyphHeight)

    def inverseScale(self):
        return self._inverseScale

    def scale(self):
        return self._scale

    def setScale(self, scale):
        self._scale = scale
        if self._scale <= 0:
            self._scale = .01
        self._inverseScale = 1.0 / self._scale
        self._impliedPointSize = self._unitsPerEm * self._scale

    def glyph(self):
        return self._glyph

    def setGlyph(self, glyph):
        # TODO: disable/enable there makes sense, right?
        self._currentTool.toolDisabled()
        self._glyph = glyph
        self._font = None
        if glyph is not None:
            font = self._font = glyph.font
            if font is not None:
                self._unitsPerEm = font.info.unitsPerEm
                if self._unitsPerEm is None:
                    self._unitsPerEm = 1000
                self._descender = font.info.descender
                if self._descender is None:
                    self._descender = -250
                self._ascender = font.info.ascender
                if self._ascender is None:
                    self._ascender = self._unitsPerEm + self._descender
                self._capHeight = font.info.capHeight
                if self._capHeight is None:
                    self._capHeight = self._ascender
            self.setScale(self._scale)
            self.adjustSize()
        self._currentTool.toolActivated()
        self.update()

    # --------------------
    # Notification Support
    # --------------------

    def glyphChanged(self):
        self.update()

    def fontInfoChanged(self):
        self.setGlyph(self._glyph)

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

    def drawingAttribute(self, attr, layerName):
        if layerName is None:
            return self._fallbackDrawingAttributes.get(attr)
        d = self._layerDrawingAttributes.get(layerName, {})
        return d.get(attr)

    def setDrawingAttribute(self, attr, value, layerName):
        if layerName is None:
            self._fallbackDrawingAttributes[attr] = value
        else:
            if layerName not in self._layerDrawingAttributes:
                self._layerDrawingAttributes[layerName] = {}
            self._layerDrawingAttributes[layerName][attr] = value
        self.update()

    def showFill(self):
        return self.drawingAttribute("showGlyphFill", None)

    def setShowFill(self, value):
        self.setDrawingAttribute("showGlyphFill", value, None)

    def showStroke(self):
        return self.drawingAttribute("showGlyphStroke", None)

    def setShowStroke(self, value):
        self.setDrawingAttribute("showGlyphStroke", value, None)

    def showMetrics(self):
        return self.drawingAttribute("showGlyphMargins", None)

    def setShowMetrics(self, value):
        self.setDrawingAttribute("showGlyphMargins", value, None)
        self.setDrawingAttribute("showFontVerticalMetrics", value, None)

    def showImage(self):
        return self.drawingAttribute("showGlyphImage", None)

    def setShowImage(self, value):
        self.setDrawingAttribute("showGlyphImage", value, None)

    def showMetricsTitles(self):
        return self.drawingAttribute("showFontVerticalMetricsTitles", None)

    def setShowMetricsTitles(self, value):
        self.setDrawingAttribute("showFontVerticalMetricsTitles", value, None)

    def showOnCurvePoints(self):
        return self.drawingAttribute("showGlyphOnCurvePoints", None)

    def setShowOnCurvePoints(self, value):
        self.setDrawingAttribute("showGlyphStartPoints", value, None)
        self.setDrawingAttribute("showGlyphOnCurvePoints", value, None)

    def showOffCurvePoints(self):
        return self.drawingAttribute("showGlyphOffCurvePoints", None)

    def setShowOffCurvePoints(self, value):
        self.setDrawingAttribute("showGlyphOffCurvePoints", value, None)

    def showPointCoordinates(self):
        return self.drawingAttribute("showGlyphPointCoordinates", None)

    def setShowPointCoordinates(self, value):
        self.setDrawingAttribute("showGlyphPointCoordinates", value, None)

    def showAnchors(self):
        return self.drawingAttribute("showGlyphAnchors", None)

    def setShowAnchors(self, value):
        self.setDrawingAttribute("showGlyphAnchors", value, None)

    def showBlues(self):
        return self.drawingAttribute("showFontPostscriptBlues", None)

    def setShowBlues(self, value):
        self.setDrawingAttribute("showFontPostscriptBlues", value, None)

    def showFamilyBlues(self):
        return self.drawingAttribute("showFontPostscriptFamilyBlues", None)

    def setShowFamilyBlues(self, value):
        self.setDrawingAttribute("showFontPostscriptFamilyBlues", value, None)

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

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

    def drawBlues(self, painter, glyph, layerName):
        drawing.drawFontPostscriptBlues(
            painter, glyph, self._inverseScale, self._drawingRect)

    def drawFamilyBlues(self, painter, glyph, layerName):
        drawing.drawFontPostscriptFamilyBlues(
            painter, glyph, self._inverseScale, self._drawingRect)

    def drawVerticalMetrics(self, painter, glyph, layerName):
        drawText = self._impliedPointSize > 175
        drawing.drawFontVerticalMetrics(
            painter, glyph, self._inverseScale, self._drawingRect,
            drawText=drawText)

    def drawMargins(self, painter, glyph, layerName):
        drawing.drawGlyphMargins(
            painter, glyph, self._inverseScale, self._drawingRect)

    def drawFillAndStroke(self, painter, glyph, layerName):
        partialAliasing = self._impliedPointSize > 175
        showFill = self.drawingAttribute("showGlyphFill", layerName)
        showStroke = self.drawingAttribute("showGlyphStroke", layerName)
        drawing.drawGlyphFillAndStroke(
            painter, glyph, self._inverseScale, self._drawingRect,
            drawFill=showFill, drawStroke=showStroke,
            partialAliasing=partialAliasing)

    def drawPoints(self, painter, glyph, layerName):
        drawStartPoints = self.drawingAttribute(
            "showGlyphStartPoints", layerName) and self._impliedPointSize > 175
        drawOnCurves = self.drawingAttribute(
            "showGlyphOnCurvePoints", layerName) and \
            self._impliedPointSize > 175
        drawOffCurves = self.drawingAttribute(
            "showGlyphOffCurvePoints", layerName) and \
            self._impliedPointSize > 175
        drawCoordinates = self.drawingAttribute(
            "showGlyphPointCoordinates", layerName) and \
            self._impliedPointSize > 250
        drawing.drawGlyphPoints(
            painter, glyph, self._inverseScale, self._drawingRect,
            drawStartPoints=drawStartPoints, drawOnCurves=drawOnCurves,
            drawOffCurves=drawOffCurves, drawCoordinates=drawCoordinates,
            backgroundColor=Qt.white)

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

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

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setFont(headerFont)
        painter.setRenderHint(QPainter.Antialiasing)
        rect = self.rect()

        # draw the background
        painter.fillRect(rect, Qt.white)
        if self._glyph is None:
            return

        # apply the overall scale
        painter.save()
        # + translate and flip
        painter.translate(0, self.height())
        painter.scale(self._scale, -self._scale)

        # move into position
        widgetWidth = self.width()
        width = self._glyph.width * self._scale
        diff = widgetWidth - width
        xOffset = round((diff / 2) * self._inverseScale)

        yOffset = self._verticalCenterYBuffer * self._inverseScale
        yOffset -= self._descender

        painter.translate(xOffset, yOffset)

        # store the current drawing rect
        w, h = self.width(), self.height()
        w *= self._inverseScale
        h *= self._inverseScale
        self._drawingRect = (-xOffset, -yOffset, w, h)

        # gather the layers
        layerSet = self._glyph.layerSet
        if layerSet is None:
            layers = [(self._glyph, None)]
        else:
            glyphName = self._glyph.name
            layers = []
            for layerName in reversed(layerSet.layerOrder):
                layer = layerSet[layerName]
                if glyphName not in layer:
                    continue
                glyph = layer[glyphName]
                if glyph == self._glyph:
                    layerName = None
                layers.append((glyph, layerName))

        for glyph, layerName in layers:
            # draw the image
            if self.drawingAttribute("showGlyphImage", layerName):
                self.drawImage(painter, glyph, layerName)
            # draw the blues
            if layerName is None and self.drawingAttribute(
                    "showFontPostscriptBlues", None):
                self.drawBlues(painter, glyph, layerName)
            if layerName is None and self.drawingAttribute(
                    "showFontPostscriptFamilyBlues", None):
                self.drawFamilyBlues(painter, glyph, layerName)
            # draw the margins
            if self.drawingAttribute("showGlyphMargins", layerName):
                self.drawMargins(painter, glyph, layerName)
            # draw the vertical metrics
            if layerName is None and self.drawingAttribute(
                    "showFontVerticalMetrics", None):
                self.drawVerticalMetrics(painter, glyph, layerName)
            # draw the glyph
            if self.drawingAttribute("showGlyphFill", layerName) or \
                    self.drawingAttribute("showGlyphStroke", layerName):
                self.drawFillAndStroke(painter, glyph, layerName)
            if self.drawingAttribute("showGlyphOnCurvePoints", layerName) or \
                    self.drawingAttribute("showGlyphOffCurvePoints",
                                          layerName):
                self.drawPoints(painter, glyph, layerName)
            if self.drawingAttribute("showGlyphAnchors", layerName):
                self.drawAnchors(painter, glyph, layerName)
        self._currentTool.paint(painter)
        painter.restore()

    def scrollArea(self):
        return self._scrollArea

    def sizeHint(self):
        viewport = self._scrollArea.viewport()
        scrollWidth, scrollHeight = viewport.width(), viewport.height()
        # pick the width and height
        glyphWidth, glyphHeight = self._getGlyphWidthHeight()
        glyphWidth = glyphWidth * self._scale
        glyphHeight = glyphHeight * self._scale
        xOffset = 1000 * 2 * self._scale
        yOffset = xOffset
        width = glyphWidth + xOffset
        height = glyphHeight + yOffset
        if scrollWidth > width:
            width = scrollWidth
        if scrollHeight > height:
            height = scrollHeight
        # calculate and store the vertical centering offset
        self._verticalCenterYBuffer = (height - glyphHeight) / 2.0
        return QSize(width, height)

    def resizeEvent(self, event):
        self.adjustSize()
        event.accept()

    def showEvent(self, event):
        # TODO: use fitScaleBBox and adjust scrollBars accordingly
        self.fitScaleMetrics()
        self.adjustSize()
        hSB = self._scrollArea.horizontalScrollBar()
        vSB = self._scrollArea.verticalScrollBar()
        hSB.setValue((hSB.minimum() + hSB.maximum()) / 2)
        vSB.setValue((vSB.minimum() + vSB.maximum()) / 2)

    def wheelEvent(self, event):
        if event.modifiers() & Qt.ControlModifier:
            factor = pow(1.2, event.angleDelta().y() / 120.0)
            pos = event.pos()
            # compute new scrollbar position
            # http://stackoverflow.com/a/32269574/2037879
            oldScale = self._scale
            newScale = self._scale * factor
            hSB = self._scrollArea.horizontalScrollBar()
            vSB = self._scrollArea.verticalScrollBar()
            scrollBarPos = QPointF(hSB.value(), vSB.value())
            deltaToPos = (self.mapToParent(pos) - self.pos()) / oldScale
            delta = deltaToPos * (newScale - oldScale)
            # TODO: maybe put out a func that does multiply by default
            self.setScale(newScale)
            # TODO: maybe merge this in setScale
            self.adjustSize()
            self.update()
            hSB.setValue(scrollBarPos.x() + delta.x())
            vSB.setValue(scrollBarPos.y() + delta.y())
            event.accept()
        else:
            super().wheelEvent(event)

    # ------------
    # 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

    # event directing

    def mapToCanvas(self, pos):
        """
        Map *pos* from GlyphView widget to canvas coordinates.

        Note that canvas coordinates are scale-independent while widget
        coordinates are not.
        Mouse events sent to tools are in canvas coordinates.
        """
        xOffsetInv, yOffsetInv, _, _ = self._drawingRect
        x = pos.x() * self._inverseScale + xOffsetInv
        y = (pos.y() - self.height()) * (- self._inverseScale) + yOffsetInv
        return QPointF(x, y)

    def mapToWidget(self, pos):
        """
        Map *pos* from canvas to GlyphView widget coordinates.

        Note that canvas coordinates are scale-independent while widget
        coordinates are not.
        Mouse events sent to tools are in canvas coordinates.
        """
        xOffsetInv, yOffsetInv, _, _ = self._drawingRect
        x = (pos.x() - xOffsetInv) / self._inverseScale
        y = (pos.y() - yOffsetInv) / (- self._inverseScale) + self.height()
        return QPointF(x, y)

    def _redirectEvent(self, event, callback, transmuteMouseEvent=False):
        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)

    def keyPressEvent(self, event):
        self._redirectEvent(event, self._currentTool.keyPressEvent)

    def keyReleaseEvent(self, event):
        self._redirectEvent(event, self._currentTool.keyReleaseEvent)

    def mousePressEvent(self, event):
        self._mouseDown = True
        self._redirectEvent(event, self._currentTool.mousePressEvent, True)

    def mouseMoveEvent(self, event):
        self._redirectEvent(event, self._currentTool.mouseMoveEvent, True)

    def mouseReleaseEvent(self, event):
        self._redirectEvent(event, self._currentTool.mouseReleaseEvent, True)
        self._mouseDown = False

    def mouseDoubleClickEvent(self, event):
        self._redirectEvent(
            event, self._currentTool.mouseDoubleClickEvent, True)

    # 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.0
        # onCurve
        onWidth = 7 * scale
        onHalf = onWidth / 2.0
        # onCurve smooth
        smoothWidth = 8 * scale
        smoothHalf = smoothWidth / 2.0

        if not justOne:
            ret = dict(
                anchors=[],
                contours=[],
                points=[],
                components=[],
            )
        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)
        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)
                elif 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)
                if func(path, obj):
                    if justOne:
                        return (point, contour)
                    ret["contours"].append(contour)
                    ret["points"].append(point)
        for component in reversed(self._glyph.components):
            path = component.getRepresentation("defconQt.QPainterPath")
            if func(path, obj):
                if justOne:
                    return (component, None)
                ret["components"].append(component)
        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)