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