Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stretchable layout with MEDM-style widgets #857

Open
prjemian opened this issue May 12, 2022 · 5 comments · May be fixed by #865
Open

Stretchable layout with MEDM-style widgets #857

prjemian opened this issue May 12, 2022 · 5 comments · May be fixed by #865

Comments

@prjemian
Copy link
Collaborator

What's the problem this feature will solve?
MEDM widgets are placed by absolute positioning. MEDM users are accustomed to changing the screen size and the widgets will stretch accordingly. In Qt, the stretchable feature is provided by a layout manager which re-positions its widgets as the layout changes size.

The widgets from the adl2pydm converter are not stretchable and this is a popular feature that is missed in MEDM screens converted for use in PyDM.

Describe the solution you'd like
Internal widgets proportionally should stretch as the window changes size. (note: These example screens are rendered with caQtDM, a C++/Qt application, showing the idea is possible in Qt.)

original size
as-drawn

stretched
stretched

Additional context
After some research, it seems a good candidate case for a Qt Custom Layout Manager. The new custom layout would accept widgets placed in absolute coordinates and then adjust the geometry (x,y,h,w) of each to fit the containing QFrame.

The PyDM project already has an existing custom layout manager (FlowLayout()) that would be a good example for a new layout manager for this feature:

class FlowLayout(QLayout):
def __init__(self, parent=None, margin=-1, h_spacing=-1, v_spacing=-1):
QLayout.__init__(self, parent)
self.setContentsMargins(margin, margin, margin, margin)
self.m_h_space = h_spacing
self.m_v_space = v_spacing
self.item_list = []
def addItem(self, item):
self.item_list.append(item)
def horizontalSpacing(self):
if self.m_h_space >= 0:
return self.m_h_space
else:
return self.smart_spacing(QStyle.PM_LayoutHorizontalSpacing)
def verticalSpacing(self):
if self.m_v_space >= 0:
return self.m_v_space
else:
return self.smart_spacing(QStyle.PM_LayoutVerticalSpacing)
def count(self):
return len(self.item_list)
def itemAt(self, index):
if index >= 0 and index < len(self.item_list):
return self.item_list[index]
else:
return None
def takeAt(self, index):
if index >= 0 and index < len(self.item_list):
return self.item_list.pop(index)
else:
return None
def expandingDirections(self):
return Qt.Orientations(0)
def hasHeightForWidth(self):
return True
def heightForWidth(self, width):
return self.do_layout(QRect(0,0, width, 0), True)
def setGeometry(self, rect):
super(FlowLayout, self).setGeometry(rect)
self.do_layout(rect, False)
def sizeHint(self):
return self.minimumSize()
def minimumSize(self):
size = QSize()
for item in self.item_list:
size = size.expandedTo(item.minimumSize())
#size += QSize(2*self.margin(), 2*self.margin())
size += QSize(2*8, 2*8)
return size
def do_layout(self, rect, test_only):
(left, top, right, bottom) = self.getContentsMargins()
effective_rect = rect.adjusted(left, top, -right, -bottom)
x = effective_rect.x()
y = effective_rect.y()
line_height = 0
for item in self.item_list:
wid = item.widget()
space_x = self.horizontalSpacing()
if space_x == -1:
space_x = wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal)
space_y = self.verticalSpacing()
if space_y == -1:
space_y = wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical)
next_x = x + item.sizeHint().width() + space_x
if next_x - space_x > effective_rect.right() and line_height > 0:
x = effective_rect.x()
y = y + line_height + space_y
next_x = x + item.sizeHint().width() + space_x
line_height = 0
if not test_only:
item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
x = next_x
line_height = max(line_height, item.sizeHint().height())
return y + line_height - rect.y() + bottom
def smart_spacing(self, pm):
parent = self.parent()
if not parent:
return -1
elif parent.isWidgetType():
return parent.style().pixelMetric(pm, None, parent)
else:
return parent.spacing()

The adl2pydm converter would use this new layout for each of the MEDM screens it converts.

@prjemian
Copy link
Collaborator Author

prjemian commented May 12, 2022

Looking at the the caQtDM repository does not reveal any Qt custom layout manager. Instead, the application likely responds to resize events of the windows directly.

To co-exist with use of the standard Qt layout managers, a custom layout manager seems the right way to proceed.

@prjemian
Copy link
Collaborator Author

A basic version of the new custom layout (name to be adjusted per agreement):

class MedmLayout(QLayout):
    """
    Layout Manager for MEDM widgets.

    Widgets are added to this layout with absolute coordinates (x,y,height,width).
    When the layout is resized, the manager will resize each of the
    widgets in the layout.
    """

shows up in designer by following the patterns in qtplugins.py:

MedmLayoutPlugin = qtplugin_factory(MedmLayout,
                                    group='PyDM Layouts',
                                    is_container=True,
                                    extensions=BASE_EXTENSIONS)

but there are problems:

(dev-pydm) zorinvm@zorin22:~/.../slaclab/pydm$ designer
Loading PyDM Widgets
Exception occurred while running Qt Designer.
Traceback (most recent call last):
  File "/home/zorinvm/Documents/projects/slaclab/pydm/pydm/widgets/qtplugin_base.py", line 119, in createWidget
    w = self.cls(parent=parent)
TypeError: 'parent' is an unknown keyword argument

Designer: The custom widget factory registered for widgets of class MedmLayout returned 0.
** WARNING Factory failed to create  "MedmLayout"

https://stackoverflow.com/questions/5659875/custom-layout-in-qt-designer

Advice is to write a custom container (QWidget, QFrame, ...) which can be interfaced as a designer plugin.
Then call the custom layout from the custom container.

@prjemian
Copy link
Collaborator Author

Now called AbsoluteGeometryLayout

prjemian added a commit to prjemian/pydm that referenced this issue May 13, 2022
@prjemian
Copy link
Collaborator Author

QFrame is a subclass of QWidget
Clipboard01

The additional attributes provided by QFrame are not expected. The AbsoluteGeometryWidget will subclass from QWidget and call AbsoluteGeometryLayout which will have zeroes for all the margins by default:
Clipboard01

@prjemian
Copy link
Collaborator Author

Drat!. Even with the custom layout called from the custom widget, the custom widget is not designable in the sense that child widgets can be dragged into it. Unless the window is based on the custom widget. In either case, widgets added will not be part of the custom layout.

Rethink it

A QWidget can have widget children. It can manipulate the geometry of each child during a resizeEvent(). One way for that to be successful is to keep the original widget size and then compute the horizontal and vertical scale factors with each new resizeEvent.

prjemian added a commit to prjemian/pydm that referenced this issue May 13, 2022
prjemian added a commit to prjemian/pydm that referenced this issue May 13, 2022
prjemian added a commit to prjemian/pydm that referenced this issue May 14, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant