2
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
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, \
8
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
9
QIcon, QTableView, QDialogButtonBox, QApplication
11
ORG_NAME = 'KovidsBrain'
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
18
NONE = QVariant() #: Null value to return from the data function of item models
20
ALL_COLUMNS = ['title', 'authors', 'size', 'timestamp', 'rating', 'publisher', 'tags', 'series']
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'))
66
# Turn off DeprecationWarnings in windows GUI
69
warnings.simplefilter('ignore', DeprecationWarning)
71
def available_heights():
72
desktop = QCoreApplication.instance().desktop()
73
return map(lambda x: x.height(), map(desktop.availableGeometry, range(desktop.numScreens())))
75
def available_height():
76
desktop = QCoreApplication.instance().desktop()
77
return desktop.availableGeometry().height()
79
def max_available_height():
80
return max(available_heights())
82
def min_available_height():
83
return min(available_heights())
85
def available_width():
86
desktop = QCoreApplication.instance().desktop()
87
return desktop.availableGeometry().width()
90
return os.path.splitext(path)[1][1:].lower()
92
def warning_dialog(parent, title, msg):
93
d = QMessageBox(QMessageBox.Warning, 'WARNING: '+title, msg, QMessageBox.Ok,
95
d.setIconPixmap(QPixmap(':/images/dialog_warning.svg'))
98
def error_dialog(parent, title, msg):
99
d = QMessageBox(QMessageBox.Critical, 'ERROR: '+title, msg, QMessageBox.Ok,
101
d.setIconPixmap(QPixmap(':/images/dialog_error.svg'))
104
def question_dialog(parent, title, msg):
105
d = QMessageBox(QMessageBox.Question, title, msg, QMessageBox.Yes|QMessageBox.No,
107
d.setIconPixmap(QPixmap(':/images/dialog_information.svg'))
110
def info_dialog(parent, title, msg):
111
d = QMessageBox(QMessageBox.Information, title, msg, QMessageBox.NoButton,
113
d.setIconPixmap(QPixmap(':/images/dialog_information.svg'))
116
def qstring_to_unicode(q):
119
def human_readable(size):
120
""" Convert a size in bytes into a human readable form """
121
divisor, suffix = 1, "B"
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'):
133
return size + " " + suffix
135
class Dispatcher(QObject):
136
'''Convenience class to ensure that a function call always happens in the GUI thread'''
138
def __init__(self, func):
139
QObject.__init__(self)
141
self.connect(self, SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject)'),
142
self.dispatch, Qt.QueuedConnection)
144
def __call__(self, *args, **kwargs):
145
self.emit(SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject)'), args, kwargs)
147
def dispatch(self, args, kwargs):
148
self.func(*args, **kwargs)
151
class TableView(QTableView):
152
def __init__(self, parent):
153
QTableView.__init__(self, parent)
156
def read_settings(self):
157
self.cw = dynamic[self.__class__.__name__+'column widths']
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))])
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])
169
class FileIconProvider(QFileIconProvider):
172
'default' : 'unknown',
200
QFileIconProvider.__init__(self)
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])
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'
214
def cached_icon(self, key):
215
candidate = self.icons[key]
216
if isinstance(candidate, QIcon):
218
icon = QIcon(candidate)
219
self.icons[key] = icon
222
def icon_from_ext(self, ext):
223
key = self.key_from_ext(ext.lower() if ext else '')
224
return self.cached_icon(key)
226
def load_icon(self, fileinfo):
229
if fileinfo.isSymLink():
230
if not fileinfo.exists():
232
fileinfo = QFileInfo(fileinfo.readLink())
236
ext = qstring_to_unicode(fileinfo.completeSuffix()).lower()
237
key = self.key_from_ext(ext)
238
return self.cached_icon(key)
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)
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()
255
def file_icon_provider():
256
global _file_icon_provider
257
return _file_icon_provider
259
_sidebar_directories = []
260
def set_sidebar_directories(dirs):
261
global _sidebar_directories
263
dirs = config['frequently_used_directories']
264
_sidebar_directories = [QUrl.fromLocalFile(i) for i in dirs]
266
class FileDialog(QObject):
267
def __init__(self, title='Choose Files',
269
add_all_files_filter=True,
273
mode = QFileDialog.ExistingFiles,
275
QObject.__init__(self)
276
initialize_file_icon_provider()
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 (*)'
286
self.dialog_name = name if name else 'dialog_' + title
287
self.selected_files = None
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
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, "")
320
if os.path.exists(qstring_to_unicode(f)):
321
self.selected_files.append(f)
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)
336
if islinux and self.fd.result() != self.fd.Accepted:
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)
344
dynamic[self.dialog_name] = self.fd.saveState()
347
def choose_dir(window, name, title):
348
fd = FileDialog(title, [], False, window, name=name,
349
mode=QFileDialog.DirectoryOnly)
354
def choose_files(window, name, title,
355
filters=[], all_files=True, select_only_single_file=False):
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
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
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,
372
return fd.get_files()
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,
382
return fd.get_files()
385
def pixmap_to_data(pixmap, format='JPEG'):
387
Return the QPixmap pixmap as a string saved in the specified format.
391
buf.open(QBuffer.WriteOnly)
392
pixmap.save(buf, format)
393
return str(ba.data())
397
from calibre.utils.single_qt_application import SingleApplication
399
SingleApplication = None
401
class Application(QApplication):
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)
409
data = getattr(resources, 'qt_'+lang, None)
411
self.translator.loadFromData(data)
412
self.installTranslator(self.translator)