~ubuntu-branches/ubuntu/karmic/calibre/karmic

« back to all changes in this revision

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

  • Committer: Bazaar Package Importer
  • Author(s): Martin Pitt
  • Date: 2009-07-30 12:49:41 UTC
  • mfrom: (1.3.2 upstream)
  • Revision ID: james.westby@ubuntu.com-20090730124941-qjdsmri25zt8zocn
Tags: 0.6.3+dfsg-0ubuntu1
* New upstream release. Please see http://calibre.kovidgoyal.net/new_in_6/
  for the list of new features and changes.
* remove_postinstall.patch: Update for new version.
* build_debug.patch: Does not apply any more, disable for now. Might not be
  necessary any more.
* debian/copyright: Fix reference to versionless GPL.
* debian/rules: Drop obsolete dh_desktop call.
* debian/rules: Add workaround for weird Python 2.6 setuptools behaviour of
  putting compiled .so files into src/calibre/plugins/calibre/plugins
  instead of src/calibre/plugins.
* debian/rules: Drop hal fdi moving, new upstream version does not use hal
  any more. Drop hal dependency, too.
* debian/rules: Install udev rules into /lib/udev/rules.d.
* Add debian/calibre.preinst: Remove unmodified
  /etc/udev/rules.d/95-calibre.rules on upgrade.
* debian/control: Bump Python dependencies to 2.6, since upstream needs
  it now.

Show diffs side-by-side

added added

removed removed

Lines of Context:
4
4
Miscellaneous widgets used in the GUI
5
5
'''
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
13
15
 
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
23
24
 
24
25
class ProgressIndicator(QWidget):
25
26
 
34
35
        self.status = QLabel(self)
35
36
        self.status.setWordWrap(True)
36
37
        self.status.setAlignment(Qt.AlignHCenter|Qt.AlignTop)
37
 
        self.status.font().setBold(True)
38
 
        self.status.font().setPointSize(self.font().pointSize()+6)
39
38
        self.setVisible(False)
40
39
 
41
40
    def start(self, msg=''):
47
46
        self.ml.move(int((self.size().width()-self.ml.size().width())/2.), 0)
48
47
        self.status.resize(self.size().width(), self.size().height()-self.ml.size().height()-10)
49
48
        self.status.move(0, self.ml.size().height()+10)
50
 
        self.status.setText(msg)
 
49
        self.status.setText('<h1>'+msg+'</h1>')
51
50
        self.setVisible(True)
52
51
        self.movie.setPaused(False)
53
52
 
56
55
            self.movie.setPaused(True)
57
56
            self.setVisible(False)
58
57
 
59
 
 
60
 
class WarningDialog(QDialog, Ui_WarningDialog):
61
 
 
62
 
    def __init__(self, title, msg, details, parent=None):
63
 
        QDialog.__init__(self, parent)
64
 
        self.setupUi(self)
65
 
        self.setWindowTitle(title)
66
 
        self.msg.setText(msg)
67
 
        self.details.setText(details)
68
 
 
69
58
class FilenamePattern(QWidget, Ui_Form):
70
59
 
71
60
    def __init__(self, parent):
152
141
            if not pmap.isNull():
153
142
                self.setPixmap(pmap)
154
143
                event.accept()
155
 
                self.emit(SIGNAL('cover_changed()'), paths, Qt.QueuedConnection)
 
144
                self.cover_data = open(path, 'rb').read()
 
145
                self.emit(SIGNAL('cover_changed(PyQt_PyObject)'), paths)
156
146
                break
157
147
 
158
148
    def dragMoveEvent(self, event):
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')]
178
 
        self.free = [-1, -1]
 
168
                     _('Card A\n%s\navailable'),
 
169
                     _('Card B\n%s\navailable')]
 
170
        self.free = [-1, -1, -1]
179
171
        self.count = 0
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')
185
178
                         ]
186
179
 
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])
 
182
 
 
183
    def get_device_row(self, row):
 
184
        if row == 2 and self.free[1] == -1 and self.free[2] > -1:
 
185
            row = 3
 
186
        return row
189
187
 
190
188
    def data(self, index, role):
191
189
        row = index.row()
 
190
        drow = self.get_device_row(row)
192
191
        data = NONE
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):
217
216
        return NONE
218
217
 
219
 
    def update_devices(self, cp=None, fs=[-1, -1, -1]):
 
218
    def update_devices(self, cp=(None, None), fs=[-1, -1, -1]):
 
219
        if cp is None:
 
220
            cp = (None, None)
 
221
        if isinstance(cp, (str, unicode)):
 
222
            cp = (cp, None)
 
223
        if len(fs) < 3:
 
224
            fs = list(fs) + [0]
220
225
        self.free[0] = fs[0]
221
 
        self.free[1] = max(fs[1:])
222
 
        if cp == None:
223
 
            self.free[1] = -1
 
226
        self.free[1] = fs[1]
 
227
        self.free[2] = fs[2]
 
228
        cpa, cpb = cp
 
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
224
231
        self.reset()
 
232
        self.emit(SIGNAL('devicesChanged()'))
225
233
 
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))
230
238
 
 
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'
 
244
 
231
245
class LocationView(QListView):
232
246
 
233
247
    def __init__(self, parent):
234
248
        QListView.__init__(self, parent)
235
249
        self.setModel(LocationModel(self))
236
250
        self.reset()
237
 
        self.setCursor(Qt.PointingHandCursor)
238
251
        self.currentChanged = self.current_changed
239
252
 
 
253
        self.eject_button = EjectButton(self)
 
254
        self.eject_button.hide()
 
255
 
 
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)
 
260
 
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)
250
271
 
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)
254
275
 
 
276
    def leaveEvent(self, event):
 
277
        self.unsetCursor()
 
278
        self.eject_button.hide()
 
279
 
 
280
    def item_entered(self, location):
 
281
        self.setCursor(Qt.PointingHandCursor)
 
282
        self.eject_button.hide()
 
283
 
 
284
        if location.row() == 1:
 
285
            rect = self.visualRect(location)
 
286
 
 
287
            self.eject_button.resize(rect.height()/2, rect.height()/2)
 
288
 
 
289
            x, y = rect.left(), rect.top()
 
290
            x = x + (rect.width() - self.eject_button.width() - 2)
 
291
            y += 6
 
292
 
 
293
            self.eject_button.move(x, y)
 
294
            self.eject_button.show()
 
295
 
 
296
    def viewport_entered(self):
 
297
        self.unsetCursor()
 
298
        self.eject_button.hide()
 
299
 
 
300
 
 
301
class EjectButton(QAbstractButton):
 
302
 
 
303
    def __init__(self, parent):
 
304
        QAbstractButton.__init__(self, parent)
 
305
        self.mouse_over = False
 
306
 
 
307
    def enterEvent(self, event):
 
308
        self.mouse_over = True
 
309
 
 
310
    def leaveEvent(self, event):
 
311
        self.mouse_over = False
 
312
 
 
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)
 
318
 
 
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)
 
324
 
 
325
        painter.drawPixmap(0, 0, image)
 
326
 
 
327
 
 
328
class DetailView(QDialog, Ui_Dialog):
 
329
 
 
330
    def __init__(self, parent, job):
 
331
        QDialog.__init__(self, parent)
 
332
        self.setupUi(self)
 
333
        self.setWindowTitle(job.description)
 
334
        self.job = job
 
335
        self.next_pos = 0
 
336
        self.update()
 
337
        self.timer = QTimer(self)
 
338
        self.connect(self.timer, SIGNAL('timeout()'), self.update)
 
339
        self.timer.start(1000)
 
340
 
 
341
 
 
342
    def update(self):
 
343
        f = self.job.log_file
 
344
        f.seek(self.next_pos)
 
345
        more = f.read()
 
346
        self.next_pos = f.tell()
 
347
        if more:
 
348
            self.log.appendPlainText(more.decode('utf-8', 'replace'))
 
349
 
 
350
 
255
351
class JobsView(TableView):
256
352
 
257
353
    def __init__(self, parent):
262
358
        row = index.row()
263
359
        job = self.model().row_to_job(row)
264
360
        d = DetailView(self, job)
265
 
        self.connect(self.model(), SIGNAL('output_received()'), d.update)
266
361
        d.exec_()
 
362
        d.timer.stop()
267
363
 
268
364
 
269
365
class FontFamilyModel(QAbstractListModel):
271
367
    def __init__(self, *args):
272
368
        QAbstractListModel.__init__(self, *args)
273
369
        try:
274
 
            self.families = find_font_families()
 
370
            self.families = fontconfig.find_font_families()
275
371
        except:
276
372
            self.families = []
277
373
            print 'WARNING: Could not load fonts'
278
374
            traceback.print_exc()
279
375
        self.families.sort()
280
 
        self.families[:0] = ['None']
 
376
        self.families[:0] = [_('None')]
281
377
 
282
378
    def rowCount(self, *args):
283
379
        return len(self.families)
297
393
    def index_of(self, family):
298
394
        return self.families.index(family.strip())
299
395
 
 
396
class BasicComboModel(QAbstractListModel):
 
397
 
 
398
    def __init__(self, items, *args):
 
399
        QAbstractListModel.__init__(self, *args)
 
400
        self.items = [i for i in items]
 
401
        self.items.sort()
 
402
 
 
403
    def rowCount(self, *args):
 
404
        return len(self.items)
 
405
 
 
406
    def data(self, index, role):
 
407
        try:
 
408
            item = self.items[index.row()]
 
409
        except:
 
410
            traceback.print_exc()
 
411
            return NONE
 
412
        if role == Qt.DisplayRole:
 
413
            return QVariant(item)
 
414
        if role == Qt.FontRole:
 
415
            return QVariant(QFont(item))
 
416
        return NONE
 
417
 
 
418
    def index_of(self, item):
 
419
        return self.items.index(item.strip())
 
420
 
300
421
 
301
422
class BasicListItem(QListWidgetItem):
302
423
 
332
453
            yield self.item(i)
333
454
 
334
455
 
 
456
class LineEditECM(object):
 
457
 
 
458
    '''
 
459
    Extend the contenxt menu of a QLineEdit to include more actions.
 
460
    '''
 
461
 
 
462
    def contextMenuEvent(self, event):
 
463
        menu = self.createStandardContextMenu()
 
464
        menu.addSeparator()
 
465
 
 
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'))
 
471
 
 
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)
 
476
 
 
477
        menu.addMenu(case_menu)
 
478
        menu.exec_(event.globalPos())
 
479
 
 
480
    def upper_case(self):
 
481
        self.setText(qstring_to_unicode(self.text()).upper())
 
482
 
 
483
    def lower_case(self):
 
484
        self.setText(qstring_to_unicode(self.text()).lower())
 
485
 
 
486
    def swap_case(self):
 
487
        self.setText(qstring_to_unicode(self.text()).swapcase())
 
488
 
 
489
    def title_case(self):
 
490
        self.setText(qstring_to_unicode(self.text()).title())
 
491
 
 
492
 
 
493
class EnLineEdit(LineEditECM, QLineEdit):
 
494
 
 
495
    '''
 
496
    Enhanced QLineEdit.
 
497
 
 
498
    Includes an extended content menu.
 
499
    '''
 
500
 
 
501
    pass
 
502
 
 
503
 
 
504
class TagsCompleter(QCompleter):
 
505
 
 
506
    '''
 
507
    A completer object that completes a list of tags. It is used in conjunction
 
508
    with a CompleterLineEdit.
 
509
    '''
 
510
 
 
511
    def __init__(self, parent, all_tags):
 
512
        QCompleter.__init__(self, all_tags, parent)
 
513
        self.all_tags = set(all_tags)
 
514
 
 
515
    def update(self, text_tags, completion_prefix):
 
516
        tags = list(self.all_tags.difference(text_tags))
 
517
        model = QStringListModel(tags, self)
 
518
        self.setModel(model)
 
519
 
 
520
        self.setCompletionPrefix(completion_prefix)
 
521
        if completion_prefix.strip() != '':
 
522
            self.complete()
 
523
 
 
524
    def update_tags_cache(self, tags):
 
525
        self.all_tags = set(tags)
 
526
        model = QStringListModel(tags, self)
 
527
        self.setModel(model)
 
528
 
 
529
 
 
530
class TagsLineEdit(EnLineEdit):
 
531
 
 
532
    '''
 
533
    A QLineEdit that can complete parts of text separated by separator.
 
534
    '''
 
535
 
 
536
    def __init__(self, parent=0, tags=[]):
 
537
        EnLineEdit.__init__(self, parent)
 
538
 
 
539
        self.separator = ','
 
540
 
 
541
        self.connect(self, SIGNAL('textChanged(QString)'), self.text_changed)
 
542
 
 
543
        self.completer = TagsCompleter(self, tags)
 
544
        self.completer.setCaseSensitivity(Qt.CaseInsensitive)
 
545
 
 
546
        self.connect(self,
 
547
            SIGNAL('text_changed(PyQt_PyObject, PyQt_PyObject)'),
 
548
            self.completer.update)
 
549
        self.connect(self.completer, SIGNAL('activated(QString)'),
 
550
            self.complete_text)
 
551
 
 
552
        self.completer.setWidget(self)
 
553
 
 
554
    def update_tags_cache(self, tags):
 
555
        self.completer.update_tags_cache(tags)
 
556
 
 
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()
 
561
 
 
562
        text_tags = []
 
563
        for t in all_text.split(self.separator):
 
564
            t1 = qstring_to_unicode(t).strip()
 
565
            if t1 != '':
 
566
                text_tags.append(t)
 
567
        text_tags = list(set(text_tags))
 
568
 
 
569
        self.emit(SIGNAL('text_changed(PyQt_PyObject, PyQt_PyObject)'),
 
570
            text_tags, prefix)
 
571
 
 
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)
 
580
 
 
581
 
 
582
class EnComboBox(QComboBox):
 
583
 
 
584
    '''
 
585
    Enhanced QComboBox.
 
586
 
 
587
    Includes an extended content menu.
 
588
    '''
 
589
 
 
590
    def __init__(self, *args):
 
591
        QComboBox.__init__(self, *args)
 
592
        self.setLineEdit(EnLineEdit(self))
 
593
 
 
594
    def text(self):
 
595
        return qstring_to_unicode(self.currentText())
 
596
 
 
597
    def setText(self, text):
 
598
        idx = self.findText(text, Qt.MatchFixedString)
 
599
        if idx == -1:
 
600
            self.insertItem(0, text)
 
601
            idx = 0
 
602
        self.setCurrentIndex(idx)
335
603
 
336
604
class PythonHighlighter(QSyntaxHighlighter):
337
605
 
517
785
            return
518
786
 
519
787
        for regex, format in PythonHighlighter.Rules:
520
 
            i = text.indexOf(regex)
 
788
            i = regex.indexIn(text)
521
789
            while i >= 0:
522
790
                length = regex.matchedLength()
523
791
                self.setFormat(i, length,
524
792
                               PythonHighlighter.Formats[format])
525
 
                i = text.indexOf(regex, i + length)
 
793
                i = regex.indexIn(text, i + length)
526
794
 
527
795
        # Slow but good quality highlighting for comments. For more
528
796
        # speed, comment this out and add the following to __init__:
547
815
 
548
816
        self.setCurrentBlockState(NORMAL)
549
817
 
550
 
        if text.indexOf(self.stringRe) != -1:
 
818
        if self.stringRe.indexIn(text) != -1:
551
819
            return
552
820
        # This is fooled by triple quotes inside single quoted strings
553
 
        for i, state in ((text.indexOf(self.tripleSingleRe),
 
821
        for i, state in ((self.tripleSingleRe.indexIn(text),
554
822
                          TRIPLESINGLE),
555
 
                         (text.indexOf(self.tripleDoubleRe),
 
823
                         (self.tripleDoubleRe.indexIn(text),
556
824
                          TRIPLEDOUBLE)):
557
825
            if self.previousBlockState() == state:
558
826
                if i == -1:
570
838
        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
571
839
        QSyntaxHighlighter.rehighlight(self)
572
840
        QApplication.restoreOverrideCursor()
573