track changes in fields dialog as well

And avoid bumping schema until user actually saves, but warn at
start.
This commit is contained in:
Damien Elmes 2020-05-15 13:59:44 +10:00
parent a2b7a30841
commit 46c363d4aa
5 changed files with 166 additions and 77 deletions

View File

@ -270,7 +270,6 @@ class ModelManager:
self.remove(m["id"])
def remove_all_notetypes(self):
self.col.modSchema(check=True)
for nt in self.all_names_and_ids():
self._remove_from_cache(nt.id)
self.col.backend.remove_notetype(nt.id)
@ -345,42 +344,25 @@ class ModelManager:
def sortIdx(self, m: NoteType) -> Any:
return m["sortf"]
def setSortIdx(self, m: NoteType, idx: int) -> None:
assert 0 <= idx < len(m["flds"])
self.col.modSchema(check=True)
m["sortf"] = idx
self.save(m)
# Adding & changing fields
##################################################
def newField(self, name: str) -> Field:
def new_field(self, name: str) -> Field:
assert isinstance(name, str)
f = defaultField.copy()
f["name"] = name
return f
def addField(self, m: NoteType, field: Field, save=True) -> None:
if m["id"]:
self.col.modSchema(check=True)
def add_field(self, m: NoteType, field: Field) -> None:
"Modifies schema."
m["flds"].append(field)
if m["id"] and save:
self.save(m)
def remField(self, m: NoteType, field: Field, save=True) -> None:
self.col.modSchema(check=True)
def remove_field(self, m: NoteType, field: Field) -> None:
"Modifies schema."
m["flds"].remove(field)
if save:
self.save(m)
def moveField(self, m: NoteType, field: Field, idx: int, save=True) -> None:
self.col.modSchema(check=True)
def reposition_field(self, m: NoteType, field: Field, idx: int) -> None:
"Modifies schema."
oldidx = m["flds"].index(field)
if oldidx == idx:
return
@ -388,48 +370,55 @@ class ModelManager:
m["flds"].remove(field)
m["flds"].insert(idx, field)
if save:
self.save(m)
def renameField(self, m: NoteType, field: Field, newName: str, save=True) -> None:
def rename_field(self, m: NoteType, field: Field, new_name: str) -> None:
assert field in m["flds"]
field["name"] = new_name
field["name"] = newName
def set_sort_index(self, nt: NoteType, idx: int) -> None:
"Modifies schema."
assert 0 <= idx < len(nt["flds"])
nt["sortf"] = idx
if save:
# legacy
newField = new_field
def addField(self, m: NoteType, field: Field) -> None:
self.add_field(m, field)
if m["id"]:
self.save(m)
def remField(self, m: NoteType, field: Field) -> None:
self.remove_field(m, field)
self.save(m)
def moveField(self, m: NoteType, field: Field, idx: int) -> None:
self.reposition_field(m, field, idx)
self.save(m)
def renameField(self, m: NoteType, field: Field, newName: str) -> None:
self.rename_field(m, field, newName)
self.save(m)
# Adding & changing templates
##################################################
def newTemplate(self, name: str) -> Template:
def new_template(self, name: str) -> Template:
t = defaultTemplate.copy()
t["name"] = name
return t
def addTemplate(self, m: NoteType, template: Template, save=True) -> None:
if m["id"]:
self.col.modSchema(check=True)
def add_template(self, m: NoteType, template: Template) -> None:
"Modifies schema."
m["tmpls"].append(template)
if save and m["id"]:
self.save(m)
def remTemplate(self, m: NoteType, template: Template, save=True) -> None:
def remove_template(self, m: NoteType, template: Template) -> None:
"Modifies schema."
assert len(m["tmpls"]) > 1
self.col.modSchema(check=True)
m["tmpls"].remove(template)
if save:
self.save(m)
def moveTemplate(
self, m: NoteType, template: Template, idx: int, save=True
) -> None:
self.col.modSchema(check=True)
def reposition_template(self, m: NoteType, template: Template, idx: int) -> None:
"Modifies schema."
oldidx = m["tmpls"].index(template)
if oldidx == idx:
return
@ -437,9 +426,23 @@ class ModelManager:
m["tmpls"].remove(template)
m["tmpls"].insert(idx, template)
if save:
# legacy
newTemplate = new_template
def addTemplate(self, m: NoteType, template: Template) -> None:
self.add_template(m, template)
if m["id"]:
self.save(m)
def remTemplate(self, m: NoteType, template: Template) -> None:
self.remove_template(m, template)
self.save(m)
def moveTemplate(self, m: NoteType, template: Template, idx: int) -> None:
self.reposition_template(m, template, idx)
self.save(m)
# Model changing
##########################################################################
# - maps are ord->ord, and there should not be duplicate targets

View File

@ -16,6 +16,7 @@ from anki.rsbackend import TemplateError
from anki.template import TemplateRenderContext
from aqt import AnkiQt, gui_hooks
from aqt.qt import *
from aqt.schema_change_tracker import ChangeTracker
from aqt.sound import av_player, play_clicked_audio
from aqt.theme import theme_manager
from aqt.utils import (
@ -36,7 +37,6 @@ from aqt.webview import AnkiWebView
# fixme: deck name on new cards
# fixme: card count when removing
# fixme: change tracking and tooltip in fields
# fixme: replay suppression
@ -61,7 +61,7 @@ class CardLayout(QDialog):
self._want_fill_empty_on = fill_empty
self.mm._remove_from_cache(self.model["id"])
self.mw.checkpoint(_("Card Types"))
self.changed = False
self.change_tracker = ChangeTracker(self.mw)
self.setupTopArea()
self.setupMainArea()
self.setupButtons()
@ -389,7 +389,7 @@ class CardLayout(QDialog):
if self.ignore_change_signals:
return
self.changed = True
self.change_tracker.mark_basic()
text = self.tform.edit_area.toPlainText()
@ -497,8 +497,9 @@ class CardLayout(QDialog):
if not askUser(msg):
return
self.changed = True
self.mm.remTemplate(self.model, template, save=False)
if not self.change_tracker.mark_schema():
return
self.mm.remove_template(self.model, template)
# ensure current ordinal is within bounds
idx = self.ord
@ -513,7 +514,8 @@ class CardLayout(QDialog):
if not name.strip():
return
self.changed = True
if not self.change_tracker.mark_schema():
return
template["name"] = name
self.redraw_everything()
@ -535,8 +537,9 @@ class CardLayout(QDialog):
if pos == current_pos:
return
new_idx = pos - 1
self.changed = True
self.mm.moveTemplate(self.model, template, new_idx, save=False)
if not self.change_tracker.mark_schema():
return
self.mm.reposition_template(self.model, template, new_idx)
self.ord = new_idx
self.redraw_everything()
@ -561,13 +564,14 @@ class CardLayout(QDialog):
)
if not askUser(txt):
return
self.changed = True
if not self.change_tracker.mark_schema():
return
name = self._newCardName()
t = self.mm.newTemplate(name)
old = self.current_template()
t["qfmt"] = old["qfmt"]
t["afmt"] = old["afmt"]
self.mm.addTemplate(self.model, t, save=False)
self.mm.add_template(self.model, t)
self.ord = len(self.templates) - 1
self.redraw_everything()
@ -587,7 +591,7 @@ adjust the template manually to switch the question and answer."""
)
)
return
self.changed = True
self.change_tracker.mark_basic()
dst["afmt"] = "{{FrontSide}}\n\n<hr id=answer>\n\n%s" % src["qfmt"]
dst["qfmt"] = m.group(2).strip()
return True
@ -639,7 +643,7 @@ adjust the template manually to switch the question and answer."""
def onBrowserDisplayOk(self, f):
t = self.current_template()
self.changed = True
self.change_tracker.mark_basic()
t["bqfmt"] = f.qfmt.text().strip()
t["bafmt"] = f.afmt.text().strip()
if f.overrideFont.isChecked():
@ -678,7 +682,7 @@ Enter deck to place new %s cards in, or leave blank:"""
l.addWidget(bb)
d.setLayout(l)
d.exec_()
self.changed = True
self.change_tracker.mark_basic()
if not te.text().strip():
t["did"] = None
else:
@ -709,7 +713,7 @@ Enter deck to place new %s cards in, or leave blank:"""
field,
)
self.tform.edit_area.setPlainText(text)
self.changed = True
self.change_tracker.mark_basic()
self.write_edits_to_template_and_redraw()
# Closing & Help
@ -733,7 +737,7 @@ Enter deck to place new %s cards in, or leave blank:"""
self.mw.taskman.with_progress(save, on_done)
def reject(self) -> None:
if self.changed:
if self.change_tracker.changed():
if not askUser("Discard changes?"):
return
self.cleanup()

View File

@ -8,18 +8,20 @@ from anki.models import NoteType
from anki.rsbackend import TemplateError
from aqt import AnkiQt
from aqt.qt import *
from aqt.utils import askUser, getOnlyText, openHelp, showWarning
from aqt.schema_change_tracker import ChangeTracker
from aqt.utils import askUser, getOnlyText, openHelp, showWarning, tooltip
class FieldDialog(QDialog):
def __init__(self, mw: AnkiQt, nt: NoteType, parent=None):
QDialog.__init__(self, parent or mw)
self.mw = mw.weakref()
self.mw = mw
self.col = self.mw.col
self.mm = self.mw.col.models
self.model = nt
self.mm._remove_from_cache(self.model["id"])
self.mw.checkpoint(_("Fields"))
self.change_tracker = ChangeTracker(self.mw)
self.form = aqt.forms.fields.Ui_Dialog()
self.form.setupUi(self)
self.setWindowTitle(_("Fields for %s") % self.model["name"])
@ -88,7 +90,9 @@ class FieldDialog(QDialog):
name = self._uniqueName(_("New name:"), self.currentIdx, f["name"])
if not name:
return
self.mm.renameField(self.model, f, name, save=False)
self.change_tracker.mark_basic()
self.mm.rename_field(self.model, f, name)
self.saveField()
self.fillFields()
self.form.fieldList.setCurrentRow(idx)
@ -97,9 +101,11 @@ class FieldDialog(QDialog):
name = self._uniqueName(_("Field name:"))
if not name:
return
if not self.change_tracker.mark_schema():
return
self.saveField()
f = self.mm.newField(name)
self.mm.addField(self.model, f, save=False)
self.mm.add_field(self.model, f)
self.fillFields()
self.form.fieldList.setCurrentRow(len(self.model["flds"]) - 1)
@ -110,8 +116,10 @@ class FieldDialog(QDialog):
c = ngettext("%d note", "%d notes", c) % c
if not askUser(_("Delete field from %s?") % c):
return
if not self.change_tracker.mark_schema():
return
f = self.model["flds"][self.form.fieldList.currentRow()]
self.mm.remField(self.model, f, save=False)
self.mm.remove_field(self.model, f)
self.fillFields()
self.form.fieldList.setCurrentRow(0)
@ -130,14 +138,18 @@ class FieldDialog(QDialog):
self.moveField(pos)
def onSortField(self):
if not self.change_tracker.mark_schema():
return False
# don't allow user to disable; it makes no sense
self.form.sortField.setChecked(True)
self.model["sortf"] = self.form.fieldList.currentRow()
self.mm.set_sort_index(self.model, self.form.fieldList.currentRow())
def moveField(self, pos):
if not self.change_tracker.mark_schema():
return False
self.saveField()
f = self.model["flds"][self.currentIdx]
self.mm.moveField(self.model, f, pos - 1, save=False)
self.mm.reposition_field(self.model, f, pos - 1)
self.fillFields()
self.form.fieldList.setCurrentRow(pos - 1)
@ -158,12 +170,28 @@ class FieldDialog(QDialog):
idx = self.currentIdx
fld = self.model["flds"][idx]
f = self.form
fld["font"] = f.fontFamily.currentFont().family()
fld["size"] = f.fontSize.value()
fld["sticky"] = f.sticky.isChecked()
fld["rtl"] = f.rtl.isChecked()
font = f.fontFamily.currentFont().family()
if fld["font"] != font:
fld["font"] = font
self.change_tracker.mark_basic()
size = f.fontSize.value()
if fld["size"] != size:
fld["size"] = size
self.change_tracker.mark_basic()
sticky = f.sticky.isChecked()
if fld["sticky"] != sticky:
fld["sticky"] = sticky
self.change_tracker.mark_basic()
rtl = f.rtl.isChecked()
if fld["rtl"] != rtl:
fld["rtl"] = rtl
self.change_tracker.mark_basic()
def reject(self):
if self.change_tracker.changed():
if not askUser("Discard changes?"):
return
QDialog.reject(self)
def accept(self):
@ -180,6 +208,7 @@ class FieldDialog(QDialog):
showWarning("Unable to save changes: " + str(e))
return
self.mw.reset()
tooltip("Changes saved.", parent=self.mw)
QDialog.accept(self)
self.mw.taskman.with_progress(save, on_done, self)

View File

@ -1282,6 +1282,7 @@ and if the problem comes up again, please ask on the support site."""
# Schema modifications
##########################################################################
# this will gradually be phased out
def onSchemaMod(self, arg):
assert self.inMainThread()
progress_shown = self.progress.busy()
@ -1300,6 +1301,22 @@ will be lost. Continue?"""
self.progress.start()
return ret
# in favour of this
def confirm_schema_modification(self) -> bool:
"""If schema unmodified, ask user to confirm change.
True if confirmed or already modified."""
if self.col.schemaChanged():
return True
return askUser(
_(
"""\
The requested change will require a full upload of the database when \
you next synchronize your collection. If you have reviews or other changes \
waiting on another device that haven't been synchronized here yet, they \
will be lost. Continue?"""
)
)
# Advanced features
##########################################################################

View File

@ -0,0 +1,36 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
import enum
from aqt import AnkiQt
class Change(enum.Enum):
NO_CHANGE = 0
BASIC_CHANGE = 1
SCHEMA_CHANGE = 2
class ChangeTracker:
_changed = Change.NO_CHANGE
def __init__(self, mw: AnkiQt):
self.mw = mw
def mark_basic(self):
if self._changed == Change.NO_CHANGE:
self._changed = Change.BASIC_CHANGE
def mark_schema(self) -> bool:
"If false, processing should be aborted."
if self._changed != Change.SCHEMA_CHANGE:
if not self.mw.confirm_schema_modification():
return False
self._changed = Change.SCHEMA_CHANGE
return True
def changed(self) -> bool:
return self._changed != Change.NO_CHANGE