probonopd / Filer

A clean rewrite of the Filer file manager for helloSystem, inspired by John Siracusa's descriptions of "Spatial Orientation"
BSD 2-Clause "Simplified" License
13 stars 1 forks source link

Staggered icon positioning to allow for longer file names #22

Open probonopd opened 4 months ago

probonopd commented 4 months ago

In icon view, when aligning to the grid, we (optionally) want every second line of items to be moved to the right by 50% of the grid.

Classic Finder used to have this:

image

image

Source: https://vintageapple.org/macbooks/pdf/The_System_7_Book_1991.pdf

This allows for much longer file names compared to the items being aligned in a normal grid, while we could reduce the vertical spacing a lot.

We can do:

image

#!/usr/bin/env python3

from PyQt6.QtWidgets import QApplication, QListView, QStyledItemDelegate, QStyleOptionViewItem
from PyQt6.QtCore import Qt, QRect, QSize
from PyQt6.QtGui import QStandardItemModel, QStandardItem, QIcon

class CustomDelegate(QStyledItemDelegate):
    def __init__(self, parent=None):
        super().__init__(parent)

    def sizeHint(self, option, index):
        # Adjust the size hint to include extra width for staggering
        size = QSize(100, 55)  # Base size of the item
        if index.row() % 2 == 1:
            size.setWidth(size.width() + 2 * size.width())  # Increase width for staggering

        return size

def main():
    app = QApplication([])

    view = QListView()
    view.setViewMode(QListView.ViewMode.IconMode)  # Updated for PyQt6
    view.setResizeMode(QListView.ResizeMode.Adjust)  # Updated for PyQt6
    view.setSpacing(20)  # Set spacing between items
    view.setStyleSheet("QListView::item:selected { background-color: #87CEEB; }")
    view.setSelectionMode(QListView.SelectionMode.MultiSelection)

    # Create the model
    model = QStandardItemModel(view)
    for i in range(20):  # Add some items to the model
        item = QStandardItem(f"Item {i} with a long name to test text wrapping")
        item.setIcon(QIcon.fromTheme("applications-internet"))
        item.setSizeHint(QSize(100, 100))  # Ensure the items have a consistent size
        model.appendRow(item)
    view.setModel(model)

    # Set the custom delegate
    delegate = CustomDelegate(view)
    view.setItemDelegate(delegate)

    view.show()
    app.exec()

if __name__ == '__main__':
    main()

However, the code above achieves this by varying the width of the items rather than their position, which is not what we want:

image

How can we achieve this properly? Also see

probonopd commented 4 months ago

Is it even possible using QListView?

Would it be possible by subclassing QListView? How?

Or would we have to go to a solution without using QListView altogether?

#!/usr/bin/env python
import sys
from PyQt6.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QGraphicsTextItem
from PyQt6.QtGui import QPixmap, QColor, QFont, QPainter, QIcon, QDragEnterEvent, QDrag
from PyQt6.QtCore import Qt, QPointF, QByteArray, QDataStream, QMimeData, QIODevice

class DraggableGraphicsPixmapItem(QGraphicsPixmapItem):
    def __init__(self, pixmap):
        super().__init__(pixmap)
        self.setFlag(QGraphicsPixmapItem.GraphicsItemFlag.ItemIsMovable, True)
        self.setAcceptHoverEvents(True)
        self.setCursor(Qt.CursorShape.OpenHandCursor)

    def mouseMoveEvent(self, event):
        if event.buttons() == Qt.MouseButton.LeftButton:
            mime_data = QMimeData()
            data_stream = QByteArray()
            stream = QDataStream(data_stream, QIODevice.OpenModeFlag.WriteOnly)
            stream << QPointF(event.pos())
            mime_data.setData("application/x-dnditemdata", data_stream)

            drag = QDrag(self.scene().views()[0])
            drag.setMimeData(mime_data)
            drag.setPixmap(self.pixmap())
            drag.setHotSpot(event.pos().toPoint() - self.pos().toPoint())
            drag.exec(Qt.DropAction.MoveAction)

class DraggableGraphicsTextItem(QGraphicsTextItem):
    def __init__(self, text):
        super().__init__(text)
        self.setTextInteractionFlags(Qt.TextInteractionFlag.TextEditorInteraction)

    def mouseMoveEvent(self, event):
        if event.buttons() == Qt.MouseButton.LeftButton:
            mime_data = QMimeData()
            data_stream = QByteArray()
            stream = QDataStream(data_stream, QIODevice.OpenModeFlag.WriteOnly)
            stream << QPointF(event.pos())
            mime_data.setData("application/x-dnditemdata", data_stream)

            drag = QDrag(self.scene().views()[0])
            drag.setMimeData(mime_data)
            drag.setPixmap(QPixmap())
            drag.setHotSpot(event.pos().toPoint() - self.pos().toPoint())
            drag.exec(Qt.DropAction.MoveAction)

class IconView(QGraphicsView):
    def __init__(self):
        super().__init__()

        # Create a QGraphicsScene
        self.scene = QGraphicsScene()
        self.setScene(self.scene)

        # Example data: icon paths and text
        icon = QIcon.fromTheme("applications-internet") 

        data = [{"icon": icon.pixmap(48, 48), "text": f"Item {i}"} for i in range(1, 21)]

        # Add items to the scene
        x = 0
        y = 0
        item_width = 100
        item_height = 50  # Adjust item height as needed

        for entry in data:
            # Add pixmap (icon)
            pixmap = QPixmap(entry["icon"])
            pixmap_item = DraggableGraphicsPixmapItem(pixmap)
            # Center pixmap horizontally
            pixmap_item.setPos(x + (item_width - pixmap.width()) / 2, y)
            self.scene.addItem(pixmap_item)

            # Add text item below the icon
            text_item = DraggableGraphicsTextItem(entry["text"])
            text_item.setDefaultTextColor(QColor("black"))
            text_item.setFont(QFont("Arial", 10))

            # Center text horizontally under the icon
            text_width = text_item.boundingRect().width()
            text_height = text_item.boundingRect().height()
            text_item.setPos(x + (item_width - text_width) / 2, y + item_height)

            self.scene.addItem(text_item)

            # Update x and y positions for the next item
            x += item_width + 20  # Horizontal spacing between items

            # Check if next item exceeds viewport width
            if x + item_width > self.viewport().width():
                # Check x to see if we are in an even or odd row
                if y == item_height / 2: 
                    x = 0
                else:
                    x = item_width / 2  # Reset x to start a new line
                y += item_height * 0.5  # Vertical spacing between items

        # Set scene rect to ensure all items are visible
        total_height = y + item_height + text_height
        self.scene.setSceneRect(0, 0, self.viewport().width(), total_height)

        # Adjust view settings
        self.setRenderHint(QPainter.RenderHint.Antialiasing)
        self.setWindowTitle("Custom Icon View")
        self.resize(600, 400)

        # Viewport gets white background
        self.viewport().setStyleSheet("background-color: white;")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    view = IconView()
    view.show()
    sys.exit(app.exec())

Going down this route would be very tedious since we need to implement everything ourselves...