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

« back to all changes in this revision

Viewing changes to src/calibre/gui2/main.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:
2
2
__license__   = 'GPL v3'
3
3
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
4
4
'''The main GUI'''
5
 
import os, sys, textwrap, collections, traceback, time
 
5
import os, sys, textwrap, collections, traceback, time, socket
6
6
from xml.parsers.expat import ExpatError
 
7
from Queue import Queue, Empty
 
8
from threading import Thread
7
9
from functools import partial
8
10
from PyQt4.Qt import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer, \
9
11
                     QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \
10
12
                     QToolButton, QDialog, QDesktopServices, QFileDialog, \
11
13
                     QSystemTrayIcon, QApplication, QKeySequence, QAction, \
12
 
                     QProgressDialog, QMessageBox, QStackedLayout
 
14
                     QMessageBox, QStackedLayout
13
15
from PyQt4.QtSvg import QSvgRenderer
14
16
 
15
 
from calibre import __version__, __appname__, islinux, sanitize_file_name, \
16
 
                    iswindows, isosx
 
17
from calibre import __version__, __appname__, \
 
18
                    iswindows, isosx, prints, patheq
 
19
from calibre.utils.filenames import ascii_filename
17
20
from calibre.ptempfile import PersistentTemporaryFile
18
21
from calibre.utils.config import prefs, dynamic
 
22
from calibre.utils.ipc.server import Server
19
23
from calibre.gui2 import APP_UID, warning_dialog, choose_files, error_dialog, \
20
24
                           initialize_file_icon_provider, question_dialog,\
21
25
                           pixmap_to_data, choose_dir, ORG_NAME, \
22
26
                           set_sidebar_directories, Dispatcher, \
23
 
                           SingleApplication, Application, available_height, \
 
27
                           Application, available_height, \
24
28
                           max_available_height, config, info_dialog, \
25
29
                           available_width, GetMetadata
26
30
from calibre.gui2.cover_flow import CoverFlow, DatabaseImages, pictureflowerror
27
 
from calibre.gui2.widgets import ProgressIndicator, WarningDialog
 
31
from calibre.gui2.widgets import ProgressIndicator
 
32
from calibre.gui2.wizard import move_library
28
33
from calibre.gui2.dialogs.scheduler import Scheduler
29
34
from calibre.gui2.update import CheckForUpdates
30
 
from calibre.gui2.dialogs.progress import ProgressDialog
31
35
from calibre.gui2.main_window import MainWindow, option_parser as _option_parser
32
36
from calibre.gui2.main_ui import Ui_MainWindow
33
37
from calibre.gui2.device import DeviceManager, DeviceMenu, DeviceGUI, Emailer
34
38
from calibre.gui2.status import StatusBar
35
 
from calibre.gui2.jobs2 import JobManager
 
39
from calibre.gui2.jobs import JobManager, JobsDialog
36
40
from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog
37
41
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
38
 
from calibre.gui2.dialogs.jobs import JobsDialog
39
 
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
40
 
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebooks, \
41
 
                                set_conversion_defaults, fetch_scheduled_recipe
 
42
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \
 
43
    fetch_scheduled_recipe
42
44
from calibre.gui2.dialogs.config import ConfigDialog
43
45
from calibre.gui2.dialogs.search import SearchDialog
44
46
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
45
47
from calibre.gui2.dialogs.book_info import BookInfo
46
48
from calibre.ebooks import BOOK_EXTENSIONS
47
49
from calibre.library.database2 import LibraryDatabase2, CoverCache
48
 
from calibre.parallel import JobKilled
49
50
from calibre.gui2.dialogs.confirm_delete import confirm
50
51
 
 
52
ADDRESS = r'\\.\pipe\CalibreGUI' if iswindows else \
 
53
    os.path.expanduser('~/.calibre-gui.socket')
 
54
 
 
55
class SaveMenu(QMenu):
 
56
 
 
57
    def __init__(self, parent):
 
58
        QMenu.__init__(self, _('Save single format to disk...'), parent)
 
59
        for ext in sorted(BOOK_EXTENSIONS):
 
60
            action = self.addAction(ext.upper())
 
61
            setattr(self, 'do_'+ext, partial(self.do, ext))
 
62
            self.connect(action, SIGNAL('triggered(bool)'),
 
63
                    getattr(self, 'do_'+ext))
 
64
 
 
65
    def do(self, ext, *args):
 
66
        self.emit(SIGNAL('save_fmt(PyQt_PyObject)'), ext)
 
67
 
 
68
class Listener(Thread):
 
69
 
 
70
    def __init__(self, listener):
 
71
        Thread.__init__(self)
 
72
        self.daemon = True
 
73
        self.listener, self.queue = listener, Queue()
 
74
        self._run = True
 
75
        self.start()
 
76
 
 
77
    def run(self):
 
78
        while self._run:
 
79
            try:
 
80
                conn = self.listener.accept()
 
81
                msg = conn.recv()
 
82
                self.queue.put(msg)
 
83
            except:
 
84
                continue
 
85
 
 
86
    def close(self):
 
87
        self._run = False
 
88
        try:
 
89
            self.listener.close()
 
90
        except:
 
91
            pass
 
92
 
 
93
 
51
94
class Main(MainWindow, Ui_MainWindow, DeviceGUI):
52
95
    'The main GUI'
53
96
 
61
104
        self.default_thumbnail = (pixmap.width(), pixmap.height(),
62
105
                pixmap_to_data(pixmap))
63
106
 
64
 
    def __init__(self, single_instance, opts, actions, parent=None):
 
107
    def __init__(self, listener, opts, actions, parent=None):
65
108
        self.preferences_action, self.quit_action = actions
 
109
        self.spare_servers = []
66
110
        MainWindow.__init__(self, opts, parent)
67
111
        # Initialize fontconfig in a separate thread as this can be a lengthy
68
112
        # process if run for the first time on this machine
69
 
        self.fc = __import__('calibre.utils.fontconfig', fromlist=1)
70
 
        self.single_instance = single_instance
71
 
        if self.single_instance is not None:
72
 
            self.connect(self.single_instance,
73
 
                    SIGNAL('message_received(PyQt_PyObject)'),
74
 
                         self.another_instance_wants_to_talk)
 
113
        from calibre.utils.fonts import fontconfig
 
114
        self.fc = fontconfig
 
115
        self.listener = Listener(listener)
 
116
        self.check_messages_timer = QTimer()
 
117
        self.connect(self.check_messages_timer, SIGNAL('timeout()'),
 
118
                self.another_instance_wants_to_talk)
 
119
        self.check_messages_timer.start(1000)
75
120
 
76
121
        Ui_MainWindow.__init__(self)
77
122
        self.setupUi(self)
90
135
        self.persistent_files = []
91
136
        self.metadata_dialogs = []
92
137
        self.default_thumbnail = None
93
 
        self.device_error_dialog = ConversionErrorDialog(self,
 
138
        self.device_error_dialog = error_dialog(self, _('Error'),
94
139
                _('Error communicating with device'), ' ')
95
140
        self.device_error_dialog.setModal(Qt.NonModal)
96
141
        self.tb_wrapper = textwrap.TextWrapper(width=40)
103
148
            self.system_tray_icon.hide()
104
149
        else:
105
150
            self.system_tray_icon.show()
 
151
        self.search.search_as_you_type(config['search_as_you_type'])
106
152
        self.system_tray_menu = QMenu(self)
107
153
        self.restore_action = self.system_tray_menu.addAction(
108
154
                QIcon(':/images/page.svg'), _('&Restore'))
123
169
        self.connect(self.quit_action, SIGNAL('triggered(bool)'), self.quit)
124
170
        self.connect(self.donate_action, SIGNAL('triggered(bool)'), self.donate)
125
171
        self.connect(self.restore_action, SIGNAL('triggered()'),
126
 
                self.show)
 
172
                self.show_windows)
127
173
        self.connect(self.action_show_book_details,
128
174
                SIGNAL('triggered(bool)'), self.show_book_info)
129
175
        self.connect(self.action_restart, SIGNAL('triggered()'),
132
178
                SIGNAL('activated(QSystemTrayIcon::ActivationReason)'),
133
179
                self.system_tray_icon_activated)
134
180
        self.tool_bar.contextMenuEvent = self.no_op
 
181
 
 
182
        ####################### Start spare job server ########################
 
183
        QTimer.singleShot(1000, self.add_spare_server)
 
184
 
 
185
        ####################### Setup device detection ########################
 
186
        self.device_manager = DeviceManager(Dispatcher(self.device_detected),
 
187
                self.job_manager)
 
188
        self.device_manager.start()
 
189
 
 
190
 
135
191
        ####################### Location View ########################
136
192
        QObject.connect(self.location_view,
137
193
                SIGNAL('location_selected(PyQt_PyObject)'),
138
194
                        self.location_selected)
139
 
 
140
 
        self.output_formats = sorted(['EPUB', 'MOBI', 'LRF'])
141
 
        for f in self.output_formats:
142
 
            self.output_format.addItem(f)
143
 
        self.output_format.setCurrentIndex(self.output_formats.index(
144
 
            prefs['output_format']))
145
 
        self.connect(self.output_format, SIGNAL('currentIndexChanged(QString)'),
146
 
                     self.change_output_format, Qt.QueuedConnection)
 
195
        QObject.connect(self.location_view,
 
196
                SIGNAL('umount_device()'),
 
197
                        self.device_manager.umount_device)
147
198
 
148
199
        ####################### Vanity ########################
149
200
        self.vanity_template  = _('<p>For help visit <a href="http://%s.'
185
236
        self.add_menu.addAction(_('Add books from directories, including '
186
237
            'sub directories (Multiple books per directory, assumes every '
187
238
            'ebook file is a different book)'))
 
239
        self.add_menu.addAction(_('Add Empty book. (Book entry with no '
 
240
            'formats)'))
188
241
        self.action_add.setMenu(self.add_menu)
189
242
        QObject.connect(self.action_add, SIGNAL("triggered(bool)"),
190
243
                self.add_books)
194
247
                self.add_recursive_single)
195
248
        QObject.connect(self.add_menu.actions()[2], SIGNAL("triggered(bool)"),
196
249
                self.add_recursive_multiple)
 
250
        QObject.connect(self.add_menu.actions()[3], SIGNAL('triggered(bool)'),
 
251
                self.add_empty)
197
252
        QObject.connect(self.action_del, SIGNAL("triggered(bool)"),
198
253
                self.delete_books)
199
254
        QObject.connect(self.action_edit, SIGNAL("triggered(bool)"),
200
255
                self.edit_metadata)
 
256
        self.__em1__ = partial(self.edit_metadata, bulk=False)
201
257
        QObject.connect(md.actions()[0], SIGNAL('triggered(bool)'),
202
 
                partial(self.edit_metadata, bulk=False))
 
258
                self.__em1__)
 
259
        self.__em2__ = partial(self.edit_metadata, bulk=True)
203
260
        QObject.connect(md.actions()[2], SIGNAL('triggered(bool)'),
204
 
                partial(self.edit_metadata, bulk=True))
 
261
                self.__em2__)
 
262
        self.__em3__ = partial(self.download_metadata, covers=True)
205
263
        QObject.connect(md.actions()[4], SIGNAL('triggered(bool)'),
206
 
                partial(self.download_metadata, covers=True))
 
264
                self.__em3__)
 
265
        self.__em4__ = partial(self.download_metadata, covers=False)
207
266
        QObject.connect(md.actions()[5], SIGNAL('triggered(bool)'),
208
 
                partial(self.download_metadata, covers=False))
 
267
                self.__em4__)
 
268
        self.__em5__ = partial(self.download_metadata, covers=True,
 
269
                    set_metadata=False)
209
270
        QObject.connect(md.actions()[6], SIGNAL('triggered(bool)'),
210
 
                partial(self.download_metadata, covers=True,
211
 
                    set_metadata=False))
 
271
                self.__em5__)
212
272
 
213
273
 
214
274
 
215
275
        self.save_menu = QMenu()
216
276
        self.save_menu.addAction(_('Save to disk'))
217
277
        self.save_menu.addAction(_('Save to disk in a single directory'))
218
 
        self.save_menu.addAction(_('Save only %s format to disk')%\
219
 
                config.get('save_to_disk_single_format').upper())
 
278
        self.save_menu.addAction(_('Save only %s format to disk')%
 
279
                prefs['output_format'].upper())
 
280
        self.save_sub_menu = SaveMenu(self)
 
281
        self.save_menu.addMenu(self.save_sub_menu)
 
282
        self.connect(self.save_sub_menu, SIGNAL('save_fmt(PyQt_PyObject)'),
 
283
                self.save_specific_format_disk)
220
284
 
221
285
        self.view_menu = QMenu()
222
286
        self.view_menu.addAction(_('View'))
248
312
        cm = QMenu()
249
313
        cm.addAction(_('Convert individually'))
250
314
        cm.addAction(_('Bulk convert'))
251
 
        cm.addSeparator()
252
 
        cm.addAction(_('Set defaults for conversion'))
253
 
        cm.addAction(_('Set defaults for conversion of comics'))
254
315
        self.action_convert.setMenu(cm)
 
316
        self._convert_single_hook = partial(self.convert_ebook, bulk=False)
255
317
        QObject.connect(cm.actions()[0],
256
 
                SIGNAL('triggered(bool)'), self.convert_single)
 
318
                SIGNAL('triggered(bool)'), self._convert_single_hook)
 
319
        self._convert_bulk_hook = partial(self.convert_ebook, bulk=True)
257
320
        QObject.connect(cm.actions()[1],
258
 
                SIGNAL('triggered(bool)'), self.convert_bulk)
259
 
        QObject.connect(cm.actions()[3],
260
 
                SIGNAL('triggered(bool)'), self.set_conversion_defaults)
261
 
        QObject.connect(cm.actions()[4],
262
 
                SIGNAL('triggered(bool)'), self.set_comic_conversion_defaults)
 
321
                SIGNAL('triggered(bool)'), self._convert_bulk_hook)
263
322
        QObject.connect(self.action_convert,
264
 
                SIGNAL('triggered(bool)'), self.convert_single)
 
323
                SIGNAL('triggered(bool)'), self.convert_ebook)
265
324
        self.convert_menu = cm
 
325
 
 
326
        pm = QMenu()
 
327
        ap = self.action_preferences
 
328
        pm.addAction(ap.icon(), ap.text())
 
329
        pm.addAction(_('Run welcome wizard'))
 
330
        self.connect(pm.actions()[0], SIGNAL('triggered(bool)'),
 
331
                self.do_config)
 
332
        self.connect(pm.actions()[1], SIGNAL('triggered(bool)'),
 
333
                self.run_wizard)
 
334
        self.action_preferences.setMenu(pm)
 
335
        self.preferences_menu = pm
 
336
 
266
337
        self.tool_bar.widgetForAction(self.action_news).\
267
338
                setPopupMode(QToolButton.MenuButtonPopup)
268
339
        self.tool_bar.widgetForAction(self.action_edit).\
277
348
                setPopupMode(QToolButton.MenuButtonPopup)
278
349
        self.tool_bar.widgetForAction(self.action_view).\
279
350
                setPopupMode(QToolButton.MenuButtonPopup)
 
351
        self.tool_bar.widgetForAction(self.action_preferences).\
 
352
                setPopupMode(QToolButton.MenuButtonPopup)
280
353
        self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu)
281
354
 
282
 
        QObject.connect(self.config_button,
283
 
                SIGNAL('clicked(bool)'), self.do_config)
284
355
        self.connect(self.preferences_action, SIGNAL('triggered(bool)'),
285
356
                self.do_config)
286
357
        self.connect(self.action_preferences, SIGNAL('triggered(bool)'),
320
391
                                        similar_menu=similar_menu)
321
392
        self.memory_view.set_context_menu(None, None, None,
322
393
                self.action_view, self.action_save, None, None)
323
 
        self.card_view.set_context_menu(None, None, None,
 
394
        self.card_a_view.set_context_menu(None, None, None,
 
395
                self.action_view, self.action_save, None, None)
 
396
        self.card_b_view.set_context_menu(None, None, None,
324
397
                self.action_view, self.action_save, None, None)
325
398
        QObject.connect(self.library_view,
326
399
                SIGNAL('files_dropped(PyQt_PyObject)'),
327
 
                        self.files_dropped)
328
 
        for func, target in [
329
 
                             ('connect_to_search_box', self.search),
 
400
                        self.files_dropped, Qt.QueuedConnection)
 
401
        for func, args in [
 
402
                             ('connect_to_search_box', (self.search,
 
403
                                 self.search_done)),
330
404
                             ('connect_to_book_display',
331
 
                                 self.status_bar.book_info.show_data),
 
405
                                 (self.status_bar.book_info.show_data,)),
332
406
                             ]:
333
 
            for view in (self.library_view, self.memory_view, self.card_view):
334
 
                getattr(view, func)(target)
 
407
            for view in (self.library_view, self.memory_view, self.card_a_view, self.card_b_view):
 
408
                getattr(view, func)(*args)
335
409
 
336
410
        self.memory_view.connect_dirtied_signal(self.upload_booklists)
337
 
        self.card_view.connect_dirtied_signal(self.upload_booklists)
 
411
        self.card_a_view.connect_dirtied_signal(self.upload_booklists)
 
412
        self.card_b_view.connect_dirtied_signal(self.upload_booklists)
338
413
 
339
414
        self.show()
340
415
        if self.system_tray_icon.isVisible() and opts.start_in_tray:
341
 
            self.hide()
 
416
            self.hide_windows()
342
417
        self.stack.setCurrentIndex(0)
343
418
        try:
344
419
            db = LibraryDatabase2(self.library_path)
345
420
        except Exception, err:
 
421
            import traceback
346
422
            error_dialog(self, _('Bad database location'),
347
 
                    unicode(err)).exec_()
 
423
                    _('Bad database location')+':'+self.library_path,
 
424
                    det_msg=traceback.format_exc()).exec_()
348
425
            dir = unicode(QFileDialog.getExistingDirectory(self,
349
426
                            _('Choose a location for your ebook library.'),
350
427
                            os.path.expanduser('~')))
351
428
            if not dir:
352
429
                QCoreApplication.exit(1)
 
430
                raise SystemExit(1)
353
431
            else:
354
432
                self.library_path = dir
355
433
                db = LibraryDatabase2(self.library_path)
381
459
                SIGNAL('count_changed(int)'), self.location_view.count_changed)
382
460
        self.connect(self.library_view.model(), SIGNAL('count_changed(int)'),
383
461
                     self.tags_view.recount)
 
462
        self.connect(self.search, SIGNAL('cleared()'), self.tags_view.clear)
384
463
        self.library_view.model().count_changed()
385
464
        ########################### Cover Flow ################################
386
465
        self.cover_flow = None
411
490
        else:
412
491
            self.status_bar.cover_flow_button.disable(pictureflowerror)
413
492
 
414
 
 
415
 
        self.setMaximumHeight(max_available_height())
416
 
 
417
 
        ####################### Setup device detection ########################
418
 
        self.device_manager = DeviceManager(Dispatcher(self.device_detected),
419
 
                self.job_manager)
420
 
        self.device_manager.start()
 
493
        self._calculated_available_height = min(max_available_height()-15,
 
494
                self.height())
 
495
        self.resize(self.width(), self._calculated_available_height)
421
496
 
422
497
 
423
498
        if config['autolaunch_server']:
443
518
        self.connect(self.action_sync, SIGNAL('triggered(bool)'),
444
519
                self._sync_menu.trigger_default)
445
520
 
 
521
    def add_spare_server(self, *args):
 
522
        self.spare_servers.append(Server())
446
523
 
 
524
    @property
 
525
    def spare_server(self):
 
526
        # Because of the use of the property decorator, we're called one
 
527
        # extra time. Ignore.
 
528
        if not hasattr(self, '__spare_server_property_limiter'):
 
529
            self.__spare_server_property_limiter = True
 
530
            return None
 
531
        try:
 
532
            QTimer.singleShot(1000, self.add_spare_server)
 
533
            return self.spare_servers.pop()
 
534
        except:
 
535
            pass
447
536
 
448
537
    def no_op(self, *args):
449
538
        pass
451
540
    def system_tray_icon_activated(self, r):
452
541
        if r == QSystemTrayIcon.Trigger:
453
542
            if self.isVisible():
454
 
                for window in QApplication.topLevelWidgets():
455
 
                    if isinstance(window, (MainWindow, QDialog)) and \
456
 
                            window.isVisible():
457
 
                        window.hide()
458
 
                        setattr(window, '__systray_minimized', True)
 
543
                self.hide_windows()
459
544
            else:
460
 
                for window in QApplication.topLevelWidgets():
461
 
                    if getattr(window, '__systray_minimized', False):
462
 
                        window.show()
463
 
                        setattr(window, '__systray_minimized', False)
464
 
 
465
 
 
466
 
    def change_output_format(self, x):
467
 
        of = unicode(x).strip()
468
 
        if of != prefs['output_format']:
469
 
            if of not in ('LRF', 'EPUB', 'MOBI'):
470
 
                warning_dialog(self, 'Warning',
471
 
                    ('<p>%s support is still in beta. If you find bugs, '
472
 
                    'please report them by opening a <a href="http://cal'
473
 
                    'ibre.kovidgoyal.net">ticket</a>.')%of).exec_()
474
 
            prefs.set('output_format', of)
475
 
 
 
545
                self.show_windows()
 
546
 
 
547
    def hide_windows(self):
 
548
        for window in QApplication.topLevelWidgets():
 
549
            if isinstance(window, (MainWindow, QDialog)) and \
 
550
                    window.isVisible():
 
551
                window.hide()
 
552
                setattr(window, '__systray_minimized', True)
 
553
 
 
554
    def show_windows(self):
 
555
        for window in QApplication.topLevelWidgets():
 
556
            if getattr(window, '__systray_minimized', False):
 
557
                window.show()
 
558
                setattr(window, '__systray_minimized', False)
476
559
 
477
560
    def test_server(self, *args):
478
561
        if self.content_server.exception is not None:
546
629
            else:
547
630
                self.cover_flow.setVisible(False)
548
631
                #self.status_bar.book_info.book_data.setMaximumHeight(1000)
549
 
            self.setMaximumHeight(available_height())
 
632
            self.resize(self.width(), self._calculated_available_height)
 
633
            #self.setMaximumHeight(available_height())
550
634
 
551
635
    def toggle_tags_view(self, show):
552
636
        if show:
561
645
            self.match_any.setVisible(False)
562
646
            self.popularity.setVisible(False)
563
647
 
 
648
    def search_done(self, view, ok):
 
649
        if view is self.current_view():
 
650
            self.search.search_done(ok)
 
651
 
564
652
    def sync_cf_to_listview(self, index, *args):
565
653
        if not hasattr(index, 'row') and \
566
654
                self.library_view.currentIndex().row() != index:
570
658
                self.cover_flow.currentSlide() != index.row():
571
659
            self.cover_flow.setCurrentSlide(index.row())
572
660
 
573
 
    def another_instance_wants_to_talk(self, msg):
 
661
    def another_instance_wants_to_talk(self):
 
662
        try:
 
663
            msg = self.listener.queue.get_nowait()
 
664
        except Empty:
 
665
            return
574
666
        if msg.startswith('launched:'):
575
667
            argv = eval(msg[len('launched:'):])
576
668
            if len(argv) > 1:
579
671
                    self.add_filesystem_book(path)
580
672
            self.setWindowState(self.windowState() & \
581
673
                    ~Qt.WindowMinimized|Qt.WindowActive)
582
 
            self.show()
 
674
            self.show_windows()
583
675
            self.raise_()
584
676
            self.activateWindow()
585
677
        elif msg.startswith('refreshdb:'):
597
689
        if idx == 1:
598
690
            return self.memory_view
599
691
        if idx == 2:
600
 
            return self.card_view
 
692
            return self.card_a_view
 
693
        if idx == 3:
 
694
            return self.card_b_view
601
695
 
602
696
    def booklists(self):
603
 
        return self.memory_view.model().db, self.card_view.model().db
 
697
        return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
604
698
 
605
699
 
606
700
 
607
701
    ########################## Connect to device ##############################
 
702
 
608
703
    def device_detected(self, connected):
609
704
        '''
610
705
        Called when a device is connected to the computer.
618
713
                self.device_manager.device.__class__.__name__+\
619
714
                        _(' detected.'), 3000)
620
715
            self.device_connected = True
621
 
            self._sync_menu.enable_device_actions(True)
 
716
            self._sync_menu.enable_device_actions(True, self.device_manager.device.card_prefix())
622
717
        else:
623
718
            self.device_connected = False
624
719
            self._sync_menu.enable_device_actions(False)
634
729
        '''
635
730
        Called once device information has been read.
636
731
        '''
637
 
        if job.exception is not None:
638
 
            self.device_job_exception(job)
639
 
            return
 
732
        if job.failed:
 
733
            return self.device_job_exception(job)
640
734
        info, cp, fs = job.result
641
735
        self.location_view.model().update_devices(cp, fs)
642
736
        self.device_info = _('Connected ')+info[0]
649
743
        '''
650
744
        Called once metadata has been read for all books on the device.
651
745
        '''
652
 
        if job.exception is not None:
 
746
        if job.failed:
653
747
            if isinstance(job.exception, ExpatError):
654
748
                error_dialog(self, _('Device database corrupted'),
655
749
                _('''
662
756
            else:
663
757
                self.device_job_exception(job)
664
758
            return
665
 
        mainlist, cardlist = job.result
 
759
        mainlist, cardalist, cardblist = job.result
666
760
        self.memory_view.set_database(mainlist)
667
761
        self.memory_view.set_editable(self.device_manager.device_class.CAN_SET_METADATA)
668
 
        self.card_view.set_database(cardlist)
669
 
        self.card_view.set_editable(self.device_manager.device_class.CAN_SET_METADATA)
670
 
        for view in (self.memory_view, self.card_view):
 
762
        self.card_a_view.set_database(cardalist)
 
763
        self.card_a_view.set_editable(self.device_manager.device_class.CAN_SET_METADATA)
 
764
        self.card_b_view.set_database(cardblist)
 
765
        self.card_b_view.set_editable(self.device_manager.device_class.CAN_SET_METADATA)
 
766
        for view in (self.memory_view, self.card_a_view, self.card_b_view):
671
767
            view.sortByColumn(3, Qt.DescendingOrder)
672
768
            if not view.restore_column_widths():
673
769
                view.resizeColumnsToContents()
685
781
                          'Select root folder')
686
782
        if not root:
687
783
            return
688
 
        from calibre.gui2.add import AddRecursive
689
 
        self._add_recursive_thread = AddRecursive(root,
690
 
                                self.library_view.model().db, self.get_metadata,
691
 
                                single, self)
692
 
        self.connect(self._add_recursive_thread, SIGNAL('finished()'),
693
 
                     self._recursive_files_added)
694
 
        self._add_recursive_thread.start()
695
 
 
696
 
    def _recursive_files_added(self):
697
 
        self._add_recursive_thread.process_duplicates()
698
 
        if self._add_recursive_thread.number_of_books_added > 0:
699
 
            self.library_view.model().resort(reset=False)
700
 
            self.library_view.model().research()
701
 
            self.library_view.model().count_changed()
702
 
        self._add_recursive_thread = None
 
784
        from calibre.gui2.add import Adder
 
785
        self._adder = Adder(self,
 
786
                self.library_view.model().db,
 
787
                Dispatcher(self._files_added), spare_server=self.spare_server)
 
788
        self._adder.add_recursive(root, single)
703
789
 
704
790
    def add_recursive_single(self, checked):
705
791
        '''
715
801
        '''
716
802
        self.add_recursive(False)
717
803
 
 
804
    def add_empty(self, checked):
 
805
        '''
 
806
        Add an empty book item to the library. This does not import any formats
 
807
        from a book file.
 
808
        '''
 
809
        from calibre.ebooks.metadata import MetaInformation
 
810
        self.library_view.model().db.import_book(MetaInformation(None), [])
 
811
        self.library_view.model().books_added(1)
 
812
 
718
813
    def files_dropped(self, paths):
719
814
        to_device = self.stack.currentIndex() != 0
720
815
        self._add_books(paths, to_device)
740
835
                        (_('LRF Books'), ['lrf']),
741
836
                        (_('HTML Books'), ['htm', 'html', 'xhtm', 'xhtml']),
742
837
                        (_('LIT Books'), ['lit']),
743
 
                        (_('MOBI Books'), ['mobi', 'prc']),
 
838
                        (_('MOBI Books'), ['mobi', 'prc', 'azw']),
744
839
                        (_('Text books'), ['txt', 'rtf']),
745
840
                        (_('PDF Books'), ['pdf']),
746
 
                        (_('Comics'), ['cbz', 'cbr']),
 
841
                        (_('Comics'), ['cbz', 'cbr', 'cbc']),
747
842
                        (_('Archives'), ['zip', 'rar']),
748
843
                        ])
749
844
        if not books:
754
849
 
755
850
    def _add_books(self, paths, to_device, on_card=None):
756
851
        if on_card is None:
757
 
            on_card = self.stack.currentIndex() == 2
 
852
            on_card = 'carda' if self.stack.currentIndex() == 2 else 'cardb' if self.stack.currentIndex() == 3 else None
758
853
        if not paths:
759
854
            return
760
 
        from calibre.gui2.add import AddFiles
761
 
        self._add_files_thread = AddFiles(paths, self.default_thumbnail,
762
 
                                          self.get_metadata,
763
 
                                          None if to_device else \
764
 
                                          self.library_view.model().db
765
 
                                          )
766
 
        self._add_files_thread.send_to_device = to_device
767
 
        self._add_files_thread.on_card = on_card
768
 
        self._add_files_thread.create_progress_dialog(_('Adding books...'),
769
 
                                                _('Reading metadata...'), self)
770
 
        self.connect(self._add_files_thread, SIGNAL('finished()'),
771
 
                     self._files_added)
772
 
        self._add_files_thread.start()
 
855
        from calibre.gui2.add import Adder
 
856
        self.__adder_func = partial(self._files_added, on_card=on_card)
 
857
        self._adder = Adder(self,
 
858
                None if to_device else self.library_view.model().db,
 
859
                Dispatcher(self.__adder_func), spare_server=self.spare_server)
 
860
        self._adder.add(paths)
773
861
 
774
 
    def _files_added(self):
775
 
        t = self._add_files_thread
776
 
        self._add_files_thread = None
777
 
        if not t.canceled:
778
 
            if t.send_to_device:
779
 
                self.upload_books(t.paths,
780
 
                                  list(map(sanitize_file_name, t.names)),
781
 
                                  t.infos, on_card=t.on_card)
782
 
                self.status_bar.showMessage(
783
 
                        _('Uploading books to device.'), 2000)
784
 
            else:
785
 
                t.process_duplicates()
786
 
        if t.number_of_books_added > 0:
787
 
            self.library_view.model().books_added(t.number_of_books_added)
 
862
    def _files_added(self, paths=[], names=[], infos=[], on_card=None):
 
863
        if paths:
 
864
            self.upload_books(paths,
 
865
                                list(map(ascii_filename, names)),
 
866
                                infos, on_card=on_card)
 
867
            self.status_bar.showMessage(
 
868
                    _('Uploading books to device.'), 2000)
 
869
        if self._adder.number_of_books_added > 0:
 
870
            self.library_view.model().books_added(self._adder.number_of_books_added)
788
871
            if hasattr(self, 'db_images'):
789
872
                self.db_images.reset()
 
873
        if self._adder.critical:
 
874
            det_msg = []
 
875
            for name, log in self._adder.critical.items():
 
876
                det_msg.append(name+'\n'+log)
 
877
            warning_dialog(self, _('Failed to read metadata'),
 
878
                    _('Failed to read metadata from the following')+':',
 
879
                    det_msg='\n\n'.join(det_msg), show=True)
 
880
 
 
881
        self._adder.cleanup()
 
882
        self._adder = None
790
883
 
791
884
 
792
885
    ############################################################################
806
899
                                   'removed from your computer. Are you sure?')
807
900
                                +'</p>', 'library_delete_books', self):
808
901
                return
 
902
            ci = view.currentIndex()
 
903
            row = None
 
904
            if ci.isValid():
 
905
                row = ci.row()
809
906
            view.model().delete_books(rows)
 
907
            if row is not None:
 
908
                ci = view.model().index(row, 0)
 
909
                if ci.isValid():
 
910
                    view.setCurrentIndex(ci)
 
911
                    sm = view.selectionModel()
 
912
                    sm.select(ci, sm.Select)
810
913
        else:
811
 
            view = self.memory_view if self.stack.currentIndex() == 1 \
812
 
                    else self.card_view
 
914
            if self.stack.currentIndex() == 1:
 
915
                view = self.memory_view
 
916
            elif self.stack.currentIndex() == 2:
 
917
                view = self.card_a_view
 
918
            else:
 
919
                view = self.card_b_view
813
920
            paths = view.model().paths(rows)
814
921
            job = self.remove_paths(paths)
815
922
            self.delete_memory[job] = (paths, view.model())
824
931
        '''
825
932
        Called once deletion is done on the device
826
933
        '''
827
 
        for view in (self.memory_view, self.card_view):
828
 
            view.model().deletion_done(job, bool(job.exception))
829
 
        if job.exception is not None:
 
934
        for view in (self.memory_view, self.card_a_view, self.card_b_view):
 
935
            view.model().deletion_done(job, job.failed)
 
936
        if job.failed:
830
937
            self.device_job_exception(job)
831
938
            return
832
939
 
875
982
            db = self.library_view.model().refresh_ids(
876
983
                x.updated, cr)
877
984
            if x.failures:
878
 
                details = ['<li><b>%s:</b> %s</li>'%(title, reason) for title,
 
985
                details = ['%s: %s'%(title, reason) for title,
879
986
                        reason in x.failures.values()]
880
 
                details = '<p><ul>%s</ul></p>'%(''.join(details))
881
 
                WarningDialog(_('Failed to download some metadata'),
 
987
                details = '%s\n'%('\n'.join(details))
 
988
                warning_dialog(self, _('Failed to download some metadata'),
882
989
                    _('Failed to download metadata for the following:'),
883
 
                    details, self).exec_()
 
990
                    det_msg=details).exec_()
884
991
        else:
885
 
            err = _('<b>Failed to download metadata:')+\
886
 
                    '</b><br><pre>'+x.tb+'</pre>'
887
 
            ConversionErrorDialog(self, _('Error'), err,
888
 
                              show=True)
889
 
 
890
 
 
 
992
            err = _('Failed to download metadata:')
 
993
            error_dialog(self, _('Error'), err, det_msg=x.tb).exec_()
891
994
 
892
995
 
893
996
    def edit_metadata(self, checked, bulk=None):
909
1012
            self.library_view.model().refresh_ids([id])
910
1013
 
911
1014
        for row in rows:
 
1015
            self._metadata_view_id = self.library_view.model().db.id(row.row())
912
1016
            d = MetadataSingleDialog(self, row.row(),
913
1017
                                    self.library_view.model().db,
914
1018
                                    accepted_callback=accepted)
 
1019
            self.connect(d, SIGNAL('view_format(PyQt_PyObject)'),
 
1020
                    self.metadata_view_format)
915
1021
            d.exec_()
916
1022
        if rows:
917
1023
            current = self.library_view.currentIndex()
938
1044
 
939
1045
    ############################## Save to disk ################################
940
1046
    def save_single_format_to_disk(self, checked):
941
 
        self.save_to_disk(checked, True, config['save_to_disk_single_format'])
 
1047
        self.save_to_disk(checked, True, prefs['output_format'])
 
1048
 
 
1049
    def save_specific_format_disk(self, fmt):
 
1050
        self.save_to_disk(False, True, fmt)
942
1051
 
943
1052
    def save_to_single_dir(self, checked):
944
1053
        self.save_to_disk(checked, True)
945
1054
 
946
1055
    def save_to_disk(self, checked, single_dir=False, single_format=None):
947
 
 
948
1056
        rows = self.current_view().selectionModel().selectedRows()
949
1057
        if not rows or len(rows) == 0:
950
 
            d = error_dialog(self, _('Cannot save to disk'),
951
 
                    _('No books selected'))
952
 
            d.exec_()
953
 
            return
954
 
 
955
 
        progress = ProgressDialog(_('Saving to disk...'), min=0, max=len(rows),
956
 
                                  parent=self)
957
 
 
958
 
        def callback(count, msg):
959
 
            progress.set_value(count)
960
 
            progress.set_msg(_('Saved')+' '+msg)
961
 
            QApplication.processEvents()
962
 
            QApplication.sendPostedEvents()
963
 
            QApplication.flush()
964
 
            return not progress.canceled
965
 
 
966
 
        dir = choose_dir(self, 'save to disk dialog',
 
1058
            return error_dialog(self, _('Cannot save to disk'),
 
1059
                    _('No books selected'), show=True)
 
1060
        path = choose_dir(self, 'save to disk dialog',
967
1061
                _('Choose destination directory'))
968
 
        if not dir:
 
1062
        if not path:
969
1063
            return
970
1064
 
971
 
        progress.show()
972
 
        QApplication.processEvents()
973
 
        QApplication.sendPostedEvents()
974
 
        QApplication.flush()
975
 
        try:
976
 
            if self.current_view() == self.library_view:
977
 
                failures = self.current_view().model().save_to_disk(rows, dir,
978
 
                                        single_dir=single_dir,
979
 
                                        callback=callback,
980
 
                                        single_format=single_format)
981
 
                if failures and single_format is not None:
982
 
                    msg = _('<p>Could not save the following books to disk, '
983
 
                       'because the %s format is not available for them:<ul>')\
984
 
                               %single_format.upper()
985
 
                    for f in failures:
986
 
                        msg += '<li>%s</li>'%f[1]
987
 
                    msg += '</ul>'
988
 
                    warning_dialog(self, _('Could not save some ebooks'),
989
 
                            msg).exec_()
990
 
                QDesktopServices.openUrl(QUrl('file:'+dir))
991
 
            else:
992
 
                paths = self.current_view().model().paths(rows)
993
 
                self.device_manager.save_books(
994
 
                        Dispatcher(self.books_saved), paths, dir)
995
 
        finally:
996
 
            progress.hide()
 
1065
        if self.current_view() is self.library_view:
 
1066
            from calibre.gui2.add import Saver
 
1067
            self._saver = Saver(self, self.library_view.model().db,
 
1068
                    Dispatcher(self._books_saved), rows, path,
 
1069
                    by_author=self.library_view.model().by_author,
 
1070
                    single_dir=single_dir,
 
1071
                    single_format=single_format,
 
1072
                    spare_server=self.spare_server)
 
1073
 
 
1074
        else:
 
1075
            paths = self.current_view().model().paths(rows)
 
1076
            self.device_manager.save_books(
 
1077
                    Dispatcher(self.books_saved), paths, path)
 
1078
 
 
1079
 
 
1080
    def _books_saved(self, path, failures, error):
 
1081
        single_format = self._saver.worker.single_format
 
1082
        self._saver = None
 
1083
        if error:
 
1084
            return error_dialog(self, _('Error while saving'),
 
1085
                    _('There was an error while saving.'),
 
1086
                    error, show=True)
 
1087
        if failures and single_format:
 
1088
            single_format = single_format.upper()
 
1089
            warning_dialog(self, _('Could not save some books'),
 
1090
            _('Could not save some books') + ', ' +
 
1091
            (_('as the %s format is not available for them.')%single_format) +
 
1092
            _('Click the show details button to see which ones.'),
 
1093
            '\n'.join(failures), show=True)
 
1094
        QDesktopServices.openUrl(QUrl.fromLocalFile(path))
997
1095
 
998
1096
    def books_saved(self, job):
999
 
        if job.exception is not None:
1000
 
            self.device_job_exception(job)
1001
 
            return
 
1097
        if job.failed:
 
1098
            return self.device_job_exception(job)
1002
1099
 
1003
1100
    ############################################################################
1004
1101
 
1016
1113
    def scheduled_recipe_fetched(self, job):
1017
1114
        temp_files, fmt, recipe, callback = self.conversion_jobs.pop(job)
1018
1115
        pt = temp_files[0]
1019
 
        if job.exception is not None:
1020
 
            self.job_exception(job)
1021
 
            return
 
1116
        if job.failed:
 
1117
            return self.job_exception(job)
1022
1118
        id = self.library_view.model().add_news(pt.name, recipe)
1023
1119
        self.library_view.model().reset()
1024
1120
        sync = dynamic.get('news_to_be_synced', set([]))
1033
1129
 
1034
1130
    ############################### Convert ####################################
1035
1131
 
 
1132
    def auto_convert(self, book_ids, on_card, format):
 
1133
        previous = self.library_view.currentIndex()
 
1134
        rows = [x.row() for x in \
 
1135
                self.library_view.selectionModel().selectedRows()]
 
1136
        jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format)
 
1137
        if jobs == []: return
 
1138
        for func, args, desc, fmt, id, temp_files in jobs:
 
1139
            if id not in bad:
 
1140
                job = self.job_manager.run_job(Dispatcher(self.book_auto_converted),
 
1141
                                        func, args=args, description=desc)
 
1142
                self.conversion_jobs[job] = (temp_files, fmt, id, on_card)
 
1143
 
 
1144
        if changed:
 
1145
            self.library_view.model().refresh_rows(rows)
 
1146
            current = self.library_view.currentIndex()
 
1147
            self.library_view.model().current_changed(current, previous)
 
1148
 
 
1149
    def auto_convert_mail(self, to, fmts, delete_from_library, book_ids, format):
 
1150
        previous = self.library_view.currentIndex()
 
1151
        rows = [x.row() for x in \
 
1152
                self.library_view.selectionModel().selectedRows()]
 
1153
        jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format)
 
1154
        if jobs == []: return
 
1155
        for func, args, desc, fmt, id, temp_files in jobs:
 
1156
            if id not in bad:
 
1157
                job = self.job_manager.run_job(Dispatcher(self.book_auto_converted_mail),
 
1158
                                        func, args=args, description=desc)
 
1159
                self.conversion_jobs[job] = (temp_files, fmt, id,
 
1160
                        delete_from_library, to, fmts)
 
1161
 
 
1162
        if changed:
 
1163
            self.library_view.model().refresh_rows(rows)
 
1164
            current = self.library_view.currentIndex()
 
1165
            self.library_view.model().current_changed(current, previous)
 
1166
 
 
1167
    def auto_convert_news(self, book_ids, format):
 
1168
        previous = self.library_view.currentIndex()
 
1169
        rows = [x.row() for x in \
 
1170
                self.library_view.selectionModel().selectedRows()]
 
1171
        jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format)
 
1172
        if jobs == []: return
 
1173
        for func, args, desc, fmt, id, temp_files in jobs:
 
1174
            if id not in bad:
 
1175
                job = self.job_manager.run_job(Dispatcher(self.book_auto_converted_news),
 
1176
                                        func, args=args, description=desc)
 
1177
                self.conversion_jobs[job] = (temp_files, fmt, id)
 
1178
 
 
1179
        if changed:
 
1180
            self.library_view.model().refresh_rows(rows)
 
1181
            current = self.library_view.currentIndex()
 
1182
            self.library_view.model().current_changed(current, previous)
 
1183
 
 
1184
 
1036
1185
    def get_books_for_conversion(self):
1037
1186
        rows = [r.row() for r in \
1038
1187
                self.library_view.selectionModel().selectedRows()]
1040
1189
            d = error_dialog(self, _('Cannot convert'),
1041
1190
                    _('No books selected'))
1042
1191
            d.exec_()
1043
 
            return [], []
1044
 
        comics, others = [], []
1045
 
        db = self.library_view.model().db
1046
 
        for r in rows:
1047
 
            formats = db.formats(r)
1048
 
            if not formats: continue
1049
 
            formats = formats.lower().split(',')
1050
 
            if 'cbr' in formats or 'cbz' in formats:
1051
 
                comics.append(r)
1052
 
            else:
1053
 
                others.append(r)
1054
 
        return comics, others
1055
 
 
1056
 
 
1057
 
    def convert_bulk(self, checked):
1058
 
        r = self.get_books_for_conversion()
1059
 
        if r is None:
1060
 
            return
1061
 
        comics, others = r
1062
 
 
1063
 
        res  = convert_bulk_ebooks(self,
1064
 
                self.library_view.model().db, comics, others)
1065
 
        if res is None:
1066
 
            return
1067
 
        jobs, changed = res
1068
 
        for func, args, desc, fmt, id, temp_files in jobs:
1069
 
            job = self.job_manager.run_job(Dispatcher(self.book_converted),
1070
 
                                            func, args=args, description=desc)
1071
 
            self.conversion_jobs[job] = (temp_files, fmt, id)
1072
 
 
1073
 
        if changed:
1074
 
            self.library_view.model().resort(reset=False)
1075
 
            self.library_view.model().research()
1076
 
 
1077
 
    def set_conversion_defaults(self, checked):
1078
 
        set_conversion_defaults(False, self, self.library_view.model().db)
1079
 
 
1080
 
    def set_comic_conversion_defaults(self, checked):
1081
 
        set_conversion_defaults(True, self, self.library_view.model().db)
1082
 
 
1083
 
    def convert_single(self, checked):
1084
 
        r = self.get_books_for_conversion()
1085
 
        if r is None: return
 
1192
            return None
 
1193
        return [self.library_view.model().db.id(r) for r in rows]
 
1194
 
 
1195
    def convert_ebook(self, checked, bulk=None):
 
1196
        book_ids = self.get_books_for_conversion()
 
1197
        if book_ids is None: return
1086
1198
        previous = self.library_view.currentIndex()
1087
1199
        rows = [x.row() for x in \
1088
1200
                self.library_view.selectionModel().selectedRows()]
1089
 
        comics, others = r
1090
 
        jobs, changed = convert_single_ebook(self,
1091
 
                self.library_view.model().db, comics, others)
 
1201
        if bulk or (bulk is None and len(book_ids) > 1):
 
1202
            jobs, changed, bad = convert_bulk_ebook(self,
 
1203
                self.library_view.model().db, book_ids, out_format=prefs['output_format'])
 
1204
        else:
 
1205
            jobs, changed, bad = convert_single_ebook(self,
 
1206
                self.library_view.model().db, book_ids, out_format=prefs['output_format'])
1092
1207
        for func, args, desc, fmt, id, temp_files in jobs:
1093
 
            job = self.job_manager.run_job(Dispatcher(self.book_converted),
 
1208
            if id not in bad:
 
1209
                job = self.job_manager.run_job(Dispatcher(self.book_converted),
1094
1210
                                            func, args=args, description=desc)
1095
 
            self.conversion_jobs[job] = (temp_files, fmt, id)
 
1211
                self.conversion_jobs[job] = (temp_files, fmt, id)
1096
1212
 
1097
1213
        if changed:
1098
1214
            self.library_view.model().refresh_rows(rows)
1099
1215
            current = self.library_view.currentIndex()
1100
1216
            self.library_view.model().current_changed(current, previous)
1101
1217
 
 
1218
    def book_auto_converted(self, job):
 
1219
        temp_files, fmt, book_id, on_card = self.conversion_jobs.pop(job)
 
1220
        try:
 
1221
            if job.failed:
 
1222
                return self.job_exception(job)
 
1223
            data = open(temp_files[-1].name, 'rb')
 
1224
            self.library_view.model().db.add_format(book_id, fmt, data, index_is_id=True)
 
1225
            data.close()
 
1226
            self.status_bar.showMessage(job.description + (' completed'), 2000)
 
1227
        finally:
 
1228
            for f in temp_files:
 
1229
                try:
 
1230
                    if os.path.exists(f.name):
 
1231
                        os.remove(f.name)
 
1232
                except:
 
1233
                    pass
 
1234
        self.tags_view.recount()
 
1235
        if self.current_view() is self.library_view:
 
1236
            current = self.library_view.currentIndex()
 
1237
            self.library_view.model().current_changed(current, QModelIndex())
 
1238
 
 
1239
        self.sync_to_device(on_card, False, specific_format=fmt, send_ids=[book_id], do_auto_convert=False)
 
1240
 
 
1241
    def book_auto_converted_mail(self, job):
 
1242
        temp_files, fmt, book_id, delete_from_library, to, fmts = self.conversion_jobs.pop(job)
 
1243
        try:
 
1244
            if job.failed:
 
1245
                self.job_exception(job)
 
1246
                return
 
1247
            data = open(temp_files[-1].name, 'rb')
 
1248
            self.library_view.model().db.add_format(book_id, fmt, data, index_is_id=True)
 
1249
            data.close()
 
1250
            self.status_bar.showMessage(job.description + (' completed'), 2000)
 
1251
        finally:
 
1252
            for f in temp_files:
 
1253
                try:
 
1254
                    if os.path.exists(f.name):
 
1255
                        os.remove(f.name)
 
1256
                except:
 
1257
                    pass
 
1258
        self.tags_view.recount()
 
1259
        if self.current_view() is self.library_view:
 
1260
            current = self.library_view.currentIndex()
 
1261
            self.library_view.model().current_changed(current, QModelIndex())
 
1262
 
 
1263
        self.send_by_mail(to, fmts, delete_from_library, specific_format=fmt, send_ids=[book_id], do_auto_convert=False)
 
1264
 
 
1265
    def book_auto_converted_news(self, job):
 
1266
        temp_files, fmt, book_id = self.conversion_jobs.pop(job)
 
1267
        try:
 
1268
            if job.failed:
 
1269
                return self.job_exception(job)
 
1270
            data = open(temp_files[-1].name, 'rb')
 
1271
            self.library_view.model().db.add_format(book_id, fmt, data, index_is_id=True)
 
1272
            data.close()
 
1273
            self.status_bar.showMessage(job.description + (' completed'), 2000)
 
1274
        finally:
 
1275
            for f in temp_files:
 
1276
                try:
 
1277
                    if os.path.exists(f.name):
 
1278
                        os.remove(f.name)
 
1279
                except:
 
1280
                    pass
 
1281
        self.tags_view.recount()
 
1282
        if self.current_view() is self.library_view:
 
1283
            current = self.library_view.currentIndex()
 
1284
            self.library_view.model().current_changed(current, QModelIndex())
 
1285
 
 
1286
        self.sync_news(send_ids=[book_id], do_auto_convert=False)
 
1287
 
1102
1288
    def book_converted(self, job):
1103
1289
        temp_files, fmt, book_id = self.conversion_jobs.pop(job)
1104
1290
        try:
1105
 
            if job.exception is not None:
 
1291
            if job.failed:
1106
1292
                self.job_exception(job)
1107
1293
                return
1108
1294
            data = open(temp_files[-1].name, 'rb')
1130
1316
        if fmt_path:
1131
1317
            self._view_file(fmt_path)
1132
1318
 
 
1319
    def metadata_view_format(self, fmt):
 
1320
        fmt_path = self.library_view.model().db.\
 
1321
                format_abspath(self._metadata_view_id,
 
1322
                        fmt, index_is_id=True)
 
1323
        if fmt_path:
 
1324
            self._view_file(fmt_path)
 
1325
 
 
1326
 
1133
1327
    def book_downloaded_for_viewing(self, job):
1134
 
        if job.exception:
 
1328
        if job.failed:
1135
1329
            self.device_job_exception(job)
1136
1330
            return
1137
1331
        self._view_file(job.result)
1145
1339
                    args.append('--raise-window')
1146
1340
                if name is not None:
1147
1341
                    args.append(name)
1148
 
                self.job_manager.server.run_free_job(viewer,
1149
 
                        kwdargs=dict(args=args))
 
1342
                self.job_manager.launch_gui_app(viewer,
 
1343
                        kwargs=dict(args=args))
1150
1344
            else:
1151
1345
                QDesktopServices.openUrl(QUrl.fromLocalFile(name))#launch(name)
1152
 
 
1153
 
            time.sleep(5) # User feedback
 
1346
                time.sleep(2) # User feedback
1154
1347
        finally:
1155
1348
            self.unsetCursor()
1156
1349
 
1192
1385
 
1193
1386
    def view_book(self, triggered):
1194
1387
        rows = self.current_view().selectionModel().selectedRows()
1195
 
        if self.current_view() is self.library_view:
1196
 
            if not rows or len(rows) == 0:
1197
 
                self._launch_viewer()
1198
 
                return
1199
 
 
1200
 
            row = rows[0].row()
1201
 
            formats = self.library_view.model().db.formats(row).upper()
1202
 
            formats = formats.split(',')
1203
 
            title   = self.library_view.model().db.title(row)
1204
 
            id      = self.library_view.model().db.id(row)
1205
 
            format = None
1206
 
            if len(formats) == 1:
1207
 
                format = formats[0]
1208
 
            if 'LRF' in formats:
1209
 
                format = 'LRF'
1210
 
            if 'EPUB' in formats:
1211
 
                format = 'EPUB'
1212
 
            if 'MOBI' in formats:
1213
 
                format = 'MOBI'
1214
 
            if not formats:
1215
 
                d = error_dialog(self, _('Cannot view'),
1216
 
                        _('%s has no available formats.')%(title,))
1217
 
                d.exec_()
1218
 
                return
1219
 
            if format is None:
1220
 
                d = ChooseFormatDialog(self, _('Choose the format to view'),
1221
 
                        formats)
1222
 
                d.exec_()
1223
 
                if d.result() == QDialog.Accepted:
1224
 
                    format = d.format()
1225
 
                else:
 
1388
 
 
1389
        if not rows or len(rows) == 0:
 
1390
            self._launch_viewer()
 
1391
            return
 
1392
 
 
1393
        if len(rows) >= 3:
 
1394
            if not question_dialog(self, _('Multiple Books Selected'),
 
1395
                _('You are attempting to open %d books. Opening too many '
 
1396
                'books at once can be slow and have a negative effect on the '
 
1397
                'responsiveness of your computer. Once started the process '
 
1398
                'cannot be stopped until complete. Do you wish to continue?'
 
1399
                )% len(rows)):
1226
1400
                    return
1227
1401
 
1228
 
            self.view_format(row, format)
 
1402
        if self.current_view() is self.library_view:
 
1403
            for row in rows:
 
1404
                row = row.row()
 
1405
 
 
1406
                formats = self.library_view.model().db.formats(row).upper()
 
1407
                formats = formats.split(',')
 
1408
                title   = self.library_view.model().db.title(row)
 
1409
 
 
1410
                if not formats:
 
1411
                    error_dialog(self, _('Cannot view'),
 
1412
                        _('%s has no available formats.')%(title,), show=True)
 
1413
                    continue
 
1414
 
 
1415
                in_prefs = False
 
1416
                for format in prefs['input_format_order']:
 
1417
                    if format in formats:
 
1418
                        in_prefs = True
 
1419
                        self.view_format(row, format)
 
1420
                        break
 
1421
                if not in_prefs:
 
1422
                    self.view_format(row, formats[0])
1229
1423
        else:
1230
1424
            paths = self.current_view().model().paths(rows)
1231
 
            if paths:
 
1425
            for path in paths:
1232
1426
                pt = PersistentTemporaryFile('_viewer_'+\
1233
 
                        os.path.splitext(paths[0])[1])
 
1427
                        os.path.splitext(path)[1])
1234
1428
                self.persistent_files.append(pt)
1235
1429
                pt.close()
1236
1430
                self.device_manager.view_book(\
1237
1431
                        Dispatcher(self.book_downloaded_for_viewing),
1238
 
                                              paths[0], pt.name)
1239
 
 
 
1432
                                              path, pt.name)
1240
1433
 
1241
1434
 
1242
1435
    ############################################################################
1264
1457
        self.content_server = d.server
1265
1458
        if d.result() == d.Accepted:
1266
1459
            self.tool_bar.setIconSize(config['toolbar_icon_size'])
 
1460
            self.search.search_as_you_type(config['search_as_you_type'])
1267
1461
            self.tool_bar.setToolButtonStyle(
1268
1462
                    Qt.ToolButtonTextUnderIcon if \
1269
1463
                            config['show_text_in_toolbar'] else \
1270
1464
                            Qt.ToolButtonIconOnly)
1271
1465
            self.save_menu.actions()[2].setText(
1272
 
                _('Save only %s format to disk')%config.get(
1273
 
                    'save_to_disk_single_format').upper())
1274
 
            if self.library_path != d.database_location:
1275
 
                try:
1276
 
                    newloc = d.database_location
1277
 
                    if not os.path.exists(os.path.join(newloc, 'metadata.db')):
1278
 
                        if os.access(self.library_path, os.R_OK):
1279
 
                            pd = QProgressDialog('', '', 0, 100, self)
1280
 
                            pd.setWindowModality(Qt.ApplicationModal)
1281
 
                            pd.setCancelButton(None)
1282
 
                            pd.setWindowTitle(_('Copying database'))
1283
 
                            pd.show()
1284
 
                            self.status_bar.showMessage(
1285
 
                                    _('Copying library to ')+newloc)
1286
 
                            self.setCursor(Qt.BusyCursor)
1287
 
                            self.library_view.setEnabled(False)
1288
 
                            self.library_view.model().db.move_library_to(
1289
 
                                    newloc, pd)
1290
 
                    else:
1291
 
                        try:
1292
 
                            db = LibraryDatabase2(newloc)
1293
 
                            self.library_view.set_database(db)
1294
 
                        except Exception, err:
1295
 
                            traceback.print_exc()
1296
 
                            d = error_dialog(self, _('Invalid database'),
1297
 
                                _('<p>An invalid database already exists at '
1298
 
                                  '%s, delete it before trying to move the '
1299
 
                                  'existing database.<br>Error: %s')%(newloc,
1300
 
                                      str(err)))
1301
 
                            d.exec_()
1302
 
                    self.library_path = \
1303
 
                            self.library_view.model().db.library_path
1304
 
                    prefs['library_path'] =  self.library_path
1305
 
                except Exception, err:
1306
 
                    traceback.print_exc()
1307
 
                    d = error_dialog(self, _('Could not move database'),
1308
 
                            unicode(err))
1309
 
                    d.exec_()
1310
 
                finally:
1311
 
                    self.unsetCursor()
1312
 
                    self.library_view.setEnabled(True)
1313
 
                    self.status_bar.clearMessage()
1314
 
                    self.search.clear_to_help()
1315
 
                    self.status_bar.reset_info()
1316
 
                    self.library_view.sortByColumn(3, Qt.DescendingOrder)
1317
 
                    self.library_view.resizeRowsToContents()
 
1466
                _('Save only %s format to disk')%
 
1467
                prefs['output_format'].upper())
1318
1468
            if hasattr(d, 'directories'):
1319
1469
                set_sidebar_directories(d.directories)
1320
1470
            self.library_view.model().read_config()
1321
1471
            self.create_device_menu()
1322
1472
 
1323
1473
 
 
1474
            if not patheq(self.library_path, d.database_location):
 
1475
                newloc = d.database_location
 
1476
                move_library(self.library_path, newloc, self,
 
1477
                        self.library_moved)
 
1478
 
 
1479
 
 
1480
    def library_moved(self, newloc):
 
1481
        if newloc is None: return
 
1482
        db = LibraryDatabase2(newloc)
 
1483
        self.library_view.set_database(db)
 
1484
        self.status_bar.clearMessage()
 
1485
        self.search.clear_to_help()
 
1486
        self.status_bar.reset_info()
 
1487
        self.library_view.sortByColumn(3, Qt.DescendingOrder)
 
1488
 
1324
1489
    ############################################################################
1325
1490
 
1326
1491
    ################################ Book info #################################
1342
1507
        '''
1343
1508
        Called when a location icon is clicked (e.g. Library)
1344
1509
        '''
1345
 
        page = 0 if location == 'library' else 1 if location == 'main' else 2
 
1510
        page = 0 if location == 'library' else 1 if location == 'main' else 2 if location == 'carda' else 3
1346
1511
        self.stack.setCurrentIndex(page)
1347
1512
        view = self.memory_view if page == 1 else \
1348
 
                self.card_view if page == 2 else None
 
1513
                self.card_a_view if page == 2 else \
 
1514
                self.card_b_view if page == 3 else None
1349
1515
        if view:
1350
1516
            if view.resize_on_select:
1351
1517
                view.resizeRowsToContents()
1374
1540
        '''
1375
1541
        try:
1376
1542
            if 'Could not read 32 bytes on the control bus.' in \
1377
 
                    unicode(job.exception):
 
1543
                    unicode(job.details):
1378
1544
                error_dialog(self, _('Error talking to device'),
1379
1545
                             _('There was a temporary error talking to the '
1380
1546
                             'device. Please unplug and reconnect the device '
1383
1549
        except:
1384
1550
            pass
1385
1551
        try:
1386
 
            print >>sys.stderr, job.console_text()
 
1552
            prints(job.details, file=sys.stderr)
1387
1553
        except:
1388
1554
            pass
1389
1555
        if not self.device_error_dialog.isVisible():
1390
 
            self.device_error_dialog.set_message(job.gui_text())
 
1556
            self.device_error_dialog.setDetailedText(job.details)
1391
1557
            self.device_error_dialog.show()
1392
1558
 
1393
1559
    def job_exception(self, job):
1394
1560
        try:
1395
 
            if job.exception[0] == 'DRMError':
 
1561
            if 'calibre.ebooks.DRMError' in job.details:
1396
1562
                error_dialog(self, _('Conversion Error'),
1397
1563
                    _('<p>Could not convert: %s<p>It is a '
1398
1564
                      '<a href="%s">DRM</a>ed book. You must first remove the '
1402
1568
                return
1403
1569
        except:
1404
1570
            pass
1405
 
        only_msg = getattr(job.exception, 'only_msg', False)
 
1571
        if job.killed:
 
1572
            return
1406
1573
        try:
1407
 
            print job.console_text()
 
1574
            prints(job.details, file=sys.stderr)
1408
1575
        except:
1409
1576
            pass
1410
 
        if only_msg:
1411
 
            try:
1412
 
                exc = unicode(job.exception)
1413
 
            except:
1414
 
                exc = repr(job.exception)
1415
 
            error_dialog(self, _('Conversion Error'), exc).exec_()
1416
 
            return
1417
 
        if isinstance(job.exception, JobKilled):
1418
 
            return
1419
 
        ConversionErrorDialog(self, _('Conversion Error'), job.gui_text(),
1420
 
                              show=True)
 
1577
        error_dialog(self, _('Conversion Error'),
 
1578
                _('<b>Failed</b>')+': '+unicode(job.description),
 
1579
                det_msg=job.details).exec_()
1421
1580
 
1422
1581
 
1423
1582
    def initialize_database(self):
1518
1677
            if self.job_manager.has_device_jobs():
1519
1678
                msg = '<p>'+__appname__ + \
1520
1679
                      _(''' is communicating with the device!<br>
1521
 
                      'Quitting may cause corruption on the device.<br>
1522
 
                      'Are you sure you want to quit?''')+'</p>'
 
1680
                      Quitting may cause corruption on the device.<br>
 
1681
                      Are you sure you want to quit?''')+'</p>'
1523
1682
 
1524
1683
            d = QMessageBox(QMessageBox.Warning, _('WARNING: Active jobs'), msg,
1525
1684
                            QMessageBox.Yes|QMessageBox.No, self)
1533
1692
    def shutdown(self, write_settings=True):
1534
1693
        if write_settings:
1535
1694
            self.write_settings()
1536
 
        self.job_manager.terminate_all_jobs()
 
1695
        self.check_messages_timer.stop()
 
1696
        self.listener.close()
 
1697
        self.job_manager.server.close()
 
1698
        while self.spare_servers:
 
1699
            self.spare_servers.pop().close()
1537
1700
        self.device_manager.keep_going = False
1538
1701
        self.cover_cache.stop()
1539
 
        self.hide()
 
1702
        self.hide_windows()
1540
1703
        self.cover_cache.terminate()
1541
1704
        self.emailer.stop()
1542
1705
        try:
1548
1711
            time.sleep(2)
1549
1712
        except KeyboardInterrupt:
1550
1713
            pass
1551
 
        self.hide()
 
1714
        self.hide_windows()
1552
1715
        return True
1553
1716
 
 
1717
    def run_wizard(self, *args):
 
1718
        if self.confirm_quit():
 
1719
            self.run_wizard_b4_shutdown = True
 
1720
            self.restart_after_quit = True
 
1721
            try:
 
1722
                self.shutdown(write_settings=False)
 
1723
            except:
 
1724
                pass
 
1725
            QApplication.instance().quit()
 
1726
 
 
1727
 
1554
1728
 
1555
1729
    def closeEvent(self, e):
1556
1730
        self.write_settings()
1561
1735
                        'choose <b>Quit</b> in the context menu of the '
1562
1736
                        'system tray.')).exec_()
1563
1737
                dynamic['systray_msg'] = True
1564
 
            self.hide()
 
1738
            self.hide_windows()
1565
1739
            e.ignore()
1566
1740
        else:
1567
1741
            if self.confirm_quit():
1576
1750
    def update_found(self, version):
1577
1751
        os = 'windows' if iswindows else 'osx' if isosx else 'linux'
1578
1752
        url = 'http://%s.kovidgoyal.net/download_%s'%(__appname__, os)
1579
 
        self.latest_version = _('<span style="color:red; font-weight:bold">'
 
1753
        self.latest_version = '<br>' + _('<span style="color:red; font-weight:bold">'
1580
1754
                'Latest version: <a href="%s">%s</a></span>')%(url, version)
1581
1755
        self.vanity.setText(self.vanity_template%\
1582
1756
                (dict(version=self.latest_version,
1584
1758
        self.vanity.update()
1585
1759
        if config.get('new_version_notification') and \
1586
1760
                dynamic.get('update to version %s'%version, True):
1587
 
            d = question_dialog(self, _('Update available'),
 
1761
            if question_dialog(self, _('Update available'),
1588
1762
                    _('%s has been updated to version %s. '
1589
1763
                    'See the <a href="http://calibre.kovidgoyal.net/wiki/'
1590
1764
                    'Changelog">new features</a>. Visit the download pa'
1591
 
                    'ge?')%(__appname__, version))
1592
 
            if d.exec_() == QMessageBox.Yes:
 
1765
                    'ge?')%(__appname__, version)):
1593
1766
                url = 'http://calibre.kovidgoyal.net/download_'+\
1594
1767
                    ('windows' if iswindows else 'osx' if isosx else 'linux')
1595
1768
                QDesktopServices.openUrl(QUrl(url))
1611
1784
                      help=_('Log debugging information to console'))
1612
1785
    return parser
1613
1786
 
 
1787
def init_qt(args):
 
1788
    parser = option_parser()
 
1789
    opts, args = parser.parse_args(args)
 
1790
    if opts.with_library is not None and os.path.isdir(opts.with_library):
 
1791
        prefs.set('library_path', opts.with_library)
 
1792
        print 'Using library at', prefs['library_path']
 
1793
    app = Application(args)
 
1794
    actions = tuple(Main.create_application_menubar())
 
1795
    app.setWindowIcon(QIcon(':/library'))
 
1796
    QCoreApplication.setOrganizationName(ORG_NAME)
 
1797
    QCoreApplication.setApplicationName(APP_UID)
 
1798
    return app, opts, args, actions
 
1799
 
 
1800
def run_gui(opts, args, actions, listener, app):
 
1801
    initialize_file_icon_provider()
 
1802
    if not dynamic.get('welcome_wizard_was_run', False):
 
1803
        from calibre.gui2.wizard import wizard
 
1804
        wizard().exec_()
 
1805
        dynamic.set('welcome_wizard_was_run', True)
 
1806
    main = Main(listener, opts, actions)
 
1807
    sys.excepthook = main.unhandled_exception
 
1808
    if len(args) > 1:
 
1809
        args[1] = os.path.abspath(args[1])
 
1810
        main.add_filesystem_book(args[1])
 
1811
    ret = app.exec_()
 
1812
    if getattr(main, 'run_wizard_b4_shutdown', False):
 
1813
        from calibre.gui2.wizard import wizard
 
1814
        wizard().exec_()
 
1815
    if getattr(main, 'restart_after_quit', False):
 
1816
        e = sys.executable if getattr(sys, 'froze', False) else sys.argv[0]
 
1817
        print 'Restarting with:', e, sys.argv
 
1818
        if hasattr(sys, 'frameworks_dir'):
 
1819
            app = os.path.dirname(os.path.dirname(sys.frameworks_dir))
 
1820
            import subprocess
 
1821
            subprocess.Popen('sleep 3s; open '+app, shell=True)
 
1822
        else:
 
1823
            os.execvp(e, sys.argv)
 
1824
    else:
 
1825
        if iswindows:
 
1826
            try:
 
1827
                main.system_tray_icon.hide()
 
1828
            except:
 
1829
                pass
 
1830
    return ret
 
1831
 
 
1832
def cant_start(msg=_('If you are sure it is not running')+', ',
 
1833
        what=None):
 
1834
    d = QMessageBox(QMessageBox.Critical, _('Cannot Start ')+__appname__,
 
1835
        '<p>'+(_('%s is already running.')%__appname__)+'</p>',
 
1836
        QMessageBox.Ok)
 
1837
    base = '<p>%s</p><p>%s %s'
 
1838
    where = __appname__ + ' '+_('may be running in the system tray, in the')+' '
 
1839
    if isosx:
 
1840
        where += _('upper right region of the screen.')
 
1841
    else:
 
1842
        where += _('lower right region of the screen.')
 
1843
    if what is None:
 
1844
        if iswindows:
 
1845
            what = _('try rebooting your computer.')
 
1846
        else:
 
1847
            what = _('try deleting the file')+': '+ADDRESS
 
1848
 
 
1849
    d.setInformativeText(base%(where, msg, what))
 
1850
    d.exec_()
 
1851
    raise SystemExit(1)
 
1852
 
 
1853
class RC(Thread):
 
1854
 
 
1855
    def run(self):
 
1856
        from multiprocessing.connection import Client
 
1857
        self.done = False
 
1858
        self.conn = Client(ADDRESS)
 
1859
        self.done = True
 
1860
 
 
1861
def communicate(args):
 
1862
    t = RC()
 
1863
    t.start()
 
1864
    time.sleep(3)
 
1865
    if not t.done:
 
1866
        f = os.path.expanduser('~/.calibre_calibre GUI.lock')
 
1867
        cant_start(what=_('try deleting the file')+': '+f)
 
1868
        raise SystemExit(1)
 
1869
 
 
1870
    if len(args) > 1:
 
1871
        args[1] = os.path.abspath(args[1])
 
1872
    t.conn.send('launched:'+repr(args))
 
1873
    t.conn.close()
 
1874
    raise SystemExit(0)
 
1875
 
 
1876
 
1614
1877
def main(args=sys.argv):
 
1878
    app, opts, args, actions = init_qt(args)
1615
1879
    from calibre.utils.lock import singleinstance
1616
 
 
1617
 
    pid = os.fork() if False and islinux else -1
1618
 
    if pid <= 0:
1619
 
        parser = option_parser()
1620
 
        opts, args = parser.parse_args(args)
1621
 
        if opts.with_library is not None and os.path.isdir(opts.with_library):
1622
 
            prefs.set('library_path', opts.with_library)
1623
 
            print 'Using library at', prefs['library_path']
1624
 
        app = Application(args)
1625
 
        actions = tuple(Main.create_application_menubar())
1626
 
        app.setWindowIcon(QIcon(':/library'))
1627
 
        QCoreApplication.setOrganizationName(ORG_NAME)
1628
 
        QCoreApplication.setApplicationName(APP_UID)
1629
 
        single_instance = None if SingleApplication is None else \
1630
 
                                  SingleApplication('calibre GUI')
1631
 
        if not singleinstance('calibre GUI'):
1632
 
            if len(args) > 1:
1633
 
                args[1] = os.path.abspath(args[1])
1634
 
            if single_instance is not None and \
1635
 
               single_instance.is_running() and \
1636
 
               single_instance.send_message('launched:'+repr(args)):
1637
 
                return 0
1638
 
            extra = '' if iswindows else \
1639
 
                    ('If you\'re sure it is not running, delete the file '
1640
 
                    '%s.'%os.path.expanduser('~/.calibre_calibre GUI.lock'))
1641
 
            QMessageBox.critical(None, _('Cannot Start ')+__appname__,
1642
 
                        _('<p>%s is already running. %s</p>')%(__appname__, extra))
1643
 
            return 1
1644
 
        initialize_file_icon_provider()
1645
 
        main = Main(single_instance, opts, actions)
1646
 
        sys.excepthook = main.unhandled_exception
1647
 
        if len(args) > 1:
1648
 
            main.add_filesystem_book(args[1])
1649
 
        ret = app.exec_()
1650
 
        if getattr(main, 'restart_after_quit', False):
1651
 
            e = sys.executable if getattr(sys, 'froze', False) else sys.argv[0]
1652
 
            print 'Restarting with:', e, sys.argv
1653
 
            os.execvp(e, sys.argv)
 
1880
    from multiprocessing.connection import Listener
 
1881
    si = singleinstance('calibre GUI')
 
1882
    if si:
 
1883
        try:
 
1884
            listener = Listener(address=ADDRESS)
 
1885
        except socket.error:
 
1886
            if iswindows:
 
1887
                cant_start()
 
1888
            os.remove(ADDRESS)
 
1889
            try:
 
1890
                listener = Listener(address=ADDRESS)
 
1891
            except socket.error:
 
1892
                cant_start()
 
1893
            else:
 
1894
                return run_gui(opts, args, actions, listener, app)
1654
1895
        else:
1655
 
            if iswindows:
1656
 
                try:
1657
 
                    main.system_tray_icon.hide()
1658
 
                except:
1659
 
                    pass
1660
 
            return ret
 
1896
            return run_gui(opts, args, actions, listener, app)
 
1897
    otherinstance = False
 
1898
    try:
 
1899
        listener = Listener(address=ADDRESS)
 
1900
    except socket.error: # Good si is correct (on UNIX)
 
1901
        otherinstance = True
 
1902
    else:
 
1903
        # On windows only singleinstance can be trusted
 
1904
        otherinstance = True if iswindows else False
 
1905
    if not otherinstance:
 
1906
        return run_gui(opts, args, actions, listener, app)
 
1907
 
 
1908
    communicate(args)
 
1909
 
1661
1910
    return 0
1662
1911
 
1663
1912
 
1671
1920
        logfile = os.path.join(os.path.expanduser('~'), 'calibre.log')
1672
1921
        if os.path.exists(logfile):
1673
1922
            log = open(logfile).read().decode('utf-8', 'ignore')
1674
 
            d = QErrorMessage(('<b>Error:</b>%s<br><b>Traceback:</b><br>'
1675
 
                '%s<b>Log:</b><br>%s')%(unicode(err), unicode(tb), log))
1676
 
            d.exec_()
 
1923
            d = QErrorMessage()
 
1924
            d.showMessage(('<b>Error:</b>%s<br><b>Traceback:</b><br>'
 
1925
                '%s<b>Log:</b><br>%s')%(unicode(err),
 
1926
                    unicode(tb).replace('\n', '<br>'),
 
1927
                    log.replace('\n', '<br>')))
 
1928