source: orange/Orange/OrangeWidgets/OWDatabasesUpdate.py @ 11505:41b983344153

Revision 11505:41b983344153, 26.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 12 months ago (diff)

Fixed 'Databases Update' widget progress bar initialization.

RevLine 
[11436]1from __future__ import with_statement
2
3import os
4import sys
5
6from datetime import datetime
[11483]7from functools import partial
[11484]8from collections import namedtuple
[11436]9
[11483]10from PyQt4.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
[11436]11
[11437]12from Orange.utils import serverfiles, environ
[11436]13from Orange.utils.serverfiles import sizeformat as sizeof_fmt
14
[8042]15from OWWidget import *
[11483]16
17from OWConcurrent import Task, ThreadExecutor, methodinvoke
[8042]18
[11436]19import OWGUIEx
[8042]20
21
[11484]22#: Update file item states
23AVAILABLE, CURRENT, OUTDATED, DEPRECATED = range(4)
24
25_icons_dir = os.path.join(environ.canvas_install_dir, "icons")
26
27
28def icon(name):
29    return QIcon(os.path.join(_icons_dir, name))
30
31
[8042]32class ItemProgressBar(QProgressBar):
[11437]33    """Progress Bar with and `advance()` slot.
[11436]34    """
[11484]35    @Slot()
[8042]36    def advance(self):
[11484]37        """
38        Advance the progress bar by 1
39        """
[8042]40        self.setValue(self.value() + 1)
[11436]41
42
[11484]43class UpdateOptionButton(QToolButton):
44    def event(self, event):
45        if event.type() == QEvent.Wheel:
46            # QAbstractButton automatically accepts all mouse events (in
47            # event method) for disabled buttons. This can prevent scrolling
48            # in a scroll area when a disabled button scrolls under the
49            # mouse.
50            event.ignore()
51            return False
52        else:
53            return QToolButton.event(self, event)
[11436]54
55
[8042]56class UpdateOptionsWidget(QWidget):
[11437]57    """
58    A Widget with download/update/remove options.
59    """
[11484]60    #: Install/update button was clicked
61    installClicked = Signal()
62    #: Remove button was clicked.
63    removeClicked = Signal()
64
65    def __init__(self, state=AVAILABLE, parent=None):
66        QWidget.__init__(self, parent)
[8042]67        layout = QHBoxLayout()
68        layout.setSpacing(1)
69        layout.setContentsMargins(1, 1, 1, 1)
[11484]70        self.installButton = UpdateOptionButton(self)
71        self.installButton.setIcon(icon("update.png"))
72        self.installButton.setToolTip("Download")
[11436]73
[11484]74        self.removeButton = UpdateOptionButton(self)
[11436]75        self.removeButton.setIcon(icon("delete.png"))
[8042]76        self.removeButton.setToolTip("Remove from system")
[11436]77
[11484]78        self.installButton.clicked.connect(self.installClicked)
79        self.removeButton.clicked.connect(self.removeClicked)
80
81        layout.addWidget(self.installButton)
82        layout.addWidget(self.removeButton)
83        self.setLayout(layout)
[11436]84
[8042]85        self.setMaximumHeight(30)
86
[11484]87        self.state = -1
88        self.setState(state)
89
90    def setState(self, state):
91        """
92        Set the current update state for the widget (AVAILABLE,
93        CURRENT, OUTDATED or DEPRECTED).
94
95        """
96        if self.state != state:
97            self.state = state
98            self._update()
99
100    def _update(self):
101        if self.state == AVAILABLE:
102            self.installButton.setIcon(icon("update.png"))
103            self.installButton.setToolTip("Download")
104            self.installButton.setEnabled(True)
105            self.removeButton.setEnabled(False)
106        elif self.state == CURRENT:
107            self.installButton.setIcon(icon("update1.png"))
108            self.installButton.setToolTip("Update")
109            self.installButton.setEnabled(False)
[8042]110            self.removeButton.setEnabled(True)
[11484]111        elif self.state == OUTDATED:
112            self.installButton.setIcon(icon("update1.png"))
113            self.installButton.setToolTip("Update")
114            self.installButton.setEnabled(True)
[8042]115            self.removeButton.setEnabled(True)
[11484]116        elif self.state == DEPRECATED:
117            self.installButton.setIcon(icon("update.png"))
118            self.installButton.setToolTip("")
119            self.installButton.setEnabled(False)
[8042]120            self.removeButton.setEnabled(True)
[11437]121        else:
[11484]122            raise ValueError("Invalid state %r" % self._state)
[8042]123
124
125class UpdateTreeWidgetItem(QTreeWidgetItem):
[11484]126    """
127    A QTreeWidgetItem for displaying an UpdateItem.
[11436]128
[11484]129    :param UpdateItem item:
130        The update item for display.
131
132    """
133    STATE_STRINGS = {0: "not downloaded",
134                     1: "downloaded, current",
135                     2: "downloaded, needs update",
136                     3: "obsolete"}
137
138    #: A role for the state item data.
139    StateRole = OWGUI.OrangeUserRole.next()
140
141    # QTreeWidgetItem stores the DisplayRole and EditRole as the same role,
142    # so we can't use EditRole to store the actual item data, instead we use
143    # custom role.
144
145    #: A custom edit role for the item's data
146    EditRole2 = OWGUI.OrangeUserRole.next()
147
148    def __init__(self, item):
149        QTreeWidgetItem.__init__(self, type=QTreeWidgetItem.UserType)
150
151        self.item = None
152        self.setUpdateItem(item)
153
154    def setUpdateItem(self, item):
155        """
156        Set the update item for display.
157
158        :param UpdateItem item:
159            The update item for display.
160
161        """
162        self.item = item
163
164        self.setData(0, UpdateTreeWidgetItem.StateRole, item.state)
165
166        self.setData(1, Qt.DisplayRole, item.title)
167        self.setData(1, self.EditRole2, item.title)
168
169        self.setData(2, Qt.DisplayRole, sizeof_fmt(item.size))
170        self.setData(2, self.EditRole2, item.size)
171
172        if item.latest is not None:
173            self.setData(3, Qt.DisplayRole, item.latest.date().isoformat())
174            self.setData(3, self.EditRole2, item.latest)
[8042]175        else:
[11484]176            self.setData(3, Qt.DisplayRole, "N/A")
177            self.setData(3, self.EditRole2, datetime.datetime())
[11436]178
[11484]179        self._updateToolTip()
[11436]180
[11484]181    def _updateToolTip(self):
182        state_str = self.STATE_STRINGS[self.item.state]
183        tooltip = ("State: %s\nTags: %s" %
184                   (state_str,
185                    ", ".join(tag for tag in self.item.tags
186                              if not tag.startswith("#"))))
[11436]187
[11484]188        if self.item.state in [CURRENT, OUTDATED, DEPRECATED]:
[11437]189            tooltip += ("\nFile: %s" %
[11484]190                        serverfiles.localpath(self.item.domain,
191                                              self.item.filename))
192        for i in range(1, 4):
[8042]193            self.setToolTip(i, tooltip)
[11436]194
[11484]195    def __lt__(self, other):
196        widget = self.treeWidget()
197        column = widget.sortColumn()
198        if column == 0:
199            role = UpdateTreeWidgetItem.StateRole
200        else:
201            role = self.EditRole2
[11436]202
[11484]203        left = self.data(column, role).toPyObject()
204        right = other.data(column, role).toPyObject()
205        return left < right
[8042]206
[11436]207
[11484]208class UpdateOptionsItemDelegate(QStyledItemDelegate):
209    """
210    An item delegate for the updates tree widget.
[11436]211
[11484]212    .. note: Must be a child of a QTreeWidget.
[8042]213
[11484]214    """
[8042]215    def sizeHint(self, option, index):
[11484]216        size = QStyledItemDelegate.sizeHint(self,  option, index)
[8042]217        parent = self.parent()
218        item = parent.itemFromIndex(index)
219        widget = parent.itemWidget(item, 0)
220        if widget:
[11436]221            size = QSize(size.width(), widget.sizeHint().height() / 2)
[8042]222        return size
[11436]223
224
[11484]225UpdateItem = namedtuple(
226    "UpdateItem",
227    ["domain",
228     "filename",
229     "state",  # Item state flag
230     "title",  # Item title (on server is available else local)
231     "size",  # Item size in bytes (on server if available else local)
232     "latest",  # Latest item date (on server), can be None
233     "local",  # Local item date, can be None
234     "tags",  # Item tags (on server if available else local)
235     "info_local",
236     "info_server"]
237)
238
239ItemInfo = namedtuple(
240    "ItemInfo",
241    ["domain",
242     "filename",
243     "title",
244     "time",  # datetime.datetime
245     "size",  # size in bytes
246     "tags"]
247)
248
249
250def UpdateItem_match(item, string):
251    """
252    Return `True` if the `UpdateItem` item contains a string in tags
253    or in the title.
254
255    """
256    string = string.lower()
257    return any(string.lower() in tag.lower()
258               for tag in item.tags + [item.title])
259
260
261def item_state(info_local, info_server):
262    """
263    Return the item state (AVAILABLE, ...) based on it's local and server side
264    `ItemInfo` instances.
265
266    """
267    if info_server is None:
268        return DEPRECATED
269
270    if info_local is None:
271        return AVAILABLE
272
273    if info_local.time < info_server.time:
274        return OUTDATED
275    else:
276        return CURRENT
277
278
279DATE_FMT_1 = "%Y-%m-%d %H:%M:%S.%f"
280DATE_FMT_2 = "%Y-%m-%d %H:%M:%S"
281
282
283def info_dict_to_item_info(domain, filename, item_dict):
284    """
285    Return an `ItemInfo` instance based on `item_dict` as returned by
286    ``serverfiles.info(domain, filename)``
287
288    """
289    time = item_dict["datetime"]
290    try:
291        time = datetime.strptime(time, DATE_FMT_1)
292    except ValueError:
293        time = datetime.strptime(time, DATE_FMT_2)
294
295    title = item_dict["title"]
296    if not title:
297        title = filename
298
299    size = int(item_dict["size"])
300    tags = item_dict["tags"]
301    return ItemInfo(domain, filename, title, time, size, tags)
302
303
304def update_item_from_info(domain, filename, info_server, info_local):
305    """
306    Return a `UpdateItem` instance for `domain`, `fileanme` based on
307    the local and server side `ItemInfo` instances `info_server` and
308    `info_local`.
309
310    """
311    latest, local, title, tags, size = None, None, None, None, None
312
313    if info_server is not None:
314        info_server = info_dict_to_item_info(domain, filename, info_server)
315        latest = info_server.time
316        tags = info_server.tags
317        title = info_server.title
318        size = info_server.size
319
320    if info_local is not None:
321        info_local = info_dict_to_item_info(domain, filename, info_local)
322        local = info_local.time
323
324        if info_server is None:
325            tags = info_local.tags
326            title = info_local.title
327            size = info_local.size
328
329    state = item_state(info_local, info_server)
330
331    return UpdateItem(domain, filename, state, title, size, latest, local,
332                      tags, info_server, info_local)
333
334
335def join_info_list(domain, files_local, files_server):
336    filenames = set(files_local.keys()).union(files_server.keys())
337    for filename in sorted(filenames):
338        info_server = files_server.get(filename, None)
339        info_local = files_local.get(filename, None)
340        yield update_item_from_info(domain, filename, info_server, info_local)
341
342
343def join_info_dict(local, server):
344    domains = set(local.keys()).union(server.keys())
345    for domain in sorted(domains):
346        files_local = local.get(domain, {})
347        files_server = server.get(domain, {})
348
349        for item in join_info_list(domain, files_local, files_server):
350            yield item
351
352
353def special_tags(item):
354    """
355    Return a dictionary of special tags in an UpdateItem instance (special
356    tags are the ones starting with #).
357
358    """
359    return dict([tuple(tag.split(":")) for tag in item.tags
360                 if tag.startswith("#") and ":" in tag])
361
362
[8042]363def retrieveFilesList(serverFiles, domains=None, advance=lambda: None):
[11436]364    """
365    Retrieve and return serverfiles.allinfo for all domains.
366    """
[8042]367    domains = serverFiles.listdomains() if domains is None else domains
368    advance()
369    serverInfo = dict([(dom, serverFiles.allinfo(dom)) for dom in domains])
370    advance()
371    return serverInfo
[11436]372
373
[8042]374class OWDatabasesUpdate(OWWidget):
[11436]375    def __init__(self, parent=None, signalManager=None,
376                 name="Databases update", wantCloseButton=False,
377                 searchString="", showAll=True, domains=None,
378                 accessCode=""):
[11484]379        OWWidget.__init__(self, parent, signalManager, name, wantMainArea=False)
[8042]380        self.searchString = searchString
381        self.accessCode = accessCode
382        self.showAll = showAll
383        self.domains = domains
[11437]384        self.serverFiles = serverfiles.ServerFiles()
[11484]385
386        box = OWGUI.widgetBox(self.controlArea, orientation="horizontal")
[11436]387
388        self.lineEditFilter = \
389            OWGUIEx.lineEditHint(box, self, "searchString", "Filter",
390                                 caseSensitive=False,
391                                 delimiters=" ",
392                                 matchAnywhere=True,
393                                 listUpdateCallback=self.SearchUpdate,
394                                 callbackOnType=True,
395                                 callback=self.SearchUpdate)
[8042]396
[11484]397        box = OWGUI.widgetBox(self.controlArea, "Files")
[8042]398        self.filesView = QTreeWidget(self)
[11437]399        self.filesView.setHeaderLabels(["Options", "Title", "Size",
400                                        "Last Updated"])
[8042]401        self.filesView.setRootIsDecorated(False)
[11484]402        self.filesView.setUniformRowHeights(True)
[8042]403        self.filesView.setSelectionMode(QAbstractItemView.NoSelection)
404        self.filesView.setSortingEnabled(True)
[11484]405        self.filesView.sortItems(1, Qt.AscendingOrder)
406        self.filesView.setItemDelegateForColumn(
407            0, UpdateOptionsItemDelegate(self.filesView))
408
409        QObject.connect(self.filesView.model(),
410                        SIGNAL("layoutChanged()"),
411                        self.SearchUpdate)
[8042]412        box.layout().addWidget(self.filesView)
413
[11484]414        box = OWGUI.widgetBox(self.controlArea, orientation="horizontal")
[11436]415        OWGUI.button(box, self, "Update all local files",
416                     callback=self.UpdateAll,
417                     tooltip="Update all updatable files")
418        OWGUI.button(box, self, "Download filtered",
419                     callback=self.DownloadFiltered,
420                     tooltip="Download all filtered files shown")
[11483]421        OWGUI.button(box, self, "Cancel", callback=self.Cancel,
422                     tooltip="Cancel scheduled downloads/updates.")
[8042]423        OWGUI.rubber(box)
[11436]424        OWGUI.lineEdit(box, self, "accessCode", "Access Code",
425                       orientation="horizontal",
426                       callback=self.RetrieveFilesList)
427        self.retryButton = OWGUI.button(box, self, "Retry",
428                                        callback=self.RetrieveFilesList)
[8042]429        self.retryButton.hide()
[11484]430        box = OWGUI.widgetBox(self.controlArea, orientation="horizontal")
[8042]431        OWGUI.rubber(box)
432        if wantCloseButton:
[11436]433            OWGUI.button(box, self, "Close",
434                         callback=self.accept,
435                         tooltip="Close")
[8042]436
437        self.infoLabel = QLabel()
438        self.infoLabel.setAlignment(Qt.AlignCenter)
[11436]439
[11484]440        self.controlArea.layout().addWidget(self.infoLabel)
[8042]441        self.infoLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
442
443        self.updateItems = []
[11436]444
[8042]445        self.resize(800, 600)
[11436]446
[11483]447        self.progress = ProgressState(self, maximum=3)
448        self.progress.valueChanged.connect(self._updateProgress)
449        self.progress.rangeChanged.connect(self._updateProgress)
450        self.executor = ThreadExecutor(
451            threadPool=QThreadPool(maxThreadCount=2)
452        )
453
454        task = Task(self, function=self.RetrieveFilesList)
455        task.exceptionReady.connect(self.HandleError)
456        task.start()
457
458        self._tasks = []
[11505]459        self._haveProgress = False
[11436]460
[8042]461    def RetrieveFilesList(self):
[11483]462        self.progress.setRange(0, 3)
[11437]463        self.serverFiles = serverfiles.ServerFiles(access_code=self.accessCode)
[11483]464
465        task = Task(function=partial(retrieveFilesList, self.serverFiles,
466                                     self.domains,
467                                     methodinvoke(self.progress, "advance")))
468
469        task.resultReady.connect(self.SetFilesList)
470        task.exceptionReady.connect(self.HandleError)
471
472        self.executor.submit(task)
[11436]473
[8042]474        self.setEnabled(False)
[11436]475
[8042]476    def SetFilesList(self, serverInfo):
[11484]477        """
478        Set the files to show.
479        """
[8042]480        self.setEnabled(True)
[11436]481
[11484]482        domains = serverInfo.keys()
483        if not domains:
484            if self.domains:
485                domains = self.domains
486            else:
487                domains = serverfiles.listdomains()
488
489        localInfo = dict([(dom, serverfiles.allinfo(dom)) for dom in domains])
490
491        all_tags = set()
492
493        self.filesView.clear()
[8042]494        self.updateItems = []
[11436]495
[11484]496        for item in join_info_dict(localInfo, serverInfo):
497            tree_item = UpdateTreeWidgetItem(item)
498            options_widget = UpdateOptionsWidget(item.state)
499            options_widget.item = item
[11436]500
[11484]501            # Connect the actions to the appropriate methods
502            options_widget.installClicked.connect(
503                partial(self.SubmitDownloadTask, item.domain, item.filename)
504            )
505            options_widget.removeClicked.connect(
506                partial(self.SubmitRemoveTask, item.domain, item.filename)
507            )
[11436]508
[11484]509            self.updateItems.append((item, tree_item, options_widget))
510            all_tags.update(item.tags)
[11436]511
[11484]512        self.filesView.addTopLevelItems(
513            [tree_item for _, tree_item, _ in self.updateItems]
514        )
515
516        for item, tree_item, options_widget in self.updateItems:
517            self.filesView.setItemWidget(tree_item, 0, options_widget)
518
[11483]519        self.progress.advance()
520
[11437]521        for column in range(4):
522            whint = self.filesView.sizeHintForColumn(column)
523            width = min(whint, 400)
524            self.filesView.setColumnWidth(column, width)
525
[11484]526        self.lineEditFilter.setItems([hint for hint in sorted(all_tags)
[11436]527                                      if not hint.startswith("#")])
[8042]528        self.SearchUpdate()
529        self.UpdateInfoLabel()
[11436]530
[11483]531        self.progress.setRange(0, 0)
532
533    def HandleError(self, exception):
534        if isinstance(exception, IOError):
[11436]535            self.error(0,
536                       "Could not connect to server! Press the Retry "
537                       "button to try again.")
[8042]538            self.SetFilesList({})
539        else:
[11483]540            sys.excepthook(type(exception), exception.args, None)
541            self.progress.setRange(0, 0)
[8042]542            self.setEnabled(True)
543
544    def UpdateInfoLabel(self):
[11484]545        local = [item for item, _, _ in self.updateItems
546                 if item.state != AVAILABLE]
547        onServer = [item for item, _, _ in self.updateItems]
548
549        size = sum(float(special_tags(item).get("#uncompressed", item.size))
[11436]550                   for item in local)
[11484]551
552        sizeOnServer = sum(float(item.size) for item, _, _ in self.updateItems)
[11436]553
[8042]554        if self.showAll:
[11436]555
556            text = ("%i items, %s (data on server: %i items, %s)" %
557                    (len(local),
558                     sizeof_fmt(size),
559                     len(onServer),
560                     sizeof_fmt(sizeOnServer)))
[8042]561        else:
[11436]562            text = "%i items, %s" % (len(local), sizeof_fmt(size))
563
564        self.infoLabel.setText(text)
565
[8042]566    def UpdateAll(self):
[11484]567        for item, _, _ in self.updateItems:
568            if item.state == OUTDATED:
569                self.SubmitDownloadTask(item.domain, item.filename)
[11436]570
[8042]571    def DownloadFiltered(self):
[11484]572        # TODO: submit items in the order shown.
573        for item, tree_item, _ in self.updateItems:
574            if not tree_item.isHidden() and item.state in \
575                    [AVAILABLE, OUTDATED]:
576                self.SubmitDownloadTask(item.domain, item.filename)
[8042]577
578    def SearchUpdate(self, searchString=None):
[11436]579        strings = unicode(self.lineEditFilter.text()).split()
[11484]580        for item, tree_item, _ in self.updateItems:
581            hide = not all(UpdateItem_match(item, string)
582                           for string in strings)
583            tree_item.setHidden(hide)
[11436]584
[11483]585    def SubmitDownloadTask(self, domain, filename):
586        """
587        Submit the (domain, filename) to be downloaded/updated.
588        """
[11484]589        index = self.updateItemIndex(domain, filename)
590        _, tree_item, opt_widget = self.updateItems[index]
[11483]591
592        if self.accessCode:
593            sf = serverfiles.ServerFiles(access_code=self.accessCode)
594        else:
595            sf = serverfiles.ServerFiles()
596
597        task = DownloadTask(domain, filename, sf)
598
[11484]599        self.executor.submit(task)
[11483]600
601        self.progress.adjustRange(0, 100)
602
603        pb = ItemProgressBar(self.filesView)
604        pb.setRange(0, 100)
605        pb.setTextVisible(False)
606
607        task.advanced.connect(pb.advance)
608        task.advanced.connect(self.progress.advance)
609        task.finished.connect(pb.hide)
610        task.finished.connect(self.onDownloadFinished, Qt.QueuedConnection)
611        task.exception.connect(self.onDownloadError, Qt.QueuedConnection)
612
[11484]613        self.filesView.setItemWidget(tree_item, 2, pb)
[11483]614
615        # Clear the text so it does not show behind the progress bar.
[11484]616        tree_item.setData(2, Qt.DisplayRole, "")
[11483]617        pb.show()
618
[11484]619        # Disable the options widget
620        opt_widget.setEnabled(False)
[11483]621        self._tasks.append(task)
622
623    def EndDownloadTask(self, task):
624        future = task.future()
[11484]625        index = self.updateItemIndex(task.domain, task.filename)
626        item, tree_item, opt_widget = self.updateItems[index]
[11483]627
[11484]628        self.filesView.removeItemWidget(tree_item, 2)
629        opt_widget.setEnabled(True)
[11483]630
631        if future.cancelled():
632            # Restore the previous state
[11484]633            tree_item.setUpdateItem(item)
634            opt_widget.setState(item.state)
[11483]635
636        elif future.exception():
[11484]637            tree_item.setUpdateItem(item)
638            opt_widget.setState(item.state)
639
640            # Show the exception string in the size column.
641            tree_item.setData(2, Qt.DisplayRole,
[11483]642                         QVariant("Error occurred while downloading:" +
643                                  str(future.exception())))
[11484]644
[11483]645        else:
[11484]646            # get the new updated info dict and replace the the old item
647            info = serverfiles.info(item.domain, item.filename)
648            new_item = update_item_from_info(item.domain, item.filename,
649                                             info, info)
650
651            self.updateItems[index] = (new_item, tree_item, opt_widget)
652
653            tree_item.setUpdateItem(new_item)
654            opt_widget.setState(new_item.state)
655
[11483]656            self.UpdateInfoLabel()
657
658    def SubmitRemoveTask(self, domain, filename):
659        serverfiles.remove(domain, filename)
[11484]660        index = self.updateItemIndex(domain, filename)
661        item, tree_item, opt_widget = self.updateItems[index]
[11483]662
[11484]663        if item.info_server:
664            new_item = item._replace(state=AVAILABLE, local=None,
665                                      info_local=None)
666        else:
667            new_item = item._replace(local=None, info_local=None)
668            # Disable the options widget. No more actions can be performed
669            # for the item.
670            opt_widget.setEnabled(False)
671
672        tree_item.setUpdateItem(new_item)
673        opt_widget.setState(new_item.state)
674        self.updateItems[index] = (new_item, tree_item, opt_widget)
[11483]675
676        self.UpdateInfoLabel()
677
678    def Cancel(self):
679        """
680        Cancel all pending update/download tasks (that have not yet started).
681        """
682        for task in self._tasks:
[11484]683            task.future().cancel()
[11483]684
685    def onDeleteWidget(self):
686        self.Cancel()
687        self.executor.shutdown(wait=False)
688        OWBaseWidget.onDeleteWidget(self)
689
690    def onDownloadFinished(self):
691        assert QThread.currentThread() is self.thread()
692        for task in list(self._tasks):
693            future = task.future()
694            if future.done():
695                self.EndDownloadTask(task)
696                self._tasks.remove(task)
697
698        if not self._tasks:
699            # Clear/reset the overall progress
700            self.progress.setRange(0, 0)
701
702    def onDownloadError(self, exc_info):
703        sys.excepthook(*exc_info)
704
[11484]705    def updateItemIndex(self, domain, filename):
706        for i, (item, _, _) in enumerate(self.updateItems):
707            if item.domain == domain and item.filename == filename:
708                return i
709        raise ValueError("%r, %r not in update list" % (domain, filename))
[11483]710
711    def _updateProgress(self, *args):
712        rmin, rmax = self.progress.range()
713        if rmin != rmax:
[11505]714            if not self._haveProgress:
715                self._haveProgress = True
[11483]716                self.progressBarInit()
717
718            self.progressBarSet(self.progress.ratioCompleted() * 100,
719                                processEventsFlags=None)
720        if rmin == rmax:
[11505]721            self._haveProgress = False
[11483]722            self.progressBarFinished()
723
724
725class ProgressState(QObject):
726    valueChanged = Signal(int)
727    rangeChanged = Signal(int, int)
728    textChanged = Signal(str)
729    started = Signal()
730    finished = Signal()
731
732    def __init__(self, parent=None, minimum=0, maximum=0, text="", value=0):
733        QObject.__init__(self, parent)
734
735        self._minimum = minimum
736        self._maximum = max(maximum, minimum)
737        self._text = text
738        self._value = value
739
740    @Slot(int, int)
741    def setRange(self, minimum, maximum):
742        maximum = max(maximum, minimum)
743
744        if self._minimum != minimum or self._maximum != maximum:
745            self._minimum = minimum
746            self._maximum = maximum
747            self.rangeChanged.emit(minimum, maximum)
748
749            # Adjust the value to fit in the range
750            newvalue = min(max(self._value, minimum), maximum)
751            if newvalue != self._value:
752                self.setValue(newvalue)
753
754    def range(self):
755        return self._minimum, self._maximum
756
757    @Slot(int)
758    def setValue(self, value):
759        if self._value != value and value >= self._minimum and \
760                value <= self._maximum:
761            self._value = value
762            self.valueChanged.emit(value)
763
764    def value(self):
765        return self._value
766
767    @Slot(str)
768    def setText(self, text):
769        if self._text != text:
770            self._text = text
771            self.textChanged.emit(text)
772
773    def text(self):
774        return self._text
775
776    @Slot()
777    @Slot(int)
778    def advance(self, value=1):
779        self.setValue(self._value + value)
780
781    def adjustRange(self, dmin, dmax):
782        self.setRange(self._minimum + dmin, self._maximum + dmax)
783
784    def ratioCompleted(self):
785        span = self._maximum - self._minimum
786        if span < 1e-3:
787            return 0.0
788
789        return min(max(float(self._value - self._minimum) / span, 0.0), 1.0)
790
791
792class DownloadTask(Task):
793    advanced = Signal()
794    exception = Signal(tuple)
795
796    def __init__(self, domain, filename, serverfiles, parent=None):
797        Task.__init__(self, parent)
798        self.filename = filename
799        self.domain = domain
800        self.serverfiles = serverfiles
801        self._interrupt = False
802
803    def interrupt(self):
804        """
805        Interrupt the download.
806        """
807        self._interrupt = True
808
809    def _advance(self):
810        self.advanced.emit()
811        if self._interrupt:
812            raise KeyboardInterrupt
813
814    def run(self):
815        try:
816            serverfiles.download(self.domain, self.filename, self.serverfiles,
817                                 callback=self._advance)
818        except Exception:
819            self.exception.emit(sys.exc_info())
820
[11436]821
[8042]822if __name__ == "__main__":
823    app = QApplication(sys.argv)
824    w = OWDatabasesUpdate(wantCloseButton=True)
825    w.show()
826    w.exec_()
Note: See TracBrowser for help on using the repository browser.