from PyQt5.QtCore import QMimeData, QRectF, QSize, Qt from PyQt5.QtGui import (QBrush, QColor, QCursor, 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.setAttribute(Qt.WA_KeyCompression) self._glyphs = [] # TODO: hide behind a façade self.squareSize = 56 self._columns = 10 self._selection = set() self._oldSelection = None self._lastSelectedCell = None self.characterSelectedCallback = None self.doubleClickCallback = None self.updateCurrentGlyph = False self._maybeDragPosition = None self.setFocusPolicy(Qt.ClickFocus) self._currentDropIndex = None self._scrollArea = QScrollArea(parent) self._scrollArea.dragEnterEvent = self.pipeDragEnterEvent self._scrollArea.dragMoveEvent = self.pipeDragMoveEvent self._scrollArea.dragLeaveEvent = self.pipeDragLeaveEvent self._scrollArea.dropEvent = self.pipeDropEvent self._scrollArea.setAcceptDrops(True) 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, glyphs): self._glyphs = glyphs 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, selection): self._selection = selection 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 getSelectedGlyphs(self): return [self._glyphs[key] for key in sorted(self._selection)] def _get_lastSelectedCell(self): return self._lastSelectedCell def _set_lastSelectedCell(self, index): self._lastSelectedCell = index if self.updateCurrentGlyph: glyph = self.lastSelectedGlyph() app = QApplication.instance() app.setCurrentGlyph(glyph) if index is not None: self.scrollToCell(index) lastSelectedCell = property(_get_lastSelectedCell, _set_lastSelectedCell, doc="The current lastSelectedCell in selection.") def lastSelectedGlyph(self): index = self._lastSelectedCell return self._glyphs[index] if index is not None else None def scrollArea(self): return self._scrollArea def scrollToCell(self, index): x = (.5 + index % self._columns) * self.squareSize y = (.5 + index // self._columns) * self.squareSize self._scrollArea.ensureVisible(x, y, .5*self.squareSize, .5*self.squareSize) def _get_currentDropIndex(self): return self._currentDropIndex def _set_currentDropIndex(self, index): self._currentDropIndex = index self.update() currentDropIndex = property(_get_currentDropIndex, _set_currentDropIndex) def pipeDragEnterEvent(self, event): # glyph reordering if event.source() == self: event.acceptProposedAction() def pipeDragMoveEvent(self, event): if event.source() == self: pos = event.posF() self.currentDropIndex = int(self._columns * (pos.y() // self.squareSize) \ + (pos.x() + .5*self.squareSize) // self.squareSize) def pipeDragLeaveEvent(self, event): self.currentDropIndex = None def pipeDropEvent(self, event): # TODO: consider dropping this check, maybe only subclasses should do it # so as to dispatch but here we presumably don't need it if event.source() == self: insert = self.currentDropIndex newGlyphNames = event.mimeData().text().split(" ") font = self._glyphs[0].getParent() # TODO: should glyphOrder change activate font.dirty? newGlyphs = [font[name] for name in newGlyphNames] # put all glyphs to be moved to None (deleting them would # invalidate our insert indexes) for index, glyph in enumerate(self._glyphs): if glyph in newGlyphs: self._glyphs[index] = None # insert newGlyphs into the list lst = self._glyphs[:insert] lst.extend(newGlyphs+self._glyphs[insert:]) self._glyphs = lst # now, elide None self.currentDropIndex = None self.glyphs = [glyph for glyph in self._glyphs if glyph != None] # 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 cnt = len(self.selection) if cnt == 1: elem = next(iter(self.selection)) self.characterSelectedCallback(self._glyphs[elem].name) else: self.characterSelectedCallback(cnt) def _arrowKeyPressEvent(self, event): count = event.count() key = event.key() modifiers = event.modifiers() # TODO: it might be the case that self._lastSelectedCell cannot be None # when we arrive here whatsoever if self._lastSelectedCell is not None: if key == Qt.Key_Up: delta = -self._columns elif key == Qt.Key_Down: delta = self._columns elif key == Qt.Key_Left: delta = -1 elif key == Qt.Key_Right: delta = 1 newSel = self._lastSelectedCell + delta*count if newSel < 0 or newSel >= len(self._glyphs): return if modifiers & Qt.ShiftModifier: sel = self._linearSelection(newSel) if sel is not None: self.selection |= sel else: self.selection = {newSel} self.lastSelectedCell = newSel def keyPressEvent(self, event): key = event.key() modifiers = event.modifiers() if key == Qt.Key_Up or key == Qt.Key_Down or key == Qt.Key_Left \ or key == Qt.Key_Right: self._arrowKeyPressEvent(event) elif 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 proceedWithDeletion(self) 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 modifiers & Qt.ShiftModifier: del self.font[gName] # XXX: need a del fn in property del self._glyphs[key] else: # XXX: have template setter clear glyph content glyph.template = True self.selection = set() else: super(GlyphCollectionWidget, self).keyPressEvent(event) return event.accept() def _findEventIndex(self, event): index = (event.y() // self.squareSize) * self._columns + event.x() // self.squareSize if index >= len(self._glyphs): return None return index def _linearSelection(self, index): if index in self._selection: newSelection = None if not self._selection: newSelection = {index} else: if index < self._lastSelectedCell: newSelection = self._selection | set(range(index, self._lastSelectedCell + 1)) else: newSelection = self._selection | set(range(self._lastSelectedCell, index + 1)) return newSelection # TODO: in mousePressEvent and mouseMoveEvent below, self._lastSelectedCell # must be updated at all exit point def mousePressEvent(self, event): if event.button() == Qt.LeftButton: self._oldSelection = self._selection index = self._findEventIndex(event) modifiers = event.modifiers() event.accept() if index is None: if not (modifiers & Qt.ControlModifier or modifiers & Qt.ShiftModifier): self.selection = set() self._lastSelectedCell = index return if modifiers & Qt.ControlModifier: if index in self._selection: selection = self.selection selection.remove(index) self.selection = selection else: selection = self.selection selection.add(index) self.selection = selection elif modifiers & Qt.ShiftModifier: newSelection = self._linearSelection(index) if newSelection is not None: self.selection = newSelection elif not index in self._selection: self.selection = {index} else: self._maybeDragPosition = event.pos() self.lastSelectedCell = index else: super(GlyphCollectionWidget, self).mousePressEvent(event) def mouseMoveEvent(self, event): if event.buttons() & Qt.LeftButton: index = self._findEventIndex(event) if self._maybeDragPosition is not None: if ((event.pos() - self._maybeDragPosition).manhattanLength() \ < QApplication.startDragDistance()): return # TODO: needs ordering or not? glyphList = " ".join(glyph.name for glyph in self.getSelectedGlyphs()) drag = QDrag(self) mimeData = QMimeData() mimeData.setText(glyphList) drag.setMimeData(mimeData) dropAction = drag.exec_() self._maybeDragPosition = None event.accept() return if index == self._lastSelectedCell: return modifiers = event.modifiers() event.accept() if index is None: if not (modifiers & Qt.ControlModifier or modifiers & Qt.ShiftModifier): self.selection = set() self._lastSelectedCell = index return if modifiers & Qt.ControlModifier: if index in self._selection and index in self._oldSelection: selection = self.selection selection.remove(index) self.selection = selection elif index not in self._selection and index not in self._oldSelection: selection = self.selection selection.add(index) self.selection = selection elif modifiers & Qt.ShiftModifier: newSelection = self._linearSelection(index) if newSelection is not None: self.selection = newSelection else: self.selection = {index} self.lastSelectedCell = index else: super(GlyphCollectionWidget, self).mouseMoveEvent(event) def mouseReleaseEvent(self, event): if event.button() == Qt.LeftButton: event.accept() self._maybeDragPosition = None self._oldSelection = None else: super(GlyphCollectionWidget, self).mouseReleaseEvent(event) def mouseDoubleClickEvent(self, event): if event.button() == Qt.LeftButton: event.accept() index = self._findEventIndex(event) if index is not None and self.doubleClickCallback is not None: self.doubleClickCallback(self._glyphs[index]) 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 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): 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: if glyph.markColor is not None: markColor = QColor.fromRgbF(*tuple(glyph.markColor)) 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) if self._currentDropIndex is not None: painter.setPen(Qt.green) if self._currentDropIndex == key: painter.drawLine(column * self.squareSize, row * self.squareSize, column * self.squareSize, bottomEdgeY) # special-case the end-column elif column == endColumn and self._currentDropIndex == key+1: yPos = self.mapFromGlobal(QCursor.pos()).y() if row == yPos // self.squareSize: painter.drawLine(rightEdgeX - 1, row * self.squareSize, rightEdgeX - 1, bottomEdgeY) # selection code if key in self._selection: painter.setRenderHint(QPainter.Antialiasing, False) 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()