Skip to content

Commit 2af0e43

Browse files
committed
ENH: indicate when a PyDMShellCommand's cmd is currently running by changing the
button's appearance and disabling the button. this adds the following: - if allowMultipleExecutions is enabled, nothing different should happen. - but if not, then while cmd is running: - update button text to signify running, disable button, and switch to 'hour-glass' icon. - if multiple-cmd button, update top-button text (but keep enabled so can still use drop-down menu), disable all submenu items, and update icon and text of current running submenu item. - return button to previous state when cmd done running. - add long-running cmd as button and multiple-cmd drop-down so can try out new behavior. also make warning icon red color so more noticeable.
1 parent 6b2bb1c commit 2af0e43

File tree

3 files changed

+381
-6
lines changed

3 files changed

+381
-6
lines changed

examples/shell_command/shell_command.ui

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,74 @@
7575
</property>
7676
</widget>
7777
</item>
78+
<item>
79+
<widget class="PyDMShellCommand" name="PyDMShellCommand_4">
80+
<property name="toolTip">
81+
<string/>
82+
</property>
83+
<property name="text">
84+
<string>Run Long-running Command</string>
85+
</property>
86+
<property name="alarmSensitiveContent" stdset="0">
87+
<bool>false</bool>
88+
</property>
89+
<property name="alarmSensitiveBorder" stdset="0">
90+
<bool>true</bool>
91+
</property>
92+
<property name="PyDMToolTip" stdset="0">
93+
<string/>
94+
</property>
95+
<property name="channel" stdset="0">
96+
<string/>
97+
</property>
98+
<property name="PyDMIcon" stdset="0">
99+
<string/>
100+
</property>
101+
<property name="showConfirmDialog" stdset="0">
102+
<bool>false</bool>
103+
</property>
104+
<property name="runCommandsInFullShell" stdset="0">
105+
<bool>true</bool>
106+
</property>
107+
<property name="confirmMessage" stdset="0">
108+
<string>Are you sure you want to proceed?</string>
109+
</property>
110+
<property name="environmentVariables" stdset="0">
111+
<string/>
112+
</property>
113+
<property name="showIcon" stdset="0">
114+
<bool>true</bool>
115+
</property>
116+
<property name="stdout" stdset="0">
117+
<enum>PyDMShellCommand::SHOW</enum>
118+
</property>
119+
<property name="stderr" stdset="0">
120+
<enum>PyDMShellCommand::SHOW</enum>
121+
</property>
122+
<property name="allowMultipleExecutions" stdset="0">
123+
<bool>false</bool>
124+
</property>
125+
<property name="titles" stdset="0">
126+
<stringlist>
127+
<string></string>
128+
</stringlist>
129+
</property>
130+
<property name="commands" stdset="0">
131+
<stringlist>
132+
<string>for i in {1..5}; do echo $i; sleep 1; done</string>
133+
</stringlist>
134+
</property>
135+
<property name="passwordProtected" stdset="0">
136+
<bool>false</bool>
137+
</property>
138+
<property name="password" stdset="0">
139+
<string/>
140+
</property>
141+
<property name="protectedPassword" stdset="0">
142+
<string/>
143+
</property>
144+
</widget>
145+
</item>
78146
<item>
79147
<spacer name="verticalSpacer">
80148
<property name="orientation">
@@ -106,21 +174,65 @@
106174
<property name="text">
107175
<string>Multiple Shell Command Menu</string>
108176
</property>
177+
<property name="alarmSensitiveContent" stdset="0">
178+
<bool>false</bool>
179+
</property>
180+
<property name="alarmSensitiveBorder" stdset="0">
181+
<bool>true</bool>
182+
</property>
183+
<property name="PyDMToolTip" stdset="0">
184+
<string/>
185+
</property>
186+
<property name="channel" stdset="0">
187+
<string/>
188+
</property>
189+
<property name="PyDMIcon" stdset="0">
190+
<string/>
191+
</property>
192+
<property name="showConfirmDialog" stdset="0">
193+
<bool>false</bool>
194+
</property>
195+
<property name="runCommandsInFullShell" stdset="0">
196+
<bool>true</bool>
197+
</property>
198+
<property name="confirmMessage" stdset="0">
199+
<string>Are you sure you want to proceed?</string>
200+
</property>
201+
<property name="environmentVariables" stdset="0">
202+
<string/>
203+
</property>
204+
<property name="showIcon" stdset="0">
205+
<bool>true</bool>
206+
</property>
109207
<property name="redirectCommandOutput" stdset="0">
110208
<bool>true</bool>
111209
</property>
210+
<property name="allowMultipleExecutions" stdset="0">
211+
<bool>false</bool>
212+
</property>
112213
<property name="titles" stdset="0">
113214
<stringlist>
114215
<string>Print &quot;Hello, World!&quot; to terminal</string>
115216
<string>Print current working directory to terminal</string>
217+
<string>Run Long-running Command</string>
116218
</stringlist>
117219
</property>
118220
<property name="commands" stdset="0">
119221
<stringlist>
120222
<string>echo &quot;Hello, World!&quot;</string>
121223
<string>pwd</string>
224+
<string>for i in {1..5}; do echo $i; sleep 1; done</string>
122225
</stringlist>
123226
</property>
227+
<property name="passwordProtected" stdset="0">
228+
<bool>false</bool>
229+
</property>
230+
<property name="password" stdset="0">
231+
<string/>
232+
</property>
233+
<property name="protectedPassword" stdset="0">
234+
<string/>
235+
</property>
124236
</widget>
125237
</item>
126238
</layout>

pydm/tests/widgets/test_shell_command.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import os
66
import pytest
7+
import time
78

89
import platform
910
from logging import ERROR
@@ -218,6 +219,143 @@ def check_command_output(command, expected_retcode, expected_stdout):
218219
assert pydm_shell_command.process is None
219220

220221

222+
def test_long_running_command_shows_currently_running_text(qtbot):
223+
"""
224+
Test that the button is updated to indicate when a command is currently running.
225+
These indications are:
226+
- Prepend "(Running...) " to the button's text
227+
- Make button text italic
228+
- Disable button while cmd is running (unless allowMultipleExecutions is set True)
229+
- Set button icon to hourglass symbol
230+
And the button should be reset back to it's original state after the command is done.
231+
"""
232+
pydm_shell_command = PyDMShellCommand()
233+
qtbot.addWidget(pydm_shell_command)
234+
pydm_shell_command.stdout = TermOutputMode.HIDE
235+
236+
# Long running cmd, which we will kill after checking button state while its running
237+
pydm_shell_command.commands = ["for i in {1..100}; do echo $i; sleep 1; done"]
238+
239+
original_text = "Run Long Cmd"
240+
pydm_shell_command.setText(original_text)
241+
pydm_shell_command.runCommandsInFullShell = True
242+
243+
# Execute the cmd
244+
qtbot.mouseClick(pydm_shell_command, QtCore.Qt.LeftButton)
245+
# Give the command some time to start
246+
qtbot.wait(100) # wait 0.1 seconds
247+
248+
# Check icon is updated while cmd is running
249+
icon_size = QSize(16, 16)
250+
default_icon = IconFont().icon("hourglass-start")
251+
default_icon_pixmap = default_icon.pixmap(icon_size)
252+
curr_icon_pixmap = pydm_shell_command.icon().pixmap(icon_size)
253+
assert curr_icon_pixmap.toImage() == default_icon_pixmap.toImage()
254+
255+
assert pydm_shell_command.font().italic()
256+
assert pydm_shell_command.text() == f"(Running...) {original_text}"
257+
258+
assert not pydm_shell_command.isEnabled()
259+
260+
# End process since we are done checking the 'running' state of the button
261+
if pydm_shell_command.process:
262+
pydm_shell_command.process.kill()
263+
264+
# Wait a bit for things to update
265+
qtbot.wait(100) # Wait 0.1 seconds
266+
267+
# Check icon is reverted back after cmd stops running
268+
default_icon = IconFont().icon("cog")
269+
default_icon_pixmap = default_icon.pixmap(icon_size)
270+
curr_icon_pixmap = pydm_shell_command.icon().pixmap(icon_size)
271+
assert curr_icon_pixmap.toImage() == default_icon_pixmap.toImage()
272+
273+
assert not pydm_shell_command.font().italic()
274+
assert pydm_shell_command.text() == original_text
275+
276+
assert pydm_shell_command.isEnabled()
277+
278+
279+
def test_long_running_command_shows_currently_running_text_dropdown(qtbot):
280+
"""
281+
Test that A button with multiple-cmds (so it has a drop-down menu) is updated to indicate
282+
a drop-down menu command is currently running.
283+
These indications are:
284+
- Prepend "(Submenu cmd running...) " to the button's text
285+
- Prepend "(Running...) " to the curr running drop-down item
286+
- Make button and curr running drop-down text italic
287+
- Disable button and all drop-down menu items while cmd is running (unless allowMultipleExecutions is set True)
288+
- Set both button and curr running drop-down icons to hourglass symbol
289+
And the button should be reset back to it's original state after the command is done.
290+
"""
291+
pydm_shell_command = PyDMShellCommand()
292+
qtbot.addWidget(pydm_shell_command)
293+
pydm_shell_command.stdout = TermOutputMode.SHOW
294+
295+
original_button_text = "Run Long Cmd"
296+
# Text of the specific item in drop-down we want to check the state of
297+
original_action_text = "for i in {1..50}; do echo $i; sleep 1; done"
298+
pydm_shell_command.setText(original_button_text)
299+
pydm_shell_command.runCommandsInFullShell = True
300+
301+
pydm_shell_command.commands = ["echo 'command 1'", "echo 'command 2'", original_action_text]
302+
303+
# We need to execute the shell-cmd's mousePressEvent() so it can build the drop-down menu,
304+
# and right-click since left-click seems to cause this test to then display the drop-down menu
305+
# and just pauses waiting for user interaction.
306+
qtbot.mouseClick(pydm_shell_command, QtCore.Qt.RightButton)
307+
308+
actions = pydm_shell_command.menu().actions()
309+
assert len(actions) >= 3
310+
311+
# Activate drop-down menu button cmd
312+
actions[2].trigger()
313+
# Wait a bit for things to update
314+
qtbot.wait(100) # Wait 0.1 seconds
315+
316+
# Check button icon is changed while cmd is running
317+
icon_size = QSize(16, 16)
318+
default_icon = IconFont().icon("hourglass-start")
319+
default_icon_pixmap = default_icon.pixmap(icon_size)
320+
curr_icon_pixmap = pydm_shell_command.icon().pixmap(icon_size)
321+
assert curr_icon_pixmap.toImage() == default_icon_pixmap.toImage()
322+
323+
assert pydm_shell_command.text() == f"(Submenu cmd running...) {original_button_text}"
324+
assert pydm_shell_command.font().italic()
325+
326+
# Check state of action buttons in drop-down menu
327+
# Check icon is changed on curr running action button while cmd is running
328+
default_icon = IconFont().icon("hourglass-start")
329+
default_icon_pixmap = default_icon.pixmap(icon_size)
330+
curr_icon_pixmap = actions[2].icon().pixmap(icon_size)
331+
assert curr_icon_pixmap.toImage() == default_icon_pixmap.toImage()
332+
333+
assert actions[2].text() == f"(Running...) {original_action_text}"
334+
assert actions[2].font().italic()
335+
336+
assert all(not action.isEnabled() for action in actions)
337+
338+
# End process since we are done checking the 'running' state of the button
339+
if pydm_shell_command.process:
340+
pydm_shell_command.process.kill()
341+
342+
# Wait a bit for things to update
343+
qtbot.wait(100) # wait 0.1 seconds
344+
345+
# Check button icon is reverted back after cmd stops running
346+
default_icon = IconFont().icon("cog")
347+
default_icon_pixmap = default_icon.pixmap(icon_size)
348+
curr_icon_pixmap = pydm_shell_command.icon().pixmap(icon_size)
349+
assert curr_icon_pixmap.toImage() == default_icon_pixmap.toImage()
350+
351+
assert pydm_shell_command.text() == original_button_text
352+
assert not pydm_shell_command.font().italic()
353+
354+
# Check drop-down menu icon is reverted
355+
actions[2].icon().isNull() # Drop-down menu cmds have no icon by default
356+
assert all(action.isEnabled() for action in actions)
357+
358+
221359
@pytest.mark.parametrize(
222360
"allow_multiple",
223361
[

0 commit comments

Comments
 (0)