# -*- coding: utf-8 -*- # Copyright (C) 2011 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 os import tarfile from PyQt4 import QtGui, QtCore from reader_ui import Ui_MainWindowReader from preferences import DialogPreferences from preference_data import Preferences from update import UpdateFinder from about import DialogAbout from constants import constants import reader_util class MainWindowReader(QtGui.QMainWindow, Ui_MainWindowReader): class State: def __init__(self): self.filename = unicode() self.definitions = list() self.searchPosition = 0 self.searchText = unicode() self.scanPosition = 0 self.archiveIndex = None def __init__(self, parent=None, languages=None, filename=None, preferences=None, anki=None, closed=None, updated=None): QtGui.QMainWindow.__init__(self, parent) self.setupUi(self) self.textContent.mouseMoveEvent = self.onContentMouseMove self.textContent.mousePressEvent = self.onContentMousePress self.dockAnki.setEnabled(bool(anki)) if preferences: self.preferences = preferences else: self.preferences = Preferences() self.preferences.load() self.updateFinder = UpdateFinder() self.state = self.State() self.languages = languages self.addedFacts = list() self.anki = anki self.closed = closed self.updated = updated self.zoom = 0 self.applyPreferences() self.updateRecentFiles() self.updateDefinitions() if filename: self.openFile(filename) elif self.preferences.generalRecentLoad: filenames = self.preferences.recentFiles() if filenames: self.openFile(filenames[0]) self.actionOpen.triggered.connect(self.onActionOpen) self.actionPreferences.triggered.connect(self.onActionPreferences) self.actionAbout.triggered.connect(self.onActionAbout) self.actionZoomIn.triggered.connect(self.onActionZoomIn) self.actionZoomOut.triggered.connect(self.onActionZoomOut) self.actionZoomReset.triggered.connect(self.onActionZoomReset) self.actionFind.triggered.connect(self.onActionFind) self.actionFindNext.triggered.connect(self.onActionFindNext) self.actionToggleWrap.toggled.connect(self.onActionToggleWrap) self.actionCopyDefinition.triggered.connect(self.onActionCopyDefinition) self.actionCopyAllDefinitions.triggered.connect(self.onActionCopyAllDefinitions) self.actionCopySentence.triggered.connect(self.onActionCopySentence) self.actionHomepage.triggered.connect(self.onActionHomepage) self.actionFeedback.triggered.connect(self.onActionFeedback) self.textDefinitions.anchorClicked.connect(self.onDefinitionsAnchorClicked) self.textDefinitionSearch.returnPressed.connect(self.onDefinitionSearchReturn) self.listDefinitions.itemDoubleClicked.connect(self.onDefinitionDoubleClicked) self.dockDefinitions.visibilityChanged.connect(self.onVisibilityChanged) self.dockAnki.visibilityChanged.connect(self.onVisibilityChanged) self.updateFinder.updateResult.connect(self.onUpdaterSearchResult) if self.preferences.generalFindUpdates: self.updateFinder.start() def applyPreferences(self): if self.preferences.uiReaderState != None: self.restoreState(QtCore.QByteArray.fromBase64(self.preferences.uiReaderState)) if self.preferences.uiReaderPosition != None: self.move(QtCore.QPoint(*self.preferences.uiReaderPosition)) if self.preferences.uiReaderSize != None: self.resize(QtCore.QSize(*self.preferences.uiReaderSize)) for tags in self.preferences.ankiTags: self.comboTags.addItem(tags) self.applyPreferencesContent() def applyPreferencesContent(self): palette = self.textContent.palette() palette.setColor(QtGui.QPalette.Base, QtGui.QColor(self.preferences.uiContentColorBg)) palette.setColor(QtGui.QPalette.Text, QtGui.QColor(self.preferences.uiContentColorFg)) self.textContent.setPalette(palette) font = self.textContent.font() font.setFamily(self.preferences.uiContentFontFamily) font.setPointSize(self.preferences.uiContentFontSize + self.zoom) self.textContent.setFont(font) self.actionToggleWrap.setChecked(self.preferences.uiContentWordWrap) def closeEvent(self, event): self.closeFile() self.preferences.uiReaderState = self.saveState().toBase64() self.preferences.save() if self.closed: self.closed() def keyPressEvent(self, event): if self.dockDefinitions.isVisible() and 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.uiReaderPosition = (event.pos().x(), event.pos().y()) def resizeEvent(self, event): self.preferences.uiReaderSize = (event.size().width(), event.size().height()) def onActionOpen(self): filename = QtGui.QFileDialog.getOpenFileName( parent=self, caption='Select a file to open', filter='Archive files (*.bz2 *.gz *.tar *.tgz);;Text files (*.txt);;All files (*.*)', selectedFilter='Text files (*.txt)' ) if not filename.isNull(): self.openFile(filename) def onActionPreferences(self): dialog = DialogPreferences(self, self.preferences, self.anki) if dialog.exec_() == QtGui.QDialog.Accepted: self.applyPreferencesContent() if self.updated: self.updated() def onActionAbout(self): dialog = 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): mode = QtGui.QPlainTextEdit.WidgetWidth if wrap else QtGui.QPlainTextEdit.NoWrap self.preferences.uiContentWordWrap = wrap self.textContent.setLineWrapMode(wrap) def onActionCopyDefinition(self): reader_util.copyDefinitions(self.state.definitions[:1]) def onActionCopyAllDefinitions(self): reader_util.copyDefinitions(self.state.definitions) def onActionCopySentence(self): content = unicode(self.textContent.toPlainText()) sentence = reader_util.findSentence(content, self.state.scanPosition) QtGui.QApplication.clipboard().setText(sentence) def onActionHomepage(self): url = QtCore.QUrl(constants['urlHomepage']) QtGui.QDesktopServices().openUrl(url) def onActionFeedback(self): url = QtCore.QUrl(constants['urlFeedback']) QtGui.QDesktopServices().openUrl(url) def onDefinitionsAnchorClicked(self, url): command, index = unicode(url.toString()).split(':') definition = self.state.definitions[int(index)] if command == 'addFactExpression': markup = reader_util.buildFactMarkupExpression( definition.expression, definition.reading, definition.glossary, definition.sentence ) self.ankiAddFact(markup) if command == 'addFactReading': markup = reader_util.buildFactMarkupReading( definition.reading, definition.glossary, definition.sentence ) self.ankiAddFact(markup) elif command == 'copyDefinition': reader_util.copyDefinitions([definition]) def onDefinitionSearchReturn(self): text = unicode(self.textDefinitionSearch.text()) definitions, length = self.language().wordSearch( text, self.preferences.searchResultMax, self.preferences.searchGroupByExp ) self.state.definitions = reader_util.convertDefinitions(definitions) self.updateDefinitions() def onDefinitionDoubleClicked(self, item): if self.anki: row = self.listDefinitions.row(item) self.anki.browseFact(self.addedFacts[row]) def onVisibilityChanged(self, visible): self.actionToggleAnki.setChecked(self.dockAnki.isVisible()) self.actionToggleDefinitions.setChecked(self.dockDefinitions.isVisible()) def onUpdaterSearchResult(self, result): if result and result > constants['version']: 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['version'], 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): filename = unicode(filename) try: content = self.openFileByExtension(filename) 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.generalReadingsStrip: content = reader_util.stripContentReadings(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.split(filename)[1], encoding)) def openFileByExtension(self, filename): self.clearArchiveFiles() if tarfile.is_tarfile(filename): # opening an empty tar file raises ReadError with tarfile.open(filename, 'r:*') as tp: files = [f for f in tp.getnames() if tp.getmember(f).isfile()] names = [f.decode('utf-8') for f in files] self.updateArchiveFiles(filename, names) if len(files) == 0: content = unicode() elif len(files) == 1: fp = tp.extractfile(files[0]) content = fp.read() fp.close() else: # Using index because of encoding difficulties (index, ok) = self.selectFileName(names) if ok: fp = tp.extractfile(files[index]) content = fp.read() fp.close() self.state.archiveIndex = index else: content = unicode() else: self.state.archiveIndex = None with open(filename, 'rb') as fp: content = fp.read() return content def selectFileName(self, names): if self.state.archiveIndex is not None: return (self.state.archiveIndex, True) (item, ok) = QtGui.QInputDialog.getItem( self, 'Yomichan', 'Select file to open:', self.formatQStringList(names), current = 0, editable=False) (index, success) = self.getItemIndex(item) return (index - 1, ok and success) def getItemIndex(self, item): return item.split('.').first().toInt() def formatQStringList(self, list): return [self.formatQString(i, x) for i, x in enumerate(list)] def formatQString(self, index, item): return QtCore.QString(str(index + 1) + '. ').append(QtCore.QString(item)) 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, markup): if not self.anki: return False fields = reader_util.replaceMarkupInFields(self.preferences.ankiFields, markup) tags = self.anki.cleanupTags(unicode(self.comboTags.currentText())) tagIndex = self.comboTags.findText(tags) if tagIndex > 0: self.comboTags.removeItem(tagIndex) if tagIndex != 0: self.comboTags.insertItem(0, tags) self.preferences.updateFactTags(tags) factId = self.anki.addFact(fields, tags) if not factId: return False expression, reading = markup['%e'], markup['%r'] summary = expression if reading: summary = u'{0} [{1}]'.format(expression, reading) self.addedFacts.append(factId) self.listDefinitions.addItem(summary) self.listDefinitions.setCurrentRow(self.listDefinitions.count() - 1) self.setStatus(u'Added expression {0}; {1} new fact(s) total'.format(expression, len(self.addedFacts))) self.updateDefinitions() return True def ankiIsFactValid(self, markup): if not self.anki: return False fields = reader_util.replaceMarkupInFields(self.preferences.ankiFields, markup) return self.anki.canAddFact(fields) def updateSampleMouseEvent(self, event): cursor = self.textContent.cursorForPosition(event.pos()) self.state.scanPosition = cursor.position() requested = event.buttons() & QtCore.Qt.MidButton or event.modifiers() & QtCore.Qt.ShiftModifier if self.dockDefinitions.isVisible() and requested: self.updateSampleFromPosition() def updateSampleFromPosition(self): samplePosStart = self.state.scanPosition samplePosEnd = self.state.scanPosition + self.preferences.searchScanMax cursor = self.textContent.textCursor() content = unicode(self.textContent.toPlainText()) contentSample = content[samplePosStart:samplePosEnd] if not contentSample or unicode.isspace(contentSample[0]): cursor.clearSelection() self.textContent.setTextCursor(cursor) return contentSampleFlat = contentSample.replace('\n', unicode()) definitionsMatched, lengthMatched = self.language().wordSearch( contentSampleFlat, self.preferences.searchResultMax, self.preferences.searchGroupByExp ) sentence = reader_util.findSentence(content, samplePosStart) self.state.definitions = reader_util.convertDefinitions(definitionsMatched, sentence) self.updateDefinitions() lengthSelect = 0 if lengthMatched: for c in contentSample: lengthSelect += 1 if c != '\n': lengthMatched -= 1 if lengthMatched <= 0: break cursor.setPosition(samplePosStart, QtGui.QTextCursor.MoveAnchor) cursor.setPosition(samplePosStart + lengthSelect, QtGui.QTextCursor.KeepAnchor) self.textContent.setTextCursor(cursor) def clearArchiveFiles(self): self.menuOpenArchive.clear() self.menuOpenArchive.setEnabled(False) def updateArchiveFiles(self, filename, names): self.menuOpenArchive.setEnabled(True) for name in self.formatQStringList(names): (index, ok) = self.getItemIndex(name) if ok: index = index - 1 self.menuOpenArchive.addAction(name, (lambda fn=filename, idx=index: self.openFileInArchive(fn, idx))) else: self.menuOpenArchive.addAction(name, (lambda fn=filename: self.openFile(fn))) def openFileInArchive(self, filename, index): self.state.scanPosition = 0 self.state.archiveIndex = index self.openFile(filename) def clearRecentFiles(self): self.preferences.clearRecentFiles() self.updateRecentFiles() def updateRecentFiles(self): self.menuOpenRecent.clear() filenames = self.preferences.recentFiles() if not filenames: return for filename in filenames: self.menuOpenRecent.addAction(filename, (lambda fn=filename: self.openFile(fn))) 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 updateDefinitions(self): html = reader_util.buildDefinitionsHtml(self.state.definitions, self.ankiIsFactValid) self.textDefinitions.setHtml(html) def setStatus(self, status): self.statusBar.showMessage(status) def language(self): return self.languages['Japanese']