4
4
Miscellaneous widgets used in the GUI
6
6
import re, os, traceback
7
from PyQt4.QtGui import QListView, QIcon, QFont, QLabel, QListWidget, \
7
from PyQt4.Qt import QListView, QIcon, QFont, QLabel, QListWidget, \
8
8
QListWidgetItem, QTextCharFormat, QApplication, \
9
QSyntaxHighlighter, QCursor, QColor, QWidget, QDialog, \
10
QPixmap, QMovie, QPalette
11
from PyQt4.QtCore import QAbstractListModel, QVariant, Qt, SIGNAL, \
12
QRegExp, QSettings, QSize, QModelIndex
9
QSyntaxHighlighter, QCursor, QColor, QWidget, \
10
QPixmap, QMovie, QPalette, QTimer, QDialog, \
11
QAbstractListModel, QVariant, Qt, SIGNAL, \
12
QRegExp, QSettings, QSize, QModelIndex, \
13
QAbstractButton, QPainter, QLineEdit, QComboBox, \
14
QMenu, QStringListModel, QCompleter
14
from calibre.gui2.jobs2 import DetailView
15
16
from calibre.gui2 import human_readable, NONE, TableView, \
16
17
qstring_to_unicode, error_dialog
18
from calibre.gui2.dialogs.job_view_ui import Ui_Dialog
17
19
from calibre.gui2.filename_pattern_ui import Ui_Form
18
20
from calibre import fit_image
19
from calibre.utils.fontconfig import find_font_families
21
from calibre.utils.fonts import fontconfig
20
22
from calibre.ebooks.metadata.meta import metadata_from_filename
21
23
from calibre.utils.config import prefs
22
from calibre.gui2.dialogs.warning_ui import Ui_Dialog as Ui_WarningDialog
24
25
class ProgressIndicator(QWidget):
171
161
QAbstractListModel.__init__(self, parent)
172
162
self.icons = [QVariant(QIcon(':/library')),
173
163
QVariant(QIcon(':/images/reader.svg')),
164
QVariant(QIcon(':/images/sd.svg')),
174
165
QVariant(QIcon(':/images/sd.svg'))]
175
166
self.text = [_('Library\n%d\nbooks'),
176
167
_('Reader\n%s\navailable'),
177
_('Card\n%s\navailable')]
168
_('Card A\n%s\navailable'),
169
_('Card B\n%s\navailable')]
170
self.free = [-1, -1, -1]
180
172
self.highlight_row = 0
181
173
self.tooltips = [
182
_('Click to see the list of books available on your computer'),
183
_('Click to see the list of books in the main memory of your reader'),
184
_('Click to see the list of books on the storage card in your reader')
174
_('Click to see the books available on your computer'),
175
_('Click to see the books in the main memory of your reader'),
176
_('Click to see the books on storage card A in your reader'),
177
_('Click to see the books on storage card B in your reader')
187
def rowCount(self, parent):
188
return 1 + sum([1 for i in self.free if i >= 0])
180
def rowCount(self, *args):
181
return 1 + len([i for i in self.free if i >= 0])
183
def get_device_row(self, row):
184
if row == 2 and self.free[1] == -1 and self.free[2] > -1:
190
188
def data(self, index, role):
191
189
row = index.row()
190
drow = self.get_device_row(row)
193
192
if role == Qt.DisplayRole:
194
text = self.text[row]%(human_readable(self.free[row-1])) if row > 0 \
195
else self.text[row]%self.count
193
text = self.text[drow]%(human_readable(self.free[drow-1])) if row > 0 \
194
else self.text[drow]%self.count
196
195
data = QVariant(text)
197
196
elif role == Qt.DecorationRole:
198
data = self.icons[row]
197
data = self.icons[drow]
199
198
elif role == Qt.ToolTipRole:
200
data = QVariant(self.tooltips[row])
199
data = QVariant(self.tooltips[drow])
201
200
elif role == Qt.SizeHintRole:
202
201
data = QVariant(QSize(155, 90))
203
202
elif role == Qt.FontRole:
216
215
def headerData(self, section, orientation, role):
219
def update_devices(self, cp=None, fs=[-1, -1, -1]):
218
def update_devices(self, cp=(None, None), fs=[-1, -1, -1]):
221
if isinstance(cp, (str, unicode)):
220
225
self.free[0] = fs[0]
221
self.free[1] = max(fs[1:])
229
self.free[1] = fs[1] if fs[1] is not None and cpa is not None else -1
230
self.free[2] = fs[2] if fs[2] is not None and cpb is not None else -1
232
self.emit(SIGNAL('devicesChanged()'))
226
234
def location_changed(self, row):
227
235
self.highlight_row = row
228
236
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
229
237
self.index(0), self.index(self.rowCount(QModelIndex())-1))
239
def location_for_row(self, row):
240
if row == 0: return 'library'
241
if row == 1: return 'main'
242
if row == 3: return 'cardb'
243
return 'carda' if self.free[1] > -1 else 'cardb'
231
245
class LocationView(QListView):
233
247
def __init__(self, parent):
234
248
QListView.__init__(self, parent)
235
249
self.setModel(LocationModel(self))
237
self.setCursor(Qt.PointingHandCursor)
238
251
self.currentChanged = self.current_changed
253
self.eject_button = EjectButton(self)
254
self.eject_button.hide()
256
self.connect(self, SIGNAL('entered(QModelIndex)'), self.item_entered)
257
self.connect(self, SIGNAL('viewportEntered()'), self.viewport_entered)
258
self.connect(self.eject_button, SIGNAL('clicked()'), lambda: self.emit(SIGNAL('umount_device()')))
259
self.connect(self.model(), SIGNAL('devicesChanged()'), self.eject_button.hide)
240
261
def count_changed(self, new_count):
241
262
self.model().count = new_count
242
263
self.model().reset()
244
265
def current_changed(self, current, previous):
245
266
if current.isValid():
246
267
i = current.row()
247
location = 'library' if i == 0 else 'main' if i == 1 else 'card'
268
location = self.model().location_for_row(i)
248
269
self.emit(SIGNAL('location_selected(PyQt_PyObject)'), location)
249
270
self.model().location_changed(i)
251
272
def location_changed(self, row):
252
if 0 <= row and row <= 2:
273
if 0 <= row and row <= 3:
253
274
self.model().location_changed(row)
276
def leaveEvent(self, event):
278
self.eject_button.hide()
280
def item_entered(self, location):
281
self.setCursor(Qt.PointingHandCursor)
282
self.eject_button.hide()
284
if location.row() == 1:
285
rect = self.visualRect(location)
287
self.eject_button.resize(rect.height()/2, rect.height()/2)
289
x, y = rect.left(), rect.top()
290
x = x + (rect.width() - self.eject_button.width() - 2)
293
self.eject_button.move(x, y)
294
self.eject_button.show()
296
def viewport_entered(self):
298
self.eject_button.hide()
301
class EjectButton(QAbstractButton):
303
def __init__(self, parent):
304
QAbstractButton.__init__(self, parent)
305
self.mouse_over = False
307
def enterEvent(self, event):
308
self.mouse_over = True
310
def leaveEvent(self, event):
311
self.mouse_over = False
313
def paintEvent(self, event):
314
painter = QPainter(self)
315
painter.setClipRect(event.rect())
316
image = QPixmap(':/images/eject').scaledToHeight(event.rect().height(),
317
Qt.SmoothTransformation)
319
if not self.mouse_over:
320
alpha_mask = QPixmap(image.width(), image.height())
321
color = QColor(128, 128, 128)
322
alpha_mask.fill(color)
323
image.setAlphaChannel(alpha_mask)
325
painter.drawPixmap(0, 0, image)
328
class DetailView(QDialog, Ui_Dialog):
330
def __init__(self, parent, job):
331
QDialog.__init__(self, parent)
333
self.setWindowTitle(job.description)
337
self.timer = QTimer(self)
338
self.connect(self.timer, SIGNAL('timeout()'), self.update)
339
self.timer.start(1000)
343
f = self.job.log_file
344
f.seek(self.next_pos)
346
self.next_pos = f.tell()
348
self.log.appendPlainText(more.decode('utf-8', 'replace'))
255
351
class JobsView(TableView):
257
353
def __init__(self, parent):
297
393
def index_of(self, family):
298
394
return self.families.index(family.strip())
396
class BasicComboModel(QAbstractListModel):
398
def __init__(self, items, *args):
399
QAbstractListModel.__init__(self, *args)
400
self.items = [i for i in items]
403
def rowCount(self, *args):
404
return len(self.items)
406
def data(self, index, role):
408
item = self.items[index.row()]
410
traceback.print_exc()
412
if role == Qt.DisplayRole:
413
return QVariant(item)
414
if role == Qt.FontRole:
415
return QVariant(QFont(item))
418
def index_of(self, item):
419
return self.items.index(item.strip())
301
422
class BasicListItem(QListWidgetItem):
332
453
yield self.item(i)
456
class LineEditECM(object):
459
Extend the contenxt menu of a QLineEdit to include more actions.
462
def contextMenuEvent(self, event):
463
menu = self.createStandardContextMenu()
466
case_menu = QMenu(_('Change Case'))
467
action_upper_case = case_menu.addAction(_('Upper Case'))
468
action_lower_case = case_menu.addAction(_('Lower Case'))
469
action_swap_case = case_menu.addAction(_('Swap Case'))
470
action_title_case = case_menu.addAction(_('Title Case'))
472
self.connect(action_upper_case, SIGNAL('triggered()'), self.upper_case)
473
self.connect(action_lower_case, SIGNAL('triggered()'), self.lower_case)
474
self.connect(action_swap_case, SIGNAL('triggered()'), self.swap_case)
475
self.connect(action_title_case, SIGNAL('triggered()'), self.title_case)
477
menu.addMenu(case_menu)
478
menu.exec_(event.globalPos())
480
def upper_case(self):
481
self.setText(qstring_to_unicode(self.text()).upper())
483
def lower_case(self):
484
self.setText(qstring_to_unicode(self.text()).lower())
487
self.setText(qstring_to_unicode(self.text()).swapcase())
489
def title_case(self):
490
self.setText(qstring_to_unicode(self.text()).title())
493
class EnLineEdit(LineEditECM, QLineEdit):
498
Includes an extended content menu.
504
class TagsCompleter(QCompleter):
507
A completer object that completes a list of tags. It is used in conjunction
508
with a CompleterLineEdit.
511
def __init__(self, parent, all_tags):
512
QCompleter.__init__(self, all_tags, parent)
513
self.all_tags = set(all_tags)
515
def update(self, text_tags, completion_prefix):
516
tags = list(self.all_tags.difference(text_tags))
517
model = QStringListModel(tags, self)
520
self.setCompletionPrefix(completion_prefix)
521
if completion_prefix.strip() != '':
524
def update_tags_cache(self, tags):
525
self.all_tags = set(tags)
526
model = QStringListModel(tags, self)
530
class TagsLineEdit(EnLineEdit):
533
A QLineEdit that can complete parts of text separated by separator.
536
def __init__(self, parent=0, tags=[]):
537
EnLineEdit.__init__(self, parent)
541
self.connect(self, SIGNAL('textChanged(QString)'), self.text_changed)
543
self.completer = TagsCompleter(self, tags)
544
self.completer.setCaseSensitivity(Qt.CaseInsensitive)
547
SIGNAL('text_changed(PyQt_PyObject, PyQt_PyObject)'),
548
self.completer.update)
549
self.connect(self.completer, SIGNAL('activated(QString)'),
552
self.completer.setWidget(self)
554
def update_tags_cache(self, tags):
555
self.completer.update_tags_cache(tags)
557
def text_changed(self, text):
558
all_text = qstring_to_unicode(text)
559
text = all_text[:self.cursorPosition()]
560
prefix = text.split(',')[-1].strip()
563
for t in all_text.split(self.separator):
564
t1 = qstring_to_unicode(t).strip()
567
text_tags = list(set(text_tags))
569
self.emit(SIGNAL('text_changed(PyQt_PyObject, PyQt_PyObject)'),
572
def complete_text(self, text):
573
cursor_pos = self.cursorPosition()
574
before_text = qstring_to_unicode(self.text())[:cursor_pos]
575
after_text = qstring_to_unicode(self.text())[cursor_pos:]
576
prefix_len = len(before_text.split(',')[-1].strip())
577
self.setText('%s%s%s %s' % (before_text[:cursor_pos - prefix_len],
578
text, self.separator, after_text))
579
self.setCursorPosition(cursor_pos - prefix_len + len(text) + 2)
582
class EnComboBox(QComboBox):
587
Includes an extended content menu.
590
def __init__(self, *args):
591
QComboBox.__init__(self, *args)
592
self.setLineEdit(EnLineEdit(self))
595
return qstring_to_unicode(self.currentText())
597
def setText(self, text):
598
idx = self.findText(text, Qt.MatchFixedString)
600
self.insertItem(0, text)
602
self.setCurrentIndex(idx)
336
604
class PythonHighlighter(QSyntaxHighlighter):