Add prototype progress dialog.

svn: r8127
This commit is contained in:
Richard Taylor
2007-02-16 13:06:49 +00:00
parent d912167cab
commit 73344ccb1c
4 changed files with 354 additions and 72 deletions

View File

@@ -1,3 +1,8 @@
2007-02-16 Richard Taylor <rjt-gramps@thegrindstone.me.uk>
* src/GrampsDb/__init__.py: add prototype progress dialog
* src/GrampsDb/_LongOpStatus.py: add prototype progress dialog
* src/ProgressDialog.py: add prototype progress dialog
2007-02-16 Richard Taylor <rjt-gramps@thegrindstone.me.uk> 2007-02-16 Richard Taylor <rjt-gramps@thegrindstone.me.uk>
* src/GrampsDb/_LongOpStatus.py: more work on long operation framework * src/GrampsDb/_LongOpStatus.py: more work on long operation framework
* src/GrampsDb/_CursorIterator.py: more work on long operation framework * src/GrampsDb/_CursorIterator.py: more work on long operation framework

View File

@@ -59,89 +59,122 @@ class LongOpStatus(GrampsDBCallback):
'op-end' : None 'op-end' : None
} }
def __init__(self,msg="",total_steps=None,interval=1,can_cancel=False): def __init__(self, msg="",
""" total_steps=None,
@param msg: A Message to indicated the purpose of the operation. interval=1,
@type msg: string can_cancel=False):
"""
@param total_steps: The total number of steps that the operation @param msg: A Message to indicated the purpose of the operation.
will perform. @type msg: string
@type total_steps:
@param total_steps: The total number of steps that the operation
@param interval: The number of iterations between emissions. will perform.
@type interval: @type total_steps:
@param can_cancel: Set to True if the operation can be cancelled. @param interval: The number of iterations between emissions.
If this is set the operation that creates the status object should @type interval:
check the 'should_cancel' method regularly so that it can cancel
the operation. @param can_cancel: Set to True if the operation can be cancelled.
@type can_cancel: If this is set the operation that creates the status object should
""" check the 'should_cancel' method regularly so that it can cancel
the operation.
@type can_cancel:
"""
GrampsDBCallback.__init__(self) GrampsDBCallback.__init__(self)
self._msg = msg self._msg = msg
self._total = total_steps self._total_steps = total_steps
self._interval = interval self._interval = interval
self._can_cancel = can_cancel self._can_cancel = can_cancel
self._cancel = False self._cancel = False
self._count = 0 self._count = 0
self._countdown = interval self._countdown = interval
self._secs_left = 0 self._secs_left = 0
self._start = time.time() self._start = time.time()
def heartbeat(self): def heartbeat(self):
"""This should be called for each step in the operation. It will """This should be called for each step in the operation. It will
emit a 'op-heartbeat' every 'interval' steps. It recalcuates the emit a 'op-heartbeat' every 'interval' steps. It recalcuates the
'estimated_secs_to_complete' from the time taken for previous 'estimated_secs_to_complete' from the time taken for previous
steps. steps.
""" """
self._countdown -= 1 self._countdown -= 1
if self._countdown <= 0: if self._countdown <= 0:
elapsed = time.time() - self._start elapsed = time.time() - self._start
self._secs_left = \ self._secs_left = \
( elapsed / self._interval ) \ ( elapsed / self._interval ) \
* (self._total - self._count) * (self._total_steps - self._count)
self._count += self._interval self._count += self._interval
self._countdown = self._interval self._countdown = self._interval
self._start = time.time() self._start = time.time()
self.emit('op-heartbeat') self.emit('op-heartbeat')
def estimated_secs_to_complete(self): def estimated_secs_to_complete(self):
"""Return the number of seconds estimated left before operation """Return the number of seconds estimated left before operation
completes. This will change as 'hearbeat' is called. completes. This will change as 'hearbeat' is called.
@return: estimated seconds to complete. @return: estimated seconds to complete.
@rtype: int @rtype: int
""" """
return self._secs_left return self._secs_left
def cancel(self): def cancel(self):
"""Inform the operation that it should complete. """Inform the operation that it should complete.
""" """
self._cancel = True self._cancel = True
self.end() self.end()
def end(self): def end(self):
"""End the operation. Causes the 'op-end' signal to be emitted. """End the operation. Causes the 'op-end' signal to be emitted.
""" """
self.emit('op-end') self.emit('op-end')
def should_cancel(self): def should_cancel(self):
"""Returns true of the user has asked for the operation to be cancelled. """Returns true of the user has asked for the operation to be cancelled.
@return: True of the operation should be cancelled. @return: True of the operation should be cancelled.
@rtype: bool @rtype: bool
""" """
return self._cancel return self._cancel
def can_cancel(self): def can_cancel(self):
return self._can_cancel """@return: True if the operation can be cancelled.
@rtype: bool
"""
return self._can_cancel
def get_msg(self): def get_msg(self):
return msg """@return: The current status description messages.
@rtype: string
"""
return self._msg
def set_msg(self, msg): def set_msg(self, msg):
self._msg = msg """Set the current description message.
@param msg: The description message.
@type msg: string
"""
self._msg = msg
def get_total_steps(self):
"""Get to total number of steps. NOTE: this is not the
number of times that the 'op-heartbeat' message will be
emited. 'op-heartbeat' is emited get_total_steps/interval
times.
@return: total number of steps.
@rtype: int
"""
return self._total_steps
def get_interval(self):
"""Get the interval between 'op-hearbeat' signals.
@return: the interval between 'op-hearbeat' signals.
@rtype: int
"""
return self._interval
if __name__ == '__main__': if __name__ == '__main__':
@@ -149,17 +182,17 @@ if __name__ == '__main__':
s = LongOpStatus("msg", 100, 10) s = LongOpStatus("msg", 100, 10)
def heartbeat(): def heartbeat():
print "heartbeat ", s.estimated_secs_to_complete() print "heartbeat ", s.estimated_secs_to_complete()
def end(): def end():
print "end" print "end"
s.connect('op-heartbeat', heartbeat) s.connect('op-heartbeat', heartbeat)
s.connect('op-end', end) s.connect('op-end', end)
for i in xrange(0,99): for i in xrange(0, 99):
time.sleep(0.1) time.sleep(0.1)
s.heartbeat() s.heartbeat()
s.end() s.end()

View File

@@ -50,3 +50,6 @@ from _GrampsDBCallback import GrampsDBCallback
from _DbUtils import * from _DbUtils import *
import _GrampsDbConst as GrampsDbConst import _GrampsDbConst as GrampsDbConst
from _LongOpStatus import LongOpStatus

241
src/ProgressDialog.py Normal file
View File

@@ -0,0 +1,241 @@
"""
This module provides a progess dialog for displaying the status of
long running operations.
"""
import gtk
class _GtkProgressBar(object):
"""This is just a structure to hold the visual elements of a
progress indicator."""
def __init__(self):
self.pbar = None
self.label = None
self.pbar_max = 0
self.pbar_index = 0.0
self.old_val = -1
class _GtkProgressDialog(gtk.Dialog):
"""A gtk window to display the status of a long running
process."""
def __init__(self, title):
gtk.Dialog.__init__(self)
self.connect('delete_event', self.warn)
self.set_has_separator(False)
self.set_title(title)
self.set_border_width(12)
self.vbox.set_spacing(10)
lbl = gtk.Label('<span size="larger" weight="bold">%s</span>' % title)
lbl.set_use_markup(True)
self.vbox.pack_start(lbl)
#self.set_size_request(350,125)
self.set_resize_mode(gtk.RESIZE_IMMEDIATE)
self.show_all()
self._progress_bars = []
def add(self,long_op_status):
# Create a new progress bar
pbar = _GtkProgressBar()
pbar.lbl = gtk.Label(long_op_status.get_msg())
pbar.lbl.set_use_markup(True)
self.vbox.set_border_width(24)
pbar.pbar = gtk.ProgressBar()
self.vbox.pack_start(pbar.lbl, expand=False, fill=False)
self.vbox.pack_start(pbar.pbar, expand=False, fill=False)
if long_op_status.get_msg() == '':
pbar.lbl.hide()
pbar.pbar_max = (long_op_status.get_total_steps()/
long_op_status.get_interval())
pbar.pbar_index = 0.0
pbar.pbar.set_fraction((float(long_op_status.get_total_steps())/
(float(long_op_status.get_interval())))/
100.0)
pbar.lbl.show()
pbar.pbar.show()
self.resize_children()
self.process_events()
self._progress_bars.append(pbar)
return len(self._progress_bars)-1
def remove(self, pbar_idx):
pbar = self._progress_bars[pbar_idx]
self.vbox.remove(pbar.pbar)
self.vbox.remove(pbar.lbl)
del self._progress_bars[pbar_idx]
def step(self, pbar_idx):
"""Click the progress bar over to the next value. Be paranoid
and insure that it doesn't go over 100%."""
pbar = self._progress_bars[pbar_idx]
pbar.pbar_index = pbar.pbar_index + 1.0
if pbar.pbar_index > pbar.pbar_max:
pbar.pbar_index = pbar.pbar_max
try:
val = int(100*pbar.pbar_index/pbar.pbar_max)
except ZeroDivisionError:
val = 0
if val != pbar.old_val:
pbar.pbar.set_text("%d%%" % val)
pbar.pbar.set_fraction(val/100.0)
pbar.old_val = val
self.process_events()
def process_events(self):
while gtk.events_pending():
gtk.main_iteration()
def show(self):
gtk.Dialog.show(self)
self.process_events()
def hide(self):
gtk.Dialog.hide(self)
self.process_events()
def warn(self):
return True
def close(self):
self.destroy()
class _StatusObjectFacade(object):
"""This provides a simple structure for recording the information
needs about a status object."""
def __init__(self, status_obj, heartbeat_cb_id=None, end_cb_id=None):
self.status_obj = status_obj
self.heartbeat_cb_id = heartbeat_cb_id
self.end_cb_id = end_cb_id
self.pbar_idx = None
self.active = False
class ProgressDialog(object):
"""A dialog for displaying the status of long running operations.
It will work with L{GrampsDb.LongOpStatus} objects to track the
progress of long running operations. If the operations is going to
take longer than I{popup_time} it will pop up a dialog with a
progress bar so that the user gets some feedback about what is
happening.
"""
__default_popup_time = 5 # seconds
def __init__(self, popup_time = None):
self._popup_time = popup_time
if self._popup_time == None:
self._popup_time = self.__class__.__default_popup_time
self._status_stack = [] # list of current status objects
self._dlg = None
def _get_dlg(self):
if self._dlg == None:
self._dlg = _GtkProgressDialog("Long running operation.")
self._dlg.show()
return self._dlg
def add_op(self, op_status):
facade = _StatusObjectFacade(op_status)
self._status_stack.append(facade)
idx = len(self._status_stack)-1
# wrap up the op_status object idx into the callback calls
def heartbeat_cb():
self._heartbeat(idx)
def end_cb():
self._end(idx)
facade.heartbeat_cb_id = op_status.connect('op-heartbeat',
heartbeat_cb)
facade.end_cb_id = op_status.connect('op-end', end_cb)
def _heartbeat(self, idx):
# check the estimated time to complete to see if we need
# to pop up a progress dialog.
facade = self._status_stack[idx]
if facade.status_obj.estimated_secs_to_complete() > self._popup_time:
facade.active = True
if facade.active:
dlg = self._get_dlg()
if facade.pbar_idx == None:
facade.pbar_idx = dlg.add(facade.status_obj)
dlg.step(facade.pbar_idx)
def _end(self, idx):
# hide any progress dialog
# remove the status object from the stack
facade = self._status_stack[idx]
if facade.active:
dlg = self._get_dlg()
if len(self._status_stack) == 1:
dlg.hide()
dlg.remove(facade.pbar_idx)
facade.status_obj.disconnect(facade.heartbeat_cb_id)
facade.status_obj.disconnect(facade.end_cb_id)
del self._status_stack[idx]
if __name__ == '__main__':
import time
from GrampsDb import LongOpStatus
def test(a,b):
d = ProgressDialog()
s = LongOpStatus("Doing very long operation", 100, 10)
d.add_op(s)
for i in xrange(0, 99):
time.sleep(0.1)
if i == 30:
t = LongOpStatus("doing a shorter one", 100, 10)
d.add_op(t)
for j in xrange(0, 99):
time.sleep(0.1)
t.heartbeat()
t.end()
if i == 60:
t = LongOpStatus("doing another shorter one", 100, 10)
d.add_op(t)
for j in xrange(0, 99):
time.sleep(0.1)
t.heartbeat()
t.end()
s.heartbeat()
s.end()
w = gtk.Window(gtk.WINDOW_TOPLEVEL)
w.connect('destroy', gtk.main_quit)
button = gtk.Button("Test")
button.connect("clicked", test, None)
w.add(button)
button.show()
w.show()
gtk.main()
print 'done'