Example #1
0
 def __init__(self, opts, parent=None, gui_debug=None):
     global _gui
     MainWindow.__init__(self,
                         opts,
                         parent=parent,
                         disable_automatic_gc=True)
     self.setWindowIcon(QApplication.instance().windowIcon())
     self.jobs_pointer = Pointer(self)
     self.proceed_requested.connect(self.do_proceed,
                                    type=Qt.QueuedConnection)
     self.proceed_question = ProceedQuestion(self)
     self.job_error_dialog = JobError(self)
     self.keyboard = Manager(self)
     _gui = self
     self.opts = opts
     self.device_connected = None
     self.gui_debug = gui_debug
     self.iactions = OrderedDict()
     # Actions
     for action in interface_actions():
         if opts.ignore_plugins and action.plugin_path is not None:
             continue
         try:
             ac = self.init_iaction(action)
         except:
             # Ignore errors in loading user supplied plugins
             import traceback
             traceback.print_exc()
             if action.plugin_path is None:
                 raise
             continue
         ac.plugin_path = action.plugin_path
         ac.interface_action_base_plugin = action
         self.add_iaction(ac)
     self.load_store_plugins()
Example #2
0
 def __init__(self, opts, parent=None, gui_debug=None):
     global _gui
     MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True)
     self.jobs_pointer = Pointer(self)
     self.proceed_requested.connect(self.do_proceed,
             type=Qt.QueuedConnection)
     self.proceed_question = ProceedQuestion(self)
     self.job_error_dialog = JobError(self)
     self.keyboard = Manager(self)
     _gui = self
     self.opts = opts
     self.device_connected = None
     self.gui_debug = gui_debug
     self.iactions = OrderedDict()
     # Actions
     for action in interface_actions():
         if opts.ignore_plugins and action.plugin_path is not None:
             continue
         try:
             ac = self.init_iaction(action)
         except:
             # Ignore errors in loading user supplied plugins
             import traceback
             traceback.print_exc()
             if action.plugin_path is None:
                 raise
             continue
         ac.plugin_path = action.plugin_path
         ac.interface_action_base_plugin = action
         self.add_iaction(ac)
     self.load_store_plugins()
Example #3
0
    def __init__(self, opts):
        MainWindow.__init__(self, opts, disable_automatic_gc=True)
        self.boss = Boss(self)
        self.setWindowTitle(self.APP_NAME)
        self.setWindowIcon(QIcon(I('tweak.png')))
        self.opts = opts
        self.path_to_ebook = None
        self.container = None
        self.current_metadata = None
        self.blocking_job = BlockingJob(self)
        self.keyboard = KeyboardManager(self, config_name='shortcuts/tweak_book')

        self.central = Central(self)
        self.setCentralWidget(self.central)

        self.create_actions()
        self.create_toolbars()
        self.create_docks()
        self.create_menubar()

        self.status_bar = self.statusBar()
        self.status_bar.addPermanentWidget(self.boss.save_manager.status_widget)
        self.status_bar.addWidget(QLabel(_('{0} {1} created by {2}').format(__appname__, get_version(), 'Kovid Goyal')))
        f = self.status_bar.font()
        f.setBold(True)
        self.status_bar.setFont(f)

        self.boss(self)
        g = QApplication.instance().desktop().availableGeometry(self)
        self.resize(g.width()-50, g.height()-50)
        self.restore_state()

        self.keyboard.finalize()
Example #4
0
    def __init__(self, opts, notify=None):
        MainWindow.__init__(self, opts, disable_automatic_gc=True)
        try:
            install_new_plugins()
        except Exception:
            import traceback
            traceback.print_exc()
        self.setWindowTitle(self.APP_NAME)
        self.boss = Boss(self, notify=notify)
        self.setWindowIcon(QIcon(I('tweak.png')))
        self.opts = opts
        self.path_to_ebook = None
        self.container = None
        self.current_metadata = None
        self.blocking_job = BlockingJob(self)
        self.keyboard = KeyboardManager(self,
                                        config_name='shortcuts/tweak_book')

        self.central = Central(self)
        self.setCentralWidget(self.central)
        self.check_book = Check(self)
        self.spell_check = SpellCheck(parent=self)
        self.toc_view = TOCViewer(self)
        self.text_search = TextSearch(self)
        self.saved_searches = SavedSearches(self)
        self.image_browser = InsertImage(self, for_browsing=True)
        self.reports = Reports(self)
        self.check_external_links = CheckExternalLinks(self)
        self.insert_char = CharSelect(self)
        self.manage_fonts = ManageFonts(self)
        self.sr_debug_output = DebugOutput(self)

        self.create_actions()
        self.create_toolbars()
        self.create_docks()
        self.create_menubar()

        self.status_bar = self.statusBar()
        self.status_bar.addPermanentWidget(
            self.boss.save_manager.status_widget)
        self.cursor_position_widget = CursorPositionWidget(self)
        self.status_bar.addPermanentWidget(self.cursor_position_widget)
        self.status_bar_default_msg = la = QLabel(
            ' ' + _('{0} {1} created by {2}').format(
                __appname__, get_version(), 'Kovid Goyal'))
        la.base_template = unicode_type(la.text())
        self.status_bar.addWidget(la)
        f = self.status_bar.font()
        f.setBold(True)
        self.status_bar.setFont(f)

        self.boss(self)
        g = QApplication.instance().desktop().availableGeometry(self)
        self.resize(g.width() - 50, g.height() - 50)

        self.restore_state()
        self.apply_settings()
Example #5
0
    def __init__(self, opts, notify=None):
        MainWindow.__init__(self, opts, disable_automatic_gc=True)
        self.setWindowTitle(self.APP_NAME)
        self.boss = Boss(self, notify=notify)
        self.setWindowIcon(QIcon(I('tweak.png')))
        self.opts = opts
        self.path_to_ebook = None
        self.container = None
        self.current_metadata = None
        self.blocking_job = BlockingJob(self)
        self.keyboard = KeyboardManager(self, config_name='shortcuts/tweak_book')

        self.central = Central(self)
        self.setCentralWidget(self.central)
        self.check_book = Check(self)
        self.spell_check = SpellCheck(parent=self)
        self.toc_view = TOCViewer(self)
        self.text_search = TextSearch(self)
        self.saved_searches = SavedSearches(self)
        self.image_browser = InsertImage(self, for_browsing=True)
        self.reports = Reports(self)
        self.check_external_links = CheckExternalLinks(self)
        self.insert_char = CharSelect(self)
        self.manage_fonts = ManageFonts(self)
        self.sr_debug_output = DebugOutput(self)

        self.create_actions()
        self.create_toolbars()
        self.create_docks()
        self.create_menubar()

        self.status_bar = self.statusBar()
        self.status_bar.addPermanentWidget(self.boss.save_manager.status_widget)
        self.cursor_position_widget = CursorPositionWidget(self)
        self.status_bar.addPermanentWidget(self.cursor_position_widget)
        self.status_bar_default_msg = la = QLabel(' ' + _('{0} {1} created by {2}').format(__appname__, get_version(), 'Kovid Goyal'))
        la.base_template = unicode(la.text())
        self.status_bar.addWidget(la)
        f = self.status_bar.font()
        f.setBold(True)
        self.status_bar.setFont(f)

        self.boss(self)
        g = QApplication.instance().desktop().availableGeometry(self)
        self.resize(g.width()-50, g.height()-50)

        self.restore_state()
        self.apply_settings()
Example #6
0
File: ui.py Project: kmshi/calibre
    def __init__(self, opts, notify=None):
        MainWindow.__init__(self, opts, disable_automatic_gc=True)
        self.boss = Boss(self, notify=notify)
        self.setWindowTitle(self.APP_NAME)
        self.setWindowIcon(QIcon(I('tweak.png')))
        self.opts = opts
        self.path_to_ebook = None
        self.container = None
        self.current_metadata = None
        self.blocking_job = BlockingJob(self)
        self.keyboard = KeyboardManager(self,
                                        config_name='shortcuts/tweak_book')

        self.central = Central(self)
        self.setCentralWidget(self.central)
        self.check_book = Check(self)
        self.toc_view = TOCViewer(self)

        self.create_actions()
        self.create_toolbars()
        self.create_docks()
        self.create_menubar()

        self.status_bar = self.statusBar()
        self.status_bar.addPermanentWidget(
            self.boss.save_manager.status_widget)
        self.cursor_position_widget = CursorPositionWidget(self)
        self.status_bar.addPermanentWidget(self.cursor_position_widget)
        self.status_bar.addWidget(
            QLabel(
                _('{0} {1} created by {2}').format(__appname__, get_version(),
                                                   'Kovid Goyal')))
        f = self.status_bar.font()
        f.setBold(True)
        self.status_bar.setFont(f)

        self.boss(self)
        g = QApplication.instance().desktop().availableGeometry(self)
        self.resize(g.width() - 50, g.height() - 50)
        self.restore_state()

        self.keyboard.finalize()
Example #7
0
    def __init__(self, opts):
        MainWindow.__init__(self, opts, disable_automatic_gc=True)
        self.boss = Boss(self)
        self.setWindowTitle(self.APP_NAME)
        self.setWindowIcon(QIcon(I('tweak.png')))
        self.opts = opts
        self.path_to_ebook = None
        self.container = None
        self.current_metadata = None
        self.blocking_job = BlockingJob(self)
        self.keyboard = KeyboardManager(parent=self, config_name='shortcuts/tweak')

        self.create_actions()
        self.create_menubar()
        self.create_toolbar()
        self.create_docks()

        self.status_bar = self.statusBar()
        self.l = QLabel('Placeholder')

        self.setCentralWidget(self.l)
        self.boss(self)

        self.keyboard.finalize()
Example #8
0
class Main(MainWindow):

    APP_NAME = _('Edit Book')
    STATE_VERSION = 0

    def __init__(self, opts, notify=None):
        MainWindow.__init__(self, opts, disable_automatic_gc=True)
        self.setWindowTitle(self.APP_NAME)
        self.boss = Boss(self, notify=notify)
        self.setWindowIcon(QIcon(I('tweak.png')))
        self.opts = opts
        self.path_to_ebook = None
        self.container = None
        self.current_metadata = None
        self.blocking_job = BlockingJob(self)
        self.keyboard = KeyboardManager(self, config_name='shortcuts/tweak_book')

        self.central = Central(self)
        self.setCentralWidget(self.central)
        self.check_book = Check(self)
        self.spell_check = SpellCheck(parent=self)
        self.toc_view = TOCViewer(self)
        self.saved_searches = SavedSearches(self)
        self.image_browser = InsertImage(self, for_browsing=True)
        self.reports = Reports(self)
        self.check_external_links = CheckExternalLinks(self)
        self.insert_char = CharSelect(self)
        self.manage_fonts = ManageFonts(self)
        self.sr_debug_output = DebugOutput(self)

        self.create_actions()
        self.create_toolbars()
        self.create_docks()
        self.create_menubar()

        self.status_bar = self.statusBar()
        self.status_bar.addPermanentWidget(self.boss.save_manager.status_widget)
        self.cursor_position_widget = CursorPositionWidget(self)
        self.status_bar.addPermanentWidget(self.cursor_position_widget)
        self.status_bar_default_msg = la = QLabel(_('{0} {1} created by {2}').format(__appname__, get_version(), 'Kovid Goyal'))
        la.base_template = unicode(la.text())
        self.status_bar.addWidget(la)
        f = self.status_bar.font()
        f.setBold(True)
        self.status_bar.setFont(f)

        self.boss(self)
        g = QApplication.instance().desktop().availableGeometry(self)
        self.resize(g.width()-50, g.height()-50)

        self.restore_state()
        self.apply_settings()

    def apply_settings(self):
        self.keyboard.finalize()
        self.setDockNestingEnabled(tprefs['nestable_dock_widgets'])
        for v, h in product(('top', 'bottom'), ('left', 'right')):
            p = 'dock_%s_%s' % (v, h)
            pref = tprefs[p] or tprefs.defaults[p]
            area = getattr(Qt, '%sDockWidgetArea' % capitalize({'vertical':h, 'horizontal':v}[pref]))
            self.setCorner(getattr(Qt, '%s%sCorner' % tuple(map(capitalize, (v, h)))), area)
        self.preview.apply_settings()
        self.live_css.apply_theme()
        for bar in (self.global_bar, self.tools_bar, self.plugins_bar):
            bar.setIconSize(QSize(tprefs['toolbar_icon_size'], tprefs['toolbar_icon_size']))

    def show_status_message(self, msg, timeout=5):
        self.status_bar.showMessage(msg, int(timeout*1000))

    def elided_text(self, text, width=300):
        return elided_text(text, font=self.font(), width=width)

    @property
    def editor_tabs(self):
        return self.central.editor_tabs

    def create_actions(self):
        group = _('Global Actions')

        def reg(icon, text, target, sid, keys, description, toolbar_allowed=False):
            if not isinstance(icon, QIcon):
                icon = QIcon(I(icon))
            ac = actions[sid] = QAction(icon, text, self) if icon else QAction(text, self)
            ac.setObjectName('action-' + sid)
            if toolbar_allowed:
                toolbar_actions[sid] = ac
            if target is not None:
                ac.triggered.connect(target)
            if isinstance(keys, type('')):
                keys = (keys,)
            self.keyboard.register_shortcut(
                sid, unicode(ac.text()).replace('&', ''), default_keys=keys, description=description, action=ac, group=group)
            self.addAction(ac)
            return ac
        def treg(icon, text, target, sid, keys, description):
            return reg(icon, text, target, sid, keys, description, toolbar_allowed=icon is not None)

        self.action_new_file = treg('document-new.png', _('&New file (images/fonts/HTML/etc.)'), self.boss.add_file,
                                   'new-file', (), _('Create a new file in the current book'))
        self.action_import_files = treg('document-import.png', _('&Import files into book'), self.boss.add_files, 'new-files', (), _('Import files into book'))
        self.action_open_book = treg('document_open.png', _('Open &book'), self.boss.open_book, 'open-book', 'Ctrl+O', _('Open a new book'))
        self.action_open_book_folder = treg('mimetypes/dir.png', _('Open &folder (unzipped EPUB) as book'), partial(self.boss.open_book, open_folder=True),
                                            'open-folder-as-book', (), _('Open a folder (unzipped EPUB) as a book'))
        # Qt does not generate shortcut overrides for cmd+arrow on os x which
        # means these shortcuts interfere with editing
        self.action_global_undo = treg('back.png', _('&Revert to before'), self.boss.do_global_undo, 'global-undo', () if isosx else 'Ctrl+Left',
                                      _('Revert book to before the last action (Undo)'))
        self.action_global_redo = treg('forward.png', _('&Revert to after'), self.boss.do_global_redo, 'global-redo', () if isosx else 'Ctrl+Right',
                                      _('Revert book state to after the next action (Redo)'))
        self.action_save = treg('save.png', _('&Save'), self.boss.save_book, 'save-book', 'Ctrl+S', _('Save book'))
        self.action_save.setEnabled(False)
        self.action_save_copy = treg('save.png', _('Save a &copy'), self.boss.save_copy, 'save-copy', 'Ctrl+Alt+S', _('Save a copy of the book'))
        self.action_quit = treg('window-close.png', _('&Quit'), self.boss.quit, 'quit', 'Ctrl+Q', _('Quit'))
        self.action_preferences = treg('config.png', _('&Preferences'), self.boss.preferences, 'preferences', 'Ctrl+P', _('Preferences'))
        self.action_new_book = treg('plus.png', _('Create &new, empty book'), self.boss.new_book, 'new-book', (), _('Create a new, empty book'))
        self.action_import_book = treg('add_book.png', _('&Import an HTML or DOCX file as a new book'),
                                      self.boss.import_book, 'import-book', (), _('Import an HTML or DOCX file as a new book'))
        self.action_quick_edit = treg('modified.png', _('&Quick open a file to edit'), self.boss.quick_open, 'quick-open', ('Ctrl+T'), _(
            'Quickly open a file from the book to edit it'))

        # Editor actions
        group = _('Editor actions')
        self.action_editor_undo = reg('edit-undo.png', _('&Undo'), self.boss.do_editor_undo, 'editor-undo', 'Ctrl+Z',
                                      _('Undo typing'))
        self.action_editor_redo = reg('edit-redo.png', _('&Redo'), self.boss.do_editor_redo, 'editor-redo', 'Ctrl+Y',
                                      _('Redo typing'))
        self.action_editor_cut = reg('edit-cut.png', _('C&ut text'), self.boss.do_editor_cut, 'editor-cut', ('Ctrl+X', 'Shift+Delete', ),
                                      _('Cut text'))
        self.action_editor_copy = reg('edit-copy.png', _('&Copy to clipboard'), self.boss.do_editor_copy, 'editor-copy', ('Ctrl+C', 'Ctrl+Insert'),
                                      _('Copy to clipboard'))
        self.action_editor_paste = reg('edit-paste.png', _('&Paste from clipboard'), self.boss.do_editor_paste, 'editor-paste', ('Ctrl+V', 'Shift+Insert', ),
                                      _('Paste from clipboard'))
        self.action_editor_cut.setEnabled(False)
        self.action_editor_copy.setEnabled(False)
        self.action_editor_undo.setEnabled(False)
        self.action_editor_redo.setEnabled(False)

        # Tool actions
        group = _('Tools')
        self.action_toc = treg('toc.png', _('&Edit Table of Contents'), self.boss.edit_toc, 'edit-toc', (), _('Edit Table of Contents'))
        self.action_inline_toc = treg('chapters.png', _('&Insert inline Table of Contents'),
                                     self.boss.insert_inline_toc, 'insert-inline-toc', (), _('Insert inline Table of Contents'))
        self.action_fix_html_current = reg('html-fix.png', _('&Fix HTML'), partial(self.boss.fix_html, True), 'fix-html-current', (),
                                           _('Fix HTML in the current file'))
        self.action_fix_html_all = treg('html-fix.png', _('&Fix HTML - all files'), partial(self.boss.fix_html, False), 'fix-html-all', (),
                                       _('Fix HTML in all files'))
        self.action_pretty_current = reg('beautify.png', _('&Beautify current file'), partial(self.boss.pretty_print, True), 'pretty-current', (),
                                           _('Beautify current file'))
        self.action_pretty_all = treg('beautify.png', _('&Beautify all files'), partial(self.boss.pretty_print, False), 'pretty-all', (),
                                       _('Beautify all files'))
        self.action_insert_char = treg('character-set.png', _('&Insert special character'), self.boss.insert_character, 'insert-character', (),
                                      _('Insert special character'))
        self.action_rationalize_folders = treg('mimetypes/dir.png', _('&Arrange into folders'), self.boss.rationalize_folders, 'rationalize-folders', (),
                                      _('Arrange into folders'))
        self.action_set_semantics = treg('tags.png', _('Set &Semantics'), self.boss.set_semantics, 'set-semantics', (),
                                        _('Set Semantics'))
        self.action_filter_css = treg('filter.png', _('&Filter style information'), self.boss.filter_css, 'filter-css', (),
                                     _('Filter style information'))
        self.action_manage_fonts = treg('font.png', _('Manage &fonts'), self.boss.manage_fonts, 'manage-fonts', (), _('Manage fonts in the book'))
        self.action_add_cover = treg('default_cover.png', _('Add &cover'), self.boss.add_cover, 'add-cover', (), _('Add a cover to the book'))
        self.action_reports = treg(
            'reports.png', _('&Reports'), self.boss.show_reports, 'show-reports', ('Ctrl+Shift+R',), _('Show a report on various aspects of the book'))
        self.action_check_external_links = treg('insert-link.png', _('Check &external links'), self.boss.check_external_links, 'check-external-links', (), _(
            'Check external links in the book'))

        def ereg(icon, text, target, sid, keys, description):
            return reg(icon, text, partial(self.boss.editor_action, target), sid, keys, description)
        register_text_editor_actions(ereg, self.palette())

        # Polish actions
        group = _('Polish Book')
        self.action_subset_fonts = treg(
            'subset-fonts.png', _('&Subset embedded fonts'), partial(
                self.boss.polish, 'subset', _('Subset fonts')), 'subset-fonts', (), _('Subset embedded fonts'))
        self.action_embed_fonts = treg(
            'embed-fonts.png', _('&Embed referenced fonts'), partial(
                self.boss.polish, 'embed', _('Embed fonts')), 'embed-fonts', (), _('Embed referenced fonts'))
        self.action_smarten_punctuation = treg(
            'smarten-punctuation.png', _('&Smarten punctuation'), partial(
                self.boss.polish, 'smarten_punctuation', _('Smarten punctuation')), 'smarten-punctuation', (), _('Smarten punctuation'))
        self.action_remove_unused_css = treg(
            'edit-clear.png', _('Remove &unused CSS rules'), partial(
                self.boss.polish, 'remove_unused_css', _('Remove unused CSS rules')), 'remove-unused-css', (), _('Remove unused CSS rules'))

        # Preview actions
        group = _('Preview')
        self.action_auto_reload_preview = reg('auto-reload.png', _('Auto reload preview'), None, 'auto-reload-preview', (), _('Auto reload preview'))
        self.action_auto_sync_preview = reg('sync-right.png', _('Sync preview position to editor position'), None, 'sync-preview-to-editor', (), _(
            'Sync preview position to editor position'))
        self.action_reload_preview = reg('view-refresh.png', _('Refresh preview'), None, 'reload-preview', ('F5',), _('Refresh preview'))
        self.action_split_in_preview = reg('auto_author_sort.png', _('Split this file'), None, 'split-in-preview', (), _(
            'Split file in the preview panel'))
        self.action_find_next_preview = reg('arrow-down.png', _('Find Next'), None, 'find-next-preview', (), _('Find next in preview'))
        self.action_find_prev_preview = reg('arrow-up.png', _('Find Previous'), None, 'find-prev-preview', (), _('Find previous in preview'))

        # Search actions
        group = _('Search')
        self.action_find = treg('search.png', _('&Find/Replace'), self.boss.show_find, 'find-replace', ('Ctrl+F',), _('Show the Find/Replace panel'))
        def sreg(name, text, action, overrides={}, keys=(), description=None, icon=None):
            return reg(icon, text, partial(self.boss.search_action_triggered, action, overrides), name, keys, description or text.replace('&', ''))
        self.action_find_next = sreg('find-next', _('Find &Next'),
                                     'find', {'direction':'down'}, ('F3', 'Ctrl+G'), _('Find next match'))
        self.action_find_previous = sreg('find-previous', _('Find &Previous'),
                                         'find', {'direction':'up'}, ('Shift+F3', 'Shift+Ctrl+G'), _('Find previous match'))
        self.action_replace = sreg('replace', _('Replace'),
                                   'replace', keys=('Ctrl+R'), description=_('Replace current match'))
        self.action_replace_next = sreg('replace-next', _('&Replace and find next'),
                                        'replace-find', {'direction':'down'}, ('Ctrl+]'), _('Replace current match and find next'))
        self.action_replace_previous = sreg('replace-previous', _('R&eplace and find previous'),
                                        'replace-find', {'direction':'up'}, ('Ctrl+['), _('Replace current match and find previous'))
        self.action_replace_all = sreg('replace-all', _('Replace &all'),
                                   'replace-all', keys=('Ctrl+A'), description=_('Replace all matches'))
        self.action_count = sreg('count-matches', _('&Count all'),
                                   'count', keys=('Ctrl+N'), description=_('Count number of matches'))
        self.action_mark = reg(None, _('&Mark selected text'), self.boss.mark_selected_text, 'mark-selected-text', ('Ctrl+Shift+M',),
                               _('Mark selected text or unmark already marked text'))
        self.action_mark.default_text = self.action_mark.text()
        self.action_go_to_line = reg(None, _('Go to &line'), self.boss.go_to_line_number, 'go-to-line-number', ('Ctrl+.',), _('Go to line number'))
        self.action_saved_searches = treg('folder_saved_search.png', _('Sa&ved searches'),
                                          self.boss.saved_searches, 'saved-searches', (), _('Show the saved searches dialog'))

        # Check Book actions
        group = _('Check Book')
        self.action_check_book = treg('debug.png', _('&Check Book'), self.boss.check_requested, 'check-book', ('F7'), _('Check book for errors'))
        self.action_spell_check_book = treg('spell-check.png', _('Check &spelling'), self.boss.spell_check_requested, 'spell-check-book', ('Alt+F7'), _(
            'Check book for spelling errors'))
        self.action_check_book_next = reg('forward.png', _('&Next error'), partial(
            self.check_book.next_error, delta=1), 'check-book-next', ('Ctrl+F7'), _('Show next error'))
        self.action_check_book_previous = reg('back.png', _('&Previous error'), partial(
            self.check_book.next_error, delta=-1), 'check-book-previous', ('Ctrl+Shift+F7'), _('Show previous error'))
        self.action_spell_check_next = reg('forward.png', _('&Next spelling mistake'),
            self.boss.next_spell_error, 'spell-next', ('F8'), _('Go to next spelling mistake'))

        # Miscellaneous actions
        group = _('Miscellaneous')
        self.action_create_checkpoint = treg(
            'marked.png', _('&Create checkpoint'), self.boss.create_checkpoint, 'create-checkpoint', (), _(
                'Create a checkpoint with the current state of the book'))
        self.action_close_current_tab = reg(
            'window-close.png', _('&Close current tab'), self.central.close_current_editor, 'close-current-tab', 'Ctrl+W', _(
                'Close the currently open tab'))
        self.action_close_all_but_current_tab = reg(
            'edit-clear.png', _('&Close other tabs'), self.central.close_all_but_current_editor, 'close-all-but-current-tab', 'Ctrl+Alt+W', _(
                'Close all tabs except the current tab'))
        self.action_help = treg(
            'help.png', _('User &Manual'), lambda : open_url(QUrl(localize_user_manual_link(
                'http://manual.calibre-ebook.com/edit.html'))), 'user-manual', 'F1', _(
                'Show User Manual'))
        self.action_browse_images = treg(
            'view-image.png', _('&Browse images in book'), self.boss.browse_images, 'browse-images', (), _(
                'Browse images in the books visually'))
        self.action_multiple_split = treg(
            'auto_author_sort.png', _('&Split at multiple locations'), self.boss.multisplit, 'multisplit', (), _(
                'Split HTML file at multiple locations'))
        self.action_compare_book = treg('diff.png', _('&Compare to another book'), self.boss.compare_book, 'compare-book', (), _(
            'Compare to another book'))
        self.action_manage_snippets = treg(
            'snippets.png', _('Manage &Snippets'), self.boss.manage_snippets, 'manage-snippets', (), _(
                'Manage user created snippets'))

        self.plugin_menu_actions = []

        create_plugin_actions(actions, toolbar_actions, self.plugin_menu_actions)

    def create_menubar(self):
        if isosx:
            p, q = self.create_application_menubar()
            q.triggered.connect(self.action_quit.trigger)
            p.triggered.connect(self.action_preferences.trigger)
        f = factory(app_id='com.calibre-ebook.EditBook-%d' % os.getpid())
        b = f.create_window_menubar(self)

        f = b.addMenu(_('&File'))
        f.addAction(self.action_new_file)
        f.addAction(self.action_import_files)
        f.addSeparator()
        f.addAction(self.action_open_book)
        f.addAction(self.action_new_book)
        f.addAction(self.action_import_book)
        f.addAction(self.action_open_book_folder)
        self.recent_books_menu = f.addMenu(_('&Recently opened books'))
        self.update_recent_books()
        f.addSeparator()
        f.addAction(self.action_save)
        f.addAction(self.action_save_copy)
        f.addSeparator()
        f.addAction(self.action_compare_book)
        f.addAction(self.action_quit)

        e = b.addMenu(_('&Edit'))
        e.addAction(self.action_global_undo)
        e.addAction(self.action_global_redo)
        e.addAction(self.action_create_checkpoint)
        e.addSeparator()
        e.addAction(self.action_editor_undo)
        e.addAction(self.action_editor_redo)
        e.addSeparator()
        e.addAction(self.action_editor_cut)
        e.addAction(self.action_editor_copy)
        e.addAction(self.action_editor_paste)
        e.addAction(self.action_insert_char)
        e.addSeparator()
        e.addAction(self.action_quick_edit)
        e.addAction(self.action_preferences)

        e = b.addMenu(_('&Tools'))
        tm = e.addMenu(_('Table of Contents'))
        tm.addAction(self.action_toc)
        tm.addAction(self.action_inline_toc)
        e.addAction(self.action_manage_fonts)
        e.addAction(self.action_embed_fonts)
        e.addAction(self.action_subset_fonts)
        e.addAction(self.action_smarten_punctuation)
        e.addAction(self.action_remove_unused_css)
        e.addAction(self.action_fix_html_all)
        e.addAction(self.action_pretty_all)
        e.addAction(self.action_rationalize_folders)
        e.addAction(self.action_add_cover)
        e.addAction(self.action_set_semantics)
        e.addAction(self.action_filter_css)
        e.addAction(self.action_spell_check_book)
        e.addAction(self.action_check_external_links)
        e.addAction(self.action_check_book)
        e.addAction(self.action_reports)

        e = b.addMenu(_('&View'))
        t = e.addMenu(_('Tool&bars'))
        e.addSeparator()
        for name in sorted(actions, key=lambda x:sort_key(actions[x].text())):
            ac = actions[name]
            if name.endswith('-dock'):
                e.addAction(ac)
            elif name.endswith('-bar'):
                t.addAction(ac)
        e.addAction(self.action_browse_images)
        e.addSeparator()
        e.addAction(self.action_close_current_tab)
        e.addAction(self.action_close_all_but_current_tab)

        e = b.addMenu(_('&Search'))
        a = e.addAction
        a(self.action_find)
        e.addSeparator()
        a(self.action_find_next)
        a(self.action_find_previous)
        e.addSeparator()
        a(self.action_replace)
        a(self.action_replace_next)
        a(self.action_replace_previous)
        a(self.action_replace_all)
        e.addSeparator()
        a(self.action_count)
        e.addSeparator()
        a(self.action_mark)
        e.addSeparator()
        a(self.action_go_to_line)
        e.addSeparator()
        a(self.action_saved_searches)
        e.aboutToShow.connect(self.search_menu_about_to_show)

        if self.plugin_menu_actions:
            e = b.addMenu(_('&Plugins'))
            for ac in sorted(self.plugin_menu_actions, key=lambda x:sort_key(unicode(x.text()))):
                e.addAction(ac)

        e = b.addMenu(_('&Help'))
        a = e.addAction
        a(self.action_help)
        a(QIcon(I('donate.png')), _('Donate to support calibre development'), open_donate)
        a(self.action_preferences)

    def search_menu_about_to_show(self):
        ed = self.central.current_editor
        update_mark_text_action(ed)

    def update_recent_books(self):
        m = self.recent_books_menu
        m.clear()
        books = tprefs.get('recent-books', [])
        for path in books:
            m.addAction(self.elided_text(path, width=500), partial(self.boss.open_book, path=path))

    def create_toolbars(self):
        def create(text, name):
            name += '-bar'
            b = self.addToolBar(text)
            b.setObjectName(name)  # Needed for saveState
            actions[name] = b.toggleViewAction()
            b.setIconSize(QSize(tprefs['toolbar_icon_size'], tprefs['toolbar_icon_size']))
            return b
        self.global_bar = create(_('Book tool bar'), 'global')
        self.tools_bar = create(_('Tools tool bar'), 'tools')
        self.plugins_bar = create(_('Plugins tool bar'), 'plugins')
        self.populate_toolbars(animate=True)

    def populate_toolbars(self, animate=False):
        self.global_bar.clear(), self.tools_bar.clear(), self.plugins_bar.clear()
        def add(bar, ac):
            if ac is None:
                bar.addSeparator()
            elif ac == 'donate':
                self.donate_button = b = ThrobbingButton(self)
                b.clicked.connect(open_donate)
                b.setAutoRaise(True)
                b.setToolTip(_('Donate to support calibre development'))
                if animate:
                    QTimer.singleShot(10, b.start_animation)
                bar.addWidget(b)
            else:
                try:
                    bar.addAction(actions[ac])
                except KeyError:
                    if DEBUG:
                        prints('Unknown action for toolbar %r: %r' % (unicode(bar.objectName()), ac))

        for x in tprefs['global_book_toolbar']:
            add(self.global_bar, x)

        for x in tprefs['global_tools_toolbar']:
            add(self.tools_bar, x)

        for x in tprefs['global_plugins_toolbar']:
            add(self.plugins_bar, x)
        self.plugins_bar.setVisible(bool(tprefs['global_plugins_toolbar']))

    def create_docks(self):

        def create(name, oname):
            oname += '-dock'
            d = QDockWidget(name, self)
            d.setObjectName(oname)  # Needed for saveState
            ac = d.toggleViewAction()
            desc = _('Toggle %s') % name.replace('&', '')
            self.keyboard.register_shortcut(
                oname, desc, description=desc, action=ac, group=_('Windows'))
            actions[oname] = ac
            setattr(self, oname.replace('-', '_'), d)
            return d

        d = create(_('Files Browser'), 'files-browser')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
        self.file_list = FileListWidget(d)
        d.setWidget(self.file_list)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)

        d = create(_('File Preview'), 'preview')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
        self.preview = Preview(d)
        d.setWidget(self.preview)
        self.addDockWidget(Qt.RightDockWidgetArea, d)

        d = create(_('Live CSS'), 'live-css')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        self.live_css = LiveCSS(self.preview, parent=d)
        d.setWidget(self.live_css)
        self.addDockWidget(Qt.RightDockWidgetArea, d)
        d.close()  # Hidden by default

        d = create(_('Check Book'), 'check-book')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        d.setWidget(self.check_book)
        self.addDockWidget(Qt.TopDockWidgetArea, d)
        d.close()  # By default the check window is closed

        d = create(_('Inspector'), 'inspector')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        d.setWidget(self.preview.inspector)
        self.preview.inspector.setParent(d)
        self.addDockWidget(Qt.BottomDockWidgetArea, d)
        d.close()  # By default the inspector window is closed
        d.setFeatures(d.DockWidgetClosable | d.DockWidgetMovable)  # QWebInspector does not work in a floating dock

        d = create(_('Table of Contents'), 'toc-viewer')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        d.setWidget(self.toc_view)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)
        d.close()  # Hidden by default

        d = create(_('Checkpoints'), 'checkpoints')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        self.checkpoints = CheckpointView(self.boss.global_undo, parent=d)
        d.setWidget(self.checkpoints)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)
        d.close()  # Hidden by default

        d = create(_('Saved Searches'), 'saved-searches')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        d.setWidget(self.saved_searches)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)
        d.close()  # Hidden by default

    def resizeEvent(self, ev):
        self.blocking_job.resize(ev.size())
        return super(Main, self).resizeEvent(ev)

    def update_window_title(self):
        fname = os.path.basename(current_container().path_to_ebook)
        self.setWindowTitle(self.current_metadata.title + ' [%s] :: %s :: %s' %(current_container().book_type.upper(), fname, self.APP_NAME))

    def closeEvent(self, e):
        if self.boss.quit():
            e.accept()
        else:
            e.ignore()

    def save_state(self):
        tprefs.set('main_window_geometry', bytearray(self.saveGeometry()))
        tprefs.set('main_window_state', bytearray(self.saveState(self.STATE_VERSION)))
        self.central.save_state()
        self.saved_searches.save_state()
        self.check_book.save_state()

    def restore_state(self):
        geom = tprefs.get('main_window_geometry', None)
        if geom is not None:
            self.restoreGeometry(geom)
        state = tprefs.get('main_window_state', None)
        if state is not None:
            self.restoreState(state, self.STATE_VERSION)
        self.central.restore_state()
        self.saved_searches.restore_state()

    def contextMenuEvent(self, ev):
        ev.ignore()
Example #9
0
class Main(MainWindow):

    APP_NAME = _('Tweak Book')
    STATE_VERSION = 0

    def __init__(self, opts):
        MainWindow.__init__(self, opts, disable_automatic_gc=True)
        self.boss = Boss(self)
        self.setWindowTitle(self.APP_NAME)
        self.setWindowIcon(QIcon(I('tweak.png')))
        self.opts = opts
        self.path_to_ebook = None
        self.container = None
        self.current_metadata = None
        self.blocking_job = BlockingJob(self)
        self.keyboard = KeyboardManager(self, config_name='shortcuts/tweak_book')

        self.central = Central(self)
        self.setCentralWidget(self.central)

        self.create_actions()
        self.create_toolbars()
        self.create_docks()
        self.create_menubar()

        self.status_bar = self.statusBar()
        self.status_bar.addPermanentWidget(self.boss.save_manager.status_widget)
        self.status_bar.addWidget(QLabel(_('{0} {1} created by {2}').format(__appname__, get_version(), 'Kovid Goyal')))
        f = self.status_bar.font()
        f.setBold(True)
        self.status_bar.setFont(f)

        self.boss(self)
        g = QApplication.instance().desktop().availableGeometry(self)
        self.resize(g.width()-50, g.height()-50)
        self.restore_state()

        self.keyboard.finalize()

    def elided_text(self, text, width=200, mode=Qt.ElideMiddle):
        return elided_text(self.font(), text, width=width, mode=mode)

    @property
    def editor_tabs(self):
        return self.central.editor_tabs

    def create_actions(self):
        group = _('Global Actions')

        def reg(icon, text, target, sid, keys, description):
            ac = actions[sid] = QAction(QIcon(I(icon)), text, self) if icon else QAction(text, self)
            ac.setObjectName('action-' + sid)
            if target is not None:
                ac.triggered.connect(target)
            if isinstance(keys, type('')):
                keys = (keys,)
            self.keyboard.register_shortcut(
                sid, unicode(ac.text()).replace('&', ''), default_keys=keys, description=description, action=ac, group=group)
            self.addAction(ac)
            return ac

        self.action_new_file = reg('document-new.png', _('&New file'), self.boss.add_file, 'new-file', (), _('Create a new file in the current book'))
        self.action_open_book = reg('document_open.png', _('Open &book'), self.boss.open_book, 'open-book', 'Ctrl+O', _('Open a new book'))
        self.action_global_undo = reg('back.png', _('&Revert to before'), self.boss.do_global_undo, 'global-undo', 'Ctrl+Left',
                                      _('Revert book to before the last action (Undo)'))
        self.action_global_redo = reg('forward.png', _('&Revert to after'), self.boss.do_global_redo, 'global-redo', 'Ctrl+Right',
                                      _('Revert book state to after the next action (Redo)'))
        self.action_save = reg('save.png', _('&Save'), self.boss.save_book, 'save-book', 'Ctrl+Shift+S', _('Save book'))
        self.action_save.setEnabled(False)
        self.action_quit = reg('quit.png', _('&Quit'), self.boss.quit, 'quit', 'Ctrl+Q', _('Quit'))
        self.action_preferences = reg('config.png', _('&Preferences'), self.boss.preferences, 'preferences', 'Ctrl+P', _('Preferences'))

        # Editor actions
        group = _('Editor actions')
        self.action_editor_undo = reg('edit-undo.png', _('&Undo'), self.boss.do_editor_undo, 'editor-undo', 'Ctrl+Z',
                                      _('Undo typing'))
        self.action_editor_redo = reg('edit-redo.png', _('&Redo'), self.boss.do_editor_redo, 'editor-redo', 'Ctrl+Y',
                                      _('Redo typing'))
        self.action_editor_save = reg('save.png', _('&Save'), self.boss.do_editor_save, 'editor-save', 'Ctrl+S',
                                      _('Save changes to the current file'))
        self.action_editor_cut = reg('edit-cut.png', _('C&ut text'), self.boss.do_editor_cut, 'editor-cut', ('Ctrl+X', 'Shift+Delete', ),
                                      _('Cut text'))
        self.action_editor_copy = reg('edit-copy.png', _('&Copy text'), self.boss.do_editor_copy, 'editor-copy', ('Ctrl+C', 'Ctrl+Insert'),
                                      _('Copy text'))
        self.action_editor_paste = reg('edit-paste.png', _('&Paste text'), self.boss.do_editor_paste, 'editor-paste', ('Ctrl+V', 'Shift+Insert', ),
                                      _('Paste text'))
        self.action_editor_cut.setEnabled(False)
        self.action_editor_copy.setEnabled(False)
        self.action_editor_undo.setEnabled(False)
        self.action_editor_redo.setEnabled(False)

        # Tool actions
        group = _('Tools')
        self.action_toc = reg('toc.png', _('&Edit Table of Contents'), self.boss.edit_toc, 'edit-toc', (), _('Edit Table of Contents'))
        self.action_fix_html_current = reg('html-fix.png', _('&Fix HTML'), partial(self.boss.fix_html, True), 'fix-html-current', (),
                                           _('Fix HTML in the current file'))
        self.action_fix_html_all = reg('html-fix.png', _('&Fix HTML - all files'), partial(self.boss.fix_html, False), 'fix-html-all', (),
                                       _('Fix HTML in all files'))
        self.action_pretty_current = reg('format-justify-fill.png', _('&Beautify current file'), partial(self.boss.pretty_print, True), 'pretty-current', (),
                                           _('Beautify current file'))
        self.action_pretty_all = reg('format-justify-fill.png', _('&Beautify all files'), partial(self.boss.pretty_print, False), 'pretty-all', (),
                                       _('Beautify all files'))

        # Polish actions
        group = _('Polish Book')
        self.action_subset_fonts = reg(
            'subset-fonts.png', _('&Subset embedded fonts'), partial(
                self.boss.polish, 'subset', _('Subset fonts')), 'subset-fonts', (), _('Subset embedded fonts'))
        self.action_embed_fonts = reg(
            'embed-fonts.png', _('&Embed referenced fonts'), partial(
                self.boss.polish, 'embed', _('Embed fonts')), 'embed-fonts', (), _('Embed referenced fonts'))
        self.action_smarten_punctuation = reg(
            'smarten-punctuation.png', _('&Smarten punctuation'), partial(
                self.boss.polish, 'smarten_punctuation', _('Smarten punctuation')), 'smarten-punctuation', (), _('Smarten punctuation'))

        # Preview actions
        group = _('Preview')
        self.action_auto_reload_preview = reg('auto-reload.png', _('Auto reload preview'), None, 'auto-reload-preview', (), _('Auto reload preview'))
        self.action_auto_sync_preview = reg('sync-right.png', _('Sync preview position to editor position'), None, 'sync-preview-to-editor', (), _(
            'Sync preview position to editor position'))
        self.action_reload_preview = reg('view-refresh.png', _('Refresh preview'), None, 'reload-preview', ('F5',), _('Refresh preview'))
        self.action_split_in_preview = reg('auto_author_sort.png', _('Split this file'), None, 'split-in-preview', (), _(
            'Split file in the preview panel'))
        self.action_find_next_preview = reg('arrow-down.png', _('Find Next'), None, 'find-next-preview', (), _('Find next in preview'))
        self.action_find_prev_preview = reg('arrow-up.png', _('Find Previous'), None, 'find-prev-preview', (), _('Find previous in preview'))

        # Search actions
        group = _('Search')
        self.action_find = reg('search.png', _('&Find/Replace'), self.boss.show_find, 'find-replace', ('Ctrl+F',), _('Show the Find/Replace panel'))
        def sreg(name, text, action, overrides={}, keys=(), description=None, icon=None):
            return reg(icon, text, partial(self.boss.search, action, overrides), name, keys, description or text.replace('&', ''))
        self.action_find_next = sreg('find-next', _('Find &Next'),
                                     'find', {'direction':'down'}, ('F3', 'Ctrl+G'), _('Find next match'))
        self.action_find_previous = sreg('find-previous', _('Find &Previous'),
                                         'find', {'direction':'up'}, ('Shift+F3', 'Shift+Ctrl+G'), _('Find previous match'))
        self.action_replace = sreg('replace', _('Replace'),
                                   'replace', keys=('Ctrl+R'), description=_('Replace current match'))
        self.action_replace_next = sreg('replace-next', _('&Replace and find next'),
                                        'replace-find', {'direction':'down'}, ('Ctrl+]'), _('Replace current match and find next'))
        self.action_replace_previous = sreg('replace-previous', _('R&eplace and find previous'),
                                        'replace-find', {'direction':'up'}, ('Ctrl+['), _('Replace current match and find previous'))
        self.action_replace_all = sreg('replace-all', _('Replace &all'),
                                   'replace-all', keys=('Ctrl+A'), description=_('Replace all matches'))
        self.action_count = sreg('count-matches', _('&Count all'),
                                   'count', keys=('Ctrl+N'), description=_('Count number of matches'))
        self.action_mark = reg(None, _('&Mark selected text'), self.boss.mark_selected_text, 'mark-selected-text', ('Ctrl+Shift+M',), _('Mark selected text'))
        self.action_go_to_line = reg(None, _('Go to &line'), self.boss.go_to_line_number, 'go-to-line-number', ('Ctrl+.',), _('Go to line number'))

        # Miscellaneous actions
        group = _('Miscellaneous')
        self.action_create_checkpoint = reg(
            'marked.png', _('&Create checkpoint'), self.boss.create_checkpoint, 'create-checkpoint', (), _(
                'Create a checkpoint with the current state of the book'))

    def create_menubar(self):
        p, q = self.create_application_menubar()
        q.triggered.connect(self.action_quit.trigger)
        p.triggered.connect(self.action_preferences.trigger)
        b = self.menuBar()

        f = b.addMenu(_('&File'))
        f.addAction(self.action_new_file)
        f.addAction(self.action_open_book)
        f.addAction(self.action_save)
        f.addAction(self.action_quit)

        e = b.addMenu(_('&Edit'))
        e.addAction(self.action_global_undo)
        e.addAction(self.action_global_redo)
        e.addAction(self.action_create_checkpoint)
        e.addSeparator()
        e.addAction(self.action_editor_undo)
        e.addAction(self.action_editor_redo)
        e.addSeparator()
        e.addAction(self.action_editor_cut)
        e.addAction(self.action_editor_copy)
        e.addAction(self.action_editor_paste)
        e.addSeparator()
        e.addAction(self.action_preferences)

        e = b.addMenu(_('&Tools'))
        e.addAction(self.action_toc)
        e.addAction(self.action_embed_fonts)
        e.addAction(self.action_subset_fonts)
        e.addAction(self.action_smarten_punctuation)
        e.addAction(self.action_fix_html_all)
        e.addAction(self.action_pretty_all)

        e = b.addMenu(_('&View'))
        t = e.addMenu(_('Tool&bars'))
        e.addSeparator()
        for name, ac in actions.iteritems():
            if name.endswith('-dock'):
                e.addAction(ac)
            elif name.endswith('-bar'):
                t.addAction(ac)

        e = b.addMenu(_('&Search'))
        a = e.addAction
        a(self.action_find)
        e.addSeparator()
        a(self.action_find_next)
        a(self.action_find_previous)
        e.addSeparator()
        a(self.action_replace)
        a(self.action_replace_next)
        a(self.action_replace_previous)
        a(self.action_replace_all)
        e.addSeparator()
        a(self.action_count)
        e.addSeparator()
        a(self.action_mark)
        e.addSeparator()
        a(self.action_go_to_line)

    def create_toolbars(self):
        def create(text, name):
            name += '-bar'
            b = self.addToolBar(text)
            b.setObjectName(name)  # Needed for saveState
            setattr(self, name.replace('-', '_'), b)
            actions[name] = b.toggleViewAction()
            return b

        a = create(_('Book tool bar'), 'global').addAction
        for x in ('new_file', 'open_book', 'global_undo', 'global_redo', 'save', 'create_checkpoint', 'toc'):
            a(getattr(self, 'action_' + x))

        a = create(_('Polish book tool bar'), 'polish').addAction
        for x in ('embed_fonts', 'subset_fonts', 'smarten_punctuation'):
            a(getattr(self, 'action_' + x))

    def create_docks(self):

        def create(name, oname):
            oname += '-dock'
            d = QDockWidget(name, self)
            d.setObjectName(oname)  # Needed for saveState
            ac = d.toggleViewAction()
            desc = _('Toggle %s') % name.replace('&', '')
            self.keyboard.register_shortcut(
                oname, desc, description=desc, action=ac, group=_('Windows'))
            actions[oname] = ac
            setattr(self, oname.replace('-', '_'), d)
            return d

        d = create(_('&Files Browser'), 'files-browser')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
        self.file_list = FileListWidget(d)
        d.setWidget(self.file_list)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)

        d = create(_('File &Preview'), 'preview')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
        self.preview = Preview(d)
        d.setWidget(self.preview)
        self.addDockWidget(Qt.RightDockWidgetArea, d)

        d = create(_('&Inspector'), 'inspector')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        d.setWidget(self.preview.inspector)
        self.preview.inspector.setParent(d)
        self.addDockWidget(Qt.BottomDockWidgetArea, d)

    def resizeEvent(self, ev):
        self.blocking_job.resize(ev.size())
        return super(Main, self).resizeEvent(ev)

    def update_window_title(self):
        self.setWindowTitle(self.current_metadata.title + ' [%s] - %s' %(current_container().book_type.upper(), self.APP_NAME))

    def closeEvent(self, e):
        if not self.boss.confirm_quit():
            e.ignore()
            return
        try:
            self.boss.shutdown()
        except:
            import traceback
            traceback.print_exc()
        e.accept()

    def save_state(self):
        tprefs.set('main_window_geometry', bytearray(self.saveGeometry()))
        tprefs.set('main_window_state', bytearray(self.saveState(self.STATE_VERSION)))
        self.central.save_state()

    def restore_state(self):
        geom = tprefs.get('main_window_geometry', None)
        if geom is not None:
            self.restoreGeometry(geom)
        state = tprefs.get('main_window_state', None)
        if state is not None:
            self.restoreState(state, self.STATE_VERSION)
        self.central.restore_state()
        # We never want to start with the inspector showing
        self.inspector_dock.close()

    def contextMenuEvent(self, ev):
        ev.ignore()
Example #10
0
class Main(MainWindow):

    APP_NAME = _('Edit book')
    STATE_VERSION = 0

    def __init__(self, opts, notify=None):
        MainWindow.__init__(self, opts, disable_automatic_gc=True)
        try:
            install_new_plugins()
        except Exception:
            import traceback
            traceback.print_exc()
        self.setWindowTitle(self.APP_NAME)
        self.boss = Boss(self, notify=notify)
        self.setWindowIcon(QIcon(I('tweak.png')))
        self.opts = opts
        self.path_to_ebook = None
        self.container = None
        self.current_metadata = None
        self.blocking_job = BlockingJob(self)
        self.keyboard = KeyboardManager(self,
                                        config_name='shortcuts/tweak_book')

        self.central = Central(self)
        self.setCentralWidget(self.central)
        self.check_book = Check(self)
        self.spell_check = SpellCheck(parent=self)
        self.toc_view = TOCViewer(self)
        self.text_search = TextSearch(self)
        self.saved_searches = SavedSearches(self)
        self.image_browser = InsertImage(self, for_browsing=True)
        self.reports = Reports(self)
        self.check_external_links = CheckExternalLinks(self)
        self.insert_char = CharSelect(self)
        self.manage_fonts = ManageFonts(self)
        self.sr_debug_output = DebugOutput(self)

        self.create_actions()
        self.create_toolbars()
        self.create_docks()
        self.create_menubar()

        self.status_bar = self.statusBar()
        self.status_bar.addPermanentWidget(
            self.boss.save_manager.status_widget)
        self.cursor_position_widget = CursorPositionWidget(self)
        self.status_bar.addPermanentWidget(self.cursor_position_widget)
        self.status_bar_default_msg = la = QLabel(
            ' ' + _('{0} {1} created by {2}').format(
                __appname__, get_version(), 'Kovid Goyal'))
        la.base_template = unicode_type(la.text())
        self.status_bar.addWidget(la)
        f = self.status_bar.font()
        f.setBold(True)
        self.status_bar.setFont(f)

        self.boss(self)
        g = QApplication.instance().desktop().availableGeometry(self)
        self.resize(g.width() - 50, g.height() - 50)

        self.restore_state()
        self.apply_settings()

    def apply_settings(self):
        self.keyboard.finalize()
        self.setDockNestingEnabled(tprefs['nestable_dock_widgets'])
        for v, h in product(('top', 'bottom'), ('left', 'right')):
            p = 'dock_%s_%s' % (v, h)
            pref = tprefs[p] or tprefs.defaults[p]
            area = getattr(
                Qt, '%sDockWidgetArea' % capitalize({
                    'vertical': h,
                    'horizontal': v
                }[pref]))
            self.setCorner(
                getattr(Qt, '%s%sCorner' % tuple(map(capitalize, (v, h)))),
                area)
        self.preview.apply_settings()
        self.live_css.apply_theme()
        for bar in (self.global_bar, self.tools_bar, self.plugins_bar):
            bar.setIconSize(
                QSize(tprefs['toolbar_icon_size'],
                      tprefs['toolbar_icon_size']))

    def show_status_message(self, msg, timeout=5):
        self.status_bar.showMessage(msg, int(timeout * 1000))

    def elided_text(self, text, width=300):
        return elided_text(text, font=self.font(), width=width)

    @property
    def editor_tabs(self):
        return self.central.editor_tabs

    def create_actions(self):
        group = _('Global actions')

        def reg(icon,
                text,
                target,
                sid,
                keys,
                description,
                toolbar_allowed=False):
            if not isinstance(icon, QIcon):
                icon = QIcon(I(icon))
            ac = actions[sid] = QAction(icon, text, self) if icon else QAction(
                text, self)
            ac.setObjectName('action-' + sid)
            if toolbar_allowed:
                toolbar_actions[sid] = ac
            if target is not None:
                ac.triggered.connect(target)
            if isinstance(keys, unicode_type):
                keys = (keys, )
            self.keyboard.register_shortcut(sid,
                                            unicode_type(ac.text()).replace(
                                                '&', ''),
                                            default_keys=keys,
                                            description=description,
                                            action=ac,
                                            group=group)
            self.addAction(ac)
            return ac

        def treg(icon, text, target, sid, keys, description):
            return reg(icon,
                       text,
                       target,
                       sid,
                       keys,
                       description,
                       toolbar_allowed=icon is not None)

        self.action_new_file = treg('document-new.png',
                                    _('&New file (images/fonts/HTML/etc.)'),
                                    self.boss.add_file, 'new-file', (),
                                    _('Create a new file in the current book'))
        self.action_import_files = treg('document-import.png',
                                        _('&Import files into book'),
                                        self.boss.add_files, 'new-files', (),
                                        _('Import files into book'))
        self.action_open_book = treg('document_open.png', _('&Open book'),
                                     self.boss.open_book, 'open-book',
                                     'Ctrl+O', _('Open a new book'))
        self.action_open_book_folder = treg(
            'mimetypes/dir.png', _('Open &folder (unzipped EPUB) as book'),
            partial(self.boss.open_book, open_folder=True),
            'open-folder-as-book', (),
            _('Open a folder (unzipped EPUB) as a book'))
        self.action_edit_next_file = treg(
            'arrow-down.png', _('Edit &next file'),
            partial(self.boss.edit_next_file, backwards=False),
            'edit-next-file', 'Ctrl+Alt+Down',
            _('Edit the next file in the spine'))
        self.action_edit_previous_file = treg(
            'arrow-up.png', _('Edit &previous file'),
            partial(self.boss.edit_next_file, backwards=True),
            'edit-previous-file', 'Ctrl+Alt+Up',
            _('Edit the previous file in the spine'))
        # Qt does not generate shortcut overrides for cmd+arrow on os x which
        # Qt does not generate shortcut overrides for cmd+arrow on os x which
        # means these shortcuts interfere with editing
        self.action_global_undo = treg(
            'back.png', _('&Revert to before'), self.boss.do_global_undo,
            'global-undo', () if isosx else 'Ctrl+Left',
            _('Revert book to before the last action (Undo)'))
        self.action_global_redo = treg(
            'forward.png', _('&Revert to after'), self.boss.do_global_redo,
            'global-redo', () if isosx else 'Ctrl+Right',
            _('Revert book state to after the next action (Redo)'))
        self.action_save = treg('save.png', _('&Save'), self.boss.save_book,
                                'save-book', 'Ctrl+S', _('Save book'))
        self.action_save.setEnabled(False)
        self.action_save_copy = treg('save.png', _('Save a &copy'),
                                     self.boss.save_copy,
                                     'save-copy', 'Ctrl+Alt+S',
                                     _('Save a copy of the book'))
        self.action_quit = treg('window-close.png', _('&Quit'), self.boss.quit,
                                'quit', 'Ctrl+Q', _('Quit'))
        self.action_preferences = treg('config.png', _('&Preferences'),
                                       self.boss.preferences, 'preferences',
                                       'Ctrl+P', _('Preferences'))
        self.action_new_book = treg('plus.png', _('Create &new, empty book'),
                                    self.boss.new_book, 'new-book', (),
                                    _('Create a new, empty book'))
        self.action_import_book = treg(
            'add_book.png', _('&Import an HTML or DOCX file as a new book'),
            self.boss.import_book, 'import-book', (),
            _('Import an HTML or DOCX file as a new book'))
        self.action_quick_edit = treg(
            'modified.png', _('&Quick open a file to edit'),
            self.boss.quick_open, 'quick-open', ('Ctrl+T'),
            _('Quickly open a file from the book to edit it'))

        # Editor actions
        group = _('Editor actions')
        self.action_editor_undo = reg('edit-undo.png', _('&Undo'),
                                      self.boss.do_editor_undo, 'editor-undo',
                                      'Ctrl+Z', _('Undo typing'))
        self.action_editor_redo = reg('edit-redo.png', _('R&edo'),
                                      self.boss.do_editor_redo, 'editor-redo',
                                      'Ctrl+Y', _('Redo typing'))
        self.action_editor_cut = reg('edit-cut.png', _('Cut &text'),
                                     self.boss.do_editor_cut, 'editor-cut', (
                                         'Ctrl+X',
                                         'Shift+Delete',
                                     ), _('Cut text'))
        self.action_editor_copy = reg('edit-copy.png', _('&Copy to clipboard'),
                                      self.boss.do_editor_copy, 'editor-copy',
                                      ('Ctrl+C', 'Ctrl+Insert'),
                                      _('Copy to clipboard'))
        self.action_editor_paste = reg('edit-paste.png',
                                       _('P&aste from clipboard'),
                                       self.boss.do_editor_paste,
                                       'editor-paste', (
                                           'Ctrl+V',
                                           'Shift+Insert',
                                       ), _('Paste from clipboard'))
        self.action_editor_cut.setEnabled(False)
        self.action_editor_copy.setEnabled(False)
        self.action_editor_undo.setEnabled(False)
        self.action_editor_redo.setEnabled(False)

        # Tool actions
        group = _('Tools')
        self.action_toc = treg('toc.png', _('&Edit Table of Contents'),
                               self.boss.edit_toc, 'edit-toc', (),
                               _('Edit Table of Contents'))
        self.action_inline_toc = treg('chapters.png',
                                      _('&Insert inline Table of Contents'),
                                      self.boss.insert_inline_toc,
                                      'insert-inline-toc', (),
                                      _('Insert inline Table of Contents'))
        self.action_fix_html_current = reg('html-fix.png', _('&Fix HTML'),
                                           partial(self.boss.fix_html, True),
                                           'fix-html-current', (),
                                           _('Fix HTML in the current file'))
        self.action_fix_html_all = treg('html-fix.png',
                                        _('&Fix HTML - all files'),
                                        partial(self.boss.fix_html, False),
                                        'fix-html-all', (),
                                        _('Fix HTML in all files'))
        self.action_pretty_current = reg('beautify.png',
                                         _('&Beautify current file'),
                                         partial(self.boss.pretty_print, True),
                                         'pretty-current', (),
                                         _('Beautify current file'))
        self.action_pretty_all = treg('beautify.png', _('&Beautify all files'),
                                      partial(self.boss.pretty_print,
                                              False), 'pretty-all', (),
                                      _('Beautify all files'))
        self.action_insert_char = treg('character-set.png',
                                       _('&Insert special character'),
                                       self.boss.insert_character,
                                       'insert-character', (),
                                       _('Insert special character'))
        self.action_rationalize_folders = treg('mimetypes/dir.png',
                                               _('&Arrange into folders'),
                                               self.boss.rationalize_folders,
                                               'rationalize-folders', (),
                                               _('Arrange into folders'))
        self.action_set_semantics = treg('tags.png', _('Set &semantics'),
                                         self.boss.set_semantics,
                                         'set-semantics', (),
                                         _('Set semantics'))
        self.action_filter_css = treg('filter.png',
                                      _('&Filter style information'),
                                      self.boss.filter_css, 'filter-css', (),
                                      _('Filter style information'))
        self.action_manage_fonts = treg('font.png', _('&Manage fonts'),
                                        self.boss.manage_fonts, 'manage-fonts',
                                        (), _('Manage fonts in the book'))
        self.action_add_cover = treg('default_cover.png', _('Add &cover'),
                                     self.boss.add_cover, 'add-cover', (),
                                     _('Add a cover to the book'))
        self.action_reports = treg(
            'reports.png', _('&Reports'), self.boss.show_reports,
            'show-reports', ('Ctrl+Shift+R', ),
            _('Show a report on various aspects of the book'))
        self.action_check_external_links = treg(
            'insert-link.png', _('Check &external links'),
            self.boss.check_external_links, 'check-external-links', (),
            _('Check external links in the book'))
        self.action_compress_images = treg('compress-image.png',
                                           _('C&ompress images losslessly'),
                                           self.boss.compress_images,
                                           'compress-images', (),
                                           _('Compress images losslessly'))
        self.action_transform_styles = treg(
            'wizard.png', _('Transform &styles'), self.boss.transform_styles,
            'transform-styles', (), _('Transform styles used in the book'))
        self.action_get_ext_resources = treg(
            'download-metadata.png', _('Download external &resources'),
            self.boss.get_external_resources, 'get-external-resources', (),
            _('Download external resources in the book (images/stylesheets/etc/ that are not included in the book)'
              ))

        def ereg(icon, text, target, sid, keys, description):
            return reg(icon, text, partial(self.boss.editor_action, target),
                       sid, keys, description)

        register_text_editor_actions(ereg, self.palette())

        # Polish actions
        group = _('Polish book')
        self.action_subset_fonts = treg(
            'subset-fonts.png', _('&Subset embedded fonts'),
            partial(self.boss.polish, 'subset', _('Subset fonts')),
            'subset-fonts', (), _('Subset embedded fonts'))
        self.action_embed_fonts = treg(
            'embed-fonts.png', _('&Embed referenced fonts'),
            partial(self.boss.polish, 'embed', _('Embed fonts')),
            'embed-fonts', (), _('Embed referenced fonts'))
        self.action_smarten_punctuation = treg(
            'smarten-punctuation.png',
            _('&Smarten punctuation (works best for English)'),
            partial(self.boss.polish, 'smarten_punctuation',
                    _('Smarten punctuation')), 'smarten-punctuation', (),
            _('Smarten punctuation'))
        self.action_remove_unused_css = treg(
            'edit-clear.png', _('Remove &unused CSS rules'),
            partial(self.boss.polish, 'remove_unused_css',
                    _('Remove unused CSS rules')), 'remove-unused-css', (),
            _('Remove unused CSS rules'))
        self.action_upgrade_book_internals = treg(
            'arrow-up.png', _('&Upgrade book internals'),
            partial(self.boss.polish, 'upgrade_book',
                    _('Upgrade book internals')), 'upgrade-book', (),
            _('Upgrade book internals'))

        # Preview actions
        group = _('Preview')
        self.action_auto_reload_preview = reg('auto-reload.png',
                                              _('Auto reload preview'), None,
                                              'auto-reload-preview', (),
                                              _('Auto reload preview'))
        self.action_auto_sync_preview = reg(
            'sync-right.png', _('Sync preview position to editor position'),
            None, 'sync-preview-to-editor', (),
            _('Sync preview position to editor position'))
        self.action_reload_preview = reg('view-refresh.png',
                                         _('Refresh preview'), None,
                                         'reload-preview', ('F5', ),
                                         _('Refresh preview'))
        self.action_split_in_preview = reg(
            'document-split.png', _('Split this file'), None,
            'split-in-preview', (), _('Split file in the preview panel'))
        self.action_find_next_preview = reg('arrow-down.png', _('Find next'),
                                            None, 'find-next-preview', (),
                                            _('Find next in preview'))
        self.action_find_prev_preview = reg('arrow-up.png', _('Find previous'),
                                            None, 'find-prev-preview', (),
                                            _('Find previous in preview'))

        # Search actions
        group = _('Search')
        self.action_find = treg('search.png', _('&Find/replace'),
                                self.boss.show_find, 'find-replace',
                                ('Ctrl+F', ), _('Show the Find/replace panel'))

        def sreg(name,
                 text,
                 action,
                 overrides={},
                 keys=(),
                 description=None,
                 icon=None):
            return reg(
                icon, text,
                partial(self.boss.search_action_triggered, action, overrides),
                name, keys, description or text.replace('&', ''))

        self.action_find_next = sreg('find-next', _('Find &next'), 'find',
                                     {'direction': 'down'}, ('F3', 'Ctrl+G'),
                                     _('Find next match'))
        self.action_find_previous = sreg('find-previous', _('Find &previous'),
                                         'find', {'direction': 'up'},
                                         ('Shift+F3', 'Shift+Ctrl+G'),
                                         _('Find previous match'))
        self.action_replace = sreg('replace',
                                   _('&Replace'),
                                   'replace',
                                   keys=('Ctrl+R'),
                                   description=_('Replace current match'))
        self.action_replace_next = sreg(
            'replace-next', _('&Replace and find next'), 'replace-find',
            {'direction': 'down'}, ('Ctrl+]'),
            _('Replace current match and find next'))
        self.action_replace_previous = sreg(
            'replace-previous', _('R&eplace and find previous'),
            'replace-find', {'direction': 'up'}, ('Ctrl+['),
            _('Replace current match and find previous'))
        self.action_replace_all = sreg('replace-all',
                                       _('Replace &all'),
                                       'replace-all',
                                       keys=('Ctrl+A'),
                                       description=_('Replace all matches'))
        self.action_count = sreg('count-matches',
                                 _('&Count all'),
                                 'count',
                                 keys=('Ctrl+N'),
                                 description=_('Count number of matches'))
        self.action_mark = reg(
            None, _('&Mark selected text'), self.boss.mark_selected_text,
            'mark-selected-text', ('Ctrl+Shift+M', ),
            _('Mark selected text or unmark already marked text'))
        self.action_mark.default_text = self.action_mark.text()
        self.action_go_to_line = reg(None, _('Go to &line'),
                                     self.boss.go_to_line_number,
                                     'go-to-line-number', ('Ctrl+.', ),
                                     _('Go to line number'))
        self.action_saved_searches = treg('folder_saved_search.png',
                                          _('Sa&ved searches'),
                                          self.boss.saved_searches,
                                          'saved-searches', (),
                                          _('Show the saved searches dialog'))
        self.action_text_search = treg('view.png',
                                       _('&Search ignoring HTML markup'),
                                       self.boss.show_text_search,
                                       'text-search', (),
                                       _('Show the text search panel'))

        # Check Book actions
        group = _('Check book')
        self.action_check_book = treg('debug.png', _('&Check book'),
                                      self.boss.check_requested, 'check-book',
                                      ('F7'), _('Check book for errors'))
        self.action_spell_check_book = treg(
            'spell-check.png', _('Check &spelling'),
            self.boss.spell_check_requested, 'spell-check-book', ('Alt+F7'),
            _('Check book for spelling errors'))
        self.action_check_book_next = reg(
            'forward.png', _('&Next error'),
            partial(self.check_book.next_error, delta=1), 'check-book-next',
            ('Ctrl+F7'), _('Show next error'))
        self.action_check_book_previous = reg(
            'back.png', _('&Previous error'),
            partial(self.check_book.next_error, delta=-1),
            'check-book-previous', ('Ctrl+Shift+F7'), _('Show previous error'))
        self.action_spell_check_next = reg('forward.png',
                                           _('&Next spelling mistake'),
                                           self.boss.next_spell_error,
                                           'spell-next', ('F8'),
                                           _('Go to next spelling mistake'))

        # Miscellaneous actions
        group = _('Miscellaneous')
        self.action_create_checkpoint = treg(
            'marked.png', _('&Create checkpoint'), self.boss.create_checkpoint,
            'create-checkpoint', (),
            _('Create a checkpoint with the current state of the book'))
        self.action_close_current_tab = reg('window-close.png',
                                            _('&Close current tab'),
                                            self.central.close_current_editor,
                                            'close-current-tab', 'Ctrl+W',
                                            _('Close the currently open tab'))
        self.action_close_all_but_current_tab = reg(
            'edit-clear.png', _('&Close other tabs'),
            self.central.close_all_but_current_editor,
            'close-all-but-current-tab', 'Ctrl+Alt+W',
            _('Close all tabs except the current tab'))
        self.action_help = treg(
            'help.png', _('User &Manual'), lambda: open_url(
                QUrl(
                    localize_user_manual_link(
                        'https://manual.calibre-ebook.com/edit.html'))),
            'user-manual', 'F1', _('Show User Manual'))
        self.action_browse_images = treg(
            'view-image.png', _('&Browse images in book'),
            self.boss.browse_images, 'browse-images', (),
            _('Browse images in the books visually'))
        self.action_multiple_split = treg(
            'document-split.png', _('&Split at multiple locations'),
            self.boss.multisplit, 'multisplit', (),
            _('Split HTML file at multiple locations'))
        self.action_compare_book = treg('diff.png',
                                        _('Compare to &another book'),
                                        self.boss.compare_book, 'compare-book',
                                        (), _('Compare to another book'))
        self.action_manage_snippets = treg('snippets.png',
                                           _('Manage &Snippets'),
                                           self.boss.manage_snippets,
                                           'manage-snippets', (),
                                           _('Manage user created snippets'))

        self.plugin_menu_actions = []

        create_plugin_actions(actions, toolbar_actions,
                              self.plugin_menu_actions)

    def create_menubar(self):
        if isosx:
            p, q = self.create_application_menubar()
            q.triggered.connect(self.action_quit.trigger)
            p.triggered.connect(self.action_preferences.trigger)
        f = factory(app_id='com.calibre-ebook.EditBook-%d' % os.getpid())
        b = f.create_window_menubar(self)

        f = b.addMenu(_('&File'))
        f.addAction(self.action_new_file)
        f.addAction(self.action_import_files)
        f.addSeparator()
        f.addAction(self.action_open_book)
        f.addAction(self.action_new_book)
        f.addAction(self.action_import_book)
        f.addAction(self.action_open_book_folder)
        self.recent_books_menu = f.addMenu(_('&Recently opened books'))
        self.update_recent_books()
        f.addSeparator()
        f.addAction(self.action_save)
        f.addAction(self.action_save_copy)
        f.addSeparator()
        f.addAction(self.action_compare_book)
        f.addAction(self.action_quit)

        e = b.addMenu(_('&Edit'))
        e.addAction(self.action_global_undo)
        e.addAction(self.action_global_redo)
        e.addAction(self.action_create_checkpoint)
        e.addSeparator()
        e.addAction(self.action_editor_undo)
        e.addAction(self.action_editor_redo)
        e.addSeparator()
        e.addAction(self.action_editor_cut)
        e.addAction(self.action_editor_copy)
        e.addAction(self.action_editor_paste)
        e.addAction(self.action_insert_char)
        e.addSeparator()
        e.addAction(self.action_quick_edit)
        e.addAction(self.action_preferences)

        e = b.addMenu(_('&Tools'))
        tm = e.addMenu(_('Table of Contents'))
        tm.addAction(self.action_toc)
        tm.addAction(self.action_inline_toc)
        e.addAction(self.action_manage_fonts)
        e.addAction(self.action_embed_fonts)
        e.addAction(self.action_subset_fonts)
        e.addAction(self.action_compress_images)
        e.addAction(self.action_smarten_punctuation)
        e.addAction(self.action_remove_unused_css)
        e.addAction(self.action_transform_styles)
        e.addAction(self.action_fix_html_all)
        e.addAction(self.action_pretty_all)
        e.addAction(self.action_rationalize_folders)
        e.addAction(self.action_add_cover)
        e.addAction(self.action_set_semantics)
        e.addAction(self.action_filter_css)
        e.addAction(self.action_spell_check_book)
        er = e.addMenu(_('External &links'))
        er.addAction(self.action_check_external_links)
        er.addAction(self.action_get_ext_resources)
        e.addAction(self.action_check_book)
        e.addAction(self.action_reports)
        e.addAction(self.action_upgrade_book_internals)

        e = b.addMenu(_('&View'))
        t = e.addMenu(_('Tool&bars'))
        e.addSeparator()
        for name in sorted(actions, key=lambda x: sort_key(actions[x].text())):
            ac = actions[name]
            if name.endswith('-dock'):
                e.addAction(ac)
            elif name.endswith('-bar'):
                t.addAction(ac)
        e.addAction(self.action_browse_images)
        e.addSeparator()
        e.addAction(self.action_close_current_tab)
        e.addAction(self.action_close_all_but_current_tab)

        e = b.addMenu(_('&Search'))
        a = e.addAction
        a(self.action_find)
        e.addSeparator()
        a(self.action_find_next)
        a(self.action_find_previous)
        e.addSeparator()
        a(self.action_replace)
        a(self.action_replace_next)
        a(self.action_replace_previous)
        a(self.action_replace_all)
        e.addSeparator()
        a(self.action_count)
        e.addSeparator()
        a(self.action_mark)
        e.addSeparator()
        a(self.action_go_to_line)
        e.addSeparator()
        a(self.action_saved_searches)
        e.aboutToShow.connect(self.search_menu_about_to_show)
        e.addSeparator()
        a(self.action_text_search)

        if self.plugin_menu_actions:
            e = b.addMenu(_('&Plugins'))
            for ac in sorted(self.plugin_menu_actions,
                             key=lambda x: sort_key(unicode_type(x.text()))):
                e.addAction(ac)

        e = b.addMenu(_('&Help'))
        a = e.addAction
        a(self.action_help)
        a(QIcon(I('donate.png')), _('&Donate to support calibre development'),
          open_donate)
        a(self.action_preferences)

    def search_menu_about_to_show(self):
        ed = self.central.current_editor
        update_mark_text_action(ed)

    def update_recent_books(self):
        m = self.recent_books_menu
        m.clear()
        books = tprefs.get('recent-books', [])
        for path in books:
            m.addAction(self.elided_text(path, width=500),
                        partial(self.boss.open_book, path=path))

    def create_toolbars(self):
        def create(text, name):
            name += '-bar'
            b = self.addToolBar(text)
            b.setObjectName(name)  # Needed for saveState
            actions[name] = b.toggleViewAction()
            b.setIconSize(
                QSize(tprefs['toolbar_icon_size'],
                      tprefs['toolbar_icon_size']))
            return b

        self.global_bar = create(_('Book tool bar'), 'global')
        self.tools_bar = create(_('Tools tool bar'), 'tools')
        self.plugins_bar = create(_('Plugins tool bar'), 'plugins')
        self.populate_toolbars(animate=True)

    def populate_toolbars(self, animate=False):
        self.global_bar.clear(), self.tools_bar.clear(
        ), self.plugins_bar.clear()

        def add(bar, ac):
            if ac is None:
                bar.addSeparator()
            elif ac == 'donate':
                self.donate_button = b = ThrobbingButton(self)
                b.clicked.connect(open_donate)
                b.setAutoRaise(True)
                b.setToolTip(_('Donate to support calibre development'))
                if animate:
                    QTimer.singleShot(10, b.start_animation)
                bar.addWidget(b)
            else:
                try:
                    bar.addAction(actions[ac])
                except KeyError:
                    if DEBUG:
                        prints('Unknown action for toolbar %r: %r' %
                               (unicode_type(bar.objectName()), ac))

        for x in tprefs['global_book_toolbar']:
            add(self.global_bar, x)

        for x in tprefs['global_tools_toolbar']:
            add(self.tools_bar, x)

        for x in tprefs['global_plugins_toolbar']:
            add(self.plugins_bar, x)
        self.plugins_bar.setVisible(bool(tprefs['global_plugins_toolbar']))

    def create_docks(self):
        def create(name, oname):
            oname += '-dock'
            d = QDockWidget(name, self)
            d.setObjectName(oname)  # Needed for saveState
            ac = d.toggleViewAction()
            desc = _('Toggle %s') % name.replace('&', '')
            self.keyboard.register_shortcut(oname,
                                            desc,
                                            description=desc,
                                            action=ac,
                                            group=_('Windows'))
            actions[oname] = ac
            setattr(self, oname.replace('-', '_'), d)
            return d

        d = create(_('File browser'), 'files-browser')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
        self.file_list = FileListWidget(d)
        d.setWidget(self.file_list)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)

        d = create(_('File preview'), 'preview')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
        self.preview = Preview(d)
        d.setWidget(self.preview)
        self.addDockWidget(Qt.RightDockWidgetArea, d)

        d = create(_('Live CSS'), 'live-css')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea
                          | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        self.live_css = LiveCSS(self.preview, parent=d)
        d.setWidget(self.live_css)
        self.addDockWidget(Qt.RightDockWidgetArea, d)
        d.close()  # Hidden by default

        d = create(_('Check book'), 'check-book')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea
                          | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        d.setWidget(self.check_book)
        self.addDockWidget(Qt.TopDockWidgetArea, d)
        d.close()  # By default the check window is closed

        d = create(_('Inspector'), 'inspector')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea
                          | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        d.setWidget(self.preview.inspector)
        self.preview.inspector.setParent(d)
        self.addDockWidget(Qt.BottomDockWidgetArea, d)
        d.close()  # By default the inspector window is closed
        QTimer.singleShot(10, self.preview.inspector.connect_to_dock)

        d = create(_('Table of Contents'), 'toc-viewer')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea
                          | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        d.setWidget(self.toc_view)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)
        d.close()  # Hidden by default

        d = create(_('Text search'), 'text-search')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea
                          | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        d.setWidget(self.text_search)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)
        d.close()  # Hidden by default

        d = create(_('Checkpoints'), 'checkpoints')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea
                          | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        self.checkpoints = CheckpointView(self.boss.global_undo, parent=d)
        d.setWidget(self.checkpoints)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)
        d.close()  # Hidden by default

        d = create(_('Saved searches'), 'saved-searches')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea
                          | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        d.setWidget(self.saved_searches)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)
        d.close()  # Hidden by default

    def resizeEvent(self, ev):
        self.blocking_job.resize(ev.size())
        return super(Main, self).resizeEvent(ev)

    def update_window_title(self):
        fname = os.path.basename(current_container().path_to_ebook)
        self.setWindowTitle(
            self.current_metadata.title + ' [%s] :: %s :: %s' %
            (current_container().book_type.upper(), fname, self.APP_NAME))

    def closeEvent(self, e):
        if self.boss.quit():
            e.accept()
        else:
            e.ignore()

    def save_state(self):
        tprefs.set('main_window_geometry', bytearray(self.saveGeometry()))
        tprefs.set('main_window_state',
                   bytearray(self.saveState(self.STATE_VERSION)))
        self.central.save_state()
        self.saved_searches.save_state()
        self.check_book.save_state()
        self.text_search.save_state()

    def restore_state(self):
        geom = tprefs.get('main_window_geometry', None)
        if geom is not None:
            self.restoreGeometry(geom)
        state = tprefs.get('main_window_state', None)
        if state is not None:
            self.restoreState(state, self.STATE_VERSION)
        self.central.restore_state()
        self.saved_searches.restore_state()

    def contextMenuEvent(self, ev):
        ev.ignore()
Example #11
0
class Main(MainWindow):

    APP_NAME = _("Edit Book")
    STATE_VERSION = 0

    def __init__(self, opts, notify=None):
        MainWindow.__init__(self, opts, disable_automatic_gc=True)
        self.boss = Boss(self, notify=notify)
        self.setWindowTitle(self.APP_NAME)
        self.setWindowIcon(QIcon(I("tweak.png")))
        self.opts = opts
        self.path_to_ebook = None
        self.container = None
        self.current_metadata = None
        self.blocking_job = BlockingJob(self)
        self.keyboard = KeyboardManager(self, config_name="shortcuts/tweak_book")

        self.central = Central(self)
        self.setCentralWidget(self.central)
        self.check_book = Check(self)
        self.spell_check = SpellCheck(parent=self)
        self.toc_view = TOCViewer(self)
        self.saved_searches = SavedSearches(self)
        self.image_browser = InsertImage(self, for_browsing=True)
        self.insert_char = CharSelect(self)
        self.manage_fonts = ManageFonts(self)

        self.create_actions()
        self.create_toolbars()
        self.create_docks()
        self.create_menubar()

        self.status_bar = self.statusBar()
        self.status_bar.addPermanentWidget(self.boss.save_manager.status_widget)
        self.cursor_position_widget = CursorPositionWidget(self)
        self.status_bar.addPermanentWidget(self.cursor_position_widget)
        self.status_bar_default_msg = la = QLabel(
            _("{0} {1} created by {2}").format(__appname__, get_version(), "Kovid Goyal")
        )
        la.base_template = unicode(la.text())
        self.status_bar.addWidget(la)
        f = self.status_bar.font()
        f.setBold(True)
        self.status_bar.setFont(f)

        self.boss(self)
        g = QApplication.instance().desktop().availableGeometry(self)
        self.resize(g.width() - 50, g.height() - 50)

        self.restore_state()
        self.apply_settings()

    def apply_settings(self):
        self.keyboard.finalize()
        self.setDockNestingEnabled(tprefs["nestable_dock_widgets"])
        for v, h in product(("top", "bottom"), ("left", "right")):
            p = "dock_%s_%s" % (v, h)
            pref = tprefs[p] or tprefs.defaults[p]
            area = getattr(Qt, "%sDockWidgetArea" % capitalize({"vertical": h, "horizontal": v}[pref]))
            self.setCorner(getattr(Qt, "%s%sCorner" % tuple(map(capitalize, (v, h)))), area)
        self.preview.apply_settings()
        self.live_css.apply_theme()

    def show_status_message(self, msg, timeout=5):
        self.status_bar.showMessage(msg, int(timeout * 1000))

    def elided_text(self, text, width=300):
        return elided_text(text, font=self.font(), width=width)

    @property
    def editor_tabs(self):
        return self.central.editor_tabs

    def create_actions(self):
        group = _("Global Actions")

        def reg(icon, text, target, sid, keys, description, toolbar_allowed=False):
            if not isinstance(icon, QIcon):
                icon = QIcon(I(icon))
            ac = actions[sid] = QAction(icon, text, self) if icon else QAction(text, self)
            ac.setObjectName("action-" + sid)
            if toolbar_allowed:
                toolbar_actions[sid] = ac
            if target is not None:
                ac.triggered.connect(target)
            if isinstance(keys, type("")):
                keys = (keys,)
            self.keyboard.register_shortcut(
                sid,
                unicode(ac.text()).replace("&", ""),
                default_keys=keys,
                description=description,
                action=ac,
                group=group,
            )
            self.addAction(ac)
            return ac

        def treg(icon, text, target, sid, keys, description):
            return reg(icon, text, target, sid, keys, description, toolbar_allowed=icon is not None)

        self.action_new_file = treg(
            "document-new.png",
            _("&New file (images/fonts/HTML/etc.)"),
            self.boss.add_file,
            "new-file",
            (),
            _("Create a new file in the current book"),
        )
        self.action_import_files = treg(
            None, _("&Import files into book"), self.boss.add_files, "new-files", (), _("Import files into book")
        )
        self.action_open_book = treg(
            "document_open.png", _("Open &book"), self.boss.open_book, "open-book", "Ctrl+O", _("Open a new book")
        )
        # Qt does not generate shortcut overrides for cmd+arrow on os x which
        # means these shortcuts interfere with editing
        self.action_global_undo = treg(
            "back.png",
            _("&Revert to before"),
            self.boss.do_global_undo,
            "global-undo",
            () if isosx else "Ctrl+Left",
            _("Revert book to before the last action (Undo)"),
        )
        self.action_global_redo = treg(
            "forward.png",
            _("&Revert to after"),
            self.boss.do_global_redo,
            "global-redo",
            () if isosx else "Ctrl+Right",
            _("Revert book state to after the next action (Redo)"),
        )
        self.action_save = treg("save.png", _("&Save"), self.boss.save_book, "save-book", "Ctrl+S", _("Save book"))
        self.action_save.setEnabled(False)
        self.action_save_copy = treg(
            "save.png", _("Save a &copy"), self.boss.save_copy, "save-copy", "Ctrl+Alt+S", _("Save a copy of the book")
        )
        self.action_quit = treg("window-close.png", _("&Quit"), self.boss.quit, "quit", "Ctrl+Q", _("Quit"))
        self.action_preferences = treg(
            "config.png", _("&Preferences"), self.boss.preferences, "preferences", "Ctrl+P", _("Preferences")
        )
        self.action_new_book = treg(
            "book.png", _("Create &new, empty book"), self.boss.new_book, "new-book", (), _("Create a new, empty book")
        )
        self.action_import_book = treg(
            "book.png",
            _("&Import an HTML or DOCX file as a new book"),
            self.boss.import_book,
            "import-book",
            (),
            _("Import an HTML or DOCX file as a new book"),
        )
        self.action_quick_edit = treg(
            "modified.png",
            _("&Quick open a file to edit"),
            self.boss.quick_open,
            "quick-open",
            ("Ctrl+T"),
            _("Quickly open a file from the book to edit it"),
        )

        # Editor actions
        group = _("Editor actions")
        self.action_editor_undo = reg(
            "edit-undo.png", _("&Undo"), self.boss.do_editor_undo, "editor-undo", "Ctrl+Z", _("Undo typing")
        )
        self.action_editor_redo = reg(
            "edit-redo.png", _("&Redo"), self.boss.do_editor_redo, "editor-redo", "Ctrl+Y", _("Redo typing")
        )
        self.action_editor_cut = reg(
            "edit-cut.png",
            _("C&ut text"),
            self.boss.do_editor_cut,
            "editor-cut",
            ("Ctrl+X", "Shift+Delete"),
            _("Cut text"),
        )
        self.action_editor_copy = reg(
            "edit-copy.png",
            _("&Copy to clipboard"),
            self.boss.do_editor_copy,
            "editor-copy",
            ("Ctrl+C", "Ctrl+Insert"),
            _("Copy to clipboard"),
        )
        self.action_editor_paste = reg(
            "edit-paste.png",
            _("&Paste from clipboard"),
            self.boss.do_editor_paste,
            "editor-paste",
            ("Ctrl+V", "Shift+Insert"),
            _("Paste from clipboard"),
        )
        self.action_editor_cut.setEnabled(False)
        self.action_editor_copy.setEnabled(False)
        self.action_editor_undo.setEnabled(False)
        self.action_editor_redo.setEnabled(False)

        # Tool actions
        group = _("Tools")
        self.action_toc = treg(
            "toc.png", _("&Edit Table of Contents"), self.boss.edit_toc, "edit-toc", (), _("Edit Table of Contents")
        )
        self.action_inline_toc = treg(
            "chapters.png",
            _("&Insert inline Table of Contents"),
            self.boss.insert_inline_toc,
            "insert-inline-toc",
            (),
            _("Insert inline Table of Contents"),
        )
        self.action_fix_html_current = reg(
            "html-fix.png",
            _("&Fix HTML"),
            partial(self.boss.fix_html, True),
            "fix-html-current",
            (),
            _("Fix HTML in the current file"),
        )
        self.action_fix_html_all = treg(
            "html-fix.png",
            _("&Fix HTML - all files"),
            partial(self.boss.fix_html, False),
            "fix-html-all",
            (),
            _("Fix HTML in all files"),
        )
        self.action_pretty_current = reg(
            "beautify.png",
            _("&Beautify current file"),
            partial(self.boss.pretty_print, True),
            "pretty-current",
            (),
            _("Beautify current file"),
        )
        self.action_pretty_all = treg(
            "beautify.png",
            _("&Beautify all files"),
            partial(self.boss.pretty_print, False),
            "pretty-all",
            (),
            _("Beautify all files"),
        )
        self.action_insert_char = treg(
            "character-set.png",
            _("&Insert special character"),
            self.boss.insert_character,
            "insert-character",
            (),
            _("Insert special character"),
        )
        self.action_rationalize_folders = treg(
            "mimetypes/dir.png",
            _("&Arrange into folders"),
            self.boss.rationalize_folders,
            "rationalize-folders",
            (),
            _("Arrange into folders"),
        )
        self.action_set_semantics = treg(
            "tags.png", _("Set &Semantics"), self.boss.set_semantics, "set-semantics", (), _("Set Semantics")
        )
        self.action_filter_css = treg(
            "filter.png",
            _("&Filter style information"),
            self.boss.filter_css,
            "filter-css",
            (),
            _("Filter style information"),
        )
        self.action_manage_fonts = treg(
            "font.png", _("Manage &fonts"), self.boss.manage_fonts, "manage-fonts", (), _("Manage fonts in the book")
        )
        self.action_add_cover = treg(
            "default_cover.png", _("Add &cover"), self.boss.add_cover, "add-cover", (), _("Add a cover to the book")
        )

        def ereg(icon, text, target, sid, keys, description):
            return reg(icon, text, partial(self.boss.editor_action, target), sid, keys, description)

        register_text_editor_actions(ereg, self.palette())

        # Polish actions
        group = _("Polish Book")
        self.action_subset_fonts = treg(
            "subset-fonts.png",
            _("&Subset embedded fonts"),
            partial(self.boss.polish, "subset", _("Subset fonts")),
            "subset-fonts",
            (),
            _("Subset embedded fonts"),
        )
        self.action_embed_fonts = treg(
            "embed-fonts.png",
            _("&Embed referenced fonts"),
            partial(self.boss.polish, "embed", _("Embed fonts")),
            "embed-fonts",
            (),
            _("Embed referenced fonts"),
        )
        self.action_smarten_punctuation = treg(
            "smarten-punctuation.png",
            _("&Smarten punctuation"),
            partial(self.boss.polish, "smarten_punctuation", _("Smarten punctuation")),
            "smarten-punctuation",
            (),
            _("Smarten punctuation"),
        )
        self.action_remove_unused_css = treg(
            "edit-clear.png",
            _("Remove &unused CSS rules"),
            partial(self.boss.polish, "remove_unused_css", _("Remove unused CSS rules")),
            "remove-unused-css",
            (),
            _("Remove unused CSS rules"),
        )

        # Preview actions
        group = _("Preview")
        self.action_auto_reload_preview = reg(
            "auto-reload.png", _("Auto reload preview"), None, "auto-reload-preview", (), _("Auto reload preview")
        )
        self.action_auto_sync_preview = reg(
            "sync-right.png",
            _("Sync preview position to editor position"),
            None,
            "sync-preview-to-editor",
            (),
            _("Sync preview position to editor position"),
        )
        self.action_reload_preview = reg(
            "view-refresh.png", _("Refresh preview"), None, "reload-preview", ("F5",), _("Refresh preview")
        )
        self.action_split_in_preview = reg(
            "auto_author_sort.png",
            _("Split this file"),
            None,
            "split-in-preview",
            (),
            _("Split file in the preview panel"),
        )
        self.action_find_next_preview = reg(
            "arrow-down.png", _("Find Next"), None, "find-next-preview", (), _("Find next in preview")
        )
        self.action_find_prev_preview = reg(
            "arrow-up.png", _("Find Previous"), None, "find-prev-preview", (), _("Find previous in preview")
        )

        # Search actions
        group = _("Search")
        self.action_find = treg(
            "search.png",
            _("&Find/Replace"),
            self.boss.show_find,
            "find-replace",
            ("Ctrl+F",),
            _("Show the Find/Replace panel"),
        )

        def sreg(name, text, action, overrides={}, keys=(), description=None, icon=None):
            return reg(
                icon,
                text,
                partial(self.boss.search, action, overrides),
                name,
                keys,
                description or text.replace("&", ""),
            )

        self.action_find_next = sreg(
            "find-next", _("Find &Next"), "find", {"direction": "down"}, ("F3", "Ctrl+G"), _("Find next match")
        )
        self.action_find_previous = sreg(
            "find-previous",
            _("Find &Previous"),
            "find",
            {"direction": "up"},
            ("Shift+F3", "Shift+Ctrl+G"),
            _("Find previous match"),
        )
        self.action_replace = sreg(
            "replace", _("Replace"), "replace", keys=("Ctrl+R"), description=_("Replace current match")
        )
        self.action_replace_next = sreg(
            "replace-next",
            _("&Replace and find next"),
            "replace-find",
            {"direction": "down"},
            ("Ctrl+]"),
            _("Replace current match and find next"),
        )
        self.action_replace_previous = sreg(
            "replace-previous",
            _("R&eplace and find previous"),
            "replace-find",
            {"direction": "up"},
            ("Ctrl+["),
            _("Replace current match and find previous"),
        )
        self.action_replace_all = sreg(
            "replace-all", _("Replace &all"), "replace-all", keys=("Ctrl+A"), description=_("Replace all matches")
        )
        self.action_count = sreg(
            "count-matches", _("&Count all"), "count", keys=("Ctrl+N"), description=_("Count number of matches")
        )
        self.action_mark = reg(
            None,
            _("&Mark selected text"),
            self.boss.mark_selected_text,
            "mark-selected-text",
            ("Ctrl+Shift+M",),
            _("Mark selected text"),
        )
        self.action_go_to_line = reg(
            None,
            _("Go to &line"),
            self.boss.go_to_line_number,
            "go-to-line-number",
            ("Ctrl+.",),
            _("Go to line number"),
        )
        self.action_saved_searches = reg(
            None,
            _("Sa&ved searches"),
            self.boss.saved_searches,
            "saved-searches",
            (),
            _("Show the saved searches dialog"),
        )

        # Check Book actions
        group = _("Check Book")
        self.action_check_book = treg(
            "debug.png", _("&Check Book"), self.boss.check_requested, "check-book", ("F7"), _("Check book for errors")
        )
        self.action_spell_check_book = treg(
            "spell-check.png",
            _("Check &spelling"),
            self.boss.spell_check_requested,
            "spell-check-book",
            ("Alt+F7"),
            _("Check book for spelling errors"),
        )
        self.action_check_book_next = reg(
            "forward.png",
            _("&Next error"),
            partial(self.check_book.next_error, delta=1),
            "check-book-next",
            ("Ctrl+F7"),
            _("Show next error"),
        )
        self.action_check_book_previous = reg(
            "back.png",
            _("&Previous error"),
            partial(self.check_book.next_error, delta=-1),
            "check-book-previous",
            ("Ctrl+Shift+F7"),
            _("Show previous error"),
        )
        self.action_spell_check_next = reg(
            "forward.png",
            _("&Next spelling mistake"),
            self.boss.next_spell_error,
            "spell-next",
            ("F8"),
            _("Go to next spelling mistake"),
        )

        # Miscellaneous actions
        group = _("Miscellaneous")
        self.action_create_checkpoint = treg(
            "marked.png",
            _("&Create checkpoint"),
            self.boss.create_checkpoint,
            "create-checkpoint",
            (),
            _("Create a checkpoint with the current state of the book"),
        )
        self.action_close_current_tab = reg(
            "window-close.png",
            _("&Close current tab"),
            self.central.close_current_editor,
            "close-current-tab",
            "Ctrl+W",
            _("Close the currently open tab"),
        )
        self.action_close_all_but_current_tab = reg(
            "edit-clear.png",
            _("&Close other tabs"),
            self.central.close_all_but_current_editor,
            "close-all-but-current-tab",
            "Ctrl+Alt+W",
            _("Close all tabs except the current tab"),
        )
        self.action_help = treg(
            "help.png",
            _("User &Manual"),
            lambda: open_url(QUrl("http://manual.calibre-ebook.com/edit.html")),
            "user-manual",
            "F1",
            _("Show User Manual"),
        )
        self.action_browse_images = treg(
            "view-image.png",
            _("&Browse images in book"),
            self.boss.browse_images,
            "browse-images",
            (),
            _("Browse images in the books visually"),
        )
        self.action_multiple_split = treg(
            "auto_author_sort.png",
            _("&Split at multiple locations"),
            self.boss.multisplit,
            "multisplit",
            (),
            _("Split HTML file at multiple locations"),
        )
        self.action_compare_book = treg(
            "diff.png",
            _("&Compare to another book"),
            self.boss.compare_book,
            "compare-book",
            (),
            _("Compare to another book"),
        )

    def create_menubar(self):
        p, q = self.create_application_menubar()
        q.triggered.connect(self.action_quit.trigger)
        p.triggered.connect(self.action_preferences.trigger)
        b = self.menuBar()

        f = b.addMenu(_("&File"))
        f.addAction(self.action_new_file)
        f.addAction(self.action_import_files)
        f.addSeparator()
        f.addAction(self.action_open_book)
        f.addAction(self.action_new_book)
        f.addAction(self.action_import_book)
        self.recent_books_menu = f.addMenu(_("&Recently opened books"))
        self.update_recent_books()
        f.addSeparator()
        f.addAction(self.action_save)
        f.addAction(self.action_save_copy)
        f.addSeparator()
        f.addAction(self.action_compare_book)
        f.addAction(self.action_quit)

        e = b.addMenu(_("&Edit"))
        e.addAction(self.action_global_undo)
        e.addAction(self.action_global_redo)
        e.addAction(self.action_create_checkpoint)
        e.addSeparator()
        e.addAction(self.action_editor_undo)
        e.addAction(self.action_editor_redo)
        e.addSeparator()
        e.addAction(self.action_editor_cut)
        e.addAction(self.action_editor_copy)
        e.addAction(self.action_editor_paste)
        e.addAction(self.action_insert_char)
        e.addSeparator()
        e.addAction(self.action_quick_edit)
        e.addAction(self.action_preferences)

        e = b.addMenu(_("&Tools"))
        tm = e.addMenu(_("Table of Contents"))
        tm.addAction(self.action_toc)
        tm.addAction(self.action_inline_toc)
        e.addAction(self.action_manage_fonts)
        e.addAction(self.action_embed_fonts)
        e.addAction(self.action_subset_fonts)
        e.addAction(self.action_smarten_punctuation)
        e.addAction(self.action_remove_unused_css)
        e.addAction(self.action_fix_html_all)
        e.addAction(self.action_pretty_all)
        e.addAction(self.action_rationalize_folders)
        e.addAction(self.action_add_cover)
        e.addAction(self.action_set_semantics)
        e.addAction(self.action_filter_css)
        e.addAction(self.action_spell_check_book)
        e.addAction(self.action_check_book)

        e = b.addMenu(_("&View"))
        t = e.addMenu(_("Tool&bars"))
        e.addSeparator()
        for name, ac in actions.iteritems():
            if name.endswith("-dock"):
                e.addAction(ac)
            elif name.endswith("-bar"):
                t.addAction(ac)
        e.addAction(self.action_browse_images)
        e.addSeparator()
        e.addAction(self.action_close_current_tab)
        e.addAction(self.action_close_all_but_current_tab)

        e = b.addMenu(_("&Search"))
        a = e.addAction
        a(self.action_find)
        e.addSeparator()
        a(self.action_find_next)
        a(self.action_find_previous)
        e.addSeparator()
        a(self.action_replace)
        a(self.action_replace_next)
        a(self.action_replace_previous)
        a(self.action_replace_all)
        e.addSeparator()
        a(self.action_count)
        e.addSeparator()
        a(self.action_mark)
        e.addSeparator()
        a(self.action_go_to_line)
        e.addSeparator()
        a(self.action_saved_searches)

        e = b.addMenu(_("&Help"))
        a = e.addAction
        a(self.action_help)
        a(QIcon(I("donate.png")), _("Donate to support calibre development"), open_donate)
        a(self.action_preferences)

    def update_recent_books(self):
        m = self.recent_books_menu
        m.clear()
        books = tprefs.get("recent-books", [])
        for path in books:
            m.addAction(self.elided_text(path, width=500), partial(self.boss.open_book, path=path))

    def create_toolbars(self):
        def create(text, name):
            name += "-bar"
            b = self.addToolBar(text)
            b.setObjectName(name)  # Needed for saveState
            actions[name] = b.toggleViewAction()
            return b

        self.global_bar = create(_("Book tool bar"), "global")
        self.tools_bar = create(_("Tools tool bar"), "tools")
        self.populate_toolbars(animate=True)

    def populate_toolbars(self, animate=False):
        self.global_bar.clear(), self.tools_bar.clear()

        def add(bar, ac):
            if ac is None:
                bar.addSeparator()
            elif ac == "donate":
                self.donate_button = b = ThrobbingButton(self)
                b.clicked.connect(open_donate)
                b.setAutoRaise(True)
                self.donate_widget = w = create_donate_widget(b)
                if hasattr(w, "filler"):
                    w.filler.setVisible(False)
                b.set_normal_icon_size(self.global_bar.iconSize().width(), self.global_bar.iconSize().height())
                b.setIcon(QIcon(I("donate.png")))
                b.setToolTip(_("Donate to support calibre development"))
                if animate:
                    QTimer.singleShot(10, b.start_animation)
                bar.addWidget(w)
            else:
                try:
                    bar.addAction(actions[ac])
                except KeyError:
                    if DEBUG:
                        prints("Unknown action for toolbar %r: %r" % (unicode(bar.objectName()), ac))

        for x in tprefs["global_book_toolbar"]:
            add(self.global_bar, x)

        for x in tprefs["global_tools_toolbar"]:
            add(self.tools_bar, x)

    def create_docks(self):
        def create(name, oname):
            oname += "-dock"
            d = QDockWidget(name, self)
            d.setObjectName(oname)  # Needed for saveState
            ac = d.toggleViewAction()
            desc = _("Toggle %s") % name.replace("&", "")
            self.keyboard.register_shortcut(oname, desc, description=desc, action=ac, group=_("Windows"))
            actions[oname] = ac
            setattr(self, oname.replace("-", "_"), d)
            return d

        d = create(_("Files Browser"), "files-browser")
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
        self.file_list = FileListWidget(d)
        d.setWidget(self.file_list)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)

        d = create(_("File Preview"), "preview")
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
        self.preview = Preview(d)
        d.setWidget(self.preview)
        self.addDockWidget(Qt.RightDockWidgetArea, d)

        d = create(_("Live CSS"), "live-css")
        d.setAllowedAreas(
            Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea
        )
        self.live_css = LiveCSS(self.preview, parent=d)
        d.setWidget(self.live_css)
        self.addDockWidget(Qt.RightDockWidgetArea, d)
        d.close()  # Hidden by default

        d = create(_("Check Book"), "check-book")
        d.setAllowedAreas(
            Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea
        )
        d.setWidget(self.check_book)
        self.addDockWidget(Qt.TopDockWidgetArea, d)
        d.close()  # By default the check window is closed

        d = create(_("Inspector"), "inspector")
        d.setAllowedAreas(
            Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea
        )
        d.setWidget(self.preview.inspector)
        self.preview.inspector.setParent(d)
        self.addDockWidget(Qt.BottomDockWidgetArea, d)
        d.close()  # By default the inspector window is closed
        d.setFeatures(d.DockWidgetClosable | d.DockWidgetMovable)  # QWebInspector does not work in a floating dock

        d = create(_("Table of Contents"), "toc-viewer")
        d.setAllowedAreas(
            Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea
        )
        d.setWidget(self.toc_view)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)
        d.close()  # Hidden by default

        d = create(_("Checkpoints"), "checkpoints")
        d.setAllowedAreas(
            Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea
        )
        self.checkpoints = CheckpointView(self.boss.global_undo, parent=d)
        d.setWidget(self.checkpoints)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)
        d.close()  # Hidden by default

    def resizeEvent(self, ev):
        self.blocking_job.resize(ev.size())
        return super(Main, self).resizeEvent(ev)

    def update_window_title(self):
        fname = os.path.basename(current_container().path_to_ebook)
        self.setWindowTitle(
            self.current_metadata.title
            + " [%s] :: %s :: %s" % (current_container().book_type.upper(), fname, self.APP_NAME)
        )

    def closeEvent(self, e):
        if not self.boss.confirm_quit():
            e.ignore()
            return
        try:
            self.boss.shutdown()
        except:
            import traceback

            traceback.print_exc()
        e.accept()

    def save_state(self):
        tprefs.set("main_window_geometry", bytearray(self.saveGeometry()))
        tprefs.set("main_window_state", bytearray(self.saveState(self.STATE_VERSION)))
        self.central.save_state()
        self.check_book.save_state()

    def restore_state(self):
        geom = tprefs.get("main_window_geometry", None)
        if geom is not None:
            self.restoreGeometry(geom)
        state = tprefs.get("main_window_state", None)
        if state is not None:
            self.restoreState(state, self.STATE_VERSION)
        self.central.restore_state()

    def contextMenuEvent(self, ev):
        ev.ignore()
Example #12
0
class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin,  # {{{
        TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
        SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin,
        EbookDownloadMixin
        ):

    'The main GUI'

    proceed_requested = pyqtSignal(object, object)
    book_converted = pyqtSignal(object, object)

    def __init__(self, opts, parent=None, gui_debug=None):
        global _gui
        MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True)
        self.jobs_pointer = Pointer(self)
        self.proceed_requested.connect(self.do_proceed,
                type=Qt.QueuedConnection)
        self.proceed_question = ProceedQuestion(self)
        self.job_error_dialog = JobError(self)
        self.keyboard = Manager(self)
        _gui = self
        self.opts = opts
        self.device_connected = None
        self.gui_debug = gui_debug
        self.iactions = OrderedDict()
        # Actions
        for action in interface_actions():
            if opts.ignore_plugins and action.plugin_path is not None:
                continue
            try:
                ac = self.init_iaction(action)
            except:
                # Ignore errors in loading user supplied plugins
                import traceback
                traceback.print_exc()
                if action.plugin_path is None:
                    raise
                continue
            ac.plugin_path = action.plugin_path
            ac.interface_action_base_plugin = action
            self.add_iaction(ac)
        self.load_store_plugins()

    def init_iaction(self, action):
        ac = action.load_actual_plugin(self)
        ac.plugin_path = action.plugin_path
        ac.interface_action_base_plugin = action
        action.actual_iaction_plugin_loaded = True
        return ac

    def add_iaction(self, ac):
        acmap = self.iactions
        if ac.name in acmap:
            if ac.priority >= acmap[ac.name].priority:
                acmap[ac.name] = ac
        else:
            acmap[ac.name] = ac

    def load_store_plugins(self):
        from calibre.gui2.store.loader import Stores
        self.istores = Stores()
        for store in available_store_plugins():
            if self.opts.ignore_plugins and store.plugin_path is not None:
                continue
            try:
                st = self.init_istore(store)
                self.add_istore(st)
            except:
                # Ignore errors in loading user supplied plugins
                import traceback
                traceback.print_exc()
                if store.plugin_path is None:
                    raise
                continue
        self.istores.builtins_loaded()

    def init_istore(self, store):
        st = store.load_actual_plugin(self)
        st.plugin_path = store.plugin_path
        st.base_plugin = store
        store.actual_istore_plugin_loaded = True
        return st

    def add_istore(self, st):
        stmap = self.istores
        if st.name in stmap:
            if st.priority >= stmap[st.name].priority:
                stmap[st.name] = st
        else:
            stmap[st.name] = st

    def initialize(self, library_path, db, listener, actions, show_gui=True):
        opts = self.opts
        self.preferences_action, self.quit_action = actions
        self.library_path = library_path
        self.content_server = None
        self._spare_pool = None
        self.must_restart_before_config = False
        self.listener = Listener(listener)
        self.check_messages_timer = QTimer()
        self.check_messages_timer.timeout.connect(self.another_instance_wants_to_talk)
        self.check_messages_timer.start(1000)

        for ac in self.iactions.values():
            try:
                ac.do_genesis()
            except Exception:
                # Ignore errors in third party plugins
                import traceback
                traceback.print_exc()
                if getattr(ac, 'plugin_path', None) is None:
                    raise
        self.donate_action = QAction(QIcon(I('donate.png')),
                _('&Donate to support calibre'), self)
        for st in self.istores.values():
            st.do_genesis()
        MainWindowMixin.init_main_window_mixin(self, db)

        # Jobs Button {{{
        self.job_manager = JobManager()
        self.jobs_dialog = JobsDialog(self, self.job_manager)
        self.jobs_button = JobsButton(horizontal=True, parent=self)
        self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
        # }}}

        LayoutMixin.init_layout_mixin(self)
        DeviceMixin.init_device_mixin(self)

        self.progress_indicator = ProgressIndicator(self)
        self.progress_indicator.pos = (0, 20)
        self.verbose = opts.verbose
        self.get_metadata = GetMetadata()
        self.upload_memory = {}
        self.metadata_dialogs = []
        self.default_thumbnail = None
        self.tb_wrapper = textwrap.TextWrapper(width=40)
        self.viewers = collections.deque()
        self.system_tray_icon = None
        if config['systray_icon']:
            self.system_tray_icon = factory(app_id='com.calibre-ebook.gui').create_system_tray_icon(parent=self, title='calibre')
        if self.system_tray_icon is not None:
            self.system_tray_icon.setIcon(QIcon(I('lt.png')))
            self.system_tray_icon.setToolTip(self.jobs_button.tray_tooltip())
            self.system_tray_icon.setVisible(True)
            self.jobs_button.tray_tooltip_updated.connect(self.system_tray_icon.setToolTip)
        elif config['systray_icon']:
            prints('Failed to create system tray icon, your desktop environment probably does not support the StatusNotifier spec')
        self.system_tray_menu = QMenu(self)
        self.toggle_to_tray_action = self.system_tray_menu.addAction(QIcon(I('page.png')), '')
        self.toggle_to_tray_action.triggered.connect(self.system_tray_icon_activated)
        self.system_tray_menu.addAction(self.donate_action)
        self.donate_button.setDefaultAction(self.donate_action)
        self.donate_button.setStatusTip(self.donate_button.toolTip())
        self.eject_action = self.system_tray_menu.addAction(
                QIcon(I('eject.png')), _('&Eject connected device'))
        self.eject_action.setEnabled(False)
        self.addAction(self.quit_action)
        self.system_tray_menu.addAction(self.quit_action)
        self.keyboard.register_shortcut('quit calibre', _('Quit calibre'),
                default_keys=('Ctrl+Q',), action=self.quit_action)
        if self.system_tray_icon is not None:
            self.system_tray_icon.setContextMenu(self.system_tray_menu)
            self.system_tray_icon.activated.connect(self.system_tray_icon_activated)
        self.quit_action.triggered[bool].connect(self.quit)
        self.donate_action.triggered[bool].connect(self.donate)

        self.esc_action = QAction(self)
        self.addAction(self.esc_action)
        self.keyboard.register_shortcut('clear current search',
                _('Clear the current search'), default_keys=('Esc',),
                action=self.esc_action)
        self.esc_action.triggered.connect(self.esc)

        self.shift_esc_action = QAction(self)
        self.addAction(self.shift_esc_action)
        self.keyboard.register_shortcut('focus book list',
                _('Focus the book list'), default_keys=('Shift+Esc',),
                action=self.shift_esc_action)
        self.shift_esc_action.triggered.connect(self.shift_esc)

        self.ctrl_esc_action = QAction(self)
        self.addAction(self.ctrl_esc_action)
        self.keyboard.register_shortcut('clear virtual library',
                _('Clear the virtual library'), default_keys=('Ctrl+Esc',),
                action=self.ctrl_esc_action)
        self.ctrl_esc_action.triggered.connect(self.ctrl_esc)

        self.alt_esc_action = QAction(self)
        self.addAction(self.alt_esc_action)
        self.keyboard.register_shortcut('clear additional restriction',
                _('Clear the additional restriction'), default_keys=('Alt+Esc',),
                action=self.alt_esc_action)
        self.alt_esc_action.triggered.connect(self.clear_additional_restriction)

        # ###################### Start spare job server ########################
        QTimer.singleShot(1000, self.create_spare_pool)

        # ###################### Location Manager ########################
        self.location_manager.location_selected.connect(self.location_selected)
        self.location_manager.unmount_device.connect(self.device_manager.umount_device)
        self.location_manager.configure_device.connect(self.configure_connected_device)
        self.location_manager.update_device_metadata.connect(self.update_metadata_on_device)
        self.eject_action.triggered.connect(self.device_manager.umount_device)

        # ################### Update notification ###################
        UpdateMixin.init_update_mixin(self, opts)

        # ###################### Search boxes ########################
        SearchRestrictionMixin.init_search_restirction_mixin(self)
        SavedSearchBoxMixin.init_saved_seach_box_mixin(self)

        # ###################### Library view ########################
        LibraryViewMixin.init_library_view_mixin(self, db)
        SearchBoxMixin.init_search_box_mixin(self)  # Requires current_db

        if show_gui:
            self.show()

        if self.system_tray_icon is not None and self.system_tray_icon.isVisible() and opts.start_in_tray:
            self.hide_windows()
        self.library_view.model().count_changed_signal.connect(
                self.iactions['Choose Library'].count_changed)
        if not gprefs.get('quick_start_guide_added', False):
            try:
                add_quick_start_guide(self.library_view)
            except:
                import traceback
                traceback.print_exc()
        for view in ('library', 'memory', 'card_a', 'card_b'):
            v = getattr(self, '%s_view' % view)
            v.selectionModel().selectionChanged.connect(self.update_status_bar)
            v.model().count_changed_signal.connect(self.update_status_bar)

        self.library_view.model().count_changed()
        self.bars_manager.database_changed(self.library_view.model().db)
        self.library_view.model().database_changed.connect(self.bars_manager.database_changed,
                type=Qt.QueuedConnection)

        # ########################## Tags Browser ##############################
        TagBrowserMixin.init_tag_browser_mixin(self, db)

        # ######################## Search Restriction ##########################
        if db.prefs['virtual_lib_on_startup']:
            self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
        self.rebuild_vl_tabs()

        # ########################## Cover Flow ################################

        CoverFlowMixin.init_cover_flow_mixin(self)

        self._calculated_available_height = min(max_available_height()-15,
                self.height())
        self.resize(self.width(), self._calculated_available_height)

        self.build_context_menus()

        for ac in self.iactions.values():
            try:
                ac.gui_layout_complete()
            except:
                import traceback
                traceback.print_exc()
                if ac.plugin_path is None:
                    raise

        if config['autolaunch_server']:
            self.start_content_server()

        self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)

        self.read_settings()
        self.finalize_layout()
        if self.bars_manager.showing_donate:
            self.donate_button.start_animation()
        self.set_window_title()

        for ac in self.iactions.values():
            try:
                ac.initialization_complete()
            except:
                import traceback
                traceback.print_exc()
                if ac.plugin_path is None:
                    raise
        self.set_current_library_information(current_library_name(), db.library_id,
                                             db.field_metadata)

        register_keyboard_shortcuts()
        self.keyboard.finalize()
        self.auto_adder = AutoAdder(gprefs['auto_add_path'], self)

        self.save_layout_state()

        # Collect cycles now
        gc.collect()

        if show_gui and self.gui_debug is not None:
            info_dialog(self, _('Debug mode'), '<p>' +
                    _('You have started calibre in debug mode. After you '
                        'quit calibre, the debug log will be available in '
                        'the file: %s<p>The '
                        'log will be displayed automatically.')%self.gui_debug, show=True)

        self.iactions['Connect Share'].check_smartdevice_menus()
        QTimer.singleShot(1, self.start_smartdevice)
        QTimer.singleShot(100, self.update_toggle_to_tray_action)

    def esc(self, *args):
        self.clear_button.click()

    def shift_esc(self):
        self.current_view().setFocus(Qt.OtherFocusReason)

    def ctrl_esc(self):
        self.apply_virtual_library()
        self.current_view().setFocus(Qt.OtherFocusReason)

    def start_smartdevice(self):
        message = None
        if self.device_manager.get_option('smartdevice', 'autostart'):
            try:
                message = self.device_manager.start_plugin('smartdevice')
            except:
                message = 'start smartdevice unknown exception'
                prints(message)
                import traceback
                traceback.print_exc()
        if message:
            if not self.device_manager.is_running('Wireless Devices'):
                error_dialog(self, _('Problem starting the wireless device'),
                             _('The wireless device driver had problems starting. '
                               'It said "%s"')%message, show=True)
        self.iactions['Connect Share'].set_smartdevice_action_state()

    def start_content_server(self, check_started=True):
        from calibre.library.server.main import start_threaded_server
        from calibre.library.server import server_config
        self.content_server = start_threaded_server(
                self.library_view.model().db, server_config().parse())
        self.content_server.state_callback = Dispatcher(
                self.iactions['Connect Share'].content_server_state_changed)
        if check_started:
            self.content_server.start_failure_callback = \
                Dispatcher(self.content_server_start_failed)

    def content_server_start_failed(self, msg):
        error_dialog(self, _('Failed to start Content Server'),
                _('Could not start the content server. Error:\n\n%s')%msg,
                show=True)

    def resizeEvent(self, ev):
        MainWindow.resizeEvent(self, ev)
        self.search.setMaximumWidth(self.width()-150)

    def create_spare_pool(self, *args):
        if self._spare_pool is None:
            num = min(detect_ncpus(), int(config['worker_limit']/2.0))
            self._spare_pool = Pool(max_workers=num, name='GUIPool')

    def spare_pool(self):
        ans, self._spare_pool = self._spare_pool, None
        QTimer.singleShot(1000, self.create_spare_pool)
        return ans

    def do_proceed(self, func, payload):
        if callable(func):
            func(payload)

    def no_op(self, *args):
        pass

    def system_tray_icon_activated(self, r=False):
        if r in (QSystemTrayIcon.Trigger, QSystemTrayIcon.MiddleClick, False):
            if self.isVisible():
                if self.isMinimized():
                    self.showNormal()
                else:
                    self.hide_windows()
            else:
                self.show_windows()
                if self.isMinimized():
                    self.showNormal()

    @property
    def is_minimized_to_tray(self):
        return getattr(self, '__systray_minimized', False)

    def ask_a_yes_no_question(self, title, msg, det_msg='',
            show_copy_button=False, ans_when_user_unavailable=True,
            skip_dialog_name=None, skipped_value=True):
        if self.is_minimized_to_tray:
            return ans_when_user_unavailable
        return question_dialog(self, title, msg, det_msg=det_msg,
                show_copy_button=show_copy_button,
                skip_dialog_name=skip_dialog_name,
                skip_dialog_skipped_value=skipped_value)

    def update_toggle_to_tray_action(self, *args):
        if hasattr(self, 'toggle_to_tray_action'):
            self.toggle_to_tray_action.setText(
                _('Hide main window') if self.isVisible() else _('Show main window'))

    def hide_windows(self):
        for window in QApplication.topLevelWidgets():
            if isinstance(window, (MainWindow, QDialog)) and \
                    window.isVisible():
                window.hide()
                setattr(window, '__systray_minimized', True)
        self.update_toggle_to_tray_action()

    def show_windows(self, *args):
        for window in QApplication.topLevelWidgets():
            if getattr(window, '__systray_minimized', False):
                window.show()
                setattr(window, '__systray_minimized', False)
        self.update_toggle_to_tray_action()

    def test_server(self, *args):
        if self.content_server is not None and \
                self.content_server.exception is not None:
            error_dialog(self, _('Failed to start content server'),
                         unicode(self.content_server.exception)).exec_()

    @property
    def current_db(self):
        return self.library_view.model().db

    def another_instance_wants_to_talk(self):
        try:
            msg = self.listener.queue.get_nowait()
        except Empty:
            return
        if msg.startswith('launched:'):
            import json
            try:
                argv = json.loads(msg[len('launched:'):])
            except ValueError:
                prints('Failed to decode message from other instance: %r' % msg)
                if DEBUG:
                    error_dialog(self, 'Invalid message',
                                 'Received an invalid message from other calibre instance.'
                                 ' Do you have multiple versions of calibre installed?',
                                 det_msg='Invalid msg: %r' % msg, show=True)
                argv = ()
            if isinstance(argv, (list, tuple)) and len(argv) > 1:
                files = [os.path.abspath(p) for p in argv[1:] if not os.path.isdir(p) and os.access(p, os.R_OK)]
                if files:
                    self.iactions['Add Books'].add_filesystem_book(files)
            self.setWindowState(self.windowState() & ~Qt.WindowMinimized|Qt.WindowActive)
            self.show_windows()
            self.raise_()
            self.activateWindow()
        elif msg.startswith('refreshdb:'):
            m = self.library_view.model()
            m.db.new_api.reload_from_db()
            m.db.data.refresh(clear_caches=False, do_search=False)
            m.resort()
            m.research()
            self.tags_view.recount()
        elif msg.startswith('shutdown:'):
            self.quit(confirm_quit=False)
        elif msg.startswith('bookedited:'):
            parts = msg.split(':')[1:]
            try:
                book_id, fmt, library_id = parts[:3]
                book_id = int(book_id)
                m = self.library_view.model()
                db = m.db.new_api
                if m.db.library_id == library_id and db.has_id(book_id):
                    db.format_metadata(book_id, fmt, allow_cache=False, update_db=True)
                    db.update_last_modified((book_id,))
                    m.refresh_ids((book_id,))
            except Exception:
                import traceback
                traceback.print_exc()
        else:
            print msg

    def current_view(self):
        '''Convenience method that returns the currently visible view '''
        idx = self.stack.currentIndex()
        if idx == 0:
            return self.library_view
        if idx == 1:
            return self.memory_view
        if idx == 2:
            return self.card_a_view
        if idx == 3:
            return self.card_b_view

    def booklists(self):
        return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db

    def library_moved(self, newloc, copy_structure=False, call_close=True,
            allow_rebuild=False):
        if newloc is None:
            return
        default_prefs = None
        try:
            olddb = self.library_view.model().db
            if copy_structure:
                default_prefs = olddb.prefs

            from calibre.utils.formatter_functions import unload_user_template_functions
            unload_user_template_functions(olddb.library_id)
        except:
            olddb = None
        try:
            db = LibraryDatabase(newloc, default_prefs=default_prefs)
        except apsw.Error:
            if not allow_rebuild:
                raise
            import traceback
            repair = question_dialog(self, _('Corrupted database'),
                    _('The library database at %s appears to be corrupted. Do '
                    'you want calibre to try and rebuild it automatically? '
                    'The rebuild may not be completely successful.')
                    % force_unicode(newloc, filesystem_encoding),
                    det_msg=traceback.format_exc()
                    )
            if repair:
                from calibre.gui2.dialogs.restore_library import repair_library_at
                if repair_library_at(newloc, parent=self):
                    db = LibraryDatabase(newloc, default_prefs=default_prefs)
                else:
                    return
            else:
                return
        if self.content_server is not None:
            self.content_server.set_database(db)
        self.library_path = newloc
        prefs['library_path'] = self.library_path
        self.book_on_device(None, reset=True)
        db.set_book_on_device_func(self.book_on_device)
        self.library_view.set_database(db)
        self.tags_view.set_database(db, self.alter_tb)
        self.library_view.model().set_book_on_device_func(self.book_on_device)
        self.status_bar.clear_message()
        self.search.clear()
        self.saved_search.clear()
        self.book_details.reset_info()
        # self.library_view.model().count_changed()
        db = self.library_view.model().db
        self.iactions['Choose Library'].count_changed(db.count())
        self.set_window_title()
        self.apply_named_search_restriction('')  # reset restriction to null
        self.saved_searches_changed(recount=False)  # reload the search restrictions combo box
        if db.prefs['virtual_lib_on_startup']:
            self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
        self.rebuild_vl_tabs()
        for action in self.iactions.values():
            action.library_changed(db)
        if olddb is not None:
            try:
                if call_close:
                    olddb.close()
            except:
                import traceback
                traceback.print_exc()
            olddb.break_cycles()
        if self.device_connected:
            self.set_books_in_library(self.booklists(), reset=True)
            self.refresh_ondevice()
            self.memory_view.reset()
            self.card_a_view.reset()
            self.card_b_view.reset()
        self.set_current_library_information(current_library_name(), db.library_id,
                                             db.field_metadata)
        self.library_view.set_current_row(0)
        # Run a garbage collection now so that it does not freeze the
        # interface later
        gc.collect()

    def set_window_title(self):
        db = self.current_db
        restrictions = [x for x in (db.data.get_base_restriction_name(),
                        db.data.get_search_restriction_name()) if x]
        restrictions = ' :: '.join(restrictions)
        font = QFont()
        if restrictions:
            restrictions = ' :: ' + restrictions
            font.setBold(True)
            font.setItalic(True)
        self.virtual_library.setFont(font)
        title = u'{0} - || {1}{2} ||'.format(
                __appname__, self.iactions['Choose Library'].library_name(), restrictions)
        self.setWindowTitle(title)

    def location_selected(self, location):
        '''
        Called when a location icon is clicked (e.g. Library)
        '''
        page = 0 if location == 'library' else 1 if location == 'main' else 2 if location == 'carda' else 3
        self.stack.setCurrentIndex(page)
        self.book_details.reset_info()
        for x in ('tb', 'cb'):
            splitter = getattr(self, x+'_splitter')
            splitter.button.setEnabled(location == 'library')
        for action in self.iactions.values():
            action.location_selected(location)
        if location == 'library':
            self.virtual_library_menu.setEnabled(True)
            self.highlight_only_button.setEnabled(True)
            self.vl_tabs.setEnabled(True)
        else:
            self.virtual_library_menu.setEnabled(False)
            self.highlight_only_button.setEnabled(False)
            self.vl_tabs.setEnabled(False)
            # Reset the view in case something changed while it was invisible
            self.current_view().reset()
        self.set_number_of_books_shown()
        self.update_status_bar()

    def job_exception(self, job, dialog_title=_('Conversion Error'), retry_func=None):
        if not hasattr(self, '_modeless_dialogs'):
            self._modeless_dialogs = []
        minz = self.is_minimized_to_tray
        if self.isVisible():
            for x in list(self._modeless_dialogs):
                if not x.isVisible():
                    self._modeless_dialogs.remove(x)
        try:
            if 'calibre.ebooks.DRMError' in job.details:
                if not minz:
                    from calibre.gui2.dialogs.drm_error import DRMErrorMessage
                    d = DRMErrorMessage(self, _('Cannot convert') + ' ' +
                        job.description.split(':')[-1].partition('(')[-1][:-1])
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.ebooks.oeb.transforms.split.SplitError' in job.details:
                title = job.description.split(':')[-1].partition('(')[-1][:-1]
                msg = _('<p><b>Failed to convert: %s')%title
                msg += '<p>'+_('''
                Many older ebook reader devices are incapable of displaying
                EPUB files that have internal components over a certain size.
                Therefore, when converting to EPUB, calibre automatically tries
                to split up the EPUB into smaller sized pieces.  For some
                files that are large undifferentiated blocks of text, this
                splitting fails.
                <p>You can <b>work around the problem</b> by either increasing the
                maximum split size under EPUB Output in the conversion dialog,
                or by turning on Heuristic Processing, also in the conversion
                dialog. Note that if you make the maximum split size too large,
                your ebook reader may have trouble with the EPUB.
                        ''')
                if not minz:
                    d = error_dialog(self, _('Conversion Failed'), msg,
                            det_msg=job.details)
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.web.feeds.input.RecipeDisabled' in job.details:
                if not minz:
                    msg = job.details
                    msg = msg[msg.find('calibre.web.feeds.input.RecipeDisabled:'):]
                    msg = msg.partition(':')[-1]
                    d = error_dialog(self, _('Recipe Disabled'),
                        '<p>%s</p>'%msg)
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.ebooks.conversion.ConversionUserFeedBack:' in job.details:
                if not minz:
                    import json
                    payload = job.details.rpartition(
                        'calibre.ebooks.conversion.ConversionUserFeedBack:')[-1]
                    payload = json.loads('{' + payload.partition('{')[-1])
                    d = {'info':info_dialog, 'warn':warning_dialog,
                            'error':error_dialog}.get(payload['level'],
                                    error_dialog)
                    d = d(self, payload['title'],
                            '<p>%s</p>'%payload['msg'],
                            det_msg=payload['det_msg'])
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return
        except:
            pass
        if job.killed:
            return
        try:
            prints(job.details, file=sys.stderr)
        except:
            pass
        if not minz:
            self.job_error_dialog.show_error(dialog_title,
                    _('<b>Failed</b>')+': '+unicode(job.description),
                    det_msg=job.details, retry_func=retry_func)

    def read_settings(self):
        geometry = config['main_window_geometry']
        if geometry is not None:
            self.restoreGeometry(geometry)
        self.read_layout_settings()

    def write_settings(self):
        with gprefs:  # Only write to gprefs once
            config.set('main_window_geometry', self.saveGeometry())
            dynamic.set('sort_history', self.library_view.model().sort_history)
            self.save_layout_state()

    def quit(self, checked=True, restart=False, debug_on_restart=False,
            confirm_quit=True):
        if confirm_quit and not self.confirm_quit():
            return
        try:
            self.shutdown()
        except:
            pass
        self.restart_after_quit = restart
        self.debug_on_restart = debug_on_restart
        QApplication.instance().quit()

    def donate(self, *args):
        open_url(QUrl('http://calibre-ebook.com/donate'))

    def confirm_quit(self):
        if self.job_manager.has_jobs():
            msg = _('There are active jobs. Are you sure you want to quit?')
            if self.job_manager.has_device_jobs():
                msg = '<p>'+__appname__ + \
                      _(''' is communicating with the device!<br>
                      Quitting may cause corruption on the device.<br>
                      Are you sure you want to quit?''')+'</p>'

            if not question_dialog(self, _('Active jobs'), msg):
                return False
        from calibre.db.delete_service import has_jobs
        if has_jobs():
            msg = _('Some deleted books are still being moved to the Recycle '
                    'Bin, if you quit now, they will be left behind. Are you '
                    'sure you want to quit?')
            if not question_dialog(self, _('Active jobs'), msg):
                return False

        return True

    def shutdown(self, write_settings=True):
        self.grid_view.shutdown()
        try:
            db = self.library_view.model().db
            cf = db.clean
        except:
            pass
        else:
            cf()
            # Save the current field_metadata for applications like calibre2opds
            # Goes here, because if cf is valid, db is valid.
            db.new_api.set_pref('field_metadata', db.field_metadata.all_metadata())
            db.commit_dirty_cache()
            db.prefs.write_serialized(prefs['library_path'])
        for action in self.iactions.values():
            if not action.shutting_down():
                return
        if write_settings:
            self.write_settings()
        self.check_messages_timer.stop()
        if hasattr(self, 'update_checker'):
            self.update_checker.shutdown()
        self.listener.close()
        self.job_manager.server.close()
        self.job_manager.threaded_server.close()
        self.device_manager.keep_going = False
        self.auto_adder.stop()
        mb = self.library_view.model().metadata_backup
        if mb is not None:
            mb.stop()

        self.hide_windows()
        try:
            try:
                if self.content_server is not None:
                    s = self.content_server
                    self.content_server = None
                    s.exit()
            except:
                pass
        except KeyboardInterrupt:
            pass
        if self._spare_pool is not None:
            self._spare_pool.shutdown()
        from calibre.db.delete_service import shutdown
        shutdown()
        time.sleep(2)
        self.istores.join()
        self.hide_windows()
        # Do not report any errors that happen after the shutdown
        sys.excepthook = sys.__excepthook__
        return True

    def run_wizard(self, *args):
        if self.confirm_quit():
            self.run_wizard_b4_shutdown = True
            self.restart_after_quit = True
            try:
                self.shutdown(write_settings=False)
            except:
                pass
            QApplication.instance().quit()

    def closeEvent(self, e):
        self.write_settings()
        if self.system_tray_icon is not None and self.system_tray_icon.isVisible():
            if not dynamic['systray_msg'] and not isosx:
                info_dialog(self, 'calibre', 'calibre '+
                        _('will keep running in the system tray. To close it, '
                        'choose <b>Quit</b> in the context menu of the '
                        'system tray.'), show_copy_button=False).exec_()
                dynamic['systray_msg'] = True
            self.hide_windows()
            e.ignore()
        else:
            if self.confirm_quit():
                try:
                    self.shutdown(write_settings=False)
                except:
                    import traceback
                    traceback.print_exc()
                e.accept()
            else:
                e.ignore()
Example #13
0
class Main(MainWindow):

    APP_NAME = _('Tweak Book')

    def __init__(self, opts):
        MainWindow.__init__(self, opts, disable_automatic_gc=True)
        self.boss = Boss(self)
        self.setWindowTitle(self.APP_NAME)
        self.setWindowIcon(QIcon(I('tweak.png')))
        self.opts = opts
        self.path_to_ebook = None
        self.container = None
        self.current_metadata = None
        self.blocking_job = BlockingJob(self)
        self.keyboard = KeyboardManager(parent=self, config_name='shortcuts/tweak')

        self.create_actions()
        self.create_menubar()
        self.create_toolbar()
        self.create_docks()

        self.status_bar = self.statusBar()
        self.l = QLabel('Placeholder')

        self.setCentralWidget(self.l)
        self.boss(self)

        self.keyboard.finalize()

    def create_actions(self):
        group = _('Global Actions')

        def reg(icon, text, target, sid, keys, description):
            ac = QAction(QIcon(I(icon)), text, self)
            ac.triggered.connect(target)
            if isinstance(keys, type('')):
                keys = (keys,)
            self.keyboard.register_shortcut(
                sid, unicode(ac.text()), default_keys=keys, description=description, action=ac, group=group)
            self.addAction(ac)
            return ac

        self.action_open_book = reg('document_open.png', _('Open &book'), self.boss.open_book, 'open-book', 'Ctrl+O', _('Open a new book'))
        self.action_global_undo = reg('back.png', _('&Revert to before'), self.boss.do_global_undo, 'global-undo', 'Ctrl+Left',
                                      _('Revert book to before the last action (Undo)'))
        self.action_global_redo = reg('forward.png', _('&Revert to after'), self.boss.do_global_redo, 'global-redo', 'Ctrl+Right',
                                      _('Revert book state to after the next action (Redo)'))

    def create_menubar(self):
        b = self.menuBar()

        f = b.addMenu(_('&File'))
        f.addAction(self.action_open_book)

        e = b.addMenu(_('&Edit'))
        e.addAction(self.action_global_undo)
        e.addAction(self.action_global_redo)

    def create_toolbar(self):
        self.global_bar = b = self.addToolBar(_('Global'))
        b.addAction(self.action_open_book)
        b.addAction(self.action_global_undo)
        b.addAction(self.action_global_redo)

    def create_docks(self):
        self.file_list_dock = d = QDockWidget(_('&Files Browser'), self)
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
        self.file_list = FileListWidget(d)
        d.setWidget(self.file_list)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)

    def resizeEvent(self, ev):
        self.blocking_job.resize(ev.size())
        return super(Main, self).resizeEvent(ev)

    def update_window_title(self):
        self.setWindowTitle(self.current_metadata.title + ' [%s] - %s' %(current_container().book_type.upper(), self.APP_NAME))
Example #14
0
class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
        TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
        SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin,
        EbookDownloadMixin
        ):
    'The main GUI'

    proceed_requested = pyqtSignal(object, object)

    def __init__(self, opts, parent=None, gui_debug=None):
        global _gui
        MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True)
        self.proceed_requested.connect(self.do_proceed,
                type=Qt.QueuedConnection)
        self.proceed_question = ProceedQuestion(self)
        self.keyboard = Manager(self)
        _gui = self
        self.opts = opts
        self.device_connected = None
        self.gui_debug = gui_debug
        self.iactions = OrderedDict()
        # Actions
        for action in interface_actions():
            if opts.ignore_plugins and action.plugin_path is not None:
                continue
            try:
                ac = self.init_iaction(action)
            except:
                # Ignore errors in loading user supplied plugins
                import traceback
                traceback.print_exc()
                if action.plugin_path is None:
                    raise
                continue
            ac.plugin_path = action.plugin_path
            ac.interface_action_base_plugin = action
            self.add_iaction(ac)
        self.load_store_plugins()

    def init_iaction(self, action):
        ac = action.load_actual_plugin(self)
        ac.plugin_path = action.plugin_path
        ac.interface_action_base_plugin = action
        action.actual_iaction_plugin_loaded = True
        return ac

    def add_iaction(self, ac):
        acmap = self.iactions
        if ac.name in acmap:
            if ac.priority >= acmap[ac.name].priority:
                acmap[ac.name] = ac
        else:
            acmap[ac.name] = ac

    def load_store_plugins(self):
        self.istores = OrderedDict()
        for store in available_store_plugins():
            if self.opts.ignore_plugins and store.plugin_path is not None:
                continue
            try:
                st = self.init_istore(store)
                self.add_istore(st)
            except:
                # Ignore errors in loading user supplied plugins
                import traceback
                traceback.print_exc()
                if store.plugin_path is None:
                    raise
                continue

    def init_istore(self, store):
        st = store.load_actual_plugin(self)
        st.plugin_path = store.plugin_path
        st.base_plugin = store
        store.actual_istore_plugin_loaded = True
        return st

    def add_istore(self, st):
        stmap = self.istores
        if st.name in stmap:
            if st.priority >= stmap[st.name].priority:
                stmap[st.name] = st
        else:
            stmap[st.name] = st


    def initialize(self, library_path, db, listener, actions, show_gui=True):
        opts = self.opts
        self.preferences_action, self.quit_action = actions
        self.library_path = library_path
        self.content_server = None
        self.spare_servers = []
        self.must_restart_before_config = False
        # Initialize fontconfig in a separate thread as this can be a lengthy
        # process if run for the first time on this machine
        from calibre.utils.fonts import fontconfig
        self.fc = fontconfig
        self.listener = Listener(listener)
        self.check_messages_timer = QTimer()
        self.connect(self.check_messages_timer, SIGNAL('timeout()'),
                self.another_instance_wants_to_talk)
        self.check_messages_timer.start(1000)

        for ac in self.iactions.values():
            ac.do_genesis()
        self.donate_action = QAction(QIcon(I('donate.png')),
                _('&Donate to support calibre'), self)
        for st in self.istores.values():
            st.do_genesis()
        MainWindowMixin.__init__(self, db)

        # Jobs Button {{{
        self.job_manager = JobManager()
        self.jobs_dialog = JobsDialog(self, self.job_manager)
        self.jobs_button = JobsButton(horizontal=True, parent=self)
        self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
        # }}}

        LayoutMixin.__init__(self)
        EmailMixin.__init__(self)
        EbookDownloadMixin.__init__(self)
        DeviceMixin.__init__(self)

        self.progress_indicator = ProgressIndicator(self)
        self.progress_indicator.pos = (0, 20)
        self.verbose = opts.verbose
        self.get_metadata = GetMetadata()
        self.upload_memory = {}
        self.metadata_dialogs = []
        self.default_thumbnail = None
        self.tb_wrapper = textwrap.TextWrapper(width=40)
        self.viewers = collections.deque()
        self.system_tray_icon = SystemTrayIcon(QIcon(I('lt.png')), self)
        self.system_tray_icon.setToolTip('calibre')
        self.system_tray_icon.tooltip_requested.connect(
                self.job_manager.show_tooltip)
        if not config['systray_icon']:
            self.system_tray_icon.hide()
        else:
            self.system_tray_icon.show()
        self.system_tray_menu = QMenu(self)
        self.restore_action = self.system_tray_menu.addAction(
                QIcon(I('page.png')), _('&Restore'))
        self.system_tray_menu.addAction(self.donate_action)
        self.donate_button.setDefaultAction(self.donate_action)
        self.donate_button.setStatusTip(self.donate_button.toolTip())
        self.eject_action = self.system_tray_menu.addAction(
                QIcon(I('eject.png')), _('&Eject connected device'))
        self.eject_action.setEnabled(False)
        self.addAction(self.quit_action)
        self.system_tray_menu.addAction(self.quit_action)
        self.keyboard.register_shortcut('quit calibre', _('Quit calibre'),
                default_keys=('Ctrl+Q',), action=self.quit_action)
        self.system_tray_icon.setContextMenu(self.system_tray_menu)
        self.connect(self.quit_action, SIGNAL('triggered(bool)'), self.quit)
        self.connect(self.donate_action, SIGNAL('triggered(bool)'), self.donate)
        self.connect(self.restore_action, SIGNAL('triggered()'),
                        self.show_windows)
        self.system_tray_icon.activated.connect(
            self.system_tray_icon_activated)

        self.esc_action = QAction(self)
        self.addAction(self.esc_action)
        self.keyboard.register_shortcut('clear current search',
                _('Clear the current search'), default_keys=('Esc',),
                action=self.esc_action)
        self.esc_action.triggered.connect(self.esc)

        ####################### Start spare job server ########################
        QTimer.singleShot(1000, self.add_spare_server)

        ####################### Location Manager ########################
        self.location_manager.location_selected.connect(self.location_selected)
        self.location_manager.unmount_device.connect(self.device_manager.umount_device)
        self.location_manager.configure_device.connect(self.configure_connected_device)
        self.eject_action.triggered.connect(self.device_manager.umount_device)

        #################### Update notification ###################
        UpdateMixin.__init__(self, opts)

        ####################### Search boxes ########################
        SavedSearchBoxMixin.__init__(self)
        SearchBoxMixin.__init__(self)

        ####################### Library view ########################
        LibraryViewMixin.__init__(self, db)

        if show_gui:
            self.show()

        if self.system_tray_icon.isVisible() and opts.start_in_tray:
            self.hide_windows()
        self.library_view.model().count_changed_signal.connect(
                self.iactions['Choose Library'].count_changed)
        if not gprefs.get('quick_start_guide_added', False):
            from calibre.ebooks.metadata.meta import get_metadata
            mi = get_metadata(open(P('quick_start.epub'), 'rb'), 'epub')
            self.library_view.model().add_books([P('quick_start.epub')], ['epub'],
                    [mi])
            gprefs['quick_start_guide_added'] = True
            self.library_view.model().books_added(1)
            if hasattr(self, 'db_images'):
                self.db_images.reset()
            if self.library_view.model().rowCount(None) < 3:
                self.library_view.resizeColumnsToContents()

        self.library_view.model().count_changed()
        self.bars_manager.database_changed(self.library_view.model().db)
        self.library_view.model().database_changed.connect(self.bars_manager.database_changed,
                type=Qt.QueuedConnection)

        ########################### Tags Browser ##############################
        TagBrowserMixin.__init__(self, db)

        ######################### Search Restriction ##########################
        SearchRestrictionMixin.__init__(self)
        if db.prefs['gui_restriction']:
            self.apply_named_search_restriction(db.prefs['gui_restriction'])

        ########################### Cover Flow ################################

        CoverFlowMixin.__init__(self)

        self._calculated_available_height = min(max_available_height()-15,
                self.height())
        self.resize(self.width(), self._calculated_available_height)

        self.build_context_menus()

        for ac in self.iactions.values():
            try:
                ac.gui_layout_complete()
            except:
                import traceback
                traceback.print_exc()
                if ac.plugin_path is None:
                    raise

        if config['autolaunch_server']:
            self.start_content_server()

        self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)

        self.read_settings()
        self.finalize_layout()
        if self.bars_manager.showing_donate:
            self.donate_button.start_animation()
        self.set_window_title()

        for ac in self.iactions.values():
            try:
                ac.initialization_complete()
            except:
                import traceback
                traceback.print_exc()
                if ac.plugin_path is None:
                    raise
        self.device_manager.set_current_library_uuid(db.library_id)

        self.keyboard.finalize()
        self.auto_adder = AutoAdder(gprefs['auto_add_path'], self)

        # Collect cycles now
        gc.collect()

        if show_gui and self.gui_debug is not None:
            info_dialog(self, _('Debug mode'), '<p>' +
                    _('You have started calibre in debug mode. After you '
                        'quit calibre, the debug log will be available in '
                        'the file: %s<p>The '
                        'log will be displayed automatically.')%self.gui_debug, show=True)

    def esc(self, *args):
        self.clear_button.click()

    def start_content_server(self, check_started=True):
        from calibre.library.server.main import start_threaded_server
        from calibre.library.server import server_config
        self.content_server = start_threaded_server(
                self.library_view.model().db, server_config().parse())
        self.content_server.state_callback = Dispatcher(
                self.iactions['Connect Share'].content_server_state_changed)
        if check_started:
            self.content_server.start_failure_callback = \
                Dispatcher(self.content_server_start_failed)

    def content_server_start_failed(self, msg):
        error_dialog(self, _('Failed to start Content Server'),
                _('Could not start the content server. Error:\n\n%s')%msg,
                show=True)

    def resizeEvent(self, ev):
        MainWindow.resizeEvent(self, ev)
        self.search.setMaximumWidth(self.width()-150)

    def add_spare_server(self, *args):
        self.spare_servers.append(Server(limit=int(config['worker_limit']/2.0)))

    @property
    def spare_server(self):
        # Because of the use of the property decorator, we're called one
        # extra time. Ignore.
        if not hasattr(self, '__spare_server_property_limiter'):
            self.__spare_server_property_limiter = True
            return None
        try:
            QTimer.singleShot(1000, self.add_spare_server)
            return self.spare_servers.pop()
        except:
            pass

    def do_proceed(self, func, payload):
        if callable(func):
            func(payload)

    def no_op(self, *args):
        pass

    def system_tray_icon_activated(self, r):
        if r == QSystemTrayIcon.Trigger:
            if self.isVisible():
                self.hide_windows()
            else:
                self.show_windows()

    @property
    def is_minimized_to_tray(self):
        return getattr(self, '__systray_minimized', False)

    def ask_a_yes_no_question(self, title, msg, det_msg='',
            show_copy_button=False, ans_when_user_unavailable=True,
            skip_dialog_name=None, skipped_value=True):
        if self.is_minimized_to_tray:
            return ans_when_user_unavailable
        return question_dialog(self, title, msg, det_msg=det_msg,
                show_copy_button=show_copy_button,
                skip_dialog_name=skip_dialog_name,
                skip_dialog_skipped_value=skipped_value)

    def hide_windows(self):
        for window in QApplication.topLevelWidgets():
            if isinstance(window, (MainWindow, QDialog)) and \
                    window.isVisible():
                window.hide()
                setattr(window, '__systray_minimized', True)

    def show_windows(self):
        for window in QApplication.topLevelWidgets():
            if getattr(window, '__systray_minimized', False):
                window.show()
                setattr(window, '__systray_minimized', False)

    def test_server(self, *args):
        if self.content_server is not None and \
                self.content_server.exception is not None:
            error_dialog(self, _('Failed to start content server'),
                         unicode(self.content_server.exception)).exec_()

    @property
    def current_db(self):
        return self.library_view.model().db

    def another_instance_wants_to_talk(self):
        try:
            msg = self.listener.queue.get_nowait()
        except Empty:
            return
        if msg.startswith('launched:'):
            argv = eval(msg[len('launched:'):])
            if len(argv) > 1:
                path = os.path.abspath(argv[1])
                if os.access(path, os.R_OK):
                    self.iactions['Add Books'].add_filesystem_book(path)
            self.setWindowState(self.windowState() & \
                    ~Qt.WindowMinimized|Qt.WindowActive)
            self.show_windows()
            self.raise_()
            self.activateWindow()
        elif msg.startswith('refreshdb:'):
            self.library_view.model().refresh()
            self.library_view.model().research()
            self.tags_view.recount()
            self.library_view.model().db.refresh_format_cache()
        elif msg.startswith('shutdown:'):
            self.quit(confirm_quit=False)
        else:
            print msg

    def current_view(self):
        '''Convenience method that returns the currently visible view '''
        idx = self.stack.currentIndex()
        if idx == 0:
            return self.library_view
        if idx == 1:
            return self.memory_view
        if idx == 2:
            return self.card_a_view
        if idx == 3:
            return self.card_b_view

    def booklists(self):
        return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db

    def library_moved(self, newloc, copy_structure=False, call_close=True,
            allow_rebuild=False):
        if newloc is None: return
        default_prefs = None
        try:
            olddb = self.library_view.model().db
            if copy_structure:
                default_prefs = olddb.prefs
        except:
            olddb = None
        try:
            db = LibraryDatabase2(newloc, default_prefs=default_prefs)
        except (DatabaseException, sqlite.Error):
            if not allow_rebuild: raise
            import traceback
            repair = question_dialog(self, _('Corrupted database'),
                    _('The library database at %s appears to be corrupted. Do '
                    'you want calibre to try and rebuild it automatically? '
                    'The rebuild may not be completely successful.')
                    % force_unicode(newloc, filesystem_encoding),
                    det_msg=traceback.format_exc()
                    )
            if repair:
                from calibre.gui2.dialogs.restore_library import repair_library_at
                if repair_library_at(newloc, parent=self):
                    db = LibraryDatabase2(newloc, default_prefs=default_prefs)
                else:
                    return
            else:
                return
        if self.content_server is not None:
            self.content_server.set_database(db)
        self.library_path = newloc
        prefs['library_path'] = self.library_path
        self.book_on_device(None, reset=True)
        db.set_book_on_device_func(self.book_on_device)
        self.library_view.set_database(db)
        self.tags_view.set_database(db, self.alter_tb)
        self.library_view.model().set_book_on_device_func(self.book_on_device)
        self.status_bar.clear_message()
        self.search.clear()
        self.saved_search.clear()
        self.book_details.reset_info()
        #self.library_view.model().count_changed()
        db = self.library_view.model().db
        self.iactions['Choose Library'].count_changed(db.count())
        self.set_window_title()
        self.apply_named_search_restriction('') # reset restriction to null
        self.saved_searches_changed(recount=False) # reload the search restrictions combo box
        self.apply_named_search_restriction(db.prefs['gui_restriction'])
        for action in self.iactions.values():
            action.library_changed(db)
        if olddb is not None:
            try:
                if call_close:
                    olddb.conn.close()
            except:
                import traceback
                traceback.print_exc()
            olddb.break_cycles()
        if self.device_connected:
            self.set_books_in_library(self.booklists(), reset=True)
            self.refresh_ondevice()
            self.memory_view.reset()
            self.card_a_view.reset()
            self.card_b_view.reset()
        self.device_manager.set_current_library_uuid(db.library_id)
        self.library_view.set_current_row(0)
        # Run a garbage collection now so that it does not freeze the
        # interface later
        gc.collect()


    def set_window_title(self):
        self.setWindowTitle(__appname__ + u' - || %s ||'%self.iactions['Choose Library'].library_name())

    def location_selected(self, location):
        '''
        Called when a location icon is clicked (e.g. Library)
        '''
        page = 0 if location == 'library' else 1 if location == 'main' else 2 if location == 'carda' else 3
        self.stack.setCurrentIndex(page)
        self.book_details.reset_info()
        for x in ('tb', 'cb'):
            splitter = getattr(self, x+'_splitter')
            splitter.button.setEnabled(location == 'library')
        for action in self.iactions.values():
            action.location_selected(location)
        if location == 'library':
            self.search_restriction.setEnabled(True)
            self.highlight_only_button.setEnabled(True)
        else:
            self.search_restriction.setEnabled(False)
            self.highlight_only_button.setEnabled(False)
            # Reset the view in case something changed while it was invisible
            self.current_view().reset()
        self.set_number_of_books_shown()



    def job_exception(self, job, dialog_title=_('Conversion Error')):
        if not hasattr(self, '_modeless_dialogs'):
            self._modeless_dialogs = []
        minz = self.is_minimized_to_tray
        if self.isVisible():
            for x in list(self._modeless_dialogs):
                if not x.isVisible():
                    self._modeless_dialogs.remove(x)
        try:
            if 'calibre.ebooks.DRMError' in job.details:
                if not minz:
                    from calibre.gui2.dialogs.drm_error import DRMErrorMessage
                    d = DRMErrorMessage(self, _('Cannot convert') + ' ' +
                        job.description.split(':')[-1].partition('(')[-1][:-1])
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.ebooks.oeb.transforms.split.SplitError' in job.details:
                title = job.description.split(':')[-1].partition('(')[-1][:-1]
                msg = _('<p><b>Failed to convert: %s')%title
                msg += '<p>'+_('''
                Many older ebook reader devices are incapable of displaying
                EPUB files that have internal components over a certain size.
                Therefore, when converting to EPUB, calibre automatically tries
                to split up the EPUB into smaller sized pieces.  For some
                files that are large undifferentiated blocks of text, this
                splitting fails.
                <p>You can <b>work around the problem</b> by either increasing the
                maximum split size under EPUB Output in the conversion dialog,
                or by turning on Heuristic Processing, also in the conversion
                dialog. Note that if you make the maximum split size too large,
                your ebook reader may have trouble with the EPUB.
                        ''')
                if not minz:
                    d = error_dialog(self, _('Conversion Failed'), msg,
                            det_msg=job.details)
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.web.feeds.input.RecipeDisabled' in job.details:
                if not minz:
                    msg = job.details
                    msg = msg[msg.find('calibre.web.feeds.input.RecipeDisabled:'):]
                    msg = msg.partition(':')[-1]
                    d = error_dialog(self, _('Recipe Disabled'),
                        '<p>%s</p>'%msg)
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.ebooks.conversion.ConversionUserFeedBack:' in job.details:
                if not minz:
                    import json
                    payload = job.details.rpartition(
                        'calibre.ebooks.conversion.ConversionUserFeedBack:')[-1]
                    payload = json.loads('{' + payload.partition('{')[-1])
                    d = {'info':info_dialog, 'warn':warning_dialog,
                            'error':error_dialog}.get(payload['level'],
                                    error_dialog)
                    d = d(self, payload['title'],
                            '<p>%s</p>'%payload['msg'],
                            det_msg=payload['det_msg'])
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return
        except:
            pass
        if job.killed:
            return
        try:
            prints(job.details, file=sys.stderr)
        except:
            pass
        if not minz:
            d = error_dialog(self, dialog_title,
                    _('<b>Failed</b>')+': '+unicode(job.description),
                    det_msg=job.details)
            d.setModal(False)
            d.show()
            self._modeless_dialogs.append(d)

    def read_settings(self):
        geometry = config['main_window_geometry']
        if geometry is not None:
            self.restoreGeometry(geometry)
        self.read_layout_settings()

    def write_settings(self):
        config.set('main_window_geometry', self.saveGeometry())
        dynamic.set('sort_history', self.library_view.model().sort_history)
        self.save_layout_state()

    def quit(self, checked=True, restart=False, debug_on_restart=False,
            confirm_quit=True):
        if confirm_quit and not self.confirm_quit():
            return
        try:
            self.shutdown()
        except:
            pass
        self.restart_after_quit = restart
        self.debug_on_restart = debug_on_restart
        QApplication.instance().quit()

    def donate(self, *args):
        open_url(QUrl('http://calibre-ebook.com/donate'))

    def confirm_quit(self):
        if self.job_manager.has_jobs():
            msg = _('There are active jobs. Are you sure you want to quit?')
            if self.job_manager.has_device_jobs():
                msg = '<p>'+__appname__ + \
                      _(''' is communicating with the device!<br>
                      Quitting may cause corruption on the device.<br>
                      Are you sure you want to quit?''')+'</p>'

            if not question_dialog(self, _('Active jobs'), msg):
                return False
        return True


    def shutdown(self, write_settings=True):
        try:
            db = self.library_view.model().db
            cf = db.clean
        except:
            pass
        else:
            cf()
            # Save the current field_metadata for applications like calibre2opds
            # Goes here, because if cf is valid, db is valid.
            db.prefs['field_metadata'] = db.field_metadata.all_metadata()
            db.commit_dirty_cache()
            db.prefs.write_serialized(prefs['library_path'])
        for action in self.iactions.values():
            if not action.shutting_down():
                return
        if write_settings:
            self.write_settings()
        self.check_messages_timer.stop()
        self.update_checker.terminate()
        self.listener.close()
        self.job_manager.server.close()
        self.job_manager.threaded_server.close()
        while self.spare_servers:
            self.spare_servers.pop().close()
        self.device_manager.keep_going = False
        self.auto_adder.stop()
        mb = self.library_view.model().metadata_backup
        if mb is not None:
            mb.stop()

        self.hide_windows()
        try:
            try:
                if self.content_server is not None:
                    s = self.content_server
                    self.content_server = None
                    s.exit()
            except:
                pass
        except KeyboardInterrupt:
            pass
        time.sleep(2)
        self.hide_windows()
        # Do not report any errors that happen after the shutdown
        sys.excepthook = sys.__excepthook__
        return True

    def run_wizard(self, *args):
        if self.confirm_quit():
            self.run_wizard_b4_shutdown = True
            self.restart_after_quit = True
            try:
                self.shutdown(write_settings=False)
            except:
                pass
            QApplication.instance().quit()



    def closeEvent(self, e):
        self.write_settings()
        if self.system_tray_icon.isVisible():
            if not dynamic['systray_msg'] and not isosx:
                info_dialog(self, 'calibre', 'calibre '+ \
                        _('will keep running in the system tray. To close it, '
                        'choose <b>Quit</b> in the context menu of the '
                        'system tray.'), show_copy_button=False).exec_()
                dynamic['systray_msg'] = True
            self.hide_windows()
            e.ignore()
        else:
            if self.confirm_quit():
                try:
                    self.shutdown(write_settings=False)
                except:
                    pass
                e.accept()
            else:
                e.ignore()
Example #15
0
class Main(
        MainWindow,
        MainWindowMixin,
        DeviceMixin,
        EmailMixin,  # {{{
        TagBrowserMixin,
        CoverFlowMixin,
        LibraryViewMixin,
        SearchBoxMixin,
        SavedSearchBoxMixin,
        SearchRestrictionMixin,
        LayoutMixin,
        UpdateMixin,
        EbookDownloadMixin):

    'The main GUI'

    proceed_requested = pyqtSignal(object, object)
    book_converted = pyqtSignal(object, object)
    shutting_down = False

    def __init__(self, opts, parent=None, gui_debug=None):
        global _gui
        MainWindow.__init__(self,
                            opts,
                            parent=parent,
                            disable_automatic_gc=True)
        self.setWindowIcon(QApplication.instance().windowIcon())
        self.jobs_pointer = Pointer(self)
        self.proceed_requested.connect(self.do_proceed,
                                       type=Qt.QueuedConnection)
        self.proceed_question = ProceedQuestion(self)
        self.job_error_dialog = JobError(self)
        self.keyboard = Manager(self)
        _gui = self
        self.opts = opts
        self.device_connected = None
        self.gui_debug = gui_debug
        self.iactions = OrderedDict()
        # Actions
        for action in interface_actions():
            if opts.ignore_plugins and action.plugin_path is not None:
                continue
            try:
                ac = self.init_iaction(action)
            except:
                # Ignore errors in loading user supplied plugins
                import traceback
                traceback.print_exc()
                if action.plugin_path is None:
                    raise
                continue
            ac.plugin_path = action.plugin_path
            ac.interface_action_base_plugin = action
            self.add_iaction(ac)
        self.load_store_plugins()

    def init_iaction(self, action):
        ac = action.load_actual_plugin(self)
        ac.plugin_path = action.plugin_path
        ac.interface_action_base_plugin = action
        action.actual_iaction_plugin_loaded = True
        return ac

    def add_iaction(self, ac):
        acmap = self.iactions
        if ac.name in acmap:
            if ac.priority >= acmap[ac.name].priority:
                acmap[ac.name] = ac
        else:
            acmap[ac.name] = ac

    def load_store_plugins(self):
        from calibre.gui2.store.loader import Stores
        self.istores = Stores()
        for store in available_store_plugins():
            if self.opts.ignore_plugins and store.plugin_path is not None:
                continue
            try:
                st = self.init_istore(store)
                self.add_istore(st)
            except:
                # Ignore errors in loading user supplied plugins
                import traceback
                traceback.print_exc()
                if store.plugin_path is None:
                    raise
                continue
        self.istores.builtins_loaded()

    def init_istore(self, store):
        st = store.load_actual_plugin(self)
        st.plugin_path = store.plugin_path
        st.base_plugin = store
        store.actual_istore_plugin_loaded = True
        return st

    def add_istore(self, st):
        stmap = self.istores
        if st.name in stmap:
            if st.priority >= stmap[st.name].priority:
                stmap[st.name] = st
        else:
            stmap[st.name] = st

    def initialize(self, library_path, db, listener, actions, show_gui=True):
        opts = self.opts
        self.preferences_action, self.quit_action = actions
        self.library_path = library_path
        self.content_server = None
        self._spare_pool = None
        self.must_restart_before_config = False
        self.listener = Listener(listener)
        self.check_messages_timer = QTimer()
        self.check_messages_timer.timeout.connect(
            self.another_instance_wants_to_talk)
        self.check_messages_timer.start(1000)

        for ac in self.iactions.values():
            try:
                ac.do_genesis()
            except Exception:
                # Ignore errors in third party plugins
                import traceback
                traceback.print_exc()
                if getattr(ac, 'plugin_path', None) is None:
                    raise
        self.donate_action = QAction(QIcon(I('donate.png')),
                                     _('&Donate to support calibre'), self)
        for st in self.istores.values():
            st.do_genesis()
        MainWindowMixin.init_main_window_mixin(self, db)

        # Jobs Button {{{
        self.job_manager = JobManager()
        self.jobs_dialog = JobsDialog(self, self.job_manager)
        self.jobs_button = JobsButton(horizontal=True, parent=self)
        self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
        # }}}

        LayoutMixin.init_layout_mixin(self)
        DeviceMixin.init_device_mixin(self)

        self.progress_indicator = ProgressIndicator(self)
        self.progress_indicator.pos = (0, 20)
        self.verbose = opts.verbose
        self.get_metadata = GetMetadata()
        self.upload_memory = {}
        self.metadata_dialogs = []
        self.default_thumbnail = None
        self.tb_wrapper = textwrap.TextWrapper(width=40)
        self.viewers = collections.deque()
        self.system_tray_icon = None
        if config['systray_icon']:
            self.system_tray_icon = factory(
                app_id='com.calibre-ebook.gui').create_system_tray_icon(
                    parent=self, title='calibre')
        if self.system_tray_icon is not None:
            self.system_tray_icon.setIcon(
                QIcon(I('lt.png', allow_user_override=False)))
            if not (iswindows or isosx):
                self.system_tray_icon.setIcon(
                    QIcon.fromTheme('calibre-gui',
                                    self.system_tray_icon.icon()))
            self.system_tray_icon.setToolTip(self.jobs_button.tray_tooltip())
            self.system_tray_icon.setVisible(True)
            self.jobs_button.tray_tooltip_updated.connect(
                self.system_tray_icon.setToolTip)
        elif config['systray_icon']:
            prints(
                'Failed to create system tray icon, your desktop environment probably does not support the StatusNotifier spec'
            )
        self.system_tray_menu = QMenu(self)
        self.toggle_to_tray_action = self.system_tray_menu.addAction(
            QIcon(I('page.png')), '')
        self.toggle_to_tray_action.triggered.connect(
            self.system_tray_icon_activated)
        self.system_tray_menu.addAction(self.donate_action)
        self.donate_button.clicked.connect(self.donate_action.trigger)
        self.donate_button.setToolTip(self.donate_action.text().replace(
            '&', ''))
        self.donate_button.setIcon(self.donate_action.icon())
        self.donate_button.setStatusTip(self.donate_button.toolTip())
        self.eject_action = self.system_tray_menu.addAction(
            QIcon(I('eject.png')), _('&Eject connected device'))
        self.eject_action.setEnabled(False)
        self.addAction(self.quit_action)
        self.system_tray_menu.addAction(self.quit_action)
        self.keyboard.register_shortcut('quit calibre',
                                        _('Quit calibre'),
                                        default_keys=('Ctrl+Q', ),
                                        action=self.quit_action)
        if self.system_tray_icon is not None:
            self.system_tray_icon.setContextMenu(self.system_tray_menu)
            self.system_tray_icon.activated.connect(
                self.system_tray_icon_activated)
        self.quit_action.triggered[bool].connect(self.quit)
        self.donate_action.triggered[bool].connect(self.donate)
        self.minimize_action = QAction(_('Minimize the calibre window'), self)
        self.addAction(self.minimize_action)
        self.keyboard.register_shortcut('minimize calibre',
                                        self.minimize_action.text(),
                                        default_keys=(),
                                        action=self.minimize_action)
        self.minimize_action.triggered.connect(self.showMinimized)

        self.esc_action = QAction(self)
        self.addAction(self.esc_action)
        self.keyboard.register_shortcut('clear current search',
                                        _('Clear the current search'),
                                        default_keys=('Esc', ),
                                        action=self.esc_action)
        self.esc_action.triggered.connect(self.esc)

        self.shift_esc_action = QAction(self)
        self.addAction(self.shift_esc_action)
        self.keyboard.register_shortcut('focus book list',
                                        _('Focus the book list'),
                                        default_keys=('Shift+Esc', ),
                                        action=self.shift_esc_action)
        self.shift_esc_action.triggered.connect(self.shift_esc)

        self.ctrl_esc_action = QAction(self)
        self.addAction(self.ctrl_esc_action)
        self.keyboard.register_shortcut('clear virtual library',
                                        _('Clear the virtual library'),
                                        default_keys=('Ctrl+Esc', ),
                                        action=self.ctrl_esc_action)
        self.ctrl_esc_action.triggered.connect(self.ctrl_esc)

        self.alt_esc_action = QAction(self)
        self.addAction(self.alt_esc_action)
        self.keyboard.register_shortcut('clear additional restriction',
                                        _('Clear the additional restriction'),
                                        default_keys=('Alt+Esc', ),
                                        action=self.alt_esc_action)
        self.alt_esc_action.triggered.connect(
            self.clear_additional_restriction)

        # ###################### Start spare job server ########################
        QTimer.singleShot(1000, self.create_spare_pool)

        # ###################### Location Manager ########################
        self.location_manager.location_selected.connect(self.location_selected)
        self.location_manager.unmount_device.connect(
            self.device_manager.umount_device)
        self.location_manager.configure_device.connect(
            self.configure_connected_device)
        self.location_manager.update_device_metadata.connect(
            self.update_metadata_on_device)
        self.eject_action.triggered.connect(self.device_manager.umount_device)

        # ################### Update notification ###################
        UpdateMixin.init_update_mixin(self, opts)

        # ###################### Search boxes ########################
        SearchRestrictionMixin.init_search_restriction_mixin(self)
        SavedSearchBoxMixin.init_saved_seach_box_mixin(self)

        # ###################### Library view ########################
        LibraryViewMixin.init_library_view_mixin(self, db)
        SearchBoxMixin.init_search_box_mixin(self)  # Requires current_db

        if show_gui:
            self.show()

        if self.system_tray_icon is not None and self.system_tray_icon.isVisible(
        ) and opts.start_in_tray:
            self.hide_windows()
        self.library_view.model().count_changed_signal.connect(
            self.iactions['Choose Library'].count_changed)
        if not gprefs.get('quick_start_guide_added', False):
            try:
                add_quick_start_guide(self.library_view)
            except:
                import traceback
                traceback.print_exc()
        for view in ('library', 'memory', 'card_a', 'card_b'):
            v = getattr(self, '%s_view' % view)
            v.selectionModel().selectionChanged.connect(self.update_status_bar)
            v.model().count_changed_signal.connect(self.update_status_bar)

        self.library_view.model().count_changed()
        self.bars_manager.database_changed(self.library_view.model().db)
        self.library_view.model().database_changed.connect(
            self.bars_manager.database_changed, type=Qt.QueuedConnection)

        # ########################## Tags Browser ##############################
        TagBrowserMixin.init_tag_browser_mixin(self, db)

        # ######################## Search Restriction ##########################
        if db.prefs['virtual_lib_on_startup']:
            self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
        self.rebuild_vl_tabs()

        # ########################## Cover Flow ################################

        CoverFlowMixin.init_cover_flow_mixin(self)

        self._calculated_available_height = min(max_available_height() - 15,
                                                self.height())
        self.resize(self.width(), self._calculated_available_height)

        self.build_context_menus()

        for ac in self.iactions.values():
            try:
                ac.gui_layout_complete()
            except:
                import traceback
                traceback.print_exc()
                if ac.plugin_path is None:
                    raise

        if config['autolaunch_server']:
            self.start_content_server()

        self.read_settings()
        self.finalize_layout()
        if self.bars_manager.showing_donate:
            self.donate_button.start_animation()
        self.set_window_title()

        for ac in self.iactions.values():
            try:
                ac.initialization_complete()
            except:
                import traceback
                traceback.print_exc()
                if ac.plugin_path is None:
                    raise
        self.set_current_library_information(current_library_name(),
                                             db.library_id, db.field_metadata)

        register_keyboard_shortcuts()
        self.keyboard.finalize()
        self.auto_adder = AutoAdder(gprefs['auto_add_path'], self)

        self.save_layout_state()

        # Collect cycles now
        gc.collect()

        QApplication.instance().shutdown_signal_received.connect(self.quit)
        if show_gui and self.gui_debug is not None:
            QTimer.singleShot(10, self.show_gui_debug_msg)

        self.iactions['Connect Share'].check_smartdevice_menus()
        QTimer.singleShot(1, self.start_smartdevice)
        QTimer.singleShot(100, self.update_toggle_to_tray_action)

    def show_gui_debug_msg(self):
        info_dialog(self,
                    _('Debug mode'),
                    '<p>' +
                    _('You have started calibre in debug mode. After you '
                      'quit calibre, the debug log will be available in '
                      'the file: %s<p>The '
                      'log will be displayed automatically.') % self.gui_debug,
                    show=True)

    def esc(self, *args):
        self.clear_button.click()

    def shift_esc(self):
        self.current_view().setFocus(Qt.OtherFocusReason)

    def ctrl_esc(self):
        self.apply_virtual_library()
        self.current_view().setFocus(Qt.OtherFocusReason)

    def start_smartdevice(self):
        message = None
        if self.device_manager.get_option('smartdevice', 'autostart'):
            try:
                message = self.device_manager.start_plugin('smartdevice')
            except:
                message = 'start smartdevice unknown exception'
                prints(message)
                import traceback
                traceback.print_exc()
        if message:
            if not self.device_manager.is_running('Wireless Devices'):
                error_dialog(
                    self,
                    _('Problem starting the wireless device'),
                    _('The wireless device driver had problems starting. '
                      'It said "%s"') % message,
                    show=True)
        self.iactions['Connect Share'].set_smartdevice_action_state()

    def start_content_server(self, check_started=True):
        from calibre.library.server.main import start_threaded_server
        from calibre.library.server import server_config
        self.content_server = start_threaded_server(
            self.library_view.model().db,
            server_config().parse())
        self.content_server.state_callback = Dispatcher(
            self.iactions['Connect Share'].content_server_state_changed)
        if check_started:
            self.content_server.start_failure_callback = \
                Dispatcher(self.content_server_start_failed)

    def content_server_start_failed(self, msg):
        error_dialog(self,
                     _('Failed to start Content Server'),
                     _('Could not start the content server. Error:\n\n%s') %
                     msg,
                     show=True)

    def resizeEvent(self, ev):
        MainWindow.resizeEvent(self, ev)
        self.search.setMaximumWidth(self.width() - 150)

    def create_spare_pool(self, *args):
        if self._spare_pool is None:
            num = min(detect_ncpus(), int(config['worker_limit'] / 2.0))
            self._spare_pool = Pool(max_workers=num, name='GUIPool')

    def spare_pool(self):
        ans, self._spare_pool = self._spare_pool, None
        QTimer.singleShot(1000, self.create_spare_pool)
        return ans

    def do_proceed(self, func, payload):
        if callable(func):
            func(payload)

    def no_op(self, *args):
        pass

    def system_tray_icon_activated(self, r=False):
        if r in (QSystemTrayIcon.Trigger, QSystemTrayIcon.MiddleClick, False):
            if self.isVisible():
                if self.isMinimized():
                    self.showNormal()
                else:
                    self.hide_windows()
            else:
                self.show_windows()
                if self.isMinimized():
                    self.showNormal()

    @property
    def is_minimized_to_tray(self):
        return getattr(self, '__systray_minimized', False)

    def ask_a_yes_no_question(self,
                              title,
                              msg,
                              det_msg='',
                              show_copy_button=False,
                              ans_when_user_unavailable=True,
                              skip_dialog_name=None,
                              skipped_value=True):
        if self.is_minimized_to_tray:
            return ans_when_user_unavailable
        return question_dialog(self,
                               title,
                               msg,
                               det_msg=det_msg,
                               show_copy_button=show_copy_button,
                               skip_dialog_name=skip_dialog_name,
                               skip_dialog_skipped_value=skipped_value)

    def update_toggle_to_tray_action(self, *args):
        if hasattr(self, 'toggle_to_tray_action'):
            self.toggle_to_tray_action.setText(
                _('Hide main window') if self.isVisible(
                ) else _('Show main window'))

    def hide_windows(self):
        for window in QApplication.topLevelWidgets():
            if isinstance(window, (MainWindow, QDialog)) and \
                    window.isVisible():
                window.hide()
                setattr(window, '__systray_minimized', True)
        self.update_toggle_to_tray_action()

    def show_windows(self, *args):
        for window in QApplication.topLevelWidgets():
            if getattr(window, '__systray_minimized', False):
                window.show()
                setattr(window, '__systray_minimized', False)
        self.update_toggle_to_tray_action()

    def test_server(self, *args):
        if self.content_server is not None and \
                self.content_server.exception is not None:
            error_dialog(self, _('Failed to start content server'),
                         unicode(self.content_server.exception)).exec_()

    @property
    def current_db(self):
        return self.library_view.model().db

    def another_instance_wants_to_talk(self):
        try:
            msg = self.listener.queue.get_nowait()
        except Empty:
            return
        if msg.startswith('launched:'):
            import json
            try:
                argv = json.loads(msg[len('launched:'):])
            except ValueError:
                prints('Failed to decode message from other instance: %r' %
                       msg)
                if DEBUG:
                    error_dialog(
                        self,
                        'Invalid message',
                        'Received an invalid message from other calibre instance.'
                        ' Do you have multiple versions of calibre installed?',
                        det_msg='Invalid msg: %r' % msg,
                        show=True)
                argv = ()
            if isinstance(argv, (list, tuple)) and len(argv) > 1:
                files = [
                    os.path.abspath(p) for p in argv[1:]
                    if not os.path.isdir(p) and os.access(p, os.R_OK)
                ]
                if files:
                    self.iactions['Add Books'].add_filesystem_book(files)
            self.setWindowState(self.windowState() & ~Qt.WindowMinimized
                                | Qt.WindowActive)
            self.show_windows()
            self.raise_()
            self.activateWindow()
        elif msg.startswith('refreshdb:'):
            m = self.library_view.model()
            m.db.new_api.reload_from_db()
            m.db.data.refresh(clear_caches=False, do_search=False)
            m.resort()
            m.research()
            self.tags_view.recount()
        elif msg.startswith('shutdown:'):
            self.quit(confirm_quit=False)
        elif msg.startswith('bookedited:'):
            parts = msg.split(':')[1:]
            try:
                book_id, fmt, library_id = parts[:3]
                book_id = int(book_id)
                m = self.library_view.model()
                db = m.db.new_api
                if m.db.library_id == library_id and db.has_id(book_id):
                    db.format_metadata(book_id,
                                       fmt,
                                       allow_cache=False,
                                       update_db=True)
                    db.update_last_modified((book_id, ))
                    m.refresh_ids((book_id, ))
            except Exception:
                import traceback
                traceback.print_exc()
        else:
            print msg

    def current_view(self):
        '''Convenience method that returns the currently visible view '''
        idx = self.stack.currentIndex()
        if idx == 0:
            return self.library_view
        if idx == 1:
            return self.memory_view
        if idx == 2:
            return self.card_a_view
        if idx == 3:
            return self.card_b_view

    def booklists(self):
        return self.memory_view.model().db, self.card_a_view.model(
        ).db, self.card_b_view.model().db

    def library_moved(self,
                      newloc,
                      copy_structure=False,
                      call_close=True,
                      allow_rebuild=False):
        if newloc is None:
            return
        default_prefs = None
        try:
            olddb = self.library_view.model().db
            if copy_structure:
                default_prefs = olddb.prefs
        except:
            olddb = None
        try:
            db = LibraryDatabase(newloc, default_prefs=default_prefs)
        except apsw.Error:
            if not allow_rebuild:
                raise
            import traceback
            repair = question_dialog(
                self,
                _('Corrupted database'),
                _('The library database at %s appears to be corrupted. Do '
                  'you want calibre to try and rebuild it automatically? '
                  'The rebuild may not be completely successful.') %
                force_unicode(newloc, filesystem_encoding),
                det_msg=traceback.format_exc())
            if repair:
                from calibre.gui2.dialogs.restore_library import repair_library_at
                if repair_library_at(newloc, parent=self):
                    db = LibraryDatabase(newloc, default_prefs=default_prefs)
                else:
                    return
            else:
                return
        if self.content_server is not None:
            self.content_server.set_database(db)
        self.library_path = newloc
        prefs['library_path'] = self.library_path
        self.book_on_device(None, reset=True)
        db.set_book_on_device_func(self.book_on_device)
        self.library_view.set_database(db)
        self.tags_view.set_database(db, self.alter_tb)
        self.library_view.model().set_book_on_device_func(self.book_on_device)
        self.status_bar.clear_message()
        self.search.clear()
        self.saved_search.clear()
        self.book_details.reset_info()
        # self.library_view.model().count_changed()
        db = self.library_view.model().db
        self.iactions['Choose Library'].count_changed(db.count())
        self.set_window_title()
        self.apply_named_search_restriction('')  # reset restriction to null
        self.saved_searches_changed(
            recount=False)  # reload the search restrictions combo box
        if db.prefs['virtual_lib_on_startup']:
            self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
        self.rebuild_vl_tabs()
        for action in self.iactions.values():
            action.library_changed(db)
        if olddb is not None:
            try:
                if call_close:
                    olddb.close()
            except:
                import traceback
                traceback.print_exc()
            olddb.break_cycles()
        if self.device_connected:
            self.set_books_in_library(self.booklists(), reset=True)
            self.refresh_ondevice()
            self.memory_view.reset()
            self.card_a_view.reset()
            self.card_b_view.reset()
        self.set_current_library_information(current_library_name(),
                                             db.library_id, db.field_metadata)
        self.library_view.set_current_row(0)
        # Run a garbage collection now so that it does not freeze the
        # interface later
        gc.collect()

    def set_window_title(self):
        db = self.current_db
        restrictions = [
            x for x in (db.data.get_base_restriction_name(),
                        db.data.get_search_restriction_name()) if x
        ]
        restrictions = ' :: '.join(restrictions)
        font = QFont()
        if restrictions:
            restrictions = ' :: ' + restrictions
            font.setBold(True)
            font.setItalic(True)
        self.virtual_library.setFont(font)
        title = u'{0} - || {1}{2} ||'.format(
            __appname__, self.iactions['Choose Library'].library_name(),
            restrictions)
        self.setWindowTitle(title)

    def location_selected(self, location):
        '''
        Called when a location icon is clicked (e.g. Library)
        '''
        page = 0 if location == 'library' else 1 if location == 'main' else 2 if location == 'carda' else 3
        self.stack.setCurrentIndex(page)
        self.book_details.reset_info()
        for x in ('tb', 'cb'):
            splitter = getattr(self, x + '_splitter')
            splitter.button.setEnabled(location == 'library')
        for action in self.iactions.values():
            action.location_selected(location)
        if location == 'library':
            self.virtual_library_menu.setEnabled(True)
            self.highlight_only_button.setEnabled(True)
            self.vl_tabs.setEnabled(True)
        else:
            self.virtual_library_menu.setEnabled(False)
            self.highlight_only_button.setEnabled(False)
            self.vl_tabs.setEnabled(False)
            # Reset the view in case something changed while it was invisible
            self.current_view().reset()
        self.set_number_of_books_shown()
        self.update_status_bar()

    def job_exception(self,
                      job,
                      dialog_title=_('Conversion Error'),
                      retry_func=None):
        if not hasattr(self, '_modeless_dialogs'):
            self._modeless_dialogs = []
        minz = self.is_minimized_to_tray
        if self.isVisible():
            for x in list(self._modeless_dialogs):
                if not x.isVisible():
                    self._modeless_dialogs.remove(x)
        try:
            if 'calibre.ebooks.DRMError' in job.details:
                if not minz:
                    from calibre.gui2.dialogs.drm_error import DRMErrorMessage
                    d = DRMErrorMessage(
                        self,
                        _('Cannot convert') + ' ' +
                        job.description.split(':')[-1].partition('(')[-1][:-1])
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.ebooks.oeb.transforms.split.SplitError' in job.details:
                title = job.description.split(':')[-1].partition('(')[-1][:-1]
                msg = _('<p><b>Failed to convert: %s') % title
                msg += '<p>' + _('''
                Many older ebook reader devices are incapable of displaying
                EPUB files that have internal components over a certain size.
                Therefore, when converting to EPUB, calibre automatically tries
                to split up the EPUB into smaller sized pieces.  For some
                files that are large undifferentiated blocks of text, this
                splitting fails.
                <p>You can <b>work around the problem</b> by either increasing the
                maximum split size under EPUB Output in the conversion dialog,
                or by turning on Heuristic Processing, also in the conversion
                dialog. Note that if you make the maximum split size too large,
                your ebook reader may have trouble with the EPUB.
                        ''')
                if not minz:
                    d = error_dialog(self,
                                     _('Conversion Failed'),
                                     msg,
                                     det_msg=job.details)
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.web.feeds.input.RecipeDisabled' in job.details:
                if not minz:
                    msg = job.details
                    msg = msg[msg.
                              find('calibre.web.feeds.input.RecipeDisabled:'):]
                    msg = msg.partition(':')[-1]
                    d = error_dialog(self, _('Recipe Disabled'),
                                     '<p>%s</p>' % msg)
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.ebooks.conversion.ConversionUserFeedBack:' in job.details:
                if not minz:
                    import json
                    payload = job.details.rpartition(
                        'calibre.ebooks.conversion.ConversionUserFeedBack:'
                    )[-1]
                    payload = json.loads('{' + payload.partition('{')[-1])
                    d = {
                        'info': info_dialog,
                        'warn': warning_dialog,
                        'error': error_dialog
                    }.get(payload['level'], error_dialog)
                    d = d(self,
                          payload['title'],
                          '<p>%s</p>' % payload['msg'],
                          det_msg=payload['det_msg'])
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return
        except:
            pass
        if job.killed:
            return
        try:
            prints(job.details, file=sys.stderr)
        except:
            pass
        if not minz:
            self.job_error_dialog.show_error(dialog_title,
                                             _('<b>Failed</b>') + ': ' +
                                             unicode(job.description),
                                             det_msg=job.details,
                                             retry_func=retry_func)

    def read_settings(self):
        geometry = config['main_window_geometry']
        if geometry is not None:
            self.restoreGeometry(geometry)
        self.read_layout_settings()

    def write_settings(self):
        with gprefs:  # Only write to gprefs once
            config.set('main_window_geometry', self.saveGeometry())
            dynamic.set('sort_history', self.library_view.model().sort_history)
            self.save_layout_state()

    def quit(self,
             checked=True,
             restart=False,
             debug_on_restart=False,
             confirm_quit=True):
        if self.shutting_down:
            return
        if confirm_quit and not self.confirm_quit():
            return
        try:
            self.shutdown()
        except:
            pass
        self.restart_after_quit = restart
        self.debug_on_restart = debug_on_restart
        QApplication.instance().quit()

    def donate(self, *args):
        open_url(QUrl('https://calibre-ebook.com/donate'))

    def confirm_quit(self):
        if self.job_manager.has_jobs():
            msg = _('There are active jobs. Are you sure you want to quit?')
            if self.job_manager.has_device_jobs():
                msg = '<p>'+__appname__ + \
                      _(''' is communicating with the device!<br>
                      Quitting may cause corruption on the device.<br>
                      Are you sure you want to quit?''')+'</p>'

            if not question_dialog(self, _('Active jobs'), msg):
                return False

        if self.proceed_question.questions:
            msg = _(
                'There are library updates waiting. Are you sure you want to quit?'
            )
            if not question_dialog(self, _('Library Updates Waiting'), msg):
                return False

        from calibre.db.delete_service import has_jobs
        if has_jobs():
            msg = _('Some deleted books are still being moved to the Recycle '
                    'Bin, if you quit now, they will be left behind. Are you '
                    'sure you want to quit?')
            if not question_dialog(self, _('Active jobs'), msg):
                return False

        return True

    def shutdown(self, write_settings=True):
        self.shutting_down = True
        self.show_shutdown_message()

        from calibre.customize.ui import has_library_closed_plugins
        if has_library_closed_plugins():
            self.show_shutdown_message(
                _('Running database shutdown plugins. This could take a few seconds...'
                  ))

        self.grid_view.shutdown()
        db = None
        try:
            db = self.library_view.model().db
            cf = db.clean
        except:
            pass
        else:
            cf()
            # Save the current field_metadata for applications like calibre2opds
            # Goes here, because if cf is valid, db is valid.
            db.new_api.set_pref('field_metadata',
                                db.field_metadata.all_metadata())
            db.commit_dirty_cache()
            db.prefs.write_serialized(prefs['library_path'])
        for action in self.iactions.values():
            if not action.shutting_down():
                return
        if write_settings:
            self.write_settings()
        self.check_messages_timer.stop()
        if hasattr(self, 'update_checker'):
            self.update_checker.shutdown()
        self.listener.close()
        self.job_manager.server.close()
        self.job_manager.threaded_server.close()
        self.device_manager.keep_going = False
        self.auto_adder.stop()
        # Do not report any errors that happen after the shutdown
        # We cannot restore the original excepthook as that causes PyQt to
        # call abort() on unhandled exceptions
        import traceback

        def eh(t, v, tb):
            try:
                traceback.print_exception(t, v, tb, file=sys.stderr)
            except:
                pass

        sys.excepthook = eh

        mb = self.library_view.model().metadata_backup
        if mb is not None:
            mb.stop()

        if db is not None:
            db.close()

        try:
            try:
                if self.content_server is not None:
                    # If the content server has any sockets being closed then
                    # this can take quite a long time (minutes). Tell the user that it is
                    # happening.
                    self.show_shutdown_message(
                        _('Shutting down the content server. This could take a while ...'
                          ))
                    s = self.content_server
                    self.content_server = None
                    s.exit()
            except:
                pass
        except KeyboardInterrupt:
            pass
        self.hide_windows()
        if self._spare_pool is not None:
            self._spare_pool.shutdown()
        from calibre.db.delete_service import shutdown
        shutdown()
        time.sleep(2)
        self.istores.join()
        return True

    def run_wizard(self, *args):
        if self.confirm_quit():
            self.run_wizard_b4_shutdown = True
            self.restart_after_quit = True
            try:
                self.shutdown(write_settings=False)
            except:
                pass
            QApplication.instance().quit()

    def closeEvent(self, e):
        if self.shutting_down:
            return
        self.write_settings()
        if self.system_tray_icon is not None and self.system_tray_icon.isVisible(
        ):
            if not dynamic['systray_msg'] and not isosx:
                info_dialog(
                    self,
                    'calibre',
                    'calibre ' +
                    _('will keep running in the system tray. To close it, '
                      'choose <b>Quit</b> in the context menu of the '
                      'system tray.'),
                    show_copy_button=False).exec_()
                dynamic['systray_msg'] = True
            self.hide_windows()
            e.ignore()
        else:
            if self.confirm_quit():
                try:
                    self.shutdown(write_settings=False)
                except:
                    import traceback
                    traceback.print_exc()
                e.accept()
            else:
                e.ignore()
Example #16
0
class Main(
    MainWindow,
    MainWindowMixin,
    DeviceMixin,
    EmailMixin,  # {{{
    TagBrowserMixin,
    CoverFlowMixin,
    LibraryViewMixin,
    SearchBoxMixin,
    SavedSearchBoxMixin,
    SearchRestrictionMixin,
    LayoutMixin,
    UpdateMixin,
    EbookDownloadMixin,
):
    "The main GUI"

    proceed_requested = pyqtSignal(object, object)

    def __init__(self, opts, parent=None, gui_debug=None):
        global _gui
        MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True)
        self.jobs_pointer = Pointer(self)
        self.proceed_requested.connect(self.do_proceed, type=Qt.QueuedConnection)
        self.proceed_question = ProceedQuestion(self)
        self.job_error_dialog = JobError(self)
        self.keyboard = Manager(self)
        _gui = self
        self.opts = opts
        self.device_connected = None
        self.gui_debug = gui_debug
        self.iactions = OrderedDict()
        # Actions
        for action in interface_actions():
            if opts.ignore_plugins and action.plugin_path is not None:
                continue
            try:
                ac = self.init_iaction(action)
            except:
                # Ignore errors in loading user supplied plugins
                import traceback

                traceback.print_exc()
                if action.plugin_path is None:
                    raise
                continue
            ac.plugin_path = action.plugin_path
            ac.interface_action_base_plugin = action
            self.add_iaction(ac)
        self.load_store_plugins()

    def init_iaction(self, action):
        ac = action.load_actual_plugin(self)
        ac.plugin_path = action.plugin_path
        ac.interface_action_base_plugin = action
        action.actual_iaction_plugin_loaded = True
        return ac

    def add_iaction(self, ac):
        acmap = self.iactions
        if ac.name in acmap:
            if ac.priority >= acmap[ac.name].priority:
                acmap[ac.name] = ac
        else:
            acmap[ac.name] = ac

    def load_store_plugins(self):
        from calibre.gui2.store.loader import Stores

        self.istores = Stores()
        for store in available_store_plugins():
            if self.opts.ignore_plugins and store.plugin_path is not None:
                continue
            try:
                st = self.init_istore(store)
                self.add_istore(st)
            except:
                # Ignore errors in loading user supplied plugins
                import traceback

                traceback.print_exc()
                if store.plugin_path is None:
                    raise
                continue
        self.istores.builtins_loaded()

    def init_istore(self, store):
        st = store.load_actual_plugin(self)
        st.plugin_path = store.plugin_path
        st.base_plugin = store
        store.actual_istore_plugin_loaded = True
        return st

    def add_istore(self, st):
        stmap = self.istores
        if st.name in stmap:
            if st.priority >= stmap[st.name].priority:
                stmap[st.name] = st
        else:
            stmap[st.name] = st

    def initialize(self, library_path, db, listener, actions, show_gui=True):
        opts = self.opts
        self.preferences_action, self.quit_action = actions
        self.library_path = library_path
        self.content_server = None
        self.spare_servers = []
        self.must_restart_before_config = False
        self.listener = Listener(listener)
        self.check_messages_timer = QTimer()
        self.connect(self.check_messages_timer, SIGNAL("timeout()"), self.another_instance_wants_to_talk)
        self.check_messages_timer.start(1000)

        for ac in self.iactions.values():
            try:
                ac.do_genesis()
            except Exception:
                # Ignore errors in third party plugins
                import traceback

                traceback.print_exc()
                if getattr(ac, "plugin_path", None) is None:
                    raise
        self.donate_action = QAction(QIcon(I("donate.png")), _("&Donate to support calibre"), self)
        for st in self.istores.values():
            st.do_genesis()
        MainWindowMixin.__init__(self, db)

        # Jobs Button {{{
        self.job_manager = JobManager()
        self.jobs_dialog = JobsDialog(self, self.job_manager)
        self.jobs_button = JobsButton(horizontal=True, parent=self)
        self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
        # }}}

        LayoutMixin.__init__(self)
        EmailMixin.__init__(self)
        EbookDownloadMixin.__init__(self)
        DeviceMixin.__init__(self)

        self.progress_indicator = ProgressIndicator(self)
        self.progress_indicator.pos = (0, 20)
        self.verbose = opts.verbose
        self.get_metadata = GetMetadata()
        self.upload_memory = {}
        self.metadata_dialogs = []
        self.default_thumbnail = None
        self.tb_wrapper = textwrap.TextWrapper(width=40)
        self.viewers = collections.deque()
        self.system_tray_icon = SystemTrayIcon(QIcon(I("lt.png")), self)
        self.system_tray_icon.setToolTip("calibre")
        self.system_tray_icon.tooltip_requested.connect(self.job_manager.show_tooltip)
        if not config["systray_icon"]:
            self.system_tray_icon.hide()
        else:
            self.system_tray_icon.show()
        self.system_tray_menu = QMenu(self)
        self.restore_action = self.system_tray_menu.addAction(QIcon(I("page.png")), _("&Restore"))
        self.system_tray_menu.addAction(self.donate_action)
        self.donate_button.setDefaultAction(self.donate_action)
        self.donate_button.setStatusTip(self.donate_button.toolTip())
        self.eject_action = self.system_tray_menu.addAction(QIcon(I("eject.png")), _("&Eject connected device"))
        self.eject_action.setEnabled(False)
        self.addAction(self.quit_action)
        self.system_tray_menu.addAction(self.quit_action)
        self.keyboard.register_shortcut(
            "quit calibre", _("Quit calibre"), default_keys=("Ctrl+Q",), action=self.quit_action
        )
        self.system_tray_icon.setContextMenu(self.system_tray_menu)
        self.connect(self.quit_action, SIGNAL("triggered(bool)"), self.quit)
        self.connect(self.donate_action, SIGNAL("triggered(bool)"), self.donate)
        self.connect(self.restore_action, SIGNAL("triggered()"), self.show_windows)
        self.system_tray_icon.activated.connect(self.system_tray_icon_activated)

        self.esc_action = QAction(self)
        self.addAction(self.esc_action)
        self.keyboard.register_shortcut(
            "clear current search", _("Clear the current search"), default_keys=("Esc",), action=self.esc_action
        )
        self.esc_action.triggered.connect(self.esc)

        self.shift_esc_action = QAction(self)
        self.addAction(self.shift_esc_action)
        self.keyboard.register_shortcut(
            "focus book list", _("Focus the book list"), default_keys=("Shift+Esc",), action=self.shift_esc_action
        )
        self.shift_esc_action.triggered.connect(self.shift_esc)

        self.ctrl_esc_action = QAction(self)
        self.addAction(self.ctrl_esc_action)
        self.keyboard.register_shortcut(
            "clear virtual library",
            _("Clear the virtual library"),
            default_keys=("Ctrl+Esc",),
            action=self.ctrl_esc_action,
        )
        self.ctrl_esc_action.triggered.connect(self.ctrl_esc)

        self.alt_esc_action = QAction(self)
        self.addAction(self.alt_esc_action)
        self.keyboard.register_shortcut(
            "clear additional restriction",
            _("Clear the additional restriction"),
            default_keys=("Alt+Esc",),
            action=self.alt_esc_action,
        )
        self.alt_esc_action.triggered.connect(self.clear_additional_restriction)

        ####################### Start spare job server ########################
        QTimer.singleShot(1000, self.add_spare_server)

        ####################### Location Manager ########################
        self.location_manager.location_selected.connect(self.location_selected)
        self.location_manager.unmount_device.connect(self.device_manager.umount_device)
        self.location_manager.configure_device.connect(self.configure_connected_device)
        self.location_manager.update_device_metadata.connect(self.update_metadata_on_device)
        self.eject_action.triggered.connect(self.device_manager.umount_device)

        #################### Update notification ###################
        UpdateMixin.__init__(self, opts)

        ####################### Search boxes ########################
        SearchRestrictionMixin.__init__(self)
        SavedSearchBoxMixin.__init__(self)

        ####################### Library view ########################
        LibraryViewMixin.__init__(self, db)
        SearchBoxMixin.__init__(self)  # Requires current_db

        if show_gui:
            self.show()

        if self.system_tray_icon.isVisible() and opts.start_in_tray:
            self.hide_windows()
        self.library_view.model().count_changed_signal.connect(self.iactions["Choose Library"].count_changed)
        if not gprefs.get("quick_start_guide_added", False):
            from calibre.ebooks.metadata.meta import get_metadata

            mi = get_metadata(open(P("quick_start.epub"), "rb"), "epub")
            self.library_view.model().add_books([P("quick_start.epub")], ["epub"], [mi])
            gprefs["quick_start_guide_added"] = True
            self.library_view.model().books_added(1)
            if hasattr(self, "db_images"):
                self.db_images.reset()
            if self.library_view.model().rowCount(None) < 3:
                self.library_view.resizeColumnsToContents()

        for view in ("library", "memory", "card_a", "card_b"):
            v = getattr(self, "%s_view" % view)
            v.selectionModel().selectionChanged.connect(self.update_status_bar)
            v.model().count_changed_signal.connect(self.update_status_bar)

        self.library_view.model().count_changed()
        self.bars_manager.database_changed(self.library_view.model().db)
        self.library_view.model().database_changed.connect(self.bars_manager.database_changed, type=Qt.QueuedConnection)

        ########################### Tags Browser ##############################
        TagBrowserMixin.__init__(self, db)

        ######################### Search Restriction ##########################
        if db.prefs["virtual_lib_on_startup"]:
            self.apply_virtual_library(db.prefs["virtual_lib_on_startup"])
        self.rebuild_vl_tabs()

        ########################### Cover Flow ################################

        CoverFlowMixin.__init__(self)

        self._calculated_available_height = min(max_available_height() - 15, self.height())
        self.resize(self.width(), self._calculated_available_height)

        self.build_context_menus()

        for ac in self.iactions.values():
            try:
                ac.gui_layout_complete()
            except:
                import traceback

                traceback.print_exc()
                if ac.plugin_path is None:
                    raise

        if config["autolaunch_server"]:
            self.start_content_server()

        self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)

        self.read_settings()
        self.finalize_layout()
        if self.bars_manager.showing_donate:
            self.donate_button.start_animation()
        self.set_window_title()

        for ac in self.iactions.values():
            try:
                ac.initialization_complete()
            except:
                import traceback

                traceback.print_exc()
                if ac.plugin_path is None:
                    raise
        self.device_manager.set_current_library_uuid(db.library_id)

        self.keyboard.finalize()
        self.auto_adder = AutoAdder(gprefs["auto_add_path"], self)

        self.save_layout_state()

        # Collect cycles now
        gc.collect()

        if show_gui and self.gui_debug is not None:
            info_dialog(
                self,
                _("Debug mode"),
                "<p>"
                + _(
                    "You have started calibre in debug mode. After you "
                    "quit calibre, the debug log will be available in "
                    "the file: %s<p>The "
                    "log will be displayed automatically."
                )
                % self.gui_debug,
                show=True,
            )

        self.iactions["Connect Share"].check_smartdevice_menus()
        QTimer.singleShot(1, self.start_smartdevice)

    def esc(self, *args):
        self.clear_button.click()

    def shift_esc(self):
        self.current_view().setFocus(Qt.OtherFocusReason)

    def ctrl_esc(self):
        self.apply_virtual_library()
        self.current_view().setFocus(Qt.OtherFocusReason)

    def start_smartdevice(self):
        message = None
        if self.device_manager.get_option("smartdevice", "autostart"):
            try:
                message = self.device_manager.start_plugin("smartdevice")
            except:
                message = "start smartdevice unknown exception"
                prints(message)
                import traceback

                traceback.print_exc()
        if message:
            if not self.device_manager.is_running("Wireless Devices"):
                error_dialog(
                    self,
                    _("Problem starting the wireless device"),
                    _("The wireless device driver had problems starting. " 'It said "%s"') % message,
                    show=True,
                )
        self.iactions["Connect Share"].set_smartdevice_action_state()

    def start_content_server(self, check_started=True):
        from calibre.library.server.main import start_threaded_server
        from calibre.library.server import server_config

        self.content_server = start_threaded_server(self.library_view.model().db, server_config().parse())
        self.content_server.state_callback = Dispatcher(self.iactions["Connect Share"].content_server_state_changed)
        if check_started:
            self.content_server.start_failure_callback = Dispatcher(self.content_server_start_failed)

    def content_server_start_failed(self, msg):
        error_dialog(
            self,
            _("Failed to start Content Server"),
            _("Could not start the content server. Error:\n\n%s") % msg,
            show=True,
        )

    def resizeEvent(self, ev):
        MainWindow.resizeEvent(self, ev)
        self.search.setMaximumWidth(self.width() - 150)

    def add_spare_server(self, *args):
        self.spare_servers.append(Server(limit=int(config["worker_limit"] / 2.0)))

    @property
    def spare_server(self):
        # Because of the use of the property decorator, we're called one
        # extra time. Ignore.
        if not hasattr(self, "__spare_server_property_limiter"):
            self.__spare_server_property_limiter = True
            return None
        try:
            QTimer.singleShot(1000, self.add_spare_server)
            return self.spare_servers.pop()
        except:
            pass

    def do_proceed(self, func, payload):
        if callable(func):
            func(payload)

    def no_op(self, *args):
        pass

    def system_tray_icon_activated(self, r):
        if r == QSystemTrayIcon.Trigger:
            if self.isVisible():
                self.hide_windows()
            else:
                self.show_windows()

    @property
    def is_minimized_to_tray(self):
        return getattr(self, "__systray_minimized", False)

    def ask_a_yes_no_question(
        self,
        title,
        msg,
        det_msg="",
        show_copy_button=False,
        ans_when_user_unavailable=True,
        skip_dialog_name=None,
        skipped_value=True,
    ):
        if self.is_minimized_to_tray:
            return ans_when_user_unavailable
        return question_dialog(
            self,
            title,
            msg,
            det_msg=det_msg,
            show_copy_button=show_copy_button,
            skip_dialog_name=skip_dialog_name,
            skip_dialog_skipped_value=skipped_value,
        )

    def hide_windows(self):
        for window in QApplication.topLevelWidgets():
            if isinstance(window, (MainWindow, QDialog)) and window.isVisible():
                window.hide()
                setattr(window, "__systray_minimized", True)

    def show_windows(self):
        for window in QApplication.topLevelWidgets():
            if getattr(window, "__systray_minimized", False):
                window.show()
                setattr(window, "__systray_minimized", False)

    def test_server(self, *args):
        if self.content_server is not None and self.content_server.exception is not None:
            error_dialog(self, _("Failed to start content server"), unicode(self.content_server.exception)).exec_()

    @property
    def current_db(self):
        return self.library_view.model().db

    def another_instance_wants_to_talk(self):
        try:
            msg = self.listener.queue.get_nowait()
        except Empty:
            return
        if msg.startswith("launched:"):
            import json

            try:
                argv = json.loads(msg[len("launched:") :])
            except ValueError:
                prints("Failed to decode message from other instance: %r" % msg)
                if DEBUG:
                    error_dialog(
                        self,
                        "Invalid message",
                        "Received an invalid message from other calibre instance."
                        " Do you have multiple versions of calibre installed?",
                        det_msg="Invalid msg: %r" % msg,
                        show=True,
                    )
                argv = ()
            if isinstance(argv, (list, tuple)) and len(argv) > 1:
                files = [os.path.abspath(p) for p in argv[1:] if not os.path.isdir(p) and os.access(p, os.R_OK)]
                if files:
                    self.iactions["Add Books"].add_filesystem_book(files)
            self.setWindowState(self.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
            self.show_windows()
            self.raise_()
            self.activateWindow()
        elif msg.startswith("refreshdb:"):
            m = self.library_view.model()
            m.db.new_api.reload_from_db()
            m.db.data.refresh(clear_caches=False, do_search=False)
            m.resort()
            m.research()
            self.tags_view.recount()
        elif msg.startswith("shutdown:"):
            self.quit(confirm_quit=False)
        elif msg.startswith("bookedited:"):
            parts = msg.split(":")[1:]
            try:
                book_id, fmt, library_id = parts[:3]
                book_id = int(book_id)
                m = self.library_view.model()
                db = m.db.new_api
                if m.db.library_id == library_id and db.has_id(book_id):
                    db.format_metadata(book_id, fmt, allow_cache=False, update_db=True)
                    db.update_last_modified((book_id,))
                    m.refresh_ids((book_id,))
            except Exception:
                import traceback

                traceback.print_exc()
        else:
            print msg

    def current_view(self):
        """Convenience method that returns the currently visible view """
        idx = self.stack.currentIndex()
        if idx == 0:
            return self.library_view
        if idx == 1:
            return self.memory_view
        if idx == 2:
            return self.card_a_view
        if idx == 3:
            return self.card_b_view

    def booklists(self):
        return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db

    def library_moved(self, newloc, copy_structure=False, call_close=True, allow_rebuild=False):
        if newloc is None:
            return
        default_prefs = None
        try:
            olddb = self.library_view.model().db
            if copy_structure:
                default_prefs = olddb.prefs

            from calibre.utils.formatter_functions import unload_user_template_functions

            unload_user_template_functions(olddb.library_id)
        except:
            olddb = None
        try:
            db = LibraryDatabase(newloc, default_prefs=default_prefs)
        except apsw.Error:
            if not allow_rebuild:
                raise
            import traceback

            repair = question_dialog(
                self,
                _("Corrupted database"),
                _(
                    "The library database at %s appears to be corrupted. Do "
                    "you want calibre to try and rebuild it automatically? "
                    "The rebuild may not be completely successful."
                )
                % force_unicode(newloc, filesystem_encoding),
                det_msg=traceback.format_exc(),
            )
            if repair:
                from calibre.gui2.dialogs.restore_library import repair_library_at

                if repair_library_at(newloc, parent=self):
                    db = LibraryDatabase(newloc, default_prefs=default_prefs)
                else:
                    return
            else:
                return
        if self.content_server is not None:
            self.content_server.set_database(db)
        self.library_path = newloc
        prefs["library_path"] = self.library_path
        self.book_on_device(None, reset=True)
        db.set_book_on_device_func(self.book_on_device)
        self.library_view.set_database(db)
        self.tags_view.set_database(db, self.alter_tb)
        self.library_view.model().set_book_on_device_func(self.book_on_device)
        self.status_bar.clear_message()
        self.search.clear()
        self.saved_search.clear()
        self.book_details.reset_info()
        # self.library_view.model().count_changed()
        db = self.library_view.model().db
        self.iactions["Choose Library"].count_changed(db.count())
        self.set_window_title()
        self.apply_named_search_restriction("")  # reset restriction to null
        self.saved_searches_changed(recount=False)  # reload the search restrictions combo box
        if db.prefs["virtual_lib_on_startup"]:
            self.apply_virtual_library(db.prefs["virtual_lib_on_startup"])
        self.rebuild_vl_tabs()
        for action in self.iactions.values():
            action.library_changed(db)
        if olddb is not None:
            try:
                if call_close:
                    olddb.close()
            except:
                import traceback

                traceback.print_exc()
            olddb.break_cycles()
        if self.device_connected:
            self.set_books_in_library(self.booklists(), reset=True)
            self.refresh_ondevice()
            self.memory_view.reset()
            self.card_a_view.reset()
            self.card_b_view.reset()
        self.device_manager.set_current_library_uuid(db.library_id)
        self.library_view.set_current_row(0)
        # Run a garbage collection now so that it does not freeze the
        # interface later
        gc.collect()

    def set_window_title(self):
        db = self.current_db
        restrictions = [x for x in (db.data.get_base_restriction_name(), db.data.get_search_restriction_name()) if x]
        restrictions = " :: ".join(restrictions)
        font = QFont()
        if restrictions:
            restrictions = " :: " + restrictions
            font.setBold(True)
            font.setItalic(True)
        self.virtual_library.setFont(font)
        title = u"{0} - || {1}{2} ||".format(__appname__, self.iactions["Choose Library"].library_name(), restrictions)
        self.setWindowTitle(title)

    def location_selected(self, location):
        """
        Called when a location icon is clicked (e.g. Library)
        """
        page = 0 if location == "library" else 1 if location == "main" else 2 if location == "carda" else 3
        self.stack.setCurrentIndex(page)
        self.book_details.reset_info()
        for x in ("tb", "cb"):
            splitter = getattr(self, x + "_splitter")
            splitter.button.setEnabled(location == "library")
        for action in self.iactions.values():
            action.location_selected(location)
        if location == "library":
            self.virtual_library_menu.setEnabled(True)
            self.highlight_only_button.setEnabled(True)
        else:
            self.virtual_library_menu.setEnabled(False)
            self.highlight_only_button.setEnabled(False)
            # Reset the view in case something changed while it was invisible
            self.current_view().reset()
        self.set_number_of_books_shown()
        self.update_status_bar()

    def job_exception(self, job, dialog_title=_("Conversion Error")):
        if not hasattr(self, "_modeless_dialogs"):
            self._modeless_dialogs = []
        minz = self.is_minimized_to_tray
        if self.isVisible():
            for x in list(self._modeless_dialogs):
                if not x.isVisible():
                    self._modeless_dialogs.remove(x)
        try:
            if "calibre.ebooks.DRMError" in job.details:
                if not minz:
                    from calibre.gui2.dialogs.drm_error import DRMErrorMessage

                    d = DRMErrorMessage(
                        self, _("Cannot convert") + " " + job.description.split(":")[-1].partition("(")[-1][:-1]
                    )
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if "calibre.ebooks.oeb.transforms.split.SplitError" in job.details:
                title = job.description.split(":")[-1].partition("(")[-1][:-1]
                msg = _("<p><b>Failed to convert: %s") % title
                msg += "<p>" + _(
                    """
                Many older ebook reader devices are incapable of displaying
                EPUB files that have internal components over a certain size.
                Therefore, when converting to EPUB, calibre automatically tries
                to split up the EPUB into smaller sized pieces.  For some
                files that are large undifferentiated blocks of text, this
                splitting fails.
                <p>You can <b>work around the problem</b> by either increasing the
                maximum split size under EPUB Output in the conversion dialog,
                or by turning on Heuristic Processing, also in the conversion
                dialog. Note that if you make the maximum split size too large,
                your ebook reader may have trouble with the EPUB.
                        """
                )
                if not minz:
                    d = error_dialog(self, _("Conversion Failed"), msg, det_msg=job.details)
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if "calibre.web.feeds.input.RecipeDisabled" in job.details:
                if not minz:
                    msg = job.details
                    msg = msg[msg.find("calibre.web.feeds.input.RecipeDisabled:") :]
                    msg = msg.partition(":")[-1]
                    d = error_dialog(self, _("Recipe Disabled"), "<p>%s</p>" % msg)
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if "calibre.ebooks.conversion.ConversionUserFeedBack:" in job.details:
                if not minz:
                    import json

                    payload = job.details.rpartition("calibre.ebooks.conversion.ConversionUserFeedBack:")[-1]
                    payload = json.loads("{" + payload.partition("{")[-1])
                    d = {"info": info_dialog, "warn": warning_dialog, "error": error_dialog}.get(
                        payload["level"], error_dialog
                    )
                    d = d(self, payload["title"], "<p>%s</p>" % payload["msg"], det_msg=payload["det_msg"])
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return
        except:
            pass
        if job.killed:
            return
        try:
            prints(job.details, file=sys.stderr)
        except:
            pass
        if not minz:
            self.job_error_dialog.show_error(
                dialog_title, _("<b>Failed</b>") + ": " + unicode(job.description), det_msg=job.details
            )

    def read_settings(self):
        geometry = config["main_window_geometry"]
        if geometry is not None:
            self.restoreGeometry(geometry)
        self.read_layout_settings()

    def write_settings(self):
        with gprefs:  # Only write to gprefs once
            config.set("main_window_geometry", self.saveGeometry())
            dynamic.set("sort_history", self.library_view.model().sort_history)
            self.save_layout_state()

    def quit(self, checked=True, restart=False, debug_on_restart=False, confirm_quit=True):
        if confirm_quit and not self.confirm_quit():
            return
        try:
            self.shutdown()
        except:
            pass
        self.restart_after_quit = restart
        self.debug_on_restart = debug_on_restart
        QApplication.instance().quit()

    def donate(self, *args):
        open_url(QUrl("http://calibre-ebook.com/donate"))

    def confirm_quit(self):
        if self.job_manager.has_jobs():
            msg = _("There are active jobs. Are you sure you want to quit?")
            if self.job_manager.has_device_jobs():
                msg = (
                    "<p>"
                    + __appname__
                    + _(
                        """ is communicating with the device!<br>
                      Quitting may cause corruption on the device.<br>
                      Are you sure you want to quit?"""
                    )
                    + "</p>"
                )

            if not question_dialog(self, _("Active jobs"), msg):
                return False
        from calibre.db.delete_service import has_jobs

        if has_jobs():
            msg = _(
                "Some deleted books are still being moved to the Recycle "
                "Bin, if you quit now, they will be left behind. Are you "
                "sure you want to quit?"
            )
            if not question_dialog(self, _("Active jobs"), msg):
                return False

        return True

    def shutdown(self, write_settings=True):
        self.grid_view.shutdown()
        try:
            db = self.library_view.model().db
            cf = db.clean
        except:
            pass
        else:
            cf()
            # Save the current field_metadata for applications like calibre2opds
            # Goes here, because if cf is valid, db is valid.
            db.prefs["field_metadata"] = db.field_metadata.all_metadata()
            db.commit_dirty_cache()
            db.prefs.write_serialized(prefs["library_path"])
        for action in self.iactions.values():
            if not action.shutting_down():
                return
        if write_settings:
            self.write_settings()
        self.check_messages_timer.stop()
        self.update_checker.terminate()
        self.listener.close()
        self.job_manager.server.close()
        self.job_manager.threaded_server.close()
        while self.spare_servers:
            self.spare_servers.pop().close()
        self.device_manager.keep_going = False
        self.auto_adder.stop()
        mb = self.library_view.model().metadata_backup
        if mb is not None:
            mb.stop()

        self.hide_windows()
        try:
            try:
                if self.content_server is not None:
                    s = self.content_server
                    self.content_server = None
                    s.exit()
            except:
                pass
        except KeyboardInterrupt:
            pass
        from calibre.db.delete_service import shutdown

        shutdown()
        time.sleep(2)
        self.istores.join()
        self.hide_windows()
        # Do not report any errors that happen after the shutdown
        sys.excepthook = sys.__excepthook__
        return True

    def run_wizard(self, *args):
        if self.confirm_quit():
            self.run_wizard_b4_shutdown = True
            self.restart_after_quit = True
            try:
                self.shutdown(write_settings=False)
            except:
                pass
            QApplication.instance().quit()

    def closeEvent(self, e):
        self.write_settings()
        if self.system_tray_icon.isVisible():
            if not dynamic["systray_msg"] and not isosx:
                info_dialog(
                    self,
                    "calibre",
                    "calibre "
                    + _(
                        "will keep running in the system tray. To close it, "
                        "choose <b>Quit</b> in the context menu of the "
                        "system tray."
                    ),
                    show_copy_button=False,
                ).exec_()
                dynamic["systray_msg"] = True
            self.hide_windows()
            e.ignore()
        else:
            if self.confirm_quit():
                try:
                    self.shutdown(write_settings=False)
                except:
                    import traceback

                    traceback.print_exc()
                e.accept()
            else:
                e.ignore()
Example #17
0
File: ui.py Project: kmshi/calibre
class Main(MainWindow):

    APP_NAME = _('Edit Book')
    STATE_VERSION = 0

    def __init__(self, opts, notify=None):
        MainWindow.__init__(self, opts, disable_automatic_gc=True)
        self.boss = Boss(self, notify=notify)
        self.setWindowTitle(self.APP_NAME)
        self.setWindowIcon(QIcon(I('tweak.png')))
        self.opts = opts
        self.path_to_ebook = None
        self.container = None
        self.current_metadata = None
        self.blocking_job = BlockingJob(self)
        self.keyboard = KeyboardManager(self,
                                        config_name='shortcuts/tweak_book')

        self.central = Central(self)
        self.setCentralWidget(self.central)
        self.check_book = Check(self)
        self.toc_view = TOCViewer(self)

        self.create_actions()
        self.create_toolbars()
        self.create_docks()
        self.create_menubar()

        self.status_bar = self.statusBar()
        self.status_bar.addPermanentWidget(
            self.boss.save_manager.status_widget)
        self.cursor_position_widget = CursorPositionWidget(self)
        self.status_bar.addPermanentWidget(self.cursor_position_widget)
        self.status_bar.addWidget(
            QLabel(
                _('{0} {1} created by {2}').format(__appname__, get_version(),
                                                   'Kovid Goyal')))
        f = self.status_bar.font()
        f.setBold(True)
        self.status_bar.setFont(f)

        self.boss(self)
        g = QApplication.instance().desktop().availableGeometry(self)
        self.resize(g.width() - 50, g.height() - 50)
        self.restore_state()

        self.keyboard.finalize()

    def show_status_message(self, msg, timeout=5):
        self.status_bar.showMessage(msg, int(timeout * 1000))

    def elided_text(self, text, width=300):
        return elided_text(text, font=self.font(), width=width)

    @property
    def editor_tabs(self):
        return self.central.editor_tabs

    def create_actions(self):
        group = _('Global Actions')

        def reg(icon, text, target, sid, keys, description):
            ac = actions[sid] = QAction(QIcon(I(icon)), text,
                                        self) if icon else QAction(text, self)
            ac.setObjectName('action-' + sid)
            if target is not None:
                ac.triggered.connect(target)
            if isinstance(keys, type('')):
                keys = (keys, )
            self.keyboard.register_shortcut(sid,
                                            unicode(ac.text()).replace(
                                                '&', ''),
                                            default_keys=keys,
                                            description=description,
                                            action=ac,
                                            group=group)
            self.addAction(ac)
            return ac

        self.action_new_file = reg('document-new.png',
                                   _('&New file (images/fonts/HTML/etc.)'),
                                   self.boss.add_file, 'new-file', (),
                                   _('Create a new file in the current book'))
        self.action_open_book = reg('document_open.png', _('Open &book'),
                                    self.boss.open_book, 'open-book', 'Ctrl+O',
                                    _('Open a new book'))
        self.action_global_undo = reg(
            'back.png', _('&Revert to before'), self.boss.do_global_undo,
            'global-undo', 'Ctrl+Left',
            _('Revert book to before the last action (Undo)'))
        self.action_global_redo = reg(
            'forward.png', _('&Revert to after'), self.boss.do_global_redo,
            'global-redo', 'Ctrl+Right',
            _('Revert book state to after the next action (Redo)'))
        self.action_save = reg('save.png', _('&Save'), self.boss.save_book,
                               'save-book', 'Ctrl+S', _('Save book'))
        self.action_save.setEnabled(False)
        self.action_save_copy = reg('save.png', _('Save a &copy'),
                                    self.boss.save_copy, 'save-copy',
                                    'Ctrl+Alt+S', _('Save a copy of the book'))
        self.action_quit = reg('quit.png', _('&Quit'), self.boss.quit, 'quit',
                               'Ctrl+Q', _('Quit'))
        self.action_preferences = reg('config.png', _('&Preferences'),
                                      self.boss.preferences, 'preferences',
                                      'Ctrl+P', _('Preferences'))

        # Editor actions
        group = _('Editor actions')
        self.action_editor_undo = reg('edit-undo.png', _('&Undo'),
                                      self.boss.do_editor_undo, 'editor-undo',
                                      'Ctrl+Z', _('Undo typing'))
        self.action_editor_redo = reg('edit-redo.png', _('&Redo'),
                                      self.boss.do_editor_redo, 'editor-redo',
                                      'Ctrl+Y', _('Redo typing'))
        self.action_editor_cut = reg('edit-cut.png', _('C&ut text'),
                                     self.boss.do_editor_cut, 'editor-cut', (
                                         'Ctrl+X',
                                         'Shift+Delete',
                                     ), _('Cut text'))
        self.action_editor_copy = reg('edit-copy.png', _('&Copy to clipboard'),
                                      self.boss.do_editor_copy, 'editor-copy',
                                      ('Ctrl+C', 'Ctrl+Insert'),
                                      _('Copy to clipboard'))
        self.action_editor_paste = reg('edit-paste.png',
                                       _('&Paste from clipboard'),
                                       self.boss.do_editor_paste,
                                       'editor-paste', (
                                           'Ctrl+V',
                                           'Shift+Insert',
                                       ), _('Paste from clipboard'))
        self.action_editor_cut.setEnabled(False)
        self.action_editor_copy.setEnabled(False)
        self.action_editor_undo.setEnabled(False)
        self.action_editor_redo.setEnabled(False)

        # Tool actions
        group = _('Tools')
        self.action_toc = reg('toc.png', _('&Edit Table of Contents'),
                              self.boss.edit_toc, 'edit-toc', (),
                              _('Edit Table of Contents'))
        self.action_fix_html_current = reg('html-fix.png', _('&Fix HTML'),
                                           partial(self.boss.fix_html, True),
                                           'fix-html-current', (),
                                           _('Fix HTML in the current file'))
        self.action_fix_html_all = reg('html-fix.png',
                                       _('&Fix HTML - all files'),
                                       partial(self.boss.fix_html, False),
                                       'fix-html-all', (),
                                       _('Fix HTML in all files'))
        self.action_pretty_current = reg('format-justify-fill.png',
                                         _('&Beautify current file'),
                                         partial(self.boss.pretty_print, True),
                                         'pretty-current', (),
                                         _('Beautify current file'))
        self.action_pretty_all = reg('format-justify-fill.png',
                                     _('&Beautify all files'),
                                     partial(self.boss.pretty_print, False),
                                     'pretty-all', (), _('Beautify all files'))

        # Polish actions
        group = _('Polish Book')
        self.action_subset_fonts = reg(
            'subset-fonts.png', _('&Subset embedded fonts'),
            partial(self.boss.polish, 'subset', _('Subset fonts')),
            'subset-fonts', (), _('Subset embedded fonts'))
        self.action_embed_fonts = reg(
            'embed-fonts.png', _('&Embed referenced fonts'),
            partial(self.boss.polish, 'embed', _('Embed fonts')),
            'embed-fonts', (), _('Embed referenced fonts'))
        self.action_smarten_punctuation = reg(
            'smarten-punctuation.png', _('&Smarten punctuation'),
            partial(self.boss.polish, 'smarten_punctuation',
                    _('Smarten punctuation')), 'smarten-punctuation', (),
            _('Smarten punctuation'))

        # Preview actions
        group = _('Preview')
        self.action_auto_reload_preview = reg('auto-reload.png',
                                              _('Auto reload preview'), None,
                                              'auto-reload-preview', (),
                                              _('Auto reload preview'))
        self.action_auto_sync_preview = reg(
            'sync-right.png', _('Sync preview position to editor position'),
            None, 'sync-preview-to-editor', (),
            _('Sync preview position to editor position'))
        self.action_reload_preview = reg('view-refresh.png',
                                         _('Refresh preview'), None,
                                         'reload-preview', ('F5', ),
                                         _('Refresh preview'))
        self.action_split_in_preview = reg(
            'auto_author_sort.png', _('Split this file'), None,
            'split-in-preview', (), _('Split file in the preview panel'))
        self.action_find_next_preview = reg('arrow-down.png', _('Find Next'),
                                            None, 'find-next-preview', (),
                                            _('Find next in preview'))
        self.action_find_prev_preview = reg('arrow-up.png', _('Find Previous'),
                                            None, 'find-prev-preview', (),
                                            _('Find previous in preview'))

        # Search actions
        group = _('Search')
        self.action_find = reg('search.png', _('&Find/Replace'),
                               self.boss.show_find, 'find-replace',
                               ('Ctrl+F', ), _('Show the Find/Replace panel'))

        def sreg(name,
                 text,
                 action,
                 overrides={},
                 keys=(),
                 description=None,
                 icon=None):
            return reg(icon, text, partial(self.boss.search, action,
                                           overrides), name, keys, description
                       or text.replace('&', ''))

        self.action_find_next = sreg('find-next', _('Find &Next'), 'find',
                                     {'direction': 'down'}, ('F3', 'Ctrl+G'),
                                     _('Find next match'))
        self.action_find_previous = sreg('find-previous', _('Find &Previous'),
                                         'find', {'direction': 'up'},
                                         ('Shift+F3', 'Shift+Ctrl+G'),
                                         _('Find previous match'))
        self.action_replace = sreg('replace',
                                   _('Replace'),
                                   'replace',
                                   keys=('Ctrl+R'),
                                   description=_('Replace current match'))
        self.action_replace_next = sreg(
            'replace-next', _('&Replace and find next'), 'replace-find',
            {'direction': 'down'}, ('Ctrl+]'),
            _('Replace current match and find next'))
        self.action_replace_previous = sreg(
            'replace-previous', _('R&eplace and find previous'),
            'replace-find', {'direction': 'up'}, ('Ctrl+['),
            _('Replace current match and find previous'))
        self.action_replace_all = sreg('replace-all',
                                       _('Replace &all'),
                                       'replace-all',
                                       keys=('Ctrl+A'),
                                       description=_('Replace all matches'))
        self.action_count = sreg('count-matches',
                                 _('&Count all'),
                                 'count',
                                 keys=('Ctrl+N'),
                                 description=_('Count number of matches'))
        self.action_mark = reg(None, _('&Mark selected text'),
                               self.boss.mark_selected_text,
                               'mark-selected-text', ('Ctrl+Shift+M', ),
                               _('Mark selected text'))
        self.action_go_to_line = reg(None, _('Go to &line'),
                                     self.boss.go_to_line_number,
                                     'go-to-line-number', ('Ctrl+.', ),
                                     _('Go to line number'))

        # Check Book actions
        group = _('Check Book')
        self.action_check_book = reg('debug.png', _('&Check Book'),
                                     self.boss.check_requested, 'check-book',
                                     ('F7'), _('Check book for errors'))
        self.action_check_book_next = reg(
            'forward.png', _('&Next error'),
            partial(self.check_book.next_error, delta=1), 'check-book-next',
            ('Ctrl+F7'), _('Show next error'))
        self.action_check_book_previous = reg(
            'back.png', _('&Previous error'),
            partial(self.check_book.next_error, delta=-1),
            'check-book-previous', ('Ctrl+Shift+F7'), _('Show previous error'))

        # Miscellaneous actions
        group = _('Miscellaneous')
        self.action_create_checkpoint = reg(
            'marked.png', _('&Create checkpoint'), self.boss.create_checkpoint,
            'create-checkpoint', (),
            _('Create a checkpoint with the current state of the book'))
        self.action_close_current_tab = reg('window-close.png',
                                            _('&Close current tab'),
                                            self.central.close_current_editor,
                                            'close-current-tab', 'Ctrl+W',
                                            _('Close the currently open tab'))
        self.action_close_all_but_current_tab = reg(
            'edit-clear.png', _('&Close other tabs'),
            self.central.close_all_but_current_editor,
            'close-all-but-current-tab', 'Ctrl+Alt+W',
            _('Close all tabs except the current tab'))
        self.action_help = reg(
            'help.png', _('User &Manual'), lambda: open_url(
                QUrl('http://manual.calibre-ebook.com/edit.html')),
            'user-manual', 'F1', _('Show User Manual'))

    def create_menubar(self):
        p, q = self.create_application_menubar()
        q.triggered.connect(self.action_quit.trigger)
        p.triggered.connect(self.action_preferences.trigger)
        b = self.menuBar()

        f = b.addMenu(_('&File'))
        f.addAction(self.action_new_file)
        f.addAction(self.action_open_book)
        self.recent_books_menu = f.addMenu(_('&Recently opened books'))
        self.update_recent_books()
        f.addSeparator()
        f.addAction(self.action_save)
        f.addAction(self.action_save_copy)
        f.addSeparator()
        f.addAction(self.action_quit)

        e = b.addMenu(_('&Edit'))
        e.addAction(self.action_global_undo)
        e.addAction(self.action_global_redo)
        e.addAction(self.action_create_checkpoint)
        e.addSeparator()
        e.addAction(self.action_editor_undo)
        e.addAction(self.action_editor_redo)
        e.addSeparator()
        e.addAction(self.action_editor_cut)
        e.addAction(self.action_editor_copy)
        e.addAction(self.action_editor_paste)
        e.addSeparator()
        e.addAction(self.action_preferences)

        e = b.addMenu(_('&Tools'))
        e.addAction(self.action_toc)
        e.addAction(self.action_embed_fonts)
        e.addAction(self.action_subset_fonts)
        e.addAction(self.action_smarten_punctuation)
        e.addAction(self.action_fix_html_all)
        e.addAction(self.action_pretty_all)
        e.addAction(self.action_check_book)

        e = b.addMenu(_('&View'))
        t = e.addMenu(_('Tool&bars'))
        e.addSeparator()
        for name, ac in actions.iteritems():
            if name.endswith('-dock'):
                e.addAction(ac)
            elif name.endswith('-bar'):
                t.addAction(ac)
        e.addSeparator()
        e.addAction(self.action_close_current_tab)
        e.addAction(self.action_close_all_but_current_tab)

        e = b.addMenu(_('&Search'))
        a = e.addAction
        a(self.action_find)
        e.addSeparator()
        a(self.action_find_next)
        a(self.action_find_previous)
        e.addSeparator()
        a(self.action_replace)
        a(self.action_replace_next)
        a(self.action_replace_previous)
        a(self.action_replace_all)
        e.addSeparator()
        a(self.action_count)
        e.addSeparator()
        a(self.action_mark)
        e.addSeparator()
        a(self.action_go_to_line)

        e = b.addMenu(_('&Help'))
        a = e.addAction
        a(self.action_help)

    def update_recent_books(self):
        m = self.recent_books_menu
        m.clear()
        books = tprefs.get('recent-books', [])
        for path in books:
            m.addAction(self.elided_text(path, width=500),
                        partial(self.boss.open_book, path=path))

    def create_toolbars(self):
        def create(text, name):
            name += '-bar'
            b = self.addToolBar(text)
            b.setObjectName(name)  # Needed for saveState
            setattr(self, name.replace('-', '_'), b)
            actions[name] = b.toggleViewAction()
            return b

        a = create(_('Book tool bar'), 'global').addAction
        for x in ('new_file', 'open_book', None, 'global_undo', 'global_redo',
                  'create_checkpoint', 'save', None, 'toc', 'check_book'):
            if x is None:
                self.global_bar.addSeparator()
                continue
            a(getattr(self, 'action_' + x))
        self.donate_button = b = ThrobbingButton(self)
        b.clicked.connect(
            lambda: open_url(QUrl('http://calibre-ebook.com/donate')))
        b.setAutoRaise(True)
        self.donate_widget = w = create_donate_widget(b)
        if hasattr(w, 'filler'):
            w.filler.setVisible(False)
        b.set_normal_icon_size(self.global_bar.iconSize().width(),
                               self.global_bar.iconSize().height())
        b.setIcon(QIcon(I('donate.png')))
        b.setToolTip(_('Donate to support calibre development'))
        QTimer.singleShot(10, b.start_animation)
        self.global_bar.addWidget(w)
        a(self.action_help)

        a = create(_('Polish book tool bar'), 'polish').addAction
        for x in ('embed_fonts', 'subset_fonts', 'smarten_punctuation'):
            a(getattr(self, 'action_' + x))

    def create_docks(self):
        def create(name, oname):
            oname += '-dock'
            d = QDockWidget(name, self)
            d.setObjectName(oname)  # Needed for saveState
            ac = d.toggleViewAction()
            desc = _('Toggle %s') % name.replace('&', '')
            self.keyboard.register_shortcut(oname,
                                            desc,
                                            description=desc,
                                            action=ac,
                                            group=_('Windows'))
            actions[oname] = ac
            setattr(self, oname.replace('-', '_'), d)
            return d

        d = create(_('Files Browser'), 'files-browser')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
        self.file_list = FileListWidget(d)
        d.setWidget(self.file_list)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)

        d = create(_('File Preview'), 'preview')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
        self.preview = Preview(d)
        d.setWidget(self.preview)
        self.addDockWidget(Qt.RightDockWidgetArea, d)

        d = create(_('Check Book'), 'check-book')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea
                          | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        d.setWidget(self.check_book)
        self.addDockWidget(Qt.TopDockWidgetArea, d)
        d.close()  # By default the check window is closed

        d = create(_('Inspector'), 'inspector')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea
                          | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        d.setWidget(self.preview.inspector)
        self.preview.inspector.setParent(d)
        self.addDockWidget(Qt.BottomDockWidgetArea, d)
        d.close()  # By default the inspector window is closed
        d.setFeatures(d.DockWidgetClosable | d.DockWidgetMovable
                      )  # QWebInspector does not work in a floating dock

        d = create(_('Table of Contents'), 'toc-viewer')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea
                          | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        d.setWidget(self.toc_view)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)
        d.close()  # Hidden by default
        d.visibilityChanged.connect(self.toc_view.visibility_changed)

    def resizeEvent(self, ev):
        self.blocking_job.resize(ev.size())
        return super(Main, self).resizeEvent(ev)

    def update_window_title(self):
        self.setWindowTitle(
            self.current_metadata.title + ' [%s] - %s' %
            (current_container().book_type.upper(), self.APP_NAME))

    def closeEvent(self, e):
        if not self.boss.confirm_quit():
            e.ignore()
            return
        try:
            self.boss.shutdown()
        except:
            import traceback
            traceback.print_exc()
        e.accept()

    def save_state(self):
        tprefs.set('main_window_geometry', bytearray(self.saveGeometry()))
        tprefs.set('main_window_state',
                   bytearray(self.saveState(self.STATE_VERSION)))
        self.central.save_state()
        self.check_book.save_state()

    def restore_state(self):
        geom = tprefs.get('main_window_geometry', None)
        if geom is not None:
            self.restoreGeometry(geom)
        state = tprefs.get('main_window_state', None)
        if state is not None:
            self.restoreState(state, self.STATE_VERSION)
        self.central.restore_state()

    def contextMenuEvent(self, ev):
        ev.ignore()
Example #18
0
class Main(MainWindow):

    APP_NAME = _('Tweak Book')
    STATE_VERSION = 0

    def __init__(self, opts):
        MainWindow.__init__(self, opts, disable_automatic_gc=True)
        self.boss = Boss(self)
        self.setWindowTitle(self.APP_NAME)
        self.setWindowIcon(QIcon(I('tweak.png')))
        self.opts = opts
        self.path_to_ebook = None
        self.container = None
        self.current_metadata = None
        self.blocking_job = BlockingJob(self)
        self.keyboard = KeyboardManager(parent=self, config_name='shortcuts/tweak')

        self.create_actions()
        self.create_menubar()
        self.create_toolbar()
        self.create_docks()

        self.status_bar = self.statusBar()
        self.l = QLabel('Placeholder')
        self.status_bar.addPermanentWidget(self.boss.save_manager.status_widget)
        self.status_bar.addWidget(QLabel(_('{0} {1} created by {2}').format(__appname__, get_version(), 'Kovid Goyal')))
        f = self.status_bar.font()
        f.setBold(True)
        self.status_bar.setFont(f)

        self.setCentralWidget(self.l)
        self.boss(self)
        g = QApplication.instance().desktop().availableGeometry(self)
        self.resize(g.width()-50, g.height()-50)
        self.restore_state()

        self.keyboard.finalize()

    def create_actions(self):
        group = _('Global Actions')

        def reg(icon, text, target, sid, keys, description):
            ac = QAction(QIcon(I(icon)), text, self)
            ac.setObjectName('action-' + sid)
            ac.triggered.connect(target)
            if isinstance(keys, type('')):
                keys = (keys,)
            self.keyboard.register_shortcut(
                sid, unicode(ac.text()), default_keys=keys, description=description, action=ac, group=group)
            self.addAction(ac)
            return ac

        self.action_open_book = reg('document_open.png', _('Open &book'), self.boss.open_book, 'open-book', 'Ctrl+O', _('Open a new book'))
        self.action_global_undo = reg('back.png', _('&Revert to before'), self.boss.do_global_undo, 'global-undo', 'Ctrl+Left',
                                      _('Revert book to before the last action (Undo)'))
        self.action_global_redo = reg('forward.png', _('&Revert to after'), self.boss.do_global_redo, 'global-redo', 'Ctrl+Right',
                                      _('Revert book state to after the next action (Redo)'))
        self.action_save = reg('save.png', _('&Save'), self.boss.save_book, 'save-book', 'Ctrl+S', _('Save book'))
        self.action_save.setEnabled(False)
        self.action_quit = reg('quit.png', _('&Quit'), self.boss.quit, 'quit', 'Ctrl+Q', _('Quit'))

    def create_menubar(self):
        b = self.menuBar()

        f = b.addMenu(_('&File'))
        f.addAction(self.action_open_book)
        f.addAction(self.action_save)
        f.addAction(self.action_quit)

        e = b.addMenu(_('&Edit'))
        e.addAction(self.action_global_undo)
        e.addAction(self.action_global_redo)

    def create_toolbar(self):
        self.global_bar = b = self.addToolBar(_('Global'))
        b.setObjectName('global_bar')  # Needed for saveState
        b.addAction(self.action_open_book)
        b.addAction(self.action_global_undo)
        b.addAction(self.action_global_redo)
        b.addAction(self.action_save)

    def create_docks(self):
        self.file_list_dock = d = QDockWidget(_('&Files Browser'), self)
        d.setObjectName('file_list_dock')  # Needed for saveState
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
        self.file_list = FileListWidget(d)
        d.setWidget(self.file_list)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)

    def resizeEvent(self, ev):
        self.blocking_job.resize(ev.size())
        return super(Main, self).resizeEvent(ev)

    def update_window_title(self):
        self.setWindowTitle(self.current_metadata.title + ' [%s] - %s' %(current_container().book_type.upper(), self.APP_NAME))

    def closeEvent(self, e):
        if not self.boss.confirm_quit():
            e.ignore()
            return
        try:
            self.boss.shutdown()
        except:
            import traceback
            traceback.print_exc()
        e.accept()

    def save_state(self):
        tprefs.set('main_window_geometry', bytearray(self.saveGeometry()))
        tprefs.set('main_window_state', bytearray(self.saveState(self.STATE_VERSION)))

    def restore_state(self):
        geom = tprefs.get('main_window_geometry', None)
        if geom is not None:
            self.restoreGeometry(geom)
        state = tprefs.get('main_window_state', None)
        if state is not None:
            self.restoreState(state, self.STATE_VERSION)
Example #19
0
File: ui.py Project: berkus/calibre
class Main(MainWindow):

    APP_NAME = _('Edit Book')
    STATE_VERSION = 0

    def __init__(self, opts, notify=None):
        MainWindow.__init__(self, opts, disable_automatic_gc=True)
        self.boss = Boss(self, notify=notify)
        self.setWindowTitle(self.APP_NAME)
        self.setWindowIcon(QIcon(I('tweak.png')))
        self.opts = opts
        self.path_to_ebook = None
        self.container = None
        self.current_metadata = None
        self.blocking_job = BlockingJob(self)
        self.keyboard = KeyboardManager(self, config_name='shortcuts/tweak_book')

        self.central = Central(self)
        self.setCentralWidget(self.central)
        self.check_book = Check(self)
        self.toc_view = TOCViewer(self)
        self.image_browser = InsertImage(self, for_browsing=True)
        self.insert_char = CharSelect(self)

        self.create_actions()
        self.create_toolbars()
        self.create_docks()
        self.create_menubar()

        self.status_bar = self.statusBar()
        self.status_bar.addPermanentWidget(self.boss.save_manager.status_widget)
        self.cursor_position_widget = CursorPositionWidget(self)
        self.status_bar.addPermanentWidget(self.cursor_position_widget)
        self.status_bar.addWidget(QLabel(_('{0} {1} created by {2}').format(__appname__, get_version(), 'Kovid Goyal')))
        f = self.status_bar.font()
        f.setBold(True)
        self.status_bar.setFont(f)

        self.boss(self)
        g = QApplication.instance().desktop().availableGeometry(self)
        self.resize(g.width()-50, g.height()-50)

        self.restore_state()
        self.apply_settings()

    def apply_settings(self):
        self.keyboard.finalize()
        self.setDockNestingEnabled(tprefs['nestable_dock_widgets'])
        for v, h in product(('top', 'bottom'), ('left', 'right')):
            p = 'dock_%s_%s' % (v, h)
            pref = tprefs[p] or tprefs.defaults[p]
            area = getattr(Qt, '%sDockWidgetArea' % capitalize({'vertical':h, 'horizontal':v}[pref]))
            self.setCorner(getattr(Qt, '%s%sCorner' % tuple(map(capitalize, (v, h)))), area)
        self.preview.apply_settings()

    def show_status_message(self, msg, timeout=5):
        self.status_bar.showMessage(msg, int(timeout*1000))

    def elided_text(self, text, width=300):
        return elided_text(text, font=self.font(), width=width)

    @property
    def editor_tabs(self):
        return self.central.editor_tabs

    def create_actions(self):
        group = _('Global Actions')

        def reg(icon, text, target, sid, keys, description):
            ac = actions[sid] = QAction(QIcon(I(icon)), text, self) if icon else QAction(text, self)
            ac.setObjectName('action-' + sid)
            if target is not None:
                ac.triggered.connect(target)
            if isinstance(keys, type('')):
                keys = (keys,)
            self.keyboard.register_shortcut(
                sid, unicode(ac.text()).replace('&', ''), default_keys=keys, description=description, action=ac, group=group)
            self.addAction(ac)
            return ac

        self.action_new_file = reg('document-new.png', _('&New file (images/fonts/HTML/etc.)'), self.boss.add_file,
                                   'new-file', (), _('Create a new file in the current book'))
        self.action_import_files = reg(None, _('&Import files into book'), self.boss.add_files, 'new-files', (), _('Import files into book'))
        self.action_open_book = reg('document_open.png', _('Open &book'), self.boss.open_book, 'open-book', 'Ctrl+O', _('Open a new book'))
        self.action_global_undo = reg('back.png', _('&Revert to before'), self.boss.do_global_undo, 'global-undo', 'Ctrl+Left',
                                      _('Revert book to before the last action (Undo)'))
        self.action_global_redo = reg('forward.png', _('&Revert to after'), self.boss.do_global_redo, 'global-redo', 'Ctrl+Right',
                                      _('Revert book state to after the next action (Redo)'))
        self.action_save = reg('save.png', _('&Save'), self.boss.save_book, 'save-book', 'Ctrl+S', _('Save book'))
        self.action_save.setEnabled(False)
        self.action_save_copy = reg('save.png', _('Save a &copy'), self.boss.save_copy, 'save-copy', 'Ctrl+Alt+S', _('Save a copy of the book'))
        self.action_quit = reg('quit.png', _('&Quit'), self.boss.quit, 'quit', 'Ctrl+Q', _('Quit'))
        self.action_preferences = reg('config.png', _('&Preferences'), self.boss.preferences, 'preferences', 'Ctrl+P', _('Preferences'))
        self.action_new_book = reg('book.png', _('Create &new, empty book'), self.boss.new_book, 'new-book', (), _('Create a new, empty book'))

        # Editor actions
        group = _('Editor actions')
        self.action_editor_undo = reg('edit-undo.png', _('&Undo'), self.boss.do_editor_undo, 'editor-undo', 'Ctrl+Z',
                                      _('Undo typing'))
        self.action_editor_redo = reg('edit-redo.png', _('&Redo'), self.boss.do_editor_redo, 'editor-redo', 'Ctrl+Y',
                                      _('Redo typing'))
        self.action_editor_cut = reg('edit-cut.png', _('C&ut text'), self.boss.do_editor_cut, 'editor-cut', ('Ctrl+X', 'Shift+Delete', ),
                                      _('Cut text'))
        self.action_editor_copy = reg('edit-copy.png', _('&Copy to clipboard'), self.boss.do_editor_copy, 'editor-copy', ('Ctrl+C', 'Ctrl+Insert'),
                                      _('Copy to clipboard'))
        self.action_editor_paste = reg('edit-paste.png', _('&Paste from clipboard'), self.boss.do_editor_paste, 'editor-paste', ('Ctrl+V', 'Shift+Insert', ),
                                      _('Paste from clipboard'))
        self.action_editor_cut.setEnabled(False)
        self.action_editor_copy.setEnabled(False)
        self.action_editor_undo.setEnabled(False)
        self.action_editor_redo.setEnabled(False)

        def ereg(icon, text, target, sid, keys, description):
            return reg(icon, text, partial(self.boss.editor_action, target), sid, keys, description)
        register_text_editor_actions(ereg)

        # Tool actions
        group = _('Tools')
        self.action_toc = reg('toc.png', _('&Edit Table of Contents'), self.boss.edit_toc, 'edit-toc', (), _('Edit Table of Contents'))
        self.action_fix_html_current = reg('html-fix.png', _('&Fix HTML'), partial(self.boss.fix_html, True), 'fix-html-current', (),
                                           _('Fix HTML in the current file'))
        self.action_fix_html_all = reg('html-fix.png', _('&Fix HTML - all files'), partial(self.boss.fix_html, False), 'fix-html-all', (),
                                       _('Fix HTML in all files'))
        self.action_pretty_current = reg('format-justify-fill.png', _('&Beautify current file'), partial(self.boss.pretty_print, True), 'pretty-current', (),
                                           _('Beautify current file'))
        self.action_pretty_all = reg('format-justify-fill.png', _('&Beautify all files'), partial(self.boss.pretty_print, False), 'pretty-all', (),
                                       _('Beautify all files'))
        self.action_insert_char = reg('character-set.png', _('&Insert special character'), self.boss.insert_character, 'insert-character', (),
                                      _('Insert special character'))

        # Polish actions
        group = _('Polish Book')
        self.action_subset_fonts = reg(
            'subset-fonts.png', _('&Subset embedded fonts'), partial(
                self.boss.polish, 'subset', _('Subset fonts')), 'subset-fonts', (), _('Subset embedded fonts'))
        self.action_embed_fonts = reg(
            'embed-fonts.png', _('&Embed referenced fonts'), partial(
                self.boss.polish, 'embed', _('Embed fonts')), 'embed-fonts', (), _('Embed referenced fonts'))
        self.action_smarten_punctuation = reg(
            'smarten-punctuation.png', _('&Smarten punctuation'), partial(
                self.boss.polish, 'smarten_punctuation', _('Smarten punctuation')), 'smarten-punctuation', (), _('Smarten punctuation'))

        # Preview actions
        group = _('Preview')
        self.action_auto_reload_preview = reg('auto-reload.png', _('Auto reload preview'), None, 'auto-reload-preview', (), _('Auto reload preview'))
        self.action_auto_sync_preview = reg('sync-right.png', _('Sync preview position to editor position'), None, 'sync-preview-to-editor', (), _(
            'Sync preview position to editor position'))
        self.action_reload_preview = reg('view-refresh.png', _('Refresh preview'), None, 'reload-preview', ('F5',), _('Refresh preview'))
        self.action_split_in_preview = reg('auto_author_sort.png', _('Split this file'), None, 'split-in-preview', (), _(
            'Split file in the preview panel'))
        self.action_find_next_preview = reg('arrow-down.png', _('Find Next'), None, 'find-next-preview', (), _('Find next in preview'))
        self.action_find_prev_preview = reg('arrow-up.png', _('Find Previous'), None, 'find-prev-preview', (), _('Find previous in preview'))

        # Search actions
        group = _('Search')
        self.action_find = reg('search.png', _('&Find/Replace'), self.boss.show_find, 'find-replace', ('Ctrl+F',), _('Show the Find/Replace panel'))
        def sreg(name, text, action, overrides={}, keys=(), description=None, icon=None):
            return reg(icon, text, partial(self.boss.search, action, overrides), name, keys, description or text.replace('&', ''))
        self.action_find_next = sreg('find-next', _('Find &Next'),
                                     'find', {'direction':'down'}, ('F3', 'Ctrl+G'), _('Find next match'))
        self.action_find_previous = sreg('find-previous', _('Find &Previous'),
                                         'find', {'direction':'up'}, ('Shift+F3', 'Shift+Ctrl+G'), _('Find previous match'))
        self.action_replace = sreg('replace', _('Replace'),
                                   'replace', keys=('Ctrl+R'), description=_('Replace current match'))
        self.action_replace_next = sreg('replace-next', _('&Replace and find next'),
                                        'replace-find', {'direction':'down'}, ('Ctrl+]'), _('Replace current match and find next'))
        self.action_replace_previous = sreg('replace-previous', _('R&eplace and find previous'),
                                        'replace-find', {'direction':'up'}, ('Ctrl+['), _('Replace current match and find previous'))
        self.action_replace_all = sreg('replace-all', _('Replace &all'),
                                   'replace-all', keys=('Ctrl+A'), description=_('Replace all matches'))
        self.action_count = sreg('count-matches', _('&Count all'),
                                   'count', keys=('Ctrl+N'), description=_('Count number of matches'))
        self.action_mark = reg(None, _('&Mark selected text'), self.boss.mark_selected_text, 'mark-selected-text', ('Ctrl+Shift+M',), _('Mark selected text'))
        self.action_go_to_line = reg(None, _('Go to &line'), self.boss.go_to_line_number, 'go-to-line-number', ('Ctrl+.',), _('Go to line number'))

        # Check Book actions
        group = _('Check Book')
        self.action_check_book = reg('debug.png', _('&Check Book'), self.boss.check_requested, 'check-book', ('F7'), _('Check book for errors'))
        self.action_check_book_next = reg('forward.png', _('&Next error'), partial(
            self.check_book.next_error, delta=1), 'check-book-next', ('Ctrl+F7'), _('Show next error'))
        self.action_check_book_previous = reg('back.png', _('&Previous error'), partial(
            self.check_book.next_error, delta=-1), 'check-book-previous', ('Ctrl+Shift+F7'), _('Show previous error'))

        # Miscellaneous actions
        group = _('Miscellaneous')
        self.action_create_checkpoint = reg(
            'marked.png', _('&Create checkpoint'), self.boss.create_checkpoint, 'create-checkpoint', (), _(
                'Create a checkpoint with the current state of the book'))
        self.action_close_current_tab = reg(
            'window-close.png', _('&Close current tab'), self.central.close_current_editor, 'close-current-tab', 'Ctrl+W', _(
                'Close the currently open tab'))
        self.action_close_all_but_current_tab = reg(
            'edit-clear.png', _('&Close other tabs'), self.central.close_all_but_current_editor, 'close-all-but-current-tab', 'Ctrl+Alt+W', _(
                'Close all tabs except the current tab'))
        self.action_help = reg(
            'help.png', _('User &Manual'), lambda : open_url(QUrl('http://manual.calibre-ebook.com/edit.html')), 'user-manual', 'F1', _(
                'Show User Manual'))
        self.action_browse_images = reg(
            'view-image.png', _('&Browse images in book'), self.boss.browse_images, 'browse-images', (), _(
                'Browse images in the books visually'))

    def create_menubar(self):
        p, q = self.create_application_menubar()
        q.triggered.connect(self.action_quit.trigger)
        p.triggered.connect(self.action_preferences.trigger)
        b = self.menuBar()

        f = b.addMenu(_('&File'))
        f.addAction(self.action_new_file)
        f.addAction(self.action_import_files)
        f.addAction(self.action_open_book)
        f.addAction(self.action_new_book)
        self.recent_books_menu = f.addMenu(_('&Recently opened books'))
        self.update_recent_books()
        f.addSeparator()
        f.addAction(self.action_save)
        f.addAction(self.action_save_copy)
        f.addSeparator()
        f.addAction(self.action_quit)

        e = b.addMenu(_('&Edit'))
        e.addAction(self.action_global_undo)
        e.addAction(self.action_global_redo)
        e.addAction(self.action_create_checkpoint)
        e.addSeparator()
        e.addAction(self.action_editor_undo)
        e.addAction(self.action_editor_redo)
        e.addSeparator()
        e.addAction(self.action_editor_cut)
        e.addAction(self.action_editor_copy)
        e.addAction(self.action_editor_paste)
        e.addAction(self.action_insert_char)
        e.addSeparator()
        e.addAction(self.action_preferences)

        e = b.addMenu(_('&Tools'))
        e.addAction(self.action_toc)
        e.addAction(self.action_embed_fonts)
        e.addAction(self.action_subset_fonts)
        e.addAction(self.action_smarten_punctuation)
        e.addAction(self.action_fix_html_all)
        e.addAction(self.action_pretty_all)
        e.addAction(self.action_check_book)

        e = b.addMenu(_('&View'))
        t = e.addMenu(_('Tool&bars'))
        e.addSeparator()
        for name, ac in actions.iteritems():
            if name.endswith('-dock'):
                e.addAction(ac)
            elif name.endswith('-bar'):
                t.addAction(ac)
        e.addAction(self.action_browse_images)
        e.addSeparator()
        e.addAction(self.action_close_current_tab)
        e.addAction(self.action_close_all_but_current_tab)

        e = b.addMenu(_('&Search'))
        a = e.addAction
        a(self.action_find)
        e.addSeparator()
        a(self.action_find_next)
        a(self.action_find_previous)
        e.addSeparator()
        a(self.action_replace)
        a(self.action_replace_next)
        a(self.action_replace_previous)
        a(self.action_replace_all)
        e.addSeparator()
        a(self.action_count)
        e.addSeparator()
        a(self.action_mark)
        e.addSeparator()
        a(self.action_go_to_line)

        e = b.addMenu(_('&Help'))
        a = e.addAction
        a(self.action_help)
        a(QIcon(I('donate.png')), _('Donate to support calibre development'), open_donate)
        a(self.action_preferences)

    def update_recent_books(self):
        m = self.recent_books_menu
        m.clear()
        books = tprefs.get('recent-books', [])
        for path in books:
            m.addAction(self.elided_text(path, width=500), partial(self.boss.open_book, path=path))

    def create_toolbars(self):
        def create(text, name):
            name += '-bar'
            b = self.addToolBar(text)
            b.setObjectName(name)  # Needed for saveState
            setattr(self, name.replace('-', '_'), b)
            actions[name] = b.toggleViewAction()
            return b

        a = create(_('Book tool bar'), 'global').addAction
        for x in ('new_file', 'open_book', None, 'global_undo', 'global_redo', 'create_checkpoint', 'save', None, 'toc', 'check_book'):
            if x is None:
                self.global_bar.addSeparator()
                continue
            a(getattr(self, 'action_' + x))
        self.donate_button = b = ThrobbingButton(self)
        b.clicked.connect(open_donate)
        b.setAutoRaise(True)
        self.donate_widget = w = create_donate_widget(b)
        if hasattr(w, 'filler'):
            w.filler.setVisible(False)
        b.set_normal_icon_size(self.global_bar.iconSize().width(), self.global_bar.iconSize().height())
        b.setIcon(QIcon(I('donate.png')))
        b.setToolTip(_('Donate to support calibre development'))
        QTimer.singleShot(10, b.start_animation)
        self.global_bar.addWidget(w)
        self.global_bar.addAction(self.action_insert_char)
        a(self.action_help)

        a = create(_('Polish book tool bar'), 'polish').addAction
        for x in ('embed_fonts', 'subset_fonts', 'smarten_punctuation'):
            a(getattr(self, 'action_' + x))

    def create_docks(self):

        def create(name, oname):
            oname += '-dock'
            d = QDockWidget(name, self)
            d.setObjectName(oname)  # Needed for saveState
            ac = d.toggleViewAction()
            desc = _('Toggle %s') % name.replace('&', '')
            self.keyboard.register_shortcut(
                oname, desc, description=desc, action=ac, group=_('Windows'))
            actions[oname] = ac
            setattr(self, oname.replace('-', '_'), d)
            return d

        d = create(_('Files Browser'), 'files-browser')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
        self.file_list = FileListWidget(d)
        d.setWidget(self.file_list)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)

        d = create(_('File Preview'), 'preview')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
        self.preview = Preview(d)
        d.setWidget(self.preview)
        self.addDockWidget(Qt.RightDockWidgetArea, d)

        d = create(_('Check Book'), 'check-book')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        d.setWidget(self.check_book)
        self.addDockWidget(Qt.TopDockWidgetArea, d)
        d.close()  # By default the check window is closed

        d = create(_('Inspector'), 'inspector')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        d.setWidget(self.preview.inspector)
        self.preview.inspector.setParent(d)
        self.addDockWidget(Qt.BottomDockWidgetArea, d)
        d.close()  # By default the inspector window is closed
        d.setFeatures(d.DockWidgetClosable | d.DockWidgetMovable)  # QWebInspector does not work in a floating dock

        d = create(_('Table of Contents'), 'toc-viewer')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        d.setWidget(self.toc_view)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)
        d.close()  # Hidden by default
        d.visibilityChanged.connect(self.toc_view.visibility_changed)

    def resizeEvent(self, ev):
        self.blocking_job.resize(ev.size())
        return super(Main, self).resizeEvent(ev)

    def update_window_title(self):
        self.setWindowTitle(self.current_metadata.title + ' [%s] - %s' %(current_container().book_type.upper(), self.APP_NAME))

    def closeEvent(self, e):
        if not self.boss.confirm_quit():
            e.ignore()
            return
        try:
            self.boss.shutdown()
        except:
            import traceback
            traceback.print_exc()
        e.accept()

    def save_state(self):
        tprefs.set('main_window_geometry', bytearray(self.saveGeometry()))
        tprefs.set('main_window_state', bytearray(self.saveState(self.STATE_VERSION)))
        self.central.save_state()
        self.check_book.save_state()

    def restore_state(self):
        geom = tprefs.get('main_window_geometry', None)
        if geom is not None:
            self.restoreGeometry(geom)
        state = tprefs.get('main_window_state', None)
        if state is not None:
            self.restoreState(state, self.STATE_VERSION)
        self.central.restore_state()

    def contextMenuEvent(self, ev):
        ev.ignore()
Example #20
0
class Main(MainWindow):

    APP_NAME = _('Tweak Book')
    STATE_VERSION = 0

    def __init__(self, opts):
        MainWindow.__init__(self, opts, disable_automatic_gc=True)
        self.boss = Boss(self)
        self.setWindowTitle(self.APP_NAME)
        self.setWindowIcon(QIcon(I('tweak.png')))
        self.opts = opts
        self.path_to_ebook = None
        self.container = None
        self.current_metadata = None
        self.blocking_job = BlockingJob(self)
        self.keyboard = KeyboardManager(self,
                                        config_name='shortcuts/tweak_book')

        self.central = Central(self)
        self.setCentralWidget(self.central)

        self.create_actions()
        self.create_toolbars()
        self.create_docks()
        self.create_menubar()

        self.status_bar = self.statusBar()
        self.status_bar.addPermanentWidget(
            self.boss.save_manager.status_widget)
        self.status_bar.addWidget(
            QLabel(
                _('{0} {1} created by {2}').format(__appname__, get_version(),
                                                   'Kovid Goyal')))
        f = self.status_bar.font()
        f.setBold(True)
        self.status_bar.setFont(f)

        self.boss(self)
        g = QApplication.instance().desktop().availableGeometry(self)
        self.resize(g.width() - 50, g.height() - 50)
        self.restore_state()

        self.keyboard.finalize()

    def elided_text(self, text, width=200, mode=Qt.ElideMiddle):
        return elided_text(self.font(), text, width=width, mode=mode)

    @property
    def editor_tabs(self):
        return self.central.editor_tabs

    def create_actions(self):
        group = _('Global Actions')

        def reg(icon, text, target, sid, keys, description):
            ac = actions[sid] = QAction(QIcon(I(icon)), text,
                                        self) if icon else QAction(text, self)
            ac.setObjectName('action-' + sid)
            if target is not None:
                ac.triggered.connect(target)
            if isinstance(keys, type('')):
                keys = (keys, )
            self.keyboard.register_shortcut(sid,
                                            unicode(ac.text()).replace(
                                                '&', ''),
                                            default_keys=keys,
                                            description=description,
                                            action=ac,
                                            group=group)
            self.addAction(ac)
            return ac

        self.action_new_file = reg('document-new.png', _('&New file'),
                                   self.boss.add_file, 'new-file', (),
                                   _('Create a new file in the current book'))
        self.action_open_book = reg('document_open.png', _('Open &book'),
                                    self.boss.open_book, 'open-book', 'Ctrl+O',
                                    _('Open a new book'))
        self.action_global_undo = reg(
            'back.png', _('&Revert to before'), self.boss.do_global_undo,
            'global-undo', 'Ctrl+Left',
            _('Revert book to before the last action (Undo)'))
        self.action_global_redo = reg(
            'forward.png', _('&Revert to after'), self.boss.do_global_redo,
            'global-redo', 'Ctrl+Right',
            _('Revert book state to after the next action (Redo)'))
        self.action_save = reg('save.png', _('&Save'), self.boss.save_book,
                               'save-book', 'Ctrl+Shift+S', _('Save book'))
        self.action_save.setEnabled(False)
        self.action_quit = reg('quit.png', _('&Quit'), self.boss.quit, 'quit',
                               'Ctrl+Q', _('Quit'))
        self.action_preferences = reg('config.png', _('&Preferences'),
                                      self.boss.preferences, 'preferences',
                                      'Ctrl+P', _('Preferences'))

        # Editor actions
        group = _('Editor actions')
        self.action_editor_undo = reg('edit-undo.png', _('&Undo'),
                                      self.boss.do_editor_undo, 'editor-undo',
                                      'Ctrl+Z', _('Undo typing'))
        self.action_editor_redo = reg('edit-redo.png', _('&Redo'),
                                      self.boss.do_editor_redo, 'editor-redo',
                                      'Ctrl+Y', _('Redo typing'))
        self.action_editor_save = reg('save.png', _('&Save'),
                                      self.boss.do_editor_save, 'editor-save',
                                      'Ctrl+S',
                                      _('Save changes to the current file'))
        self.action_editor_cut = reg('edit-cut.png', _('C&ut text'),
                                     self.boss.do_editor_cut, 'editor-cut', (
                                         'Ctrl+X',
                                         'Shift+Delete',
                                     ), _('Cut text'))
        self.action_editor_copy = reg('edit-copy.png', _('&Copy text'),
                                      self.boss.do_editor_copy,
                                      'editor-copy', ('Ctrl+C', 'Ctrl+Insert'),
                                      _('Copy text'))
        self.action_editor_paste = reg('edit-paste.png', _('&Paste text'),
                                       self.boss.do_editor_paste,
                                       'editor-paste', (
                                           'Ctrl+V',
                                           'Shift+Insert',
                                       ), _('Paste text'))
        self.action_editor_cut.setEnabled(False)
        self.action_editor_copy.setEnabled(False)
        self.action_editor_undo.setEnabled(False)
        self.action_editor_redo.setEnabled(False)

        # Tool actions
        group = _('Tools')
        self.action_toc = reg('toc.png', _('&Edit Table of Contents'),
                              self.boss.edit_toc, 'edit-toc', (),
                              _('Edit Table of Contents'))
        self.action_fix_html_current = reg('html-fix.png', _('&Fix HTML'),
                                           partial(self.boss.fix_html, True),
                                           'fix-html-current', (),
                                           _('Fix HTML in the current file'))
        self.action_fix_html_all = reg('html-fix.png',
                                       _('&Fix HTML - all files'),
                                       partial(self.boss.fix_html, False),
                                       'fix-html-all', (),
                                       _('Fix HTML in all files'))
        self.action_pretty_current = reg('format-justify-fill.png',
                                         _('&Beautify current file'),
                                         partial(self.boss.pretty_print, True),
                                         'pretty-current', (),
                                         _('Beautify current file'))
        self.action_pretty_all = reg('format-justify-fill.png',
                                     _('&Beautify all files'),
                                     partial(self.boss.pretty_print, False),
                                     'pretty-all', (), _('Beautify all files'))

        # Polish actions
        group = _('Polish Book')
        self.action_subset_fonts = reg(
            'subset-fonts.png', _('&Subset embedded fonts'),
            partial(self.boss.polish, 'subset', _('Subset fonts')),
            'subset-fonts', (), _('Subset embedded fonts'))
        self.action_embed_fonts = reg(
            'embed-fonts.png', _('&Embed referenced fonts'),
            partial(self.boss.polish, 'embed', _('Embed fonts')),
            'embed-fonts', (), _('Embed referenced fonts'))
        self.action_smarten_punctuation = reg(
            'smarten-punctuation.png', _('&Smarten punctuation'),
            partial(self.boss.polish, 'smarten_punctuation',
                    _('Smarten punctuation')), 'smarten-punctuation', (),
            _('Smarten punctuation'))

        # Preview actions
        group = _('Preview')
        self.action_auto_reload_preview = reg('auto-reload.png',
                                              _('Auto reload preview'), None,
                                              'auto-reload-preview', (),
                                              _('Auto reload preview'))
        self.action_auto_sync_preview = reg(
            'sync-right.png', _('Sync preview position to editor position'),
            None, 'sync-preview-to-editor', (),
            _('Sync preview position to editor position'))
        self.action_reload_preview = reg('view-refresh.png',
                                         _('Refresh preview'), None,
                                         'reload-preview', ('F5', ),
                                         _('Refresh preview'))
        self.action_split_in_preview = reg(
            'auto_author_sort.png', _('Split this file'), None,
            'split-in-preview', (), _('Split file in the preview panel'))
        self.action_find_next_preview = reg('arrow-down.png', _('Find Next'),
                                            None, 'find-next-preview', (),
                                            _('Find next in preview'))
        self.action_find_prev_preview = reg('arrow-up.png', _('Find Previous'),
                                            None, 'find-prev-preview', (),
                                            _('Find previous in preview'))

        # Search actions
        group = _('Search')
        self.action_find = reg('search.png', _('&Find/Replace'),
                               self.boss.show_find, 'find-replace',
                               ('Ctrl+F', ), _('Show the Find/Replace panel'))

        def sreg(name,
                 text,
                 action,
                 overrides={},
                 keys=(),
                 description=None,
                 icon=None):
            return reg(icon, text, partial(self.boss.search, action,
                                           overrides), name, keys, description
                       or text.replace('&', ''))

        self.action_find_next = sreg('find-next', _('Find &Next'), 'find',
                                     {'direction': 'down'}, ('F3', 'Ctrl+G'),
                                     _('Find next match'))
        self.action_find_previous = sreg('find-previous', _('Find &Previous'),
                                         'find', {'direction': 'up'},
                                         ('Shift+F3', 'Shift+Ctrl+G'),
                                         _('Find previous match'))
        self.action_replace = sreg('replace',
                                   _('Replace'),
                                   'replace',
                                   keys=('Ctrl+R'),
                                   description=_('Replace current match'))
        self.action_replace_next = sreg(
            'replace-next', _('&Replace and find next'), 'replace-find',
            {'direction': 'down'}, ('Ctrl+]'),
            _('Replace current match and find next'))
        self.action_replace_previous = sreg(
            'replace-previous', _('R&eplace and find previous'),
            'replace-find', {'direction': 'up'}, ('Ctrl+['),
            _('Replace current match and find previous'))
        self.action_replace_all = sreg('replace-all',
                                       _('Replace &all'),
                                       'replace-all',
                                       keys=('Ctrl+A'),
                                       description=_('Replace all matches'))
        self.action_count = sreg('count-matches',
                                 _('&Count all'),
                                 'count',
                                 keys=('Ctrl+N'),
                                 description=_('Count number of matches'))
        self.action_mark = reg(None, _('&Mark selected text'),
                               self.boss.mark_selected_text,
                               'mark-selected-text', ('Ctrl+Shift+M', ),
                               _('Mark selected text'))
        self.action_go_to_line = reg(None, _('Go to &line'),
                                     self.boss.go_to_line_number,
                                     'go-to-line-number', ('Ctrl+.', ),
                                     _('Go to line number'))

        # Miscellaneous actions
        group = _('Miscellaneous')
        self.action_create_checkpoint = reg(
            'marked.png', _('&Create checkpoint'), self.boss.create_checkpoint,
            'create-checkpoint', (),
            _('Create a checkpoint with the current state of the book'))

    def create_menubar(self):
        p, q = self.create_application_menubar()
        q.triggered.connect(self.action_quit.trigger)
        p.triggered.connect(self.action_preferences.trigger)
        b = self.menuBar()

        f = b.addMenu(_('&File'))
        f.addAction(self.action_new_file)
        f.addAction(self.action_open_book)
        f.addAction(self.action_save)
        f.addAction(self.action_quit)

        e = b.addMenu(_('&Edit'))
        e.addAction(self.action_global_undo)
        e.addAction(self.action_global_redo)
        e.addAction(self.action_create_checkpoint)
        e.addSeparator()
        e.addAction(self.action_editor_undo)
        e.addAction(self.action_editor_redo)
        e.addSeparator()
        e.addAction(self.action_editor_cut)
        e.addAction(self.action_editor_copy)
        e.addAction(self.action_editor_paste)
        e.addSeparator()
        e.addAction(self.action_preferences)

        e = b.addMenu(_('&Tools'))
        e.addAction(self.action_toc)
        e.addAction(self.action_embed_fonts)
        e.addAction(self.action_subset_fonts)
        e.addAction(self.action_smarten_punctuation)
        e.addAction(self.action_fix_html_all)
        e.addAction(self.action_pretty_all)

        e = b.addMenu(_('&View'))
        t = e.addMenu(_('Tool&bars'))
        e.addSeparator()
        for name, ac in actions.iteritems():
            if name.endswith('-dock'):
                e.addAction(ac)
            elif name.endswith('-bar'):
                t.addAction(ac)

        e = b.addMenu(_('&Search'))
        a = e.addAction
        a(self.action_find)
        e.addSeparator()
        a(self.action_find_next)
        a(self.action_find_previous)
        e.addSeparator()
        a(self.action_replace)
        a(self.action_replace_next)
        a(self.action_replace_previous)
        a(self.action_replace_all)
        e.addSeparator()
        a(self.action_count)
        e.addSeparator()
        a(self.action_mark)
        e.addSeparator()
        a(self.action_go_to_line)

    def create_toolbars(self):
        def create(text, name):
            name += '-bar'
            b = self.addToolBar(text)
            b.setObjectName(name)  # Needed for saveState
            setattr(self, name.replace('-', '_'), b)
            actions[name] = b.toggleViewAction()
            return b

        a = create(_('Book tool bar'), 'global').addAction
        for x in ('new_file', 'open_book', 'global_undo', 'global_redo',
                  'save', 'create_checkpoint', 'toc'):
            a(getattr(self, 'action_' + x))

        a = create(_('Polish book tool bar'), 'polish').addAction
        for x in ('embed_fonts', 'subset_fonts', 'smarten_punctuation'):
            a(getattr(self, 'action_' + x))

    def create_docks(self):
        def create(name, oname):
            oname += '-dock'
            d = QDockWidget(name, self)
            d.setObjectName(oname)  # Needed for saveState
            ac = d.toggleViewAction()
            desc = _('Toggle %s') % name.replace('&', '')
            self.keyboard.register_shortcut(oname,
                                            desc,
                                            description=desc,
                                            action=ac,
                                            group=_('Windows'))
            actions[oname] = ac
            setattr(self, oname.replace('-', '_'), d)
            return d

        d = create(_('&Files Browser'), 'files-browser')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
        self.file_list = FileListWidget(d)
        d.setWidget(self.file_list)
        self.addDockWidget(Qt.LeftDockWidgetArea, d)

        d = create(_('File &Preview'), 'preview')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
        self.preview = Preview(d)
        d.setWidget(self.preview)
        self.addDockWidget(Qt.RightDockWidgetArea, d)

        d = create(_('&Inspector'), 'inspector')
        d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea
                          | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
        d.setWidget(self.preview.inspector)
        self.preview.inspector.setParent(d)
        self.addDockWidget(Qt.BottomDockWidgetArea, d)

    def resizeEvent(self, ev):
        self.blocking_job.resize(ev.size())
        return super(Main, self).resizeEvent(ev)

    def update_window_title(self):
        self.setWindowTitle(
            self.current_metadata.title + ' [%s] - %s' %
            (current_container().book_type.upper(), self.APP_NAME))

    def closeEvent(self, e):
        if not self.boss.confirm_quit():
            e.ignore()
            return
        try:
            self.boss.shutdown()
        except:
            import traceback
            traceback.print_exc()
        e.accept()

    def save_state(self):
        tprefs.set('main_window_geometry', bytearray(self.saveGeometry()))
        tprefs.set('main_window_state',
                   bytearray(self.saveState(self.STATE_VERSION)))
        self.central.save_state()

    def restore_state(self):
        geom = tprefs.get('main_window_geometry', None)
        if geom is not None:
            self.restoreGeometry(geom)
        state = tprefs.get('main_window_state', None)
        if state is not None:
            self.restoreState(state, self.STATE_VERSION)
        self.central.restore_state()
        # We never want to start with the inspector showing
        self.inspector_dock.close()

    def contextMenuEvent(self, ev):
        ev.ignore()