diff options
Diffstat (limited to 'Lib/defconQt/metricsWindow.py')
| -rw-r--r-- | Lib/defconQt/metricsWindow.py | 879 | 
1 files changed, 879 insertions, 0 deletions
| diff --git a/Lib/defconQt/metricsWindow.py b/Lib/defconQt/metricsWindow.py new file mode 100644 index 0000000..27689ab --- /dev/null +++ b/Lib/defconQt/metricsWindow.py @@ -0,0 +1,879 @@ +from defconQt import icons_db  # noqa +from defconQt.glyphCollectionView import arrowKeys, cellSelectionColor +from defconQt.glyphView import MainGfxWindow +from defconQt.objects.defcon import TGlyph +from getpass import getuser +from PyQt5.QtCore import QEvent, QSettings, QSize, Qt +from PyQt5.QtGui import ( +    QBrush, QColor, QIcon, QIntValidator, QKeySequence, QPainter, QPalette, +    QPen) +from PyQt5.QtWidgets import ( +    QAbstractItemView, QActionGroup, QApplication, QComboBox, QLineEdit, QMenu, +    QPushButton, QScrollArea, QStyledItemDelegate, QTableWidget, +    QTableWidgetItem, QVBoxLayout, QSizePolicy, QToolBar, QWidget) +import re + +comboBoxItems = [ +    "abcdefghijklmnopqrstuvwxyz", +    "ABCDEFGHIJKLMNOPQRSTUVWXYZ", +    "0123456789", +    "nn/? nono/? oo", +    "HH/? HOHO/? OO", +] + +defaultPointSize = 150 +glyphSelectionColor = QColor(cellSelectionColor) +glyphSelectionColor.setAlphaF(.09) + +escapeRep = { +    "//": "/slash ", +    "\\n": "\u2029", +} +escapeRep = dict((re.escape(k), v) for k, v in escapeRep.items()) +escapeRe = re.compile("|".join(escapeRep.keys())) + + +class MainMetricsWindow(QWidget): + +    def __init__(self, font, string=None, pointSize=defaultPointSize, +                 parent=None): +        super().__init__(parent, Qt.Window) + +        if string is None: +            try: +                string = getuser() +            except: +                string = "World" +            string = "Hello %s" % string +        # TODO: drop self.font and self.glyphs, store in the widgets only +        self.font = font +        self.glyphs = [] +        self.toolbar = FontToolBar(pointSize, self) +        self.canvas = GlyphsCanvas(font, pointSize, self) +        self.table = SpaceTable(self) +        self.toolbar.comboBox.currentIndexChanged[ +            str].connect(self.canvas.setPointSize) +        self.canvas.doubleClickCallback = self._glyphOpened +        self.canvas.pointSizeChangedCallback = self.toolbar.setPointSize +        self.canvas.selectionChangedCallback = self.table.setCurrentGlyph +        self.table.selectionChangedCallback = self.canvas.setSelected + +        self.toolbar.textField.editTextChanged.connect(self._textChanged) +        self.toolbar.textField.setEditText(string) +        app = QApplication.instance() +        app.currentGlyphChanged.connect(self._textChanged) + +        layout = QVBoxLayout(self) +        layout.addWidget(self.toolbar) +        layout.addWidget(self.canvas.scrollArea()) +        layout.addWidget(self.table) +        layout.setContentsMargins(0, 0, 0, 0) +        layout.setSpacing(0) +        self.setLayout(layout) +        self.resize(600, 500) + +        self.font.info.addObserver(self, "_fontInfoChanged", "Info.Changed") + +        self.setWindowTitle("Metrics Window – %s %s" % ( +            self.font.info.familyName, self.font.info.styleName)) + +    def setupFileMenu(self): +        fileMenu = QMenu("&File", self) +        fileMenu.addAction("&Save...", self.save, QKeySequence.Save) +        fileMenu.addAction("E&xit", self.close, QKeySequence.Quit) +        self.menuBar().addMenu(fileMenu) + +    def close(self): +        self.font.info.removeObserver(self, "Info.Changed") +        self._unsubscribeFromGlyphs() +        super().close() + +    def _fontInfoChanged(self, notification): +        self.canvas.fetchFontMetrics() +        self.canvas.update() + +    def _glyphChanged(self, notification): +        if not self.canvas._editing: +            self.canvas.update() +        if not self.table._editing: +            self.table.updateCells(self.canvas._editing) + +    def _glyphOpened(self, glyph): +        glyphViewWindow = MainGfxWindow(glyph, self.parent()) +        glyphViewWindow.show() + +    def _textChanged(self): +        def fetchGlyphs(glyphNames, leftGlyphs=[], rightGlyphs=[]): +            ret = [] +            for name in glyphNames: +                if name == "\u2029": +                    glyph = TGlyph() +                    glyph.unicode = 2029 +                    ret.append(glyph) +                elif name in self.font: +                    ret.extend(leftGlyphs) +                    ret.append(self.font[name]) +                    ret.extend(rightGlyphs) +            return ret + +        # unsubscribe from the old glyphs +        self._unsubscribeFromGlyphs() +        # subscribe to the new glyphs +        left = self.textToGlyphNames(self.toolbar.leftTextField.text()) +        newText = self.textToGlyphNames(self.toolbar.textField.currentText()) +        right = self.textToGlyphNames(self.toolbar.rightTextField.text()) +        leftGlyphs = fetchGlyphs(left) +        rightGlyphs = fetchGlyphs(right) +        finalGlyphs = fetchGlyphs(newText, leftGlyphs, rightGlyphs) +        self._subscribeToGlyphs(finalGlyphs) +        # set the records into the view +        self.canvas.setGlyphs(self.glyphs) +        self.table.setGlyphs(self.glyphs) + +    # Tal Leming. Edited. +    def textToGlyphNames(self, text): +        def catchCompile(): +            if compileStack[0] == "?": +                glyph = app.currentGlyph() +                if glyph is not None: +                    glyphNames.append(glyph.name) +            elif compileStack: +                glyphNames.append("".join(compileStack)) + +        app = QApplication.instance() +        # escape //, \n +        text = escapeRe.sub(lambda m: escapeRep[re.escape(m.group(0))], text) +        # +        glyphNames = [] +        compileStack = None +        for c in text: +            # start a glyph name compile. +            if c == "/": +                # finishing a previous compile. +                if compileStack is not None: +                    # only add the compile if something has been added to the +                    # stack. +                    if compileStack: +                        glyphNames.append("".join(compileStack)) +                # reset the stack. +                compileStack = [] +            # adding to or ending a glyph name compile. +            elif compileStack is not None: +                # space. conclude the glyph name compile. +                if c == " ": +                    # only add the compile if something has been added to the +                    # stack. +                    catchCompile() +                    compileStack = None +                # add the character to the stack. +                else: +                    compileStack.append(c) +            # adding a character that needs to be converted to a glyph name. +            else: +                uni = ord(c) +                if uni == 0x2029: +                    glyphName = c +                else: +                    glyphName = self.font.unicodeData.glyphNameForUnicode(uni) +                glyphNames.append(glyphName) +        # catch remaining compile. +        if compileStack is not None and compileStack: +            catchCompile() +        return glyphNames + +    def _subscribeToGlyphs(self, glyphs): +        self.glyphs = glyphs + +        handledGlyphs = set() +        for glyph in self.glyphs: +            if glyph in handledGlyphs: +                continue +            handledGlyphs.add(glyph) +            glyph.addObserver(self, "_glyphChanged", "Glyph.Changed") + +    def _unsubscribeFromGlyphs(self): +        handledGlyphs = set() +        for glyph in self.glyphs: +            if glyph in handledGlyphs: +                continue +            handledGlyphs.add(glyph) +            glyph.removeObserver(self, "Glyph.Changed") +        # self.glyphs = None + +    def setGlyphs(self, glyphs): +        # unsubscribe from the old glyphs +        self._unsubscribeFromGlyphs() +        # subscribe to the new glyphs +        self._subscribeToGlyphs(glyphs) +        glyphNames = [] +        for glyph in glyphs: +            if glyph.unicode: +                glyphNames.append(chr(glyph.unicode)) +            else: +                glyphNames.append("".join(("/", glyph.name, " "))) +        self.toolbar.textField.setEditText("".join(glyphNames)) +        # set the records into the view +        self.canvas.setGlyphs(self.glyphs) +        self.table.setGlyphs(self.glyphs) + +pointSizes = [50, 75, 100, 125, 150, 200, 250, 300, 350, 400, 450, 500] + + +class FontToolBar(QToolBar): + +    def __init__(self, pointSize, parent=None): +        super(FontToolBar, self).__init__(parent) +        auxiliaryWidth = self.fontMetrics().width('0') * 8 +        self.leftTextField = QLineEdit(self) +        self.leftTextField.setMaximumWidth(auxiliaryWidth) +        self.textField = QComboBox(self) +        self.textField.setEditable(True) +        completer = self.textField.completer() +        completer.setCaseSensitivity(Qt.CaseSensitive) +        self.textField.setCompleter(completer) +        # XXX: had to use Maximum because Preferred did entend the widget(?) +        self.textField.setSizePolicy(QSizePolicy.Expanding, +                                     QSizePolicy.Maximum) +        items = QSettings().value("metricsWindow/comboBoxItems", comboBoxItems, +                                  str) +        self.textField.addItems(items) +        self.rightTextField = QLineEdit(self) +        self.rightTextField.setMaximumWidth(auxiliaryWidth) +        self.leftTextField.textEdited.connect(self.textField.editTextChanged) +        self.rightTextField.textEdited.connect(self.textField.editTextChanged) +        self.comboBox = QComboBox(self) +        self.comboBox.setEditable(True) +        self.comboBox.setValidator(QIntValidator(self)) +        for p in pointSizes: +            self.comboBox.addItem(str(p)) +        self.comboBox.setEditText(str(pointSize)) + +        self.configBar = QPushButton(self) +        self.configBar.setFlat(True) +        self.configBar.setIcon(QIcon(":/resources/settings.svg")) +        self.configBar.setStyleSheet("padding: 2px 0px; padding-right: 10px") +        self.toolsMenu = QMenu(self) +        showKerning = self.toolsMenu.addAction( +            "Show Kerning", self.showKerning) +        showKerning.setCheckable(True) +        showMetrics = self.toolsMenu.addAction( +            "Show Metrics", self.showMetrics) +        showMetrics.setCheckable(True) +        self.toolsMenu.addSeparator() +        wrapLines = self.toolsMenu.addAction("Wrap lines", self.wrapLines) +        wrapLines.setCheckable(True) +        noWrapLines = self.toolsMenu.addAction("No wrap", self.noWrapLines) +        noWrapLines.setCheckable(True) +        self.toolsMenu.addSeparator() +        verticalFlip = self.toolsMenu.addAction( +            "Vertical flip", self.verticalFlip) +        verticalFlip.setCheckable(True) +        """ +        lineHeight = QWidgetAction(self.toolsMenu) +        lineHeight.setText("Line height:") +        lineHeightSlider = QSlider(Qt.Horizontal, self) +        # QSlider works with integers so we'll just divide by 100 what comes +        # out of it +        lineHeightSlider.setMinimum(80) +        lineHeightSlider.setMaximum(160) +        lineHeightSlider.setValue(100) +        #lineHeightSlider.setContentsMargins(30, 0, 30, 0) +        lineHeightSlider.valueChanged.connect(self.lineHeight) +        lineHeight.setDefaultWidget(lineHeightSlider) +        self.toolsMenu.addAction(lineHeight) +        """ + +        wrapLinesGroup = QActionGroup(self) +        wrapLinesGroup.addAction(wrapLines) +        wrapLinesGroup.addAction(noWrapLines) +        wrapLines.setChecked(True) +        # self.toolsMenu.setActiveAction(wrapLines) +        self.configBar.setMenu(self.toolsMenu) + +        self.addWidget(self.leftTextField) +        self.addWidget(self.textField) +        self.addWidget(self.rightTextField) +        self.addWidget(self.comboBox) +        self.addWidget(self.configBar) + +    def showEvent(self, event): +        super(FontToolBar, self).showEvent(event) +        self.textField.setFocus(True) + +    def setPointSize(self, pointSize): +        self.comboBox.blockSignals(True) +        self.comboBox.setEditText(str(pointSize)) +        self.comboBox.blockSignals(False) + +    def showKerning(self): +        action = self.sender() +        self.parent().canvas.setShowKerning(action.isChecked()) + +    def showMetrics(self): +        action = self.sender() +        self.parent().canvas.setShowMetrics(action.isChecked()) + +    def verticalFlip(self): +        action = self.sender() +        self.parent().canvas.setVerticalFlip(action.isChecked()) + +    def lineHeight(self, value): +        self.parent().canvas.setLineHeight(value / 100) + +    def wrapLines(self): +        self.parent().canvas.setWrapLines(True) + +    def noWrapLines(self): +        self.parent().canvas.setWrapLines(False) + + +class GlyphsCanvas(QWidget): + +    def __init__(self, font, pointSize=defaultPointSize, parent=None): +        super(GlyphsCanvas, self).__init__(parent) +        self.setAttribute(Qt.WA_KeyCompression) +        # TODO: should we take focus by tabbing +        self.setFocusPolicy(Qt.ClickFocus) +        # XXX: make canvas font-agnostic as in defconAppkit and use +        # glyph.getParent() instead +        self.font = font +        self.fetchFontMetrics() +        self.glyphs = [] +        self.ptSize = pointSize +        self.calculateScale() +        self.padding = 10 +        self._editing = False +        self._showKerning = False +        self._showMetrics = False +        self._verticalFlip = False +        self._lineHeight = 1.1 +        self._positions = None +        self._selected = None +        self.doubleClickCallback = None +        self.pointSizeChangedCallback = None +        self.selectionChangedCallback = None + +        self._wrapLines = True +        self._scrollArea = QScrollArea(self.parent()) +        self._scrollArea.resizeEvent = self.resizeEvent +        self._scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) +        self._scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) +        self._scrollArea.setWidget(self) +        self.resize(581, 400) + +    def scrollArea(self): +        return self._scrollArea + +    def calculateScale(self): +        scale = self.ptSize / self.upm +        if scale < .01: +            scale = 0.01 +        self.scale = scale + +    def setShowKerning(self, showKerning): +        self._showKerning = showKerning +        self.update() + +    def setShowMetrics(self, showMetrics): +        self._showMetrics = showMetrics +        self.update() + +    def setVerticalFlip(self, verticalFlip): +        self._verticalFlip = verticalFlip +        self.update() + +    def setLineHeight(self, lineHeight): +        self._lineHeight = lineHeight +        self.update() + +    def setWrapLines(self, wrapLines): +        if self._wrapLines == wrapLines: +            return +        self._wrapLines = wrapLines +        if self._wrapLines: +            self.resize(self._scrollArea.viewport().width(), self.height()) +            self._scrollArea.setHorizontalScrollBarPolicy( +                Qt.ScrollBarAlwaysOff) +            self._scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) +        else: +            self.resize(self.width(), self._scrollArea.viewport().height()) +            self._scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) +            self._scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) +        self.update() + +    def fetchFontMetrics(self): +        self.ascender = self.font.info.ascender +        if self.ascender is None: +            self.ascender = 750 +        self.descender = self.font.info.descender +        if self.descender is None: +            self.descender = 250 +        self.upm = self.font.info.unitsPerEm +        if self.upm is None or not self.upm > 0: +            self.upm = 1000 + +    def setGlyphs(self, newGlyphs): +        self.glyphs = newGlyphs +        self._selected = None +        self.update() + +    def setPointSize(self, pointSize): +        self.ptSize = int(pointSize) +        self.calculateScale() +        self.update() + +    def setSelected(self, selected): +        self._selected = selected +        if self._positions is not None: +            cur_len = 0 +            line = -1 +            for index, li in enumerate(self._positions): +                if cur_len + len(li) > self._selected: +                    pos, width = li[self._selected - cur_len] +                    line = index +                    break +                cur_len += len(li) +            if line > -1: +                x = self.padding + pos + width / 2 +                y = self.padding + (line + .5) * self.ptSize * self._lineHeight +                self._scrollArea.ensureVisible( +                    x, y, width / 2 + 20, +                    .5 * self.ptSize * self._lineHeight + 20) +        self.update() + +    def resizeEvent(self, event): +        if self._wrapLines: +            self.resize(self._scrollArea.viewport().width(), self.height()) +        else: +            self.resize(self.width(), self._scrollArea.viewport().height()) + +    def wheelEvent(self, event): +        if event.modifiers() & Qt.ControlModifier: +            # TODO: should it snap to predefined pointSizes? +            #       is the scaling factor okay? +            # XXX: current alg. is not reversible... +            decay = event.angleDelta().y() / 120.0 +            scale = round(self.ptSize / 10) +            if scale == 0 and decay >= 0: +                scale = 1 +            newPointSize = self.ptSize + int(decay) * scale +            if newPointSize <= 0: +                return + +            self.setPointSize(newPointSize) +            if self.pointSizeChangedCallback is not None: +                self.pointSizeChangedCallback(newPointSize) +            event.accept() +        else: +            super(GlyphsCanvas, self).wheelEvent(event) + +    # Tal Leming. Edited. +    def lookupKerningValue(self, first, second): +        kerning = self.font.kerning +        groups = self.font.groups +        # quickly check to see if the pair is in the kerning dictionary +        pair = (first, second) +        if pair in kerning: +            return kerning[pair] +        # get group names and make sure first and second are glyph names +        firstGroup = secondGroup = None +        if first.startswith("@MMK_L"): +            firstGroup = first +            first = None +        else: +            for group, groupMembers in groups.items(): +                if group.startswith("@MMK_L"): +                    if first in groupMembers: +                        firstGroup = group +                        break +        if second.startswith("@MMK_R"): +            secondGroup = second +            second = None +        else: +            for group, groupMembers in groups.items(): +                if group.startswith("@MMK_R"): +                    if second in groupMembers: +                        secondGroup = group +                        break +        # make an ordered list of pairs to look up +        pairs = [ +            (first, second), +            (first, secondGroup), +            (firstGroup, second), +            (firstGroup, secondGroup) +        ] +        # look up the pairs and return any matches +        for pair in pairs: +            if pair in kerning: +                return kerning[pair] +        return 0 + +    def _arrowKeyPressEvent(self, event): +        key = event.key() +        modifiers = event.modifiers() +        self._editing = True +        if self._selected is not None: +            glyph = self.glyphs[self._selected] +            # TODO: not really DRY w other widgets +            delta = event.count() +            if modifiers & Qt.ShiftModifier: +                delta *= 10 +                if modifiers & Qt.ControlModifier: +                    delta *= 10 +            if key == Qt.Key_Left: +                delta = -delta +            if modifiers & Qt.AltModifier: +                if glyph.leftMargin is not None: +                    glyph.leftMargin += delta +            else: +                glyph.width += delta +        self._editing = False +        event.accept() + +    def keyPressEvent(self, event): +        key = event.key() +        if key in arrowKeys: +            self._arrowKeyPressEvent(event) +        else: +            super().keyPressEvent(event) + +    def mousePressEvent(self, event): +        if event.button() == Qt.LeftButton: +            # XXX: investigate, baselineShift is unused +            # if self._verticalFlip: +            #     baselineShift = -self.descender +            # else: +            #     baselineShift = self.ascender +            found = False +            line = \ +                (event.y() - self.padding) // (self.ptSize * self._lineHeight) +            # XXX: Shouldnt // yield an int? +            line = int(line) +            if line >= len(self._positions): +                self._selected = None +                # XXX: find a way to DRY notification of self._selected changed +                # w ability to block notifications as well +                if self.selectionChangedCallback is not None: +                    self.selectionChangedCallback(self._selected) +                event.accept() +                self.update() +                return +            x = event.x() - self.padding +            for index, data in enumerate(self._positions[line]): +                pos, width = data +                if pos <= x and pos + width > x: +                    count = 0 +                    for i in range(line): +                        count += len(self._positions[i]) +                    self._selected = count + index +                    found = True +                    break +            if not found: +                self._selected = None +            if self.selectionChangedCallback is not None: +                self.selectionChangedCallback(self._selected) +            event.accept() +            self.update() +            # restore focus to ourselves, the table widget did take it when we +            # sent notification +            # TODO: maybe not set focus on notifiee instead +            self.setFocus(Qt.MouseFocusReason) +        else: +            super(GlyphsCanvas, self).mousePressEvent(event) + +    def mouseDoubleClickEvent(self, event): +        if event.button() == Qt.LeftButton and self._selected is not None: +            if self.doubleClickCallback is not None: +                self.doubleClickCallback(self.glyphs[self._selected]) +        else: +            super(GlyphsCanvas, self).mouseDoubleClickEvent(event) + +    def paintEvent(self, event): +        linePen = QPen(Qt.black) +        linePen.setWidth(3) +        width = self.width() / self.scale + +        def paintLineMarks(painter): +            painter.save() +            painter.scale(self.scale, yDirection * self.scale) +            painter.setPen(linePen) +            painter.drawLine(0, self.ascender, width, self.ascender) +            painter.drawLine(0, 0, width, 0) +            painter.drawLine(0, self.descender, width, self.descender) +            painter.restore() + +        painter = QPainter(self) +        painter.setRenderHint(QPainter.Antialiasing) +        painter.fillRect(0, 0, self.width(), self.height(), Qt.white) +        if self._verticalFlip: +            baselineShift = -self.descender +            yDirection = 1 +        else: +            baselineShift = self.ascender +            yDirection = -1 +        painter.translate(self.padding, self.padding + +                          baselineShift * self.scale * self._lineHeight) +        # TODO: scale painter here to avoid g*scale everywhere below + +        cur_width = 0 +        lines = 1 +        self._positions = [[]] +        if self._showMetrics: +            paintLineMarks(painter) +        for index, glyph in enumerate(self.glyphs): +            # line wrapping +            gWidth = glyph.width * self.scale +            doKern = index > 0 and self._showKerning and cur_width > 0 +            if doKern: +                kern = self.lookupKerningValue( +                    self.glyphs[index - 1].name, glyph.name) * self.scale +            else: +                kern = 0 +            if (self._wrapLines and cur_width + gWidth + kern + +                    2 * self.padding > self.width()) or glyph.unicode == 2029: +                painter.translate(-cur_width, self.ptSize * self._lineHeight) +                if self._showMetrics: +                    paintLineMarks(painter) +                self._positions.append([(0, gWidth)]) +                cur_width = gWidth +                lines += 1 +            else: +                if doKern: +                    painter.translate(kern, 0) +                self._positions[-1].append((cur_width, gWidth)) +                cur_width += gWidth + kern +            glyphPath = glyph.getRepresentation("defconQt.QPainterPath") +            painter.save() +            painter.scale(self.scale, yDirection * self.scale) +            if self._showMetrics: +                halfDescent = self.descender / 2 +                painter.drawLine(0, 0, 0, halfDescent) +                painter.drawLine(glyph.width, 0, glyph.width, halfDescent) +            if self._selected is not None and index == self._selected: +                painter.fillRect(0, self.descender, glyph.width, +                                 self.upm, glyphSelectionColor) +            painter.fillPath(glyphPath, Qt.black) +            painter.restore() +            painter.translate(gWidth, 0) + +        innerHeight = self._scrollArea.viewport().height() +        if not self._wrapLines: +            innerWidth = self._scrollArea.viewport().width() +            width = max(innerWidth, cur_width + self.padding * 2) +        else: +            width = self.width() +        self.resize(width, max(innerHeight, lines * self.ptSize * +                               self._lineHeight + 2 * self.padding)) + + +class SpaceTableWidgetItem(QTableWidgetItem): + +    def setData(self, role, value): +        if role & Qt.EditRole: +            # don't set empty data +            # XXX: maybe fetch the value from cell back to the editor +            if value == "": +                return +        super(SpaceTableWidgetItem, self).setData(role, value) + + +class GlyphCellItemDelegate(QStyledItemDelegate): + +    def createEditor(self, parent, option, index): +        editor = super(GlyphCellItemDelegate, self).createEditor( +            parent, option, index) +        # editor.setAlignment(Qt.AlignCenter) +        editor.setValidator(QIntValidator(self)) +        return editor + +    # TODO: implement =... lexer +    # TODO: Alt+left or Alt+right don't SelectAll of the new cell +    # cell by default. Implement this. +    # TODO: cycle b/w editable cell area +    def eventFilter(self, editor, event): +        if event.type() == QEvent.KeyPress: +            chg = None +            count = event.count() +            key = event.key() +            if key == Qt.Key_Up: +                chg = count +            elif key == Qt.Key_Down: +                chg = -count +            elif not key == Qt.Key_Return: +                return False +            if chg is not None: +                modifiers = event.modifiers() +                if modifiers & Qt.AltModifier: +                    return False +                elif modifiers & Qt.ShiftModifier: +                    chg *= 10 +                    if modifiers & Qt.ControlModifier: +                        chg *= 10 +                cur = int(editor.text()) +                editor.setText(str(cur + chg)) +            self.commitData.emit(editor) +            editor.selectAll() +            return True +        return False + + +class SpaceTable(QTableWidget): + +    def __init__(self, parent=None): +        super(SpaceTable, self).__init__(4, 1, parent) +        self.setAttribute(Qt.WA_KeyCompression) +        self.setItemDelegate(GlyphCellItemDelegate(self)) +        data = [None, "Width", "Left", "Right"] +        # Don't grey-out disabled cells +        palette = self.palette() +        fgColor = palette.color(QPalette.Text) +        palette.setColor(QPalette.Disabled, QPalette.Text, fgColor) +        self.setPalette(palette) +        for index, title in enumerate(data): +            item = SpaceTableWidgetItem(title) +            item.setFlags(Qt.NoItemFlags) +            self.setItem(index, 0, item) +        # let's use this one column to compute the width of others +        self._cellWidth = .5 * self.columnWidth(0) +        self.setColumnWidth(0, self._cellWidth) +        self.horizontalHeader().hide() +        self.verticalHeader().hide() +        self._coloredColumn = None + +        # always show a scrollbar to fix layout +        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) +        self.setSizePolicy(QSizePolicy( +            QSizePolicy.Preferred, QSizePolicy.Fixed)) +        self.glyphs = [] +        self.fillGlyphs() +        self.resizeRowsToContents() +        self.currentItemChanged.connect(self._itemChanged) +        self.cellChanged.connect(self._cellEdited) +        self.setSelectionMode(QAbstractItemView.SingleSelection) +        # edit cell on single click, not double +        self.setEditTriggers(QAbstractItemView.CurrentChanged) +        self._editing = False +        self.selectionChangedCallback = None + +    def setGlyphs(self, newGlyphs): +        self.glyphs = newGlyphs +        # TODO: we don't need to reallocate cells, split alloc and fill +        self.updateCells() + +    def updateCells(self, keepColor=False): +        self.blockSignals(True) +        self.setEditTriggers(QAbstractItemView.NoEditTriggers) +        coloredColumn = self._coloredColumn +        self.fillGlyphs() +        if keepColor and coloredColumn is not None and \ +                coloredColumn < self.columnCount(): +            self.colorColumn(coloredColumn) +        self.setEditTriggers(QAbstractItemView.CurrentChanged) +        self.blockSignals(False) + +    def _cellEdited(self, row, col): +        if row == 0 or col == 0: +            return +        item = self.item(row, col).text() +        # Glyphs that do not have outlines leave empty cells, can't convert +        # that to a scalar +        if not item: +            return +        item = int(item) +        # -1 because the first col contains descriptive text +        glyph = self.glyphs[col - 1] +        # != comparisons avoid making glyph dirty when editor content is +        # unchanged +        self._editing = True +        if row == 1: +            if item != glyph.width: +                glyph.width = item +        elif row == 2: +            if item != glyph.leftMargin: +                glyph.leftMargin = item +        elif row == 3: +            if item != glyph.rightMargin: +                glyph.rightMargin = item +        self._editing = False +        # defcon callbacks do the update + +    def _itemChanged(self, current, previous): +        if current is not None: +            cur = current.column() +        if previous is not None: +            prev = previous.column() +            if current is not None and cur == prev: +                return +        self.colorColumn(current if current is None else cur) +        if self.selectionChangedCallback is not None: +            if current is not None: +                self.selectionChangedCallback(cur - 1) +            else: +                self.selectionChangedCallback(None) + +    def colorColumn(self, column): +        emptyBrush = QBrush(Qt.NoBrush) +        selectionColor = QColor(235, 235, 235) +        for i in range(4): +            if self._coloredColumn is not None: +                item = self.item(i, self._coloredColumn) +                # cached column might be invalid if user input deleted it +                if item is not None: +                    item.setBackground(emptyBrush) +            if column is not None: +                self.item(i, column).setBackground(selectionColor) +        self._coloredColumn = column + +    def sizeHint(self): +        # http://stackoverflow.com/a/7216486/2037879 +        height = sum(self.rowHeight(k) for k in range(self.rowCount())) +        height += self.horizontalScrollBar().sizeHint().height() +        margins = self.contentsMargins() +        height += margins.top() + margins.bottom() +        return QSize(self.width(), height) + +    def setCurrentGlyph(self, glyphIndex): +        self.blockSignals(True) +        if glyphIndex is not None: +            # so we can scroll to the item +            self.setCurrentCell(1, glyphIndex + 1) +        self.setCurrentItem(None) +        if glyphIndex is not None: +            self.colorColumn(glyphIndex + 1) +        else: +            self.colorColumn(glyphIndex) +        self.blockSignals(False) + +    def fillGlyphs(self): +        def glyphTableWidgetItem(content, disableCell=False): +            if isinstance(content, float): +                content = round(content) +            if content is not None: +                content = str(content) +            item = SpaceTableWidgetItem(content) +            if disableCell: +                item.setFlags(Qt.NoItemFlags) +            elif content is None: +                item.setFlags(Qt.ItemIsEnabled) +            # TODO: should fields be centered? I find left-aligned more +            # natural to read, personally... +            # item.setTextAlignment(Qt.AlignCenter) +            return item + +        self._coloredColumn = None +        self.setColumnCount(len(self.glyphs) + 1) +        for index, glyph in enumerate(self.glyphs): +            # TODO: see about allowing glyph name edit here +            self.setItem(0, index + 1, glyphTableWidgetItem(glyph.name, True)) +            self.setItem(1, index + 1, glyphTableWidgetItem(glyph.width)) +            self.setItem(2, index + 1, glyphTableWidgetItem(glyph.leftMargin)) +            self.setItem(3, index + 1, glyphTableWidgetItem(glyph.rightMargin)) +            self.setColumnWidth(index + 1, self._cellWidth) + +    def wheelEvent(self, event): +        # A mouse can only scroll along the y-axis. Use x-axis if we have one +        # (e.g. from touchpad), otherwise use y-axis. +        angleDelta = event.angleDelta().x() or event.angleDelta().y() +        cur = self.horizontalScrollBar().value() +        self.horizontalScrollBar().setValue(cur - angleDelta / 120) +        event.accept() | 
