diff options
| author | Adrien Tétar | 2015-09-21 20:40:35 +0200 | 
|---|---|---|
| committer | Adrien Tétar | 2015-09-21 20:40:35 +0200 | 
| commit | f83831e31597e8c811f5b3b75cb98a0ff3590a8c (patch) | |
| tree | 81665d9e00cf3752bd245b1c64a920b1781cc669 /Lib/defconQt/fontView.py | |
| parent | 748bb567eef19dcd8e067934786950c5866dc580 (diff) | |
| download | trufont-f83831e31597e8c811f5b3b75cb98a0ff3590a8c.tar.bz2 | |
meta: refactorings and cleanups, fontView: partial rewrite, new AddGlyphsWindow, extract cells widget to a separate location, support CharacterSet objects fully
Diffstat (limited to 'Lib/defconQt/fontView.py')
| -rw-r--r-- | Lib/defconQt/fontView.py | 755 | 
1 files changed, 315 insertions, 440 deletions
| diff --git a/Lib/defconQt/fontView.py b/Lib/defconQt/fontView.py index cf08fe9..ba773c3 100644 --- a/Lib/defconQt/fontView.py +++ b/Lib/defconQt/fontView.py @@ -1,55 +1,149 @@ -import math -import os -import unicodedata -  from defcon import Font  from defconQt.featureTextEditor import MainEditWindow  from defconQt.fontInfo import TabDialog +from defconQt.glyphCollectionView import GlyphCollectionWidget  from defconQt.glyphView import MainGfxWindow  from defconQt.groupsView import GroupsWindow +from defconQt.objects.defcon import CharacterSet  from defconQt.spaceCenter import MainSpaceWindow +from fontTools.agl import AGL2UV  # TODO: remove globs when things start to stabilize  from PyQt5.QtCore import *  from PyQt5.QtGui import *  from PyQt5.QtWidgets import * +import os +import unicodedata  cannedDesign = [      dict(type="cannedDesign", allowPseudoUnicode=True)  ]  sortItems = ["alphabetical", "category", "unicode", "script", "suffix",      "decompositionBase", "weightedSuffix", "ligature"] +latin1 = CharacterSet( +["space","exclam","quotesingle","quotedbl","numbersign","dollar", +"percent","ampersand","parenleft","parenright","asterisk","plus","comma", +"hyphen","period","slash","zero","one","two","three","four","five", +"six","seven","eight","nine","colon","semicolon","less","equal", +"greater","question","at","A","B","C","D","E","F","G","H","I","J", +"K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z", +"bracketleft","backslash","bracketright","asciicircum","underscore","grave", +"a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t", +"u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","exclamdown", +"cent","sterling","currency","yen","brokenbar","section","dieresis","copyright", +"ordfeminine","guillemotleft","logicalnot","registered","macron","degree", +"plusminus","twosuperior","threesuperior","acute","mu","paragraph", +"periodcentered","cedilla","onesuperior","ordmasculine","guillemotright", +"onequarter","onehalf","threequarters","questiondown","Agrave","Aacute", +"Acircumflex","Atilde","Adieresis","Aring","AE","Ccedilla","Egrave","Eacute", +"Ecircumflex","Edieresis","Igrave","Iacute","Icircumflex","Idieresis","Eth", +"Ntilde","Ograve","Oacute","Ocircumflex","Otilde","Odieresis","multiply", +"Oslash","Ugrave","Uacute","Ucircumflex","Udieresis","Yacute","Thorn", +"germandbls","agrave","aacute","acircumflex","atilde","adieresis","aring","ae", +"ccedilla","egrave","eacute","ecircumflex","edieresis","igrave","iacute", +"icircumflex","idieresis","eth","ntilde","ograve","oacute","ocircumflex", +"otilde","odieresis","divide","oslash","ugrave","uacute","ucircumflex", +"udieresis","yacute","thorn","ydieresis","dotlessi","circumflex","caron", +"breve","dotaccent","ring","ogonek","tilde","hungarumlaut","quoteleft", +"quoteright","minus"], "Latin-1") + +# TODO: implement Frederik's Glyph Construction Builder +class AddGlyphDialog(QDialog): +    def __init__(self, currentGlyphs=None, parent=None): +        super(AddGlyphDialog, self).__init__(parent) +        self.setWindowModality(Qt.WindowModal) +        self.setWindowTitle("Add glyphs…") +        self.currentGlyphs = currentGlyphs +        self.currentGlyphNames = [glyph.name for glyph in currentGlyphs] + +        layout = QGridLayout(self) +        self.importCharDrop = QComboBox(self) +        self.importCharDrop.addItem("Import glyphnames…") +        self.importCharDrop.addItem("Latin-1", latin1) +        self.importCharDrop.currentIndexChanged[int].connect(self.importCharacters) +        self.addGlyphEdit = QPlainTextEdit(self) +        self.addGlyphEdit.setFocus(True) + +        self.sortFontBox = QCheckBox("Sort font", self) +        self.overwriteBox = QCheckBox("Override", self) +        buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) +        buttonBox.accepted.connect(self.accept) +        buttonBox.rejected.connect(self.reject) -cellGridColor = QColor(130, 130, 130) -cellHeaderBaseColor = QColor(230, 230, 230) -cellHeaderLineColor = QColor(220, 220, 220) -cellHeaderHighlightLineColor = QColor(240, 240, 240) -cellSelectionColor = QColor.fromRgbF(.2, .3, .7, .15) - -GlyphCellBufferHeight = .2 -GlyphCellHeaderHeight = 14 +        l = 0 +        layout.addWidget(self.importCharDrop, l, 3) +        l += 1 +        layout.addWidget(self.addGlyphEdit, l, 0, 1, 4) +        l += 1 +        layout.addWidget(self.sortFontBox, l, 0) +        layout.addWidget(self.overwriteBox, l, 1) +        layout.addWidget(buttonBox, l, 3) +        self.setLayout(layout) -class SortingWindow(QDialog): -    def __init__(self, parent=None): -        super(SortingWindow, self).__init__(parent) +    @staticmethod +    def getNewGlyphNames(parent=None, currentGlyphs=None): +        dialog = AddGlyphDialog(currentGlyphs, parent) +        result = dialog.exec_() +        sortFont = False +        newGlyphNames = [] +        for name in dialog.addGlyphEdit.toPlainText().split(): +            if name not in dialog.currentGlyphNames: +                newGlyphNames.append(name) +        if dialog.sortFontBox.isChecked(): +            # XXX: if we get here with previous sort being by character set, +            # should it just stick? +            sortFont = True +        return (newGlyphNames, sortFont, result) + +    def importCharacters(self, index): +        if index == 0: return +        charset = self.importCharDrop.currentData() +        editorNames = self.addGlyphEdit.toPlainText().split() +        currentNames = set(self.currentGlyphNames) ^ set(editorNames) +        changed = False +        for name in charset.glyphNames: +            if name not in currentNames: +                changed = True +                editorNames.append(name) +        if changed: +            self.addGlyphEdit.setPlainText(" ".join(editorNames)) +            cursor = self.addGlyphEdit.textCursor() +            cursor.movePosition(QTextCursor.End, QTextCursor.MoveAnchor) +            self.addGlyphEdit.setTextCursor(cursor) +        self.importCharDrop.setCurrentIndex(0) +        self.addGlyphEdit.setFocus(True) + +class SortDialog(QDialog): +    def __init__(self, desc=None, parent=None): +        super(SortDialog, self).__init__(parent)          self.setWindowModality(Qt.WindowModal)          self.setWindowTitle("Sort…") -         +          self.smartSortBox = QRadioButton("Smart sort", self)          self.characterSetBox = QRadioButton("Character set", self) -        self.characterSetBox.setEnabled(False) +        self.characterSetBox.toggled.connect(self.characterSetToggle) +        self.characterSetDrop = QComboBox(self) +        self.characterSetDrop.setEnabled(False) +        # XXX: fetch from settings +        self.characterSetDrop.insertItem(0, "Latin 1")          self.customSortBox = QRadioButton("Custom…", self)          self.customSortBox.toggled.connect(self.customSortToggle) -         +          self.customSortGroup = QGroupBox(parent=self) -        desc = self.parent().characterWidget.sortDescriptor -        if desc[0]["type"] == "cannedDesign": +        self.customSortGroup.setEnabled(False) +        descriptorsCount = 6 +        if desc is None: +            # sort fetched from public.glyphOrder. no-op +            pass +        elif isinstance(desc, CharacterSet): +            self.characterSetBox.setChecked(True) +            self.characterSetDrop.setEnabled(True) +            # TODO: match cset name when QSettings support lands +        elif desc[0]["type"] == "cannedDesign":              self.smartSortBox.setChecked(True) -            self.customSortGroup.setEnabled(False) -            descriptorsCount = 6          else:              self.customSortBox.setChecked(True) +            self.customSortGroup.setEnabled(True)              descriptorsCount = len(desc) -        #elif desc ==           self.customDescriptors = [[] for i in range(descriptorsCount)]          self.customSortLayout = QGridLayout()          for i, line in enumerate(self.customDescriptors): @@ -81,19 +175,20 @@ class SortingWindow(QDialog):                  btn.setText("−")                  btn.pressed.connect(self._deleteRow)          self.customSortGroup.setLayout(self.customSortLayout) -         +          buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)          buttonBox.accepted.connect(self.accept)          buttonBox.rejected.connect(self.reject) -         +          layout = QVBoxLayout(self)          layout.addWidget(self.smartSortBox)          layout.addWidget(self.characterSetBox) +        layout.addWidget(self.characterSetDrop)          layout.addWidget(self.customSortBox)          layout.addWidget(self.customSortGroup)          layout.addWidget(buttonBox)          self.setLayout(layout) -     +      def _addRow(self):          i = len(self.customDescriptors)          line = [] @@ -113,8 +208,7 @@ class SortingWindow(QDialog):          self.customSortLayout.addWidget(line[2], i, 2)          self.customSortLayout.addWidget(line[3], i, 3)          if i == 7: self.sender().setEnabled(False) -         -     +      def _deleteRow(self):          rel = self.sender().property("index")          desc = self.customDescriptors @@ -127,356 +221,53 @@ class SortingWindow(QDialog):          del self.customDescriptors[-1]          self.addLineButton.setEnabled(True)          self.adjustSize() -     +      def indexFromItemName(self, name):          for index, item in enumerate(sortItems):              if name == item: return index          print("Unknown descriptor name: %s", name)          return 0 -     -    def accept(self): -        if self.smartSortBox.isChecked(): -            descriptors = cannedDesign -        elif self.customSortBox.isChecked(): + +    @staticmethod +    def getDescriptor(parent=None, sortDescriptor=None): +        dialog = SortDialog(sortDescriptor, parent) +        result = dialog.exec_() +        if dialog.characterSetBox.isChecked(): +            # TODO: dispatch csets when QSettings support lands +            ret = latin1 +        elif dialog.customSortBox.isChecked():              descriptors = [] -            for line in self.customDescriptors: +            for line in dialog.customDescriptors:                  descriptors.append(dict(type=line[0].currentText(), ascending=line[1].isChecked(),                      allowPseudoUnicode=line[2].isChecked())) -        self.parent().characterWidget.updateGlyphsFromFont(descriptors) -        super(SortingWindow, self).accept() -     +            ret = descriptors +        else: +            ret = cannedDesign +        return (ret, result) + +    def characterSetToggle(self): +        checkBox = self.sender() +        self.characterSetDrop.setEnabled(checkBox.isChecked()) +      def customSortToggle(self):          checkBox = self.sender()          self.customSortGroup.setEnabled(checkBox.isChecked()) -class CharacterWidget(QWidget): -    characterSelected = pyqtSignal(int, str) -    glyphOpened = pyqtSignal(str) - -    def __init__(self, font, squareSize=56, scrollArea=None, parent=None): -        super(CharacterWidget, self).__init__(parent) - -        self.font = font -        self.glyphs = [] -        self.scrollArea = scrollArea -        self.scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) -        self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) -        self.squareSize = squareSize -        self.columns = 10 -        self._selection = set() -        self.lastKey = -1 -        self.moveKey = -1 -        self.sortDescriptor = cannedDesign -         -        self._maybeDragPosition = None -        self.setFocusPolicy(Qt.ClickFocus) - -    def updateFont(self, font): -        self.font = font -        self.updateGlyphsFromFont() -     -    def updateGlyphsFromFont(self, descriptor=None): -        if descriptor is not None: self.sortDescriptor = descriptor -        self.glyphs = [self.font[k] for k in self.font.unicodeData.sortGlyphNames(self.font.keys(), self.sortDescriptor)] -        self._selection = set() -        self.adjustSize() -        self.update() -     -    def setGlyphs(self, glyphs): -        self.glyphs = glyphs -        self._selection = set() -        self.adjustSize() -        self.update() - -    def _sizeEvent(self, width, squareSize=None): -        if self.scrollArea is not None: sw = self.scrollArea.verticalScrollBar().width() + self.scrollArea.contentsMargins().right() -        else: sw = 0 -        if squareSize is not None: self.squareSize = squareSize -        columns = (width - sw) // self.squareSize -        if not columns > 0: return -        self.columns = columns -        self.adjustSize() - -    def sizeHint(self): -        # Calculate sizeHint with max(height, scrollArea.height()) because if scrollArea is -        # bigger than widget height after an update, we risk leaving old painted content on screen -        return QSize(self.columns * self.squareSize, -                max(math.ceil(len(self.glyphs) / self.columns) * self.squareSize, self.scrollArea.height())) -     -    def markSelection(self, color): -        for key in self._selection: -            glyph = self.glyphs[key] -            if color is None: -                if "public.markColor" in glyph.lib: -                    del glyph.lib["public.markColor"] -            else: -                glyph.lib["public.markColor"] = ",".join(str(c) for c in color.getRgbF()) -        self.update() -     -    # TODO: eventually get rid of the signal -    def computeCharacterSelected(self): -        lKey, mKey = self.lastKey, self.moveKey -        mKey = self.moveKey if self.moveKey < len(self.glyphs) else len(self.glyphs)-1 -        lKey = self.lastKey if self.lastKey < len(self.glyphs) else len(self.glyphs)-1 -        if lKey == -1: -            elements = set() -        elif lKey > mKey: -            elements = set(range(mKey, lKey+1)) -        else: -            elements = set(range(lKey, mKey+1)) -        elements ^= self._selection -        if len(elements)>1: self.characterSelected.emit(len(elements), "") -        elif len(elements)>0: self.characterSelected.emit(1, self.glyphs[elements.pop()].name) -        else: self.characterSelected.emit(0, "") -     -    def keyPressEvent(self, event): -        if event.key() == Qt.Key_A and event.modifiers() & Qt.ControlModifier: -            self._selection = set(range(len(self.glyphs))) -            self.computeCharacterSelected() -            self.update() -            event.accept() -        elif event.key() == Qt.Key_D and event.modifiers() & Qt.ControlModifier: -            self._selection = set() -            self.computeCharacterSelected() -            self.update() -            event.accept() -        else: -            super(CharacterWidget, self).keyPressEvent(event) - -    def mousePressEvent(self, event): -        if event.button() == Qt.LeftButton: -            key = (event.y() // self.squareSize) * self.columns + event.x() // self.squareSize -            if key > len(self.glyphs)-1: return -            modifiers = event.modifiers() -            if modifiers & Qt.ShiftModifier and len(self._selection) == 1: -                self.lastKey = self._selection.pop() -                self.moveKey = key -            elif modifiers & Qt.ControlModifier: -                self.lastKey = key -                self.moveKey = self.lastKey -            elif key in self._selection and not modifiers & Qt.ShiftModifier: -                self._maybeDragPosition = event.pos() -                event.accept() -                return -            else: -                self._selection = set() -                self.lastKey = key -                self.moveKey = self.lastKey -             -            self.computeCharacterSelected() -            event.accept() -            self.update() -        else: -            super(CharacterWidget, self).mousePressEvent(event) -     -    def mouseMoveEvent(self, event): -        if event.buttons() & Qt.LeftButton: -            if self._maybeDragPosition is not None: -                if ((event.pos() - self._maybeDragPosition).manhattanLength() \ -                    < QApplication.startDragDistance()): return -                # TODO: needs ordering or not? -                glyphList = " ".join(self.glyphs[key].name for key in self._selection) -                drag = QDrag(self) -                mimeData = QMimeData() -                mimeData.setData("text/plain", glyphList) -                drag.setMimeData(mimeData) -                 -                dropAction = drag.exec_() -                self._maybeDragPosition = None -                event.accept() -                return -            key = (event.y() // self.squareSize) * self.columns + min(event.x() // self.squareSize, self.columns-1) -            if key > len(self.glyphs)-1: return -            self.moveKey = key -             -            self.computeCharacterSelected() -            event.accept() -            self.update() -        else: -            super(CharacterWidget, self).mouseMoveEvent(event) -     -    def mouseReleaseEvent(self, event): -        if event.button() == Qt.LeftButton: -            self._maybeDragPosition = None -            if self.lastKey == -1: -                if self._maybeDragPosition is None: -                    key = (event.y() // self.squareSize) * self.columns + event.x() // self.squareSize -                    if key > len(self.glyphs)-1: return -                    self._selection = {key} -                    self.computeCharacterSelected() -            else: -                lastKey = self.lastKey if self.lastKey < len(self.glyphs) else len(self.glyphs)-1 -                moveKey = self.moveKey if self.moveKey < len(self.glyphs) else len(self.glyphs)-1 -                if event.modifiers() & Qt.ControlModifier: -                    if moveKey > lastKey: -                        self._selection ^= set(range(lastKey, moveKey+1)) -                    else: -                        self._selection ^= set(range(moveKey, lastKey+1)) -                else: -                    if moveKey > lastKey: -                        self._selection = set(range(lastKey, moveKey+1)) -                    else: -                        self._selection = set(range(moveKey, lastKey+1)) -                self.lastKey = -1 -                self.moveKey = -1 -            event.accept() -            self.update() -        else: -            super(CharacterWidget, self).mouseReleaseEvent(event) -    -    def mouseDoubleClickEvent(self, event): -        if event.button() == Qt.LeftButton: -            key = (event.y() // self.squareSize) * self.columns + event.x() // self.squareSize -            if key > len(self.glyphs)-1: event.ignore(); return -            self._selection -= {key} -            self.lastKey = key -            self.moveKey = self.lastKey -            event.accept() -            self.glyphOpened.emit(self.glyphs[key].name) -        else: -            super(CharacterWidget, self).mousePressEvent(event) - -    # TODO: see if more of this process can be delegated to a factory -    def paintEvent(self, event): -        painter = QPainter(self) -        painter.setRenderHint(QPainter.Antialiasing) - -        redrawRect = event.rect() -        beginRow = redrawRect.top() // self.squareSize -        endRow = redrawRect.bottom() // self.squareSize -        beginColumn = redrawRect.left() // self.squareSize -        endColumn = redrawRect.right() // self.squareSize -         -        # painter.setPen(cellGridColor) -        # painter.drawLine(redrawRect.left(), redrawRect.top(), redrawRect.left()+self.squareSize \ -            # *min(len(self.glyphs), self.columns), redrawRect.top()+self.squareSize*(math.ceil(len(self.glyphs)/self.columns))) -        # painter.drawLine(0, 0, redrawRect.right(), 0) - -        # selection code -        if self.moveKey != -1: -            if self.moveKey > self.lastKey: -                curSelection = set(range(self.lastKey, self.moveKey+1)) -            else: -                curSelection = set(range(self.moveKey, self.lastKey+1)) -        elif self.lastKey != -1: # XXX: necessary? -            curSelection = {self.lastKey} -        else: -            curSelection = set() -        curSelection ^= self._selection -             -        gradient = QLinearGradient(0, 0, 0, GlyphCellHeaderHeight) -        gradient.setColorAt(0.0, cellHeaderBaseColor) -        gradient.setColorAt(1.0, cellHeaderLineColor) -        dirtyGradient = QLinearGradient(0, 0, 0, GlyphCellHeaderHeight) -        dirtyGradient.setColorAt(0.0, cellHeaderBaseColor.darker(125)) -        dirtyGradient.setColorAt(1.0, cellHeaderLineColor.darker(125)) -        #markGradient = QRadialGradient(self.squareSize/2, GlyphCellHeaderHeight/2, -        #      self.squareSize-GlyphCellHeaderHeight, self.squareSize/2, self.squareSize) -        markGradient = QLinearGradient(0, 0, 0, self.squareSize-GlyphCellHeaderHeight) -        headerFont = QFont() -        headerFont.setFamily('Lucida Sans Unicode') -        headerFont.insertSubstitution('Lucida Sans Unicode', 'Luxi Sans') -        headerFont.setPointSize(8) -        metrics = QFontMetrics(headerFont) - -        for row in range(beginRow, endRow + 1): -            for column in range(beginColumn, endColumn + 1): -                key = row * self.columns + column -                if key > len(self.glyphs)-1: break - -                painter.save() -                painter.translate(column * self.squareSize, row * self.squareSize) -                # background -                painter.fillRect(0, 0, self.squareSize, self.squareSize, Qt.white) -                glyph = self.glyphs[key] -                if "public.markColor" in glyph.lib: -                    colorStr = glyph.lib["public.markColor"].split(",") -                    if len(colorStr) == 4: -                        comp = [] -                        for c in colorStr: -                            comp.append(float(c.strip())) -                        markColor = QColor.fromRgbF(*comp) -                        markGradient.setColorAt(1.0, markColor) -                        markGradient.setColorAt(0.0, markColor.lighter(125)) -                        painter.fillRect(0, GlyphCellHeaderHeight, self.squareSize, -                              self.squareSize - GlyphCellHeaderHeight, QBrush(markGradient)) -                 -                # header gradient -                if glyph.dirty: col = dirtyGradient -                else: col = gradient -                painter.fillRect(0, 0, self.squareSize, -                      GlyphCellHeaderHeight, QBrush(col)) -                # header lines -                if glyph.dirty: col = cellHeaderHighlightLineColor.darker(110) -                else: col = cellHeaderHighlightLineColor -                painter.setPen(col) -                minOffset = painter.pen().width() -                painter.setRenderHint(QPainter.Antialiasing, False) -                painter.drawLine(0, 0, 0, GlyphCellHeaderHeight - 1) -                painter.drawLine(self.squareSize - 2, 0, self.squareSize - 2, GlyphCellHeaderHeight -1) -                painter.setPen(QColor(170, 170, 170)) -                painter.drawLine(0, GlyphCellHeaderHeight, self.squareSize, GlyphCellHeaderHeight) -                painter.setRenderHint(QPainter.Antialiasing) -                # header text -                painter.setFont(headerFont) -                painter.setPen(QColor(80, 80, 80)) -                name = metrics.elidedText(self.glyphs[key].name, Qt.ElideRight, self.squareSize - 2) -                painter.drawText(1, 0, self.squareSize - 2, GlyphCellHeaderHeight - minOffset, -                      Qt.TextSingleLine | Qt.AlignCenter, name) -                painter.restore() -                 -                painter.setPen(cellGridColor) -                rightEdgeX = column * self.squareSize + self.squareSize -                bottomEdgeY = row * self.squareSize + self.squareSize -                painter.drawLine(rightEdgeX, row * self.squareSize + 1, rightEdgeX, bottomEdgeY) -                painter.drawLine(rightEdgeX, bottomEdgeY, column * self.squareSize + 1, bottomEdgeY) - -                # selection code -                painter.setRenderHint(QPainter.Antialiasing, False) -                if key in curSelection: -                    painter.fillRect(column * self.squareSize + 1, -                            row * self.squareSize + 1, self.squareSize - 3, -                            self.squareSize - 3, cellSelectionColor) -                painter.setRenderHint(QPainter.Antialiasing) - -                glyph = self.glyphs[key].getRepresentation("defconQt.QPainterPath") -                if self.font.info.unitsPerEm is None: break -                if not self.font.info.unitsPerEm > 0: self.font.info.unitsPerEm = 1000 -                factor = (self.squareSize-GlyphCellHeaderHeight)/(self.font.info.unitsPerEm*(1+2*GlyphCellBufferHeight)) -                x_offset = (self.squareSize-self.glyphs[key].width*factor)/2 -                # If the glyph overflows horizontally we need to adjust the scaling factor -                if x_offset < 0: -                    factor *= 1+2*x_offset/(self.glyphs[key].width*factor) -                    x_offset = 0 -                # TODO: the * 1.8 below is somewhat artificial -                y_offset = self.font.info.descender*factor * 1.8 -                painter.save() -                painter.setClipRect(column * self.squareSize, row * self.squareSize+GlyphCellHeaderHeight, -                      self.squareSize, self.squareSize-GlyphCellHeaderHeight) -                painter.translate(column * self.squareSize + x_offset, row * self.squareSize + self.squareSize + y_offset) -                painter.scale(factor, -factor) -                painter.fillPath(glyph, Qt.black) -                painter.restore() -  class MainWindow(QMainWindow): -    def __init__(self, font=Font()): +    def __init__(self, font):          super(MainWindow, self).__init__() - -        self.font = font -        self.font.addObserver(self, "_fontChanged", "Font.Changed") -        # TODO: have the scrollarea be part of the widget itself? -        # or better yet, switch to QGraphicsScene -        self.scrollArea = QScrollArea(self)          squareSize = 56 -        self.characterWidget = CharacterWidget(self.font, squareSize, self.scrollArea, self) -        self.characterWidget.updateGlyphsFromFont() -        self.characterWidget.setFocus() -        self.scrollArea.setWidget(self.characterWidget) +        self.collectionWidget = GlyphCollectionWidget(self) +        self._font = None +        self._sortDescriptor = None +        self.font = font +        self.collectionWidget.characterSelectedCallback = self._selectionChanged +        self.collectionWidget.doubleClickCallback = self._glyphOpened +        self.collectionWidget.setFocus() +        menuBar = self.menuBar()          # TODO: make shortcuts platform-independent          fileMenu = QMenu("&File", self) -        self.menuBar().addMenu(fileMenu) -          fileMenu.addAction("&New…", self.newFile, QKeySequence.New)          fileMenu.addAction("&Open…", self.openFile, QKeySequence.Open)          # TODO: add functionality @@ -485,47 +276,48 @@ class MainWindow(QMainWindow):          fileMenu.addAction("&Save", self.saveFile, QKeySequence.Save)          fileMenu.addAction("Save &As…", self.saveFileAs, QKeySequence.SaveAs)          fileMenu.addAction("E&xit", self.close, QKeySequence.Quit) +        menuBar.addMenu(fileMenu)          selectionMenu = QMenu("&Selection", self) -        self.menuBar().addMenu(selectionMenu) -                  markColorMenu = QMenu("Mark color", self)          pixmap = QPixmap(24, 24) -        none = markColorMenu.addAction("None", self.colorFill) +        none = markColorMenu.addAction("None", self.markColor)          none.setData(None) -        red = markColorMenu.addAction("Red", self.colorFill) +        red = markColorMenu.addAction("Red", self.markColor)          pixmap.fill(Qt.red)          red.setIcon(QIcon(pixmap))          red.setData(QColor(Qt.red)) -        yellow = markColorMenu.addAction("Yellow", self.colorFill) +        yellow = markColorMenu.addAction("Yellow", self.markColor)          pixmap.fill(Qt.yellow)          yellow.setIcon(QIcon(pixmap))          yellow.setData(QColor(Qt.yellow)) -        green = markColorMenu.addAction("Green", self.colorFill) +        green = markColorMenu.addAction("Green", self.markColor)          pixmap.fill(Qt.green)          green.setIcon(QIcon(pixmap))          green.setData(QColor(Qt.green))          selectionMenu.addMenu(markColorMenu) -        selectionMenu.addSeparator() -        selectionMenu.addAction("Sort…", self.sortCharacters) +        menuBar.addMenu(selectionMenu)          fontMenu = QMenu("&Font", self) -        self.menuBar().addMenu(fontMenu) -         -        # TODO: work out sensible shortcuts -        fontMenu.addAction("Font &info", self.fontInfo, "Ctrl+I") -        fontMenu.addAction("Font &features", self.fontFeatures, "Ctrl+F") +        # TODO: work out sensible shortcuts and make sure they're cross-platform +        # ready - consider extracting them into separate file?          fontMenu.addAction("&Add glyph", self.addGlyph, "Ctrl+U") +        fontMenu.addAction("Font &info", self.fontInfo, "Ctrl+M") +        fontMenu.addAction("Font &features", self.fontFeatures, "Ctrl+F")          fontMenu.addSeparator() -        fontMenu.addAction("&Space center", self.spaceCenter, "Ctrl+Y") -        fontMenu.addAction("&Groups window", self.fontGroups, "Ctrl+G") +        fontMenu.addAction("Sort…", self.sortCharacters) +        menuBar.addMenu(fontMenu) -        helpMenu = QMenu("&Help", self) -        self.menuBar().addMenu(helpMenu) +        windowMenu = QMenu("&Windows", self) +        windowMenu.addAction("&Space center", self.spaceCenter, "Ctrl+Y") +        windowMenu.addAction("&Groups window", self.fontGroups, "Ctrl+G") +        menuBar.addMenu(windowMenu) +        helpMenu = QMenu("&Help", self)          helpMenu.addAction("&About", self.about)          helpMenu.addAction("About &Qt", QApplication.instance().aboutQt) -         +        menuBar.addMenu(helpMenu) +          self.sqSizeSlider = QSlider(Qt.Horizontal, self)          self.sqSizeSlider.setMinimum(36)          self.sqSizeSlider.setMaximum(96) @@ -533,15 +325,13 @@ class MainWindow(QMainWindow):          self.sqSizeSlider.setValue(squareSize)          self.sqSizeSlider.valueChanged.connect(self._squareSizeChanged)          self.selectionLabel = QLabel(self) -        self.statusBar().addPermanentWidget(self.sqSizeSlider) -        self.statusBar().addWidget(self.selectionLabel) +        statusBar = self.statusBar() +        statusBar.addPermanentWidget(self.sqSizeSlider) +        statusBar.addWidget(self.selectionLabel) -        self.setCentralWidget(self.scrollArea) -        self.characterWidget.characterSelected.connect(self._selectionChanged) -        self.characterWidget.glyphOpened.connect(self._glyphOpened) -        self.setWindowTitle() -        # TODO: dump the hardcoded path -        #self.setWindowIcon(QIcon("C:\\Users\\Adrien\\Downloads\\defconQt\\Lib\\defconQt\\resources\\icon.png")) +        self.setCentralWidget(self.collectionWidget.scrollArea()) +        self.resize(605, 430) +        self.setWindowTitle() # TODO: maybe clean this up      def newFile(self):          ok = self.maybeSaveBeforeExit() @@ -553,28 +343,36 @@ class MainWindow(QMainWindow):          self.font.info.capHeight = 750          self.font.info.xHeight = 500          self.setWindowTitle("Untitled.ufo") -        self.characterWidget.updateFont(self.font) +        self.sortDescriptor = latin1      def openFile(self, path=None):          if not path: -            path, _ = QFileDialog.getOpenFileName(self, "Open File", '', +            path, ok = QFileDialog.getOpenFileName(self, "Open File", '',                      "UFO Fonts (metainfo.plist)") - +            if not ok: return          if path:              # TODO: error handling              path = os.path.dirname(path) +            # TODO: I note that a change of self.font often goes with setWindowTitle(). +            # Be more DRY.              self.font = Font(path) -            self.characterWidget.updateFont(self.font)              self.setWindowTitle()      def saveFile(self, path=None):          if path is None and self.font.path is None:              self.saveFileAs()          else: +            glyphs = self.collectionWidget.glyphs +            # TODO: save sortDescriptor somewhere in lib as well +            glyphNames = [] +            for glyph in glyphs: +                glyphNames.append(glyph.name) +            self.font.lib["public.glyphOrder"] = glyphNames              self.font.save(path=path) -#            self.font.dirty = False +            self.font.dirty = False +            for glyph in self.font: +                glyph.dirty = False              self.setWindowModified(False) -#            self.font.path = path # done by defcon      def saveFileAs(self):          path, ok = QFileDialog.getSaveFileName(self, "Save File", '', @@ -583,20 +381,21 @@ class MainWindow(QMainWindow):              self.saveFile(path)              self.setWindowTitle()          #return ok -     +      def close(self):          self.font.removeObserver(self, "Font.Changed")          QApplication.instance().quit() -         +      def closeEvent(self, event):          ok = self.maybeSaveBeforeExit()          if not ok: event.ignore()          else: event.accept() -     +      def maybeSaveBeforeExit(self):          if self.font.dirty:              title = "Me"              if self.font.path is not None: +                # TODO: maybe cache this font name in the Font                  currentFont = os.path.basename(self.font.path.rstrip(os.sep))              else:                  currentFont = "Untitled.ufo" @@ -613,42 +412,110 @@ class MainWindow(QMainWindow):                  return True              return False          return True -     -    def colorFill(self): -        action = self.sender() -        self.characterWidget.markSelection(action.data()) -     + +    def _get_font(self): +        return self._font + +    # TODO: consider that user may want to change font without sortDescriptor +    # be calculated and set magically (and therefore, arbitrarily) +    # In that case is it reasonable to just leave self._font? +    def _set_font(self, font): +        if self._font is not None: +            self._font.removeObserver(self, "Font.Changed") +        self._font = font +        self._font.addObserver(self, "_fontChanged", "Font.Changed") +        if "public.glyphOrder" in self._font.lib: +            self.sortDescriptor = CharacterSet( +                self._font.lib["public.glyphOrder"]) +        else: +            # TODO: cannedDesign or carry sortDescriptor from previous font? +            self.sortDescriptor = cannedDesign + +    font = property(_get_font, _set_font, doc="The fontView font. Subscribes \ +        to the new font, updates the sorting order and cells widget when set.") + +    def _get_sortDescriptor(self): +        return self._sortDescriptor + +    def _set_sortDescriptor(self, desc): +        if isinstance(desc, CharacterSet): +            cnt = 0 +            glyphs = [] +            for glyphName in desc.glyphNames: +                if not glyphName in self._font: +                    # create a template glyph +                    self.newStandardGlyph(glyphName) +                    self._font[glyphName].template = True +                else: +                    cnt += 1 +                glyphs.append(self._font[glyphName]) +            if cnt < len(self._font): +                # somehow some glyphs in the font are not present in the glyph +                # order, loop again to add these at the end +                for glyph in self._font: +                    if not glyph in glyphs: +                     glyphs.append(glyph) +        else: +            glyphs = [self._font[k] for k in self._font.unicodeData +                .sortGlyphNames(self._font.keys(), desc)] +        self.collectionWidget.glyphs = glyphs +        self._sortDescriptor = desc + +    sortDescriptor = property(_get_sortDescriptor, _set_sortDescriptor, +        doc="The sortDescriptor. Takes glyphs from the font and sorts them \ +        when set.") + +    def markColor(self): +        color = self.sender().data() +        glyphs = self.collectionWidget.glyphs +        for key in self.collectionWidget.selection: +            glyph = glyphs[key] +            if color is None: +                if "public.markColor" in glyph.lib: +                    del glyph.lib["public.markColor"] +            else: +                glyph.lib["public.markColor"] = ",".join(str(c) for c in color.getRgbF()) + +    def newStandardGlyph(self, name): +        self.font.newGlyph(name) +        self.font[name].width = 500 +        # TODO: we should not force-add unicode, also list ought to be +        # changeable from AGL2UV +        if name in AGL2UV: self.font[name].unicode = AGL2UV[name] +      def _fontChanged(self, notification): -        self.characterWidget.update() +        self.collectionWidget.update()          self.setWindowModified(True) -    def _glyphOpened(self, name): -        glyphViewWindow = MainGfxWindow(self.font, self.font[name], self) +    def _glyphOpened(self, glyph): +        glyphViewWindow = MainGfxWindow(glyph, self)          glyphViewWindow.show() -     -    def _selectionChanged(self, count, glyph): -        if count == 0: self.selectionLabel.setText("") -        else: self.selectionLabel.setText("%s%s%s%d %s" % (glyph, " " if count <= 1 else "", "(", count, "selected)")) + +    def _selectionChanged(self, selection): +        if selection is not None: +            if isinstance(selection, str): +                count = 1 +                text = "%s " % selection +            else: +                count = selection +                text = "" +            if not count == 0: +                text = "%s(%d selected)" % (text, count) +        else: text = "" +        self.selectionLabel.setText(text)      def _squareSizeChanged(self):          val = self.sqSizeSlider.value() -        self.characterWidget._sizeEvent(self.width(), val) +        self.collectionWidget._sizeEvent(self.width(), val)          QToolTip.showText(QCursor.pos(), str(val), self)      def resizeEvent(self, event): -        if self.isVisible(): self.characterWidget._sizeEvent(event.size().width()) +        if self.isVisible(): self.collectionWidget._sizeEvent(event.size().width())          super(MainWindow, self).resizeEvent(event) -     +      def setWindowTitle(self, title=None):          if title is None: title = os.path.basename(self.font.path.rstrip(os.sep))          super(MainWindow, self).setWindowTitle("[*]{}".format(title)) -     -    def sortCharacters(self): -        if not (hasattr(self, 'sortingWindow') and self.sortingWindow.isVisible()): -           self.sortingWindow = SortingWindow(self) -           self.sortingWindow.show() -        else: -           self.sortingWindow.raise_()      def fontInfo(self):          # If a window is already opened, bring it to the front, else spawn one. @@ -656,20 +523,20 @@ class MainWindow(QMainWindow):          # it seems we're just leaking memory after each close... (both raise_ and          # show allocate memory instead of using the hidden widget it seems)          if not (hasattr(self, 'fontInfoWindow') and self.fontInfoWindow.isVisible()): -           self.fontInfoWindow = TabDialog(self.font, self) -           self.fontInfoWindow.show() +            self.fontInfoWindow = TabDialog(self.font, self) +            self.fontInfoWindow.show()          else: -           # Should data be rewind if user calls font info while one is open? -           # I'd say no, but this has yet to be settled. -           self.fontInfoWindow.raise_() +            # Should data be rewind if user calls font info while one is open? +            # I'd say no, but this has yet to be settled. +            self.fontInfoWindow.raise_()      def fontFeatures(self):          # TODO: see up here          if not (hasattr(self, 'fontFeaturesWindow') and self.fontFeaturesWindow.isVisible()): -           self.fontFeaturesWindow = MainEditWindow(self.font, self) -           self.fontFeaturesWindow.show() +            self.fontFeaturesWindow = MainEditWindow(self.font, self) +            self.fontFeaturesWindow.show()          else: -           self.fontFeaturesWindow.raise_() +            self.fontFeaturesWindow.raise_()      def spaceCenter(self):          # TODO: see up here @@ -679,27 +546,36 @@ class MainWindow(QMainWindow):              self.spaceCenterWindow.show()          else:              self.spaceCenterWindow.raise_() -        if self.characterWidget._selection: +        selection = self.collectionWidget.selection +        if selection:              glyphs = [] -            for item in sorted(self.characterWidget._selection): -                glyphs.append(self.characterWidget.glyphs[item]) +            for item in sorted(selection): +                glyph = self.collectionWidget.glyphs[item] +                glyphs.append(glyph)              self.spaceCenterWindow.setGlyphs(glyphs) -     +      def fontGroups(self):          # TODO: see up here          if not (hasattr(self, 'fontGroupsWindow') and self.fontGroupsWindow.isVisible()): -           self.fontGroupsWindow = GroupsWindow(self.font, self) -           self.fontGroupsWindow.show() +            self.fontGroupsWindow = GroupsWindow(self.font, self) +            self.fontGroupsWindow.show()          else: -           self.fontGroupsWindow.raise_() -     +            self.fontGroupsWindow.raise_() + +    def sortCharacters(self): +        sortDescriptor, ok = SortDialog.getDescriptor(self, self.sortDescriptor) +        if ok: +            self.sortDescriptor = sortDescriptor +      def addGlyph(self): -        gName, ok = QInputDialog.getText(self, "Add glyph", "Name of the glyph:") -        # Not overwriting existing glyphs. Should it warn in this case? (rf) -        if ok and gName != '': -            self.font.newGlyph(gName) -            self.font[gName].width = 500 -            self.characterWidget.updateGlyphsFromFont() +        glyphs = self.collectionWidget.glyphs +        newGlyphNames, sortFont, ok = AddGlyphDialog.getNewGlyphNames(self, glyphs) +        if ok: +            for name in newGlyphNames: +                # XXX: if sortFont +                self.newStandardGlyph(name) +                glyphs.append(self.font[name]) +            self.collectionWidget.glyphs = glyphs      def about(self):          QMessageBox.about(self, "About Me", @@ -707,4 +583,3 @@ class MainWindow(QMainWindow):                  "<p>I am a new UFO-centric font editor and I aim to bring the <b>robofab</b> " \                  "ecosystem to all main operating systems, in a fast and dependency-free " \                  "package.</p>") - | 
