diff options
Diffstat (limited to 'Lib/defconQt/glyphCollectionView.py')
| -rw-r--r-- | Lib/defconQt/glyphCollectionView.py | 381 | 
1 files changed, 381 insertions, 0 deletions
| diff --git a/Lib/defconQt/glyphCollectionView.py b/Lib/defconQt/glyphCollectionView.py new file mode 100644 index 0000000..e49b32a --- /dev/null +++ b/Lib/defconQt/glyphCollectionView.py @@ -0,0 +1,381 @@ +from PyQt5.QtCore import QMimeData, QRectF, QSize, Qt +from PyQt5.QtGui import (QBrush, QColor, QDrag, QFont, QFontMetrics, QKeySequence, +    QLinearGradient, QPainter, QPen) +from PyQt5.QtWidgets import QApplication, QMessageBox, QScrollArea, QWidget +import math + +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 + +# TODO: consider extracting each platform-specific thing (fonts, shortcuts) in a +# purposed folder +headerFont = QFont() +headerFont.setFamily('Lucida Sans Unicode') +headerFont.insertSubstitution('Lucida Sans Unicode', 'Lucida Grande') +headerFont.insertSubstitution('Lucida Sans Unicode', 'Luxi Sans') +headerFont.setPointSize(8) +voidFont = QFont(headerFont) +voidFont.setPointSize(24) +metrics = QFontMetrics(headerFont) + +def proceedWithDeletion(self): +    closeDialog = QMessageBox(QMessageBox.Question, "", "Delete glyphs", +      QMessageBox.Yes | QMessageBox.No, self) +    closeDialog.setInformativeText("Are you sure you want to delete them?") +    closeDialog.setModal(True) +    ret = closeDialog.exec_() +    if ret == QMessageBox.Yes: +        return True +    return False + +""" +A widget that presents a list of glyphs in cells. +""" +class GlyphCollectionWidget(QWidget): +    def __init__(self, parent=None): +        super(GlyphCollectionWidget, self).__init__(parent) +        self._glyphs = [] +        # TODO: hide behind a façade +        self.squareSize = 56 +        self._columns = 10 +        self._selection = {} +        # TODO: consider replacing this with moveKey + set (which is generated +        # when painting anyway) +        self.lastKey = -1 +        self.moveKey = -1 + +        self.characterSelectedCallback = None +        self.doubleClickCallback = None +        self._maybeDragPosition = None + +        self.setFocusPolicy(Qt.ClickFocus) +        self._scrollArea = QScrollArea(parent) +        self._scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) +        self._scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) +        self._scrollArea.setWidget(self) + +    def _get_glyphs(self): +        return self._glyphs + +    def _set_glyphs(self, value): +        self._glyphs = value +        self.adjustSize() +        self.selection = set() +        #self.update() # self.selection changed will do it + +    glyphs = property(_get_glyphs, _set_glyphs, doc="A list of glyphs \ +        displayed. Clears selection and schedules display refresh when set.") + +    def _get_selection(self): +        return self._selection + +    def _set_selection(self, value): +        self._selection = value +        self.computeCharacterSelected() +        self.update() + +    selection = property(_get_selection, _set_selection, doc="A set that contains \ +        indexes of selected glyphs. Schedules display refresh when set.") + +    def scrollArea(self): +        return self._scrollArea + +    def scrollToCell(self, index): +        raise NotImplementedError + +    # TODO: break this down into set width/set square +    # TODO: see whether scrollArea gets resizeEvents +    def _sizeEvent(self, width, squareSize=None): +        sw = self._scrollArea.verticalScrollBar().width() + self._scrollArea.contentsMargins().right() +        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 computeCharacterSelected(self): +        if self.characterSelectedCallback is None: +            return +        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 + +        cnt = len(elements) +        if cnt == 1: +            self.characterSelectedCallback(self.glyphs[elements.pop()].name) +        else: +            self.characterSelectedCallback(cnt) + +    def keyPressEvent(self, event): +        key = event.key() +        modifiers = event.modifiers() +        if event.matches(QKeySequence.SelectAll): +            self.selection = set(range(len(self.glyphs))) +        elif key == Qt.Key_D and modifiers & Qt.ControlModifier: +            self.selection = set() +        # XXX: this is specific to fontView so should be done thru subclassing of a base widget, +        # as is done in groupsView +        elif key == Qt.Key_Delete: +            #if self.characterDeletionCallback is not None: +            if self.proceedWithDeletion() and self.selection: +                # we need to del in reverse order to keep key references valid +                for key in sorted(self._selection, reverse=True): +                    glyph = self.glyphs[key] +                    font = glyph.getParent() +                    if glyph in font: +                        del self.font[gName] +                    if modifiers & Qt.ShiftModifier: +                        # XXX: need a del fn in property +                        del self.glyphs[key] +                self.selection = set() +        else: +            super(GlyphCollectionWidget, self).keyPressEvent(event) +            return +        event.accept() + +    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 + +            # TODO: make sure lastKey/moveKey are taken care of before rmin this +            self.computeCharacterSelected() +            event.accept() +            self.update() +        else: +            super(GlyphCollectionWidget, 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.setText(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 < 0 or key > len(self.glyphs)-1: return +            self.moveKey = key + +            self.computeCharacterSelected() +            event.accept() +            self.update() +        else: +            super(GlyphCollectionWidget, 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} +            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 moveKey > lastKey: +                    sel = set(range(lastKey, moveKey+1)) +                else: +                    sel = set(range(moveKey, lastKey+1)) +                self.lastKey = -1 +                self.moveKey = -1 +                if event.modifiers() & Qt.ControlModifier: +                    self.selection ^= sel +                else: +                    self.selection = sel +            event.accept() +            self.update() +        else: +            super(GlyphCollectionWidget, 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() +            if self.doubleClickCallback is not None: +                self.doubleClickCallback(self.glyphs[key]) +        else: +            super(GlyphCollectionWidget, 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 +        # XXX: do we need to maintain self._column when we have (endColumn - +        # beginColumn)? +        beginColumn = redrawRect.left() // self.squareSize +        endColumn = redrawRect.right() // self.squareSize + +        # 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 = QLinearGradient(0, 0, 0, self.squareSize-GlyphCellHeaderHeight) + +        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 +                glyph = self.glyphs[key] + +                painter.save() +                painter.translate(column * self.squareSize, row * self.squareSize) +                painter.fillRect(0, 0, self.squareSize, self.squareSize, Qt.white) +                # prepare header colors +                brushColor = gradient +                linesColor = cellHeaderHighlightLineColor +                # mark color +                if not glyph.template: +                    # TODO: fetch via defcon dict +                    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)) +                    if glyph.dirty: +                        brushColor = dirtyGradient +                        linesColor = cellHeaderHighlightLineColor.darker(110) + +                # header gradient +                painter.fillRect(0, 0, self.squareSize, GlyphCellHeaderHeight, +                    QBrush(brushColor)) +                # header lines +                painter.setPen(linesColor) +                minOffset = painter.pen().width() +                # disable antialiasing to avoid lines bleeding over background +                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(glyph.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) + +                if not glyph.template: +                    font = glyph.getParent() +                    outline = glyph.getRepresentation("defconQt.QPainterPath") +                    uPM = font.info.unitsPerEm +                    if uPM is None or not uPM > 0: +                        uPM = 1000 +                    descender = font.info.descender +                    if descender is None or not descender < 0: +                        descender = -250 +                    factor = (self.squareSize-GlyphCellHeaderHeight) / (uPM*(1+2*GlyphCellBufferHeight)) +                    x_offset = (self.squareSize-glyph.width*factor)/2 +                    # If the glyph overflows horizontally we need to adjust the scaling factor +                    if x_offset < 0: +                        factor *= 1+2*x_offset/(glyph.width*factor) +                        x_offset = 0 +                    # TODO: the * 1.8 below is somewhat artificial +                    y_offset = 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(outline, Qt.black) +                    painter.restore() +                else: +                    painter.save() +                    painter.setFont(voidFont) +                    painter.setPen(QPen(Qt.lightGray)) +                    rect = QRectF(column * self.squareSize, row * self.squareSize+GlyphCellHeaderHeight, +                          self.squareSize, self.squareSize-GlyphCellHeaderHeight) +                    # TODO: need to flag template glyphs as to whether they have unicodings or not +                    if glyph.unicode is not None: +                        text = chr(glyph.unicode) +                    else: +                        text = "✌" +                    painter.drawText(rect, Qt.AlignCenter, text) +                    painter.restore() | 
