ProgressBar.py 8.84 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
# -*- coding: utf-8 -*-
"""
$Id$

Copyright 2011 Lars Kruse <devel@sumpfralle.de>

This file is part of PyCAM.

PyCAM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

PyCAM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with PyCAM.  If not, see <http://www.gnu.org/licenses/>.
"""

import os
import time
import datetime

import pycam.Plugins


30

31 32 33
class ProgressBar(pycam.Plugins.PluginBase):

    UI_FILE = "progress_bar.ui"
34
    CATEGORIES = ["System"]
35 36 37 38 39 40

    def setup(self):
        if self.gui:
            box = self.gui.get_object("ProgressBox")
            box.unparent()
            self.core.register_ui("main_window", "Progress", box, 50)
41 42 43 44
            self.core.add_item("progress", lambda: ProgressGTK(self.core,
                    self.gui, self.log))
            show_progress_button = self.gui.get_object(
                    "ShowToolpathProgressButton")
45
            # TODO: move this setting somewhere else or rename it
46 47 48 49 50 51
            self.core.add_item("show_drill_progress",
                    show_progress_button.get_active,
                    show_progress_button.set_active)
        return True

    def teardown(self):
52 53 54
        if self.gui:
            self.core.unregister_ui("main_window",
                    self.gui.get_object("ProgressBox"))
55 56 57 58 59
        self.core.set("progress", None)


class ProgressGTK(object):

60 61
    _PROGRESS_STACK = []

62
    def __init__(self, core, gui, log):
63
        ProgressGTK._PROGRESS_STACK.append(self)
64
        import gtk
65
        self._finished = False
66 67
        self._gtk = gtk
        self._gui = gui
68
        self.log = log
69 70 71 72 73 74 75 76 77 78 79 80
        self.core = core
        self._cancel_requested = False
        self._start_time = 0
        self._multi_maximum = 0
        self._multi_counter = 0
        self._multi_base_text = ""
        self._last_gtk_events_time = None
        self._main_widget = self._gui.get_object("ProgressWidget")
        self._multi_widget = self._gui.get_object("MultipleProgressBar")
        self._cancel_button = self._gui.get_object("ProgressCancelButton")
        self._cancel_button.connect("clicked", self.cancel)
        self._progress_bar = self._gui.get_object("ProgressBar")
81 82
        self._progress_button = self._gui.get_object(
                "ShowToolpathProgressButton")
83 84
        self._start_time = time.time()
        self._progress_button.show()
85 86
        self._last_text = None
        self._last_percent = None
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
        self.update(text="", percent=0)
        self._cancel_button.set_sensitive(True)
        self._progress_button.hide()
        # enable "pulse" mode for a start (in case of unknown ETA)
        self._progress_bar.pulse()
        self._main_widget.show()
        self._multi_widget.hide()
        self._multi_widget.set_text("")
        self._multi_widget.set_fraction(0)
        self.core.emit_event("gui-disable")

    def set_multiple(self, count, base_text=None):
        if base_text:
            self._multi_base_text = base_text
        else:
            self._multi_base_text = ""
103
        self._multi_counter = 0
104 105 106 107 108 109 110 111
        if count > 1:
            self._multi_maximum = count
            self.update_multiple(increment=False)
        else:
            self._multi_maximum = 0

    def update_multiple(self, increment=True):
        if self._multi_maximum <= 1:
112
            self._multi_widget.hide()
113
            return
114
        self._multi_widget.show()
115 116 117 118
        if increment:
            self._multi_counter += 1
            self._progress_bar.set_fraction(0)
        if self._multi_base_text:
119 120
            text = "%s %d/%d" % (self._multi_base_text,
                    self._multi_counter + 1, self._multi_maximum)
121
        else:
122
            text = "%d/%d" % (self._multi_counter + 1, self._multi_maximum)
123
        self._multi_widget.set_text(text)
124 125
        self._multi_widget.set_fraction(min(1.0,
                float(self._multi_counter) / self._multi_maximum))
126 127 128 129

    def disable_cancel(self):
        self._cancel_button.set_sensitive(False)

130
    def cancel(self, widget=None):
131 132 133
        self._cancel_requested = True

    def finish(self):
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
        if self._finished:
            self.log.debug("Called progressbar 'finish' twice: %s" % self)
            return
        ProgressGTK._PROGRESS_STACK.remove(self)
        if ProgressGTK._PROGRESS_STACK:
            # restore the latest state of the previous progress
            current = ProgressGTK._PROGRESS_STACK[-1]
            current.update(text=current._last_text,
                    percent=current._last_percent)
            current.update_multiple(increment=False)
        else:
            # hide the widget
            self._main_widget.hide()
            self._multi_widget.hide()
            widget = self._main_widget
            while widget:
                if hasattr(widget, "resize_children"):
                    widget.resize_children()
                if hasattr(widget, "check_resize"):
                    widget.check_resize()
                widget = widget.get_parent()
            self.core.emit_event("gui-enable")
        self._finished = True
157 158

    def __del__(self):
159 160
        if not self._finished:
            self.finish()
161 162

    def update(self, text=None, percent=None):
163 164 165 166
        if text:
            self._last_text = text
        if percent:
            self._last_percent = percent
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
        if not percent is None:
            percent = min(max(percent, 0.0), 100.0)
            self._progress_bar.set_fraction(percent/100.0)
        if (not percent) and (self._progress_bar.get_fraction() == 0):
            # use "pulse" mode until we reach 1% of the work to be done
            self._progress_bar.pulse()
        # update the GUI
        current_time = time.time()
        # Don't update the GUI more often than once per second.
        # Exception: text-only updates
        # This restriction improves performance and reduces the
        # "snappiness" of the GUI.
        if (self._last_gtk_events_time is None) \
                or text \
                or (self._last_gtk_events_time + 0.5 <= current_time):
            # "estimated time of arrival" text
            time_estimation_suffix = " remaining ..."
            if self._progress_bar.get_fraction() > 0:
                total_fraction = (self._progress_bar.get_fraction() + self._multi_counter) / max(1, self._multi_maximum)
186
                total_fraction = max(0.0, min(total_fraction, 1.0))
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
                eta_full = (time.time() - self._start_time) / total_fraction
                if eta_full > 0:
                    eta_delta = eta_full - (time.time() - self._start_time)
                    eta_delta = int(round(eta_delta))
                    if hasattr(self, "_last_eta_delta"):
                        previous_eta_delta = self._last_eta_delta
                        if eta_delta == previous_eta_delta + 1:
                            # We are currently toggling between two numbers.
                            # We want to avoid screen flicker, thus we just live
                            # with the slight inaccuracy.
                            eta_delta = self._last_eta_delta
                    self._last_eta_delta = eta_delta
                    eta_delta_obj = datetime.timedelta(seconds=eta_delta)
                    eta_text = "%s%s" % (eta_delta_obj, time_estimation_suffix)
                else:
                    eta_text = None
            else:
                eta_text = None
            if not text is None:
                lines = [text]
            else:
                old_lines = self._progress_bar.get_text().split(os.linesep)
                # skip the time estimation line
                lines = [line for line in old_lines
                        if not line.endswith(time_estimation_suffix)]
            if eta_text:
                lines.append(eta_text)
            self._progress_bar.set_text(os.linesep.join(lines))
            # show the "show_tool_button" ("hide" is called in the progress decorator)
            # TODO: move "in_progress" somewhere else
            if self.core.get("toolpath_in_progress"):
                self._progress_button.show()
            while self._gtk.events_pending():
                self._gtk.main_iteration()
            if not text or (self._start_time + 5 < current_time):
                # We don't store the timining if the text was changed.
                # This is especially nice for the snappines during font
                # initialization. This exception is only valid for the first
                # five seconds of the operation.
                self._last_gtk_events_time = current_time
        # return if the user requested a break
        return self._cancel_requested