1
yomichan-anki/yomi_base/reader.py
Alex Yatskov f8dda8d21a Better handling of spaces and line breaks in files
Former-commit-id: a3e47eb122cfbfc847f7aa324f0f9f7486f08d04
2013-11-16 12:08:08 -08:00

505 lines
18 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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 <http://www.gnu.org/licenses/>.
from PyQt4 import QtGui, QtCore
import about
import constants
import gen.reader_ui
import os
import preferences
import reader_util
import tarfile
import update
class MainWindowReader(QtGui.QMainWindow, gen.reader_ui.Ui_MainWindowReader):
class State:
def __init__(self):
self.filename = unicode()
self.kanjiDefs = list()
self.scanPosition = 0
self.searchPosition = 0
self.searchText = unicode()
self.vocabDefs = list()
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 = list()
self.anki = anki
self.closed = closed
self.language = language
self.preferences = preferences
self.state = self.State()
self.updater = update.UpdateFinder()
self.zoom = 0
self.applyPreferences()
self.updateRecentFiles()
self.updateVocabDefs()
self.updateKanjiDefs()
if filename is not None:
self.openFile(filename)
elif self.preferences['loadRecentFile']:
filenames = self.preferences.recentFiles()
if len(filenames) > 0:
self.openFile(filenames[0])
self.actionAbout.triggered.connect(self.onActionAbout)
self.actionFeedback.triggered.connect(self.onActionFeedback)
self.actionFind.triggered.connect(self.onActionFind)
self.actionFindNext.triggered.connect(self.onActionFindNext)
self.actionHomepage.triggered.connect(self.onActionHomepage)
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.updater.updateResult.connect(self.onUpdaterSearchResult)
if self.preferences['checkForUpdates']:
self.updater.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()
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)
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()
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 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
self.textContent.setLineWrapMode(QtGui.QPlainTextEdit.WidgetWidth if self.preferences['wordWrap'] else QtGui.QPlainTextEdit.NoWrap)
def onActionHomepage(self):
url = QtCore.QUrl('http://foosoft.net/yomichan/')
QtGui.QDesktopServices().openUrl(url)
def onActionFeedback(self):
url = QtCore.QUrl('http://foosoft.net/comments/')
QtGui.QDesktopServices().openUrl(url)
def onVocabDefsAnchorClicked(self, url):
command, index = unicode(url.toString()).split(':')
definition = self.state.vocabDefs[int(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 onKanjiDefsAnchorClicked(self, url):
command, index = unicode(url.toString()).split(':')
definition = self.state.kanjiDefs[int(index)]
if command == 'addKanji':
markup = reader_util.markupKanji(definition)
self.ankiAddFact('kanji', markup)
elif command == 'copyKanjiDef':
reader_util.copyKanjiDef(definition)
def onVocabDefSearchReturn(self):
text = unicode(self.textVocabSearch.text())
self.state.vocabDefs, length = self.language.findTerm(text, True)
self.updateVocabDefs()
def onKanjiDefSearchReturn(self):
text = unicode(self.textKanjiSearch.text())
self.state.kanjiDefs = self.language.findCharacters(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, result):
if result and unicode(result) > constants.c['appVersion']:
QtGui.QMessageBox.information(
self,
'Yomichan',
'A new version of Yomichan is available for download!\n\nYou can download this update ({0} > {1}) ' \
'from "Shared Plugins" in Anki or directly from the Yomichan homepage.'.format(constants.c['appVersion'], result)
)
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):
self.setWindowTitle('Yomichan')
self.textContent.setPlainText(unicode())
self.updateRecentFile(False)
self.state = self.State()
def findText(self, text):
content = self.textContent.toPlainText()
index = content.indexOf(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 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)
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()
self.updateKanjiDefs()
return True
def ankiIsFactValid(self, profile, markup):
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 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']
cursor = self.textContent.textCursor()
content = unicode(self.textContent.toPlainText())
contentSample = content[samplePosStart:samplePosEnd]
contentSampleFlat = contentSample.replace(u'\n', unicode())
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.findCharacters(contentSampleFlat[0])
if len(self.state.kanjiDefs) > 0:
lengthMatched = 1
else:
self.state.kanjiDefs = self.language.findCharacters(contentSampleFlat[:lengthMatched])
self.updateKanjiDefs()
lengthSelect = 0
for c in contentSample:
if lengthMatched <= 0:
break
lengthSelect += 1
if c not in [u'\n', u' ', u' ']:
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 updateVocabDefs(self):
html = reader_util.buildVocabDefs(
self.state.vocabDefs[:self.preferences['maxResults']],
self.ankiIsFactValid
)
self.textVocabDefs.setHtml(html)
def updateKanjiDefs(self):
html = reader_util.buildKanjiDefs(
self.state.kanjiDefs[:self.preferences['maxRsults']],
self.ankiIsFactValid
)
self.textKanjiDefs.setHtml(html)
def setStatus(self, status):
self.statusBar.showMessage(status)