--- /dev/null
+#!/usr/bin/env python3
+
+#############################################################################
+##
+## Copyright (C) 2013 Riverbank Computing Limited.
+## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
+## All rights reserved.
+##
+## This file is part of the examples of PyQt.
+##
+## $QT_BEGIN_LICENSE:BSD$
+## You may use this file under the terms of the BSD license as follows:
+##
+## "Redistribution and use in source and binary forms, with or without
+## modification, are permitted provided that the following conditions are
+## met:
+## * Redistributions of source code must retain the above copyright
+## notice, this list of conditions and the following disclaimer.
+## * Redistributions in binary form must reproduce the above copyright
+## notice, this list of conditions and the following disclaimer in
+## the documentation and/or other materials provided with the
+## distribution.
+## * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
+## the names of its contributors may be used to endorse or promote
+## products derived from this software without specific prior written
+## permission.
+##
+## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+try:
+ from PyQt6.QtCore import *
+ from PyQt6.QtWidgets import *
+except:
+ try:
+ from PyQt5.QtCore import *
+ from PyQt5.QtWidgets import *
+ except:
+ from PyQt4.QtCore import *
+ from PyQt4.QtWidgets import *
+
+class FlowLayout(QLayout):
+
+ widthChanged = pyqtSignal(int)
+
+ def __init__(self, parent=None, margin=0, spacing=-1, orientation=Qt.Orientation.Horizontal):
+ super(FlowLayout, self).__init__(parent)
+
+ if parent is not None:
+ self.setContentsMargins(margin, margin, margin, margin)
+
+ self.setSpacing(spacing)
+ self.itemList = []
+ self.orientation = orientation
+
+ def __del__(self):
+ item = self.takeAt(0)
+ while item:
+ item = self.takeAt(0)
+
+ def addItem(self, item):
+ self.itemList.append(item)
+
+ def count(self):
+ return len(self.itemList)
+
+ def itemAt(self, index):
+ if index >= 0 and index < len(self.itemList):
+ return self.itemList[index]
+
+ return None
+
+ def takeAt(self, index):
+ if index >= 0 and index < len(self.itemList):
+ return self.itemList.pop(index)
+
+ return None
+
+ def expandingDirections(self):
+ return Qt.Orientations(Qt.Orientation(0))
+
+ def hasHeightForWidth(self):
+ return True
+
+ def heightForWidth(self, width):
+ if (self.orientation == Qt.Orientation.Horizontal):
+ return self.doLayoutHorizontal(QRect(0, 0, width, 0), True)
+ elif (self.orientation == Qt.Orientation.Vertical):
+ return self.doLayoutVertical(QRect(0, 0, width, 0), True)
+
+ def setGeometry(self, rect):
+ super(FlowLayout, self).setGeometry(rect)
+ if (self.orientation == Qt.Orientation.Horizontal):
+ self.doLayoutHorizontal(rect, False)
+ elif (self.orientation == Qt.Orientation.Vertical):
+ self.doLayoutVertical(rect, False)
+
+ def sizeHint(self):
+ return self.minimumSize()
+
+ def minimumSize(self):
+ size = QSize()
+
+ for item in self.itemList:
+ size = size.expandedTo(item.minimumSize())
+
+ margin, _, _, _ = self.getContentsMargins()
+
+ size += QSize(2 * margin, 2 * margin)
+ return size
+
+ def doLayoutHorizontal(self, rect, testOnly):
+ # Get initial coordinates of the drawing region (should be 0, 0)
+ x = rect.x()
+ y = rect.y()
+ lineHeight = 0
+ i = 0
+ for item in self.itemList:
+ wid = item.widget()
+ # Space X and Y is item spacing horizontally and vertically
+ spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.ControlType.PushButton, QSizePolicy.ControlType.PushButton, Qt.Orientation.Horizontal)
+ spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.ControlType.PushButton, QSizePolicy.ControlType.PushButton, Qt.Orientation.Vertical)
+ # Determine the coordinate we want to place the item at
+ # It should be placed at : initial coordinate of the rect + width of the item + spacing
+ nextX = x + item.sizeHint().width() + spaceX
+ # If the calculated nextX is greater than the outer bound...
+ if nextX - spaceX > rect.right() and lineHeight > 0:
+ x = rect.x() # Reset X coordinate to origin of drawing region
+ y = y + lineHeight + spaceY # Move Y coordinate to the next line
+ nextX = x + item.sizeHint().width() + spaceX # Recalculate nextX based on the new X coordinate
+ lineHeight = 0
+
+ if not testOnly:
+ item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
+
+ x = nextX # Store the next starting X coordinate for next item
+ lineHeight = max(lineHeight, item.sizeHint().height())
+ i = i + 1
+
+ return y + lineHeight - rect.y()
+
+ def doLayoutVertical(self, rect, testOnly):
+ # Get initial coordinates of the drawing region (should be 0, 0)
+ x = rect.x()
+ y = rect.y()
+ # Initalize column width and line height
+ columnWidth = 0
+ lineHeight = 0
+
+ # Space between items
+ spaceX = 0
+ spaceY = 0
+
+ # Variables that will represent the position of the widgets in a 2D Array
+ i = 0
+ j = 0
+ for item in self.itemList:
+ wid = item.widget()
+ # Space X and Y is item spacing horizontally and vertically
+ spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.ControlType.PushButton, QSizePolicy.ControlType.PushButton, Qt.Orientation.Horizontal)
+ spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.ControlType.PushButton, QSizePolicy.ControlType.PushButton, Qt.Orientation.Vertical)
+ # Determine the coordinate we want to place the item at
+ # It should be placed at : initial coordinate of the rect + width of the item + spacing
+ nextY = y + item.sizeHint().height() + spaceY
+ # If the calculated nextY is greater than the outer bound, move to the next column
+ if nextY - spaceY > rect.bottom() and columnWidth > 0:
+ y = rect.y() # Reset y coordinate to origin of drawing region
+ x = x + columnWidth + spaceX # Move X coordinate to the next column
+ nextY = y + item.sizeHint().height() + spaceY # Recalculate nextX based on the new X coordinate
+ # Reset the column width
+ columnWidth = 0
+
+ # Set indexes of the item for the 2D array
+ j += 1
+ i = 0
+
+ # Assign 2D array indexes
+ item.x_index = i
+ item.y_index = j
+
+ # Only call setGeometry (which place the actual widget using coordinates) if testOnly is false
+ # For some reason, Qt framework calls the doLayout methods with testOnly set to true (WTF ??)
+ if not testOnly:
+ item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
+
+ y = nextY # Store the next starting Y coordinate for next item
+ columnWidth = max(columnWidth, item.sizeHint().width()) # Update the width of the column
+ lineHeight = max(lineHeight, item.sizeHint().height()) # Update the height of the line
+
+ i += 1 # Increment i
+
+ # Only call setGeometry (which place the actual widget using coordinates) if testOnly is false
+ # For some reason, Qt framework calls the doLayout methods with testOnly set to true (WTF ??)
+ if not testOnly:
+ self.calculateMaxWidth(i)
+ #print("Total width : " + str(totalWidth))
+ self.widthChanged.emit(self.totalMaxWidth + spaceX * self.itemsOnWidestRow)
+ #self.widthChanged.emit(self.totalMaxWidth)
+ return lineHeight
+
+ # Method to calculate the maximum width among each "row" of the flow layout
+ # This will be useful to let the UI know the total width of the flow layout
+ def calculateMaxWidth(self, numberOfRows):
+ # Init variables
+ self.totalMaxWidth = 0
+ self.itemsOnWidestRow = 0
+
+ # For each "row", calculate the total width by adding the width of each item
+ # and then update the totalMaxWidth if the calculated width is greater than the current value
+ # Also update the number of items on the widest row
+ for i in range(numberOfRows):
+ rowWidth = 0
+ itemsOnWidestRow = 0
+ for item in self.itemList:
+ # Only compare items from the same row
+ if (item.x_index == i):
+ rowWidth += item.sizeHint().width()
+ itemsOnWidestRow += 1
+ if (rowWidth > self.totalMaxWidth):
+ self.totalMaxWidth = rowWidth
+ self.itemsOnWidestRow = itemsOnWidestRow
+
+class Window(QWidget):
+ def __init__(self):
+ super(Window, self).__init__()
+
+ flowLayout = FlowLayout(
+ # Vertical 1/2
+ #orientation=Qt.Vertical
+ )
+
+ flowLayout.addWidget(QPushButton("Short"))
+ flowLayout.addWidget(QPushButton("Longer"))
+ flowLayout.addWidget(QPushButton("Different text"))
+ flowLayout.addWidget(QPushButton("More text"))
+ flowLayout.addWidget(QPushButton("Even longer button text"))
+ self.setLayout(flowLayout)
+
+ # Vertical 2/2
+ if (flowLayout.orientation == Qt.Vertical):
+ flowLayout.widthChanged.connect(self.setMinimumWidth)
+
+ self.setWindowTitle("Flow Layout")
+
+if __name__ == '__main__':
+
+ import sys
+
+ app = QApplication(sys.argv)
+ mainWin = Window()
+ mainWin.show()
+ sys.exit(app.exec_())
+
--- /dev/null
+#!/usr/bin/env python3
+
+# TODO: add QScrollArea
+
+from sys import argv, exit
+from enum import IntFlag
+from pathlib import PurePath
+
+try:
+ from PyQt6.QtCore import *
+ from PyQt6.QtWidgets import *
+ from PyQt6.QtGui import QPixmap
+except:
+ try:
+ from PyQt5.QtCore import *
+ from PyQt5.QtWidgets import *
+ from PyQt5.QtGui import QPixmap
+ except:
+ from PyQt4.QtCore import *
+ from PyQt4.QtWidgets import *
+ from PyQt4.QtGui import QPixmap
+
+from gamiki import Game
+from gamiki.qt.flow_layout import FlowLayout
+
+pool = QThreadPool()
+def startGame(game):
+ class Worker(QRunnable):
+ def run(self): game.start()
+ worker = Worker()
+ pool.start(worker)
+
+def setBg(widget: QWidget, color: str):
+ if (";" in color):
+ raise RuntimeException("No colour can contain a `;'")
+
+ if (color):
+ bg = ("background-color: " + color + ";")
+ elif hasattr(widget, "bgColor"):
+ bg = ("background-color: " + widget.bgColor + ";")
+ else:
+ bg = ""
+
+ changed = False
+ attribs = widget.styleSheet().split(";")
+ for i, attr in enumerate(attribs):
+ if (attr.strip().startswith("background-color:")):
+ attribs[i] = bg if color else ""
+ changed = True
+ if (not changed) and (color):
+ attribs.append(bg)
+
+ widget.setStyleSheet(";".join(attribs) + ";")
+
+def addOverlay(self: QWidget, target: QWidget):
+ if (not hasattr(self, "overlay" )): self.overlay = []
+ if (not hasattr(self, "clicked" )): self.clicked = False
+ if (not hasattr(self, "hovered" )): self.hovered = False
+ if (not hasattr(self, "selected")): self.selected = False
+
+ overlay = QWidget()
+ overlay.setAttribute(Qt.WA_TransparentForMouseEvents)
+ overlay.setParent(target)
+ overlay.move(0, 0)
+ overlay.bgColor = "rgba(0, 0, 0, 0)"
+
+ self.overlay.append(overlay)
+
+def fixColor(widget: QWidget):
+ if (widget.clicked):
+ bg = "rgba(200, 200, 250, 60)"
+ elif (widget.hovered and widget.selected):
+ bg = "rgba(0, 0, 100, 60)"
+ elif (widget.hovered):
+ bg = "rgba(150, 150, 200, 60)"
+ elif (widget.selected):
+ bg = "rgba(0, 0, 100, 60)"
+ else:
+ bg = ""
+
+ for overlay in widget.overlay:
+ setBg(overlay, bg)
+
+class CoverMode(IntFlag):
+ LIST = 1,
+ COVER = 2,
+ FIXED = 4
+
+class Cover(QWidget):
+ onClick = pyqtSignal(Game)
+ clicked = False
+ selected = False
+ hovered = False
+
+ def __init__(self, game: Game, mode: CoverMode = None, size: int = 0):
+ super().__init__()
+
+ self.game = game
+ self.cover_mode = mode if (mode) else (CoverMode.LIST|CoverMode.FIXED)
+
+ if (size > 0):
+ self.cover_size = size
+ else:
+ self.cover_size = 48 if (CoverMode.LIST in self.cover_mode) else 200
+
+ self.initUI()
+
+ def _resetMinMax(self):
+ if (CoverMode.LIST in self.cover_mode):
+ self.line1.setMinimumWidth(self.cover_size)
+ else:
+ self.line1.setMaximumWidth(self.cover_size)
+
+ if (CoverMode.LIST in self.cover_mode):
+ self.line2.setMinimumWidth(self.cover_size)
+ else:
+ self.line2.setMaximumWidth(self.cover_size)
+
+ def _initUI_text(self):
+ self.line1 = QLabel(self.game.name)
+ self.line1.setStyleSheet("font-size: 20px; font-weight: bold;")
+
+ self.line2 = QLabel(self.game.tags[0] if (self.game.tags) else "")
+ self.line2.setStyleSheet("")
+ self.line2.setAlignment(Qt.AlignRight)
+
+ self._resetMinMax()
+
+ def initUI(self):
+ self._initUI_text()
+
+ pixmap = QPixmap(self.game["Icon"] if ("Icon" in self.game) else "")
+
+ self.cover = QLabel()
+ self.cover.setPixmap(pixmap)
+ self.cover.setScaledContents(True)
+ self.cover.setMinimumSize(self.cover_size, self.cover_size)
+ self.cover.setMaximumSize(self.cover_size, self.cover_size)
+
+ line1sub = QWidget()
+ hlay = QHBoxLayout()
+ hlay.setContentsMargins(0, 0, 0, 0)
+ hlay.addWidget(self.line1)
+ hlay.addStretch()
+ line1sub.setLayout(hlay)
+
+ sub = QWidget()
+ hlay = QVBoxLayout()
+ hlay.setContentsMargins(0, 0, 0, 0)
+ hlay.addWidget(line1sub)
+ hlay.addWidget(self.line2)
+ sub.setLayout(hlay)
+
+ if CoverMode.LIST in self.cover_mode:
+ layout = QHBoxLayout()
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addWidget(self.cover)
+ layout.addWidget(sub)
+ else:
+ layout = QVBoxLayout()
+ layout.addWidget(self.cover)
+ layout.addWidget(sub)
+ layout.addStretch()
+
+ addOverlay(self, self)
+ addOverlay(self, self.cover)
+
+ self.setLayout(layout)
+
+ def resizeEvent(self, event):
+ self.overlay[0].setFixedSize(event.size())
+
+ if (CoverMode.FIXED in self.cover_mode):
+ return super().resizeEvent(event)
+
+ m = self.contentsMargins()
+ diff_w = (m.left() + m.right())
+ diff_h = (m.top() + m.bottom())
+
+ s = event.size()
+ petit = min(s.width() - diff_w, s.height() - diff_h)
+ self.cover.setMaximumSize(petit, petit)
+ self.overlay[1].setFixedSize(self.cover.size())
+
+ self._resetMinMax()
+
+ super().resizeEvent(event)
+
+ def enterEvent(self, event):
+ self.hovered = True
+ fixColor(self)
+ super().enterEvent(event)
+
+ def leaveEvent(self, event):
+ self.hovered = False
+ fixColor(self)
+ super().leaveEvent(event)
+
+ def mousePressEvent(self, event):
+ if event.button() == Qt.LeftButton:
+ self.clicked = True
+ fixColor(self)
+
+ def mouseReleaseEvent(self, event):
+ if event.button() == Qt.LeftButton:
+ self.clicked = False
+ self.onClick.emit(self.game)
+ fixColor(self)
+
+class List(QWidget):
+ onClick = pyqtSignal(Game)
+
+ def __init__(self, games: list[Game], icon_size: int = 0):
+ super().__init__()
+
+ self.games = games
+ self.icon_size = icon_size
+
+ self.initUI()
+
+ def initUI(self):
+ layout = QVBoxLayout()
+ #layout.setSpacing(0)
+
+ for cover in self._gen_covers(CoverMode.LIST|CoverMode.FIXED):
+ layout.addWidget(cover)
+ layout.addStretch()
+
+ self.setLayout(layout)
+
+ def _gen_covers(self, mode: CoverMode) -> list[Cover]:
+ covers = []
+ for game in self.games:
+ cover = Cover(game, mode, self.icon_size)
+ cover.onClick.connect(lambda game: self.onClick.emit(game))
+ covers.append(cover)
+ return covers
+
+ def _on_click(self, game):
+ print(repr(game))
+
+class Grid(List):
+ def __init__(self, games: list[Game], icon_size: int = 0):
+ super().__init__(games, icon_size)
+
+ def initUI(self):
+ layout = FlowLayout()
+
+ for cover in self._gen_covers(CoverMode.COVER|CoverMode.FIXED):
+ layout.addWidget(cover)
+
+ self.setLayout(layout)
+
+if __name__ == '__main__':
+ app = QApplication(argv)
+
+ game1 = Game(None, PurePath(""))
+ game1["Name"] = "Super Buba"
+ game1["Icon"] = "./buba.jpg"
+ game1._init()
+ game2 = Game(None, PurePath(""))
+ game2["Name"] = "Super Buba II: le retour de la nouvelle bĂȘte"
+ game2["Icon"] = "./buba.jpg"
+ game2._init()
+ game3 = Game(None, PurePath(""))
+ game3["Name"] = "Fluffy"
+ game3["Icon"] = "./fluffy.jpg"
+ game3._init()
+
+ #game_widget1 = Cover(game1) ; game_widget1.show()
+ #game_widget2 = Cover(game3, CoverMode.COVER) ; game_widget2.show()
+ list1 = List([ game1, game2, game3 ]) ; list1.show()
+ grid = Grid([ game1, game2, game3 ]) ; grid.show()
+
+ list1.onClick.connect(lambda game: print("LIST:", game))
+ grid.onClick.connect(lambda game: print("GRID:", game))
+
+ exit(app.exec_())
+