~ubuntu-branches/ubuntu/oneiric/calibre/oneiric

« back to all changes in this revision

Viewing changes to src/calibre/gui2/library.py

  • Committer: Bazaar Package Importer
  • Author(s): Martin Pitt
  • Date: 2010-06-21 10:18:08 UTC
  • mfrom: (1.3.12 upstream)
  • Revision ID: james.westby@ubuntu.com-20100621101808-aue828f532tmo4zt
Tags: 0.7.2+dfsg-1
* New major upstream version. See http://calibre-ebook.com/new-in/seven for
  details.
* Refresh patches to apply cleanly.
* debian/control: Bump python-cssutils to >= 0.9.7~ to ensure the existence
  of the CSSRuleList.rulesOfType attribute. This makes epub conversion work
  again. (Closes: #584756)
* Add debian/local/calibre-mount-helper: Simple and safe replacement for
  upstream's calibre-mount-helper, using udisks --mount and eject.
  (Closes: #584915, LP: #561958)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
__license__   = 'GPL v3'
2
 
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
3
 
 
4
 
import os, textwrap, traceback, re, shutil
5
 
from operator import attrgetter
6
 
from math import cos, sin, pi
7
 
from contextlib import closing
8
 
 
9
 
from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \
10
 
                        QPainterPath, QLinearGradient, QBrush, \
11
 
                        QPen, QStyle, QPainter, QStyleOptionViewItemV4, \
12
 
                        QImage, QMenu, \
13
 
                        QStyledItemDelegate, QCompleter
14
 
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, pyqtSignal, \
15
 
                         SIGNAL, QObject, QSize, QModelIndex, QDate
16
 
 
17
 
from calibre import strftime
18
 
from calibre.ptempfile import PersistentTemporaryFile
19
 
from calibre.utils.pyparsing import ParseException
20
 
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, REGEXP_MATCH
21
 
from calibre.gui2 import NONE, TableView, qstring_to_unicode, config, \
22
 
                         error_dialog
23
 
from calibre.gui2.widgets import EnLineEdit, TagsLineEdit
24
 
from calibre.utils.search_query_parser import SearchQueryParser
25
 
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
26
 
from calibre.ebooks.metadata import string_to_authors, fmt_sidx, \
27
 
                                    authors_to_string
28
 
from calibre.utils.config import tweaks
29
 
from calibre.utils.date import dt_factory, qt_to_dt, isoformat
30
 
 
31
 
class LibraryDelegate(QStyledItemDelegate):
32
 
    COLOR    = QColor("blue")
33
 
    SIZE     = 16
34
 
    PEN      = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
35
 
 
36
 
    def __init__(self, parent):
37
 
        QStyledItemDelegate.__init__(self, parent)
38
 
        self._parent = parent
39
 
        self.dummy = QModelIndex()
40
 
        self.star_path = QPainterPath()
41
 
        self.star_path.moveTo(90, 50)
42
 
        for i in range(1, 5):
43
 
            self.star_path.lineTo(50 + 40 * cos(0.8 * i * pi), \
44
 
                                  50 + 40 * sin(0.8 * i * pi))
45
 
        self.star_path.closeSubpath()
46
 
        self.star_path.setFillRule(Qt.WindingFill)
47
 
        gradient = QLinearGradient(0, 0, 0, 100)
48
 
        gradient.setColorAt(0.0, self.COLOR)
49
 
        gradient.setColorAt(1.0, self.COLOR)
50
 
        self.brush = QBrush(gradient)
51
 
        self.factor = self.SIZE/100.
52
 
 
53
 
    def sizeHint(self, option, index):
54
 
        #num = index.model().data(index, Qt.DisplayRole).toInt()[0]
55
 
        return QSize(5*(self.SIZE), self.SIZE+4)
56
 
 
57
 
    def paint(self, painter, option, index):
58
 
        style = self._parent.style()
59
 
        option = QStyleOptionViewItemV4(option)
60
 
        self.initStyleOption(option, self.dummy)
61
 
        num = index.model().data(index, Qt.DisplayRole).toInt()[0]
62
 
        def draw_star():
63
 
            painter.save()
64
 
            painter.scale(self.factor, self.factor)
65
 
            painter.translate(50.0, 50.0)
66
 
            painter.rotate(-20)
67
 
            painter.translate(-50.0, -50.0)
68
 
            painter.drawPath(self.star_path)
69
 
            painter.restore()
70
 
 
71
 
        painter.save()
72
 
        if hasattr(QStyle, 'CE_ItemViewItem'):
73
 
            style.drawControl(QStyle.CE_ItemViewItem, option,
74
 
                    painter, self._parent)
75
 
        elif option.state & QStyle.State_Selected:
76
 
            painter.fillRect(option.rect, option.palette.highlight())
77
 
        try:
78
 
            painter.setRenderHint(QPainter.Antialiasing)
79
 
            painter.setClipRect(option.rect)
80
 
            y = option.rect.center().y()-self.SIZE/2.
81
 
            x = option.rect.right()  - self.SIZE
82
 
            painter.setPen(self.PEN)
83
 
            painter.setBrush(self.brush)
84
 
            painter.translate(x, y)
85
 
            i = 0
86
 
            while i < num:
87
 
                draw_star()
88
 
                painter.translate(-self.SIZE, 0)
89
 
                i += 1
90
 
        except:
91
 
            traceback.print_exc()
92
 
        painter.restore()
93
 
 
94
 
    def createEditor(self, parent, option, index):
95
 
        sb = QStyledItemDelegate.createEditor(self, parent, option, index)
96
 
        sb.setMinimum(0)
97
 
        sb.setMaximum(5)
98
 
        return sb
99
 
 
100
 
class DateDelegate(QStyledItemDelegate):
101
 
 
102
 
    def displayText(self, val, locale):
103
 
        d = val.toDate()
104
 
        return d.toString('dd MMM yyyy')
105
 
 
106
 
    def createEditor(self, parent, option, index):
107
 
        qde = QStyledItemDelegate.createEditor(self, parent, option, index)
108
 
        stdformat = unicode(qde.displayFormat())
109
 
        if 'yyyy' not in stdformat:
110
 
            stdformat = stdformat.replace('yy', 'yyyy')
111
 
        qde.setDisplayFormat(stdformat)
112
 
        qde.setMinimumDate(QDate(101,1,1))
113
 
        qde.setCalendarPopup(True)
114
 
        return qde
115
 
 
116
 
class PubDateDelegate(QStyledItemDelegate):
117
 
 
118
 
    def displayText(self, val, locale):
119
 
        return val.toDate().toString('MMM yyyy')
120
 
 
121
 
    def createEditor(self, parent, option, index):
122
 
        qde = QStyledItemDelegate.createEditor(self, parent, option, index)
123
 
        qde.setDisplayFormat('MM yyyy')
124
 
        qde.setMinimumDate(QDate(101,1,1))
125
 
        qde.setCalendarPopup(True)
126
 
        return qde
127
 
 
128
 
class TextDelegate(QStyledItemDelegate):
129
 
 
130
 
    def __init__(self, parent):
131
 
        '''
132
 
        Delegate for text data. If auto_complete_function needs to return a list
133
 
        of text items to auto-complete with. The funciton is None no
134
 
        auto-complete will be used.
135
 
        '''
136
 
        QStyledItemDelegate.__init__(self, parent)
137
 
        self.auto_complete_function = None
138
 
 
139
 
    def set_auto_complete_function(self, f):
140
 
        self.auto_complete_function = f
141
 
 
142
 
    def createEditor(self, parent, option, index):
143
 
        editor = EnLineEdit(parent)
144
 
        if self.auto_complete_function:
145
 
            complete_items = [i[1] for i in self.auto_complete_function()]
146
 
            completer = QCompleter(complete_items, self)
147
 
            completer.setCaseSensitivity(Qt.CaseInsensitive)
148
 
            completer.setCompletionMode(QCompleter.InlineCompletion)
149
 
            editor.setCompleter(completer)
150
 
        return editor
151
 
 
152
 
class TagsDelegate(QStyledItemDelegate):
153
 
 
154
 
    def __init__(self, parent):
155
 
        QStyledItemDelegate.__init__(self, parent)
156
 
        self.db = None
157
 
 
158
 
    def set_database(self, db):
159
 
        self.db = db
160
 
 
161
 
    def createEditor(self, parent, option, index):
162
 
        if self.db:
163
 
            editor = TagsLineEdit(parent, self.db.all_tags())
164
 
        else:
165
 
            editor = EnLineEdit(parent)
166
 
        return editor
167
 
 
168
 
class BooksModel(QAbstractTableModel):
169
 
 
170
 
    about_to_be_sorted = pyqtSignal(object, name='aboutToBeSorted')
171
 
    sorting_done       = pyqtSignal(object, name='sortingDone')
172
 
 
173
 
    headers = {
174
 
                        'title'     : _("Title"),
175
 
                        'authors'   : _("Author(s)"),
176
 
                        'size'      : _("Size (MB)"),
177
 
                        'timestamp' : _("Date"),
178
 
                        'pubdate'   : _('Published'),
179
 
                        'rating'    : _('Rating'),
180
 
                        'publisher' : _("Publisher"),
181
 
                        'tags'      : _("Tags"),
182
 
                        'series'    : _("Series"),
183
 
                        }
184
 
 
185
 
    def __init__(self, parent=None, buffer=40):
186
 
        QAbstractTableModel.__init__(self, parent)
187
 
        self.db = None
188
 
        self.column_map = config['column_map']
189
 
        self.column_map = [x for x in self.column_map if x in self.headers]
190
 
        self.editable_cols = ['title', 'authors', 'rating', 'publisher',
191
 
                              'tags', 'series', 'timestamp', 'pubdate']
192
 
        self.default_image = QImage(I('book.svg'))
193
 
        self.sorted_on = ('timestamp', Qt.AscendingOrder)
194
 
        self.last_search = '' # The last search performed on this model
195
 
        self.read_config()
196
 
        self.buffer_size = buffer
197
 
        self.cover_cache = None
198
 
 
199
 
    def clear_caches(self):
200
 
        if self.cover_cache:
201
 
            self.cover_cache.clear_cache()
202
 
 
203
 
    def read_config(self):
204
 
        self.use_roman_numbers = config['use_roman_numerals_for_series_number']
205
 
        cols = config['column_map']
206
 
        cols = [x for x in cols if x in self.headers]
207
 
        if cols != self.column_map:
208
 
            self.column_map = cols
209
 
            self.reset()
210
 
            self.emit(SIGNAL('columns_sorted()'))
211
 
 
212
 
    def set_database(self, db):
213
 
        self.db = db
214
 
        self.build_data_convertors()
215
 
 
216
 
    def refresh_ids(self, ids, current_row=-1):
217
 
        rows = self.db.refresh_ids(ids)
218
 
        if rows:
219
 
            self.refresh_rows(rows, current_row=current_row)
220
 
 
221
 
    def refresh_cover_cache(self, ids):
222
 
        if self.cover_cache:
223
 
            self.cover_cache.refresh(ids)
224
 
 
225
 
    def refresh_rows(self, rows, current_row=-1):
226
 
        for row in rows:
227
 
            if self.cover_cache:
228
 
                id = self.db.id(row)
229
 
                self.cover_cache.refresh([id])
230
 
            if row == current_row:
231
 
                self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'),
232
 
                          self.get_book_display_info(row))
233
 
            self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
234
 
                      self.index(row, 0), self.index(row, self.columnCount(QModelIndex())-1))
235
 
 
236
 
    def close(self):
237
 
        self.db.close()
238
 
        self.db = None
239
 
        self.reset()
240
 
 
241
 
    def add_books(self, paths, formats, metadata, add_duplicates=False):
242
 
        ret = self.db.add_books(paths, formats, metadata,
243
 
                                 add_duplicates=add_duplicates)
244
 
        self.count_changed()
245
 
        return ret
246
 
 
247
 
    def add_news(self, path, arg):
248
 
        ret = self.db.add_news(path, arg)
249
 
        self.count_changed()
250
 
        return ret
251
 
 
252
 
    def add_catalog(self, path, title):
253
 
        ret = self.db.add_catalog(path, title)
254
 
        self.count_changed()
255
 
        return ret
256
 
 
257
 
    def count_changed(self, *args):
258
 
        self.emit(SIGNAL('count_changed(int)'), self.db.count())
259
 
 
260
 
    def row_indices(self, index):
261
 
        ''' Return list indices of all cells in index.row()'''
262
 
        return [ self.index(index.row(), c) for c in range(self.columnCount(None))]
263
 
 
264
 
    @property
265
 
    def by_author(self):
266
 
        return self.sorted_on[0] == 'authors'
267
 
 
268
 
    def delete_books(self, indices):
269
 
        ids = map(self.id, indices)
270
 
        for id in ids:
271
 
            self.db.delete_book(id, notify=False)
272
 
        self.count_changed()
273
 
        self.clear_caches()
274
 
        self.reset()
275
 
 
276
 
 
277
 
    def delete_books_by_id(self, ids):
278
 
        for id in ids:
279
 
            try:
280
 
                row = self.db.row(id)
281
 
            except:
282
 
                row = -1
283
 
            if row > -1:
284
 
                self.beginRemoveRows(QModelIndex(), row, row)
285
 
            self.db.delete_book(id)
286
 
            if row > -1:
287
 
                self.endRemoveRows()
288
 
        self.count_changed()
289
 
        self.clear_caches()
290
 
 
291
 
    def books_added(self, num):
292
 
        if num > 0:
293
 
            self.beginInsertRows(QModelIndex(), 0, num-1)
294
 
            self.endInsertRows()
295
 
            self.count_changed()
296
 
 
297
 
    def search(self, text, refinement, reset=True):
298
 
        try:
299
 
            self.db.search(text)
300
 
        except ParseException:
301
 
            self.emit(SIGNAL('searched(PyQt_PyObject)'), False)
302
 
            return
303
 
        self.last_search = text
304
 
        if reset:
305
 
            self.clear_caches()
306
 
            self.reset()
307
 
        if self.last_search:
308
 
            self.emit(SIGNAL('searched(PyQt_PyObject)'), True)
309
 
 
310
 
 
311
 
    def sort(self, col, order, reset=True):
312
 
        if not self.db:
313
 
            return
314
 
        self.about_to_be_sorted.emit(self.db.id)
315
 
        ascending = order == Qt.AscendingOrder
316
 
        self.db.sort(self.column_map[col], ascending)
317
 
        if reset:
318
 
            self.clear_caches()
319
 
            self.reset()
320
 
        self.sorted_on = (self.column_map[col], order)
321
 
        self.sorting_done.emit(self.db.index)
322
 
 
323
 
    def refresh(self, reset=True):
324
 
        try:
325
 
            col = self.column_map.index(self.sorted_on[0])
326
 
        except:
327
 
            col = 0
328
 
        self.db.refresh(field=self.column_map[col],
329
 
                        ascending=self.sorted_on[1]==Qt.AscendingOrder)
330
 
        if reset:
331
 
            self.reset()
332
 
 
333
 
    def resort(self, reset=True):
334
 
        try:
335
 
            col = self.column_map.index(self.sorted_on[0])
336
 
        except:
337
 
            col = 0
338
 
        self.sort(col, self.sorted_on[1], reset=reset)
339
 
 
340
 
    def research(self, reset=True):
341
 
        self.search(self.last_search, False, reset=reset)
342
 
 
343
 
    def columnCount(self, parent):
344
 
        if parent and parent.isValid():
345
 
            return 0
346
 
        return len(self.column_map)
347
 
 
348
 
    def rowCount(self, parent):
349
 
        if parent and parent.isValid():
350
 
            return 0
351
 
        return len(self.db.data) if self.db else 0
352
 
 
353
 
    def count(self):
354
 
        return self.rowCount(None)
355
 
 
356
 
    def get_book_display_info(self, idx):
357
 
        data = {}
358
 
        cdata = self.cover(idx)
359
 
        if cdata:
360
 
            data['cover'] = cdata
361
 
        tags = self.db.tags(idx)
362
 
        if tags:
363
 
            tags = tags.replace(',', ', ')
364
 
        else:
365
 
            tags = _('None')
366
 
        data[_('Tags')] = tags
367
 
        formats = self.db.formats(idx)
368
 
        if formats:
369
 
            formats = formats.replace(',', ', ')
370
 
        else:
371
 
            formats = _('None')
372
 
        data[_('Formats')] = formats
373
 
        data[_('Path')] = self.db.abspath(idx)
374
 
        comments = self.db.comments(idx)
375
 
        if not comments:
376
 
            comments = _('None')
377
 
        data[_('Comments')] = comments
378
 
        series = self.db.series(idx)
379
 
        if series:
380
 
            sidx = self.db.series_index(idx)
381
 
            sidx = fmt_sidx(sidx, use_roman = self.use_roman_numbers)
382
 
            data[_('Series')] = _('Book <font face="serif">%s</font> of %s.')%(sidx, series)
383
 
 
384
 
        return data
385
 
 
386
 
    def set_cache(self, idx):
387
 
        l, r = 0, self.count()-1
388
 
        if self.cover_cache:
389
 
            l = max(l, idx-self.buffer_size)
390
 
            r = min(r, idx+self.buffer_size)
391
 
            k = min(r-idx, idx-l)
392
 
            ids = [idx]
393
 
            for i in range(1, k):
394
 
                ids.extend([idx-i, idx+i])
395
 
            ids = ids + [i for i in range(l, r, 1) if i not in ids]
396
 
            try:
397
 
                ids = [self.db.id(i) for i in ids]
398
 
            except IndexError:
399
 
                return
400
 
            self.cover_cache.set_cache(ids)
401
 
 
402
 
    def current_changed(self, current, previous, emit_signal=True):
403
 
        idx = current.row()
404
 
        self.set_cache(idx)
405
 
        data = self.get_book_display_info(idx)
406
 
        if emit_signal:
407
 
            self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data)
408
 
        else:
409
 
            return data
410
 
 
411
 
    def get_book_info(self, index):
412
 
        if isinstance(index, int):
413
 
            index = self.index(index, 0)
414
 
        data = self.current_changed(index, None, False)
415
 
        row = index.row()
416
 
        data[_('Title')] = self.db.title(row)
417
 
        au = self.db.authors(row)
418
 
        if not au:
419
 
            au = _('Unknown')
420
 
        au = ', '.join([a.strip() for a in au.split(',')])
421
 
        data[_('Author(s)')] = au
422
 
        return data
423
 
 
424
 
    def metadata_for(self, ids):
425
 
        ans = []
426
 
        for id in ids:
427
 
            mi = self.db.get_metadata(id, index_is_id=True, get_cover=True)
428
 
            if mi.series is not None:
429
 
                mi.tag_order = { mi.series: self.db.books_in_series_of(id,
430
 
                    index_is_id=True)}
431
 
            ans.append(mi)
432
 
        return ans
433
 
 
434
 
    def get_metadata(self, rows, rows_are_ids=False, full_metadata=False):
435
 
        metadata, _full_metadata = [], []
436
 
        if not rows_are_ids:
437
 
            rows = [self.db.id(row.row()) for row in rows]
438
 
        for id in rows:
439
 
            mi = self.db.get_metadata(id, index_is_id=True)
440
 
            _full_metadata.append(mi)
441
 
            au = authors_to_string(mi.authors if mi.authors else [_('Unknown')])
442
 
            tags = mi.tags if mi.tags else []
443
 
            if mi.series is not None:
444
 
                tags.append(mi.series)
445
 
            info = {
446
 
                  'title'   : mi.title,
447
 
                  'authors' : au,
448
 
                  'author_sort' : mi.author_sort,
449
 
                  'cover'   : self.db.cover(id, index_is_id=True),
450
 
                  'tags'    : tags,
451
 
                  'comments': mi.comments,
452
 
                  }
453
 
            if mi.series is not None:
454
 
                info['tag order'] = {
455
 
                    mi.series:self.db.books_in_series_of(id, index_is_id=True)
456
 
                }
457
 
 
458
 
            metadata.append(info)
459
 
        if full_metadata:
460
 
            return metadata, _full_metadata
461
 
        else:
462
 
            return metadata
463
 
 
464
 
    def get_preferred_formats_from_ids(self, ids, formats, paths=False,
465
 
                              set_metadata=False, specific_format=None,
466
 
                              exclude_auto=False, mode='r+b'):
467
 
        ans = []
468
 
        need_auto = []
469
 
        if specific_format is not None:
470
 
            formats = [specific_format.lower()]
471
 
        for id in ids:
472
 
            format = None
473
 
            fmts = self.db.formats(id, index_is_id=True)
474
 
            if not fmts:
475
 
                fmts = ''
476
 
            db_formats = set(fmts.lower().split(','))
477
 
            available_formats = set([f.lower() for f in formats])
478
 
            u = available_formats.intersection(db_formats)
479
 
            for f in formats:
480
 
                if f.lower() in u:
481
 
                    format = f
482
 
                    break
483
 
            if format is not None:
484
 
                pt = PersistentTemporaryFile(suffix='.'+format)
485
 
                with closing(self.db.format(id, format, index_is_id=True,
486
 
                    as_file=True)) as src:
487
 
                    shutil.copyfileobj(src, pt)
488
 
                    pt.flush()
489
 
                pt.seek(0)
490
 
                if set_metadata:
491
 
                    _set_metadata(pt, self.db.get_metadata(id, get_cover=True, index_is_id=True),
492
 
                                  format)
493
 
                pt.close() if paths else pt.seek(0)
494
 
                ans.append(pt)
495
 
            else:
496
 
                need_auto.append(id)
497
 
                if not exclude_auto:
498
 
                    ans.append(None)
499
 
        return ans, need_auto
500
 
 
501
 
    def get_preferred_formats(self, rows, formats, paths=False,
502
 
                              set_metadata=False, specific_format=None,
503
 
                              exclude_auto=False):
504
 
        ans = []
505
 
        need_auto = []
506
 
        if specific_format is not None:
507
 
            formats = [specific_format.lower()]
508
 
        for row in (row.row() for row in rows):
509
 
            format = None
510
 
            fmts = self.db.formats(row)
511
 
            if not fmts:
512
 
                fmts = ''
513
 
            db_formats = set(fmts.lower().split(','))
514
 
            available_formats = set([f.lower() for f in formats])
515
 
            u = available_formats.intersection(db_formats)
516
 
            for f in formats:
517
 
                if f.lower() in u:
518
 
                    format = f
519
 
                    break
520
 
            if format is not None:
521
 
                pt = PersistentTemporaryFile(suffix='.'+format)
522
 
                with closing(self.db.format(row, format, as_file=True)) as src:
523
 
                    shutil.copyfileobj(src, pt)
524
 
                    pt.flush()
525
 
                pt.seek(0)
526
 
                if set_metadata:
527
 
                    _set_metadata(pt, self.db.get_metadata(row, get_cover=True),
528
 
                                  format)
529
 
                pt.close() if paths else pt.seek(0)
530
 
                ans.append(pt)
531
 
            else:
532
 
                need_auto.append(row)
533
 
                if not exclude_auto:
534
 
                    ans.append(None)
535
 
        return ans, need_auto
536
 
 
537
 
    def id(self, row):
538
 
        return self.db.id(getattr(row, 'row', lambda:row)())
539
 
 
540
 
    def title(self, row_number):
541
 
        return self.db.title(row_number)
542
 
 
543
 
    def cover(self, row_number):
544
 
        data = None
545
 
        try:
546
 
            id = self.db.id(row_number)
547
 
            if self.cover_cache:
548
 
                img = self.cover_cache.cover(id)
549
 
                if img:
550
 
                    if img.isNull():
551
 
                        img = self.default_image
552
 
                    return img
553
 
            if not data:
554
 
                data = self.db.cover(row_number)
555
 
        except IndexError: # Happens if database has not yet been refreshed
556
 
            pass
557
 
 
558
 
        if not data:
559
 
            return self.default_image
560
 
        img = QImage()
561
 
        img.loadFromData(data)
562
 
        if img.isNull():
563
 
            img = self.default_image
564
 
        return img
565
 
 
566
 
    def build_data_convertors(self):
567
 
 
568
 
        tidx = self.db.FIELD_MAP['title']
569
 
        aidx = self.db.FIELD_MAP['authors']
570
 
        sidx = self.db.FIELD_MAP['size']
571
 
        ridx = self.db.FIELD_MAP['rating']
572
 
        pidx = self.db.FIELD_MAP['publisher']
573
 
        tmdx = self.db.FIELD_MAP['timestamp']
574
 
        pddx = self.db.FIELD_MAP['pubdate']
575
 
        srdx = self.db.FIELD_MAP['series']
576
 
        tgdx = self.db.FIELD_MAP['tags']
577
 
        siix = self.db.FIELD_MAP['series_index']
578
 
 
579
 
        def authors(r):
580
 
            au = self.db.data[r][aidx]
581
 
            if au:
582
 
                au = [a.strip().replace('|', ',') for a in au.split(',')]
583
 
                return ' & '.join(au)
584
 
 
585
 
        def timestamp(r):
586
 
            dt = self.db.data[r][tmdx]
587
 
            if dt:
588
 
                return QDate(dt.year, dt.month, dt.day)
589
 
 
590
 
        def pubdate(r):
591
 
            dt = self.db.data[r][pddx]
592
 
            if dt:
593
 
                return QDate(dt.year, dt.month, dt.day)
594
 
 
595
 
        def rating(r):
596
 
            r = self.db.data[r][ridx]
597
 
            r = r/2 if r else 0
598
 
            return r
599
 
 
600
 
        def publisher(r):
601
 
            pub = self.db.data[r][pidx]
602
 
            if pub:
603
 
                return pub
604
 
 
605
 
        def tags(r):
606
 
            tags = self.db.data[r][tgdx]
607
 
            if tags:
608
 
                return ', '.join(sorted(tags.split(',')))
609
 
 
610
 
        def series(r):
611
 
            series = self.db.data[r][srdx]
612
 
            if series:
613
 
                idx = fmt_sidx(self.db.data[r][siix])
614
 
                return series + ' [%s]'%idx
615
 
        def size(r):
616
 
            size = self.db.data[r][sidx]
617
 
            if size:
618
 
                return '%.1f'%(float(size)/(1024*1024))
619
 
 
620
 
        self.dc = {
621
 
                   'title'    : lambda r : self.db.data[r][tidx],
622
 
                   'authors'  : authors,
623
 
                   'size'     : size,
624
 
                   'timestamp': timestamp,
625
 
                   'pubdate' : pubdate,
626
 
                   'rating'   : rating,
627
 
                   'publisher': publisher,
628
 
                   'tags'     : tags,
629
 
                   'series'   : series,
630
 
                   }
631
 
 
632
 
    def data(self, index, role):
633
 
        if role in (Qt.DisplayRole, Qt.EditRole):
634
 
            ans = self.dc[self.column_map[index.column()]](index.row())
635
 
            return NONE if ans is None else QVariant(ans)
636
 
        #elif role == Qt.TextAlignmentRole and self.column_map[index.column()] in ('size', 'timestamp'):
637
 
        #    return QVariant(Qt.AlignVCenter | Qt.AlignCenter)
638
 
        #elif role == Qt.ToolTipRole and index.isValid():
639
 
        #    if self.column_map[index.column()] in self.editable_cols:
640
 
        #        return QVariant(_("Double click to <b>edit</b> me<br><br>"))
641
 
        return NONE
642
 
 
643
 
    def headerData(self, section, orientation, role):
644
 
        if role != Qt.DisplayRole:
645
 
            return NONE
646
 
        if orientation == Qt.Horizontal:
647
 
            return QVariant(self.headers[self.column_map[section]])
648
 
        else:
649
 
            return QVariant(section+1)
650
 
 
651
 
    def flags(self, index):
652
 
        flags = QAbstractTableModel.flags(self, index)
653
 
        if index.isValid():
654
 
            if self.column_map[index.column()] in self.editable_cols:
655
 
                flags |= Qt.ItemIsEditable
656
 
        return flags
657
 
 
658
 
    def setData(self, index, value, role):
659
 
        if role == Qt.EditRole:
660
 
            row, col = index.row(), index.column()
661
 
            column = self.column_map[col]
662
 
            if column not in self.editable_cols:
663
 
                return False
664
 
            val = int(value.toInt()[0]) if column == 'rating' else \
665
 
                  value.toDate() if column in ('timestamp', 'pubdate') else \
666
 
                  unicode(value.toString())
667
 
            id = self.db.id(row)
668
 
            if column == 'rating':
669
 
                val = 0 if val < 0 else 5 if val > 5 else val
670
 
                val *= 2
671
 
                self.db.set_rating(id, val)
672
 
            elif column == 'series':
673
 
                val = val.strip()
674
 
                pat = re.compile(r'\[([.0-9]+)\]')
675
 
                match = pat.search(val)
676
 
                if match is not None:
677
 
                    self.db.set_series_index(id, float(match.group(1)))
678
 
                    val = pat.sub('', val).strip()
679
 
                elif val:
680
 
                    if tweaks['series_index_auto_increment'] == 'next':
681
 
                        ni = self.db.get_next_series_num_for(val)
682
 
                        if ni != 1:
683
 
                            self.db.set_series_index(id, ni)
684
 
                if val:
685
 
                    self.db.set_series(id, val)
686
 
            elif column == 'timestamp':
687
 
                if val.isNull() or not val.isValid():
688
 
                    return False
689
 
                self.db.set_timestamp(id, qt_to_dt(val, as_utc=False))
690
 
            elif column == 'pubdate':
691
 
                if val.isNull() or not val.isValid():
692
 
                    return False
693
 
                self.db.set_pubdate(id, qt_to_dt(val, as_utc=False))
694
 
            else:
695
 
                self.db.set(row, column, val)
696
 
            self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
697
 
                                index, index)
698
 
            #if column == self.sorted_on[0]:
699
 
            #    self.resort()
700
 
 
701
 
        return True
702
 
 
703
 
class BooksView(TableView):
704
 
    TIME_FMT = '%d %b %Y'
705
 
    wrapper = textwrap.TextWrapper(width=20)
706
 
 
707
 
    @classmethod
708
 
    def wrap(cls, s, width=20):
709
 
        cls.wrapper.width = width
710
 
        return cls.wrapper.fill(s)
711
 
 
712
 
    @classmethod
713
 
    def human_readable(cls, size, precision=1):
714
 
        """ Convert a size in bytes into megabytes """
715
 
        return ('%.'+str(precision)+'f') % ((size/(1024.*1024.)),)
716
 
 
717
 
    def __init__(self, parent, modelcls=BooksModel):
718
 
        TableView.__init__(self, parent)
719
 
        self.rating_delegate = LibraryDelegate(self)
720
 
        self.timestamp_delegate = DateDelegate(self)
721
 
        self.pubdate_delegate = PubDateDelegate(self)
722
 
        self.tags_delegate = TagsDelegate(self)
723
 
        self.authors_delegate = TextDelegate(self)
724
 
        self.series_delegate = TextDelegate(self)
725
 
        self.publisher_delegate = TextDelegate(self)
726
 
        self.display_parent = parent
727
 
        self._model = modelcls(self)
728
 
        self.setModel(self._model)
729
 
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
730
 
        self.setSortingEnabled(True)
731
 
        for i in range(10):
732
 
            self.setItemDelegateForColumn(i, TextDelegate(self))
733
 
        self.columns_sorted()
734
 
        QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
735
 
                        self._model.current_changed)
736
 
        self.connect(self._model, SIGNAL('columns_sorted()'),
737
 
                self.columns_sorted, Qt.QueuedConnection)
738
 
        hv = self.verticalHeader()
739
 
        hv.setClickable(True)
740
 
        hv.setCursor(Qt.PointingHandCursor)
741
 
        self.selected_ids = []
742
 
        self._model.about_to_be_sorted.connect(self.about_to_be_sorted)
743
 
        self._model.sorting_done.connect(self.sorting_done)
744
 
 
745
 
    def about_to_be_sorted(self, idc):
746
 
        selected_rows = [r.row() for r in self.selectionModel().selectedRows()]
747
 
        self.selected_ids = [idc(r) for r in selected_rows]
748
 
 
749
 
    def sorting_done(self, indexc):
750
 
        if self.selected_ids:
751
 
            indices = [self.model().index(indexc(i), 0) for i in
752
 
                    self.selected_ids]
753
 
            sm = self.selectionModel()
754
 
            for idx in indices:
755
 
                sm.select(idx, sm.Select|sm.Rows)
756
 
        self.selected_ids = []
757
 
 
758
 
    def columns_sorted(self):
759
 
        for i in range(self.model().columnCount(None)):
760
 
            if self.itemDelegateForColumn(i) in (self.rating_delegate,
761
 
                    self.timestamp_delegate, self.pubdate_delegate):
762
 
                self.setItemDelegateForColumn(i, self.itemDelegate())
763
 
 
764
 
        cm = self._model.column_map
765
 
 
766
 
        if 'rating' in cm:
767
 
            self.setItemDelegateForColumn(cm.index('rating'), self.rating_delegate)
768
 
        if 'timestamp' in cm:
769
 
            self.setItemDelegateForColumn(cm.index('timestamp'), self.timestamp_delegate)
770
 
        if 'pubdate' in cm:
771
 
            self.setItemDelegateForColumn(cm.index('pubdate'), self.pubdate_delegate)
772
 
        if 'tags' in cm:
773
 
            self.setItemDelegateForColumn(cm.index('tags'), self.tags_delegate)
774
 
        if 'authors' in cm:
775
 
            self.setItemDelegateForColumn(cm.index('authors'), self.authors_delegate)
776
 
        if 'publisher' in cm:
777
 
            self.setItemDelegateForColumn(cm.index('publisher'), self.publisher_delegate)
778
 
        if 'series' in cm:
779
 
            self.setItemDelegateForColumn(cm.index('series'), self.series_delegate)
780
 
 
781
 
    def set_context_menu(self, edit_metadata, send_to_device, convert, view,
782
 
                         save, open_folder, book_details, delete, similar_menu=None):
783
 
        self.setContextMenuPolicy(Qt.DefaultContextMenu)
784
 
        self.context_menu = QMenu(self)
785
 
        if edit_metadata is not None:
786
 
            self.context_menu.addAction(edit_metadata)
787
 
        if send_to_device is not None:
788
 
            self.context_menu.addAction(send_to_device)
789
 
        if convert is not None:
790
 
            self.context_menu.addAction(convert)
791
 
        self.context_menu.addAction(view)
792
 
        self.context_menu.addAction(save)
793
 
        if open_folder is not None:
794
 
            self.context_menu.addAction(open_folder)
795
 
        if delete is not None:
796
 
            self.context_menu.addAction(delete)
797
 
        if book_details is not None:
798
 
            self.context_menu.addAction(book_details)
799
 
        if similar_menu is not None:
800
 
            self.context_menu.addMenu(similar_menu)
801
 
 
802
 
    def contextMenuEvent(self, event):
803
 
        self.context_menu.popup(event.globalPos())
804
 
        event.accept()
805
 
 
806
 
    def sortByColumn(self, colname, order):
807
 
        try:
808
 
            idx = self._model.column_map.index(colname)
809
 
        except ValueError:
810
 
            idx = 0
811
 
        TableView.sortByColumn(self, idx, order)
812
 
 
813
 
    @classmethod
814
 
    def paths_from_event(cls, event):
815
 
        '''
816
 
        Accept a drop event and return a list of paths that can be read from
817
 
        and represent files with extensions.
818
 
        '''
819
 
        if event.mimeData().hasFormat('text/uri-list'):
820
 
            urls = [qstring_to_unicode(u.toLocalFile()) for u in event.mimeData().urls()]
821
 
            return [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
822
 
 
823
 
    def dragEnterEvent(self, event):
824
 
        if int(event.possibleActions() & Qt.CopyAction) + \
825
 
           int(event.possibleActions() & Qt.MoveAction) == 0:
826
 
            return
827
 
        paths = self.paths_from_event(event)
828
 
 
829
 
        if paths:
830
 
            event.acceptProposedAction()
831
 
 
832
 
    def dragMoveEvent(self, event):
833
 
        event.acceptProposedAction()
834
 
 
835
 
    def dropEvent(self, event):
836
 
        paths = self.paths_from_event(event)
837
 
        event.setDropAction(Qt.CopyAction)
838
 
        event.accept()
839
 
        self.emit(SIGNAL('files_dropped(PyQt_PyObject)'), paths)
840
 
 
841
 
 
842
 
    def set_database(self, db):
843
 
        self._model.set_database(db)
844
 
        self.tags_delegate.set_database(db)
845
 
        self.authors_delegate.set_auto_complete_function(db.all_authors)
846
 
        self.series_delegate.set_auto_complete_function(db.all_series)
847
 
        self.publisher_delegate.set_auto_complete_function(db.all_publishers)
848
 
 
849
 
    def close(self):
850
 
        self._model.close()
851
 
 
852
 
    def set_editable(self, editable):
853
 
        self._model.set_editable(editable)
854
 
 
855
 
    def connect_to_search_box(self, sb, search_done):
856
 
        QObject.connect(sb, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'),
857
 
                        self._model.search)
858
 
        self._search_done = search_done
859
 
        self.connect(self._model, SIGNAL('searched(PyQt_PyObject)'),
860
 
                self.search_done)
861
 
 
862
 
    def connect_to_book_display(self, bd):
863
 
        QObject.connect(self._model, SIGNAL('new_bookdisplay_data(PyQt_PyObject)'),
864
 
                        bd)
865
 
 
866
 
    def search_done(self, ok):
867
 
        self._search_done(self, ok)
868
 
 
869
 
    def row_count(self):
870
 
        return self._model.count()
871
 
 
872
 
 
873
 
class DeviceBooksView(BooksView):
874
 
 
875
 
    def __init__(self, parent):
876
 
        BooksView.__init__(self, parent, DeviceBooksModel)
877
 
        self.columns_resized = False
878
 
        self.resize_on_select = False
879
 
        self.rating_delegate = None
880
 
        for i in range(10):
881
 
            self.setItemDelegateForColumn(i, TextDelegate(self))
882
 
        self.setDragDropMode(self.NoDragDrop)
883
 
        self.setAcceptDrops(False)
884
 
 
885
 
    def set_database(self, db):
886
 
        self._model.set_database(db)
887
 
 
888
 
    def resizeColumnsToContents(self):
889
 
        QTableView.resizeColumnsToContents(self)
890
 
        self.columns_resized = True
891
 
 
892
 
    def connect_dirtied_signal(self, slot):
893
 
        QObject.connect(self._model, SIGNAL('booklist_dirtied()'), slot)
894
 
 
895
 
    def sortByColumn(self, col, order):
896
 
        TableView.sortByColumn(self, col, order)
897
 
 
898
 
    def dropEvent(self, *args):
899
 
        error_dialog(self, _('Not allowed'),
900
 
        _('Dropping onto a device is not supported. First add the book to the calibre library.')).exec_()
901
 
 
902
 
class OnDeviceSearch(SearchQueryParser):
903
 
 
904
 
    def __init__(self, model):
905
 
        SearchQueryParser.__init__(self)
906
 
        self.model = model
907
 
 
908
 
    def universal_set(self):
909
 
        return set(range(0, len(self.model.db)))
910
 
 
911
 
    def get_matches(self, location, query):
912
 
        location = location.lower().strip()
913
 
 
914
 
        matchkind = CONTAINS_MATCH
915
 
        if len(query) > 1:
916
 
            if query.startswith('\\'):
917
 
                query = query[1:]
918
 
            elif query.startswith('='):
919
 
                matchkind = EQUALS_MATCH
920
 
                query = query[1:]
921
 
            elif query.startswith('~'):
922
 
                matchkind = REGEXP_MATCH
923
 
                query = query[1:]
924
 
        if matchkind != REGEXP_MATCH: ### leave case in regexps because it can be significant e.g. \S \W \D
925
 
            query = query.lower()
926
 
 
927
 
        if location not in ('title', 'author', 'tag', 'all', 'format'):
928
 
            return set([])
929
 
        matches = set([])
930
 
        locations = ['title', 'author', 'tag', 'format'] if location == 'all' else [location]
931
 
        q = {
932
 
             'title' : lambda x : getattr(x, 'title').lower(),
933
 
             'author': lambda x: getattr(x, 'authors').lower(),
934
 
             'tag':lambda x: ','.join(getattr(x, 'tags')).lower(),
935
 
             'format':lambda x: os.path.splitext(x.path)[1].lower()
936
 
             }
937
 
        for index, row in enumerate(self.model.db):
938
 
            for locvalue in locations:
939
 
                accessor = q[locvalue]
940
 
                try:
941
 
                    ### Can't separate authors because comma is used for name sep and author sep
942
 
                    ### Exact match might not get what you want. For that reason, turn author
943
 
                    ### exactmatch searches into contains searches.
944
 
                    if locvalue == 'author' and matchkind == EQUALS_MATCH:
945
 
                        m = CONTAINS_MATCH
946
 
                    else:
947
 
                        m = matchkind
948
 
 
949
 
                    if locvalue == 'tag':
950
 
                        vals = accessor(row).split(',')
951
 
                    else:
952
 
                        vals = [accessor(row)]
953
 
                    if _match(query, vals, m):
954
 
                        matches.add(index)
955
 
                        break
956
 
                except ValueError: # Unicode errors
957
 
                    import traceback
958
 
                    traceback.print_exc()
959
 
        return matches
960
 
 
961
 
 
962
 
class DeviceBooksModel(BooksModel):
963
 
 
964
 
    def __init__(self, parent):
965
 
        BooksModel.__init__(self, parent)
966
 
        self.db  = []
967
 
        self.map = []
968
 
        self.sorted_map = []
969
 
        self.unknown = _('Unknown')
970
 
        self.marked_for_deletion = {}
971
 
        self.search_engine = OnDeviceSearch(self)
972
 
        self.editable = True
973
 
 
974
 
    def mark_for_deletion(self, job, rows):
975
 
        self.marked_for_deletion[job] = self.indices(rows)
976
 
        for row in rows:
977
 
            indices = self.row_indices(row)
978
 
            self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1])
979
 
 
980
 
    def deletion_done(self, job, succeeded=True):
981
 
        if not self.marked_for_deletion.has_key(job):
982
 
            return
983
 
        rows = self.marked_for_deletion.pop(job)
984
 
        for row in rows:
985
 
            if not succeeded:
986
 
                indices = self.row_indices(self.index(row, 0))
987
 
                self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1])
988
 
 
989
 
    def paths_deleted(self, paths):
990
 
        self.map = list(range(0, len(self.db)))
991
 
        self.resort(False)
992
 
        self.research(True)
993
 
 
994
 
    def indices_to_be_deleted(self):
995
 
        ans = []
996
 
        for v in self.marked_for_deletion.values():
997
 
            ans.extend(v)
998
 
        return ans
999
 
 
1000
 
    def flags(self, index):
1001
 
        if self.map[index.row()] in self.indices_to_be_deleted():
1002
 
            return Qt.ItemIsUserCheckable  # Can't figure out how to get the disabled flag in python
1003
 
        flags = QAbstractTableModel.flags(self, index)
1004
 
        if index.isValid() and self.editable:
1005
 
            if index.column() in [0, 1] or (index.column() == 4 and self.db.supports_tags()):
1006
 
                flags |= Qt.ItemIsEditable
1007
 
        return flags
1008
 
 
1009
 
 
1010
 
    def search(self, text, refinement, reset=True):
1011
 
        if not text or not text.strip():
1012
 
            self.map = list(range(len(self.db)))
1013
 
        else:
1014
 
            try:
1015
 
                matches = self.search_engine.parse(text)
1016
 
            except ParseException:
1017
 
                self.emit(SIGNAL('searched(PyQt_PyObject)'), False)
1018
 
                return
1019
 
 
1020
 
            self.map = []
1021
 
            for i in range(len(self.db)):
1022
 
                if i in matches:
1023
 
                    self.map.append(i)
1024
 
        self.resort(reset=False)
1025
 
        if reset:
1026
 
            self.reset()
1027
 
        self.last_search = text
1028
 
        if self.last_search:
1029
 
            self.emit(SIGNAL('searched(PyQt_PyObject)'), True)
1030
 
 
1031
 
 
1032
 
    def resort(self, reset):
1033
 
        self.sort(self.sorted_on[0], self.sorted_on[1], reset=reset)
1034
 
 
1035
 
    def sort(self, col, order, reset=True):
1036
 
        descending = order != Qt.AscendingOrder
1037
 
        def strcmp(attr):
1038
 
            ag = attrgetter(attr)
1039
 
            def _strcmp(x, y):
1040
 
                x = ag(self.db[x])
1041
 
                y = ag(self.db[y])
1042
 
                if x == None:
1043
 
                    x = ''
1044
 
                if y == None:
1045
 
                    y = ''
1046
 
                x, y = x.strip().lower(), y.strip().lower()
1047
 
                return cmp(x, y)
1048
 
            return _strcmp
1049
 
        def datecmp(x, y):
1050
 
            x = self.db[x].datetime
1051
 
            y = self.db[y].datetime
1052
 
            return cmp(dt_factory(x, assume_utc=True), dt_factory(y,
1053
 
                assume_utc=True))
1054
 
        def sizecmp(x, y):
1055
 
            x, y = int(self.db[x].size), int(self.db[y].size)
1056
 
            return cmp(x, y)
1057
 
        def tagscmp(x, y):
1058
 
            x, y = ','.join(self.db[x].tags), ','.join(self.db[y].tags)
1059
 
            return cmp(x, y)
1060
 
        fcmp = strcmp('title_sorter') if col == 0 else strcmp('authors') if col == 1 else \
1061
 
               sizecmp if col == 2 else datecmp if col == 3 else tagscmp
1062
 
        self.map.sort(cmp=fcmp, reverse=descending)
1063
 
        if len(self.map) == len(self.db):
1064
 
            self.sorted_map = list(self.map)
1065
 
        else:
1066
 
            self.sorted_map = list(range(len(self.db)))
1067
 
            self.sorted_map.sort(cmp=fcmp, reverse=descending)
1068
 
        self.sorted_on = (col, order)
1069
 
        if reset:
1070
 
            self.reset()
1071
 
 
1072
 
    def columnCount(self, parent):
1073
 
        if parent and parent.isValid():
1074
 
            return 0
1075
 
        return 5
1076
 
 
1077
 
    def rowCount(self, parent):
1078
 
        if parent and parent.isValid():
1079
 
            return 0
1080
 
        return len(self.map)
1081
 
 
1082
 
    def set_database(self, db):
1083
 
        self.db = db
1084
 
        self.map = list(range(0, len(db)))
1085
 
 
1086
 
    def current_changed(self, current, previous):
1087
 
        data = {}
1088
 
        item = self.db[self.map[current.row()]]
1089
 
        cdata = item.thumbnail
1090
 
        if cdata:
1091
 
            img = QImage()
1092
 
            img.loadFromData(cdata)
1093
 
            if img.isNull():
1094
 
                img = self.default_image
1095
 
            data['cover'] = img
1096
 
        type = _('Unknown')
1097
 
        ext = os.path.splitext(item.path)[1]
1098
 
        if ext:
1099
 
            type = ext[1:].lower()
1100
 
        data[_('Format')] = type
1101
 
        data[_('Path')] = item.path
1102
 
        dt = dt_factory(item.datetime, assume_utc=True)
1103
 
        data[_('Timestamp')] = isoformat(dt, sep=' ', as_utc=False)
1104
 
        data[_('Tags')] = ', '.join(item.tags)
1105
 
        self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data)
1106
 
 
1107
 
    def paths(self, rows):
1108
 
        return [self.db[self.map[r.row()]].path for r in rows ]
1109
 
 
1110
 
    def indices(self, rows):
1111
 
        '''
1112
 
        Return indices into underlying database from rows
1113
 
        '''
1114
 
        return [ self.map[r.row()] for r in rows]
1115
 
 
1116
 
 
1117
 
    def data(self, index, role):
1118
 
        if role == Qt.DisplayRole or role == Qt.EditRole:
1119
 
            row, col = index.row(), index.column()
1120
 
            if col == 0:
1121
 
                text = self.db[self.map[row]].title
1122
 
                if not text:
1123
 
                    text = self.unknown
1124
 
                return QVariant(text)
1125
 
            elif col == 1:
1126
 
                au = self.db[self.map[row]].authors
1127
 
                if not au:
1128
 
                    au = self.unknown
1129
 
                if role == Qt.EditRole:
1130
 
                    return QVariant(au)
1131
 
                authors = string_to_authors(au)
1132
 
                return QVariant("\n".join(authors))
1133
 
            elif col == 2:
1134
 
                size = self.db[self.map[row]].size
1135
 
                return QVariant(BooksView.human_readable(size))
1136
 
            elif col == 3:
1137
 
                dt = self.db[self.map[row]].datetime
1138
 
                dt = dt_factory(dt, assume_utc=True, as_utc=False)
1139
 
                return QVariant(strftime(BooksView.TIME_FMT, dt.timetuple()))
1140
 
            elif col == 4:
1141
 
                tags = self.db[self.map[row]].tags
1142
 
                if tags:
1143
 
                    return QVariant(', '.join(tags))
1144
 
        elif role == Qt.TextAlignmentRole and index.column() in [2, 3]:
1145
 
            return QVariant(Qt.AlignRight | Qt.AlignVCenter)
1146
 
        elif role == Qt.ToolTipRole and index.isValid():
1147
 
            if self.map[index.row()] in self.indices_to_be_deleted():
1148
 
                return QVariant('Marked for deletion')
1149
 
            col = index.column()
1150
 
            if col in [0, 1] or (col == 4 and self.db.supports_tags()):
1151
 
                return QVariant(_("Double click to <b>edit</b> me<br><br>"))
1152
 
        return NONE
1153
 
 
1154
 
    def headerData(self, section, orientation, role):
1155
 
        if role != Qt.DisplayRole:
1156
 
            return NONE
1157
 
        text = ""
1158
 
        if orientation == Qt.Horizontal:
1159
 
            if   section == 0: text = _("Title")
1160
 
            elif section == 1: text = _("Author(s)")
1161
 
            elif section == 2: text = _("Size (MB)")
1162
 
            elif section == 3: text = _("Date")
1163
 
            elif section == 4: text = _("Tags")
1164
 
            return QVariant(text)
1165
 
        else:
1166
 
            return QVariant(section+1)
1167
 
 
1168
 
    def setData(self, index, value, role):
1169
 
        done = False
1170
 
        if role == Qt.EditRole:
1171
 
            row, col = index.row(), index.column()
1172
 
            if col in [2, 3]:
1173
 
                return False
1174
 
            val = qstring_to_unicode(value.toString()).strip()
1175
 
            idx = self.map[row]
1176
 
            if col == 0:
1177
 
                self.db[idx].title = val
1178
 
                self.db[idx].title_sorter = val
1179
 
            elif col == 1:
1180
 
                self.db[idx].authors = val
1181
 
            elif col == 4:
1182
 
                tags = [i.strip() for i in val.split(',')]
1183
 
                tags = [t for t in tags if t]
1184
 
                self.db.set_tags(self.db[idx], tags)
1185
 
            self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
1186
 
            self.emit(SIGNAL('booklist_dirtied()'))
1187
 
            if col == self.sorted_on[0]:
1188
 
                self.sort(col, self.sorted_on[1])
1189
 
            done = True
1190
 
        return done
1191
 
 
1192
 
    def set_editable(self, editable):
1193
 
        self.editable = editable
1194
 
 
1195
 
 
1196