Skip to content

Update GraphicsScene.py#2580

Closed
Gdalik wants to merge 1 commit intopyqtgraph:masterfrom
Gdalik:patch-1
Closed

Update GraphicsScene.py#2580
Gdalik wants to merge 1 commit intopyqtgraph:masterfrom
Gdalik:patch-1

Conversation

@Gdalik
Copy link
Copy Markdown

@Gdalik Gdalik commented Jan 8, 2023

Changes in 225 in order to prevent ValueError which appears in some cases.

Changes in 225 in order to prevent ValueError which appears in some cases.
@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jan 15, 2023

Hi @Gdalik I thought I posted a response to this PR while I'm on mobile, but now that I pull up the web I realize it didn't post, so my apologies for the late response.

I've heard reports of this error before, but I have never had a reproducible example. I don't see how this error occurs though, and I'm worried by implementing a fix like the one you suggested, we are merely masking a bug somewhere else in the library. Do you have an example you can share that triggers this error?

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 16, 2023

Hi @j9ac9k ! Thanks for your reply.
I use pyqtgraph in my currently non-distributed in-house app, dealing with musical audio, for interactive view of waveform+song timeline (starting and ending points, music bars/beats, sections of a song etc.), playback controls (triggering QMediaPlayer), some data manipulation (such as changing song start or beat time positions with dragging corresponding vertical lines), etc. It is used in deep conjunction with other PySide2 GUI elements, and the [smelling-but-working] codebase contains some platform-dependent (macOS/Windows) Qt5 libraries' issues workarounds, so providing clean excerpts would be quite challenging as well. Moreover, I doubt that this makes sense, because this error traceback doesn't point to any line of my code. So, all I get each time it occurs looks like:
"Traceback (most recent call last):
File "/{MyAppFolder}/venv/lib/python3.9/site-packages/pyqtgraph/GraphicsScene/GraphicsScene.py", line 225, in mouseReleaseEvent
self.clickEvents.remove(cev[0])
ValueError: list.remove(x): x not in list" and that's it.
It is not very easily reproducible, and doesn't drastically affect the overall performance when occurred, so the error message might seem more annoying than the error itself :-).

Nevertheless, I will try to give you a typical scenario.

Here is the short screencast of my app:

In the left bottom part, you can see the dockable window (“Audio panel”) with PlotWidget inside. The waveform is drawn with the PlotItem. The red cursor showing current playback position, and the grey labeled solid/dashed lines representing music bars/beats are corresponding subclass objects of InfiniteLine. The method chain causing the playback position change is triggered with sigMouseClicked
signal of the GraphicsScene of the waveform curve PlotItem object or with sigClicked signal of a BeatLine (InfiniteLine subclass) correspondingly. Initially, the mouse click signal was emitted only with the waveform curve, and I seemed not to get this error. But the problem was that clicking the BeatLines produced mouseClickEvents with incorrect x positions, returning values lesser than 1 instead of actual x-points. So, I had to filter them out, and to add sigClicked signals for BeatLines to make it work. The downside of that was that sometimes, especially with high zoom levels, both mouseClickEvent with valid x-position from waveform and from beat line were produced. And in some very high-zoom cases where multiple beat lines were too close to each other, I got more than 1 signal from different beat lines simultaneously because of collision. In order to avoid unwanted playback position change method calls from a single click, I used blocking flag variables, and measurement of time between method calls. I don't know if this is important or not here, though. The most common scenario when the ValueError happens to occur is when I try to click on the PlotWidget as close to a beat line as possible while resizing the view with my mouse wheel. In other cases I can hardly, if ever, reproduce it.

Sorry for not being able to provide a meaningful excerpt from my code, and for being verbose. Please, let me know if you need any additional info from me. I hope this could help.

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jan 19, 2023

Hi @Gdalik,

I too have dabbled in audio processing https://github.com/j9ac9k/barney Although I never had great luck w/ the lower level QMedia objects, so I had to resort to soundfile and sounddevice libraries for reading in and playback.

But to this PR, here's the relevant chunk of code.

cev = [e for e in self.clickEvents if e.button() == ev.button()]
if cev:
    if self.sendClickEvent(cev[0]):
        ev.accept()
    self.clickEvents.remove(cev[0])

We construct the list cev using a list comprehension, where we filter down the list of self.clickEvents based on the event button matching. If the list is not empty, we send the first element of the cev list to sendClickEvent method. I'm not seeing anything in sendClickEvent that would remove or discard the event, it returns a boolean of cev[0].isAccepted().

the next thing that happens is we remove that element from the list of clickEvents. I don't see how cev[0] could suddenly not be in that list, when it was just a few lines earlier. As it's being stored in a list, the garbage collection shouldn't be cleaning it up.

If you're able to reproduce the error in your own closed application, I would suggest inserting some print statements before/after the call to if self.sendClickEvent(cev[0]) , something like

cev = [e for e in self.clickEvents if e.button() == ev.button()]
if cev:
    print(f"Before self.sendClickEvent call event in self.clickEvents: {cev[0] in self.clickEvents}")
    if self.sendClickEvent(cev[0]):
        ev.accept()
    print(f"After self.sendClickEvent call event in self.clickEvents: {cev[0] in self.clickEvents}")
    self.clickEvents.remove(cev[0])

If that event is no longer in clickEvents after the call to sendClickEvent then try and insert some print statements in self.sendClickEvent to try and figure out which if/else code-branch is being executed.

I'm not saying there isn't a bug; there very well may be one, but I'm very nervous about trying to cover it up the way this PR attempts to do so.

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 19, 2023

Hi @j9ac9k,

Thanks for getting back and for sharing your audio processing project. I haven't launched it yet, but I have looked at it briefly, and I have already found some interesting and inspiring things for myself as an aspiring coding hobbyist with musical/audio engineering background.

As for the current PR, I have followed your suggestion. And here is the sample of the console output I get when I manage to reproduce the error:

Before self.sendClickEvent call event in self.clickEvents: True
After self.sendClickEvent call event in self.clickEvents: True
Before self.sendClickEvent call event in self.clickEvents: True
After self.sendClickEvent call event in self.clickEvents: True
Before self.sendClickEvent call event in self.clickEvents: True
Before self.sendClickEvent call event in self.clickEvents: True
After self.sendClickEvent call event in self.clickEvents: True
Traceback (most recent call last):
File "/Users/gdaliymac/Desktop/JazzABC_Slide_Studio/venv/lib/python3.9/site-packages/pyqtgraph/GraphicsScene/GraphicsScene.py", line 226, in mouseReleaseEvent
self.clickEvents.remove(cev[0])
ValueError: list.remove(x): x not in list
After self.sendClickEvent call event in self.clickEvents: False

I will try to test the sendClickEvent with the print statements and will get back to you.

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 19, 2023

@j9ac9k , here is the sendClickEvent code with print statements:

    def sendClickEvent(self, ev):
        ## if we are in mid-drag, click events may only go to the dragged item.
        if self.dragItem is not None and hasattr(self.dragItem, 'mouseClickEvent'):
            print("--br1- if self.dragItem is not None and hasattr(self.dragItem, 'mouseClickEvent'):")
            ev.currentItem = self.dragItem
            self.dragItem.mouseClickEvent(ev)
            
        ## otherwise, search near the cursor
        else:
            print('--br1-else-')
            if self.lastHoverEvent is not None:
                print("--br2- if self.lastHoverEvent is not None:")
                acceptedItem = self.lastHoverEvent.clickItems().get(ev.button(), None)
            else:
                print("--br2-else-")
                acceptedItem = None
            if acceptedItem is not None:
                print("--br3- if acceptedItem is not None:")
                ev.currentItem = acceptedItem
                try:
                    acceptedItem.mouseClickEvent(ev)
                except:
                    debug.printExc("Error sending click event:")
            else:
                print("--br3-else-")
                for item in self.itemsNearEvent(ev):
                    if not item.isVisible() or not item.isEnabled():
                        print("--br4- if not item.isVisible() or not item.isEnabled():")
                        continue
                    if hasattr(item, 'mouseClickEvent'):
                        print("--br5- if hasattr(item, 'mouseClickEvent'):")
                        ev.currentItem = item
                        try:
                            item.mouseClickEvent(ev)
                        except:
                            debug.printExc("Error sending click event:")
                            
                        if ev.isAccepted():
                            print("--br6- if ev.isAccepted():")
                            if item.flags() & item.GraphicsItemFlag.ItemIsFocusable:
                                print("--br7- if item.flags() & item.GraphicsItemFlag.ItemIsFocusable:")
                                item.setFocus(QtCore.Qt.FocusReason.MouseFocusReason)
                            break
        self.sigMouseClicked.emit(ev)
        return ev.isAccepted()

And the output:

Before self.sendClickEvent call event in self.clickEvents: True
--br1-else-
--br2- if self.lastHoverEvent is not None:
--br3-else-
--br5- if hasattr(item, 'mouseClickEvent'):
Before self.sendClickEvent call event in self.clickEvents: True
--br1-else-
--br2- if self.lastHoverEvent is not None:
--br3-else-
--br5- if hasattr(item, 'mouseClickEvent'):
--br5- if hasattr(item, 'mouseClickEvent'):
--br5- if hasattr(item, 'mouseClickEvent'):
--br5- if hasattr(item, 'mouseClickEvent'):
After self.sendClickEvent call event in self.clickEvents: True
--br5- if hasattr(item, 'mouseClickEvent'):
--br5- if hasattr(item, 'mouseClickEvent'):
Before self.sendClickEvent call event in self.clickEvents: True
--br1-else-
--br2- if self.lastHoverEvent is not None:
--br3-else-
--br5- if hasattr(item, 'mouseClickEvent'):
--br5- if hasattr(item, 'mouseClickEvent'):
--br5- if hasattr(item, 'mouseClickEvent'):
Traceback (most recent call last):
File "/Users/gdaliymac/Desktop/JazzABC_Slide_Studio/venv/lib/python3.9/site-packages/pyqtgraph/GraphicsScene/GraphicsScene.py", line 226, in mouseReleaseEvent
self.clickEvents.remove(cev[0])
ValueError: list.remove(x): x not in list
Traceback (most recent call last):
File "/Users/gdaliymac/Desktop/JazzABC_Slide_Studio/venv/lib/python3.9/site-packages/pyqtgraph/GraphicsScene/GraphicsScene.py", line 226, in mouseReleaseEvent
self.clickEvents.remove(cev[0])
ValueError: list.remove(x): x not in list
After self.sendClickEvent call event in self.clickEvents: False
After self.sendClickEvent call event in self.clickEvents: False
Before self.sendClickEvent call event in self.clickEvents: True
--br1-else-
--br2- if self.lastHoverEvent is not None:
--br3-else-
--br5- if hasattr(item, 'mouseClickEvent'):
--br5- if hasattr(item, 'mouseClickEvent'):
Before self.sendClickEvent call event in self.clickEvents: True
--br1-else-
--br2- if self.lastHoverEvent is not None:
--br3-else-
--br5- if hasattr(item, 'mouseClickEvent'):
--br5- if hasattr(item, 'mouseClickEvent'):
--br5- if hasattr(item, 'mouseClickEvent'):
After self.sendClickEvent call event in self.clickEvents: True
Traceback (most recent call last):
File "/Users/gdaliymac/Desktop/JazzABC_Slide_Studio/venv/lib/python3.9/site-packages/pyqtgraph/GraphicsScene/GraphicsScene.py", line 226, in mouseReleaseEvent
self.clickEvents.remove(cev[0])
ValueError: list.remove(x): x not in list
After self.sendClickEvent call event in self.clickEvents: False

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jan 20, 2023

Thanks for the debug info; looks like the sendClickEvent method is indeed removing the event from self.clickEvents.

In the sendClickEvent method, before this bit of code

Can you modify your line

                        print("--br5- if hasattr(item, 'mouseClickEvent'):")

to

print(f"--br5- {type(item)=}\t {(ev in self.click events)=}")

Thanks for this info, this is really helpful!

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 20, 2023

Hi @j9ac9k ,

Here it is:

Before self.sendClickEvent call event in self.clickEvents: True
--br1-else-
--br2- if self.lastHoverEvent is not None:
--br3-else-
--br5- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=True
--br5- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=True
After self.sendClickEvent call event in self.clickEvents: True
Before self.sendClickEvent call event in self.clickEvents: True
--br1-else-
--br2- if self.lastHoverEvent is not None:
--br3-else-
--br5- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br5- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=True
--br5- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=True
After self.sendClickEvent call event in self.clickEvents: True
Before self.sendClickEvent call event in self.clickEvents: True
--br1-else-
--br2- if self.lastHoverEvent is not None:
--br3-else-
--br5- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=True
--br5- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=True
--br5- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
After self.sendClickEvent call event in self.clickEvents: False
Before self.sendClickEvent call event in self.clickEvents: True
--br1-else-
--br2- if self.lastHoverEvent is not None:
--br3-else-
--br5- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=True
--br5- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=True
After self.sendClickEvent call event in self.clickEvents: False
Traceback (most recent call last):
File "/Users/gdaliymac/Desktop/JazzABC_Slide_Studio/venv/lib/python3.9/site-packages/pyqtgraph/GraphicsScene/GraphicsScene.py", line 226, in mouseReleaseEvent
self.clickEvents.remove(cev[0])
ValueError: list.remove(x): x not in list
Traceback (most recent call last):
File "/Users/gdaliymac/Desktop/JazzABC_Slide_Studio/venv/lib/python3.9/site-packages/pyqtgraph/GraphicsScene/GraphicsScene.py", line 226, in mouseReleaseEvent
self.clickEvents.remove(cev[0])
ValueError: list.remove(x): x not in list

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jan 20, 2023

Thanks @Gdalik It's getting narrowed down I think.

Can you add

print(f"--br6- {type(item)=}\t {(ev in self.click events)=}")

just before

                        if ev.isAccepted():

Also, can you replace

self.sigMouseClicked.emit(ev)

with

print(f"--br7- Before sigMouseClicked.emit {(ev in self.click events)=}")
self.sigMouseClicked.emit(ev)
print(f"--br8- After sigMouseClicked.emit {(ev in self.click events)=}")

Thanks!

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 20, 2023

My pleasure to help you, @j9ac9k !
A couple of excerpts.

Before self.sendClickEvent call event in self.clickEvents: True
--br1-else-
--br2- if self.lastHoverEvent is not None:
--br3-else-
--br5- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=True
--br6- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=True
--br5- type(item)=<class 'GUI.WinContr.AudioPanel.Cursor'> (ev in self.clickEvents)=True
--br6- type(item)=<class 'GUI.WinContr.AudioPanel.Cursor'> (ev in self.clickEvents)=True
--br5- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=True
--br6- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=True
--br7- Before sigMouseClicked.emit (ev in self.clickEvents)=True
--br8- After sigMouseClicked.emit (ev in self.clickEvents)=True
After self.sendClickEvent call event in self.clickEvents: True
--br8- After sigMouseClicked.emit (ev in self.clickEvents)=False
After self.sendClickEvent call event in self.clickEvents: False
Traceback (most recent call last):
File "/Users/gdaliymac/Desktop/JazzABC_Slide_Studio/venv/lib/python3.9/site-packages/pyqtgraph/GraphicsScene/GraphicsScene.py", line 226, in mouseReleaseEvent
self.clickEvents.remove(cev[0])
ValueError: list.remove(x): x not in list

[...]

Before self.sendClickEvent call event in self.clickEvents: True
--br1-else-
--br2- if self.lastHoverEvent is not None:
--br3-else-
--br5- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br6- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br5- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=True
--br6- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=True
--br5- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=True
--br6- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=True
--br5- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br6- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br5- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br6- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br7- Before sigMouseClicked.emit (ev in self.clickEvents)=True
--br8- After sigMouseClicked.emit (ev in self.clickEvents)=True
After self.sendClickEvent call event in self.clickEvents: True
--br6- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=False
--br5- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=False
--br6- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=False
--br5- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=False
--br6- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=False
--br5- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=False
--br6- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=False
--br5- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=False
--br6- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=False
--br7- Before sigMouseClicked.emit (ev in self.clickEvents)=False
--br8- After sigMouseClicked.emit (ev in self.clickEvents)=False
After self.sendClickEvent call event in self.clickEvents: False
--br6- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=False
--br7- Before sigMouseClicked.emit (ev in self.clickEvents)=False
--br8- After sigMouseClicked.emit (ev in self.clickEvents)=False
After self.sendClickEvent call event in self.clickEvents: False
Traceback (most recent call last):
File "/Users/gdaliymac/Desktop/JazzABC_Slide_Studio/venv/lib/python3.9/site-packages/pyqtgraph/GraphicsScene/GraphicsScene.py", line 226, in mouseReleaseEvent
self.clickEvents.remove(cev[0])
ValueError: list.remove(x): x not in list
Traceback (most recent call last):
File "/Users/gdaliymac/Desktop/JazzABC_Slide_Studio/venv/lib/python3.9/site-packages/pyqtgraph/GraphicsScene/GraphicsScene.py", line 226, in mouseReleaseEvent
self.clickEvents.remove(cev[0])
ValueError: list.remove(x): x not in list
Traceback (most recent call last):
File "/Users/gdaliymac/Desktop/JazzABC_Slide_Studio/venv/lib/python3.9/site-packages/pyqtgraph/GraphicsScene/GraphicsScene.py", line 226, in mouseReleaseEvent
self.clickEvents.remove(cev[0])
ValueError: list.remove(x): x not in list
Traceback (most recent call last):
File "/Users/gdaliymac/Desktop/JazzABC_Slide_Studio/venv/lib/python3.9/site-packages/pyqtgraph/GraphicsScene/GraphicsScene.py", line 226, in mouseReleaseEvent
self.clickEvents.remove(cev[0])
ValueError: list.remove(x): x not in list

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jan 20, 2023

Hmm...

do you have sigMouseClicked signal connected to anything in your codebase? If so, can you share the function/method that that signal connects to?

It's hard to read from just the print output, but the duplicate output here is a little concerning...

--br7- Before sigMouseClicked.emit (ev in self.clickEvents)=True
--br8- After sigMouseClicked.emit (ev in self.clickEvents)=True
After self.sendClickEvent call event in self.clickEvents: True
--br8- After sigMouseClicked.emit (ev in self.clickEvents)=False
After self.sendClickEvent call event in self.clickEvents: False

I don't understand why br7 is only being printed once but br8 is printed twice...

oh, lastly, I would wrap the self.clickEvents.remove(cev[0]) in a try/except, and try and probe more information about the item/event that's triggering this issue

if cev:
    e = cev[0]
    if self.sendClickEvent(e)
        ev.accept()
try:
    self.clickEvents.remove(e)
except ValueError:
    print("Have Value Error")
    print("===========")
    print(f"{type(e.currentItem)=}")
    print(f"{e.isAccepted()=}")

Thanks again for your debug efforts here!

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 20, 2023

@j9ac9k , I will split my answer into some parts.

First, the current code after all the modifications, just to make sure we are on the same page.

    def sendClickEvent(self, ev):
        ## if we are in mid-drag, click events may only go to the dragged item.
        if self.dragItem is not None and hasattr(self.dragItem, 'mouseClickEvent'):
            print("--br1- if self.dragItem is not None and hasattr(self.dragItem, 'mouseClickEvent'):")
            ev.currentItem = self.dragItem
            self.dragItem.mouseClickEvent(ev)
            
        ## otherwise, search near the cursor
        else:
            print('--br1-else-')
            if self.lastHoverEvent is not None:
                print("--br2- if self.lastHoverEvent is not None:")
                acceptedItem = self.lastHoverEvent.clickItems().get(ev.button(), None)
            else:
                print("--br2-else-")
                acceptedItem = None
            if acceptedItem is not None:
                print("--br3- if acceptedItem is not None:")
                ev.currentItem = acceptedItem
                try:
                    acceptedItem.mouseClickEvent(ev)
                except:
                    debug.printExc("Error sending click event:")
            else:
                print("--br3-else-")
                for item in self.itemsNearEvent(ev):
                    if not item.isVisible() or not item.isEnabled():
                        print("--br4- if not item.isVisible() or not item.isEnabled():")
                        continue
                    if hasattr(item, 'mouseClickEvent'):
                        print(f"--br5- {type(item)=}\t {(ev in self.clickEvents)=}")
                        ev.currentItem = item
                        try:
                            item.mouseClickEvent(ev)
                        except:
                            debug.printExc("Error sending click event:")
                        print(f"--br6- {type(item)=}\t {(ev in self.clickEvents)=}")
                        if ev.isAccepted():
                            print("--br6- if ev.isAccepted():")
                            if item.flags() & item.GraphicsItemFlag.ItemIsFocusable:
                                print("--br7- if item.flags() & item.GraphicsItemFlag.ItemIsFocusable:")
                                item.setFocus(QtCore.Qt.FocusReason.MouseFocusReason)
                            break
        print(f"--br7- Before sigMouseClicked.emit {(ev in self.clickEvents)=}")
        self.sigMouseClicked.emit(ev)
        print(f"--br8- After sigMouseClicked.emit {(ev in self.clickEvents)=}")
        return ev.isAccepted()
    def mouseReleaseEvent(self, ev):
        if self.mouseGrabberItem() is None:
            if ev.button() in self.dragButtons:
                if self.sendDragEvent(ev, final=True):
                    #print "sent drag event"
                    ev.accept()
                self.dragButtons.remove(ev.button())
            else:
                cev = [e for e in self.clickEvents if e.button() == ev.button()]
                if cev:
                    e = cev[0]
                    if self.sendClickEvent(e):
                        ev.accept()
                try:
                    self.clickEvents.remove(e)
                except ValueError:
                    print("Have Value Error")
                    print("===========")
                    print(f"{type(e.currentItem)=}")
                    print(f"{e.isAccepted()=}")
        # [...]

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jan 20, 2023

that looks right to me 👍🏻

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 20, 2023

Second, the output. I don't know why GitHub formats the text like this ((.

> --br1-else-
> --br2- if self.lastHoverEvent is not None:
> --br3-else-
> --br5- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'>	 (ev in self.clickEvents)=True
> --br6- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'>	 (ev in self.clickEvents)=False
> --br5- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'>	 (ev in self.clickEvents)=False
> --br6- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'>	 (ev in self.clickEvents)=False
> --br5- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'>	 (ev in self.clickEvents)=False
> --br6- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'>	 (ev in self.clickEvents)=False
> --br5- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'>	 (ev in self.clickEvents)=False
> --br6- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'>	 (ev in self.clickEvents)=False
> --br7- Before sigMouseClicked.emit (ev in self.clickEvents)=False
> --br8- After sigMouseClicked.emit (ev in self.clickEvents)=False
> Have Value Error
> ===========
> type(e.currentItem)=<class 'GUI.WinContr.AudioPanel.BeatLine'>
> e.isAccepted()=False
> --br1-else-
> --br2- if self.lastHoverEvent is not None:
> --br3-else-
> --br5- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'>	 (ev in self.clickEvents)=True
> --br6- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'>	 (ev in self.clickEvents)=True
> --br5- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'>	 (ev in self.clickEvents)=True
> --br6- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'>	 (ev in self.clickEvents)=True
> --br7- Before sigMouseClicked.emit (ev in self.clickEvents)=True
> --br1-else-
> --br2- if self.lastHoverEvent is not None:
> --br3-else-
> --br5- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'>	 (ev in self.clickEvents)=True
> --br6- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'>	 (ev in self.clickEvents)=True
> --br5- type(item)=<class 'GUI.WinContr.AudioPanel.Cursor'>	 (ev in self.clickEvents)=True
> --br6- type(item)=<class 'GUI.WinContr.AudioPanel.Cursor'>	 (ev in self.clickEvents)=True
> --br5- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'>	 (ev in self.clickEvents)=True
> --br6- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'>	 (ev in self.clickEvents)=True
> --br7- Before sigMouseClicked.emit (ev in self.clickEvents)=True
> --br8- After sigMouseClicked.emit (ev in self.clickEvents)=True
> --br8- After sigMouseClicked.emit (ev in self.clickEvents)=False
> Have Value Error
> ===========
> type(e.currentItem)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'>
> e.isAccepted()=False
> --br1-else-
> --br2- if self.lastHoverEvent is not None:
> --br3-else-
> --br5- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'>	 (ev in self.clickEvents)=True
> --br6- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'>	 (ev in self.clickEvents)=True
> --br5- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'>	 (ev in self.clickEvents)=True
> --br6- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'>	 (ev in self.clickEvents)=True
> --br7- Before sigMouseClicked.emit (ev in self.clickEvents)=True
> --br1-else-
> --br2- if self.lastHoverEvent is not None:
> --br3-else-
> --br5- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'>	 (ev in self.clickEvents)=True
> --br6- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'>	 (ev in self.clickEvents)=True
> --br5- type(item)=<class 'GUI.WinContr.AudioPanel.Cursor'>	 (ev in self.clickEvents)=True
> --br6- type(item)=<class 'GUI.WinContr.AudioPanel.Cursor'>	 (ev in self.clickEvents)=True
> --br5- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'>	 (ev in self.clickEvents)=True
> --br6- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'>	 (ev in self.clickEvents)=True
> --br7- Before sigMouseClicked.emit (ev in self.clickEvents)=True
> --br8- After sigMouseClicked.emit (ev in self.clickEvents)=True
> --br8- After sigMouseClicked.emit (ev in self.clickEvents)=False
> Have Value Error
> ===========
> type(e.currentItem)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'>
> e.isAccepted()=False
> Traceback (most recent call last):
>   File "/Users/gdaliymac/Desktop/JazzABC_Slide_Studio/venv/lib/python3.9/site-packages/pyqtgraph/GraphicsScene/GraphicsScene.py", line 226, in mouseReleaseEvent
>     self.clickEvents.remove(e)
> UnboundLocalError: local variable 'e' referenced before assignment

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jan 21, 2023

hmm....some interesting output:

> --br5- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'>	 (ev in self.clickEvents)=True
> --br6- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'>	 (ev in self.clickEvents)=False

also

> --br7- Before sigMouseClicked.emit (ev in self.clickEvents)=True
> --br8- After sigMouseClicked.emit (ev in self.clickEvents)=True
> --br8- After sigMouseClicked.emit (ev in self.clickEvents)=False

why is br8 printing multiple times 🤔 ...this happens twice, with viewboxes

> Traceback (most recent call last):
>   File "/Users/gdaliymac/Desktop/JazzABC_Slide_Studio/venv/lib/python3.9/site-packages/pyqtgraph/GraphicsScene/GraphicsScene.py", line 226, in mouseReleaseEvent
>     self.clickEvents.remove(e)
> UnboundLocalError: local variable 'e' referenced before assignment

This is because that try/except block needs to be indented over once more...so unrelated

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 21, 2023

Now, as for my code. As I have mentioned before, I have sigMouseClicked signal connected to both the PlotItem (waveform curve), and the sigClicked connected to BeatLines / InfiniteLines (all of them). This causes some issues, especially when I have a lot of these lines, and they are zoomed close to each other. I would be happy to leave only the sigMouseClicked connected to the PlotItem. But in this case I get the wrong mouse x positions with ev.pos()[0] (lesser than 1, sometimes negative) or no signal at all, when I click on the BeatLines. I don't know if this is a bug or a feature of pyqtgraph, btw. But this produces several signals simultaneously, and I have to work around this issue with timer and blocking flag variables to avoid bad performance (I do this inside the self.cursor_action method further, but it doesn't do anything with click events or click signals) . Maybe this causes the 'out of sync' issues with creation/deletion of clickEvents, including the ValueError. But I don't know how to make it work in a more correct way with the issues described above.

Now, some excerpts from my code which could be important from my point of view:

class AudioPanel:
    def __init__(self, mw):
        self.mw = mw   # MainWindow
        #  [...]
        self.WaveFormArea = mw.WaveForm   # the PlotWidget
        self.curve = self.WaveFormArea.plot(self.audiofile)
        self.GScene = self.curve.scene()
        self.GScene.sigMouseClicked.connect(self.on_mouseClicked)
        #  [...]
        
    def on_mouseClicked(self, ev):
        if ev.button() != Qt.MouseButton.LeftButton:
            return
        mouse_x = ev.pos()[0]
        if mouse_x < 1:
            return
        self.cursor_action(round(self.point_to_value(mouse_x)))

    #   [...]

class BeatLine(pg.InfiniteLine):
    #   [...]
    def draw(self):
        self.parent.WaveFormArea.addItem(self)
        self.sigClicked.connect(self.on_BL_clicked)
        #   [...]
        
    def on_BL_clicked(self, obj, ev):
        if ev.button() != Qt.MouseButton.LeftButton:
            return
        pos = round(obj.pos().x())
        self.parent.cursor_action(pos, FromBL=True)

Please, let me know if you need more.

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 21, 2023

This is because that try/except block needs to be indented over once more...so unrelated

Ah, sure, it's my fault here. Do you need more console output with the right try/except indent?

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 21, 2023

I don't know if that matters, but I also have the sigMouseHoversignal connected to self.GScene. And the method connected to it emits my self-made sigGSMousePressed or sigGSMouseReleased signals, depending on the left mouse button state.

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jan 21, 2023

Don't worry about the try/except indent, go ahead and fix it in case we want to do more runs later.

This causes some issues, especially when I have a lot of these lines, and they are zoomed close to each other. I would be happy to leave only the sigMouseClicked connected to the PlotItem. But in this case I get the wrong mouse x positions with ev.pos()[0] (lesser than 1, sometimes negative) or no signal at all, when I click on the BeatLines. I don't know if this is a bug or a feature of pyqtgraph, btw.

So, there are different coordinates, and you may need to use one of the map functions to get the correct coordinates; one thing you could try

position = viewbox.mapSceneToView(ev.scenePos())

Let me know if that gives you the "correct" coordinates. One of the most confusing aspects of working with the graphics view framework is knowing what coordinate system your coordinates are in...there are a ton of mapping functions and it can involve a fair amount of trial and error before you get it right.

Try this out and see if you can avoid triggering this issue.

The code you shared looks fine, but the bit about multiple signals emitting seems somewhat problematic.

Perhaps a compromise in this PR would be to do something like...

Replace

                if cev:
                    if self.sendClickEvent(cev[0]):
                        #print "sent click event"
                        ev.accept()
                    self.clickEvents.remove(cev[0])

with

if cev:
    e = self.clickEvents.pop(0)
    if self.sendClickEvent(e):
       ev.accept()

This change changes the order, but should ensure all the relevant operations occur.

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 21, 2023

if cev:
    e = self.clickEvents.pop(0)
    if self.sendClickEvent(e):
       ev.accept()

Cool! The ValueError seems to be gone, and the console output looks much more ordered!

And thanks a lot for your hint. I have modified my on_mouseClicked method, according to it. Now it looks like this:

    def on_mouseClicked(self, ev):
        if ev.button() != Qt.MouseButton.LeftButton:
            return
        mouse_x = self.ViewBox.mapSceneToView(ev.scenePos()).toPoint().x()
        self.cursor_action(mouse_x)

I have removed the sigClicked from the BeatLines with the corresponding method. And now it works like a charm without those terrible workarounds in my self.cursor_action method, with speed improvements as well, because I have got rid of timer conditions.

And I seem not to need the method which converted mouse positions to scene positions with formula self.viewRange[0] + self.viewLength * pointX/self.WaveFormArea.width(), because I get them right from scenePos() method.

Thank you so much!

@Gdalik Gdalik closed this Jan 21, 2023
@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jan 21, 2023

First thing first, was using mouse_x = self.ViewBox.mapSceneToView(ev.scenePos()).toPoint().x() resulting in doing away with your other band-aids enough to do away with the ValueError (without having to do the other re-ordering)?

Specifically, I want to make sure you're no longer getting a ValueError, without this change

if cev:
    e = self.clickEvents.pop(0)
    if self.sendClickEvent(e):
       ev.accept()

To the point of the mouse positions, there are a ton of mapping methods:

https://pyqtgraph.readthedocs.io/en/latest/api_reference/graphicsItems/viewbox.html#pyqtgraph.ViewBox.mapFromItemToView

http://doc.qt.io/qt-6/qgraphicsitem.html#mapFromItem

It's not easy to sort out which one is appropriate, but getting accurate coordinates for the mouse cursor for the view, scene, window, etc is doable, it just takes a little tinkering to get the right map methods. Given that events give access to the scenePos() I often like working from those as it's a little more obvious what the coordinate system the coordinates you get are in.

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 21, 2023

First thing first, was using mouse_x = self.ViewBox.mapSceneToView(ev.scenePos()).toPoint().x() resulting in doing away with your other band-aids enough to do away with the ValueError (without having to do the other re-ordering)?

Nope, I have rolled back the changes in GraphicsScene code, and the ValueError is back, unfortunately, despite the last tweaks in my codebase. So, I'm definitely a fan of your re-ordering solution :-).

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jan 21, 2023

That reordering solution masks the issue ... this ValueError should not be occurring; there is a bug somewhere.

With the simpler code, can you post some of that debug print statements again? Hoping that we don't have some duplicates here...

@j9ac9k j9ac9k reopened this Jan 21, 2023
@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 21, 2023

Ok, what print statements should I leave?

there is a bug somewhere.

Can it be at lower (e.g., Qt5) level?

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jan 21, 2023

Ok, what print statements should I leave?

there is a bug somewhere.

Can it be at lower (e.g., Qt5) level?

Yeah, but probably a bug in our library :)

Leave the print statements in the sendClickEvent method, specifically the ones around the line item.mouseClickEvent(ev) and around self.sigMouseClicked.emit(ev)

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jan 21, 2023

Even better would be if you can provide a reproducible example that I can debug myself, perhaps that is possible now that your code is potentially simplified.

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 21, 2023

Even better would be if you can provide a reproducible example that I can debug myself, perhaps that is possible now that your code is potentially simplified.

I'm sorry, but I would have to write another test app to make it work :-). This is not a small/simple app, there are too many dependencies, cross-dependencies, etc., and I haven't strictly followed the MVC pattern, or something like this.

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 21, 2023

I have changed the enumeration a little bit, so that it would better follow the branches.
Now the code looks like this:

    def sendClickEvent(self, ev):
        ## if we are in mid-drag, click events may only go to the dragged item.
        if self.dragItem is not None and hasattr(self.dragItem, 'mouseClickEvent'):
            print("--br1- if self.dragItem is not None and hasattr(self.dragItem, 'mouseClickEvent'):")
            ev.currentItem = self.dragItem
            self.dragItem.mouseClickEvent(ev)
            
        ## otherwise, search near the cursor
        else:
            print('--br1-else-')
            if self.lastHoverEvent is not None:
                print("--br2- if self.lastHoverEvent is not None:")
                acceptedItem = self.lastHoverEvent.clickItems().get(ev.button(), None)
            else:
                print("--br2-else-")
                acceptedItem = None
            if acceptedItem is not None:
                print("--br3- if acceptedItem is not None:")
                ev.currentItem = acceptedItem
                try:
                    acceptedItem.mouseClickEvent(ev)
                except:
                    debug.printExc("Error sending click event:")
            else:
                print("--br3-else-")
                for item in self.itemsNearEvent(ev):
                    if not item.isVisible() or not item.isEnabled():
                        print("--br4- if not item.isVisible() or not item.isEnabled():")
                        continue
                    if hasattr(item, 'mouseClickEvent'):
                        print(f"--br5.1- {type(item)=}\t {(ev in self.clickEvents)=}")
                        ev.currentItem = item
                        try:
                            item.mouseClickEvent(ev)
                        except:
                            debug.printExc("Error sending click event:")
                        print(f"--br5.2- {type(item)=}\t {(ev in self.clickEvents)=}")
                        if ev.isAccepted():
                            print("--br7- if ev.isAccepted():")
                            if item.flags() & item.GraphicsItemFlag.ItemIsFocusable:
                                print("--br8- if item.flags() & item.GraphicsItemFlag.ItemIsFocusable:")
                                item.setFocus(QtCore.Qt.FocusReason.MouseFocusReason)
                            break
        print(f"--br9.1- Before sigMouseClicked.emit {(ev in self.clickEvents)=}")
        self.sigMouseClicked.emit(ev)
        print(f"--br9.2- After sigMouseClicked.emit {(ev in self.clickEvents)=}")
        return ev.isAccepted()

--br1-else-
--br2- if self.lastHoverEvent is not None:
--br3-else-
--br5.1- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=True
--br5.2- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=True
--br5.1- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=True
--br5.2- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=True
--br5.1- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br5.2- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br9.1- Before sigMouseClicked.emit (ev in self.clickEvents)=True
--br1-else-
--br2- if self.lastHoverEvent is not None:
--br3-else-
--br5.1- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=True
--br5.2- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=True
--br5.1- type(item)=<class 'GUI.WinContr.AudioPanel.Cursor'> (ev in self.clickEvents)=True
--br5.2- type(item)=<class 'GUI.WinContr.AudioPanel.Cursor'> (ev in self.clickEvents)=True
--br5.1- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=True
--br5.2- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=True
--br5.1- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br5.2- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br9.1- Before sigMouseClicked.emit (ev in self.clickEvents)=True
--br9.2- After sigMouseClicked.emit (ev in self.clickEvents)=True
--br1-else-
--br2- if self.lastHoverEvent is not None:
--br3-else-
--br5.1- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=True
--br5.2- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=True
--br5.1- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=True
--br5.2- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=True
--br5.1- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br5.2- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br9.1- Before sigMouseClicked.emit (ev in self.clickEvents)=True
--br9.2- After sigMouseClicked.emit (ev in self.clickEvents)=False
--br9.2- After sigMouseClicked.emit (ev in self.clickEvents)=False
Traceback (most recent call last):
File "/Users/gdaliymac/Desktop/JazzABC_Slide_Studio/venv/lib/python3.9/site-packages/pyqtgraph/GraphicsScene/GraphicsScene.py", line 224, in mouseReleaseEvent
self.clickEvents.remove(cev[0])
ValueError: list.remove(x): x not in list
Traceback (most recent call last):
File "/Users/gdaliymac/Desktop/JazzABC_Slide_Studio/venv/lib/python3.9/site-packages/pyqtgraph/GraphicsScene/GraphicsScene.py", line 224, in mouseReleaseEvent
self.clickEvents.remove(cev[0])
ValueError: list.remove(x): x not in list

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jan 21, 2023

Crud, this time the issue occurs when the sigMouseClicked signal is fired... so whatever the issue is; it has to do with whatever slot that signal is connected to.

In an earlier post you said you had sigMouseClicked connected to a PlotItem? Can you share the function/method it's connected to?

Also wherever you connect `sigMouseClicked, can you add the following argument?

sigMouseClicked.connect(..., type=pg.Qt.ConnectionType.UniqueConnection)

This will raise an exception if you connect the same signal to the same slot more than once I believe.

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 21, 2023

Now I have only self.GScene.sigMouseClicked.connect(self.on_mouseClicked) , and that's it.
And I have already posted the on_mouseClicked method before:

def on_mouseClicked(self, ev):
    if ev.button() != Qt.MouseButton.LeftButton:
        return
    mouse_x = self.ViewBox.mapSceneToView(ev.scenePos()).toPoint().x()
    self.cursor_action(mouse_x)

self.GScene.sigMouseClicked.connect(self.on_mouseClicked, type=pg.Qt.ConnectionType.UniqueConnection) raises

AttributeError: module 'pyqtgraph.Qt' has no attribute 'ConnectionType'

PyCharm doesn't see the reference to 'ConnectionType' as well, even before running the app.

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 21, 2023

Here is the link to the whole module if that helps.

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jan 21, 2023

That will teach me to go off of memory; I think it should be:

type=pg.Qt.QtCore.Qt.ConnectionType.UniqueConnection

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 21, 2023

Still no luck. I have the latest 0.13.1 version installed.

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 21, 2023

I have PySide2, not PyQt5, does that matter?

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jan 21, 2023

I have PySide2, not PyQt5, does that matter?

As long as it's a recent version of pyside2 it should not; and we only support 5.15+ so ... would have to be a recent version...

Mind double-checking, I just did this on the plotting example, and it worked w/o issue for me.

image

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 21, 2023

Oh, sorry. I have missed Qt after QtCore... Now it has launched

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jan 21, 2023

Oh, sorry. I have missed Qt after QtCore... Now it has launched

Cool, with specifying the unique connection, do you still trigger the ValueError? if so, can you show the console output?

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 21, 2023

--br1-else-
--br2- if self.lastHoverEvent is not None:
--br3-else-
--br5.1- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=True
--br5.2- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=True
--br5.1- type(item)=<class 'GUI.WinContr.AudioPanel.Cursor'> (ev in self.clickEvents)=True
--br5.2- type(item)=<class 'GUI.WinContr.AudioPanel.Cursor'> (ev in self.clickEvents)=True
--br5.1- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=True
--br5.2- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=True
--br9.1- Before sigMouseClicked.emit (ev in self.clickEvents)=True
--br9.2- After sigMouseClicked.emit (ev in self.clickEvents)=True
Traceback (most recent call last):
File "/Users/gdaliymac/Desktop/JazzABC_Slide_Studio/venv/lib/python3.9/site-packages/pyqtgraph/GraphicsScene/GraphicsScene.py", line 224, in mouseReleaseEvent
self.clickEvents.remove(cev[0])
ValueError: list.remove(x): x not in list
--br9.2- After sigMouseClicked.emit (ev in self.clickEvents)=False

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jan 21, 2023

(╯°□°)╯︵ ┻━┻

Having one line for br9.1 but two lines for br9.2 is a signal for sure here, I would imagine it would be due to that signal being connected to multiple places, which I suppose UniqueConnection wouldn't catch.

Can you comment out self.sigMouseClicked.emit(ev) (which means obviously your application's mouse events won't work) and try and trigger the same issue?

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 21, 2023

Hi @j9ac9k ,

After commenting out self.sigMouseClicked.emit(ev) I couldn't trigger the same issue.

The output pattern looks quite repetitive, like:

--br1-else-
--br2- if self.lastHoverEvent is not None:
--br3-else-
--br5.1- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br5.2- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br5.1- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br5.2- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br5.1- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=True
--br5.2- type(item)=<class 'pyqtgraph.graphicsItems.PlotCurveItem.PlotCurveItem'> (ev in self.clickEvents)=True
--br5.1- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=True
--br5.2- type(item)=<class 'pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox'> (ev in self.clickEvents)=True
--br5.1- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br5.2- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br5.1- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br5.2- type(item)=<class 'GUI.WinContr.AudioPanel.BeatLine'> (ev in self.clickEvents)=True
--br9.1- Before sigMouseClicked.emit (ev in self.clickEvents)=True
--br9.2- After sigMouseClicked.emit (ev in self.clickEvents)=True

And I haven't noticed any double 9.2.
But as for my code, I have made sure with PyCharm's search tools sigMouseClicked appears only once when the AudioPanel is initialized. And the __init__ method cannot be called twice as well without raising errors.

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 21, 2023

After dabbling for a while, I have found out that these issues are correlated with the number of BeatLines. When there are no/few of them, it's hard to reproduce these errors and those duplications of 9.2. But when there are hundreds/thousands of them, everything noticeably slows down, and the issues occur. The more lines, the more issues. Though, the sigClicked is not connected to them anymore. And this is not connected with any other signals connected to them. Disconnecting the sigPositionChanged and sigPositionChangeFinished from them makes no difference. You can see the full code of the BeatLine class from the link I provided earlier.

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 21, 2023

@j9ac9k , I seem to have found the real source of the issue.
In the method update_pos which updates the Cursor position, I have commented out the QApplication.processEvents() , and I couldn't reproduce the error after that:

class Cursor(pg.InfiniteLine):
    def __init__(self, parent, *args, **kwargs):
        super(Cursor, self).__init__(*args, **kwargs)
        self.setAngle(90)
        self.hide()
        self.setPos(0)
        self.parent = parent

    def update_pos(self, ms):
        self.setPos(ms2samp(ms))
        if self.parent.mw.EBC is not None:
            self.parent.mw.EBC.upd_view(ms)
        if self.parent.curve is not None:
            self.make_visible_in_range()
        #QApplication.processEvents()

    def make_visible_in_range(self):
        if self.parent.viewRange[0] > self.pos().x() or self.parent.viewRange[1] < self.pos().x():
            self.parent.ViewBox.setXRange(self.pos().x(), self.pos().x() + self.parent.viewLength, padding=padding)
            QApplication.processEvents()

    def show(self):
        self.setPen(style=Qt.SolidLine, color='r', width=2)

    def hide(self):
        self.setPen(style=Qt.NoPen)

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jan 21, 2023

Nice troubleshooting!

Generally speaking, QApplication.processEvents() calls are not needed. We use them in some examples when we want to do testing of some benchmarks and in test code where we want to try and guarantee (1) a screen redraw. I've found processEvents calls to be problematic in user/application code, so I would certainly advise avoiding them whenever possible. they can cause code to run in an unexpected order which Is likely the source of the issue here.

  1. does not actually guarantee a screen redraw but it's the best we can do.

Now that we've gotten to the bottom of this, I think we can try/except and emit a warning.

Something along the lines of

if cev:
    if self.sendClickEvent(cev[0]):
        ev.accept()
    try:
        self.clickEvents.remove(cev[0])
    except ValueError:
        warnings.warn(
            ("A ValueError can occur here with errant QApplication.processEvent() calls, see "
            "https://github.com/pyqtgraph/pyqtgraph/pull/2580 for more discussion."),
            RuntimeWarning,
            stacklevel=2
        )

@Gdalik
Copy link
Copy Markdown
Author

Gdalik commented Jan 21, 2023

Thank you very much for your time, efforts and expertise!

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.

2 participants