def __init__(self, parent, bodyhtml, jsSavecommand, wintitle, dialogname): super(ExtraWysiwygEditorForField, self).__init__(parent) self.jsSavecommand = jsSavecommand self.parent = parent self.setWindowTitle(wintitle) self.resize(810, 700) restoreGeom(self, "805891399_winsize") mainLayout = QVBoxLayout() mainLayout.setContentsMargins(0, 0, 0, 0) mainLayout.setSpacing(0) self.setLayout(mainLayout) self.web = MyWebView(self) # maybe also self.parent? self.web.allowDrops = True # default in webview/AnkiWebView is False self.web.title = dialogname self.web.contextMenuEvent = self.contextMenuEvent mainLayout.addWidget(self.web) self.buttonBox = QDialogButtonBox(self) self.buttonBox.setOrientation(Qt.Horizontal) self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Save) mainLayout.addWidget(self.buttonBox) self.buttonBox.accepted.connect(self.onAccept) self.buttonBox.rejected.connect(self.onReject) QMetaObject.connectSlotsByName(self) acceptShortcut = QShortcut(QKeySequence("Ctrl+Return"), self) acceptShortcut.activated.connect(self.onAccept) zoomIn_Shortcut = QShortcut(QKeySequence("Ctrl++"), self) zoomIn_Shortcut.activated.connect(self.web.zoom_in) zoomOut_Shortcut = QShortcut(QKeySequence("Ctrl+-"), self) zoomOut_Shortcut.activated.connect(self.web.zoom_out) self.web.stdHtml(body=bodyhtml, css=cssfiles, js=addon_jsfiles + other_jsfiles, head="", context=self)
def displaygrid(self, config, units): self.generate(config, units) self.win = QDialog(mw) self.wv = KanjiGridWebView() vl = QVBoxLayout() vl.setContentsMargins(0, 0, 0, 0) vl.addWidget(self.wv) self.wv.stdHtml(self.html) hl = QHBoxLayout() vl.addLayout(hl) sh = QPushButton("Save HTML", clicked=lambda: self.savehtml(config)) hl.addWidget(sh) sp = QPushButton("Save Image", clicked=self.savepng) hl.addWidget(sp) bb = QPushButton("Close", clicked=self.win.reject) hl.addWidget(bb) self.win.setLayout(vl) self.win.resize(500, 400) self.timepoint("Window complete") return 0
def __init__(self, parent, rootHtmlPath, size=None): super().__init__(parent) mw.setupDialogGC(self) self.setWindowFlags(Qt.Window) self.setWindowModality(Qt.WindowModal) # Populate content self.web = AnkiWebView() self.web._page._isMiniBrowser = True # Support window.close self.web._page.windowCloseRequested.connect(self.close) l = QVBoxLayout() l.setContentsMargins(0, 0, 0, 0) l.addWidget(self.web) self.setLayout(l) if type(size) == tuple: w, h = size self.resize(w, h) self.show() elif size is None: self.resize(800, 600) self.show() elif size == "maximized" or size == "maximize": self.resize(800, 600) self.showMaximized() elif size == "minimized" or size == "minimize": self.resize(800, 600) self.showMinimized() else: print("MiniBrowser - bad size (%s)" % size) self.resize(800, 600) self.show() # OK self.gotoLocalFile(rootHtmlPath)
def __init__(self, mw, note, ord=0, parent=None, addMode=False): QDialog.__init__(self, parent or mw, Qt.Window) mw.setupDialogGC(self) self.mw = aqt.mw self.parent = parent or mw self.note = note self.ord = ord self.col = self.mw.col self.mm = self.mw.col.models self.model = note.model() self.mw.checkpoint(_("Card Types")) self.addMode = addMode if addMode: # save it to DB temporarily self.emptyFields = [] for name, val in list(note.items()): if val.strip(): continue self.emptyFields.append(name) note[name] = "(%s)" % name note.flush() self.setupTopArea() self.setupMainArea() self.setupButtons() self.setupShortcuts() self.setWindowTitle(_("Card Types for %s") % self.model['name']) v1 = QVBoxLayout() v1.addWidget(self.topArea) v1.addWidget(self.mainArea) v1.addLayout(self.buttons) v1.setContentsMargins(12,12,12,12) self.setLayout(v1) self.redraw() restoreGeom(self, "CardLayout") self.setWindowModality(Qt.ApplicationModal) self.show() # take the focus away from the first input area when starting up, # as users tend to accidentally type into the template self.setFocus()
class Previewer(QDialog): _last_state = None _card_changed = False _last_render: Union[int, float] = 0 _timer = None _show_both_sides = False def __init__(self, parent: QWidget, mw: AnkiQt, on_close: Callable[[], None]): super().__init__(None, Qt.Window) self._open = True self._parent = parent self._close_callback = on_close self.mw = mw icon = QIcon() icon.addPixmap(QPixmap(":/icons/anki.png"), QIcon.Normal, QIcon.Off) self.setWindowIcon(icon) def card(self) -> Optional[Card]: raise NotImplementedError def card_changed(self) -> bool: raise NotImplementedError def open(self): self._state = "question" self._last_state = None self._create_gui() self._setup_web_view() self.render_card() self.show() def _create_gui(self): self.setWindowTitle(tr(TR.ACTIONS_PREVIEW)) qconnect(self.finished, self._on_finished) self.silentlyClose = True self.vbox = QVBoxLayout() self.vbox.setContentsMargins(0, 0, 0, 0) self._web = AnkiWebView(title="previewer") self.vbox.addWidget(self._web) self.bbox = QDialogButtonBox() self._replay = self.bbox.addButton(tr(TR.ACTIONS_REPLAY_AUDIO), QDialogButtonBox.ActionRole) self._replay.setAutoDefault(False) self._replay.setShortcut(QKeySequence("R")) self._replay.setToolTip(tr(TR.ACTIONS_SHORTCUT_KEY, val="R")) qconnect(self._replay.clicked, self._on_replay_audio) both_sides_button = QCheckBox(tr(TR.QT_MISC_BACK_SIDE_ONLY)) both_sides_button.setShortcut(QKeySequence("B")) both_sides_button.setToolTip(tr(TR.ACTIONS_SHORTCUT_KEY, val="B")) self.bbox.addButton(both_sides_button, QDialogButtonBox.ActionRole) self._show_both_sides = self.mw.col.conf.get("previewBothSides", False) both_sides_button.setChecked(self._show_both_sides) qconnect(both_sides_button.toggled, self._on_show_both_sides) self.vbox.addWidget(self.bbox) self.setLayout(self.vbox) restoreGeom(self, "preview") def _on_finished(self, ok): saveGeom(self, "preview") self.mw.progress.timer(100, self._on_close, False) def _on_replay_audio(self): if self._state == "question": replay_audio(self.card(), True) elif self._state == "answer": replay_audio(self.card(), False) def close(self): self._on_close() super().close() def _on_close(self): self._open = False self._close_callback() def _setup_web_view(self): jsinc = [ "js/vendor/jquery.min.js", "js/vendor/browsersel.js", "js/mathjax.js", "js/vendor/mathjax/tex-chtml.js", "js/reviewer.js", ] self._web.stdHtml( self.mw.reviewer.revHtml(), css=["css/reviewer.css"], js=jsinc, context=self, ) self._web.set_bridge_command(self._on_bridge_cmd, self) def _on_bridge_cmd(self, cmd: str) -> Any: if cmd.startswith("play:"): play_clicked_audio(cmd, self.card()) def render_card(self): self.cancel_timer() # Keep track of whether render() has ever been called # with cardChanged=True since the last successful render self._card_changed |= self.card_changed() # avoid rendering in quick succession elap_ms = int((time.time() - self._last_render) * 1000) delay = 300 if elap_ms < delay: self._timer = self.mw.progress.timer(delay - elap_ms, self._render_scheduled, False) else: self._render_scheduled() def cancel_timer(self): if self._timer: self._timer.stop() self._timer = None def _render_scheduled(self) -> None: self.cancel_timer() self._last_render = time.time() if not self._open: return c = self.card() func = "_showQuestion" if not c: txt = tr(TR.QT_MISC_PLEASE_SELECT_1_CARD) bodyclass = "" self._last_state = None else: if self._show_both_sides: self._state = "answer" elif self._card_changed: self._state = "question" currentState = self._state_and_mod() if currentState == self._last_state: # nothing has changed, avoid refreshing return # need to force reload even if answer txt = c.q(reload=True) if self._state == "answer": func = "_showAnswer" txt = c.a() txt = re.sub(r"\[\[type:[^]]+\]\]", "", txt) bodyclass = theme_manager.body_classes_for_card_ord(c.ord) if c.autoplay(): AnkiWebView.setPlaybackRequiresGesture(False) if self._show_both_sides: # if we're showing both sides at once, remove any audio # from the answer that's appeared on the question already question_audio = c.question_av_tags() only_on_answer_audio = [ x for x in c.answer_av_tags() if x not in question_audio ] audio = question_audio + only_on_answer_audio elif self._state == "question": audio = c.question_av_tags() else: audio = c.answer_av_tags() av_player.play_tags(audio) else: AnkiWebView.setPlaybackRequiresGesture(True) av_player.clear_queue_and_maybe_interrupt() txt = self.mw.prepare_card_text_for_display(txt) txt = gui_hooks.card_will_show( txt, c, "preview" + self._state.capitalize()) self._last_state = self._state_and_mod() self._web.eval("{}({},'{}');".format(func, json.dumps(txt), bodyclass)) self._card_changed = False def _on_show_both_sides(self, toggle): self._show_both_sides = toggle self.mw.col.conf["previewBothSides"] = toggle self.mw.col.setMod() if self._state == "answer" and not toggle: self._state = "question" self.render_card() def _state_and_mod(self): c = self.card() n = c.note() n.load() return (self._state, c.id, n.mod) def state(self) -> str: return self._state
def setupOuter(self): l = QVBoxLayout() l.setContentsMargins(0,0,0,0) l.setSpacing(0) self.widget.setLayout(l) self.outerLayout = l
class AnkiEmperor(QDialog): def __init__(self): mw.addonManager.setWebExports("AnkiEmperor", ".*") # Setup self.db = DBConnect() self.__treasureChest = TreasureChest(self.db) self.__options = Options(self.db) self.__eventManager = EventManager(self, self.__options, self.__treasureChest) self.__stats = Stats(self.db, self.__eventManager) world = World(self.db, self.__options.getOption("activeCountry")) self.__buildingAuthority = BuildingAuthority(self, world) self.__ranks = Ranks(self.db, self.__eventManager, world) self.__ranks.updateRank( self.__treasureChest.getTotalGold(), self.__buildingAuthority.getActiveCountry(). getCompletedObjectsPercentage(), True, ) self.__layout = None # Setup as a property as we must be able to clear it # Keep's track of current view. Useful if we want to update a view, but we're not sure which one self.__view = None self.deckSelected = False # Setup window QDialog.__init__(self, mw, Qt.WindowTitleHint) self.setWindowTitle(getPluginName()) self.resize(300, 800) gui_hooks.reviewer_did_answer_card.append(self.answerCard) gui_hooks.webview_did_receive_js_message.append(self.links) self.open_main() # Wrap Anki methods # This should probably be handled better # Should each view have its own call to did_receive_js_message? # FIXME Investigate if there are native versions of these functions.. _Collection.undo = wrap(_Collection.undo, self.undo) DeckManager.select = wrap(DeckManager.select, self.refreshSettings) # Add AnkiEmperor to the Anki menu action = QAction(getPluginName(), mw) action.triggered.connect(self.show) # mw.connect(action, SIGNAL("triggered()"), self.show) mw.form.menuTools.addAction(action) # remember how the card was answered: easy, good, ... def setQuality(self, quality): self.__lastQuality = quality # Gets def getTreasureChest(self): return self.__treasureChest def getBuildingAuthority(self): return self.__buildingAuthority def getRanks(self): return self.__ranks def getOptions(self): return self.__options def getEventManager(self): return self.__eventManager # Sets def setView(self, view): self.__view = view # Show window # Make sure AnkiEmperor shows to the right or left of Anki if possible def show(self): # Can we display it on the right? if (QDesktopWidget().width() - mw.pos().x() - mw.width() - self.width() - 50) > 0: self.move(mw.pos().x() + mw.width() + 50, mw.pos().y() - 100) # Can we display it on the left? elif (QDesktopWidget().width() - mw.pos().x() + self.width() + 50) < QDesktopWidget().width(): self.move(mw.pos().x() - self.width() - 50, mw.pos().y() - 100) # Show window super(AnkiEmperor, self).show() # This hook makes sure that AnkiEmperor shows after Anki has loaded. # This lets us show AnkiEmperor in the correct position def onProfileLoaded(self): # Show window if required. if self.__options.getOption("openOnLaunch"): self.show() # Take gold away if card undone def undo(self, _Collection): if self.__options.getOption("pluginEnabled"): self.__treasureChest.undo() self.__buildingAuthority.undo() self.__stats.undo() self.open_main() self.__buildingAuthority.save() self.__stats.save() # Update AnkiEmperor when we answer a card def answerCard(self, Reviewer, card, ease): if self.__options.getOption("pluginEnabled"): self.setQuality(ease) cardsAnsweredToday = self.__stats.cardAnswered(self.__lastQuality) self.__stats.save() # Update the building process self.__buildingAuthority.updateBuildingProgress( self.__lastQuality, cardsAnsweredToday) self.__buildingAuthority.save() # Update rank self.__ranks.updateRank( self.__treasureChest.getTotalGold(), self.__buildingAuthority.getActiveCountry(). getCompletedObjectsPercentage(), False, ) # Display popup and perform event action whenever a major event has occured event = self.__eventManager.getNextEvent() if event: msg = QMessageBox() msg.setIcon(QMessageBox.NoIcon) msg.setWindowTitle(getPluginName()) msg.setText(event.performEventAndGetText()) msg.addButton("OK", QMessageBox.AcceptRole) msg.exec_() # calculate earned gold self.__treasureChest.updateGold(card, self.__lastQuality, cardsAnsweredToday, False) self.__treasureChest.save() self.open_main() # Update the Anki Emperor Window def updateWindow(self, html): if html is None: return False # build view webview = AnkiWebView() webview.stdHtml(html) # Clear old layout if self.__layout: QObjectCleanupHandler().add(self.__layout) # build layout self.__layout = QVBoxLayout() self.__layout.setContentsMargins(0, 0, 0, 0) self.__layout.addWidget(webview) # Update window self.setLayout(self.__layout) self.update() def open_main(self) -> None: m = MainView( self.deckSelected, self.__treasureChest, self.__buildingAuthority, self.__ranks, self.__options, ) self.__view = m.__class__.__name__ self.updateWindow(m.main()) def open_settings(self) -> None: m = SettingsView(self.__options, self.deckSelected, self.open_settings) self.__view = m.__class__.__name__ self.updateWindow(m.main()) # Anki's pycmd syntax is kind of like React's action/reducers # if you squint your eyes a little bit. # Except, that it's really an entire API by itself # So, it's probably a good idea to treat it like a HTTP request, kinda. # So, this function is like a HTTP-request handler (with requests pre-parsed in JSON) # This function has to return valid JSON def process_request(self, request: Dict[str, Any], context: Any) -> Any: raise Exception("HIDE") def h(*_): raise Exception("HIDE") handlers = { OPEN_MAIN: lambda _: self._open_main(), HIDE: h, } # lambda _: raise Exception("HIDE")} #self.hide()} return handlers[request["type"]](request["payload"]) def links(self, handled, message, context): QMessageBox(self) raise Exception("HIDE") ret = self.process_request(json.loads(message), context) return (True, ret) # Refresh the settings if the deck is changed def refreshSettings(self, DeckManager, did): if mw.col is not None: self.deckSelected = True deck = mw.col.decks.current() self.__options.readDeckOptions(deck["id"]) if self.__view.__class__.__name__ == "SettingsView": self.open_settings() # Show window if autoOpen is enabled if self.__options.getOption("autoOpen") and self.isHidden(): self.show()
class Previewer(QDialog): _last_state: LastStateAndMod | None = None _card_changed = False _last_render: int | float = 0 _timer: QTimer | None = None _show_both_sides = False def __init__(self, parent: QWidget, mw: AnkiQt, on_close: Callable[[], None]) -> None: super().__init__(None, Qt.WindowType.Window) mw.garbage_collect_on_dialog_finish(self) self._open = True self._parent = parent self._close_callback = on_close self.mw = mw disable_help_button(self) setWindowIcon(self) def card(self) -> Card | None: raise NotImplementedError def card_changed(self) -> bool: raise NotImplementedError def open(self) -> None: self._state = "question" self._last_state = None self._create_gui() self._setup_web_view() self.render_card() self.show() def _create_gui(self) -> None: self.setWindowTitle(tr.actions_preview()) self.close_shortcut = QShortcut(QKeySequence("Ctrl+Shift+P"), self) qconnect(self.close_shortcut.activated, self.close) qconnect(self.finished, self._on_finished) self.silentlyClose = True self.vbox = QVBoxLayout() self.vbox.setContentsMargins(0, 0, 0, 0) self._web = AnkiWebView(title="previewer") self.vbox.addWidget(self._web) self.bbox = QDialogButtonBox() self._replay = self.bbox.addButton( tr.actions_replay_audio(), QDialogButtonBox.ButtonRole.ActionRole) self._replay.setAutoDefault(False) self._replay.setShortcut(QKeySequence("R")) self._replay.setToolTip(tr.actions_shortcut_key(val="R")) qconnect(self._replay.clicked, self._on_replay_audio) both_sides_button = QCheckBox(tr.qt_misc_back_side_only()) both_sides_button.setShortcut(QKeySequence("B")) both_sides_button.setToolTip(tr.actions_shortcut_key(val="B")) self.bbox.addButton(both_sides_button, QDialogButtonBox.ButtonRole.ActionRole) self._show_both_sides = self.mw.col.get_config_bool( Config.Bool.PREVIEW_BOTH_SIDES) both_sides_button.setChecked(self._show_both_sides) qconnect(both_sides_button.toggled, self._on_show_both_sides) self.vbox.addWidget(self.bbox) self.setLayout(self.vbox) restoreGeom(self, "preview") def _on_finished(self, ok: int) -> None: saveGeom(self, "preview") self.mw.progress.timer(100, self._on_close, False) def _on_replay_audio(self) -> None: if self._state == "question": replay_audio(self.card(), True) elif self._state == "answer": replay_audio(self.card(), False) def close(self) -> None: self._on_close() super().close() def _on_close(self) -> None: self._open = False self._close_callback() self._web = None def _setup_web_view(self) -> None: self._web.stdHtml( self.mw.reviewer.revHtml(), css=["css/reviewer.css"], js=[ "js/mathjax.js", "js/vendor/mathjax/tex-chtml.js", "js/reviewer.js", ], context=self, ) self._web.set_bridge_command(self._on_bridge_cmd, self) def _on_bridge_cmd(self, cmd: str) -> Any: if cmd.startswith("play:"): play_clicked_audio(cmd, self.card()) def _update_flag_and_mark_icons(self, card: Card | None) -> None: if card: flag = card.user_flag() marked = card.note(reload=True).has_tag(MARKED_TAG) else: flag = 0 marked = False self._web.eval(f"_drawFlag({flag}); _drawMark({json.dumps(marked)});") def render_card(self) -> None: self.cancel_timer() # Keep track of whether render() has ever been called # with cardChanged=True since the last successful render self._card_changed |= self.card_changed() # avoid rendering in quick succession elap_ms = int((time.time() - self._last_render) * 1000) delay = 300 if elap_ms < delay: self._timer = self.mw.progress.timer(delay - elap_ms, self._render_scheduled, False) else: self._render_scheduled() def cancel_timer(self) -> None: if self._timer: self._timer.stop() self._timer = None def _render_scheduled(self) -> None: self.cancel_timer() self._last_render = time.time() if not self._open: return c = self.card() self._update_flag_and_mark_icons(c) func = "_showQuestion" ans_txt = "" if not c: txt = tr.qt_misc_please_select_1_card() bodyclass = "" self._last_state = None else: if self._show_both_sides: self._state = "answer" elif self._card_changed: self._state = "question" currentState = self._state_and_mod() if currentState == self._last_state: # nothing has changed, avoid refreshing return # need to force reload even if answer txt = c.question(reload=True) ans_txt = c.answer() if self._state == "answer": func = "_showAnswer" txt = ans_txt txt = re.sub(r"\[\[type:[^]]+\]\]", "", txt) bodyclass = theme_manager.body_classes_for_card_ord(c.ord) if c.autoplay(): if self._show_both_sides: # if we're showing both sides at once, remove any audio # from the answer that's appeared on the question already question_audio = c.question_av_tags() only_on_answer_audio = [ x for x in c.answer_av_tags() if x not in question_audio ] audio = question_audio + only_on_answer_audio elif self._state == "question": audio = c.question_av_tags() else: audio = c.answer_av_tags() av_player.play_tags(audio) else: av_player.clear_queue_and_maybe_interrupt() txt = self.mw.prepare_card_text_for_display(txt) txt = gui_hooks.card_will_show( txt, c, f"preview{self._state.capitalize()}") self._last_state = self._state_and_mod() js: str if self._state == "question": ans_txt = self.mw.col.media.escape_media_filenames(ans_txt) js = f"{func}({json.dumps(txt)}, {json.dumps(ans_txt)}, '{bodyclass}');" else: js = f"{func}({json.dumps(txt)}, '{bodyclass}');" self._web.eval(js) self._card_changed = False def _on_show_both_sides(self, toggle: bool) -> None: self._show_both_sides = toggle self.mw.col.set_config_bool(Config.Bool.PREVIEW_BOTH_SIDES, toggle) if self._state == "answer" and not toggle: self._state = "question" self.render_card() def _state_and_mod(self) -> tuple[str, int, int]: c = self.card() n = c.note() n.load() return (self._state, c.id, n.mod) def state(self) -> str: return self._state
class AnkiQt(QMainWindow): def __init__(self, app, profileManager, opts, args): QMainWindow.__init__(self) self.state = "startup" self.opts = opts aqt.mw = self self.app = app self.pm = profileManager # init rest of app self.safeMode = self.app.queryKeyboardModifiers() & Qt.ShiftModifier try: self.setupUI() self.setupAddons() except: showInfo(_("Error during startup:\n%s") % traceback.format_exc()) sys.exit(1) # must call this after ui set up if self.safeMode: tooltip(_("Shift key was held down. Skipping automatic " "syncing and add-on loading.")) # were we given a file to import? if args and args[0]: self.onAppMsg(args[0]) # Load profile in a timer so we can let the window finish init and not # close on profile load error. if isWin: fn = self.setupProfileAfterWebviewsLoaded else: fn = self.setupProfile self.progress.timer(10, fn, False, requiresCollection=False) def setupUI(self): self.col = None self.setupCrashLog() self.disableGC() self.setupAppMsg() self.setupKeys() self.setupThreads() self.setupMediaServer() self.setupSound() self.setupSpellCheck() self.setupMainWindow() self.setupSystemSpecific() self.setupStyle() self.setupMenus() self.setupProgress() self.setupErrorHandler() self.setupSignals() self.setupAutoUpdate() self.setupHooks() self.setupRefreshTimer() self.updateTitleBar() # screens self.setupDeckBrowser() self.setupOverview() self.setupReviewer() def setupProfileAfterWebviewsLoaded(self): for w in (self.web, self.bottomWeb): if not w._domDone: self.progress.timer(10, self.setupProfileAfterWebviewsLoaded, False, requiresCollection=False) return else: w.requiresCol = True self.setupProfile() # Profiles ########################################################################## class ProfileManager(QMainWindow): onClose = pyqtSignal() closeFires = True def closeEvent(self, evt): if self.closeFires: self.onClose.emit() evt.accept() def closeWithoutQuitting(self): self.closeFires = False self.close() self.closeFires = True def setupProfile(self): if self.pm.meta['firstRun']: # load the new deck user profile self.pm.load(self.pm.profiles()[0]) self.pm.meta['firstRun'] = False self.pm.save() self.pendingImport = None self.restoringBackup = False # profile not provided on command line? if not self.pm.name: # if there's a single profile, load it automatically profs = self.pm.profiles() if len(profs) == 1: self.pm.load(profs[0]) if not self.pm.name: self.showProfileManager() else: self.loadProfile() def showProfileManager(self): self.pm.profile = None self.state = "profileManager" d = self.profileDiag = self.ProfileManager() f = self.profileForm = aqt.forms.profiles.Ui_MainWindow() f.setupUi(d) f.login.clicked.connect(self.onOpenProfile) f.profiles.itemDoubleClicked.connect(self.onOpenProfile) f.openBackup.clicked.connect(self.onOpenBackup) f.quit.clicked.connect(d.close) d.onClose.connect(self.cleanupAndExit) f.add.clicked.connect(self.onAddProfile) f.rename.clicked.connect(self.onRenameProfile) f.delete_2.clicked.connect(self.onRemProfile) f.profiles.currentRowChanged.connect(self.onProfileRowChange) f.statusbar.setVisible(False) # enter key opens profile QShortcut(QKeySequence("Return"), d, activated=self.onOpenProfile) self.refreshProfilesList() # raise first, for osx testing d.show() d.activateWindow() d.raise_() def refreshProfilesList(self): f = self.profileForm f.profiles.clear() profs = self.pm.profiles() f.profiles.addItems(profs) try: idx = profs.index(self.pm.name) except: idx = 0 f.profiles.setCurrentRow(idx) def onProfileRowChange(self, n): if n < 0: # called on .clear() return name = self.pm.profiles()[n] f = self.profileForm self.pm.load(name) def openProfile(self): name = self.pm.profiles()[self.profileForm.profiles.currentRow()] return self.pm.load(name) def onOpenProfile(self): self.loadProfile(self.profileDiag.closeWithoutQuitting) def profileNameOk(self, str): return not checkInvalidFilename(str) def onAddProfile(self): name = getOnlyText(_("Name:")) if name: name = name.strip() if name in self.pm.profiles(): return showWarning(_("Name exists.")) if not self.profileNameOk(name): return self.pm.create(name) self.pm.name = name self.refreshProfilesList() def onRenameProfile(self): name = getOnlyText(_("New name:"), default=self.pm.name) if not name: return if name == self.pm.name: return if name in self.pm.profiles(): return showWarning(_("Name exists.")) if not self.profileNameOk(name): return self.pm.rename(name) self.refreshProfilesList() def onRemProfile(self): profs = self.pm.profiles() if len(profs) < 2: return showWarning(_("There must be at least one profile.")) # sure? if not askUser(_("""\ All cards, notes, and media for this profile will be deleted. \ Are you sure?"""), msgfunc=QMessageBox.warning, defaultno=True): return self.pm.remove(self.pm.name) self.refreshProfilesList() def onOpenBackup(self): if not askUser(_("""\ Replace your collection with an earlier backup?"""), msgfunc=QMessageBox.warning, defaultno=True): return def doOpen(path): self._openBackup(path) getFile(self.profileDiag, _("Revert to backup"), cb=doOpen, filter="*.colpkg", dir=self.pm.backupFolder()) def _openBackup(self, path): try: # move the existing collection to the trash, as it may not open self.pm.trashCollection() except: showWarning(_("Unable to move existing file to trash - please try restarting your computer.")) return self.pendingImport = path self.restoringBackup = True showInfo(_("""\ Automatic syncing and backups have been disabled while restoring. To enable them again, \ close the profile or restart Anki.""")) self.onOpenProfile() def loadProfile(self, onsuccess=None): self.maybeAutoSync() if not self.loadCollection(): return # show main window if self.pm.profile['mainWindowState']: restoreGeom(self, "mainWindow") restoreState(self, "mainWindow") # titlebar self.setWindowTitle(self.pm.name + " - Anki") # show and raise window for osx self.show() self.activateWindow() self.raise_() # import pending? if self.pendingImport: self.handleImport(self.pendingImport) self.pendingImport = None runHook("profileLoaded") if onsuccess: onsuccess() def unloadProfile(self, onsuccess): def callback(): self._unloadProfile() onsuccess() runHook("unloadProfile") self.unloadCollection(callback) def _unloadProfile(self): self.pm.profile['mainWindowGeom'] = self.saveGeometry() self.pm.profile['mainWindowState'] = self.saveState() self.pm.save() self.hide() self.restoringBackup = False # at this point there should be no windows left self._checkForUnclosedWidgets() self.maybeAutoSync() def _checkForUnclosedWidgets(self): for w in self.app.topLevelWidgets(): if w.isVisible(): # windows with this property are safe to close immediately if getattr(w, "silentlyClose", None): w.close() else: print("Window should have been closed: {}".format(w)) def unloadProfileAndExit(self): self.unloadProfile(self.cleanupAndExit) def unloadProfileAndShowProfileManager(self): self.unloadProfile(self.showProfileManager) def cleanupAndExit(self): self.errorHandler.unload() self.mediaServer.shutdown() anki.sound.cleanupMPV() self.app.exit(0) # Sound/video ########################################################################## def setupSound(self): if isWin: return try: anki.sound.setupMPV() except FileNotFoundError: print("mpv not found, reverting to mplayer") except anki.mpv.MPVProcessError: print("mpv too old, reverting to mplayer") # Collection load/unload ########################################################################## def loadCollection(self): try: return self._loadCollection() except Exception as e: showWarning(_("""\ Anki was unable to open your collection file. If problems persist after \ restarting your computer, please use the Open Backup button in the profile \ manager. Debug info: """)+traceback.format_exc()) # clean up open collection if possible if self.col: try: self.col.close(save=False) except: pass self.col = None # return to profile manager self.showProfileManager() return False def _loadCollection(self): cpath = self.pm.collectionPath() self.col = Collection(cpath, log=True) self.setEnabled(True) self.progress.setupDB(self.col.db) self.maybeEnableUndo() self.moveToState("deckBrowser") return True def unloadCollection(self, onsuccess): def callback(): self.setEnabled(False) self._unloadCollection() onsuccess() self.closeAllWindows(callback) def _unloadCollection(self): if not self.col: return if self.restoringBackup: label = _("Closing...") else: label = _("Backing Up...") self.progress.start(label=label, immediate=True) corrupt = False try: self.maybeOptimize() if not devMode: corrupt = self.col.db.scalar("pragma integrity_check") != "ok" except: corrupt = True try: self.col.close() except: corrupt = True finally: self.col = None if corrupt: showWarning(_("Your collection file appears to be corrupt. \ This can happen when the file is copied or moved while Anki is open, or \ when the collection is stored on a network or cloud drive. If problems \ persist after restarting your computer, please open an automatic backup \ from the profile screen.")) if not corrupt and not self.restoringBackup: self.backup() self.progress.finish() # Backup and auto-optimize ########################################################################## class BackupThread(Thread): def __init__(self, path, data): Thread.__init__(self) self.path = path self.data = data # create the file in calling thread to ensure the same # file is not created twice open(self.path, "wb").close() def run(self): z = zipfile.ZipFile(self.path, "w", zipfile.ZIP_DEFLATED) z.writestr("collection.anki2", self.data) z.writestr("media", "{}") z.close() def backup(self): nbacks = self.pm.profile['numBackups'] if not nbacks or devMode: return dir = self.pm.backupFolder() path = self.pm.collectionPath() # do backup fname = time.strftime("backup-%Y-%m-%d-%H.%M.%S.colpkg", time.localtime(time.time())) newpath = os.path.join(dir, fname) with open(path, "rb") as f: data = f.read() b = self.BackupThread(newpath, data) b.start() # find existing backups backups = [] for file in os.listdir(dir): # only look for new-style format m = re.match(r"backup-\d{4}-\d{2}-.+.colpkg", file) if not m: continue backups.append(file) backups.sort() # remove old ones while len(backups) > nbacks: fname = backups.pop(0) path = os.path.join(dir, fname) os.unlink(path) def maybeOptimize(self): # have two weeks passed? if (intTime() - self.pm.profile['lastOptimize']) < 86400*14: return self.progress.start(label=_("Optimizing..."), immediate=True) self.col.optimize() self.pm.profile['lastOptimize'] = intTime() self.pm.save() self.progress.finish() # State machine ########################################################################## def moveToState(self, state, *args): #print("-> move from", self.state, "to", state) oldState = self.state or "dummy" cleanup = getattr(self, "_"+oldState+"Cleanup", None) if cleanup: # pylint: disable=not-callable cleanup(state) self.clearStateShortcuts() self.state = state runHook('beforeStateChange', state, oldState, *args) getattr(self, "_"+state+"State")(oldState, *args) if state != "resetRequired": self.bottomWeb.show() runHook('afterStateChange', state, oldState, *args) def _deckBrowserState(self, oldState): self.deckBrowser.show() def _colLoadingState(self, oldState): "Run once, when col is loaded." self.enableColMenuItems() # ensure cwd is set if media dir exists self.col.media.dir() runHook("colLoading", self.col) self.moveToState("overview") def _selectedDeck(self): did = self.col.decks.selected() if not self.col.decks.nameOrNone(did): showInfo(_("Please select a deck.")) return return self.col.decks.get(did) def _overviewState(self, oldState): if not self._selectedDeck(): return self.moveToState("deckBrowser") self.col.reset() self.overview.show() def _reviewState(self, oldState): self.reviewer.show() def _reviewCleanup(self, newState): if newState != "resetRequired" and newState != "review": self.reviewer.cleanup() def noteChanged(self, nid): "Called when a card or note is edited (but not deleted)." runHook("noteChanged", nid) # Resetting state ########################################################################## def reset(self, guiOnly=False): "Called for non-trivial edits. Rebuilds queue and updates UI." if self.col: if not guiOnly: self.col.reset() runHook("reset") self.maybeEnableUndo() self.moveToState(self.state) def requireReset(self, modal=False): "Signal queue needs to be rebuilt when edits are finished or by user." self.autosave() self.resetModal = modal if self.interactiveState(): self.moveToState("resetRequired") def interactiveState(self): "True if not in profile manager, syncing, etc." return self.state in ("overview", "review", "deckBrowser") def maybeReset(self): self.autosave() if self.state == "resetRequired": self.state = self.returnState self.reset() def delayedMaybeReset(self): # if we redraw the page in a button click event it will often crash on # windows self.progress.timer(100, self.maybeReset, False) def _resetRequiredState(self, oldState): if oldState != "resetRequired": self.returnState = oldState if self.resetModal: # we don't have to change the webview, as we have a covering window return self.web.resetHandlers() self.web.onBridgeCmd = lambda url: self.delayedMaybeReset() i = _("Waiting for editing to finish.") b = self.button("refresh", _("Resume Now"), id="resume") self.web.stdHtml(""" <center><div style="height: 100%%"> <div style="position:relative; vertical-align: middle;"> %s<br><br> %s</div></div></center> <script>$('#resume').focus()</script> """ % (i, b)) self.bottomWeb.hide() self.web.setFocus() # HTML helpers ########################################################################## def button(self, link, name, key=None, class_="", id="", extra=""): class_ = "but "+ class_ if key: key = _("Shortcut key: %s") % key else: key = "" return ''' <button id="%s" class="%s" onclick="pycmd('%s');return false;" title="%s" %s>%s</button>''' % ( id, class_, link, key, extra, name) # Main window setup ########################################################################## def setupMainWindow(self): # main window self.form = aqt.forms.main.Ui_MainWindow() self.form.setupUi(self) # toolbar tweb = self.toolbarWeb = aqt.webview.AnkiWebView() tweb.title = "top toolbar" tweb.setFocusPolicy(Qt.WheelFocus) self.toolbar = aqt.toolbar.Toolbar(self, tweb) self.toolbar.draw() # main area self.web = aqt.webview.AnkiWebView() self.web.title = "main webview" self.web.setFocusPolicy(Qt.WheelFocus) self.web.setMinimumWidth(400) # bottom area sweb = self.bottomWeb = aqt.webview.AnkiWebView() sweb.title = "bottom toolbar" sweb.setFocusPolicy(Qt.WheelFocus) # add in a layout self.mainLayout = QVBoxLayout() self.mainLayout.setContentsMargins(0,0,0,0) self.mainLayout.setSpacing(0) self.mainLayout.addWidget(tweb) self.mainLayout.addWidget(self.web) self.mainLayout.addWidget(sweb) self.form.centralwidget.setLayout(self.mainLayout) # force webengine processes to load before cwd is changed if isWin: for o in self.web, self.bottomWeb: o.requiresCol = False o._domReady = False o._page.setContent(bytes("", "ascii")) def closeAllWindows(self, onsuccess): aqt.dialogs.closeAll(onsuccess) # Components ########################################################################## def setupSignals(self): signal.signal(signal.SIGINT, self.onSigInt) def onSigInt(self, signum, frame): # interrupt any current transaction and schedule a rollback & quit if self.col: self.col.db.interrupt() def quit(): self.col.db.rollback() self.close() self.progress.timer(100, quit, False) def setupProgress(self): self.progress = aqt.progress.ProgressManager(self) def setupErrorHandler(self): import aqt.errors self.errorHandler = aqt.errors.ErrorHandler(self) def setupAddons(self): import aqt.addons self.addonManager = aqt.addons.AddonManager(self) if not self.safeMode: self.addonManager.loadAddons() def setupSpellCheck(self): os.environ["QTWEBENGINE_DICTIONARIES_PATH"] = ( os.path.join(self.pm.base, "dictionaries")) def setupThreads(self): self._mainThread = QThread.currentThread() def inMainThread(self): return self._mainThread == QThread.currentThread() def setupDeckBrowser(self): from aqt.deckbrowser import DeckBrowser self.deckBrowser = DeckBrowser(self) def setupOverview(self): from aqt.overview import Overview self.overview = Overview(self) def setupReviewer(self): from aqt.reviewer import Reviewer self.reviewer = Reviewer(self) # Syncing ########################################################################## # expects a current profile and a loaded collection; reloads # collection after sync completes def onSync(self): self.unloadCollection(self._onSync) def _onSync(self): self._sync() if not self.loadCollection(): return # expects a current profile, but no collection loaded def maybeAutoSync(self): if (not self.pm.profile['syncKey'] or not self.pm.profile['autoSync'] or self.safeMode or self.restoringBackup): return # ok to sync self._sync() def _sync(self): from aqt.sync import SyncManager self.state = "sync" self.syncer = SyncManager(self, self.pm) self.syncer.sync() # Tools ########################################################################## def raiseMain(self): if not self.app.activeWindow(): # make sure window is shown self.setWindowState(self.windowState() & ~Qt.WindowMinimized) return True def setupStyle(self): buf = "" if isWin and platform.release() == '10': # add missing bottom border to menubar buf += """ QMenuBar { border-bottom: 1px solid #aaa; background: white; } """ # qt bug? setting the above changes the browser sidebar # to white as well, so set it back buf += """ QTreeWidget { background: #eee; } """ # allow addons to modify the styling buf = runFilter("setupStyle", buf) # allow users to extend styling p = os.path.join(aqt.mw.pm.base, "style.css") if os.path.exists(p): buf += open(p).read() self.app.setStyleSheet(buf) # Key handling ########################################################################## def setupKeys(self): globalShortcuts = [ ("Ctrl+:", self.onDebug), ("d", lambda: self.moveToState("deckBrowser")), ("s", self.onStudyKey), ("a", self.onAddCard), ("b", self.onBrowse), ("t", self.onStats), ("y", self.onSync) ] self.applyShortcuts(globalShortcuts) self.stateShortcuts = [] def applyShortcuts(self, shortcuts): qshortcuts = [] for key, fn in shortcuts: scut = QShortcut(QKeySequence(key), self, activated=fn) scut.setAutoRepeat(False) qshortcuts.append(scut) return qshortcuts def setStateShortcuts(self, shortcuts): runHook(self.state+"StateShortcuts", shortcuts) self.stateShortcuts = self.applyShortcuts(shortcuts) def clearStateShortcuts(self): for qs in self.stateShortcuts: sip.delete(qs) self.stateShortcuts = [] def onStudyKey(self): if self.state == "overview": self.col.startTimebox() self.moveToState("review") else: self.moveToState("overview") # App exit ########################################################################## def closeEvent(self, event): if self.state == "profileManager": # if profile manager active, this event may fire via OS X menu bar's # quit option self.profileDiag.close() event.accept() else: # ignore the event for now, as we need time to clean up event.ignore() self.unloadProfileAndExit() # Undo & autosave ########################################################################## def onUndo(self): n = self.col.undoName() if not n: return cid = self.col.undo() if cid and self.state == "review": card = self.col.getCard(cid) self.col.sched.reset() self.reviewer.cardQueue.append(card) self.reviewer.nextCard() self.maybeEnableUndo() return tooltip(_("Reverted to state prior to '%s'.") % n.lower()) self.reset() self.maybeEnableUndo() def maybeEnableUndo(self): if self.col and self.col.undoName(): self.form.actionUndo.setText(_("Undo %s") % self.col.undoName()) self.form.actionUndo.setEnabled(True) runHook("undoState", True) else: self.form.actionUndo.setText(_("Undo")) self.form.actionUndo.setEnabled(False) runHook("undoState", False) def checkpoint(self, name): self.col.save(name) self.maybeEnableUndo() def autosave(self): saved = self.col.autosave() self.maybeEnableUndo() if saved: self.doGC() # Other menu operations ########################################################################## def onAddCard(self): aqt.dialogs.open("AddCards", self) def onBrowse(self): aqt.dialogs.open("Browser", self) def onEditCurrent(self): aqt.dialogs.open("EditCurrent", self) def onDeckConf(self, deck=None): if not deck: deck = self.col.decks.current() if deck['dyn']: import aqt.dyndeckconf aqt.dyndeckconf.DeckConf(self, deck=deck) else: import aqt.deckconf aqt.deckconf.DeckConf(self, deck) def onOverview(self): self.col.reset() self.moveToState("overview") def onStats(self): deck = self._selectedDeck() if not deck: return aqt.dialogs.open("DeckStats", self) def onPrefs(self): aqt.dialogs.open("Preferences", self) def onNoteTypes(self): import aqt.models aqt.models.Models(self, self, fromMain=True) def onAbout(self): aqt.dialogs.open("About", self) def onDonate(self): openLink(aqt.appDonate) def onDocumentation(self): openHelp("") # Importing & exporting ########################################################################## def handleImport(self, path): import aqt.importing if not os.path.exists(path): return showInfo(_("Please use File>Import to import this file.")) aqt.importing.importFile(self, path) def onImport(self): import aqt.importing aqt.importing.onImport(self) def onExport(self, did=None): import aqt.exporting aqt.exporting.ExportDialog(self, did=did) # Cramming ########################################################################## def onCram(self, search=""): import aqt.dyndeckconf n = 1 deck = self.col.decks.current() if not search: if not deck['dyn']: search = 'deck:"%s" ' % deck['name'] decks = self.col.decks.allNames() while _("Filtered Deck %d") % n in decks: n += 1 name = _("Filtered Deck %d") % n did = self.col.decks.newDyn(name) diag = aqt.dyndeckconf.DeckConf(self, first=True, search=search) if not diag.ok: # user cancelled first config self.col.decks.rem(did) self.col.decks.select(deck['id']) # Menu, title bar & status ########################################################################## def setupMenus(self): m = self.form m.actionSwitchProfile.triggered.connect( self.unloadProfileAndShowProfileManager) m.actionImport.triggered.connect(self.onImport) m.actionExport.triggered.connect(self.onExport) m.actionExit.triggered.connect(self.close) m.actionPreferences.triggered.connect(self.onPrefs) m.actionAbout.triggered.connect(self.onAbout) m.actionUndo.triggered.connect(self.onUndo) if qtminor < 11: m.actionUndo.setShortcut(QKeySequence(_("Ctrl+Alt+Z"))) m.actionFullDatabaseCheck.triggered.connect(self.onCheckDB) m.actionCheckMediaDatabase.triggered.connect(self.onCheckMediaDB) m.actionDocumentation.triggered.connect(self.onDocumentation) m.actionDonate.triggered.connect(self.onDonate) m.actionStudyDeck.triggered.connect(self.onStudyDeck) m.actionCreateFiltered.triggered.connect(self.onCram) m.actionEmptyCards.triggered.connect(self.onEmptyCards) m.actionNoteTypes.triggered.connect(self.onNoteTypes) def updateTitleBar(self): self.setWindowTitle("Anki") # Auto update ########################################################################## def setupAutoUpdate(self): import aqt.update self.autoUpdate = aqt.update.LatestVersionFinder(self) self.autoUpdate.newVerAvail.connect(self.newVerAvail) self.autoUpdate.newMsg.connect(self.newMsg) self.autoUpdate.clockIsOff.connect(self.clockIsOff) self.autoUpdate.start() def newVerAvail(self, ver): if self.pm.meta.get('suppressUpdate', None) != ver: aqt.update.askAndUpdate(self, ver) def newMsg(self, data): aqt.update.showMessages(self, data) def clockIsOff(self, diff): diffText = ngettext("%s second", "%s seconds", diff) % diff warn = _("""\ In order to ensure your collection works correctly when moved between \ devices, Anki requires your computer's internal clock to be set correctly. \ The internal clock can be wrong even if your system is showing the correct \ local time. Please go to the time settings on your computer and check the following: - AM/PM - Clock drift - Day, month and year - Timezone - Daylight savings Difference to correct time: %s.""") % diffText showWarning(warn) self.app.closeAllWindows() # Count refreshing ########################################################################## def setupRefreshTimer(self): # every 10 minutes self.progress.timer(10*60*1000, self.onRefreshTimer, True) def onRefreshTimer(self): if self.state == "deckBrowser": self.deckBrowser.refresh() elif self.state == "overview": self.overview.refresh() # Permanent libanki hooks ########################################################################## def setupHooks(self): addHook("modSchema", self.onSchemaMod) addHook("remNotes", self.onRemNotes) addHook("odueInvalid", self.onOdueInvalid) addHook("mpvWillPlay", self.onMpvWillPlay) addHook("mpvIdleHook", self.onMpvIdle) self._activeWindowOnPlay = None def onOdueInvalid(self): showWarning(_("""\ Invalid property found on card. Please use Tools>Check Database, \ and if the problem comes up again, please ask on the support site.""")) def _isVideo(self, file): head, ext = os.path.splitext(file.lower()) return ext in (".mp4", ".mov", ".mpg", ".mpeg", ".mkv", ".avi") def onMpvWillPlay(self, file): if not self._isVideo(file): return self._activeWindowOnPlay = self.app.activeWindow() or self._activeWindowOnPlay def onMpvIdle(self): w = self._activeWindowOnPlay if not self.app.activeWindow() and w and not sip.isdeleted(w) and w.isVisible(): w.activateWindow() w.raise_() self._activeWindowOnPlay = None # Log note deletion ########################################################################## def onRemNotes(self, col, nids): path = os.path.join(self.pm.profileFolder(), "deleted.txt") existed = os.path.exists(path) with open(path, "ab") as f: if not existed: f.write(b"nid\tmid\tfields\n") for id, mid, flds in col.db.execute( "select id, mid, flds from notes where id in %s" % ids2str(nids)): fields = splitFields(flds) f.write(("\t".join([str(id), str(mid)] + fields)).encode("utf8")) f.write(b"\n") # Schema modifications ########################################################################## def onSchemaMod(self, arg): return askUser(_("""\ The requested change will require a full upload of the database when \ you next synchronize your collection. If you have reviews or other changes \ waiting on another device that haven't been synchronized here yet, they \ will be lost. Continue?""")) # Advanced features ########################################################################## def onCheckDB(self): "True if no problems" self.progress.start(immediate=True) ret, ok = self.col.fixIntegrity() self.progress.finish() if not ok: showText(ret) else: tooltip(ret) # if an error has directed the user to check the database, # silently clean up any broken reset hooks which distract from # the underlying issue while True: try: self.reset() break except Exception as e: print("swallowed exception in reset hook:", e) continue return ret def onCheckMediaDB(self): self.progress.start(immediate=True) (nohave, unused, warnings) = self.col.media.check() self.progress.finish() # generate report report = "" if warnings: report += "\n".join(warnings) + "\n" if unused: if report: report += "\n\n\n" report += _( "In media folder but not used by any cards:") report += "\n" + "\n".join(unused) if nohave: if report: report += "\n\n\n" report += _( "Used on cards but missing from media folder:") report += "\n" + "\n".join(nohave) if not report: tooltip(_("No unused or missing files found.")) return # show report and offer to delete diag = QDialog(self) diag.setWindowTitle("Anki") layout = QVBoxLayout(diag) diag.setLayout(layout) text = QTextEdit() text.setReadOnly(True) text.setPlainText(report) layout.addWidget(text) box = QDialogButtonBox(QDialogButtonBox.Close) layout.addWidget(box) if unused: b = QPushButton(_("Delete Unused Files")) b.setAutoDefault(False) box.addButton(b, QDialogButtonBox.ActionRole) b.clicked.connect( lambda c, u=unused, d=diag: self.deleteUnused(u, d)) box.rejected.connect(diag.reject) diag.setMinimumHeight(400) diag.setMinimumWidth(500) restoreGeom(diag, "checkmediadb") diag.exec_() saveGeom(diag, "checkmediadb") def deleteUnused(self, unused, diag): if not askUser( _("Delete unused media?")): return mdir = self.col.media.dir() for f in unused: path = os.path.join(mdir, f) if os.path.exists(path): send2trash(path) tooltip(_("Deleted.")) diag.close() def onStudyDeck(self): from aqt.studydeck import StudyDeck ret = StudyDeck( self, dyn=True, current=self.col.decks.current()['name']) if ret.name: self.col.decks.select(self.col.decks.id(ret.name)) self.moveToState("overview") def onEmptyCards(self): self.progress.start(immediate=True) cids = self.col.emptyCids() if not cids: self.progress.finish() tooltip(_("No empty cards.")) return report = self.col.emptyCardReport(cids) self.progress.finish() part1 = ngettext("%d card", "%d cards", len(cids)) % len(cids) part1 = _("%s to delete:") % part1 diag, box = showText(part1 + "\n\n" + report, run=False, geomKey="emptyCards") box.addButton(_("Delete Cards"), QDialogButtonBox.AcceptRole) box.button(QDialogButtonBox.Close).setDefault(True) def onDelete(): saveGeom(diag, "emptyCards") QDialog.accept(diag) self.checkpoint(_("Delete Empty")) self.col.remCards(cids) tooltip(ngettext("%d card deleted.", "%d cards deleted.", len(cids)) % len(cids)) self.reset() box.accepted.connect(onDelete) diag.show() # Debugging ###################################################################### def onDebug(self): d = self.debugDiag = QDialog() d.silentlyClose = True frm = aqt.forms.debug.Ui_Dialog() frm.setupUi(d) font = QFontDatabase.systemFont(QFontDatabase.FixedFont) font.setPointSize(frm.text.font().pointSize() + 1) frm.text.setFont(font) frm.log.setFont(font) s = self.debugDiagShort = QShortcut(QKeySequence("ctrl+return"), d) s.activated.connect(lambda: self.onDebugRet(frm)) s = self.debugDiagShort = QShortcut( QKeySequence("ctrl+shift+return"), d) s.activated.connect(lambda: self.onDebugPrint(frm)) s = self.debugDiagShort = QShortcut(QKeySequence("ctrl+l"), d) s.activated.connect(frm.log.clear) s = self.debugDiagShort = QShortcut(QKeySequence("ctrl+shift+l"), d) s.activated.connect(frm.text.clear) d.show() def _captureOutput(self, on): mw = self class Stream: def write(self, data): mw._output += data if on: self._output = "" self._oldStderr = sys.stderr self._oldStdout = sys.stdout s = Stream() sys.stderr = s sys.stdout = s else: sys.stderr = self._oldStderr sys.stdout = self._oldStdout def _debugCard(self): return self.reviewer.card.__dict__ def _debugBrowserCard(self): return aqt.dialogs._dialogs['Browser'][1].card.__dict__ def onDebugPrint(self, frm): cursor = frm.text.textCursor() position = cursor.position() cursor.select(QTextCursor.LineUnderCursor) line = cursor.selectedText() pfx, sfx = "pp(", ")" if not line.startswith(pfx): line = "{}{}{}".format(pfx, line, sfx) cursor.insertText(line) cursor.setPosition(position + len(pfx)) frm.text.setTextCursor(cursor) self.onDebugRet(frm) def onDebugRet(self, frm): text = frm.text.toPlainText() card = self._debugCard bcard = self._debugBrowserCard mw = self pp = pprint.pprint self._captureOutput(True) try: # pylint: disable=exec-used exec(text) except: self._output += traceback.format_exc() self._captureOutput(False) buf = "" for c, line in enumerate(text.strip().split("\n")): if c == 0: buf += ">>> %s\n" % line else: buf += "... %s\n" % line try: frm.log.appendPlainText(buf + (self._output or "<no output>")) except UnicodeDecodeError: frm.log.appendPlainText(_("<non-unicode text>")) frm.log.ensureCursorVisible() # System specific code ########################################################################## def setupSystemSpecific(self): self.hideMenuAccels = False if isMac: # mac users expect a minimize option self.minimizeShortcut = QShortcut("Ctrl+M", self) self.minimizeShortcut.activated.connect(self.onMacMinimize) self.hideMenuAccels = True self.maybeHideAccelerators() self.hideStatusTips() elif isWin: # make sure ctypes is bundled from ctypes import windll, wintypes _dummy = windll _dummy = wintypes def maybeHideAccelerators(self, tgt=None): if not self.hideMenuAccels: return tgt = tgt or self for action in tgt.findChildren(QAction): txt = str(action.text()) m = re.match(r"^(.+)\(&.+\)(.+)?", txt) if m: action.setText(m.group(1) + (m.group(2) or "")) def hideStatusTips(self): for action in self.findChildren(QAction): action.setStatusTip("") def onMacMinimize(self): self.setWindowState(self.windowState() | Qt.WindowMinimized) # Single instance support ########################################################################## def setupAppMsg(self): self.app.appMsg.connect(self.onAppMsg) def onAppMsg(self, buf): if self.state == "startup": # try again in a second return self.progress.timer(1000, lambda: self.onAppMsg(buf), False) elif self.state == "profileManager": # can't raise window while in profile manager if buf == "raise": return self.pendingImport = buf return tooltip(_("Deck will be imported when a profile is opened.")) if not self.interactiveState() or self.progress.busy(): # we can't raise the main window while in profile dialog, syncing, etc if buf != "raise": showInfo(_("""\ Please ensure a profile is open and Anki is not busy, then try again."""), parent=None) return # raise window if isWin: # on windows we can raise the window by minimizing and restoring self.showMinimized() self.setWindowState(Qt.WindowActive) self.showNormal() else: # on osx we can raise the window. on unity the icon in the tray will just flash. self.activateWindow() self.raise_() if buf == "raise": return # import self.handleImport(buf) # GC ########################################################################## # ensure gc runs in main thread def setupDialogGC(self, obj): obj.finished.connect(lambda: self.gcWindow(obj)) def gcWindow(self, obj): obj.deleteLater() self.progress.timer(1000, self.doGC, False, requiresCollection=False) def disableGC(self): gc.collect() gc.disable() def doGC(self): assert not self.progress.inDB gc.collect() # Crash log ########################################################################## def setupCrashLog(self): p = os.path.join(self.pm.base, "crash.log") self._crashLog = open(p, "ab", 0) faulthandler.enable(self._crashLog) # Media server ########################################################################## def setupMediaServer(self): self.mediaServer = aqt.mediasrv.MediaServer(self) self.mediaServer.start() def baseHTML(self): return '<base href="%s">' % self.serverURL() def serverURL(self): return "http://127.0.0.1:%d/" % self.mediaServer.getPort()
def _setupUI(self): layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) self._browser = QWebEngineView(self) layout.addWidget(self._browser)
class ImportDialog(QDialog): def __init__(self, mw, importer): QDialog.__init__(self, mw, Qt.Window) self.mw = mw self.importer = importer self.frm = aqt.forms.importing.Ui_ImportDialog() self.frm.setupUi(self) self.frm.buttonBox.button(QDialogButtonBox.Help).clicked.connect( self.helpRequested) self.setupMappingFrame() self.setupOptions() self.modelChanged() self.frm.autoDetect.setVisible(self.importer.needDelimiter) addHook("currentModelChanged", self.modelChanged) self.frm.autoDetect.clicked.connect(self.onDelimiter) self.updateDelimiterButtonText() self.frm.allowHTML.setChecked(self.mw.pm.profile.get( 'allowHTML', True)) self.frm.importMode.setCurrentIndex( self.mw.pm.profile.get('importMode', 1)) # import button b = QPushButton(_("Import")) self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole) self.exec_() def setupOptions(self): self.model = self.mw.col.models.current() self.modelChooser = aqt.modelchooser.ModelChooser(self.mw, self.frm.modelArea, label=False) self.deck = aqt.deckchooser.DeckChooser(self.mw, self.frm.deckArea, label=False) def modelChanged(self): self.importer.model = self.mw.col.models.current() self.importer.initMapping() self.showMapping() if self.mw.col.conf.get("addToCur", True): did = self.mw.col.conf['curDeck'] if self.mw.col.decks.isDyn(did): did = 1 else: did = self.importer.model['did'] #self.deck.setText(self.mw.col.decks.name(did)) def onDelimiter(self): str = getOnlyText(_("""\ By default, Anki will detect the character between fields, such as a tab, comma, and so on. If Anki is detecting the character incorrectly, you can enter it here. Use \\t to represent tab."""), self, help="importing") or "\t" str = str.replace("\\t", "\t") if len(str) > 1: showWarning( _("Multi-character separators are not supported. " "Please enter one character only.")) return self.hideMapping() def updateDelim(): self.importer.delimiter = str self.importer.updateDelimiter() self.showMapping(hook=updateDelim) self.updateDelimiterButtonText() def updateDelimiterButtonText(self): if not self.importer.needDelimiter: return if self.importer.delimiter: d = self.importer.delimiter else: d = self.importer.dialect.delimiter if d == "\t": d = _("Tab") elif d == ",": d = _("Comma") elif d == " ": d = _("Space") elif d == ";": d = _("Semicolon") elif d == ":": d = _("Colon") else: d = repr(d) txt = _("Fields separated by: %s") % d self.frm.autoDetect.setText(txt) def accept(self): self.importer.mapping = self.mapping if not self.importer.mappingOk(): showWarning(_("The first field of the note type must be mapped.")) return self.importer.importMode = self.frm.importMode.currentIndex() self.mw.pm.profile['importMode'] = self.importer.importMode self.importer.allowHTML = self.frm.allowHTML.isChecked() self.mw.pm.profile['allowHTML'] = self.importer.allowHTML did = self.deck.selectedId() if did != self.importer.model['did']: self.importer.model['did'] = did self.mw.col.models.save(self.importer.model) self.mw.col.decks.select(did) self.mw.progress.start(immediate=True) self.mw.checkpoint(_("Import")) try: self.importer.run() except UnicodeDecodeError: showUnicodeWarning() return except Exception as e: msg = _("Import failed.\n") err = repr(str(e)) if "1-character string" in err: msg += err elif "invalidTempFolder" in err: msg += self.mw.errorHandler.tempFolderMsg() else: msg += str(traceback.format_exc(), "ascii", "replace") showText(msg) return finally: self.mw.progress.finish() txt = _("Importing complete.") + "\n" if self.importer.log: txt += "\n".join(self.importer.log) self.close() showText(txt) self.mw.reset() def setupMappingFrame(self): # qt seems to have a bug with adding/removing from a grid, so we add # to a separate object and add/remove that instead self.frame = QFrame(self.frm.mappingArea) self.frm.mappingArea.setWidget(self.frame) self.mapbox = QVBoxLayout(self.frame) self.mapbox.setContentsMargins(0, 0, 0, 0) self.mapwidget = None def hideMapping(self): self.frm.mappingGroup.hide() def showMapping(self, keepMapping=False, hook=None): if hook: hook() if not keepMapping: self.mapping = self.importer.mapping self.frm.mappingGroup.show() assert self.importer.fields() # set up the mapping grid if self.mapwidget: self.mapbox.removeWidget(self.mapwidget) self.mapwidget.deleteLater() self.mapwidget = QWidget() self.mapbox.addWidget(self.mapwidget) self.grid = QGridLayout(self.mapwidget) self.mapwidget.setLayout(self.grid) self.grid.setContentsMargins(3, 3, 3, 3) self.grid.setSpacing(6) fields = self.importer.fields() for num in range(len(self.mapping)): text = _("Field <b>%d</b> of file is:") % (num + 1) self.grid.addWidget(QLabel(text), num, 0) if self.mapping[num] == "_tags": text = _("mapped to <b>Tags</b>") elif self.mapping[num]: text = _("mapped to <b>%s</b>") % self.mapping[num] else: text = _("<ignored>") self.grid.addWidget(QLabel(text), num, 1) button = QPushButton(_("Change")) self.grid.addWidget(button, num, 2) button.clicked.connect( lambda _, s=self, n=num: s.changeMappingNum(n)) def changeMappingNum(self, n): f = ChangeMap(self.mw, self.importer.model, self.mapping[n]).getField() try: # make sure we don't have it twice index = self.mapping.index(f) self.mapping[index] = None except ValueError: pass self.mapping[n] = f if getattr(self.importer, "delimiter", False): self.savedDelimiter = self.importer.delimiter def updateDelim(): self.importer.delimiter = self.savedDelimiter self.showMapping(hook=updateDelim, keepMapping=True) else: self.showMapping(keepMapping=True) def reject(self): self.modelChooser.cleanup() self.deck.cleanup() remHook("currentModelChanged", self.modelChanged) QDialog.reject(self) def helpRequested(self): openHelp("importing")