9
9
from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \
10
10
QItemDelegate, QPainterPath, QLinearGradient, QBrush, \
11
11
QPen, QStyle, QPainter, QLineEdit, \
12
QPalette, QImage, QApplication, QMenu, QStyledItemDelegate
12
QPalette, QImage, QApplication, QMenu, \
13
QStyledItemDelegate, QCompleter
13
14
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \
14
15
SIGNAL, QObject, QSize, QModelIndex, QDate
16
17
from calibre import strftime
17
18
from calibre.ptempfile import PersistentTemporaryFile
19
from calibre.utils.pyparsing import ParseException
18
20
from calibre.library.database2 import FIELD_MAP
19
21
from calibre.gui2 import NONE, TableView, qstring_to_unicode, config, \
23
from calibre.gui2.widgets import EnLineEdit, TagsLineEdit
21
24
from calibre.utils.search_query_parser import SearchQueryParser
22
25
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
23
from calibre.ebooks.metadata import string_to_authors
26
from calibre.ebooks.metadata import string_to_authors, fmt_sidx
25
28
class LibraryDelegate(QItemDelegate):
26
29
COLOR = QColor("blue")
98
101
qde.setCalendarPopup(True)
104
class PubDateDelegate(QStyledItemDelegate):
106
def displayText(self, val, locale):
107
return val.toDate().toString('MMM yyyy')
109
def createEditor(self, parent, option, index):
110
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
111
qde.setDisplayFormat('MM yyyy')
112
qde.setMinimumDate(QDate(101,1,1))
113
qde.setCalendarPopup(True)
116
class TextDelegate(QStyledItemDelegate):
118
def __init__(self, parent):
120
Delegate for text data. If auto_complete_function needs to return a list
121
of text items to auto-complete with. The funciton is None no
122
auto-complete will be used.
124
QStyledItemDelegate.__init__(self, parent)
125
self.auto_complete_function = None
127
def set_auto_complete_function(self, f):
128
self.auto_complete_function = f
130
def createEditor(self, parent, option, index):
131
editor = EnLineEdit(parent)
132
if self.auto_complete_function:
133
complete_items = [i[1] for i in self.auto_complete_function()]
134
completer = QCompleter(complete_items, self)
135
completer.setCaseSensitivity(Qt.CaseInsensitive)
136
completer.setCompletionMode(QCompleter.InlineCompletion)
137
editor.setCompleter(completer)
140
class TagsDelegate(QStyledItemDelegate):
142
def __init__(self, parent):
143
QStyledItemDelegate.__init__(self, parent)
146
def set_database(self, db):
149
def createEditor(self, parent, option, index):
151
editor = TagsLineEdit(parent, self.db.all_tags())
153
editor = EnLineEdit(parent)
101
156
class BooksModel(QAbstractTableModel):
103
[1000,900,500,400,100,90,50,40,10,9,5,4,1],
104
["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"]
108
158
'title' : _("Title"),
109
159
'authors' : _("Author(s)"),
110
160
'size' : _("Size (MB)"),
111
161
'timestamp' : _("Date"),
162
'pubdate' : _('Published'),
112
163
'rating' : _('Rating'),
113
164
'publisher' : _("Publisher"),
114
165
'tags' : _("Tags"),
115
166
'series' : _("Series"),
120
if num <= 0 or num >= 4000 or int(num) != num:
123
for d, r in cls.coding:
127
return ''.join(result)
129
169
def __init__(self, parent=None, buffer=40):
130
170
QAbstractTableModel.__init__(self, parent)
132
172
self.column_map = config['column_map']
133
173
self.editable_cols = ['title', 'authors', 'rating', 'publisher',
134
'tags', 'series', 'timestamp']
174
'tags', 'series', 'timestamp', 'pubdate']
135
175
self.default_image = QImage(':/images/book.svg')
136
176
self.sorted_on = ('timestamp', Qt.AscendingOrder)
137
177
self.last_search = '' # The last search performed on this model
189
def add_books(self, paths, formats, metadata, uris=[], add_duplicates=False):
190
ret = self.db.add_books(paths, formats, metadata, uris,
219
def add_books(self, paths, formats, metadata, add_duplicates=False):
220
ret = self.db.add_books(paths, formats, metadata,
191
221
add_duplicates=add_duplicates)
192
222
self.count_changed()
204
234
''' Return list indices of all cells in index.row()'''
205
235
return [ self.index(index.row(), c) for c in range(self.columnCount(None))]
207
def save_to_disk(self, rows, path, single_dir=False, single_format=None,
209
rows = [row.row() for row in rows]
210
if single_format is None:
211
return self.db.export_to_dir(path, rows,
212
self.sorted_on[0] == 'authors',
213
single_dir=single_dir,
216
return self.db.export_single_format_to_dir(path, rows,
239
return self.sorted_on[0] == 'authors'
221
241
def delete_books(self, indices):
222
242
ids = map(self.id, indices)
400
def get_preferred_formats_from_ids(self, ids, all_formats, mode='r+b'):
427
def get_preferred_formats_from_ids(self, ids, formats, paths=False,
428
set_metadata=False, specific_format=None,
429
exclude_auto=False, mode='r+b'):
432
if specific_format is not None:
433
formats = [specific_format.lower()]
404
436
fmts = self.db.formats(id, index_is_id=True)
407
available_formats = set(fmts.lower().split(','))
408
for f in all_formats:
409
if f.lower() in available_formats:
439
db_formats = set(fmts.lower().split(','))
440
available_formats = set([f.lower() for f in formats])
441
u = available_formats.intersection(db_formats)
446
if format is not None:
447
pt = PersistentTemporaryFile(suffix='.'+format)
448
pt.write(self.db.format(id, format, index_is_id=True))
451
_set_metadata(pt, self.db.get_metadata(id, get_cover=True, index_is_id=True),
453
pt.close() if paths else pt.seek(0)
415
f = self.db.format(id, format, index_is_id=True, as_file=True,
459
return ans, need_auto
422
461
def get_preferred_formats(self, rows, formats, paths=False,
423
set_metadata=False, specific_format=None):
462
set_metadata=False, specific_format=None,
425
466
if specific_format is not None:
426
467
formats = [specific_format.lower()]
427
468
for row in (row.row() for row in rows):
593
644
dt = datetime(val.year(), val.month(), val.day()) + timedelta(seconds=time.timezone) - timedelta(hours=time.daylight)
594
645
self.db.set_timestamp(id, dt)
646
elif column == 'pubdate':
647
if val.isNull() or not val.isValid():
649
dt = datetime(val.year(), val.month(), val.day()) + timedelta(seconds=time.timezone) - timedelta(hours=time.daylight)
650
self.db.set_pubdate(id, dt)
596
652
self.db.set(row, column, val)
597
653
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
619
675
TableView.__init__(self, parent)
620
676
self.rating_delegate = LibraryDelegate(self)
621
677
self.timestamp_delegate = DateDelegate(self)
678
self.pubdate_delegate = PubDateDelegate(self)
679
self.tags_delegate = TagsDelegate(self)
680
self.authors_delegate = TextDelegate(self)
681
self.series_delegate = TextDelegate(self)
682
self.publisher_delegate = TextDelegate(self)
622
683
self.display_parent = parent
623
684
self._model = modelcls(self)
624
685
self.setModel(self._model)
625
686
self.setSelectionBehavior(QAbstractItemView.SelectRows)
626
687
self.setSortingEnabled(True)
628
self.columns_sorted(self._model.column_map.index('rating'),
629
self._model.column_map.index('timestamp'))
689
self.setItemDelegateForColumn(i, TextDelegate(self))
690
self.columns_sorted()
632
691
QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
633
692
self._model.current_changed)
634
self.connect(self._model, SIGNAL('columns_sorted(int, int)'), self.columns_sorted, Qt.QueuedConnection)
693
self.connect(self._model, SIGNAL('columns_sorted()'),
694
self.columns_sorted, Qt.QueuedConnection)
636
def columns_sorted(self, rating_col, timestamp_col):
696
def columns_sorted(self):
637
697
for i in range(self.model().columnCount(None)):
638
698
if self.itemDelegateForColumn(i) in (self.rating_delegate,
639
self.timestamp_delegate):
699
self.timestamp_delegate, self.pubdate_delegate):
640
700
self.setItemDelegateForColumn(i, self.itemDelegate())
642
self.setItemDelegateForColumn(rating_col, self.rating_delegate)
643
if timestamp_col > -1:
644
self.setItemDelegateForColumn(timestamp_col, self.timestamp_delegate)
702
cm = self._model.column_map
705
self.setItemDelegateForColumn(cm.index('rating'), self.rating_delegate)
706
if 'timestamp' in cm:
707
self.setItemDelegateForColumn(cm.index('timestamp'), self.timestamp_delegate)
709
self.setItemDelegateForColumn(cm.index('pubdate'), self.pubdate_delegate)
711
self.setItemDelegateForColumn(cm.index('tags'), self.tags_delegate)
713
self.setItemDelegateForColumn(cm.index('authors'), self.authors_delegate)
714
if 'publisher' in cm:
715
self.setItemDelegateForColumn(cm.index('publisher'), self.publisher_delegate)
717
self.setItemDelegateForColumn(cm.index('series'), self.series_delegate)
646
719
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
647
720
save, open_folder, book_details, similar_menu=None):
699
772
paths = self.paths_from_event(event)
700
773
event.setDropAction(Qt.CopyAction)
702
self.emit(SIGNAL('files_dropped(PyQt_PyObject)'), paths, Qt.QueuedConnection)
775
self.emit(SIGNAL('files_dropped(PyQt_PyObject)'), paths)
705
778
def set_database(self, db):
706
779
self._model.set_database(db)
780
self.tags_delegate.set_database(db)
781
self.authors_delegate.set_auto_complete_function(db.all_authors)
782
self.series_delegate.set_auto_complete_function(db.all_series)
783
self.publisher_delegate.set_auto_complete_function(db.all_publishers)
709
786
self._model.close()
711
788
def set_editable(self, editable):
712
789
self._model.set_editable(editable)
714
def connect_to_search_box(self, sb):
791
def connect_to_search_box(self, sb, search_done):
715
792
QObject.connect(sb, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'),
716
793
self._model.search)
794
self._search_done = search_done
795
self.connect(self._model, SIGNAL('searched(PyQt_PyObject)'),
718
798
def connect_to_book_display(self, bd):
719
799
QObject.connect(self._model, SIGNAL('new_bookdisplay_data(PyQt_PyObject)'),
802
def search_done(self, ok):
803
self._search_done(self, ok)
723
806
class DeviceBooksView(BooksView):
728
811
self.resize_on_select = False
729
812
self.rating_delegate = None
730
813
for i in range(10):
731
self.setItemDelegateForColumn(i, self.itemDelegate())
814
self.setItemDelegateForColumn(i, TextDelegate(self))
732
815
self.setDragDropMode(self.NoDragDrop)
733
816
self.setAcceptDrops(False)
818
def set_database(self, db):
819
self._model.set_database(db)
735
821
def resizeColumnsToContents(self):
736
822
QTableView.resizeColumnsToContents(self)
737
823
self.columns_resized = True
758
844
def get_matches(self, location, query):
759
845
location = location.lower().strip()
760
846
query = query.lower().strip()
761
if location not in ('title', 'authors', 'tags', 'all'):
847
if location not in ('title', 'author', 'tag', 'all', 'format'):
763
849
matches = set([])
764
locations = ['title', 'authors', 'tags'] if location == 'all' else [location]
850
locations = ['title', 'author', 'tag', 'format'] if location == 'all' else [location]
766
852
'title' : lambda x : getattr(x, 'title').lower(),
767
'authors': lambda x: getattr(x, 'authors').lower(),
768
'tags':lambda x: ','.join(getattr(x, 'tags')).lower()
853
'author': lambda x: getattr(x, 'authors').lower(),
854
'tag':lambda x: ','.join(getattr(x, 'tags')).lower(),
855
'format':lambda x: os.path.splitext(x.path)[1].lower()
770
857
for i, v in enumerate(locations):
771
858
locations[i] = q[v]
1031
1127
self.setText(self.help_text)
1032
1128
self.home(False)
1033
1129
self.initial_state = True
1130
self.setStyleSheet("background-color: white")
1131
self.emit(SIGNAL('cleared()'))
1035
1133
def clear(self):
1036
1134
self.clear_to_help()
1037
1135
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), '', False)
1137
def search_done(self, ok):
1138
col = 'rgba(0,255,0,25%)' if ok else 'rgb(255,0,0,25%)'
1139
self.setStyleSheet('background-color: '+col)
1040
1141
def keyPressEvent(self, event):
1041
1142
if self.initial_state:
1042
1143
self.normalize_state()
1043
1144
self.initial_state = False
1145
if not self.as_you_type:
1146
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
1044
1148
QLineEdit.keyPressEvent(self, event)
1046
1150
def mouseReleaseEvent(self, event):
1050
1154
QLineEdit.mouseReleaseEvent(self, event)
1052
1156
def text_edited_slot(self, text):
1053
text = qstring_to_unicode(text) if isinstance(text, QString) else unicode(text)
1054
self.prev_text = text
1055
self.timer = self.startTimer(self.__class__.INTERVAL)
1157
if self.as_you_type:
1158
text = qstring_to_unicode(text) if isinstance(text, QString) else unicode(text)
1159
self.prev_text = text
1160
self.timer = self.startTimer(self.__class__.INTERVAL)
1057
1162
def timerEvent(self, event):
1058
1163
self.killTimer(event.timerId())
1059
1164
if event.timerId() == self.timer:
1060
text = qstring_to_unicode(self.text())
1061
refinement = text.startswith(self.prev_search) and ':' not in text
1062
self.prev_search = text
1063
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), text, refinement)
1167
def do_search(self):
1168
text = qstring_to_unicode(self.text())
1169
refinement = text.startswith(self.prev_search) and ':' not in text
1170
self.prev_search = text
1171
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), text, refinement)
1065
1173
def search_from_tokens(self, tokens, all):
1066
1174
ans = u' '.join([u'%s:%s'%x for x in tokens])