Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 179 additions & 4 deletions cc3d/player5/Plugins/ViewManagerPlugins/MainArea.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from cc3d.player5 import Graphics
from .WindowInventory import WindowInventory
import sys
from math import ceil, sqrt
from weakref import ref
from gc import collect

Expand Down Expand Up @@ -281,19 +282,193 @@ def addSteeringSubWindow(self, widget):

def tileSubWindows(self):
"""
dummy function to make conform to QMdiArea API
Tiles floating Player windows across the available screen area.

:return: None
"""
pass
windows = self.__arrangeable_windows()
if not windows:
return

available_rect = self.__available_arrangement_rect(reserve_main_window=True)
margin = 12
spacing = 8
window_count = len(windows)
column_count = int(ceil(sqrt(window_count)))
row_count = int(ceil(float(window_count) / column_count))
tile_width = max(240, int((available_rect.width() - 2 * margin - spacing * (column_count - 1)) / column_count))
tile_height = max(200, int((available_rect.height() - 2 * margin - spacing * (row_count - 1)) / row_count))

for idx, win in enumerate(windows):
row = int(idx / column_count)
column = idx % column_count
x = available_rect.x() + margin + column * (tile_width + spacing)
y = available_rect.y() + margin + row * (tile_height + spacing)
win.showNormal()
win.setGeometry(x, y, tile_width, tile_height)
win.raise_()

self.setActiveSubWindow(windows[-1])

def cascadeSubWindows(self):
"""
dummy function to make conform to QMdiArea API
Cascades floating Player windows across the available screen area.

:return: None
"""
pass
windows = self.__arrangeable_windows()
if not windows:
return

available_rect = self.__available_arrangement_rect(reserve_main_window=True)
margin = 24
offset = 32
cascade_width = min(900, max(360, int(available_rect.width() * 0.68)))
cascade_height = min(700, max(300, int(available_rect.height() * 0.68)))
max_x = available_rect.right() - cascade_width - margin
max_y = available_rect.bottom() - cascade_height - margin
x = available_rect.x() + margin
y = available_rect.y() + margin

for win in windows:
if x > max_x or y > max_y:
x = available_rect.x() + margin
y = available_rect.y() + margin
win.showNormal()
win.setGeometry(x, y, cascade_width, cascade_height)
win.raise_()
x += offset
y += offset

self.setActiveSubWindow(windows[-1])

def move_windows_to_screen(self, screen_index):
"""
Moves the main Player window and floating subwindows to another screen without resizing them.

:param screen_index: target screen index in QApplication.screens()
:type screen_index: int
:return: None
"""
screens = QApplication.screens()
if screen_index < 0 or screen_index >= len(screens):
return

windows = [self.UI] + self.__arrangeable_windows()
if not windows:
return

target_rect = screens[screen_index].availableGeometry()
margin = 12
source_rect = QRect(windows[0].frameGeometry())
for win in windows[1:]:
source_rect = source_rect.united(win.frameGeometry())

offset = target_rect.topLeft() + QPoint(margin, margin) - source_rect.topLeft()
for win in windows:
target_pos = win.pos() + offset
target_pos = self.__constrained_window_position(win, target_pos, target_rect, margin)
win.move(target_pos)
win.raise_()

def __constrained_window_position(self, win, position, available_rect, margin):
"""
Keeps a moved window's top-left position within the target screen.

:param win: window being moved
:param position: requested top-left position
:param available_rect: target screen available geometry
:param margin: screen-edge margin
:return: QPoint
"""
width = win.frameGeometry().width()
height = win.frameGeometry().height()
min_x = available_rect.left() + margin
min_y = available_rect.top() + margin
max_x = max(min_x, available_rect.right() - width - margin + 1)
max_y = max(min_y, available_rect.bottom() - height - margin + 1)
x = max(min_x, min(position.x(), max_x))
y = max(min_y, min(position.y(), max_y))

return QPoint(x, y)

def __arrangeable_windows(self):
"""
Returns visible floating subwindows that should participate in layout operations.

:return: list of SubWindow objects
"""
windows = []
for win in self.subWindowList():
widget = win.widget()
if widget is not None and getattr(widget, 'is_screenshot_widget', False):
continue
if not win.isVisible():
continue
windows.append(win)

return windows

def __available_arrangement_rect(self, reserve_main_window=False):
"""
Returns the available geometry of the screen that owns the active Player window.

:param reserve_main_window: optional flag that reserves space for the main Player window
:type reserve_main_window: bool
:return: QRect
"""
reference_point = self.UI.frameGeometry().center()
if self.lastActiveRealWindow is not None:
reference_point = self.lastActiveRealWindow.frameGeometry().center()

screen = QApplication.screenAt(reference_point)
if screen is None:
screen = QApplication.primaryScreen()
if screen is not None:
available_rect = screen.availableGeometry()
else:
available_rect = QApplication.desktop().availableGeometry()

if reserve_main_window:
return self.__arrangement_rect_excluding_main_window(available_rect)

return available_rect

def __arrangement_rect_excluding_main_window(self, available_rect):
"""
Moves the main Player window to the top-left and returns the remaining area for floating windows.

:param available_rect: available screen geometry
:type available_rect: QRect
:return: QRect
"""
margin = 12
spacing = 8
main_window = self.UI
main_window.move(available_rect.topLeft() + QPoint(margin, margin))
main_window.raise_()

remaining_x = main_window.frameGeometry().right() + spacing
remaining_width = available_rect.right() - remaining_x - margin + 1
if remaining_width >= 320:
return QRect(
remaining_x,
available_rect.y(),
remaining_width,
available_rect.height()
)

remaining_y = main_window.frameGeometry().bottom() + spacing
remaining_height = available_rect.bottom() - remaining_y - margin + 1
if remaining_height >= 240:
return QRect(
available_rect.x(),
remaining_y,
available_rect.width(),
remaining_height
)

return available_rect

def activeSubWindow(self):
"""
Expand Down
30 changes: 28 additions & 2 deletions cc3d/player5/Plugins/ViewManagerPlugins/SimpleTabView.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,9 +350,11 @@ def update_window_menu(self) -> None:
window_menu.clear()
window_menu.addAction(self.new_graphics_window_act)

window_menu.addAction(self.tile_act)
window_menu.addAction(self.cascade_act)
if not self.MDI_ON:
self.__add_move_windows_to_screen_menu(window_menu)
if self.MDI_ON:
window_menu.addAction(self.tile_act)
window_menu.addAction(self.cascade_act)
window_menu.addAction(self.minimize_all_graphics_windows_act)
window_menu.addAction(self.restore_all_graphics_windows_act)
window_menu.addSeparator()
Expand Down Expand Up @@ -397,6 +399,30 @@ def update_window_menu(self) -> None:
self.windowMapper.setMapping(action, win)
counter += 1

def __add_move_windows_to_screen_menu(self, window_menu):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feature is neat. I think there is value in keeping GUIs simple and straightforward when it comes to future enhancements. Up to you!

"""
Adds monitor-placement actions to the Window menu for floating-window layout.

:param window_menu: Window menu
:type window_menu: QMenu
:return: None
"""
screens = QApplication.screens()
if len(screens) < 2:
return

move_menu = window_menu.addMenu("Move All Windows To Screen")
for idx, screen in enumerate(screens):
geometry = screen.availableGeometry()
action_text = "{0}. {1} ({2}x{3})".format(
idx + 1,
screen.name() or "Screen",
geometry.width(),
geometry.height()
)
action = move_menu.addAction(action_text)
action.triggered.connect(lambda checked=False, screen_idx=idx: self.move_windows_to_screen(screen_idx))

def handle_vis_field_created(self, field_name: str, field_type: int, precision_type: str) -> None:
"""
slot that handles new visualization field creation. This mechanism is necessary to handle fields
Expand Down
21 changes: 20 additions & 1 deletion cc3d/player5/UI/ModelEditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,29 @@ def __init__(self, parent):
QTreeView.__init__(self, parent)
self.setFrameStyle(QFrame.NoFrame)
self.parent = parent
self.__setup_actions()

def getParent(self):
return self.parent

def __setup_actions(self):
self.expand_all_action = QAction("Expand All", self)
self.expand_all_action.setToolTip("Expand all model parameters")
self.expand_all_action.triggered.connect(self.expandAll)

self.collapse_all_action = QAction("Collapse All", self)
self.collapse_all_action.setToolTip("Collapse all model parameters")
self.collapse_all_action.triggered.connect(self.collapseAll)

self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.__show_context_menu)

def __show_context_menu(self, position):
menu = QMenu(self)
menu.addAction(self.expand_all_action)
menu.addAction(self.collapse_all_action)
menu.exec_(self.viewport().mapToGlobal(position))

def setParams(self):
# Column widths should be set after setting the model!
# Fixme: Before setting the column sizes, make sure that
Expand Down Expand Up @@ -42,4 +61,4 @@ def setParams(self):
self.setColumnWidth(0, 180)
selectionModel = QItemSelectionModel(model)
self.setSelectionModel(selectionModel)
"""
"""
46 changes: 43 additions & 3 deletions cc3d/player5/Utilities/SimModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ def __init__(self, domDoc, parent=None):
self.__isDirty = False
self.__dirtyModules = {}
self.__headers = ["Property", "Value"]
self.__moduleStyleColors = {
"Plugin": QColor("#2457A6"),
"Steppable": QColor("#0F766E"),
"Potts": QColor("#7C3AED"),
"Metadata": QColor("#B45309")
}
self.__editableValueColor = QColor("#166534")
self.__editableValueBackgroundColor = QColor("#ECFDF3")
self.__attributeColor = QColor("#64748B")

def setPrintFlag(self, _flag):
self.__printFlag = _flag
Expand Down Expand Up @@ -55,10 +64,43 @@ def treeItemFromIndex(self, _itemIndex):
return self.__rootItem

def data(self, index, role=Qt.DisplayRole): # interface: done
if role != Qt.DisplayRole or not index.isValid():
if not index.isValid():
return QVariant()

node = index.internalPointer()
module_color = self.__moduleStyleColors.get(node.name())
is_editable_value = index.column() == VALUE and node.type() is not None
is_attribute = node.elementType() == "attribute"

if role == Qt.FontRole:
font = QFont()
if module_color is not None:
font.setBold(True)
return QVariant(font)
if is_editable_value:
if is_attribute:
font.setItalic(True)
else:
font.setBold(True)
Comment on lines +81 to +84

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great idea.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it came out nice. Few small fixes make the steering panel and window management much better

return QVariant(font)
if is_attribute:
font.setItalic(True)
return QVariant(font)

if role == Qt.ForegroundRole:
if module_color is not None:
return QVariant(QBrush(module_color))
if is_editable_value:
return QVariant(QBrush(self.__editableValueColor))
if is_attribute:
return QVariant(QBrush(self.__attributeColor))

if role == Qt.BackgroundRole and is_editable_value:
return QVariant(QBrush(self.__editableValueBackgroundColor))

if role != Qt.DisplayRole:
return QVariant()

rowdata = [node.name(), node.value()]

# Specify which data to display in each column!
Expand Down Expand Up @@ -195,5 +237,3 @@ def checkSanity(self):
cc3d_xml_2_obj_converter = CompuCellSetup.parseXML(file_name)

sim_model = SimModel(domDoc=cc3d_xml_2_obj_converter)


3 changes: 3 additions & 0 deletions cc3d/player5/Utilities/TreeMapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ def setCC3DXMLElement(self, _cc3dXMLElement):
def setElementType(self, _elementType):
self.__elementType = _elementType

def elementType(self):
return self.__elementType

def setSuperParent(self, _superParent):
self.__superParent = _superParent

Expand Down
9 changes: 9 additions & 0 deletions cc3d/player5/ViewManager/SimpleViewManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,15 @@ def init_window_menu(self):
:return:
"""
menu = QMenu(QApplication.translate('ViewManager', '&Window'), self.ui)
menu.menuAction().setMenuRole(QAction.NoRole)
menu.addAction(self.new_graphics_window_act)
menu.addAction(self.tile_act)
menu.addAction(self.cascade_act)
if getattr(self, 'MDI_ON', False):
menu.addAction(self.minimize_all_graphics_windows_act)
menu.addAction(self.restore_all_graphics_windows_act)
menu.addSeparator()
menu.addAction(self.close_active_window_act)

# NOTE initialization of the menu is done in the updateWindowMenu function in SimpleTabView

Expand Down