ColumnarFlowLayout.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # File by: David Robertson (c) #
  4. # Date: 5/2020 #
  5. # License: MIT Licence #
  6. # ##########################################################
  7. import sys
  8. from PyQt5.QtCore import QPoint, QRect, QSize, Qt
  9. from PyQt5.QtWidgets import QLayout, QSizePolicy
  10. import math
  11. class ColumnarFlowLayout(QLayout):
  12. def __init__(self, parent=None, margin=0, spacing=-1):
  13. super().__init__(parent)
  14. if parent is not None:
  15. self.setContentsMargins(margin, margin, margin, margin)
  16. self.setSpacing(spacing)
  17. self.itemList = []
  18. def __del__(self):
  19. del_item = self.takeAt(0)
  20. while del_item:
  21. del_item = self.takeAt(0)
  22. def addItem(self, item):
  23. self.itemList.append(item)
  24. def count(self):
  25. return len(self.itemList)
  26. def itemAt(self, index):
  27. if 0 <= index < len(self.itemList):
  28. return self.itemList[index]
  29. return None
  30. def takeAt(self, index):
  31. if 0 <= index < len(self.itemList):
  32. return self.itemList.pop(index)
  33. return None
  34. def expandingDirections(self):
  35. return Qt.Orientations(Qt.Orientation(0))
  36. def hasHeightForWidth(self):
  37. return True
  38. def heightForWidth(self, width):
  39. height = self.doLayout(QRect(0, 0, width, 0), True)
  40. return height
  41. def setGeometry(self, rect):
  42. super().setGeometry(rect)
  43. self.doLayout(rect, False)
  44. def sizeHint(self):
  45. return self.minimumSize()
  46. def minimumSize(self):
  47. size = QSize()
  48. for item in self.itemList:
  49. size = size.expandedTo(item.minimumSize())
  50. margin, _, _, _ = self.getContentsMargins()
  51. size += QSize(2 * margin, 2 * margin)
  52. return size
  53. def doLayout(self, rect: QRect, testOnly: bool) -> int:
  54. spacing = self.spacing()
  55. x = rect.x()
  56. y = rect.y()
  57. # Determine width of widest item
  58. widest = 0
  59. for item in self.itemList:
  60. widest = max(widest, item.sizeHint().width())
  61. # Determine how many equal-width columns we can get, and how wide each one should be
  62. column_count = math.floor(rect.width() / (widest + spacing))
  63. column_count = min(column_count, len(self.itemList))
  64. column_count = max(1, column_count)
  65. column_width = math.floor((rect.width() - (column_count-1)*spacing - 1) / column_count)
  66. # Get the heights for all of our items
  67. item_heights = {}
  68. for item in self.itemList:
  69. height = item.heightForWidth(column_width) if item.hasHeightForWidth() else item.sizeHint().height()
  70. item_heights[item] = height
  71. # Prepare our column representation
  72. column_contents = []
  73. column_heights = []
  74. for column_index in range(column_count):
  75. column_contents.append([])
  76. column_heights.append(0)
  77. def add_to_column(column: int, item):
  78. column_contents[column].append(item)
  79. column_heights[column] += (item_heights[item] + spacing)
  80. def shove_one(from_column: int) -> bool:
  81. if len(column_contents[from_column]) >= 1:
  82. item = column_contents[from_column].pop(0)
  83. column_heights[from_column] -= (item_heights[item] + spacing)
  84. add_to_column(from_column-1, item)
  85. return True
  86. return False
  87. def shove_cascade_consider(from_column: int) -> bool:
  88. changed_item = False
  89. if len(column_contents[from_column]) > 1:
  90. item = column_contents[from_column][0]
  91. item_height = item_heights[item]
  92. if column_heights[from_column-1] + item_height < max(column_heights):
  93. changed_item = shove_one(from_column) or changed_item
  94. if from_column+1 < column_count:
  95. changed_item = shove_cascade_consider(from_column+1) or changed_item
  96. return changed_item
  97. def shove_cascade() -> bool:
  98. if column_count < 2:
  99. return False
  100. changed_item = True
  101. while changed_item:
  102. changed_item = shove_cascade_consider(1)
  103. return changed_item
  104. def pick_best_shoving_position() -> int:
  105. best_pos = 1
  106. best_height = sys.maxsize
  107. for column_idx in range(1, column_count):
  108. if len(column_contents[column_idx]) == 0:
  109. continue
  110. item = column_contents[column_idx][0]
  111. height_after_shove = column_heights[column_idx-1] + item_heights[item]
  112. if height_after_shove < best_height:
  113. best_height = height_after_shove
  114. best_pos = column_idx
  115. return best_pos
  116. # Calculate the best layout
  117. column_index = 0
  118. for item in self.itemList:
  119. item_height = item_heights[item]
  120. if column_heights[column_index] != 0 and (column_heights[column_index] + item_height) > max(column_heights):
  121. column_index += 1
  122. if column_index >= column_count:
  123. # Run out of room, need to shove more stuff in each column
  124. if column_count >= 2:
  125. changed = shove_cascade()
  126. if not changed:
  127. shoving_pos = pick_best_shoving_position()
  128. shove_one(shoving_pos)
  129. shove_cascade()
  130. column_index = column_count-1
  131. add_to_column(column_index, item)
  132. shove_cascade()
  133. # Set geometry according to the layout we have calculated
  134. if not testOnly:
  135. for column_index, items in enumerate(column_contents):
  136. x = column_index * (column_width + spacing)
  137. y = 0
  138. for item in items:
  139. height = item_heights[item]
  140. item.setGeometry(QRect(x, y, column_width, height))
  141. y += (height + spacing)
  142. # Return the overall height
  143. return max(column_heights)