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
41 changes: 35 additions & 6 deletions Orange/widgets/utils/stickygraphicsview.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import sys
import math

from PyQt5.QtCore import Qt, QRectF, QEvent, QCoreApplication, QObject, QPointF
from PyQt5.QtGui import QBrush, QPalette, QTransform
from PyQt5.QtCore import (
Qt, QRectF, QEvent, QCoreApplication, QObject, QPointF, QRect
)
from PyQt5.QtGui import QBrush, QPalette, QTransform, QPolygonF
from PyQt5.QtWidgets import (
QGraphicsView, QGraphicsScene, QWidget, QVBoxLayout, QSizePolicy,
QScrollBar, QGraphicsDropShadowEffect
Expand Down Expand Up @@ -109,7 +111,7 @@ def setFooterSceneRect(self, rect: QRectF) -> None:
self.__updateFooter()

def footerSceneRect(self) -> QRectF:
return QRectF(self.__headerRect)
return QRectF(self.__footerRect)

def setScene(self, scene: QGraphicsScene) -> None:
"""Reimplemented"""
Expand Down Expand Up @@ -218,10 +220,11 @@ def __updateView(self, view: QGraphicsView, rect: QRectF) -> None:
container.setVisible(False)
return
# map the rect to (main) viewport coordinates
viewrect = self.mapFromScene(rect).boundingRect()
viewrect = qgraphicsview_map_rect_from_scene(self, rect).boundingRect()
viewrect = qrectf_to_inscribed_rect(viewrect)
viewportrect = self.viewport().rect()
visible = not (viewrect.top() >= viewportrect.top()
and viewrect.bottom() <= viewportrect.bottom())
visible = (viewrect.top() < viewportrect.top() or
viewrect.y() + viewrect.height() > viewportrect.y() + viewportrect.height())
container.setVisible(visible)
# force immediate layout of the container overlay
QCoreApplication.sendEvent(container, QEvent(QEvent.LayoutRequest))
Expand All @@ -248,6 +251,32 @@ def viewportEvent(self, event: QEvent) -> bool:
return super().viewportEvent(event)


def qgraphicsview_map_rect_from_scene(
view: QGraphicsView, rect: QRectF
) -> QPolygonF:
"""Like QGraphicsView.mapFromScene(QRectF) but returning a QPolygonF
(without rounding).
"""
tr = view.viewportTransform()
p1 = tr.map(rect.topLeft())
p2 = tr.map(rect.topRight())
p3 = tr.map(rect.bottomRight())
p4 = tr.map(rect.bottomLeft())
return QPolygonF([p1, p2, p3, p4])


def qrectf_to_inscribed_rect(rect: QRectF) -> QRect:
"""
Return the largest integer QRect such that it is completely contained in
`rect`.
"""
xmin = int(math.ceil(rect.x()))
xmax = int(math.floor(rect.right()))
ymin = int(math.ceil(rect.top()))
ymax = int(math.floor(rect.bottom()))
return QRect(xmin, ymin, max(xmax - xmin, 0), max(ymax - ymin, 0))


def main(args): # pragma: no cover
# pylint: disable=import-outside-toplevel,protected-access
from PyQt5.QtWidgets import QApplication, QAction
Expand Down
71 changes: 68 additions & 3 deletions Orange/widgets/utils/tests/test_stickygraphicsview.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
from PyQt5.QtCore import Qt, QRectF, QPoint, QPointF
from PyQt5.QtGui import QBrush, QWheelEvent
from PyQt5.QtWidgets import QGraphicsScene, QWidget, QApplication
from PyQt5.QtWidgets import QGraphicsScene, QWidget, QApplication, QStyle

from Orange.widgets.tests.base import GuiTest

from Orange.widgets.utils.stickygraphicsview import StickyGraphicsView


class TestStickyGraphicsView(GuiTest):
def test(self):
def create_view(self):
view = StickyGraphicsView()
scene = QGraphicsScene(view)
scene.setBackgroundBrush(QBrush(Qt.lightGray, Qt.CrossPattern))
view.setScene(scene)
return view

def test(self):
view = self.create_view()
scene = view.scene()
scene.setBackgroundBrush(QBrush(Qt.lightGray, Qt.CrossPattern))
scene.addRect(
QRectF(0, 0, 300, 20), Qt.red, QBrush(Qt.red, Qt.BDiagPattern))
scene.addRect(QRectF(0, 25, 300, 100))
Expand Down Expand Up @@ -48,6 +53,66 @@ def test(self):

qWheelScroll(header.viewport(), angleDelta=QPoint(0, -720 * 8))

@staticmethod
def _ensure_laid_out(view: QWidget) -> None:
"""Ensure view has had pending resize events flushed."""
# when a widget is not visible it does not get resizeEvents dispatched
# immediately, only before it is actually shown or its rendering is
# requested.
view.grab()

def _test_visibility(self, view: StickyGraphicsView) -> None:
header = view.headerView()
footer = view.footerView()
vsbar = view.verticalScrollBar()
vsbar.triggerAction(vsbar.SliderToMinimum)
self._ensure_laid_out(view)

self.assertFalse(header.isVisibleTo(view))
self.assertTrue(footer.isVisibleTo(view))

vsbar.triggerAction(vsbar.SliderSingleStepAdd)
self._ensure_laid_out(view)

self.assertTrue(header.isVisibleTo(view))
self.assertTrue(footer.isVisibleTo(view))

vsbar.triggerAction(vsbar.SliderToMaximum)
self._ensure_laid_out(view)

self.assertTrue(header.isVisibleTo(view))
self.assertFalse(footer.isVisibleTo(view))

vsbar.triggerAction(vsbar.SliderSingleStepSub)
self._ensure_laid_out(view)
if not view.style().styleHint(QStyle.SH_ScrollBar_Transient, None, vsbar):
# cannot reliably test due to QTBUG-65074
self.assertTrue(header.isVisibleTo(view))
self.assertTrue(footer.isVisibleTo(view))

def test_fractional_1(self):
view = self.create_view()
view.resize(300, 100)
scenerect = QRectF(-0.1, -0.1, 300.2, 300.2)
headerrect = QRectF(-0.1, -0.1, 300.2, 20.2)
footerrect = QRectF(-0.1, 279.9, 300.2, 20.2)
view.setSceneRect(scenerect)
view.setHeaderSceneRect(headerrect)
view.setFooterSceneRect(footerrect)
self._test_visibility(view)

def test_fractional_2(self):
view = self.create_view()
view.resize(300, 100)
view.grab()
scenerect = QRectF(0.1, 0.1, 300, 299.8)
headerrect = QRectF(0.1, 0.1, 300, 20)
footerrect = QRectF(0.1, 299.9 - 20, 300, 20)
view.setSceneRect(scenerect)
view.setHeaderSceneRect(headerrect)
view.setFooterSceneRect(footerrect)
self._test_visibility(view)


def qWheelScroll(
widget: QWidget, buttons=Qt.NoButton, modifiers=Qt.NoModifier,
Expand Down