diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b3b3cf3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ +Copyright 2011-2019 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 . diff --git a/yomi_base/japanese/dictionary.db b/yomi_base/japanese/dictionary.db deleted file mode 100644 index a5335f8..0000000 --- a/yomi_base/japanese/dictionary.db +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c7716fdf1e98bc42f89e2cb2e8a7511bd050b1b781fe185d8a84b3612fa192f2 -size 21696512 diff --git a/yomi_base/japanese/dictionary.py b/yomi_base/japanese/dictionary.py deleted file mode 100644 index e94675a..0000000 --- a/yomi_base/japanese/dictionary.py +++ /dev/null @@ -1,115 +0,0 @@ -# -*- 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 . - - -import operator -import sqlite3 - - -class Dictionary: - def __init__(self, filename, index=True): - self.db = sqlite3.connect(filename) - self.indices = set() - - - def findTerm(self, text, wildcards=False): - self.requireIndex('Vocab', 'expression') - self.requireIndex('Vocab', 'reading') - self.requireIndex('VocabGloss', 'vocabId') - - cursor = self.db.cursor() - - definitions = [] - cursor.execute('SELECT * FROM Vocab WHERE expression {0} ? OR reading=?'.format('LIKE' if wildcards else '='), (text, text)) - for vocabId, expression, reading, tags in cursor.fetchall(): - tags = tags.split() - - cursor.execute('SELECT glossary From VocabGloss WHERE vocabId=?', (vocabId,)) - glossary = map(operator.itemgetter(0), cursor) - - # - # TODO: Handle addons through data. - # - - addons = [] - for tag in tags: - if tag.startswith('v5') and tag != 'v5': - addons.append('v5') - elif tag.startswith('vs-'): - addons.append('vs') - - definitions.append({ - 'id': vocabId, - 'expression': expression, - 'reading': reading, - 'glossary': glossary, - 'tags': tags + addons, - 'addons': addons - }) - - return definitions - - - def findKanji(self, text): - assert len(text) == 1 - - self.requireIndex('Kanji', 'character') - self.requireIndex('KanjiGloss', 'kanjiId') - - cursor = self.db.cursor() - - cursor.execute('SELECT * FROM Kanji WHERE character=? LIMIT 1', text) - query = cursor.fetchone() - if query is None: - return - - kanjiId, character, kunyomi, onyomi = query - cursor.execute('SELECT glossary From KanjiGloss WHERE kanjiId=?', (kanjiId,)) - glossary = map(operator.itemgetter(0), cursor) - - return { - 'id': kanjiId, - 'character': character, - 'kunyomi': [] if kunyomi is None else kunyomi.split(), - 'onyomi': [] if onyomi is None else onyomi.split(), - 'glossary': glossary - } - - - def requireIndex(self, table, column): - name = 'index_{0}_{1}'.format(table, column) - if not self.hasIndex(name): - self.buildIndex(name, table, column) - - - def buildIndex(self, name, table, column): - cursor = self.db.cursor() - cursor.execute('CREATE INDEX {0} ON {1}({2})'.format(name, table, column)) - self.db.commit() - - - def hasIndex(self, name): - if name in self.indices: - return True - - cursor = self.db.cursor() - cursor.execute('SELECT * FROM sqlite_master WHERE name=?', (name,)) - if len(cursor.fetchall()) == 0: - return False - - self.indices.update([name]) - return True diff --git a/yomi_base/japanese/translate.py b/yomi_base/japanese/translate.py deleted file mode 100644 index db38c98..0000000 --- a/yomi_base/japanese/translate.py +++ /dev/null @@ -1,96 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2013 Alex Yatskov -# This module is based on Rikaichan code written by Jonathan Zarate -# -# 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 . - - -import re - - -class Translator: - def __init__(self, deinflector, dictionary): - self.deinflector = deinflector - self.dictionary = dictionary - - - def findTerm(self, text, wildcards=False): - if wildcards: - text = re.sub(u'[\**]', u'%', text) - text = re.sub(u'[\??]', u'_', text) - - groups = {} - for i in xrange(len(text), 0, -1): - term = text[:i] - - dfs = self.deinflector.deinflect(term, lambda term: [d['tags'] for d in self.dictionary.findTerm(term)]) - if dfs is None: - continue - - for df in dfs: - self.processTerm(groups, **df) - - definitions = groups.values() - definitions = sorted( - definitions, - reverse=True, - key=lambda d: ( - len(d['source']), - 'P' in d['tags'], - -len(d['rules']), - d['expression'] - ) - ) - - length = 0 - for result in definitions: - length = max(length, len(result['source'])) - - return definitions, length - - - def findKanji(self, text): - processed = {} - results = [] - for c in text: - if c not in processed: - match = self.dictionary.findKanji(c) - if match is not None: - results.append(match) - processed[c] = match - - return results - - - def processTerm(self, groups, source, tags, rules=[], root='', wildcards=False): - for entry in self.dictionary.findTerm(root, wildcards): - if entry['id'] in groups: - continue - - matched = len(tags) == 0 - for tag in tags: - if tag in entry['tags']: - matched = True - break - - if matched: - groups[entry['id']] = { - 'expression': entry['expression'], - 'reading': entry['reading'], - 'glossary': entry['glossary'], - 'tags': entry['tags'], - 'source': source, - 'rules': rules - } diff --git a/yomi_base/preference_data.py b/yomi_base/preference_data.py deleted file mode 100644 index 666b8e4..0000000 --- a/yomi_base/preference_data.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- 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 . - - -import codecs -import json -import operator -import os - - -class Preferences(object): - def __init__(self): - self.filename = os.path.expanduser(u'~/.yomichan.json') - self.defaults = os.path.join(os.path.dirname(__file__), u'defaults.json') - self.settings = {} - - - def __getitem__(self, name): - return self.settings.get(name) - - - def __setitem__(self, name, value): - self.settings[name] = value - - - def load(self): - with codecs.open(self.defaults, 'rb', 'utf-8') as fp: - self.settings = json.load(fp) - - try: - if os.path.exists(self.filename): - with codecs.open(self.filename, 'rb', 'utf-8') as fp: - self.settings.update(json.load(fp)) - except ValueError: - pass - - - def save(self): - with codecs.open(self.filename, 'wb', 'utf-8') as fp: - json.dump(self.settings, fp, indent=4, sort_keys=True) - - - def filePosition(self, filename): - matches = filter(lambda f: f['path'] == filename, self['recentFiles']) - return 0 if len(matches) == 0 else matches[0]['position'] - - - def recentFiles(self): - return map(operator.itemgetter('path'), self['recentFiles']) - - - def updateFactTags(self, tags): - if tags in self['tags']: - self['tags'].remove(tags) - self['tags'].insert(0, tags) - - - def updateRecentFile(self, filename, position): - self['recentFiles'] = filter(lambda f: f['path'] != filename, self['recentFiles']) - self['recentFiles'].insert(0, {'path': filename, 'position': position}) - - - def clearRecentFiles(self): - self['recentFiles'] = [] diff --git a/yomi_base/preferences.py b/yomi_base/preferences.py deleted file mode 100644 index e28960f..0000000 --- a/yomi_base/preferences.py +++ /dev/null @@ -1,226 +0,0 @@ -# -*- 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 copy -import gen.preferences_ui - - -class DialogPreferences(QtGui.QDialog, gen.preferences_ui.Ui_DialogPreferences): - def __init__(self, parent, preferences, anki): - QtGui.QDialog.__init__(self, parent) - self.setupUi(self) - - self.accepted.connect(self.onAccept) - self.buttonColorBg.clicked.connect(self.onButtonColorBgClicked) - self.buttonColorFg.clicked.connect(self.onButtonColorFgClicked) - self.comboBoxDeck.currentIndexChanged.connect(self.onDeckChanged) - self.comboBoxModel.currentIndexChanged.connect(self.onModelChanged) - self.comboFontFamily.currentFontChanged.connect(self.onFontFamilyChanged) - self.radioButtonKanji.toggled.connect(self.onProfileChanged) - self.radioButtonVocab.toggled.connect(self.onProfileChanged) - self.spinFontSize.valueChanged.connect(self.onFontSizeChanged) - self.tableFields.itemChanged.connect(self.onFieldsChanged) - - self.preferences = preferences - self.anki = anki - - self.dataToDialog() - - - def dataToDialog(self): - self.checkCheckForUpdates.setChecked(self.preferences['checkForUpdates']) - self.checkRememberTextContent.setChecked(self.preferences['rememberTextContent']) - self.checkAllowEditing.setChecked(self.preferences['allowEditing']) - self.checkLoadRecentFile.setChecked(self.preferences['loadRecentFile']) - self.checkStripReadings.setChecked(self.preferences['stripReadings']) - self.spinScanLength.setValue(self.preferences['scanLength']) - self.checkEnableAnkiConnect.setChecked(self.preferences['enableAnkiConnect']) - - self.updateSampleText() - font = self.textSample.font() - self.comboFontFamily.setCurrentFont(font) - self.spinFontSize.setValue(font.pointSize()) - - if self.anki is not None: - self.tabAnki.setEnabled(True) - self.profiles = copy.deepcopy(self.preferences['profiles']) - self.profileToDialog() - - - def dialogToData(self): - self.preferences['checkForUpdates'] = self.checkCheckForUpdates.isChecked() - self.preferences['rememberTextContent'] = self.checkRememberTextContent.isChecked() - self.preferences['allowEditing'] = self.checkAllowEditing.isChecked() - self.preferences['loadRecentFile'] = self.checkLoadRecentFile.isChecked() - self.preferences['scanLength'] = self.spinScanLength.value() - self.preferences['stripReadings'] = self.checkStripReadings.isChecked() - self.preferences['enableAnkiConnect'] = self.checkEnableAnkiConnect.isChecked() - self.preferences['firstRun'] = False - - if self.anki is not None: - self.dialogToProfile() - self.preferences['profiles'] = self.profiles - - - def dialogToProfile(self): - self.setActiveProfile({ - 'deck': unicode(self.comboBoxDeck.currentText()), - 'model': unicode(self.comboBoxModel.currentText()), - 'fields': self.ankiFields() - }) - - - def profileToDialog(self): - profile, name = self.activeProfile() - - deck = u'' if profile is None else profile['deck'] - model = u'' if profile is None else profile['model'] - - self.comboBoxDeck.blockSignals(True) - self.comboBoxDeck.clear() - self.comboBoxDeck.addItems(self.anki.deckNames()) - self.comboBoxDeck.setCurrentIndex(self.comboBoxDeck.findText(deck)) - self.comboBoxDeck.blockSignals(False) - - self.comboBoxModel.blockSignals(True) - self.comboBoxModel.clear() - self.comboBoxModel.addItems(self.anki.modelNames()) - self.comboBoxModel.setCurrentIndex(self.comboBoxModel.findText(model)) - self.comboBoxModel.blockSignals(False) - - allowedTags = { - 'vocab': ['expression', 'reading', 'glossary', 'sentence'], - 'kanji': ['character', 'onyomi', 'kunyomi', 'glossary'], - }[name] - - allowedTags = map(lambda t: '{' + t + '}', 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 deleted file mode 100644 index 36fdd10..0000000 --- a/yomi_base/reader.py +++ /dev/null @@ -1,604 +0,0 @@ -# -*- 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 deleted file mode 100644 index 0ff4fd9..0000000 --- a/yomi_base/reader_util.py +++ /dev/null @@ -1,293 +0,0 @@ -# -*- 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'
  1. {0}
  2. '.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 deleted file mode 100644 index 57a6970..0000000 --- a/yomi_base/updates.py +++ /dev/null @@ -1,72 +0,0 @@ -# -*- 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 deleted file mode 100755 index 8df5bbd..0000000 --- a/yomichan.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/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()