From 1a889dc612c036763d80815a00d4b9a8fc73ba78 Mon Sep 17 00:00:00 2001 From: Adrien Tétar Date: Thu, 29 Oct 2015 21:09:19 +0100 Subject: meta: shufflings, loadMostRecentFile, metricsWindow: canvas key shortcuts --- Lib/defconQt/metricsWindow.py | 879 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 879 insertions(+) create mode 100644 Lib/defconQt/metricsWindow.py (limited to 'Lib/defconQt/metricsWindow.py') 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() -- cgit v1.2.3