source: orange/Orange/OrangeWidgets/OWDatabasesUpdate.py @ 11484:180ff4d9e42e

Revision 11484:180ff4d9e42e, 26.1 KB checked in by Ales Erjavec <ales.erjavec@…>, 12 months ago (diff)

A major rewrite of the 'Databases Update' widget.

Now has better separation between gui and the update logic, the initialization
is significantly faster and the update items can be properly sorted.

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 = []
[11436]459
[8042]460    def RetrieveFilesList(self):
[11483]461        self.progress.setRange(0, 3)
[11437]462        self.serverFiles = serverfiles.ServerFiles(access_code=self.accessCode)
[11483]463
464        task = Task(function=partial(retrieveFilesList, self.serverFiles,
465                                     self.domains,
466                                     methodinvoke(self.progress, "advance")))
467
468        task.resultReady.connect(self.SetFilesList)
469        task.exceptionReady.connect(self.HandleError)
470
471        self.executor.submit(task)
[11436]472
[8042]473        self.setEnabled(False)
[11436]474
[8042]475    def SetFilesList(self, serverInfo):
[11484]476        """
477        Set the files to show.
478        """
[8042]479        self.setEnabled(True)
[11436]480
[11484]481        domains = serverInfo.keys()
482        if not domains:
483            if self.domains:
484                domains = self.domains
485            else:
486                domains = serverfiles.listdomains()
487
488        localInfo = dict([(dom, serverfiles.allinfo(dom)) for dom in domains])
489
490        all_tags = set()
491
492        self.filesView.clear()
[8042]493        self.updateItems = []
[11436]494
[11484]495        for item in join_info_dict(localInfo, serverInfo):
496            tree_item = UpdateTreeWidgetItem(item)
497            options_widget = UpdateOptionsWidget(item.state)
498            options_widget.item = item
[11436]499
[11484]500            # Connect the actions to the appropriate methods
501            options_widget.installClicked.connect(
502                partial(self.SubmitDownloadTask, item.domain, item.filename)
503            )
504            options_widget.removeClicked.connect(
505                partial(self.SubmitRemoveTask, item.domain, item.filename)
506            )
[11436]507
[11484]508            self.updateItems.append((item, tree_item, options_widget))
509            all_tags.update(item.tags)
[11436]510
[11484]511        self.filesView.addTopLevelItems(
512            [tree_item for _, tree_item, _ in self.updateItems]
513        )
514
515        for item, tree_item, options_widget in self.updateItems:
516            self.filesView.setItemWidget(tree_item, 0, options_widget)
517
[11483]518        self.progress.advance()
519
[11437]520        for column in range(4):
521            whint = self.filesView.sizeHintForColumn(column)
522            width = min(whint, 400)
523            self.filesView.setColumnWidth(column, width)
524
[11484]525        self.lineEditFilter.setItems([hint for hint in sorted(all_tags)
[11436]526                                      if not hint.startswith("#")])
[8042]527        self.SearchUpdate()
528        self.UpdateInfoLabel()
[11436]529
[11483]530        self.progress.setRange(0, 0)
531
532    def HandleError(self, exception):
533        if isinstance(exception, IOError):
[11436]534            self.error(0,
535                       "Could not connect to server! Press the Retry "
536                       "button to try again.")
[8042]537            self.SetFilesList({})
538        else:
[11483]539            sys.excepthook(type(exception), exception.args, None)
540            self.progress.setRange(0, 0)
[8042]541            self.setEnabled(True)
542
543    def UpdateInfoLabel(self):
[11484]544        local = [item for item, _, _ in self.updateItems
545                 if item.state != AVAILABLE]
546        onServer = [item for item, _, _ in self.updateItems]
547
548        size = sum(float(special_tags(item).get("#uncompressed", item.size))
[11436]549                   for item in local)
[11484]550
551        sizeOnServer = sum(float(item.size) for item, _, _ in self.updateItems)
[11436]552
[8042]553        if self.showAll:
[11436]554
555            text = ("%i items, %s (data on server: %i items, %s)" %
556                    (len(local),
557                     sizeof_fmt(size),
558                     len(onServer),
559                     sizeof_fmt(sizeOnServer)))
[8042]560        else:
[11436]561            text = "%i items, %s" % (len(local), sizeof_fmt(size))
562
563        self.infoLabel.setText(text)
564
[8042]565    def UpdateAll(self):
[11484]566        for item, _, _ in self.updateItems:
567            if item.state == OUTDATED:
568                self.SubmitDownloadTask(item.domain, item.filename)
[11436]569
[8042]570    def DownloadFiltered(self):
[11484]571        # TODO: submit items in the order shown.
572        for item, tree_item, _ in self.updateItems:
573            if not tree_item.isHidden() and item.state in \
574                    [AVAILABLE, OUTDATED]:
575                self.SubmitDownloadTask(item.domain, item.filename)
[8042]576
577    def SearchUpdate(self, searchString=None):
[11436]578        strings = unicode(self.lineEditFilter.text()).split()
[11484]579        for item, tree_item, _ in self.updateItems:
580            hide = not all(UpdateItem_match(item, string)
581                           for string in strings)
582            tree_item.setHidden(hide)
[11436]583
[11483]584    def SubmitDownloadTask(self, domain, filename):
585        """
586        Submit the (domain, filename) to be downloaded/updated.
587        """
[11484]588        index = self.updateItemIndex(domain, filename)
589        _, tree_item, opt_widget = self.updateItems[index]
[11483]590
591        if self.accessCode:
592            sf = serverfiles.ServerFiles(access_code=self.accessCode)
593        else:
594            sf = serverfiles.ServerFiles()
595
596        task = DownloadTask(domain, filename, sf)
597
[11484]598        self.executor.submit(task)
[11483]599
600        self.progress.adjustRange(0, 100)
601
602        pb = ItemProgressBar(self.filesView)
603        pb.setRange(0, 100)
604        pb.setTextVisible(False)
605
606        task.advanced.connect(pb.advance)
607        task.advanced.connect(self.progress.advance)
608        task.finished.connect(pb.hide)
609        task.finished.connect(self.onDownloadFinished, Qt.QueuedConnection)
610        task.exception.connect(self.onDownloadError, Qt.QueuedConnection)
611
[11484]612        self.filesView.setItemWidget(tree_item, 2, pb)
[11483]613
614        # Clear the text so it does not show behind the progress bar.
[11484]615        tree_item.setData(2, Qt.DisplayRole, "")
[11483]616        pb.show()
617
[11484]618        # Disable the options widget
619        opt_widget.setEnabled(False)
[11483]620        self._tasks.append(task)
621
622    def EndDownloadTask(self, task):
623        future = task.future()
[11484]624        index = self.updateItemIndex(task.domain, task.filename)
625        item, tree_item, opt_widget = self.updateItems[index]
[11483]626
[11484]627        self.filesView.removeItemWidget(tree_item, 2)
628        opt_widget.setEnabled(True)
[11483]629
630        if future.cancelled():
631            # Restore the previous state
[11484]632            tree_item.setUpdateItem(item)
633            opt_widget.setState(item.state)
[11483]634
635        elif future.exception():
[11484]636            tree_item.setUpdateItem(item)
637            opt_widget.setState(item.state)
638
639            # Show the exception string in the size column.
640            tree_item.setData(2, Qt.DisplayRole,
[11483]641                         QVariant("Error occurred while downloading:" +
642                                  str(future.exception())))
[11484]643
[11483]644        else:
[11484]645            # get the new updated info dict and replace the the old item
646            info = serverfiles.info(item.domain, item.filename)
647            new_item = update_item_from_info(item.domain, item.filename,
648                                             info, info)
649
650            self.updateItems[index] = (new_item, tree_item, opt_widget)
651
652            tree_item.setUpdateItem(new_item)
653            opt_widget.setState(new_item.state)
654
[11483]655            self.UpdateInfoLabel()
656
657    def SubmitRemoveTask(self, domain, filename):
658        serverfiles.remove(domain, filename)
[11484]659        index = self.updateItemIndex(domain, filename)
660        item, tree_item, opt_widget = self.updateItems[index]
[11483]661
[11484]662        if item.info_server:
663            new_item = item._replace(state=AVAILABLE, local=None,
664                                      info_local=None)
665        else:
666            new_item = item._replace(local=None, info_local=None)
667            # Disable the options widget. No more actions can be performed
668            # for the item.
669            opt_widget.setEnabled(False)
670
671        tree_item.setUpdateItem(new_item)
672        opt_widget.setState(new_item.state)
673        self.updateItems[index] = (new_item, tree_item, opt_widget)
[11483]674
675        self.UpdateInfoLabel()
676
677    def Cancel(self):
678        """
679        Cancel all pending update/download tasks (that have not yet started).
680        """
681        for task in self._tasks:
[11484]682            task.future().cancel()
[11483]683
684    def onDeleteWidget(self):
685        self.Cancel()
686        self.executor.shutdown(wait=False)
687        OWBaseWidget.onDeleteWidget(self)
688
689    def onDownloadFinished(self):
690        assert QThread.currentThread() is self.thread()
691        for task in list(self._tasks):
692            future = task.future()
693            if future.done():
694                self.EndDownloadTask(task)
695                self._tasks.remove(task)
696
697        if not self._tasks:
698            # Clear/reset the overall progress
699            self.progress.setRange(0, 0)
700
701    def onDownloadError(self, exc_info):
702        sys.excepthook(*exc_info)
703
[11484]704    def updateItemIndex(self, domain, filename):
705        for i, (item, _, _) in enumerate(self.updateItems):
706            if item.domain == domain and item.filename == filename:
707                return i
708        raise ValueError("%r, %r not in update list" % (domain, filename))
[11483]709
710    def _updateProgress(self, *args):
711        rmin, rmax = self.progress.range()
712        if rmin != rmax:
713            if self.progressBarValue <= 0:
714                self.progressBarInit()
715
716            self.progressBarSet(self.progress.ratioCompleted() * 100,
717                                processEventsFlags=None)
718        if rmin == rmax:
719            self.progressBarFinished()
720
721
722class ProgressState(QObject):
723    valueChanged = Signal(int)
724    rangeChanged = Signal(int, int)
725    textChanged = Signal(str)
726    started = Signal()
727    finished = Signal()
728
729    def __init__(self, parent=None, minimum=0, maximum=0, text="", value=0):
730        QObject.__init__(self, parent)
731
732        self._minimum = minimum
733        self._maximum = max(maximum, minimum)
734        self._text = text
735        self._value = value
736
737    @Slot(int, int)
738    def setRange(self, minimum, maximum):
739        maximum = max(maximum, minimum)
740
741        if self._minimum != minimum or self._maximum != maximum:
742            self._minimum = minimum
743            self._maximum = maximum
744            self.rangeChanged.emit(minimum, maximum)
745
746            # Adjust the value to fit in the range
747            newvalue = min(max(self._value, minimum), maximum)
748            if newvalue != self._value:
749                self.setValue(newvalue)
750
751    def range(self):
752        return self._minimum, self._maximum
753
754    @Slot(int)
755    def setValue(self, value):
756        if self._value != value and value >= self._minimum and \
757                value <= self._maximum:
758            self._value = value
759            self.valueChanged.emit(value)
760
761    def value(self):
762        return self._value
763
764    @Slot(str)
765    def setText(self, text):
766        if self._text != text:
767            self._text = text
768            self.textChanged.emit(text)
769
770    def text(self):
771        return self._text
772
773    @Slot()
774    @Slot(int)
775    def advance(self, value=1):
776        self.setValue(self._value + value)
777
778    def adjustRange(self, dmin, dmax):
779        self.setRange(self._minimum + dmin, self._maximum + dmax)
780
781    def ratioCompleted(self):
782        span = self._maximum - self._minimum
783        if span < 1e-3:
784            return 0.0
785
786        return min(max(float(self._value - self._minimum) / span, 0.0), 1.0)
787
788
789class DownloadTask(Task):
790    advanced = Signal()
791    exception = Signal(tuple)
792
793    def __init__(self, domain, filename, serverfiles, parent=None):
794        Task.__init__(self, parent)
795        self.filename = filename
796        self.domain = domain
797        self.serverfiles = serverfiles
798        self._interrupt = False
799
800    def interrupt(self):
801        """
802        Interrupt the download.
803        """
804        self._interrupt = True
805
806    def _advance(self):
807        self.advanced.emit()
808        if self._interrupt:
809            raise KeyboardInterrupt
810
811    def run(self):
812        try:
813            serverfiles.download(self.domain, self.filename, self.serverfiles,
814                                 callback=self._advance)
815        except Exception:
816            self.exception.emit(sys.exc_info())
817
[11436]818
[8042]819if __name__ == "__main__":
820    app = QApplication(sys.argv)
821    w = OWDatabasesUpdate(wantCloseButton=True)
822    w.show()
823    w.exec_()
Note: See TracBrowser for help on using the repository browser.