~ubuntu-branches/ubuntu/natty/miro/natty

« back to all changes in this revision

Viewing changes to .pc/100_catch_keyerror_in_update_items.patch/lib/frontends/widgets/itemlist.py

  • Committer: Bazaar Package Importer
  • Author(s): Bryce Harrington
  • Date: 2011-01-22 02:46:33 UTC
  • mfrom: (1.4.10 upstream) (1.7.5 experimental)
  • Revision ID: james.westby@ubuntu.com-20110122024633-kjme8u93y2il5nmf
Tags: 3.5.1-1ubuntu1
* Merge from debian.  Remaining ubuntu changes:
  - Use python 2.7 instead of python 2.6
  - Relax dependency on python-dbus to >= 0.83.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Miro - an RSS based video player application
 
2
# Copyright (C) 2005-2010 Participatory Culture Foundation
 
3
#
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (at your option) any later version.
 
8
#
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License
 
15
# along with this program; if not, write to the Free Software
 
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 
17
#
 
18
# In addition, as a special exception, the copyright holders give
 
19
# permission to link the code of portions of this program with the OpenSSL
 
20
# library.
 
21
#
 
22
# You must obey the GNU General Public License in all respects for all of
 
23
# the code used other than OpenSSL. If you modify file(s) with this
 
24
# exception, you may extend this exception to your version of the file(s),
 
25
# but you are not obligated to do so. If you do not wish to do so, delete
 
26
# this exception statement from your version. If you delete this exception
 
27
# statement from all source files in the program, then also delete it here.
 
28
 
 
29
"""itemlist.py -- Handles TableModel objects that store items.
 
30
 
 
31
itemlist, itemlistcontroller and itemlistwidgets work together using the MVC
 
32
pattern.  itemlist handles the Model, itemlistwidgets handles the View and
 
33
itemlistcontroller handles the Controller.
 
34
 
 
35
ItemList manages a TableModel that stores ItemInfo objects.  It handles
 
36
filtering out items from the list (for example in the Downloading items list).
 
37
They also handle temporarily filtering out items based the user's search
 
38
terms.
 
39
"""
 
40
 
 
41
import re
 
42
import sys
 
43
import unicodedata
 
44
 
 
45
from miro import search
 
46
from miro import signals
 
47
from miro import util
 
48
from miro.frontends.widgets import imagepool
 
49
from miro.plat.utils import filename_to_unicode
 
50
from miro.plat.frontends.widgets import timer
 
51
from miro.plat.frontends.widgets import widgetset
 
52
 
 
53
def item_matches_search(item_info, search_text):
 
54
    """Check if an item matches search text."""
 
55
    if search_text == '':
 
56
        return True
 
57
    match_against = [item_info.name, item_info.description]
 
58
    if item_info.video_path is not None:
 
59
        match_against.append(filename_to_unicode(item_info.video_path))
 
60
    return search.match(search_text, match_against)
 
61
 
 
62
class ItemSort(object):
 
63
    """Class that sorts items in an item list."""
 
64
 
 
65
    def __init__(self, ascending):
 
66
        self._reverse = not ascending
 
67
 
 
68
    def is_ascending(self):
 
69
        return not self._reverse
 
70
 
 
71
    def sort_key(self, item):
 
72
        """Return a value that can be used to sort item.
 
73
 
 
74
        Must be implemented by subclasses.
 
75
        """
 
76
        raise NotImplementedError()
 
77
 
 
78
    def compare(self, item, other):
 
79
        """Compare two items
 
80
 
 
81
        Returns -1 if item < other, 1 if other > item and 0 if item == other
 
82
        (same as cmp)
 
83
        """
 
84
        if self._reverse:
 
85
            return -cmp(self.sort_key(item), self.sort_key(other))
 
86
        else:
 
87
            return cmp(self.sort_key(item), self.sort_key(other))
 
88
 
 
89
    def sort_items(self, item_list):
 
90
        """Sort a list of items (in place)."""
 
91
        item_list.sort(key=self.sort_key, reverse=self._reverse)
 
92
 
 
93
    def sort_item_rows(self, rows):
 
94
        rows.sort(key=lambda row: self.sort_key(row[0]),
 
95
                reverse=self._reverse)
 
96
 
 
97
class DateSort(ItemSort):
 
98
    KEY = 'date'
 
99
    def sort_key(self, item):
 
100
        return item.release_date
 
101
 
 
102
class NameSort(ItemSort):
 
103
    KEY = 'name'
 
104
    def sort_key(self, item):
 
105
        return util.name_sort_key(item.name)
 
106
 
 
107
class LengthSort(ItemSort):
 
108
    KEY = 'length'
 
109
    def sort_key(self, item):
 
110
        return item.duration
 
111
 
 
112
class SizeSort(ItemSort):
 
113
    KEY = 'size'
 
114
    def sort_key(self, item):
 
115
        return item.size
 
116
 
 
117
class DescriptionSort(ItemSort):
 
118
    KEY = 'description'
 
119
    def sort_key(self, item):
 
120
        return item.description
 
121
 
 
122
class FeedNameSort(ItemSort):
 
123
    KEY = 'feed-name'
 
124
    def sort_key(self, item):
 
125
        if item.feed_name:
 
126
            return item.feed_name.lower()
 
127
        return item.feed_name
 
128
 
 
129
class StatusCircleSort(ItemSort):
 
130
    KEY = 'state'
 
131
    # Weird sort, this one is for when the user clicks on the header above the
 
132
    # status bumps.  It's almost the same as StatusSort, but there isn't a
 
133
    # bump for expiring.
 
134
    def sort_key(self, item):
 
135
        if item.state == 'downloading':
 
136
            return 1 # downloading
 
137
        elif item.downloaded and not item.video_watched:
 
138
            return 2 # unwatched
 
139
        elif not item.item_viewed and not item.expiration_date:
 
140
            return 0 # new
 
141
        else:
 
142
            return 3 # other
 
143
 
 
144
class StatusSort(ItemSort):
 
145
    KEY = 'status'
 
146
    def sort_key(self, item):
 
147
        if item.state == 'downloading':
 
148
            return (2, ) # downloading
 
149
        elif item.downloaded and not item.video_watched:
 
150
            return (3, ) # unwatched
 
151
        elif item.expiration_date:
 
152
            # the tuple here creates a subsort on expiration_date
 
153
            return (4, item.expiration_date) # expiring
 
154
        elif not item.item_viewed:
 
155
            return (0, ) # new
 
156
        else:
 
157
            return (1, ) # other
 
158
 
 
159
class ETASort(ItemSort):
 
160
    KEY = 'eta'
 
161
    def sort_key(self, item):
 
162
        if item.state == 'downloading':
 
163
            eta = item.download_info.eta
 
164
            if eta > 0:
 
165
                return eta
 
166
        elif not self._reverse:
 
167
            return sys.maxint
 
168
        else:
 
169
            return -sys.maxint
 
170
 
 
171
class DownloadRateSort(ItemSort):
 
172
    KEY = 'rate'
 
173
    def sort_key(self, item):
 
174
        if item.state == 'downloading':
 
175
            return item.download_info.rate
 
176
        elif not self._reverse:
 
177
            return sys.maxint
 
178
        else:
 
179
            return -1
 
180
 
 
181
class ProgressSort(ItemSort):
 
182
    KEY = 'progress'
 
183
    def sort_key(self, item):
 
184
        if item.state in ('downloading', 'paused'):
 
185
            return float(item.download_info.downloaded_size) / item.size
 
186
        elif not self._reverse:
 
187
            return sys.maxint
 
188
        else:
 
189
            return -1
 
190
 
 
191
SORT_KEY_MAP = {
 
192
    DateSort.KEY:         DateSort,
 
193
    NameSort.KEY:         NameSort,
 
194
    LengthSort.KEY:       LengthSort,
 
195
    SizeSort.KEY:         SizeSort,
 
196
    DescriptionSort.KEY:  DescriptionSort,
 
197
    FeedNameSort.KEY:     FeedNameSort,
 
198
    StatusCircleSort.KEY: StatusCircleSort,
 
199
    StatusSort.KEY:       StatusSort,
 
200
    ETASort.KEY:          ETASort,
 
201
    DownloadRateSort.KEY: DownloadRateSort,
 
202
    ProgressSort.KEY:     ProgressSort,
 
203
}
 
204
 
 
205
class ItemListGroup(object):
 
206
    """Manages a set of ItemLists.
 
207
 
 
208
    ItemListGroup keep track of one or more ItemLists.  When items are
 
209
    added/changed/removed they take care of making sure each child list
 
210
    updates itself.
 
211
 
 
212
    ItemLists maintain an item sorting and a search filter that are shared by
 
213
    each child list.
 
214
    """
 
215
 
 
216
    def __init__(self, item_lists, sorter):
 
217
        """Construct in ItemLists.
 
218
 
 
219
        item_lists is a list of ItemList objects that should be grouped
 
220
        together.
 
221
        """
 
222
        self.item_lists = item_lists
 
223
        if sorter is None:
 
224
            self.set_sort(DateSort(False))
 
225
        else:
 
226
            self.set_sort(sorter)
 
227
        self._throbber_timeouts = {}
 
228
        self.html_stripper = util.HTMLStripper()
 
229
 
 
230
    def _throbber_timeout(self, id):
 
231
        for item_list in self.item_lists:
 
232
            item_list.update_throbber(id)
 
233
        self._schedule_throbber_timeout(id)
 
234
 
 
235
    def _schedule_throbber_timeout(self, id):
 
236
        timeout = timer.add(0.4, self._throbber_timeout, id)
 
237
        self._throbber_timeouts[id] = timeout
 
238
 
 
239
    def _setup_info(self, info):
 
240
        """Initialize a newly received ItemInfo."""
 
241
        info.icon = imagepool.LazySurface(info.thumbnail, (154, 105))
 
242
        info.description_text, info.description_links = \
 
243
                self.html_stripper.strip(info.description)
 
244
        download_info = info.download_info
 
245
        if (download_info is not None and
 
246
                not download_info.finished and
 
247
                download_info.state != 'paused' and
 
248
                download_info.downloaded_size > 0 and info.size == -1):
 
249
            # We are downloading an item without a content-length.  Take steps
 
250
            # to update the progress throbbers.
 
251
            if info.id not in self._throbber_timeouts:
 
252
                self._schedule_throbber_timeout(info.id)
 
253
        elif info.id in self._throbber_timeouts:
 
254
            timer.cancel(self._throbber_timeouts.pop(info.id))
 
255
 
 
256
    def add_items(self, item_list):
 
257
        """Add a list of new items to the item list.
 
258
 
 
259
        Note: This method will sort item_list
 
260
        """
 
261
        self._sorter.sort_items(item_list)
 
262
        for item_info in item_list:
 
263
            self._setup_info(item_info)
 
264
        for sublist in self.item_lists:
 
265
            sublist.add_items(item_list, already_sorted=True)
 
266
 
 
267
    def update_items(self, changed_items):
 
268
        """Update items.
 
269
 
 
270
        Note: This method will sort changed_items
 
271
        """
 
272
        self._sorter.sort_items(changed_items)
 
273
        for item_info in changed_items:
 
274
            self._setup_info(item_info)
 
275
        for sublist in self.item_lists:
 
276
            sublist.update_items(changed_items, already_sorted=True)
 
277
 
 
278
    def remove_items(self, removed_ids):
 
279
        """Remove items from the list."""
 
280
        for sublist in self.item_lists:
 
281
            sublist.remove_items(removed_ids)
 
282
 
 
283
    def set_sort(self, sorter):
 
284
        """Change the way items are sorted in the list (and filtered lists)
 
285
 
 
286
        sorter must be a subclass of ItemSort.
 
287
        """
 
288
        self._sorter = sorter
 
289
        for sublist in self.item_lists:
 
290
            sublist.set_sort(sorter)
 
291
 
 
292
    def get_sort(self):
 
293
        return self._sorter
 
294
 
 
295
    def set_search_text(self, search_text):
 
296
        """Update the search for each child list."""
 
297
        for sublist in self.item_lists:
 
298
            sublist.set_search_text(search_text)
 
299
 
 
300
class ItemList(signals.SignalEmitter):
 
301
    """
 
302
    Attributes:
 
303
 
 
304
    model -- TableModel for this item list.  It contains these columns:
 
305
        * ItemInfo (object)
 
306
        * show_details flag (boolean)
 
307
        * counter used to change the progress throbber (integer)
 
308
 
 
309
    new_only -- Are we only displaying the new items?
 
310
    unwatched_only -- Are we only displaying the unwatched items?
 
311
    non_feed_only -- Are we only displaying file items?
 
312
    resort_on_update -- Should we re-sort the list when items change?
 
313
 
 
314
    Signals:
 
315
      item-added(item, next_item): an item was added to the list
 
316
    """
 
317
 
 
318
    def __init__(self):
 
319
        signals.SignalEmitter.__init__(self)
 
320
        self.create_signal('item-added')
 
321
        self.model = widgetset.TableModel('object', 'boolean', 'integer')
 
322
        self._iter_map = {}
 
323
        self._sorter = None
 
324
        self._search_text = ''
 
325
        self.new_only = False
 
326
        self.unwatched_only = False
 
327
        self.non_feed_only = False
 
328
        self.resort_on_update = False
 
329
        self._hidden_items = {}
 
330
        # maps ids -> items that should be in this list, but are filtered out
 
331
        # for some reason
 
332
 
 
333
    def set_sort(self, sorter):
 
334
        self._sorter = sorter
 
335
        self._resort_items()
 
336
 
 
337
    def get_sort(self):
 
338
        return self._sorter
 
339
 
 
340
    def get_count(self):
 
341
        """Get the number of items in this list that are displayed."""
 
342
        return len(self.model)
 
343
 
 
344
    def get_hidden_count(self):
 
345
        """Get the number of items in this list that are hidden."""
 
346
        return len(self._hidden_items)
 
347
 
 
348
    def get_items(self, start_id=None):
 
349
        """Get a list of ItemInfo objects in this list"""
 
350
        if start_id is None:
 
351
            return [row[0] for row in self.model]
 
352
        else:
 
353
            iter = self._iter_map[start_id]
 
354
            retval = []
 
355
            while iter is not None:
 
356
                retval.append(self.model[iter][0])
 
357
                iter = self.model.next_iter(iter)
 
358
            return retval
 
359
 
 
360
    def _resort_items(self):
 
361
        rows = []
 
362
        iter = self.model.first_iter()
 
363
        while iter is not None:
 
364
            rows.append(tuple(self.model[iter]))
 
365
            iter = self.model.remove(iter)
 
366
        self._sorter.sort_item_rows(rows)
 
367
        for row in rows:
 
368
            self._iter_map[row[0].id] = self.model.append(*row)
 
369
 
 
370
    def _resort_item(self, info):
 
371
        """Put an item into it's correct position using the current sort."""
 
372
        itr = self._iter_map[info.id]
 
373
        row = tuple(self.model[itr])
 
374
        self.model.remove(itr)
 
375
 
 
376
        itr = self.model.first_iter()
 
377
        while itr is not None:
 
378
            current_info = self.model[itr][0]
 
379
            if(self._sorter.compare(info, current_info) < 1):
 
380
                new_itr = self.model.insert_before(itr, *row)
 
381
                break
 
382
            itr = self.model.next_iter(itr)
 
383
        else:
 
384
            new_itr = self.model.append(*row)
 
385
        self._iter_map[info.id] = new_itr
 
386
 
 
387
    def filter(self, item_info):
 
388
        """Can be overrided by subclasses to filter out items from the list.
 
389
        """
 
390
        return True
 
391
 
 
392
    def _should_show_item(self, item_info):
 
393
        if not self.filter(item_info):
 
394
            return False
 
395
        return (not (self.new_only and item_info.item_viewed) and
 
396
                not (self.unwatched_only and item_info.video_watched) and
 
397
                not (self.non_feed_only and (not item_info.is_external and item_info.feed_url != 'dtv:searchDownloads')) and
 
398
                item_matches_search(item_info, self._search_text))
 
399
 
 
400
    def set_show_details(self, item_id, value):
 
401
        """Change the show details value for an item"""
 
402
        iter = self._iter_map[item_id]
 
403
        self.model.update_value(iter, 1, value)
 
404
 
 
405
    def find_show_details_rows(self):
 
406
        """Return a list of iters for rows with in show details mode."""
 
407
        retval = []
 
408
        iter = self.model.first_iter()
 
409
        while iter is not None:
 
410
            if self.model[iter][1]:
 
411
                retval.append(iter)
 
412
            iter = self.model.next_iter(iter)
 
413
        return retval
 
414
 
 
415
    def update_throbber(self, item_id):
 
416
        try:
 
417
            iter = self._iter_map[item_id]
 
418
        except KeyError:
 
419
            pass
 
420
        else:
 
421
            counter = self.model[iter][2]
 
422
            self.model.update_value(iter, 2, counter + 1)
 
423
 
 
424
    def _insert_sorted_items(self, item_list):
 
425
        pos = self.model.first_iter()
 
426
        for item_info in item_list:
 
427
            while (pos is not None and
 
428
                    self._sorter.compare(self.model[pos][0], item_info) < 0):
 
429
                pos = self.model.next_iter(pos)
 
430
            iter = self.model.insert_before(pos, item_info, False, 0)
 
431
            if pos is not None:
 
432
                next_item_info = self.model[pos][0]
 
433
            else:
 
434
                next_item_info = None
 
435
            self.emit('item-added', item_info, next_item_info)
 
436
            self._iter_map[item_info.id] = iter
 
437
 
 
438
    def add_items(self, item_list, already_sorted=False):
 
439
        to_add = []
 
440
        for item in item_list:
 
441
            if self._should_show_item(item):
 
442
                to_add.append(item)
 
443
            else:
 
444
                self._hidden_items[item.id] = item
 
445
        if not already_sorted:
 
446
            self._sorter.sort_items(to_add)
 
447
        self._insert_sorted_items(to_add)
 
448
 
 
449
    def update_items(self, changed_items, already_sorted=False):
 
450
        to_add = []
 
451
        for info in changed_items:
 
452
            should_show = self._should_show_item(info)
 
453
            if info.id in self._iter_map:
 
454
                # Item already displayed
 
455
                if not should_show:
 
456
                    self.remove_item(info.id)
 
457
                    self._hidden_items[info.id] = info
 
458
                else:
 
459
                    self.update_item(info)
 
460
            else:
 
461
                # Item not already displayed
 
462
                if should_show:
 
463
                    to_add.append(info)
 
464
                    del self._hidden_items[info.id]
 
465
                else:
 
466
                    self._hidden_items[info.id] = info
 
467
        if not already_sorted:
 
468
            self._sorter.sort_items(to_add)
 
469
        self._insert_sorted_items(to_add)
 
470
 
 
471
    def remove_item(self, id):
 
472
        try:
 
473
            iter = self._iter_map.pop(id)
 
474
        except KeyError:
 
475
            # The item is hidden
 
476
            del self._hidden_items[id]
 
477
        else:
 
478
            self.model.remove(iter)
 
479
 
 
480
    def update_item(self, info):
 
481
        iter = self._iter_map[info.id]
 
482
        old_item = self.model[iter][0]
 
483
        self.model.update_value(iter, 0, info)
 
484
        if self.resort_on_update and self._sorter.compare(info, old_item):
 
485
            # If we've changed the sort value of the item, then we need to
 
486
            # re-sort the list (#12003).
 
487
            self._resort_item(info)
 
488
 
 
489
    def remove_items(self, id_list):
 
490
        for id in id_list:
 
491
            self.remove_item(id)
 
492
 
 
493
    def set_new_only(self, new_only):
 
494
        """Set if only new items are to be displayed (default False)."""
 
495
        self.new_only = new_only
 
496
        self._recalculate_hidden_items()
 
497
 
 
498
    def view_all(self):
 
499
        self.unwatched_only = False
 
500
        self.non_feed_only = False
 
501
        self._recalculate_hidden_items()
 
502
 
 
503
    def toggle_unwatched_only(self):
 
504
        self.unwatched_only = not self.unwatched_only
 
505
        self._recalculate_hidden_items()
 
506
 
 
507
    def toggle_non_feed(self):
 
508
        self.non_feed_only = not self.non_feed_only
 
509
        self._recalculate_hidden_items()
 
510
 
 
511
    def set_filters(self, unwatched, non_feed):
 
512
        self.unwatched_only = unwatched
 
513
        self.non_feed_only = non_feed
 
514
        self._recalculate_hidden_items()
 
515
 
 
516
    def set_search_text(self, search_text):
 
517
        self._search_text = search_text
 
518
        self._recalculate_hidden_items()
 
519
 
 
520
    def _recalculate_hidden_items(self):
 
521
        newly_matching = self._find_newly_matching_items()
 
522
        removed = self._remove_non_matching_items()
 
523
        self._sorter.sort_items(newly_matching)
 
524
        self._insert_sorted_items(newly_matching)
 
525
        for item in removed:
 
526
            self._hidden_items[item.id] = item
 
527
        for item in newly_matching:
 
528
            del self._hidden_items[item.id]
 
529
 
 
530
    def move_items(self, insert_before, item_ids):
 
531
        """Move a group of items inside the list.
 
532
 
 
533
        The items for item_ids will be positioned before insert_before.
 
534
        insert_before should be an iterator, or None to position the items at
 
535
        the end of the list.
 
536
        """
 
537
 
 
538
        new_iters = _ItemReorderer().reorder(self.model, insert_before,
 
539
                item_ids)
 
540
        self._iter_map.update(new_iters)
 
541
 
 
542
    def _find_newly_matching_items(self):
 
543
        retval = []
 
544
        for item in self._hidden_items.values():
 
545
            if self._should_show_item(item):
 
546
                retval.append(item)
 
547
        return retval
 
548
 
 
549
    def _remove_non_matching_items(self):
 
550
        removed = []
 
551
        iter = self.model.first_iter()
 
552
        while iter is not None:
 
553
            item = self.model[iter][0]
 
554
            if not self._should_show_item(item):
 
555
                iter = self.model.remove(iter)
 
556
                del self._iter_map[item.id]
 
557
                removed.append(item)
 
558
            else:
 
559
                iter = self.model.next_iter(iter)
 
560
        return removed
 
561
 
 
562
class IndividualDownloadItemList(ItemList):
 
563
    """ItemList that only displays single downloads items.
 
564
 
 
565
    Used in the downloads tab."""
 
566
    def filter(self, item_info):
 
567
        return (item_info.is_external
 
568
                and not (item_info.download_info
 
569
                         and item_info.download_info.state in ('uploading', 'uploading-paused')))
 
570
 
 
571
class ChannelDownloadItemList(ItemList):
 
572
    """ItemList that only displays channel downloads items.
 
573
 
 
574
    Used in the downloads tab."""
 
575
    def filter(self, item_info):
 
576
        return (not item_info.is_external
 
577
                and not (item_info.download_info
 
578
                         and item_info.download_info.state in ('uploading', 'uploading-paused')))
 
579
 
 
580
class SeedingItemList(ItemList):
 
581
    """ItemList that only displays seeding items.
 
582
 
 
583
    Used in the downloads tab."""
 
584
    def filter(self, item_info):
 
585
        return (item_info.download_info
 
586
                and item_info.download_info.state in ('uploading', 'uploading-paused'))
 
587
 
 
588
class DownloadingItemList(ItemList):
 
589
    """ItemList that only displays downloading items."""
 
590
    def filter(self, item_info):
 
591
        return (item_info.download_info
 
592
                and not item_info.download_info.finished
 
593
                and not item_info.download_info.state == 'failed')
 
594
 
 
595
class ConversionsItemList(ItemList):
 
596
    """ItemList that displays items being converted."""
 
597
    def filter(self, item_info):
 
598
        return item_info.converting
 
599
 
 
600
class DownloadedItemList(ItemList):
 
601
    """ItemList that only displays downloaded items."""
 
602
    def filter(self, item_info):
 
603
        return (item_info.download_info and
 
604
                item_info.download_info.finished)
 
605
 
 
606
class _ItemReorderer(object):
 
607
    """Handles re-ordering items inside an itemlist.
 
608
 
 
609
    This object is just around for utility sake.  It's only created to track
 
610
    the state during the call to ItemList.move_items()
 
611
    """
 
612
 
 
613
    def __init__(self):
 
614
        self.removed_rows = []
 
615
 
 
616
    def calc_insert_id(self, model):
 
617
        if self.insert_iter is not None:
 
618
            self.insert_id = model[self.insert_iter][0].id
 
619
        else:
 
620
            self.insert_id = None
 
621
 
 
622
    def reorder(self, model, insert_iter, ids):
 
623
        self.insert_iter = insert_iter
 
624
        self.calc_insert_id(model)
 
625
        self.remove_rows(model, ids)
 
626
        return self.put_rows_back(model)
 
627
 
 
628
    def remove_row(self, model, iter, row):
 
629
        self.removed_rows.append(row)
 
630
        if row[0].id == self.insert_id:
 
631
            self.insert_iter = model.next_iter(self.insert_iter)
 
632
            self.calc_insert_id(model)
 
633
        return model.remove(iter)
 
634
 
 
635
    def remove_rows(self, model, ids):
 
636
        # iterating through the entire table seems inefficient, but we have to
 
637
        # know the order of rows so we can insert them back in the right
 
638
        # order.
 
639
        iter = model.first_iter()
 
640
        while iter is not None:
 
641
            row = model[iter]
 
642
            if row[0].id in ids:
 
643
                # need to make a copy of the row data, since we're removing it
 
644
                # from the table
 
645
                iter = self.remove_row(model, iter, tuple(row))
 
646
            else:
 
647
                iter = model.next_iter(iter)
 
648
 
 
649
    def put_rows_back(self, model):
 
650
        if self.insert_iter is None:
 
651
            def put_back(moved_row):
 
652
                return model.append(*moved_row)
 
653
        else:
 
654
            def put_back(moved_row):
 
655
                return model.insert_before(self.insert_iter, *moved_row)
 
656
        retval = {}
 
657
        for removed_row in self.removed_rows:
 
658
            iter = put_back(removed_row)
 
659
            retval[removed_row[0].id] = iter
 
660
        return retval