def onFocusLost(self, fact, field): if not self.sessionExistsFor(field): return # This is necessary because the QT controls screw with the HTML. If we don't normalise # away the changes before the comparison, we might report changes where none took place. def normaliseHtml(html): if html == None: return None te = QtGui.QTextEdit() te.setHtml(html) return te.toHtml() log.info("User moved focus from the field %s", field.name) # Check old field contents against remembered value to determine changed status.. self.knownfieldcontents[field.name], fieldchanged = None, normaliseHtml(self.knownfieldcontents[field.name]) != normaliseHtml(field.value) # Changed fields have their "generated" tag stripped. NB: ALWAYS update the fact (even if the # field hasn't changed) because we might have changed ANOTHER field to e.g. blank it, and now # by moving focus from the expression field we indicate that we want to fill it out. # # NB: be careful with this ternary statement! It's perfectly OK for field.value to be "", and if # that happens we CAN'T let fieldvalue in updatefact be None, or autoblanking gets broken. self.updatefact(fact, field.name, (fieldchanged and [pinyin.factproxy.unmarkgeneratedfield(field.value)] or [None])[0])
def hanziData(self): # If we have some data already, just give up if self.__hanzidatacache is not None: return self.__hanzidatacache log.info("Updating Hanzi graph data") # Retrieve information about the card contents that were first answered on each day # # NB: the KanjiGraph uses information from ANY field (i.e. does not look at the fieldModels.name # value at all). However, Nick was confused by this behaviour because he had some radicals in his # deck, so his graph looked like he had `learnt' thousands of characters based on the listing of # every character in the `examples' fields of his radical facts. # # NB: the first answered time can be 0 but repeats > 1 due to a bug in an Anki feature which will # have screwed up the data in old decks. We select the created date for use in such cases: # <http://github.com/batterseapower/pinyin-toolkit/issues/closed/#issue/48> self.__hanzidatacache = self.mw.deck.s.all(""" select fields.value, cards.firstAnswered, cards.created from cards, fields, fieldModels, notes where cards.reps > 0 and cards.factId = fields.factId and cards.factId = notes.id and notes.modelId in %s and fields.fieldModelId = fieldModels.id and fieldModels.name in %s order by firstAnswered """ % (anki.utils.ids2str(self.suitableModelIds()), self.toSqlLiteral( self.config.candidateFieldNamesByKey['expression']))) return self.__hanzidatacache
def setupHanziGraph(self, graphwindow): log.info("Beginning setup of Hanzi graph on the graph window") # Don't add a graph if the deck doesn't have a Mandarin tag. This might be too conservative (the user # could add a Mandarin tag and then refresh) but in general it's going to work well to hide it from the # user on their non-Mandarin decks. if len(self.suitableModelIds()) == 0: return # NB: we used to preload the Hanzi data at this point, but that makes the ``refresh'' button not work, # because we have no means of clearing the preloaded data, so now it lives on the class. We also used to # avoid adding the graph if the current deck was not # Append our own graph at the end from ankiqt.ui.graphs import AdjustableFigure extragraph = AdjustableFigure( graphwindow.parent, 'hanzi', lambda days: self.calculateHanziData(graphwindow, days), graphwindow.range) extragraph.addWidget( QLabel("<h1>Unique Hanzi (Cumulative, By HSK Level)</h1>")) graphwindow.vbox.addWidget(extragraph) graphwindow.widgets.append(extragraph) # Add our graph to the name map - this is necessary to avoid exceptions when using show/hide graphwindow.nameMap[ 'hanzi'] = "Unique Hanzi (Cumulative, By HSK Level)" # To allow refreshing to work properly, we have to intercept the call to updateFigure() made by Ankis onRefresh code extragraph.updateFigure = wrap(extragraph.updateFigure, self.invalidateHanziData, "before")
def calculateHanziData(self, graphwindow, days): log.info("Calculating %d days worth of Hanzi graph data", days) # NB: must lazy-load matplotlib to give Anki a chance to set up the paths # to the data files, or we get an error like "Could not find the matplotlib # data files" as documented at <http://www.py2exe.org/index.cgi/MatPlotLib> try: from matplotlib.figure import Figure except UnicodeEncodeError: # Haven't tracked down the cause of this yet, but reloading fixes it log.warn("UnicodeEncodeError loading matplotlib - trying again") from matplotlib.figure import Figure # Use the statistics engine to generate the data for graphing. # NB: should add one to the number of days because we want to # return e.g. 8 days of data for a 7 day plot (the extra day is "today"). xs, _totaly, gradeys = pinyin.statistics.hanziDailyStats( self.hanziData(), days + 1) # Set up the figure into which we will add all our graphs figure = Figure(figsize=(graphwindow.dg.width, graphwindow.dg.height), dpi=graphwindow.dg.dpi) self.addLegend(figure) self.addGraph(figure, graphwindow, days, xs, gradeys) return figure
def inner(): source = dictionarydir(path) if os.path.exists(source): return path, os.path.getmtime(source), lambda target: shutil.copyfile(source, target) else: log.info("Missing ordinary file at %s", source) return None
def onFocusLost(self, flag, note, fldIdx): savedNoteValues = deepcopy(note.values()) fieldNames = self.mw.col.models.fieldNames(note.model()) currentFieldName = fieldNames[fldIdx] log.info("User moved focus from the field %s", currentFieldName) # Are we not in a Mandarin model? if not(pinyin.utils.ismandarinmodel(note.model()['name'])): return flag # Need a fact proxy because the updater works on dictionary-like objects factproxy = pinyin.factproxy.FactProxy(self.config.candidateFieldNamesByKey, note) # Find which kind of field we have just moved off updater = None for key, fieldname in factproxy.fieldnames.items(): if currentFieldName == fieldname: updater = self.updaters.get(key) break # Update the card, ignoring any errors fieldValue = note.fields[fldIdx] if not updater: return flag pinyin.utils.suppressexceptions( lambda: updater.updatefact(factproxy, fieldValue)) noteChanged = (savedNoteValues != note.values()) return noteChanged
def runBulkFill(mw, config, notifier, updaters, field, updatehow, notification): if mw.web.key == "deckBrowser": return showInfo(u"No deck selected 同志!") log.info("User triggered missing information fill for %s" % field) queryStr = "deck:current " for tag in config.getmodeltagslist(): queryStr += " or note:*" + tag + "* " notes = Finder(mw.col).findNotes(queryStr) for noteId in notes: note = mw.col.getNote(noteId) # Need a fact proxy because the updater works on dictionary-like objects factproxy = pinyin.factproxy.FactProxy(config.candidateFieldNamesByKey, note) if field not in factproxy: continue getattr(updaters[field], updatehow)(factproxy, factproxy[field]) # NB: very important to mark the fact as modified (see #105) because otherwise # the HTML etc won't be regenerated by Anki, so users may not e.g. get working # sounds that have just been filled in by the updater. note.flush() # For good measure, mark the deck as modified as well (see #105) mw.col.setMod() # DEBUG consider future feature to add missing measure words cards after doing so (not now) notifier.info(notification)
def install(self): from anki.hooks import wrap import ankiqt.ui.facteditor log.info("Installing color shortcut keys hook") ankiqt.ui.facteditor.FactEditor.setupFields = wrap(ankiqt.ui.facteditor.FactEditor.setupFields, self.setupShortcuts, "after") self.setupShortcuts(self.mw.editor)
def setupHanziGraph(self, graphwindow): log.info("Beginning setup of Hanzi graph on the graph window") # Don't add a graph if the deck doesn't have a Mandarin tag. This might be too conservative (the user # could add a Mandarin tag and then refresh) but in general it's going to work well to hide it from the # user on their non-Mandarin decks. if len(self.suitableModelIds()) == 0: return # NB: we used to preload the Hanzi data at this point, but that makes the ``refresh'' button not work, # because we have no means of clearing the preloaded data, so now it lives on the class. We also used to # avoid adding the graph if the current deck was not # Append our own graph at the end from ankiqt.ui.graphs import AdjustableFigure extragraph = AdjustableFigure(graphwindow.parent, 'hanzi', lambda days: self.calculateHanziData(graphwindow, days), graphwindow.range) extragraph.addWidget(QtGui.QLabel("<h1>Unique Hanzi (Cumulative, By HSK Level)</h1>")) graphwindow.vbox.addWidget(extragraph) graphwindow.widgets.append(extragraph) # Add our graph to the name map - this is necessary to avoid exceptions when using show/hide graphwindow.nameMap['hanzi'] = "Unique Hanzi (Cumulative, By HSK Level)" # To allow refreshing to work properly, we have to intercept the call to updateFigure() made by Ankis onRefresh code extragraph.updateFigure = wrap(extragraph.updateFigure, self.invalidateHanziData, "before")
def hanziData(self): # If we have some data already, just give up if self.__hanzidatacache is not None: return self.__hanzidatacache log.info("Updating Hanzi graph data") # Retrieve information about the card contents that were first answered on each day # # NB: the KanjiGraph uses information from ANY field (i.e. does not look at the fieldModels.name # value at all). However, Nick was confused by this behaviour because he had some radicals in his # deck, so his graph looked like he had `learnt' thousands of characters based on the listing of # every character in the `examples' fields of his radical facts. # # NB: the first answered time can be 0 but repeats > 1 due to a bug in an Anki feature which will # have screwed up the data in old decks. We select the created date for use in such cases: # <http://github.com/batterseapower/pinyin-toolkit/issues/closed/#issue/48> self.__hanzidatacache = self.mw.deck.s.all(""" select fields.value, cards.firstAnswered, cards.created from cards, fields, fieldModels, facts where cards.reps > 0 and cards.factId = fields.factId and cards.factId = facts.id and facts.modelId in %s and fields.fieldModelId = fieldModels.id and fieldModels.name in %s order by firstAnswered """ % (anki.utils.ids2str(self.suitableModelIds()), pinyin.utils.toSqlLiteral(self.config.candidateFieldNamesByKey['expression']))) return self.__hanzidatacache
def inner(): path, timestamp = findtimestampedfile(pathpattern) if path is None: log.info("Missing archive matching the timestamped pattern %s", pathpattern) return None return plainArchiveSource(path, ["%s" in pizp and (pizp % timestamp) or pizp for pizp in pathinzippattern])()
def inner(): path, _timestamp = findtimestampedfile(pathpattern) if path is None: log.info("Missing file matching the timestamped pattern %s", pathpattern) return None return fileSource(path)()
def onFocusGot(self, field): if not self.sessionExistsFor(field): return log.info("User put focus on the field %s", field.name) # Remember old field contents self.knownfieldcontents[field.name] = field.value
def setupShortcuts(self, editor): # Loop through the 8 F[x] keys, setting each one up # Note: Ctrl-F9 is the HTML editor. Don't do this as it causes a conflict log.info("Setting up shortcut keys on fact editor") for i in range(1, 9): for sandhify in [True, False]: keysequence = (sandhify and pinyin.anki.keys.sandhiModifier + "+" or "") + pinyin.anki.keys.shortcutKeyFor(i) QtGui.QShortcut(QtGui.QKeySequence(keysequence), editor.widget, lambda i=i, sandhify=sandhify: self.setColor(editor, i, sandhify))
def install(self): from anki.hooks import addHook, remHook # Install hook into focus event of Anki: we regenerate the model information when # the cursor moves from the Expression/Reading/whatever field to another field log.info("Installing focus hook") # Unconditionally add our new hook to Anki addHook('editFocusLost', self.onFocusLost)
def setupShortcuts(self, editor): # Loop through the 8 F[x] keys, setting each one up # Note: Ctrl-F9 is the HTML editor. Don't do this as it causes a conflict log.info("Setting up shortcut keys on fact editor") for i in range(1, 9): for sandhify in [True, False]: keysequence = (sandhify and pinyin.anki.keys.sandhiModifier + "+" or "") + pinyin.anki.keys.shortcutKeyFor(i) QShortcut(QKeySequence(keysequence), editor.widget, lambda i=i, sandhify=sandhify: self.setColor(editor, i, sandhify))
def chooseField(candidateFieldNames, targetkeys): # Find the first field that is present in the fact for candidateField in candidateFieldNames: for factfieldname in [factfieldname for factfieldname in targetkeys if factfieldname.lower() == candidateField.lower()]: log.info("Choose %s as a field name from the fact for %s", factfieldname, candidateField) return factfieldname # No suitable field found! log.warn("No field matching %s in the fact", candidateFieldNames) return None
def install(self): log.info("Installing Hanzi graph hook") # NB: must lazy-load ankiqt.ui.graphs because importing it will fail if the user doesn't # have python-matplotlib installed on Linux. try: from ankiqt.ui.graphs import GraphWindow GraphWindow.setupGraphs = wrap(GraphWindow.setupGraphs, self.setupHanziGraph, "after") except ImportError, e: self.notifier.exception("There was a problem setting up the Hanzi Graph! If you are using Linux, " + "you may need to install the package providing matplotlib to Python. On Ubuntu " + "you can do that by running 'sudo apt-get install python-matplotlib' in the Terminal.")
def __init__(self, candidateFieldNamesByKey, fact): self.fact = fact # NB: the fieldnames dictionary IS part of the interface of this class self.fieldnames = {} for key, candidateFieldNames in candidateFieldNamesByKey.items(): # Don't add a key into the dictionary if we can't find a field, or we end # up reporting that we the contain the field but die during access fieldname = chooseField(candidateFieldNames, fact.keys()) if fieldname is not None: self.fieldnames[key] = fieldname log.info("Choose field mapping %r", self.fieldnames)
def setColor(self, editor, i, sandhify): log.info("Got color change event for color %d, sandhify %s", i, sandhify) color = (self.config.tonecolors + self.config.extraquickaccesscolors)[i - 1] if sandhify: color = pinyin.transformations.sandhifycolor(color) focusededit = editor.focusedEdit() cursor = focusededit.textCursor() focusededit.setTextColor(QtGui.QColor(color)) cursor.clearSelection() focusededit.setTextCursor(cursor)
def setColor(self, editor, i, sandhify): log.info("Got color change event for color %d, sandhify %s", i, sandhify) color = (self.config.tonecolors + self.config.extraquickaccesscolors)[i - 1] if sandhify: color = pinyin.transformations.sandhifycolor(color) focusededit = editor.focusedEdit() cursor = focusededit.textCursor() focusededit.setTextColor(QColor(color)) cursor.clearSelection() focusededit.setTextCursor(cursor)
def initialize(self, mw): log.info("Pinyin Toolkit is initializing") # Build basic objects we use to interface with Anki thenotifier = notifier.AnkiNotifier() themediamanager = mediamanager.AnkiMediaManager(mw) # Open up the database if not self.tryCreateAndLoadDatabase(mw, thenotifier): # Eeek! Database building failed, so we better turn off the toolkit log.error("Database construction failed: disabling the Toolkit") return # Build the updaters updaters = { 'expression': pinyin.updater.FieldUpdaterFromExpression(thenotifier, themediamanager), 'reading': pinyin.updater.FieldUpdaterFromReading(), 'meaning': pinyin.updater.FieldUpdaterFromMeaning(), 'audio': pinyin.updater.FieldUpdaterFromAudio(thenotifier, themediamanager) } # Finally, build the hooks. Make sure you store a reference to these, because otherwise they # get garbage collected, causing garbage collection of the actions they contain self.hooks = [ hookbuilder(mw, thenotifier, themediamanager, updaters) for hookbuilder in hookbuilders ] for hook in self.hooks: hook.install() # add hooks and menu items # use wrap() instead of addHook to ensure menu already created def ptkRebuildAddonsMenu(self): ptkMenu = None for menu in self._menus: if menu.title() == "Pinyin Toolkit": ptkMenu = menu break ptkMenu.addSeparator() config = getconfig() hooks.buildHooks(ptkMenu, mw, config, thenotifier, themediamanager, updaters) aqt.addons.AddonManager.rebuildAddonsMenu = wrap( aqt.addons.AddonManager.rebuildAddonsMenu, ptkRebuildAddonsMenu)
def chooseField(candidateFieldNames, fact): # Find the first field that is present in the fact for candidateField in candidateFieldNames: for factfieldname in [ factfieldname for factfieldname in fact.keys() if factfieldname.lower() == candidateField.lower() ]: log.info("Choose %s as a field name from the fact for %s", factfieldname, candidateField) return factfieldname # No suitable field found! log.warn("No field matching %s in the fact", candidateFieldNames) return None
def openPreferences(mw, config, notifier, mediamanager): # NB: must import these lazily to break a loop between preferencescontroller and here import pinyin.forms.preferences import pinyin.forms.preferencescontroller log.info("User opened preferences dialog") # Instantiate and show the preferences dialog modally preferences = pinyin.forms.preferences.Preferences(mw) controller = pinyin.forms.preferencescontroller.PreferencesController(preferences, notifier, mediamanager, config) result = preferences.exec_() if result == QDialog.Accepted: config.settings = controller.model.settings saveconfig()
def install(self): log.info("Installing Hanzi graph hook") # NB: must lazy-load ankiqt.ui.graphs because importing it will fail if the user doesn't # have python-matplotlib installed on Linux. try: from ankiqt.ui.graphs import GraphWindow GraphWindow.setupGraphs = wrap(GraphWindow.setupGraphs, self.setupHanziGraph, "after") except ImportError, e: self.notifier.exception( "There was a problem setting up the Hanzi Graph! If you are using Linux, " + "you may need to install the package providing matplotlib to Python. On Ubuntu " + "you can do that by running 'sudo apt-get install python-matplotlib' in the Terminal." )
def discovermediapacks(self): packs = [] themediadir = self.mediadir() for packname in os.listdir(themediadir): # Skip the download cache directory if packname.lower() == "downloads": continue # Only try and process directories as packs: packpath = os.path.join(themediadir, packname) if os.path.isdir(packpath): log.info("Considering %s as a media pack", packname) packs.append(pinyin.media.MediaPack.frompath(packpath)) else: log.info("Ignoring the file %s in the media directory", packname) return packs
def install(self): from anki.hooks import addHook, removeHook # Install hook into focus event of Anki: we regenerate the model information when # the cursor moves from the Expression/Reading/whatever field to another field log.info("Installing focus hook") try: # On versions of Anki that still had Chinese support baked in, remove the # provided hook from this event before we replace it with our own: from anki.features.chinese import onFocusLost as oldHook removeHook('fact.focusLost', oldHook) except ImportError: pass # Unconditionally add our new hooks to Anki addHook('makeField', self.makeField) addHook('fact.focusLost', self.onFocusLost)
def initialize(self, mw): log.info("Pinyin Toolkit is initializing") # Build basic objects we use to interface with Anki thenotifier = notifier.AnkiNotifier() themediamanager = mediamanager.AnkiMediaManager(mw) # Open up the database if not self.tryCreateAndLoadDatabase(mw, thenotifier): # Eeek! Database building failed, so we better turn off the toolkit log.error("Database construction failed: disabling the Toolkit") return # Try and load the settings from the Anki config database settings = mw.config.get("pinyintoolkit") if settings is None: # Initialize the configuration with default settings config = pinyin.config.Config() utils.persistconfig(mw, config) # TODO: first-run activities: # 1) Guide user around the interface and what they can do # 2) Link to getting started guide else: # Initialize the configuration with the stored settings config = pinyin.config.Config(settings) # Build the updaters updaters = { 'expression' : pinyin.updater.FieldUpdaterFromExpression, 'reading' : lambda *args: pinyin.updater.FieldUpdater("reading", *args), 'meaning' : lambda *args: pinyin.updater.FieldUpdater("meaning", *args), 'audio' : lambda *args: pinyin.updater.FieldUpdater("audio", *args) } # Finally, build the hooks. Make sure you store a reference to these, because otherwise they # get garbage collected, causing garbage collection of the actions they contain self.hooks = [hookbuilder(mw, thenotifier, themediamanager, config, updaters) for hookbuilder in hookbuilders] for hook in self.hooks: hook.install() # Tell Anki about the plugin mw.registerPlugin("Mandarin Chinese Pinyin Toolkit", 4) self.registerStandardModels()
def install(self): from anki.hooks import addHook, removeHook # Install hook into focus event of Anki: we regenerate the model information when # the cursor moves from the Expression/Reading/whatever field to another field log.info("Installing focus hook") try: # On versions of Anki that still had Chinese support baked in, remove the # provided hook from this event before we replace it with our own: from anki.features.chinese import onFocusLost as oldHook removeHook("fact.focusLost", oldHook) except ImportError: pass # Unconditionally add our new hooks to Anki addHook("makeField", self.makeField) addHook("fact.focusLost", self.onFocusLost)
def onFocusLost(self, fact, field): if not self.sessionExistsFor(field): return log.info("User moved focus from the field %s", field.name) # Determine whether the field was modified by checking the document modified status fieldwidget = self.knownfactedit.fields[field.name][1] fieldchanged = fieldwidget.document().isModified() # Changed fields have their "generated" tag stripped. NB: ALWAYS update the fact (even if the # field hasn't changed) because we might have changed ANOTHER field to e.g. blank it, and now # by moving focus from the expression field we indicate that we want to fill it out. # # NB: be careful with this ternary statement! It's perfectly OK for field.value to be "", and if # that happens we CAN'T let fieldvalue in updatefact be None, or autoblanking gets broken. self.updatefact( fact, field.name, (fieldchanged and [pinyin.factproxy.unmarkgeneratedfield(field.value)] or [None])[0] )
def initialize(self, mw): log.info("Pinyin Toolkit is initializing") # Build basic objects we use to interface with Anki thenotifier = notifier.AnkiNotifier() themediamanager = mediamanager.AnkiMediaManager(mw) # Open up the database if not self.tryCreateAndLoadDatabase(mw, thenotifier): # Eeek! Database building failed, so we better turn off the toolkit log.error("Database construction failed: disabling the Toolkit") return # Build the updaters updaters = { 'expression' : pinyin.updater.FieldUpdaterFromExpression(thenotifier, themediamanager), 'reading' : pinyin.updater.FieldUpdaterFromReading(), 'meaning' : pinyin.updater.FieldUpdaterFromMeaning(), 'audio' : pinyin.updater.FieldUpdaterFromAudio(thenotifier, themediamanager) } # Finally, build the hooks. Make sure you store a reference to these, because otherwise they # get garbage collected, causing garbage collection of the actions they contain self.hooks = [hookbuilder(mw, thenotifier, themediamanager, updaters) for hookbuilder in hookbuilders] for hook in self.hooks: hook.install() # add hooks and menu items # use wrap() instead of addHook to ensure menu already created def ptkRebuildAddonsMenu(self): ptkMenu = None for menu in self._menus: if menu.title() == "Pinyin Toolkit": ptkMenu = menu break ptkMenu.addSeparator() config = getconfig() hooks.buildHooks(ptkMenu, mw, config, thenotifier, themediamanager, updaters) aqt.addons.AddonManager.rebuildAddonsMenu = wrap(aqt.addons.AddonManager.rebuildAddonsMenu, ptkRebuildAddonsMenu)
def install(self): # Install menu item log.info("Installing a menu hook (%s)", type(self)) # Build and install the top level menu if it doesn't already exist if ToolMenuHook.pinyinToolkitMenu is None: ToolMenuHook.pinyinToolkitMenu = QtGui.QMenu("Pinyin Toolkit", self.mw.mainWin.menuTools) self.mw.mainWin.menuTools.addMenu(ToolMenuHook.pinyinToolkitMenu) # Store the action on the class. Storing a reference to it is necessary to avoid it getting garbage collected. self.action = QtGui.QAction(self.__class__.menutext, self.mw) self.action.setStatusTip(self.__class__.menutooltip) self.action.setEnabled(True) # HACK ALERT: must use lambda here, or the signal never gets raised! I think this is due to garbage collection... # We try and make sure that we don't run the action if there is no deck presently, to at least suppress some errors # in situations where the users select the menu items (this is possible on e.g. OS X). It would be better to disable # the menu items entirely in these situations, but there is no suitable hook for that presently. self.mw.connect(self.action, QtCore.SIGNAL('triggered()'), lambda: self.mw.deck is not None and self.triggered()) ToolMenuHook.pinyinToolkitMenu.addAction(self.action)
def triggered(self): # NB: must import these lazily to break a loop between preferencescontroller and here import pinyin.forms.preferences import pinyin.forms.preferencescontroller log.info("User opened preferences dialog") # Instantiate and show the preferences dialog modally preferences = pinyin.forms.preferences.Preferences(self.mw) controller = pinyin.forms.preferencescontroller.PreferencesController(preferences, self.notifier, self.mediamanager, self.config) result = preferences.exec_() # We only need to change the configuration if the user accepted the dialog if result == QtGui.QDialog.Accepted: # Update by the simple method of replacing the settings dictionaries: better make sure that no # other part of the code has cached parts of the configuration self.config.settings = controller.model.settings # Ensure this is saved in Anki's configuration utils.persistconfig(self.mw, self.config)
def triggered(self): # NB: must import these lazily to break a loop between preferencescontroller and here import pinyin.forms.preferences import pinyin.forms.preferencescontroller log.info("User opened preferences dialog") # Instantiate and show the preferences dialog modally preferences = pinyin.forms.preferences.Preferences(self.mw) controller = pinyin.forms.preferencescontroller.PreferencesController( preferences, self.notifier, self.mediamanager, self.config) result = preferences.exec_() # We only need to change the configuration if the user accepted the dialog if result == QtGui.QDialog.Accepted: # Update by the simple method of replacing the settings dictionaries: better make sure that no # other part of the code has cached parts of the configuration self.config.settings = controller.model.settings # Ensure this is saved in Anki's configuration utils.persistconfig(self.mw, self.config)
def onFocusLost(self, fact, field): if not self.sessionExistsFor(field): return log.info("User moved focus from the field %s", field.name) # Determine whether the field was modified by checking the document modified status fieldwidget = self.knownfactedit.fields[field.name][1] fieldchanged = fieldwidget.document().isModified() # Changed fields have their "generated" tag stripped. NB: ALWAYS update the fact (even if the # field hasn't changed) because we might have changed ANOTHER field to e.g. blank it, and now # by moving focus from the expression field we indicate that we want to fill it out. # # NB: be careful with this ternary statement! It's perfectly OK for field.value to be "", and if # that happens we CAN'T let fieldvalue in updatefact be None, or autoblanking gets broken. self.updatefact( fact, field.name, (fieldchanged and [pinyin.factproxy.unmarkgeneratedfield(field.value)] or [None])[0])
def install(self): log.info("Installing Hanzi statistics hook") # NB: must store reference to action on the class to prevent it being GCed self.action = QtGui.QAction("Hanzi Statistics by PyTK", self.mw) self.action.setStatusTip("Hanzi Statistics by PyTK") self.action.setEnabled(True) self.action.setIcon(QtGui.QIcon("../icons/hanzi.png")) def finish(x): html, python_actions = x self.mw.help.showText( html, py=dict([(k, lambda action=action: finish(action())) for k, action in python_actions]) ) self.mw.connect( self.action, QtCore.SIGNAL("triggered()"), lambda: finish(hanziStats(self.config, self.mw.deck.s)) ) self.mw.mainWin.menuTools.addAction(self.action) log.info("Hanzi statistics plugin loaded")
def tryCreateAndLoadDatabase(self, mw, notifier): datatimestamp, satisfiers = pinyin.db.builder.getSatisfiers() cjklibtimestamp = os.path.getmtime( pinyin.utils.toolkitdir("pinyin", "vendor", "cjklib", "cjklib", "build", "builder.py")) if not (os.path.exists(dbpath)): # MUST rebuild - DB doesn't exist log.info( "The database was missing entirely from %s. We had better build it!", dbpath) compulsory = True elif os.path.getmtime(dbpath) < cjklibtimestamp: # MUST rebuild - version upgrade might have changed DB format log.info( "The cjklib was upgraded at %d, which is since the database was built (at %d) - for safety we must rebuild", cjklibtimestamp, os.path.getmtime(dbpath)) compulsory = True elif os.path.getmtime(dbpath) < datatimestamp: # SHOULD rebuild log.info( "The database had a timestamp of %d but we saw a data update at %d - let's rebuild", os.path.getmtime(dbpath), datatimestamp) compulsory = False else: # Do nothing log.info("Database up to date") compulsory = None if compulsory is not None: # We at least have the option to rebuild the DB: setup the builder dbbuilder = pinyin.db.builder.DBBuilder(satisfiers) # Show the form, which kicks off the builder and may give the user the option to cancel builddb = pinyin.forms.builddb.BuildDB(mw) # NB: VERY IMPORTANT to save the useless controller reference somewhere. This prevents the # QThread it spawns being garbage collected while the thread is still running! I hate PyQT4! _controller = pinyin.forms.builddbcontroller.BuildDBController( builddb, notifier, dbbuilder, compulsory) if builddb.exec_() == QDialog.Accepted: # Successful completion of the build process: replace the existing database, if any shutil.copyfile(dbbuilder.builtdatabasepath, dbpath) elif compulsory: # Eeek! The dialog was "rejected" despite being compulsory. This can only happen if there # was an error while building the database. Better give up now! return False # Finally, force the database connection to the (possibly fresh) DB to begin database() return True
def build(self): # [1/4]: copy and extract necessary files into a location cjklib can deal with log.info("Copying in dictionary data") for requirement, satisfier in self.satisfiers: satisfier(os.path.join(self.dictionarydatapath, requirement)) # [2/4]: setup the database builder with a standard set of requirements log.info("Initializing builder") database = cjklib.dbconnector.getDBConnector({ "url" : sqlalchemy.engine.url.URL("sqlite", database=self.builtdatabasepath) }) self.cjkdbbuilder = cjklib.build.DatabaseBuilder( dbConnectInst=database, # We need to turn quiet on, because Anki throws a hissy fit if you write to stderr # We turn disableFTS3 on because it makes my SELECTs 4 times faster on SQLite 3.4.0 quiet=True, enableFTS3=False, rebuildExisting=False, noFail=False, dataPath=[self.dictionarydatapath, self.cjkdatapath], prefer=['CharacterVariantBMPBuilder', 'CombinedStrokeCountBuilder', 'CombinedCharacterResidualStrokeCountBuilder', 'HanDeDictFulltextSearchBuilder', 'UnihanBMPBuilder']) # [3/4]: build the database log.info("Building the cjklib database: the target file is %s", self.builtdatabasepath) self.cjkdbbuilder.build(DBBuilder.wantgroups) # [4/4]: clean up, so that we don't get errors if (when) the temporary database is deleted database.connection.close() del database.connection database.engine.dispose() del database.engine
def install(self): # Install menu item log.info("Installing a menu hook (%s)", type(self)) # Build and install the top level menu if it doesn't already exist if ToolMenuHook.pinyinToolkitMenu is None: ToolMenuHook.pinyinToolkitMenu = QtGui.QMenu( "Pinyin Toolkit", self.mw.mainWin.menuTools) self.mw.mainWin.menuTools.addMenu(ToolMenuHook.pinyinToolkitMenu) # Store the action on the class. Storing a reference to it is necessary to avoid it getting garbage collected. self.action = QtGui.QAction(self.__class__.menutext, self.mw) self.action.setStatusTip(self.__class__.menutooltip) self.action.setEnabled(True) # HACK ALERT: must use lambda here, or the signal never gets raised! I think this is due to garbage collection... # We try and make sure that we don't run the action if there is no deck presently, to at least suppress some errors # in situations where the users select the menu items (this is possible on e.g. OS X). It would be better to disable # the menu items entirely in these situations, but there is no suitable hook for that presently. self.mw.connect(self.action, QtCore.SIGNAL('triggered()'), lambda: self.mw.deck is not None and self.triggered()) ToolMenuHook.pinyinToolkitMenu.addAction(self.action)
def install(self): from anki.hooks import addHook, removeHook # Install hook into focus event of Anki: we regenerate the model information when # the cursor moves from the Expression/Reading/whatever field to another field log.info("Installing focus hook") try: # On versions of Anki that still had Chinese support baked in, remove the # provided hook from this event before we replace it with our own: from anki.features.chinese import onFocusLost as oldHook removeHook('fact.focusLost', oldHook) except ImportError: pass # Unconditionally add our new hooks to Anki addHook('makeField', self.makeField) addHook('fact.focusLost', self.onFocusLost) # Global hook app = QtGui.QApplication.instance() app.connect(app, QtCore.SIGNAL("focusChanged(QWidget*, QWidget*)"), self.onFocusChanged)
def triggered(self): field = self.__class__.field log.info("User triggered missing information fill for %s" % field) for fact in utils.suitableFacts(self.config.modelTag, self.mw.deck): # Need a fact proxy because the updater works on dictionary-like objects factproxy = pinyin.factproxy.FactProxy(self.config.candidateFieldNamesByKey, fact) if field not in factproxy: continue self.buildupdater(field).updatefact(factproxy, None, **self.__class__.updatefactkwargs) # NB: very important to mark the fact as modified (see #105) because otherwise # the HTML etc won't be regenerated by Anki, so users may not e.g. get working # sounds that have just been filled in by the updater. fact.setModified(textChanged=True) # For good measure, mark the deck as modified as well (see #105) self.mw.deck.setModified() # DEBUG consider future feature to add missing measure words cards after doing so (not now) self.notifier.info(self.__class__.notification)
def triggered(self): field = self.__class__.field log.info("User triggered missing information fill for %s" % field) for fact in utils.suitableFacts(self.config.modelTag, self.mw.deck): # Need a fact proxy because the updater works on dictionary-like objects factproxy = pinyin.factproxy.FactProxy( self.config.candidateFieldNamesByKey, fact) if field not in factproxy: continue self.buildupdater(field).updatefact( factproxy, None, **self.__class__.updatefactkwargs) # NB: very important to mark the fact as modified (see #105) because otherwise # the HTML etc won't be regenerated by Anki, so users may not e.g. get working # sounds that have just been filled in by the updater. fact.setModified(textChanged=True) # For good measure, mark the deck as modified as well (see #105) self.mw.deck.setModified() # DEBUG consider future feature to add missing measure words cards after doing so (not now) self.notifier.info(self.__class__.notification)
def calculateHanziData(self, graphwindow, days): log.info("Calculating %d days worth of Hanzi graph data", days) # NB: must lazy-load matplotlib to give Anki a chance to set up the paths # to the data files, or we get an error like "Could not find the matplotlib # data files" as documented at <http://www.py2exe.org/index.cgi/MatPlotLib> try: from matplotlib.figure import Figure except UnicodeEncodeError: # Haven't tracked down the cause of this yet, but reloading fixes it log.warn("UnicodeEncodeError loading matplotlib - trying again") from matplotlib.figure import Figure # Use the statistics engine to generate the data for graphing. # NB: should add one to the number of days because we want to # return e.g. 8 days of data for a 7 day plot (the extra day is "today"). xs, _totaly, gradeys = pinyin.statistics.hanziDailyStats(self.hanziData(), days + 1) # Set up the figure into which we will add all our graphs figure = Figure(figsize=(graphwindow.dg.width, graphwindow.dg.height), dpi=graphwindow.dg.dpi) self.addLegend(figure) self.addGraph(figure, graphwindow, days, xs, gradeys) return figure
def install(self): log.info("Installing Hanzi statistics hook") # NB: must store reference to action on the class to prevent it being GCed self.action = QtGui.QAction('Hanzi Statistics by PyTK', self.mw) self.action.setStatusTip('Hanzi Statistics by PyTK') self.action.setEnabled(True) self.action.setIcon(QtGui.QIcon("../icons/hanzi.png")) def finish(x): html, python_actions = x self.mw.help.showText(html, py=dict([ (k, lambda action=action: finish(action())) for k, action in python_actions ])) self.mw.connect( self.action, QtCore.SIGNAL('triggered()'), lambda: finish(hanziStats(self.config, self.mw.deck.s))) self.mw.mainWin.menuTools.addAction(self.action) log.info('Hanzi statistics plugin loaded')
def tryCreateAndLoadDatabase(self, mw, notifier): datatimestamp, satisfiers = pinyin.db.builder.getSatisfiers() cjklibtimestamp = os.path.getmtime(pinyin.utils.toolkitdir("pinyin", "vendor", "cjklib", "cjklib", "build", "builder.py")) if not(os.path.exists(dbpath)): # MUST rebuild - DB doesn't exist log.info("The database was missing entirely from %s. We had better build it!", dbpath) compulsory = True elif os.path.getmtime(dbpath) < cjklibtimestamp: # MUST rebuild - version upgrade might have changed DB format log.info("The cjklib was upgraded at %d, which is since the database was built (at %d) - for safety we must rebuild", cjklibtimestamp, os.path.getmtime(dbpath)) compulsory = True elif os.path.getmtime(dbpath) < datatimestamp: # SHOULD rebuild log.info("The database had a timestamp of %d but we saw a data update at %d - let's rebuild", os.path.getmtime(dbpath), datatimestamp) compulsory = False else: # Do nothing log.info("Database up to date") compulsory = None if compulsory is not None: # We at least have the option to rebuild the DB: setup the builder dbbuilder = pinyin.db.builder.DBBuilder(satisfiers) # Show the form, which kicks off the builder and may give the user the option to cancel builddb = pinyin.forms.builddb.BuildDB(mw) # NB: VERY IMPORTANT to save the useless controller reference somewhere. This prevents the # QThread it spawns being garbage collected while the thread is still running! I hate PyQT4! _controller = pinyin.forms.builddbcontroller.BuildDBController(builddb, notifier, dbbuilder, compulsory) if builddb.exec_() == QDialog.Accepted: # Successful completion of the build process: replace the existing database, if any shutil.copyfile(dbbuilder.builtdatabasepath, dbpath) elif compulsory: # Eeek! The dialog was "rejected" despite being compulsory. This can only happen if there # was an error while building the database. Better give up now! return False # Finally, force the database connection to the (possibly fresh) DB to begin database() return True
def inner(): # Ensure that the zip exists before we open it zipsource = dictionarydir(path) if not(os.path.exists(zipsource)): log.info("Missing zip file at %s", zipsource) return None # Load up the zip sourcezip = zipfile.ZipFile(zipsource, "r") log.info("Available zip file contents %s", sourcezip.namelist()) # Find the correct file in the zip: we should try both sorts of # slashes because the zip may have been created on Windows or Unix pathinzip = None for possiblepath in ["/".join(pathinzipcomponents), "\\".join(pathinzipcomponents)]: try: sourcezip.getinfo(possiblepath) pathinzip = possiblepath break except KeyError: pass # Maybe the file didn't exist at all? if pathinzip is None: log.info("Zip file at %s lacked a file called %s", path, os.path.join(*pathinzipcomponents)) return None def go(target): # Extract the selected file from the zip targetfile = open(target, 'w') try: targetfile.write(sourcezip.read(pathinzip)) finally: targetfile.close() return path + ":" + pathinzip, os.path.getmtime(zipsource), go
def getSatisfiers(): dictionarydir = lambda *components: pinyin.utils.toolkitdir("pinyin", "dictionaries", *components) def fileSource(path): def inner(): source = dictionarydir(path) if os.path.exists(source): return path, os.path.getmtime(source), lambda target: shutil.copyfile(source, target) else: log.info("Missing ordinary file at %s", source) return None return inner def plainArchiveSource(path, pathinzipcomponents): def inner(): # Ensure that the zip exists before we open it zipsource = dictionarydir(path) if not(os.path.exists(zipsource)): log.info("Missing zip file at %s", zipsource) return None # Load up the zip sourcezip = zipfile.ZipFile(zipsource, "r") log.info("Available zip file contents %s", sourcezip.namelist()) # Find the correct file in the zip: we should try both sorts of # slashes because the zip may have been created on Windows or Unix pathinzip = None for possiblepath in ["/".join(pathinzipcomponents), "\\".join(pathinzipcomponents)]: try: sourcezip.getinfo(possiblepath) pathinzip = possiblepath break except KeyError: pass # Maybe the file didn't exist at all? if pathinzip is None: log.info("Zip file at %s lacked a file called %s", path, os.path.join(*pathinzipcomponents)) return None def go(target): # Extract the selected file from the zip targetfile = open(target, 'w') try: targetfile.write(sourcezip.read(pathinzip)) finally: targetfile.close() return path + ":" + pathinzip, os.path.getmtime(zipsource), go return inner def findtimestampedfile(pathpattern): path, timestamp = None, None for file in os.listdir(dictionarydir()): # We want to find the file with the maximal timestamp. Luckily, I have carefully # constructed the filenames so that this is just the ordering on the strings match = re.match(pathpattern % "(.+)", file) if match and match.group(1) > timestamp: path, timestamp = match.group(0), match.group(1) return path, timestamp def timestampedFileSource(pathpattern): def inner(): path, _timestamp = findtimestampedfile(pathpattern) if path is None: log.info("Missing file matching the timestamped pattern %s", pathpattern) return None return fileSource(path)() return inner def timestampedArchiveSource(pathpattern, pathinzippattern): def inner(): path, timestamp = findtimestampedfile(pathpattern) if path is None: log.info("Missing archive matching the timestamped pattern %s", pathpattern) return None return plainArchiveSource(path, ["%s" in pizp and (pizp % timestamp) or pizp for pizp in pathinzippattern])() return inner # NB: because we currently use the first matching source, I've put the timestamped .txt files that # come with the Toolkit at the end of the list. This ensures that if we ever implement dictionary # download, the resulting .zip files will be used in preference to the .txt files. requirements = { "cedict_ts.u8" : [fileSource("cedict_ts.u8"), plainArchiveSource("cedict_1_0_ts_utf-8_mdbg.zip", ["cedict_ts.u8"]), timestampedArchiveSource("cedict-%s.zip", ["cedict_ts.u8"]), timestampedFileSource("cedict-%s.txt")], "handedict.u8" : [fileSource("handedict_nb.u8"), timestampedArchiveSource("handedict-%s.zip", ["handedict-%s", "handedict_nb.u8"]), timestampedFileSource("handedict-%s.txt")], "cfdict.u8" : [fileSource("cfdict_nb.u8"), timestampedArchiveSource("cfdict-%s.zip", ["cfdict-%s", "cfdict_nb.u8"]), plainArchiveSource("shipped.zip", ["cfdict_nb.u8"]), timestampedFileSource("cfdict-%s.txt")], "Unihan.txt" : [fileSource("Unihan.txt"), plainArchiveSource("Unihan.zip", ["Unihan.txt"])] } maxtimestamp = 0 satisfiers = [] for requirement, sources in requirements.items(): success = False for source in sources: timestampsatisfier = source() if timestampsatisfier: satisfiedby, timestamp, satisfier = timestampsatisfier log.info("The requirement for %s was satisified by %s (with a timestamp of %d)", requirement, satisfiedby, timestamp) maxtimestamp = max(maxtimestamp, timestamp) satisfiers.append((requirement, satisfier)) success = True break if not(success): raise IOError("Couldn't satisfy our need for '%s' during dictionary generation" % requirement) return maxtimestamp, satisfiers
def __del__(self): try: log.info("Cleaning up temporary dictionary builder directory") shutil.rmtree(self.dictionarydatapath) except IOError: pass