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

[FIX] Silhouette plot text width #5756

Merged
merged 12 commits into from
Jan 7, 2022
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
16 changes: 15 additions & 1 deletion Orange/widgets/unsupervised/owdistancemap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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."
Expand Down Expand Up @@ -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()
Expand Down
57 changes: 57 additions & 0 deletions Orange/widgets/utils/graphicsscene.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from AnyQt.QtCore import Qt
from AnyQt.QtGui import QTransform
from AnyQt.QtWidgets import (
QGraphicsScene, QGraphicsSceneHelpEvent,QGraphicsView, QToolTip
)

__all__ = [
"GraphicsScene",
"graphicsscene_help_event",
]


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.
"""
graphicsscene_help_event(self, event)


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))
77 changes: 72 additions & 5 deletions Orange/widgets/utils/graphicstextlist.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
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

__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.
Expand All @@ -33,6 +49,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] = []
Expand All @@ -45,6 +62,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)
Expand Down Expand Up @@ -126,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:
Expand All @@ -147,7 +189,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
Expand All @@ -170,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()
Expand All @@ -197,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)
Expand Down Expand Up @@ -252,6 +308,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.
Expand Down
24 changes: 23 additions & 1 deletion Orange/widgets/utils/tests/test_graphicstextlist.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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):
Expand Down
7 changes: 4 additions & 3 deletions Orange/widgets/visualize/owheatmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
Loading