diff --git a/flatcamGUI/ColumnarFlowLayout.py b/flatcamGUI/ColumnarFlowLayout.py new file mode 100644 index 00000000..753c070b --- /dev/null +++ b/flatcamGUI/ColumnarFlowLayout.py @@ -0,0 +1,174 @@ +import sys + +from PyQt5.QtCore import QPoint, QRect, QSize, Qt +from PyQt5.QtWidgets import QLayout, QSizePolicy +import math + +class ColumnarFlowLayout(QLayout): + def __init__(self, parent=None, margin=0, spacing=-1): + super().__init__(parent) + + if parent is not None: + self.setContentsMargins(margin, margin, margin, margin) + + self.setSpacing(spacing) + self.itemList = [] + + 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 0 <= index < len(self.itemList): + return self.itemList[index] + return None + + def takeAt(self, index): + if 0 <= 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): + height = self.doLayout(QRect(0, 0, width, 0), True) + return height + + def setGeometry(self, rect): + super().setGeometry(rect) + self.doLayout(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 doLayout(self, rect: QRect, testOnly: bool) -> int: + spacing = self.spacing() + x = rect.x() + y = rect.y() + + # Determine width of widest item + widest = 0 + for item in self.itemList: + widest = max(widest, item.sizeHint().width()) + + # Determine how many equal-width columns we can get, and how wide each one should be + column_count = math.floor(rect.width() / (widest + spacing)) + column_count = min(column_count, len(self.itemList)) + column_count = max(1, column_count) + column_width = math.floor((rect.width() - (column_count-1)*spacing - 1) / column_count) + + # Get the heights for all of our items + item_heights = {} + for item in self.itemList: + height = item.heightForWidth(column_width) if item.hasHeightForWidth() else item.sizeHint().height() + item_heights[item] = height + + # Prepare our column representation + column_contents = [] + column_heights = [] + for column_index in range(column_count): + column_contents.append([]) + column_heights.append(0) + + def add_to_column(column: int, item): + column_contents[column].append(item) + column_heights[column] += (item_heights[item] + spacing) + + def shove_one(from_column: int) -> bool: + if len(column_contents[from_column]) >= 1: + item = column_contents[from_column].pop(0) + column_heights[from_column] -= (item_heights[item] + spacing) + add_to_column(from_column-1, item) + return True + return False + + def shove_cascade_consider(from_column: int) -> bool: + changed = False + + if len(column_contents[from_column]) > 1: + item = column_contents[from_column][0] + item_height = item_heights[item] + if column_heights[from_column-1] + item_height < max(column_heights): + changed = shove_one(from_column) or changed + + if from_column+1 < column_count: + changed = shove_cascade_consider(from_column+1) or changed + + return changed + + def shove_cascade() -> bool: + if column_count < 2: + return False + changed = True + while changed: + changed = shove_cascade_consider(1) + return changed + + def pick_best_shoving_position() -> int: + best_pos = 1 + best_height = sys.maxsize + for column_index in range(1, column_count): + if len(column_contents[column_index]) == 0: + continue + item = column_contents[column_index][0] + height_after_shove = column_heights[column_index-1] + item_heights[item] + if height_after_shove < best_height: + best_height = height_after_shove + best_pos = column_index + return best_pos + + # Calculate the best layout + column_index = 0 + for item in self.itemList: + item_height = item_heights[item] + if column_heights[column_index] != 0 and (column_heights[column_index] + item_height) > max(column_heights): + column_index += 1 + if column_index >= column_count: + # Run out of room, need to shove more stuff in each column + if column_count >= 2: + changed = shove_cascade() + if not changed: + shoving_pos = pick_best_shoving_position() + shove_one(shoving_pos) + shove_cascade() + column_index = column_count-1 + + add_to_column(column_index, item) + + shove_cascade() + + # Set geometry according to the layout we have calculated + if not testOnly: + for column_index, items in enumerate(column_contents): + x = column_index * (column_width + spacing) + y = 0 + for item in items: + height = item_heights[item] + item.setGeometry(QRect(x, y, column_width, height)) + y += (height + spacing) + + # Return the overall height + return max(column_heights) + diff --git a/flatcamGUI/preferences/PreferencesSectionUI.py b/flatcamGUI/preferences/PreferencesSectionUI.py index fa55ec31..92168377 100644 --- a/flatcamGUI/preferences/PreferencesSectionUI.py +++ b/flatcamGUI/preferences/PreferencesSectionUI.py @@ -1,7 +1,7 @@ from typing import Dict +from PyQt5 import QtWidgets, QtCore -from PyQt5 import QtWidgets - +from flatcamGUI.ColumnarFlowLayout import ColumnarFlowLayout from flatcamGUI.preferences.OptionUI import OptionUI from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI @@ -10,8 +10,7 @@ class PreferencesSectionUI(QtWidgets.QWidget): def __init__(self, **kwargs): super().__init__(**kwargs) - - self.layout = QtWidgets.QHBoxLayout() + self.layout = ColumnarFlowLayout() #QtWidgets.QHBoxLayout() self.setLayout(self.layout) self.groups = self.build_groups() @@ -19,7 +18,6 @@ class PreferencesSectionUI(QtWidgets.QWidget): group.setMinimumWidth(250) self.layout.addWidget(group) - self.layout.addStretch() def build_groups(self) -> [OptionsGroupUI]: return [] @@ -34,6 +32,7 @@ class PreferencesSectionUI(QtWidgets.QWidget): def build_tab(self): scroll_area = QtWidgets.QScrollArea() scroll_area.setWidget(self) + scroll_area.setWidgetResizable(True) return scroll_area def get_tab_id(self) -> str: