#------------------------------------------------------------------------------
# Copyright (c) 2012, Enthought, Inc.
# All rights reserved.
#------------------------------------------------------------------------------
from .qt.QtCore import Qt, QEvent, QSize, Signal
from .qt.QtGui import QScrollArea
from .qt_constraints_widget import QtConstraintsWidget
from .qt_container import QtContainer
SCROLLBAR_MAP = {
'as_needed' : Qt.ScrollBarAsNeeded,
'always_off' : Qt.ScrollBarAlwaysOff,
'always_on' : Qt.ScrollBarAlwaysOn
}
class QCustomScrollArea(QScrollArea):
""" A custom QScrollArea for use with the QtScrollArea.
This subclass fixes some bugs related to size hints.
"""
#: A signal emitted when a LayoutRequest event is posted to the
#: scroll area. This will typically occur when the size hint of
#: the scroll area is no longer valid.
layoutRequested = Signal()
#: A private internally cached size hint.
_size_hint = QSize()
def event(self, event):
""" A custom event handler which handles LayoutRequest events.
When a LayoutRequest event is posted to this widget, it will
emit the `layoutRequested` signal. This allows an external
consumer of this widget to update their external layout.
"""
res = super(QCustomScrollArea, self).event(event)
if event.type() == QEvent.LayoutRequest:
self._size_hint = QSize()
self.layoutRequested.emit()
return res
def setWidget(self, widget):
""" Set the widget for this scroll area.
This is a reimplemented parent class method which invalidates
the cached size hint before setting the widget.
"""
self._size_hint = QSize()
self.takeWidget() # Let Python keep ownership of the old widget
super(QCustomScrollArea, self).setWidget(widget)
def sizeHint(self):
""" Get the size hint for the scroll area.
This reimplemented method fixes a Qt bug where the size hint
is not updated after the scroll widget is first shown. The
bug is documented on the Qt bug tracker:
https://bugreports.qt-project.org/browse/QTBUG-10545
"""
# This code is ported directly from QScrollArea.cpp but instead
# of caching the size hint of the scroll widget, it caches the
# size hint for the entire scroll area, and invalidates it when
# the widget is changed or it receives a LayoutRequest event.
hint = self._size_hint
if hint.isValid():
return QSize(hint)
fw = 2 * self.frameWidth()
hint = QSize(fw, fw)
font_height = self.fontMetrics().height()
widget = self.widget()
if widget is not None:
if self.widgetResizable():
hint += widget.sizeHint()
else:
hint += widget.size()
else:
hint += QSize(12 * font_height, 8 * font_height)
if self.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOn:
vbar = self.verticalScrollBar()
hint.setWidth(hint.width() + vbar.sizeHint().width())
if self.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOn:
hbar = self.horizontalScrollBar()
hint.setHeight(hint.height() + hbar.sizeHint().height())
hint = hint.boundedTo(QSize(36 * font_height, 24 * font_height))
self._size_hint = hint
return QSize(hint)
[docs]class QtScrollArea(QtConstraintsWidget):
""" A Qt implementation of an Enaml ScrollArea.
"""
#: A private cache of the old size hint for the scroll area.
_old_hint = None
#--------------------------------------------------------------------------
# Setup Methods
#--------------------------------------------------------------------------
[docs] def create_widget(self, parent, tree):
""" Create the underlying QScrollArea widget.
"""
return QCustomScrollArea(parent)
[docs] def create(self, tree):
""" Create and initialize the underlying widget.
"""
super(QtScrollArea, self).create(tree)
self.set_horizontal_policy(tree['horizontal_policy'])
self.set_vertical_policy(tree['vertical_policy'])
self.set_widget_resizable(tree['widget_resizable'])
[docs] def init_layout(self):
""" Initialize the layout of the underlying widget.
"""
super(QtScrollArea, self).init_layout()
widget = self.widget()
widget.setWidget(self.scroll_widget())
widget.layoutRequested.connect(self.on_layout_requested)
#--------------------------------------------------------------------------
# Utility Methods
#--------------------------------------------------------------------------
[docs] def scroll_widget(self):
""" Find and return the scroll widget child for this widget.
Returns
-------
result : QWidget or None
The scroll widget defined for this widget, or None if one is
not defined.
"""
widget = None
for child in self.children():
if isinstance(child, QtContainer):
widget = child.widget()
return widget
#--------------------------------------------------------------------------
# Child Events
#--------------------------------------------------------------------------
[docs] def child_removed(self, child):
""" Handle the child removed event for a QtScrollArea.
"""
if isinstance(child, QtContainer):
self.widget().setWidget(self.scroll_widget())
[docs] def child_added(self, child):
""" Handle the child added event for a QtScrollArea.
"""
if isinstance(child, QtContainer):
self.widget().setWidget(self.scroll_widget())
#--------------------------------------------------------------------------
# Signal Handlers
#--------------------------------------------------------------------------
[docs] def on_layout_requested(self):
""" Handle the `layoutRequested` signal from the QScrollArea.
"""
new_hint = self.widget().sizeHint()
if new_hint != self._old_hint:
self._old_hint = new_hint
self.size_hint_updated()
#--------------------------------------------------------------------------
# Overrides
#--------------------------------------------------------------------------
[docs] def replace_constraints(self, old_cns, new_cns):
""" A reimplemented QtConstraintsWidget layout method.
Constraints layout may not cross the boundary of a ScrollArea,
so this method is no-op which stops the layout propagation.
"""
pass
#--------------------------------------------------------------------------
# Message Handlers
#--------------------------------------------------------------------------
[docs] def on_action_set_horizontal_policy(self, content):
""" Handle the 'set_horizontal_policy' action from the Enaml
widget.
"""
self.set_horizontal_policy(content['horizontal_policy'])
[docs] def on_action_set_vertical_policy(self, content):
""" Handle the 'set_vertical_policy' action from the Enaml
widget.
"""
self.set_vertical_policy(content['vertical_policy'])
[docs] def on_action_set_widget_resizable(self, content):
""" Handle the 'set_widget_resizable' action from the Enaml
widget.
"""
self.set_widget_resizable(content['widget_resizable'])
#--------------------------------------------------------------------------
# Widget Update Methods
#--------------------------------------------------------------------------
[docs] def set_horizontal_policy(self, policy):
""" Set the horizontal scrollbar policy of the widget.
"""
self.widget().setHorizontalScrollBarPolicy(SCROLLBAR_MAP[policy])
[docs] def set_vertical_policy(self, policy):
""" Set the vertical scrollbar policy of the widget.
"""
self.widget().setVerticalScrollBarPolicy(SCROLLBAR_MAP[policy])
[docs] def set_widget_resizable(self, resizable):
""" Set whether or not the scroll widget is resizable.
"""
self.widget().setWidgetResizable(resizable)