~ubuntu-branches/ubuntu/jaunty/calibre/jaunty-backports

« back to all changes in this revision

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

  • Committer: Bazaar Package Importer
  • Author(s): Martin Pitt
  • Date: 2009-01-20 17:14:02 UTC
  • Revision ID: james.westby@ubuntu.com-20090120171402-8y3znf6nokwqe80k
Tags: upstream-0.4.125+dfsg
ImportĀ upstreamĀ versionĀ 0.4.125+dfsg

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
""" The GUI """
 
4
import sys, os, re, StringIO, traceback
 
5
from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
 
6
                         QByteArray, QLocale, QUrl, QTranslator, QCoreApplication, \
 
7
                         QModelIndex
 
8
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
 
9
                        QIcon, QTableView, QDialogButtonBox, QApplication
 
10
 
 
11
ORG_NAME = 'KovidsBrain'
 
12
APP_UID  = 'libprs500'
 
13
from calibre import __author__, islinux, iswindows, isosx
 
14
from calibre.startup import get_lang
 
15
from calibre.utils.config import Config, ConfigProxy, dynamic
 
16
import calibre.resources as resources
 
17
 
 
18
NONE = QVariant() #: Null value to return from the data function of item models
 
19
 
 
20
ALL_COLUMNS = ['title', 'authors', 'size', 'timestamp', 'rating', 'publisher', 'tags', 'series']
 
21
 
 
22
def _config():
 
23
    c = Config('gui', 'preferences for the calibre GUI')
 
24
    c.add_opt('frequently_used_directories', default=[],
 
25
              help=_('Frequently used directories'))
 
26
    c.add_opt('send_to_storage_card_by_default', default=False,
 
27
              help=_('Send file to storage card instead of main memory by default'))
 
28
    c.add_opt('save_to_disk_single_format', default='lrf',
 
29
              help=_('The format to use when saving single files to disk'))
 
30
    c.add_opt('confirm_delete', default=False,
 
31
              help=_('Confirm before deleting'))
 
32
    c.add_opt('toolbar_icon_size', default=QSize(48, 48), 
 
33
              help=_('Toolbar icon size')) # value QVariant.toSize
 
34
    c.add_opt('show_text_in_toolbar', default=True,
 
35
              help=_('Show button labels in the toolbar'))
 
36
    c.add_opt('main_window_geometry', default=None,
 
37
              help=_('Main window geometry')) # value QVariant.toByteArray
 
38
    c.add_opt('new_version_notification', default=True,
 
39
              help=_('Notify when a new version is available'))
 
40
    c.add_opt('use_roman_numerals_for_series_number', default=True,
 
41
              help=_('Use Roman numerals for series number'))
 
42
    c.add_opt('sort_by_popularity', default=False,
 
43
              help=_('Sort tags list by popularity'))
 
44
    c.add_opt('cover_flow_queue_length', default=6,
 
45
              help=_('Number of covers to show in the cover browsing mode'))
 
46
    c.add_opt('LRF_conversion_defaults', default=[],
 
47
              help=_('Defaults for conversion to LRF'))
 
48
    c.add_opt('LRF_ebook_viewer_options', default=None,
 
49
              help=_('Options for the LRF ebook viewer'))
 
50
    c.add_opt('internally_viewed_formats', default=['LRF', 'EPUB', 'LIT', 'MOBI', 'PRC', 'HTML', 'FB2'],
 
51
              help=_('Formats that are viewed using the internal viewer'))
 
52
    c.add_opt('column_map', default=ALL_COLUMNS,
 
53
              help=_('Columns to be displayed in the book list'))
 
54
    c.add_opt('autolaunch_server', default=False, help=_('Automatically launch content server on application startup'))
 
55
    c.add_opt('oldest_news', default=60, help=_('Oldest news kept in database'))
 
56
    c.add_opt('systray_icon', default=True, help=_('Show system tray icon'))
 
57
    c.add_opt('upload_news_to_device', default=True, 
 
58
              help=_('Upload downloaded news to device'))
 
59
    c.add_opt('delete_news_from_library_on_upload', default=False, 
 
60
              help=_('Delete books from library after uploading to device'))
 
61
    c.add_opt('separate_cover_flow', default=False, 
 
62
              help=_('Show the cover flow in a separate window instead of in the main calibre window'))
 
63
    return ConfigProxy(c)
 
64
    
 
65
config = _config()
 
66
# Turn off DeprecationWarnings in windows GUI
 
67
if iswindows:
 
68
    import warnings
 
69
    warnings.simplefilter('ignore', DeprecationWarning)
 
70
 
 
71
def available_heights():
 
72
    desktop  = QCoreApplication.instance().desktop()
 
73
    return map(lambda x: x.height(), map(desktop.availableGeometry, range(desktop.numScreens())))
 
74
 
 
75
def available_height():
 
76
    desktop  = QCoreApplication.instance().desktop()
 
77
    return desktop.availableGeometry().height()
 
78
 
 
79
def max_available_height():
 
80
    return max(available_heights())
 
81
 
 
82
def min_available_height():
 
83
    return min(available_heights())
 
84
 
 
85
def available_width():
 
86
    desktop       = QCoreApplication.instance().desktop()
 
87
    return desktop.availableGeometry().width()
 
88
 
 
89
def extension(path):
 
90
    return os.path.splitext(path)[1][1:].lower()
 
91
 
 
92
def warning_dialog(parent, title, msg):
 
93
    d = QMessageBox(QMessageBox.Warning, 'WARNING: '+title, msg, QMessageBox.Ok,
 
94
                    parent)
 
95
    d.setIconPixmap(QPixmap(':/images/dialog_warning.svg'))
 
96
    return d
 
97
 
 
98
def error_dialog(parent, title, msg):
 
99
    d = QMessageBox(QMessageBox.Critical, 'ERROR: '+title, msg, QMessageBox.Ok,
 
100
                    parent)
 
101
    d.setIconPixmap(QPixmap(':/images/dialog_error.svg'))
 
102
    return d
 
103
 
 
104
def question_dialog(parent, title, msg):
 
105
    d = QMessageBox(QMessageBox.Question, title, msg, QMessageBox.Yes|QMessageBox.No,
 
106
                    parent)
 
107
    d.setIconPixmap(QPixmap(':/images/dialog_information.svg'))
 
108
    return d
 
109
 
 
110
def info_dialog(parent, title, msg):
 
111
    d = QMessageBox(QMessageBox.Information, title, msg, QMessageBox.NoButton,
 
112
                    parent)
 
113
    d.setIconPixmap(QPixmap(':/images/dialog_information.svg'))
 
114
    return d
 
115
 
 
116
def qstring_to_unicode(q):
 
117
    return unicode(q)
 
118
 
 
119
def human_readable(size):
 
120
    """ Convert a size in bytes into a human readable form """
 
121
    divisor, suffix = 1, "B"
 
122
    if size < 1024*1024:
 
123
        divisor, suffix = 1024., "KB"
 
124
    elif size < 1024*1024*1024:
 
125
        divisor, suffix = 1024*1024, "MB"
 
126
    elif size < 1024*1024*1024*1024:
 
127
        divisor, suffix = 1024*1024*1024, "GB"
 
128
    size = str(float(size)/divisor)
 
129
    if size.find(".") > -1:
 
130
        size = size[:size.find(".")+2]
 
131
    if size.endswith('.0'):
 
132
        size = size[:-2]
 
133
    return size + " " + suffix
 
134
 
 
135
class Dispatcher(QObject):
 
136
    '''Convenience class to ensure that a function call always happens in the GUI thread'''
 
137
    
 
138
    def __init__(self, func):
 
139
        QObject.__init__(self)
 
140
        self.func = func
 
141
        self.connect(self, SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject)'), 
 
142
                     self.dispatch, Qt.QueuedConnection)
 
143
        
 
144
    def __call__(self, *args, **kwargs):
 
145
        self.emit(SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject)'), args, kwargs)
 
146
        
 
147
    def dispatch(self, args, kwargs):
 
148
        self.func(*args, **kwargs)
 
149
        
 
150
 
 
151
class TableView(QTableView):
 
152
    def __init__(self, parent):
 
153
        QTableView.__init__(self, parent)
 
154
        self.read_settings()
 
155
        
 
156
    def read_settings(self):
 
157
        self.cw = dynamic[self.__class__.__name__+'column widths']
 
158
    
 
159
    def write_settings(self):
 
160
        dynamic[self.__class__.__name__+'column widths'] = \
 
161
         tuple([int(self.columnWidth(i)) for i in range(self.model().columnCount(None))])
 
162
    
 
163
    def restore_column_widths(self):
 
164
        if self.cw and len(self.cw):
 
165
            for i in range(len(self.cw)):
 
166
                self.setColumnWidth(i, self.cw[i])
 
167
            return True
 
168
    
 
169
class FileIconProvider(QFileIconProvider):
 
170
    
 
171
    ICONS = {
 
172
             'default' : 'unknown',
 
173
             'dir'     : 'dir',
 
174
             'zero'    : 'zero',
 
175
             
 
176
             'jpeg'    : 'jpeg',
 
177
             'jpg'     : 'jpeg',
 
178
             'gif'     : 'gif',
 
179
             'png'     : 'png',
 
180
             'bmp'     : 'bmp',
 
181
             'svg'     : 'svg',
 
182
             'html'    : 'html',
 
183
             'htm'     : 'html',
 
184
             'xhtml'   : 'html',
 
185
             'xhtm'    : 'html',
 
186
             'lit'     : 'lit',
 
187
             'lrf'     : 'lrf',
 
188
             'lrx'     : 'lrx',
 
189
             'pdf'     : 'pdf',
 
190
             'rar'     : 'rar',
 
191
             'zip'     : 'zip',
 
192
             'txt'     : 'txt',
 
193
             'prc'     : 'mobi',
 
194
             'azw'     : 'mobi',
 
195
             'mobi'    : 'mobi',
 
196
             'epub'    : 'epub',
 
197
             }
 
198
    
 
199
    def __init__(self):
 
200
        QFileIconProvider.__init__(self)
 
201
        self.icons = {}
 
202
        for key in self.__class__.ICONS.keys():
 
203
            self.icons[key] = ':/images/mimetypes/'+self.__class__.ICONS[key]+'.svg'
 
204
        for i in ('dir', 'default', 'zero'):
 
205
            self.icons[i] = QIcon(self.icons[i])
 
206
    
 
207
    def key_from_ext(self, ext):
 
208
        key = ext if ext in self.icons.keys() else 'default'
 
209
        if key == 'default' and ext.count('.') > 0:
 
210
            ext = ext.rpartition('.')[2]
 
211
            key = ext if ext in self.icons.keys() else 'default'
 
212
        return key
 
213
    
 
214
    def cached_icon(self, key):
 
215
        candidate = self.icons[key]
 
216
        if isinstance(candidate, QIcon):
 
217
            return candidate
 
218
        icon = QIcon(candidate)
 
219
        self.icons[key] = icon
 
220
        return icon
 
221
    
 
222
    def icon_from_ext(self, ext):
 
223
        key = self.key_from_ext(ext.lower() if ext else '')
 
224
        return self.cached_icon(key)
 
225
    
 
226
    def load_icon(self, fileinfo):
 
227
        key = 'default'
 
228
        icons = self.icons
 
229
        if fileinfo.isSymLink():
 
230
            if not fileinfo.exists():
 
231
                return icons['zero']
 
232
            fileinfo = QFileInfo(fileinfo.readLink())
 
233
        if fileinfo.isDir():
 
234
            key = 'dir'
 
235
        else:
 
236
            ext = qstring_to_unicode(fileinfo.completeSuffix()).lower()
 
237
            key = self.key_from_ext(ext)
 
238
        return self.cached_icon(key)
 
239
    
 
240
    def icon(self, arg):
 
241
        if isinstance(arg, QFileInfo):
 
242
            return self.load_icon(arg)
 
243
        if arg == QFileIconProvider.Folder:
 
244
            return self.icons['dir']
 
245
        if arg == QFileIconProvider.File:
 
246
            return self.icons['default']
 
247
        return QFileIconProvider.icon(self, arg)
 
248
        
 
249
_file_icon_provider = None
 
250
def initialize_file_icon_provider():
 
251
    global _file_icon_provider
 
252
    if _file_icon_provider is None:
 
253
        _file_icon_provider = FileIconProvider()
 
254
        
 
255
def file_icon_provider():
 
256
    global _file_icon_provider
 
257
    return _file_icon_provider
 
258
 
 
259
_sidebar_directories = []
 
260
def set_sidebar_directories(dirs):
 
261
    global _sidebar_directories
 
262
    if dirs is None:
 
263
        dirs = config['frequently_used_directories']        
 
264
    _sidebar_directories = [QUrl.fromLocalFile(i) for i in dirs]
 
265
        
 
266
class FileDialog(QObject):
 
267
    def __init__(self, title='Choose Files', 
 
268
                       filters=[],
 
269
                       add_all_files_filter=True, 
 
270
                       parent=None,
 
271
                       modal = True,
 
272
                       name = '',
 
273
                       mode = QFileDialog.ExistingFiles,
 
274
                       ):
 
275
        QObject.__init__(self)
 
276
        initialize_file_icon_provider()
 
277
        ftext = ''
 
278
        if filters:
 
279
            for filter in filters:
 
280
                text, extensions = filter
 
281
                extensions = ['*.'+i if not i.startswith('.') else i for i in extensions]
 
282
                ftext += '%s (%s);;'%(text, ' '.join(extensions))
 
283
        if add_all_files_filter or not ftext:
 
284
            ftext += 'All files (*)'
 
285
        
 
286
        self.dialog_name = name if name else 'dialog_' + title
 
287
        self.selected_files = None
 
288
        self.fd = None
 
289
        
 
290
        if islinux:
 
291
            self.fd = QFileDialog(parent)
 
292
            self.fd.setFileMode(mode)  
 
293
            self.fd.setIconProvider(_file_icon_provider)
 
294
            self.fd.setModal(modal)            
 
295
            self.fd.setNameFilter(ftext)
 
296
            self.fd.setWindowTitle(title)
 
297
            state = dynamic[self.dialog_name]
 
298
            if not state or not self.fd.restoreState(state):
 
299
                self.fd.setDirectory(os.path.expanduser('~'))
 
300
            osu = [i for i in self.fd.sidebarUrls()]
 
301
            self.fd.setSidebarUrls(osu + _sidebar_directories)
 
302
            QObject.connect(self.fd, SIGNAL('accepted()'), self.save_dir)
 
303
            self.accepted = self.fd.exec_() == QFileDialog.Accepted
 
304
        else:
 
305
            dir = dynamic.get(self.dialog_name, os.path.expanduser('~'))
 
306
            self.selected_files = []
 
307
            if mode == QFileDialog.AnyFile:
 
308
                f = qstring_to_unicode(
 
309
                    QFileDialog.getSaveFileName(parent, title, dir, ftext, ""))
 
310
                if os.path.exists(f):
 
311
                    self.selected_files.append(f)                
 
312
            elif mode == QFileDialog.ExistingFile:
 
313
                f = qstring_to_unicode(
 
314
                    QFileDialog.getOpenFileName(parent, title, dir, ftext, ""))
 
315
                if os.path.exists(f):
 
316
                    self.selected_files.append(f)
 
317
            elif mode == QFileDialog.ExistingFiles:
 
318
                fs = QFileDialog.getOpenFileNames(parent, title, dir, ftext, "")
 
319
                for f in fs:
 
320
                    if os.path.exists(qstring_to_unicode(f)):
 
321
                        self.selected_files.append(f)
 
322
            else:
 
323
                opts = QFileDialog.ShowDirsOnly if mode == QFileDialog.DirectoryOnly else QFileDialog.Option()
 
324
                f = qstring_to_unicode(
 
325
                        QFileDialog.getExistingDirectory(parent, title, dir, opts))
 
326
                if os.path.exists(f):
 
327
                    self.selected_files.append(f)
 
328
            if self.selected_files:
 
329
                self.selected_files = [qstring_to_unicode(q) for q in self.selected_files]
 
330
                dynamic[self.dialog_name] =  os.path.dirname(self.selected_files[0])
 
331
            self.accepted = bool(self.selected_files)        
 
332
        
 
333
        
 
334
    
 
335
    def get_files(self):
 
336
        if islinux and self.fd.result() != self.fd.Accepted:
 
337
            return tuple() 
 
338
        if self.selected_files is None:       
 
339
            return tuple(os.path.abspath(qstring_to_unicode(i)) for i in self.fd.selectedFiles())
 
340
        return tuple(self.selected_files)
 
341
    
 
342
    def save_dir(self):
 
343
        if self.fd:
 
344
            dynamic[self.dialog_name] =  self.fd.saveState()
 
345
        
 
346
 
 
347
def choose_dir(window, name, title):
 
348
    fd = FileDialog(title, [], False, window, name=name, 
 
349
                    mode=QFileDialog.DirectoryOnly)
 
350
    dir = fd.get_files()
 
351
    if dir:
 
352
        return dir[0]
 
353
 
 
354
def choose_files(window, name, title,  
 
355
                 filters=[], all_files=True, select_only_single_file=False):
 
356
    '''
 
357
    Ask user to choose a bunch of files.
 
358
    @param name: Unique dialog name used to store the opened directory
 
359
    @param title: Title to show in dialogs titlebar
 
360
    @param filters: list of allowable extensions. Each element of the list
 
361
                     must be a 2-tuple with first element a string describing 
 
362
                     the type of files to be filtered and second element a list
 
363
                     of extensions. 
 
364
    @param all_files: If True add All files to filters.
 
365
    @param select_only_single_file: If True only one file can be selected  
 
366
    '''
 
367
    mode = QFileDialog.ExistingFile if select_only_single_file else QFileDialog.ExistingFiles
 
368
    fd = FileDialog(title=title, name=name, filters=filters, 
 
369
                    parent=window, add_all_files_filter=all_files, mode=mode,
 
370
                    )
 
371
    if fd.accepted:
 
372
        return fd.get_files()
 
373
    return None
 
374
 
 
375
def choose_images(window, name, title, select_only_single_file=True):
 
376
    mode = QFileDialog.ExistingFile if select_only_single_file else QFileDialog.ExistingFiles
 
377
    fd = FileDialog(title=title, name=name, 
 
378
                    filters=[('Images', ['png', 'gif', 'jpeg', 'jpg', 'svg'])], 
 
379
                    parent=window, add_all_files_filter=False, mode=mode,
 
380
                    )
 
381
    if fd.accepted:
 
382
        return fd.get_files()
 
383
    return None
 
384
 
 
385
def pixmap_to_data(pixmap, format='JPEG'):
 
386
    '''
 
387
    Return the QPixmap pixmap as a string saved in the specified format.
 
388
    '''
 
389
    ba = QByteArray()
 
390
    buf = QBuffer(ba)
 
391
    buf.open(QBuffer.WriteOnly)
 
392
    pixmap.save(buf, format)
 
393
    return str(ba.data())
 
394
 
 
395
 
 
396
try:
 
397
    from calibre.utils.single_qt_application import SingleApplication
 
398
except:
 
399
    SingleApplication = None
 
400
    
 
401
class Application(QApplication):
 
402
    
 
403
    def __init__(self, args):
 
404
        qargs = [i.encode('utf-8') if isinstance(i, unicode) else i for i in args]
 
405
        QApplication.__init__(self, qargs)
 
406
        self.translator = QTranslator(self)
 
407
        lang = get_lang()
 
408
        if lang:
 
409
            data = getattr(resources, 'qt_'+lang, None)
 
410
            if data:
 
411
                self.translator.loadFromData(data)
 
412
                self.installTranslator(self.translator)
 
413
                
 
414
         
 
415