Пример #1
0
 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)
Пример #2
0
 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)
Пример #3
0
 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()
Пример #4
0
 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            
Пример #5
0
 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)
Пример #6
0
 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()
Пример #7
0
 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."))            
Пример #8
0
 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))
Пример #9
0
 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)
Пример #10
0
 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
Пример #11
0
    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)
Пример #12
0
    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)
Пример #14
0
    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)."))
Пример #15
0
 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."))
Пример #16
0
 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.")
Пример #17
0
 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))
Пример #18
0
 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()
Пример #19
0
 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)
Пример #20
0
    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!")
Пример #21
0
 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()
Пример #22
0
    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)
Пример #23
0
    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
Пример #24
0
 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()
Пример #25
0
    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))
Пример #26
0
    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
Пример #27
0
 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()
Пример #28
0
    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()
Пример #29
0
    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()
Пример #30
0
 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
Пример #31
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)
Пример #34
0
 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)
Пример #35
0
 def retranslateUi(self, StatisticsDlg):
     _translate = QtCore.QCoreApplication.translate
     StatisticsDlg.setWindowTitle(_('Statistics'))
     self.ok_button.setText(_('&OK'))
Пример #36
0
    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()
Пример #37
0
 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)
Пример #38
0
 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'))
Пример #39
0
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)
Пример #40
0
 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))
Пример #41
0
 def format(self):
     for _format in self.component_manager.all("file_format"):
         if _(_format.description) == self.file_formats.currentText():
             return _format
Пример #42
0
 def work_ended(self):
     self.main_widget().close_progress()
     self.main_widget().show_information(_("Done!"))
     QtWidgets.QDialog.accept(self)
Пример #43
0
 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. &#33267;).
         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
Пример #44
0
 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)
Пример #45
0
 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)
Пример #46
0
 def show_information(self, text):
     QtWidgets.QMessageBox.information(self.top_window(), _("Mnemosyne"),
                                       text)
Пример #47
0
    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
Пример #48
0
 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)
Пример #49
0
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()
Пример #50
0
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
Пример #51
0
 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
Пример #52
0
 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'))
Пример #53
0
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()
Пример #54
0
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
Пример #55
0
 def show_error(self, text):
     QtWidgets.QMessageBox.critical(self.top_window(), _("Mnemosyne"), text)
Пример #56
0
 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
Пример #57
0
 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'))
Пример #58
0
 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()
Пример #59
0
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. &#33267;).
            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()
Пример #60
0
 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")))