diff options
| author | kali <kali@leap.se> | 2013-01-30 06:52:59 +0900 | 
|---|---|---|
| committer | kali <kali@leap.se> | 2013-01-30 06:52:59 +0900 | 
| commit | 570f84756b3d1f689a172a3ff0c55abf6a60b9dd (patch) | |
| tree | 0f262acacf35743664c6408edbafe6ba6f119d14 /src/leap/gui/progress.py | |
| parent | e60abdcb796ad9e2c44e9b37d3a68e7f159c035c (diff) | |
| parent | 10a2303fe2d21999bce56940daecb78576f5b741 (diff) | |
Merge branch 'pre-release-0.2.0' into release-0.2.0
This merge contains today's state of develop branch,
minus soledad and email components.
Diffstat (limited to 'src/leap/gui/progress.py')
| -rw-r--r-- | src/leap/gui/progress.py | 488 | 
1 files changed, 488 insertions, 0 deletions
| diff --git a/src/leap/gui/progress.py b/src/leap/gui/progress.py new file mode 100644 index 00000000..ca4f6cc3 --- /dev/null +++ b/src/leap/gui/progress.py @@ -0,0 +1,488 @@ +""" +classes used in progress pages +from first run wizard +""" +try: +    from collections import OrderedDict +except ImportError:  # pragma: no cover +    # We must be in 2.6 +    from leap.util.dicts import OrderedDict + +import logging + +from PyQt4 import QtCore +from PyQt4 import QtGui + +from leap.gui.threads import FunThread + +from leap.gui import mainwindow_rc + +ICON_CHECKMARK = ":/images/Dialog-accept.png" +ICON_FAILED = ":/images/Dialog-error.png" +ICON_WAITING = ":/images/Emblem-question.png" + +logger = logging.getLogger(__name__) + + +class ImgWidget(QtGui.QWidget): + +    # XXX move to widgets + +    def __init__(self, parent=None, img=None): +        super(ImgWidget, self).__init__(parent) +        self.pic = QtGui.QPixmap(img) + +    def paintEvent(self, event): +        painter = QtGui.QPainter(self) +        painter.drawPixmap(0, 0, self.pic) + + +class ProgressStep(object): +    """ +    Data model for sequential steps +    to be used in a progress page in +    connection wizard +    """ +    NAME = 0 +    DONE = 1 + +    def __init__(self, stepname, done, index=None): +        """ +        @param step: the name of  the step +        @type step: str +        @param done: whether is completed or not +        @type done: bool +        """ +        self.index = int(index) if index else 0 +        self.name = unicode(stepname) +        self.done = bool(done) + +    @classmethod +    def columns(self): +        return ('name', 'done') + + +class ProgressStepContainer(object): +    """ +    a container for ProgressSteps objects +    access data in the internal dict +    """ + +    def __init__(self): +        self.dirty = False +        self.steps = {} + +    def step(self, identity): +        return self.steps.get(identity, None) + +    def addStep(self, step): +        self.steps[step.index] = step + +    def removeStep(self, step): +        if step and self.steps.get(step.index, None): +            del self.steps[step.index] +            del step +            self.dirty = True + +    def removeAllSteps(self): +        for item in iter(self): +            self.removeStep(item) + +    @property +    def columns(self): +        return ProgressStep.columns() + +    def __len__(self): +        return len(self.steps) + +    def __iter__(self): +        for step in self.steps.values(): +            yield step + + +class StepsTableWidget(QtGui.QTableWidget): +    """ +    initializes a TableWidget +    suitable for our display purposes, like removing +    header info and grid display +    """ + +    def __init__(self, parent=None): +        super(StepsTableWidget, self).__init__(parent=parent) + +        # remove headers and all edit/select behavior +        self.horizontalHeader().hide() +        self.verticalHeader().hide() +        self.setEditTriggers( +            QtGui.QAbstractItemView.NoEditTriggers) +        self.setSelectionMode( +            QtGui.QAbstractItemView.NoSelection) +        width = self.width() + +        # WTF? Here init width is 100... +        # but on populating is 456... :( +        #logger.debug('init table. width=%s' % width) + +        # XXX do we need this initial? +        self.horizontalHeader().resizeSection(0, width * 0.7) + +        # this disables the table grid. +        # we should add alignment to the ImgWidget (it's top-left now) +        self.setShowGrid(False) +        self.setFocusPolicy(QtCore.Qt.NoFocus) +        #self.setStyleSheet("QTableView{outline: 0;}") + +        # XXX change image for done to rc + +        # Note about the "done" status painting: +        # +        # XXX currently we are setting the CellWidget +        # for the whole table on a per-row basis +        # (on add_status_line method on ValidationPage). +        # However, a more generic solution might be +        # to implement a custom Delegate that overwrites +        # the paint method (so it paints a checked tickmark if +        # done is True and some other thing if checking or false). +        # What we have now is quick and works because +        # I'm supposing that on first fail we will +        # go back to previous wizard page to signal the failure. +        # A more generic solution could be used for +        # some failing tests if they are not critical. + + +class WithStepsMixIn(object): +    """ +    This Class is a mixin that can be inherited +    by InlineValidation pages (which will display +    a progress steps widget in the same page as the form) +    or by Validation Pages (which will only display +    the progress steps in the page, below a progress bar widget) +    """ +    STEPS_TIMER_MS = 100 + +    # +    # methods related to worker threads +    # launched for individual checks +    # + +    def setupStepsProcessingQueue(self): +        """ +        should be called from the init method +        of the derived classes +        """ +        self.steps_queue = Queue.Queue() +        self.stepscheck_timer = QtCore.QTimer() +        self.stepscheck_timer.timeout.connect(self.processStepsQueue) +        self.stepscheck_timer.start(self.STEPS_TIMER_MS) +        # we need to keep a reference to child threads +        self.threads = [] + +    def do_checks(self): +        """ +        main entry point for checks. +        it calls _do_checks in derived classes, +        and it expects it to be a generator +        yielding a tuple in the form (("message", progress_int), checkfunction) +        """ + +        # yo dawg, I heard you like checks +        # so I put a __do_checks in your do_checks +        # for calling others' _do_checks + +        def __do_checks(fun=None, queue=None): + +            for checkcase in fun():  # pragma: no cover +                checkmsg, checkfun = checkcase + +                queue.put(checkmsg) +                if checkfun() is False: +                    queue.put("failed") +                    break + +        t = FunThread(fun=partial( +            __do_checks, +            fun=self._do_checks, +            queue=self.steps_queue)) +        if hasattr(self, 'on_checks_validation_ready'): +            t.finished.connect(self.on_checks_validation_ready) +        t.begin() +        self.threads.append(t) + +    def processStepsQueue(self): +        """ +        consume steps queue +        and pass messages +        to the ui updater functions +        """ +        while self.steps_queue.qsize(): +            try: +                status = self.steps_queue.get(0) +                if status == "failed": +                    self.set_failed_icon() +                else: +                    self.onStepStatusChanged(*status) +            except Queue.Empty:  # pragma: no cover +                pass + +    def fail(self, err=None): +        """ +        return failed state +        and send error notification as +        a nice side effect. this function is called from +        the _do_checks check functions returned in the +        generator. +        """ +        wizard = self.wizard() +        senderr = lambda err: wizard.set_validation_error( +            self.current_page, err) +        self.set_undone() +        if err: +            senderr(err) +        return False + +    @QtCore.pyqtSlot() +    def launch_checks(self): +        self.do_checks() + +    # (gui) presentation stuff begins ##################### + +    # slot +    #@QtCore.pyqtSlot(str, int) +    def onStepStatusChanged(self, status, progress=None): +        status = unicode(status) +        if status not in ("head_sentinel", "end_sentinel"): +            self.add_status_line(status) +        if status in ("end_sentinel"): +            #self.checks_finished = True +            self.set_checked_icon() +        if progress and hasattr(self, 'progress'): +            self.progress.setValue(progress) +            self.progress.update() + +    def setupSteps(self): +        self.steps = ProgressStepContainer() +        # steps table widget +        if isinstance(self, QtCore.QObject): +            parent = self +        else: +            parent = None +        self.stepsTableWidget = StepsTableWidget(parent=parent) +        zeros = (0, 0, 0, 0) +        self.stepsTableWidget.setContentsMargins(*zeros) +        self.errors = OrderedDict() + +    def set_error(self, name, error): +        self.errors[name] = error + +    def pop_first_error(self): +        errkey, errval = list(reversed(self.errors.items())).pop() +        del self.errors[errkey] +        return errkey, errval + +    def clean_errors(self): +        self.errors = OrderedDict() + +    def clean_wizard_errors(self, pagename=None): +        if pagename is None:  # pragma: no cover +            pagename = getattr(self, 'prev_page', None) +        if pagename is None:  # pragma: no cover +            return +        #logger.debug('cleaning wizard errors for %s' % pagename) +        self.wizard().set_validation_error(pagename, None) + +    def populateStepsTable(self): +        # from examples, +        # but I guess it's not needed to re-populate +        # the whole table. +        table = self.stepsTableWidget +        table.setRowCount(len(self.steps)) +        columns = self.steps.columns +        table.setColumnCount(len(columns)) + +        for row, step in enumerate(self.steps): +            item = QtGui.QTableWidgetItem(step.name) +            item.setData(QtCore.Qt.UserRole, +                         long(id(step))) +            table.setItem(row, columns.index('name'), item) +            table.setItem(row, columns.index('done'), +                          QtGui.QTableWidgetItem(step.done)) +        self.resizeTable() +        self.update() + +    def clearTable(self): +        # ??? -- not sure what's the difference +        #self.stepsTableWidget.clear() +        self.stepsTableWidget.clearContents() + +    def resizeTable(self): +        # resize first column to ~80% +        table = self.stepsTableWidget +        FIRST_COLUMN_PERCENT = 0.70 +        width = table.width() +        #logger.debug('populate table. width=%s' % width) +        table.horizontalHeader().resizeSection(0, width * FIRST_COLUMN_PERCENT) + +    def set_item_icon(self, img=ICON_CHECKMARK, current=True): +        """ +        mark the last item +        as done +        """ +        # setting cell widget. +        # see note on StepsTableWidget about plans to +        # change this for a better solution. +        if not hasattr(self, 'steps'): +            return +        index = len(self.steps) +        table = self.stepsTableWidget +        _index = index - 1 if current else index - 2 +        table.setCellWidget( +            _index, +            ProgressStep.DONE, +            ImgWidget(img=img)) +        table.update() + +    def set_failed_icon(self): +        self.set_item_icon(img=ICON_FAILED, current=True) + +    def set_checking_icon(self): +        self.set_item_icon(img=ICON_WAITING, current=True) + +    def set_checked_icon(self, current=True): +        self.set_item_icon(current=current) + +    def add_status_line(self, message): +        """ +        adds a new status line +        and mark the next-to-last item +        as done +        """ +        index = len(self.steps) +        step = ProgressStep(message, False, index=index) +        self.steps.addStep(step) +        self.populateStepsTable() +        self.set_checking_icon() +        self.set_checked_icon(current=False) + +    # Sets/unsets done flag +    # for isComplete checks + +    def set_done(self): +        self.done = True +        self.completeChanged.emit() + +    def set_undone(self): +        self.done = False +        self.completeChanged.emit() + +    def is_done(self): +        return self.done + +    # convenience for going back and forth +    # in the wizard pages. + +    def go_back(self): +        self.wizard().back() + +    def go_next(self): +        self.wizard().next() + + +""" +We will use one base class for the intermediate pages +and another one for the in-page validations, both sharing the creation +of the tablewidgets. +The logic of this split comes from where I was trying to solve +the ui update using signals, but now that it's working well with +queues I could join them again. +""" + +import Queue +from functools import partial + + +class InlineValidationPage(QtGui.QWizardPage, WithStepsMixIn): + +    def __init__(self, parent=None): +        super(InlineValidationPage, self).__init__(parent) +        self.setupStepsProcessingQueue() +        self.done = False + +    # slot + +    @QtCore.pyqtSlot() +    def showStepsFrame(self): +        self.valFrame.show() +        self.update() + +    # progress frame + +    def setupValidationFrame(self): +        qframe = QtGui.QFrame +        valFrame = qframe() +        valFrame.setFrameStyle(qframe.NoFrame) +        valframeLayout = QtGui.QVBoxLayout() +        zeros = (0, 0, 0, 0) +        valframeLayout.setContentsMargins(*zeros) + +        valframeLayout.addWidget(self.stepsTableWidget) +        valFrame.setLayout(valframeLayout) +        self.valFrame = valFrame + + +class ValidationPage(QtGui.QWizardPage, WithStepsMixIn): +    """ +    class to be used as an intermediate +    between two pages in a wizard. +    shows feedback to the user and goes back if errors, +    goes forward if ok. +    initializePage triggers a one shot timer +    that calls do_checks. +    Derived classes should implement +    _do_checks and +    _do_validation +    """ + +    # signals +    stepChanged = QtCore.pyqtSignal([str, int]) + +    def __init__(self, parent=None): +        super(ValidationPage, self).__init__(parent) +        self.setupSteps() +        #self.connect_step_status() + +        layout = QtGui.QVBoxLayout() +        self.progress = QtGui.QProgressBar(self) +        layout.addWidget(self.progress) +        layout.addWidget(self.stepsTableWidget) + +        self.setLayout(layout) +        self.layout = layout + +        self.timer = QtCore.QTimer() +        self.done = False + +        self.setupStepsProcessingQueue() + +    def isComplete(self): +        return self.is_done() + +    ######################## + +    def show_progress(self): +        self.progress.show() +        self.stepsTableWidget.show() + +    def hide_progress(self): +        self.progress.hide() +        self.stepsTableWidget.hide() + +    # pagewizard methods. +    # if overriden, child classes should call super. + +    def initializePage(self): +        self.clean_errors() +        self.clean_wizard_errors() +        self.steps.removeAllSteps() +        self.clearTable() +        self.resizeTable() +        self.timer.singleShot(0, self.do_checks) | 
