Repository: pyqtgraph/pyqtgraph

Issue:
Lazy rectangle selection box contruction breaks subclassed ViewBoxes
### Short description
#2168 introduced creating selection boxes lazily. This is, however, compatibility breaking for any classes that extend `ViewBox` and expect the attribute to behave as it did. For example, we use these in Orange here and I am sure this is not the only place we touch it:
https://github.com/biolab/orange3/blob/269d7fa75c0f36f69b318d925c080b3626d65837/Orange/widgets/visualize/utils/plotutils.py#L156

Should we work around the issue or are you perhaps planning on reverting this? Were there any substantial gains achieved by that particular optimization? Does anyone open thousands of viewboxes quickly?


Discussion:
@danielhrisca, did you need that particular optimization for your use case? Or did you just try to make the code a bit cleaner? If the latter, may I suggest reverting the PR for now?

I think these kind of compatibility-breaking changes are best done before new major releases.
@markotoplak in my use case I was creating plot widgets with hundreds of curves, each on dedicated viewboz so the initial rectangle creation was wasting time on something that was not used 
Hi @markotoplak 

I didn't see a case when of breaking downstream usage when merging #2168 and considered it a low-risk change; but if this is indeed breaking stuff for downstream users, I'm inclined to revert.  I'm open to that change being implemented in general, but should probably do in a way that emits a deprecation warning.
Daniel, an interesting approach. So you are like doing hundreds of viewboxes, one for each curve, right? Could you describe the context? I have never used more that ~10 viewboxes at the same time.

I have to admit I would also think of this a low-risk change. :) If we are the only users for whom this is a breaking change, we could work around it.

Perhaps a parameter in init could solve the same speedup in a backword-compatible way.

Right now this PR is absolete for me because I've switched to using a single viewbox. 

The initial reason for using individual viwboxes for each curve was to have independent Y scrolling for each curve. The user would select the current curve from a qlistwidget and the scrolling would o ly apply for that curve.
Sorry I've been absent here, I'm curious if there is a way we can maintain original and current functionality, with a warning being emitted with the original functionality.

@markotoplak is the issue that `self.rbScaleBox` can now be `None`?
@j9ac9k, yes, a class that extends ViewBox assumes that `self.rbScaleBox` is a `QGraphicsRectItem` after initialization and uses it independently of `ViewBox.RectMode`.

I raised an issue to notify of a breaking change. I do understand that you could judge our use case to be too exotic and will stick with it.
> @j9ac9k, yes, a class that extends ViewBox assumes that `self.rbScaleBox` is a `QGraphicsRectItem` after initialization and uses it independently of `ViewBox.RectMode`.
> 
> I raised an issue to notify of a breaking change. I do understand that you could judge our use case to be too exotic and will stick with it.

I am sort of not great at these kinds of decisions so I'll try and get a consensus w/ other maintainers and report back.  Generally speaking, we want to avoid releasing breaking changes between versions w/o a deprecation warning phase.  

We appreciate you testing from the `master` branch and alerting us to this breaking change.
I'm for not breaking other people's code. Since I was the only one using this optimization maybe you should just close the PR
> I'm for not breaking other people's code. Since I was the only one using this optimization maybe you should just close the PR

I've pinged some of the other maintainers to get their input.  I do think your PR is a good change, but I'm scratching my head on how to go about emitting warnings in the correct fashion.
>  but I'm scratching my head on how to go about emitting warnings in the correct fashion.

A possibility I see would be to rename the attribute, make it behave as it should, and use it with the new name everywhere in the library. And perhaps even make it private. :) Then, I'd make a property (effectively a pass-through) with the old name that complains on use (but on the first access also initializes it).
My initial impression from a quick glance at the discussion and the diff is that an `@property` would solve most of the problems, allowing any computation/allocation to be done lazily while maintaining the external interface that as soon as you access the attribute it is populated.

I don't even really think a deprecation or warning is warranted, just have a property with a setter that sets to a cached private variable and a getter that retrieves that cached value if it exists or allocates a new one if it was None
> My initial impression from a quick glance at the discussion and the diff is that an `@property` would solve most of the problems, allowing any computation/allocation to be done lazily while maintaining the external interface that as soon as you access the attribute it is populated.
> 
> I don't even really think a deprecation or warning is warranted, just have a property with a setter that sets to a cached private variable and a getter that retrieves that cached value if it exists or allocates a new one if it was None

This is the way!

Resolving patch:
diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py
--- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py
+++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py
@@ -204,8 +204,7 @@ def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True,
         self.borderRect.setZValue(1e3)
         self.borderRect.setPen(self.border)
 
-        self.rbScaleBox = None
-        self._add_rectangle_selection()
+        self._rbScaleBox = None
 
         ## show target rect for debugging
         self.target = QtWidgets.QGraphicsRectItem(0, 0, 1, 1)
@@ -232,8 +231,33 @@ def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True,
 
         self._viewPixelSizeCache  = None
 
+    @property
+    def rbScaleBox(self):
+        if self._rbScaleBox is None:
+            # call the setter with the default value
+            scaleBox = QtWidgets.QGraphicsRectItem(0, 0, 1, 1)
+            scaleBox.setPen(fn.mkPen((255, 255, 100), width=1))
+            scaleBox.setBrush(fn.mkBrush(255, 255, 0, 100))
+            scaleBox.setZValue(1e9)
+            scaleBox.hide()
+            self._rbScaleBox = scaleBox
+            self.addItem(scaleBox, ignoreBounds=True)
+        return self._rbScaleBox
+
+    @rbScaleBox.setter
+    def rbScaleBox(self, scaleBox):
+        if self._rbScaleBox is not None:
+            self.removeItem(self._rbScaleBox)
+        self._rbScaleBox = scaleBox
+        if scaleBox is None:
+            return None
+        scaleBox.setZValue(1e9)
+        scaleBox.hide()
+        self.addItem(scaleBox, ignoreBounds=True)
+        return None
+
     def getAspectRatio(self):
-        '''return the current aspect ratio'''
+        """return the current aspect ratio"""
         rect = self.rect()
         vr = self.viewRect()
         if rect.height() == 0 or vr.width() == 0 or vr.height() == 0:
@@ -349,21 +373,11 @@ def setMouseMode(self, mode):
         """
         if mode not in [ViewBox.PanMode, ViewBox.RectMode]:
             raise Exception("Mode must be ViewBox.PanMode or ViewBox.RectMode")
+        if mode == ViewBox.PanMode:
+            self._rbScaleBox = None
         self.state['mouseMode'] = mode
-        self._add_rectangle_selection()
-
         self.sigStateChanged.emit(self)
 
-    def _add_rectangle_selection(self):
-        if self.rbScaleBox is None and self.state['mouseMode'] == ViewBox.RectMode:
-            ## Make scale box that is shown when dragging on the view
-            self.rbScaleBox = QtWidgets.QGraphicsRectItem(0, 0, 1, 1)
-            self.rbScaleBox.setPen(fn.mkPen((255, 255, 100), width=1))
-            self.rbScaleBox.setBrush(fn.mkBrush(255, 255, 0, 100))
-            self.rbScaleBox.setZValue(1e9)
-            self.rbScaleBox.hide()
-            self.addItem(self.rbScaleBox, ignoreBounds=True)
-
     def setLeftButtonAction(self, mode='rect'):  ## for backward compatibility
         if mode.lower() == 'rect':
             self.setMouseMode(ViewBox.RectMode)


Test patch: