def update_title(self): database_name = os.path.basename(self.config()["path"]).\ split(self.database().suffix)[0] title = _("Mnemosyne") if database_name != _("default"): title += " - " + database_name self.main_widget().set_window_title(title)
def __init__(self, component_manager): StatisticsDialog.__init__(self, component_manager) QtGui.QDialog.__init__(self, self.main_widget()) self.setWindowTitle(_("Statistics")) self.vbox_layout = QtGui.QVBoxLayout(self) self.tab_widget = QtGui.QTabWidget(self.main_widget()) page_index = 0 for page in self.statistics_pages(): page = page(self.component_manager) self.tab_widget.addTab(StatisticsPageWdgt(component_manager, self, page, page_index), page.name) page_index += 1 self.vbox_layout.addWidget(self.tab_widget) self.button_layout = QtGui.QHBoxLayout() self.button_layout.addItem(QtGui.QSpacerItem(20, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)) self.ok_button = QtGui.QPushButton(_("&OK"), self) self.button_layout.addWidget(self.ok_button) self.vbox_layout.addLayout(self.button_layout) self.connect(self.ok_button, QtCore.SIGNAL("clicked()"), self.accept) self.connect(self.tab_widget, QtCore.SIGNAL("currentChanged(int)"), self.display_page) page_index = self.config()["last_statistics_page"] if page_index >= self.tab_widget.count(): page_index = 0 self.tab_widget.setCurrentIndex(page_index) self.display_page(page_index)
def file_new(self): self.stopwatch().pause() db = self.database() suffix = db.suffix filename = self.main_widget().save_file_dialog(\ path=self.config().basedir, filter=_("Mnemosyne databases") + \ " (*%s)" % suffix, caption=_("New")) if not filename: self.stopwatch().unpause() return if not filename.endswith(suffix): filename += suffix db.backup() db.unload() # Confirmation on overwrite has happened in the file dialog code, if os.path.exists(filename): import shutil shutil.rmtree(filename + "_media") db.new(filename) db.load(self.config()["path"]) self.log().loaded_database() self.review_controller().reset() self.review_controller().update_dialog() self.update_title() self.stopwatch().unpause()
def file_open(self): self.stopwatch().pause() db = self.database() basedir = self.config().basedir old_path = expand_path(self.config()["path"], basedir) filename = self.main_widget().open_file_dialog(path=old_path, filter=_("Mnemosyne databases") + " (*%s)" % db.suffix) if not filename: self.stopwatch().unpause() return if filename.startswith(os.path.join(basedir, "backups")): result = self.main_widget().question_box(\ _("Do you want to restore from this backup?"), _("Yes"), _("No"), "") if result == 0: # Yes db.abandon() db_path = expand_path(self.config()["path"], basedir) import shutil shutil.copy(filename, db_path) db.load(db_path) self.review_controller().reset() self.update_title() self.stopwatch().unpause() return try: self.log().saved_database() db.backup() db.unload() except RuntimeError, error: self.main_widget().error_box(unicode(error)) self.stopwatch().unpause() return
def save(self, path=None): # Don't erase a database which failed to load. if self.load_failed == True: return -1 if not path: path = self.config()["path"] path = expand_path(path, self.config().basedir) # Update version. self.global_variables["version"] = self.version # Work around a sip bug: don't store card types, but their ids. for f in self.facts: f.card_type = f.card_type.id try: # Write to a backup file first, as shutting down Windows can # interrupt the dump command and corrupt the database. outfile = file(path + "~", 'wb') db = [self.tags, self.facts, self.cards, self.global_variables] cPickle.dump(db, outfile) outfile.close() shutil.move(path + "~", path) # Should be atomic. except: raise RuntimeError, _("Unable to save file.") \ + "\n" + traceback_string() self.config()["path"] = contract_path(path, self.config().basedir) # Work around sip bug again. for f in self.facts: f.card_type = self.card_type_by_id(f.card_type)
def delete_current_fact(self): self.stopwatch().pause() db = self.database() review_controller = self.review_controller() fact = review_controller.card.fact no_of_cards = len(db.cards_from_fact(fact)) if no_of_cards == 1: question = _("Delete this card?") elif no_of_cards == 2: question = _("Delete this card and 1 related card?") + " " +\ _("Are you sure you want to do this,") + " " +\ _("and not just deactivate cards in the 'Activate cards' dialog?") else: question = _("Delete this card and") + " " + str(no_of_cards - 1) \ + " " + _("related cards?") + " " +\ _("Are you sure you want to do this,") + " " +\ _("and not just deactivate cards in the 'Activate cards' dialog?") answer = self.main_widget().question_box(question, _("&Delete"), _("&Cancel"), "") if answer == 1: # Cancel. self.stopwatch().unpause() return db.delete_fact_and_related_data(fact) db.save() review_controller.reload_counters() review_controller.rebuild_queue() review_controller.new_question() review_controller.update_status_bar() review_controller.update_dialog(redraw_all=True) self.stopwatch().unpause()
def add_card_type_from_log_entry(self, log_entry): already_imported = self.con.execute(\ "select count() from card_types where id=?", (log_entry["o_id"], )).fetchone()[0] != 0 if "name" not in log_entry: log_entry["name"] = "dummy" # Added and immediately deleted. same_name_in_database = self.con.execute(\ "select count() from card_types where name=? and id!=?", (log_entry["name"], log_entry["o_id"] )).fetchone()[0] == 1 if same_name_in_database: # Merging with the card type which is already in the database # is more difficult, as then the card type links in the cards # would need to be updated. if self.importing: # Don't interrupt sync with dialog. self.main_widget().show_information(\ _("Card type '%s' already in database, renaming new card type to '%s (1)'" \ % (log_entry["name"], log_entry["name"]))) log_entry["name"] += " (1)" if already_imported and self.importing: log_entry["type"] = EventTypes.EDITED_CARD_TYPE return self.update_card_type(\ self.card_type_from_log_entry(log_entry)) try: card_type = self.card_type_from_log_entry(log_entry) self.activate_plugins_for_card_type_with_id(card_type.id) self.add_card_type(card_type) except sqlite3.IntegrityError: # Leftover from old bug, should not reoccur. self.main_widget().show_information(\ _("Creating same card type twice during sync. Inform the developpers."))
def _import_logs(self, filename): progress = self.main_widget().get_progress_dialog() progress.set_text(_("Importing history...")) parser = ScienceLogParser(self.database(), ids_to_parse=self.items_by_id) log_dir = os.path.join(os.path.dirname(filename), "history") if not os.path.exists(log_dir): self.main_widget().information_box(\ _("No history found to import.")) return filenames = [os.path.join(log_dir, logname) for logname in \ sorted(os.listdir(unicode(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")) progress.set_range(0, len(filenames)) for count, filename in enumerate(filenames): progress.set_value(count) try: parser.parse(filename) except: self.main_widget().information_box(\ _("Ignoring unparsable file:") + " " + filename) progress.set_value(len(filenames))
def build_plugin_list(self): plugin_dir = os.path.join(self.config().data_dir, "plugins") self.can_be_deleted = [filename.rsplit(".", 1)[0] for \ filename in os.listdir(plugin_dir) \ if filename.endswith(".manifest")] self.plugin_list.clear() self.previously_active = {} self.plugin_with_name = {} for plugin in self.plugins(): list_item = QtGui.QListWidgetItem(_(plugin.name)) list_item.setFlags(list_item.flags() \ | QtCore.Qt.ItemIsUserCheckable) self.plugin_with_name[_(plugin.name)] = plugin active = \ plugin.__class__.__name__ in self.config()["active_plugins"] self.previously_active[_(plugin.name)] = active if active: list_item.setCheckState(QtCore.Qt.Checked) else: list_item.setCheckState(QtCore.Qt.Unchecked) self.plugin_list.addItem(list_item) self.plugin_list.setCurrentRow(self.last_selected_row) self.plugin_description.setText(_(self.plugins()[0].description)) self.delete_button.setEnabled(\ self.plugins()[0].__class__.__name__ in self.can_be_deleted)
def save_set(self): criterion = self.tab_widget.currentWidget().criterion() if criterion.is_empty(): self.main_widget().show_error(_("This set can never contain any cards!")) return CardSetNameDlg(self.component_manager, criterion, self.criteria_by_name.keys(), self).exec_() if not criterion.name: # User cancelled. return if criterion.name in self.criteria_by_name.keys(): answer = self.main_widget().show_question(_("Update this set?"), _("&OK"), _("&Cancel"), "") if answer == 1: # Cancel. return original_criterion = self.criteria_by_name[criterion.name] criterion._id = original_criterion._id criterion.id = original_criterion.id self.database().update_criterion(criterion) else: self.database().add_criterion(criterion) self.update_saved_sets_pane() item = self.saved_sets.findItems(criterion.name, QtCore.Qt.MatchExactly)[0] self.saved_sets.setCurrentItem(item) if self.config()["showed_help_on_renaming_sets"] == False: self.main_widget().show_information( _("You can right-click on the name of a saved set to rename or delete it.") ) self.config()["showed_help_on_renaming_sets"] = True
def run(self): # Use shown_question here, since this is implemented to block. answer = self.main_widget().show_question(\ _("About to archive old logs to improve running speed. Depending on the size of your database and the speed of your device, this can take 10 minutes or more. Please leave Mnemosyne running in the foreground."), _("OK, proceed"), "", "") if answer == 0: DatabaseMaintenance.run(self)
def grade_answer(self, grade): """Note that this also pulls in a new question.""" card_to_grade = self.card old_grade = card_to_grade.grade self.update_counters(old_grade, grade) self.rep_count += 1 if self.scheduler().allow_prefetch(): self.new_question() interval = self.scheduler().grade_answer(card_to_grade, grade) self.database().update_card(card_to_grade, repetition_only=True) if self.rep_count % self.config()["save_after_n_reps"] == 0: self.database().save() else: interval = self.scheduler().grade_answer(card_to_grade, grade) self.database().update_card(card_to_grade, repetition_only=True) if self.rep_count % self.config()["save_after_n_reps"] == 0: self.database().save() self.new_question() if self.config()["show_intervals"] == "status_bar": import math days = int(math.ceil(interval / (24.0 * 60 * 60))) self.widget.update_status_bar(_("Returns in") + " " + \ str(interval) + _(" day(s)."))
def reset_to_defaults(self): answer = self.main_widget().show_question(\ _("Reset current tab to defaults?"), _("&Yes"), _("&No"), "") if answer == 1: return self.order.setCurrentIndex(0) self.store_state.setCheckState(QtCore.Qt.Checked)
def grade_answer(self, grade): """Note that this also pulls in a new question.""" self.flush_sync_server() # Guide the learning process. if self.config()["shown_learn_new_cards_help"] == False: if self.scheduled_count == 1: self.main_widget().show_information(_("You have finished your scheduled reviews. Now, learn as many failed or new cards as you feel like.")) self.config()["shown_learn_new_cards_help"] = True card_to_grade = self.card previous_grade = card_to_grade.grade self.update_counters(previous_grade, grade) self.rep_count += 1 if self.scheduler().is_prefetch_allowed(card_to_grade): self.show_new_question() interval = self.scheduler().grade_answer(card_to_grade, grade) self.database().update_card(card_to_grade, repetition_only=True) if self.rep_count % self.config()["save_after_n_reps"] == 0: self.database().save() else: interval = self.scheduler().grade_answer(card_to_grade, grade) self.database().update_card(card_to_grade, repetition_only=True) if self.rep_count % self.config()["save_after_n_reps"] == 0: self.database().save() self.show_new_question() if self.config()["show_intervals"] == "status_bar": import math days = int(math.ceil(interval / (24.0 * 60 * 60))) self.main_widget().set_status_bar_message(_("Returns in") + \ " " + str(days) + _(" day(s)."))
def __init__(self, component_manager, parent): ConfigurationWidget.__init__(self, component_manager) QtGui.QDialog.__init__(self, parent) self.setupUi(self) sync_port = self.config()["sync_server_port"] web_port = self.config()["web_server_port"] self.sync_server_initially_running = self.is_server_running(sync_port) self.web_server_initially_running = self.is_server_running(web_port) self.run_sync_server.setChecked(self.config()["run_sync_server"]) self.sync_port.setValue(sync_port) self.username.setText(self.config()["remote_access_username"]) self.password.setText(self.config()["remote_access_password"]) self.check_for_edited_local_media_files.setChecked(\ self.config()["check_for_edited_local_media_files"]) self.run_web_server.setChecked(self.config()["run_web_server"]) self.web_port.setValue(web_port) if self.is_server_running(sync_port): self.sync_server_status.setText(_("Sync server running on ") + \ localhost_IP() + " .") else: self.sync_server_status.setText(_("Sync server NOT running.")) if self.is_server_running(web_port): self.web_server_status.setText(_("Web server running on ") + \ "http://" + localhost_IP() + ":" + str(web_port) + " .") else: self.web_server_status.setText(_("Web server NOT running."))
def next_rep_string(self, days): if days == 0: return '\n' + _("Next repetition: today.") elif days == 1: return '\n' + _("Next repetition: tomorrow.") else: return '\n' + _("Next repetition in ") + str(days) + _(" days.")
def create_cards_from_mnemosyne1(self, extra_tag_names): w = self.main_widget() # See if the file was imported before. try: card = self.database().card(self.items[0].id, is_id_internal=False) except: card = None if card: w.show_error(\ _("These cards seem to have been imported before. Aborting...")) raise MnemosyneError w.set_progress_text(_("Importing cards...")) w.set_progress_range(len(self.items)) w.set_progress_update_interval(len(self.items)/50) self.map_plugin_activated = False self.items_by_id = {} for item in self.items: item.id = str(item.id) while item.id in self.items_by_id: item.id = "dup" + item.id self.items_by_id[item.id] = item for item in self.items: w.increase_progress(1) self.create_card_from_item(item, extra_tag_names) w.set_progress_value(len(self.items))
def initialise_card_types_combobox(self, current_card_type_name): # We calculate card_type_by_name here because these names can change # if the user chooses another translation. self.card_type_by_name = {} self.card_type = None self.card_type_index = 0 self.card_type_widget = None self.previous_tags = None self.previous_card_type_name = current_card_type_name db_sorted_card_types = self.database().sorted_card_types() for card_type in db_sorted_card_types: if _(card_type.name) == current_card_type_name: self.card_type = card_type self.card_type_index = self.card_types_widget.count() self.card_type_by_name[_(card_type.name)] = card_type self.card_types_widget.addItem(_(card_type.name)) if not self.card_type: self.card_type = db_sorted_card_types[0] self.card_type_index = 0 self.card_types_widget.setCurrentIndex(self.card_type_index) # Now that the combobox is filled, we can connect the signal. self.card_types_widget.currentIndexChanged[QtCore.QString].\ connect(self.card_type_changed) self.correspondence = {} # Used when changing card types. self.update_card_widget()
def __init__(self, old_card_type, new_card_type, correspondence, check_required_fact_keys=True, parent=None): QtGui.QDialog.__init__(self, parent) self.setupUi(self) self.setWindowFlags(self.windowFlags() \ | QtCore.Qt.WindowMinMaxButtonsHint) self.setWindowFlags(self.windowFlags() \ & ~ QtCore.Qt.WindowContextHelpButtonHint) self.old_card_type = old_card_type self.new_card_type = new_card_type self.correspondence = correspondence self.check_required_fact_keys = check_required_fact_keys self.comboboxes = {} index = 1 for old_fact_key, old_fact_key_name in \ old_card_type.fact_keys_and_names: label = QtGui.QLabel(self) label.setText(_(old_fact_key_name) + ":") font = QtGui.QFont() font.setWeight(50) font.setBold(False) label.setFont(font) self.gridLayout.addWidget(label, index, 0, 1, 1) combobox = QtGui.QComboBox(self) for new_fact_key, new_key_name in \ new_card_type.fact_keys_and_names: combobox.addItem(_(new_key_name)) combobox.addItem(_("<none>")) combobox.setCurrentIndex(combobox.count()-1) self.gridLayout.addWidget(combobox, index, 1, 1, 1) self.comboboxes[old_fact_key] = combobox index += 1 combobox.currentIndexChanged.connect(self.combobox_updated)
def deactivate(self): if self.upload_thread: from mnemosyne.libmnemosyne.translator import _ print _("Waiting for uploader thread to stop...") self.upload_thread.join() print _("Done!")
def show_new_file_dialog(self): self.stopwatch().pause() self.flush_sync_server() if self.config()["single_database_help_shown"] == False: self.main_widget().show_information(self.single_database_help) self.config()["single_database_help_shown"] = True db = self.database() suffix = db.suffix filename = self.main_widget().get_filename_to_save(\ path=self.config().data_dir, filter=_("Mnemosyne databases") + \ " (*%s)" % suffix, caption=_("New")) if not filename: self.stopwatch().unpause() return if not filename.endswith(suffix): filename += suffix self.main_widget().set_progress_text(_("Creating new database...")) db.backup() db.unload() # Confirmation on overwrite has happened in the file dialog code. if os.path.exists(filename + "_media"): import shutil shutil.rmtree(filename + "_media") db.new(filename) self.main_widget().close_progress() db.load(self.config()["last_database"]) self.log().loaded_database() self.review_controller().reset() self.review_controller().update_dialog() self.update_title() self.stopwatch().unpause()
def show_insert_img_dialog(self, filter): """Show a file dialog filtered on the supported filetypes, get a filename, massage it, and return it to the widget to be inserted. There is more media file logic inside the database code too, as the user could also just type in the html tags as opposed to passing through the file selector here. The reason we don't do all the operations in the database code, is that we want to display a nice short relative path back in the edit field. """ from mnemosyne.libmnemosyne.utils import copy_file_to_dir data_dir, media_dir = \ self.config().data_dir, self.database().media_dir() path = expand_path(self.config()["import_img_dir"], data_dir) filter = _("Image files") + " " + filter filename = self.main_widget().get_filename_to_open(\ path, filter, _("Insert image")) if not filename: return "" else: self.config()["import_img_dir"] = contract_path(\ os.path.dirname(filename), data_dir) filename = copy_file_to_dir(filename, media_dir) return contract_path(filename, media_dir)
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 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, error: self.main_widget().show_error(unicode(error.message)) self.stopwatch().unpause() return
def delete_current_card(self): self.stopwatch().pause() self.flush_sync_server() db = self.database() review_controller = self.review_controller() fact = review_controller.card.fact no_of_cards = len(db.cards_from_fact(fact)) if no_of_cards == 1: question = _("Delete this card?") elif no_of_cards == 2: question = _("Delete this card and 1 sister card?") + " " +\ _("Are you sure you want to do this,") + " " +\ _("and not just deactivate cards in the 'Activate cards' dialog?") else: question = _("Delete this card and") + " " + str(no_of_cards - 1) \ + " " + _("sister cards?") + " " +\ _("Are you sure you want to do this,") + " " +\ _("and not just deactivate cards in the 'Activate cards' dialog?") answer = self.main_widget().show_question(question, _("&Cancel"), _("&Delete"), "") if answer == 0: # Cancel. self.stopwatch().unpause() return self.delete_facts_and_their_cards([fact]) review_controller.reload_counters() review_controller.show_new_question() self.stopwatch().unpause()
def load_database(self, filename): if not filename: filename = self.config()["last_database"] path = expand_path(filename, self.config().data_dir) try: if not os.path.exists(path): try: self.database().new(path) except: from mnemosyne.libmnemosyne.translator import _ raise RuntimeError(_("Previous drive letter no longer available.")) else: self.database().load(path) self.controller().update_title() except RuntimeError, e: from mnemosyne.libmnemosyne.translator import _ self.main_widget().show_error(unicode(e)) self.main_widget().show_information(\ _("If you are using a USB key, refer to the instructions on the website so as not to be affected by drive letter changes.")) success = False while not success: try: self.database().abandon() self.controller().show_open_file_dialog() success = True except RuntimeError, e: self.main_widget().show_error(unicode(e))
def initialise(self, data_dir=None, config_dir=None, filename=None, automatic_upgrades=True, debug_file=None, server_only=False): """The automatic upgrades of the database can be turned off by setting 'automatic_upgrade' to False. This is mainly useful for the testsuite. """ if debug_file: self.component_manager.debug_file = open(debug_file, "w", 0) self.register_components() # Upgrade from 1.x if needed. if automatic_upgrades: from mnemosyne.libmnemosyne.upgrades.upgrade1 import Upgrade1 Upgrade1(self.component_manager).backup_old_dir() if data_dir: self.config().data_dir = data_dir self.config().config_dir = data_dir if config_dir: self.config().config_dir = config_dir # Upgrade config if needed. if automatic_upgrades: from mnemosyne.libmnemosyne.upgrades.upgrade3 import Upgrade3 Upgrade3(self.component_manager).run() self.activate_components() register_component_manager(self.component_manager, self.config()["user_id"]) self.execute_user_plugin_dir() self.activate_saved_plugins() # If we are only running a sync or a review server, do not yet load # the database to prevent threading access issues. if server_only: if filename: self.config()["last_database"] = \ contract_path(filename, self.config().data_dir) return # Loading the database should come after all user plugins have been # loaded, since these could be needed e.g. for a card type in the # database. if filename and not filename.endswith(".db"): from mnemosyne.libmnemosyne.translator import _ self.main_widget().show_error(\ _("Command line argument is not a *.db file.")) sys.exit() self.load_database(filename) # Only now that the database is loaded, we can start writing log # events to it. This is why we log started_scheduler and # loaded_database manually. try: self.log().started_program() except Exception, e: if "lock" in str(e): from mnemosyne.libmnemosyne.translator import _ self.main_widget().show_error(\ _("Another copy of Mnemosyne is still running.") + "\n" + \ _("Continuing is impossible and will lead to data loss!")) sys.exit() else: raise e
def delete_facts_and_their_cards(self, facts, progress_bar=True): assert len(facts) == len([fact.id for fact in facts]) db = self.database() w = self.main_widget() if progress_bar: w.set_progress_text(_("Deleting cards...")) w.set_progress_range(len(facts)) w.set_progress_update_interval(50) for fact in facts: for card in db.cards_from_fact(fact): self.scheduler().remove_from_queue_if_present(card) db.delete_card(card, check_for_unused_tags=False) db.delete_fact(fact) if progress_bar: w.increase_progress(1) tags = db.tags() if progress_bar: w.set_progress_text(_("Checking for unused tags...")) w.set_progress_range(len(tags)) tags = db.tags() for tag in tags: db.delete_tag_if_unused(tag) if progress_bar: w.increase_progress(1) db.save() if progress_bar: w.close_progress()
def show_download_source_dialog(self): """The following code is here to be able to enforce the AGPL licence. If you run Mnemosyne as a service over the network, you need to provide users the option to download your modified version of libmnemosyne and the Mnemosyne HTML server. The recommended way to do this is to provide a link at the bottom of the webpage saying "Flash cards by Mnemosyne", with "Mnemosyne" a link taking you to a page with download instructions for the copy of Mnemosyne you are using. Even if you are using an unmodified version of Mnemosyne, you should still host a copy of that source code on your site, in order to set an example for people who do modify the source. """ self.stopwatch().pause() self.flush_sync_server() self.main_widget().show_information(\ _("For instructions on how to download Mnemosyne's source,") + \ " " + _("go to http://www.mnemosyne-proj.org")) self.stopwatch().unpause()
def backup_old_dir(self): # pragma: no cover join = os.path.join # We only do this on OSX, since on the other platforms, we use a # different directory anyway. if sys.platform == "darwin": home = os.path.expanduser("~") old_data_dir = join(unicode(home), "Library", "Mnemosyne") backup_dir = join(unicode(home), "Library", "Mnemosyne_1") # Work around os.path.exists seeming to give wrong results on # OSX 10.6 (but not 10.7). if os.path.exists(join(old_data_dir, "default.db")): # Data was already backed up. return if os.path.exists(old_data_dir): if not os.path.exists(backup_dir): old_files = sorted(os.listdir(old_data_dir)) shutil.move(old_data_dir, backup_dir) new_files = sorted(os.listdir(backup_dir)) assert old_files == new_files self.main_widget().show_information(\ _("Your old 1.x files are now stored here:\n\n" + backup_dir)) else: self.main_widget().show_error(\ _("Tried to backup your old 1.x files to %s, but that directory already exists.") \ % (backup_dir,)) sys.exit()
def edit_card_and_sisters(self, card, new_fact_data, new_card_type, new_tag_names, correspondence): db = self.database() sch = self.scheduler() assert new_card_type.is_fact_data_valid(new_fact_data) # Determine the current tags in use for the sister cards. This # needs to be done before e.g. editing a cloze card creates new # cards which are as yet untagged. fact = db.fact(card.fact._id, is_id_internal=True) current_sister_cards = self.database().cards_from_fact(fact) current_tag_strings = set([sister_card.tag_string() \ for sister_card in current_sister_cards]) # Change the card type if needed. This does not take into account # changes to fact yet, which will come just afterwards. result = self._change_card_type(card.fact, card.card_type, new_card_type, correspondence, new_fact_data) if result in [-2, -1]: # Error, aborted. return result # When there was no card type conversion possible, the cards had to # be recreated from the new fact data. In that case, it is needed to # reload the fact from the database. fact = db.fact(card.fact._id, is_id_internal=True) # Update fact and create, delete and update cards. new_cards, edited_cards, deleted_cards = \ new_card_type.edit_fact(fact, new_fact_data) fact.data = new_fact_data db.update_fact(fact) for deleted_card in deleted_cards: if self.review_controller().card == deleted_card: self.review_controller().card = None sch.remove_from_queue_if_present(deleted_card) db.delete_card(deleted_card) for new_card in new_cards: db.add_card(new_card) for edited_card in edited_cards: db.update_card(edited_card) if new_cards and self.review_controller().learning_ahead == True: self.review_controller().reset() # Apply new tags and modification time to cards and save them back to # the database. Note that this makes sure there is an EDITED_CARD log # entry for each sister card, which is needed when syncing with a # partner that does not have the concept of facts. tag_for_current_card_only = False if len(current_tag_strings) > 1: tag_for_current_card_only = bool(self.main_widget().show_question( _("This card has different tags than its sister cards. Update tags for current card only or for all sister cards?"), _("Current card only"), _("All sister cards"), "") == 0) old_tags = set() tags = db.get_or_create_tags_with_names(new_tag_names) modification_time = int(time.time()) for sister_card in self.database().cards_from_fact(fact): sister_card.modification_time = modification_time if sister_card == card or not tag_for_current_card_only: old_tags = old_tags.union(sister_card.tags) sister_card.tags = tags db.update_card(sister_card) for tag in old_tags: db.delete_tag_if_unused(tag) db.save() return 0
class BrowseCardsDlg(QtWidgets.QDialog, BrowseCardsDialog, TipAfterStartingNTimes, Ui_BrowseCardsDlg): started_n_times_counter = "started_browse_cards_n_times" tip_after_n_times = \ {3 : _("Right-click on a tag name in the card browser to edit or delete it."), 6 : _("Double-click on a card or tag name in the card browser to edit them."), 9 : _("You can reorder columns in the card browser by dragging the header label."), 12 : _("You can resize columns in the card browser by dragging between the header labels."), 15 : _("When editing or previewing cards from the card browser, PageUp/PageDown can be used to move to the previous/next card."), 18 : _("You change the relative size of the card list, card type tree and tag tree by dragging the dividers between them."), 21 : _("In the search box, you can use SQL wildcards like _ (matching a single character) and % (matching one or more characters)."), 24 : _("Cards with strike-through text are inactive in the current set.")} def __init__(self, **kwds): super().__init__(**kwds) self.show_tip_after_starting_n_times() self.setupUi(self) self.setWindowFlags(self.windowFlags() \ | QtCore.Qt.WindowMinMaxButtonsHint) self.setWindowFlags(self.windowFlags() \ & ~ QtCore.Qt.WindowContextHelpButtonHint) self.saved_index = None self.card_model = None # Set up card type tree. self.container_1 = QtWidgets.QWidget(self.splitter_1) self.layout_1 = QtWidgets.QVBoxLayout(self.container_1) self.label_1 = QtWidgets.QLabel(_("Show cards from these card types:"), self.container_1) self.layout_1.addWidget(self.label_1) self.card_type_tree_wdgt = \ CardTypesTreeWdgt(acquire_database=self.unload_qt_database, component_manager=kwds["component_manager"], parent=self.container_1) self.card_type_tree_wdgt.card_types_changed_signal.\ connect(self.reload_database_and_redraw) self.layout_1.addWidget(self.card_type_tree_wdgt) self.splitter_1.insertWidget(0, self.container_1) # Set up tag tree plus search box. self.container_2 = QtWidgets.QWidget(self.splitter_1) self.layout_2 = QtWidgets.QVBoxLayout(self.container_2) self.any_all_tags = QtWidgets.QComboBox(self.container_2) self.any_all_tags.addItem(_("having any of these tags:")) self.any_all_tags.addItem(_("having all of these tags:")) self.layout_2.addWidget(self.any_all_tags) self.tag_tree_wdgt = \ TagsTreeWdgt(acquire_database=self.unload_qt_database, component_manager=kwds["component_manager"], parent=self.container_2) self.tag_tree_wdgt.tags_changed_signal.\ connect(self.reload_database_and_redraw) self.layout_2.addWidget(self.tag_tree_wdgt) self.label_3 = QtWidgets.QLabel( _("containing this text in the cards:"), self.container_2) self.layout_2.addWidget(self.label_3) self.search_box = QtWidgets.QLineEdit(self.container_2) self.search_box.textChanged.connect(self.search_text_changed) self.timer = QtCore.QTimer(self) self.timer.setSingleShot(True) self.timer.timeout.connect(self.update_filter) self.search_box.setFocus() self.layout_2.addWidget(self.search_box) self.splitter_1.insertWidget(1, self.container_2) # Fill tree widgets. criterion = self.database().current_criterion() self.card_type_tree_wdgt.display(criterion) self.tag_tree_wdgt.display(criterion) # When starting the widget, we default with the current criterion # as filter. In this case, we can make a shortcut simply by selecting # on 'active=1' self.load_qt_database() self.display_card_table(run_filter=False) self.card_model.setFilter("cards.active=1") self.card_model.select() self.update_card_counters() self.card_type_tree_wdgt.tree_wdgt.\ itemClicked.connect(self.update_filter) self.tag_tree_wdgt.tree_wdgt.\ itemClicked.connect(self.update_filter) self.any_all_tags.\ currentIndexChanged.connect(self.update_filter) # Context menu. self.table.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.table.customContextMenuRequested.connect(self.context_menu) # Restore state. state = self.config()["browse_cards_dlg_state"] if state: self.restoreGeometry(state) splitter_1_state = self.config()["browse_cards_dlg_splitter_1_state"] if not splitter_1_state: self.splitter_1.setSizes([230, 320]) else: self.splitter_1.restoreState(splitter_1_state) splitter_2_state = self.config()["browse_cards_dlg_splitter_2_state"] if not splitter_2_state: self.splitter_2.setSizes([333, 630]) else: self.splitter_2.restoreState(splitter_2_state) for column in (_ID, ID, CARD_TYPE_ID, _FACT_ID, FACT_VIEW_ID, ACQ_REPS_SINCE_LAPSE, RET_REPS_SINCE_LAPSE, EXTRA_DATA, ACTIVE, SCHEDULER_DATA): self.table.setColumnHidden(column, True) 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 keyPressEvent(self, event): if len(self.table.selectionModel().selectedRows()) == 0: QtWidgets.QDialog.keyPressEvent(self, event) if event.key() in [QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return]: self.menu_edit() elif event.key() == QtCore.Qt.Key_E and \ event.modifiers() == QtCore.Qt.ControlModifier: self.menu_edit() elif event.key() == QtCore.Qt.Key_P and \ event.modifiers() == QtCore.Qt.ControlModifier: self.menu_preview() elif event.key() == QtCore.Qt.Key_F and \ event.modifiers() == QtCore.Qt.ControlModifier: self.search_box.setFocus() elif event.key() in [QtCore.Qt.Key_Delete, QtCore.Qt.Key_Backspace]: self.menu_delete() else: QtWidgets.QDialog.keyPressEvent(self, event) def sister_cards_from_single_selection(self): selected_rows = self.table.selectionModel().selectedRows() if len(selected_rows) == 0: return [] index = selected_rows[0] _fact_id_index = index.model().index(\ index.row(), _FACT_ID, index.parent()) _fact_id = index.model().data(_fact_id_index) fact = self.database().fact(_fact_id, is_id_internal=True) return self.database().cards_from_fact(fact) def facts_from_selection(self): _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)) return facts def _card_ids_from_selection(self): _card_ids = set() for index in self.table.selectionModel().selectedRows(): _card_id_index = index.model().index(\ index.row(), _ID, index.parent()) _card_id = index.model().data(_card_id_index) _card_ids.add(_card_id) return _card_ids def menu_edit(self, index=None): # 'index' gets passed if this function gets called through the # table.doubleClicked event. _card_ids = self._card_ids_from_selection() if len(_card_ids) == 0: return card = self.database().card(_card_ids.pop(), is_id_internal=True) self.edit_dlg = self.component_manager.current("edit_card_dialog")\ (card, allow_cancel=True, started_from_card_browser=True, parent=self, component_manager=self.component_manager) # Here, we don't unload the database already by ourselves, but leave # it to the edit dialog to only do so if needed. self.edit_dlg.before_apply_hook = self.unload_qt_database self.edit_dlg.after_apply_hook = None self.edit_dlg.page_up_down_signal.connect(self.page_up_down_edit) if self.edit_dlg.exec_() == QtWidgets.QDialog.Accepted: self.card_type_tree_wdgt.rebuild() self.tag_tree_wdgt.rebuild() self.load_qt_database() self.display_card_table() # Avoid multiple connections. self.edit_dlg.page_up_down_signal.disconnect(self.page_up_down_edit) def page_up_down_edit(self, up_down): current_row = self.table.selectionModel().selectedRows()[0].row() if up_down == self.edit_dlg.UP: shift = -1 elif up_down == self.edit_dlg.DOWN: shift = 1 self.table.selectRow(current_row + shift) self.edit_dlg.before_apply_hook = self.unload_qt_database def after_apply(): self.load_qt_database() self.display_card_table() self.edit_dlg.after_apply_hook = after_apply self.edit_dlg.apply_changes() # Reload card to make sure the changes are picked up. _card_ids = self._card_ids_from_selection() card = self.database().card(_card_ids.pop(), is_id_internal=True) self.edit_dlg.set_new_card(card) def menu_preview(self): from mnemosyne.pyqt_ui.preview_cards_dlg import PreviewCardsDlg cards = self.sister_cards_from_single_selection() tag_text = cards[0].tag_string() self.preview_dlg = \ PreviewCardsDlg(cards, tag_text, component_manager=self.component_manager, parent=self) self.preview_dlg.page_up_down_signal.connect(\ self.page_up_down_preview) self.preview_dlg.exec_() # Avoid multiple connections. self.preview_dlg.page_up_down_signal.disconnect(\ self.page_up_down_preview) def page_up_down_preview(self, up_down): from mnemosyne.pyqt_ui.preview_cards_dlg import PreviewCardsDlg current_row = self.table.selectionModel().selectedRows()[0].row() if up_down == PreviewCardsDlg.UP: shift = -1 elif up_down == PreviewCardsDlg.DOWN: shift = 1 self.table.selectRow(current_row + shift) self.preview_dlg.index = 0 self.preview_dlg.cards = self.sister_cards_from_single_selection() self.preview_dlg.tag_text = self.preview_dlg.cards[0].tag_string() self.preview_dlg.update_dialog() 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() def menu_change_card_type(self): # Test if all selected cards have the same card type. current_card_type_ids = set() for index in self.table.selectionModel().selectedRows(): card_type_id_index = index.model().index(\ index.row(), CARD_TYPE_ID, index.parent()) card_type_id = index.model().data(card_type_id_index) current_card_type_ids.add(card_type_id) if len(current_card_type_ids) > 1: self.main_widget().show_error\ (_("The selected cards should have the same card type.")) return current_card_type = self.card_type_with_id(current_card_type_ids.pop()) # Get new card type. Use a dict as backdoor to return values # from the dialog. return_values = {} from mnemosyne.pyqt_ui.change_card_type_dlg import ChangeCardTypeDlg dlg = ChangeCardTypeDlg(current_card_type, return_values, component_manager=self.component_manager, parent=self) if dlg.exec_() != QtWidgets.QDialog.Accepted: return new_card_type = return_values["new_card_type"] # Get correspondence. self.correspondence = {} if not current_card_type.fact_keys().issubset( new_card_type.fact_keys()): dlg = ConvertCardTypeKeysDlg(current_card_type, new_card_type, self.correspondence, check_required_fact_keys=True, parent=self) if dlg.exec_() != QtWidgets.QDialog.Accepted: return # Start the actual conversion. facts = self.facts_from_selection() self.unload_qt_database() self.controller().change_card_type(facts, current_card_type, new_card_type, self.correspondence) self.card_type_tree_wdgt.rebuild() self.tag_tree_wdgt.rebuild() self.load_qt_database() self.display_card_table() def menu_add_tags(self): if not self.config()["showed_help_on_adding_tags"]: self.main_widget().show_information(\ "With this option, can you edit the tags of individual cards, without affecting sister cards.") self.config()["showed_help_on_adding_tags"] = True # Get new tag names. Use a dict as backdoor to return values # from the dialog. return_values = {} from mnemosyne.pyqt_ui.add_tags_dlg import AddTagsDlg dlg = AddTagsDlg(return_values, component_manager=self.component_manager, parent=self) if dlg.exec_() != QtWidgets.QDialog.Accepted: return # Add the tags. _card_ids = self._card_ids_from_selection() self.unload_qt_database() for tag_name in return_values["tag_names"]: if not tag_name: continue tag = self.database().get_or_create_tag_with_name(tag_name) self.database().add_tag_to_cards_with_internal_ids(tag, _card_ids) self.tag_tree_wdgt.rebuild() self.load_qt_database() self.display_card_table() def menu_remove_tags(self): if not self.config()["showed_help_on_adding_tags"]: self.main_widget().show_information(\ "With this option, can you edit the tags of individual cards, without affecting sister cards.") self.config()["showed_help_on_adding_tags"] = True # Figure out the tags used by the selected cards. _card_ids = self._card_ids_from_selection() tags = self.database().tags_from_cards_with_internal_ids(_card_ids) # Get new tag names. Use a dict as backdoor to return values # from the dialog. return_values = {} from mnemosyne.pyqt_ui.remove_tags_dlg import RemoveTagsDlg dlg = RemoveTagsDlg(tags, return_values, parent=self) if dlg.exec_() != QtWidgets.QDialog.Accepted: return # Remove the tags. self.unload_qt_database() for tag_name in return_values["tag_names"]: if not tag_name: continue tag = self.database().get_or_create_tag_with_name(tag_name) self.database().remove_tag_from_cards_with_internal_ids(\ tag, _card_ids) self.tag_tree_wdgt.rebuild() self.load_qt_database() self.display_card_table() def load_qt_database(self): self.database().release_connection() qt_db = QtSql.QSqlDatabase.addDatabase("QSQLITE") qt_db.setDatabaseName(self.database().path()) if not qt_db.open(): QtWidgets.QMessageBox.warning( None, _("Mnemosyne"), _("Database error: ") + qt_db.lastError().text()) sys.exit(1) def unload_qt_database(self): # Don't save state twice when closing dialog. if self.card_model is None: return self.saved_index = self.table.indexAt(QtCore.QPoint(0, 0)) self.saved_selection = self.table.selectionModel().selectedRows() self.config()["browse_cards_dlg_table_settings"] \ = self.table.horizontalHeader().saveState() self.table.setModel(QtGui.QStandardItemModel()) del self.card_model self.card_model = None QtSql.QSqlDatabase.removeDatabase(\ QtSql.QSqlDatabase.database().connectionName()) def display_card_table(self, run_filter=True): self.card_model = CardModel(component_manager=self.component_manager) self.card_model.setTable("cards") headers = { QUESTION: _("Question"), ANSWER: _("Answer"), TAGS: _("Tags"), GRADE: _("Grade"), NEXT_REP: _("Next rep"), LAST_REP: _("Last rep"), EASINESS: _("Easiness"), ACQ_REPS: _("Learning\nreps"), RET_REPS: _("Review\nreps"), LAPSES: _("Lapses"), CREATION_TIME: _("Created"), MODIFICATION_TIME: _("Modified") } for key, value in headers.items(): self.card_model.setHeaderData(key, QtCore.Qt.Horizontal, QtCore.QVariant(value)) self.table.setModel(self.card_model) # Slow, and doesn't work very well. #self.table.verticalHeader().setSectionResizeMode(\ # QtWidgets.QHeaderView.ResizeToContents) self.table.horizontalHeader().sectionClicked.connect(\ self.horizontal_header_section_clicked) table_settings = self.config()["browse_cards_dlg_table_settings"] if table_settings: self.table.horizontalHeader().restoreState(table_settings) self.table.horizontalHeader().setSectionsMovable(True) self.table.setItemDelegateForColumn(\ QUESTION, QA_Delegate(QUESTION, component_manager=self.component_manager, parent=self)) self.table.setItemDelegateForColumn(\ ANSWER, QA_Delegate(ANSWER, component_manager=self.component_manager, parent=self)) self.table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) # Since this function can get called multiple times, we need to make # sure there is only a single connection for the double-click event. try: self.table.doubleClicked.disconnect(self.menu_edit) except TypeError: pass self.table.doubleClicked.connect(self.menu_edit) self.table.verticalHeader().hide() query = QtSql.QSqlQuery("select count() from tags") query.first() self.tag_count = query.value(0) if run_filter: self.update_filter() # Needed after tag rename. if self.saved_index: # All of the statements below are needed. # Qt does not (yet) seem to allow to restore the previous column # correctly. self.saved_index = self.card_model.index(self.saved_index.row(), self.saved_index.column()) self.table.scrollTo(self.saved_index) self.table.scrollTo(self.saved_index, QtWidgets.QAbstractItemView.PositionAtTop) # Restore selection. old_selection_mode = self.table.selectionMode() self.table.setSelectionMode( QtWidgets.QAbstractItemView.MultiSelection) # Note that there seem to be serious Qt preformance problems with # selectRow, so we only do this for a small number of rows. if len(self.saved_selection) < 10: for index in self.saved_selection: self.table.selectRow(index.row()) self.table.setSelectionMode(old_selection_mode) def reload_database_and_redraw(self): self.load_qt_database() self.display_card_table() def horizontal_header_section_clicked(self, index): if not self.config()["browse_cards_dlg_sorting_warning_shown"]: self.main_widget().show_information(\ _("You chose to sort this table. Operations in the card browser could now be slower. Next time you start the card browser, the table will be unsorted again.")) self.config()["browse_cards_dlg_sorting_warning_shown"] = True def activate(self): BrowseCardsDialog.activate(self) self.exec_() def search_text_changed(self): # Don't immediately start updating the filter, but wait until the last # keypress was 300 ms ago. self.timer.start(300) def update_filter(self, dummy=None): # Card types and fact views. criterion = DefaultCriterion(self.component_manager) self.card_type_tree_wdgt.checked_to_criterion(criterion) filter = "" for card_type_id, fact_view_id in \ criterion.deactivated_card_type_fact_view_ids: filter += """not (cards.fact_view_id='%s' and cards.card_type_id='%s') and """ \ % (fact_view_id, card_type_id) filter = filter.rsplit("and ", 1)[0] # Tags. self.tag_tree_wdgt.checked_to_active_tags_in_criterion(criterion) if len(criterion._tag_ids_active) == 0: filter = "_id='not_there'" elif len(criterion._tag_ids_active) != self.tag_count: if filter: filter += "and " # Determine all _card_ids. query = QtSql.QSqlQuery("select _id from cards") all__card_ids = set() while query.next(): all__card_ids.add(str(query.value(0))) # Determine _card_ids of card with an active tag. if self.any_all_tags.currentIndex() == 0: query = "select _card_id from tags_for_card where _tag_id in (" for _tag_id in criterion._tag_ids_active: query += "'%s', " % (_tag_id, ) query = query[:-2] + ")" # Determine _card_ids of cards which have all active tags. else: query = "" for _tag_id in criterion._tag_ids_active: query += "select _card_id from tags_for_card where " + \ "_tag_id='%s' intersect " % (_tag_id, ) query = query[:-(len(" intersect "))] query = QtSql.QSqlQuery(query) active__card_ids = set() while query.next(): active__card_ids.add(str(query.value(0))) # Construct most optimal query. if len(active__card_ids) > len(all__card_ids) / 2: filter += "_id not in (" + \ ",".join(all__card_ids - active__card_ids) + ")" else: filter += "_id in (" + ",".join(active__card_ids) + ")" # Search string. search_string = self.search_box.text().replace("'", "''") self.card_model.search_string = search_string if search_string: if filter: filter += " and " filter += "(question like '%%%s%%' or answer like '%%%s%%')" \ % (search_string, search_string) self.card_model.setFilter(filter) self.card_model.select() self.update_card_counters() def update_card_counters(self): filter = self.card_model.filter() # Selected count. query_string = "select count() from cards" if filter: query_string += " where " + filter query = QtSql.QSqlQuery(query_string) query.first() selected = query.value(0) # Active selected count. if not filter: query_string += " where active=1" else: query_string += " and active=1" query = QtSql.QSqlQuery(query_string) query.first() active = query.value(0) self.counter_label.setText(\ _("%d cards shown, of which %d active.") % (selected, active)) def _store_state(self): self.config()["browse_cards_dlg_state"] = self.saveGeometry() self.config()["browse_cards_dlg_splitter_1_state"] = \ self.splitter_1.saveState() self.config()["browse_cards_dlg_splitter_2_state"] = \ self.splitter_2.saveState() # Make sure we start unsorted again next time. if not self.config()["start_card_browser_sorted"]: self.table.horizontalHeader().setSortIndicator\ (-1, QtCore.Qt.AscendingOrder) def closeEvent(self, event): # Generated when clicking the window's close button. self._store_state() self.unload_qt_database() # This allows the state of the tag tree to be saved. self.tag_tree_wdgt.close() def reject(self): self._store_state() # Generated when pressing escape. self.unload_qt_database() return QtWidgets.QDialog.reject(self) def accept(self): # 'accept' does not generate a close event. self._store_state() self.unload_qt_database() return QtWidgets.QDialog.accept(self)
def card_type_changed(self, new_card_type_name): non_latin_widgets = [self.label_non_latin_1, self.label_non_latin_2, self.label_non_latin_3, self.line_non_latin, self.non_latin_font_size_increase] background_align_widgets = [self.background_label, self.background_button, self.align_label, self.alignment] if new_card_type_name == _("<all card types>"): self.affected_card_types = self.card_types() self.fact_key_names = [_("Text")] for widget in non_latin_widgets + background_align_widgets: widget.show() else: new_card_type_name = new_card_type_name new_card_type = self.card_type_by_name[new_card_type_name] self.affected_card_types = [new_card_type] self.fact_key_names = new_card_type.fact_key_names() for widget in background_align_widgets: if new_card_type.id.startswith("7::"): widget.hide() else: widget.show() for widget in non_latin_widgets: widget.hide() for widget in self.dynamic_widgets: self.gridLayout.removeWidget(widget) widget.close() self.dynamic_widgets = [] row = 0 self.font_buttons = QtWidgets.QButtonGroup() self.colour_buttons = QtWidgets.QButtonGroup() self.align_buttons = QtWidgets.QButtonGroup() self.align_buttons.setExclusive(False) for key_name in self.fact_key_names: label = QtWidgets.QLabel(_(key_name) + ":", self) self.gridLayout.addWidget(label, row, 0, 1, 1) self.dynamic_widgets.append(label) font = QtWidgets.QPushButton(_("Select font"), self) self.font_buttons.addButton(font, row) self.gridLayout.addWidget(font, row, 1, 1, 1) self.dynamic_widgets.append(font) colour = QtWidgets.QPushButton(_("Select colour"),self) self.colour_buttons.addButton(colour, row) self.gridLayout.addWidget(colour, row, 2, 1, 1) self.dynamic_widgets.append(colour) row += 1 self.gridLayout.setColumnStretch(1, 10) self.gridLayout.setColumnStretch(2, 10) self.font_buttons.buttonClicked[int].connect(self.update_font) self.colour_buttons.buttonClicked[int].\ connect(self.update_font_colour) current_alignment = self.config().card_type_property(\ "alignment", self.affected_card_types[0], default="center") if current_alignment == "left": self.alignment.setCurrentIndex(0) elif current_alignment == "center": self.alignment.setCurrentIndex(1) elif current_alignment == "right": self.alignment.setCurrentIndex(2) # Make font light if different alignments are active. self.alignment.setFont(self.font()) values = set() for card_type in self.affected_card_types: if not card_type.id in self.config()["alignment"]: values.add("center") else: values.add(self.config()["alignment"][card_type.id]) if len(values) > 1: self.alignment.font().setWeight(25) else: self.alignment.font().setWeight(50) self.adjustSize()
class ConfigurationWdgtCardAppearance(QtWidgets.QWidget, ConfigurationWidget, Ui_ConfigurationWdgtCardAppearance): name = _("Card appearance") def __init__(self, **kwds): super().__init__(**kwds) self.setupUi(self) self.dynamic_widgets = [] self.affected_card_types = [] self.fact_key_names = [] self.non_latin_font_size_increase.setValue\ (self.config()['non_latin_font_size_increase']) # We calculate card_type_by_name here because these names can change # if the user chooses another translation. self.card_types_widget.addItem(_("<all card types>")) self.card_type_by_name = {} for card_type in self.database().sorted_card_types(): if card_type.hidden_from_UI: continue self.card_type_by_name[_(card_type.name)] = card_type self.card_types_widget.addItem(_(card_type.name)) # Store backups in order to be able to revert our changes. self.old_font = deepcopy(self.config()["font"]) self.old_background_colour = \ deepcopy(self.config()["background_colour"]) self.old_font_colour = deepcopy(self.config()["font_colour"]) self.old_alignment = deepcopy(self.config()["alignment"]) def card_type_changed(self, new_card_type_name): non_latin_widgets = [self.label_non_latin_1, self.label_non_latin_2, self.label_non_latin_3, self.line_non_latin, self.non_latin_font_size_increase] background_align_widgets = [self.background_label, self.background_button, self.align_label, self.alignment] if new_card_type_name == _("<all card types>"): self.affected_card_types = self.card_types() self.fact_key_names = [_("Text")] for widget in non_latin_widgets + background_align_widgets: widget.show() else: new_card_type_name = new_card_type_name new_card_type = self.card_type_by_name[new_card_type_name] self.affected_card_types = [new_card_type] self.fact_key_names = new_card_type.fact_key_names() for widget in background_align_widgets: if new_card_type.id.startswith("7::"): widget.hide() else: widget.show() for widget in non_latin_widgets: widget.hide() for widget in self.dynamic_widgets: self.gridLayout.removeWidget(widget) widget.close() self.dynamic_widgets = [] row = 0 self.font_buttons = QtWidgets.QButtonGroup() self.colour_buttons = QtWidgets.QButtonGroup() self.align_buttons = QtWidgets.QButtonGroup() self.align_buttons.setExclusive(False) for key_name in self.fact_key_names: label = QtWidgets.QLabel(_(key_name) + ":", self) self.gridLayout.addWidget(label, row, 0, 1, 1) self.dynamic_widgets.append(label) font = QtWidgets.QPushButton(_("Select font"), self) self.font_buttons.addButton(font, row) self.gridLayout.addWidget(font, row, 1, 1, 1) self.dynamic_widgets.append(font) colour = QtWidgets.QPushButton(_("Select colour"),self) self.colour_buttons.addButton(colour, row) self.gridLayout.addWidget(colour, row, 2, 1, 1) self.dynamic_widgets.append(colour) row += 1 self.gridLayout.setColumnStretch(1, 10) self.gridLayout.setColumnStretch(2, 10) self.font_buttons.buttonClicked[int].connect(self.update_font) self.colour_buttons.buttonClicked[int].\ connect(self.update_font_colour) current_alignment = self.config().card_type_property(\ "alignment", self.affected_card_types[0], default="center") if current_alignment == "left": self.alignment.setCurrentIndex(0) elif current_alignment == "center": self.alignment.setCurrentIndex(1) elif current_alignment == "right": self.alignment.setCurrentIndex(2) # Make font light if different alignments are active. self.alignment.setFont(self.font()) values = set() for card_type in self.affected_card_types: if not card_type.id in self.config()["alignment"]: values.add("center") else: values.add(self.config()["alignment"][card_type.id]) if len(values) > 1: self.alignment.font().setWeight(25) else: self.alignment.font().setWeight(50) self.adjustSize() def update_background_colour(self): # Determine current colour. current_rgb = self.config().card_type_property("background_colour", self.affected_card_types[0]) if current_rgb: current_colour = QtGui.QColor(current_rgb) else: current_colour = self.palette().color(QtGui.QPalette.Base) # Set new colour. colour = QtWidgets.QColorDialog.getColor(current_colour, self) if colour.isValid(): for card_type in self.affected_card_types: self.config().set_card_type_property("background_colour", colour.rgb(), card_type) def update_font(self, index): # Determine keys affected. if len(self.affected_card_types) > 1: affected_fact_key = None # Actually means all the keys. else: affected_fact_key = \ self.affected_card_types[0].fact_keys_and_names[index][0] # Determine current font. if len(self.affected_card_types) > 1: font_strings = set() for card_type in self.affected_card_types: if card_type.hidden_from_UI: continue font_strings.add(self.config().card_type_property(\ "font", card_type, card_type.fact_keys_and_names[0][0])) if len(font_strings) > 1: font_string = None else: font_string = font_strings.pop() else: font_string = self.config().card_type_property(\ "font", self.affected_card_types[0], affected_fact_key) current_font = QtGui.QFont(self.font()) if font_string: current_font.fromString(font_string) # Set new font. font, ok = QtWidgets.QFontDialog.getFont(current_font, self) if ok: font_string = font.toString() for card_type in self.affected_card_types: self.config().set_card_type_property("font", font_string, card_type, affected_fact_key) def update_font_colour(self, index): # Determine keys affected. if len(self.affected_card_types) > 1: affected_fact_key = None # Actually means all the keys. else: affected_fact_key = \ self.affected_card_types[0].fact_keys_and_names[index][0] # Determine current colour. if len(self.affected_card_types) > 1: current_rgb = self.config().card_type_property(\ "font_colour", self.card_type_with_id("1"), "f") else: current_rgb = self.config().card_type_property(\ "font_colour", self.affected_card_types[0], affected_fact_key) if current_rgb: current_colour = QtGui.QColor(current_rgb) else: current_colour = QtGui.QColor(QtCore.Qt.black) # Set new colour. colour = QtWidgets.QColorDialog.getColor(current_colour, self) if colour.isValid(): for card_type in self.affected_card_types: self.config().set_card_type_property("font_colour", colour.rgb(), card_type, affected_fact_key) def update_alignment(self, index): if index == 0: new_alignment = "left" elif index == 1: new_alignment = "center" elif index == 2: new_alignment = "right" for card_type in self.affected_card_types: self.config().set_card_type_property("alignment", new_alignment, card_type) self.alignment.font().setWeight(50) def apply(self): self.config()["non_latin_font_size_increase"] = \ self.non_latin_font_size_increase.value() for card_type in self.card_types(): for render_chain in self.component_manager.all("render_chain"): render_chain.renderer_for_card_type(card_type).\ update(card_type) 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 reset_to_defaults(self): if len(self.affected_card_types) > 1: message = _("Reset all card types to default system font?") else: message = _("Reset '%s' to default system font?") \ % (_(self.affected_card_types[0].name)) result = self.main_widget().show_question(\ message, _("&Yes"), _("&No"), "") if result == 1: return self.non_latin_font_size_increase.setValue(0) if len(self.affected_card_types) > 1: self.config()["font"] = {} self.config()["background_colour"] = {} self.config()["font_colour"] = {} self.config()["alignment"] = {} else: card_type_id = self.affected_card_types[0].id self.config()["font"].pop(card_type_id, None) self.config()["background_colour"].pop(card_type_id, None) self.config()["font_colour"].pop(card_type_id, None) self.config()["alignment"].pop(card_type_id, None) self.alignment.setCurrentIndex(1) def reject(self): self.config()["font"] = self.old_font self.config()["background_colour"] = self.old_background_colour self.config()["font_colour"] = self.old_font_colour self.config()["alignment"] = self.old_alignment for card_type in self.card_types(): for render_chain in self.component_manager.all("render_chain"): render_chain.renderer_for_card_type(card_type).\ update(card_type)
def upgrade_from_old_data_dir(self, old_data_dir): join = os.path.join # Warn people that this directory is no longer used. open(join(old_data_dir, "DIRECTORY_NO_LONGER_USED_BY_MNEMOSYNE2"), "w").close() # Read old configuration. old_config = {} config_file = open(join(old_data_dir, "config"), "rb") import pickle for key, value in pickle.load(config_file).items(): old_config[key] = value # Migrate configuration settings. if "user_id" in old_config: self.config()["user_id"] = old_config["user_id"] if "upload_logs" in old_config: self.config()["upload_science_logs"] = old_config["upload_logs"] if "non_latin_font_size_increase" in old_config: self.config()["non_latin_font_size_increase"] \ = old_config["non_latin_font_size_increase"] for card_type in self.card_types(): if "QA_font" in old_config: self.config().set_card_type_property("font", old_config["QA_font"], card_type) if "left_align" in old_config and old_config["left_align"]: for card_type in self.card_types(): self.config().set_card_type_property("alignment", "left", card_type) # Migrate latex settings. setting_for_file = { "dvipng": "dvipng", "preamble": "latex_preamble", "postamble": "latex_postamble" } for filename, setting in setting_for_file.items(): full_filename = join(old_data_dir, "latex", filename) self.config()[setting] = "" if os.path.exists(full_filename): for line in open(full_filename): self.config()[setting] += line # Copy over everything that does not interfere with Mnemosyne 2. new_data_dir = self.config().data_dir new_media_dir = self.database().media_dir() import shutil shutil.rmtree(join(new_data_dir, "history")) names = [name for name in os.listdir(old_data_dir) if name not in ["backups", "config", "config.py", "config.pyc", "DIRECTORY_NO_LONGER_USED_BY_MNEMOSYNE2", "error_log.txt", "latex", "plugins", "log.txt", "history"] \ and not name.endswith(".mem") and not name is None] self.main_widget().set_progress_text(_("Copying files from 1.x...")) # By copying over the history folder and log.txt, we also completely # preserve the state of all the files that need to uploaded to the # science server. self.main_widget().set_progress_range(len(names) + 2) if os.path.exists(join(old_data_dir, "history")): shutil.copytree(join(old_data_dir, "history"), join(new_data_dir, "history")) self.main_widget().increase_progress(1) shutil.copyfile(join(old_data_dir, "log.txt"), join(new_data_dir, "log.txt")) self.main_widget().increase_progress(1) # We copy all the other files to the media directory. In this way, # if there are media files that are not explicitly referenced in the # cards, it will be easier for the user to fix his path errors after # the upgrade. for name in names: if os.path.isdir(join(old_data_dir, name)): try: shutil.copytree(join(old_data_dir, name), join(new_media_dir, name)) except OSError as e: # https://bugs.launchpad.net/mnemosyne-proj/+bug/1210435 import errno if e.errno != errno.EEXIST: raise e self.main_widget().show_information(\ "Skipping copying of %s because it already exists.") \ % (name, ) else: shutil.copyfile(join(old_data_dir, name), join(new_media_dir, name)) self.main_widget().increase_progress(1) # Upgrade database. old_database = expand_path("default.mem", old_data_dir) for format in self.component_manager.all("file_format"): if format.__class__.__name__ == "Mnemosyne1Mem": format.do_import(old_database) self.controller().reset_study_mode() # Give info to the user. info = _("Upgrade from Mnemosyne 1.x complete!") + "\n\n" info += _("Mnemosyne 2.x now stores its data here:") + "\n\n" info += self.config().data_dir + "\n" if self.config().config_dir != \ self.config().data_dir: # pragma: no cover # Only happens on Linux, outside of the test suite. info += self.config().config_dir self.main_widget().show_information(info)
def retranslateUi(self, StatisticsDlg): _translate = QtCore.QCoreApplication.translate StatisticsDlg.setWindowTitle(_('Statistics')) self.ok_button.setText(_('&OK'))
def __init__(self, component_manager): TipDialog.__init__(self, component_manager) QtGui.QDialog.__init__(self, self.main_widget()) self.tips = [] self.tips.append( _("""For optimal results, it's best to do your repetitions every day.""" )) self.tips.append( _("""You don't need to finish all your daily scheduled repetitions in a single session.""" )) self.tips.append( _("""If you've been away for a few days, don't worry about your backlog. Do as many cards as you feel like to catch up, the rest will be automatically rescheduled to the future in the most optimal way.""" )) self.tips.append( _("""Sister cards are cards which are based on the same information, e.g. a front-to-back card and the corresponding back-to-front card. Mnemosyne will avoid scheduling these on the same day.""" )) self.tips.append( _("""The 'number of non-memorised cards to hold in your hand' setting determines how many cards you are trying to (re)learn at the same time. It does <b>not</b> tell you how many new cards you need to learn per day. You are the judge of that: you can learn more cards or less cards, depending on how you feel.""" )) self.tips.append( _("""In summary, try to do your repetitions every day, but don't worry too much about getting the 'Scheduled' counter to zero, and certainly not about getting the 'Not memorised' counter to zero.""" )) self.tips.append( _("""Grade 1 cards are different from grade 0 cards in the sense that they show up less often.""" )) self.tips.append( _("""Use 'Learn ahead of schedule' sparingly. For cramming before an exam, it's much better to use the cramming scheduler plugin.""" )) self.tips.append( _("""You can use keyboard shortcuts to do your repetitions. Enter, Return or Space stand for the default action. The number keys can be used for grading.""" )) self.tips.append( _("""You can select which cards you wish to study in the '(De)activate cards' menu option.""" )) self.tips.append( _("""It is recommended to put all your cards in a single database and use tag to organise them. Using '(De)activate cards' is much more convenient than having to load and unload several databases.""" )) self.tips.append( _("""You can add multiple tags to a card by separating tags with a comma in the 'Tag(s)' input field.""" )) self.tips.append( _("""You can organise tags in a hierarchy by using :: as separator, e.g. My book::Lesson 1.""" )) self.tips.append( _("""You can add images and sounds to your cards. Right-click on an input field when editing a card to bring up a pop-up menu to do so.""" )) self.tips.append( _("""You can make clones of existing card types. This allows you to format cards in this type independently from cards in the original type. E.g. you can make a clone of 'Vocabulary', call it 'Thai' and set a Thai font specifically for this card type without disturbing your other cards.""" )) self.tips.append( _("""If for a certain card type cloned from Vocabulary you don't need a pronunciation field, you can hide it by right-clicking on it and using the pop-up menu.""" )) self.tips.append( _("""You can use basic HTML tags in your cards to control their appearance. However, if you want all the fields in a certain card type to look the same, it's easier to use the 'Set card appearance' menu option.""" )) self.tips.append( _("""Using 'File - Sync', you can sync this machine with a remote server. Of course, that remote computer needs to have a server running, which can be started from the configuration screen on that remote machine.""" )) self.tips.append( _(""" If you want to sync a mobile device with this computer, don't use 'File - Sync', but first enable a sync server in the configuration dialog, and then start the sync from the mobile device.""" )) self.tips.append( _("""In the 'Activate cards' dialog, you can right-click on a saved set to rename or delete it.""" )) self.tips.append( _("""In the 'Activate cards' dialog, you can double-click on a saved set to activate it and close the dialog.""" )) self.tips.append( _("""Right-click on a tag name in the card browser to edit or delete it.""" )) self.tips.append( _("""Double-click on a card or tag name in the card browser to edit them.""" )) self.tips.append( _("""You can reorder columns in the card browser by dragging the header label.""" )) self.tips.append( _("""You can resize columns in the card browser by dragging between the header labels.""" )) self.tips.append( _("""In the card browser, cards with strike-through text are inactive in the current set.""" )) self.tips.append( _("""When editing or previewing cards from the card browser, PageUp/PageDown can be used to move to the previous/next card.""" )) self.tips.append( _("""In the search box of the card browser, you can use SQL wildcards like _ (matching a single character) and % (matching one or more characters).""" )) self.tips.append( _("""In the 'Add cards' dialog, use Tab to move between different fields, Ctrl+Enter for 'Yet to learn', and Ctrl+2, etc. for the grades.""" )) self.tips.append( _("""In the 'Edit card' dialog, use Tab to move between different fields and Ctrl+Enter to close the dialog and accept the changes.""" )) self.tips.append( _("""Double-click on the name of a saved set in '(De)activate cards' to quickly activate it and close the dialog.""" )) self.tips.append( _("""If you single-click the name of a saved set in '(De)activate cards', modifications to the selected tags and card types are not saved to that set unless you press 'Save this set for later use' again. This allows you to make some quick-and-dirty temporary modifications.""" )) self.tips.append( _("""Mnemosyne can use LaTeX to render mathematical formulas, e.g. <$>x^2+y^2=z^2</$>. (For this, you need LaTeX and dvipng installed.)""" )) self.tips.append( _("""The best way to backup your data is to copy your mnemosyne data directory and move it to a different drive. Mnemosyne keeps automatic backups, but that won't help you if that drive dies...""" )) self.tips.append( _("""You can sort the cards in the 'Browse cards' dialog by by clicking on a column title. Clicking again changes the sort order.""" )) self.tips.append( _("""If you want more fine-grained control over LaTeX's behaviour, see the explanation of the <$$>...</$$> and <latex>...</latex> tags on Mnemosyne's website.""" )) self.tips.append( _("""For optimal performance, keep your drives defragmented.""")) self.tips.append( _("""For optimal performance, do not put your database on a network drive.""" )) self.tips.append( _("""For optimal performance, run 'File - Compact' from time to time, especially after deleting many cards.""" )) self.tips.append( _("""Advanced users can customise more of Mnemosyne by editing the config.py file in their mnemosyne directory. They can also install additional plugins to customise Mnemosyne even further.""" )) self.tips.append( _("""You can follow the development of Mnemosyne at <a href="https://plus.google.com/b/112456861177827156549/112456861177827156549/posts">Google+</a>.""" )) self.tips.append( _("""You can request new features and vote for exisiting requests at <a href="https://mnemosyne.uservoice.com/">uservoice</a>. This helps the developers decide what to work on next.""" )) self.setupUi(self) self.setWindowFlags(self.windowFlags() \ | QtCore.Qt.WindowMinMaxButtonsHint) self.setWindowFlags(self.windowFlags() \ & ~ QtCore.Qt.WindowContextHelpButtonHint) if self.config()["show_daily_tips"] == True: self.show_tips.setCheckState(QtCore.Qt.Checked) else: self.show_tips.setCheckState(QtCore.Qt.Unchecked) # Note: the svg file does not seem to work under windows. #watermark = QtGui.QPixmap(":/mnemosyne/pixmaps/mnemosyne.svg").\ # scaledToHeight(200, QtCore.Qt.SmoothTransformation) watermark = QtGui.QPixmap(":/mnemosyne/pixmaps/mnemosyne.png") self.watermark.setPixmap(watermark) self.update_dialog()
def data(self, index, role=QtCore.Qt.DisplayRole): if role == QtCore.Qt.TextColorRole: card_type_id_index = self.index(index.row(), CARD_TYPE_ID) card_type_id = QtSql.QSqlTableModel.data(\ self, card_type_id_index) colour = QtGui.QColor(QtCore.Qt.black) if card_type_id in self.font_colour_for_card_type_id: colour = self.font_colour_for_card_type_id[card_type_id] return QtCore.QVariant(colour) if role == QtCore.Qt.BackgroundColorRole: card_type_id_index = self.index(index.row(), CARD_TYPE_ID) card_type_id = QtSql.QSqlTableModel.data(\ self, card_type_id_index) if card_type_id in self.background_colour_for_card_type_id: return QtCore.QVariant(\ self.background_colour_for_card_type_id[card_type_id]) else: return QtCore.QVariant(\ QtWidgets.qApp.palette().color(QtGui.QPalette.Base)) column = index.column() if role == QtCore.Qt.TextAlignmentRole and column not in \ (QUESTION, ANSWER, TAGS): return QtCore.QVariant(QtCore.Qt.AlignCenter) if role == QtCore.Qt.FontRole and column not in \ (QUESTION, ANSWER, TAGS): active_index = self.index(index.row(), ACTIVE) active = super().data(active_index) font = QtGui.QFont() if not active: font.setStrikeOut(True) return QtCore.QVariant(font) if role != QtCore.Qt.DisplayRole: return super().data(index, role) # Display roles to format some columns in a more pretty way. Note that # sorting still uses the orginal database keys, which is good # for speed. if column == GRADE: grade = super().data(index) if grade == -1: return QtCore.QVariant(_("Yet to learn")) else: return QtCore.QVariant(grade) if column == NEXT_REP: grade_index = self.index(index.row(), GRADE) grade = super().data(grade_index) if grade < 2: return QtCore.QVariant("") next_rep = super().data(index, role) if next_rep <= 0: return QtCore.QVariant("") return QtCore.QVariant(\ self.scheduler().next_rep_to_interval_string(next_rep)) if column == LAST_REP: last_rep = super().data(index, role) if last_rep <= 0: return QtCore.QVariant("") return QtCore.QVariant(\ self.scheduler().last_rep_to_interval_string(last_rep)) if column == EASINESS: old_data = super().data(index, role) return QtCore.QVariant("%.2f" % float(old_data)) if column in (CREATION_TIME, MODIFICATION_TIME): old_data = super().data(index, role) return QtCore.QVariant( time.strftime(self.date_format, time.localtime(old_data))) return super().data(index, role)
def retranslateUi(self, ConfigurationWdgtMain): _translate = QtCore.QCoreApplication.translate ConfigurationWdgtMain.setWindowTitle(_('Form')) self.groupBox.setTitle(_('Scheduler')) self.label_2.setText(_('Show new cards for the first time')) self.new_cards.setItemText(0, _('in the order they were added')) self.new_cards.setItemText(1, _('in random order')) self.label_3.setText(_('Review memorised cards')) self.scheduled_cards.setItemText(0, _('most urgent first')) self.scheduled_cards.setItemText(1, _('in random order')) self.label.setText(_('Hold')) self.label_4.setText(_('non-memorised cards in your hand')) self.label_5.setText(_('Autosave after')) self.label_6.setText(_('repetitions')) self.audio_box.setTitle(_('Audio/video')) self.media_autoplay.setText(_('Start automatically when displaying card')) self.media_controls.setText(_('Show controls (pause, ...)')) self.groupBox_3.setTitle(_('Science')) self.upload_science_logs.setText(_('Upload anonymous science logs')) self.groupBox_4.setTitle(_('Language'))
class ActivateCardsDlg(QtWidgets.QDialog, ActivateCardsDialog, TipAfterStartingNTimes, Ui_ActivateCardsDlg): started_n_times_counter = "started_activate_cards_n_times" tip_after_n_times = \ {3 : _("If you find yourself selecting the same tags and card types many types, you can press the button 'Save this set for later use' to give it a name to select it more quickly later."), 6 : _("Double-click on the name of a saved set to quickly activate it and close the dialog."), 9 : _("You can right-click on the name of a saved set to rename or delete it."), 12 : _("If you single-click the name of a saved set, modifications to the selected tags and card types are not saved to that set unless you press 'Save this set for later use' again. This allows you to make some quick-and-dirty temporary modifications.")} def __init__(self, **kwds): super().__init__(**kwds) self.setupUi(self) self.setWindowFlags(self.windowFlags() \ | QtCore.Qt.WindowMinMaxButtonsHint) self.setWindowFlags(self.windowFlags() \ & ~ QtCore.Qt.WindowContextHelpButtonHint) # Initialise widgets. self.was_showing_a_saved_set = False self.is_shutting_down = False criterion = self.database().current_criterion() self.criterion_classes = \ self.component_manager.all("criterion") current_criterion = self.database().current_criterion() self.widget_for_criterion_type = {} for criterion_class in self.criterion_classes: widget = self.component_manager.current\ ("criterion_widget", used_for=criterion_class)\ (component_manager=self.component_manager, parent=self) self.tab_widget.addTab(widget, criterion_class.criterion_type) self.widget_for_criterion_type[criterion_class.criterion_type] \ = widget self.tab_widget.setCurrentWidget(self.widget_for_criterion_type\ [current_criterion.criterion_type]) self.tab_widget.tabBar().setVisible(self.tab_widget.count() > 1) self.tab_widget.currentWidget().display_criterion(current_criterion) # Restore state. state = self.config()["activate_cards_dlg_state"] if state: self.restoreGeometry(state) splitter_state = self.config()["activate_cards_dlg_splitter_state"] if not splitter_state: self.splitter.setSizes([100, 350]) else: self.splitter.restoreState(splitter_state) # Should go last, otherwise the selection of the saved sets pane will # always be cleared. self.update_saved_sets_pane() def keyPressEvent(self, event): if event.key() in [QtCore.Qt.Key_Delete, QtCore.Qt.Key_Backspace]: self.delete_set() else: QtWidgets.QDialog.keyPressEvent(self, event) def change_widget(self, index): self.saved_sets.clearSelection() def activate(self): self.exec_() def update_saved_sets_pane(self): self.saved_sets.clear() self.criteria_by_name = {} active_name = "" active_criterion = self.database().current_criterion() for criterion in self.database().criteria(): if criterion._id != 1: self.criteria_by_name[criterion.name] = criterion self.saved_sets.addItem(criterion.name) if criterion == active_criterion: active_name = criterion.name self.saved_sets.sortItems() if active_name: item = self.saved_sets.findItems(active_name, QtCore.Qt.MatchExactly)[0] self.saved_sets.setCurrentItem(item) self.was_showing_a_saved_set = True else: self.saved_sets.clearSelection() self.was_showing_a_saved_set = False splitter_sizes = self.splitter.sizes() if self.saved_sets.count() == 0: self.splitter.setSizes([0, sum(splitter_sizes)]) else: if splitter_sizes[0] == 0: # First time we add a set. self.splitter.setSizes( [0.3 * sum(splitter_sizes), 0.7 * sum(splitter_sizes)]) def saved_sets_custom_menu(self, pos): menu = QtWidgets.QMenu() menu.addAction(_("Delete"), self.delete_set) menu.addAction(_("Rename"), self.rename_set) menu.exec_(self.saved_sets.mapToGlobal(pos)) def save_set(self): criterion = self.tab_widget.currentWidget().criterion() if criterion.is_empty(): self.main_widget().show_error(\ _("This set can never contain any cards!")) return CardSetNameDlg(criterion, self.criteria_by_name.keys(), component_manager=self.component_manager, parent=self).exec_() if not criterion.name: # User cancelled. return if criterion.name in self.criteria_by_name.keys(): answer = self.main_widget().show_question(_("Update this set?"), _("&OK"), _("&Cancel"), "") if answer == 1: # Cancel. return original_criterion = self.criteria_by_name[criterion.name] criterion._id = original_criterion._id criterion.id = original_criterion.id self.database().update_criterion(criterion) else: self.database().add_criterion(criterion) self.update_saved_sets_pane() item = self.saved_sets.findItems(criterion.name, QtCore.Qt.MatchExactly)[0] self.saved_sets.setCurrentItem(item) if self.config()["showed_help_on_renaming_sets"] == False: self.main_widget().show_information(\ _("You can right-click on the name of a saved set to rename or delete it.")) self.config()["showed_help_on_renaming_sets"] = True def delete_set(self): if not self.saved_sets.currentItem(): return answer = self.main_widget().show_question(_("Delete this set?"), _("&OK"), _("&Cancel"), "") if answer == 1: # Cancel. return -1 else: name = self.saved_sets.currentItem().text() criterion = self.criteria_by_name[name] self.database().delete_criterion(criterion) self.database().save() self.update_saved_sets_pane() def rename_set(self): name = self.saved_sets.currentItem().text() criterion = self.criteria_by_name[name] criterion.name = name other_names = list(self.criteria_by_name.keys()) other_names.remove(name) CardSetNameDlg(criterion, other_names, component_manager=self.component_manager, parent=self).exec_() if criterion.name == name: # User cancelled. return self.database().update_criterion(criterion) self.database().save() self.update_saved_sets_pane() item = self.saved_sets.findItems(criterion.name, QtCore.Qt.MatchExactly)[0] self.saved_sets.setCurrentItem(item) # load_set gets triggered by ItemActivated, but this does not happen # when the user changes the sets through the arrow keys (Qt bug?). # Therefore, we also catch currentItemChanged and forward it to # change_set, but to prevent unwanted firing when loading the widget # for the first time (which would erase the current criterion in case # it is not a saved criterion), we discard this event if previous_item # is None. # # To test when editing this code: initial start, with and without # current criterion being a saved criterion, changing the set through # clicking or through the arrows. def load_set(self, item, dummy=None): # Sometimes item is None, e.g. during the process of deleting a saved # set, so we need to discard the event then. if item is None: return name = item.text() criterion = self.criteria_by_name[name] self.tab_widget.setCurrentWidget(self.widget_for_criterion_type\ [criterion.criterion_type]) self.tab_widget.currentWidget().display_criterion(criterion) # Restore the selection that got cleared in change_widget. item = self.saved_sets.findItems(criterion.name, QtCore.Qt.MatchExactly)[0] self.saved_sets.setCurrentItem(item) self.was_showing_a_saved_set = True def change_set(self, item, previous_item): if previous_item is not None: self.load_set(item) def select_set_and_close(self, item): self.load_set(item) # Work around a Qt bug where these calls would still fire when clicking # in the same area where e.g. the tag browser used to be, even after # closing the 'Activate cards' window. self.is_shutting_down = True self.accept() def _store_state(self): self.config()["activate_cards_dlg_state"] = \ self.saveGeometry() self.config()["activate_cards_dlg_splitter_state"] = \ self.splitter.saveState() def closeEvent(self, event): # Generated when clicking the window's close button. self._store_state() # This allows the state of the tag tree to be saved. self.tab_widget.currentWidget().close() def accept(self): criterion = self.tab_widget.currentWidget().criterion() if criterion.is_empty(): self.main_widget().show_error(\ _("This set can never contain any cards!")) return if self.saved_sets.count() != 0 and self.config()\ ["showed_help_on_double_clicking_sets"] == False: self.main_widget().show_information(\ _("You can double-click on the name of a saved set to activate it and close the dialog.")) self.config()["showed_help_on_double_clicking_sets"] = True if len(self.saved_sets.selectedItems()) > 0: criterion.name = self.saved_sets.currentItem().text() self.database().set_current_criterion(criterion) # 'accept' does not generate a close event. self._store_state() return QtWidgets.QDialog.accept(self)
def saved_sets_custom_menu(self, pos): menu = QtWidgets.QMenu() menu.addAction(_("Delete"), self.delete_set) menu.addAction(_("Rename"), self.rename_set) menu.exec_(self.saved_sets.mapToGlobal(pos))
def format(self): for _format in self.component_manager.all("file_format"): if _(_format.description) == self.file_formats.currentText(): return _format
def work_ended(self): self.main_widget().close_progress() self.main_widget().show_information(_("Done!")) QtWidgets.QDialog.accept(self)
def do_import(self, filename, extra_tag_names=None): FileFormat.do_import(self, filename, extra_tag_names) try: f = open(filename, encoding="utf-8") except: self.main_widget().show_error(_("Could not load file.")) return facts_data = [] line_number = 0 for line in f: line_number += 1 line = line.rstrip() # Parse html style escaped unicode (e.g. 至). for match in re0.finditer(line): # Integer part. u = chr(int(match.group(1))) # Integer part with &# and ;. line = line.replace(match.group(), u) if len(line) == 0: continue if line[0] == "\\ufeff": # Remove byte-order mark. line = line[1:] fields = line.split("\t") if len(fields) >= 3: # Vocabulary card. if len(fields) >= 4: facts_data.append({ "f": fields[0], "p_1": fields[1], "m_1": fields[2], "n": fields[3] }) else: facts_data.append({ "f": fields[0], "p_1": fields[1], "m_1": fields[2] }) elif len(fields) == 2: # Front-to-back only. facts_data.append({"f": fields[0], "b": fields[1]}) else: # Malformed line. self.main_widget().show_error(_("Badly formed input on line") \ + " " + str(line_number) + ":\n" + line) return # Now that we know all the data is well-formed, create the cards. tag_names = [] if extra_tag_names: tag_names += [tag_name.strip() for tag_name \ in extra_tag_names.split(",")] for fact_data in facts_data: if len(list(fact_data.keys())) == 2: card_type = self.card_type_with_id("1") else: card_type = self.card_type_with_id("3") self.preprocess_media(fact_data, tag_names) self.controller().create_new_cards(fact_data, card_type, grade=-1, tag_names=tag_names, check_for_duplicates=False, save=False) if _("MISSING_MEDIA") in tag_names: tag_names.remove(_("MISSING_MEDIA")) self.warned_about_missing_media = False
def __init__(self, **kwds): super().__init__(**kwds) self.show_tip_after_starting_n_times() self.setupUi(self) self.setWindowFlags(self.windowFlags() \ | QtCore.Qt.WindowMinMaxButtonsHint) self.setWindowFlags(self.windowFlags() \ & ~ QtCore.Qt.WindowContextHelpButtonHint) self.saved_index = None self.card_model = None # Set up card type tree. self.container_1 = QtWidgets.QWidget(self.splitter_1) self.layout_1 = QtWidgets.QVBoxLayout(self.container_1) self.label_1 = QtWidgets.QLabel(_("Show cards from these card types:"), self.container_1) self.layout_1.addWidget(self.label_1) self.card_type_tree_wdgt = \ CardTypesTreeWdgt(acquire_database=self.unload_qt_database, component_manager=kwds["component_manager"], parent=self.container_1) self.card_type_tree_wdgt.card_types_changed_signal.\ connect(self.reload_database_and_redraw) self.layout_1.addWidget(self.card_type_tree_wdgt) self.splitter_1.insertWidget(0, self.container_1) # Set up tag tree plus search box. self.container_2 = QtWidgets.QWidget(self.splitter_1) self.layout_2 = QtWidgets.QVBoxLayout(self.container_2) self.any_all_tags = QtWidgets.QComboBox(self.container_2) self.any_all_tags.addItem(_("having any of these tags:")) self.any_all_tags.addItem(_("having all of these tags:")) self.layout_2.addWidget(self.any_all_tags) self.tag_tree_wdgt = \ TagsTreeWdgt(acquire_database=self.unload_qt_database, component_manager=kwds["component_manager"], parent=self.container_2) self.tag_tree_wdgt.tags_changed_signal.\ connect(self.reload_database_and_redraw) self.layout_2.addWidget(self.tag_tree_wdgt) self.label_3 = QtWidgets.QLabel( _("containing this text in the cards:"), self.container_2) self.layout_2.addWidget(self.label_3) self.search_box = QtWidgets.QLineEdit(self.container_2) self.search_box.textChanged.connect(self.search_text_changed) self.timer = QtCore.QTimer(self) self.timer.setSingleShot(True) self.timer.timeout.connect(self.update_filter) self.search_box.setFocus() self.layout_2.addWidget(self.search_box) self.splitter_1.insertWidget(1, self.container_2) # Fill tree widgets. criterion = self.database().current_criterion() self.card_type_tree_wdgt.display(criterion) self.tag_tree_wdgt.display(criterion) # When starting the widget, we default with the current criterion # as filter. In this case, we can make a shortcut simply by selecting # on 'active=1' self.load_qt_database() self.display_card_table(run_filter=False) self.card_model.setFilter("cards.active=1") self.card_model.select() self.update_card_counters() self.card_type_tree_wdgt.tree_wdgt.\ itemClicked.connect(self.update_filter) self.tag_tree_wdgt.tree_wdgt.\ itemClicked.connect(self.update_filter) self.any_all_tags.\ currentIndexChanged.connect(self.update_filter) # Context menu. self.table.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.table.customContextMenuRequested.connect(self.context_menu) # Restore state. state = self.config()["browse_cards_dlg_state"] if state: self.restoreGeometry(state) splitter_1_state = self.config()["browse_cards_dlg_splitter_1_state"] if not splitter_1_state: self.splitter_1.setSizes([230, 320]) else: self.splitter_1.restoreState(splitter_1_state) splitter_2_state = self.config()["browse_cards_dlg_splitter_2_state"] if not splitter_2_state: self.splitter_2.setSizes([333, 630]) else: self.splitter_2.restoreState(splitter_2_state) for column in (_ID, ID, CARD_TYPE_ID, _FACT_ID, FACT_VIEW_ID, ACQ_REPS_SINCE_LAPSE, RET_REPS_SINCE_LAPSE, EXTRA_DATA, ACTIVE, SCHEDULER_DATA): self.table.setColumnHidden(column, True)
def display_card_table(self, run_filter=True): self.card_model = CardModel(component_manager=self.component_manager) self.card_model.setTable("cards") headers = { QUESTION: _("Question"), ANSWER: _("Answer"), TAGS: _("Tags"), GRADE: _("Grade"), NEXT_REP: _("Next rep"), LAST_REP: _("Last rep"), EASINESS: _("Easiness"), ACQ_REPS: _("Learning\nreps"), RET_REPS: _("Review\nreps"), LAPSES: _("Lapses"), CREATION_TIME: _("Created"), MODIFICATION_TIME: _("Modified") } for key, value in headers.items(): self.card_model.setHeaderData(key, QtCore.Qt.Horizontal, QtCore.QVariant(value)) self.table.setModel(self.card_model) # Slow, and doesn't work very well. #self.table.verticalHeader().setSectionResizeMode(\ # QtWidgets.QHeaderView.ResizeToContents) self.table.horizontalHeader().sectionClicked.connect(\ self.horizontal_header_section_clicked) table_settings = self.config()["browse_cards_dlg_table_settings"] if table_settings: self.table.horizontalHeader().restoreState(table_settings) self.table.horizontalHeader().setSectionsMovable(True) self.table.setItemDelegateForColumn(\ QUESTION, QA_Delegate(QUESTION, component_manager=self.component_manager, parent=self)) self.table.setItemDelegateForColumn(\ ANSWER, QA_Delegate(ANSWER, component_manager=self.component_manager, parent=self)) self.table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) # Since this function can get called multiple times, we need to make # sure there is only a single connection for the double-click event. try: self.table.doubleClicked.disconnect(self.menu_edit) except TypeError: pass self.table.doubleClicked.connect(self.menu_edit) self.table.verticalHeader().hide() query = QtSql.QSqlQuery("select count() from tags") query.first() self.tag_count = query.value(0) if run_filter: self.update_filter() # Needed after tag rename. if self.saved_index: # All of the statements below are needed. # Qt does not (yet) seem to allow to restore the previous column # correctly. self.saved_index = self.card_model.index(self.saved_index.row(), self.saved_index.column()) self.table.scrollTo(self.saved_index) self.table.scrollTo(self.saved_index, QtWidgets.QAbstractItemView.PositionAtTop) # Restore selection. old_selection_mode = self.table.selectionMode() self.table.setSelectionMode( QtWidgets.QAbstractItemView.MultiSelection) # Note that there seem to be serious Qt preformance problems with # selectRow, so we only do this for a small number of rows. if len(self.saved_selection) < 10: for index in self.saved_selection: self.table.selectRow(index.row()) self.table.setSelectionMode(old_selection_mode)
def show_information(self, text): QtWidgets.QMessageBox.information(self.top_window(), _("Mnemosyne"), text)
def horizontal_header_section_clicked(self, index): if not self.config()["browse_cards_dlg_sorting_warning_shown"]: self.main_widget().show_information(\ _("You chose to sort this table. Operations in the card browser could now be slower. Next time you start the card browser, the table will be unsorted again.")) self.config()["browse_cards_dlg_sorting_warning_shown"] = True
def read_items_from_mnemosyne1_xml(self, filename): # Reset anonymiser when importing a new file, otherwise information # from the previous file still lingers and we get erroneously think # we've imported this before. self.anon_to_id = {} w = self.main_widget() try: tree = cElementTree.parse(filename) except cElementTree.ParseError as e: w.show_error(_("Unable to parse file:") + str(e)) raise MnemosyneError except: w.show_error(_("Unable to open file.")) raise MnemosyneError if tree.getroot().tag != "mnemosyne" or \ tree.getroot().get("core_version") != "1": w.show_error( _("XML file does not seem to be a Mnemosyne 1.x XML file.")) raise MnemosyneError self.starttime = 0 if tree.getroot().get("time_of_start"): self.starttime = int(tree.getroot().get("time_of_start")) category_with_name = {} self.categories = [] for element in tree.findall("category"): category = Mnemosyne1.MnemosyneCore.Category() category.name = element.find("name").text category.active = bool(element.get("active")) self.categories.append(category) category_with_name[category.name] = category self.items = [] warned_about_import = False for element in tree.findall("item"): item = Mnemosyne1.MnemosyneCore.Item() item.id = element.get("id") if not item.id: item.id = rand_uuid() if item.id.startswith('_'): item.id = self.unanonymise_id(item.id) item.q = element.find("Q").text item.a = element.find("A").text if item.a is None: item.a = "" item.cat = category_with_name[element.find("cat").text] if element.get("gr"): if not warned_about_import: result = w.show_question( _("This XML file contains learning data. It's best to import this from a mem file, in order to preserve historical statistics. Continue?" ), _("Yes"), _("No"), "") warned_about_import = True if result == 1: # No return item.grade = int(element.get("gr")) else: item.grade = 0 if element.get("e"): item.easiness = float(element.get("e")) else: item.easiness = 2.5 if element.get("ac_rp"): item.acq_reps = int(element.get("ac_rp")) else: item.acq_reps = 0 if element.get("rt_rp"): item.ret_reps = int(element.get("rt_rp")) else: item.ret_reps = 0 if element.get("lps"): item.lapses = int(element.get("lps")) else: item.lapses = 0 if element.get("ac_rp_l"): item.acq_reps_since_lapse = int(element.get("ac_rp_l")) else: item.acq_reps_since_lapse = 0 if element.get("rt_rp_l"): item.ret_reps_since_lapse = int(element.get("rt_rp_l")) else: item.ret_reps_since_lapse = 0 if element.get("l_rp"): item.last_rep = int(float(element.get("l_rp"))) else: item.last_rep = 0 if element.get("n_rp"): item.next_rep = int(float(element.get("n_rp"))) else: item.next_rep = 0 if element.get("u"): item.unseen = bool(element.get("u")) else: if item.acq_reps <= 1 and item.ret_reps == 0 \ and item.grade == 0: item.unseen = True else: item.unseen = False self.items.append(item)
class Schedule(PlotStatisticsPage): name = _("Schedule") NEXT_WEEK = 1 NEXT_MONTH = 2 NEXT_3_MONTHS = 3 NEXT_6_MONTHS = 4 NEXT_YEAR = 5 LAST_WEEK = 6 LAST_MONTH = 7 LAST_3_MONTHS = 8 LAST_6_MONTHS = 9 LAST_YEAR = 10 variants = [(NEXT_WEEK, _("Next week (active cards only)")), (NEXT_MONTH, _("Next month (active cards only)")), (NEXT_3_MONTHS, _("Next 3 months (active cards only)")), (NEXT_6_MONTHS, _("Next 6 months (active cards only)")), (NEXT_YEAR, _("Next year (active cards only)")), (LAST_WEEK, _("Last week (all cards)")), (LAST_MONTH, _("Last month (all cards)")), (LAST_3_MONTHS, _("Last 3 months (all cards)")), (LAST_6_MONTHS, _("Last 6 months (all cards)")), (LAST_YEAR, _("Last year (all cards)"))] def prepare_statistics(self, variant): if variant == self.NEXT_WEEK: self.x = list(range(1, 8, 1)) elif variant == self.NEXT_MONTH: self.x = list(range(1, 32, 1)) elif variant == self.NEXT_3_MONTHS: self.x = list(range(1, 92, 1)) elif variant == self.NEXT_6_MONTHS: self.x = list(range(1, 183, 1)) elif variant == self.NEXT_YEAR: self.x = list(range(1, 366, 1)) elif variant == self.LAST_WEEK: self.x = list(range(-7, 1, 1)) elif variant == self.LAST_MONTH: self.x = list(range(-31, 1, 1)) elif variant == self.LAST_3_MONTHS: self.x = list(range(-91, 1, 1)) elif variant == self.LAST_6_MONTHS: self.x = list(range(-182, 1, 1)) elif variant == self.LAST_YEAR: self.x = list(range(-365, 1, 1)) else: raise AttributeError("Invalid variant") self.main_widget().set_progress_text(_("Calculating statistics...")) self.main_widget().set_progress_range(len(self.x)) self.main_widget().set_progress_update_interval(3) self.y = [] for day in self.x: self.y.append(\ self.scheduler().card_count_scheduled_n_days_from_now(n=day)) self.main_widget().increase_progress(1) self.main_widget().close_progress()
class Smconv_XML(FileFormat, MediaPreprocessor): """Import the xml file created by the smconv.pl script to Mnemosyne. smconv.pl is available at http://smconvpl.sourceforge.net and reads SuperMemo for Palm databases and exports them to XML. In order to import the generated XML into mnemosyne, care must be taken to ensure the correct charset encoding of the input file. In my case, the palm databases are "windows-1252". The xml file generated by smconv.pl was set to "us-ascii". This makes the XML parser fail. For me, changing the xml header to <?xml version="1.0" encoding="windows-1252"?> worked well. However, your mileage may vary. Restrictions: - SM for Palm has six fields for each card. Templates can be used to format these fields and to control whether they are part of the question or of the answer. However this class assumes that the first field is the question and the second field is the answer. """ description = _("Supermemo for Palm through smconv.pl") extension = ".xml" filename_filter = _("Supermemo for Palm XML files (*.xml *.XML)") 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.find("cards").findall("card"): category = element.attrib["category"] commit = not (element.attrib["commit"] == "0") for field in element.find("card_fields").findall("card_field"): if field.attrib["idx"] == "1": question = field.text else: answer = field.text card_other = element.find("card_other") if card_other is None: difficulty = 40 difficulty_prev = 40 else: difficulty = int(card_other.attrib["difficulty"]) difficulty_prev = int(card_other.attrib["difficulty_prev"]) # Grades are 0-5. In SM for Palm there are commited and uncommited # cards. Uncommited cards go to grade -1. # Otherwise try to extrapolate something from difficulty in SM # I have implemented guess_grade such, that the distribution of # grades looks reasonable for my test database of 4000 entries. # By "reasonable" I mean than most of the entries should be # at grade 4. I've been learning that database for 4 years, so the # cards should have converged by now. if commit == False: grade = -1 # Very easy items are scarce in SM and must be easiest grade. elif difficulty < 10: grade = 5 # Assign passing grades, based upon whether the difficulty has # changed. elif difficulty > difficulty_prev: grade = 2 elif difficulty == difficulty_prev: grade = 3 elif difficulty < difficulty_prev: grade = 4 # If the interval becomes shorter, it must have been a failure. if card_other is None: interval = 0 interval_prev = 0 else: interval = int(card_other.attrib["interval"]) * DAY interval_prev = int(card_other.attrib["interval_prev"]) * DAY if interval < interval_prev: grade = 0 # Construct card. fact_data = {"f": question, "b": answer} self.preprocess_media(fact_data, tag_names) card = self.controller().create_new_cards( fact_data, card_type, grade=grade, tag_names=tag_names + [category], check_for_duplicates=False, save=False)[0] if _("MISSING_MEDIA") in tag_names: tag_names.remove(_("MISSING_MEDIA")) if card_other is not None: card.creation_time = int(time.mktime(time.strptime(\ card_other.attrib["datecreate"], "%Y-%m-%d"))) card.modification_time = int(time.mktime(time.strptime(\ card_other.attrib["datecommit"], "%Y-%m-%d"))) card.next_rep = self.scheduler().midnight_UTC(int(time.mktime(\ time.strptime(card_other.attrib["datenexttest"], "%Y-%m-%d")))) card.last_rep = card.next_rep - interval card.lapses = int(card_other.attrib["lapses"]) # Try to fill acquisiton reps and retention reps. # Since SM statistics are only available for commited # cards, I take acq_reps = 0 and ret_reps = lapses + recalls. card.ret_reps = card.lapses + int(card_other.attrib["recalls"]) # Try to derive an easines factor EF from [1.3 .. 3.2] from # difficulty d from [1% .. 100%]. # The math below is set to translate # difficulty=100% --> easiness = 1.3 # difficulty=40% --> easiness = 2.5 # difficulty=1% --> easiness = 3.2 dp = difficulty * 0.01 # Small values should be easy, large ones hard. if dp > 0.4: card.easiness = 1.28 - 1.32 * math.log(dp) else: card.easiness = 4.2 - 1.139 * math.exp(dp) self.database().update_card(card) self.warned_about_missing_media = False
def fact_key_with_name(self, name): for fact_key, fact_key_name in self.fact_keys_and_names: if fact_key_name == name or _(fact_key_name) == name: return fact_key
def retranslateUi(self, RenameTagDlg): _translate = QtCore.QCoreApplication.translate RenameTagDlg.setWindowTitle(_('Rename tag')) self.label.setText(_('Enter new tag name:')) self.ok_button.setText(_('&OK')) self.cancel_button.setText(_('&Cancel'))
class Mnemosyne1Mem(FileFormat, Mnemosyne1): description = _("Mnemosyne 1.x *.mem files") extension = ".mem" filename_filter = _("Mnemosyne 1.x *.mem databases (*.mem)") import_possible = True export_possible = False def do_import(self, filename, extra_tag_names=""): FileFormat.do_import(self, filename, extra_tag_names) w = self.main_widget() w.set_progress_text(_("Importing cards...")) db = self.database() # The import process generates card log entries, which we will delete # in favour of those events that are recorded in the logs and which # capture the true timestamps. They also have new 2.0 ids, as opposed # to their old 1.x ids. log_index = db.current_log_index() try: self.read_items_from_mnemosyne1_mem(filename) self.create_cards_from_mnemosyne1(extra_tag_names) except MnemosyneError: w.close_progress() return db.remove_card_log_entries_since(log_index) self.import_logs(filename) # Force an ADDED_CARD log entry for those cards that did not figure in # the txt logs, e.g. due to missing or corrupt logs. db.add_missing_added_card_log_entries( set(item.id for item in self.items)) # In 2.x, repetition events are used to update a card's last_rep and # next_rep during sync. In 1.x, there was no such information, and # calculating it from the logs will fail if they are incomplete. # Therefore, we force a card edit event for all cards. timestamp = int(time.time()) for item in self.items: db.log_edited_card(timestamp, item.id) # Detect inverses. db.link_inverse_cards() w.close_progress() self.warned_about_missing_media = False def read_items_from_mnemosyne1_mem(self, filename): sys.modules["mnemosyne.core"] = object() sys.modules["mnemosyne.core.mnemosyne_core"] \ = Mnemosyne1.MnemosyneCore() # For importing Python 2 pickles, we run into this bug: # http://bugs.python.org/issue22005 # Workaround is opening this file using 'bytes' encoding, but # this requires extra work for us in setting up the data members. try: memfile = open(filename, "rb") header = memfile.readline() self.starttime, self.categories, self.items \ = pickle.load(memfile, encoding="bytes") self.starttime = self.starttime.__dict__[b"time"] for category in self.categories: category.name = category.__dict__[b"name"] category.active = category.__dict__[b"active"] del category.__dict__[b"name"] del category.__dict__[b"active"] for item in self.items: if type(item.__dict__[b"id"]) == bytes: item.id = str(item.__dict__[b"id"], "utf-8") else: item.id = item.__dict__[b"id"] item.cat = item.__dict__[b"cat"] item.q = item.__dict__[b"q"] item.a = item.__dict__[b"a"] item.unseen = item.__dict__[b"unseen"] item.grade = item.__dict__[b"grade"] item.next_rep = item.__dict__[b"next_rep"] item.last_rep = item.__dict__[b"last_rep"] item.easiness = item.__dict__[b"easiness"] item.acq_reps = item.__dict__[b"acq_reps"] item.ret_reps = item.__dict__[b"ret_reps"] item.lapses = item.__dict__[b"lapses"] item.acq_reps_since_lapse = \ item.__dict__[b"acq_reps_since_lapse"] item.ret_reps_since_lapse = \ item.__dict__[b"ret_reps_since_lapse"] del item.__dict__[b"id"] del item.__dict__[b"cat"] del item.__dict__[b"q"] del item.__dict__[b"a"] del item.__dict__[b"unseen"] del item.__dict__[b"grade"] del item.__dict__[b"next_rep"] del item.__dict__[b"last_rep"] del item.__dict__[b"easiness"] del item.__dict__[b"acq_reps"] del item.__dict__[b"ret_reps"] del item.__dict__[b"lapses"] del item.__dict__[b"acq_reps_since_lapse"] del item.__dict__[b"ret_reps_since_lapse"] except (FileNotFoundError, PermissionError) as e: self.main_widget().show_error(_("Unable to open file.")) raise MnemosyneError except Exception as e: import traceback self.main_widget().show_error(traceback.format_exc()) raise MnemosyneError 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()
class Mnemosyne1XML(FileFormat, Mnemosyne1): description = _("Mnemosyne 1.x *.XML files") extension = ".xml" filename_filter = _("Mnemosyne 1.x XML files") + " (*.xml)" import_possible = True export_possible = False def __init__(self, component_manager): FileFormat.__init__(self, component_manager) Mnemosyne1.__init__(self, component_manager) self.anon_to_id = {} def do_import(self, filename, extra_tag_names=None): FileFormat.do_import(self, filename, extra_tag_names) w = self.main_widget() # The import process generates card log entries which have new 2.0 # ids as opposed to their old 1.x ids, so we need to delete them # later. db = self.database() log_index = db.current_log_index() try: w.set_progress_text(_("Importing cards...")) self.read_items_from_mnemosyne1_xml(filename) self.create_cards_from_mnemosyne1(extra_tag_names) except MnemosyneError: w.close_progress() return db.remove_card_log_entries_since(log_index) # We now generate 'added card' events with the proper ids. timestamp = int(time.time()) for item in self.items: db.log_added_card(timestamp, item.id) self.database().link_inverse_cards() w.close_progress() self.warned_about_missing_media = False def read_items_from_mnemosyne1_xml(self, filename): # Reset anonymiser when importing a new file, otherwise information # from the previous file still lingers and we get erroneously think # we've imported this before. self.anon_to_id = {} w = self.main_widget() try: tree = cElementTree.parse(filename) except cElementTree.ParseError as e: w.show_error(_("Unable to parse file:") + str(e)) raise MnemosyneError except: w.show_error(_("Unable to open file.")) raise MnemosyneError if tree.getroot().tag != "mnemosyne" or \ tree.getroot().get("core_version") != "1": w.show_error( _("XML file does not seem to be a Mnemosyne 1.x XML file.")) raise MnemosyneError self.starttime = 0 if tree.getroot().get("time_of_start"): self.starttime = int(tree.getroot().get("time_of_start")) category_with_name = {} self.categories = [] for element in tree.findall("category"): category = Mnemosyne1.MnemosyneCore.Category() category.name = element.find("name").text category.active = bool(element.get("active")) self.categories.append(category) category_with_name[category.name] = category self.items = [] warned_about_import = False for element in tree.findall("item"): item = Mnemosyne1.MnemosyneCore.Item() item.id = element.get("id") if not item.id: item.id = rand_uuid() if item.id.startswith('_'): item.id = self.unanonymise_id(item.id) item.q = element.find("Q").text item.a = element.find("A").text if item.a is None: item.a = "" item.cat = category_with_name[element.find("cat").text] if element.get("gr"): if not warned_about_import: result = w.show_question( _("This XML file contains learning data. It's best to import this from a mem file, in order to preserve historical statistics. Continue?" ), _("Yes"), _("No"), "") warned_about_import = True if result == 1: # No return item.grade = int(element.get("gr")) else: item.grade = 0 if element.get("e"): item.easiness = float(element.get("e")) else: item.easiness = 2.5 if element.get("ac_rp"): item.acq_reps = int(element.get("ac_rp")) else: item.acq_reps = 0 if element.get("rt_rp"): item.ret_reps = int(element.get("rt_rp")) else: item.ret_reps = 0 if element.get("lps"): item.lapses = int(element.get("lps")) else: item.lapses = 0 if element.get("ac_rp_l"): item.acq_reps_since_lapse = int(element.get("ac_rp_l")) else: item.acq_reps_since_lapse = 0 if element.get("rt_rp_l"): item.ret_reps_since_lapse = int(element.get("rt_rp_l")) else: item.ret_reps_since_lapse = 0 if element.get("l_rp"): item.last_rep = int(float(element.get("l_rp"))) else: item.last_rep = 0 if element.get("n_rp"): item.next_rep = int(float(element.get("n_rp"))) else: item.next_rep = 0 if element.get("u"): item.unseen = bool(element.get("u")) else: if item.acq_reps <= 1 and item.ret_reps == 0 \ and item.grade == 0: item.unseen = True else: item.unseen = False self.items.append(item) def unanonymise_id(self, item_id): if "." in item_id: old_id, suffix = item_id.split(".", 1) suffix = "." + suffix else: old_id, suffix = item_id, "" if old_id in self.anon_to_id: item_id = self.anon_to_id[old_id] else: item_id = rand_uuid() self.anon_to_id[old_id] = item_id return item_id + suffix
def show_error(self, text): QtWidgets.QMessageBox.critical(self.top_window(), _("Mnemosyne"), text)
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.find("cards").findall("card"): category = element.attrib["category"] commit = not (element.attrib["commit"] == "0") for field in element.find("card_fields").findall("card_field"): if field.attrib["idx"] == "1": question = field.text else: answer = field.text card_other = element.find("card_other") if card_other is None: difficulty = 40 difficulty_prev = 40 else: difficulty = int(card_other.attrib["difficulty"]) difficulty_prev = int(card_other.attrib["difficulty_prev"]) # Grades are 0-5. In SM for Palm there are commited and uncommited # cards. Uncommited cards go to grade -1. # Otherwise try to extrapolate something from difficulty in SM # I have implemented guess_grade such, that the distribution of # grades looks reasonable for my test database of 4000 entries. # By "reasonable" I mean than most of the entries should be # at grade 4. I've been learning that database for 4 years, so the # cards should have converged by now. if commit == False: grade = -1 # Very easy items are scarce in SM and must be easiest grade. elif difficulty < 10: grade = 5 # Assign passing grades, based upon whether the difficulty has # changed. elif difficulty > difficulty_prev: grade = 2 elif difficulty == difficulty_prev: grade = 3 elif difficulty < difficulty_prev: grade = 4 # If the interval becomes shorter, it must have been a failure. if card_other is None: interval = 0 interval_prev = 0 else: interval = int(card_other.attrib["interval"]) * DAY interval_prev = int(card_other.attrib["interval_prev"]) * DAY if interval < interval_prev: grade = 0 # Construct card. fact_data = {"f": question, "b": answer} self.preprocess_media(fact_data, tag_names) card = self.controller().create_new_cards( fact_data, card_type, grade=grade, tag_names=tag_names + [category], check_for_duplicates=False, save=False)[0] if _("MISSING_MEDIA") in tag_names: tag_names.remove(_("MISSING_MEDIA")) if card_other is not None: card.creation_time = int(time.mktime(time.strptime(\ card_other.attrib["datecreate"], "%Y-%m-%d"))) card.modification_time = int(time.mktime(time.strptime(\ card_other.attrib["datecommit"], "%Y-%m-%d"))) card.next_rep = self.scheduler().midnight_UTC(int(time.mktime(\ time.strptime(card_other.attrib["datenexttest"], "%Y-%m-%d")))) card.last_rep = card.next_rep - interval card.lapses = int(card_other.attrib["lapses"]) # Try to fill acquisiton reps and retention reps. # Since SM statistics are only available for commited # cards, I take acq_reps = 0 and ret_reps = lapses + recalls. card.ret_reps = card.lapses + int(card_other.attrib["recalls"]) # Try to derive an easines factor EF from [1.3 .. 3.2] from # difficulty d from [1% .. 100%]. # The math below is set to translate # difficulty=100% --> easiness = 1.3 # difficulty=40% --> easiness = 2.5 # difficulty=1% --> easiness = 3.2 dp = difficulty * 0.01 # Small values should be easy, large ones hard. if dp > 0.4: card.easiness = 1.28 - 1.32 * math.log(dp) else: card.easiness = 4.2 - 1.139 * math.exp(dp) self.database().update_card(card) self.warned_about_missing_media = False
def retranslateUi(self, RemoveTagsDlg): _translate = QtCore.QCoreApplication.translate RemoveTagsDlg.setWindowTitle(_('Remove tags')) self.label.setText(_('Tags to remove:')) self.ok_button.setText(_('&OK')) self.cancel_button.setText(_('&Cancel'))
def display(self, criterion=None): # Create criterion if needed. if criterion is None: criterion = DefaultCriterion(self.component_manager) # Determine number of cards at each level of the tree. root_count = 0 count_for_card_type = {} count_for_fact_view = {} for card_type in self.card_types(): card_type_count = 0 for fact_view in card_type.fact_views: count = self.database().card_count_for_fact_view \ (fact_view, active_only=False) card_type_count += count count_for_fact_view[fact_view] = count count_for_card_type[card_type] = card_type_count root_count += card_type_count # Fill widget. self.nodes_which_can_be_deleted = [] self.nodes_which_can_be_renamed = [] self.tree_wdgt.clear() self.node_items = [] self.card_type_fact_view_ids_for_node_item = [] root_item = QtWidgets.QTreeWidgetItem( self.tree_wdgt, [_("All card types (%d)") % root_count], 0) root_item.setFlags(root_item.flags() | \ QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsTristate) root_item.setCheckState(0, QtCore.Qt.Checked) for card_type in self.database().sorted_card_types(): if card_type.hidden_from_UI: continue card_type_item = QtWidgets.QTreeWidgetItem(root_item, ["%s (%d)" % \ (_(card_type.name), count_for_card_type[card_type])], 0) card_type_item.setFlags(card_type_item.flags() | \ QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsTristate) card_type_item.setCheckState(0, QtCore.Qt.Checked) card_type_item.setData(NODE, QtCore.Qt.DisplayRole, QtCore.QVariant(card_type.id)) if count_for_card_type[card_type] == 0 and \ self.database().is_user_card_type(card_type): self.nodes_which_can_be_deleted.append(card_type.id) if self.database().is_user_card_type(card_type): self.nodes_which_can_be_renamed.append(card_type.id) card_type_item.setFlags(card_type_item.flags() | \ QtCore.Qt.ItemIsEditable) for fact_view in card_type.fact_views: fact_view_item = QtWidgets.QTreeWidgetItem( card_type_item, [ "%s (%d)" % (_(fact_view.name), count_for_fact_view[fact_view]) ], 0) fact_view_item.setFlags(fact_view_item.flags() | \ QtCore.Qt.ItemIsUserCheckable) if (card_type.id, fact_view.id) in \ criterion.deactivated_card_type_fact_view_ids: check_state = QtCore.Qt.Unchecked else: check_state = QtCore.Qt.Checked fact_view_item.setCheckState(0, check_state) # Since fact_view_item seems mutable, we cannot use a dict. self.node_items.append(fact_view_item) self.card_type_fact_view_ids_for_node_item.append(\ (card_type.id, fact_view.id)) self.tree_wdgt.expandAll()
class Tsv(FileFormat, MediaPreprocessor): """Question and answers on a single line, separated by tabs. Or, for three-sided cards: foreign word, pronunciation, meaning, separated by tabs. """ description = _("Tab-separated text files") extension = ".txt" filename_filter = _("Tab-separated text files (*.txt *.tsv *.tab)") import_possible = True export_possible = True def __init__(self, component_manager): FileFormat.__init__(self, component_manager) MediaPreprocessor.__init__(self, component_manager) def do_import(self, filename, extra_tag_names=None): FileFormat.do_import(self, filename, extra_tag_names) try: f = open(filename, encoding="utf-8") except: self.main_widget().show_error(_("Could not load file.")) return facts_data = [] line_number = 0 for line in f: line_number += 1 line = line.rstrip() # Parse html style escaped unicode (e.g. 至). for match in re0.finditer(line): # Integer part. u = chr(int(match.group(1))) # Integer part with &# and ;. line = line.replace(match.group(), u) if len(line) == 0: continue if line[0] == "\\ufeff": # Remove byte-order mark. line = line[1:] fields = line.split("\t") if len(fields) >= 3: # Vocabulary card. if len(fields) >= 4: facts_data.append({ "f": fields[0], "p_1": fields[1], "m_1": fields[2], "n": fields[3] }) else: facts_data.append({ "f": fields[0], "p_1": fields[1], "m_1": fields[2] }) elif len(fields) == 2: # Front-to-back only. facts_data.append({"f": fields[0], "b": fields[1]}) else: # Malformed line. self.main_widget().show_error(_("Badly formed input on line") \ + " " + str(line_number) + ":\n" + line) return # Now that we know all the data is well-formed, create the cards. tag_names = [] if extra_tag_names: tag_names += [tag_name.strip() for tag_name \ in extra_tag_names.split(",")] for fact_data in facts_data: if len(list(fact_data.keys())) == 2: card_type = self.card_type_with_id("1") else: card_type = self.card_type_with_id("3") self.preprocess_media(fact_data, tag_names) self.controller().create_new_cards(fact_data, card_type, grade=-1, tag_names=tag_names, check_for_duplicates=False, save=False) if _("MISSING_MEDIA") in tag_names: tag_names.remove(_("MISSING_MEDIA")) self.warned_about_missing_media = False def process_string_for_text_export(self, text): text = text.replace("\n", "<br>").replace("\t", " ") if text == "": text = "<br>" return text def do_export(self, filename): if not os.path.isabs(filename): filename = os.path.join(self.config()["export_dir"], filename) db = self.database() w = self.main_widget() w.set_progress_text(_("Exporting cards...")) number_of_cards = db.active_count() w.set_progress_range(number_of_cards) w.set_progress_update_interval(number_of_cards / 50) outfile = open(filename, "w", encoding="utf-8") for _card_id, _fact_id in db.active_cards(): card = db.card(_card_id, is_id_internal=True) q = self.process_string_for_text_export(\ card.question(render_chain="plain_text")) a = self.process_string_for_text_export(\ card.answer(render_chain="plain_text")) outfile.write("%s\t%s\n" % (q, a)) w.increase_progress(1) w.close_progress()
def deactivate(self): if self.upload_thread: from mnemosyne.libmnemosyne.translator import _ print((_("Waiting for uploader thread to stop...").encode("utf-8"))) self.upload_thread.join() print((_("Done!").encode("utf-8")))