', allowedTags)
+ self.labelTags.setText('Allowed tags are {0}'.format(', '.join(allowedTags)))
+
+ self.updateAnkiFields()
+
+
+ def updateSampleText(self):
+ palette = self.textSample.palette()
+ palette.setColor(QtGui.QPalette.Base, QtGui.QColor(self.preferences['bgColor']))
+ palette.setColor(QtGui.QPalette.Text, QtGui.QColor(self.preferences['fgColor']))
+ self.textSample.setPalette(palette)
+
+ font = self.textSample.font()
+ font.setFamily(self.preferences['fontFamily'])
+ font.setPointSize(self.preferences['fontSize'])
+ self.textSample.setFont(font)
+
+
+ def setAnkiFields(self, fields, fieldsPrefs):
+ if fields is None:
+ fields = []
+
+ self.tableFields.blockSignals(True)
+ self.tableFields.setRowCount(len(fields))
+
+ for i, name in enumerate(fields):
+ columns = []
+
+ itemName = QtGui.QTableWidgetItem(name)
+ itemName.setFlags(QtCore.Qt.ItemIsSelectable)
+ columns.append(itemName)
+
+ itemValue = QtGui.QTableWidgetItem(fieldsPrefs.get(name, u''))
+ columns.append(itemValue)
+
+ for j, column in enumerate(columns):
+ self.tableFields.setItem(i, j, column)
+
+ self.tableFields.blockSignals(False)
+
+
+ def ankiFields(self):
+ result = {}
+
+ for i in range(0, self.tableFields.rowCount()):
+ itemName = unicode(self.tableFields.item(i, 0).text())
+ itemValue = unicode(self.tableFields.item(i, 1).text())
+ result[itemName] = itemValue
+
+ return result
+
+
+ def onAccept(self):
+ self.dialogToData()
+
+
+ def onButtonColorFgClicked(self):
+ color, ok = QtGui.QColorDialog.getRgba(self.preferences['fgColor'], self)
+ if ok:
+ self.preferences['fgColor'] = color
+ self.updateSampleText()
+
+
+ def onButtonColorBgClicked(self):
+ color, ok = QtGui.QColorDialog.getRgba(self.preferences['bgColor'], self)
+ if ok:
+ self.preferences['bgColor'] = color
+ self.updateSampleText()
+
+
+ def onFontFamilyChanged(self, font):
+ self.preferences['fontFamily'] = unicode(font.family())
+ self.updateSampleText()
+
+
+ def onFontSizeChanged(self, size):
+ self.preferences['fontSize'] = size
+ self.updateSampleText()
+
+
+ def onModelChanged(self, index):
+ self.updateAnkiFields()
+ self.dialogToProfile()
+
+
+ def onDeckChanged(self, index):
+ self.dialogToProfile()
+
+
+ def onFieldsChanged(self, item):
+ self.dialogToProfile()
+
+
+ def onProfileChanged(self, data):
+ self.profileToDialog()
+
+
+ def updateAnkiFields(self):
+ modelName = self.comboBoxModel.currentText()
+ fieldNames = self.anki.modelFieldNames(modelName) or []
+
+ profile, name = self.activeProfile()
+ fields = {} if profile is None else profile['fields']
+
+ self.setAnkiFields(fieldNames, fields)
+
+
+ def activeProfile(self):
+ name = 'vocab' if self.radioButtonVocab.isChecked() else 'kanji'
+ return self.profiles.get(name), name
+
+
+ def setActiveProfile(self, profile):
+ name = 'vocab' if self.radioButtonVocab.isChecked() else 'kanji'
+ self.profiles[name] = profile
diff --git a/yomi_base/reader.py b/yomi_base/reader.py
new file mode 100644
index 0000000..36fdd10
--- /dev/null
+++ b/yomi_base/reader.py
@@ -0,0 +1,604 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2013 Alex Yatskov
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+
+from PyQt4 import QtGui, QtCore
+import about
+import constants
+import gen.reader_ui
+import os
+import preferences
+import reader_util
+import updates
+
+
+class MainWindowReader(QtGui.QMainWindow, gen.reader_ui.Ui_MainWindowReader):
+ class State:
+ def __init__(self):
+ self.filename = u''
+ self.searchText = u''
+ self.kanjiDefs = []
+ self.vocabDefs = []
+ self.scanPosition = 0
+ self.searchPosition = 0
+
+
+ def __init__(self, parent, preferences, language, filename=None, anki=None, closed=None):
+ QtGui.QMainWindow.__init__(self, parent)
+ self.setupUi(self)
+
+ self.textContent.mouseMoveEvent = self.onContentMouseMove
+ self.textContent.mousePressEvent = self.onContentMousePress
+ self.dockAnki.setEnabled(anki is not None)
+
+ self.facts = []
+ self.anki = anki
+ self.closed = closed
+ self.language = language
+ self.preferences = preferences
+ self.state = self.State()
+ self.updates = updates.UpdateFinder()
+ self.zoom = 0
+
+ self.applyPreferences()
+ self.updateRecentFiles()
+ self.updateVocabDefs()
+ self.updateKanjiDefs()
+
+ if filename is not None:
+ self.openFile(filename)
+ elif self.preferences['rememberTextContent']:
+ self.textContent.setPlainText(self.preferences['textContent'])
+ elif self.preferences['loadRecentFile']:
+ filenames = self.preferences.recentFiles()
+ if len(filenames) > 0 and os.path.isfile(filenames[0]):
+ self.openFile(filenames[0])
+
+ self.actionAbout.triggered.connect(self.onActionAbout)
+ self.actionFind.triggered.connect(self.onActionFind)
+ self.actionFindNext.triggered.connect(self.onActionFindNext)
+ self.actionHomepage.triggered.connect(self.onActionHomepage)
+ self.actionKindleDeck.triggered.connect(self.onActionKindleDeck)
+ self.actionWordList.triggered.connect(self.onActionWordList)
+ self.actionOpen.triggered.connect(self.onActionOpen)
+ self.actionPreferences.triggered.connect(self.onActionPreferences)
+ self.actionToggleWrap.toggled.connect(self.onActionToggleWrap)
+ self.actionZoomIn.triggered.connect(self.onActionZoomIn)
+ self.actionZoomOut.triggered.connect(self.onActionZoomOut)
+ self.actionZoomReset.triggered.connect(self.onActionZoomReset)
+ self.dockAnki.visibilityChanged.connect(self.onVisibilityChanged)
+ self.dockKanji.visibilityChanged.connect(self.onVisibilityChanged)
+ self.dockVocab.visibilityChanged.connect(self.onVisibilityChanged)
+ self.listDefinitions.itemDoubleClicked.connect(self.onDefinitionDoubleClicked)
+ self.textKanjiDefs.anchorClicked.connect(self.onKanjiDefsAnchorClicked)
+ self.textKanjiSearch.returnPressed.connect(self.onKanjiDefSearchReturn)
+ self.textVocabDefs.anchorClicked.connect(self.onVocabDefsAnchorClicked)
+ self.textVocabSearch.returnPressed.connect(self.onVocabDefSearchReturn)
+ self.updates.updateResult.connect(self.onUpdaterSearchResult)
+
+ if self.preferences['checkForUpdates']:
+ self.updates.start()
+
+
+ def applyPreferences(self):
+ if self.preferences['windowState'] is not None:
+ self.restoreState(QtCore.QByteArray.fromBase64(self.preferences['windowState']))
+ if self.preferences['windowPosition'] is not None:
+ self.move(QtCore.QPoint(*self.preferences['windowPosition']))
+ if self.preferences['windowSize'] is not None:
+ self.resize(QtCore.QSize(*self.preferences['windowSize']))
+
+ self.comboTags.addItems(self.preferences['tags'])
+ self.applyPreferencesContent()
+
+ if self.preferences['firstRun']:
+ QtGui.QMessageBox.information(
+ self,
+ 'Yomichan',
+ 'This may be the first time you are running Yomichan.\n' \
+ 'Please take some time to configure this extension.'
+ )
+
+ self.onActionPreferences()
+
+
+ def applyPreferencesContent(self):
+ palette = self.textContent.palette()
+ palette.setColor(QtGui.QPalette.Base, QtGui.QColor(self.preferences['bgColor']))
+ palette.setColor(QtGui.QPalette.Text, QtGui.QColor(self.preferences['fgColor']))
+ self.textContent.setPalette(palette)
+
+ self.textContent.setReadOnly(not self.preferences['allowEditing'])
+ self.textContent.setAttribute(QtCore.Qt.WA_InputMethodEnabled)
+
+ font = self.textContent.font()
+ font.setFamily(self.preferences['fontFamily'])
+ font.setPointSize(self.preferences['fontSize'] + self.zoom)
+ self.textContent.setLineWrapMode(QtGui.QPlainTextEdit.WidgetWidth if self.preferences['wordWrap'] else QtGui.QPlainTextEdit.NoWrap)
+ self.textContent.setFont(font)
+
+ self.actionToggleWrap.setChecked(self.preferences['wordWrap'])
+
+
+ def closeEvent(self, event):
+ self.closeFile()
+ self.preferences['windowState'] = str(self.saveState().toBase64())
+ self.preferences.save()
+
+ if self.anki is not None:
+ self.anki.stopEditing()
+
+ if self.closed is not None:
+ self.closed()
+
+
+ def keyPressEvent(self, event):
+ if event.key() == QtCore.Qt.Key_Shift:
+ self.updateSampleFromPosition()
+ elif ord('0') <= event.key() <= ord('9'):
+ index = event.key() - ord('0') - 1
+ if index < 0:
+ index = 9
+ if event.modifiers() & QtCore.Qt.ShiftModifier:
+ if event.modifiers() & QtCore.Qt.ControlModifier:
+ self.executeKanjiCommand('addKanji', index)
+ else:
+ if event.modifiers() & QtCore.Qt.ControlModifier:
+ self.executeVocabCommand('addVocabExp', index)
+ if event.modifiers() & QtCore.Qt.AltModifier:
+ self.executeVocabCommand('addVocabReading', index)
+ elif event.key() == ord('[') and self.state.scanPosition > 0:
+ self.state.scanPosition -= 1
+ self.updateSampleFromPosition()
+ elif event.key() == ord(']') and self.state.scanPosition < len(self.textContent.toPlainText()) - 1:
+ self.state.scanPosition += 1
+ self.updateSampleFromPosition()
+
+
+ def dragEnterEvent(self, event):
+ if event.mimeData().hasUrls():
+ event.acceptProposedAction()
+
+
+ def dropEvent(self, event):
+ url = event.mimeData().urls()[0]
+ self.openFile(url.toLocalFile())
+
+
+ def moveEvent(self, event):
+ self.preferences['windowPosition'] = event.pos().x(), event.pos().y()
+
+
+ def resizeEvent(self, event):
+ self.preferences['windowSize'] = event.size().width(), event.size().height()
+
+
+ def onActionOpen(self):
+ filename = QtGui.QFileDialog.getOpenFileName(
+ parent=self,
+ caption='Select a file to open',
+ filter='Text files (*.txt);;All files (*.*)'
+ )
+ if filename:
+ self.openFile(filename)
+
+
+ def onActionKindleDeck(self):
+ filename = QtGui.QFileDialog.getOpenFileName(
+ parent=self,
+ caption='Select a Kindle deck to import',
+ filter='Deck files (*.db)'
+ )
+ if filename:
+ words = reader_util.extractKindleDeck(filename)
+ self.importWordList(words)
+
+
+ def onActionWordList(self):
+ filename = QtGui.QFileDialog.getOpenFileName(
+ parent=self,
+ caption='Select a word list file to import',
+ filter='Text files (*.txt);;All files (*.*)'
+ )
+ if filename:
+ words = reader_util.extractWordList(filename)
+ self.importWordList(words)
+
+
+ def onActionPreferences(self):
+ dialog = preferences.DialogPreferences(self, self.preferences, self.anki)
+ if dialog.exec_() == QtGui.QDialog.Accepted:
+ self.applyPreferencesContent()
+
+
+ def onActionAbout(self):
+ dialog = about.DialogAbout(self)
+ dialog.exec_()
+
+
+ def onActionZoomIn(self):
+ font = self.textContent.font()
+ if font.pointSize() < 72:
+ font.setPointSize(font.pointSize() + 1)
+ self.textContent.setFont(font)
+ self.zoom += 1
+
+
+ def onActionZoomOut(self):
+ font = self.textContent.font()
+ if font.pointSize() > 1:
+ font.setPointSize(font.pointSize() - 1)
+ self.textContent.setFont(font)
+ self.zoom -= 1
+
+
+ def onActionZoomReset(self):
+ if self.zoom:
+ font = self.textContent.font()
+ font.setPointSize(font.pointSize() - self.zoom)
+ self.textContent.setFont(font)
+ self.zoom = 0
+
+
+ def onActionFind(self):
+ searchText = self.state.searchText
+
+ cursor = self.textContent.textCursor()
+ if cursor.hasSelection():
+ searchText = cursor.selectedText()
+
+ searchText, ok = QtGui.QInputDialog.getText(self, 'Find', 'Search text:', text=searchText)
+ if searchText and ok:
+ self.findText(searchText)
+
+
+ def onActionFindNext(self):
+ if self.state.searchText:
+ self.findText(self.state.searchText)
+
+
+ def onActionToggleWrap(self, wrap):
+ self.preferences['wordWrap'] = wrap
+ mode = QtGui.QPlainTextEdit.WidgetWidth if self.preferences['wordWrap'] else QtGui.QPlainTextEdit.NoWrap
+ self.textContent.setLineWrapMode(mode)
+
+
+ def onActionHomepage(self):
+ url = QtCore.QUrl('https://foosoft.net/projects/yomichan')
+ QtGui.QDesktopServices().openUrl(url)
+
+
+ def onVocabDefsAnchorClicked(self, url):
+ command, index = unicode(url.toString()).split(':')
+ self.executeVocabCommand(command, int(index))
+
+
+ def onKanjiDefsAnchorClicked(self, url):
+ command, index = unicode(url.toString()).split(':')
+ self.executeKanjiCommand(command, int(index))
+
+
+ def onVocabDefSearchReturn(self):
+ text = unicode(self.textVocabSearch.text())
+ self.state.vocabDefs, length = self.language.findTerm(text, True)
+ self.updateVocabDefs()
+ if self.dockKanji.isVisible():
+ self.state.kanjiDefs = self.language.findKanji(text)
+ self.updateKanjiDefs()
+
+
+ def onKanjiDefSearchReturn(self):
+ text = unicode(self.textKanjiSearch.text())
+ self.state.kanjiDefs = self.language.findKanji(text)
+ self.updateKanjiDefs()
+
+
+ def onDefinitionDoubleClicked(self, item):
+ if self.anki is not None:
+ row = self.listDefinitions.row(item)
+ self.anki.browseNote(self.facts[row])
+
+
+ def onVisibilityChanged(self, visible):
+ self.actionToggleAnki.setChecked(self.dockAnki.isVisible())
+ self.actionToggleVocab.setChecked(self.dockVocab.isVisible())
+ self.actionToggleKanji.setChecked(self.dockKanji.isVisible())
+
+
+ def onUpdaterSearchResult(self, versions):
+ if versions['latest'] > constants.c['appVersion']:
+ dialog = updates.DialogUpdates(self, versions)
+ dialog.exec_()
+
+
+ def onContentMouseMove(self, event):
+ QtGui.QPlainTextEdit.mouseMoveEvent(self.textContent, event)
+ self.updateSampleMouseEvent(event)
+
+
+ def onContentMousePress(self, event):
+ QtGui.QPlainTextEdit.mousePressEvent(self.textContent, event)
+ self.updateSampleMouseEvent(event)
+
+
+ def openFile(self, filename):
+ try:
+ filename = unicode(filename)
+ with open(filename) as fp:
+ content = fp.read()
+ except IOError:
+ self.setStatus(u'Failed to load file {0}'.format(filename))
+ QtGui.QMessageBox.critical(self, 'Yomichan', 'Cannot open file for read')
+ return
+
+ self.closeFile()
+
+ self.state.filename = filename
+ self.state.scanPosition = self.preferences.filePosition(filename)
+ if self.state.scanPosition > len(content):
+ self.state.scanPosition = 0
+
+ self.updateRecentFile()
+ self.updateRecentFiles()
+
+ content, encoding = reader_util.decodeContent(content)
+ if self.preferences['stripReadings']:
+ content = reader_util.stripReadings(content)
+
+ self.textContent.setPlainText(content)
+ if self.state.scanPosition > 0:
+ cursor = self.textContent.textCursor()
+ cursor.setPosition(self.state.scanPosition)
+ self.textContent.setTextCursor(cursor)
+ self.textContent.centerCursor()
+
+ self.setStatus(u'Loaded file {0}'.format(filename))
+ self.setWindowTitle(u'Yomichan - {0} ({1})'.format(os.path.basename(filename), encoding))
+
+
+ def closeFile(self):
+ if self.preferences['rememberTextContent']:
+ self.preferences['textContent'] = unicode(self.textContent.toPlainText())
+
+ self.setWindowTitle('Yomichan')
+ self.textContent.setPlainText(u'')
+ self.updateRecentFile(False)
+ self.state = self.State()
+
+
+ def findText(self, text):
+ content = unicode(self.textContent.toPlainText())
+ index = content.find(unicode(text), self.state.searchPosition)
+
+ if index == -1:
+ wrap = self.state.searchPosition != 0
+ self.state.searchPosition = 0
+ if wrap:
+ self.findText(text)
+ else:
+ QtGui.QMessageBox.information(self, 'Yomichan', 'Search text not found')
+ else:
+ self.state.searchPosition = index + len(text)
+ cursor = self.textContent.textCursor()
+ cursor.setPosition(index, QtGui.QTextCursor.MoveAnchor)
+ cursor.setPosition(self.state.searchPosition, QtGui.QTextCursor.KeepAnchor)
+ self.textContent.setTextCursor(cursor)
+
+ self.state.searchText = text
+
+
+ def ankiAddFact(self, profile, markup):
+ if markup is None:
+ return False
+
+ if self.anki is None:
+ return False
+
+ profile = self.preferences['profiles'].get(profile)
+ if profile is None:
+ return False
+
+ fields = reader_util.formatFields(profile['fields'], markup)
+ tagsSplit = reader_util.splitTags(unicode(self.comboTags.currentText()))
+ tagsJoined = ' '.join(tagsSplit)
+
+ tagIndex = self.comboTags.findText(tagsJoined)
+ if tagIndex > 0:
+ self.comboTags.removeItem(tagIndex)
+ if tagIndex != 0:
+ self.comboTags.insertItem(0, tagsJoined)
+ self.preferences.updateFactTags(tagsJoined)
+
+ factId = self.anki.addNote(profile['deck'], profile['model'], fields, tagsSplit, None)
+ if factId is None:
+ return False
+
+ self.facts.append(factId)
+ self.listDefinitions.addItem(markup['summary'])
+ self.listDefinitions.setCurrentRow(self.listDefinitions.count() - 1)
+ self.setStatus(u'Added fact {0}; {1} new fact(s) total'.format(markup['summary'], len(self.facts)))
+
+ self.updateVocabDefs(scroll=True)
+ self.updateKanjiDefs(scroll=True)
+ return True
+
+
+ def ankiIsFactValid(self, profile, markup):
+ if markup is None:
+ return False
+
+ if self.anki is None:
+ return False
+
+ profile = self.preferences['profiles'].get(profile)
+ if profile is None:
+ return False
+
+ fields = reader_util.formatFields(profile['fields'], markup)
+ return self.anki.canAddNote(profile['deck'], profile['model'], fields)
+
+
+ def executeVocabCommand(self, command, index):
+ if index >= len(self.state.vocabDefs):
+ return
+
+ definition = self.state.vocabDefs[index]
+ if command == 'addVocabExp':
+ markup = reader_util.markupVocabExp(definition)
+ self.ankiAddFact('vocab', markup)
+ if command == 'addVocabReading':
+ markup = reader_util.markupVocabReading(definition)
+ self.ankiAddFact('vocab', markup)
+ elif command == 'copyVocabDef':
+ reader_util.copyVocabDef(definition)
+
+
+ def executeKanjiCommand(self, command, index):
+ if index >= len(self.state.kanjiDefs):
+ return
+
+ definition = self.state.kanjiDefs[index]
+ if command == 'addKanji':
+ markup = reader_util.markupKanji(definition)
+ self.ankiAddFact('kanji', markup)
+ elif command == 'copyKanjiDef':
+ reader_util.copyKanjiDef(definition)
+
+
+ def updateSampleMouseEvent(self, event):
+ cursor = self.textContent.cursorForPosition(event.pos())
+ self.state.scanPosition = cursor.position()
+ if event.buttons() & QtCore.Qt.MidButton or event.modifiers() & QtCore.Qt.ShiftModifier:
+ self.updateSampleFromPosition()
+
+
+ def updateSampleFromPosition(self):
+ samplePosStart = self.state.scanPosition
+ samplePosEnd = self.state.scanPosition + self.preferences['scanLength']
+
+ content = unicode(self.textContent.toPlainText())
+ contentSample = content[samplePosStart:samplePosEnd]
+ contentSampleFlat = contentSample.replace(u'\n', u'')
+
+ cursor = self.textContent.textCursor()
+
+ if len(contentSampleFlat) == 0:
+ cursor.clearSelection()
+ self.textContent.setTextCursor(cursor)
+ return
+
+ lengthMatched = 0
+ if self.dockVocab.isVisible():
+ self.state.vocabDefs, lengthMatched = self.language.findTerm(contentSampleFlat)
+ sentence = reader_util.findSentence(content, samplePosStart)
+ for definition in self.state.vocabDefs:
+ definition['sentence'] = sentence
+ self.updateVocabDefs()
+
+ if self.dockKanji.isVisible():
+ if lengthMatched == 0:
+ self.state.kanjiDefs = self.language.findKanji(contentSampleFlat[0])
+ if len(self.state.kanjiDefs) > 0:
+ lengthMatched = 1
+ else:
+ self.state.kanjiDefs = self.language.findKanji(contentSampleFlat[:lengthMatched])
+ self.updateKanjiDefs()
+
+ lengthSelect = 0
+ for c in contentSample:
+ if lengthMatched <= 0:
+ break
+ lengthSelect += 1
+ if c != u'\n':
+ lengthMatched -= 1
+
+ cursor.setPosition(samplePosStart, QtGui.QTextCursor.MoveAnchor)
+ cursor.setPosition(samplePosStart + lengthSelect, QtGui.QTextCursor.KeepAnchor)
+ self.textContent.setTextCursor(cursor)
+
+
+ def clearRecentFiles(self):
+ self.preferences.clearRecentFiles()
+ self.updateRecentFiles()
+
+
+ def updateRecentFiles(self):
+ self.menuOpenRecent.clear()
+
+ filenames = self.preferences.recentFiles()
+ if len(filenames) == 0:
+ return
+
+ for filename in filenames:
+ self.menuOpenRecent.addAction(filename, lambda f=filename: self.openFile(f))
+
+ self.menuOpenRecent.addSeparator()
+ self.menuOpenRecent.addAction('Clear file history', self.clearRecentFiles)
+
+
+ def updateRecentFile(self, addIfNeeded=True):
+ if self.state.filename:
+ if addIfNeeded or self.state.filename in self.preferences.recentFiles():
+ self.preferences.updateRecentFile(self.state.filename, self.state.scanPosition)
+
+
+ def updateDefs(self, defs, builder, control, **options):
+ scrollbar = control.verticalScrollBar()
+ position = scrollbar.sliderPosition()
+
+ html = builder(defs, self.ankiIsFactValid)
+ control.setHtml(html)
+
+ if options.get('scroll', False):
+ scrollbar.setSliderPosition(position)
+
+
+ def updateVocabDefs(self, **options):
+ self.updateDefs(
+ self.state.vocabDefs,
+ reader_util.buildVocabDefs,
+ self.textVocabDefs,
+ **options
+ )
+
+
+ def updateKanjiDefs(self, **options):
+ self.updateDefs(
+ self.state.kanjiDefs,
+ reader_util.buildKanjiDefs,
+ self.textKanjiDefs,
+ **options
+ )
+
+
+ def importWordList(self, words):
+ self.state.vocabDefs = []
+ self.state.kanjiDefs = []
+
+ for word in words:
+ if self.dockVocab.isVisible():
+ self.state.vocabDefs += self.language.dictionary.findTerm(word)
+
+ if self.dockKanji.isVisible():
+ self.state.kanjiDefs += self.language.findKanji(word)
+
+ self.updateVocabDefs(scroll=True)
+ self.updateKanjiDefs(scroll=True)
+
+
+ def setStatus(self, status):
+ self.statusBar.showMessage(status)
diff --git a/yomi_base/reader_util.py b/yomi_base/reader_util.py
new file mode 100644
index 0000000..0ff4fd9
--- /dev/null
+++ b/yomi_base/reader_util.py
@@ -0,0 +1,293 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2013 Alex Yatskov
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+
+from PyQt4 import QtGui
+import re
+import codecs
+import sqlite3
+
+
+def decodeContent(content):
+ encodings = ['utf-8', 'shift_jis', 'euc-jp', 'utf-16']
+ errors = {}
+
+ for encoding in encodings:
+ try:
+ return content.decode(encoding), encoding
+ except UnicodeDecodeError, e:
+ errors[encoding] = e[2]
+
+ encoding = sorted(errors, key=errors.get, reverse=True)[0]
+ return content.decode(encoding, 'replace'), encoding
+
+
+def stripReadings(content):
+ return re.sub(u'《[^》]+》', u'', content)
+
+
+def findSentence(content, position):
+ quotesFwd = {u'「': u'」', u'『': u'』', u"'": u"'", u'"': u'"'}
+ quotesBwd = {u'」': u'「', u'』': u'『', u"'": u"'", u'"': u'"'}
+ terminators = u'。..??!!'
+
+ quoteStack = []
+
+ start = 0
+ for i in xrange(position, start, -1):
+ c = content[i]
+
+ if not quoteStack and (c in terminators or c in quotesFwd or c == '\n'):
+ start = i + 1
+ break
+
+ if quoteStack and c == quoteStack[0]:
+ quoteStack.pop()
+ elif c in quotesBwd:
+ quoteStack.insert(0, quotesBwd[c])
+
+ quoteStack = []
+
+ end = len(content)
+ for i in xrange(position, end):
+ c = content[i]
+
+ if not quoteStack:
+ if c in terminators:
+ end = i + 1
+ break
+ elif c in quotesBwd:
+ end = i
+ break
+
+ if quoteStack and c == quoteStack[0]:
+ quoteStack.pop()
+ elif c in quotesFwd:
+ quoteStack.insert(0, quotesFwd[c])
+
+ return content[start:end].strip()
+
+
+def formatFields(fields, markup):
+ result = {}
+ for field, value in fields.items():
+ try:
+ result[field] = value.format(**markup)
+ except KeyError:
+ pass
+ except ValueError:
+ pass
+
+ return result
+
+
+def splitTags(tags):
+ return filter(lambda tag: tag.strip(), re.split('[;,\s]', tags))
+
+
+def markupVocabExp(definition):
+ if definition['reading']:
+ summary = u'{expression} [{reading}]'.format(**definition)
+ else:
+ summary = u'{expression}'.format(**definition)
+
+ return {
+ 'expression': definition['expression'],
+ 'reading': definition['reading'] or u'',
+ 'glossary': '; '.join(definition['glossary']),
+ 'sentence': definition.get('sentence'),
+ 'summary': summary
+ }
+
+
+def markupVocabReading(definition):
+ if definition['reading']:
+ return {
+ 'expression': definition['reading'],
+ 'reading': u'',
+ 'glossary': '; '.join(definition['glossary']),
+ 'sentence': definition.get('sentence'),
+ 'summary': definition['reading']
+ }
+
+
+def copyVocabDef(definition):
+ glossary = '; '.join(definition['glossary'])
+ if definition['reading']:
+ result = u'{0}\t{1}\t{2}\n'.format(definition['expression'], definition['reading'], glossary)
+ else:
+ result = u'{0}\t{1}\n'.format(definition['expression'], glossary)
+
+ QtGui.QApplication.clipboard().setText(result)
+
+
+def markupKanji(definition):
+ return {
+ 'character': definition['character'],
+ 'onyomi': ', '.join(definition['onyomi']),
+ 'kunyomi': ', '.join(definition['kunyomi']),
+ 'glossary': ', '.join(definition['glossary']),
+ 'summary': definition['character']
+ }
+
+
+def copyKanjiDef(definition):
+ result = u'{0}\t{1}\t{2}\t{3}'.format(
+ definition['character'],
+ ', '.join(definition['kunyomi']),
+ ', '.join(definition['onyomi']),
+ ', '.join(definition['glossary'])
+ )
+
+ QtGui.QApplication.clipboard().setText(result)
+
+
+def buildDefHeader():
+ palette = QtGui.QApplication.palette()
+ toolTipBg = palette.color(QtGui.QPalette.Window).name()
+ toolTipFg = palette.color(QtGui.QPalette.WindowText).name()
+
+ return u'''
+ '''.format(toolTipBg, toolTipFg)
+
+
+def buildDefFooter():
+ return ''
+
+
+def buildEmpty():
+ return u'''
+ No definitions to display.
+ Mouse over text with the middle mouse button or shift key pressed to search.
+ You can also also input terms in the search box below.'''
+
+
+def buildVocabDef(definition, index, query):
+ reading = u''
+ if definition['reading']:
+ reading = u'[{0}]
'.format(definition['reading'])
+
+ rules = u''
+ if definition.get('rules'):
+ rules = ' < '.join(definition['rules'])
+ rules = '({0})
'.format(rules)
+
+ links = ''.format(index)
+ if query is not None:
+ if query('vocab', markupVocabExp(definition)):
+ links += ''.format(index)
+ if query('vocab', markupVocabReading(definition)):
+ links += ''.format(index)
+
+ glossary = u'
'
+ for g in definition['glossary']:
+ glossary += u'- {0}
'.format(g)
+ glossary += u'
'
+
+ expression = u''
+ if 'P' in definition['tags']:
+ expression += u'{}'.format(definition['expression'])
+ else:
+ expression += definition['expression']
+ expression += u''
+
+ html = u'''
+ {links}
+ {expression}
+ {reading}
+ {rules}
+ {glossary}
+
'''.format(
+ links = links,
+ expression = expression,
+ reading = reading,
+ glossary = glossary,
+ rules = rules
+ )
+
+ return html
+
+
+def buildVocabDefs(definitions, query):
+ html = buildDefHeader()
+ if len(definitions) > 0:
+ for i, definition in enumerate(definitions):
+ html += buildVocabDef(definition, i, query)
+ else:
+ html += buildEmpty()
+
+ return html + buildDefFooter()
+
+
+def buildKanjiDef(definition, index, query):
+ links = ''.format(index)
+ if query is not None and query('kanji', markupKanji(definition)):
+ links += ''.format(index)
+
+ readings = ', '.join(definition['kunyomi'] + definition['onyomi'])
+ glossary = ', '.join(definition['glossary'])
+
+ html = u'''
+ {links}
+ {expression}
+ [{reading}]
+ {glossary}
+
'''.format(
+ links = links,
+ expression = definition['character'],
+ reading = readings,
+ glossary = glossary
+ )
+
+ return html
+
+
+def buildKanjiDefs(definitions, query):
+ html = buildDefHeader()
+
+ if len(definitions) > 0:
+ for i, definition in enumerate(definitions):
+ html += buildKanjiDef(definition, i, query)
+ else:
+ html += buildEmpty()
+
+ return html + buildDefFooter()
+
+
+def extractKindleDeck(filename):
+ words = []
+
+ try:
+ with sqlite3.connect(unicode(filename)) as db:
+ for row in db.execute('select word from WORDS'):
+ words.append(row[0])
+ except sqlite3.OperationalError:
+ pass
+
+ return words
+
+
+def extractWordList(filename):
+ words = []
+
+ with codecs.open(unicode(filename), 'rb', 'utf-8') as fp:
+ words = re.split('[;,\s]', fp.read())
+
+ return filter(None, words)
diff --git a/yomi_base/updates.py b/yomi_base/updates.py
new file mode 100644
index 0000000..57a6970
--- /dev/null
+++ b/yomi_base/updates.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2013 Alex Yatskov
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+
+from PyQt4 import QtCore, QtGui
+import constants
+import gen.updates_ui
+import json
+import urllib2
+
+
+class DialogUpdates(QtGui.QDialog, gen.updates_ui.Ui_DialogUpdates):
+ def __init__(self, parent, versions):
+ QtGui.QDialog.__init__(self, parent)
+ self.setupUi(self)
+
+ self.updateHtml(versions)
+ self.labelUpdates.setText(
+ unicode(self.labelUpdates.text()).format(
+ constants.c['appVersion'],
+ versions['latest']
+ )
+ )
+
+
+ def updateHtml(self, versions):
+ html = ''
+
+ for update in versions['updates']:
+ version = update.get('version')
+ if version > constants.c['appVersion']:
+ html += 'Version {0}'.format(version)
+ html += ''
+ for feature in update['features']:
+ html += '- {0}
'.format(feature)
+ html += '
'
+
+ self.textBrowser.setHtml(html)
+
+
+class UpdateFinder(QtCore.QThread):
+ updateResult = QtCore.pyqtSignal(dict)
+
+ def run(self):
+ latest = constants.c['appVersion']
+ updates = []
+
+ try:
+ fp = urllib2.urlopen('https://foosoft.net/projects/yomichan/dl/updates.json')
+ updates = json.loads(fp.read())
+ fp.close()
+
+ for update in updates:
+ latest = max(latest, update.get('version'))
+ except:
+ pass
+ finally:
+ self.updateResult.emit({'latest': latest, 'updates': updates})
diff --git a/yomichan.py b/yomichan.py
new file mode 100755
index 0000000..8df5bbd
--- /dev/null
+++ b/yomichan.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2013 Alex Yatskov
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+
+from PyQt4 import QtGui
+from yomi_base import japanese
+from yomi_base.anki_connect import AnkiConnect
+from yomi_base.preference_data import Preferences
+from yomi_base.reader import MainWindowReader
+import sys
+
+
+class Yomichan:
+ def __init__(self):
+ self.language = japanese.initLanguage()
+
+ self.preferences = Preferences()
+ self.preferences.load()
+
+
+class YomichanPlugin(Yomichan):
+ def __init__(self):
+ Yomichan.__init__(self)
+
+ self.toolIconVisible = False
+ self.window = None
+ self.anki = anki_bridge.Anki()
+ self.parent = self.anki.window()
+ self.ankiConnect = AnkiConnect(self.anki, self.preferences)
+
+ separator = QtGui.QAction(self.parent)
+ separator.setSeparator(True)
+ self.anki.addUiAction(separator)
+
+ action = QtGui.QAction(QtGui.QIcon(':/img/img/icon_logo_32.png'), '&Yomichan...', self.parent)
+ action.setIconVisibleInMenu(True)
+ action.setShortcut('Ctrl+Y')
+ action.triggered.connect(self.onShowRequest)
+ self.anki.addUiAction(action)
+
+
+ def onShowRequest(self):
+ if self.window:
+ self.window.setVisible(True)
+ self.window.activateWindow()
+ else:
+ self.window = MainWindowReader(
+ self.parent,
+ self.preferences,
+ self.language,
+ None,
+ self.anki,
+ self.onWindowClose
+ )
+ self.window.show()
+
+
+ def onWindowClose(self):
+ self.window = None
+
+
+class YomichanStandalone(Yomichan):
+ def __init__(self):
+ Yomichan.__init__(self)
+
+ self.application = QtGui.QApplication(sys.argv)
+ self.window = MainWindowReader(
+ None,
+ self.preferences,
+ self.language,
+ filename=sys.argv[1] if len(sys.argv) >= 2 else None
+ )
+
+ self.window.show()
+ self.application.exec_()
+
+
+if __name__ == '__main__':
+ instance = YomichanStandalone()
+else:
+ from yomi_base import anki_bridge
+ instance = YomichanPlugin()