Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6877e36
Add StepsFuture implementation
mdickinson Jul 5, 2021
2a406f8
Merge remote-tracking branch 'origin/main' into feature/steps-future
mdickinson Jul 5, 2021
6df53d3
Add to traits_futures.api, rename the exception
mdickinson Jul 5, 2021
2f4dd45
Add example scripts
mdickinson Jul 5, 2021
04540d4
Fix copyright headers
mdickinson Jul 5, 2021
54c718b
Remove debugging code
mdickinson Jul 5, 2021
29a3fd7
Import sort order fixes
mdickinson Jul 5, 2021
d398004
Fix doc build(?)
mdickinson Jul 5, 2021
0b81f2f
Add IStepsReporter to the public API
mdickinson Jul 5, 2021
b685aaf
Merge remote-tracking branch 'origin/main' into feature/steps-future
mdickinson Jul 5, 2021
5edc3ba
Adapt for changes to 'send' introduced in enthought/traits-futures#364
mdickinson Jul 5, 2021
ee04686
Add some test machinery
mdickinson Jul 5, 2021
eec72cd
Add tests for StepsFuture
mdickinson Jul 5, 2021
66a7449
Stash
mdickinson Jul 5, 2021
7f4c60c
Stash
mdickinson Jul 6, 2021
c4fdbf4
Merge remote-tracking branch 'origin/main' into feature/steps-future
mdickinson Aug 11, 2021
63e5a67
Merge main branch
mdickinson Aug 11, 2021
18681c3
flake8 appeasement
mdickinson Aug 11, 2021
2b38a1c
Rework internal state
mdickinson Aug 12, 2021
9d0ca41
Cleanup
mdickinson Aug 12, 2021
92a24f9
Fix the dialog example
mdickinson Aug 12, 2021
2a2429b
Add 'start' back
mdickinson Aug 12, 2021
5eadcfa
Fix up progress dialogs example to work as before
mdickinson Aug 12, 2021
ffd8f44
Add note about errors seen using the non-modal dialog
mdickinson Aug 12, 2021
6b35774
Some renaming and cleanup
mdickinson Aug 13, 2021
59b90ee
Remove duplicate file; add missing docstring
mdickinson Aug 13, 2021
217b0c2
Stash
mdickinson Aug 13, 2021
2d9b437
Make IStepsReporter and StepsReporter match
mdickinson Aug 13, 2021
44fe696
Reworking - allow setting total and message at start time
mdickinson Aug 16, 2021
2b6196a
Cleanup and updates
mdickinson Aug 16, 2021
f2e4c25
Merge branch 'feature/steps-future-only' into feature/steps-future
mdickinson Aug 16, 2021
cd98395
Merge branch 'feature/steps-future-only' into feature/steps-future
mdickinson Aug 18, 2021
17b7362
A couple more todo comments
mdickinson Nov 12, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions docs/source/guide/examples/steps_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# (C) Copyright 2018-2021 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

""" Qt implementation of a Pyface Dialog that listens to a StepsFuture.
"""

from pyface.api import Dialog
from pyface.qt import QtCore, QtGui
from traits.api import Any, Bool, Instance, Int, observe, Property, Str
from traits_futures.api import EXECUTING, StepsFuture

# XXX Fix behaviour on dialog close button. Should match pressing the
# "cancelling" button, and also pressing ESC. (What do users want?)
# XXX What should behaviour be on a Ctrl-C?
# XXX Two possible desirable behaviours: (1) cancel then close immediately
# (2) prevent closing, close when cancel completes
# (1) is more likely to be important with a modal dialog
# All three of (a) pressing the cancel button, (b) using the dialog close
# button, (c) hitting ESC should have the same behaviour.
# For the preventing closing behaviour, the dialog itself needs some state.


# XXX Diagnose and fix errors seen when launching a non-modal dialog and then
# clicking its close button:
# Exception occurred in traits notification handler for event object:
# TraitChangeEvent(object=<background_progress_dialog.StepsDialog
# object at 0x10d8cd040>, name='message', old=<undefined>, new='executing:
# processing item 10 of 10')
# Traceback (most recent call last):
# File "/Users/mdickinson/.venvs/traits-futures/lib/python3.9/site-packages/
# traits/observation/_trait_event_notifier.py", line 122, in __call__
# self.dispatcher(handler, event)
# File "/Users/mdickinson/.venvs/traits-futures/lib/python3.9/site-packages/
# traits/observation/observe.py", line 26, in dispatch_same
# handler(event)
# File "/Users/mdickinson/Enthought/Projects/traits-futures/docs/source/
# guide/examples/background_progress_dialog.py", line 137, in
# _update_message_in_message_control
# self._message_control.setText(event.new)
# AttributeError: 'NoneType' object has no attribute 'setText'


class StepsDialog(Dialog):
"""Show a cancellable progress dialog listening to a progress manager."""

#: Text to show for cancellation label.
cancel_label = "Cancel"

#: Text to show for the ok label. (Not used here.)
ok_label = ""

#: Whether to show a 'Cancel' button or not.
cancellable = Bool(True)

#: Whether to show the percentage complete or not.
show_percent = Bool(True)

#: The traited ``Future`` representing the state of the background call.
future = Instance(StepsFuture)

#: The message to display.
message = Property(Str(), observe="future:[state,message]")

#: The maximum for the dialog.
maximum = Property(Int(), observe="future:total")

def cancel(self):
"""Cancel the job.

Users of the dialog should call this instead of reaching
down to the progress manager since this method will prevent
the job from starting if it has not already.
"""
self.future.cancel()
self._cancel_button_control.setEnabled(False)

# Private implementation ##################################################

_progress_bar = Any()
_message_control = Any()
_cancel_button_control = Any()

def _create_contents(self, parent):
layout = QtGui.QVBoxLayout()

if not self.resizeable:
layout.setSizeConstraint(QtGui.QLayout.SetFixedSize)

layout.addWidget(self._create_message(parent, layout))
layout.addWidget(self._create_gauge(parent, layout))
if self.cancellable:
layout.addWidget(self._create_buttons(parent))

parent.setLayout(layout)

def _create_buttons(self, parent):
buttons = QtGui.QDialogButtonBox()

# 'Cancel' button.
btn = buttons.addButton(
self.cancel_label, QtGui.QDialogButtonBox.RejectRole
)
btn.setDefault(True)
# The QueuedConnection here appears to be necessary to avoid a
# Qt/macOS bug where the dialog widgets are only partially updated
# after a button click.
# xref: https://github.com/enthought/traitsui/issues/1308
# xref: https://bugreports.qt.io/browse/QTBUG-68067
buttons.rejected.connect(self.cancel, type=QtCore.Qt.QueuedConnection)
self._cancel_button_control = btn
return buttons

def _create_message(self, dialog, layout):
self._message_control = QtGui.QLabel(self.message, dialog)
self._message_control.setAlignment(
QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft
)
return self._message_control

def _create_gauge(self, dialog, layout):
self._progress_bar = QtGui.QProgressBar(dialog)
self._progress_bar.setRange(0, self.maximum)
self._progress_bar.setValue(self.future.complete)
if self.show_percent:
self._progress_bar.setFormat("%p%")
else:
self._progress_bar.setFormat("%v")
return self._progress_bar

@observe("closing")
def _destroy_traits_on_dialog_closing(self, event):
self._message_control = None
self._progress_bar = None
self._cancel_button_control = None

@observe("maximum")
def _update_max_on_progress_bar(self, event):
maximum = event.new
if self._progress_bar is not None:
self._progress_bar.setRange(0, maximum)

@observe("message")
def _update_message_in_message_control(self, event):
self._message_control.setText(event.new)

def _get_message(self):
"""
Property getter for the 'message' trait.
"""
future = self.future
if future.state == EXECUTING and future.message is not None:
return f"{future.state}: {future.message}"
else:
return f"{future.state}"

def _get_maximum(self):
"""
Property getter for the 'maximum' trait.
"""
future = self.future
if future.total is None:
return 0
else:
return max(future.total, 0)

@observe("future:complete")
def _update_value(self, event):
complete = event.new
if self._progress_bar is not None:
self._progress_bar.setValue(complete)

@observe("future:done")
def _on_end(self, event):
self.close()
119 changes: 119 additions & 0 deletions docs/source/guide/examples/steps_dialogs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# (C) Copyright 2018-2021 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

"""
Demo script for modal progress dialog.
"""

# XXX To do list (including for StepsFutures)
# * Add NullReporter implementation of IStepsReporter, for convenience in
# testing progress-reporting tasks.
# * Add IStepsReporter "check" method that simply checks for cancellation.

import concurrent.futures
import time

from traits.api import (
Any,
Button,
HasStrictTraits,
Instance,
List,
observe,
Range,
)
from traits_futures.api import submit_steps, TraitsExecutor
from traitsui.api import Item, View

from steps_dialog import StepsDialog


def count(target, *, sleep=1.0, reporter):
"""
Parameters
----------
target : int
Value to count to.
sleep : float
Time to sleep between each count.
reporter : IStepsReporter
Object used to report progress.
"""
for i in range(target):
reporter.step(f"processing item {i+1} of {target}")
time.sleep(sleep)


class MyView(HasStrictTraits):

calculate = Button()

nonmodal = Button()

target = Range(0, 100, 10)

executor = Instance(TraitsExecutor)

def _executor_default(self):
return TraitsExecutor() # worker_pool=self.cf_executor)

dialogs = List(StepsDialog)

cf_executor = Any()

@observe("calculate")
def _open_modal_dialog(self, event):
target = self.target
future = submit_steps(
self.executor,
target,
"Starting processing...",
count,
target=target,
)
dialog = StepsDialog(
future=future,
style="modal", # this is the default
title=f"Counting to {target}",
)
dialog.open()

@observe("nonmodal")
def _open_nonmodal_dialog(self, event):
target = self.target
future = submit_steps(
self.executor, target, "Starting processing...", count, target
)
dialog = StepsDialog(
future=future,
style="nonmodal", # this is the default
title=f"Counting to {target}",
)
dialog.open()
# Keep a reference to open dialogs, else they'll
# mysteriously disappear. A better solution might be to
# parent them appropriately.
self.dialogs.append(dialog)

view = View(
Item("calculate", label="Count modal"),
Item("nonmodal", label="Count nonmodal"),
Item("target"),
)


def main():
with concurrent.futures.ThreadPoolExecutor() as executor:
view = MyView(cf_executor=executor)
view.configure_traits()


if __name__ == "__main__":
main()