Exemplo n.º 1
0
 def contextMenuEvent(self, event):
     menu = QMenu()
     testAction = QAction('Go Inside', None)
     testAction.triggered.connect(self.showTempleView)
     menu.addAction(testAction)
     menu.exec_(event.screenPos())
     event.accept()
Exemplo n.º 2
0
    def show_context_menu(self, point):
        self.context_item = self.table.itemAt(point)
        case_menu = QMenu(_('Change Case'))
        action_upper_case = case_menu.addAction(_('Upper Case'))
        action_lower_case = case_menu.addAction(_('Lower Case'))
        action_swap_case = case_menu.addAction(_('Swap Case'))
        action_title_case = case_menu.addAction(_('Title Case'))
        action_capitalize = case_menu.addAction(_('Capitalize'))

        action_upper_case.triggered.connect(self.upper_case)
        action_lower_case.triggered.connect(self.lower_case)
        action_swap_case.triggered.connect(self.swap_case)
        action_title_case.triggered.connect(self.title_case)
        action_capitalize.triggered.connect(self.capitalize)

        m = self.au_context_menu = QMenu()
        ca = m.addAction(_('Copy'))
        ca.triggered.connect(self.copy_to_clipboard)
        ca = m.addAction(_('Paste'))
        ca.triggered.connect(self.paste_from_clipboard)
        m.addSeparator()

        if self.context_item is not None and self.context_item.column() == 0:
            ca = m.addAction(_('Copy to author sort'))
            ca.triggered.connect(self.copy_au_to_aus)
        else:
            ca = m.addAction(_('Copy to author'))
            ca.triggered.connect(self.copy_aus_to_au)
        m.addSeparator()
        m.addMenu(case_menu)
        m.exec_(self.table.mapToGlobal(point))
Exemplo n.º 3
0
    def setupFileActions(self):
        tb = QToolBar(self)
        tb.setWindowTitle("File Actions")
        self.addToolBar(tb)

        menu = QMenu("&File", self)
        self.menuBar().addMenu(menu)

        self.actionNew = QAction("&New", self, priority=QAction.LowPriority,
                shortcut=QKeySequence.New, triggered=self.fileNew)
        tb.addAction(self.actionNew)
        menu.addAction(self.actionNew)

        self.actionOpen = QAction("&Open...", self, shortcut=QKeySequence.Open,
                triggered=self.fileOpen)
        tb.addAction(self.actionOpen)
        menu.addAction(self.actionOpen)
        menu.addSeparator()

        self.actionSave = QAction("&Save", self, shortcut=QKeySequence.Save,
                triggered=self.fileSave, enabled=False)
        tb.addAction(self.actionSave)
        menu.addAction(self.actionSave)

        self.actionSaveAs = QAction("Save &As...", self, priority=QAction.LowPriority,
                shortcut=Qt.CTRL + Qt.SHIFT + Qt.Key_S, triggered=self.fileSaveAs)
        menu.addAction(self.actionSaveAs)
        menu.addSeparator()
 
        self.actionQuit = QAction("&Quit", self, shortcut=QKeySequence.Quit, triggered=self.close)
        menu.addAction(self.actionQuit)
Exemplo n.º 4
0
    def setupEditActions(self):
        tb = QToolBar(self)
        tb.setWindowTitle("Edit Actions")
        self.addToolBar(tb)

        menu = QMenu("&Edit", self)
        self.menuBar().addMenu(menu)

        self.actionUndo = QAction("&Undo", self, shortcut=QKeySequence.Undo)
        tb.addAction(self.actionUndo)
        menu.addAction(self.actionUndo)

        self.actionRedo = QAction("&Redo", self, priority=QAction.LowPriority, shortcut=QKeySequence.Redo)
        tb.addAction(self.actionRedo)
        menu.addAction(self.actionRedo)
        menu.addSeparator()

        self.actionCut = QAction("Cu&t", self, priority=QAction.LowPriority, shortcut=QKeySequence.Cut)
        tb.addAction(self.actionCut)
        menu.addAction(self.actionCut)

        self.actionCopy = QAction("&Copy", self, priority=QAction.LowPriority, shortcut=QKeySequence.Copy)
        tb.addAction(self.actionCopy)
        menu.addAction(self.actionCopy)

        self.actionPaste = QAction("&Paste", self, priority=QAction.LowPriority,
                shortcut=QKeySequence.Paste, enabled=(len(QApplication.clipboard().text()) != 0))
        tb.addAction(self.actionPaste)
        menu.addAction(self.actionPaste)
Exemplo n.º 5
0
 def contextMenuEvent(self, ev):
     m = QMenu(self)
     m.addAction(_('Set to undefined') + '\t' + QKeySequence(Qt.Key_Space).toString(QKeySequence.NativeText),
                 self.clear_to_undefined)
     m.addSeparator()
     populate_standard_spinbox_context_menu(self, m)
     m.popup(ev.globalPos())
Exemplo n.º 6
0
    def contextMenuEvent(self, ev):
        from calibre.gui2.open_with import populate_menu, edit_programs

        cm = QMenu(self)
        paste = cm.addAction(_("Paste Cover"))
        copy = cm.addAction(_("Copy Cover"))
        remove = cm.addAction(_("Remove Cover"))
        gc = cm.addAction(_("Generate Cover from metadata"))
        if not QApplication.instance().clipboard().mimeData().hasImage():
            paste.setEnabled(False)
        copy.triggered.connect(self.copy_to_clipboard)
        paste.triggered.connect(self.paste_from_clipboard)
        remove.triggered.connect(self.remove_cover)
        gc.triggered.connect(self.generate_cover)

        m = QMenu(_("Open cover with..."))
        populate_menu(m, self.open_with, "cover_image")
        if len(m.actions()) == 0:
            cm.addAction(_("Open cover with..."), self.choose_open_with)
        else:
            m.addSeparator()
            m.addAction(_("Add another application to open cover..."), self.choose_open_with)
            m.addAction(_("Edit Open With applications..."), partial(edit_programs, "cover_image", self))
            cm.addMenu(m)
        cm.exec_(ev.globalPos())
Exemplo n.º 7
0
 def clone_changed(self):
     otext = self.text()
     self.setText(self.clone.text())
     if otext != self.text:
         self.text_changed.emit()
     ov = self.isVisible()
     self.setVisible(self.clone.isVisible())
     if ov != self.isVisible():
         self.visibility_changed.emit()
     self.setEnabled(self.clone.isEnabled())
     self.setCheckable(self.clone.isCheckable())
     self.setChecked(self.clone.isChecked())
     self.setIcon(self.clone.icon())
     if self.clone_shortcuts:
         self.setShortcuts(self.clone.shortcuts())
     if self.clone.menu() is None:
         if not self.is_top_level:
             self.setMenu(None)
     else:
         m = QMenu(self.text(), self.parent())
         for ac in QMenu.actions(self.clone.menu()):
             if ac.isSeparator():
                 m.addSeparator()
             else:
                 m.addAction(CloneAction(ac, self.parent(), clone_shortcuts=self.clone_shortcuts))
         self.setMenu(m)
Exemplo n.º 8
0
 def show_context_menu(self, point):
     idx = self.currentIndex()
     if idx and idx.isValid() and not idx.data(Qt.UserRole):
         m = QMenu()
         m.addAction(QIcon(I('view.png')), _('View this cover at full size'), self.show_cover)
         m.addAction(QIcon(I('edit-copy.png')), _('Copy this cover to clipboard'), self.copy_cover)
         m.exec_(QCursor.pos())
Exemplo n.º 9
0
        def ac(name, text, icon, tooltip):
            icon = QIcon(I(icon))
            ac = self.location_actions.addAction(icon, text)
            setattr(self, 'location_'+name, ac)
            ac.setAutoRepeat(False)
            ac.setCheckable(True)
            receiver = partial(self._location_selected, name)
            ac.triggered.connect(receiver)
            self.tooltips[name] = tooltip

            m = QMenu(parent)
            self._mem.append(m)
            a = m.addAction(icon, tooltip)
            a.triggered.connect(receiver)
            if name != 'library':
                self._mem.append(a)
                a = m.addAction(QIcon(I('eject.png')), _('Eject this device'))
                a.triggered.connect(self._eject_requested)
                self._mem.append(a)
                a = m.addAction(QIcon(I('config.png')), _('Configure this device'))
                a.triggered.connect(self._configure_requested)
                self._mem.append(a)
                a = m.addAction(QIcon(I('sync.png')), _('Update cached metadata on device'))
                a.triggered.connect(lambda x : self.update_device_metadata.emit())
                self._mem.append(a)

            else:
                ac.setToolTip(tooltip)
            ac.setMenu(m)
            ac.calibre_name = name

            self.all_actions.append(ac)
            return ac
Exemplo n.º 10
0
    def contextMenuEvent(self, ev):
        from calibre.gui2.open_with import populate_menu, edit_programs
        cm = QMenu(self)
        paste = cm.addAction(_('Paste cover'))
        copy = cm.addAction(_('Copy cover'))
        remove = cm.addAction(_('Remove cover'))
        gc = cm.addAction(_('Generate cover from metadata'))
        cm.addSeparator()
        if not QApplication.instance().clipboard().mimeData().hasImage():
            paste.setEnabled(False)
        copy.triggered.connect(self.copy_to_clipboard)
        paste.triggered.connect(self.paste_from_clipboard)
        remove.triggered.connect(self.remove_cover)
        gc.triggered.connect(self.generate_cover)

        m = QMenu(_('Open cover with...'))
        populate_menu(m, self.open_with, 'cover_image')
        if len(m.actions()) == 0:
            cm.addAction(_('Open cover with...'), self.choose_open_with)
        else:
            m.addSeparator()
            m.addAction(_('Add another application to open cover...'), self.choose_open_with)
            m.addAction(_('Edit Open with applications...'), partial(edit_programs, 'cover_image', self))
            cm.ocw = m
            cm.addMenu(m)
        cm.si = m = create_search_internet_menu(self.search_internet.emit)
        cm.addMenu(m)
        cm.exec_(ev.globalPos())
Exemplo n.º 11
0
 def show_context_menu(self, point):
     item = self.folders.itemAt(point)
     if item is None:
         return
     m = QMenu(self)
     m.addAction(QIcon(I('mimetypes/dir.png')), _('Create new folder'), partial(self.create_folder, item))
     m.popup(self.folders.mapToGlobal(point))
Exemplo n.º 12
0
 def context_menu_requested(self, widget, ev):
     if isinstance(widget, Heading):
         start = widget
     else:
         found = False
         for w in reversed(self.widgets):
             if w is widget:
                 found = True
             elif found and isinstance(w, Heading):
                 start = w
                 break
         else:
             return
     found = False
     lines = []
     for w in self.widgets:
         if found and isinstance(w, Heading):
             break
         if w is start:
             found = True
         if found:
             lines += w.lines_for_copy
     if not lines:
         return
     block = '\n'.join(lines).replace('\xa0', ' ')
     heading = lines[0]
     m = QMenu(self)
     m.addAction(QIcon(I('edit-copy.png')), _('Copy') + ' ' + heading.replace('\xa0', ' '), lambda : QApplication.instance().clipboard().setText(block))
     all_lines = []
     for w in self.widgets:
         all_lines += w.lines_for_copy
     all_text = '\n'.join(all_lines).replace('\xa0', ' ')
     m.addAction(QIcon(I('edit-copy.png')), _('Copy everything'), lambda : QApplication.instance().clipboard().setText(all_text))
     m.exec_(ev.globalPos())
Exemplo n.º 13
0
 def build_menu(self, action):
     m = action.menu()
     ac = MenuAction(action, self)
     if m is None:
         m = QMenu()
         m.addAction(action)
     ac.setMenu(m)
     return ac
Exemplo n.º 14
0
 def showTreeMenu(self,point):
     item = self.ui.treeWidget.itemAt(point)
     if item != None and item.parent() != None:
         self.curDb = item.parent().text(0)
         self.curCol = item.text(0)
         ctxMenu = QMenu()
         ctxMenu.addAction(self.ctxAction)
         ctxMenu.exec_(QtGui.QCursor.pos())
Exemplo n.º 15
0
 def show_context_menu(self, pos):
     pos = self.viewport().mapToGlobal(pos)
     locations = self.selected_locations
     m = QMenu(self)
     if locations:
         m.addAction(_('Delete selected files'), self.delete_selected)
     self.customize_context_menu(m, locations, self.current_location)
     if len(m.actions()) > 0:
         m.exec_(pos)
Exemplo n.º 16
0
 def contextMenuEvent(self, ev):
     m = QMenu(self)
     m.addAction(_('Set date to undefined') + '\t' + QKeySequence(Qt.Key_Minus).toString(QKeySequence.NativeText),
                 self.clear_date)
     m.addAction(_('Set date to today') + '\t' + QKeySequence(Qt.Key_Equal).toString(QKeySequence.NativeText),
                 self.today_date)
     m.addSeparator()
     populate_standard_spinbox_context_menu(self, m)
     m.popup(ev.globalPos())
Exemplo n.º 17
0
 def clone_one_menu(m):
     ans = QMenu(m.parent())
     for ac in m.actions():
         cac = clone_action(ac, ans)
         ans.addAction(cac)
         m = ac.menu()
         if m is not None:
             cac.setMenu(clone_menu(m))
     return ans
Exemplo n.º 18
0
 def build_context_menu(self):
     cm = QMenu(self)
     paste = cm.addAction(_('Paste cover'))
     copy = cm.addAction(_('Copy cover'))
     if not QApplication.instance().clipboard().mimeData().hasImage():
         paste.setEnabled(False)
     copy.triggered.connect(self.copy_to_clipboard)
     paste.triggered.connect(self.paste_from_clipboard)
     return cm
Exemplo n.º 19
0
    def create_action(self, spec=None, attr='qaction', shortcut_name=None):
        if spec is None:
            spec = self.action_spec
        text, icon, tooltip, shortcut = spec
        if icon is not None:
            action = QAction(QIcon(I(icon)), text, self.gui)
        else:
            action = QAction(text, self.gui)
        if attr == 'qaction':
            mt = (action.text() if self.action_menu_clone_qaction is True else
                    unicode(self.action_menu_clone_qaction))
            self.menuless_qaction = ma = QAction(action.icon(), mt, self.gui)
            ma.triggered.connect(action.trigger)
        for a in ((action, ma) if attr == 'qaction' else (action,)):
            a.setAutoRepeat(self.auto_repeat)
            text = tooltip if tooltip else text
            a.setToolTip(text)
            a.setStatusTip(text)
            a.setWhatsThis(text)
        shortcut_action = action
        desc = tooltip if tooltip else None
        if attr == 'qaction':
            shortcut_action = ma
        if shortcut is not None:
            keys = ((shortcut,) if isinstance(shortcut, basestring) else
                    tuple(shortcut))
            if shortcut_name is None and spec[0]:
                shortcut_name = unicode(spec[0])

            if shortcut_name and self.action_spec[0] and not (
                    attr == 'qaction' and self.popup_type == QToolButton.InstantPopup):
                try:
                    self.gui.keyboard.register_shortcut(self.unique_name + ' - ' + attr,
                        shortcut_name, default_keys=keys,
                        action=shortcut_action, description=desc,
                        group=self.action_spec[0])
                except NameConflict as e:
                    try:
                        prints(unicode(e))
                    except:
                        pass
                    shortcut_action.setShortcuts([QKeySequence(key,
                        QKeySequence.PortableText) for key in keys])
                else:
                    if isosx:
                        # In Qt 5 keyboard shortcuts dont work unless the
                        # action is explicitly added to the main window
                        self.gui.addAction(shortcut_action)

        if attr is not None:
            setattr(self, attr, action)
        if attr == 'qaction' and self.action_add_menu:
            menu = QMenu()
            action.setMenu(menu)
            if self.action_menu_clone_qaction:
                menu.addAction(self.menuless_qaction)
        return action
Exemplo n.º 20
0
 def publish_new_menu(self):
     menu = self.notifier.contextMenu()
     if menu is None:
         menu = QMenu()
     if len(menu.actions()) == 0:
         menu.addAction(self.notifier.icon(), _('Show/hide %s') % self.title, self.notifier.emit_activated)
     # The menu must have at least one entry, namely the show/hide entry.
     # This is necessary as Canonical in their infinite wisdom decided to
     # force all tray icons to show their popup menus when clicked.
     self.dbus_menu.publish_new_menu(menu)
Exemplo n.º 21
0
def create_search_internet_menu(callback, author=None):
    m = QMenu((
        _('Search the internet for the author {}').format(author)
        if author is not None else
        _('Search the internet for this book')) + '…'
    )
    items = all_book_searches() if author is None else all_author_searches()
    for k in sorted(items, key=lambda k: name_for(k).lower()):
        m.addAction(name_for(k), partial(callback, InternetSearch(author, k)))
    return m
Exemplo n.º 22
0
 def contextMenuEvent(self, ev):
     m = QMenu(self)
     m.addAction(_('Sort alphabetically'), self.sort_alphabetically)
     hidden = self.current_db.prefs['virt_libs_hidden']
     if hidden:
         s = m._s = m.addMenu(_('Restore hidden tabs'))
         for x in hidden:
             s.addAction(x, partial(self.restore, x))
     m.addAction(_('Hide virtual library tabs'), self.disable_bar)
     m.exec_(ev.globalPos())
Exemplo n.º 23
0
 def contextMenuEvent(self, ev):
     cm = QMenu(self)
     paste = cm.addAction(_("Paste Cover"))
     copy = cm.addAction(_("Copy Cover"))
     remove = cm.addAction(_("Remove Cover"))
     if not QApplication.instance().clipboard().mimeData().hasImage():
         paste.setEnabled(False)
     copy.triggered.connect(self.copy_to_clipboard)
     paste.triggered.connect(self.paste_from_clipboard)
     remove.triggered.connect(self.remove_cover)
     cm.exec_(ev.globalPos())
Exemplo n.º 24
0
    class MenuBar(QMenuBar):
        def __init__(self, location_manager, parent):
            QMenuBar.__init__(self, parent)
            parent.setMenuBar(self)
            self.gui = parent

            self.location_manager = location_manager
            self.added_actions = []

            self.donate_action = QAction(_("Donate"), self)
            self.donate_menu = QMenu()
            self.donate_menu.addAction(self.gui.donate_action)
            self.donate_action.setMenu(self.donate_menu)

        def init_bar(self, actions):
            for ac in self.added_actions:
                m = ac.menu()
                if m is not None:
                    m.setVisible(False)

            self.clear()
            self.added_actions = []

            for what in actions:
                if what is None:
                    continue
                elif what == "Location Manager":
                    for ac in self.location_manager.all_actions:
                        ac = self.build_menu(ac)
                        self.addAction(ac)
                        self.added_actions.append(ac)
                        ac.setVisible(False)
                elif what == "Donate":
                    self.addAction(self.donate_action)
                elif what in self.gui.iactions:
                    action = self.gui.iactions[what]
                    ac = self.build_menu(action.qaction)
                    self.addAction(ac)
                    self.added_actions.append(ac)

        def build_menu(self, action):
            m = action.menu()
            ac = MenuAction(action, self)
            if m is None:
                m = QMenu()
                m.addAction(action)
            ac.setMenu(m)
            return ac

        def update_lm_actions(self):
            for ac in self.added_actions:
                clone = getattr(ac, "clone", None)
                if clone is not None and clone in self.location_manager.all_actions:
                    ac.setVisible(clone in self.location_manager.available_actions)
Exemplo n.º 25
0
 def create_application_menubar(cls):
     if not cls.__actions:
         mb = QMenuBar(None)
         menu = QMenu()
         for action in cls.get_menubar_actions():
             menu.addAction(action)
             cls.__actions.append(action)
         mb.addMenu(menu)
         cls.___menu_bar = mb
         cls.___menu = menu
     return cls.__actions
Exemplo n.º 26
0
 def contextMenuEvent(self, event):
     if not self.isColumnHidden(0):
         menu = QMenu(self)
         checkAction = menu.addAction(_("Check Selected"))
         uncheckAction = menu.addAction(_("Uncheck Selected"))
         action = menu.exec_(self.mapToGlobal(event.pos()))
         for row in self.get_selected_rows():
             cb = self.item(self.get_row_linenum(row),0)
             if action == checkAction:
                 cb.setCheckState(Qt.Checked)
             if action == uncheckAction:
                 cb.setCheckState(Qt.Unchecked)
Exemplo n.º 27
0
 def show_context_menu(self, pos):
     menu = QMenu(self)
     menu.addAction(actions['edit-toc'])
     menu.addAction(_('&Expand all'), self.view.expandAll)
     menu.addAction(_('&Collapse all'), self.view.collapseAll)
     menu.addAction(self.refresh_action)
     menu.exec_(self.view.mapToGlobal(pos))
Exemplo n.º 28
0
 def populate_open_with(self):
     from calibre.gui2.open_with import populate_menu, edit_programs
     menu = self.own
     menu.clear()
     fmt = self._formats[self.formats.currentRow()]
     m = QMenu(_('Open %s with...') % fmt.upper(), menu)
     populate_menu(m, self.open_with, fmt)
     if len(m.actions()) == 0:
         menu.addAction(_('Open %s with...') % fmt.upper(), self.choose_open_with)
     else:
         m.addSeparator()
         m.addAction(_('Add other application for %s files...') % fmt.upper(), self.choose_open_with)
         m.addAction(_('Edit Open With applications...'), partial(edit_programs, fmt, self))
         menu.addMenu(m)
Exemplo n.º 29
0
 def build_menu(self, ac):
     ans = CloneAction(ac, self.native_menubar, is_top_level=True)
     if ans.menu() is None:
         m = QMenu()
         m.addAction(CloneAction(ac, self.native_menubar))
         ans.setMenu(m)
     # Qt (as of 5.3.0) does not update global menubar entries
     # correctly, so we have to rebuild the global menubar.
     # Without this the Choose Library action shows the text
     # 'Untitled' and the Location Manager items do not work.
     ans.text_changed.connect(self.refresh_timer.start)
     ans.visibility_changed.connect(self.refresh_timer.start)
     self.native_menubar.addAction(ans)
     self.added_actions.append(ans)
     return ans
Exemplo n.º 30
0
 def contextMenuEvent(self, event):
     index = self.indexAt(event.pos())
     
     if not index.isValid():
         return
     
     result = self.model().get_result(index)
     
     menu = QMenu()
     da = menu.addAction(_('Download...'), partial(self.download_requested.emit, result))
     if not result.downloads:
         da.setEnabled(False)
     menu.addSeparator()
     menu.addAction(_('Goto in store...'), partial(self.open_requested.emit, result))
     menu.exec_(event.globalPos())
Exemplo n.º 31
0
    def setup_ui(self):
        self.setWindowIcon(QIcon(I('diff.png')))
        self.stacks = st = QStackedLayout(self)
        self.busy = BusyWidget(self)
        self.w = QWidget(self)
        st.addWidget(self.busy), st.addWidget(self.w)

        self.setLayout(st)
        self.l = l = QGridLayout()
        self.w.setLayout(l)

        self.view = v = DiffView(self,
                                 show_open_in_editor=self.show_open_in_editor)
        l.addWidget(v, l.rowCount(), 0, 1, -1)

        r = l.rowCount()
        self.bp = b = QToolButton(self)
        b.setIcon(QIcon(I('back.png')))
        connect_lambda(b.clicked, self, lambda self: self.view.next_change(-1))
        b.setToolTip(_('Go to previous change') + ' [p]')
        b.setText(_('&Previous change')), b.setToolButtonStyle(
            Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
        l.addWidget(b, r, 0)

        self.bn = b = QToolButton(self)
        b.setIcon(QIcon(I('forward.png')))
        connect_lambda(b.clicked, self, lambda self: self.view.next_change(1))
        b.setToolTip(_('Go to next change') + ' [n]')
        b.setText(_('&Next change')), b.setToolButtonStyle(
            Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
        l.addWidget(b, r, 1)

        self.search = s = HistoryLineEdit2(self)
        s.initialize('diff_search_history')
        l.addWidget(s, r, 2)
        s.setPlaceholderText(_('Search for text'))
        connect_lambda(s.returnPressed, self,
                       lambda self: self.do_search(False))
        self.sbn = b = QToolButton(self)
        b.setIcon(QIcon(I('arrow-down.png')))
        connect_lambda(b.clicked, self, lambda self: self.do_search(False))
        b.setToolTip(_('Find next match'))
        b.setText(_('Next &match')), b.setToolButtonStyle(
            Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
        l.addWidget(b, r, 3)
        self.sbp = b = QToolButton(self)
        b.setIcon(QIcon(I('arrow-up.png')))
        connect_lambda(b.clicked, self, lambda self: self.do_search(True))
        b.setToolTip(_('Find previous match'))
        b.setText(_('P&revious match')), b.setToolButtonStyle(
            Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
        l.addWidget(b, r, 4)
        self.lb = b = QRadioButton(_('Left panel'), self)
        b.setToolTip(_('Perform search in the left panel'))
        l.addWidget(b, r, 5)
        self.rb = b = QRadioButton(_('Right panel'), self)
        b.setToolTip(_('Perform search in the right panel'))
        l.addWidget(b, r, 6)
        b.setChecked(True)
        self.pb = b = QToolButton(self)
        b.setIcon(QIcon(I('config.png')))
        b.setText(_('&Options')), b.setToolButtonStyle(
            Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
        b.setToolTip(_('Change how the differences are displayed'))
        b.setPopupMode(b.InstantPopup)
        m = QMenu(b)
        b.setMenu(m)
        cm = self.cm = QMenu(_('Lines of context around each change'))
        for i in (3, 5, 10, 50):
            cm.addAction(
                _('Show %d lines of context') % i,
                partial(self.change_context, i))
        cm.addAction(_('Show all text'), partial(self.change_context, None))
        self.beautify_action = m.addAction('', self.toggle_beautify)
        self.set_beautify_action_text()
        m.addMenu(cm)
        l.addWidget(b, r, 7)

        self.hl = QHBoxLayout()
        l.addLayout(self.hl, l.rowCount(), 0, 1, -1)
        self.names = QLabel('')
        self.hl.addWidget(self.names, r)

        self.bb.setStandardButtons(self.bb.Close)
        if self.revert_button_msg is not None:
            self.rvb = b = self.bb.addButton(self.revert_button_msg,
                                             self.bb.ActionRole)
            b.setIcon(QIcon(I('edit-undo.png'))), b.setAutoDefault(False)
            b.clicked.connect(self.revert_requested)
            b.clicked.connect(self.reject)
        self.bb.button(self.bb.Close).setDefault(True)
        self.hl.addWidget(self.bb, r)

        self.view.setFocus(Qt.FocusReason.OtherFocusReason)
Exemplo n.º 32
0
def details_context_menu_event(view, ev, book_info):  # {{{
    p = view.page()
    mf = p.mainFrame()
    r = mf.hitTestContent(ev.pos())
    url = unicode_type(r.linkUrl().toString(NO_URL_FORMATTING)).strip()
    menu = p.createStandardContextMenu()
    ca = view.pageAction(p.Copy)
    for action in list(menu.actions()):
        if action is not ca:
            menu.removeAction(action)
    menu.addAction(QIcon(I('edit-copy.png')), _('Copy &all'),
                   partial(copy_all, book_info))
    search_internet_added = False
    if not r.isNull():
        from calibre.ebooks.oeb.polish.main import SUPPORTED
        if url.startswith('format:'):
            parts = url.split(':')
            try:
                book_id, fmt = int(parts[1]), parts[2].upper()
            except:
                import traceback
                traceback.print_exc()
            else:
                from calibre.gui2.ui import get_gui
                db = get_gui().current_db.new_api
                ofmt = fmt.upper() if fmt.startswith(
                    'ORIGINAL_') else 'ORIGINAL_' + fmt
                nfmt = ofmt[len('ORIGINAL_'):]
                fmts = {x.upper() for x in db.formats(book_id)}
                for a, t in [
                    ('remove', _('Delete the %s format')),
                    ('save', _('Save the %s format to disk')),
                    ('restore', _('Restore the %s format')),
                    ('compare', ''),
                    ('set_cover', _('Set the book cover from the %s file')),
                ]:
                    if a == 'restore' and not fmt.startswith('ORIGINAL_'):
                        continue
                    if a == 'compare':
                        if ofmt not in fmts or nfmt not in SUPPORTED:
                            continue
                        t = _('Compare to the %s format') % (
                            fmt[9:] if fmt.startswith('ORIGINAL_') else ofmt)
                    else:
                        t = t % fmt
                    ac = getattr(book_info, '%s_format_action' % a)
                    ac.current_fmt = (book_id, fmt)
                    ac.setText(t)
                    menu.addAction(ac)
                if not fmt.upper().startswith('ORIGINAL_'):
                    from calibre.gui2.open_with import populate_menu, edit_programs
                    m = QMenu(_('Open %s with...') % fmt.upper())

                    def connect_action(ac, entry):
                        connect_lambda(
                            ac.triggered, book_info,
                            lambda book_info: book_info.open_with(
                                book_id, fmt, entry))

                    populate_menu(m, connect_action, fmt)
                    if len(m.actions()) == 0:
                        menu.addAction(
                            _('Open %s with...') % fmt.upper(),
                            partial(book_info.choose_open_with, book_id, fmt))
                    else:
                        m.addSeparator()
                        m.addAction(
                            _('Add other application for %s files...') %
                            fmt.upper(),
                            partial(book_info.choose_open_with, book_id, fmt))
                        m.addAction(_('Edit Open With applications...'),
                                    partial(edit_programs, fmt, book_info))
                        menu.addMenu(m)
                        menu.ow = m
                    if fmt.upper() in SUPPORTED:
                        menu.addSeparator()
                        menu.addAction(
                            _('Edit %s...') % fmt.upper(),
                            partial(book_info.edit_fmt, book_id, fmt))
                ac = book_info.copy_link_action
                ac.current_url = r.linkElement().attribute('data-full-path')
                if ac.current_url:
                    ac.setText(_('&Copy path to file'))
                    menu.addAction(ac)
        else:
            el = r.linkElement()
            data = el.attribute('data-item')
            author = el.toPlainText() if unicode_type(
                el.attribute('calibre-data')) == u'authors' else None
            if url and not url.startswith('search:'):
                for a, t in [
                    ('copy', _('&Copy link')),
                ]:
                    ac = getattr(book_info, '%s_link_action' % a)
                    ac.current_url = url
                    if url.startswith('path:'):
                        ac.current_url = el.attribute('title')
                    ac.setText(t)
                    menu.addAction(ac)
            if author is not None:
                menu.addAction(
                    init_manage_action(book_info.manage_action, 'authors',
                                       author))
                if hasattr(book_info, 'search_internet'):
                    menu.sia = sia = create_search_internet_menu(
                        book_info.search_internet, author)
                    menu.addMenu(sia)
                    search_internet_added = True
                if hasattr(book_info, 'search_requested'):
                    menu.addAction(
                        _('Search calibre for %s') % author, lambda: book_info.
                        search_requested('authors:"={}"'.format(
                            author.replace('"', r'\"'))))
            if data:
                try:
                    field, value, book_id = json_loads(unhexlify(data))
                except Exception:
                    field = value = book_id = None
                if field:
                    if author is None:
                        if field in ('tags', 'series',
                                     'publisher') or is_category(field):
                            menu.addAction(
                                init_manage_action(book_info.manage_action,
                                                   field, value))
                        elif field == 'identifiers':
                            menu.addAction(book_info.edit_identifiers_action)
                    ac = book_info.remove_item_action
                    ac.data = (field, value, book_id)
                    ac.setText(_('Remove %s from this book') % value)
                    menu.addAction(ac)

    if not search_internet_added and hasattr(book_info, 'search_internet'):
        menu.addSeparator()
        menu.si = create_search_internet_menu(book_info.search_internet)
        menu.addMenu(menu.si)
    if len(menu.actions()) > 0:
        menu.exec_(ev.globalPos())
Exemplo n.º 33
0
    def contextMenuEvent(self, ev):
        from calibre.gui2.open_with import populate_menu, edit_programs
        cm = QMenu(self)
        paste = cm.addAction(_('Paste cover'))
        copy = cm.addAction(_('Copy cover'))
        save = cm.addAction(_('Save cover to disk'))
        remove = cm.addAction(_('Remove cover'))
        gc = cm.addAction(_('Generate cover from metadata'))
        cm.addSeparator()
        if not QApplication.instance().clipboard().mimeData().hasImage():
            paste.setEnabled(False)
        copy.triggered.connect(self.copy_to_clipboard)
        paste.triggered.connect(self.paste_from_clipboard)
        remove.triggered.connect(self.remove_cover)
        gc.triggered.connect(self.generate_cover)
        save.triggered.connect(self.save_cover)

        m = QMenu(_('Open cover with...'))

        def connect_action(ac, entry):
            connect_lambda(ac.triggered, self,
                           lambda self: self.open_with(entry))

        populate_menu(m, connect_action, 'cover_image')
        if len(m.actions()) == 0:
            cm.addAction(_('Open cover with...'), self.choose_open_with)
        else:
            m.addSeparator()
            m.addAction(_('Add another application to open cover...'),
                        self.choose_open_with)
            m.addAction(_('Edit Open with applications...'),
                        partial(edit_programs, 'cover_image', self))
            cm.ocw = m
            cm.addMenu(m)
        cm.si = m = create_search_internet_menu(self.search_internet.emit)
        cm.addMenu(m)
        cm.exec_(ev.globalPos())
Exemplo n.º 34
0
class BooksView(QTableView):  # {{{

    files_dropped = pyqtSignal(object)
    add_column_signal = pyqtSignal()
    is_library_view = True

    def viewportEvent(self, event):
        if (event.type() == event.ToolTip
                and not gprefs['book_list_tooltips']):
            return False
        return QTableView.viewportEvent(self, event)

    def __init__(self,
                 parent,
                 modelcls=BooksModel,
                 use_edit_metadata_dialog=True):
        QTableView.__init__(self, parent)
        self.default_row_height = self.verticalHeader().defaultSectionSize()
        self.gui = parent
        self.setProperty('highlight_current_item', 150)
        self.row_sizing_done = False
        self.alternate_views = AlternateViews(self)

        if not tweaks['horizontal_scrolling_per_column']:
            self.setHorizontalScrollMode(self.ScrollPerPixel)

        self.setEditTriggers(self.EditKeyPressed)
        if tweaks['doubleclick_on_library_view'] == 'edit_cell':
            self.setEditTriggers(self.DoubleClicked | self.editTriggers())
        elif tweaks['doubleclick_on_library_view'] == 'open_viewer':
            self.setEditTriggers(self.SelectedClicked | self.editTriggers())
            self.doubleClicked.connect(parent.iactions['View'].view_triggered)
        elif tweaks['doubleclick_on_library_view'] == 'edit_metadata':
            # Must not enable single-click to edit, or the field will remain
            # open in edit mode underneath the edit metadata dialog
            if use_edit_metadata_dialog:
                self.doubleClicked.connect(
                    partial(parent.iactions['Edit Metadata'].edit_metadata,
                            checked=False))
            else:
                self.setEditTriggers(self.DoubleClicked | self.editTriggers())

        setup_dnd_interface(self)
        self.setAlternatingRowColors(True)
        self.setShowGrid(False)
        self.setWordWrap(False)

        self.rating_delegate = RatingDelegate(self)
        self.timestamp_delegate = DateDelegate(self)
        self.pubdate_delegate = PubDateDelegate(self)
        self.last_modified_delegate = DateDelegate(
            self, tweak_name='gui_last_modified_display_format')
        self.languages_delegate = LanguagesDelegate(self)
        self.tags_delegate = CompleteDelegate(self, ',', 'all_tag_names')
        self.authors_delegate = CompleteDelegate(self, '&', 'all_author_names',
                                                 True)
        self.cc_names_delegate = CompleteDelegate(self, '&', 'all_custom',
                                                  True)
        self.series_delegate = TextDelegate(self)
        self.publisher_delegate = TextDelegate(self)
        self.text_delegate = TextDelegate(self)
        self.cc_text_delegate = CcTextDelegate(self)
        self.cc_enum_delegate = CcEnumDelegate(self)
        self.cc_bool_delegate = CcBoolDelegate(self)
        self.cc_comments_delegate = CcCommentsDelegate(self)
        self.cc_template_delegate = CcTemplateDelegate(self)
        self.cc_number_delegate = CcNumberDelegate(self)
        self.display_parent = parent
        self._model = modelcls(self)
        self.setModel(self._model)
        self._model.count_changed_signal.connect(self.do_row_sizing,
                                                 type=Qt.QueuedConnection)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.setSortingEnabled(True)
        self.selectionModel().currentRowChanged.connect(
            self._model.current_changed)
        self.preserve_state = partial(PreserveViewState, self)
        self.marked_changed_listener = FunctionDispatcher(self.marked_changed)

        # {{{ Column Header setup
        self.can_add_columns = True
        self.was_restored = False
        self.column_header = HeaderView(Qt.Horizontal, self)
        self.setHorizontalHeader(self.column_header)
        self.column_header.sortIndicatorChanged.disconnect()
        self.column_header.sortIndicatorChanged.connect(
            self.user_sort_requested)
        self.column_header.setSectionsMovable(True)
        self.column_header.setSectionsClickable(True)
        self.column_header.sectionMoved.connect(self.save_state)
        self.column_header.setContextMenuPolicy(Qt.CustomContextMenu)
        self.column_header.customContextMenuRequested.connect(
            self.show_column_header_context_menu)
        self.column_header.sectionResized.connect(self.column_resized,
                                                  Qt.QueuedConnection)
        self.row_header = HeaderView(Qt.Vertical, self)
        self.row_header.setSectionResizeMode(self.row_header.Fixed)
        self.setVerticalHeader(self.row_header)
        # }}}

        self._model.database_changed.connect(self.database_changed)
        hv = self.verticalHeader()
        hv.setSectionsClickable(True)
        hv.setCursor(Qt.PointingHandCursor)
        self.selected_ids = []
        self._model.about_to_be_sorted.connect(self.about_to_be_sorted)
        self._model.sorting_done.connect(self.sorting_done,
                                         type=Qt.QueuedConnection)

    # Column Header Context Menu {{{
    def column_header_context_handler(self, action=None, column=None):
        if not action or not column:
            return
        try:
            idx = self.column_map.index(column)
        except:
            return
        h = self.column_header

        if action == 'hide':
            h.setSectionHidden(idx, True)
        elif action == 'show':
            h.setSectionHidden(idx, False)
            if h.sectionSize(idx) < 3:
                sz = h.sectionSizeHint(idx)
                h.resizeSection(idx, sz)
        elif action == 'ascending':
            self.sort_by_column_and_order(idx, True)
        elif action == 'descending':
            self.sort_by_column_and_order(idx, False)
        elif action == 'defaults':
            self.apply_state(self.get_default_state())
        elif action == 'addcustcol':
            self.add_column_signal.emit()
        elif action.startswith('align_'):
            alignment = action.partition('_')[-1]
            self._model.change_alignment(column, alignment)
        elif action == 'quickview':
            from calibre.customize.ui import find_plugin
            qv = find_plugin('Show Quickview')
            if qv:
                rows = self.selectionModel().selectedRows()
                if len(rows) > 0:
                    current_row = rows[0].row()
                    current_col = self.column_map.index(column)
                    index = self.model().index(current_row, current_col)
                    qv.actual_plugin_.change_quickview_column(index)

        self.save_state()

    def show_column_header_context_menu(self, pos):
        idx = self.column_header.logicalIndexAt(pos)
        if idx > -1 and idx < len(self.column_map):
            col = self.column_map[idx]
            name = unicode(
                self.model().headerData(idx, Qt.Horizontal, Qt.DisplayRole)
                or '')
            self.column_header_context_menu = QMenu(self)
            if col != 'ondevice':
                self.column_header_context_menu.addAction(
                    _('Hide column %s') % name,
                    partial(self.column_header_context_handler,
                            action='hide',
                            column=col))
            m = self.column_header_context_menu.addMenu(_('Sort on %s') % name)
            a = m.addAction(
                _('Ascending'),
                partial(self.column_header_context_handler,
                        action='ascending',
                        column=col))
            d = m.addAction(
                _('Descending'),
                partial(self.column_header_context_handler,
                        action='descending',
                        column=col))
            if self._model.sorted_on[0] == col:
                ac = a if self._model.sorted_on[1] else d
                ac.setCheckable(True)
                ac.setChecked(True)
            if col not in ('ondevice', 'inlibrary') and \
                    (not self.model().is_custom_column(col) or
                    self.model().custom_columns[col]['datatype'] not in ('bool',
                        )):
                m = self.column_header_context_menu.addMenu(
                    _('Change text alignment for %s') % name)
                al = self._model.alignment_map.get(col, 'left')
                for x, t in (('left', _('Left')), ('right', _('Right')),
                             ('center', _('Center'))):
                    a = m.addAction(
                        t,
                        partial(self.column_header_context_handler,
                                action='align_' + x,
                                column=col))
                    if al == x:
                        a.setCheckable(True)
                        a.setChecked(True)

            if not isinstance(self, DeviceBooksView):
                if self._model.db.field_metadata[col]['is_category']:
                    act = self.column_header_context_menu.addAction(
                        _('Quickview column %s') % name,
                        partial(self.column_header_context_handler,
                                action='quickview',
                                column=col))
                    rows = self.selectionModel().selectedRows()
                    if len(rows) > 1:
                        act.setEnabled(False)

            hidden_cols = [
                self.column_map[i] for i in range(self.column_header.count())
                if self.column_header.isSectionHidden(i)
            ]
            try:
                hidden_cols.remove('ondevice')
            except:
                pass
            if hidden_cols:
                self.column_header_context_menu.addSeparator()
                m = self.column_header_context_menu.addMenu(_('Show column'))
                for col in hidden_cols:
                    hidx = self.column_map.index(col)
                    name = unicode(self.model().headerData(
                        hidx, Qt.Horizontal, Qt.DisplayRole) or '')
                    m.addAction(
                        name,
                        partial(self.column_header_context_handler,
                                action='show',
                                column=col))

            self.column_header_context_menu.addSeparator()
            self.column_header_context_menu.addAction(
                _('Shrink column if it is too wide to fit'),
                partial(self.resize_column_to_fit,
                        column=self.column_map[idx]))
            self.column_header_context_menu.addAction(
                _('Restore default layout'),
                partial(self.column_header_context_handler,
                        action='defaults',
                        column=col))

            if self.can_add_columns:
                self.column_header_context_menu.addAction(
                    QIcon(I('column.png')), _('Add your own columns'),
                    partial(self.column_header_context_handler,
                            action='addcustcol',
                            column=col))

            self.column_header_context_menu.popup(
                self.column_header.mapToGlobal(pos))

    # }}}

    # Sorting {{{
    def sort_by_column_and_order(self, col, ascending):
        self.column_header.blockSignals(True)
        self.sortByColumn(
            col, Qt.AscendingOrder if ascending else Qt.DescendingOrder)
        self.column_header.blockSignals(False)

    def user_sort_requested(self, col, order=Qt.AscendingOrder):
        if col >= len(self.column_map) or col < 0:
            return QTableView.sortByColumn(self, col)
        field = self.column_map[col]
        self.intelligent_sort(field, order == Qt.AscendingOrder)

    def intelligent_sort(self, field, ascending):
        m = self.model()
        pname = 'previous_sort_order_' + self.__class__.__name__
        previous = gprefs.get(pname, {})
        if field == m.sorted_on[0] or field not in previous:
            self.sort_by_named_field(field, ascending)
            previous[field] = ascending
            gprefs[pname] = previous
            return
        previous[m.sorted_on[0]] = m.sorted_on[1]
        gprefs[pname] = previous
        self.sort_by_named_field(field, previous[field])

    def about_to_be_sorted(self, idc):
        selected_rows = [r.row() for r in self.selectionModel().selectedRows()]
        self.selected_ids = [idc(r) for r in selected_rows]

    def sorting_done(self, indexc):
        pos = self.horizontalScrollBar().value()
        self.select_rows(self.selected_ids,
                         using_ids=True,
                         change_current=True,
                         scroll=True)
        self.selected_ids = []
        self.horizontalScrollBar().setValue(pos)

    def sort_by_named_field(self, field, order, reset=True):
        if field in self.column_map:
            idx = self.column_map.index(field)
            self.sort_by_column_and_order(idx, order)
        else:
            self._model.sort_by_named_field(field, order, reset)
            self.column_header.blockSignals(True)
            self.column_header.setSortIndicator(-1, Qt.AscendingOrder)
            self.column_header.blockSignals(False)

    def multisort(self, fields, reset=True, only_if_different=False):
        if len(fields) == 0:
            return
        sh = self.cleanup_sort_history(self._model.sort_history,
                                       ignore_column_map=True)
        if only_if_different and len(sh) >= len(fields):
            ret = True
            for i, t in enumerate(fields):
                if t[0] != sh[i][0]:
                    ret = False
                    break
            if ret:
                return

        for n, d in reversed(fields):
            if n in self._model.db.field_metadata.keys():
                sh.insert(0, (n, d))
        sh = self.cleanup_sort_history(sh, ignore_column_map=True)
        self._model.sort_history = [tuple(x) for x in sh]
        self._model.resort(reset=reset)
        col = fields[0][0]
        dir = Qt.AscendingOrder if fields[0][1] else Qt.DescendingOrder
        if col in self.column_map:
            col = self.column_map.index(col)
            self.column_header.blockSignals(True)
            try:
                self.column_header.setSortIndicator(col, dir)
            finally:
                self.column_header.blockSignals(False)

    # }}}

    # Ondevice column {{{
    def set_ondevice_column_visibility(self):
        col, h = self._model.column_map.index('ondevice'), self.column_header
        h.setSectionHidden(col, not self._model.device_connected)

    def set_device_connected(self, is_connected):
        self._model.set_device_connected(is_connected)
        self.set_ondevice_column_visibility()

    # }}}

    # Save/Restore State {{{
    def get_state(self):
        h = self.column_header
        cm = self.column_map
        state = {}
        state['hidden_columns'] = [
            cm[i] for i in range(h.count())
            if h.isSectionHidden(i) and cm[i] != 'ondevice'
        ]
        state['last_modified_injected'] = True
        state['languages_injected'] = True
        state['sort_history'] = \
            self.cleanup_sort_history(self.model().sort_history, ignore_column_map=self.is_library_view)
        state['column_positions'] = {}
        state['column_sizes'] = {}
        state['column_alignment'] = self._model.alignment_map
        for i in range(h.count()):
            name = cm[i]
            state['column_positions'][name] = h.visualIndex(i)
            if name != 'ondevice':
                state['column_sizes'][name] = h.sectionSize(i)
        return state

    def write_state(self, state):
        db = getattr(self.model(), 'db', None)
        name = unicode(self.objectName())
        if name and db is not None:
            db.new_api.set_pref(name + ' books view state', state)

    def save_state(self):
        # Only save if we have been initialized (set_database called)
        if len(self.column_map) > 0 and self.was_restored:
            state = self.get_state()
            self.write_state(state)

    def cleanup_sort_history(self, sort_history, ignore_column_map=False):
        history = []

        for col, order in sort_history:
            if not isinstance(order, bool):
                continue
            col = {'date': 'timestamp', 'sort': 'title'}.get(col, col)
            if ignore_column_map or col in self.column_map:
                if (not history or history[-1][0] != col):
                    history.append([col, order])
        return history

    def apply_sort_history(self, saved_history, max_sort_levels=3):
        if not saved_history:
            return
        if self.is_library_view:
            for col, order in reversed(
                    self.cleanup_sort_history(
                        saved_history,
                        ignore_column_map=True)[:max_sort_levels]):
                self.sort_by_named_field(col, order)
        else:
            for col, order in reversed(
                    self.cleanup_sort_history(saved_history)
                [:max_sort_levels]):
                self.sort_by_column_and_order(self.column_map.index(col),
                                              order)

    def apply_state(self, state, max_sort_levels=3):
        h = self.column_header
        cmap = {}
        hidden = state.get('hidden_columns', [])
        for i, c in enumerate(self.column_map):
            cmap[c] = i
            if c != 'ondevice':
                h.setSectionHidden(i, c in hidden)

        positions = state.get('column_positions', {})
        pmap = {}
        for col, pos in positions.items():
            if col in cmap:
                pmap[pos] = col
        for pos in sorted(pmap.keys()):
            col = pmap[pos]
            idx = cmap[col]
            current_pos = h.visualIndex(idx)
            if current_pos != pos:
                h.moveSection(current_pos, pos)

        # Because of a bug in Qt 5 we have to ensure that the header is actually
        # relaid out by changing this value, without this sometimes ghost
        # columns remain visible when changing libraries
        for i in xrange(h.count()):
            val = h.isSectionHidden(i)
            h.setSectionHidden(i, not val)
            h.setSectionHidden(i, val)

        sizes = state.get('column_sizes', {})
        for col, size in sizes.items():
            if col in cmap:
                sz = sizes[col]
                if sz < 3:
                    sz = h.sectionSizeHint(cmap[col])
                h.resizeSection(cmap[col], sz)

        self.apply_sort_history(state.get('sort_history', None),
                                max_sort_levels=max_sort_levels)

        for col, alignment in state.get('column_alignment', {}).items():
            self._model.change_alignment(col, alignment)

        for i in range(h.count()):
            if not h.isSectionHidden(i) and h.sectionSize(i) < 3:
                sz = h.sectionSizeHint(i)
                h.resizeSection(i, sz)

    def get_default_state(self):
        old_state = {
            'hidden_columns': ['last_modified', 'languages'],
            'sort_history': [DEFAULT_SORT],
            'column_positions': {},
            'column_sizes': {},
            'column_alignment': {
                'size': 'center',
                'timestamp': 'center',
                'pubdate': 'center'
            },
            'last_modified_injected': True,
            'languages_injected': True,
        }
        h = self.column_header
        cm = self.column_map
        for i in range(h.count()):
            name = cm[i]
            old_state['column_positions'][name] = i
            if name != 'ondevice':
                old_state['column_sizes'][name] = \
                    min(350, max(self.sizeHintForColumn(i),
                        h.sectionSizeHint(i)))
                if name in ('timestamp', 'last_modified'):
                    old_state['column_sizes'][name] += 12
        return old_state

    def get_old_state(self):
        ans = None
        name = unicode(self.objectName())
        if name:
            name += ' books view state'
            db = getattr(self.model(), 'db', None)
            if db is not None:
                ans = db.prefs.get(name, None)
                if ans is None:
                    ans = gprefs.get(name, None)
                    try:
                        del gprefs[name]
                    except:
                        pass
                    if ans is not None:
                        db.new_api.set_pref(name, ans)
                else:
                    injected = False
                    if not ans.get('last_modified_injected', False):
                        injected = True
                        ans['last_modified_injected'] = True
                        hc = ans.get('hidden_columns', [])
                        if 'last_modified' not in hc:
                            hc.append('last_modified')
                    if not ans.get('languages_injected', False):
                        injected = True
                        ans['languages_injected'] = True
                        hc = ans.get('hidden_columns', [])
                        if 'languages' not in hc:
                            hc.append('languages')
                    if injected:
                        db.new_api.set_pref(name, ans)
        return ans

    def restore_state(self):
        old_state = self.get_old_state()
        if old_state is None:
            old_state = self.get_default_state()
        max_levels = 3

        if tweaks['sort_columns_at_startup'] is not None:
            sh = []
            try:
                for c, d in tweaks['sort_columns_at_startup']:
                    if not isinstance(d, bool):
                        d = True if d == 0 else False
                    sh.append((c, d))
            except:
                # Ignore invalid tweak values as users seem to often get them
                # wrong
                print(
                    'Ignoring invalid sort_columns_at_startup tweak, with error:'
                )
                import traceback
                traceback.print_exc()
            old_state['sort_history'] = sh
            max_levels = max(3, len(sh))

        self.column_header.blockSignals(True)
        self.apply_state(old_state, max_sort_levels=max_levels)
        self.column_header.blockSignals(False)

        self.do_row_sizing()

        self.was_restored = True

    def refresh_row_sizing(self):
        self.row_sizing_done = False
        self.do_row_sizing()

    def do_row_sizing(self):
        # Resize all rows to have the correct height
        if not self.row_sizing_done and self.model().rowCount(
                QModelIndex()) > 0:
            vh = self.verticalHeader()
            vh.setDefaultSectionSize(
                max(
                    vh.minimumSectionSize(), self.default_row_height +
                    gprefs['book_list_extra_row_spacing']))
            self._model.set_row_height(self.rowHeight(0))
            self.row_sizing_done = True

    def resize_column_to_fit(self, column):
        col = self.column_map.index(column)
        self.column_resized(col, self.columnWidth(col), self.columnWidth(col))

    def column_resized(self, col, old_size, new_size):
        # arbitrary: scroll bar + header + some
        max_width = self.width() - (self.verticalScrollBar().width() +
                                    self.verticalHeader().width() + 10)
        if max_width < 200:
            max_width = 200
        if new_size > max_width:
            self.column_header.blockSignals(True)
            self.setColumnWidth(col, max_width)
            self.column_header.blockSignals(False)

    # }}}

    # Initialization/Delegate Setup {{{

    def set_database(self, db):
        self.alternate_views.set_database(db)
        self.save_state()
        self._model.set_database(db)
        self.tags_delegate.set_database(db)
        self.cc_names_delegate.set_database(db)
        self.authors_delegate.set_database(db)
        self.series_delegate.set_auto_complete_function(db.all_series)
        self.publisher_delegate.set_auto_complete_function(db.all_publishers)
        self.alternate_views.set_database(db, stage=1)

    def marked_changed(self, old_marked, current_marked):
        self.alternate_views.marked_changed(old_marked, current_marked)
        if bool(old_marked) == bool(current_marked):
            changed = old_marked | current_marked
            i = self.model().db.data.id_to_index

            def f(x):
                try:
                    return i(x)
                except ValueError:
                    pass

            sections = tuple(x for x in map(f, changed) if x is not None)
            if sections:
                self.row_header.headerDataChanged(Qt.Vertical, min(sections),
                                                  max(sections))
                # This is needed otherwise Qt does not always update the
                # viewport correctly. See https://bugs.launchpad.net/bugs/1404697
                self.row_header.viewport().update()
        else:
            # Marked items have either appeared or all been removed
            self.model().set_row_decoration(current_marked)
            self.row_header.headerDataChanged(Qt.Vertical, 0,
                                              self.row_header.count() - 1)
            self.row_header.geometriesChanged.emit()

    def database_changed(self, db):
        db.data.add_marked_listener(self.marked_changed_listener)
        for i in range(self.model().columnCount(None)):
            if self.itemDelegateForColumn(i) in (self.rating_delegate,
                                                 self.timestamp_delegate,
                                                 self.pubdate_delegate,
                                                 self.last_modified_delegate,
                                                 self.languages_delegate):
                self.setItemDelegateForColumn(i, self.itemDelegate())

        cm = self.column_map

        for colhead in cm:
            if self._model.is_custom_column(colhead):
                cc = self._model.custom_columns[colhead]
                if cc['datatype'] == 'datetime':
                    delegate = CcDateDelegate(self)
                    delegate.set_format(cc['display'].get('date_format', ''))
                    self.setItemDelegateForColumn(cm.index(colhead), delegate)
                elif cc['datatype'] == 'comments':
                    self.setItemDelegateForColumn(cm.index(colhead),
                                                  self.cc_comments_delegate)
                elif cc['datatype'] == 'text':
                    if cc['is_multiple']:
                        if cc['display'].get('is_names', False):
                            self.setItemDelegateForColumn(
                                cm.index(colhead), self.cc_names_delegate)
                        else:
                            self.setItemDelegateForColumn(
                                cm.index(colhead), self.tags_delegate)
                    else:
                        self.setItemDelegateForColumn(cm.index(colhead),
                                                      self.cc_text_delegate)
                elif cc['datatype'] == 'series':
                    self.setItemDelegateForColumn(cm.index(colhead),
                                                  self.cc_text_delegate)
                elif cc['datatype'] in ('int', 'float'):
                    self.setItemDelegateForColumn(cm.index(colhead),
                                                  self.cc_number_delegate)
                elif cc['datatype'] == 'bool':
                    self.setItemDelegateForColumn(cm.index(colhead),
                                                  self.cc_bool_delegate)
                elif cc['datatype'] == 'rating':
                    self.setItemDelegateForColumn(cm.index(colhead),
                                                  self.rating_delegate)
                elif cc['datatype'] == 'composite':
                    self.setItemDelegateForColumn(cm.index(colhead),
                                                  self.cc_template_delegate)
                elif cc['datatype'] == 'enumeration':
                    self.setItemDelegateForColumn(cm.index(colhead),
                                                  self.cc_enum_delegate)
            else:
                dattr = colhead + '_delegate'
                delegate = colhead if hasattr(self, dattr) else 'text'
                self.setItemDelegateForColumn(
                    cm.index(colhead), getattr(self, delegate + '_delegate'))

        self.restore_state()
        self.set_ondevice_column_visibility()
        # incase there were marked books
        self.model().set_row_decoration(set())
        self.row_header.headerDataChanged(Qt.Vertical, 0,
                                          self.row_header.count() - 1)
        self.row_header.geometriesChanged.emit()
        # }}}

    # Context Menu {{{
    def set_context_menu(self, menu, edit_collections_action):
        self.setContextMenuPolicy(Qt.DefaultContextMenu)
        self.context_menu = menu
        self.alternate_views.set_context_menu(menu)
        self.edit_collections_action = edit_collections_action

    def contextMenuEvent(self, event):
        sac = self.gui.iactions['Sort By']
        sort_added = tuple(ac for ac in self.context_menu.actions()
                           if ac is sac.qaction)
        if sort_added:
            sac.update_menu()
        self.context_menu.popup(event.globalPos())
        event.accept()

    # }}}

    @property
    def column_map(self):
        return self._model.column_map

    @property
    def visible_columns(self):
        h = self.horizontalHeader()
        logical_indices = (x for x in xrange(h.count())
                           if not h.isSectionHidden(x))
        rmap = {i: x for i, x in enumerate(self.column_map)}
        return (rmap[h.visualIndex(x)] for x in logical_indices
                if h.visualIndex(x) > -1)

    def refresh_book_details(self):
        idx = self.currentIndex()
        if idx.isValid():
            self._model.current_changed(idx, idx)
            return True
        return False

    def scrollContentsBy(self, dx, dy):
        # Needed as Qt bug causes headerview to not always update when scrolling
        QTableView.scrollContentsBy(self, dx, dy)
        if dy != 0:
            self.column_header.update()

    def scroll_to_row(self, row):
        if row > -1:
            h = self.horizontalHeader()
            for i in range(h.count()):
                if not h.isSectionHidden(
                        i) and h.sectionViewportPosition(i) >= 0:
                    self.scrollTo(self.model().index(row, i),
                                  self.PositionAtCenter)
                    break

    @property
    def current_book(self):
        ci = self.currentIndex()
        if ci.isValid():
            try:
                return self.model().db.data.index_to_id(ci.row())
            except (IndexError, ValueError, KeyError, TypeError,
                    AttributeError):
                pass

    def current_book_state(self):
        return self.current_book, self.horizontalScrollBar().value()

    def restore_current_book_state(self, state):
        book_id, hpos = state
        try:
            row = self.model().db.data.id_to_index(book_id)
        except (IndexError, ValueError, KeyError, TypeError, AttributeError):
            return
        self.set_current_row(row)
        self.scroll_to_row(row)
        self.horizontalScrollBar().setValue(hpos)

    def set_current_row(self, row=0, select=True, for_sync=False):
        if row > -1 and row < self.model().rowCount(QModelIndex()):
            h = self.horizontalHeader()
            logical_indices = list(range(h.count()))
            logical_indices = [
                x for x in logical_indices if not h.isSectionHidden(x)
            ]
            pairs = [(x, h.visualIndex(x)) for x in logical_indices
                     if h.visualIndex(x) > -1]
            if not pairs:
                pairs = [(0, 0)]
            pairs.sort(cmp=lambda x, y: cmp(x[1], y[1]))
            i = pairs[0][0]
            index = self.model().index(row, i)
            if for_sync:
                sm = self.selectionModel()
                sm.setCurrentIndex(index, sm.NoUpdate)
            else:
                self.setCurrentIndex(index)
                if select:
                    sm = self.selectionModel()
                    sm.select(index, sm.ClearAndSelect | sm.Rows)

    def row_at_top(self):
        pos = 0
        while pos < 100:
            ans = self.rowAt(pos)
            if ans > -1:
                return ans
            pos += 5

    def row_at_bottom(self):
        pos = self.viewport().height()
        limit = pos - 100
        while pos > limit:
            ans = self.rowAt(pos)
            if ans > -1:
                return ans
            pos -= 5

    def moveCursor(self, action, modifiers):
        orig = self.currentIndex()
        index = QTableView.moveCursor(self, action, modifiers)
        if action == QTableView.MovePageDown:
            moved = index.row() - orig.row()
            try:
                rows = self.row_at_bottom() - self.row_at_top()
            except TypeError:
                rows = moved
            if moved > rows:
                index = self.model().index(orig.row() + rows, index.column())
        elif action == QTableView.MovePageUp:
            moved = orig.row() - index.row()
            try:
                rows = self.row_at_bottom() - self.row_at_top()
            except TypeError:
                rows = moved
            if moved > rows:
                index = self.model().index(orig.row() - rows, index.column())
        elif action == QTableView.MoveHome and modifiers & Qt.ControlModifier:
            return self.model().index(0, orig.column())
        elif action == QTableView.MoveEnd and modifiers & Qt.ControlModifier:
            return self.model().index(self.model().rowCount(QModelIndex()) - 1,
                                      orig.column())
        return index

    def selectionCommand(self, index, event):
        if event and event.type() == event.KeyPress and event.key() in (
                Qt.Key_Home, Qt.Key_End) and event.modifiers() & Qt.CTRL:
            return QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows
        return super(BooksView, self).selectionCommand(index, event)

    def ids_to_rows(self, ids):
        row_map = OrderedDict()
        ids = frozenset(ids)
        m = self.model()
        for row in xrange(m.rowCount(QModelIndex())):
            if len(row_map) >= len(ids):
                break
            c = m.id(row)
            if c in ids:
                row_map[c] = row
        return row_map

    def select_rows(self,
                    identifiers,
                    using_ids=True,
                    change_current=True,
                    scroll=True):
        '''
        Select rows identified by identifiers. identifiers can be a set of ids,
        row numbers or QModelIndexes.
        '''
        rows = set([x.row() if hasattr(x, 'row') else x for x in identifiers])
        if using_ids:
            rows = set([])
            identifiers = set(identifiers)
            m = self.model()
            for row in xrange(m.rowCount(QModelIndex())):
                if m.id(row) in identifiers:
                    rows.add(row)
        rows = list(sorted(rows))
        if rows:
            row = rows[0]
            if change_current:
                self.set_current_row(row, select=False)
            if scroll:
                self.scroll_to_row(row)
        sm = self.selectionModel()
        sel = QItemSelection()
        m = self.model()
        max_col = m.columnCount(QModelIndex()) - 1
        # Create a range based selector for each set of contiguous rows
        # as supplying selectors for each individual row causes very poor
        # performance if a large number of rows has to be selected.
        for k, g in itertools.groupby(enumerate(rows), lambda (i, x): i - x):
            group = list(map(operator.itemgetter(1), g))
            sel.merge(
                QItemSelection(m.index(min(group), 0),
                               m.index(max(group), max_col)), sm.Select)
        sm.select(sel, sm.ClearAndSelect)
Exemplo n.º 35
0
 def contextMenuEvent(self, ev):
     m = QMenu(self)
     m.addAction(_('Sort tabs alphabetically'), self.sort_alphabetically)
     hidden = self.current_db.prefs['virt_libs_hidden']
     if hidden:
         s = m._s = m.addMenu(_('Restore hidden tabs'))
         for x in hidden:
             s.addAction(x, partial(self.restore, x))
     m.addAction(_('Hide Virtual library tabs'), self.disable_bar)
     if gprefs['vl_tabs_closable']:
         m.addAction(_('Lock Virtual library tabs'), self.lock_tab)
     else:
         m.addAction(_('Unlock Virtual library tabs'), self.unlock_tab)
     i = self.tabAt(ev.pos())
     if i > -1:
         vl = unicode_type(self.tabData(i) or '')
         if vl:
             m.addSeparator()
             m.addAction(
                 _('Edit "%s"') % vl,
                 partial(self.gui.do_create_edit, name=vl))
             m.addAction(
                 _('Delete "%s"') % vl,
                 partial(self.gui.remove_vl_triggered, name=vl))
     m.exec_(ev.globalPos())
Exemplo n.º 36
0
    def create_action(self, spec=None, attr='qaction', shortcut_name=None):
        if spec is None:
            spec = self.action_spec
        text, icon, tooltip, shortcut = spec
        if icon is not None:
            action = QAction(QIcon(I(icon)), text, self.gui)
        else:
            action = QAction(text, self.gui)
        if attr == 'qaction':
            mt = (action.text() if self.action_menu_clone_qaction is True else
                  unicode_type(self.action_menu_clone_qaction))
            self.menuless_qaction = ma = QAction(action.icon(), mt, self.gui)
            ma.triggered.connect(action.trigger)
        for a in ((action, ma) if attr == 'qaction' else (action, )):
            a.setAutoRepeat(self.auto_repeat)
            text = tooltip if tooltip else text
            a.setToolTip(text)
            a.setStatusTip(text)
            a.setWhatsThis(text)
        shortcut_action = action
        desc = tooltip if tooltip else None
        if attr == 'qaction':
            shortcut_action = ma
        if shortcut is not None:
            keys = ((shortcut, ) if isinstance(shortcut, string_or_bytes) else
                    tuple(shortcut))
            if shortcut_name is None and spec[0]:
                shortcut_name = unicode_type(spec[0])

            if shortcut_name and self.action_spec[0] and not (
                    attr == 'qaction'
                    and self.popup_type == QToolButton.InstantPopup):
                try:
                    self.gui.keyboard.register_shortcut(
                        self.unique_name + ' - ' + attr,
                        shortcut_name,
                        default_keys=keys,
                        action=shortcut_action,
                        description=desc,
                        group=self.action_spec[0])
                except NameConflict as e:
                    try:
                        prints(unicode_type(e))
                    except:
                        pass
                    shortcut_action.setShortcuts([
                        QKeySequence(key, QKeySequence.PortableText)
                        for key in keys
                    ])
                else:
                    if isosx:
                        # In Qt 5 keyboard shortcuts dont work unless the
                        # action is explicitly added to the main window
                        self.gui.addAction(shortcut_action)

        if attr is not None:
            setattr(self, attr, action)
        if attr == 'qaction' and self.action_add_menu:
            menu = QMenu()
            action.setMenu(menu)
            if self.action_menu_clone_qaction:
                menu.addAction(self.menuless_qaction)
        return action
Exemplo n.º 37
0
class TrayIcon(QtWidgets.QSystemTrayIcon):
    def __init__(self, parent=None):
        super(TrayIcon, self).__init__(parent)
        self.push_msg_type = ['<API>']
        self.messager = self.system_message_handler(self)
        self.init_Menu()
        self.init_icon()

    def init_Menu(self):
        self.menu = QMenu()
        self.action_win = QAction("实盘图表", self)
        self.action_susp = QAction('悬浮窗', self)
        self.action_quit = QAction("退出", self)
        # self.menu1 = QMenu()
        # self.menu1.addAction(self.showAction1)
        # self.menu1.addAction(self.showAction2)
        # self.menu.addMenu(self.menu1, )
        # self.menu1.setTitle("二级菜单")
        self.menu.addAction(self.action_win)
        self.menu.addAction(self.action_susp)
        self.menu.addAction(self.action_quit)
        self.setContextMenu(self.menu)

    def init_icon(self):
        # self.activated.connect(self.iconClied)
        # #把鼠标点击图标的信号和槽连接
        # self.messageClicked.connect(self.mClied)
        # #把鼠标点击弹出消息的信号和槽连接
        self.setIcon(QIcon(os.path.join('ui', 'tracking.png')))
        self.icon = self.MessageIcon()
        # 设置图标

    def iconClied(self, reason):
        """鼠标点击icon传递的信号会带有一个整形的值,1是表示单击右键,2是双击,3是单击左键,4是用鼠标中键点击"""
        if reason == 2 or reason == 3:
            pw = self.parent()
            if pw.isVisible():
                pw.hide()
            else:
                pw.show()
        print(reason)

    def mClied(self):
        print(22222)
        self.showMessage("提示", "你点了消息", self.icon)

    def showM(self):
        print(1111)
        self.showMessage("测试", "我是消息", self.icon)

    def aaa(self):
        print('sfsdfsf')

    def push_message(self, info_type, info):
        if info_type in self.push_msg_type:
            self.showMessage(info_type, info, self.icon)

    class system_message_handler(Handler):
        def __init__(self, tray_icon):
            Handler.__init__(self)
            self.tray_icon = tray_icon
            formatter = Formatter('%(message)s')
            self.setLevel('INFO')
            self.setFormatter(formatter)
            self.pattern = r'(<.+>)(.+)'

        def emit(self, record):
            try:
                msg = self.format(record)
                info_re = re.search(self.pattern, msg)
                if info_re:
                    info_type = info_re.group(1)
                    info = info_re.group(2)
                    self.tray_icon.push_message(info_type, info)
            except Exception as e:
                print(e)
Exemplo n.º 38
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.library_broker = GuiLibraryBroker(db)
        self.content_server = None
        self.server_change_notification_timer = t = QTimer(self)
        self.server_changes = Queue()
        t.setInterval(1000), t.timeout.connect(
            self.handle_changes_from_server_debounced), t.setSingleShot(True)
        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(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.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

        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)
        self.library_view.model().database_changed.connect(
            self.populate_tb_manage_menu, type=Qt.QueuedConnection)

        # ######################## 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()
        self.bars_manager.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()
        if show_gui:
            # Note this has to come after restoreGeometry() because of
            # https://bugreports.qt.io/browse/QTBUG-56831
            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.auto_adder = AutoAdder(gprefs['auto_add_path'], self)

        # Now that the gui is initialized we can restore the quickview state
        # The same thing will be true for any action-based operation with a
        # layout button
        from calibre.gui2.actions.show_quickview import get_quickview_action_plugin
        qv = get_quickview_action_plugin()
        if qv:
            qv.qv_button.restore_state()
        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.search.clear()

    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.srv.embedded import Server
        if not gprefs.get('server3_warning_done', False):
            gprefs.set('server3_warning_done', True)
            if os.path.exists(os.path.join(config_dir, 'server.py')):
                try:
                    os.remove(os.path.join(config_dir, 'server.py'))
                except EnvironmentError:
                    pass
                warning_dialog(
                    self,
                    _('Content server changed!'),
                    _('calibre 3 comes with a completely re-written content server.'
                      ' As such any custom configuration you have for the content'
                      ' server no longer applies. You should check and refresh your'
                      ' settings in Preferences->Sharing->Sharing over the net'
                      ),
                    show=True)
        self.content_server = Server(
            self.library_broker, Dispatcher(self.handle_changes_from_server))
        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)
        self.content_server.start()

    def handle_changes_from_server(self, library_path, change_event):
        if DEBUG:
            prints('Received server change event: {} for {}'.format(
                change_event, library_path))
        if self.library_broker.is_gui_library(library_path):
            self.server_changes.put((library_path, change_event))
            self.server_change_notification_timer.start()

    def handle_changes_from_server_debounced(self):
        if self.shutting_down:
            return
        changes = []
        while True:
            try:
                library_path, change_event = self.server_changes.get_nowait()
            except Empty:
                break
            if self.library_broker.is_gui_library(library_path):
                changes.append(change_event)
        if changes:
            handle_changes(changes, self)

    def content_server_start_failed(self, msg):
        self.content_server = None
        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 refresh_all(self):
        m = self.library_view.model()
        m.db.data.refresh(clear_caches=False, do_search=False)
        self.saved_searches_changed(recount=False)
        m.resort()
        m.research()
        self.tags_view.recount()

    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()
            self.refresh_all()
        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, allow_rebuild=False):
        if newloc is None:
            return
        with self.library_broker:
            default_prefs = None
            try:
                olddb = self.library_view.model().db
                if copy_structure:
                    default_prefs = olddb.prefs
            except:
                olddb = None
            if copy_structure and olddb is not None and default_prefs is not None:
                default_prefs[
                    'field_metadata'] = olddb.new_api.field_metadata.all_metadata(
                    )
            db = self.library_broker.prepare_for_gui_library_change(newloc)
            if db is 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
            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)
            self.library_broker.gui_library_changed(db, olddb)
            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 e-book 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 <i>EPUB output</i> 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 e-book 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.ebooks.mobi.reader.mobi6.KFXError:' in job.details:
                if not minz:
                    title = job.description.split(':')[-1].partition(
                        '(')[-1][:-1]
                    msg = _('<p><b>Failed to convert: %s') % title
                    idx = job.details.index(
                        'calibre.ebooks.mobi.reader.mobi6.KFXError:')
                    msg += '<p>' + re.sub(
                        r'(https:\S+)', r'<a href="\1">{}</a>'.format(
                            _('here')),
                        job.details[idx:].partition(':')[2].strip())
                    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()
            self.stack.tb_widget.save_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()
        self.server_change_notification_timer.stop()

        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()

        self.library_view.model().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()
Exemplo n.º 39
0
    class MenuBar(QObject):

        is_native_menubar = True

        @property
        def native_menubar(self):
            return self.gui.native_menubar

        def __init__(self, location_manager, parent):
            QObject.__init__(self, parent)
            self.gui = parent

            self.location_manager = location_manager
            self.added_actions = []
            self.last_actions = []

            self.donate_action = QAction(_('Donate'), self)
            self.donate_menu = QMenu()
            self.donate_menu.addAction(self.gui.donate_action)
            self.donate_action.setMenu(self.donate_menu)
            self.refresh_timer = t = QTimer(self)
            t.setInterval(200), t.setSingleShot(True), t.timeout.connect(
                self.refresh_bar)

        def init_bar(self, actions):
            self.last_actions = actions
            for ac in self.added_actions:
                m = ac.menu()
                if m is not None:
                    m.setVisible(False)

            mb = self.native_menubar
            for ac in self.added_actions:
                mb.removeAction(ac)
                if ac is not self.donate_action:
                    ac.setMenu(None)
                    ac.deleteLater()
            self.added_actions = []

            for what in actions:
                if what is None:
                    continue
                elif what == 'Location Manager':
                    for ac in self.location_manager.available_actions:
                        self.build_menu(ac)
                elif what == 'Donate':
                    mb.addAction(self.donate_action)
                elif what in self.gui.iactions:
                    action = self.gui.iactions[what]
                    self.build_menu(action.qaction)

        def build_menu(self, ac):
            ans = CloneAction(ac, self.native_menubar, is_top_level=True)
            if ans.menu() is None:
                m = QMenu()
                m.addAction(CloneAction(ac, self.native_menubar))
                ans.setMenu(m)
            # Qt (as of 5.3.0) does not update global menubar entries
            # correctly, so we have to rebuild the global menubar.
            # Without this the Choose Library action shows the text
            # 'Untitled' and the Location Manager items do not work.
            ans.text_changed.connect(self.refresh_timer.start)
            ans.visibility_changed.connect(self.refresh_timer.start)
            self.native_menubar.addAction(ans)
            self.added_actions.append(ans)
            return ans

        def setVisible(self, yes):
            pass  # no-op on OS X since menu bar is always visible

        def update_lm_actions(self):
            pass  # no-op as this is taken care of by init_bar()

        def refresh_bar(self):
            self.init_bar(self.last_actions)
Exemplo n.º 40
0
class ConfigWidget(ConfigWidgetBase, Ui_Form):
    def genesis(self, gui):
        self.gui = gui
        self.delegate = Delegate(self.tweaks_view)
        self.tweaks_view.setItemDelegate(self.delegate)
        self.tweaks_view.currentChanged = self.current_changed
        self.view = self.tweaks_view
        self.highlighter = PythonHighlighter(self.edit_tweak.document())
        self.restore_default_button.clicked.connect(self.restore_to_default)
        self.apply_button.clicked.connect(self.apply_tweak)
        self.plugin_tweaks_button.clicked.connect(self.plugin_tweaks)
        self.splitter.setStretchFactor(0, 1)
        self.splitter.setStretchFactor(1, 100)
        self.next_button.clicked.connect(self.find_next)
        self.previous_button.clicked.connect(self.find_previous)
        self.search.initialize('tweaks_search_history',
                               help_text=_('Search for tweak'))
        self.search.search.connect(self.find)
        self.view.setContextMenuPolicy(Qt.CustomContextMenu)
        self.view.customContextMenuRequested.connect(self.show_context_menu)
        self.copy_icon = QIcon(I('edit-copy.png'))

    def show_context_menu(self, point):
        idx = self.tweaks_view.currentIndex()
        if not idx.isValid():
            return True
        tweak = self.tweaks.data(idx, Qt.UserRole)
        self.context_menu = QMenu(self)
        self.context_menu.addAction(
            self.copy_icon, _('Copy to clipboard'),
            partial(self.copy_item_to_clipboard,
                    val=u"%s (%s: %s)" %
                    (tweak.name, _('ID'), tweak.var_names[0])))
        self.context_menu.popup(self.mapToGlobal(point))
        return True

    def copy_item_to_clipboard(self, val):
        cb = QApplication.clipboard()
        cb.clear()
        cb.setText(val)

    def plugin_tweaks(self):
        raw = self.tweaks.plugin_tweaks_string
        d = PluginTweaks(raw, self)
        if d.exec_() == d.Accepted:
            g, l = {}, {}
            try:
                exec(unicode(d.edit.toPlainText()), g, l)
            except:
                import traceback
                return error_dialog(
                    self,
                    _('Failed'),
                    _('There was a syntax error in your tweak. Click '
                      'the show details button for details.'),
                    show=True,
                    det_msg=traceback.format_exc())
            self.tweaks.set_plugin_tweaks(l)
            self.changed()

    def current_changed(self, current, previous):
        self.tweaks_view.scrollTo(current)
        tweak = self.tweaks.data(current, Qt.UserRole)
        self.help.setPlainText(tweak.doc)
        self.edit_tweak.setPlainText(tweak.edit_text)

    def changed(self, *args):
        self.changed_signal.emit()

    def initialize(self):
        self.tweaks = self._model = Tweaks()
        self.tweaks_view.setModel(self.tweaks)

    def restore_to_default(self, *args):
        idx = self.tweaks_view.currentIndex()
        if idx.isValid():
            self.tweaks.restore_to_default(idx)
            tweak = self.tweaks.data(idx, Qt.UserRole)
            self.edit_tweak.setPlainText(tweak.edit_text)
            self.changed()

    def restore_defaults(self):
        ConfigWidgetBase.restore_defaults(self)
        self.tweaks.restore_to_defaults()
        self.changed()

    def apply_tweak(self):
        idx = self.tweaks_view.currentIndex()
        if idx.isValid():
            l, g = {}, {}
            try:
                exec(unicode(self.edit_tweak.toPlainText()), g, l)
            except:
                import traceback
                error_dialog(self.gui,
                             _('Failed'),
                             _('There was a syntax error in your tweak. Click '
                               'the show details button for details.'),
                             det_msg=traceback.format_exc(),
                             show=True)
                return
            self.tweaks.update_tweak(idx, l)
            self.changed()

    def commit(self):
        raw = self.tweaks.to_string()
        try:
            exec(raw)
        except:
            import traceback
            error_dialog(
                self,
                _('Invalid tweaks'),
                _('The tweaks you entered are invalid, try resetting the'
                  ' tweaks to default and changing them one by one until'
                  ' you find the invalid setting.'),
                det_msg=traceback.format_exc(),
                show=True)
            raise AbortCommit('abort')
        write_tweaks(raw)
        ConfigWidgetBase.commit(self)
        return True

    def find(self, query):
        if not query:
            return
        try:
            idx = self._model.find(query)
        except ParseException:
            self.search.search_done(False)
            return
        self.search.search_done(True)
        if not idx.isValid():
            info_dialog(self,
                        _('No matches'),
                        _('Could not find any shortcuts matching %s') % query,
                        show=True,
                        show_copy_button=False)
            return
        self.highlight_index(idx)

    def highlight_index(self, idx):
        if not idx.isValid():
            return
        self.view.scrollTo(idx)
        self.view.selectionModel().select(
            idx,
            self.view.selectionModel().ClearAndSelect)
        self.view.setCurrentIndex(idx)

    def find_next(self, *args):
        idx = self.view.currentIndex()
        if not idx.isValid():
            idx = self._model.index(0)
        idx = self._model.find_next(idx, unicode(self.search.currentText()))
        self.highlight_index(idx)

    def find_previous(self, *args):
        idx = self.view.currentIndex()
        if not idx.isValid():
            idx = self._model.index(0)
        idx = self._model.find_next(idx,
                                    unicode(self.search.currentText()),
                                    backwards=True)
        self.highlight_index(idx)
Exemplo n.º 41
0
class TagsView(QTreeView):  # {{{

    refresh_required = pyqtSignal()
    tags_marked = pyqtSignal(object)
    edit_user_category = pyqtSignal(object)
    delete_user_category = pyqtSignal(object)
    del_item_from_user_cat = pyqtSignal(object, object, object)
    add_item_to_user_cat = pyqtSignal(object, object, object)
    add_subcategory = pyqtSignal(object)
    tags_list_edit = pyqtSignal(object, object)
    saved_search_edit = pyqtSignal(object)
    rebuild_saved_searches = pyqtSignal()
    author_sort_edit = pyqtSignal(object, object, object, object)
    tag_item_renamed = pyqtSignal()
    search_item_renamed = pyqtSignal()
    drag_drop_finished = pyqtSignal(object)
    restriction_error = pyqtSignal()
    tag_item_delete = pyqtSignal(object, object, object, object)

    def __init__(self, parent=None):
        QTreeView.__init__(self, parent=None)
        self.setMouseTracking(True)
        self.alter_tb = None
        self.disable_recounting = False
        self.setUniformRowHeights(True)
        self.setIconSize(QSize(20, 20))
        self.setTabKeyNavigation(True)
        self.setAnimated(True)
        self.setHeaderHidden(True)
        self.setItemDelegate(TagDelegate(self))
        self.made_connections = False
        self.setAcceptDrops(True)
        self.setDragEnabled(True)
        self.setDragDropMode(self.DragDrop)
        self.setDropIndicatorShown(True)
        self.in_drag_drop = False
        self.setAutoExpandDelay(500)
        self.pane_is_visible = False
        self.search_icon = QIcon(I('search.png'))
        self.user_category_icon = QIcon(I('tb_folder.png'))
        self.delete_icon = QIcon(I('list_remove.png'))
        self.rename_icon = QIcon(I('edit-undo.png'))

        self._model = TagsModel(self)
        self._model.search_item_renamed.connect(self.search_item_renamed)
        self._model.refresh_required.connect(self.refresh_required,
                                             type=Qt.QueuedConnection)
        self._model.tag_item_renamed.connect(self.tag_item_renamed)
        self._model.restriction_error.connect(self.restriction_error)
        self._model.user_categories_edited.connect(self.user_categories_edited,
                                                   type=Qt.QueuedConnection)
        self._model.drag_drop_finished.connect(self.drag_drop_finished)
        stylish_tb = '''
                QTreeView {
                    background-color: palette(window);
                    color: palette(window-text);
                    border: none;
                }
        '''
        self.setStyleSheet('''
                QTreeView::item {
                    border: 1px solid transparent;
                    padding-top:0.8ex;
                    padding-bottom:0.8ex;
                }

                QTreeView::item:hover {
                    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1);
                    border: 1px solid #bfcde4;
                    border-radius: 6px;
                }
        ''' + ('' if gprefs['tag_browser_old_look'] else stylish_tb))
        if gprefs['tag_browser_old_look']:
            self.setAlternatingRowColors(True)
        # Allowing keyboard focus looks bad in the Qt Fusion style and is useless
        # anyway since the enter/spacebar keys do nothing
        self.setFocusPolicy(Qt.NoFocus)

    @property
    def hidden_categories(self):
        return self._model.hidden_categories

    @property
    def db(self):
        return self._model.db

    @property
    def collapse_model(self):
        return self._model.collapse_model

    def set_pane_is_visible(self, to_what):
        pv = self.pane_is_visible
        self.pane_is_visible = to_what
        if to_what and not pv:
            self.recount()

    def get_state(self):
        state_map = {}
        expanded_categories = []
        for row, category in enumerate(self._model.category_nodes):
            if self.isExpanded(self._model.index(row, 0, QModelIndex())):
                expanded_categories.append(category.category_key)
            states = [c.tag.state for c in category.child_tags()]
            names = [(c.tag.name, c.tag.category)
                     for c in category.child_tags()]
            state_map[category.category_key] = dict(izip(names, states))
        return expanded_categories, state_map

    def reread_collapse_parameters(self):
        self._model.reread_collapse_model(self.get_state()[1])

    def set_database(self, db, alter_tb):
        self._model.set_database(db)
        self.alter_tb = alter_tb
        self.pane_is_visible = True  # because TagsModel.set_database did a recount
        self.setModel(self._model)
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        pop = self.db.CATEGORY_SORTS.index(config['sort_tags_by'])
        self.alter_tb.sort_menu.actions()[pop].setChecked(True)
        try:
            match_pop = self.db.MATCH_TYPE.index(config['match_tags_type'])
        except ValueError:
            match_pop = 0
        self.alter_tb.match_menu.actions()[match_pop].setChecked(True)
        if not self.made_connections:
            self.clicked.connect(self.toggle)
            self.customContextMenuRequested.connect(self.show_context_menu)
            self.refresh_required.connect(self.recount,
                                          type=Qt.QueuedConnection)
            self.alter_tb.sort_menu.triggered.connect(self.sort_changed)
            self.alter_tb.match_menu.triggered.connect(self.match_changed)
            self.made_connections = True
        self.refresh_signal_processed = True
        db.add_listener(self.database_changed)
        self.expanded.connect(self.item_expanded)

    def database_changed(self, event, ids):
        if self.refresh_signal_processed:
            self.refresh_signal_processed = False
            self.refresh_required.emit()

    def user_categories_edited(self, user_cats, nkey):
        state_map = self.get_state()[1]
        self.db.new_api.set_pref('user_categories', user_cats)
        self._model.rebuild_node_tree(state_map=state_map)
        p = self._model.find_category_node('@' + nkey)
        self.show_item_at_path(p)

    @property
    def match_all(self):
        return (self.alter_tb
                and self.alter_tb.match_menu.actions()[1].isChecked())

    def sort_changed(self, action):
        for i, ac in enumerate(self.alter_tb.sort_menu.actions()):
            if ac is action:
                config.set('sort_tags_by', self.db.CATEGORY_SORTS[i])
                self.recount()
                break

    def match_changed(self, action):
        try:
            for i, ac in enumerate(self.alter_tb.match_menu.actions()):
                if ac is action:
                    config.set('match_tags_type', self.db.MATCH_TYPE[i])
        except:
            pass

    def mouseMoveEvent(self, event):
        dex = self.indexAt(event.pos())
        if dex.isValid():
            self.setCursor(Qt.PointingHandCursor)
        else:
            self.unsetCursor()
        if not event.buttons() & Qt.LeftButton:
            return
        if self.in_drag_drop or not dex.isValid():
            QTreeView.mouseMoveEvent(self, event)
            return
        # Must deal with odd case where the node being dragged is 'virtual',
        # created to form a hierarchy. We can't really drag this node, but in
        # addition we can't allow drag recognition to notice going over some
        # other node and grabbing that one. So we set in_drag_drop to prevent
        # this from happening, turning it off when the user lifts the button.
        self.in_drag_drop = True
        if not self._model.flags(dex) & Qt.ItemIsDragEnabled:
            QTreeView.mouseMoveEvent(self, event)
            return
        md = self._model.mimeData([dex])
        pixmap = dex.data(DRAG_IMAGE_ROLE).pixmap(self.iconSize())
        drag = QDrag(self)
        drag.setPixmap(pixmap)
        drag.setMimeData(md)
        if self._model.is_in_user_category(dex):
            drag.exec_(Qt.CopyAction | Qt.MoveAction, Qt.CopyAction)
        else:
            drag.exec_(Qt.CopyAction)

    def mouseReleaseEvent(self, event):
        # Swallow everything except leftButton so context menus work correctly
        if event.button() == Qt.LeftButton or self.in_drag_drop:
            QTreeView.mouseReleaseEvent(self, event)
            self.in_drag_drop = False

    def mouseDoubleClickEvent(self, event):
        # swallow these to avoid toggling and editing at the same time
        pass

    @property
    def search_string(self):
        tokens = self._model.tokens()
        joiner = ' and ' if self.match_all else ' or '
        return joiner.join(tokens)

    def toggle_current_index(self):
        ci = self.currentIndex()
        if ci.isValid():
            self.toggle(ci)

    def toggle(self, index):
        self._toggle(index, None)

    def _toggle(self, index, set_to):
        '''
        set_to: if None, advance the state. Otherwise must be one of the values
        in TAG_SEARCH_STATES
        '''
        modifiers = int(QApplication.keyboardModifiers())
        exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT)
        if self._model.toggle(index, exclusive, set_to=set_to):
            self.tags_marked.emit(self.search_string)

    def conditional_clear(self, search_string):
        if search_string != self.search_string:
            self.clear()

    def context_menu_handler(self,
                             action=None,
                             category=None,
                             key=None,
                             index=None,
                             search_state=None,
                             use_vl=None):
        if not action:
            return
        try:
            if action == 'set_icon':
                try:
                    path = choose_files(self,
                                        'choose_category_icon',
                                        _('Change icon for: %s') % key,
                                        filters=[
                                            ('Images',
                                             ['png', 'gif', 'jpg', 'jpeg'])
                                        ],
                                        all_files=False,
                                        select_only_single_file=True)
                    if path:
                        path = path[0]
                        p = QIcon(path).pixmap(QSize(128, 128))
                        d = os.path.join(config_dir, 'tb_icons')
                        if not os.path.exists(d):
                            os.makedirs(d)
                        with open(
                                os.path.join(
                                    d, 'icon_' +
                                    sanitize_file_name_unicode(key) + '.png'),
                                'wb') as f:
                            f.write(pixmap_to_data(p, format='PNG'))
                            path = os.path.basename(f.name)
                        self._model.set_custom_category_icon(
                            key, unicode(path))
                        self.recount()
                except:
                    import traceback
                    traceback.print_exc()
                return
            if action == 'clear_icon':
                self._model.set_custom_category_icon(key, None)
                self.recount()
                return

            if action == 'edit_item_no_vl':
                item = self.model().get_node(index)
                item.use_vl = False
                self.edit(index)
                return
            if action == 'edit_item_in_vl':
                item = self.model().get_node(index)
                item.use_vl = True
                self.edit(index)
                return
            if action == 'delete_item_in_vl':
                self.tag_item_delete.emit(key, index.id, index.original_name,
                                          self.model().get_book_ids_to_use())
                return
            if action == 'delete_item_no_vl':
                self.tag_item_delete.emit(key, index.id, index.original_name,
                                          None)
                return
            if action == 'open_editor':
                self.tags_list_edit.emit(category, key)
                return
            if action == 'manage_categories':
                self.edit_user_category.emit(category)
                return
            if action == 'search':
                self._toggle(index, set_to=search_state)
                return
            if action == 'add_to_category':
                tag = index.tag
                if len(index.children) > 0:
                    for c in index.all_children():
                        self.add_item_to_user_cat.emit(category,
                                                       c.tag.original_name,
                                                       c.tag.category)
                self.add_item_to_user_cat.emit(category, tag.original_name,
                                               tag.category)
                return
            if action == 'add_subcategory':
                self.add_subcategory.emit(key)
                return
            if action == 'search_category':
                self._toggle(index, set_to=search_state)
                return
            if action == 'delete_user_category':
                self.delete_user_category.emit(key)
                return
            if action == 'delete_search':
                self.model().db.saved_search_delete(key)
                self.rebuild_saved_searches.emit()
                return
            if action == 'delete_item_from_user_category':
                tag = index.tag
                if len(index.children) > 0:
                    for c in index.children:
                        self.del_item_from_user_cat.emit(
                            key, c.tag.original_name, c.tag.category)
                self.del_item_from_user_cat.emit(key, tag.original_name,
                                                 tag.category)
                return
            if action == 'manage_searches':
                self.saved_search_edit.emit(category)
                return
            if action == 'edit_author_sort':
                self.author_sort_edit.emit(self, index, True, False)
                return
            if action == 'edit_author_link':
                self.author_sort_edit.emit(self, index, False, True)
                return

            reset_filter_categories = True
            if action == 'hide':
                self.hidden_categories.add(category)
            elif action == 'show':
                self.hidden_categories.discard(category)
            elif action == 'categorization':
                changed = self.collapse_model != category
                self._model.collapse_model = category
                if changed:
                    reset_filter_categories = False
                    gprefs['tags_browser_partition_method'] = category
            elif action == 'defaults':
                self.hidden_categories.clear()
            self.db.new_api.set_pref('tag_browser_hidden_categories',
                                     list(self.hidden_categories))
            if reset_filter_categories:
                self._model.set_categories_filter(None)
            self._model.rebuild_node_tree()
        except:
            return

    def show_context_menu(self, point):
        def display_name(tag):
            if tag.category == 'search':
                n = tag.name
                if len(n) > 45:
                    n = n[:45] + '...'
                return "'" + n + "'"
            return tag.name

        index = self.indexAt(point)
        self.context_menu = QMenu(self)

        if index.isValid():
            item = index.data(Qt.UserRole)
            tag = None

            if item.type == TagTreeItem.TAG:
                tag_item = item
                tag = item.tag
                while item.type != TagTreeItem.CATEGORY:
                    item = item.parent

            if item.type == TagTreeItem.CATEGORY:
                if not item.category_key.startswith('@'):
                    while item.parent != self._model.root_item:
                        item = item.parent
                category = unicode(item.name or '')
                key = item.category_key
                # Verify that we are working with a field that we know something about
                if key not in self.db.field_metadata:
                    return True

                # Did the user click on a leaf node?
                if tag:
                    # If the user right-clicked on an editable item, then offer
                    # the possibility of renaming that item.
                    if tag.is_editable:
                        # Add the 'rename' items
                        if self.model().get_in_vl():
                            self.context_menu.addAction(
                                self.rename_icon,
                                _('Rename %s in virtual library') %
                                display_name(tag),
                                partial(self.context_menu_handler,
                                        action='edit_item_in_vl',
                                        index=index))
                        self.context_menu.addAction(
                            self.rename_icon,
                            _('Rename %s') % display_name(tag),
                            partial(self.context_menu_handler,
                                    action='edit_item_no_vl',
                                    index=index))
                        if key in ('tags', 'series', 'publisher') or \
                                self._model.db.field_metadata.is_custom_field(key):
                            if self.model().get_in_vl():
                                self.context_menu.addAction(
                                    self.delete_icon,
                                    _('Delete %s in virtual library') %
                                    display_name(tag),
                                    partial(self.context_menu_handler,
                                            action='delete_item_in_vl',
                                            key=key,
                                            index=tag))

                            self.context_menu.addAction(
                                self.delete_icon,
                                _('Delete %s') % display_name(tag),
                                partial(self.context_menu_handler,
                                        action='delete_item_no_vl',
                                        key=key,
                                        index=tag))
                        if key == 'authors':
                            self.context_menu.addAction(
                                _('Edit sort for %s') % display_name(tag),
                                partial(self.context_menu_handler,
                                        action='edit_author_sort',
                                        index=tag.id))
                            self.context_menu.addAction(
                                _('Edit link for %s') % display_name(tag),
                                partial(self.context_menu_handler,
                                        action='edit_author_link',
                                        index=tag.id))

                        # is_editable is also overloaded to mean 'can be added
                        # to a user category'
                        m = self.context_menu.addMenu(
                            self.user_category_icon,
                            _('Add %s to user category') % display_name(tag))
                        nt = self.model().user_category_node_tree

                        def add_node_tree(tree_dict, m, path):
                            p = path[:]
                            for k in sorted(tree_dict.keys(), key=sort_key):
                                p.append(k)
                                n = k[1:] if k.startswith('@') else k
                                m.addAction(
                                    self.user_category_icon, n,
                                    partial(self.context_menu_handler,
                                            'add_to_category',
                                            category='.'.join(p),
                                            index=tag_item))
                                if len(tree_dict[k]):
                                    tm = m.addMenu(self.user_category_icon,
                                                   _('Children of %s') % n)
                                    add_node_tree(tree_dict[k], tm, p)
                                p.pop()

                        add_node_tree(nt, m, [])
                    elif key == 'search' and tag.is_searchable:
                        self.context_menu.addAction(
                            self.rename_icon,
                            _('Rename %s') % display_name(tag),
                            partial(self.context_menu_handler,
                                    action='edit_item_no_vl',
                                    index=index))
                        self.context_menu.addAction(
                            self.delete_icon,
                            _('Delete search %s') % display_name(tag),
                            partial(self.context_menu_handler,
                                    action='delete_search',
                                    key=tag.original_name))
                    if key.startswith('@') and not item.is_gst:
                        self.context_menu.addAction(
                            self.user_category_icon,
                            _('Remove %(item)s from category %(cat)s') %
                            dict(item=display_name(tag), cat=item.py_name),
                            partial(self.context_menu_handler,
                                    action='delete_item_from_user_category',
                                    key=key,
                                    index=tag_item))
                    if tag.is_searchable:
                        # Add the search for value items. All leaf nodes are searchable
                        self.context_menu.addAction(
                            self.search_icon,
                            _('Search for %s') % display_name(tag),
                            partial(
                                self.context_menu_handler,
                                action='search',
                                search_state=TAG_SEARCH_STATES['mark_plus'],
                                index=index))
                        self.context_menu.addAction(
                            self.search_icon,
                            _('Search for everything but %s') %
                            display_name(tag),
                            partial(
                                self.context_menu_handler,
                                action='search',
                                search_state=TAG_SEARCH_STATES['mark_minus'],
                                index=index))
                    self.context_menu.addSeparator()
                elif key.startswith('@') and not item.is_gst:
                    if item.can_be_edited:
                        self.context_menu.addAction(
                            self.rename_icon,
                            _('Rename %s') % item.py_name,
                            partial(self.context_menu_handler,
                                    action='edit_item_no_vl',
                                    index=index))
                    self.context_menu.addAction(
                        self.user_category_icon,
                        _('Add sub-category to %s') % item.py_name,
                        partial(self.context_menu_handler,
                                action='add_subcategory',
                                key=key))
                    self.context_menu.addAction(
                        self.delete_icon,
                        _('Delete user category %s') % item.py_name,
                        partial(self.context_menu_handler,
                                action='delete_user_category',
                                key=key))
                    self.context_menu.addSeparator()
                # Hide/Show/Restore categories
                self.context_menu.addAction(
                    _('Hide category %s') % category,
                    partial(self.context_menu_handler,
                            action='hide',
                            category=key))
                if self.hidden_categories:
                    m = self.context_menu.addMenu(_('Show category'))
                    for col in sorted(self.hidden_categories,
                                      key=lambda x: sort_key(
                                          self.db.field_metadata[x]['name'])):
                        m.addAction(
                            self.db.field_metadata[col]['name'],
                            partial(self.context_menu_handler,
                                    action='show',
                                    category=col))

                # search by category. Some categories are not searchable, such
                # as search and news
                if item.tag.is_searchable:
                    self.context_menu.addAction(
                        self.search_icon,
                        _('Search for books in category %s') % category,
                        partial(self.context_menu_handler,
                                action='search_category',
                                index=self._model.createIndex(
                                    item.row(), 0, item),
                                search_state=TAG_SEARCH_STATES['mark_plus']))
                    self.context_menu.addAction(
                        self.search_icon,
                        _('Search for books not in category %s') % category,
                        partial(self.context_menu_handler,
                                action='search_category',
                                index=self._model.createIndex(
                                    item.row(), 0, item),
                                search_state=TAG_SEARCH_STATES['mark_minus']))
                # Offer specific editors for tags/series/publishers/saved searches
                self.context_menu.addSeparator()
                if key in ['tags', 'publisher', 'series'] or \
                            (self.db.field_metadata[key]['is_custom'] and
                             self.db.field_metadata[key]['datatype'] != 'composite'):
                    self.context_menu.addAction(
                        _('Manage %s') % category,
                        partial(self.context_menu_handler,
                                action='open_editor',
                                category=tag.original_name if tag else None,
                                key=key))
                elif key == 'authors':
                    self.context_menu.addAction(
                        _('Manage %s') % category,
                        partial(self.context_menu_handler,
                                action='edit_author_sort'))
                elif key == 'search':
                    self.context_menu.addAction(
                        _('Manage Saved searches'),
                        partial(self.context_menu_handler,
                                action='manage_searches',
                                category=tag.name if tag else None))

                self.context_menu.addSeparator()
                self.context_menu.addAction(
                    _('Change category icon'),
                    partial(self.context_menu_handler,
                            action='set_icon',
                            key=key))
                self.context_menu.addAction(
                    _('Restore default icon'),
                    partial(self.context_menu_handler,
                            action='clear_icon',
                            key=key))

                # Always show the user categories editor
                self.context_menu.addSeparator()
                if key.startswith('@') and \
                        key[1:] in self.db.prefs.get('user_categories', {}).keys():
                    self.context_menu.addAction(
                        _('Manage User categories'),
                        partial(self.context_menu_handler,
                                action='manage_categories',
                                category=key[1:]))
                else:
                    self.context_menu.addAction(
                        _('Manage User categories'),
                        partial(self.context_menu_handler,
                                action='manage_categories',
                                category=None))

        if self.hidden_categories:
            if not self.context_menu.isEmpty():
                self.context_menu.addSeparator()
            self.context_menu.addAction(
                _('Show all categories'),
                partial(self.context_menu_handler, action='defaults'))

        m = self.context_menu.addMenu(_('Change sub-categorization scheme'))
        da = m.addAction(
            _('Disable'),
            partial(self.context_menu_handler,
                    action='categorization',
                    category='disable'))
        fla = m.addAction(
            _('By first letter'),
            partial(self.context_menu_handler,
                    action='categorization',
                    category='first letter'))
        pa = m.addAction(
            _('Partition'),
            partial(self.context_menu_handler,
                    action='categorization',
                    category='partition'))
        if self.collapse_model == 'disable':
            da.setCheckable(True)
            da.setChecked(True)
        elif self.collapse_model == 'first letter':
            fla.setCheckable(True)
            fla.setChecked(True)
        else:
            pa.setCheckable(True)
            pa.setChecked(True)

        if config['sort_tags_by'] != "name":
            fla.setEnabled(False)
            m.hovered.connect(self.collapse_menu_hovered)
            fla.setToolTip(
                _('First letter is usable only when sorting by name'))
            # Apparently one cannot set a tooltip to empty, so use a star and
            # deal with it in the hover method
            da.setToolTip('*')
            pa.setToolTip('*')

        if index.isValid() and self.model().rowCount(index) > 0:
            self.context_menu.addSeparator()
            self.context_menu.addAction(
                _('E&xpand all children'),
                partial(self.expand_node_and_descendants, index))

        if not self.context_menu.isEmpty():
            self.context_menu.popup(self.mapToGlobal(point))
        return True

    def expand_node_and_descendants(self, index):
        if not index.isValid():
            return
        self.expand(index)
        for r in xrange(self.model().rowCount(index)):
            self.expand_node_and_descendants(index.child(r, 0))

    def collapse_menu_hovered(self, action):
        tip = action.toolTip()
        if tip == '*':
            tip = ''
        QToolTip.showText(QCursor.pos(), tip)

    def dragMoveEvent(self, event):
        QTreeView.dragMoveEvent(self, event)
        self.setDropIndicatorShown(False)
        index = self.indexAt(event.pos())
        if not index.isValid():
            return
        src_is_tb = event.mimeData().hasFormat(
            'application/calibre+from_tag_browser')
        item = index.data(Qt.UserRole)
        if item.type == TagTreeItem.ROOT:
            return
        flags = self._model.flags(index)
        if item.type == TagTreeItem.TAG and flags & Qt.ItemIsDropEnabled:
            self.setDropIndicatorShown(not src_is_tb)
            return
        if item.type == TagTreeItem.CATEGORY and not item.is_gst:
            fm_dest = self.db.metadata_for_field(item.category_key)
            if fm_dest['kind'] == 'user':
                if src_is_tb:
                    if event.dropAction() == Qt.MoveAction:
                        data = str(event.mimeData().data(
                            'application/calibre+from_tag_browser'))
                        src = cPickle.loads(data)
                        for s in src:
                            if s[0] == TagTreeItem.TAG and \
                                    (not s[1].startswith('@') or s[2]):
                                return
                    self.setDropIndicatorShown(True)
                    return
                md = event.mimeData()
                if hasattr(md, 'column_name'):
                    fm_src = self.db.metadata_for_field(md.column_name)
                    if md.column_name in ['authors', 'publisher', 'series'] or \
                            (fm_src['is_custom'] and (
                             (fm_src['datatype'] in ['series', 'text', 'enumeration'] and
                              not fm_src['is_multiple']) or
                             (fm_src['datatype'] == 'composite' and
                              fm_src['display'].get('make_category', False)))):
                        self.setDropIndicatorShown(True)

    def clear(self):
        if self.model():
            self.model().clear_state()

    def is_visible(self, idx):
        item = idx.data(Qt.UserRole)
        if getattr(item, 'type', None) == TagTreeItem.TAG:
            idx = idx.parent()
        return self.isExpanded(idx)

    def recount(self, *args):
        '''
        Rebuild the category tree, expand any categories that were expanded,
        reset the search states, and reselect the current node.
        '''
        if self.disable_recounting or not self.pane_is_visible:
            return
        self.refresh_signal_processed = True
        ci = self.currentIndex()
        if not ci.isValid():
            ci = self.indexAt(QPoint(10, 10))
        path = self.model().path_for_index(ci) if self.is_visible(ci) else None
        expanded_categories, state_map = self.get_state()
        self._model.rebuild_node_tree(state_map=state_map)
        self.blockSignals(True)
        for category in expanded_categories:
            idx = self._model.index_for_category(category)
            if idx is not None and idx.isValid():
                self.expand(idx)
        self.show_item_at_path(path)
        self.blockSignals(False)

    def show_item_at_path(self,
                          path,
                          box=False,
                          position=QTreeView.PositionAtCenter):
        '''
        Scroll the browser and open categories to show the item referenced by
        path. If possible, the item is placed in the center. If box=True, a
        box is drawn around the item.
        '''
        if path:
            self.show_item_at_index(self._model.index_for_path(path),
                                    box=box,
                                    position=position)

    def expand_parent(self, idx):
        # Needed otherwise Qt sometimes segfaults if the node is buried in a
        # collapsed, off screen hierarchy. To be safe, we expand from the
        # outermost in
        p = self._model.parent(idx)
        if p.isValid():
            self.expand_parent(p)
        self.expand(idx)

    def show_item_at_index(self,
                           idx,
                           box=False,
                           position=QTreeView.PositionAtCenter):
        if idx.isValid() and idx.data(
                Qt.UserRole) is not self._model.root_item:
            self.expand_parent(idx)
            self.setCurrentIndex(idx)
            self.scrollTo(idx, position)
            if box:
                self._model.set_boxed(idx)

    def item_expanded(self, idx):
        '''
        Called by the expanded signal
        '''
        self.setCurrentIndex(idx)
Exemplo n.º 42
0
class EditorWidget(QTextEdit, LineEditECM):  # {{{

    data_changed = pyqtSignal()

    @property
    def readonly(self):
        return self.isReadOnly()

    @readonly.setter
    def readonly(self, val):
        self.setReadOnly(bool(val))

    @contextmanager
    def editing_cursor(self, set_cursor=True):
        c = self.textCursor()
        c.beginEditBlock()
        yield c
        c.endEditBlock()
        if set_cursor:
            self.setTextCursor(c)
        self.focus_self()

    def __init__(self, parent=None):
        QTextEdit.__init__(self, parent)
        self.setTabChangesFocus(True)
        self.document().setDefaultStyleSheet(css())
        font = self.font()
        f = QFontInfo(font)
        delta = tweaks['change_book_details_font_size_by'] + 1
        if delta:
            font.setPixelSize(f.pixelSize() + delta)
            self.setFont(font)
        f = QFontMetrics(self.font())
        self.em_size = f.horizontalAdvance('m')
        self.base_url = None
        self._parent = weakref.ref(parent)
        self.comments_pat = re.compile(r'<!--.*?-->', re.DOTALL)

        extra_shortcuts = {
            'bold': 'Bold',
            'italic': 'Italic',
            'underline': 'Underline',
        }

        for rec in (
            ('bold', 'format-text-bold', _('Bold'), True),
            ('italic', 'format-text-italic', _('Italic'), True),
            ('underline', 'format-text-underline', _('Underline'), True),
            ('strikethrough', 'format-text-strikethrough', _('Strikethrough'),
             True),
            ('superscript', 'format-text-superscript', _('Superscript'), True),
            ('subscript', 'format-text-subscript', _('Subscript'), True),
            ('ordered_list', 'format-list-ordered', _('Ordered list'), True),
            ('unordered_list', 'format-list-unordered', _('Unordered list'),
             True),
            ('align_left', 'format-justify-left', _('Align left'), True),
            ('align_center', 'format-justify-center', _('Align center'), True),
            ('align_right', 'format-justify-right', _('Align right'), True),
            ('align_justified', 'format-justify-fill', _('Align justified'),
             True),
            (
                'undo',
                'edit-undo',
                _('Undo'),
            ),
            (
                'redo',
                'edit-redo',
                _('Redo'),
            ),
            (
                'remove_format',
                'edit-clear',
                _('Remove formatting'),
            ),
            (
                'copy',
                'edit-copy',
                _('Copy'),
            ),
            (
                'paste',
                'edit-paste',
                _('Paste'),
            ),
            (
                'paste_and_match_style',
                'edit-paste',
                _('Paste and match style'),
            ),
            (
                'cut',
                'edit-cut',
                _('Cut'),
            ),
            (
                'indent',
                'format-indent-more',
                _('Increase indentation'),
            ),
            (
                'outdent',
                'format-indent-less',
                _('Decrease indentation'),
            ),
            (
                'select_all',
                'edit-select-all',
                _('Select all'),
            ),
            ('color', 'format-text-color', _('Foreground color')),
            ('background', 'format-fill-color', _('Background color')),
            (
                'insert_link',
                'insert-link',
                _('Insert link or image'),
            ),
            (
                'insert_hr',
                'format-text-hr',
                _('Insert separator'),
            ),
            ('clear', 'trash', _('Clear')),
        ):
            name, icon, text = rec[:3]
            checkable = len(rec) == 4
            ac = QAction(QIcon(I(icon + '.png')), text, self)
            if checkable:
                ac.setCheckable(checkable)
            setattr(self, 'action_' + name, ac)
            ss = extra_shortcuts.get(name)
            if ss is not None:
                ac.setShortcut(QKeySequence(getattr(QKeySequence, ss)))
            ac.triggered.connect(getattr(self, 'do_' + name))

        self.action_block_style = QAction(QIcon(I('format-text-heading.png')),
                                          _('Style text block'), self)
        self.action_block_style.setToolTip(_('Style the selected text block'))
        self.block_style_menu = QMenu(self)
        self.action_block_style.setMenu(self.block_style_menu)
        self.block_style_actions = []
        h = _('Heading {0}')
        for text, name in (
            (_('Normal'), 'p'),
            (h.format(1), 'h1'),
            (h.format(2), 'h2'),
            (h.format(3), 'h3'),
            (h.format(4), 'h4'),
            (h.format(5), 'h5'),
            (h.format(6), 'h6'),
            (_('Blockquote'), 'blockquote'),
        ):
            ac = QAction(text, self)
            self.block_style_menu.addAction(ac)
            ac.block_name = name
            ac.setCheckable(True)
            self.block_style_actions.append(ac)
            ac.triggered.connect(self.do_format_block)

        self.setHtml('')
        self.copyAvailable.connect(self.update_clipboard_actions)
        self.update_clipboard_actions(False)
        self.selectionChanged.connect(self.update_selection_based_actions)
        self.update_selection_based_actions()
        connect_lambda(self.undoAvailable, self,
                       lambda self, yes: self.action_undo.setEnabled(yes))
        connect_lambda(self.redoAvailable, self,
                       lambda self, yes: self.action_redo.setEnabled(yes))
        self.action_undo.setEnabled(False), self.action_redo.setEnabled(False)
        self.textChanged.connect(self.update_cursor_position_actions)
        self.cursorPositionChanged.connect(self.update_cursor_position_actions)
        self.textChanged.connect(self.data_changed)
        self.update_cursor_position_actions()

    def update_clipboard_actions(self, copy_available):
        self.action_copy.setEnabled(copy_available)
        self.action_cut.setEnabled(copy_available)

    def update_selection_based_actions(self):
        pass

    def update_cursor_position_actions(self):
        c = self.textCursor()
        ls = c.currentList()
        self.action_ordered_list.setChecked(
            ls is not None
            and ls.format().style() == QTextListFormat.ListDecimal)
        self.action_unordered_list.setChecked(
            ls is not None and ls.format().style() == QTextListFormat.ListDisc)
        tcf = c.charFormat()
        vert = tcf.verticalAlignment()
        self.action_superscript.setChecked(
            vert == QTextCharFormat.AlignSuperScript)
        self.action_subscript.setChecked(
            vert == QTextCharFormat.AlignSubScript)
        self.action_bold.setChecked(tcf.fontWeight() == QFont.Bold)
        self.action_italic.setChecked(tcf.fontItalic())
        self.action_underline.setChecked(tcf.fontUnderline())
        self.action_strikethrough.setChecked(tcf.fontStrikeOut())
        bf = c.blockFormat()
        a = bf.alignment()
        self.action_align_left.setChecked(a == Qt.AlignLeft)
        self.action_align_right.setChecked(a == Qt.AlignRight)
        self.action_align_center.setChecked(a == Qt.AlignHCenter)
        self.action_align_justified.setChecked(a == Qt.AlignJustify)
        lvl = bf.headingLevel()
        name = 'p'
        if lvl == 0:
            if bf.leftMargin() == bf.rightMargin() and bf.leftMargin() > 0:
                name = 'blockquote'
        else:
            name = 'h{}'.format(lvl)
        for ac in self.block_style_actions:
            ac.setChecked(ac.block_name == name)

    def set_readonly(self, what):
        self.readonly = what

    def focus_self(self):
        self.setFocus(Qt.TabFocusReason)

    def do_clear(self, *args):
        c = self.textCursor()
        c.beginEditBlock()
        c.movePosition(QTextCursor.Start, QTextCursor.MoveAnchor)
        c.movePosition(QTextCursor.End, QTextCursor.KeepAnchor)
        c.removeSelectedText()
        c.endEditBlock()
        self.focus_self()

    clear_text = do_clear

    def do_bold(self):
        with self.editing_cursor() as c:
            fmt = QTextCharFormat()
            fmt.setFontWeight(QFont.Bold if c.charFormat().fontWeight(
            ) != QFont.Bold else QFont.Normal)
            c.mergeCharFormat(fmt)

    def do_italic(self):
        with self.editing_cursor() as c:
            fmt = QTextCharFormat()
            fmt.setFontItalic(not c.charFormat().fontItalic())
            c.mergeCharFormat(fmt)

    def do_underline(self):
        with self.editing_cursor() as c:
            fmt = QTextCharFormat()
            fmt.setFontUnderline(not c.charFormat().fontUnderline())
            c.mergeCharFormat(fmt)

    def do_strikethrough(self):
        with self.editing_cursor() as c:
            fmt = QTextCharFormat()
            fmt.setFontStrikeOut(not c.charFormat().fontStrikeOut())
            c.mergeCharFormat(fmt)

    def do_vertical_align(self, which):
        with self.editing_cursor() as c:
            fmt = QTextCharFormat()
            fmt.setVerticalAlignment(which)
            c.mergeCharFormat(fmt)

    def do_superscript(self):
        self.do_vertical_align(QTextCharFormat.AlignSuperScript)

    def do_subscript(self):
        self.do_vertical_align(QTextCharFormat.AlignSubScript)

    def do_list(self, fmt):
        with self.editing_cursor() as c:
            ls = c.currentList()
            if ls is not None:
                lf = ls.format()
                if lf.style() == fmt:
                    c.setBlockFormat(QTextBlockFormat())
                else:
                    lf.setStyle(fmt)
                    ls.setFormat(lf)
            else:
                ls = c.createList(fmt)

    def do_ordered_list(self):
        self.do_list(QTextListFormat.ListDecimal)

    def do_unordered_list(self):
        self.do_list(QTextListFormat.ListDisc)

    def do_alignment(self, which):
        with self.editing_cursor() as c:
            fmt = QTextBlockFormat()
            fmt.setAlignment(which)
            c.setBlockFormat(fmt)

    def do_align_left(self):
        self.do_alignment(Qt.AlignLeft)

    def do_align_center(self):
        self.do_alignment(Qt.AlignHCenter)

    def do_align_right(self):
        self.do_alignment(Qt.AlignRight)

    def do_align_justified(self):
        self.do_alignment(Qt.AlignJustify)

    def do_undo(self):
        self.undo()
        self.focus_self()

    def do_redo(self):
        self.redo()
        self.focus_self()

    def do_remove_format(self):
        with self.editing_cursor() as c:
            c.setBlockFormat(QTextBlockFormat())
            c.setCharFormat(QTextCharFormat())

    def do_copy(self):
        self.copy()
        self.focus_self()

    def do_paste(self):
        self.paste()
        self.focus_self()

    def do_paste_and_match_style(self):
        text = QApplication.instance().clipboard().text()
        if text:
            self.setText(text)

    def do_cut(self):
        self.cut()
        self.focus_self()

    def indent_block(self, mult=1):
        with self.editing_cursor() as c:
            bf = c.blockFormat()
            bf.setTextIndent(bf.textIndent() + 2 * self.em_size * mult)
            c.setBlockFormat(bf)

    def do_indent(self):
        self.indent_block()

    def do_outdent(self):
        self.indent_block(-1)

    def do_select_all(self):
        with self.editing_cursor() as c:
            c.movePosition(QTextCursor.Start, QTextCursor.MoveAnchor)
            c.movePosition(QTextCursor.End, QTextCursor.KeepAnchor)

    def level_for_block_type(self, name):
        if name == 'blockquote':
            return 0
        return {q: i
                for i, q in enumerate('p h1 h2 h3 h4 h5 h6'.split())}[name]

    def do_format_block(self):
        name = self.sender().block_name
        with self.editing_cursor() as c:
            bf = QTextBlockFormat()
            cf = QTextCharFormat()
            bcf = c.blockCharFormat()
            lvl = self.level_for_block_type(name)
            wt = QFont.Bold if lvl else None
            adjust = (0, 3, 2, 1, 0, -1, -1)[lvl]
            pos = None
            if not c.hasSelection():
                pos = c.position()
                c.movePosition(QTextCursor.StartOfBlock,
                               QTextCursor.MoveAnchor)
                c.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
            # margin values are taken from qtexthtmlparser.cpp
            hmargin = 0
            if name == 'blockquote':
                hmargin = 40
            tmargin = bmargin = 12
            if name == 'h1':
                tmargin, bmargin = 18, 12
            elif name == 'h2':
                tmargin, bmargin = 16, 12
            elif name == 'h3':
                tmargin, bmargin = 14, 12
            elif name == 'h4':
                tmargin, bmargin = 12, 12
            elif name == 'h5':
                tmargin, bmargin = 12, 4
            bf.setLeftMargin(hmargin), bf.setRightMargin(hmargin)
            bf.setTopMargin(tmargin), bf.setBottomMargin(bmargin)
            bf.setHeadingLevel(lvl)
            if adjust:
                bcf.setProperty(QTextCharFormat.FontSizeAdjustment, adjust)
                cf.setProperty(QTextCharFormat.FontSizeAdjustment, adjust)
            if wt:
                bcf.setProperty(QTextCharFormat.FontWeight, wt)
                cf.setProperty(QTextCharFormat.FontWeight, wt)
            c.setBlockCharFormat(bcf)
            c.mergeCharFormat(cf)
            c.mergeBlockFormat(bf)
            if pos is not None:
                c.setPosition(pos)

    def do_color(self):
        col = QColorDialog.getColor(Qt.black, self,
                                    _('Choose foreground color'),
                                    QColorDialog.ShowAlphaChannel)
        if col.isValid():
            fmt = QTextCharFormat()
            fmt.setForeground(QBrush(col))
            with self.editing_cursor() as c:
                c.mergeCharFormat(fmt)

    def do_background(self):
        col = QColorDialog.getColor(Qt.white, self,
                                    _('Choose background color'),
                                    QColorDialog.ShowAlphaChannel)
        if col.isValid():
            fmt = QTextCharFormat()
            fmt.setBackground(QBrush(col))
            with self.editing_cursor() as c:
                c.mergeCharFormat(fmt)

    def do_insert_hr(self, *args):
        with self.editing_cursor() as c:
            c.movePosition(c.EndOfBlock, c.MoveAnchor)
            c.insertHtml('<hr>')

    def do_insert_link(self, *args):
        link, name, is_image = self.ask_link()
        if not link:
            return
        url = self.parse_link(link)
        if url.isValid():
            url = unicode_type(url.toString(NO_URL_FORMATTING))
            self.focus_self()
            with self.editing_cursor() as c:
                if is_image:
                    c.insertImage(url)
                else:
                    oldfmt = QTextCharFormat(c.charFormat())
                    fmt = QTextCharFormat()
                    fmt.setAnchor(True)
                    fmt.setAnchorHref(url)
                    fmt.setForeground(QBrush(QColor('blue')))
                    if name or not c.hasSelection():
                        c.mergeCharFormat(fmt)
                        c.insertText(name or url)
                    else:
                        pos, anchor = c.position(), c.anchor()
                        start, end = min(pos, anchor), max(pos, anchor)
                        for i in range(start, end):
                            cur = self.textCursor()
                            cur.setPosition(i), cur.setPosition(
                                i + 1, c.KeepAnchor)
                            cur.mergeCharFormat(fmt)
                    c.setPosition(c.position())
                    c.setCharFormat(oldfmt)

        else:
            error_dialog(self,
                         _('Invalid URL'),
                         _('The url %r is invalid') % link,
                         show=True)

    def ask_link(self):
        class Ask(QDialog):
            def accept(self):
                if self.treat_as_image.isChecked():
                    url = self.url.text()
                    if url.lower().split(':', 1)[0] in ('http', 'https'):
                        error_dialog(
                            self,
                            _('Remote images not supported'),
                            _('You must download the image to your computer, URLs pointing'
                              ' to remote images are not supported.'),
                            show=True)
                        return
                QDialog.accept(self)

        d = Ask(self)
        d.setWindowTitle(_('Create link'))
        l = QFormLayout()
        l.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
        d.setLayout(l)
        d.url = QLineEdit(d)
        d.name = QLineEdit(d)
        d.treat_as_image = QCheckBox(d)
        d.setMinimumWidth(600)
        d.bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        d.br = b = QPushButton(_('&Browse'))
        b.setIcon(QIcon(I('document_open.png')))

        def cf():
            files = choose_files(d,
                                 'select link file',
                                 _('Choose file'),
                                 [(_('Images'), 'png jpeg jpg gif'.split())],
                                 select_only_single_file=True)
            if files:
                path = files[0]
                d.url.setText(path)
                if path and os.path.exists(path):
                    with lopen(path, 'rb') as f:
                        q = what(f)
                    is_image = q in {'jpeg', 'png', 'gif'}
                    d.treat_as_image.setChecked(is_image)

        b.clicked.connect(cf)
        d.la = la = QLabel(
            _('Enter a URL. If you check the "Treat the URL as an image" box '
              'then the URL will be added as an image reference instead of as '
              'a link. You can also choose to create a link to a file on '
              'your computer. '
              'Note that if you create a link to a file on your computer, it '
              'will stop working if the file is moved.'))
        la.setWordWrap(True)
        la.setStyleSheet('QLabel { margin-bottom: 1.5ex }')
        l.setWidget(0, l.SpanningRole, la)
        l.addRow(_('Enter &URL:'), d.url)
        l.addRow(_('Treat the URL as an &image'), d.treat_as_image)
        l.addRow(_('Enter &name (optional):'), d.name)
        l.addRow(_('Choose a file on your computer:'), d.br)
        l.addRow(d.bb)
        d.bb.accepted.connect(d.accept)
        d.bb.rejected.connect(d.reject)
        d.resize(d.sizeHint())
        link, name, is_image = None, None, False
        if d.exec_() == d.Accepted:
            link, name = unicode_type(d.url.text()).strip(), unicode_type(
                d.name.text()).strip()
            is_image = d.treat_as_image.isChecked()
        return link, name, is_image

    def parse_link(self, link):
        link = link.strip()
        if link and os.path.exists(link):
            return QUrl.fromLocalFile(link)
        has_schema = re.match(r'^[a-zA-Z]+:', link)
        if has_schema is not None:
            url = QUrl(link, QUrl.TolerantMode)
            if url.isValid():
                return url
        if os.path.exists(link):
            return QUrl.fromLocalFile(link)

        if has_schema is None:
            first, _, rest = link.partition('.')
            prefix = 'http'
            if first == 'ftp':
                prefix = 'ftp'
            url = QUrl(prefix + '://' + link, QUrl.TolerantMode)
            if url.isValid():
                return url

        return QUrl(link, QUrl.TolerantMode)

    def sizeHint(self):
        return QSize(150, 150)

    @property
    def html(self):
        raw = original_html = self.toHtml()
        check = self.toPlainText().strip()
        raw = xml_to_unicode(raw,
                             strip_encoding_pats=True,
                             resolve_entities=True)[0]
        raw = self.comments_pat.sub('', raw)
        if not check and '<img' not in raw.lower():
            return ''

        root = parse(raw, maybe_xhtml=False, sanitize_names=True)
        if root.xpath('//meta[@name="calibre-dont-sanitize"]'):
            # Bypass cleanup if special meta tag exists
            return original_html

        try:
            cleanup_qt_markup(root)
        except Exception:
            import traceback
            traceback.print_exc()
        elems = []
        for body in root.xpath('//body'):
            if body.text:
                elems.append(body.text)
            elems += [
                html.tostring(x, encoding='unicode') for x in body
                if x.tag not in ('script', 'style')
            ]

        if len(elems) > 1:
            ans = '<div>%s</div>' % (u''.join(elems))
        else:
            ans = ''.join(elems)
            if not ans.startswith('<'):
                ans = '<p>%s</p>' % ans
        return xml_replace_entities(ans)

    @html.setter
    def html(self, val):
        self.setHtml(val)

    def set_base_url(self, qurl):
        self.base_url = qurl

    @pyqtSlot(int, 'QUrl', result='QVariant')
    def loadResource(self, rtype, qurl):
        if self.base_url:
            if qurl.isRelative():
                qurl = self.base_url.resolved(qurl)
            if qurl.isLocalFile():
                path = qurl.toLocalFile()
                try:
                    with lopen(path, 'rb') as f:
                        data = f.read()
                except EnvironmentError:
                    pass
                else:
                    return QByteArray(data)

    def set_html(self, val, allow_undo=True):
        if not allow_undo or self.readonly:
            self.html = val
            return
        with self.editing_cursor() as c:
            c.movePosition(QTextCursor.Start, QTextCursor.MoveAnchor)
            c.movePosition(QTextCursor.End, QTextCursor.KeepAnchor)
            c.removeSelectedText()
            c.insertHtml(val)

    def text(self):
        return self.textCursor().selectedText()

    def setText(self, text):
        with self.editing_cursor() as c:
            c.insertText(text)

    def contextMenuEvent(self, ev):
        menu = self.createStandardContextMenu()
        for action in menu.actions():
            parts = action.text().split('\t')
            if len(parts) == 2 and QKeySequence(QKeySequence.Paste).toString(
                    QKeySequence.NativeText) in parts[-1]:
                menu.insertAction(action, self.action_paste_and_match_style)
                break
        else:
            menu.addAction(self.action_paste_and_match_style)
        st = self.text()
        m = QMenu(_('Fonts'))
        m.addAction(self.action_bold), m.addAction(
            self.action_italic), m.addAction(self.action_underline)
        menu.addMenu(m)

        if st and st.strip():
            self.create_change_case_menu(menu)
        parent = self._parent()
        if hasattr(parent, 'toolbars_visible'):
            vis = parent.toolbars_visible
            menu.addAction(
                _('%s toolbars') % (_('Hide') if vis else _('Show')),
                parent.toggle_toolbars)
        menu.exec_(ev.globalPos())
Exemplo n.º 43
0
class FicCard(QtWidgets.QWidget):
    def __init__(self, refMainLayout, ficModel):

        super().__init__()

        self.layout = QtWidgets.QVBoxLayout()
        self.layout.setAlignment(Qt.AlignTop)
        self.setLayout(self.layout)

        # Looks weird due to the gaps caused by the layout
        #self.setStyleSheet("background-color: {}".format(const.COLOR_FIC_CARD))
        #self.setStyleSheet("QWidget { border: 1px solid black; }")
        self.setBgColor(const.COL_TUPLE_FIC_CARD)
        self.setAutoFillBackground(True)

        self.labelTitle = QtWidgets.QLabel(ficModel.metadata.title)
        self.labelTitle.setStyleSheet("font-weight: bold;")
        self.labelTitle.setWordWrap(True)

        if ficModel.metadata.crossover:
            label = ficModel.metadata.fandomsCrossover[
                0] + " and " + ficModel.metadata.fandomsCrossover[
                    1] + " crossover"
            self.labelFandom = QtWidgets.QLabel(label)
        else:
            self.labelFandom = QtWidgets.QLabel(ficModel.metadata.fandom)
        self.labelFandom.setWordWrap(True)

        self.labelAuthor = QtWidgets.QLabel("By " + ficModel.metadata.author)
        self.labelAuthor.setStyleSheet("font-style: italic;")
        self.labelAuthor.setWordWrap(True)

        tag_string = "Tags: " + ", ".join(ficModel.getTagList())
        self.labelTags = QtWidgets.QLabel(tag_string)
        self.labelTags.setWordWrap(True)

        self.layout.addWidget(self.labelTitle)
        self.layout.addWidget(self.labelFandom)
        self.layout.addWidget(self.labelAuthor)
        self.layout.addWidget(self.labelTags)

        self.refMainLayout = refMainLayout
        self.ficModel = ficModel

        self.menu = QMenu(self)
        #self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.action_tags = self.menu.addAction("Edit Tags")

    def setBgColor(self, col_tuple):
        myPalette = self.palette()
        bgColor = QColor(*col_tuple)
        myPalette.setColor(QPalette.Background, bgColor)
        self.setPalette(myPalette)

    def mousePressEvent(self, event):
        if event.buttons() != Qt.LeftButton:
            return
        self.setBgColor(const.COL_TUPLE_FIC_CARD_PRESSED)

    def mouseReleaseEvent(self, event):
        self.refMainLayout.setReadingFic(self.ficModel)
        self.setBgColor(const.COL_TUPLE_FIC_CARD)

    # Context menu policy
    def contextMenuEvent(self, event):
        action = self.menu.exec_(self.mapToGlobal(event.pos()))
        if action == self.action_tags:
            self.refMainLayout.showFicDetails(self.ficModel)

    def refreshCards(self):
        pass
Exemplo n.º 44
0
class EditorWidget(QWebView):  # {{{
    def __init__(self, parent=None):
        QWebView.__init__(self, parent)
        self._parent = weakref.ref(parent)
        self.readonly = False

        self.comments_pat = re.compile(r'<!--.*?-->', re.DOTALL)

        extra_shortcuts = {
            'ToggleBold': 'Bold',
            'ToggleItalic': 'Italic',
            'ToggleUnderline': 'Underline',
        }

        for wac, name, icon, text, checkable in [
            ('ToggleBold', 'bold', 'format-text-bold', _('Bold'), True),
            ('ToggleItalic', 'italic', 'format-text-italic', _('Italic'),
             True),
            ('ToggleUnderline', 'underline', 'format-text-underline',
             _('Underline'), True),
            ('ToggleStrikethrough', 'strikethrough',
             'format-text-strikethrough', _('Strikethrough'), True),
            ('ToggleSuperscript', 'superscript', 'format-text-superscript',
             _('Superscript'), True),
            ('ToggleSubscript', 'subscript', 'format-text-subscript',
             _('Subscript'), True),
            ('InsertOrderedList', 'ordered_list', 'format-list-ordered',
             _('Ordered list'), True),
            ('InsertUnorderedList', 'unordered_list', 'format-list-unordered',
             _('Unordered list'), True),
            ('AlignLeft', 'align_left', 'format-justify-left', _('Align left'),
             False),
            ('AlignCenter', 'align_center', 'format-justify-center',
             _('Align center'), False),
            ('AlignRight', 'align_right', 'format-justify-right',
             _('Align right'), False),
            ('AlignJustified', 'align_justified', 'format-justify-fill',
             _('Align justified'), False),
            ('Undo', 'undo', 'edit-undo', _('Undo'), False),
            ('Redo', 'redo', 'edit-redo', _('Redo'), False),
            ('RemoveFormat', 'remove_format', 'trash', _('Remove formatting'),
             False),
            ('Copy', 'copy', 'edit-copy', _('Copy'), False),
            ('Paste', 'paste', 'edit-paste', _('Paste'), False),
            ('Cut', 'cut', 'edit-cut', _('Cut'), False),
            ('Indent', 'indent', 'format-indent-more',
             _('Increase Indentation'), False),
            ('Outdent', 'outdent', 'format-indent-less',
             _('Decrease Indentation'), False),
            ('SelectAll', 'select_all', 'edit-select-all', _('Select all'),
             False),
        ]:
            ac = PageAction(wac, icon, text, checkable, self)
            setattr(self, 'action_' + name, ac)
            ss = extra_shortcuts.get(wac, None)
            if ss:
                ac.setShortcut(QKeySequence(getattr(QKeySequence, ss)))
            if wac == 'RemoveFormat':
                ac.triggered.connect(self.remove_format_cleanup,
                                     type=Qt.QueuedConnection)

        self.action_color = QAction(QIcon(I('format-text-color')),
                                    _('Foreground color'), self)
        self.action_color.triggered.connect(self.foreground_color)

        self.action_background = QAction(QIcon(I('format-fill-color')),
                                         _('Background color'), self)
        self.action_background.triggered.connect(self.background_color)

        self.action_block_style = QAction(QIcon(I('format-text-heading')),
                                          _('Style text block'), self)
        self.action_block_style.setToolTip(_('Style the selected text block'))
        self.block_style_menu = QMenu(self)
        self.action_block_style.setMenu(self.block_style_menu)
        self.block_style_actions = []
        for text, name in [
            (_('Normal'), 'p'),
            (_('Heading') + ' 1', 'h1'),
            (_('Heading') + ' 2', 'h2'),
            (_('Heading') + ' 3', 'h3'),
            (_('Heading') + ' 4', 'h4'),
            (_('Heading') + ' 5', 'h5'),
            (_('Heading') + ' 6', 'h6'),
            (_('Pre-formatted'), 'pre'),
            (_('Blockquote'), 'blockquote'),
            (_('Address'), 'address'),
        ]:
            ac = BlockStyleAction(text, name, self)
            self.block_style_menu.addAction(ac)
            self.block_style_actions.append(ac)

        self.action_insert_link = QAction(QIcon(I('insert-link.png')),
                                          _('Insert link or image'), self)
        self.action_insert_link.triggered.connect(self.insert_link)
        self.pageAction(QWebPage.ToggleBold).changed.connect(
            self.update_link_action)
        self.action_insert_link.setEnabled(False)
        self.action_clear = QAction(QIcon(I('edit-clear')), _('Clear'), self)
        self.action_clear.triggered.connect(self.clear_text)

        self.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks)
        self.page().linkClicked.connect(self.link_clicked)

        self.setHtml('')
        self.set_readonly(False)

    def update_link_action(self):
        wac = self.pageAction(QWebPage.ToggleBold)
        self.action_insert_link.setEnabled(wac.isEnabled())

    def set_readonly(self, what):
        self.readonly = what
        self.page().setContentEditable(not self.readonly)

    def clear_text(self, *args):
        us = self.page().undoStack()
        us.beginMacro('clear all text')
        self.action_select_all.trigger()
        self.action_remove_format.trigger()
        self.exec_command('delete')
        us.endMacro()
        self.set_font_style()
        self.setFocus(Qt.OtherFocusReason)

    def link_clicked(self, url):
        open_url(url)

    def foreground_color(self):
        col = QColorDialog.getColor(Qt.black, self,
                                    _('Choose foreground color'),
                                    QColorDialog.ShowAlphaChannel)
        if col.isValid():
            self.exec_command('foreColor', unicode(col.name()))

    def background_color(self):
        col = QColorDialog.getColor(Qt.white, self,
                                    _('Choose background color'),
                                    QColorDialog.ShowAlphaChannel)
        if col.isValid():
            self.exec_command('hiliteColor', unicode(col.name()))

    def insert_link(self, *args):
        link, name, is_image = self.ask_link()
        if not link:
            return
        url = self.parse_link(link)
        if url.isValid():
            url = unicode(url.toString(QUrl.None))
            self.setFocus(Qt.OtherFocusReason)
            if is_image:
                self.exec_command(
                    'insertHTML', '<img src="%s" alt="%s"></img>' %
                    (prepare_string_for_xml(url, True),
                     prepare_string_for_xml(name or _('Image'), True)))
            elif name:
                self.exec_command(
                    'insertHTML',
                    '<a href="%s">%s</a>' % (prepare_string_for_xml(
                        url, True), prepare_string_for_xml(name)))
            else:
                self.exec_command('createLink', url)
        else:
            error_dialog(self,
                         _('Invalid URL'),
                         _('The url %r is invalid') % link,
                         show=True)

    def ask_link(self):
        d = QDialog(self)
        d.setWindowTitle(_('Create link'))
        l = QFormLayout()
        d.setLayout(l)
        d.url = QLineEdit(d)
        d.name = QLineEdit(d)
        d.treat_as_image = QCheckBox(d)
        d.setMinimumWidth(600)
        d.bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        d.br = b = QPushButton(_('&Browse'))
        b.setIcon(QIcon(I('document_open.png')))

        def cf():
            files = choose_files(d,
                                 'select link file',
                                 _('Choose file'),
                                 select_only_single_file=True)
            if files:
                path = files[0]
                d.url.setText(path)
                if path and os.path.exists(path):
                    with lopen(path, 'rb') as f:
                        q = what(f)
                    is_image = q in {'jpeg', 'png', 'gif'}
                    d.treat_as_image.setChecked(is_image)

        b.clicked.connect(cf)
        d.la = la = QLabel(
            _('Enter a URL. If you check the "Treat the URL as an image" box '
              'then the URL will be added as an image reference instead of as '
              'a link. You can also choose to create a link to a file on '
              'your computer. '
              'Note that if you create a link to a file on your computer, it '
              'will stop working if the file is moved.'))
        la.setWordWrap(True)
        la.setStyleSheet('QLabel { margin-bottom: 1.5ex }')
        l.setWidget(0, l.SpanningRole, la)
        l.addRow(_('Enter &URL:'), d.url)
        l.addRow(_('Treat the URL as an &image'), d.treat_as_image)
        l.addRow(_('Enter &name (optional):'), d.name)
        l.addRow(_('Choose a file on your computer:'), d.br)
        l.addRow(d.bb)
        d.bb.accepted.connect(d.accept)
        d.bb.rejected.connect(d.reject)
        d.resize(d.sizeHint())
        link, name, is_image = None, None, False
        if d.exec_() == d.Accepted:
            link, name = unicode(d.url.text()).strip(), unicode(
                d.name.text()).strip()
            is_image = d.treat_as_image.isChecked()
        return link, name, is_image

    def parse_link(self, link):
        link = link.strip()
        if link and os.path.exists(link):
            return QUrl.fromLocalFile(link)
        has_schema = re.match(r'^[a-zA-Z]+:', link)
        if has_schema is not None:
            url = QUrl(link, QUrl.TolerantMode)
            if url.isValid():
                return url
        if os.path.exists(link):
            return QUrl.fromLocalFile(link)

        if has_schema is None:
            first, _, rest = link.partition('.')
            prefix = 'http'
            if first == 'ftp':
                prefix = 'ftp'
            url = QUrl(prefix + '://' + link, QUrl.TolerantMode)
            if url.isValid():
                return url

        return QUrl(link, QUrl.TolerantMode)

    def sizeHint(self):
        return QSize(150, 150)

    def exec_command(self, cmd, arg=None):
        frame = self.page().mainFrame()
        if arg is not None:
            js = 'document.execCommand("%s", false, %s);' % (
                cmd, json.dumps(unicode(arg)))
        else:
            js = 'document.execCommand("%s", false, null);' % cmd
        frame.evaluateJavaScript(js)

    def remove_format_cleanup(self):
        self.html = self.html

    @dynamic_property
    def html(self):
        def fget(self):
            ans = u''
            try:
                if not self.page().mainFrame().documentElement().findFirst(
                        'meta[name="calibre-dont-sanitize"]').isNull():
                    # Bypass cleanup if special meta tag exists
                    return unicode(self.page().mainFrame().toHtml())
                check = unicode(self.page().mainFrame().toPlainText()).strip()
                raw = unicode(self.page().mainFrame().toHtml())
                raw = xml_to_unicode(raw,
                                     strip_encoding_pats=True,
                                     resolve_entities=True)[0]
                raw = self.comments_pat.sub('', raw)
                if not check and '<img' not in raw.lower():
                    return ans

                try:
                    root = html.fromstring(raw)
                except:
                    root = fromstring(raw)

                elems = []
                for body in root.xpath('//body'):
                    if body.text:
                        elems.append(body.text)
                    elems += [
                        html.tostring(x, encoding=unicode) for x in body
                        if x.tag not in ('script', 'style')
                    ]

                if len(elems) > 1:
                    ans = u'<div>%s</div>' % (u''.join(elems))
                else:
                    ans = u''.join(elems)
                    if not ans.startswith('<'):
                        ans = '<p>%s</p>' % ans
                ans = xml_replace_entities(ans)
            except:
                import traceback
                traceback.print_exc()

            return ans

        def fset(self, val):
            self.setHtml(val)
            self.set_font_style()

        return property(fget=fget, fset=fset)

    def set_font_style(self):
        fi = QFontInfo(QApplication.font(self))
        f = fi.pixelSize() + 1 + int(
            tweaks['change_book_details_font_size_by'])
        fam = unicode(fi.family()).strip().replace('"', '')
        if not fam:
            fam = 'sans-serif'
        style = 'font-size: %fpx; font-family:"%s",sans-serif;' % (f, fam)

        # toList() is needed because PyQt on Debian is old/broken
        for body in self.page().mainFrame().documentElement().findAll(
                'body').toList():
            body.setAttribute('style', style)
        self.page().setContentEditable(not self.readonly)

    def keyPressEvent(self, ev):
        if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab):
            ev.ignore()
        else:
            return QWebView.keyPressEvent(self, ev)

    def keyReleaseEvent(self, ev):
        if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab):
            ev.ignore()
        else:
            return QWebView.keyReleaseEvent(self, ev)

    def contextMenuEvent(self, ev):
        menu = self.page().createStandardContextMenu()
        paste = self.pageAction(QWebPage.Paste)
        for action in menu.actions():
            if action == paste:
                menu.insertAction(action,
                                  self.pageAction(QWebPage.PasteAndMatchStyle))
        parent = self._parent()
        if hasattr(parent, 'toolbars_visible'):
            vis = parent.toolbars_visible
            menu.addAction(
                _('%s toolbars') % (_('Hide') if vis else _('Show')),
                (parent.hide_toolbars if vis else parent.show_toolbars))
        menu.exec_(ev.globalPos())
Exemplo n.º 45
0
class ConfigWidget(ConfigWidgetBase):
    def setupUi(self, x):
        self.l = l = QVBoxLayout(self)
        self.la1 = la = QLabel(
            _("Values for the tweaks are shown below. Edit them to change the behavior of calibre."
              " Your changes will only take effect <b>after a restart</b> of calibre."
              ))
        l.addWidget(la), la.setWordWrap(True)
        self.splitter = s = QSplitter(self)
        s.setChildrenCollapsible(False)
        l.addWidget(s, 10)

        self.lv = lv = QWidget(self)
        lv.l = l2 = QVBoxLayout(lv)
        l2.setContentsMargins(0, 0, 0, 0)
        self.tweaks_view = tv = TweaksView(self)
        l2.addWidget(tv)
        self.plugin_tweaks_button = b = QPushButton(self)
        b.setToolTip(
            _("Edit tweaks for any custom plugins you have installed"))
        b.setText(_("&Plugin tweaks"))
        l2.addWidget(b)
        s.addWidget(lv)

        self.lv1 = lv = QWidget(self)
        s.addWidget(lv)
        lv.g = g = QGridLayout(lv)
        g.setContentsMargins(0, 0, 0, 0)

        self.search = sb = SearchBox2(self)
        sb.sizePolicy().setHorizontalStretch(10)
        sb.setSizeAdjustPolicy(sb.AdjustToMinimumContentsLength)
        sb.setMinimumContentsLength(10)
        g.addWidget(self.search, 0, 0, 1, 1)
        self.next_button = b = QPushButton(self)
        b.setIcon(QIcon(I("arrow-down.png")))
        b.setText(_("&Next"))
        g.addWidget(self.next_button, 0, 1, 1, 1)
        self.previous_button = b = QPushButton(self)
        b.setIcon(QIcon(I("arrow-up.png")))
        b.setText(_("&Previous"))
        g.addWidget(self.previous_button, 0, 2, 1, 1)

        self.hb = hb = QGroupBox(self)
        hb.setTitle(_("Help"))
        hb.l = l2 = QVBoxLayout(hb)
        self.help = h = QPlainTextEdit(self)
        l2.addWidget(h)
        h.setReadOnly(True)
        g.addWidget(hb, 1, 0, 1, 3)

        self.eb = eb = QGroupBox(self)
        g.addWidget(eb, 2, 0, 1, 3)
        eb.setTitle(_("Edit tweak"))
        eb.g = ebg = QGridLayout(eb)
        self.edit_tweak = et = QPlainTextEdit(self)
        et.setMinimumWidth(400)
        et.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
        ebg.addWidget(et, 0, 0, 1, 2)
        self.restore_default_button = b = QPushButton(self)
        b.setToolTip(_("Restore this tweak to its default value"))
        b.setText(_("&Reset this tweak"))
        ebg.addWidget(b, 1, 0, 1, 1)
        self.apply_button = ab = QPushButton(self)
        ab.setToolTip(_("Apply any changes you made to this tweak"))
        ab.setText(_("&Apply changes to this tweak"))
        ebg.addWidget(ab, 1, 1, 1, 1)

    def genesis(self, gui):
        self.gui = gui
        self.delegate = Delegate(self.tweaks_view)
        self.tweaks_view.setItemDelegate(self.delegate)
        self.tweaks_view.current_changed.connect(self.current_changed)
        self.view = self.tweaks_view
        self.highlighter = PythonHighlighter(self.edit_tweak.document())
        self.restore_default_button.clicked.connect(self.restore_to_default)
        self.apply_button.clicked.connect(self.apply_tweak)
        self.plugin_tweaks_button.clicked.connect(self.plugin_tweaks)
        self.splitter.setStretchFactor(0, 1)
        self.splitter.setStretchFactor(1, 100)
        self.next_button.clicked.connect(self.find_next)
        self.previous_button.clicked.connect(self.find_previous)
        self.search.initialize('tweaks_search_history',
                               help_text=_('Search for tweak'))
        self.search.search.connect(self.find)
        self.view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.view.customContextMenuRequested.connect(self.show_context_menu)
        self.copy_icon = QIcon(I('edit-copy.png'))

    def show_context_menu(self, point):
        idx = self.tweaks_view.currentIndex()
        if not idx.isValid():
            return True
        tweak = self.tweaks.data(idx, Qt.ItemDataRole.UserRole)
        self.context_menu = QMenu(self)
        self.context_menu.addAction(
            self.copy_icon, _('Copy to clipboard'),
            partial(self.copy_item_to_clipboard,
                    val="%s (%s: %s)" %
                    (tweak.name, _('ID'), tweak.var_names[0])))
        self.context_menu.popup(self.mapToGlobal(point))
        return True

    def copy_item_to_clipboard(self, val):
        cb = QApplication.clipboard()
        cb.clear()
        cb.setText(val)

    def plugin_tweaks(self):
        raw = self.tweaks.plugin_tweaks_string
        d = PluginTweaks(raw, self)
        if d.exec_() == QDialog.DialogCode.Accepted:
            g, l = {}, {}
            try:
                exec(unicode_type(d.edit.toPlainText()), g, l)
            except:
                import traceback
                return error_dialog(
                    self,
                    _('Failed'),
                    _('There was a syntax error in your tweak. Click '
                      'the "Show details" button for details.'),
                    show=True,
                    det_msg=traceback.format_exc())
            self.tweaks.set_plugin_tweaks(l)
            self.changed()

    def current_changed(self, current, previous):
        self.tweaks_view.scrollTo(current)
        tweak = self.tweaks.data(current, Qt.ItemDataRole.UserRole)
        self.help.setPlainText(tweak.doc)
        self.edit_tweak.setPlainText(tweak.edit_text)

    def changed(self, *args):
        self.changed_signal.emit()

    def initialize(self):
        self.tweaks = self._model = Tweaks()
        self.tweaks_view.setModel(self.tweaks)

    def restore_to_default(self, *args):
        idx = self.tweaks_view.currentIndex()
        if idx.isValid():
            self.tweaks.restore_to_default(idx)
            tweak = self.tweaks.data(idx, Qt.ItemDataRole.UserRole)
            self.edit_tweak.setPlainText(tweak.edit_text)
            self.changed()

    def restore_defaults(self):
        ConfigWidgetBase.restore_defaults(self)
        self.tweaks.restore_to_defaults()
        self.changed()

    def apply_tweak(self):
        idx = self.tweaks_view.currentIndex()
        if idx.isValid():
            l, g = {}, {}
            try:
                exec(unicode_type(self.edit_tweak.toPlainText()), g, l)
            except:
                import traceback
                error_dialog(self.gui,
                             _('Failed'),
                             _('There was a syntax error in your tweak. Click '
                               'the "Show details" button for details.'),
                             det_msg=traceback.format_exc(),
                             show=True)
                return
            self.tweaks.update_tweak(idx, l)
            self.changed()

    def commit(self):
        raw = self.tweaks.to_string()
        if not isinstance(raw, bytes):
            raw = raw.encode('utf-8')
        try:
            custom_tweaks = exec_tweaks(raw)
        except:
            import traceback
            error_dialog(
                self,
                _('Invalid tweaks'),
                _('The tweaks you entered are invalid, try resetting the'
                  ' tweaks to default and changing them one by one until'
                  ' you find the invalid setting.'),
                det_msg=traceback.format_exc(),
                show=True)
            raise AbortCommit('abort')
        write_custom_tweaks(custom_tweaks)
        ConfigWidgetBase.commit(self)
        return True

    def find(self, query):
        if not query:
            return
        try:
            idx = self._model.find(query)
        except ParseException:
            self.search.search_done(False)
            return
        self.search.search_done(True)
        if not idx.isValid():
            info_dialog(self,
                        _('No matches'),
                        _('Could not find any shortcuts matching %s') % query,
                        show=True,
                        show_copy_button=False)
            return
        self.highlight_index(idx)

    def highlight_index(self, idx):
        if not idx.isValid():
            return
        self.view.scrollTo(idx)
        self.view.selectionModel().select(
            idx,
            self.view.selectionModel().ClearAndSelect)
        self.view.setCurrentIndex(idx)

    def find_next(self, *args):
        idx = self.view.currentIndex()
        if not idx.isValid():
            idx = self._model.index(0)
        idx = self._model.find_next(idx,
                                    unicode_type(self.search.currentText()))
        self.highlight_index(idx)

    def find_previous(self, *args):
        idx = self.view.currentIndex()
        if not idx.isValid():
            idx = self._model.index(0)
        idx = self._model.find_next(idx,
                                    unicode_type(self.search.currentText()),
                                    backwards=True)
        self.highlight_index(idx)
Exemplo n.º 46
0
class Quickview(QDialog, Ui_Quickview):

    reopen_after_dock_change = pyqtSignal()
    tab_pressed_signal = pyqtSignal(object, object)
    quickview_closed = pyqtSignal()

    def __init__(self, gui, row):
        self.is_pane = gprefs.get('quickview_is_pane', False)

        if not self.is_pane:
            QDialog.__init__(self, gui, flags=Qt.Widget)
        else:
            QDialog.__init__(self, gui)
        Ui_Quickview.__init__(self)
        self.setupUi(self)
        self.isClosed = False
        self.current_book = None
        self.closed_by_button = False

        if self.is_pane:
            self.main_grid_layout.setContentsMargins(0, 0, 0, 0)

        self.books_table_column_widths = None
        try:
            self.books_table_column_widths = \
                        gprefs.get('quickview_dialog_books_table_widths', None)
            if not self.is_pane:
                geom = gprefs.get('quickview_dialog_geometry', bytearray(''))
                self.restoreGeometry(QByteArray(geom))
        except:
            pass

        if not self.is_pane:
            # Remove the help button from the window title bar
            icon = self.windowIcon()
            self.setWindowFlags(self.windowFlags()
                                & (~Qt.WindowContextHelpButtonHint))
            self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint)
            self.setWindowIcon(icon)

        self.view = gui.library_view
        self.db = self.view.model().db
        self.gui = gui
        self.is_closed = False
        self.current_book_id = None  # the db id of the book used to fill the lh pane
        self.current_column = None  # current logical column in books list
        self.current_key = None  # current lookup key in books list
        self.last_search = None
        self.no_valid_items = False

        self.fm = self.db.field_metadata

        self.items.setSelectionMode(QAbstractItemView.SingleSelection)
        self.items.currentTextChanged.connect(self.item_selected)
        self.items.setProperty('highlight_current_item', 150)

        focus_filter = WidgetFocusFilter(self.items)
        focus_filter.focus_entered_signal.connect(self.focus_entered)
        self.items.installEventFilter(focus_filter)

        self.tab_pressed_signal.connect(self.tab_pressed)
        return_filter = BooksTableFilter(self.books_table)
        return_filter.return_pressed_signal.connect(self.return_pressed)
        self.books_table.installEventFilter(return_filter)

        focus_filter = WidgetFocusFilter(self.books_table)
        focus_filter.focus_entered_signal.connect(self.focus_entered)
        self.books_table.installEventFilter(focus_filter)

        self.close_button.clicked.connect(self.close_button_clicked)
        self.refresh_button.clicked.connect(self.refill)

        self.tab_order_widgets = [
            self.items, self.books_table, self.lock_qv, self.dock_button,
            self.search_button, self.refresh_button, self.close_button
        ]
        for idx, widget in enumerate(self.tab_order_widgets):
            widget.installEventFilter(
                WidgetTabFilter(widget, idx, self.tab_pressed_signal))

        self.books_table.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.books_table.setSelectionMode(QAbstractItemView.SingleSelection)
        self.books_table.setProperty('highlight_current_item', 150)

        # Set up the books table columns
        self.add_columns_to_widget()

        self.books_table_header_height = self.books_table.height()
        self.books_table.cellDoubleClicked.connect(self.book_doubleclicked)
        self.books_table.currentCellChanged.connect(
            self.books_table_cell_changed)
        self.books_table.cellClicked.connect(
            self.books_table_set_search_string)
        self.books_table.cellActivated.connect(
            self.books_table_set_search_string)
        self.books_table.sortByColumn(0, Qt.AscendingOrder)

        # get the standard table row height. Do this here because calling
        # resizeRowsToContents can word wrap long cell contents, creating
        # double-high rows
        self.books_table.setRowCount(1)
        self.books_table.setItem(0, 0, TableItem('A', ''))
        self.books_table.resizeRowsToContents()
        self.books_table_row_height = self.books_table.rowHeight(0)
        self.books_table.setRowCount(0)

        # Add the data
        self.refresh(row)

        self.view.clicked.connect(self.slave)
        self.view.selectionModel().currentColumnChanged.connect(
            self.column_slave)
        QCoreApplication.instance().aboutToQuit.connect(self.save_state)
        self.search_button.clicked.connect(self.do_search)
        self.view.model().new_bookdisplay_data.connect(self.book_was_changed)

        self.close_button.setDefault(False)
        self.close_button_tooltip = _(
            'The Quickview shortcut ({0}) shows/hides the Quickview panel')
        self.search_button_tooltip = _(
            'Search in the library view for the currently highlighted selection'
        )
        self.search_button.setToolTip(self.search_button_tooltip)
        if self.is_pane:
            self.dock_button.setText(_('Undock'))
            self.dock_button.setToolTip(
                _('Pop up the quickview panel into its own floating window'))
            self.dock_button.setIcon(QIcon(I('arrow-up.png')))
            # Remove the ampersands from the buttons because shortcuts exist.
            self.lock_qv.setText(_('Lock Quickview contents'))
            self.search_button.setText(_('Search'))
            self.refresh_button.setText(_('Refresh'))
            self.gui.quickview_splitter.add_quickview_dialog(self)
            self.close_button.setVisible(False)
        else:
            self.dock_button.setToolTip(
                _('Embed the quickview panel into the main calibre window'))
            self.dock_button.setIcon(QIcon(I('arrow-down.png')))
        self.set_focus()

        self.books_table.horizontalHeader().sectionResized.connect(
            self.section_resized)
        self.dock_button.clicked.connect(self.show_as_pane_changed)
        self.gui.search.cleared.connect(self.indicate_no_items)

        # Enable the refresh button only when QV is locked
        self.refresh_button.setEnabled(False)
        self.lock_qv.stateChanged.connect(self.lock_qv_changed)

        self.view_icon = QIcon(I('view.png'))
        self.view_plugin = self.gui.iactions['View']
        self.books_table.setContextMenuPolicy(Qt.CustomContextMenu)
        self.books_table.customContextMenuRequested.connect(
            self.show_context_menu)

    def show_context_menu(self, point):
        index = self.books_table.indexAt(point)
        item = self.books_table.item(index.row(), 0)
        book_id = int(item.data(Qt.UserRole))
        self.context_menu = QMenu(self)
        self.context_menu.addAction(
            self.view_icon, _('View'),
            partial(self.view_plugin._view_calibre_books, [book_id]))
        self.context_menu.popup(self.books_table.mapToGlobal(point))
        return True

    def lock_qv_changed(self, state):
        self.refresh_button.setEnabled(state)

    def add_columns_to_widget(self):
        '''
        Get the list of columns from the preferences. Clear the current table
        and add the current column set
        '''
        self.column_order = [x[0] for x in get_qv_field_list(self.fm) if x[1]]
        self.books_table.clear()
        self.books_table.setRowCount(0)
        self.books_table.setColumnCount(len(self.column_order))
        for idx, col in enumerate(self.column_order):
            t = QTableWidgetItem(self.fm[col]['name'])
            self.books_table.setHorizontalHeaderItem(idx, t)

    def refill(self):
        '''
            Refill the table in case the columns displayed changes
        '''
        self.add_columns_to_widget()
        self._refresh(self.current_book_id, self.current_key)

    def set_search_text(self, txt):
        if txt:
            self.search_button.setEnabled(True)
        else:
            self.search_button.setEnabled(False)
        self.last_search = txt

    def set_shortcuts(self, search_sc, qv_sc):
        if self.is_pane:
            self.search_button.setToolTip(self.search_button_tooltip + ' (' +
                                          search_sc + ')')
            self.close_button.setToolTip(
                self.close_button_tooltip.format(qv_sc))

    def focus_entered(self, obj):
        if obj == self.books_table:
            self.books_table_set_search_string(
                self.books_table.currentRow(),
                self.books_table.currentColumn())
        elif obj.currentItem():
            self.item_selected(obj.currentItem().text())

    def books_table_cell_changed(self, cur_row, cur_col, prev_row, prev_col):
        self.books_table_set_search_string(cur_row, cur_col)

    def books_table_set_search_string(self, current_row, current_col):
        '''
        Given the contents of a cell, compute a search string that will find
        that book and any others with identical contents in the cell.
        '''
        current = self.books_table.item(current_row, current_col)
        if current is None:
            return
        book_id = current.data(Qt.UserRole)

        if current is None:
            return
        col = self.column_order[current.column()]
        if col == 'title':
            self.set_search_text('title:="' +
                                 current.text().replace('"', '\\"') + '"')
        elif col == 'authors':
            authors = []
            for aut in [t.strip() for t in current.text().split('&')]:
                authors.append('authors:="' + aut.replace('"', '\\"') + '"')
            self.set_search_text(' and '.join(authors))
        elif self.fm[col]['datatype'] == 'series':
            mi = self.db.get_metadata(book_id,
                                      index_is_id=True,
                                      get_user_categories=False)
            t = mi.get(col)
            if t:
                self.set_search_text(col + ':="' + t + '"')
            else:
                self.set_search_text(None)
        else:
            if self.fm[col]['is_multiple']:
                items = [(col + ':"=' + v.strip() + '"')
                         for v in current.text().split(
                             self.fm[col]['is_multiple']['ui_to_list'])]
                self.set_search_text(' and '.join(items))
            else:
                self.set_search_text(col + ':"=' + current.text() + '"')

    def tab_pressed(self, in_widget, isForward):
        if isForward:
            in_widget += 1
            if in_widget >= len(self.tab_order_widgets):
                in_widget = 0
        else:
            in_widget -= 1
            if in_widget < 0:
                in_widget = len(self.tab_order_widgets) - 1
        self.tab_order_widgets[in_widget].setFocus(Qt.TabFocusReason)

    def show(self):
        QDialog.show(self)
        if self.is_pane:
            self.gui.quickview_splitter.show_quickview_widget()

    def show_as_pane_changed(self):
        gprefs['quickview_is_pane'] = not gprefs.get('quickview_is_pane',
                                                     False)
        self.reopen_after_dock_change.emit()

    # search button
    def do_search(self):
        if self.no_valid_items:
            return
        if self.last_search is not None:
            self.gui.search.set_search_string(self.last_search)

    def book_was_changed(self, mi):
        '''
        Called when book information is changed in the library view. Make that
        book info current. This means that prev and next in edit metadata will move
        the current book and change quickview
        '''
        if self.is_closed or self.current_column is None:
            return
        # There is an ordering problem when libraries are changed. The library
        # view is changed, triggering a book_was_changed signal. Unfortunately
        # this happens before the library_changed actions are run, meaning we
        # still have the old database. To avoid the problem we just ignore the
        # operation if we get an exception. The "close" will come
        # eventually.
        try:
            self.refresh(self.view.model().index(self.db.row(mi.id),
                                                 self.current_column))
        except:
            pass

    # clicks on the items listWidget
    def item_selected(self, txt):
        if self.no_valid_items:
            return
        self.fill_in_books_box(unicode(txt))
        self.set_search_text(self.current_key + ':"=' +
                             txt.replace('"', '\\"') + '"')

    def refresh(self, idx):
        '''
        Given a cell in the library view, display the information. This method
        converts the index into the lookup ken
        '''
        if self.lock_qv.isChecked():
            return

        try:
            bv_row = idx.row()
            self.current_column = idx.column()
            key = self.view.column_map[self.current_column]
            book_id = self.view.model().id(bv_row)
            if self.current_book_id == book_id and self.current_key == key:
                return
            self._refresh(book_id, key)
        except:
            self.indicate_no_items()

    def _refresh(self, book_id, key):
        '''
        Actually fill in the left-hand pane from the information in the
        selected column of the selected book
        '''
        # Only show items for categories
        if key is None or not self.fm[key]['is_category']:
            if self.current_key is None:
                self.indicate_no_items()
                return
            key = self.current_key
        label_text = _('&Item: {0} ({1})')
        if self.is_pane:
            label_text = label_text.replace('&', '')
        self.items_label.setText(label_text.format(self.fm[key]['name'], key))

        self.items.blockSignals(True)
        self.items.clear()
        self.books_table.setRowCount(0)

        mi = self.db.get_metadata(book_id,
                                  index_is_id=True,
                                  get_user_categories=False)
        vals = mi.get(key, None)

        if vals:
            self.no_valid_items = False
            if self.fm[key]['datatype'] == 'rating':
                if self.fm[key]['display'].get('allow_half_stars', False):
                    vals = unicode(vals / 2.0)
                else:
                    vals = unicode(vals / 2)
            if not isinstance(vals, list):
                vals = [vals]
            vals.sort(key=sort_key)

            for v in vals:
                a = QListWidgetItem(v)
                self.items.addItem(a)
            self.items.setCurrentRow(0)

            self.current_book_id = book_id
            self.current_key = key

            self.fill_in_books_box(vals[0])
        else:
            self.indicate_no_items()

        self.items.blockSignals(False)

    def indicate_no_items(self):
        self.no_valid_items = True
        self.items.clear()
        self.add_columns_to_widget()
        self.items.addItem(QListWidgetItem(_('**No items found**')))
        self.books_label.setText(
            _('Click in a column  in the library view '
              'to see the information for that book'))

    def fill_in_books_box(self, selected_item):
        '''
        Given the selected row in the left-hand box, fill in the grid with
        the books that contain that data.
        '''
        # Do a bit of fix-up on the items so that the search works.
        if selected_item.startswith('.'):
            sv = '.' + selected_item
        else:
            sv = selected_item
        sv = self.current_key + ':"=' + sv.replace('"', r'\"') + '"'
        if gprefs['qv_respects_vls']:
            books = self.db.search(sv, return_matches=True, sort_results=False)
        else:
            books = self.db.new_api.search(sv)

        self.books_table.setRowCount(len(books))
        label_text = _('&Books with selected item "{0}": {1}')
        if self.is_pane:
            label_text = label_text.replace('&', '')
        self.books_label.setText(label_text.format(selected_item, len(books)))

        select_item = None
        self.books_table.setSortingEnabled(False)
        self.books_table.blockSignals(True)
        tt = ('<p>' + _(
            'Double click on a book to change the selection in the library view or '
            'change the column shown in the left-hand pane. '
            'Shift- or Control- double click to edit the metadata of a book, '
            'which also changes the selected book.') + '</p>')
        for row, b in enumerate(books):
            mi = self.db.get_metadata(b,
                                      index_is_id=True,
                                      get_user_categories=False)
            for col in self.column_order:
                try:
                    if col == 'title':
                        a = TableItem(mi.title, mi.title_sort)
                        if b == self.current_book_id:
                            select_item = a
                    elif col == 'authors':
                        a = TableItem(' & '.join(mi.authors), mi.author_sort)
                    elif col == 'series':
                        series = mi.format_field('series')[1]
                        if series is None:
                            a = TableItem('', '', 0)
                        else:
                            a = TableItem(series, mi.series, mi.series_index)
                    elif self.fm[col]['datatype'] == 'series':
                        v = mi.format_field(col)[1]
                        a = TableItem(v, mi.get(col), mi.get(col + '_index'))
                    elif self.fm[col]['datatype'] == 'datetime':
                        v = mi.format_field(col)[1]
                        d = mi.get(col)
                        if d is None:
                            d = UNDEFINED_DATE
                        a = TableItem(v, timestampfromdt(d))
                    elif self.fm[col]['datatype'] in ('float', 'int'):
                        v = mi.format_field(col)[1]
                        sv = mi.get(col)
                        a = TableItem(v, sv)
                    else:
                        v = mi.format_field(col)[1]
                        a = TableItem(v, v)
                except:
                    traceback.print_exc()
                    a = TableItem(
                        _('Something went wrong while filling in the table'),
                        '')
                a.setData(Qt.UserRole, b)
                a.setToolTip(tt)
                self.books_table.setItem(row,
                                         self.key_to_table_widget_column(col),
                                         a)
                self.books_table.setRowHeight(row, self.books_table_row_height)
        self.books_table.blockSignals(False)
        self.books_table.setSortingEnabled(True)
        if select_item is not None:
            self.books_table.setCurrentItem(select_item)
            self.books_table.scrollToItem(select_item,
                                          QAbstractItemView.PositionAtCenter)
        self.set_search_text(sv)

    # Deal with sizing the table columns. Done here because the numbers are not
    # correct until the first paint.
    def resizeEvent(self, *args):
        QDialog.resizeEvent(self, *args)

        # Do this if we are resizing for the first time to reset state.
        if self.is_pane and self.height() == 0:
            self.gui.quickview_splitter.set_sizes()

        if self.books_table_column_widths is not None:
            for c, w in enumerate(self.books_table_column_widths):
                self.books_table.setColumnWidth(c, w)
        else:
            # the vertical scroll bar might not be rendered, so might not yet
            # have a width. Assume 25. Not a problem because user-changed column
            # widths will be remembered
            w = self.books_table.width(
            ) - 25 - self.books_table.verticalHeader().width()
            w /= self.books_table.columnCount()
            for c in range(0, self.books_table.columnCount()):
                self.books_table.setColumnWidth(c, w)
        self.save_state()

    def key_to_table_widget_column(self, key):
        return self.column_order.index(key)

    def return_pressed(self):
        row = self.books_table.currentRow()
        if gprefs['qv_retkey_changes_column']:
            self.select_book(row, self.books_table.currentColumn())
        else:
            self.select_book(row,
                             self.key_to_table_widget_column(self.current_key))

    def book_doubleclicked(self, row, column):
        if self.no_valid_items:
            return
        try:
            if gprefs['qv_dclick_changes_column']:
                self.select_book(row, column)
            else:
                self.select_book(
                    row, self.key_to_table_widget_column(self.current_key))
        except:
            from calibre.gui2 import error_dialog
            error_dialog(
                self,
                _('Quickview: Book not in library view'),
                _('The book you selected is not currently displayed in '
                  'the library view, perhaps because of a search, so '
                  'Quickview cannot select it.'),
                show=True,
                show_copy_button=False)

    def select_book(self, row, column):
        '''
        row and column both refer the qv table. In particular, column is not
        the logical column in the book list.
        '''
        item = self.books_table.item(row, column)
        if item is None:
            return
        book_id = int(self.books_table.item(row, column).data(Qt.UserRole))
        key = self.column_order[column]
        modifiers = int(QApplication.keyboardModifiers())
        if modifiers in (Qt.CTRL, Qt.SHIFT):
            self.view.select_rows([book_id])
            em = find_plugin('Edit Metadata')
            if em and em.actual_plugin_:
                em.actual_plugin_.edit_metadata(None)
        else:
            self.view.select_cell(self.db.data.id_to_index(book_id),
                                  self.view.column_map.index(key))

    def set_focus(self):
        self.activateWindow()
        self.books_table.setFocus()

    def column_slave(self, current):
        '''
        called when the column is changed on the booklist
        '''
        if gprefs['qv_follows_column']:
            self.slave(current)

    def slave(self, current):
        '''
        called when a book is clicked on the library view
        '''
        if self.is_closed:
            return
        self.refresh(current)
        self.view.activateWindow()

    def section_resized(self, logicalIndex, oldSize, newSize):
        self.save_state()

    def save_state(self):
        if self.is_closed:
            return
        self.books_table_column_widths = []
        for c in range(0, self.books_table.columnCount()):
            self.books_table_column_widths.append(
                self.books_table.columnWidth(c))
        gprefs[
            'quickview_dialog_books_table_widths'] = self.books_table_column_widths
        if not self.is_pane:
            gprefs['quickview_dialog_geometry'] = bytearray(
                self.saveGeometry())

    def _close(self):
        self.save_state()
        # clean up to prevent memory leaks
        self.db = self.view = self.gui = None
        self.is_closed = True

    def close_button_clicked(self):
        self.closed_by_button = True
        self.quickview_closed.emit()

    def reject(self):
        if not self.closed_by_button:
            self.close_button_clicked()
        else:
            self._reject()

    def _reject(self):
        if self.is_pane:
            self.gui.quickview_splitter.hide_quickview_widget()
        self.gui.library_view.setFocus(Qt.ActiveWindowFocusReason)
        self._close()
        QDialog.reject(self)
Exemplo n.º 47
0
    class MenuBar(QObject):

        is_native_menubar = False

        def __init__(self, location_manager, parent):
            QObject.__init__(self, parent)
            f = factory(app_id='com.calibre-ebook.gui')
            self.menu_bar = f.create_window_menubar(parent)
            self.is_native_menubar = self.menu_bar.is_native_menubar
            self.gui = parent

            self.location_manager = location_manager
            self.added_actions = []

            self.donate_action = QAction(_('Donate'), self)
            self.donate_menu = QMenu()
            self.donate_menu.addAction(self.gui.donate_action)
            self.donate_action.setMenu(self.donate_menu)

        def addAction(self, *args):
            self.menu_bar.addAction(*args)

        def setVisible(self, visible):
            self.menu_bar.setVisible(visible)

        def clear(self):
            self.menu_bar.clear()

        def init_bar(self, actions):
            for ac in self.added_actions:
                m = ac.menu()
                if m is not None:
                    m.setVisible(False)

            self.clear()
            self.added_actions = []

            for what in actions:
                if what is None:
                    continue
                elif what == 'Location Manager':
                    for ac in self.location_manager.all_actions:
                        ac = self.build_menu(ac)
                        self.addAction(ac)
                        self.added_actions.append(ac)
                        ac.setVisible(False)
                elif what == 'Donate':
                    self.addAction(self.donate_action)
                elif what in self.gui.iactions:
                    action = self.gui.iactions[what]
                    ac = self.build_menu(action.qaction)
                    self.addAction(ac)
                    self.added_actions.append(ac)

        def build_menu(self, action):
            m = action.menu()
            ac = MenuAction(action, self)
            if m is None:
                m = QMenu()
                m.addAction(action)
            ac.setMenu(m)
            return ac

        def update_lm_actions(self):
            for ac in self.added_actions:
                clone = getattr(ac, 'clone', None)
                if clone is not None and clone in self.location_manager.all_actions:
                    ac.setVisible(
                        clone in self.location_manager.available_actions)
Exemplo n.º 48
0
class Scheduler(QObject):

    INTERVAL = 1  # minutes

    delete_old_news = pyqtSignal(object)
    start_recipe_fetch = pyqtSignal(object)

    def __init__(self, parent, db):
        QObject.__init__(self, parent)
        self.internet_connection_failed = False
        self._parent = parent
        self.no_internet_msg = _('Cannot download news as no internet connection '
                'is active')
        self.no_internet_dialog = d = error_dialog(self._parent,
                self.no_internet_msg, _('No internet connection'),
                show_copy_button=False)
        d.setModal(False)

        self.recipe_model = RecipeModel()
        self.db = db
        self.lock = QMutex(QMutex.Recursive)
        self.download_queue = set([])

        self.news_menu = QMenu()
        self.news_icon = QIcon(I('news.png'))
        self.scheduler_action = QAction(QIcon(I('scheduler.png')), _('Schedule news download'), self)
        self.news_menu.addAction(self.scheduler_action)
        self.scheduler_action.triggered[bool].connect(self.show_dialog)
        self.cac = QAction(QIcon(I('user_profile.png')), _('Add a custom news source'), self)
        self.cac.triggered[bool].connect(self.customize_feeds)
        self.news_menu.addAction(self.cac)
        self.news_menu.addSeparator()
        self.all_action = self.news_menu.addAction(
                _('Download all scheduled news sources'),
                self.download_all_scheduled)

        self.timer = QTimer(self)
        self.timer.start(int(self.INTERVAL * 60 * 1000))
        self.timer.timeout.connect(self.check)
        self.oldest = gconf['oldest_news']
        QTimer.singleShot(5 * 1000, self.oldest_check)

    def database_changed(self, db):
        self.db = db

    def oldest_check(self):
        if self.oldest > 0:
            delta = timedelta(days=self.oldest)
            try:
                ids = list(self.db.tags_older_than(_('News'),
                    delta, must_have_authors=['calibre']))
            except:
                # Happens if library is being switched
                ids = []
            if ids:
                if ids:
                    self.delete_old_news.emit(ids)
        QTimer.singleShot(60 * 60 * 1000, self.oldest_check)

    def show_dialog(self, *args):
        self.lock.lock()
        try:
            d = SchedulerDialog(self.recipe_model)
            d.download.connect(self.download_clicked)
            d.exec_()
            gconf['oldest_news'] = self.oldest = d.old_news.value()
            d.break_cycles()
        finally:
            self.lock.unlock()

    def customize_feeds(self, *args):
        from calibre.gui2.dialogs.custom_recipes import CustomRecipes
        d = CustomRecipes(self.recipe_model, self._parent)
        try:
            d.exec_()
        finally:
            d.deleteLater()

    def do_download(self, urn):
        self.lock.lock()
        try:
            account_info = self.recipe_model.get_account_info(urn)
            customize_info = self.recipe_model.get_customize_info(urn)
            recipe = self.recipe_model.recipe_from_urn(urn)
            un = pw = None
            if account_info is not None:
                un, pw = account_info
            add_title_tag, custom_tags, keep_issues = customize_info
            script = self.recipe_model.get_recipe(urn)
            pt = PersistentTemporaryFile('_builtin.recipe')
            pt.write(script)
            pt.close()
            arg = {
                    'username': un,
                    'password': pw,
                    'add_title_tag':add_title_tag,
                    'custom_tags':custom_tags,
                    'recipe':pt.name,
                    'title':recipe.get('title',''),
                    'urn':urn,
                    'keep_issues':keep_issues
                   }
            self.download_queue.add(urn)
            self.start_recipe_fetch.emit(arg)
        finally:
            self.lock.unlock()

    def recipe_downloaded(self, arg):
        self.lock.lock()
        try:
            self.recipe_model.update_last_downloaded(arg['urn'])
            self.download_queue.remove(arg['urn'])
        finally:
            self.lock.unlock()

    def recipe_download_failed(self, arg):
        self.lock.lock()
        try:
            self.recipe_model.update_last_downloaded(arg['urn'])
            self.download_queue.remove(arg['urn'])
        finally:
            self.lock.unlock()

    def download_clicked(self, urn):
        if urn is not None:
            return self.download(urn)
        for urn in self.recipe_model.scheduled_urns():
            if not self.download(urn):
                break

    def download_all_scheduled(self):
        self.download_clicked(None)

    def has_internet_connection(self):
        if not internet_connected():
            if not self.internet_connection_failed:
                self.internet_connection_failed = True
                if self._parent.is_minimized_to_tray:
                    self._parent.status_bar.show_message(self.no_internet_msg,
                            5000)
                elif not self.no_internet_dialog.isVisible():
                    self.no_internet_dialog.show()
            return False
        self.internet_connection_failed = False
        if self.no_internet_dialog.isVisible():
            self.no_internet_dialog.hide()
        return True

    def download(self, urn):
        self.lock.lock()
        if not self.has_internet_connection():
            return False
        doit = urn not in self.download_queue
        self.lock.unlock()
        if doit:
            self.do_download(urn)
        return True

    def check(self):
        recipes = self.recipe_model.get_to_be_downloaded_recipes()
        for urn in recipes:
            if not self.download(urn):
                # No internet connection, we will try again in a minute
                break
Exemplo n.º 49
0
class ChooseLibraryAction(InterfaceAction):

    name = 'Choose Library'
    action_spec = (_('Choose Library'), 'lt.png',
                   _('Choose calibre library to work with'), None)
    dont_add_to = frozenset(['context-menu-device'])
    action_add_menu = True
    action_menu_clone_qaction = _('Switch/create library...')
    restore_view_state = pyqtSignal(object)

    def genesis(self):
        self.count_changed(0)
        self.action_choose = self.menuless_qaction

        self.stats = LibraryUsageStats()
        self.popup_type = (QToolButton.InstantPopup
                           if len(self.stats.stats) > 1 else
                           QToolButton.MenuButtonPopup)
        if len(self.stats.stats) > 1:
            self.action_choose.triggered.connect(self.choose_library)
        else:
            self.qaction.triggered.connect(self.choose_library)

        self.choose_menu = self.qaction.menu()

        ac = self.create_action(spec=(_('Pick a random book'), 'random.png',
                                      None, None),
                                attr='action_pick_random')
        ac.triggered.connect(self.pick_random)

        if not os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
            self.choose_menu.addAction(self.action_choose)

            self.quick_menu = QMenu(_('Quick switch'))
            self.quick_menu_action = self.choose_menu.addMenu(self.quick_menu)
            self.rename_menu = QMenu(_('Rename library'))
            self.rename_menu_action = self.choose_menu.addMenu(
                self.rename_menu)
            self.choose_menu.addAction(ac)
            self.delete_menu = QMenu(_('Remove library'))
            self.delete_menu_action = self.choose_menu.addMenu(
                self.delete_menu)
        else:
            self.choose_menu.addAction(ac)

        self.rename_separator = self.choose_menu.addSeparator()

        self.switch_actions = []
        for i in range(5):
            ac = self.create_action(spec=('', None, None, None),
                                    attr='switch_action%d' % i)
            self.switch_actions.append(ac)
            ac.setVisible(False)
            ac.triggered.connect(partial(self.qs_requested, i),
                                 type=Qt.QueuedConnection)
            self.choose_menu.addAction(ac)

        self.rename_separator = self.choose_menu.addSeparator()

        self.maintenance_menu = QMenu(_('Library Maintenance'))
        ac = self.create_action(spec=(_('Library metadata backup status'),
                                      'lt.png', None, None),
                                attr='action_backup_status')
        ac.triggered.connect(self.backup_status, type=Qt.QueuedConnection)
        self.maintenance_menu.addAction(ac)
        ac = self.create_action(spec=(_('Check library'), 'lt.png', None,
                                      None),
                                attr='action_check_library')
        ac.triggered.connect(self.check_library, type=Qt.QueuedConnection)
        self.maintenance_menu.addAction(ac)
        ac = self.create_action(spec=(_('Restore database'), 'lt.png', None,
                                      None),
                                attr='action_restore_database')
        ac.triggered.connect(self.restore_database, type=Qt.QueuedConnection)
        self.maintenance_menu.addAction(ac)

        self.choose_menu.addMenu(self.maintenance_menu)
        self.view_state_map = {}
        self.restore_view_state.connect(self._restore_view_state,
                                        type=Qt.QueuedConnection)

    @property
    def preserve_state_on_switch(self):
        ans = getattr(self, '_preserve_state_on_switch', None)
        if ans is None:
            self._preserve_state_on_switch = ans = \
                self.gui.library_view.preserve_state(require_selected_ids=False)
        return ans

    def pick_random(self, *args):
        self.gui.iactions['Pick Random Book'].pick_random()

    def library_name(self):
        db = self.gui.library_view.model().db
        path = db.library_path
        if isbytestring(path):
            path = path.decode(filesystem_encoding)
        path = path.replace(os.sep, '/')
        return self.stats.pretty(path)

    def update_tooltip(self, count):
        tooltip = self.action_spec[2] + '\n\n' + ngettext(
            '{0} [{1} book]', '{0} [{1} books]', count).format(
                getattr(self, 'last_lname', ''), count)
        a = self.qaction
        a.setToolTip(tooltip)
        a.setStatusTip(tooltip)
        a.setWhatsThis(tooltip)

    def library_changed(self, db):
        lname = self.stats.library_used(db)
        self.last_lname = lname
        if len(lname) > 16:
            lname = lname[:16] + u'…'
        a = self.qaction
        a.setText(lname.replace(
            '&', '&&&'))  # I have no idea why this requires a triple ampersand
        self.update_tooltip(db.count())
        self.build_menus()
        state = self.view_state_map.get(
            self.stats.canonicalize_path(db.library_path), None)
        if state is not None:
            self.restore_view_state.emit(state)

    def _restore_view_state(self, state):
        self.preserve_state_on_switch.state = state

    def initialization_complete(self):
        self.library_changed(self.gui.library_view.model().db)

    def build_menus(self):
        if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
            return
        db = self.gui.library_view.model().db
        locations = list(self.stats.locations(db))

        for ac in self.switch_actions:
            ac.setVisible(False)
        self.quick_menu.clear()
        self.qs_locations = [i[1] for i in locations]
        self.rename_menu.clear()
        self.delete_menu.clear()
        quick_actions, rename_actions, delete_actions = [], [], []
        for name, loc in locations:
            name = name.replace('&', '&&')
            ac = self.quick_menu.addAction(
                name, Dispatcher(partial(self.switch_requested, loc)))
            ac.setStatusTip(_('Switch to: %s') % loc)
            quick_actions.append(ac)
            ac = self.rename_menu.addAction(
                name, Dispatcher(partial(self.rename_requested, name, loc)))
            rename_actions.append(ac)
            ac.setStatusTip(_('Rename: %s') % loc)
            ac = self.delete_menu.addAction(
                name, Dispatcher(partial(self.delete_requested, name, loc)))
            delete_actions.append(ac)
            ac.setStatusTip(_('Remove: %s') % loc)

        qs_actions = []
        locations_by_frequency = locations
        if len(locations) >= tweaks['many_libraries']:
            locations_by_frequency = list(
                self.stats.locations(db, limit=sys.maxsize))
        for i, x in enumerate(
                locations_by_frequency[:len(self.switch_actions)]):
            name, loc = x
            name = name.replace('&', '&&')
            ac = self.switch_actions[i]
            ac.setText(name)
            ac.setStatusTip(_('Switch to: %s') % loc)
            ac.setVisible(True)
            qs_actions.append(ac)

        self.quick_menu_action.setVisible(bool(locations))
        self.rename_menu_action.setVisible(bool(locations))
        self.delete_menu_action.setVisible(bool(locations))
        self.gui.location_manager.set_switch_actions(quick_actions,
                                                     rename_actions,
                                                     delete_actions,
                                                     qs_actions,
                                                     self.action_choose)
        # Allow the cloned actions in the OS X global menubar to update
        for a in (self.qaction, self.menuless_qaction):
            a.changed.emit()

    def location_selected(self, loc):
        enabled = loc == 'library'
        self.qaction.setEnabled(enabled)

    def rename_requested(self, name, location):
        LibraryDatabase = db_class()
        loc = location.replace('/', os.sep)
        base = os.path.dirname(loc)
        old_name = name.replace('&&', '&')
        newname, ok = QInputDialog.getText(
            self.gui,
            _('Rename') + ' ' + old_name,
            '<p>' + _('Choose a new name for the library <b>%s</b>. ') % name +
            '<p>' + _('Note that the actual library folder will be renamed.'),
            text=old_name)
        newname = sanitize_file_name_unicode(unicode(newname))
        if not ok or not newname or newname == old_name:
            return
        newloc = os.path.join(base, newname)
        if os.path.exists(newloc):
            return error_dialog(
                self.gui,
                _('Already exists'),
                _('The folder %s already exists. Delete it first.') % newloc,
                show=True)
        if (iswindows
                and len(newloc) > LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT):
            return error_dialog(self.gui,
                                _('Too long'),
                                _('Path to library too long. Must be less than'
                                  ' %d characters.') %
                                LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT,
                                show=True)
        if not os.path.exists(loc):
            error_dialog(
                self.gui,
                _('Not found'),
                _('Cannot rename as no library was found at %s. '
                  'Try switching to this library first, then switch back '
                  'and retry the renaming.') % loc,
                show=True)
            return
        try:
            os.rename(loc, newloc)
        except:
            import traceback
            det_msg = 'Location: %r New Location: %r\n%s' % (
                loc, newloc, traceback.format_exc())
            error_dialog(
                self.gui,
                _('Rename failed'),
                _('Failed to rename the library at %s. '
                  'The most common cause for this is if one of the files'
                  ' in the library is open in another program.') % loc,
                det_msg=det_msg,
                show=True)
            return
        self.stats.rename(location, newloc)
        self.build_menus()
        self.gui.iactions['Copy To Library'].build_menus()

    def delete_requested(self, name, location):
        loc = location.replace('/', os.sep)
        if not question_dialog(
                self.gui,
                _('Library removed'),
                _('The library %s has been removed from calibre. '
                  'The files remain on your computer, if you want '
                  'to delete them, you will have to do so manually.') %
            ('<code>%s</code>' % loc),
                override_icon='dialog_information.png',
                yes_text=_('&OK'),
                no_text=_('&Undo'),
                yes_icon='ok.png',
                no_icon='edit-undo.png'):
            return
        self.stats.remove(location)
        self.build_menus()
        self.gui.iactions['Copy To Library'].build_menus()
        if os.path.exists(loc):
            open_local_file(loc)

    def backup_status(self, location):
        self.__backup_status_dialog = d = BackupStatus(self.gui)
        d.show()

    def mark_dirty(self):
        db = self.gui.library_view.model().db
        db.dirtied(list(db.data.iterallids()))
        info_dialog(
            self.gui,
            _('Backup metadata'),
            _('Metadata will be backed up while calibre is running, at the '
              'rate of approximately 1 book every three seconds.'),
            show=True)

    def restore_database(self):
        LibraryDatabase = db_class()
        m = self.gui.library_view.model()
        db = m.db
        if (iswindows and len(db.library_path) >
                LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT):
            return error_dialog(
                self.gui,
                _('Too long'),
                _('Path to library too long. Must be less than'
                  ' %d characters. Move your library to a location with'
                  ' a shorter path using Windows Explorer, then point'
                  ' calibre to the new location and try again.') %
                LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT,
                show=True)

        from calibre.gui2.dialogs.restore_library import restore_database
        m = self.gui.library_view.model()
        m.stop_metadata_backup()
        db = m.db
        db.prefs.disable_setting = True
        if restore_database(db, self.gui):
            self.gui.library_moved(db.library_path, call_close=False)

    def check_library(self):
        from calibre.gui2.dialogs.check_library import CheckLibraryDialog, DBCheck
        self.gui.library_view.save_state()
        m = self.gui.library_view.model()
        m.stop_metadata_backup()
        db = m.db
        db.prefs.disable_setting = True

        d = DBCheck(self.gui, db)
        d.start()
        try:
            d.conn.close()
        except:
            pass
        d.break_cycles()
        self.gui.library_moved(db.library_path,
                               call_close=not d.closed_orig_conn)
        if d.rejected:
            return
        if d.error is None:
            if not question_dialog(
                    self.gui, _('Success'),
                    _('Found no errors in your calibre library database.'
                      ' Do you want calibre to check if the files in your '
                      ' library match the information in the database?')):
                return
        else:
            return error_dialog(
                self.gui,
                _('Failed'),
                _('Database integrity check failed, click Show details'
                  ' for details.'),
                show=True,
                det_msg=d.error[1])

        self.gui.status_bar.show_message(
            _('Starting library scan, this may take a while'))
        try:
            QCoreApplication.processEvents()
            d = CheckLibraryDialog(self.gui, m.db)

            if not d.do_exec():
                info_dialog(
                    self.gui,
                    _('No problems found'),
                    _('The files in your library match the information '
                      'in the database.'),
                    show=True)
        finally:
            self.gui.status_bar.clear_message()

    def look_for_portable_lib(self, db, location):
        base = get_portable_base()
        if base is None:
            return False, None
        loc = location.replace('/', os.sep)
        candidate = os.path.join(base, os.path.basename(loc))
        if db.exists_at(candidate):
            newloc = candidate.replace(os.sep, '/')
            self.stats.rename(location, newloc)
            return True, newloc
        return False, None

    def switch_requested(self, location):
        if not self.change_library_allowed():
            return
        db = self.gui.library_view.model().db
        current_lib = self.stats.canonicalize_path(db.library_path)
        self.view_state_map[current_lib] = self.preserve_state_on_switch.state
        loc = location.replace('/', os.sep)
        exists = db.exists_at(loc)
        if not exists:
            exists, new_location = self.look_for_portable_lib(db, location)
            if exists:
                location = new_location
                loc = location.replace('/', os.sep)

        if not exists:
            d = MovedDialog(self.stats, location, self.gui)
            ret = d.exec_()
            self.build_menus()
            self.gui.iactions['Copy To Library'].build_menus()
            if ret == d.Accepted:
                loc = d.newloc.replace('/', os.sep)
            else:
                return

        # from calibre.utils.mem import memory
        # import weakref
        # from PyQt5.Qt import QTimer
        # self.dbref = weakref.ref(self.gui.library_view.model().db)
        # self.before_mem = memory()
        self.gui.library_moved(loc, allow_rebuild=True)
        # QTimer.singleShot(5000, self.debug_leak)

    def debug_leak(self):
        import gc
        from calibre.utils.mem import memory
        ref = self.dbref
        for i in xrange(3):
            gc.collect()
        if ref() is not None:
            print 'DB object alive:', ref()
            for r in gc.get_referrers(ref())[:10]:
                print r
                print
        print 'before:', self.before_mem
        print 'after:', memory()
        print
        self.dbref = self.before_mem = None

    def qs_requested(self, idx, *args):
        self.switch_requested(self.qs_locations[idx])

    def count_changed(self, new_count):
        self.update_tooltip(new_count)

    def choose_library(self, *args):
        if not self.change_library_allowed():
            return
        from calibre.gui2.dialogs.choose_library import ChooseLibrary
        self.gui.library_view.save_state()
        db = self.gui.library_view.model().db
        location = self.stats.canonicalize_path(db.library_path)
        self.pre_choose_dialog_location = location
        c = ChooseLibrary(db, self.choose_library_callback, self.gui)
        c.exec_()
        self.choose_dialog_library_renamed = getattr(c, 'library_renamed',
                                                     False)

    def choose_library_callback(self, newloc, copy_structure=False):
        self.gui.library_moved(newloc,
                               copy_structure=copy_structure,
                               allow_rebuild=True)
        if getattr(self, 'choose_dialog_library_renamed', False):
            self.stats.rename(self.pre_choose_dialog_location,
                              prefs['library_path'])
        self.build_menus()
        self.gui.iactions['Copy To Library'].build_menus()

    def change_library_allowed(self):
        if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
            warning_dialog(
                self.gui,
                _('Not allowed'),
                _('You cannot change libraries while using the environment'
                  ' variable CALIBRE_OVERRIDE_DATABASE_PATH.'),
                show=True)
            return False
        if self.gui.job_manager.has_jobs():
            warning_dialog(self.gui,
                           _('Not allowed'),
                           _('You cannot change libraries while jobs'
                             ' are running.'),
                           show=True)
            return False

        if self.gui.proceed_question.questions:
            warning_dialog(self.gui,
                           _('Not allowed'),
                           _('You cannot change libraries until all'
                             ' updates are accepted or rejected.'),
                           show=True)
            return False

        return True
Exemplo n.º 50
0
def details_context_menu_event(view, ev, book_info):  # {{{
    p = view.page()
    mf = p.mainFrame()
    r = mf.hitTestContent(ev.pos())
    url = unicode(r.linkUrl().toString(QUrl.None)).strip()
    menu = p.createStandardContextMenu()
    ca = view.pageAction(p.Copy)
    for action in list(menu.actions()):
        if action is not ca:
            menu.removeAction(action)
    if not r.isNull():
        if url.startswith('format:'):
            parts = url.split(':')
            try:
                book_id, fmt = int(parts[1]), parts[2].upper()
            except:
                import traceback
                traceback.print_exc()
            else:
                from calibre.gui2.ui import get_gui
                from calibre.ebooks.oeb.polish.main import SUPPORTED
                db = get_gui().current_db.new_api
                ofmt = fmt.upper() if fmt.startswith('ORIGINAL_') else 'ORIGINAL_' + fmt
                nfmt = ofmt[len('ORIGINAL_'):]
                fmts = {x.upper() for x in db.formats(book_id)}
                for a, t in [('remove', _('Delete the %s format')),
                    ('save', _('Save the %s format to disk')),
                    ('restore', _('Restore the %s format')),
                    ('compare', ''),
                ]:
                    if a == 'restore' and not fmt.startswith('ORIGINAL_'):
                        continue
                    if a == 'compare':
                        if ofmt not in fmts or nfmt not in SUPPORTED:
                            continue
                        t = _('Compare to the %s format') % (fmt[9:] if fmt.startswith('ORIGINAL_') else ofmt)
                    else:
                        t = t % fmt
                    ac = getattr(book_info, '%s_format_action'%a)
                    ac.current_fmt = (book_id, fmt)
                    ac.setText(t)
                    menu.addAction(ac)
                if not fmt.upper().startswith('ORIGINAL_'):
                    from calibre.gui2.open_with import populate_menu, edit_programs
                    m = QMenu(_('Open %s with...') % fmt.upper())
                    populate_menu(m, partial(book_info.open_with, book_id, fmt), fmt)
                    if len(m.actions()) == 0:
                        menu.addAction(_('Open %s with...') % fmt.upper(), partial(book_info.choose_open_with, book_id, fmt))
                    else:
                        m.addSeparator()
                        m.addAction(_('Add other application for %s files...') % fmt.upper(), partial(book_info.choose_open_with, book_id, fmt))
                        m.addAction(_('Edit Open With applications...'), partial(edit_programs, fmt, book_info))
                        menu.addMenu(m)
                ac = book_info.copy_link_action
                ac.current_url = r.linkElement().attribute('data-full-path')
                if ac.current_url:
                    ac.setText(_('&Copy path to file'))
                    menu.addAction(ac)
        else:
            el = r.linkElement()
            data = el.attribute('data-item')
            author = el.toPlainText() if unicode(el.attribute('calibre-data')) == u'authors' else None
            if not url.startswith('search:'):
                for a, t in [('copy', _('&Copy Link')),
                ]:
                    ac = getattr(book_info, '%s_link_action'%a)
                    ac.current_url = url
                    if url.startswith('path:'):
                        ac.current_url = el.attribute('title')
                    ac.setText(t)
                    menu.addAction(ac)
            if author is not None:
                ac = book_info.manage_author_action
                ac.current_fmt = author
                ac.setText(_('Manage %s') % author)
                menu.addAction(ac)
            if data:
                try:
                    field, value, book_id = cPickle.loads(unhexlify(data))
                except Exception:
                    field = value = book_id = None
                if field:
                    ac = book_info.remove_item_action
                    ac.data = (field, value, book_id)
                    ac.setText(_('Remove %s from this book') % value)
                    menu.addAction(ac)

    if len(menu.actions()) > 0:
        menu.exec_(ev.globalPos())
Exemplo n.º 51
0
class ShareConnMenu(QMenu):  # {{{

    connect_to_folder = pyqtSignal()
    connect_to_itunes = pyqtSignal()
    connect_to_bambook = pyqtSignal()

    config_email = pyqtSignal()
    toggle_server = pyqtSignal()
    control_smartdevice = pyqtSignal()
    dont_add_to = frozenset(['context-menu-device'])

    DEVICE_MSGS = [
        _('Start wireless device connection'),
        _('Stop wireless device connection')
    ]

    def __init__(self, parent=None):
        QMenu.__init__(self, parent)
        mitem = self.addAction(QIcon(I('devices/folder.png')),
                               _('Connect to folder'))
        mitem.setEnabled(True)
        mitem.triggered.connect(lambda x: self.connect_to_folder.emit())
        self.connect_to_folder_action = mitem
        mitem = self.addAction(QIcon(I('devices/itunes.png')),
                               _('Connect to iTunes'))
        mitem.setEnabled(True)
        mitem.triggered.connect(lambda x: self.connect_to_itunes.emit())
        self.connect_to_itunes_action = mitem
        itunes_ok = iswindows or (isosx and get_osx_version() < (10, 9, 0))
        mitem.setVisible(itunes_ok)
        mitem = self.addAction(QIcon(I('devices/bambook.png')),
                               _('Connect to Bambook'))
        mitem.setEnabled(True)
        mitem.triggered.connect(lambda x: self.connect_to_bambook.emit())
        self.connect_to_bambook_action = mitem
        bambook_visible = False
        if not is_disabled(BAMBOOK):
            device_ip = BAMBOOK.settings().extra_customization
            if device_ip:
                bambook_visible = True
        self.connect_to_bambook_action.setVisible(bambook_visible)

        self.addSeparator()
        self.toggle_server_action = \
            self.addAction(QIcon(I('network-server.png')),
            _('Start Content Server'))
        self.toggle_server_action.triggered.connect(
            lambda x: self.toggle_server.emit())
        self.control_smartdevice_action = \
            self.addAction(QIcon(I('dot_red.png')),
            self.DEVICE_MSGS[0])
        self.control_smartdevice_action.triggered.connect(
            lambda x: self.control_smartdevice.emit())
        self.addSeparator()

        self.email_actions = []

        if hasattr(parent, 'keyboard'):
            r = parent.keyboard.register_shortcut
            prefix = 'Share/Connect Menu '
            gr = ConnectShareAction.action_spec[0]
            for attr in ('folder', 'bambook', 'itunes'):
                if not (iswindows or isosx) and attr == 'itunes':
                    continue
                ac = getattr(self, 'connect_to_%s_action' % attr)
                r(prefix + attr, unicode(ac.text()), action=ac, group=gr)
            r(prefix + ' content server',
              _('Start/stop content server'),
              action=self.toggle_server_action,
              group=gr)

    def server_state_changed(self, running):
        from calibre.utils.mdns import get_external_ip, verify_ipV4_address
        text = _('Start Content Server')
        if running:
            listen_on = (verify_ipV4_address(tweaks['server_listen_on'])
                         or get_external_ip())
            try:
                cs_port = content_server_config().parse().port
                ip_text = _(' [%(ip)s, port %(port)d]') % dict(ip=listen_on,
                                                               port=cs_port)
            except:
                ip_text = ' [%s]' % listen_on
            text = _('Stop Content Server') + ip_text
        self.toggle_server_action.setText(text)

    def hide_smartdevice_menus(self):
        self.control_smartdevice_action.setVisible(False)

    def build_email_entries(self, sync_menu):
        from calibre.gui2.device import DeviceAction
        for ac in self.email_actions:
            self.removeAction(ac)
        self.email_actions = []
        self.memory = []
        opts = email_config().parse()
        if opts.accounts:
            self.email_to_menu = QMenu(_('Email to') + '...', self)
            ac = self.addMenu(self.email_to_menu)
            self.email_actions.append(ac)
            self.email_to_and_delete_menu = QMenu(
                _('Email to and delete from library') + '...', self)
            keys = sorted(opts.accounts.keys())
            for account in keys:
                formats, auto, default = opts.accounts[account]
                subject = opts.subjects.get(account, '')
                alias = opts.aliases.get(account, '')
                dest = 'mail:' + account + ';' + formats + ';' + subject
                action1 = DeviceAction(dest, False, False, I('mail.png'), alias
                                       or account)
                action2 = DeviceAction(dest, True, False, I('mail.png'),
                                       (alias or account) + ' ' +
                                       _('(delete from library)'))
                self.email_to_menu.addAction(action1)
                self.email_to_and_delete_menu.addAction(action2)
                map(self.memory.append, (action1, action2))
                if default:
                    ac = DeviceAction(dest, False, False, I('mail.png'),
                                      _('Email to') + ' ' + (alias or account))
                    self.addAction(ac)
                    self.email_actions.append(ac)
                    ac.a_s.connect(sync_menu.action_triggered)
                action1.a_s.connect(sync_menu.action_triggered)
                action2.a_s.connect(sync_menu.action_triggered)
            action1 = DeviceAction('choosemail:', False, False, I('mail.png'),
                                   _('Select recipients'))
            action2 = DeviceAction(
                'choosemail:', True, False, I('mail.png'),
                _('Select recipients') + ' ' + _('(delete from library)'))
            self.email_to_menu.addAction(action1)
            self.email_to_and_delete_menu.addAction(action2)
            map(self.memory.append, (action1, action2))
            tac1 = DeviceAction('choosemail:', False, False, I('mail.png'),
                                _('Email to selected recipients...'))
            self.addAction(tac1)
            tac1.a_s.connect(sync_menu.action_triggered)
            self.memory.append(tac1)
            ac = self.addMenu(self.email_to_and_delete_menu)
            self.email_actions.append(ac)
            action1.a_s.connect(sync_menu.action_triggered)
            action2.a_s.connect(sync_menu.action_triggered)
        else:
            ac = self.addAction(_('Setup email based sharing of books'))
            self.email_actions.append(ac)
            ac.triggered.connect(self.setup_email)

    def setup_email(self, *args):
        self.config_email.emit()

    def set_state(self, device_connected, device):
        self.connect_to_folder_action.setEnabled(not device_connected)
        self.connect_to_itunes_action.setEnabled(not device_connected)
        self.connect_to_bambook_action.setEnabled(not device_connected)
Exemplo n.º 52
0
    def show_context_menu(self, point):
        item = self.currentItem()

        def key(k):
            sc = unicode(
                QKeySequence(k | Qt.CTRL).toString(QKeySequence.NativeText))
            return ' [%s]' % sc

        if item is not None:
            m = QMenu()
            ci = unicode(item.data(0, Qt.DisplayRole) or '')
            p = item.parent() or self.invisibleRootItem()
            idx = p.indexOfChild(item)
            if idx > 0:
                m.addAction(QIcon(I('arrow-up.png')),
                            (_('Move "%s" up') % ci) + key(Qt.Key_Up),
                            self.move_up)
            if idx + 1 < p.childCount():
                m.addAction(QIcon(I('arrow-down.png')),
                            (_('Move "%s" down') % ci) + key(Qt.Key_Down),
                            self.move_down)
            m.addAction(QIcon(I('trash.png')), _('Remove all selected items'),
                        self.del_items)
            if item.parent() is not None:
                m.addAction(QIcon(I('back.png')),
                            (_('Unindent "%s"') % ci) + key(Qt.Key_Left),
                            self.move_left)
            if idx > 0:
                m.addAction(QIcon(I('forward.png')),
                            (_('Indent "%s"') % ci) + key(Qt.Key_Right),
                            self.move_right)
            m.addAction(QIcon(I('edit_input.png')),
                        _('Change the location this entry points to'),
                        self.edit_item)
            m.addAction(_('Change all selected items to title case'),
                        self.title_case)
            m.addAction(_('Change all selected items to upper case'),
                        self.upper_case)
            m.addAction(QIcon(I('modified.png')),
                        _('Bulk rename all selected items'), self.bulk_rename)
            m.exec_(QCursor.pos())
Exemplo n.º 53
0
def add_format_entries(menu, data, book_info):
    from calibre.ebooks.oeb.polish.main import SUPPORTED
    from calibre.gui2.ui import get_gui
    book_id = int(data['book_id'])
    fmt = data['fmt']
    init_find_in_tag_browser(menu, book_info.find_in_tag_browser_action,
                             'formats', fmt)
    db = get_gui().current_db.new_api
    ofmt = fmt.upper() if fmt.startswith('ORIGINAL_') else 'ORIGINAL_' + fmt
    nfmt = ofmt[len('ORIGINAL_'):]
    fmts = {x.upper() for x in db.formats(book_id)}
    for a, t in [
        ('remove', _('Delete the %s format')),
        ('save', _('Save the %s format to disk')),
        ('restore', _('Restore the %s format')),
        ('compare', ''),
        ('set_cover', _('Set the book cover from the %s file')),
    ]:
        if a == 'restore' and not fmt.startswith('ORIGINAL_'):
            continue
        if a == 'compare':
            if ofmt not in fmts or nfmt not in SUPPORTED:
                continue
            t = _('Compare to the %s format') % (
                fmt[9:] if fmt.startswith('ORIGINAL_') else ofmt)
        else:
            t = t % fmt
        ac = getattr(book_info, '%s_format_action' % a)
        ac.current_fmt = (book_id, fmt)
        ac.setText(t)
        menu.addAction(ac)
    if not fmt.upper().startswith('ORIGINAL_'):
        from calibre.gui2.open_with import edit_programs, populate_menu
        m = QMenu(_('Open %s with...') % fmt.upper())

        def connect_action(ac, entry):
            connect_lambda(
                ac.triggered, book_info,
                lambda book_info: book_info.open_with(book_id, fmt, entry))

        populate_menu(m, connect_action, fmt)
        if len(m.actions()) == 0:
            menu.addAction(
                _('Open %s with...') % fmt.upper(),
                partial(book_info.choose_open_with, book_id, fmt))
        else:
            m.addSeparator()
            m.addAction(
                _('Add other application for %s files...') % fmt.upper(),
                partial(book_info.choose_open_with, book_id, fmt))
            m.addAction(_('Edit Open with applications...'),
                        partial(edit_programs, fmt, book_info))
            menu.addMenu(m)
            menu.ow = m
        if fmt.upper() in SUPPORTED:
            menu.addSeparator()
            menu.addAction(
                _('Edit %s...') % fmt.upper(),
                partial(book_info.edit_fmt, book_id, fmt))
    path = data['path']
    if path:
        if data.get('fname'):
            path = os.path.join(path,
                                data['fname'] + '.' + data['fmt'].lower())
        ac = book_info.copy_link_action
        ac.current_url = path
        ac.setText(_('&Copy path to file'))
        menu.addAction(ac)
Exemplo n.º 54
0
    def show_context_menu(self, point):
        item = self.itemAt(point)
        if item is None or item in tuple(itervalues(self.categories)):
            return
        m = QMenu(self)
        sel = self.selectedItems()
        num = len(sel)
        container = current_container()
        ci = self.currentItem()
        if ci is not None:
            cn = unicode_type(ci.data(0, NAME_ROLE) or '')
            mt = unicode_type(ci.data(0, MIME_ROLE) or '')
            cat = unicode_type(ci.data(0, CATEGORY_ROLE) or '')
            n = elided_text(cn.rpartition('/')[-1])
            m.addAction(QIcon(I('save.png')), _('Export %s') % n, partial(self.export, cn))
            if cn not in container.names_that_must_not_be_changed and cn not in container.names_that_must_not_be_removed and mt not in OEB_FONTS:
                m.addAction(_('Replace %s with file...') % n, partial(self.replace, cn))
            if num > 1:
                m.addAction(QIcon(I('save.png')), _('Export all %d selected files') % num, self.export_selected)
            if cn not in container.names_that_must_not_be_changed:
                self.add_open_with_actions(m, cn)

            m.addSeparator()

            m.addAction(QIcon(I('modified.png')), _('&Rename %s') % n, self.edit_current_item)
            if is_raster_image(mt):
                m.addAction(QIcon(I('default_cover.png')), _('Mark %s as cover image') % n, partial(self.mark_as_cover, cn))
            elif current_container().SUPPORTS_TITLEPAGES and mt in OEB_DOCS and cat == 'text':
                m.addAction(QIcon(I('default_cover.png')), _('Mark %s as cover page') % n, partial(self.mark_as_titlepage, cn))
            m.addSeparator()

        if num > 0:
            m.addSeparator()
            if num > 1:
                m.addAction(QIcon(I('modified.png')), _('&Bulk rename the selected files'), self.request_bulk_rename)
            m.addAction(QIcon(I('modified.png')), _('Change the file extension for the selected files'), self.request_change_ext)
            m.addAction(QIcon(I('trash.png')), ngettext(
                '&Delete the selected file', '&Delete the {} selected files', num).format(num), self.request_delete)
            m.addAction(QIcon(I('edit-copy.png')), ngettext(
                '&Copy the selected file to another editor instance',
                '&Copy the {} selected files to another editor instance', num).format(num), self.copy_selected_files)
            m.addSeparator()
        md = QApplication.instance().clipboard().mimeData()
        if md.hasUrls() and md.hasFormat(FILE_COPY_MIME):
            m.addAction(_('Paste files from other editor instance'), self.paste_from_other_instance)

        selected_map = defaultdict(list)
        for item in sel:
            selected_map[unicode_type(item.data(0, CATEGORY_ROLE) or '')].append(unicode_type(item.data(0, NAME_ROLE) or ''))

        for items in itervalues(selected_map):
            items.sort(key=self.index_of_name)

        if selected_map['text']:
            m.addAction(QIcon(I('format-text-color.png')), _('Link &stylesheets...'), partial(self.link_stylesheets, selected_map['text']))

        if len(selected_map['text']) > 1:
            m.addAction(QIcon(I('merge.png')), _('&Merge selected text files'), partial(self.start_merge, 'text', selected_map['text']))
        if len(selected_map['styles']) > 1:
            m.addAction(QIcon(I('merge.png')), _('&Merge selected style files'), partial(self.start_merge, 'styles', selected_map['styles']))

        if len(list(m.actions())) > 0:
            m.popup(self.mapToGlobal(point))
Exemplo n.º 55
0
class ShareConnMenu(QMenu):  # {{{

    connect_to_folder = pyqtSignal()

    config_email = pyqtSignal()
    toggle_server = pyqtSignal()
    control_smartdevice = pyqtSignal()
    server_state_changed_signal = pyqtSignal(object, object)
    dont_add_to = frozenset(('context-menu-device', ))

    DEVICE_MSGS = [
        _('Start wireless device connection'),
        _('Stop wireless device connection')
    ]

    def __init__(self, parent=None):
        QMenu.__init__(self, parent)
        self.ip_text = ''
        mitem = self.addAction(QIcon(I('devices/folder.png')),
                               _('Connect to folder'))
        mitem.setEnabled(True)
        connect_lambda(mitem.triggered, self,
                       lambda self: self.connect_to_folder.emit())
        self.connect_to_folder_action = mitem

        self.addSeparator()
        self.toggle_server_action = \
            self.addAction(QIcon(I('network-server.png')),
            _('Start Content server'))
        connect_lambda(self.toggle_server_action.triggered, self,
                       lambda self: self.toggle_server.emit())
        self.control_smartdevice_action = \
            self.addAction(QIcon(I('dot_red.png')),
            self.DEVICE_MSGS[0])
        connect_lambda(self.control_smartdevice_action.triggered, self,
                       lambda self: self.control_smartdevice.emit())
        self.addSeparator()

        self.email_actions = []

        if hasattr(parent, 'keyboard'):
            r = parent.keyboard.register_shortcut
            prefix = 'Share/Connect Menu '
            gr = ConnectShareAction.action_spec[0]
            for attr in ('folder', ):
                ac = getattr(self, 'connect_to_%s_action' % attr)
                r(prefix + attr, unicode_type(ac.text()), action=ac, group=gr)
            r(prefix + ' content server',
              _('Start/stop Content server'),
              action=self.toggle_server_action,
              group=gr)

    def server_state_changed(self, running):
        from calibre.utils.mdns import get_external_ip, verify_ipV4_address
        text = _('Start Content server')
        if running:
            from calibre.srv.opts import server_config
            opts = server_config()
            listen_on = verify_ipV4_address(
                opts.listen_on) or get_external_ip()
            protocol = 'HTTPS' if opts.ssl_certfile and opts.ssl_keyfile else 'HTTP'
            try:
                ip_text = ' ' + _('[{ip}, port {port}, {protocol}]').format(
                    ip=listen_on, port=opts.port, protocol=protocol)
            except Exception:
                ip_text = ' [{} {}]'.format(listen_on, protocol)
            self.ip_text = ip_text
            self.server_state_changed_signal.emit(running, ip_text)
            text = _('Stop Content server') + ip_text
        else:
            self.ip_text = ''
        self.toggle_server_action.setText(text)

    def hide_smartdevice_menus(self):
        self.control_smartdevice_action.setVisible(False)

    def build_email_entries(self, sync_menu):
        from calibre.gui2.device import DeviceAction
        for ac in self.email_actions:
            self.removeAction(ac)
        self.email_actions = []
        self.memory = []
        opts = email_config().parse()
        if opts.accounts:
            self.email_to_menu = QMenu(_('Email to') + '...', self)
            ac = self.addMenu(self.email_to_menu)
            self.email_actions.append(ac)
            self.email_to_and_delete_menu = QMenu(
                _('Email to and delete from library') + '...', self)
            keys = sorted(opts.accounts.keys())

            def sk(account):
                return primary_sort_key(opts.aliases.get(account) or account)

            for account in sorted(keys, key=sk):
                formats, auto, default = opts.accounts[account]
                subject = opts.subjects.get(account, '')
                alias = opts.aliases.get(account, '')
                dest = 'mail:' + account + ';' + formats + ';' + subject
                action1 = DeviceAction(dest, False, False, I('mail.png'), alias
                                       or account)
                action2 = DeviceAction(dest, True, False, I('mail.png'),
                                       (alias or account) + ' ' +
                                       _('(delete from library)'))
                self.email_to_menu.addAction(action1)
                self.email_to_and_delete_menu.addAction(action2)
                self.memory.append(action1)
                self.memory.append(action2)
                if default:
                    ac = DeviceAction(dest, False, False, I('mail.png'),
                                      _('Email to') + ' ' + (alias or account))
                    self.addAction(ac)
                    self.email_actions.append(ac)
                    ac.a_s.connect(sync_menu.action_triggered)
                action1.a_s.connect(sync_menu.action_triggered)
                action2.a_s.connect(sync_menu.action_triggered)
            action1 = DeviceAction('choosemail:', False, False, I('mail.png'),
                                   _('Select recipients'))
            action2 = DeviceAction(
                'choosemail:', True, False, I('mail.png'),
                _('Select recipients') + ' ' + _('(delete from library)'))
            self.email_to_menu.addAction(action1)
            self.email_to_and_delete_menu.addAction(action2)
            self.memory.append(action1)
            self.memory.append(action2)
            tac1 = DeviceAction('choosemail:', False, False, I('mail.png'),
                                _('Email to selected recipients...'))
            self.addAction(tac1)
            tac1.a_s.connect(sync_menu.action_triggered)
            self.memory.append(tac1)
            self.email_actions.append(tac1)
            ac = self.addMenu(self.email_to_and_delete_menu)
            self.email_actions.append(ac)
            action1.a_s.connect(sync_menu.action_triggered)
            action2.a_s.connect(sync_menu.action_triggered)
        else:
            ac = self.addAction(_('Setup email based sharing of books'))
            self.email_actions.append(ac)
            ac.triggered.connect(self.setup_email)

    def setup_email(self, *args):
        self.config_email.emit()

    def set_state(self, device_connected, device):
        self.connect_to_folder_action.setEnabled(not device_connected)
Exemplo n.º 56
0
    def show_context_menu(self, point):
        item = self.itemAt(point)
        if item is None or item in set(self.categories.itervalues()):
            return
        m = QMenu(self)
        sel = self.selectedItems()
        num = len(sel)
        container = current_container()
        ci = self.currentItem()
        if ci is not None:
            cn = unicode(ci.data(0, NAME_ROLE) or '')
            mt = unicode(ci.data(0, MIME_ROLE) or '')
            cat = unicode(ci.data(0, CATEGORY_ROLE) or '')
            n = elided_text(cn.rpartition('/')[-1])
            m.addAction(QIcon(I('save.png')), _('Export %s') % n, partial(self.export, cn))
            if cn not in container.names_that_must_not_be_changed and cn not in container.names_that_must_not_be_removed and mt not in OEB_FONTS:
                m.addAction(_('Replace %s with file...') % n, partial(self.replace, cn))
            m.addSeparator()

            m.addAction(QIcon(I('modified.png')), _('&Rename %s') % n, self.edit_current_item)
            if is_raster_image(mt):
                m.addAction(QIcon(I('default_cover.png')), _('Mark %s as cover image') % n, partial(self.mark_as_cover, cn))
            elif current_container().SUPPORTS_TITLEPAGES and mt in OEB_DOCS and cat == 'text':
                m.addAction(QIcon(I('default_cover.png')), _('Mark %s as cover page') % n, partial(self.mark_as_titlepage, cn))
            m.addSeparator()

        if num > 0:
            m.addSeparator()
            if num > 1:
                m.addAction(QIcon(I('modified.png')), _('&Bulk rename selected files'), self.request_bulk_rename)
            m.addAction(QIcon(I('trash.png')), _('&Delete the %d selected file(s)') % num, self.request_delete)
            m.addSeparator()

        selected_map = defaultdict(list)
        for item in sel:
            selected_map[unicode(item.data(0, CATEGORY_ROLE) or '')].append(unicode(item.data(0, NAME_ROLE) or ''))

        for items in selected_map.itervalues():
            items.sort(key=self.index_of_name)

        if selected_map['text']:
            m.addAction(QIcon(I('format-text-color.png')), _('Link &stylesheets...'), partial(self.link_stylesheets, selected_map['text']))

        if len(selected_map['text']) > 1:
            m.addAction(QIcon(I('merge.png')), _('&Merge selected text files'), partial(self.start_merge, 'text', selected_map['text']))
        if len(selected_map['styles']) > 1:
            m.addAction(QIcon(I('merge.png')), _('&Merge selected style files'), partial(self.start_merge, 'styles', selected_map['styles']))

        if len(list(m.actions())) > 0:
            m.popup(self.mapToGlobal(point))
Exemplo n.º 57
0
    def show_context_menu(self, pos):
        m = QMenu(self)
        a = m.addAction
        c = self.editor.cursorForPosition(pos)
        origc = QTextCursor(c)
        current_cursor = self.editor.textCursor()
        r = origr = self.editor.syntax_range_for_cursor(c)
        if (
                r is None or not r.format.property(SPELL_PROPERTY)
        ) and c.positionInBlock() > 0 and not current_cursor.hasSelection():
            c.setPosition(c.position() - 1)
            r = self.editor.syntax_range_for_cursor(c)

        if r is not None and r.format.property(SPELL_PROPERTY):
            word = self.editor.text_for_range(c.block(), r)
            locale = self.editor.spellcheck_locale_for_cursor(c)
            orig_pos = c.position()
            c.setPosition(orig_pos - utf16_length(word))
            found = False
            self.editor.setTextCursor(c)
            if self.editor.find_spell_word([word],
                                           locale.langcode,
                                           center_on_cursor=False):
                found = True
                fc = self.editor.textCursor()
                if fc.position() < c.position():
                    self.editor.find_spell_word([word],
                                                locale.langcode,
                                                center_on_cursor=False)
            spell_cursor = self.editor.textCursor()
            if current_cursor.hasSelection():
                # Restore the current cursor so that any selection is preserved
                # for the change case actions
                self.editor.setTextCursor(current_cursor)
            if found:
                suggestions = dictionaries.suggestions(word, locale)[:7]
                if suggestions:
                    for suggestion in suggestions:
                        ac = m.addAction(
                            suggestion,
                            partial(self.editor.simple_replace,
                                    suggestion,
                                    cursor=spell_cursor))
                        f = ac.font()
                        f.setBold(True), ac.setFont(f)
                    m.addSeparator()
                m.addAction(actions['spell-next'])
                m.addAction(_('Ignore this word'),
                            partial(self._nuke_word, None, word, locale))
                dics = dictionaries.active_user_dictionaries
                if len(dics) > 0:
                    if len(dics) == 1:
                        m.addAction(
                            _('Add this word to the dictionary: {0}').format(
                                dics[0].name),
                            partial(self._nuke_word, dics[0].name, word,
                                    locale))
                    else:
                        ac = m.addAction(_('Add this word to the dictionary'))
                        dmenu = QMenu(m)
                        ac.setMenu(dmenu)
                        for dic in dics:
                            dmenu.addAction(
                                dic.name,
                                partial(self._nuke_word, dic.name, word,
                                        locale))
                m.addSeparator()

        if origr is not None and origr.format.property(LINK_PROPERTY):
            href = self.editor.text_for_range(origc.block(), origr)
            m.addAction(
                _('Open %s') % href, partial(self.link_clicked.emit, href))

        if origr is not None and (origr.format.property(TAG_NAME_PROPERTY)
                                  or origr.format.property(CSS_PROPERTY)):
            word = self.editor.text_for_range(origc.block(), origr)
            item_type = 'tag_name' if origr.format.property(
                TAG_NAME_PROPERTY) else 'css_property'
            url = help_url(word,
                           item_type,
                           self.editor.highlighter.doc_name,
                           extra_data=current_container().opf_version)
            if url is not None:
                m.addAction(
                    _('Show help for: %s') % word, partial(open_url, url))

        for x in ('undo', 'redo'):
            ac = actions['editor-%s' % x]
            if ac.isEnabled():
                a(ac)
        m.addSeparator()
        for x in ('cut', 'copy', 'paste'):
            ac = actions['editor-' + x]
            if ac.isEnabled():
                a(ac)
        m.addSeparator()
        m.addAction(_('&Select all'), self.editor.select_all)
        if self.selected_text or self.has_marked_text:
            update_mark_text_action(self)
            m.addAction(actions['mark-selected-text'])
        if self.syntax != 'css' and actions['editor-cut'].isEnabled():
            cm = QMenu(_('Change &case'), m)
            for ac in 'upper lower swap title capitalize'.split():
                cm.addAction(actions['transform-case-' + ac])
            m.addMenu(cm)
        if self.syntax == 'html':
            m.addAction(actions['multisplit'])
        m.exec_(self.editor.mapToGlobal(pos))
Exemplo n.º 58
0
class EKWindow(QDialog, dialog_ui.Ui_Dialog):
    def __init__(self, app):
        QDialog.__init__(self)
        self.app = app
        self.app_path = os.getenv("APPDATA") + "\\" + qApp.applicationName()
        self.registrySettings = QSettings("HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run", QSettings.NativeFormat)
        self.table_path = self.app_path + "\\tables"
        self.engine = Engine()
        self.minimize_action = QAction("Minimize", self)
        self.maximize_action = QAction("Maximize", self)
        self.settings_action = QAction("Settings", self)
        self.about_action = QAction("About", self)
        self.quit_action = QAction("Quit", self)
        self.tray_icon_menu = QMenu(self)
        self.tray_icon = QSystemTrayIcon(self)
        self.setupUi(self)
        self.icon = QIcon(QPixmap(":icon/off_logo"))
        self.construct_tray_icon()
        self.signal_connectors()
        self.database = DatabaseManager()
        self.shortcut_key = self.database.get_shortcut_key()
        self.populate_modifier_cbox()
        if self.database.get_current_state() == "True":
            self.engine.conv_state = False
        else:
            self.engine.conv_state = True
        self.icon_activated(QSystemTrayIcon.Trigger)
        self.file_path_tview.setEnabled(False)
        self.check_app_path()
        self.update_table(True)
        self.init_combobox()
        if self.registrySettings.contains(qApp.applicationName()):
            self.start_windows_check.setChecked(True)
        else:
            self.start_windows_check.setChecked(False)

    def check_app_path(self):
        if not os.path.exists(self.app_path):
            os.makedirs(self.app_path)
        if not os.path.exists(self.table_path):
            os.makedirs(self.table_path)
        return

    def construct_tray_icon(self):
        self.tray_icon.setIcon(self.icon)
        self.tray_icon_menu.addAction(self.settings_action)
        self.tray_icon_menu.addSeparator()
        self.tray_icon_menu.addAction(self.about_action)
        self.tray_icon_menu.addSeparator()
        self.tray_icon_menu.addAction(self.quit_action)
        self.tray_icon.setContextMenu(self.tray_icon_menu)
        self.tray_icon.show()

    def signal_connectors(self):
        self.tray_icon.activated.connect(self.icon_activated)
        self.settings_action.triggered.connect(self.show_setting)
        self.about_action.triggered.connect(self.show_about)
        self.quit_action.triggered.connect(self.quit)
        self.add_new_button.clicked.connect(self.change_dialog_index)
        self.back_button.clicked.connect(self.change_dialog_index)
        self.modifier_cbox.currentIndexChanged.connect(self.populate_shortcut_key)
        self.shortcut_key_cbox.currentIndexChanged.connect(self.save_shortcut_key)
        self.browse_button.clicked.connect(self.open_file_dialog)
        self.add_button.clicked.connect(self.save_file)
        self.clear_button.clicked.connect(self.reset_form)
        self.remove_button.clicked.connect(self.remove_keyboard)
        self.keyboard_cbox.currentIndexChanged.connect(self.save_current_keyboard)
        self.start_windows_check.stateChanged.connect(self.change_start_windows)

    def reset_form(self):
        self.clear_file_error()
        self.file_path_tview.setText("")

    def open_file_dialog(self):
        file_dialog = QFileDialog()
        self.file_path_tview.setText(QFileDialog.getOpenFileName(file_dialog,
                                                                 str("Choose  a SCIM Table"),
                                                                 "",
                                                                 str("Scim Tables (*.in *.txt)"))[0])

    def validate(self):
        try:
            with open(str(self.file_path_tview.text()), encoding="utf-8") as search:
                for line in search:
                    line = line.rstrip()  # remove '\n' at end of line
                    if "SCIM_Generic_Table_Phrase_Library_TEXT" in line:
                        return True
            self.show_file_error("Invalid SCIM Table file")
            return False
        except:
            self.show_file_error("Some error occurred")
            return False

    def save_file(self):
        if self.validate():
            self.clear_file_error()
            filepath = str(self.file_path_tview.text())
            fileinfo = QFileInfo(filepath)
            filename = str(int(time.time())) + "_" + fileinfo.fileName()
            keyboard_name = "Unknown"
            with open(filepath, encoding="utf-8") as search:
                for line in search:
                    line = line.rstrip()  # remove '\n' at end of line
                    if "NAME" in line:
                        name_line = line
                        name_list = name_line.split('=', 1)
                        if len(name_list) > 0:
                            keyboard_name = name_list[1]
            if keyboard_name == "Unknown":
                self.show_file_error("SCIM table name header not found")
            elif DatabaseManager.check_keyboard_exist(keyboard_name):
                self.show_file_error("Keyboard already exists")
            else:
                shutil.copyfile(filepath, self.table_path + "\\" + filename)
                DatabaseManager.add_keyboard(keyboard_name, self.table_path + "\\" + filename)
                self.file_path_tview.setText("")
                self.update_table()

    def show_file_error(self, message):
        self.error_msg.setText(message)

    def clear_file_error(self):
        self.error_msg.setText("")

    def show_about(self):
        pass

    def quit(self):
        self.engine.un_hook()
        self.app.exit(0)

    def show_setting(self):
        self.stacked_widget.setCurrentIndex(0)
        self.showNormal()

    def change_dialog_index(self):
        current_index = self.stacked_widget.currentIndex()
        if current_index == 0:
            self.reset_form()
            self.init_table()
            self.stacked_widget.setCurrentIndex(1)
        else:
            self.init_combobox()
            self.stacked_widget.setCurrentIndex(0)

    def populate_modifier_cbox(self):
        self.modifier_cbox.blockSignals(True)
        modifiers = DatabaseManager.get_keys()
        for modifier in modifiers:
            self.modifier_cbox.addItem(modifier.name, modifier.id)
            if modifier.id == self.shortcut_key.parent.id:
                self.modifier_cbox.setCurrentText(modifier.name)
        self.populate_shortcut_key()
        self.modifier_cbox.blockSignals(False)

    def populate_shortcut_key(self):
        self.shortcut_key_cbox.blockSignals(True)
        self.shortcut_key_cbox.clear()
        keys = DatabaseManager.get_keys(self.modifier_cbox.currentData())
        for key in keys:
            self.shortcut_key_cbox.addItem(key.name, key.id)
            if key.id == self.shortcut_key.id:
                self.shortcut_key_cbox.setCurrentText(key.name)
        self.shortcut_key_cbox.blockSignals(False)
        self.save_shortcut_key()

    def save_shortcut_key(self):
        DatabaseManager.set_shortcut_key(self.shortcut_key_cbox.currentData())
        self.shortcut_key = DatabaseManager.get_shortcut_key()
        self.register_shortcut_listener()

    def register_shortcut_listener(self):
        self.engine.event_queue.remove_all()
        if self.shortcut_key.parent.name == "NONE":
            self.engine.event_queue.register_event(
                [
                    [self.shortcut_key.name],
                    self.icon_activated,
                    QSystemTrayIcon.Trigger
                ]
            )
        elif self.shortcut_key.parent.name == "CTRL":
            self.engine.event_queue.register_event(
                [
                    ['Lcontrol', self.shortcut_key.name],
                    self.icon_activated,
                    QSystemTrayIcon.Trigger
                ]
            )
            self.engine.event_queue.register_event(
                [
                    ['Rcontrol', self.shortcut_key.name],
                    self.icon_activated,
                    QSystemTrayIcon.Trigger
                ]
            )
        elif self.shortcut_key.parent.name == "ALT":
            self.engine.event_queue.register_event(
                [
                    ['LMenu', self.shortcut_key.name],
                    self.icon_activated,
                    QSystemTrayIcon.Trigger
                ]
            )
            self.engine.event_queue.register_event(
                [
                    ['RMenu', self.shortcut_key.name],
                    self.icon_activated,
                    QSystemTrayIcon.Trigger
                ]
            )
        return True

    def change_status(self):
        self.engine.conv_state = not self.engine.conv_state
        DatabaseManager.set_current_state(self.engine.conv_state)
        if self.engine.conv_state:
            self.show_on_status()
            self.load_keyboard()
        else:
            self.show_off_status()

    def icon_activated(self, reason):
        if reason == QSystemTrayIcon.DoubleClick:
            pass
        elif reason == QSystemTrayIcon.Trigger:
            self.change_status()
        elif reason == QSystemTrayIcon.MiddleClick:
            pass
        else:
            pass

    def show_on_status(self):
        self.icon = QIcon(QPixmap(":icon/on_logo"))
        self.change_icons()

    def show_off_status(self):
        self.icon = QIcon(QPixmap(":icon/off_logo"))
        self.change_icons()

    def change_icons(self):
        self.tray_icon.setIcon(self.icon)
        self.setWindowIcon(self.icon)
        # TODO : Need to implement this method with current keyboard name
        self.tray_icon.setToolTip("Keyboard Name")
        self.show_tray_message()

    def show_tray_message(self):
        if self.engine.conv_state:
            message = "Ekalappai is Switched ON"
        else:
            message = "Ekalappai is Switched OFF"
        self.tray_icon.showMessage(
            qApp.applicationName() + " " + qApp.applicationVersion(),
            message,
            QSystemTrayIcon.MessageIcon(0),
            100
        )

    def update_table(self, init=False):
        if init:
            self.init_table()
        records = DatabaseManager.get_all_keyboards()
        self.keyboard_table.setRowCount(records[0])
        for idx, record in enumerate(records[1]):
            self.keyboard_table.setItem(idx, 1, QTableWidgetItem(record.language_name))
            self.keyboard_table.setItem(idx, 2, QTableWidgetItem(str(record.id)))
            chk_box = QTableWidgetItem()
            chk_box.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
            chk_box.setCheckState(Qt.Unchecked)
            self.keyboard_table.setItem(idx, 0, chk_box)
        self.keyboard_table.resizeRowsToContents()
        return

    """
        Initialize the grid with the default options
    """
    def init_table(self):
        self.keyboard_table.setColumnCount(3)
        self.keyboard_table.setHorizontalHeaderLabels(["", "Name", "Id"])
        self.keyboard_table.setColumnHidden(2, True)
        self.keyboard_table.setColumnWidth(0, 30)
        self.keyboard_table.horizontalHeader().setStretchLastSection(True)
        self.keyboard_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.keyboard_table.setSelectionBehavior(QTableView.SelectRows)
        self.keyboard_table.setSelectionMode(QAbstractItemView.SingleSelection)

    def remove_keyboard(self):
        for row in range(0, self.keyboard_table.rowCount()):
            if self.keyboard_table.item(row, 0).checkState() == Qt.Checked and \
                            DatabaseManager.get_current_keyboard() != self.keyboard_table.item(row, 2).text():
                DatabaseManager.remove_keyboard(int(self.keyboard_table.item(row, 2).text()))
        self.update_table()

    def init_combobox(self):
        self.keyboard_cbox.blockSignals(True)
        self.keyboard_cbox.clear()
        current_keyboard = DatabaseManager.get_current_keyboard()
        index = 0
        for keyboard in DatabaseManager.get_all_keyboards()[1]:
            self.keyboard_cbox.addItem(keyboard.language_name, keyboard.id)
            if int(current_keyboard) == keyboard.id:
                self.keyboard_cbox.setCurrentText(keyboard.language_name)
                self.keyboard_cbox.setCurrentIndex(index)
            index += 1
        self.keyboard_cbox.blockSignals(False)

    def save_current_keyboard(self):
        DatabaseManager.set_current_keyboard(self.keyboard_cbox.currentData())
        self.engine.conv_state = True
        DatabaseManager.set_current_state(self.engine.conv_state)
        self.show_on_status()
        self.load_keyboard()

    def load_keyboard(self):
        try:
            self.engine.file_name = DatabaseManager.get_keyboard_path(DatabaseManager.get_current_keyboard())
            self.engine.initialize()
        except Exception as error:
            # TODO: Need to throw an error
            print('Error in loading keyboard')
            print(error)
            pass
        

    def change_start_windows(self):
        if self.start_windows_check.isChecked():
            self.registrySettings.setValue(qApp.applicationName(), qApp.applicationFilePath())
        else:
            self.registrySettings.remove(qApp.applicationName())
Exemplo n.º 59
0
class LocationManager(QObject):  # {{{

    locations_changed = pyqtSignal()
    unmount_device = pyqtSignal()
    location_selected = pyqtSignal(object)
    configure_device = pyqtSignal()
    update_device_metadata = pyqtSignal()

    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        self.free = [-1, -1, -1]
        self.count = 0
        self.location_actions = QActionGroup(self)
        self.location_actions.setExclusive(True)
        self.current_location = 'library'
        self._mem = []
        self.tooltips = {}

        self.all_actions = []

        def ac(name, text, icon, tooltip):
            icon = QIcon(I(icon))
            ac = self.location_actions.addAction(icon, text)
            setattr(self, 'location_'+name, ac)
            ac.setAutoRepeat(False)
            ac.setCheckable(True)
            receiver = partial(self._location_selected, name)
            ac.triggered.connect(receiver)
            self.tooltips[name] = tooltip

            m = QMenu(parent)
            self._mem.append(m)
            a = m.addAction(icon, tooltip)
            a.triggered.connect(receiver)
            if name != 'library':
                self._mem.append(a)
                a = m.addAction(QIcon(I('eject.png')), _('Eject this device'))
                a.triggered.connect(self._eject_requested)
                self._mem.append(a)
                a = m.addAction(QIcon(I('config.png')), _('Configure this device'))
                a.triggered.connect(self._configure_requested)
                self._mem.append(a)
                a = m.addAction(QIcon(I('sync.png')), _('Update cached metadata on device'))
                a.triggered.connect(lambda x : self.update_device_metadata.emit())
                self._mem.append(a)

            else:
                ac.setToolTip(tooltip)
            ac.setMenu(m)
            ac.calibre_name = name

            self.all_actions.append(ac)
            return ac

        self.library_action = ac('library', _('Library'), 'lt.png',
                _('Show books in calibre library'))
        ac('main', _('Device'), 'reader.png',
                _('Show books in the main memory of the device'))
        ac('carda', _('Card A'), 'sd.png',
                _('Show books in storage card A'))
        ac('cardb', _('Card B'), 'sd.png',
                _('Show books in storage card B'))

    def set_switch_actions(self, quick_actions, rename_actions, delete_actions,
            switch_actions, choose_action):
        self.switch_menu = self.library_action.menu()
        if self.switch_menu:
            self.switch_menu.addSeparator()
        else:
            self.switch_menu = QMenu()

        self.switch_menu.addAction(choose_action)
        self.cs_menus = []
        for t, acs in [(_('Quick switch'), quick_actions),
                (_('Rename library'), rename_actions),
                (_('Delete library'), delete_actions)]:
            if acs:
                self.cs_menus.append(QMenu(t))
                for ac in acs:
                    self.cs_menus[-1].addAction(ac)
                self.switch_menu.addMenu(self.cs_menus[-1])
        self.switch_menu.addSeparator()
        for ac in switch_actions:
            self.switch_menu.addAction(ac)

        if self.switch_menu != self.library_action.menu():
            self.library_action.setMenu(self.switch_menu)

    def _location_selected(self, location, *args):
        if location != self.current_location and hasattr(self,
                'location_'+location):
            self.current_location = location
            self.location_selected.emit(location)
            getattr(self, 'location_'+location).setChecked(True)

    def _eject_requested(self, *args):
        self.unmount_device.emit()

    def _configure_requested(self):
        self.configure_device.emit()

    def update_devices(self, cp=(None, None), fs=[-1, -1, -1], icon=None):
        if icon is None:
            icon = I('reader.png')
        self.location_main.setIcon(QIcon(icon))
        had_device = self.has_device
        if cp is None:
            cp = (None, None)
        if isinstance(cp, (str, unicode)):
            cp = (cp, None)
        if len(fs) < 3:
            fs = list(fs) + [0]
        self.free[0] = fs[0]
        self.free[1] = fs[1]
        self.free[2] = fs[2]
        cpa, cpb = cp
        self.free[1] = fs[1] if fs[1] is not None and cpa is not None else -1
        self.free[2] = fs[2] if fs[2] is not None and cpb is not None else -1
        self.update_tooltips()
        if self.has_device != had_device:
            self.location_library.setChecked(True)
            self.locations_changed.emit()
            if not self.has_device:
                self.location_library.trigger()

    def update_tooltips(self):
        for i, loc in enumerate(('main', 'carda', 'cardb')):
            t = self.tooltips[loc]
            if self.free[i] > -1:
                t += u'\n\n%s '%human_readable(self.free[i]) + _('available')
            ac = getattr(self, 'location_'+loc)
            ac.setToolTip(t)
            ac.setWhatsThis(t)
            ac.setStatusTip(t)

    @property
    def has_device(self):
        return max(self.free) > -1

    @property
    def available_actions(self):
        ans = [self.location_library]
        for i, loc in enumerate(('main', 'carda', 'cardb')):
            if self.free[i] > -1:
                ans.append(getattr(self, 'location_'+loc))
        return ans
Exemplo n.º 60
0
 def show_context_menu(self, pos):
     item = self.itemAt(pos)
     result = item.data(0, Qt.UserRole)
     items = self.selectedItems()
     m = QMenu(self)
     if isinstance(result, dict):
         m.addAction(_('Open in viewer'), partial(self.item_activated,
                                                  item))
         m.addAction(_('Show in calibre'),
                     partial(self.show_in_calibre, item))
         if result.get('annotation', {}).get('type') == 'highlight':
             m.addAction(_('Edit notes'), partial(self.edit_notes, item))
     if items:
         m.addSeparator()
         m.addAction(
             ngettext('Export selected item', 'Export {} selected items',
                      len(items)).format(len(items)),
             self.export_requested.emit)
         m.addAction(
             ngettext('Delete selected item', 'Delete {} selected items',
                      len(items)).format(len(items)),
             self.delete_requested.emit)
     m.addSeparator()
     m.addAction(_('Expand all'), self.expandAll)
     m.addAction(_('Collapse all'), self.collapseAll)
     m.exec_(self.mapToGlobal(pos))