From 281cb8825e89623bc200bbeaaf89012140c2b0f5 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Thu, 9 Dec 2021 13:23:54 +0100 Subject: [PATCH 01/12] Revert "[FIX] Silhouette Plot: elide hover labels if labels longer than n characters" This reverts commit 2b024786c9f32f0c8133f998f269d53ab01253ea. --- Orange/widgets/visualize/owsilhouetteplot.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Orange/widgets/visualize/owsilhouetteplot.py b/Orange/widgets/visualize/owsilhouetteplot.py index b39fd611ebd..0ed49c7e311 100644 --- a/Orange/widgets/visualize/owsilhouetteplot.py +++ b/Orange/widgets/visualize/owsilhouetteplot.py @@ -38,9 +38,6 @@ from Orange.widgets.widget import Msg, Input, Output -ROW_NAMES_WIDTH = 200 - - class InputValidationError(ValueError): message: str @@ -630,10 +627,7 @@ def setRowNames(self, names): item = layout.itemAt(i + 1, 3) assert isinstance(item, TextListWidget) if grp.rownames is not None: - metrics = QFontMetrics(self.font()) - rownames = [metrics.elidedText(rowname, Qt.ElideRight, ROW_NAMES_WIDTH) - for rowname in grp.rownames] - item.setItems(rownames) + item.setItems(grp.rownames) item.setVisible(self.__rowNamesVisible) else: item.setItems([]) @@ -1122,7 +1116,8 @@ def event(self, event): return super().event(event) def sizeHint(self, which, constraint=QSizeF()): - return QSizeF(300, (self.__barsize + self.__spacing) * self.count()) + spacing = max(self.__spacing * (self.count() - 1), 0) + return QSizeF(300, self.__barsize * self.count() + spacing) def setPreferredBarSize(self, size): if self.__barsize != size: @@ -1132,6 +1127,11 @@ def setPreferredBarSize(self, size): def spacing(self): return self.__spacing + def setSpacing(self, spacing): + if self.__spacing != spacing: + self.__spacing = spacing + self.updateGeometry() + def setPen(self, pen): pen = QPen(pen) if self.__pen != pen: From a95ab04691e272ad828890ab06eae0aa33f5acc6 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Thu, 9 Dec 2021 14:18:15 +0100 Subject: [PATCH 02/12] owsilhouetteplot: Do not clip children --- Orange/widgets/visualize/owsilhouetteplot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Orange/widgets/visualize/owsilhouetteplot.py b/Orange/widgets/visualize/owsilhouetteplot.py index 0ed49c7e311..b55207ef184 100644 --- a/Orange/widgets/visualize/owsilhouetteplot.py +++ b/Orange/widgets/visualize/owsilhouetteplot.py @@ -737,6 +737,7 @@ def __setup(self): layout.addItem(item, i + 1, 0, Qt.AlignCenter) textlist = TextListWidget(self, font=font) + textlist.setFlag(TextListWidget.ItemClipsChildrenToShape, False) sp = textlist.sizePolicy() sp.setVerticalPolicy(QSizePolicy.Ignored) textlist.setSizePolicy(sp) From 54ffad235c088f0c6960cf7e7fd92f9fe8f513de Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Tue, 21 Dec 2021 14:03:37 +0100 Subject: [PATCH 03/12] owsilhouetteplot: Constrain the AxisItem's widths --- Orange/widgets/visualize/owsilhouetteplot.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Orange/widgets/visualize/owsilhouetteplot.py b/Orange/widgets/visualize/owsilhouetteplot.py index b55207ef184..75b237d254c 100644 --- a/Orange/widgets/visualize/owsilhouetteplot.py +++ b/Orange/widgets/visualize/owsilhouetteplot.py @@ -764,19 +764,25 @@ def bottomScaleItem(self): # type: () -> Optional[QGraphicsWidget] return self.__bottomScale - def __updateTextSizeConstraint(self): + def __updateSizeConstraints(self): # set/update fixed height constraint on the text annotation items so # it matches the silhouette's height for silitem, textitem in zip(self.__plotItems(), self.__textItems()): height = silitem.effectiveSizeHint(Qt.PreferredSize).height() textitem.setMaximumHeight(height) textitem.setMinimumHeight(height) + mwidth = max((silitem.effectiveSizeHint(Qt.PreferredSize).width() + for silitem in self.__plotItems()), default=300) + # match the AxisItem's width to the bars + for axis in self.__axisItems(): + axis.setMaximumWidth(mwidth) + axis.setMinimumWidth(mwidth) def event(self, event): # Reimplemented if event.type() == QEvent.LayoutRequest and \ self.parentLayoutItem() is None: - self.__updateTextSizeConstraint() + self.__updateSizeConstraints() self.resize(self.effectiveSizeHint(Qt.PreferredSize)) return super().event(event) @@ -1005,6 +1011,9 @@ def __textItems(self): assert isinstance(item, TextListWidget) yield item + def __axisItems(self): + return self.__topScale, self.__bottomScale + def setSelection(self, indices): indices = np.unique(np.asarray(indices, dtype=int)) select = np.setdiff1d(indices, self.__selection) From 520efbdfb6200401f11d32b7d59173aa59468ca9 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Tue, 21 Dec 2021 15:12:56 +0100 Subject: [PATCH 04/12] owsilhouetteplot: Vertical center align text annotations --- Orange/widgets/visualize/owsilhouetteplot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Orange/widgets/visualize/owsilhouetteplot.py b/Orange/widgets/visualize/owsilhouetteplot.py index 75b237d254c..59ef7f07926 100644 --- a/Orange/widgets/visualize/owsilhouetteplot.py +++ b/Orange/widgets/visualize/owsilhouetteplot.py @@ -737,6 +737,7 @@ def __setup(self): layout.addItem(item, i + 1, 0, Qt.AlignCenter) textlist = TextListWidget(self, font=font) + textlist.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) textlist.setFlag(TextListWidget.ItemClipsChildrenToShape, False) sp = textlist.sizePolicy() sp.setVerticalPolicy(QSizePolicy.Ignored) From da9170c96f7dd83dcf3ea009a286bdaa5db70236 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Thu, 23 Dec 2021 10:39:32 +0100 Subject: [PATCH 05/12] graphicstextlist: Add option to elide text --- Orange/widgets/utils/graphicstextlist.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Orange/widgets/utils/graphicstextlist.py b/Orange/widgets/utils/graphicstextlist.py index 1a3c785264d..43c7e8ecc80 100644 --- a/Orange/widgets/utils/graphicstextlist.py +++ b/Orange/widgets/utils/graphicstextlist.py @@ -33,6 +33,7 @@ def __init__( alignment: Union[Qt.AlignmentFlag, Qt.Alignment] = Qt.AlignLeading, orientation: Qt.Orientation = Qt.Vertical, autoScale=False, + elideMode=Qt.ElideNone, **kwargs: Any ) -> None: self.__items: List[str] = [] @@ -45,6 +46,7 @@ def __init__( # The effective font when autoScale is in effect self.__effectiveFont = QFont() self.__widthCache = {} + self.__elideMode = elideMode sizePolicy = kwargs.pop( "sizePolicy", None) # type: Optional[QSizePolicy] super().__init__(None, **kwargs) @@ -147,7 +149,7 @@ def __width_for_font(self, font: QFont) -> float: if key in self.__widthCache: return self.__widthCache[key] fm = QFontMetrics(font) - width = max((fm.horizontalAdvance(text) for text in self.__items), + width = max((fm.boundingRect(text).width() for text in self.__items), default=0) self.__widthCache[key] = width return width @@ -252,6 +254,17 @@ def __layout(self) -> None: self.__effectiveFont = font apply_all(self.__textitems, lambda it: it.setFont(font)) + if self.__elideMode != Qt.ElideNone: + if self.__orientation == Qt.Vertical: + textwidth = math.ceil(crect.width()) + else: + textwidth = math.ceil(crect.height()) + for text, item in zip(self.__items, self.__textitems): + textelide = fm.elidedText( + text, self.__elideMode, textwidth, Qt.TextSingleLine + ) + item.setText(textelide) + advance = cell_height + spacing if align_vertical == Qt.AlignTop: align_dy = 0. From 09b00f44066457ec1775ce179af74dd0bcb4d6ea Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Thu, 23 Dec 2021 12:15:24 +0100 Subject: [PATCH 06/12] graphicsscene: Add a helper GraphicsScene with better tooltip dispatch --- Orange/widgets/utils/graphicsscene.py | 46 +++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 Orange/widgets/utils/graphicsscene.py diff --git a/Orange/widgets/utils/graphicsscene.py b/Orange/widgets/utils/graphicsscene.py new file mode 100644 index 00000000000..c04475a5d8e --- /dev/null +++ b/Orange/widgets/utils/graphicsscene.py @@ -0,0 +1,46 @@ +from AnyQt.QtCore import Qt +from AnyQt.QtGui import QTransform +from AnyQt.QtWidgets import ( + QGraphicsScene, QGraphicsSceneHelpEvent,QGraphicsView, QToolTip +) + +__all__ = [ + "GraphicsScene" +] + + +class GraphicsScene(QGraphicsScene): + """ + A QGraphicsScene with better tool tip event dispatch. + """ + def helpEvent(self, event: QGraphicsSceneHelpEvent) -> None: + """ + Reimplemented. + + Send the help event to every graphics item that is under the event's + scene position (default `QGraphicsScene` only dispatches help events + to `QGraphicsProxyWidget`s. + """ + widget = event.widget() + if widget is not None and isinstance(widget.parentWidget(), + QGraphicsView): + view = widget.parentWidget() + deviceTransform = view.viewportTransform() + else: + deviceTransform = QTransform() + items = self.items( + event.scenePos(), Qt.IntersectsItemShape, Qt.DescendingOrder, + deviceTransform, + ) + text = None + event.setAccepted(False) + for item in items: + self.sendEvent(item, event) + if event.isAccepted(): + return + elif item.toolTip(): + text = item.toolTip() + break + + if text is not None: + QToolTip.showText(event.screenPos(), text, event.widget()) From c29321e513915cd2067e161a5fd80878a2d3bcb6 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Thu, 23 Dec 2021 12:15:24 +0100 Subject: [PATCH 07/12] graphicstextlist: Implement help event handler for tooltips Don't set them on individual items --- Orange/widgets/utils/graphicstextlist.py | 62 +++++++++++++++++-- .../utils/tests/test_graphicstextlist.py | 24 ++++++- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/Orange/widgets/utils/graphicstextlist.py b/Orange/widgets/utils/graphicstextlist.py index 43c7e8ecc80..da7352c9b3a 100644 --- a/Orange/widgets/utils/graphicstextlist.py +++ b/Orange/widgets/utils/graphicstextlist.py @@ -1,11 +1,13 @@ +import bisect import math -from typing import Optional, Union, Any, Iterable, List, Callable +from typing import Optional, Union, Any, Iterable, List, Callable, cast -from AnyQt.QtCore import Qt, QSizeF, QEvent, QMarginsF +from AnyQt.QtCore import Qt, QSizeF, QEvent, QMarginsF, QPointF from AnyQt.QtGui import QFont, QFontMetrics, QFontInfo from AnyQt.QtWidgets import ( QGraphicsWidget, QSizePolicy, QGraphicsItemGroup, QGraphicsSimpleTextItem, - QGraphicsItem, QGraphicsScene, QGraphicsSceneResizeEvent + QGraphicsItem, QGraphicsScene, QGraphicsSceneResizeEvent, QToolTip, + QGraphicsSceneHelpEvent ) from . import apply_all from .graphicslayoutitem import scaled @@ -13,6 +15,20 @@ __all__ = ["TextListWidget"] +class _FuncArray: + __slots__ = ("func", "length") + + def __init__(self, func, length): + self.func = func + self.length = length + + def __getitem__(self, item): + return self.func(item) + + def __len__(self): + return self.length + + class TextListWidget(QGraphicsWidget): """ A linear text list widget. @@ -128,6 +144,30 @@ def count(self) -> int: """ return len(self.__items) + def indexAt(self, pos: QPointF) -> Optional[int]: + """ + Return the index of item at `pos`. + """ + def brect(item): + return item.mapRectToParent(item.boundingRect()) + + if self.__orientation == Qt.Vertical: + y = lambda pos: pos.y() + else: + y = lambda pos: pos.x() + top = lambda idx: brect(items[idx]).top() + bottom = lambda idx: brect(items[idx]).bottom() + items = self.__textitems + if not items: + return None + idx = bisect.bisect_right(_FuncArray(top, len(items)), y(pos)) - 1 + if idx == -1: + idx = 0 + if top(idx) <= y(pos) <= bottom(idx): + return idx + else: + return None + def sizeHint(self, which: Qt.SizeHint, constraint=QSizeF()) -> QSizeF: """Reimplemented.""" if which == Qt.PreferredSize: @@ -172,8 +212,23 @@ def event(self, event: QEvent) -> bool: self.__layout() elif event.type() == QEvent.ContentsRectChange: self.__layout() + elif event.type() == QEvent.GraphicsSceneHelp: + self.helpEvent(cast(QGraphicsSceneHelpEvent, event)) + if event.isAccepted(): + return True return super().event(event) + def helpEvent(self, event: QGraphicsSceneHelpEvent): + idx = self.indexAt(self.mapFromScene(event.scenePos())) + if idx is not None: + rect = self.__textitems[idx].sceneBoundingRect() + viewport = event.widget() + view = viewport.parentWidget() + rect = view.mapFromScene(rect).boundingRect() + QToolTip.showText(event.screenPos(), self.__items[idx], + view, rect) + event.setAccepted(True) + def changeEvent(self, event): if event.type() == QEvent.FontChange: self.updateGeometry() @@ -199,7 +254,6 @@ def __setup(self) -> None: t = QGraphicsSimpleTextItem(group) t.setFont(font) t.setText(text) - t.setToolTip(text) t.setData(0, text) self.__textitems.append(t) group.setParentItem(self) diff --git a/Orange/widgets/utils/tests/test_graphicstextlist.py b/Orange/widgets/utils/tests/test_graphicstextlist.py index 903e410d111..44df4da43a6 100644 --- a/Orange/widgets/utils/tests/test_graphicstextlist.py +++ b/Orange/widgets/utils/tests/test_graphicstextlist.py @@ -1,8 +1,11 @@ import unittest -from AnyQt.QtCore import Qt, QSizeF +from AnyQt.QtCore import Qt, QSizeF, QPoint +from AnyQt.QtGui import QHelpEvent +from AnyQt.QtWidgets import QGraphicsView, QApplication, QToolTip from orangewidget.tests.base import GuiTest +from Orange.widgets.utils.graphicsscene import GraphicsScene from Orange.widgets.utils.graphicstextlist import TextListWidget, scaled @@ -57,6 +60,25 @@ def brect(item): w.setAlignment(Qt.AlignVCenter) self.assertTrue(45 <= brect(item).center().y() < 55) + def test_tool_tips(self): + scene = GraphicsScene() + view = QGraphicsView(scene) + w = TextListWidget() + text = "A" * 10 + w.setItems([text, text]) + scene.addItem(w) + view.grab() # ensure w is laid out + wrect = view.mapFromScene(w.mapToScene(w.contentsRect())).boundingRect() + p = QPoint(wrect.topLeft() + QPoint(5, 5)) + ev = QHelpEvent( + QHelpEvent.ToolTip, p, view.viewport().mapToGlobal(p) + ) + try: + QApplication.sendEvent(view.viewport(), ev) + self.assertEqual(QToolTip.text(), text) + finally: + QToolTip.hideText() + class TestUtils(unittest.TestCase): def test_scaled(self): From 95ffacd75a54ef301bf8c61538a320f5a1886c9b Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Thu, 23 Dec 2021 13:42:26 +0100 Subject: [PATCH 08/12] owsilhouetteplot: Rework tooltip handling --- Orange/widgets/visualize/owsilhouetteplot.py | 124 ++++++++++++------- 1 file changed, 82 insertions(+), 42 deletions(-) diff --git a/Orange/widgets/visualize/owsilhouetteplot.py b/Orange/widgets/visualize/owsilhouetteplot.py index 59ef7f07926..d41afb07b85 100644 --- a/Orange/widgets/visualize/owsilhouetteplot.py +++ b/Orange/widgets/visualize/owsilhouetteplot.py @@ -3,18 +3,23 @@ from xml.sax.saxutils import escape from types import SimpleNamespace as namespace -from typing import Optional, Union +from typing import Optional, Union, Tuple, cast import numpy as np import sklearn.metrics from AnyQt.QtWidgets import ( - QGraphicsScene, QGraphicsWidget, QGraphicsGridLayout, + QGraphicsWidget, QGraphicsGridLayout, QGraphicsRectItem, QStyleOptionGraphicsItem, QSizePolicy, QWidget, QVBoxLayout, QGraphicsSimpleTextItem, QWIDGETSIZE_MAX, + QGraphicsSceneHelpEvent, QToolTip, QApplication, +) +from AnyQt.QtGui import ( + QColor, QPen, QBrush, QPainter, QFontMetrics, QPalette, +) +from AnyQt.QtCore import ( + Qt, QEvent, QRectF, QSizeF, QSize, QPointF, QPoint, QRect ) -from AnyQt.QtGui import QColor, QPen, QBrush, QPainter, QFontMetrics, QPalette -from AnyQt.QtCore import Qt, QEvent, QRectF, QSizeF, QSize, QPointF from AnyQt.QtCore import pyqtSignal as Signal import pyqtgraph as pg @@ -27,6 +32,7 @@ from Orange.misc import DistMatrix from Orange.widgets import widget, gui, settings +from Orange.widgets.utils.graphicsscene import GraphicsScene from Orange.widgets.utils.stickygraphicsview import StickyGraphicsView from Orange.widgets.utils import itemmodels from Orange.widgets.utils.annotated_data import (create_annotated_table, @@ -173,7 +179,7 @@ def __init__(self): gui.auto_send(self.buttonsArea, self, "auto_commit") - self.scene = QGraphicsScene(self) + self.scene = GraphicsScene(self) self.view = StickyGraphicsView(self.scene) self.view.setRenderHint(QPainter.Antialiasing, True) self.view.setAlignment(Qt.AlignTop | Qt.AlignLeft) @@ -539,6 +545,30 @@ class SelectAction(enum.IntEnum): NoUpdate, Clear, Select, Deselect, Toogle, Current = 1, 2, 4, 8, 16, 32 +def show_tool_tip(pos: QPoint, text: str, widget: Optional[QWidget] = None, + rect=QRect(), elide=Qt.ElideRight): + """ + Show a plain text tool tip with limited length, eliding if necessary. + """ + if widget is not None: + screen = widget.screen() + else: + screen = QApplication.screenAt(pos) + font = QApplication.font("QTipLabel") + fm = QFontMetrics(font) + geom = screen.availableSize() + etext = fm.elidedText(text, elide, geom.width()) + if etext != text: + text = f"{etext}" + QToolTip.showText(pos, text, widget, rect) + + +class _SilhouettePlotTextListWidget(TextListWidget): + # disable default tooltips, SilhouettePlot handles them + def helpEvent(self, event: QGraphicsSceneHelpEvent): + return + + class SilhouettePlot(QGraphicsWidget): """ A silhouette plot widget. @@ -632,18 +662,6 @@ def setRowNames(self, names): else: item.setItems([]) item.setVisible(False) - - barplot = list(self.__plotItems())[i] - baritems = barplot.items() - - if grp.rownames is None: - tooltips = itertools.repeat("") - else: - tooltips = grp.rownames - - for baritem, tooltip in zip(baritems, tooltips): - baritem.setToolTip(tooltip) - layout.activate() def setRowNamesVisible(self, visible): @@ -736,13 +754,15 @@ def __setup(self): item.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) layout.addItem(item, i + 1, 0, Qt.AlignCenter) - textlist = TextListWidget(self, font=font) - textlist.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + textlist = _SilhouettePlotTextListWidget( + self, font=font, elideMode=Qt.ElideRight, + alignment=Qt.AlignLeft | Qt.AlignVCenter + ) + textlist.setMaximumWidth(750) textlist.setFlag(TextListWidget.ItemClipsChildrenToShape, False) sp = textlist.sizePolicy() sp.setVerticalPolicy(QSizePolicy.Ignored) textlist.setSizePolicy(sp) - textlist.setParent(self) if group.rownames is not None: textlist.setItems(group.items) textlist.setVisible(self.__rowNamesVisible) @@ -779,14 +799,33 @@ def __updateSizeConstraints(self): axis.setMaximumWidth(mwidth) axis.setMinimumWidth(mwidth) - def event(self, event): + def event(self, event: QEvent) -> bool: # Reimplemented if event.type() == QEvent.LayoutRequest and \ self.parentLayoutItem() is None: self.__updateSizeConstraints() self.resize(self.effectiveSizeHint(Qt.PreferredSize)) + elif event.type() == QEvent.GraphicsSceneHelp: + self.helpEvent(cast(QGraphicsSceneHelpEvent, event)) + if event.isAccepted(): + return True return super().event(event) + def helpEvent(self, event: QGraphicsSceneHelpEvent): + pos = self.mapFromScene(event.scenePos()) + item = self.__itemDataAtPos(pos) + if item is None: + return + data, index, rect = item + if data.rownames is None: + return + ttip = data.rownames[index] + if ttip: + view = event.widget().parentWidget() + rect = view.mapFromScene(self.mapToScene(rect)).boundingRect() + show_tool_tip(event.screenPos(), ttip, event.widget(), rect) + event.setAccepted(True) + def __setHoveredItem(self, item): # Set the current hovered `item` (:class:`QGraphicsRectItem`) if self.__hoveredItem is not item: @@ -957,30 +996,31 @@ def itemAtPos(self, pos): else: return None - def indexAtPos(self, pos): - items = [item for item in self.__plotItems() - if item.geometry().contains(pos)] + def __itemDataAtPos(self, pos) -> Optional[Tuple[namespace, int, QRectF]]: + items = [(sitem, tlist, grp) for sitem, tlist, grp + in zip(self.__plotItems(), self.__textItems(), self.__groups) + if sitem.geometry().contains(pos) or tlist.isVisible() + and tlist.geometry().contains(pos)] if not items: - return -1 + return None else: - item = items[0] - indices = item.data(0) + sitem, _, grp = items[0] + indices = grp.indices assert (isinstance(indices, np.ndarray) and - indices.shape == (item.count(),)) - crect = item.contentsRect() - pos = item.mapFromParent(pos) - if not crect.contains(pos): - return -1 - - assert pos.x() >= 0 - rowh = crect.height() / item.count() - index = np.floor(pos.y() / rowh) + indices.shape == (sitem.count(),)) + crect = sitem.contentsRect() + pos = sitem.mapFromParent(pos) + if not crect.top() <= pos.y() <= crect.bottom(): + return None + rowh = crect.height() / sitem.count() + index = int(np.floor(pos.y() / rowh)) index = min(index, indices.size - 1) - - if index >= 0: - return indices[index] - else: - return -1 + baritem = sitem.items()[index] + rect = self.mapRectFromItem(baritem, baritem.rect()) + crect = self.contentsRect() + rect.setLeft(crect.left()) + rect.setRight(crect.right()) + return grp, index, rect def __selectionChanged(self, selected, deselected): for item, grp in zip(self.__plotItems(), self.__groups): @@ -1223,4 +1263,4 @@ def __layout(self): if __name__ == "__main__": # pragma: no cover - WidgetPreview(OWSilhouettePlot).run(Orange.data.Table("iris")) + WidgetPreview(OWSilhouettePlot).run(Orange.data.Table("brown-selected")) From 654fa092ba88d1c98241fc55f8e4630815822591 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Thu, 23 Dec 2021 14:52:48 +0100 Subject: [PATCH 09/12] owheatmap: Replace use of plain QGraphicsScene with GraphicsScene --- Orange/widgets/visualize/owheatmap.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Orange/widgets/visualize/owheatmap.py b/Orange/widgets/visualize/owheatmap.py index 873da164944..b3e4eca1bbe 100644 --- a/Orange/widgets/visualize/owheatmap.py +++ b/Orange/widgets/visualize/owheatmap.py @@ -10,8 +10,8 @@ import scipy.sparse as sp from AnyQt.QtWidgets import ( - QGraphicsScene, QGraphicsView, QFormLayout, QComboBox, QGroupBox, - QMenu, QAction, QSizePolicy + QGraphicsView, QFormLayout, QComboBox, QGroupBox, QMenu, QAction, + QSizePolicy ) from AnyQt.QtGui import QStandardItemModel, QStandardItem, QFont, QKeySequence from AnyQt.QtCore import Qt, QSize, QRectF, QObject @@ -27,6 +27,7 @@ from Orange.widgets.utils.itemmodels import DomainModel from Orange.widgets.utils.stickygraphicsview import StickyGraphicsView from Orange.widgets.utils.graphicsview import GraphicsWidgetView +from Orange.widgets.utils.graphicsscene import GraphicsScene from Orange.widgets.utils.colorpalettes import Palette from Orange.widgets.utils.annotated_data import (create_annotated_table, @@ -444,7 +445,7 @@ def _(idx, cb=cb): gui.auto_send(self.buttonsArea, self, "auto_commit") # Scene with heatmap - class HeatmapScene(QGraphicsScene): + class HeatmapScene(GraphicsScene): widget: Optional[HeatmapGridWidget] = None self.scene = self.scene = HeatmapScene(parent=self) From 8a52352712c4f29e87f32f009fe2d657055cbc0c Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Fri, 24 Dec 2021 09:12:38 +0100 Subject: [PATCH 10/12] graphicsscene: Extract help handler to a helper function --- Orange/widgets/utils/graphicsscene.py | 57 ++++++++++++++++----------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/Orange/widgets/utils/graphicsscene.py b/Orange/widgets/utils/graphicsscene.py index c04475a5d8e..6a91334d14b 100644 --- a/Orange/widgets/utils/graphicsscene.py +++ b/Orange/widgets/utils/graphicsscene.py @@ -5,7 +5,8 @@ ) __all__ = [ - "GraphicsScene" + "GraphicsScene", + "graphicsscene_help_event", ] @@ -21,26 +22,36 @@ def helpEvent(self, event: QGraphicsSceneHelpEvent) -> None: scene position (default `QGraphicsScene` only dispatches help events to `QGraphicsProxyWidget`s. """ - widget = event.widget() - if widget is not None and isinstance(widget.parentWidget(), - QGraphicsView): - view = widget.parentWidget() - deviceTransform = view.viewportTransform() - else: - deviceTransform = QTransform() - items = self.items( - event.scenePos(), Qt.IntersectsItemShape, Qt.DescendingOrder, - deviceTransform, - ) - text = None - event.setAccepted(False) - for item in items: - self.sendEvent(item, event) - if event.isAccepted(): - return - elif item.toolTip(): - text = item.toolTip() - break + graphicsscene_help_event(self, event) - if text is not None: - QToolTip.showText(event.screenPos(), text, event.widget()) + +def graphicsscene_help_event( + scene: QGraphicsScene, event: QGraphicsSceneHelpEvent +) -> None: + """ + Send the help event to every graphics item that is under the `event` + scene position. + """ + widget = event.widget() + if widget is not None and isinstance(widget.parentWidget(), + QGraphicsView): + view = widget.parentWidget() + deviceTransform = view.viewportTransform() + else: + deviceTransform = QTransform() + items = scene.items( + event.scenePos(), Qt.IntersectsItemShape, Qt.DescendingOrder, + deviceTransform, + ) + text = "" + event.setAccepted(False) + for item in items: + scene.sendEvent(item, event) + if event.isAccepted(): + return + elif item.toolTip(): + text = item.toolTip() + break + + QToolTip.showText(event.screenPos(), text, event.widget()) + event.setAccepted(bool(text)) From f9f6bfa262c832298a4c37e7fb4a8b515be5e788 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Fri, 24 Dec 2021 09:13:15 +0100 Subject: [PATCH 11/12] owdistancemap: Use help events for tooltips --- Orange/widgets/unsupervised/owdistancemap.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Orange/widgets/unsupervised/owdistancemap.py b/Orange/widgets/unsupervised/owdistancemap.py index 78e51786af4..9543b0d84e4 100644 --- a/Orange/widgets/unsupervised/owdistancemap.py +++ b/Orange/widgets/unsupervised/owdistancemap.py @@ -22,8 +22,10 @@ from Orange.widgets.utils import itemmodels, colorpalettes from Orange.widgets.utils.annotated_data import (create_annotated_table, ANNOTATED_DATA_SIGNAL_NAME) +from Orange.widgets.utils.graphicsscene import graphicsscene_help_event from Orange.widgets.utils.graphicstextlist import TextListWidget from Orange.widgets.utils.widgetpreview import WidgetPreview +from Orange.widgets.visualize.utils.plotutils import HelpEventDelegate from Orange.widgets.widget import Input, Output from Orange.widgets.utils.dendrogram import DendrogramWidget from Orange.widgets.visualize.utils.heatmap import ( @@ -245,6 +247,18 @@ def hoverMoveEvent(self, event): self.setToolTip("") +class GraphicsView(pg.GraphicsView): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + scene = self.scene() + delegate = HelpEventDelegate(self.__helpEvent, parent=self) + scene.installEventFilter(delegate) + + def __helpEvent(self, event): + graphicsscene_help_event(self.scene(), event) + return event.isAccepted() + + class OWDistanceMap(widget.OWWidget): name = "Distance Map" description = "Visualize a distance matrix." @@ -330,7 +344,7 @@ def _set_thresholds(low, high): gui.auto_send(self.buttonsArea, self, "autocommit") - self.view = pg.GraphicsView(background="w") + self.view = GraphicsView(background="w") self.mainArea.layout().addWidget(self.view) self.grid_widget = pg.GraphicsWidget() From 6645e312e7de39714718d52ff911c0b5ced526ac Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Fri, 24 Dec 2021 12:08:03 +0100 Subject: [PATCH 12/12] owsilhouetteplot: Use any discrete or string var for annotations --- Orange/widgets/visualize/owsilhouetteplot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Orange/widgets/visualize/owsilhouetteplot.py b/Orange/widgets/visualize/owsilhouetteplot.py index d41afb07b85..4e7dd04bc62 100644 --- a/Orange/widgets/visualize/owsilhouetteplot.py +++ b/Orange/widgets/visualize/owsilhouetteplot.py @@ -262,7 +262,8 @@ def _setup_control_models(self, domain: Domain): self.cluster_var_idx = groupvars.index(domain.class_var) else: self.cluster_var_idx = 0 - annotvars = [var for var in domain.metas if var.is_string] + annotvars = [var for var in domain.variables + domain.metas + if var.is_string or var.is_discrete] self.annotation_var_model[:] = ["None"] + annotvars self.annotation_var_idx = 1 if annotvars else 0 self.openContext(Orange.data.Domain(groupvars))