source: orange/Orange/OrangeWidgets/OWDatabasesUpdate.py @ 11740:47e4c2f7265c

Revision 11740:47e4c2f7265c, 27.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 6 months ago (diff)

Fixed cancel button enabled state updates.

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