Ignore:
File:
1 edited

Legend:

Unmodified
Added
Removed
  • Orange/OrangeWidgets/OWDatabasesUpdate.py

    r11437 r11484  
    55 
    66from datetime import datetime 
    7  
    8 import Orange 
     7from functools import partial 
     8from collections import namedtuple 
     9 
     10from PyQt4.QtCore import pyqtSignal as Signal, pyqtSlot as Slot 
    911 
    1012from Orange.utils import serverfiles, environ 
     
    1214 
    1315from OWWidget import * 
    14 from OWConcurrent import * 
     16 
     17from OWConcurrent import Task, ThreadExecutor, methodinvoke 
    1518 
    1619import OWGUIEx 
     20 
     21 
     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)) 
    1730 
    1831 
     
    2033    """Progress Bar with and `advance()` slot. 
    2134    """ 
    22     @pyqtSignature("advance()") 
     35    @Slot() 
    2336    def advance(self): 
     37        """ 
     38        Advance the progress bar by 1 
     39        """ 
    2440        self.setValue(self.value() + 1) 
    2541 
    2642 
    27 class ProgressBarRedirect(QObject): 
    28     def __init__(self, parent, redirect): 
    29         QObject.__init__(self, parent) 
    30         self.redirect = redirect 
    31         self._delay = False 
    32  
    33     @pyqtSignature("advance()") 
    34     def advance(self): 
    35         # delay OWBaseWidget.progressBarSet call, because it calls 
    36         # qApp.processEvents which can result in 'event queue climbing' 
    37         # and max. recursion error if GUI thread gets another advance 
    38         # signal before it finishes with this one 
    39         if not self._delay: 
    40             try: 
    41                 self._delay = True 
    42                 self.redirect.advance() 
    43             finally: 
    44                 self._delay = False 
     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 
    4552        else: 
    46             QTimer.singleShot(10, self.advance) 
    47  
    48 _icons_dir = os.path.join(environ.canvas_install_dir, "icons") 
    49  
    50  
    51 def icon(name): 
    52     return QIcon(os.path.join(_icons_dir, name)) 
     53            return QToolButton.event(self, event) 
    5354 
    5455 
     
    5758    A Widget with download/update/remove options. 
    5859    """ 
    59     def __init__(self, updateCallback, removeCallback, state, *args): 
    60         QWidget.__init__(self, *args) 
    61         self.updateCallback = updateCallback 
    62         self.removeCallback = removeCallback 
     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) 
    6367        layout = QHBoxLayout() 
    6468        layout.setSpacing(1) 
    6569        layout.setContentsMargins(1, 1, 1, 1) 
    66         self.updateButton = QToolButton(self) 
    67         self.updateButton.setIcon(icon("update.png")) 
    68         self.updateButton.setToolTip("Download") 
    69  
    70         self.removeButton = QToolButton(self) 
     70        self.installButton = UpdateOptionButton(self) 
     71        self.installButton.setIcon(icon("update.png")) 
     72        self.installButton.setToolTip("Download") 
     73 
     74        self.removeButton = UpdateOptionButton(self) 
    7175        self.removeButton.setIcon(icon("delete.png")) 
    7276        self.removeButton.setToolTip("Remove from system") 
    7377 
    74         self.connect(self.updateButton, SIGNAL("released()"), 
    75                      self.updateCallback) 
    76         self.connect(self.removeButton, SIGNAL("released()"), 
    77                      self.removeCallback) 
    78  
    79         self.setMaximumHeight(30) 
    80         layout.addWidget(self.updateButton) 
     78        self.installButton.clicked.connect(self.installClicked) 
     79        self.removeButton.clicked.connect(self.removeClicked) 
     80 
     81        layout.addWidget(self.installButton) 
    8182        layout.addWidget(self.removeButton) 
    8283        self.setLayout(layout) 
    83         self.SetState(state) 
    84  
    85     def SetState(self, state): 
    86         self.state = state 
    87         if state == 0: 
    88             self.updateButton.setIcon(icon("update1.png")) 
    89             self.updateButton.setToolTip("Update") 
    90             self.updateButton.setEnabled(False) 
     84 
     85        self.setMaximumHeight(30) 
     86 
     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) 
    91110            self.removeButton.setEnabled(True) 
    92         elif state == 1: 
    93             self.updateButton.setIcon(icon("update1.png")) 
    94             self.updateButton.setToolTip("Update") 
    95             self.updateButton.setEnabled(True) 
     111        elif self.state == OUTDATED: 
     112            self.installButton.setIcon(icon("update1.png")) 
     113            self.installButton.setToolTip("Update") 
     114            self.installButton.setEnabled(True) 
    96115            self.removeButton.setEnabled(True) 
    97         elif state == 2: 
    98             self.updateButton.setIcon(icon("update.png")) 
    99             self.updateButton.setToolTip("Download") 
    100             self.updateButton.setEnabled(True) 
    101             self.removeButton.setEnabled(False) 
    102         elif state == 3: 
    103             self.updateButton.setIcon(icon("update.png")) 
    104             self.updateButton.setToolTip("") 
    105             self.updateButton.setEnabled(False) 
     116        elif self.state == DEPRECATED: 
     117            self.installButton.setIcon(icon("update.png")) 
     118            self.installButton.setToolTip("") 
     119            self.installButton.setEnabled(False) 
    106120            self.removeButton.setEnabled(True) 
    107121        else: 
    108             raise ValueError("Invalid state %r" % state) 
     122            raise ValueError("Invalid state %r" % self._state) 
    109123 
    110124 
    111125class UpdateTreeWidgetItem(QTreeWidgetItem): 
    112     stateDict = {0: "up-to-date", 
    113                  1: "new version available", 
    114                  2: "not downloaded", 
    115                  3: "obsolete"} 
    116  
    117     def __init__(self, master, treeWidget, domain, filename, infoLocal, 
    118                  infoServer, *args): 
    119         dateServer = dateLocal = None 
    120         if infoServer: 
    121             dateServer = datetime.strptime( 
    122                 infoServer["datetime"].split(".")[0], "%Y-%m-%d %H:%M:%S" 
    123             ) 
    124         if infoLocal: 
    125             dateLocal = datetime.strptime( 
    126                 infoLocal["datetime"].split(".")[0], "%Y-%m-%d %H:%M:%S" 
    127             ) 
    128         if not infoLocal: 
    129             self.state = 2 
    130         elif not infoServer: 
    131             self.state = 3 
     126    """ 
     127    A QTreeWidgetItem for displaying an UpdateItem. 
     128 
     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) 
    132175        else: 
    133             self.state = 0 if dateLocal >= dateServer else 1 
    134  
    135         title = infoServer["title"] if infoServer else (infoLocal["title"]) 
    136         tags = infoServer["tags"] if infoServer else infoLocal["tags"] 
    137         specialTags = dict([tuple(tag.split(":")) 
    138                             for tag in tags 
    139                             if tag.startswith("#") and ":" in tag]) 
    140         tags = ", ".join(tag for tag in tags if not tag.startswith("#")) 
    141         self.size = infoServer["size"] if infoServer else infoLocal["size"] 
    142  
    143         size = sizeof_fmt(float(self.size)) 
    144         state = self.stateDict[self.state] 
    145         if self.state == 1: 
    146             state += dateServer.strftime(" (%Y, %b, %d)") 
    147  
    148         QTreeWidgetItem.__init__(self, treeWidget, ["", title, size]) 
    149         if dateServer is not None: 
    150             self.setData(3, Qt.DisplayRole, 
    151                          dateServer.date().isoformat()) 
    152  
    153         self.updateWidget = UpdateOptionsWidget( 
    154             self.StartDownload, self.Remove, self.state, treeWidget 
    155         ) 
    156  
    157         self.treeWidget().setItemWidget(self, 0, self.updateWidget) 
    158         self.updateWidget.show() 
    159         self.master = master 
    160         self.title = title 
    161         self.tags = tags.split(", ") 
    162         self.specialTags = specialTags 
    163         self.domain = domain 
    164         self.filename = filename 
    165         self.UpdateToolTip() 
    166  
    167     def UpdateToolTip(self): 
    168         state = {0: "local, updated", 
    169                  1: "local, needs update", 
    170                  2: "on server, download for local use", 
    171                  3: "obsolete"} 
    172         tooltip = "State: %s\nTags: %s" % (state[self.state], 
    173                                            ", ".join(self.tags)) 
    174         if self.state != 2: 
     176            self.setData(3, Qt.DisplayRole, "N/A") 
     177            self.setData(3, self.EditRole2, datetime.datetime()) 
     178 
     179        self._updateToolTip() 
     180 
     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("#")))) 
     187 
     188        if self.item.state in [CURRENT, OUTDATED, DEPRECATED]: 
    175189            tooltip += ("\nFile: %s" % 
    176                         serverfiles.localpath(self.domain, self.filename)) 
    177         for i in range(1, 5): 
     190                        serverfiles.localpath(self.item.domain, 
     191                                              self.item.filename)) 
     192        for i in range(1, 4): 
    178193            self.setToolTip(i, tooltip) 
    179194 
    180     def StartDownload(self): 
    181         self.updateWidget.removeButton.setEnabled(False) 
    182         self.updateWidget.updateButton.setEnabled(False) 
    183         self.setData(2, Qt.DisplayRole, QVariant("")) 
    184         serverFiles = serverfiles.ServerFiles( 
    185             access_code=self.master.accessCode if self.master.accessCode 
    186             else None 
    187         ) 
    188  
    189         pb = ItemProgressBar(self.treeWidget()) 
    190         pb.setRange(0, 100) 
    191         pb.setTextVisible(False) 
    192  
    193         self.task = AsyncCall(threadPool=QThreadPool.globalInstance()) 
    194  
    195         if not getattr(self.master, "_sum_progressBar", None): 
    196             self.master._sum_progressBar = OWGUI.ProgressBar(self.master, 0) 
    197             self.master._sum_progressBar.in_progress = 0 
    198         master_pb = self.master._sum_progressBar 
    199         master_pb.iter += 100 
    200         master_pb.in_progress += 1 
    201         self._progressBarRedirect = \ 
    202             ProgressBarRedirect(QThread.currentThread(), master_pb) 
    203         QObject.connect(self.task, 
    204                         SIGNAL("advance()"), 
    205                         pb.advance, 
    206                         Qt.QueuedConnection) 
    207         QObject.connect(self.task, 
    208                         SIGNAL("advance()"), 
    209                         self._progressBarRedirect.advance, 
    210                         Qt.QueuedConnection) 
    211         QObject.connect(self.task, 
    212                         SIGNAL("finished(QString)"), 
    213                         self.EndDownload, 
    214                         Qt.QueuedConnection) 
    215         self.treeWidget().setItemWidget(self, 2, pb) 
    216         pb.show() 
    217  
    218         self.task.apply_async(serverfiles.download, 
    219                               args=(self.domain, self.filename, serverFiles), 
    220                               kwargs=dict(callback=self.task.emitAdvance)) 
    221  
    222     def EndDownload(self, exitCode=0): 
    223         self.treeWidget().removeItemWidget(self, 2) 
    224         if str(exitCode) == "Ok": 
    225             self.state = 0 
    226             self.updateWidget.SetState(self.state) 
    227             self.setData(2, Qt.DisplayRole, 
    228                          QVariant(sizeof_fmt(float(self.size)))) 
    229             self.master.UpdateInfoLabel() 
    230             self.UpdateToolTip() 
     195    def __lt__(self, other): 
     196        widget = self.treeWidget() 
     197        column = widget.sortColumn() 
     198        if column == 0: 
     199            role = UpdateTreeWidgetItem.StateRole 
    231200        else: 
    232             self.updateWidget.SetState(1) 
    233             self.setData(2, Qt.DisplayRole, 
    234                          QVariant("Error occurred while downloading:" + 
    235                                   str(exitCode))) 
    236  
    237         master_pb = self.master._sum_progressBar 
    238  
    239         if master_pb and master_pb.in_progress == 1: 
    240             master_pb.finish() 
    241             self.master._sum_progressBar = None 
    242         elif master_pb: 
    243             master_pb.in_progress -= 1 
    244  
    245     def Remove(self): 
    246         serverfiles.remove(self.domain, self.filename) 
    247         self.state = 2 
    248         self.updateWidget.SetState(self.state) 
    249         self.master.UpdateInfoLabel() 
    250         self.UpdateToolTip() 
    251  
    252     def __contains__(self, item): 
    253         return any(item.lower() in tag.lower() 
    254                    for tag in self.tags + [self.title]) 
    255  
    256     def __lt__(self, other): 
    257         return getattr(self, "title", "") < getattr(other, "title", "") 
    258  
    259  
    260 class UpdateItemDelegate(QItemDelegate): 
     201            role = self.EditRole2 
     202 
     203        left = self.data(column, role).toPyObject() 
     204        right = other.data(column, role).toPyObject() 
     205        return left < right 
     206 
     207 
     208class UpdateOptionsItemDelegate(QStyledItemDelegate): 
     209    """ 
     210    An item delegate for the updates tree widget. 
     211 
     212    .. note: Must be a child of a QTreeWidget. 
     213 
     214    """ 
    261215    def sizeHint(self, option, index): 
    262         size = QItemDelegate.sizeHint(self, option, index) 
     216        size = QStyledItemDelegate.sizeHint(self, option, index) 
    263217        parent = self.parent() 
    264218        item = parent.itemFromIndex(index) 
     
    269223 
    270224 
     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 
    271363def retrieveFilesList(serverFiles, domains=None, advance=lambda: None): 
    272364    """ 
     
    285377                 searchString="", showAll=True, domains=None, 
    286378                 accessCode=""): 
    287         OWWidget.__init__(self, parent, signalManager, name) 
     379        OWWidget.__init__(self, parent, signalManager, name, wantMainArea=False) 
    288380        self.searchString = searchString 
    289381        self.accessCode = accessCode 
     
    291383        self.domains = domains 
    292384        self.serverFiles = serverfiles.ServerFiles() 
    293         box = OWGUI.widgetBox(self.mainArea, orientation="horizontal") 
     385 
     386        box = OWGUI.widgetBox(self.controlArea, orientation="horizontal") 
    294387 
    295388        self.lineEditFilter = \ 
     
    302395                                 callback=self.SearchUpdate) 
    303396 
    304         box = OWGUI.widgetBox(self.mainArea, "Files") 
     397        box = OWGUI.widgetBox(self.controlArea, "Files") 
    305398        self.filesView = QTreeWidget(self) 
    306399        self.filesView.setHeaderLabels(["Options", "Title", "Size", 
    307400                                        "Last Updated"]) 
    308401        self.filesView.setRootIsDecorated(False) 
     402        self.filesView.setUniformRowHeights(True) 
    309403        self.filesView.setSelectionMode(QAbstractItemView.NoSelection) 
    310404        self.filesView.setSortingEnabled(True) 
    311         self.filesView.setItemDelegate(UpdateItemDelegate(self.filesView)) 
    312         self.connect(self.filesView.model(), 
    313                      SIGNAL("layoutChanged()"), 
    314                      self.SearchUpdate) 
     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) 
    315412        box.layout().addWidget(self.filesView) 
    316413 
    317         box = OWGUI.widgetBox(self.mainArea, orientation="horizontal") 
     414        box = OWGUI.widgetBox(self.controlArea, orientation="horizontal") 
    318415        OWGUI.button(box, self, "Update all local files", 
    319416                     callback=self.UpdateAll, 
     
    322419                     callback=self.DownloadFiltered, 
    323420                     tooltip="Download all filtered files shown") 
     421        OWGUI.button(box, self, "Cancel", callback=self.Cancel, 
     422                     tooltip="Cancel scheduled downloads/updates.") 
    324423        OWGUI.rubber(box) 
    325424        OWGUI.lineEdit(box, self, "accessCode", "Access Code", 
     
    329428                                        callback=self.RetrieveFilesList) 
    330429        self.retryButton.hide() 
    331         box = OWGUI.widgetBox(self.mainArea, orientation="horizontal") 
     430        box = OWGUI.widgetBox(self.controlArea, orientation="horizontal") 
    332431        OWGUI.rubber(box) 
    333432        if wantCloseButton: 
     
    339438        self.infoLabel.setAlignment(Qt.AlignCenter) 
    340439 
    341         self.mainArea.layout().addWidget(self.infoLabel) 
     440        self.controlArea.layout().addWidget(self.infoLabel) 
    342441        self.infoLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 
    343442 
    344443        self.updateItems = [] 
    345         self.allTags = [] 
    346444 
    347445        self.resize(800, 600) 
    348446 
    349         QTimer.singleShot(50, self.RetrieveFilesList) 
     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 = [] 
    350459 
    351460    def RetrieveFilesList(self): 
     461        self.progress.setRange(0, 3) 
    352462        self.serverFiles = serverfiles.ServerFiles(access_code=self.accessCode) 
    353         self.pb = ProgressBar(self, 3) 
    354         self.async_retrieve = createTask(retrieveFilesList, 
    355                                          (self.serverFiles, self.domains, 
    356                                           self.pb.advance), 
    357                                          onResult=self.SetFilesList, 
    358                                          onError=self.HandleError) 
     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) 
    359472 
    360473        self.setEnabled(False) 
    361474 
    362475    def SetFilesList(self, serverInfo): 
     476        """ 
     477        Set the files to show. 
     478        """ 
    363479        self.setEnabled(True) 
    364         domains = serverInfo.keys() or serverfiles.listdomains() 
    365         localInfo = dict([(dom, serverfiles.allinfo(dom)) 
    366                           for dom in domains]) 
    367         items = [] 
    368  
    369         self.allTags = set() 
    370         allTitles = set() 
     480 
     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() 
    371493        self.updateItems = [] 
    372494 
    373         for domain in set(domains) - set(["test", "demo"]): 
    374             local = localInfo.get(domain, {}) 
    375             server = serverInfo.get(domain, {}) 
    376             files = sorted(set(server.keys() + local.keys())) 
    377             for filename in files: 
    378                 infoServer = server.get(filename, None) 
    379                 infoLocal = local.get(filename, None) 
    380  
    381                 items.append((self.filesView, domain, filename, infoLocal, 
    382                               infoServer)) 
    383  
    384                 displayInfo = infoServer if infoServer else infoLocal 
    385                 self.allTags.update(displayInfo["tags"]) 
    386                 allTitles.update(displayInfo["title"].split()) 
    387  
    388         for item in items: 
    389             self.updateItems.append(UpdateTreeWidgetItem(self, *item)) 
    390         self.pb.advance() 
     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 
     499 
     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            ) 
     507 
     508            self.updateItems.append((item, tree_item, options_widget)) 
     509            all_tags.update(item.tags) 
     510 
     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 
     518        self.progress.advance() 
     519 
    391520        for column in range(4): 
    392521            whint = self.filesView.sizeHintForColumn(column) 
     
    394523            self.filesView.setColumnWidth(column, width) 
    395524 
    396         self.lineEditFilter.setItems([hint for hint in sorted(self.allTags) 
     525        self.lineEditFilter.setItems([hint for hint in sorted(all_tags) 
    397526                                      if not hint.startswith("#")]) 
    398527        self.SearchUpdate() 
    399528        self.UpdateInfoLabel() 
    400         self.pb.finish() 
    401  
    402     def HandleError(self, (exc_type, exc_value, tb)): 
    403         if exc_type >= IOError: 
     529 
     530        self.progress.setRange(0, 0) 
     531 
     532    def HandleError(self, exception): 
     533        if isinstance(exception, IOError): 
    404534            self.error(0, 
    405535                       "Could not connect to server! Press the Retry " 
     
    407537            self.SetFilesList({}) 
    408538        else: 
    409             sys.excepthook(exc_type, exc_value, tb) 
    410             self.pb.finish() 
     539            sys.excepthook(type(exception), exception.args, None) 
     540            self.progress.setRange(0, 0) 
    411541            self.setEnabled(True) 
    412542 
    413543    def UpdateInfoLabel(self): 
    414         local = [item for item in self.updateItems if item.state != 2] 
    415         onServer = [item for item in self.updateItems] 
    416         size = sum(float(item.specialTags.get("#uncompressed", item.size)) 
     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)) 
    417549                   for item in local) 
    418         sizeOnServer = sum(float(item.size) for item in self.updateItems) 
     550 
     551        sizeOnServer = sum(float(item.size) for item, _, _ in self.updateItems) 
    419552 
    420553        if self.showAll: 
     
    431564 
    432565    def UpdateAll(self): 
    433         for item in self.updateItems: 
    434             if item.state == 1: 
    435                 item.StartDownload() 
     566        for item, _, _ in self.updateItems: 
     567            if item.state == OUTDATED: 
     568                self.SubmitDownloadTask(item.domain, item.filename) 
    436569 
    437570    def DownloadFiltered(self): 
    438         for item in self.updateItems: 
    439             if not item.isHidden() and item.state != 0: 
    440                 item.StartDownload() 
     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) 
    441576 
    442577    def SearchUpdate(self, searchString=None): 
    443578        strings = unicode(self.lineEditFilter.text()).split() 
    444         tags = set() 
    445         for item in self.updateItems: 
    446             hide = not all(str(string) in item for string in strings) 
    447             item.setHidden(hide) 
    448             if not hide: 
    449                 tags.update(item.tags) 
     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) 
     583 
     584    def SubmitDownloadTask(self, domain, filename): 
     585        """ 
     586        Submit the (domain, filename) to be downloaded/updated. 
     587        """ 
     588        index = self.updateItemIndex(domain, filename) 
     589        _, tree_item, opt_widget = self.updateItems[index] 
     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 
     598        self.executor.submit(task) 
     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 
     612        self.filesView.setItemWidget(tree_item, 2, pb) 
     613 
     614        # Clear the text so it does not show behind the progress bar. 
     615        tree_item.setData(2, Qt.DisplayRole, "") 
     616        pb.show() 
     617 
     618        # Disable the options widget 
     619        opt_widget.setEnabled(False) 
     620        self._tasks.append(task) 
     621 
     622    def EndDownloadTask(self, task): 
     623        future = task.future() 
     624        index = self.updateItemIndex(task.domain, task.filename) 
     625        item, tree_item, opt_widget = self.updateItems[index] 
     626 
     627        self.filesView.removeItemWidget(tree_item, 2) 
     628        opt_widget.setEnabled(True) 
     629 
     630        if future.cancelled(): 
     631            # Restore the previous state 
     632            tree_item.setUpdateItem(item) 
     633            opt_widget.setState(item.state) 
     634 
     635        elif future.exception(): 
     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, 
     641                         QVariant("Error occurred while downloading:" + 
     642                                  str(future.exception()))) 
     643 
     644        else: 
     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 
     655            self.UpdateInfoLabel() 
     656 
     657    def SubmitRemoveTask(self, domain, filename): 
     658        serverfiles.remove(domain, filename) 
     659        index = self.updateItemIndex(domain, filename) 
     660        item, tree_item, opt_widget = self.updateItems[index] 
     661 
     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) 
     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: 
     682            task.future().cancel() 
     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 
     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)) 
     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()) 
    450817 
    451818 
Note: See TracChangeset for help on using the changeset viewer.