def __init__(self, **kwds):
     super().__init__(**kwds)
     self.parent = kwds["parent"]
     component_manager = kwds["component_manager"]
     self.setupUi(self)
     self.card_type_tree_wdgt = CardTypesTreeWdgt(\
         component_manager=component_manager, parent=self)
     # Bug in Qt: need to explicitly reset the text of this label.
     self.label.setText(_("Activate cards from these card types:"))
     self.gridLayout.addWidget(self.card_type_tree_wdgt, 1, 0)
     self.tag_tree_wdgt = TagsTreeWdgt(\
         component_manager=component_manager, parent=self)
     self.gridLayout.addWidget(self.tag_tree_wdgt, 1, 1)
     
     criterion = DefaultCriterion(component_manager=self.component_manager)
     for tag in self.database().tags():
         criterion._tag_ids_active.add(tag._id)
     self.display_criterion(criterion)
     self.card_type_tree_wdgt.tree_wdgt.\
         itemChanged.connect(self.criterion_changed)
     self.tag_tree_wdgt.tree_wdgt.\
         itemChanged.connect(self.criterion_changed)
     self.card_type_tree_wdgt.tree_wdgt.\
         itemClicked.connect(self.criterion_clicked)
     self.tag_tree_wdgt.tree_wdgt.\
         itemClicked.connect(self.criterion_clicked)
 def __init__(self, component_manager, parent):
     CriterionWidget.__init__(self, component_manager)
     QtGui.QWidget.__init__(self, parent)
     self.parent = parent
     self.setupUi(self)
     self.card_type_tree_wdgt = CardTypesTreeWdgt(component_manager, self)
     # Bug in Qt: need to explicitly reset the text of this label.
     self.label.setText(_("Activate cards from these card types:"))
     self.gridLayout.addWidget(self.card_type_tree_wdgt, 1, 0)
     self.tag_tree_wdgt = TagsTreeWdgt(component_manager, self)
     self.gridLayout.addWidget(self.tag_tree_wdgt, 1, 1)
     
     criterion = DefaultCriterion(self.component_manager)
     for tag in self.database().tags():
         criterion._tag_ids_active.add(tag._id)
     self.display_criterion(criterion)
     self.card_type_tree_wdgt.tree_wdgt.\
         itemChanged.connect(self.criterion_changed)
     self.tag_tree_wdgt.tree_wdgt.\
         itemChanged.connect(self.criterion_changed)
     self.card_type_tree_wdgt.tree_wdgt.\
         itemClicked.connect(self.criterion_clicked)
     self.tag_tree_wdgt.tree_wdgt.\
         itemClicked.connect(self.criterion_clicked)
 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.label_2 = QtWidgets.QLabel(_("having any of these tags:"),
                                     self.container_2)
     self.layout_2.addWidget(self.label_2)
     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)
     # 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)
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.label_2 = QtWidgets.QLabel(_("having any of these tags:"),
                                        self.container_2)
        self.layout_2.addWidget(self.label_2)
        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)
        # 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):
        # 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.
            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] + ")"
            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)
Beispiel #5
0
 def __init__(self, component_manager):
     BrowseCardsDialog.__init__(self, component_manager)
     self.show_tip_after_starting_n_times()
     QtGui.QDialog.__init__(self, self.main_widget())
     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 = QtGui.QWidget(self.splitter_1)
     self.layout_1 = QtGui.QVBoxLayout(self.container_1)
     self.label_1 = QtGui.QLabel(_("Show cards from these card types:"),
         self.container_1)
     self.layout_1.addWidget(self.label_1)
     self.card_type_tree_wdgt = \
         CardTypesTreeWdgt(component_manager, self.container_1, 
         self.unload_qt_database)
     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 = QtGui.QWidget(self.splitter_1)
     self.layout_2 = QtGui.QVBoxLayout(self.container_2)
     self.label_2 = QtGui.QLabel(_("having any of these tags:"),
         self.container_2)
     self.layout_2.addWidget(self.label_2)
     self.tag_tree_wdgt = \
         TagsTreeWdgt(component_manager, self.container_2,
         self.unload_qt_database)
     self.tag_tree_wdgt.tags_changed_signal.\
         connect(self.reload_database_and_redraw) 
     self.layout_2.addWidget(self.tag_tree_wdgt)
     self.label_3 = QtGui.QLabel(_("containing this text in the cards:"),
         self.container_2)
     self.layout_2.addWidget(self.label_3)
     self.search_box = QtGui.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)        
     # 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)
Beispiel #6
0
class BrowseCardsDlg(QtGui.QDialog, Ui_BrowseCardsDlg, BrowseCardsDialog,
                     TipAfterStartingNTimes):

    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, component_manager):
        BrowseCardsDialog.__init__(self, component_manager)
        self.show_tip_after_starting_n_times()
        QtGui.QDialog.__init__(self, self.main_widget())
        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 = QtGui.QWidget(self.splitter_1)
        self.layout_1 = QtGui.QVBoxLayout(self.container_1)
        self.label_1 = QtGui.QLabel(_("Show cards from these card types:"),
            self.container_1)
        self.layout_1.addWidget(self.label_1)
        self.card_type_tree_wdgt = \
            CardTypesTreeWdgt(component_manager, self.container_1, 
            self.unload_qt_database)
        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 = QtGui.QWidget(self.splitter_1)
        self.layout_2 = QtGui.QVBoxLayout(self.container_2)
        self.label_2 = QtGui.QLabel(_("having any of these tags:"),
            self.container_2)
        self.layout_2.addWidget(self.label_2)
        self.tag_tree_wdgt = \
            TagsTreeWdgt(component_manager, self.container_2,
            self.unload_qt_database)
        self.tag_tree_wdgt.tags_changed_signal.\
            connect(self.reload_database_and_redraw) 
        self.layout_2.addWidget(self.tag_tree_wdgt)
        self.label_3 = QtGui.QLabel(_("containing this text in the cards:"),
            self.container_2)
        self.layout_2.addWidget(self.label_3)
        self.search_box = QtGui.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)        
        # 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 = QtGui.QMenu(self)
        edit_action = QtGui.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 = QtGui.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 = QtGui.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 = QtGui.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 = QtGui.QAction(_("&Add tags"), menu)
        add_tags_action.triggered.connect(self.menu_add_tags)
        menu.addAction(add_tags_action)
        remove_tags_action = QtGui.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:
            QtGui.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:
            QtGui.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).toInt()[0]
        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).toInt()[0]
            _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).toInt()[0]
            _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, self.component_manager, started_from_card_browser=True,
            parent=self)
        # 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_() == QtGui.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
        print shift, current_row + shift
        self.table.selectRow(current_row + shift)
        _card_ids = self._card_ids_from_selection()
        card = self.database().card(_card_ids.pop(), is_id_internal=True)
        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()
        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(self.component_manager, cards, tag_text, 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).toInt()[0]
            _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 = \
                unicode(index.model().data(card_type_id_index).toString())
            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(self.component_manager,
            current_card_type, return_values, parent=self)
        if dlg.exec_() != QtGui.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_() != QtGui.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(self.component_manager, return_values, parent=self)
        if dlg.exec_() != QtGui.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(self, tags, return_values)
        if dlg.exec_() != QtGui.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():
            QtGui.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(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.iteritems():
              self.card_model.setHeaderData(key, QtCore.Qt.Horizontal,
                  QtCore.QVariant(value))
        self.table.setModel(self.card_model)
        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().setMovable(True)
        self.table.setItemDelegateForColumn(\
            QUESTION, QA_Delegate(self.component_manager, QUESTION, self))
        self.table.setItemDelegateForColumn(\
            ANSWER, QA_Delegate(self.component_manager, ANSWER, self))
        self.table.setEditTriggers(QtGui.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).toInt()[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,
                QtGui.QAbstractItemView.PositionAtTop)
            # Restore selection.
            old_selection_mode = self.table.selectionMode()
            self.table.setSelectionMode(QtGui.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):
        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):  
        # 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).toInt()[0]))
            # Determine _card_ids of card with an active tag.
            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] + ")"
            query = QtSql.QSqlQuery(query)
            active__card_ids = set()
            while query.next():
                active__card_ids.add(str(query.value(0).toInt()[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 = unicode(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).toInt()[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).toInt()[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):
        # Generated when pressing escape.
        self.unload_qt_database()
        return QtGui.QDialog.reject(self)

    def accept(self):
        # 'accept' does not generate a close event.
        self._store_state()
        self.unload_qt_database()
        return QtGui.QDialog.accept(self)
class DefaultCriterionWdgt(QtWidgets.QWidget, CriterionWidget,
                           Ui_DefaultCriterionWdgt):

    """Note that this dialog can support active tags and forbidden tags,
    but not at the same time, in order to keep the interface compact.

    """

    used_for = DefaultCriterion

    def __init__(self, **kwds):
        super().__init__(**kwds)
        self.parent = kwds["parent"]
        component_manager = kwds["component_manager"]
        self.setupUi(self)
        self.card_type_tree_wdgt = CardTypesTreeWdgt(\
            component_manager=component_manager, parent=self)
        # Bug in Qt: need to explicitly reset the text of this label.
        self.label.setText(_("Activate cards from these card types:"))
        self.gridLayout.addWidget(self.card_type_tree_wdgt, 1, 0)
        self.tag_tree_wdgt = TagsTreeWdgt(\
            component_manager=component_manager, parent=self)
        self.gridLayout.addWidget(self.tag_tree_wdgt, 1, 1)
        
        criterion = DefaultCriterion(component_manager=self.component_manager)
        for tag in self.database().tags():
            criterion._tag_ids_active.add(tag._id)
        self.display_criterion(criterion)
        self.card_type_tree_wdgt.tree_wdgt.\
            itemChanged.connect(self.criterion_changed)
        self.tag_tree_wdgt.tree_wdgt.\
            itemChanged.connect(self.criterion_changed)
        self.card_type_tree_wdgt.tree_wdgt.\
            itemClicked.connect(self.criterion_clicked)
        self.tag_tree_wdgt.tree_wdgt.\
            itemClicked.connect(self.criterion_clicked)

    def display_criterion(self, criterion):
        self.card_type_tree_wdgt.display(criterion)
        self.tag_tree_wdgt.display(criterion)
        if len(criterion._tag_ids_forbidden):
            self.active_or_forbidden.setCurrentIndex(1)
        else:
            self.active_or_forbidden.setCurrentIndex(0)

    def criterion(self):

        """Build the criterion from the information the user entered in the
        widget.

        """

        criterion = DefaultCriterion(component_manager=self.component_manager)
        criterion = self.card_type_tree_wdgt.checked_to_criterion(criterion)
        # Tag tree contains active tags.
        if self.active_or_forbidden.currentIndex() == 0:
            self.tag_tree_wdgt.checked_to_active_tags_in_criterion(criterion)
        # Tag tree contains forbidden tags.
        else:
            self.tag_tree_wdgt.\
                checked_to_forbidden_tags_in_criterion(criterion)
            for tag in self.database().tags():
                criterion._tag_ids_active.add(tag._id)
        return criterion
    
    def criterion_clicked(self):
        if self.parent.was_showing_a_saved_set and not self.parent.is_shutting_down:
            self.main_widget().show_information(\
_("Cards you (de)activate now will not be stored in the previously selected set unless you click 'Save this set for later use' again. This allows you to make some quick-and-dirty modifications."))
            self.parent.was_showing_a_saved_set = False

    def criterion_changed(self):
        self.parent.saved_sets.clearSelection()       

    def closeEvent(self, event):
        # This allows the state of the tag tree to be saved.
        self.tag_tree_wdgt.close()
class DefaultCriterionWdgt(QtGui.QWidget, Ui_DefaultCriterionWdgt,
                           CriterionWidget):

    """Note that this dialog can support active tags and forbidden tags,
    but not at the same time, in order to keep the interface compact.

    """

    used_for = DefaultCriterion

    def __init__(self, component_manager, parent):
        CriterionWidget.__init__(self, component_manager)
        QtGui.QWidget.__init__(self, parent)
        self.parent = parent
        self.setupUi(self)
        self.card_type_tree_wdgt = CardTypesTreeWdgt(component_manager, self)
        # Bug in Qt: need to explicitly reset the text of this label.
        self.label.setText(_("Activate cards from these card types:"))
        self.gridLayout.addWidget(self.card_type_tree_wdgt, 1, 0)
        self.tag_tree_wdgt = TagsTreeWdgt(component_manager, self)
        self.gridLayout.addWidget(self.tag_tree_wdgt, 1, 1)
        
        criterion = DefaultCriterion(self.component_manager)
        for tag in self.database().tags():
            criterion._tag_ids_active.add(tag._id)
        self.display_criterion(criterion)
        self.card_type_tree_wdgt.tree_wdgt.\
            itemChanged.connect(self.criterion_changed)
        self.tag_tree_wdgt.tree_wdgt.\
            itemChanged.connect(self.criterion_changed)
        self.card_type_tree_wdgt.tree_wdgt.\
            itemClicked.connect(self.criterion_clicked)
        self.tag_tree_wdgt.tree_wdgt.\
            itemClicked.connect(self.criterion_clicked)

    def display_criterion(self, criterion):
        self.card_type_tree_wdgt.display(criterion)
        self.tag_tree_wdgt.display(criterion)
        if len(criterion._tag_ids_forbidden):
            self.active_or_forbidden.setCurrentIndex(1)
        else:
            self.active_or_forbidden.setCurrentIndex(0)

    def criterion(self):

        """Build the criterion from the information the user entered in the
        widget.

        """

        criterion = DefaultCriterion(self.component_manager)
        criterion = self.card_type_tree_wdgt.checked_to_criterion(criterion)
        # Tag tree contains active tags.
        if self.active_or_forbidden.currentIndex() == 0:
            self.tag_tree_wdgt.checked_to_active_tags_in_criterion(criterion)
        # Tag tree contains forbidden tags.
        else:
            self.tag_tree_wdgt.\
                checked_to_forbidden_tags_in_criterion(criterion)
            for tag in self.database().tags():
                criterion._tag_ids_active.add(tag._id)
        return criterion
    
    def criterion_clicked(self):
        if self.parent.was_showing_a_saved_set and not self.parent.is_shutting_down:
            self.main_widget().show_information(\
                _("Cards you (de)activate now will not be stored in the previously selected set unless you click 'Save this set for later use' again. This allows you to make some quick-and-dirty modifications."))
            self.parent.was_showing_a_saved_set = False

    def criterion_changed(self):
        self.parent.saved_sets.clearSelection()       

    def closeEvent(self, event):
        # This allows the state of the tag tree to be saved.
        self.tag_tree_wdgt.close()