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() + '\n\nli { margin-top: 0.5ex; margin-bottom: 0.5ex; }') 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) 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) 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 keyPressEvent(self, ev): if ev.matches(QKeySequence.StandardKey.Bold): ev.accept() self.action_bold.toggle(), self.action_bold.trigger() return if ev.matches(QKeySequence.StandardKey.Italic): ev.accept() self.action_italic.toggle(), self.action_italic.trigger() return if ev.matches(QKeySequence.StandardKey.Underline): ev.accept() self.action_underline.toggle(), self.action_underline.trigger() return return QTextEdit.keyPressEvent(self, ev) 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.Style.ListDecimal) self.action_unordered_list.setChecked(ls is not None and ls.format().style() == QTextListFormat.Style.ListDisc) tcf = c.charFormat() vert = tcf.verticalAlignment() self.action_superscript.setChecked(vert == QTextCharFormat.VerticalAlignment.AlignSuperScript) self.action_subscript.setChecked(vert == QTextCharFormat.VerticalAlignment.AlignSubScript) self.action_bold.setChecked(tcf.fontWeight() == QFont.Weight.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.AlignmentFlag.AlignLeft) self.action_align_right.setChecked(a == Qt.AlignmentFlag.AlignRight) self.action_align_center.setChecked(a == Qt.AlignmentFlag.AlignHCenter) self.action_align_justified.setChecked(a == Qt.AlignmentFlag.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.FocusReason.TabFocusReason) def do_clear(self, *args): c = self.textCursor() c.beginEditBlock() c.movePosition(QTextCursor.MoveOperation.Start, QTextCursor.MoveMode.MoveAnchor) c.movePosition(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.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.Weight.Bold if c.charFormat().fontWeight() != QFont.Weight.Bold else QFont.Weight.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.VerticalAlignment.AlignSuperScript) def do_subscript(self): self.do_vertical_align(QTextCharFormat.VerticalAlignment.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.Style.ListDecimal) def do_unordered_list(self): self.do_list(QTextListFormat.Style.ListDisc) def do_alignment(self, which): with self.editing_cursor() as c: c = self.textCursor() fmt = QTextBlockFormat() fmt.setAlignment(which) c.mergeBlockFormat(fmt) def do_align_left(self): self.do_alignment(Qt.AlignmentFlag.AlignLeft) def do_align_center(self): self.do_alignment(Qt.AlignmentFlag.AlignHCenter) def do_align_right(self): self.do_alignment(Qt.AlignmentFlag.AlignRight) def do_align_justified(self): self.do_alignment(Qt.AlignmentFlag.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.MoveOperation.Start, QTextCursor.MoveMode.MoveAnchor) c.movePosition(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.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.Weight.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.MoveOperation.StartOfBlock, QTextCursor.MoveMode.MoveAnchor) c.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.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(QTextFormat.Property.FontSizeAdjustment, adjust) cf.setProperty(QTextFormat.Property.FontSizeAdjustment, adjust) if wt: bcf.setProperty(QTextFormat.Property.FontWeight, wt) cf.setProperty(QTextFormat.Property.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.GlobalColor.black, self, _('Choose foreground color'), QColorDialog.ColorDialogOption.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.GlobalColor.white, self, _('Choose background color'), QColorDialog.ColorDialogOption.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(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.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(self.palette().color(QPalette.ColorRole.Link))) 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, QTextCursor.MoveMode.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.FieldGrowthPolicy.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.StandardButton.Ok|QDialogButtonBox.StandardButton.Cancel) d.br = b = QPushButton(_('&Browse')) b.setIcon(QIcon(I('document_open.png'))) def cf(): filetypes = [] if d.treat_as_image.isChecked(): filetypes = [(_('Images'), 'png jpeg jpg gif'.split())] files = choose_files(d, 'select link file', _('Choose file'), filetypes, 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, QFormLayout.ItemRole.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_() == QDialog.DialogCode.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.ParsingMode.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.ParsingMode.TolerantMode) if url.isValid(): return url return QUrl(link, QUrl.ParsingMode.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: if path.rpartition('.')[-1].lower() in {'jpg', 'jpeg', 'gif', 'png', 'bmp', 'webp'}: return QByteArray(bytearray.fromhex( '89504e470d0a1a0a0000000d49484452' '000000010000000108060000001f15c4' '890000000a49444154789c6300010000' '0500010d0a2db40000000049454e44ae' '426082')) 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.MoveOperation.Start, QTextCursor.MoveMode.MoveAnchor) c.movePosition(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.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.StandardKey.Paste).toString(QKeySequence.SequenceFormat.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.addSeparator() am = QMenu(_('Advanced')) menu.addMenu(am) am.addAction(self.action_block_style) am.addAction(self.action_insert_link) am.addAction(self.action_background) am.addAction(self.action_color) menu.addAction(_('Smarten punctuation'), parent.smarten_punctuation) menu.exec_(ev.globalPos())
class MenuBar(QObject): is_native_menubar = False def __init__(self, location_manager, parent): QObject.__init__(self, parent) self.menu_bar = QMenuBar(parent) self.menu_bar.is_native_menubar = False parent.setMenuBar(self.menu_bar) 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)
def create_action(self, spec=None, attr='qaction', shortcut_name=None, persist_shortcut=False): 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': if hasattr(self.action_menu_clone_qaction, 'rstrip'): mt = str(self.action_menu_clone_qaction) else: mt = action.text() 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 = str(spec[0]) if shortcut_name and self.action_spec[0] and not ( attr == 'qaction' and self.popup_type == QToolButton.ToolButtonPopupMode.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], persist_shortcut=persist_shortcut) except NameConflict as e: try: prints(str(e)) except: pass shortcut_action.setShortcuts([ QKeySequence(key, QKeySequence.SequenceFormat.PortableText) for key in keys ]) else: self.shortcut_action_for_context_menu = shortcut_action if ismacos: # 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 MenuBar(QObject): is_native_menubar = True @property def native_menubar(self): mb = self.gui.native_menubar if mb.parent() is None: # Without this the menubar does not update correctly with Qt >= # 5.6. See the last couple of lines in updateMenuBarImmediately # in qcocoamenubar.mm mb.setParent(self.gui) return mb 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 adapt_for_dialog(self, enter): def ac(text, key, role=QAction.MenuRole.TextHeuristicRole): ans = QAction(text, self) ans.setMenuRole(role) ans.setShortcut(QKeySequence(key)) self.edit_menu.addAction(ans) return ans mb = self.native_menubar if enter: self.clear_bar(mb) self.edit_menu = QMenu() self.edit_action = QAction(_('Edit'), self) self.edit_action.setMenu(self.edit_menu) ac(_('Copy'), QKeySequence.StandardKey.Copy), ac(_('Paste'), QKeySequence.StandardKey.Paste), ac(_('Select all'), QKeySequence.StandardKey.SelectAll), mb.addAction(self.edit_action) self.added_actions = [self.edit_action] else: self.refresh_bar() def clear_bar(self, mb): for ac in self.added_actions: m = ac.menu() if m is not None: m.setVisible(False) for ac in self.added_actions: mb.removeAction(ac) if ac is not self.donate_action: ac.setMenu(None) ac.deleteLater() self.added_actions = [] def init_bar(self, actions): mb = self.native_menubar self.last_actions = actions self.clear_bar(mb) 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)