Wait for animation to finish - Python - python

I am trying to fade out some objects in pyside6, I managed to do it, but after the animation I need to change index of Stacked Widget to go to another page. This simulates transition from page to page.
def transition_in(self):
effect1 = QGraphicsOpacityEffect(self.title)
effect2 = QGraphicsOpacityEffect(self.reminder_btn)
#
self.title.setGraphicsEffect(effect1)
self.reminder_btn.setGraphicsEffect(effect2)
#
self.anim_1 = QPropertyAnimation(effect1, b"opacity")
self.anim_2 = QPropertyAnimation(effect2, b"opacity")
#
self.anim_1.setStartValue(0.7)
self.anim_1.setEndValue(0)
self.anim_1.setDuration(200)
self.anim_2.setStartValue(0.7)
self.anim_2.setEndValue(0)
self.anim_2.setDuration(200)
#
self.anim_group = QParallelAnimationGroup()
self.anim_group.addAnimation(self.anim_1)
self.anim_group.addAnimation(self.anim_2)
self.anim_group.start()
The problem is that when trying without changing index of stacked widget, animation works, but when calling setCurrentIndex in stacked widget, it instantly goes to the next page. I tried sleep, but it just freezes the program for some time. calling it in a thread does not work either.

Related

How can I use async Python to allow Jupyter button widget clicks to process while running an async recursive function?

I set up a Jupyter Notebook where I'm running a recursive function that clears the output like so, creating an animated output:
from IPython.display import clear_output
active = True
def animate:
myOutput = ""
# do stuff
clear_output(wait=True)
print(myOutput)
sleep(0.2)
if active:
animate()
and that's working perfectly.
But now I want to add in one more step: A speed toggle. What I'm animating is a debugging visualization of a cursor moving through interpreted code as an interpreter I'm writing parses that code. I tried conditional slow-downs to have more time to read what's going on as the parsing continues, but what I really need is to be able to click a button to toggle the speed between fast and slow. Maybe I'll use a slider, but for now I just want a button for proof of concept.
This sounds simple enough. Note that I'm writing this statefully as a class because I need to read / write the state from within another imported class.
Jupyter block 1:
import ipywidgets as widgets
from IPython.display import display
out = widgets.Output()
class ToggleState():
def __init__(self):
self.button = widgets.Button(description="Toggle")
self.button.on_click(self.toggle)
display(self.button)
self.toggleState = False
print("Toggle State:", self.toggleState)
def toggle(self, arg): # arg has to be accepted here to meet on_click requirements
self.toggleState = not self.toggleState
print("Toggle State:", self.toggleState)
def read(self):
return self.toggleState
toggleState = ToggleState()
Then, in Jupyter block 2, note I decided to to do this in a separate block because the clear_output I'm doing with the animate func clears the button if it's in the same block, and therein lies the problem:
active = True
def animate:
myOutput = ""
# do stuff
clear_output(wait=True)
print(myOutput)
if toggleState.read():
sleep(5)
else:
sleep(0.2)
if active:
animate()
But the problem with this approach was that two blocks don't actually run at the same time (without using parallel kernels which is way more complexity than I care for) so that button can't keep receiving input in the previous block. Seems obvious now, but I didn't think about it.
How can I clear input in a way that doesn't delete my button too (so I can put the button in the animating block)?
Edit:
I thought I figured the solution, but only part of it:
Using the Output widget:
out = widgets.Output()
and
with out:
clear_output(wait=True) # clears only the logged output
# logging code
We can render to two separate stdouts within the same block. This works to an extent, as in the animation renders and the button isn't cleared. But still while the animation loop is running the button seems to be incapable of processing input. So it does seem like a synchronous code / event loop blocking problem. What's the issue here?
Do I need an alternative to sleep that frees up the event loop?
Edit 2:
After searching async code in Python, I learned about asyncio but I'm still struggling. Jupyter already runs the code via asyncio.run(), but the components obviously have to be defined as async for that to matter. I defined animate as async and tried using async sleeps, but the event loops still seems to be locked for the button.

Direct calls to function to update matplotlib figure works, calling it repeatedly from a loop only shows final result

Background: I am developing GUI for analyzing experimental imaging data. I have a viewing window (i.e., the matplotlib figure) where I overlay manually selected data points (if any) and optical values (if normalization has occurred) on top of a background image. Below the axis I have a QScrollBar that I can use to manually move to different time points in the data. I initialize it like so:
self.movie_scroll_obj.valueChanged.connect(self.update_axes)
With the relevant part of the associated function looking like the following:
def update_axes(self):
# Determine if data is prepped or unprepped
data = self.data_filt
# UPDATE THE OPTICAL IMAGE AXIS
# Clear axis for update
self.mpl_canvas.axes.cla()
# Update the UI with an image off the top of the stack
self.mpl_canvas.axes.imshow(self.data[0], cmap='gray')
# Match the matplotlib figure background color to the GUI
self.mpl_canvas.fig.patch.set_facecolor(self.bkgd_color)
# If normalized, overlay the potential values
if self.norm_flag == 1:
# Get the current value of the movie slider
sig_id = self.movie_scroll_obj.value()
# Create the transparency mask
mask = ~self.mask
thresh = self.data_filt[sig_id, :, :] > 0.3
transp = mask == thresh
transp = transp.astype(float)
# Overlay the voltage on the background image
self.mpl_canvas.axes.imshow(self.data_filt[sig_id, :, :],
alpha=transp, vmin=0, vmax=1,
cmap='jet')
# Plot the select signal points
for cnt, ind in enumerate(self.signal_coord):
if self.signal_toggle[cnt] == 0:
continue
else:
self.mpl_canvas.axes.scatter(
ind[0], ind[1], color=self.cnames[cnt])
# Tighten the border on the figure
self.mpl_canvas.fig.tight_layout()
self.mpl_canvas.draw()
The overlay of normalized data occurs within the IF statement in the above block of code. When I interact with the QScrollBar object it works exactly as intended. However, I have a play button that I want to be able to click and have the plot update like a movie from whatever the current scrollbar value is. I also want the string on the button to change to "Stop Movie" and have a subsequent click stop the movie. To accomplish this I have connected the pushbutton to the following code:
def play_toggle(self, event):
# Grab the button string
button_str = self.play_movie_button.text()
if button_str == 'Play Movie':
# Update the button string
self.play_movie_button.setText('Stop Movie')
# time.sleep(1)
# Run the play movie function
self.play_movie()
else:
# Update the play movie boolean
self.play_bool = 0
# Update the button string
self.play_movie_button.setText('Play Movie')
Which calls this function:
def play_movie(self):
# Set the play boolean to true
self.play_bool = 1
# Grab the current value of the movie scroll bar
cur_val = self.movie_scroll_obj.value()
# Grab the maximum value of the movie scroll bar
max_val = self.movie_scroll_obj.maximum()
# Being updating the scroll bar value and the movie window
# while self.play_bool == 1:
for n in np.arange(cur_val+5, cur_val+30, 5):
# Check to make sure you haven't reached the end of the signal
# if self.movie_scroll_obj.value() < max_val:
# Update the slider value
if self.play_bool == 1:
self.movie_scroll_obj.setValue(n)
plt.pause(1)
# If button is hit again, break loop
else:
break
Because I used the "valueChanged" signal for the QScrollBar, updating its value in this function calls the update_axes() function. The current iteration is the biproduct of a handful of hours scouring the Internet for solutions to the two following issues:
) The reason I have limited the np.arange call in the for loop (and that I'm using a for loop rather than a while loop) is because if I let it run its course without a built in stop point the GUI crashes and I get a "SpyderKernelApp WARNING No such comm" error.
) The figure does not update with each iteration through the loop. It only updates at the end of the short for loop I currently have implemented.
I am currently developing this using Spyder 4.1.4, Python 3.8, and Qt Designer (using Qt 5.11.1). I'll include a picture of the GUI on the off chance it helps folks orient themselves. The code is currently up on GitHub, but I would need to push my current version and fix a *.yml if anyone decided they were that invested in rescuing me. Happy to provide additional code or access to code as needed.
Image of GUI with visualized data present.:
A potential reason for the figure not updating:
def play_movie(self):
[...]
self.movie_scroll_obj.setValue(n)
This only changes the value of the move_scroll_obj. You are expecting that to call self.update_axes, since you connected it via
self.movie_scroll_obj.valueChanged.connect(self.update_axes)
However, since you are in the play_movie function, all these connect-updates are postponed until the play_movie function returns to the main event loop, i.e. until the loop finishes. Hence you only see the update at the end of function.
This is potentially also the reason for the spyder crash, as with a while loop you would indefinitely fill up the event-queue.
Directly implementing the function call into the loop should fix this behaviour:
def play_movie(self):
[...]
self.movie_scroll_obj.setValue(n)
self.update_axes()
The self.movie_scroll_obj.valueChanged.connect gets redundant at this point and should be deleted (elsewise the event-queue would still be filled up during the loop).

Force update of bokeh widgets?

I am new to bokeh and writing a small bokeh server app, which has plot and a button. When the button is pressed, data is recalculated and plot updates. The idea is that as soon as the button pressed, it changes the color and label, also a text "calculating..." appears. When calculations are done, plot updates and the text disappears.
However, when button is pressed, it doesn't change color and the text does not appear before the calculations are done (takes several seconds). All this widget update happens after calculations. Question, is it possible to force a widget to update, like flush=True in case of print() or something similar may be?
I could not find anything in bokeh documetation. I have tried also to separate widget changes and calculations and execute them in two separate functions, but it didn't help. Setting a delay between button change and invoke of calculation function also did not help. Seems, like update on widgets only happens on exit from callback function or even later. The only thing which I did not check is CustomJS, but I don't know how to write js code for button update.
Thanks for any help!
Here is a code sample close to what I actually use:
from bokeh.plotting import figure
from bokeh.models import Button, PreText, ColumnDataSource
from bokeh.layouts import row
p = figure()
source = ColumnDataSource(data={"x":[0], "y":[0]})
p.line(x="x", y="y", source=source)
variable = False
# initialise widgets
switchButton = Button(label='Anticrossing OFF', button_type="default")
process_markup = PreText(text='Calculating...', visible=False)
def callback(arg):
global variable
global process_markup
variable = not variable
# change button style
if variable:
switchButton.update(label = 'Anticrossing ON',
button_type = 'success')
else:
switchButton.update(label = 'Anticrossing OFF',
button_type = 'default')
# show "calculating..."
process_markup.update(visible=True)
# do long calculations
x, y = calculate_data(variable)
source.data = {"x":x, "y":y}
# hide "calculating..."
process_markup.update(visible=False)
switchButton.on_click(callback)
col = column(switchButton, process_markup)
curdoc().add_root(row(col, p))
Bokeh can send data updates only when the control is returned back to the server event loop. In your case, you run the computation without every yielding the control, so it sends all of the updates when the callback is already done.
The simplest thing to do in your case is to split the callback into blocks that require synchronization and run each block in a next tick callback:
def callback(arg):
global variable
global process_markup
variable = not variable
# change button style
if variable:
switchButton.update(label = 'Anticrossing ON',
button_type = 'success')
else:
switchButton.update(label = 'Anticrossing OFF',
button_type = 'default')
# show "calculating..."
process_markup.update(visible=True)
def calc():
# do long calculations
x, y = calculate_data(variable)
source.data = {"x":x, "y":y}
# hide "calculating..."
process_markup.update(visible=False)
curdoc().add_next_tick_callback(calc)
Note however that such a solution is only suitable if you're the only user and you don't need to do anything while the computation is running. The reason is that the computation is blocking - you cannot communicate with Bokeh in any way while it's running. A proper solution would require some async, e.g. threads. For more details, check out the Updating From Threads section of the Bokeh User Guide.

How to start multiple animations at once in Kivy?

Goal:
To start multiple animations all at once.
Expected Result:
All animation defined and started at once must run parallel.
Actual Result:
The only animation that has been started first shows, the below animations doesn't even start.
The Code for Reference:
class KivySplash(Screen):
def __init__(self, **kwargs):
super(KivySplash, self).__init__(**kwargs)
anim1 = MyAnimation(duration=4, opacity=0)
anim1.bind(on_complete=self.on_anim1_complete)
self.animation = MyAnimation(duration=3) + MyAnimation(duration=4, opacity=1) + MyAnimation(duration=5) + anim1
self.img1 = Image(source=os.path.join(original_dir, "Kivy-logo-black-512.png"), opacity=0)
self.img2 = Image(source=os.path.join(original_dir, "python-powered-w-200x80.png"))
self.label1 = Label(text="Powered by:", font_size=48)
box_layout = BoxLayout(orientation="vertical")
box_layout1 = BoxLayout()
box_layout.add_widget(self.label1)
box_layout1.add_widget(self.img1)
box_layout1.add_widget(self.img2)
box_layout.add_widget(box_layout1)
self.add_widget(box_layout)
def on_anim1_complete(self, *args):
do_nothing(self, *args)
if self.img1 in self.animation.animated_widgets:
pass
def on_enter(self, *args):
self.animation.start(self.img1)
self.animation.start(self.img2)
Thanking You.
I believe you have encountered a bug in the kivy Animation. If you are just using a simple Animation, then starting that Animation on multiple Widgets should work fine. The bug happens when you are using a Sequence (Animations connected with '+'). Sequences work by running the first Animation and binding an internal on_complete method that starts the next Animation in the Sequence. When you call start, that on_complete method is bound. But as soon as the first Animation on the first Widget in the Sequence completes, the second Animation is started and the on_complete method is unbound. Now, when the first Animation on the second Widget completes, the on_complete is not called (is was unbound after the first Widget completed), and the second Animation is not started.
Here is the code from Sequence:
def on_anim1_complete(self, instance, widget):
self.anim1.unbind(on_complete=self.on_anim1_complete)
self.anim2.start(widget)
In your case, it looks like the Animation is not starting on the second Widget, but because your first Animation doesn't actually animate anything, you don't see it.
Unfortunately, there are not many alternatives to avoid this problem.
You can build a copy of the entire Animation a second time (copy() or deepcopy() will not work), and just use two different animations (one for each Widget).
You can do your own sequencing by just using simple Animations and use your own on_complete to start the next Animation. Conveniently, the on_complete arguments includes the animated widget that you need for the next start() call.
In some situations, you may be able to animate a single container (like a Layout). Since that is only animating a single Widget, the sequencing should work correctly.

Update changes smoothly of a PyQt widget

I have a PyQt5 application that looks like this This. Now, with the "bailar" button I want the character on the grid to look left and right a couple of times. I do this in the following method:
def dance(self):
"""
Make the P1 dance
"""
p1 = self._objects['P1']
x = p1.x
y = p1.y
cell = self.worldGrid[y][x]
for i in xrange(3):
print("Moving my head...")
cell.objectLeaves('P1')
p1.pixmap = p1.pixmap.transformed(QTransform().scale(-1, 1))
cell.objectArrives('P1', p1)
time.sleep(0.2)
However, the label containing the pixmap updates only at the last iteration. I know this must be a problem of the update function being asynchronous and the time.sleep() blocking the main thread, but I don't know how else could I show the animation. I tried using a QThread with the moveToThread method, but it failed as the gird widget is a child of the main window. Any ideas?
You see only the last frame of your animation because the UI isn't updated by Qt until the dance function terminates. If you add QtGui.qApp.processEvents() just before the time.sleep(0.2) statement, the UI will be updated for every frame of the animation.
Note that the user still cannot interact with your application until the animation has finished. This might not be a problem for short animations like this, but for longer animations you might better use a QTimer or the Qt animation framework that Brendan Abel suggested.

Categories

Resources