~ubuntu-branches/ubuntu/trusty/miro/trusty

« back to all changes in this revision

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

  • Committer: Bazaar Package Importer
  • Author(s): Uwe Hermann
  • Date: 2010-07-31 20:00:43 UTC
  • mfrom: (1.4.9 upstream) (1.7.4 sid)
  • Revision ID: james.westby@ubuntu.com-20100731200043-0r2gn1z0amlsndas
Tags: 3.0.3-1
* New upstream release (Closes: #588299).
   + Upstream now uses webkit instead of xulrunner (Closes: 499652).
   + Fixes broken Youtube downloads.
* debian/copyright: Various updates, drop obsolete entries.
* README.Debian: Mention libwebkit-1.0-2-dbg as useful for debugging.
* Standards-Version: 3.9.1 (no changes required).
* debian/patches/:
   + 10_movies_dir.patch: Update.
   + 50_miro_debug_fix.patch: Update.
   + 102_xulrunner_bug_workaround.patch: Drop, obsolete.
   + Make all patches work with -p1 and add descriptions for all of them.
* Use new '3.0 (quilt)' source format.
* No longer use deprecated cdbs 'simple-patchsys.mk'.
* Drop no longer needed build-dependencies:
  + python-libtorrent (>= 0.14.10-2)
  + libtorrent-rasterbar-dev (>= 0.14.10-2)
  + python-gnome2-extras-dev (>= 2.19.1)
  + xulrunner-dev (>= 1.9.1)
  + libxv-dev
  + libssl-dev
  + libffi-dev
* Drop no longer needed dependencies:
  + python-gtkmozembed (>= 2.19.1) | python-gnome2-extras (>= 2.19.1)
  + xulrunner-1.9.1
* Drop democracyplayer, democracyplayer-data transitional packages.
* Use versioned python-dbus (>= 0.83.1) dependency (Closes: #587963).
* Use versioned python-gst0.10 (>= 0.10.18-2) dependency (Closes: #580609).

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 util
 
47
from miro.frontends.widgets import imagepool
 
48
from miro.plat.utils import filenameToUnicode
 
49
from miro.plat.frontends.widgets import timer
 
50
from miro.plat.frontends.widgets import widgetset
 
51
 
 
52
def item_matches_search(item_info, search_text):
 
53
    """Check if an item matches search text."""
 
54
    if search_text == '':
 
55
        return True
 
56
    match_against = [item_info.name, item_info.description]
 
57
    if item_info.video_path is not None:
 
58
        match_against.append(filenameToUnicode(item_info.video_path))
 
59
    return search.match(search_text, match_against)
 
60
 
 
61
class ItemSort(object):
 
62
    """Class that sorts items in an item list."""
 
63
 
 
64
    def __init__(self, ascending):
 
65
        self._reverse = not ascending
 
66
 
 
67
    def is_ascending(self):
 
68
        return not self._reverse
 
69
 
 
70
    def sort_key(self, item):
 
71
        """Return a value that can be used to sort item.
 
72
 
 
73
        Must be implemented by subclasses.
 
74
        """
 
75
        raise NotImplementedError()
 
76
 
 
77
    def compare(self, item, other):
 
78
        """Compare two items
 
79
 
 
80
        Returns -1 if item < other, 1 if other > item and 0 if item == other
 
81
        (same as cmp)
 
82
        """
 
83
        if self._reverse:
 
84
            return -cmp(self.sort_key(item), self.sort_key(other))
 
85
        else:
 
86
            return cmp(self.sort_key(item), self.sort_key(other))
 
87
 
 
88
    def sort_items(self, item_list):
 
89
        """Sort a list of items (in place)."""
 
90
        item_list.sort(key=self.sort_key, reverse=self._reverse)
 
91
 
 
92
    def sort_item_rows(self, rows):
 
93
        rows.sort(key=lambda row: self.sort_key(row[0]),
 
94
                reverse=self._reverse)
 
95
 
 
96
class DateSort(ItemSort):
 
97
    KEY = 'date'
 
98
    def sort_key(self, item):
 
99
        return item.release_date
 
100
 
 
101
class NameSort(ItemSort):
 
102
    KEY = 'name'
 
103
    def _strip_accents(self, str):
 
104
        nfkd_form = unicodedata.normalize('NFKD', unicode(str))
 
105
        return u"".join([c for c in nfkd_form if not unicodedata.combining(c)])
 
106
    def _trynum(self, str):
 
107
        try:
 
108
            return float(str)
 
109
        except:
 
110
            return self._strip_accents(str)
 
111
    def sort_key(self, item):
 
112
        if item.name:
 
113
            return [ self._trynum(c) for c in re.split('([0-9]+\.?[0-9]*)', item.name) ]
 
114
        return item.name
 
115
 
 
116
class LengthSort(ItemSort):
 
117
    KEY = 'length'
 
118
    def sort_key(self, item):
 
119
        return item.duration
 
120
 
 
121
class SizeSort(ItemSort):
 
122
    KEY = 'size'
 
123
    def sort_key(self, item):
 
124
        return item.size
 
125
 
 
126
class DescriptionSort(ItemSort):
 
127
    KEY = 'description'
 
128
    def sort_key(self, item):
 
129
        return item.description
 
130
 
 
131
class FeedNameSort(ItemSort):
 
132
    KEY = 'feed-name'
 
133
    def sort_key(self, item):
 
134
        if item.feed_name:
 
135
            return item.feed_name.lower()
 
136
        return item.feed_name
 
137
 
 
138
class StatusCircleSort(ItemSort):
 
139
    KEY = 'state'
 
140
    # Weird sort, this one is for when the user clicks on the header above the
 
141
    # status bumps.  It's almost the same as StatusSort, but there isn't a
 
142
    # bump for expiring.
 
143
    def sort_key(self, item):
 
144
        if item.state == 'downloading':
 
145
            return 1 # downloading
 
146
        elif item.downloaded and not item.video_watched:
 
147
            return 2 # unwatched
 
148
        elif not item.item_viewed and not item.expiration_date:
 
149
            return 0 # new
 
150
        else:
 
151
            return 3 # other
 
152
 
 
153
class StatusSort(ItemSort):
 
154
    KEY = 'status'
 
155
    def sort_key(self, item):
 
156
        if item.state == 'downloading':
 
157
            return (2, ) # downloading
 
158
        elif item.downloaded and not item.video_watched:
 
159
            return (3, ) # unwatched
 
160
        elif item.expiration_date:
 
161
            # the tuple here creates a subsort on expiration_date
 
162
            return (4, item.expiration_date) # expiring
 
163
        elif not item.item_viewed:
 
164
            return (0, ) # new
 
165
        else:
 
166
            return (1, ) # other
 
167
 
 
168
class ETASort(ItemSort):
 
169
    KEY = 'eta'
 
170
    def sort_key(self, item):
 
171
        if item.state in ('downloading', 'paused'):
 
172
            eta = item.download_info.eta
 
173
            if eta > 0:
 
174
                return eta
 
175
        elif not self._reverse:
 
176
            return sys.maxint
 
177
        else:
 
178
            return -sys.maxint
 
179
 
 
180
class DownloadRateSort(ItemSort):
 
181
    KEY = 'rate'
 
182
    def sort_key(self, item):
 
183
        if item.state in ('downloading', 'paused'):
 
184
            return item.download_info.rate
 
185
        elif not self._reverse:
 
186
            return sys.maxint
 
187
        else:
 
188
            return -1
 
189
 
 
190
class ProgressSort(ItemSort):
 
191
    KEY = 'progress'
 
192
    def sort_key(self, item):
 
193
        if item.state in ('downloading', 'paused'):
 
194
            return float(item.download_info.downloaded_size) / item.size
 
195
        elif not self._reverse:
 
196
            return sys.maxint
 
197
        else:
 
198
            return -1
 
199
 
 
200
SORT_KEY_MAP = {
 
201
    DateSort.KEY:         DateSort,
 
202
    NameSort.KEY:         NameSort,
 
203
    LengthSort.KEY:       LengthSort,
 
204
    SizeSort.KEY:         SizeSort,
 
205
    DescriptionSort.KEY:  DescriptionSort,
 
206
    FeedNameSort.KEY:     FeedNameSort,
 
207
    StatusCircleSort.KEY: StatusCircleSort,
 
208
    StatusSort.KEY:       StatusSort,
 
209
    ETASort.KEY:          ETASort,
 
210
    DownloadRateSort.KEY: DownloadRateSort,
 
211
    ProgressSort.KEY:     ProgressSort,
 
212
}
 
213
 
 
214
class ItemListGroup(object):
 
215
    """Manages a set of ItemLists.
 
216
 
 
217
    ItemListGroup keep track of one or more ItemLists.  When items are
 
218
    added/changed/removed they take care of making sure each child list
 
219
    updates itself.
 
220
 
 
221
    ItemLists maintain an item sorting and a search filter that are shared by
 
222
    each child list.
 
223
    """
 
224
 
 
225
    def __init__(self, item_lists, sorter):
 
226
        """Construct in ItemLists.
 
227
 
 
228
        item_lists is a list of ItemList objects that should be grouped
 
229
        together.
 
230
        """
 
231
        self.item_lists = item_lists
 
232
        if sorter is None:
 
233
            self.set_sort(DateSort(False))
 
234
        else:
 
235
            self.set_sort(sorter)
 
236
        self._throbber_timeouts = {}
 
237
        self.html_stripper = util.HTMLStripper()
 
238
 
 
239
    def _throbber_timeout(self, id):
 
240
        for item_list in self.item_lists:
 
241
            item_list.update_throbber(id)
 
242
        self._schedule_throbber_timeout(id)
 
243
 
 
244
    def _schedule_throbber_timeout(self, id):
 
245
        timeout = timer.add(0.4, self._throbber_timeout, id)
 
246
        self._throbber_timeouts[id] = timeout
 
247
 
 
248
    def _setup_info(self, info):
 
249
        """Initialize a newly received ItemInfo."""
 
250
        info.icon = imagepool.LazySurface(info.thumbnail, (154, 105))
 
251
        info.description_text, info.description_links = \
 
252
                self.html_stripper.strip(info.description)
 
253
        download_info = info.download_info
 
254
        if (download_info is not None and
 
255
                not download_info.finished and
 
256
                download_info.state != 'paused' and
 
257
                download_info.downloaded_size > 0 and info.size == -1):
 
258
            # We are downloading an item without a content-length.  Take steps
 
259
            # to update the progress throbbers.
 
260
            if info.id not in self._throbber_timeouts:
 
261
                self._schedule_throbber_timeout(info.id)
 
262
        elif info.id in self._throbber_timeouts:
 
263
            timer.cancel(self._throbber_timeouts.pop(info.id))
 
264
 
 
265
    def add_items(self, item_list):
 
266
        """Add a list of new items to the item list.
 
267
 
 
268
        Note: This method will sort item_list
 
269
        """
 
270
        self._sorter.sort_items(item_list)
 
271
        for item_info in item_list:
 
272
            self._setup_info(item_info)
 
273
        for sublist in self.item_lists:
 
274
            sublist.add_items(item_list, already_sorted=True)
 
275
 
 
276
    def update_items(self, changed_items):
 
277
        """Update items.
 
278
 
 
279
        Note: This method will sort changed_items
 
280
        """
 
281
        self._sorter.sort_items(changed_items)
 
282
        for item_info in changed_items:
 
283
            self._setup_info(item_info)
 
284
        for sublist in self.item_lists:
 
285
            sublist.update_items(changed_items, already_sorted=True)
 
286
 
 
287
    def remove_items(self, removed_ids):
 
288
        """Remove items from the list."""
 
289
        for sublist in self.item_lists:
 
290
            sublist.remove_items(removed_ids)
 
291
 
 
292
    def set_sort(self, sorter):
 
293
        """Change the way items are sorted in the list (and filtered lists)
 
294
 
 
295
        sorter must be a subclass of ItemSort.
 
296
        """
 
297
        self._sorter = sorter
 
298
        for sublist in self.item_lists:
 
299
            sublist.set_sort(sorter)
 
300
 
 
301
    def get_sort(self):
 
302
        return self._sorter
 
303
 
 
304
    def set_search_text(self, search_text):
 
305
        """Update the search for each child list."""
 
306
        for sublist in self.item_lists:
 
307
            sublist.set_search_text(search_text)
 
308
 
 
309
class ItemList(object):
 
310
    """
 
311
    Attributes:
 
312
 
 
313
    model -- TableModel for this item list.  It contains these columns:
 
314
        * ItemInfo (object)
 
315
        * show_details flag (boolean)
 
316
        * counter used to change the progress throbber (integer)
 
317
 
 
318
    new_only -- Are we only displaying the new items?
 
319
    """
 
320
 
 
321
    def __init__(self):
 
322
        self.model = widgetset.TableModel('object', 'boolean', 'integer')
 
323
        self._iter_map = {}
 
324
        self._sorter = None
 
325
        self._search_text = ''
 
326
        self.new_only = False
 
327
        self.unwatched_only = False
 
328
        self.non_feed_only = 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 filter(self, item_info):
 
371
        """Can be overrided by subclasses to filter out items from the list.
 
372
        """
 
373
        return True
 
374
 
 
375
    def _should_show_item(self, item_info):
 
376
        if not self.filter(item_info):
 
377
            return False
 
378
        return (not (self.new_only and item_info.item_viewed) and
 
379
                not (self.unwatched_only and item_info.video_watched) and
 
380
                not (self.non_feed_only and (not item_info.is_external and item_info.feed_url != 'dtv:searchDownloads')) and
 
381
                item_matches_search(item_info, self._search_text))
 
382
 
 
383
    def set_show_details(self, item_id, value):
 
384
        """Change the show details value for an item"""
 
385
        iter = self._iter_map[item_id]
 
386
        self.model.update_value(iter, 1, value)
 
387
 
 
388
    def find_show_details_rows(self):
 
389
        """Return a list of iters for rows with in show details mode."""
 
390
        retval = []
 
391
        iter = self.model.first_iter()
 
392
        while iter is not None:
 
393
            if self.model[iter][1]:
 
394
                retval.append(iter)
 
395
            iter = self.model.next_iter(iter)
 
396
        return retval
 
397
 
 
398
    def update_throbber(self, item_id):
 
399
        try:
 
400
            iter = self._iter_map[item_id]
 
401
        except KeyError:
 
402
            pass
 
403
        else:
 
404
            counter = self.model[iter][2]
 
405
            self.model.update_value(iter, 2, counter + 1)
 
406
 
 
407
    def _insert_sorted_items(self, item_list):
 
408
        pos = self.model.first_iter()
 
409
        for item_info in item_list:
 
410
            while (pos is not None and
 
411
                    self._sorter.compare(self.model[pos][0], item_info) < 0):
 
412
                pos = self.model.next_iter(pos)
 
413
            iter = self.model.insert_before(pos, item_info, False, 0)
 
414
            self._iter_map[item_info.id] = iter
 
415
 
 
416
    def add_items(self, item_list, already_sorted=False):
 
417
        to_add = []
 
418
        for item in item_list:
 
419
            if self._should_show_item(item):
 
420
                to_add.append(item)
 
421
            else:
 
422
                self._hidden_items[item.id] = item
 
423
        if not already_sorted:
 
424
            self._sorter.sort_items(to_add)
 
425
        self._insert_sorted_items(to_add)
 
426
 
 
427
    def update_items(self, changed_items, already_sorted=False):
 
428
        to_add = []
 
429
        for info in changed_items:
 
430
            should_show = self._should_show_item(info)
 
431
            if info.id in self._iter_map:
 
432
                # Item already displayed
 
433
                if not should_show:
 
434
                    self.remove_item(info.id)
 
435
                    self._hidden_items[info.id] = info
 
436
                else:
 
437
                    self.update_item(info)
 
438
            else:
 
439
                # Item not already displayed
 
440
                if should_show:
 
441
                    to_add.append(info)
 
442
                    del self._hidden_items[info.id]
 
443
                else:
 
444
                    self._hidden_items[info.id] = info
 
445
        if not already_sorted:
 
446
            self._sorter.sort_items(to_add)
 
447
        self._insert_sorted_items(to_add)
 
448
 
 
449
    def remove_item(self, id):
 
450
        try:
 
451
            iter = self._iter_map.pop(id)
 
452
        except KeyError:
 
453
            pass # The item isn't in our current list, just skip it
 
454
        else:
 
455
            self.model.remove(iter)
 
456
 
 
457
    def update_item(self, info):
 
458
        iter = self._iter_map[info.id]
 
459
        self.model.update_value(iter, 0, info)
 
460
 
 
461
    def remove_items(self, id_list):
 
462
        for id in id_list:
 
463
            self.remove_item(id)
 
464
 
 
465
    def set_new_only(self, new_only):
 
466
        """Set if only new items are to be displayed (default False)."""
 
467
        self.new_only = new_only
 
468
        self._recalculate_hidden_items()
 
469
 
 
470
    def view_all(self):
 
471
        self.unwatched_only = False
 
472
        self.non_feed_only = False
 
473
        self._recalculate_hidden_items()
 
474
 
 
475
    def toggle_unwatched_only(self):
 
476
        self.unwatched_only = not self.unwatched_only
 
477
        self._recalculate_hidden_items()
 
478
 
 
479
    def toggle_non_feed(self):
 
480
        self.non_feed_only = not self.non_feed_only
 
481
        self._recalculate_hidden_items()
 
482
 
 
483
    def set_search_text(self, search_text):
 
484
        self._search_text = search_text
 
485
        self._recalculate_hidden_items()
 
486
 
 
487
    def _recalculate_hidden_items(self):
 
488
        newly_matching = self._find_newly_matching_items()
 
489
        removed = self._remove_non_matching_items()
 
490
        self._sorter.sort_items(newly_matching)
 
491
        self._insert_sorted_items(newly_matching)
 
492
        for item in removed:
 
493
            self._hidden_items[item.id] = item
 
494
        for item in newly_matching:
 
495
            del self._hidden_items[item.id]
 
496
 
 
497
    def move_items(self, insert_before, item_ids):
 
498
        """Move a group of items inside the list.
 
499
 
 
500
        The items for item_ids will be positioned before insert_before.
 
501
        insert_before should be an iterator, or None to position the items at
 
502
        the end of the list.
 
503
        """
 
504
 
 
505
        new_iters = _ItemReorderer().reorder(self.model, insert_before,
 
506
                item_ids)
 
507
        self._iter_map.update(new_iters)
 
508
 
 
509
    def _find_newly_matching_items(self):
 
510
        retval = []
 
511
        for item in self._hidden_items.values():
 
512
            if self._should_show_item(item):
 
513
                retval.append(item)
 
514
        return retval
 
515
 
 
516
    def _remove_non_matching_items(self):
 
517
        removed = []
 
518
        iter = self.model.first_iter()
 
519
        while iter is not None:
 
520
            item = self.model[iter][0]
 
521
            if not self._should_show_item(item):
 
522
                iter = self.model.remove(iter)
 
523
                del self._iter_map[item.id]
 
524
                removed.append(item)
 
525
            else:
 
526
                iter = self.model.next_iter(iter)
 
527
        return removed
 
528
 
 
529
class IndividualDownloadItemList(ItemList):
 
530
    """ItemList that only displays single downloads items.
 
531
 
 
532
    Used in the downloads tab."""
 
533
    def filter(self, item_info):
 
534
        return (item_info.is_external
 
535
                and not (item_info.download_info
 
536
                         and item_info.download_info.state in ('uploading', 'uploading-paused')))
 
537
 
 
538
class ChannelDownloadItemList(ItemList):
 
539
    """ItemList that only displays channel downloads items.
 
540
 
 
541
    Used in the downloads tab."""
 
542
    def filter(self, item_info):
 
543
        return (not item_info.is_external
 
544
                and not (item_info.download_info
 
545
                         and item_info.download_info.state in ('uploading', 'uploading-paused')))
 
546
 
 
547
class SeedingItemList(ItemList):
 
548
    """ItemList that only displays seeding items.
 
549
 
 
550
    Used in the downloads tab."""
 
551
    def filter(self, item_info):
 
552
        return (item_info.download_info
 
553
                and item_info.download_info.state in ('uploading', 'uploading-paused'))
 
554
 
 
555
class DownloadingItemList(ItemList):
 
556
    """ItemList that only displays downloading items."""
 
557
    def filter(self, item_info):
 
558
        return (item_info.download_info
 
559
                and not item_info.download_info.finished
 
560
                and not item_info.download_info.state == 'failed')
 
561
 
 
562
class DownloadedItemList(ItemList):
 
563
    """ItemList that only displays downloaded items."""
 
564
    def filter(self, item_info):
 
565
        return (item_info.download_info and
 
566
                item_info.download_info.finished)
 
567
 
 
568
class _ItemReorderer(object):
 
569
    """Handles re-ordering items inside an itemlist.
 
570
 
 
571
    This object is just around for utility sake.  It's only created to track
 
572
    the state during the call to ItemList.move_items()
 
573
    """
 
574
 
 
575
    def __init__(self):
 
576
        self.removed_rows = []
 
577
 
 
578
    def calc_insert_id(self, model):
 
579
        if self.insert_iter is not None:
 
580
            self.insert_id = model[self.insert_iter][0].id
 
581
        else:
 
582
            self.insert_id = None
 
583
 
 
584
    def reorder(self, model, insert_iter, ids):
 
585
        self.insert_iter = insert_iter
 
586
        self.calc_insert_id(model)
 
587
        self.remove_rows(model, ids)
 
588
        return self.put_rows_back(model)
 
589
 
 
590
    def remove_row(self, model, iter, row):
 
591
        self.removed_rows.append(row)
 
592
        if row[0].id == self.insert_id:
 
593
            self.insert_iter = model.next_iter(self.insert_iter)
 
594
            self.calc_insert_id(model)
 
595
        return model.remove(iter)
 
596
 
 
597
    def remove_rows(self, model, ids):
 
598
        # iterating through the entire table seems inefficient, but we have to
 
599
        # know the order of rows so we can insert them back in the right
 
600
        # order.
 
601
        iter = model.first_iter()
 
602
        while iter is not None:
 
603
            row = model[iter]
 
604
            if row[0].id in ids:
 
605
                # need to make a copy of the row data, since we're removing it
 
606
                # from the table
 
607
                iter = self.remove_row(model, iter, tuple(row))
 
608
            else:
 
609
                iter = model.next_iter(iter)
 
610
 
 
611
    def put_rows_back(self, model):
 
612
        if self.insert_iter is None:
 
613
            def put_back(moved_row):
 
614
                return model.append(*moved_row)
 
615
        else:
 
616
            def put_back(moved_row):
 
617
                return model.insert_before(self.insert_iter, *moved_row)
 
618
        retval = {}
 
619
        for removed_row in self.removed_rows:
 
620
            iter = put_back(removed_row)
 
621
            retval[removed_row[0].id] = iter
 
622
        return retval