1
yomichan-anki/yomi_base/reader.py

605 lines
21 KiB
Python
Raw Normal View History

2011-08-28 18:01:32 +00:00
# -*- coding: utf-8 -*-
2011-10-27 15:18:57 +00:00
# Copyright (C) 2013 Alex Yatskov
2011-08-28 18:01:32 +00:00
#
# 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 <http://www.gnu.org/licenses/>.
2012-12-25 20:51:46 +00:00
from PyQt4 import QtGui, QtCore
import about
import constants
import gen.reader_ui
import os
import preferences
2011-08-28 18:01:32 +00:00
import reader_util
import updates
2011-08-28 18:01:32 +00:00
class MainWindowReader(QtGui.QMainWindow, gen.reader_ui.Ui_MainWindowReader):
2011-08-28 18:01:32 +00:00
class State:
def __init__(self):
2016-05-18 05:12:05 +00:00
self.filename = u''
self.searchText = u''
2016-05-02 01:42:17 +00:00
self.kanjiDefs = []
2016-05-08 03:24:59 +00:00
self.vocabDefs = []
2016-05-02 01:42:17 +00:00
self.scanPosition = 0
2011-08-28 18:01:32 +00:00
self.searchPosition = 0
2016-05-02 03:43:09 +00:00
def __init__(self, parent, preferences, language, filename=None, anki=None, closed=None):
2011-08-28 18:01:32 +00:00
QtGui.QMainWindow.__init__(self, parent)
2012-12-25 20:51:46 +00:00
self.setupUi(self)
2011-08-28 18:01:32 +00:00
2016-05-02 01:42:17 +00:00
self.textContent.mouseMoveEvent = self.onContentMouseMove
2011-08-28 18:01:32 +00:00
self.textContent.mousePressEvent = self.onContentMousePress
self.dockAnki.setEnabled(anki is not None)
2011-08-28 18:01:32 +00:00
2016-05-02 01:42:17 +00:00
self.facts = []
self.anki = anki
self.closed = closed
self.language = language
self.preferences = preferences
2016-05-02 01:42:17 +00:00
self.state = self.State()
self.updates = updates.UpdateFinder()
self.zoom = 0
2011-08-28 18:01:32 +00:00
self.applyPreferences()
self.updateRecentFiles()
self.updateVocabDefs()
self.updateKanjiDefs()
2011-08-28 18:01:32 +00:00
if filename is not None:
2011-08-28 18:01:32 +00:00
self.openFile(filename)
elif self.preferences['rememberTextContent']:
self.textContent.setPlainText(self.preferences['textContent'])
elif self.preferences['loadRecentFile']:
2011-08-28 18:01:32 +00:00
filenames = self.preferences.recentFiles()
if len(filenames) > 0 and os.path.isfile(filenames[0]):
2011-08-28 18:01:32 +00:00
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)
2011-10-27 14:24:55 +00:00
self.actionOpen.triggered.connect(self.onActionOpen)
self.actionPreferences.triggered.connect(self.onActionPreferences)
self.actionToggleWrap.toggled.connect(self.onActionToggleWrap)
2011-10-27 14:24:55 +00:00
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)
2011-08-28 18:01:32 +00:00
if self.preferences['checkForUpdates']:
self.updates.start()
2011-10-27 15:18:57 +00:00
2011-08-28 18:01:32 +00:00
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'])
2011-08-28 18:01:32 +00:00
self.applyPreferencesContent()
if self.preferences['firstRun']:
QtGui.QMessageBox.information(
self,
'Yomichan',
2016-05-08 03:24:59 +00:00
'This may be the first time you are running Yomichan.\n' \
'Please take some time to configure this extension.'
)
self.onActionPreferences()
2011-08-28 18:01:32 +00:00
2011-08-28 18:01:32 +00:00
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']))
2011-08-28 18:01:32 +00:00
self.textContent.setPalette(palette)
self.textContent.setReadOnly(not self.preferences['allowEditing'])
self.textContent.setAttribute(QtCore.Qt.WA_InputMethodEnabled)
2011-08-28 18:01:32 +00:00
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)
2011-08-28 18:01:32 +00:00
self.textContent.setFont(font)
self.actionToggleWrap.setChecked(self.preferences['wordWrap'])
2011-08-28 18:01:32 +00:00
def closeEvent(self, event):
self.closeFile()
self.preferences['windowState'] = str(self.saveState().toBase64())
2011-08-28 18:01:32 +00:00
self.preferences.save()
2012-12-25 02:53:41 +00:00
if self.anki is not None:
self.anki.stopEditing()
if self.closed is not None:
2011-08-28 18:01:32 +00:00
self.closed()
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Shift:
2011-08-28 18:01:32 +00:00
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()
2011-08-28 18:01:32 +00:00
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()
2011-08-28 18:01:32 +00:00
def resizeEvent(self, event):
self.preferences['windowSize'] = event.size().width(), event.size().height()
2011-08-28 18:01:32 +00:00
def onActionOpen(self):
filename = QtGui.QFileDialog.getOpenFileName(
parent=self,
caption='Select a file to open',
filter='Text files (*.txt);;All files (*.*)'
2011-08-28 18:01:32 +00:00
)
2012-12-25 22:21:10 +00:00
if filename:
2011-08-28 18:01:32 +00:00
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)
2011-08-28 18:01:32 +00:00
def onActionPreferences(self):
dialog = preferences.DialogPreferences(self, self.preferences, self.anki)
2011-08-28 18:01:32 +00:00
if dialog.exec_() == QtGui.QDialog.Accepted:
self.applyPreferencesContent()
def onActionAbout(self):
dialog = about.DialogAbout(self)
2011-08-28 18:01:32 +00:00
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
2016-05-02 03:58:55 +00:00
mode = QtGui.QPlainTextEdit.WidgetWidth if self.preferences['wordWrap'] else QtGui.QPlainTextEdit.NoWrap
self.textContent.setLineWrapMode(mode)
2011-08-28 18:01:32 +00:00
def onActionHomepage(self):
url = QtCore.QUrl('https://foosoft.net/projects/yomichan')
2011-08-28 18:01:32 +00:00
QtGui.QDesktopServices().openUrl(url)
def onVocabDefsAnchorClicked(self, url):
2011-08-28 18:01:32 +00:00
command, index = unicode(url.toString()).split(':')
self.executeVocabCommand(command, int(index))
2011-08-28 18:01:32 +00:00
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():
2016-05-18 05:12:05 +00:00
self.state.kanjiDefs = self.language.findKanji(text)
self.updateKanjiDefs()
def onKanjiDefSearchReturn(self):
text = unicode(self.textKanjiSearch.text())
2016-05-18 05:12:05 +00:00
self.state.kanjiDefs = self.language.findKanji(text)
self.updateKanjiDefs()
2011-08-28 18:01:32 +00:00
def onDefinitionDoubleClicked(self, item):
2012-12-25 02:58:39 +00:00
if self.anki is not None:
row = self.listDefinitions.row(item)
self.anki.browseNote(self.facts[row])
2011-08-28 18:01:32 +00:00
2011-10-08 03:04:46 +00:00
def onVisibilityChanged(self, visible):
self.actionToggleAnki.setChecked(self.dockAnki.isVisible())
self.actionToggleVocab.setChecked(self.dockVocab.isVisible())
self.actionToggleKanji.setChecked(self.dockKanji.isVisible())
2011-10-08 03:04:46 +00:00
def onUpdaterSearchResult(self, versions):
if versions['latest'] > constants.c['appVersion']:
dialog = updates.DialogUpdates(self, versions)
dialog.exec_()
2011-08-28 18:01:32 +00:00
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()
2011-08-28 18:01:32 +00:00
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)
2011-08-28 18:01:32 +00:00
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))
2011-11-19 17:19:17 +00:00
2012-12-25 02:53:41 +00:00
2011-08-28 18:01:32 +00:00
def closeFile(self):
if self.preferences['rememberTextContent']:
self.preferences['textContent'] = unicode(self.textContent.toPlainText())
2011-08-28 18:01:32 +00:00
self.setWindowTitle('Yomichan')
2016-05-18 05:12:05 +00:00
self.textContent.setPlainText(u'')
2011-08-28 18:01:32 +00:00
self.updateRecentFile(False)
self.state = self.State()
def findText(self, text):
content = unicode(self.textContent.toPlainText())
2016-05-02 03:43:09 +00:00
index = content.find(unicode(text), self.state.searchPosition)
2011-08-28 18:01:32 +00:00
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
2012-12-25 02:53:41 +00:00
if self.anki is None:
2011-08-28 18:01:32 +00:00
return False
profile = self.preferences['profiles'].get(profile)
if profile is None:
return False
2016-05-02 03:43:09 +00:00
fields = reader_util.formatFields(profile['fields'], markup)
tagsSplit = reader_util.splitTags(unicode(self.comboTags.currentText()))
2012-12-24 17:24:02 +00:00
tagsJoined = ' '.join(tagsSplit)
tagIndex = self.comboTags.findText(tagsJoined)
2011-08-28 18:01:32 +00:00
if tagIndex > 0:
self.comboTags.removeItem(tagIndex)
if tagIndex != 0:
2012-12-24 17:24:02 +00:00
self.comboTags.insertItem(0, tagsJoined)
self.preferences.updateFactTags(tagsJoined)
2011-08-28 18:01:32 +00:00
factId = self.anki.addNote(profile['deck'], profile['model'], fields, tagsSplit)
2012-12-25 02:53:41 +00:00
if factId is None:
2011-08-28 18:01:32 +00:00
return False
self.facts.append(factId)
self.listDefinitions.addItem(markup['summary'])
2011-08-28 18:01:32 +00:00
self.listDefinitions.setCurrentRow(self.listDefinitions.count() - 1)
self.setStatus(u'Added fact {0}; {1} new fact(s) total'.format(markup['summary'], len(self.facts)))
2011-08-28 18:01:32 +00:00
self.updateVocabDefs(scroll=True)
self.updateKanjiDefs(scroll=True)
2011-08-28 18:01:32 +00:00
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)
2011-08-28 18:01:32 +00:00
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)
2011-08-28 18:01:32 +00:00
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:
2011-08-28 18:01:32 +00:00
self.updateSampleFromPosition()
def updateSampleFromPosition(self):
samplePosStart = self.state.scanPosition
2016-05-02 03:43:09 +00:00
samplePosEnd = self.state.scanPosition + self.preferences['scanLength']
2011-08-28 18:01:32 +00:00
2016-05-02 03:43:09 +00:00
content = unicode(self.textContent.toPlainText())
contentSample = content[samplePosStart:samplePosEnd]
2016-05-18 05:12:05 +00:00
contentSampleFlat = contentSample.replace(u'\n', u'')
2011-08-28 18:01:32 +00:00
2016-05-02 03:43:09 +00:00
cursor = self.textContent.textCursor()
2016-05-29 23:18:07 +00:00
if len(contentSampleFlat) == 0:
2011-08-28 18:01:32 +00:00
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:
2016-05-18 05:12:05 +00:00
self.state.kanjiDefs = self.language.findKanji(contentSampleFlat[0])
if len(self.state.kanjiDefs) > 0:
lengthMatched = 1
else:
2016-05-18 05:12:05 +00:00
self.state.kanjiDefs = self.language.findKanji(contentSampleFlat[:lengthMatched])
self.updateKanjiDefs()
2011-08-28 18:01:32 +00:00
lengthSelect = 0
for c in contentSample:
if lengthMatched <= 0:
break
lengthSelect += 1
if c != u'\n':
lengthMatched -= 1
2011-08-28 18:01:32 +00:00
cursor.setPosition(samplePosStart, QtGui.QTextCursor.MoveAnchor)
cursor.setPosition(samplePosStart + lengthSelect, QtGui.QTextCursor.KeepAnchor)
self.textContent.setTextCursor(cursor)
2011-11-19 17:19:17 +00:00
2011-08-28 18:01:32 +00:00
def clearRecentFiles(self):
self.preferences.clearRecentFiles()
2011-08-28 18:01:32 +00:00
self.updateRecentFiles()
def updateRecentFiles(self):
self.menuOpenRecent.clear()
filenames = self.preferences.recentFiles()
if len(filenames) == 0:
2011-08-28 18:01:32 +00:00
return
for filename in filenames:
self.menuOpenRecent.addAction(filename, lambda f=filename: self.openFile(f))
2011-08-28 18:01:32 +00:00
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()
2016-05-02 03:43:09 +00:00
position = scrollbar.sliderPosition()
2016-05-02 03:58:55 +00:00
html = builder(defs, self.ankiIsFactValid)
control.setHtml(html)
2011-08-28 18:01:32 +00:00
if options.get('scroll', False):
scrollbar.setSliderPosition(position)
2011-08-28 18:01:32 +00:00
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):
2016-05-02 01:42:17 +00:00
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():
2016-05-18 05:12:05 +00:00
self.state.kanjiDefs += self.language.findKanji(word)
2016-05-08 03:24:59 +00:00
self.updateVocabDefs(scroll=True)
self.updateKanjiDefs(scroll=True)
2011-08-28 18:01:32 +00:00
def setStatus(self, status):
self.statusBar.showMessage(status)