Skip to content

Conversation

abmajith
Copy link

In this pull request, created a unique LegendIcon, and LegendItem widget that is listed in vertical in qt.MainWindow,

the respective example are provided, but so far no unit test,

also I still not sure, whether I added some what generic use case to the LegendItem widget,

@loichuder loichuder requested a review from t20100 September 11, 2025 14:29
@loichuder
Copy link
Member

@abmajith We introduced black and flake8 in the CI in the mean time. Please reformat and check the code, thanks 🙂

Copy link
Member

@t20100 t20100 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First round of feedbacks, here are the 2 main ones:

  • LegendItemList should make minimal changes (add/remove) on the list rather than re-creating it on changes.
  • It seems LegendItemIcon and LegendItemWidget are having similar logic, maybe best not to inherit from LegendIconWidget and implement the update logic at a single place in LegendItemWidget?

self.setFixedWidth(200)
self._layout = qt.QVBoxLayout(self)
self._plot: PlotWidget | None = None
self._binding(parent)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PlotWidget that is "observed" by the list of legends is not the "parent": "parent" is Qt widget tree structure, the plot can be anywhere in it.
Best to provide get|setPlotWidget methods to set it separately, and eventually pass it in __init__:

def __init__(
    self,
    parent: qt.QWidget | None = None,
    plotWidget: PlotWidget | None = None,
):

Comment on lines 173 to 180
if hasattr(parent, "sigVisibilityChanged"):
parent.sigVisibilityChanged.connect(self._setVisibility)

def _setVisibility(self, status: bool) -> None:
if status:
self.show()
else:
self.hide()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This widget shouldn't need to care about this, Qt does it already:

Suggested change
if hasattr(parent, "sigVisibilityChanged"):
parent.sigVisibilityChanged.connect(self._setVisibility)
def _setVisibility(self, status: bool) -> None:
if status:
self.show()
else:
self.hide()

Comment on lines 185 to 187
self._plot.sigActiveCurveChanged.connect(self._onActiveItemChanged)
self._plot.sigActiveImageChanged.connect(self._onActiveItemChanged)
self._plot.sigActiveScatterChanged.connect(self._onActiveItemChanged)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "active" state of the items is not used in _onActiveItemChanged, so no need to handle those signals

Suggested change
self._plot.sigActiveCurveChanged.connect(self._onActiveItemChanged)
self._plot.sigActiveImageChanged.connect(self._onActiveItemChanged)
self._plot.sigActiveScatterChanged.connect(self._onActiveItemChanged)

Comment on lines 188 to 189
self._plot.sigItemAdded.connect(self._onContentChanged)
self._plot.sigItemRemoved.connect(self._onContentChanged)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for wrapper method:

Suggested change
self._plot.sigItemAdded.connect(self._onContentChanged)
self._plot.sigItemRemoved.connect(self._onContentChanged)
self._plot.sigItemAdded.connect(self._updateAllItemsList)
self._plot.sigItemRemoved.connect(self._updateAllItemsList)

But here, we don't want to redo the whole list each time an item is added/removed, just add/remove the corresponding LegendItemWidget

Comment on lines 211 to 220
self._updateHeigth()

def _updateHeigth(self):
_totalHeight = (self._layout.count() - 1) * self._layout.spacing()
_totalHeight += self._layout.contentsMargins().top()
_totalHeight += self._layout.contentsMargins().bottom()
if self._layout.count() > 0:
item = self._layout.itemAt(0)
_totalHeight += item.widget().sizeHint().height() * self._layout.count()
self.setFixedHeight(_totalHeight)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is issue with layout, it should be dealt otherwise.
It's always risky to play with this, for instance, the text of the legend is clipped on my laptop.

def __init__(self, parent: PlotWidget):
super().__init__(parent, qt.Qt.Window)
self.setFixedWidth(200)
self._layout = qt.QVBoxLayout(self)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to keep a reference on the layout, it can be retrieved with self.layout()


class LegendItemList(qt.QWidget):
def __init__(self, parent: PlotWidget):
super().__init__(parent, qt.Qt.Window)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either pass flags from __init__ or use the default:

Suggested change
super().__init__(parent, qt.Qt.Window)
super().__init__(parent)

class LegendItemList(qt.QWidget):
def __init__(self, parent: PlotWidget):
super().__init__(parent, qt.Qt.Window)
self.setFixedWidth(200)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do use fixed size, best to let Qt handles it: rendering changes depending on OS & screen...

super().__init__(parent, qt.Qt.Window)
self.setFixedWidth(200)
self._layout = qt.QVBoxLayout(self)
self._plot: PlotWidget | None = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best to keep a weakreference

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the weekref not pointing to the item attribute class, it throws error, for example when i call ite,getName(), throws no methods called getName() something

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you be more explicit?
Without the code and the complete error message, it is impossible to help.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

python3 examples/plotLegendLists.py 
Traceback (most recent call last):
  File "/home/noordhee/DAU_Projects/silx/src/silx/gui/plot/LegendItem.py", line 162, in _updateAllItemsList
    self._onItemAdded(item)
  File "/home/noordhee/DAU_Projects/silx/src/silx/gui/plot/LegendItem.py", line 171, in _onItemAdded
    legendIcon = LegendItemWidget(None, item)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/noordhee/DAU_Projects/silx/src/silx/gui/plot/LegendItem.py", line 29, in __init__
    self._label = qt.QLabel(self._item.getName())
                            ^^^^^^^^^^^^^^^^^^
AttributeError: 'weakref.ReferenceType' object has no attribute 'getName'
Traceback (most recent call last):
  File "/home/noordhee/DAU_Projects/silx/src/silx/gui/plot/LegendItem.py", line 162, in _updateAllItemsList
    self._onItemAdded(item)
  File "/home/noordhee/DAU_Projects/silx/src/silx/gui/plot/LegendItem.py", line 171, in _onItemAdded
    legendIcon = LegendItemWidget(None, item)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/noordhee/DAU_Projects/silx/src/silx/gui/plot/LegendItem.py", line 29, in __init__
    self._label = qt.QLabel(self._item.getName())
                            ^^^^^^^^^^^^^^^^^^
AttributeError: 'weakref.ReferenceType' object has no attribute 'getName'
Traceback (most recent call last):
  File "/home/noordhee/DAU_Projects/silx/src/silx/gui/plot/LegendItem.py", line 162, in _updateAllItemsList
    self._onItemAdded(item)
  File "/home/noordhee/DAU_Projects/silx/src/silx/gui/plot/LegendItem.py", line 171, in _onItemAdded
    legendIcon = LegendItemWidget(None, item)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/noordhee/DAU_Projects/silx/src/silx/gui/plot/LegendItem.py", line 29, in __init__
    self._label = qt.QLabel(self._item.getName())
                            ^^^^^^^^^^^^^^^^^^
AttributeError: 'weakref.ReferenceType' object has no attribute 'getName'

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when i store the item with weakref, this is the error i got

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weakref.ref cannot be accessed as the item it references.
You have to "call" it to get back the item:

myweakref = weakref.ref(myitem)
...
item = myweakref()
if item is not None:  # It returns None if the item has been deleted
    item.getName()

An alternative is to use weakref.proxy, but best IMO to know if the object still exists

Look at: https://docs.python.org/3/library/weakref.html#weak-reference-objects

self.layout().addStretch()

self.label.setToolTip("Click to toggle visibility")
self.label.mousePressEvent = self._onLabelClicked
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a good idea to "monkey patch" a Qt method.
Since it would make sense to also accept the press on the icon, you can implement mousePressEvent in this class.

@abmajith
Copy link
Author

First round of feedbacks, here are the 2 main ones:

* `LegendItemList` should make minimal changes (add/remove) on the list rather than re-creating it on changes.

* It seems `LegendItemIcon` and `LegendItemWidget` are having similar logic, maybe best not to inherit from `LegendIconWidget` and implement the update logic at a single place in `LegendItemWidget`?

Yes, i think add/removal should be minimum, and also we the second point, making single class for icon and widget,

…dget (that shows name and legen item symbol in a row)
@abmajith abmajith requested a review from t20100 September 16, 2025 08:37
Copy link
Member

@t20100 t20100 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second round of comments:
Much simpler with a single LegendItemWidget, ch and update only on add/remove!

There's something to check about removing/clearing widget, this needs to be robust: it should not let widgets alive that are no longer displayed.

There's a few comments from last review that where not taken into account nor commented. Before requesting a new review, can you address all comments, either by applying them if you agree or by commenting if you don't agree or need clarifications.



class LegendItemWidget(qt.QWidget):
def __init__(self, parent, item: items):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

items is a module, not a class

Suggested change
def __init__(self, parent, item: items):
def __init__(self, parent, item: items.Item):

Comment on lines 130 to 132
def _clearLayout(self, layout):
"""Helper to clear all widgets from a layout."""
if layout is not None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to pass the layout as an argument:

Suggested change
def _clearLayout(self, layout):
"""Helper to clear all widgets from a layout."""
if layout is not None:
def _clear(self):
"""Helper to clear all widgets from a layout."""
layout = self.layout()
if layout is not None:
  • this does not clear self._itemWidgets

if self._plot is None:
return
self._clearLayout(self.layout())
_activeItem: tuple = self._plot.getItems()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No _ prefix for local variables (there's others in this PR).
Already mentionned in #4404 (comment)


def _onItemRemoved(self, item: items.Item):
if item in self._itemWidgets:
self.layout().removeWidget(self._itemWidgets[item])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this enough to fully remove the widget? What is it's parent after the removal?
If it's the LegendItemList then it's not deleted.

Either this is enough and there is no need for deletedLater in _clearLayout or it's fully destroying the widget and something like in _clearLayout should be enough.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay for the consistency, I will follow the same mechanism as in _clearLayout

self.layout().addStretch()

self._label.setToolTip("Click to toggle visibility")
self._label.mousePressEvent = self._onLabelClicked
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already mentioned in #4404 (comment)
This is not a good pattern. you can implement mousePressEvent in this class instead


self._label.setToolTip("Click to toggle visibility")
self._label.mousePressEvent = self._onLabelClicked
self._label.setCursor(qt.Qt.PointingHandCursor)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can set it for the whole widget

Suggested change
self._label.setCursor(qt.Qt.PointingHandCursor)
self.setCursor(qt.Qt.PointingHandCursor)

Comment on lines +61 to +77
if isinstance(item, items.SymbolMixIn):
self._icon.setSymbol(item.getSymbol())
if isinstance(item, items.LineMixIn):
self._icon.setLineWidth(item.getLineWidth())
self._icon.setLineStyle(item.getLineStyle())
if isinstance(item, items.ColorMixIn):
color_rgba = item.getColor()
color = qt.QColor.fromRgbF(
color_rgba[0],
color_rgba[1],
color_rgba[2],
color_rgba[3],
)
self._icon.setLineColor(color)
self._icon.setSymbolColor(color)
if isinstance(item, items.ColormapMixIn):
self._icon.setColormap(item.getColormap())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment on lines +44 to +46
items.ItemChangedType.VISIBLE,
items.ItemChangedType.HIGHLIGHTED,
items.ItemChangedType.NAME,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite a few change events seems to be missing here: line, symbol, color...

@abmajith
Copy link
Author

Hi @t20100

I worked on your review,

I have a problem with weakref, (provided the error message),

@abmajith
Copy link
Author

abmajith commented Oct 6, 2025

Hi @t20100,

I tested the LegendItem gui,

there is one discomfort right now, When I am closing plot window that linked to LegendItem (as in this example/plotLegendItem), the legendItem has to be closed separately, Is this a problem, or we have to set up the parent widget to plot window so that it will automatically closed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants