""" 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)