# anki-search-inside-add-card # Copyright (C) 2019 - 2020 Tom Z. # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. from aqt.qt import * from aqt.utils import tooltip import aqt.editor import aqt import functools import time import math import re import random from aqt.utils import saveGeom, restoreGeom from anki.hooks import addHook, remHook from anki.lang import _ from ..notes import * from ..notes import _get_priority_list from ..hooks import run_hooks from ..state import get_index from ..config import get_config_value_or_default, update_config from ..web_import import import_webpage from .url_import import UrlImporter from .components import QtPrioritySlider from .url_input_dialog import URLInputDialog import utility.text import utility.misc import state def openEditor(mw, nid): note = mw.col.getNote(nid) dialog = EditDialog(mw, note) class EditDialog(QDialog): """ Edit dialog for Anki notes. """ def __init__(self, mw, note): QDialog.__init__(self, None, Qt.Window) mw.setupDialogGC(self) self.mw = mw self.form = aqt.forms.editcurrent.Ui_Dialog() self.form.setupUi(self) self.form.buttonBox.button(QDialogButtonBox.Close).setShortcut(QKeySequence("Ctrl+Return")) self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self) self.setWindowTitle(_("Edit Note")) self.setMinimumHeight(400) self.setMinimumWidth(500) self.resize(500, 850) self.editor.setNote(note, focusTo=0) addHook("reset", self.onReset) self.mw.requireReset() self.show() self.mw.progress.timer(100, lambda: self.editor.web.setFocus(), False) def onReset(self): try: n = self.editor.note n.load() except: remHook("reset", self.onReset) self.editor.setNote(None) self.mw.reset() self.close() return self.editor.setNote(n) def reopen(self, mw): tooltip("Please finish editing the existing card first.") self.onReset() def reject(self): self.saveAndClose() def saveAndClose(self): self.editor.saveNow(self._saveAndClose) def _saveAndClose(self): remHook("reset", self.onReset) self.editor.cleanup() QDialog.reject(self) def closeWithCallback(self, onsuccess): def callback(): self._saveAndClose() onsuccess() self.editor.saveNow(callback) class NoteEditor(QDialog): """ The editor window for non-anki notes. """ last_tags = "" def __init__(self, parent, note_id = None, add_only = False, read_note_id = None, tag_prefill = None, source_prefill = None, text_prefill = None, title_prefill = None, prio_prefill = None): QDialog.__init__(self, parent, Qt.WindowSystemMenuHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint | Qt.WindowMaximizeButtonHint | Qt.WindowMinimizeButtonHint) self.mw = aqt.mw self.parent = parent self.note_id = note_id self.note = None self.add_only = add_only self.read_note_id = read_note_id self.tag_prefill = tag_prefill self.source_prefill = source_prefill self.text_prefill = text_prefill self.title_prefill = title_prefill self.prio_prefill = prio_prefill self.dark_mode_used = state.night_mode if self.note_id is not None: self.note = get_note(note_id) #self.mw.setupDialogGC(self) #self.setWindowModality(Qt.WindowModal) #self.setAttribute(Qt.WA_DeleteOnClose) self.setup_ui() def setup_ui(self): # editing an existing note if self.note_id is not None: self.save = QPushButton("\u2714 Save") self.setWindowTitle('Edit Note') self.save.clicked.connect(self.on_update_clicked) self.priority = get_priority(self.note_id) # creating a new note else: self.save = QPushButton("\u2714 Create") self.setWindowTitle('New Note') self.save.clicked.connect(self.on_create_clicked) self.priority = 0 self.save_and_stay = QPushButton(" \u2714 Create && Keep Open ") self.save_and_stay.clicked.connect(self.on_create_and_keep_open_clicked) self.save_and_stay.setShortcut("Ctrl+Shift+Return") self.save.setShortcut("Ctrl+Return") self.cancel = QPushButton("Cancel") self.cancel.clicked.connect(self.reject) priority_list = _get_priority_list() self.priority_list = priority_list self.tabs = QTabWidget() self.create_tab = CreateTab(self) #self.browse_tab = BrowseTab() self.tabs.addTab(self.create_tab, "Create") if not self.add_only: self.priority_tab = PriorityTab(priority_list, self) self.tabs.addTab(self.priority_tab, "Queue") self.settings_tab = SettingsTab(self) self.tabs.addTab(self.settings_tab, "Settings") # tabs.addTab(self.browse_tab, "Browse") layout_main = QVBoxLayout() layout_main.addWidget(self.tabs) self.setLayout(layout_main) self.create_tab.title.setFocus() # self.exec_() state.note_editor_shown = True self.show() def on_create_clicked(self): success = self._create_note() if not success: return #aqt.dialogs.close("UserNoteEditor") run_hooks("user-note-created") self.reject() # if reading modal is open, we might have to update the bottom bar if self.read_note_id is not None: get_index().ui.reading_modal.update_reading_bottom_bar(self.read_note_id) def on_create_and_keep_open_clicked(self): success = self._create_note() if not success: return run_hooks("user-note-created") if self.read_note_id is not None: get_index().ui.reading_modal.update_reading_bottom_bar(self.read_note_id) self._reset() def _create_note(self): title = self.create_tab.title.text() title = utility.text.clean_user_note_title(title) if self.create_tab.plain_text_cb.checkState() == Qt.Checked: text = self.create_tab.text.toPlainText() else: # if this check is missing, text is sometimes saved as an empty paragraph if self.create_tab.text.document().isEmpty(): text = "" else: text = self.create_tab.text.toHtml() source = self.create_tab.source.text() tags = self.create_tab.tag.text() queue_schedule = self.create_tab.slider.value() specific_schedule = self.create_tab.slider.schedule() # if source is a pdf, title must be given if len(title.strip()) == 0 and source.lower().strip().endswith(".pdf"): tooltip("Title must be set if source is PDF.") return False if len(title.strip()) + len(text.strip()) == 0: tooltip("Either Text or Title have to be filled out.") return False if len(tags.strip()) == 0: default_tags = get_config_value_or_default("notes.editor.defaultTagsIfEmpty", "") if len(default_tags) > 0: tags = default_tags NoteEditor.last_tags = tags create_note(title, text, source, tags, None, specific_schedule, queue_schedule) return True def _reset(self): """ Called after a note is created with the save_and_stay button. Clear the fields for the next note. """ self.create_tab.title.setText("") self.create_tab.text.setText("") if self.create_tab.source.text().endswith(".pdf"): self.create_tab.source.setText("") self.create_tab.title.setFocus() def on_update_clicked(self): title = self.create_tab.title.text() title = utility.text.clean_user_note_title(title) if self.create_tab.plain_text_cb.checkState() == Qt.Checked: text = self.create_tab.text.toPlainText() else: # if this check is missing, text is sometimes saved as an empty paragraph if self.create_tab.text.document().isEmpty(): text = "" else: text = self.create_tab.text.toHtml() source = self.create_tab.source.text() tags = self.create_tab.tag.text() priority = self.create_tab.slider.value() if not self.create_tab.slider.has_changed_value(): priority = -1 specific_schedule = self.create_tab.slider.schedule() NoteEditor.last_tags = tags update_note(self.note_id, title, text, source, tags, specific_schedule, priority) run_hooks("user-note-edited", self.note_id) self.reject() def reject(self): if not self.add_only: self.priority_tab.t_view.setModel(None) state.note_editor_shown = False QDialog.reject(self) def accept(self): state.note_editor_shown = False self.reject() class CreateTab(QWidget): def __init__(self, parent): QWidget.__init__(self) self.queue_schedule = 0 self.parent = parent self.tree = QTreeWidget() self.original_bg = None self.original_fg = None web_path = utility.misc.get_web_folder_path() self.tree.setColumnCount(1) self.tree.setIconSize(QSize(0,0)) self.build_tree(get_all_tags_as_hierarchy(False)) self.tree.itemClicked.connect(self.tree_item_clicked) self.tree.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.tree.setMinimumHeight(150) self.tree.setMinimumWidth(220) self.tree.setHeaderHidden(True) self.highlight_map = { "ob": [0,0,0,232,151,0], "rw": [255, 255, 255, 209, 46, 50], "yb": [0,0,0,235, 239, 69], "bw": [255,255,255,2, 119,189], "gb": [255,255,255,34,177,76] } recently_used_tags = get_recently_used_tags() config = mw.addonManager.getConfig(__name__) if self.parent.dark_mode_used: tag_bg = config["styles.night.tagBackgroundColor"] tag_fg = config["styles.night.tagForegroundColor"] else: tag_bg = config["styles.tagBackgroundColor"] tag_fg = config["styles.tagForegroundColor"] self.recent_tbl = QWidget() self.recent_tbl.setObjectName("recentDisp") self.recent_tbl.setStyleSheet("background-color: transparent;") bs = f""" background-color: {tag_bg}; color: {tag_fg}; padding: 2px 3px 2px 3px; border-radius: 5px; """ lo = FlowLayout() self.recent_tbl.setLayout(lo) for ix, t in enumerate(recently_used_tags): btn = QPushButton(t) btn.setStyleSheet(bs) btn.clicked.connect(functools.partial(self.add_tag, t)) lo.addWidget(btn) self.recent_tbl.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.recent_tbl.setMaximumHeight(100) queue_len = len(parent.priority_list) schedule = None if self.parent.note is None else self.parent.note.reminder self.slider = QtPrioritySlider(self.parent.priority, schedule=schedule) self.layout = QHBoxLayout() vbox_left = QVBoxLayout() tag_lbl = QLabel() tag_icn = QPixmap(utility.misc.get_web_folder_path() + "icons/icon-tag-24.png").scaled(14,14) tag_lbl.setPixmap(tag_icn) tag_hb = QHBoxLayout() tag_hb.setAlignment(Qt.AlignLeft) tag_hb.addWidget(tag_lbl) tag_hb.addWidget(QLabel("Tags (Click to Add)")) vbox_left.addLayout(tag_hb) vbox_left.addWidget(self.tree) self.all_tags_cb = QCheckBox("Show All Tags") self.all_tags_cb.stateChanged.connect(self.tag_cb_changed) vbox_left.addWidget(self.all_tags_cb) if len(recently_used_tags) > 0: tag_lbl1 = QLabel() tag_lbl1.setPixmap(tag_icn) tag_hb1 = QHBoxLayout() tag_hb1.setAlignment(Qt.AlignLeft) tag_hb1.addWidget(tag_lbl1) tag_hb1.addWidget(QLabel("Recent (Click to Add)")) vbox_left.addLayout(tag_hb1) qs = QScrollArea() qs.setStyleSheet(""" QScrollArea { background-color: transparent; } """) qs.setFrameShape(QFrame.NoFrame) qs.setWidgetResizable(True) qs.setWidget(self.recent_tbl) vbox_left.addWidget(qs) vbox_left.addWidget(self.slider) vbox_left.setContentsMargins(0,0,0,0) self.left_pane = QWidget() self.left_pane.setLayout(vbox_left) self.layout.addWidget(self.left_pane, 7) hbox = QHBoxLayout() self.toggle_btn = QPushButton("<") self.toggle_btn.clicked.connect(self.toggle_left_pane) hbox.addWidget(self.toggle_btn) hbox.addStretch(1) if parent.note_id is None: hbox.addWidget(parent.save_and_stay) hbox.addWidget(parent.save) hbox.addWidget(parent.cancel) vbox = QVBoxLayout() # vbox.addStretch(1) title_lbl = QLabel("Title") self.title = QLineEdit() if self.parent.title_prefill is not None: self.title.setText(self.parent.title_prefill) f = self.title.font() f.setPointSize(14) self.title.setFont(f) vbox.addWidget(title_lbl) vbox.addWidget(self.title) text_lbl = QLabel("Text") text_lbl.setToolTip("Text may contain HTML (some tags and inline styles may be removed).") self.text = QTextEdit() f = self.text.font() f.setPointSize(12) self.text.setFont(f) self.text.setMinimumHeight(380) self.text.setMinimumWidth(470) self.text.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.Expanding) self.text.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.text.setLineWidth(2) self.text.cursorPositionChanged.connect(self.on_text_cursor_change) if hasattr(self.text, "setTabStopDistance"): self.text.setTabStopDistance(QFontMetricsF(f).horizontalAdvance(' ') * 4) t_h = QHBoxLayout() t_h.addWidget(text_lbl) self.tb = QToolBar("Format") self.tb.setStyleSheet("background-color: transparent; border: 0px;") self.tb.setHidden(False) self.tb.setOrientation(Qt.Horizontal) self.tb.setIconSize(QSize(12, 12)) self.vtb = QToolBar("Highlight") self.vtb.setStyleSheet("background-color: transparent; border: 0px;") self.vtb.setOrientation(Qt.Vertical) self.vtb.setIconSize(QSize(16, 16)) self.orange_black = self.vtb.addAction(QIcon(web_path + "icons/icon-orange-black.png"), "Highlight 1") self.orange_black.triggered.connect(self.on_highlight_ob_clicked) self.red_white = self.vtb.addAction(QIcon(web_path + "icons/icon-red-white.png"), "Highlight 2") self.red_white.triggered.connect(self.on_highlight_rw_clicked) self.yellow_black = self.vtb.addAction(QIcon(web_path + "icons/icon-yellow-black.png"), "Highlight 3") self.yellow_black.triggered.connect(self.on_highlight_yb_clicked) self.blue_white = self.vtb.addAction(QIcon(web_path + "icons/icon-blue-white.png"), "Highlight 4") self.blue_white.triggered.connect(self.on_highlight_bw_clicked) self.green_black = self.vtb.addAction(QIcon(web_path + "icons/icon-green-black.png"), "Highlight 5") self.green_black.triggered.connect(self.on_highlight_gb_clicked) bold = self.tb.addAction("b") f = bold.font() f.setBold(True) bold.setFont(f) bold.setCheckable(True) bold.triggered.connect(self.on_bold_clicked) bold.setShortcut(QKeySequence("Ctrl+b")) italic = self.tb.addAction("i") italic.setCheckable(True) italic.triggered.connect(self.on_italic_clicked) f = italic.font() f.setItalic(True) italic.setFont(f) italic.setShortcut(QKeySequence("Ctrl+i")) underline = self.tb.addAction("u") underline.setCheckable(True) underline.triggered.connect(self.on_underline_clicked) f = underline.font() f.setUnderline(True) underline.setFont(f) underline.setShortcut(QKeySequence("Ctrl+u")) strike = self.tb.addAction("s") strike.setCheckable(True) strike.triggered.connect(self.on_strike_clicked) f = strike.font() f.setStrikeOut(True) strike.setFont(f) color = self.tb.addAction(QIcon(web_path + "icons/icon-color-change.png"), "Foreground Color") color.triggered.connect(self.on_color_clicked) bullet_list = self.tb.addAction("BL") bullet_list.setToolTip("Bullet List") bullet_list.triggered.connect(self.on_bullet_list_clicked) numbered_list = self.tb.addAction("NL") numbered_list.setToolTip("Numbered List") numbered_list.triggered.connect(self.on_numbered_list_clicked) clean_btn = QToolButton() clean_btn.setText("Clean Text ") clean_btn.setPopupMode(QToolButton.InstantPopup) clean_btn.setFocusPolicy(Qt.NoFocus) clean_menu = QMenu(clean_btn) if self.parent.dark_mode_used: clean_menu.setStyleSheet(""" QMenu { background: #3a3a3a; color: lightgrey; } QMenu:item:selected { background: #aaa; color: white; } """) else: clean_menu.setStyleSheet(""" QMenu { background: white; color: black; } QMenu:item:selected { background: #2496dc; color: white; } """) header_a = clean_menu.addAction("Remove Headers") header_a.triggered.connect(self.on_remove_headers_clicked) header_a1 = clean_menu.addAction("Remove All Bold Formatting") header_a1.triggered.connect(self.on_remove_bold_clicked) header_a2 = clean_menu.addAction("Convert Images to Base64") header_a2.triggered.connect(self.on_convert_images_clicked) header_a3 = clean_menu.addAction("Remove All Colors") header_a3.triggered.connect(self.on_remove_colors_clicked) clean_btn.setMenu(clean_menu) self.tb.addWidget(clean_btn) t_h.addWidget(self.tb) vbox.addLayout(t_h) text_h = QHBoxLayout() text_h.addWidget(self.text) text_h.addWidget(self.vtb) vbox.addLayout(text_h) self.plain_text_cb = QCheckBox("Save as Plain Text") self.line_status = QLabel("Ln: 0, Col: 0") p_hb = QHBoxLayout() p_hb.addSpacing(5) p_hb.addWidget(self.line_status) p_hb.addStretch(1) p_hb.addWidget(self.plain_text_cb) url_btn = QPushButton(u" Fetch from URL ... ") url_btn.clicked.connect(self.on_url_clicked) p_hb.addWidget(url_btn) p_hb.addSpacing(35) vbox.addLayout(p_hb) self.source = QLineEdit() # if note is an extract, prevent source editing if self.parent.note is not None and self.parent.note.extract_end is not None: source_lbl = QLabel("Source - Note is an extract") # self.source.setReadOnly(True) else: source_lbl = QLabel("Source") source_hb = QHBoxLayout() source_hb.addWidget(self.source) if self.parent.source_prefill is not None: self.source.setText(self.parent.source_prefill.replace("\\", "/")) pdf_btn = QPushButton("PDF") pdf_btn.clicked.connect(self.on_pdf_clicked) source_hb.addWidget(pdf_btn) pdf_from_url_btn = QPushButton("PDF from Webpage") pdf_from_url_btn.clicked.connect(self.on_pdf_from_url_clicked) source_hb.addWidget(pdf_from_url_btn) # if note is an extract, prevent source editing # if self.parent.note is not None and self.parent.note.extract_end is not None: # pdf_btn.setDisabled(True) # pdf_from_url_btn.setDisabled(True) vbox.addWidget(source_lbl) vbox.addLayout(source_hb) if self.parent.text_prefill is not None: self.text.setText(self.parent.text_prefill) btn_styles = """ QPushButton#q_1 { padding-left: 20px; padding-right: 20px; } QPushButton#q_2 { padding-left: 17px; padding-right: 17px; } QPushButton#q_3 { padding-left: 13px; padding-right: 13px; } QPushButton#q_4 { padding-left: 8px; padding-right: 8px; } QPushButton#q_5 { padding-left: 2px; padding-right: 2px; } QPushButton#q_6 { padding-left: 0px; padding-right: 0px; } QPushButton:hover#q_1,QPushButton:hover#q_2,QPushButton:hover#q_3,QPushButton:hover#q_4,QPushButton:hover#q_5,QPushButton:hover#q_6 { background-color: lightblue; } """ styles = """ QPushButton#q_1,QPushButton#q_2,QPushButton#q_3,QPushButton#q_4,QPushButton#q_5,QPushButton#q_6 { border-radius: 5px; } %s QTextEdit { border-radius: 5px; border: 1px solid #717378; padding: 3px; } QLineEdit { border-radius: 5px; border: 1px solid #717378; padding: 2px;} #recentDisp { margin: 5px; } """ % btn_styles if parent.dark_mode_used: styles += """ QToolTip { background: #3a3a3a; color: lightgrey; } """ else: styles += """ QToolTip { color: black; background-color: white; } """ # styles += """ # QPushButton#q_1,QPushButton#q_2,QPushButton#q_22,QPushButton#q_3,QPushButton#q_33,QPushButton#q_4,QPushButton#q_44,QPushButton#q_5,QPushButton#q_6 { color: beige; } # QPushButton:hover#q_1,QPushButton:hover#q_2,QPushButton:hover#q_22,QPushButton:hover#q_3,QPushButton:hover#q_33,QPushButton:hover#q_4,QPushButton:hover#q_44,QPushButton:hover#q_5,QPushButton:hover#q_6 { background-color: grey; border-color: blue; color: white; } # """ self.setStyleSheet(styles) # vbox.addStretch(1) tag_lbl2 = QLabel() tag_lbl2.setPixmap(tag_icn) tag_hb2 = QHBoxLayout() tag_hb2.setAlignment(Qt.AlignLeft) tag_hb2.addWidget(tag_lbl2) tag_hb2.addWidget(QLabel("Tags")) vbox.addLayout(tag_hb2) self.tag = QLineEdit() tags = get_all_tags() if tags: completer = QCompleter(tags) completer.setCaseSensitivity(Qt.CaseInsensitive) self.tag.setCompleter(completer) vbox.addWidget(self.tag) if self.parent.tag_prefill is not None: self.tag.setText(self.parent.tag_prefill) vbox.setAlignment(Qt.AlignTop) vbox.addSpacing(10) vbox.addLayout(hbox) self.layout.addSpacing(5) self.layout.addLayout(vbox, 73) self.setLayout(self.layout) if parent.note is not None: self.tag.setText(parent.note.tags.lstrip()) self.title.setText(parent.note.title) self.text.setHtml(parent.note.text) self.source.setText(parent.note.source) # toggle left pane by default if enabled in settings if get_config_value_or_default("notes.editor.autoHideLeftPaneOnOpen", False): self.left_pane.setVisible(False) self.toggle_btn.setText(">") # fill tags with last tags if enabled in settings if (parent.note is None and self.parent.tag_prefill is None and len(NoteEditor.last_tags.strip()) > 0 and get_config_value_or_default("notes.editor.autoFillWithLastTagsOnOpen", False)): self.tag.setText(NoteEditor.last_tags.lstrip()) def _add_to_tree(self, map, prefix): res = [] for t, children in map.items(): ti = QTreeWidgetItem([t]) ti.setData(0, 1, QVariant(prefix + t)) prefix_c = prefix + t + "::" for c,m in children.items(): ti.addChildren(self._add_to_tree({c: m}, prefix_c)) res.append(ti) return res def tree_item_clicked(self, item, col): tag = item.data(0, 1) self.add_tag(tag) def recent_item_clicked(self, item): tag = item.data(Qt.UserRole) self.add_tag(tag) def add_tag(self, tag): if tag is None or len(tag.strip()) == 0: return existing = self.tag.text().split() if tag in existing: return existing.append(tag) existing = sorted(existing) self.tag.setText(" ".join(existing)) def tag_cb_changed(self, state): self.tree.clear() tmap = None if state == Qt.Checked: tmap = get_all_tags_as_hierarchy(include_anki_tags = True) else: tmap = get_all_tags_as_hierarchy(include_anki_tags = False) self.build_tree(tmap) def build_tree(self, tmap): for t, children in tmap.items(): ti = QTreeWidgetItem([t]) ti.setData(0, 1, QVariant(t)) ti.addChildren(self._add_to_tree(children, t + "::")) self.tree.addTopLevelItem(ti) def toggle_left_pane(self): self.left_pane.setVisible(not self.left_pane.isVisible()) if self.left_pane.isVisible(): self.toggle_btn.setText("<") else: self.toggle_btn.setText(">") def on_pdf_clicked(self): fname = QFileDialog.getOpenFileName(self, 'Pick a PDF', '',"PDF (*.pdf)") if fname is not None: self.source.setText(fname[0]) def on_pdf_from_url_clicked(self): dialog = UrlImporter(self, show_schedule=False) if dialog.exec_(): if dialog.chosen_url is not None and len(dialog.chosen_url.strip()) > 0: name = dialog.get_name() path = get_config_value_or_default("pdfUrlImportSavePath", "") if path is None or len(path) == 0: tooltip("""You have to set a save path for imported URLs first. <center>Config value: <i>pdfUrlImportSavePath</i></center> """, period=4000) return path = utility.misc.get_pdf_save_full_path(path, name) utility.misc.url_to_pdf(dialog.chosen_url, path) self.source.setText(path) else: tooltip("Invalid URL") def on_url_clicked(self): dialog = URLInputDialog(self) if dialog.exec_(): if dialog.chosen_url is not None and len(dialog.chosen_url.strip()) > 0: text = import_webpage(dialog.chosen_url) if text is None: tooltip("Failed to fetch text from page.") else: self.text.setHtml(text) def on_italic_clicked(self): self.text.setFontItalic(not self.text.fontItalic()) def on_bold_clicked(self): self.text.setFontWeight(QFont.Normal if self.text.fontWeight() > QFont.Normal else QFont.Bold) def on_remove_headers_clicked(self): html = self.text.toHtml() html = utility.text.remove_headers(html) self.text.setHtml(html) def on_remove_bold_clicked(self): html = self.text.toHtml() html = utility.text.remove_all_bold_formatting(html) self.text.setHtml(html) def on_bullet_list_clicked(self): cursor = self.text.textCursor() cursor.createList(QTextListFormat.ListDisc) def on_numbered_list_clicked(self): cursor = self.text.textCursor() cursor.createList(QTextListFormat.ListDecimal) def on_underline_clicked(self): state = self.text.fontUnderline() self.text.setFontUnderline(not state) def on_strike_clicked(self): format = self.text.currentCharFormat() format.setFontStrikeOut(not format.fontStrikeOut()) self.text.setCurrentCharFormat(format) def on_color_clicked(self): color = QColorDialog.getColor() self.text.setTextColor(color) def highlight(self, type): self.save_original_bg_and_fg() if self.text.textCursor().selectedText() is not None and len(self.text.textCursor().selectedText()) > 0: c = self.get_color_at_selection() cursor = self.text.textCursor() fmt = QTextCharFormat() colors = self.highlight_map[type] if (c[0] == self.original_fg or (c[0] == QColor(0,0,0) and self.original_fg == QColor(255,255,255))) and (c[1] == self.original_bg or c[1] == QColor(0, 0, 0) or c[1] == QColor(255,255,255)): fmt.setBackground(QBrush(QColor(colors[3], colors[4], colors[5]))) fmt.setForeground(QBrush(QColor(colors[0], colors[1], colors[2]))) else: fmt.setBackground(QBrush(self.original_bg)) fmt.setForeground(QBrush(self.original_fg)) cursor.mergeCharFormat(fmt) cursor.clearSelection() self.text.setTextCursor(cursor) self.restore_original_bg_and_fg() def on_highlight_ob_clicked(self): self.highlight("ob") def on_highlight_rw_clicked(self): self.highlight("rw") def on_highlight_yb_clicked(self): self.highlight("yb") def on_highlight_bw_clicked(self): self.highlight("bw") def on_highlight_gb_clicked(self): self.highlight("gb") def get_color_at_selection(self): fg = self.text.textCursor().charFormat().foreground().color() #fg = self.text.textColor() # bg = self.text.textCursor().charFormat().background().color() bg = self.text.textBackgroundColor() return (fg, bg) def save_original_bg_and_fg(self): if self.original_bg is None: self.original_bg = self.text.palette().color(QPalette.Base) self.original_fg = self.text.palette().color(QPalette.Foreground) #self.original_fg = self.text.textColor() def restore_original_bg_and_fg(self): self.text.setTextBackgroundColor(self.original_bg) self.text.setTextColor(self.original_fg) def on_text_cursor_change(self): cursor = self.text.textCursor() line = cursor.blockNumber() + 1 col = cursor.columnNumber() self.line_status.setText("Ln: {}, Col: {}".format(line,col)) def on_remove_colors_clicked(self): html = self.text.toHtml() html = utility.text.remove_colors(html) self.text.setHtml(html) def on_convert_images_clicked(self): html = self.text.toHtml() images_contained = utility.text.find_all_images(html) if images_contained is None: return for image_tag in images_contained: #ignore images already in base64 if re.findall("src=['\"] *data:image/(png|jpe?g);[^;]{0,50};base64,", image_tag, flags=re.IGNORECASE): continue url = re.search("src=(\"[^\"]+\"|'[^']+')", image_tag, flags=re.IGNORECASE).group(1)[1:-1] try: base64 = utility.misc.url_to_base64(url) if base64 is None or len(base64) == 0: return ending = "" if url.lower().endswith("jpg") or url.lower().endswith("jpeg"): ending = "jpeg" elif url.lower().endswith("png"): ending = "png" elif "jpg" in url.lower() or "jpeg" in url.lower(): ending = "jpeg" elif "png" in url.lower(): ending = "png" else: ending = "jpeg" html = html.replace(image_tag, "<img src=\"data:image/%s;base64,%s\">" % (ending,base64)) except: continue self.text.setHtml(html) class PriorityTab(QWidget): def __init__(self, priority_list, parent): QWidget.__init__(self) self.parent = parent self.t_view = QTableView() html_delegate = HTMLDelegate() model = self.get_model(priority_list) self.t_view.setItemDelegateForColumn(0, html_delegate) self.t_view.setItemDelegateForColumn(1, html_delegate) self.t_view.setModel(model) self.set_remove_btns(priority_list) self.t_view.resizeColumnsToContents() self.t_view.setSelectionBehavior(QAbstractItemView.SelectRows) # self.t_view.setDragEnabled(True) # self.t_view.setDropIndicatorShown(True) # self.t_view.setAcceptDrops(True) # self.t_view.viewport().setAcceptDrops(True) # self.t_view.setDragDropOverwriteMode(False) # self.t_view.setDragDropMode(QAbstractItemView.InternalMove) # self.t_view.setDefaultDropAction(Qt.MoveAction) if priority_list is not None and len(priority_list) > 0: self.t_view.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) self.t_view.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) self.t_view.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents) self.t_view.verticalHeader().setSectionsMovable(False) self.t_view.setSelectionMode(QAbstractItemView.SingleSelection) self.vbox = QVBoxLayout() lbl = QLabel("'Remove' will only remove the item from the queue, not delete it.") self.vbox.addWidget(lbl) self.vbox.addWidget(self.t_view) # bottom_box = QHBoxLayout() # # self.shuffle_btn = QPushButton("Shuffle") # # # self.shuffle_btn.clicked.connect(self.on_shuffle_btn_clicked) # # bottom_box.addWidget(self.shuffle_btn) # # bottom_box.addStretch(1) # self.vbox.addLayout(bottom_box) self.setLayout(self.vbox) if parent.dark_mode_used: self.setStyleSheet(""" QHeaderView::section { background-color: #313233; color: white; } QTableCornerButton::section { background-color: #313233; } """) def on_remove_clicked(self, id): """ Remove an item from the queue. """ row_len = self.t_view.model().rowCount() for r in range(row_len): n_id = self.t_view.model().item(r, 0).data() if n_id == id: self.t_view.model().removeRow(r) remove_from_priority_list(n_id) break def get_model(self, priority_list): model = PriorityListModel(self) config = mw.addonManager.getConfig(__name__) if self.parent.dark_mode_used: tag_bg = config["styles.night.tagBackgroundColor"] tag_fg = config["styles.night.tagForegroundColor"] else: tag_bg = config["styles.tagBackgroundColor"] tag_fg = config["styles.tagForegroundColor"] for c, pitem in enumerate(priority_list): # build display text text = pitem.title if pitem.title is not None and len(pitem.title.strip()) > 0 else "Untitled" text = "<b>%s</b>" % text tags = pitem.tags if tags is not None and len(tags.strip()) > 0: tag_sep = " </span> <span style='color: %s; background-color: %s; margin-right: 5px; border-radius: 5px;'> " % (tag_fg, tag_bg) tags = "<span style='color: %s; background-color: %s; margin-right: 5px; border-radius: 5px;'> %s </span>" % (tag_fg, tag_bg, tag_sep.join([t for t in tags.split(" ") if len(t) > 0])) item = QStandardItem(text) item.setData(QVariant(pitem.id)) item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled) model.setItem(c, 0, item) titem = QStandardItem(tags) titem.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled) model.setItem(c, 1, titem) oitem = QStandardItem() oitem.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled) model.setItem(c, 2, oitem) model.setHeaderData(0, Qt.Horizontal, "Title") model.setHeaderData(1, Qt.Horizontal, "Tags") model.setHeaderData(2, Qt.Horizontal, "Actions") return model def set_remove_btns(self, priority_list): for r in range(len(priority_list)): rem_btn = QPushButton("Remove") if self.parent.dark_mode_used: rem_btn.setStyleSheet("border: 1px solid darkgrey; border-style: outset; font-size: 10px; background: #313233; color: white; margin: 0px; padding: 3px;") else: rem_btn.setStyleSheet("border: 1px solid black; border-style: outset; font-size: 10px; background: white; color: black; margin: 0px; padding: 3px;") rem_btn.setCursor(Qt.PointingHandCursor) rem_btn.setMinimumHeight(18) rem_btn.clicked.connect(functools.partial(self.on_remove_clicked, priority_list[r].id)) h_l = QHBoxLayout() h_l.addWidget(rem_btn) cell_widget = QWidget() cell_widget.setLayout(h_l) self.t_view.setIndexWidget(self.t_view.model().index(r,2), cell_widget) def on_shuffle_btn_clicked(self): priority_list = _get_priority_list() if priority_list is None or len(priority_list) == 0: return random.shuffle(priority_list) model = self.get_model(priority_list) ids = [p.id for p in priority_list] #persist reordering to db set_priority_list(ids) self.t_view.setModel(model) self.set_remove_btns(priority_list) class SettingsTab(QWidget): def __init__(self, parent): QWidget.__init__(self) self.parent = parent self.setup_ui() def setup_ui(self): self.layout = QVBoxLayout() self.layout.addWidget(QLabel("If note is created untagged, give it the following tag(s)")) self.auto_tag_le = QLineEdit() self.auto_tag_le.setText(get_config_value_or_default("notes.editor.defaultTagsIfEmpty", "")) self.auto_tag_le.editingFinished.connect(self.update_default_tags) self.layout.addWidget(self.auto_tag_le) self.auto_fill_with_last_tag_cb = QCheckBox("Auto-fill with last tag(s) on open") self.auto_fill_with_last_tag_cb.setChecked(get_config_value_or_default("notes.editor.autoFillWithLastTagsOnOpen", False)) self.auto_fill_with_last_tag_cb.clicked.connect(self.auto_fill_with_last_tag_cb_clicked) self.layout.addWidget(self.auto_fill_with_last_tag_cb) self.auto_hide_left_pane_cb = QCheckBox("Hide left side by default on open") self.auto_hide_left_pane_cb.setChecked(get_config_value_or_default("notes.editor.autoHideLeftPaneOnOpen", False)) self.auto_hide_left_pane_cb.clicked.connect(self.auto_hide_left_pane_cb_clicked) self.layout.addWidget(self.auto_hide_left_pane_cb) self.layout.addWidget(QLabel("Shortcut for this modal (default \"Ctrl+Shift+n\", requires Anki restart):")) self.shortcut_le = QLineEdit() self.shortcut_le.setText(get_config_value_or_default("notes.editor.shortcut", "Ctrl+Shift+n")) self.layout.addWidget(self.shortcut_le) self.shortcut_le.editingFinished.connect(self.update_shortcut) self.layout.addSpacing(15) lbl = QLabel("Queue Settings") lbl.setAlignment(Qt.AlignCenter) self.layout.addWidget(lbl) line = QFrame() line.setFrameShape(QFrame.HLine) line.setFrameShadow(QFrame.Sunken) self.layout.addWidget(line) self.priority_mod_le = QDoubleSpinBox() self.priority_mod_le.setMinimum(0.1) self.priority_mod_le.setDecimals(1) self.priority_mod_le.setSingleStep(0.1) self.priority_mod_le.setValue(get_config_value_or_default("notes.queue.priorityMod", 1.0)) self.priority_mod_le.valueChanged.connect(self.priority_mod_changed) hb = QHBoxLayout() hb.addWidget(QLabel("Priority Weight (default: 1.0, requires Anki restart):")) hb.addStretch(1) hb.addWidget(self.priority_mod_le) self.layout.addLayout(hb) self.layout.addSpacing(15) self.layout.addWidget(QLabel("Example queue calculation with current settings:")) self.qu_examples = [QLineEdit() for ix in range(0, 13)] for le in self.qu_examples: self.layout.addWidget(le) self.layout.addStretch(1) self.setLayout(self.layout) self.update_queue_example() def auto_hide_left_pane_cb_clicked(self): update_config("notes.editor.autoHideLeftPaneOnOpen", self.auto_hide_left_pane_cb.isChecked()) def auto_fill_with_last_tag_cb_clicked(self): update_config("notes.editor.autoFillWithLastTagsOnOpen", self.auto_fill_with_last_tag_cb.isChecked()) def priority_mod_changed(self, new_val): update_config("notes.queue.priorityMod", new_val) self.update_queue_example() def update_default_tags(self): tags = self.auto_tag_le.text() update_config("notes.editor.defaultTagsIfEmpty", tags) self.update_queue_example def update_shortcut(self): text = self.shortcut_le.text() if text is None or len(text.strip()) == 0: return update_config("notes.editor.shortcut", text) def update_queue_example(self): """ Example queue calculation with the current parameters. """ def _calc_score(priority, days_delta, prio_scale, prio_mod): prio_score = 1 + ((priority - 1) / 99) * (prio_scale - 1) if days_delta < 1: return days_delta + prio_score / 50000 return days_delta + prio_mod * prio_score prio_scale = get_config_value_or_default("notes.queue.priorityScaleFactor", 5) prio_mod = get_config_value_or_default("notes.queue.priorityMod", 1.0) items = [(0.1, 50), (1, 20), (1, 80), (3, 40), (0.5, 90), (0.2, 30), (10, 30), (10, 60), (6.4, 10), (1.4, 20), (0.1, 100), (0.2, 90), (0.01, 90)] scores = [] for item in items: score = _calc_score(item[1], item[0], prio_scale, prio_mod) scores.append((score, item)) scores = sorted(scores, key=lambda x: x[0], reverse=True) for ix, s in enumerate(scores): self.qu_examples[ix].setText(f"{ix+1}. Score: {round(s[0], 2)}, Days since last seen: {s[1][0]}, Priority: {s[1][1]}") self.qu_examples[ix].setReadOnly(True) class PriorityListModel(QStandardItemModel): def __init__(self, parent): super(PriorityListModel, self).__init__() self.parent = parent def dropMimeData(self, data, action, row, col, parent): if row == -1 and (parent is None or parent.row() == -1): return False success = super(PriorityListModel, self).dropMimeData(data, action, row, 0, parent) if success: if row == -1 and parent is not None: row = parent.row() max_row = self.rowCount() ids = list() for i in range(0, max_row): item = self.item(i) data = item.data() ids.append(data) id = ids[row] ids = [i for ix ,i in enumerate(ids) if i != id or ix == row] set_priority_list(ids) rem_btn = QPushButton("Remove") if self.parent.parent.dark_mode_used: rem_btn.setStyleSheet("border: 1px solid darkgrey; border-style: outset; font-size: 10px; background: #313233; color: white; margin: 0px; padding: 3px;") else: rem_btn.setStyleSheet("border: 1px solid black; border-style: outset; font-size: 10px; background: white; margin: 0px; padding: 3px;") rem_btn.setCursor(Qt.PointingHandCursor) rem_btn.setMinimumHeight(18) rem_btn.clicked.connect(functools.partial(self.parent.on_remove_clicked, self.item(row).data())) h_l = QHBoxLayout() h_l.addWidget(rem_btn) cell_widget = QWidget() cell_widget.setLayout(h_l) self.parent.t_view.setIndexWidget(self.index(row,2), cell_widget) return success class BrowseTab(QWidget): def __init__(self): QWidget.__init__(self) def _add_to_tree(self, map): res = [] for t, children in map.items(): ti = QTreeWidgetItem([t]) for c,m in children.items(): ti.addChildren(self._add_to_tree(children)) res.append(ti) return res def tree_item_clicked(self, item, col): tag = item.text(0) pass class HTMLDelegate(QStyledItemDelegate): def __init__(self, parent=None): super().__init__() self.doc = QTextDocument(self) def paint(self, painter, option, index): painter.save() options = QStyleOptionViewItem(option) self.initStyleOption(options, index) self.doc.setHtml(options.text) options.text = "" style = QApplication.style() if options.widget is None \ else options.widget.style() style.drawControl(QStyle.CE_ItemViewItem, options, painter) ctx = QAbstractTextDocumentLayout.PaintContext() if option.state & QStyle.State_Selected: ctx.palette.setColor(QPalette.Text, option.palette.color( QPalette.Active, QPalette.HighlightedText)) else: ctx.palette.setColor(QPalette.Text, option.palette.color( QPalette.Active, QPalette.Text)) textRect = style.subElementRect( QStyle.SE_ItemViewItemText, options) painter.translate(textRect.topLeft()) painter.setClipRect(textRect.translated(-textRect.topLeft())) self.doc.documentLayout().draw(painter, ctx) painter.restore() def sizeHint(self, option, index): return QSize(self.doc.idealWidth(), self.doc.size().height()) class FlowLayout(QLayout): def __init__(self, parent=None, margin=0, spacing=-1): super(FlowLayout, self).__init__(parent) if parent is not None: self.setContentsMargins(margin, margin, margin, margin) self.setSpacing(spacing) self.itemList = [] def __del__(self): item = self.takeAt(0) while item: item = self.takeAt(0) def addItem(self, item): self.itemList.append(item) def count(self): return len(self.itemList) def itemAt(self, index): if index >= 0 and index < len(self.itemList): return self.itemList[index] return None def takeAt(self, index): if index >= 0 and index < len(self.itemList): return self.itemList.pop(index) return None def expandingDirections(self): return Qt.Orientations(Qt.Orientation(0)) def hasHeightForWidth(self): return True def heightForWidth(self, width): height = self.doLayout(QRect(0, 0, width, 0), True) return height def setGeometry(self, rect): super(FlowLayout, self).setGeometry(rect) self.doLayout(rect, False) def sizeHint(self): return self.minimumSize() def minimumSize(self): size = QSize() for item in self.itemList: size = size.expandedTo(item.minimumSize()) margin, _, _, _ = self.getContentsMargins() size += QSize(2 * margin, 2 * margin) return size def doLayout(self, rect, testOnly): x = rect.x() y = rect.y() lineHeight = 0 for item in self.itemList: wid = item.widget() spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal) spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical) nextX = x + item.sizeHint().width() + spaceX if nextX - spaceX > rect.right() and lineHeight > 0: x = rect.x() y = y + lineHeight + spaceY nextX = x + item.sizeHint().width() + spaceX lineHeight = 0 if not testOnly: item.setGeometry(QRect(QPoint(x, y), item.sizeHint())) x = nextX lineHeight = max(lineHeight, item.sizeHint().height()) return y + lineHeight - rect.y()