def deactivate(self): db = self.database() # Check if we are allowed to deactivate a card type. can_deactivate = True if db and db.is_loaded(): for component in self.instantiated_components: if component.component_type == "card_type": if self.database().has_clones(component): can_deactivate = False for card_type in self.database().card_types_in_use(): if issubclass(card_type.__class__, component.__class__): can_deactivate = False break if can_deactivate == False: self.main_widget().show_error(_("Cannot deactivate") \ + " '" + component.name + "'. " + \ _("There are cards with this card type (or a clone of it) in the database.")) return False for criterion in db.criteria(): criterion.card_type_deleted(component) db.update_criterion(criterion) # Deactivate and unregister components. for component in self.instantiated_components: component.deactivate() self.component_manager.unregister(component) for component in self.registered_components: self.component_manager.unregister(component) # If we are shutting down and the database is gone, don't worry about # side effects. if not self.database(): return True # Make necessary side effects happen. if self.review_reset_needed: self.controller().reset_study_mode() self.log().started_scheduler() # Use names instead of instances here in order to survive pickling. if self.__class__.__name__ in self.config()["active_plugins"]: self.config()["active_plugins"].remove(self.__class__.__name__) self.instantiated_components = [] self.registered_components = [] return True
def import_logs(self, filename): w = self.main_widget() db = self.database() w.set_progress_text(_("Importing history...")) log_dir = os.path.join(os.path.dirname(filename), "history") if not os.path.exists(log_dir): w.close_progress() w.show_information(_("No history found to import.")) return # The events that we import from the science logs obviously should not # be reexported to these logs (this is true for both the archived logs # and log.txt). So, before the import, we flush the SQL logs to the # science logs, and after the import we edit the partership index to # skip these entries. db.dump_to_science_log() # Manage database indexes. db.before_1x_log_import() filenames = [os.path.join(log_dir, logname) for logname in \ sorted(os.listdir(log_dir)) if logname.endswith(".bz2")] # log.txt can also contain data we need to import, especially on the # initial upgrade from 1.x. 'ids_to_parse' will make sure we only pick # up the relevant events. (If we do the importing after having used # 2.x for a while, there could be duplicate load events, etc, but these # don't matter.) filenames.append(os.path.join(os.path.dirname(filename), "log.txt")) w.set_progress_range(len(filenames)) ignored_files = [] parser = ScienceLogParser(self.database(), ids_to_parse=self.items_by_id, machine_id=self.config().machine_id()) for filename in filenames: try: parser.parse(filename) except: ignored_files.append(filename) w.increase_progress(1) if ignored_files: w.show_information(_("Ignoring unparsable files:<br/>") +\ '<br/>'.join(ignored_files)) # Manage database indexes. db.after_1x_log_import() db.skip_science_log()
def accept(self): filename = self.filename_box.text() if not filename: return QtWidgets.QDialog.accept(self) if not filename.endswith(self.format().extension): filename += self.format().extension self.config()["export_format"] = str(type(self.format())) result = self.format().do_export(filename) if result != -1: # Cancelled. self.main_widget().show_information(_("Done!")) QtWidgets.QDialog.accept(self)
def update_title(self): title = _("Mnemosyne") db = self.database() database_name = db.display_name() if database_name and database_name != db.default_name: title += " - " + database_name if db.current_criterion() and \ db.current_criterion().name and \ db.current_criterion().name != db.default_criterion_name: title += " - " + db.current_criterion().name self.main_widget().set_window_title(title)
def data(self, index, role): if role == QtCore.Qt.DisplayRole: row, column = index.row(), index.column() card_type = self.card_types[row] if column == 0: if not card_type.id.startswith("7") and ":" in card_type.id: return "%s (%s)" % (_(card_type.name), _(card_type.__class__.__bases__[0].name)) else: return card_type.name elif column == 1: language_id = self.config().card_type_property(\ "language_id", card_type, default="") return "" if not language_id else \ self.language_with_id(language_id).name elif column == 2: key = self.config().card_type_property(\ "foreign_fact_key", card_type, default="") return "" if not key else \ card_type.name_for_fact_key(key)
def accept(self): self.correspondence.clear() for old_fact_key, old_fact_key_name in \ self.old_card_type.fact_keys_and_names: new_fact_key_name = \ self.comboboxes[old_fact_key].currentText() if new_fact_key_name != _("<none>"): new_fact_key = \ self.new_card_type.fact_key_with_name(new_fact_key_name) self.correspondence[old_fact_key] = new_fact_key QtWidgets.QDialog.accept(self)
class ScheduledForgottenNew(StudyMode): id = "ScheduledForgottenNew" name = _("Scheduled -> forgotten -> new") menu_weight = 1 Scheduler = SM2Mnemosyne ReviewController = SM2Controller def activate(self): self.activate_components() self.review_controller().reset(new_only=False)
def activate(self): if not hasattr(self, "supported_API_level") or \ self.supported_API_level < self.current_API_level: self.main_widget().show_error(_("Plugin '" + self.name +\ "' needs to be upgraded. Please contact its author.")) return # Don't activate a plugin twice. if self.instantiated_components or self.registered_components: return # See if we need to reset the review process. self.review_reset_needed = False for component in self.components: if component.component_type in \ ["scheduler", "review_controller", "review_widget"]: self.review_reset_needed = True # Register all our regular components. Instantiate them if needed. for component in self.components: if component.instantiate == Component.IMMEDIATELY: component = component(component_manager=self.component_manager) self.component_manager.register(component) component.activate() self.instantiated_components.append(component) else: self.component_manager.register(component) self.registered_components.append(component) # Register gui components for certain classes. for key, value in self.gui_for_component.items(): component_name = key assert type(component_name) == str for gui_module_name, gui_class_name in value: gui_class = getattr(\ importlib.import_module(gui_module_name), gui_class_name) self.component_manager.add_gui_to_component(\ component_name, gui_class) # Make necessary side effects happen. for component in self.components: if component.used_for == "configuration_defaults": component(self.component_manager).run() db = self.database() if db and db.is_loaded(): for component in self.instantiated_components: if component.component_type == "card_type": for criterion in db.criteria(): criterion.active_card_type_added(component) db.update_criterion(criterion) if self.database().is_loaded() and self.review_reset_needed: self.controller().reset_study_mode() # We need to log 'started_scheduler' events manually and not # from e.g. the 'activate' function of the scheduler because # during startup, the database is not yet loaded when the # scheduler gets activated. self.log().started_scheduler() # Use names instead of instances here in order to survive pickling. self.config()["active_plugins"].add(self.__class__.__name__)
def run(self): try: translation = self.translator.translate(\ self.card_type, self.foreign_text, self.target_language_id) if translation: self.finished_signal.emit(translation) else: self.error_signal.emit( _("Could not contact Google servers...")) except Exception as e: self.error_signal.emit(str(e) + "\n" + traceback_string())
class CramRecent(StudyMode): id = "CramRecent" name = _("Cram recently learned new cards") menu_weight = 4 Scheduler = Cramming ReviewController = SM2ControllerCramming def activate(self): self.activate_components() self.review_controller().reset(new_only=True)
def delete_card_type(self, card_type): if not self.database().is_user_card_type(card_type) or \ self.database().is_in_use(card_type): self.main_widget().show_error(\ _("Card type %s is in use or is a system card type, cannot delete it.") \ % (card_type.name, )) return if self.database().has_clones(card_type): self.main_widget().show_error(\ _("Card type %s has clones, cannot delete it.") \ % (card_type.name, )) return fact_views = card_type.fact_views self.config().delete_card_type_properties(card_type) self.database().delete_card_type(card_type) # Correct ordering for the sync protocol is deleting the fact views # last. for fact_view in fact_views: self.database().delete_fact_view(fact_view) self.database().save()
class NewOnly(StudyMode): id = "NewOnly" name = _("Study new unlearned cards only") menu_weight = 2 Scheduler = SM2Mnemosyne ReviewController = SM2Controller def activate(self): self.activate_components() self.review_controller().reset(new_only=True)
class CramAll(StudyMode): id = "CramAll" name = _("Cram all cards") menu_weight = 3 Scheduler = Cramming ReviewController = SM2ControllerCramming def activate(self): self.activate_components() self.review_controller().reset(new_only=False)
class DecoratedVocabulary(Vocabulary): id = "3_decorated" name = _("Vocabulary (decorated)") # The keys we inherit from Vocabulary, we just override the FactViews. # Recognition. v1 = FactView(_("Recognition"), "3::1") v1.q_fact_keys = ["f"] v1.a_fact_keys = ["p_1", "m_1", "n"] v1.q_fact_key_decorators = {"f": "What is the translation of ${f}?"} # Production. v2 = FactView(_("Production"), "3::2") v2.q_fact_keys = ["m_1"] v2.a_fact_keys = ["f", "p_1", "n"] v2.q_fact_key_decorators = {"m_1": "How do you say ${m_1}?"} fact_views = [v1, v2]
def find_duplicates(self): self.stopwatch().pause() self.flush_sync_server() if self.config()["find_duplicates_help_shown"] == False: self.main_widget().show_information(\ _("This will tag all the cards in a given card type which have the same question. That way you can reformulate them to make the answer unambiguous. Note that this will not tag duplicates in different card types, e.g. card types for 'French' and 'Spanish'.")) self.config()["find_duplicates_help_shown"] = True self.database().tag_all_duplicates() review_controller = self.review_controller() review_controller.reset_but_try_to_keep_current_card() review_controller.update_status_bar_counters() self.stopwatch().unpause()
def show_statistics(self, variant): self.activate() if not self.page.y: self.display_message(_("No stats available.")) return self.axes.bar(self.page.x, self.page.y, width=0.7, align="center", color="blue", alpha=0.75) self.axes.set_title(_("Number of cards")) self.axes.set_xlabel(_("Grades")) self.axes.set_xticks(self.page.x) self.axes.set_xticklabels([_("Not seen")] + list(range(0, 6))) xmin, xmax = min(self.page.x), max(self.page.x) self.axes.set_xlim(xmin=xmin - 0.5, xmax=xmax + 0.5) self.axes.set_ylim(ymax=int(max(self.page.y) * 1.1) + 1) from matplotlib.ticker import FuncFormatter self.axes.yaxis.set_major_formatter(FuncFormatter(self.integers_only)) self._add_numbers_to_bars()
def __init__(self, statistics_page, page_index, **kwds): super().__init__(**kwds) self.statistics_page = statistics_page self.page_index = page_index self.vbox_layout = QtWidgets.QVBoxLayout(self) self.combobox = QtWidgets.QComboBox(self) self.variant_ids = [] self.variant_widgets = [] self.current_variant_widget = None variants = statistics_page.variants if not variants: variants = [(0, _("Default"))] for variant_id, variant_name in variants: self.variant_ids.append(variant_id) self.variant_widgets.append(None) self.combobox.addItem(_(variant_name)) if len(self.variant_ids) <= 1 or \ self.statistics_page.show_variants_in_combobox == False: self.combobox.hide() self.vbox_layout.addWidget(self.combobox) self.combobox.currentIndexChanged.connect(self.display_variant)
def process_latex_img_tag(self, latex_command): """Transform the latex tags to image tags.""" img_file = self.create_latex_img_file(latex_command) if not img_file: return "<b>" + \ _("Problem with latex. Are latex and dvipng installed?") + "</b>" # Note that we leave the the expanding of paths to the expand_paths # plugin, as during export, we should not do this and we disable the # expand_paths plugin. This means however that the expand_paths plugin # should always run at the end. return "<img src=\"" + img_file + "\" align=middle>"
def preview(self): card_type = self.affected_card_types[0] for render_chain in self.component_manager.all("render_chain"): render_chain.renderer_for_card_type(card_type).update(card_type) fact_data = {} for fact_key, fact_key_name in card_type.fact_keys_and_names: fact_data[fact_key] = _(fact_key_name) # Tmp hack for cloze. if fact_key == "text": fact_data[fact_key] = "[" + _(fact_key_name) + "]" fact = Fact(fact_data) cards = card_type.create_sister_cards(fact) # Tmp hack for Anki for card in cards: card.extra_data["ord"] = 1 tag_text = "" dlg = PreviewCardsDlg(cards, tag_text, component_manager=self.component_manager, parent=self) dlg.exec_()
def activate_saved_plugins(self): for plugin in self.config()["active_plugins"]: try: for p in self.plugins(): if plugin == p.__class__.__name__: p.activate() break except: from mnemosyne.libmnemosyne.gui_translator import _ msg = _("Error when running plugin:") \ + "\n" + traceback_string() self.main_widget().show_error(msg)
def show_answer(self): if self._state == "SELECT AHEAD": if self.config()["warned_about_learning_ahead"] == False: self.main_widget().show_information(\ _("Use 'Learn ahead of schedule' sparingly. For cramming before an exam, it's much better to use the cramming scheduler plugin")) self.config()["warned_about_learning_ahead"] = True self.learning_ahead = True self.show_new_question() else: self.stopwatch().stop() self._state = "SELECT GRADE" self.update_dialog()
def last_rep_to_interval_string(self, last_rep, now=None): """Converts next_rep to a string like 'yesterday', '2 weeks ago', ... """ if now is None: now = time.time() # To perform the calculation, we need to 'snap' the two timestamps # to midnight UTC before calculating the interval. now = self.midnight_UTC(\ now - self.config()["day_starts_at"] * HOUR) last_rep = self.midnight_UTC(\ last_rep - self.config()["day_starts_at"] * HOUR) interval_days = (last_rep - now) / DAY if interval_days > -1: return _("today") elif interval_days > -2: return _("yesterday") elif interval_days > -31: return str(int(-interval_days)) + " " + _("days ago") elif interval_days > -62: return _("1 month ago") elif interval_days > -365: interval_months = int(-interval_days / 31.) return str(interval_months) + " " + _("months ago") else: interval_years = -interval_days / 365. return "%.1f " % interval_years + _("years ago")
class Map(CardType): id = "4" name = _("Map") # List and name the keys. fact_keys_and_names = [("loc", _("Location")), ("blank", _("Blank map")), ("marked", _("Marked map"))] # Recognition. v1 = FactView(_("Recognition"), "4.1") v1.q_fact_keys = ["_", "marked"] v1.a_fact_keys = ["loc", "marked"] v1.a_on_top_of_q = True # Production. v2 = FactView(_("Production"), "4.2") v2.q_fact_keys = ["loc", "blank"] v2.a_fact_keys = ["loc", "marked"] v2.a_on_top_of_q = True fact_views = [v1, v2] unique_fact_keys = ["loc"] required_fact_keys = ["loc", "blank", "marked"] def fact_data(self, card): _fact_data = copy.copy(card.fact.data) _fact_data["_"] = " " # Insert a blank line to improve layout. return _fact_data def fact_key_format_proxies(self): return {"loc": "loc", "blank": "blank", "marked": "marked", "_": "loc"}
def context_menu(self, point): menu = QtWidgets.QMenu(self) edit_action = QtWidgets.QAction(_("&Edit"), menu) edit_action.setShortcut(QtCore.Qt.CTRL + QtCore.Qt.Key_E) edit_action.triggered.connect(self.menu_edit) menu.addAction(edit_action) preview_action = QtWidgets.QAction(_("&Preview"), menu) preview_action.setShortcut(QtCore.Qt.CTRL + QtCore.Qt.Key_P) preview_action.triggered.connect(self.menu_preview) menu.addAction(preview_action) delete_action = QtWidgets.QAction(_("&Delete"), menu) delete_action.setShortcut(QtGui.QKeySequence.Delete) delete_action.triggered.connect(self.menu_delete) menu.addAction(delete_action) menu.addSeparator() change_card_type_action = QtWidgets.QAction(_("Change card &type"), menu) change_card_type_action.triggered.connect(self.menu_change_card_type) menu.addAction(change_card_type_action) menu.addSeparator() add_tags_action = QtWidgets.QAction(_("&Add tags"), menu) add_tags_action.triggered.connect(self.menu_add_tags) menu.addAction(add_tags_action) remove_tags_action = QtWidgets.QAction(_("&Remove tags"), menu) remove_tags_action.triggered.connect(self.menu_remove_tags) menu.addAction(remove_tags_action) indexes = self.table.selectionModel().selectedRows() if len(indexes) > 1: edit_action.setEnabled(False) preview_action.setEnabled(False) if len(indexes) >= 1: menu.exec_(self.table.mapToGlobal(point))
def _rebuild(self): for key in dict(self): del self[key] self["__ALL__"] = [] self.display_name_for_node = {"__ALL__": _("All tags")} self.card_count_for_node = {} self.tag_for_node = {} # Preprocess tag names such that each tag results in a leaf of the # tree, i.e. if you have tags like "A::B" and "A", rename the latter # to "A::Untagged". tags = self.database().tags() tag_names = [tag.name for tag in tags] preprocessed_tag_name_for = {} for tag in tags: preprocessed_tag_name_for[tag] = tag.name for other_tag_name in tag_names: if other_tag_name.startswith(tag.name + "::") \ and other_tag_name != tag.name: preprocessed_tag_name_for[tag] = \ tag.name + "::" + _("Untagged") break # Build the actual tag tree. for tag in tags: name = preprocessed_tag_name_for[tag] self.tag_for_node[name] = tag parent = "__ALL__" partial_tag = "" for node in name.split("::"): if partial_tag: partial_tag += "::" partial_tag += node if not partial_tag in self.display_name_for_node: self[parent].append(partial_tag) self[partial_tag] = [] self.display_name_for_node[partial_tag] = \ node.replace("::" + _("Untagged"), "") parent = partial_tag if "__UNTAGGED__" in self.display_name_for_node: self.display_name_for_node["__UNTAGGED__"] = _("Untagged")
def download_translation(self): self.last_foreign_text = self.foreign_text.toPlainText() if not self.last_foreign_text: return # Note that we need to save the QtThread as a class variable, # otherwise it will get garbage collected. self.download_thread = DownloadThread(self.translator, self.card_type, self.last_foreign_text, self.target_language_id) self.download_thread.finished_signal.connect(self.show_translation) self.download_thread.error_signal.connect(self.show_error) self.main_widget().set_progress_text(_("Downloading...")) self.download_thread.start()
def show_statistics(self, variant): self.activate() # Determine variant-dependent formatting. if not self.page.y: self.display_message(_("No stats available.")) return ticklabels_neg = lambda i, j, k: ["%d" % x for x in range(i, j, k)] if hasattr(self.page, "LAST_WEEK") and \ variant == self.page.LAST_WEEK: xticks = list(range(-7, 1, 1)) xticklabels = ticklabels_neg(-7, 1, 1) elif hasattr(self.page, "LAST_MONTH") and \ variant == self.page.LAST_MONTH: xticks = list(range(-30, -4, 5)) + [0] xticklabels = ticklabels_neg(-30, -4, 5) + ["0"] elif hasattr(self.page, "LAST_3_MONTHS") and \ variant == self.page.LAST_3_MONTHS: xticks = list(range(-90, -9, 10)) + [0] xticklabels = ticklabels_neg(-90, -9, 10) + ["0"] elif hasattr(self.page, "LAST_6_MONTHS") and \ variant == self.page.LAST_6_MONTHS: xticks = list(range(-180, -19, 20)) + [0] xticklabels = ticklabels_neg(-180, -19, 20) + ["0"] elif hasattr(self.page, "LAST_YEAR") and \ variant == self.page.LAST_YEAR: xticks = list(range(-360, -59, 60)) + [0] xticklabels = ticklabels_neg(-360, -59, 60) + ["0"] else: raise AttributeError("Invalid variant") # Plot data. self.axes.plot(self.page.x, self.page.y) self.axes.set_title(self.title) self.axes.set_xlabel(_("Days")) self.axes.set_xticks(xticks) self.axes.set_xticklabels(xticklabels) xmin, xmax = min(self.page.x), max(self.page.x) self.axes.set_xlim(xmin=xmin - 0.5, xmax=xmax + 0.5)
class CuecardWcu(FileFormat, MediaPreprocessor): description = _("Cuecard .wcu") extension = ".wcu" filename_filter = _("Cuecard files (*.wcu *.WCU)") import_possible = True export_possible = False def __init__(self, component_manager): FileFormat.__init__(self, component_manager) MediaPreprocessor.__init__(self, component_manager) def do_import(self, filename, extra_tag_names=""): FileFormat.do_import(self, filename, extra_tag_names) w = self.main_widget() try: tree = cElementTree.parse(filename) except cElementTree.ParseError as e: w.show_error(_("Unable to parse file:") + str(e)) return card_type = self.card_type_with_id("1") tag_names = [tag_name.strip() for \ tag_name in extra_tag_names.split(",") if tag_name.strip()] for element in tree.getroot().findall("Card"): fact_data = { "f": element.attrib["Question"], "b": element.attrib["Answer"] } self.preprocess_media(fact_data, tag_names) card = self.controller().create_new_cards( fact_data, card_type, grade=-1, tag_names=tag_names, check_for_duplicates=False, save=False)[0] if _("MISSING_MEDIA") in tag_names: tag_names.remove(_("MISSING_MEDIA")) self.warned_about_missing_media = False
def show_save_file_as_dialog(self): self.stopwatch().pause() if self.config()["single_database_help_shown"] == False: self.main_widget().show_information(_(self.single_database_help)) self.config()["single_database_help_shown"] = True self.flush_sync_server() suffix = self.database().suffix old_path = expand_path(self.config()["last_database"], self.config().data_dir) old_media_dir = self.database().media_dir() filename = self.main_widget().get_filename_to_save( path=old_path, filter=_("Mnemosyne databases") + " (*%s)" % suffix) if not filename: self.stopwatch().unpause() return if filename.endswith("config.db"): self.main_widget().show_information(\ _("The configuration database cannot be used to store cards.")) self.stopwatch().unpause() return if not filename.endswith(suffix): filename += suffix try: self.database().save(filename) new_media_dir = self.database().media_dir() if old_media_dir == new_media_dir: return import shutil if os.path.exists(new_media_dir): shutil.rmtree(new_media_dir) shutil.copytree(old_media_dir, new_media_dir) self.log().saved_database() except RuntimeError as error: self.main_widget().show_error(str(error)) self.stopwatch().unpause() return self.review_controller().update_dialog() self.update_title() self.stopwatch().unpause()
def menu_delete(self): answer = self.main_widget().show_question\ (_("Go ahead with delete? Sister cards will be deleted as well."), _("&OK"), _("&Cancel"), "") if answer == 1: # Cancel. return _fact_ids = set() for index in self.table.selectionModel().selectedRows(): _fact_id_index = index.model().index(\ index.row(), _FACT_ID, index.parent()) _fact_id = index.model().data(_fact_id_index) _fact_ids.add(_fact_id) facts = [] for _fact_id in _fact_ids: facts.append(self.database().fact(_fact_id, is_id_internal=True)) self.unload_qt_database() self.saved_selection = [] self.controller().delete_facts_and_their_cards(facts) self.card_type_tree_wdgt.rebuild() self.tag_tree_wdgt.rebuild() self.load_qt_database() self.display_card_table()