diff --git a/ChangeLog b/ChangeLog index 8c0fc0589..203c46ee6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2007-02-16 Richard Taylor + * 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 * src/GrampsDb/_LongOpStatus.py: more work on long operation framework * src/GrampsDb/_CursorIterator.py: more work on long operation framework diff --git a/src/GrampsDb/_LongOpStatus.py b/src/GrampsDb/_LongOpStatus.py index 4021343d7..745ee13f1 100644 --- a/src/GrampsDb/_LongOpStatus.py +++ b/src/GrampsDb/_LongOpStatus.py @@ -59,89 +59,122 @@ class LongOpStatus(GrampsDBCallback): 'op-end' : None } - def __init__(self,msg="",total_steps=None,interval=1,can_cancel=False): - """ - @param msg: A Message to indicated the purpose of the operation. - @type msg: string - - @param total_steps: The total number of steps that the operation - will perform. - @type total_steps: - - @param interval: The number of iterations between emissions. - @type interval: - - @param can_cancel: Set to True if the operation can be cancelled. - 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: - """ + def __init__(self, msg="", + total_steps=None, + interval=1, + can_cancel=False): + """ + @param msg: A Message to indicated the purpose of the operation. + @type msg: string + + @param total_steps: The total number of steps that the operation + will perform. + @type total_steps: + + @param interval: The number of iterations between emissions. + @type interval: + + @param can_cancel: Set to True if the operation can be cancelled. + 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) - self._msg = msg - self._total = total_steps - self._interval = interval - self._can_cancel = can_cancel - - self._cancel = False - self._count = 0 - self._countdown = interval - self._secs_left = 0 - self._start = time.time() + self._msg = msg + self._total_steps = total_steps + self._interval = interval + self._can_cancel = can_cancel + + self._cancel = False + self._count = 0 + self._countdown = interval + self._secs_left = 0 + self._start = time.time() def heartbeat(self): - """This should be called for each step in the operation. It will - emit a 'op-heartbeat' every 'interval' steps. It recalcuates the - 'estimated_secs_to_complete' from the time taken for previous - steps. - """ - self._countdown -= 1 - if self._countdown <= 0: - elapsed = time.time() - self._start - self._secs_left = \ - ( elapsed / self._interval ) \ - * (self._total - self._count) - self._count += self._interval - self._countdown = self._interval - self._start = time.time() - self.emit('op-heartbeat') + """This should be called for each step in the operation. It will + emit a 'op-heartbeat' every 'interval' steps. It recalcuates the + 'estimated_secs_to_complete' from the time taken for previous + steps. + """ + self._countdown -= 1 + if self._countdown <= 0: + elapsed = time.time() - self._start + self._secs_left = \ + ( elapsed / self._interval ) \ + * (self._total_steps - self._count) + self._count += self._interval + self._countdown = self._interval + self._start = time.time() + self.emit('op-heartbeat') def estimated_secs_to_complete(self): - """Return the number of seconds estimated left before operation - completes. This will change as 'hearbeat' is called. - - @return: estimated seconds to complete. - @rtype: int - """ - return self._secs_left + """Return the number of seconds estimated left before operation + completes. This will change as 'hearbeat' is called. + + @return: estimated seconds to complete. + @rtype: int + """ + return self._secs_left def cancel(self): - """Inform the operation that it should complete. - """ - self._cancel = True - self.end() + """Inform the operation that it should complete. + """ + self._cancel = True + self.end() def end(self): - """End the operation. Causes the 'op-end' signal to be emitted. - """ - self.emit('op-end') + """End the operation. Causes the 'op-end' signal to be emitted. + """ + self.emit('op-end') def should_cancel(self): - """Returns true of the user has asked for the operation to be cancelled. - - @return: True of the operation should be cancelled. - @rtype: bool - """ - return self._cancel + """Returns true of the user has asked for the operation to be cancelled. + + @return: True of the operation should be cancelled. + @rtype: bool + """ + return self._cancel 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): - return msg + """@return: The current status description messages. + @rtype: string + """ + return self._msg - def set_msg(self, msg): - self._msg = msg + def set_msg(self, 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__': @@ -149,17 +182,17 @@ if __name__ == '__main__': s = LongOpStatus("msg", 100, 10) def heartbeat(): - print "heartbeat ", s.estimated_secs_to_complete() + print "heartbeat ", s.estimated_secs_to_complete() def end(): - print "end" + print "end" s.connect('op-heartbeat', heartbeat) s.connect('op-end', end) - for i in xrange(0,99): - time.sleep(0.1) - s.heartbeat() + for i in xrange(0, 99): + time.sleep(0.1) + s.heartbeat() s.end() diff --git a/src/GrampsDb/__init__.py b/src/GrampsDb/__init__.py index 12c02a6aa..f64c3ec30 100644 --- a/src/GrampsDb/__init__.py +++ b/src/GrampsDb/__init__.py @@ -50,3 +50,6 @@ from _GrampsDBCallback import GrampsDBCallback from _DbUtils import * import _GrampsDbConst as GrampsDbConst + +from _LongOpStatus import LongOpStatus + diff --git a/src/ProgressDialog.py b/src/ProgressDialog.py new file mode 100644 index 000000000..cf710b80f --- /dev/null +++ b/src/ProgressDialog.py @@ -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('%s' % 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' + \ No newline at end of file