def __init__(self, parent=None): QGraphicsWidget.__init__(self, parent) self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) self.setContentsMargins(10, 10, 10, 10) layout = QGraphicsGridLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(10) self.setLayout(layout)
class GraphicsThumbnailGrid(QGraphicsWidget): class LayoutMode(enum.Enum): FixedColumnCount, AutoReflow = 0, 1 FixedColumnCount, AutoReflow = LayoutMode #: Signal emitted when the current (thumbnail) changes currentThumbnailChanged = Signal(object) def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self.__layoutMode = GraphicsThumbnailGrid.AutoReflow self.__columnCount = -1 self.__thumbnails = [] # type: List[GraphicsThumbnailWidget] #: The current 'focused' thumbnail item. This is the item that last #: received the keyboard focus (though it does not necessarily have #: it now) self.__current = None # type: Optional[GraphicsThumbnailWidget] self.__reflowPending = False self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) self.setContentsMargins(10, 10, 10, 10) # NOTE: Keeping a reference to the layout. self.layout() # returns a QGraphicsLayout wrapper (i.e. strips the # QGraphicsGridLayout-nes of the object). self.__layout = QGraphicsGridLayout() self.__layout.setContentsMargins(0, 0, 0, 0) self.__layout.setSpacing(10) self.setLayout(self.__layout) def resizeEvent(self, event): super().resizeEvent(event) if event.newSize().width() != event.oldSize().width() and \ self.__layoutMode == GraphicsThumbnailGrid.AutoReflow: self.__reflow() def setGeometry(self, rect): self.prepareGeometryChange() super().setGeometry(rect) def count(self): """ Returns ------- count: int Number of thumbnails in the widget """ return len(self.__thumbnails) def addThumbnail(self, thumbnail): """ Add/append a thumbnail to the widget Parameters ---------- thumbnail: Union[GraphicsThumbnailWidget, QPixmap] The thumbnail to insert """ self.insertThumbnail(self.count(), thumbnail) def insertThumbnail(self, index, thumbnail): """ Insert a new thumbnail into a widget. Raise a ValueError if thumbnail is already in the view. Parameters ---------- index : int Index where to insert thumbnail : Union[GraphicsThumbnailWidget, QPixmap] The thumbnail to insert. GraphicsThumbnailGrid takes ownership of the item. """ if isinstance(thumbnail, QPixmap): thumbnail = GraphicsThumbnailWidget(thumbnail, parentItem=self) elif thumbnail in self.__thumbnails: raise ValueError("{!r} is already inserted".format(thumbnail)) elif not isinstance(thumbnail, GraphicsThumbnailWidget): raise TypeError index = max(min(index, self.count()), 0) moved = self.__takeItemsFrom(index) assert moved == self.__thumbnails[index:] self.__thumbnails.insert(index, thumbnail) self.__appendItems([thumbnail] + moved) thumbnail.setParentItem(self) thumbnail.installEventFilter(self) assert self.count() == self.layout().count() self.__scheduleLayout() def removeThumbnail(self, thumbnail): """ Remove a single thumbnail from the grid. Raise a ValueError if thumbnail is not in the grid. Parameters ---------- thumbnail : GraphicsThumbnailWidget Thumbnail to remove. Items ownership is transferred to the caller. """ index = self.__thumbnails.index(thumbnail) moved = self.__takeItemsFrom(index) del self.__thumbnails[index] assert moved[0] is thumbnail and self.__thumbnails[index:] == moved[1:] self.__appendItems(moved[1:]) thumbnail.removeEventFilter(self) if thumbnail.parentItem() is self: thumbnail.setParentItem(None) if self.__current is thumbnail: self.__current = None self.currentThumbnailChanged.emit(None) assert self.count() == self.layout().count() def thumbnailAt(self, index): """ Return the thumbnail widget at `index` Parameters ---------- index : int Returns ------- thumbnail : GraphicsThumbnailWidget """ return self.__thumbnails[index] def clear(self): """ Remove all thumbnails from the grid. """ removed = self.__takeItemsFrom(0) assert removed == self.__thumbnails self.__thumbnails = [] for thumb in removed: thumb.removeEventFilter(self) if thumb.parentItem() is self: thumb.setParentItem(None) if self.__current is not None: self.__current = None self.currentThumbnailChanged.emit(None) def __takeItemsFrom(self, fromindex): # remove all items starting at fromindex from the layout and # return them # NOTE: Operate on layout only layout = self.__layout taken = [] for i in reversed(range(fromindex, layout.count())): item = layout.itemAt(i) layout.removeAt(i) taken.append(item) return list(reversed(taken)) def __appendItems(self, items): # Append/insert items into the layout at the end # NOTE: Operate on layout only layout = self.__layout columns = max(layout.columnCount(), 1) for i, item in enumerate(items, layout.count()): layout.addItem(item, i // columns, i % columns) def __scheduleLayout(self): if not self.__reflowPending: self.__reflowPending = True QApplication.postEvent(self, QEvent(QEvent.LayoutRequest), Qt.HighEventPriority) def event(self, event): if event.type() == QEvent.LayoutRequest: if self.__layoutMode == GraphicsThumbnailGrid.AutoReflow: self.__reflow() else: self.__gridlayout() if self.parentLayoutItem() is None: sh = self.effectiveSizeHint(Qt.PreferredSize) self.resize(sh) if self.layout(): self.layout().activate() return super().event(event) def setFixedColumnCount(self, count): if count < 0: if self.__layoutMode != GraphicsThumbnailGrid.AutoReflow: self.__layoutMode = GraphicsThumbnailGrid.AutoReflow self.__reflow() else: if self.__layoutMode != GraphicsThumbnailGrid.FixedColumnCount: self.__layoutMode = GraphicsThumbnailGrid.FixedColumnCount if self.__columnCount != count: self.__columnCount = count self.__gridlayout() def __reflow(self): self.__reflowPending = False layout = self.__layout width = self.contentsRect().width() hints = [item.effectiveSizeHint(Qt.PreferredSize) for item in self.__thumbnails] widths = [max(24, h.width()) for h in hints] ncol = self._fitncols(widths, layout.horizontalSpacing(), width) self.__relayoutGrid(ncol) def __gridlayout(self): assert self.__layoutMode == GraphicsThumbnailGrid.FixedColumnCount self.__relayoutGrid(self.__columnCount) def __relayoutGrid(self, columnCount): layout = self.__layout if columnCount == layout.columnCount(): return # remove all items from the layout, then re-add them back in # updated positions items = self.__takeItemsFrom(0) for i, item in enumerate(items): layout.addItem(item, i // columnCount, i % columnCount) def items(self): """ Return all thumbnail items. Returns ------- thumbnails : List[GraphicsThumbnailWidget] """ return list(self.__thumbnails) def currentItem(self): """ Return the current (last focused) thumbnail item. """ return self.__current def _fitncols(self, widths, spacing, constraint): def sliced(seq, ncol): return [seq[i:i + ncol] for i in range(0, len(seq), ncol)] def flow_width(widths, spacing, ncol): W = sliced(widths, ncol) col_widths = map(max, zip_longest(*W, fillvalue=0)) return sum(col_widths) + (ncol - 1) * spacing ncol_best = 1 for ncol in range(2, len(widths) + 1): w = flow_width(widths, spacing, ncol) if w <= constraint: ncol_best = ncol else: break return ncol_best def keyPressEvent(self, event): if event.key() in [Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, Qt.Key_Down]: self._moveCurrent(event.key(), event.modifiers()) event.accept() return super().keyPressEvent(event) def eventFilter(self, receiver, event): if isinstance(receiver, GraphicsThumbnailWidget) and \ event.type() == QEvent.FocusIn and \ receiver in self.__thumbnails: self.__current = receiver self.currentThumbnailChanged.emit(receiver) return super().eventFilter(receiver, event) def _moveCurrent(self, key, modifiers=Qt.NoModifier): """ Move the current thumbnail focus (`currentItem`) based on a key press (Qt.Key{Up,Down,Left,Right}) Parameters ---------- key : Qt.Key modifiers : Qt.Modifiers """ current = self.__current layout = self.__layout columns = layout.columnCount() rows = layout.rowCount() itempos = {} for i, j in itertools.product(range(rows), range(columns)): if i * columns + j >= layout.count(): break item = layout.itemAt(i, j) if item is not None: itempos[item] = (i, j) pos = itempos.get(current, None) if pos is None: return False i, j = pos index = i * columns + j if key == Qt.Key_Left: index = index - 1 elif key == Qt.Key_Right: index = index + 1 elif key == Qt.Key_Down: index = index + columns elif key == Qt.Key_Up: index = index - columns index = min(max(index, 0), layout.count() - 1) i = index // columns j = index % columns newcurrent = layout.itemAt(i, j) assert newcurrent is self.__thumbnails[index] if newcurrent is not None: if not modifiers & (Qt.ShiftModifier | Qt.ControlModifier): for item in self.__thumbnails: if item is not newcurrent: item.setSelected(False) # self.scene().clearSelection() newcurrent.setSelected(True) newcurrent.setFocus(Qt.TabFocusReason) newcurrent.ensureVisible() if self.__current is not newcurrent: self.__current = newcurrent self.currentThumbnailChanged.emit(newcurrent)