1# vim: ts=8:sts=8:sw=8:noexpandtab 2# 3# This file is part of ReText 4# Copyright: 2012-2021 Dmitry Shachnev 5# 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 2 of the License, or 9# (at your option) any later version. 10# 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with this program. If not, see <http://www.gnu.org/licenses/>. 18 19import markups 20import sys 21import os 22from subprocess import Popen 23import warnings 24 25from ReText import (getBundledIcon, app_version, globalSettings, 26 readListFromSettings, writeListToSettings, datadirs) 27from ReText.tab import (ReTextTab, ReTextWebKitPreview, ReTextWebEnginePreview, 28 PreviewDisabled, PreviewNormal, PreviewLive) 29from ReText.dialogs import HtmlDialog, LocaleDialog 30from ReText.config import ConfigDialog, setIconThemeFromSettings 31from ReText.tabledialog import InsertTableDialog 32 33try: 34 from ReText.fakevimeditor import ReTextFakeVimHandler, FakeVimMode 35except ImportError: 36 ReTextFakeVimHandler = None 37 38try: 39 import enchant 40except ImportError: 41 enchant = None 42 43from PyQt5.QtCore import QDir, QFile, QFileInfo, QFileSystemWatcher, \ 44 QIODevice, QLocale, QMarginsF, QTextCodec, QTextStream, QTimer, QUrl, Qt, pyqtSlot 45from PyQt5.QtGui import QColor, QDesktopServices, QIcon, \ 46 QKeySequence, QPageLayout, QPageSize, QPagedPaintDevice, QPalette, \ 47 QTextDocument, QTextDocumentWriter 48from PyQt5.QtWidgets import QAction, QActionGroup, QApplication, QCheckBox, \ 49 QComboBox, QDialog, QFileDialog, QFileSystemModel, QFontDialog, \ 50 QInputDialog, QLineEdit, QMainWindow, QMenu, QMessageBox, QSplitter, QTabWidget, \ 51 QToolBar, QToolButton, QTreeView 52from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog, QPrinter 53 54class ReTextWindow(QMainWindow): 55 def __init__(self, parent=None): 56 QMainWindow.__init__(self, parent) 57 self.resize(950, 700) 58 qApp = QApplication.instance() 59 if hasattr(self, 'screen'): # Available since Qt 5.14 60 screenRect = self.screen().geometry() 61 else: 62 screenRect = qApp.desktop().screenGeometry() 63 if globalSettings.windowGeometry: 64 self.restoreGeometry(globalSettings.windowGeometry) 65 else: 66 self.move((screenRect.width() - self.width()) // 2, 67 (screenRect.height() - self.height()) // 2) 68 if not screenRect.contains(self.geometry()): 69 self.showMaximized() 70 if sys.platform.startswith('darwin'): 71 # https://github.com/retext-project/retext/issues/198 72 searchPaths = QIcon.themeSearchPaths() 73 searchPaths.append('/opt/local/share/icons') 74 searchPaths.append('/usr/local/share/icons') 75 QIcon.setThemeSearchPaths(searchPaths) 76 setIconThemeFromSettings() 77 if QFile.exists(getBundledIcon('retext')): 78 self.setWindowIcon(QIcon(getBundledIcon('retext'))) 79 elif QFile.exists('/usr/share/pixmaps/retext.png'): 80 self.setWindowIcon(QIcon('/usr/share/pixmaps/retext.png')) 81 else: 82 self.setWindowIcon(QIcon.fromTheme('retext', 83 QIcon.fromTheme('accessories-text-editor'))) 84 self.splitter = QSplitter(self) 85 self.treeView = QTreeView(self.splitter) 86 self.treeView.doubleClicked.connect(self.treeItemSelected) 87 self.tabWidget = QTabWidget(self.splitter) 88 self.initTabWidget() 89 self.splitter.setSizes([self.width() // 5, self.width() * 4 // 5]) 90 self.initDirectoryTree(globalSettings.showDirectoryTree, globalSettings.directoryPath) 91 self.setCentralWidget(self.splitter) 92 self.tabWidget.currentChanged.connect(self.changeIndex) 93 self.tabWidget.tabCloseRequested.connect(self.closeTab) 94 self.toolBar = QToolBar(self.tr('File toolbar'), self) 95 self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.toolBar) 96 self.editBar = QToolBar(self.tr('Edit toolbar'), self) 97 self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.editBar) 98 self.searchBar = QToolBar(self.tr('Search toolbar'), self) 99 self.addToolBar(Qt.ToolBarArea.BottomToolBarArea, self.searchBar) 100 self.toolBar.setVisible(not globalSettings.hideToolBar) 101 self.editBar.setVisible(not globalSettings.hideToolBar) 102 self.actionNew = self.act(self.tr('New'), 'document-new', 103 self.createNew, shct=QKeySequence.StandardKey.New) 104 self.actionOpen = self.act(self.tr('Open'), 'document-open', 105 self.openFile, shct=QKeySequence.StandardKey.Open) 106 self.actionSetEncoding = self.act(self.tr('Set encoding'), 107 trig=self.showEncodingDialog) 108 self.actionSetEncoding.setEnabled(False) 109 self.actionReload = self.act(self.tr('Reload'), 'view-refresh', 110 lambda: self.currentTab.readTextFromFile()) 111 self.actionReload.setEnabled(False) 112 self.actionSave = self.act(self.tr('Save'), 'document-save', 113 self.saveFile, shct=QKeySequence.StandardKey.Save) 114 self.actionSave.setEnabled(False) 115 self.actionSaveAs = self.act(self.tr('Save as'), 'document-save-as', 116 self.saveFileAs, shct=QKeySequence.StandardKey.SaveAs) 117 self.actionNextTab = self.act(self.tr('Next tab'), 'go-next', 118 lambda: self.switchTab(1), shct=Qt.Modifier.CTRL+Qt.Key.Key_PageDown) 119 self.actionPrevTab = self.act(self.tr('Previous tab'), 'go-previous', 120 lambda: self.switchTab(-1), shct=Qt.Modifier.CTRL+Qt.Key.Key_PageUp) 121 self.actionCloseCurrentTab = self.act(self.tr('Close tab'), 'window-close', 122 lambda: self.closeTab(self.ind), shct=QKeySequence.StandardKey.Close) 123 self.actionPrint = self.act(self.tr('Print'), 'document-print', 124 self.printFile, shct=QKeySequence.StandardKey.Print) 125 self.actionPrintPreview = self.act(self.tr('Print preview'), 'document-print-preview', 126 self.printPreview) 127 self.actionViewHtml = self.act(self.tr('View HTML code'), 'text-html', self.viewHtml) 128 self.actionChangeEditorFont = self.act(self.tr('Change editor font'), 129 trig=self.changeEditorFont) 130 self.actionChangePreviewFont = self.act(self.tr('Change preview font'), 131 trig=self.changePreviewFont) 132 self.actionSearch = self.act(self.tr('Find text'), 'edit-find', 133 self.search, shct=QKeySequence.StandardKey.Find) 134 self.actionGoToLine = self.act(self.tr('Go to line'), 135 trig=self.goToLine, shct=Qt.Modifier.CTRL+Qt.Key.Key_G) 136 self.searchBar.visibilityChanged.connect(self.searchBarVisibilityChanged) 137 self.actionPreview = self.act(self.tr('Preview'), shct=Qt.Modifier.CTRL+Qt.Key.Key_E, 138 trigbool=self.preview) 139 if QIcon.hasThemeIcon('document-preview'): 140 self.actionPreview.setIcon(QIcon.fromTheme('document-preview')) 141 elif QIcon.hasThemeIcon('preview-file'): 142 self.actionPreview.setIcon(QIcon.fromTheme('preview-file')) 143 elif QIcon.hasThemeIcon('x-office-document'): 144 self.actionPreview.setIcon(QIcon.fromTheme('x-office-document')) 145 else: 146 self.actionPreview.setIcon(QIcon(getBundledIcon('document-preview'))) 147 self.actionLivePreview = self.act(self.tr('Live preview'), shct=Qt.Modifier.CTRL+Qt.Key.Key_L, 148 trigbool=self.enableLivePreview) 149 menuPreview = QMenu() 150 menuPreview.addAction(self.actionLivePreview) 151 self.actionInsertTable = self.act(self.tr('Insert table'), 152 trig=lambda: self.insertFormatting('table')) 153 self.actionTableMode = self.act(self.tr('Table editing mode'), 154 shct=Qt.Modifier.CTRL+Qt.Key.Key_T, 155 trigbool=lambda x: self.currentTab.editBox.enableTableMode(x)) 156 self.actionInsertImages = self.act(self.tr('Insert images by file path'), 157 trig=lambda: self.insertImages()) 158 if ReTextFakeVimHandler: 159 self.actionFakeVimMode = self.act(self.tr('FakeVim mode'), 160 shct=Qt.Modifier.CTRL+Qt.Modifier.ALT+Qt.Key.Key_V, trigbool=self.enableFakeVimMode) 161 if globalSettings.useFakeVim: 162 self.actionFakeVimMode.setChecked(True) 163 self.enableFakeVimMode(True) 164 self.actionFullScreen = self.act(self.tr('Fullscreen mode'), 'view-fullscreen', 165 shct=QKeySequence.StandardKey.FullScreen, trigbool=self.enableFullScreen) 166 self.actionFullScreen.setChecked(self.isFullScreen()) 167 self.actionConfig = self.act(self.tr('Preferences'), icon='preferences-system', 168 trig=self.openConfigDialog) 169 self.actionConfig.setMenuRole(QAction.MenuRole.PreferencesRole) 170 self.actionSaveHtml = self.act('HTML', 'text-html', self.saveFileHtml) 171 self.actionPdf = self.act('PDF', 'application-pdf', self.savePdf) 172 self.actionOdf = self.act('ODT', 'x-office-document', self.saveOdf) 173 self.getExportExtensionsList() 174 self.actionQuit = self.act(self.tr('Quit'), 'application-exit', shct=QKeySequence.StandardKey.Quit) 175 self.actionQuit.setMenuRole(QAction.MenuRole.QuitRole) 176 self.actionQuit.triggered.connect(self.close) 177 self.actionUndo = self.act(self.tr('Undo'), 'edit-undo', 178 lambda: self.currentTab.editBox.undo(), shct=QKeySequence.StandardKey.Undo) 179 self.actionRedo = self.act(self.tr('Redo'), 'edit-redo', 180 lambda: self.currentTab.editBox.redo(), shct=QKeySequence.StandardKey.Redo) 181 self.actionCopy = self.act(self.tr('Copy'), 'edit-copy', 182 lambda: self.currentTab.editBox.copy(), shct=QKeySequence.StandardKey.Copy) 183 self.actionCut = self.act(self.tr('Cut'), 'edit-cut', 184 lambda: self.currentTab.editBox.cut(), shct=QKeySequence.StandardKey.Cut) 185 self.actionPaste = self.act(self.tr('Paste'), 'edit-paste', 186 lambda: self.currentTab.editBox.paste(), shct=QKeySequence.StandardKey.Paste) 187 self.actionPasteImage = self.act(self.tr('Paste image'), 'edit-paste', 188 lambda: self.currentTab.editBox.pasteImage(), shct=Qt.Modifier.CTRL+Qt.Modifier.SHIFT+Qt.Key.Key_V) 189 self.actionMoveUp = self.act(self.tr('Move line up'), 'go-up', 190 lambda: self.currentTab.editBox.moveLineUp(), shct=Qt.Modifier.ALT+Qt.Key.Key_Up) 191 self.actionMoveDown = self.act(self.tr('Move line down'), 'go-down', 192 lambda: self.currentTab.editBox.moveLineDown(), shct=Qt.Modifier.ALT+Qt.Key.Key_Down) 193 self.actionUndo.setEnabled(False) 194 self.actionRedo.setEnabled(False) 195 self.actionCopy.setEnabled(False) 196 self.actionCut.setEnabled(False) 197 qApp.clipboard().dataChanged.connect(self.clipboardDataChanged) 198 self.clipboardDataChanged() 199 if enchant is not None: 200 self.actionEnableSC = self.act(self.tr('Enable'), trigbool=self.enableSpellCheck) 201 self.actionSetLocale = self.act(self.tr('Set locale'), trig=self.changeLocale) 202 self.actionWebKit = self.act(self.tr('Use WebKit renderer'), trigbool=self.enableWebKit) 203 if ReTextWebKitPreview is None: 204 globalSettings.useWebKit = False 205 self.actionWebKit.setEnabled(False) 206 self.actionWebKit.setChecked(globalSettings.useWebKit) 207 self.actionWebEngine = self.act(self.tr('Use WebEngine (Chromium) renderer'), 208 trigbool=self.enableWebEngine) 209 if ReTextWebEnginePreview is None: 210 globalSettings.useWebEngine = False 211 self.actionWebEngine.setChecked(globalSettings.useWebEngine) 212 self.actionShow = self.act(self.tr('Show directory'), 'system-file-manager', self.showInDir) 213 self.actionFind = self.act(self.tr('Next'), 'go-next', self.find, 214 shct=QKeySequence.StandardKey.FindNext) 215 self.actionFindPrev = self.act(self.tr('Previous'), 'go-previous', 216 lambda: self.find(back=True), shct=QKeySequence.StandardKey.FindPrevious) 217 self.actionReplace = self.act(self.tr('Replace'), 'edit-find-replace', 218 lambda: self.find(replace=True)) 219 self.actionReplaceAll = self.act(self.tr('Replace all'), trig=self.replaceAll) 220 menuReplace = QMenu() 221 menuReplace.addAction(self.actionReplaceAll) 222 self.actionCloseSearch = self.act(self.tr('Close'), 'window-close', 223 lambda: self.searchBar.setVisible(False), 224 shct=QKeySequence.StandardKey.Cancel) 225 self.actionCloseSearch.setPriority(QAction.Priority.LowPriority) 226 self.actionHelp = self.act(self.tr('Get help online'), 'help-contents', self.openHelp) 227 self.aboutWindowTitle = self.tr('About ReText') 228 self.actionAbout = self.act(self.aboutWindowTitle, 'help-about', self.aboutDialog) 229 self.actionAbout.setMenuRole(QAction.MenuRole.AboutRole) 230 self.actionAboutQt = self.act(self.tr('About Qt')) 231 self.actionAboutQt.setMenuRole(QAction.MenuRole.AboutQtRole) 232 self.actionAboutQt.triggered.connect(qApp.aboutQt) 233 availableMarkups = markups.get_available_markups() 234 if not availableMarkups: 235 print('Warning: no markups are available!') 236 if len(availableMarkups) > 1: 237 self.chooseGroup = QActionGroup(self) 238 markupActions = [] 239 for markup in availableMarkups: 240 markupAction = self.act(markup.name, trigbool=self.markupFunction(markup)) 241 if markup.name == globalSettings.defaultMarkup: 242 markupAction.setChecked(True) 243 self.chooseGroup.addAction(markupAction) 244 markupActions.append(markupAction) 245 self.actionBold = self.act(self.tr('Bold'), shct=QKeySequence.StandardKey.Bold, 246 trig=lambda: self.insertFormatting('bold')) 247 self.actionItalic = self.act(self.tr('Italic'), shct=QKeySequence.StandardKey.Italic, 248 trig=lambda: self.insertFormatting('italic')) 249 self.actionUnderline = self.act(self.tr('Underline'), shct=QKeySequence.StandardKey.Underline, 250 trig=lambda: self.insertFormatting('underline')) 251 self.usefulTags = ('header', 'italic', 'bold', 'underline', 'numbering', 252 'bullets', 'image', 'link', 'inline code', 'code block', 'blockquote', 253 'table') 254 self.usefulChars = ('deg', 'divide', 'euro', 'hellip', 'laquo', 'larr', 255 'lsquo', 'mdash', 'middot', 'minus', 'nbsp', 'ndash', 'raquo', 256 'rarr', 'rsquo', 'times') 257 self.formattingBox = QComboBox(self.editBar) 258 self.formattingBox.addItem(self.tr('Formatting')) 259 self.formattingBox.addItems(self.usefulTags) 260 if hasattr(self.formattingBox, 'textActivated'): # Available since Qt 5.14 261 self.formattingBox.textActivated.connect(self.insertFormatting) 262 else: 263 self.formattingBox.activated[str].connect(self.insertFormatting) 264 self.symbolBox = QComboBox(self.editBar) 265 self.symbolBox.addItem(self.tr('Symbols')) 266 self.symbolBox.addItems(self.usefulChars) 267 self.symbolBox.activated.connect(self.insertSymbol) 268 self.updateStyleSheet() 269 menubar = self.menuBar() 270 menuFile = menubar.addMenu(self.tr('&File')) 271 menuEdit = menubar.addMenu(self.tr('&Edit')) 272 menuHelp = menubar.addMenu(self.tr('&Help')) 273 menuFile.addAction(self.actionNew) 274 menuFile.addAction(self.actionOpen) 275 self.menuRecentFiles = menuFile.addMenu(self.tr('Open recent')) 276 self.menuRecentFiles.aboutToShow.connect(self.updateRecentFiles) 277 menuFile.addAction(self.actionShow) 278 menuFile.addAction(self.actionSetEncoding) 279 menuFile.addAction(self.actionReload) 280 menuFile.addSeparator() 281 menuFile.addAction(self.actionSave) 282 menuFile.addAction(self.actionSaveAs) 283 menuFile.addSeparator() 284 menuFile.addAction(self.actionNextTab) 285 menuFile.addAction(self.actionPrevTab) 286 menuFile.addAction(self.actionCloseCurrentTab) 287 menuFile.addSeparator() 288 menuExport = menuFile.addMenu(self.tr('Export')) 289 menuExport.addAction(self.actionSaveHtml) 290 menuExport.addAction(self.actionOdf) 291 menuExport.addAction(self.actionPdf) 292 if self.extensionActions: 293 menuExport.addSeparator() 294 for action, mimetype in self.extensionActions: 295 menuExport.addAction(action) 296 menuExport.aboutToShow.connect(self.updateExtensionsVisibility) 297 menuFile.addAction(self.actionPrint) 298 menuFile.addAction(self.actionPrintPreview) 299 menuFile.addSeparator() 300 menuFile.addAction(self.actionQuit) 301 menuEdit.addAction(self.actionUndo) 302 menuEdit.addAction(self.actionRedo) 303 menuEdit.addSeparator() 304 menuEdit.addAction(self.actionCut) 305 menuEdit.addAction(self.actionCopy) 306 menuEdit.addAction(self.actionPaste) 307 menuEdit.addAction(self.actionPasteImage) 308 menuEdit.addSeparator() 309 menuEdit.addAction(self.actionMoveUp) 310 menuEdit.addAction(self.actionMoveDown) 311 menuEdit.addSeparator() 312 if enchant is not None: 313 menuSC = menuEdit.addMenu(self.tr('Spell check')) 314 menuSC.addAction(self.actionEnableSC) 315 menuSC.addAction(self.actionSetLocale) 316 menuEdit.addAction(self.actionSearch) 317 menuEdit.addAction(self.actionGoToLine) 318 menuEdit.addAction(self.actionChangeEditorFont) 319 menuEdit.addAction(self.actionChangePreviewFont) 320 menuEdit.addSeparator() 321 if len(availableMarkups) > 1: 322 self.menuMode = menuEdit.addMenu(self.tr('Default markup')) 323 for markupAction in markupActions: 324 self.menuMode.addAction(markupAction) 325 menuFormat = menuEdit.addMenu(self.tr('Formatting')) 326 menuFormat.addAction(self.actionBold) 327 menuFormat.addAction(self.actionItalic) 328 menuFormat.addAction(self.actionUnderline) 329 if ReTextWebKitPreview is not None or ReTextWebEnginePreview is None: 330 menuEdit.addAction(self.actionWebKit) 331 else: 332 menuEdit.addAction(self.actionWebEngine) 333 menuEdit.addSeparator() 334 menuEdit.addAction(self.actionViewHtml) 335 menuEdit.addAction(self.actionPreview) 336 menuEdit.addAction(self.actionLivePreview) 337 menuEdit.addAction(self.actionInsertTable) 338 menuEdit.addAction(self.actionTableMode) 339 menuEdit.addAction(self.actionInsertImages) 340 if ReTextFakeVimHandler: 341 menuEdit.addAction(self.actionFakeVimMode) 342 menuEdit.addSeparator() 343 menuEdit.addAction(self.actionFullScreen) 344 menuEdit.addAction(self.actionConfig) 345 menuHelp.addAction(self.actionHelp) 346 menuHelp.addSeparator() 347 menuHelp.addAction(self.actionAbout) 348 menuHelp.addAction(self.actionAboutQt) 349 self.toolBar.addAction(self.actionNew) 350 self.toolBar.addSeparator() 351 self.toolBar.addAction(self.actionOpen) 352 self.toolBar.addAction(self.actionSave) 353 self.toolBar.addAction(self.actionPrint) 354 self.toolBar.addSeparator() 355 previewButton = QToolButton(self.toolBar) 356 previewButton.setDefaultAction(self.actionPreview) 357 previewButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) 358 previewButton.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup) 359 previewButton.setMenu(menuPreview) 360 self.toolBar.addWidget(previewButton) 361 self.toolBar.addAction(self.actionFullScreen) 362 self.editBar.addAction(self.actionUndo) 363 self.editBar.addAction(self.actionRedo) 364 self.editBar.addSeparator() 365 self.editBar.addAction(self.actionCut) 366 self.editBar.addAction(self.actionCopy) 367 self.editBar.addAction(self.actionPaste) 368 self.editBar.addSeparator() 369 self.editBar.addWidget(self.formattingBox) 370 self.editBar.addWidget(self.symbolBox) 371 self.searchEdit = QLineEdit(self.searchBar) 372 self.searchEdit.setPlaceholderText(self.tr('Search')) 373 self.searchEdit.returnPressed.connect(self.find) 374 self.replaceEdit = QLineEdit(self.searchBar) 375 self.replaceEdit.setPlaceholderText(self.tr('Replace with')) 376 self.replaceEdit.returnPressed.connect(self.find) 377 self.csBox = QCheckBox(self.tr('Case sensitively'), self.searchBar) 378 self.searchBar.addWidget(self.searchEdit) 379 self.searchBar.addWidget(self.replaceEdit) 380 self.searchBar.addSeparator() 381 self.searchBar.addWidget(self.csBox) 382 self.searchBar.addAction(self.actionFindPrev) 383 self.searchBar.addAction(self.actionFind) 384 replaceButton = QToolButton(self.searchBar) 385 replaceButton.setDefaultAction(self.actionReplace) 386 replaceButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) 387 replaceButton.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup) 388 replaceButton.setMenu(menuReplace) 389 self.searchBar.addWidget(replaceButton) 390 self.searchBar.addAction(self.actionCloseSearch) 391 self.searchBar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) 392 self.searchBar.setVisible(False) 393 self.autoSaveEnabled = globalSettings.autoSave 394 if self.autoSaveEnabled: 395 timer = QTimer(self) 396 timer.start(60000) 397 timer.timeout.connect(self.saveAll) 398 self.ind = None 399 if enchant is not None: 400 self.sl = globalSettings.spellCheckLocale 401 try: 402 enchant.Dict(self.sl or None) 403 except enchant.errors.Error as e: 404 warnings.warn(str(e), RuntimeWarning) 405 globalSettings.spellCheck = False 406 if globalSettings.spellCheck: 407 self.actionEnableSC.setChecked(True) 408 self.fileSystemWatcher = QFileSystemWatcher() 409 self.fileSystemWatcher.fileChanged.connect(self.fileChanged) 410 411 def restoreLastOpenedFiles(self): 412 for file in readListFromSettings("lastFileList"): 413 self.openFileWrapper(file) 414 415 # Show the tab of last opened file 416 lastTabIndex = globalSettings.lastTabIndex 417 if lastTabIndex >= 0 and lastTabIndex < self.tabWidget.count(): 418 self.tabWidget.setCurrentIndex(lastTabIndex) 419 420 def iterateTabs(self): 421 for i in range(self.tabWidget.count()): 422 yield self.tabWidget.widget(i) 423 424 def updateStyleSheet(self): 425 self.ss = None 426 if globalSettings.styleSheet: 427 sheetfile = QFile(globalSettings.styleSheet) 428 sheetfile.open(QIODevice.OpenModeFlag.ReadOnly) 429 self.ss = QTextStream(sheetfile).readAll() 430 sheetfile.close() 431 432 def initTabWidget(self): 433 def dragEnterEvent(e): 434 e.acceptProposedAction() 435 def dropEvent(e): 436 fn = bytes(e.mimeData().data('text/plain')).decode().rstrip() 437 if fn.startswith('file:'): 438 fn = QUrl(fn).toLocalFile() 439 self.openFileWrapper(fn) 440 self.tabWidget.setTabsClosable(True) 441 self.tabWidget.setAcceptDrops(True) 442 self.tabWidget.setMovable(True) 443 self.tabWidget.dragEnterEvent = dragEnterEvent 444 self.tabWidget.dropEvent = dropEvent 445 self.tabWidget.setTabBarAutoHide(globalSettings.tabBarAutoHide) 446 447 def initDirectoryTree(self, visible, path): 448 if visible: 449 self.fileSystemModel = QFileSystemModel(self.treeView) 450 self.fileSystemModel.setRootPath(path) 451 supportedExtensions = ['.txt'] 452 for markup in markups.get_all_markups(): 453 supportedExtensions += markup.file_extensions 454 filters = ["*" + s for s in supportedExtensions] 455 self.fileSystemModel.setNameFilters(filters) 456 self.fileSystemModel.setNameFilterDisables(False) 457 self.treeView.setModel(self.fileSystemModel) 458 self.treeView.setRootIndex(self.fileSystemModel.index(path)) 459 self.treeView.setColumnHidden(1, True) 460 self.treeView.setColumnHidden(2, True) 461 self.treeView.setColumnHidden(3, True) 462 self.treeView.setHeaderHidden(True) 463 self.treeView.setVisible(visible) 464 465 def treeItemSelected(self, signal): 466 file_path = self.fileSystemModel.filePath(signal) 467 if os.path.isdir(file_path): 468 return 469 self.openFileWrapper(file_path) 470 471 def act(self, name, icon=None, trig=None, trigbool=None, shct=None): 472 if not isinstance(shct, QKeySequence): 473 shct = QKeySequence(shct) 474 if icon: 475 action = QAction(self.actIcon(icon), name, self) 476 else: 477 action = QAction(name, self) 478 if trig: 479 action.triggered.connect(trig) 480 elif trigbool: 481 action.setCheckable(True) 482 action.triggered[bool].connect(trigbool) 483 if shct: 484 action.setShortcut(shct) 485 return action 486 487 def actIcon(self, name): 488 return QIcon.fromTheme(name, QIcon(getBundledIcon(name))) 489 490 def printError(self): 491 import traceback 492 print('Exception occurred while parsing document:', file=sys.stderr) 493 traceback.print_exc() 494 495 def updateTabTitle(self, ind, tab): 496 changed = tab.editBox.document().isModified() 497 if changed and not self.autoSaveActive(tab): 498 title = tab.getBaseName() + '*' 499 else: 500 title = tab.getBaseName() 501 self.tabWidget.setTabText(ind, title) 502 503 def tabFileNameChanged(self, tab): 504 ''' 505 Perform all UI state changes that need to be done when the 506 filename of the current tab has changed. 507 ''' 508 if tab == self.currentTab: 509 if tab.fileName: 510 self.setWindowTitle("") 511 if globalSettings.windowTitleFullPath: 512 self.setWindowTitle(tab.fileName + '[*]') 513 self.setWindowFilePath(tab.fileName) 514 self.updateTabTitle(self.ind, tab) 515 self.tabWidget.setTabToolTip(self.ind, tab.fileName) 516 QDir.setCurrent(QFileInfo(tab.fileName).dir().path()) 517 else: 518 self.setWindowFilePath('') 519 self.setWindowTitle(self.tr('New document') + '[*]') 520 521 canReload = bool(tab.fileName) and not self.autoSaveActive(tab) 522 self.actionSetEncoding.setEnabled(canReload) 523 self.actionReload.setEnabled(canReload) 524 525 def tabActiveMarkupChanged(self, tab): 526 ''' 527 Perform all UI state changes that need to be done when the 528 active markup class of the current tab has changed. 529 ''' 530 if tab == self.currentTab: 531 markupClass = tab.getActiveMarkupClass() 532 dtMarkdown = (markupClass == markups.MarkdownMarkup) 533 dtMkdOrReST = dtMarkdown or (markupClass == markups.ReStructuredTextMarkup) 534 self.formattingBox.setEnabled(dtMarkdown) 535 self.symbolBox.setEnabled(dtMarkdown) 536 self.actionUnderline.setEnabled(dtMarkdown) 537 self.actionBold.setEnabled(dtMkdOrReST) 538 self.actionItalic.setEnabled(dtMkdOrReST) 539 540 def tabModificationStateChanged(self, tab): 541 ''' 542 Perform all UI state changes that need to be done when the 543 modification state of the current tab has changed. 544 ''' 545 546 if tab == self.currentTab: 547 changed = tab.editBox.document().isModified() 548 if self.autoSaveActive(tab): 549 changed = False 550 self.actionSave.setEnabled(changed) 551 self.updateTabTitle(self.ind, tab) 552 self.setWindowModified(changed) 553 554 def createTab(self, fileName): 555 previewStatesByName = { 556 'editor': PreviewDisabled, 557 'normal-preview': PreviewNormal, 558 'live-preview': PreviewLive, 559 } 560 previewState = previewStatesByName.get(globalSettings.defaultPreviewState, PreviewDisabled) 561 if previewState == PreviewNormal and not fileName: 562 previewState = PreviewDisabled # Opening empty document in preview mode makes no sense 563 self.currentTab = ReTextTab(self, fileName, previewState) 564 self.currentTab.fileNameChanged.connect(lambda: self.tabFileNameChanged(self.currentTab)) 565 self.currentTab.modificationStateChanged.connect(lambda: self.tabModificationStateChanged(self.currentTab)) 566 self.currentTab.activeMarkupChanged.connect(lambda: self.tabActiveMarkupChanged(self.currentTab)) 567 self.tabWidget.addTab(self.currentTab, self.tr("New document")) 568 self.currentTab.updateBoxesVisibility() 569 if previewState > 0: 570 QTimer.singleShot(500, self.currentTab.triggerPreviewUpdate) 571 572 def closeTab(self, ind): 573 if self.maybeSave(ind): 574 if self.tabWidget.count() == 1: 575 self.createTab("") 576 closedTab = self.tabWidget.widget(ind) 577 if closedTab.fileName: 578 self.fileSystemWatcher.removePath(closedTab.fileName) 579 self.tabWidget.removeTab(ind) 580 closedTab.deleteLater() 581 582 def changeIndex(self, ind): 583 ''' 584 This function is called when a different tab is selected. 585 It changes the state of the window to mirror the current state 586 of the newly selected tab. Future changes to this state will be 587 done in response to signals emitted by the tab, to which the 588 window was subscribed when the tab was created. The window is 589 subscribed to all tabs like this, but only the active tab will 590 logically generate these signals. 591 Aside from the above this function also calls the handlers for 592 the other changes that are implied by a tab switch: filename 593 change, modification state change and active markup change. 594 ''' 595 self.currentTab = self.tabWidget.currentWidget() 596 editBox = self.currentTab.editBox 597 previewState = self.currentTab.previewState 598 self.actionUndo.setEnabled(editBox.document().isUndoAvailable()) 599 self.actionRedo.setEnabled(editBox.document().isRedoAvailable()) 600 self.actionCopy.setEnabled(editBox.textCursor().hasSelection()) 601 self.actionCut.setEnabled(editBox.textCursor().hasSelection()) 602 self.actionPreview.setChecked(previewState >= PreviewLive) 603 self.actionLivePreview.setChecked(previewState == PreviewLive) 604 self.actionTableMode.setChecked(editBox.tableModeEnabled) 605 self.editBar.setEnabled(previewState < PreviewNormal) 606 self.ind = ind 607 editBox.setFocus(Qt.FocusReason.OtherFocusReason) 608 609 self.tabFileNameChanged(self.currentTab) 610 self.tabModificationStateChanged(self.currentTab) 611 self.tabActiveMarkupChanged(self.currentTab) 612 613 def changeEditorFont(self): 614 font, ok = QFontDialog.getFont(globalSettings.editorFont, self) 615 if ok: 616 self.setEditorFont(font) 617 618 def setEditorFont(self, font): 619 globalSettings.editorFont = font 620 for tab in self.iterateTabs(): 621 tab.editBox.updateFont() 622 623 def changePreviewFont(self): 624 font, ok = QFontDialog.getFont(globalSettings.font, self) 625 if ok: 626 self.setPreviewFont(font) 627 628 def setPreviewFont(self, font): 629 globalSettings.font = font 630 for tab in self.iterateTabs(): 631 tab.triggerPreviewUpdate() 632 633 def preview(self, viewmode): 634 self.currentTab.previewState = viewmode * 2 635 self.actionLivePreview.setChecked(False) 636 self.editBar.setDisabled(viewmode) 637 self.currentTab.updateBoxesVisibility() 638 self.currentTab.triggerPreviewUpdate() 639 640 def enableLivePreview(self, livemode): 641 self.currentTab.previewState = int(livemode) 642 self.actionPreview.setChecked(livemode) 643 self.editBar.setEnabled(True) 644 self.currentTab.updateBoxesVisibility() 645 self.currentTab.triggerPreviewUpdate() 646 647 def enableWebKit(self, enable): 648 globalSettings.useWebKit = enable 649 globalSettings.useWebEngine = False 650 for tab in self.iterateTabs(): 651 tab.rebuildPreviewBox() 652 653 def enableWebEngine(self, enable): 654 globalSettings.useWebKit = False 655 globalSettings.useWebEngine = enable 656 for tab in self.iterateTabs(): 657 tab.rebuildPreviewBox() 658 659 def enableCopy(self, copymode): 660 self.actionCopy.setEnabled(copymode) 661 self.actionCut.setEnabled(copymode) 662 663 def enableFullScreen(self, yes): 664 if yes: 665 self.showFullScreen() 666 else: 667 self.showNormal() 668 669 def openConfigDialog(self): 670 dlg = ConfigDialog(self) 671 dlg.setWindowTitle(self.tr('Preferences')) 672 dlg.show() 673 674 def enableFakeVimMode(self, yes): 675 globalSettings.useFakeVim = yes 676 if yes: 677 FakeVimMode.init(self) 678 for tab in self.iterateTabs(): 679 tab.editBox.installFakeVimHandler() 680 else: 681 FakeVimMode.exit(self) 682 683 def enableSpellCheck(self, yes): 684 try: 685 dict = enchant.Dict(self.sl or None) 686 except enchant.errors.Error as e: 687 QMessageBox.warning(self, '', str(e)) 688 self.actionEnableSC.setChecked(False) 689 yes = False 690 self.setAllDictionaries(dict if yes else None) 691 globalSettings.spellCheck = yes 692 693 def setAllDictionaries(self, dictionary): 694 for tab in self.iterateTabs(): 695 hl = tab.highlighter 696 hl.dictionary = dictionary 697 hl.rehighlight() 698 699 def changeLocale(self): 700 localedlg = LocaleDialog(self, defaultText=self.sl) 701 if localedlg.exec() != QDialog.DialogCode.Accepted: 702 return 703 sl = localedlg.localeEdit.text() 704 try: 705 enchant.Dict(sl or None) 706 except enchant.errors.Error as e: 707 QMessageBox.warning(self, '', str(e)) 708 else: 709 self.sl = sl or None 710 self.enableSpellCheck(self.actionEnableSC.isChecked()) 711 if localedlg.checkBox.isChecked(): 712 globalSettings.spellCheckLocale = sl 713 714 def search(self): 715 self.searchBar.setVisible(True) 716 self.searchEdit.setFocus(Qt.FocusReason.ShortcutFocusReason) 717 718 def goToLine(self): 719 line, ok = QInputDialog.getInt(self, self.tr("Go to line"), self.tr("Type the line number")) 720 if ok: 721 self.currentTab.goToLine(line-1) 722 723 def searchBarVisibilityChanged(self, visible): 724 if visible: 725 self.searchEdit.setFocus(Qt.FocusReason.ShortcutFocusReason) 726 727 def find(self, back=False, replace=False): 728 flags = QTextDocument.FindFlags() 729 if back: 730 flags |= QTextDocument.FindFlag.FindBackward 731 if self.csBox.isChecked(): 732 flags |= QTextDocument.FindFlag.FindCaseSensitively 733 text = self.searchEdit.text() 734 replaceText = self.replaceEdit.text() if replace else None 735 found = self.currentTab.find(text, flags, replaceText=replaceText) 736 self.setSearchEditColor(found) 737 738 def replaceAll(self): 739 text = self.searchEdit.text() 740 replaceText = self.replaceEdit.text() 741 found = self.currentTab.replaceAll(text, replaceText) 742 self.setSearchEditColor(found) 743 744 def setSearchEditColor(self, found): 745 palette = self.searchEdit.palette() 746 palette.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.Base, 747 Qt.GlobalColor.white if found else QColor(255, 102, 102)) 748 self.searchEdit.setPalette(palette) 749 750 def showInDir(self): 751 if self.currentTab.fileName: 752 path = QFileInfo(self.currentTab.fileName).path() 753 QDesktopServices.openUrl(QUrl.fromLocalFile(path)) 754 else: 755 QMessageBox.warning(self, '', self.tr("Please, save the file somewhere.")) 756 757 def moveToTopOfRecentFileList(self, fileName): 758 if fileName: 759 files = readListFromSettings("recentFileList") 760 if fileName in files: 761 files.remove(fileName) 762 files.insert(0, fileName) 763 recentCount = globalSettings.recentDocumentsCount 764 if len(files) > recentCount: 765 del files[recentCount:] 766 writeListToSettings("recentFileList", files) 767 768 def createNew(self, text=None): 769 self.createTab("") 770 self.ind = self.tabWidget.count()-1 771 self.tabWidget.setCurrentIndex(self.ind) 772 if text: 773 self.currentTab.editBox.textCursor().insertText(text) 774 775 def switchTab(self, shift=1): 776 self.tabWidget.setCurrentIndex((self.ind + shift) % self.tabWidget.count()) 777 778 def updateRecentFiles(self): 779 self.menuRecentFiles.clear() 780 self.recentFilesActions = [] 781 filesOld = readListFromSettings("recentFileList") 782 files = [] 783 for f in filesOld: 784 if QFile.exists(f): 785 files.append(f) 786 self.recentFilesActions.append(self.act(f, trig=self.openFunction(f))) 787 writeListToSettings("recentFileList", files) 788 for action in self.recentFilesActions: 789 self.menuRecentFiles.addAction(action) 790 791 def markupFunction(self, markup): 792 return lambda: self.setDefaultMarkup(markup) 793 794 def openFunction(self, fileName): 795 return lambda: self.openFileWrapper(fileName) 796 797 def extensionFunction(self, data): 798 return lambda: \ 799 self.runExtensionCommand(data['Exec'], data['FileFilter'], data['DefaultExtension']) 800 801 def getExportExtensionsList(self): 802 extensions = [] 803 for extsprefix in datadirs: 804 extsdir = QDir(extsprefix+'/export-extensions/') 805 if extsdir.exists(): 806 for fileInfo in extsdir.entryInfoList(['*.desktop', '*.ini'], 807 QDir.Filter.Files | QDir.Filter.Readable): 808 extensions.append(self.readExtension(fileInfo.filePath())) 809 locale = QLocale.system().name() 810 self.extensionActions = [] 811 for extension in extensions: 812 try: 813 if ('Name[%s]' % locale) in extension: 814 name = extension['Name[%s]' % locale] 815 elif ('Name[%s]' % locale.split('_')[0]) in extension: 816 name = extension['Name[%s]' % locale.split('_')[0]] 817 else: 818 name = extension['Name'] 819 data = {} 820 for prop in ('FileFilter', 'DefaultExtension', 'Exec'): 821 if 'X-ReText-'+prop in extension: 822 data[prop] = extension['X-ReText-'+prop] 823 elif prop in extension: 824 data[prop] = extension[prop] 825 else: 826 data[prop] = '' 827 action = self.act(name, trig=self.extensionFunction(data)) 828 if 'Icon' in extension: 829 action.setIcon(self.actIcon(extension['Icon'])) 830 mimetype = extension['MimeType'] if 'MimeType' in extension else None 831 except KeyError: 832 print('Failed to parse extension: Name is required', file=sys.stderr) 833 else: 834 self.extensionActions.append((action, mimetype)) 835 836 def updateExtensionsVisibility(self): 837 markupClass = self.currentTab.getActiveMarkupClass() 838 for action in self.extensionActions: 839 if markupClass is None: 840 action[0].setEnabled(False) 841 continue 842 mimetype = action[1] 843 if mimetype is None: 844 enabled = True 845 elif markupClass == markups.MarkdownMarkup: 846 enabled = (mimetype == "text/markdown") 847 elif markupClass == markups.ReStructuredTextMarkup: 848 enabled = (mimetype == "text/x-rst") 849 else: 850 enabled = False 851 action[0].setEnabled(enabled) 852 853 def readExtension(self, fileName): 854 extFile = QFile(fileName) 855 extFile.open(QIODevice.OpenModeFlag.ReadOnly) 856 extension = {} 857 stream = QTextStream(extFile) 858 while not stream.atEnd(): 859 line = stream.readLine() 860 if '=' in line: 861 index = line.index('=') 862 extension[line[:index].rstrip()] = line[index+1:].lstrip() 863 extFile.close() 864 return extension 865 866 def openFile(self): 867 supportedExtensions = ['.txt'] 868 for markup in markups.get_all_markups(): 869 supportedExtensions += markup.file_extensions 870 fileFilter = ' (' + str.join(' ', ['*'+ext for ext in supportedExtensions]) + ');;' 871 fileNames = QFileDialog.getOpenFileNames(self, 872 self.tr("Select one or several files to open"), QDir.currentPath(), 873 self.tr("Supported files") + fileFilter + self.tr("All files (*)")) 874 for fileName in fileNames[0]: 875 self.openFileWrapper(fileName) 876 877 @pyqtSlot(str) 878 def openFileWrapper(self, fileName): 879 if not fileName: 880 return 881 fileName = QFileInfo(fileName).canonicalFilePath() 882 exists = False 883 for i, tab in enumerate(self.iterateTabs()): 884 if tab.fileName == fileName: 885 exists = True 886 ex = i 887 if exists: 888 self.tabWidget.setCurrentIndex(ex) 889 elif QFile.exists(fileName): 890 noEmptyTab = ( 891 (self.ind is None) or 892 self.currentTab.fileName or 893 self.currentTab.editBox.toPlainText() or 894 self.currentTab.editBox.document().isModified() 895 ) 896 if noEmptyTab: 897 self.createTab(fileName) 898 self.ind = self.tabWidget.count()-1 899 self.tabWidget.setCurrentIndex(self.ind) 900 if fileName: 901 self.fileSystemWatcher.addPath(fileName) 902 self.currentTab.readTextFromFile(fileName) 903 self.moveToTopOfRecentFileList(self.currentTab.fileName) 904 905 def showEncodingDialog(self): 906 if not self.maybeSave(self.ind): 907 return 908 codecsSet = set(bytes(QTextCodec.codecForName(alias).name()) 909 for alias in QTextCodec.availableCodecs()) 910 encoding, ok = QInputDialog.getItem(self, '', 911 self.tr('Select file encoding from the list:'), 912 [bytes(b).decode() for b in sorted(codecsSet)], 913 0, False) 914 if ok: 915 self.currentTab.readTextFromFile(None, encoding) 916 917 def saveFileAs(self): 918 self.saveFile(dlg=True) 919 920 def saveAll(self): 921 for tab in self.iterateTabs(): 922 if (tab.fileName and tab.editBox.document().isModified() 923 and QFileInfo(tab.fileName).isWritable()): 924 tab.saveTextToFile() 925 926 def saveFile(self, dlg=False): 927 fileNameToSave = self.currentTab.fileName 928 929 if (not fileNameToSave) or dlg: 930 proposedFileName = "" 931 markupClass = self.currentTab.getActiveMarkupClass() 932 if (markupClass is None) or not hasattr(markupClass, 'default_extension'): 933 defaultExt = self.tr("Plain text (*.txt)") 934 ext = ".txt" 935 else: 936 defaultExt = self.tr('%s files', 937 'Example of final string: Markdown files') \ 938 % markupClass.name + ' (' + str.join(' ', 939 ('*'+extension for extension in markupClass.file_extensions)) + ')' 940 if markupClass == markups.MarkdownMarkup: 941 ext = globalSettings.markdownDefaultFileExtension 942 elif markupClass == markups.ReStructuredTextMarkup: 943 ext = globalSettings.restDefaultFileExtension 944 else: 945 ext = markupClass.default_extension 946 if fileNameToSave is not None: 947 proposedFileName = fileNameToSave 948 fileNameToSave = QFileDialog.getSaveFileName(self, 949 self.tr("Save file"), proposedFileName, defaultExt)[0] 950 if fileNameToSave: 951 if not QFileInfo(fileNameToSave).suffix(): 952 fileNameToSave += ext 953 # Make sure we don't overwrite a file opened in other tab 954 for tab in self.iterateTabs(): 955 if tab is not self.currentTab and tab.fileName == fileNameToSave: 956 QMessageBox.warning(self, "", 957 self.tr("Cannot save to file which is open in another tab!")) 958 return False 959 self.actionSetEncoding.setDisabled(self.autoSaveActive()) 960 if fileNameToSave: 961 if self.currentTab.saveTextToFile(fileNameToSave): 962 self.moveToTopOfRecentFileList(self.currentTab.fileName) 963 return True 964 else: 965 QMessageBox.warning(self, '', 966 self.tr("Cannot save to file because it is read-only!")) 967 return False 968 969 def saveHtml(self, fileName): 970 if not QFileInfo(fileName).suffix(): 971 fileName += ".html" 972 try: 973 _, htmltext, _ = self.currentTab.getDocumentForExport(webenv=True) 974 except Exception: 975 return self.printError() 976 htmlFile = QFile(fileName) 977 result = htmlFile.open(QIODevice.OpenModeFlag.WriteOnly) 978 if not result: 979 QMessageBox.warning(self, '', 980 self.tr("Cannot save to file because it is read-only!")) 981 return 982 html = QTextStream(htmlFile) 983 if globalSettings.defaultCodec: 984 html.setCodec(globalSettings.defaultCodec) 985 html << htmltext 986 htmlFile.close() 987 988 def textDocument(self, title, htmltext): 989 td = QTextDocument() 990 td.setMetaInformation(QTextDocument.MetaInformation.DocumentTitle, title) 991 td.setHtml(htmltext) 992 td.setDefaultFont(globalSettings.font) 993 return td 994 995 def saveOdf(self): 996 title, htmltext, _ = self.currentTab.getDocumentForExport() 997 try: 998 document = self.textDocument(title, htmltext) 999 except Exception: 1000 return self.printError() 1001 fileName = QFileDialog.getSaveFileName(self, 1002 self.tr("Export document to ODT"), self.currentTab.getBaseName() + ".odt", 1003 self.tr("OpenDocument text files (*.odt)"))[0] 1004 if not QFileInfo(fileName).suffix(): 1005 fileName += ".odt" 1006 writer = QTextDocumentWriter(fileName) 1007 writer.setFormat(b"odf") 1008 writer.write(document) 1009 1010 def saveFileHtml(self): 1011 fileName = QFileDialog.getSaveFileName(self, 1012 self.tr("Save file"), self.currentTab.getBaseName() + ".html", 1013 self.tr("HTML files (*.html *.htm)"))[0] 1014 if fileName: 1015 self.saveHtml(fileName) 1016 1017 def getDocumentForPrint(self, title, htmltext, preview): 1018 if globalSettings.useWebKit: 1019 return preview 1020 try: 1021 return self.textDocument(title, htmltext) 1022 except Exception: 1023 self.printError() 1024 1025 def standardPrinter(self, title): 1026 printer = QPrinter(QPrinter.PrinterMode.HighResolution) 1027 printer.setDocName(title) 1028 printer.setCreator('ReText %s' % app_version) 1029 if globalSettings.paperSize: 1030 pageSize = self.getPageSizeByName(globalSettings.paperSize) 1031 if pageSize is not None: 1032 printer.setPaperSize(pageSize) 1033 else: 1034 QMessageBox.warning(self, '', 1035 self.tr('Unrecognized paperSize setting "%s".') % 1036 globalSettings.paperSize) 1037 return printer 1038 1039 def getPageSizeByName(self, pageSizeName): 1040 """ Returns a validated PageSize instance corresponding to the given 1041 name. Returns None if the name is not a valid PageSize. 1042 """ 1043 pageSize = None 1044 1045 lowerCaseNames = {pageSize.lower(): pageSize for pageSize in 1046 self.availablePageSizes()} 1047 if pageSizeName.lower() in lowerCaseNames: 1048 pageSize = getattr(QPagedPaintDevice, lowerCaseNames[pageSizeName.lower()]) 1049 1050 return pageSize 1051 1052 def availablePageSizes(self): 1053 """ List available page sizes. """ 1054 1055 sizes = [x for x in dir(QPagedPaintDevice) 1056 if type(getattr(QPagedPaintDevice, x)) == QPagedPaintDevice.PageSize] 1057 return sizes 1058 1059 def savePdf(self): 1060 fileName = QFileDialog.getSaveFileName(self, 1061 self.tr("Export document to PDF"), 1062 self.currentTab.getBaseName() + ".pdf", 1063 self.tr("PDF files (*.pdf)"))[0] 1064 if fileName: 1065 if not QFileInfo(fileName).suffix(): 1066 fileName += ".pdf" 1067 title, htmltext, preview = self.currentTab.getDocumentForExport() 1068 if globalSettings.useWebEngine: 1069 pageSize = self.getPageSizeByName(globalSettings.paperSize) 1070 if pageSize is None: 1071 pageSize = QPageSize(QPageSize.PageSizeId.A4) 1072 margins = QMarginsF(20, 20, 13, 20) # left, top, right, bottom (in millimeters) 1073 layout = QPageLayout(pageSize, QPageLayout.Orientation.Portrait, margins, QPageLayout.Unit.Millimeter) 1074 preview.page().printToPdf(fileName, layout) 1075 return 1076 printer = self.standardPrinter(title) 1077 printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat) 1078 printer.setOutputFileName(fileName) 1079 document = self.getDocumentForPrint(title, htmltext, preview) 1080 if document != None: 1081 document.print(printer) 1082 1083 def printFile(self): 1084 title, htmltext, preview = self.currentTab.getDocumentForExport() 1085 printer = self.standardPrinter(title) 1086 dlg = QPrintDialog(printer, self) 1087 dlg.setWindowTitle(self.tr("Print document")) 1088 if (dlg.exec() == QDialog.DialogCode.Accepted): 1089 document = self.getDocumentForPrint(title, htmltext, preview) 1090 if document != None: 1091 document.print(printer) 1092 1093 def printPreview(self): 1094 title, htmltext, preview = self.currentTab.getDocumentForExport() 1095 document = self.getDocumentForPrint(title, htmltext, preview) 1096 if document is None: 1097 return 1098 printer = self.standardPrinter(title) 1099 preview = QPrintPreviewDialog(printer, self) 1100 preview.paintRequested.connect(document.print) 1101 preview.exec() 1102 1103 def runExtensionCommand(self, command, filefilter, defaultext): 1104 import shlex 1105 of = ('%of' in command) 1106 html = ('%html' in command) 1107 if of: 1108 if defaultext and not filefilter: 1109 filefilter = '*'+defaultext 1110 fileName = QFileDialog.getSaveFileName(self, 1111 self.tr('Export document'), '', filefilter)[0] 1112 if not fileName: 1113 return 1114 if defaultext and not QFileInfo(fileName).suffix(): 1115 fileName += defaultext 1116 else: 1117 fileName = 'out' + defaultext 1118 basename = '.%s.retext-temp' % self.currentTab.getBaseName() 1119 if html: 1120 tmpname = basename+'.html' 1121 self.saveHtml(tmpname) 1122 else: 1123 tmpname = basename + self.currentTab.getActiveMarkupClass().default_extension 1124 self.currentTab.writeTextToFile(tmpname) 1125 command = command.replace('%of', shlex.quote(fileName)) 1126 command = command.replace('%html' if html else '%if', shlex.quote(tmpname)) 1127 try: 1128 Popen(str(command), shell=True).wait() 1129 except Exception as error: 1130 errorstr = str(error) 1131 QMessageBox.warning(self, '', self.tr('Failed to execute the command:') 1132 + '\n' + errorstr) 1133 QFile(tmpname).remove() 1134 1135 def autoSaveActive(self, tab=None): 1136 tab = tab if tab else self.currentTab 1137 return bool(self.autoSaveEnabled and tab.fileName and 1138 QFileInfo(tab.fileName).isWritable()) 1139 1140 def clipboardDataChanged(self): 1141 mimeData = QApplication.instance().clipboard().mimeData() 1142 if mimeData is not None: 1143 self.actionPaste.setEnabled(mimeData.hasText()) 1144 self.actionPasteImage.setEnabled(mimeData.hasImage()) 1145 1146 def insertFormatting(self, formatting): 1147 if formatting == 'table': 1148 dialog = InsertTableDialog(self) 1149 dialog.show() 1150 self.formattingBox.setCurrentIndex(0) 1151 return 1152 1153 cursor = self.currentTab.editBox.textCursor() 1154 text = cursor.selectedText() 1155 moveCursorTo = None 1156 1157 def c(cursor): 1158 nonlocal moveCursorTo 1159 moveCursorTo = cursor.position() 1160 1161 def ensurenl(cursor): 1162 if not cursor.atBlockStart(): 1163 cursor.insertText('\n\n') 1164 1165 toinsert = { 1166 'header': (ensurenl, '# ', text), 1167 'italic': ('*', text, c, '*'), 1168 'bold': ('**', text, c, '**'), 1169 'underline': ('<u>', text, c, '</u>'), 1170 'numbering': (ensurenl, ' 1. ', text), 1171 'bullets': (ensurenl, ' * ', text), 1172 'image': ('![', text or self.tr('Alt text'), c, '](', self.tr('URL'), ')'), 1173 'link': ('[', text or self.tr('Link text'), c, '](', self.tr('URL'), ')'), 1174 'inline code': ('`', text, c, '`'), 1175 'code block': (ensurenl, ' ', text), 1176 'blockquote': (ensurenl, '> ', text), 1177 } 1178 1179 if formatting not in toinsert: 1180 return 1181 1182 cursor.beginEditBlock() 1183 for token in toinsert[formatting]: 1184 if callable(token): 1185 token(cursor) 1186 else: 1187 cursor.insertText(token) 1188 cursor.endEditBlock() 1189 1190 self.formattingBox.setCurrentIndex(0) 1191 # Bring back the focus on the editor 1192 self.currentTab.editBox.setFocus(Qt.FocusReason.OtherFocusReason) 1193 1194 if moveCursorTo: 1195 cursor.setPosition(moveCursorTo) 1196 self.currentTab.editBox.setTextCursor(cursor) 1197 1198 def insertSymbol(self, num): 1199 if num: 1200 self.currentTab.editBox.insertPlainText('&'+self.usefulChars[num-1]+';') 1201 self.symbolBox.setCurrentIndex(0) 1202 1203 def fileChanged(self, fileName): 1204 tab = None 1205 for testtab in self.iterateTabs(): 1206 if testtab.fileName == fileName: 1207 tab = testtab 1208 if tab is None: 1209 self.fileSystemWatcher.removePath(fileName) 1210 return 1211 if not QFile.exists(fileName): 1212 self.tabWidget.setCurrentWidget(tab) 1213 tab.editBox.document().setModified(True) 1214 QMessageBox.warning(self, '', self.tr( 1215 'This file has been deleted by other application.\n' 1216 'Please make sure you save the file before exit.')) 1217 elif not tab.editBox.document().isModified(): 1218 # File was not modified in ReText, reload silently 1219 tab.readTextFromFile() 1220 else: 1221 self.tabWidget.setCurrentWidget(tab) 1222 text = self.tr( 1223 'This document has been modified by other application.\n' 1224 'Do you want to reload the file (this will discard all ' 1225 'your changes)?\n') 1226 if self.autoSaveEnabled: 1227 text += self.tr( 1228 'If you choose to not reload the file, auto save mode will ' 1229 'be disabled for this session to prevent data loss.') 1230 messageBox = QMessageBox(QMessageBox.Icon.Warning, '', text) 1231 reloadButton = messageBox.addButton(self.tr('Reload'), QMessageBox.ButtonRole.YesRole) 1232 messageBox.addButton(QMessageBox.StandardButton.Cancel) 1233 messageBox.exec() 1234 if messageBox.clickedButton() is reloadButton: 1235 tab.readTextFromFile() 1236 else: 1237 self.autoSaveEnabled = False 1238 tab.editBox.document().setModified(True) 1239 if fileName not in self.fileSystemWatcher.files(): 1240 # https://github.com/retext-project/retext/issues/137 1241 self.fileSystemWatcher.addPath(fileName) 1242 1243 def maybeSave(self, ind): 1244 tab = self.tabWidget.widget(ind) 1245 if self.autoSaveActive(tab): 1246 tab.saveTextToFile() 1247 return True 1248 if not tab.editBox.document().isModified(): 1249 return True 1250 self.tabWidget.setCurrentIndex(ind) 1251 ret = QMessageBox.warning(self, '', 1252 self.tr("The document has been modified.\nDo you want to save your changes?"), 1253 QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel) 1254 if ret == QMessageBox.StandardButton.Save: 1255 return self.saveFile(False) 1256 elif ret == QMessageBox.StandardButton.Cancel: 1257 return False 1258 return True 1259 1260 def closeEvent(self, closeevent): 1261 for ind in range(self.tabWidget.count()): 1262 if not self.maybeSave(ind): 1263 return closeevent.ignore() 1264 if globalSettings.saveWindowGeometry: 1265 globalSettings.windowGeometry = self.saveGeometry() 1266 if globalSettings.openLastFilesOnStartup: 1267 files = [tab.fileName for tab in self.iterateTabs()] 1268 writeListToSettings("lastFileList", files) 1269 globalSettings.lastTabIndex = self.tabWidget.currentIndex() 1270 closeevent.accept() 1271 1272 def viewHtml(self): 1273 htmlDlg = HtmlDialog(self) 1274 try: 1275 _, htmltext, _ = self.currentTab.getDocumentForExport(includeStyleSheet=False) 1276 except Exception: 1277 return self.printError() 1278 winTitle = self.currentTab.getBaseName() 1279 htmlDlg.setWindowTitle(winTitle+" ("+self.tr("HTML code")+")") 1280 htmlDlg.textEdit.setPlainText(htmltext.rstrip()) 1281 htmlDlg.hl.rehighlight() 1282 htmlDlg.show() 1283 htmlDlg.raise_() 1284 htmlDlg.activateWindow() 1285 1286 def insertImages(self): 1287 supportedExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.bmp'] 1288 fileFilter = ' (%s);;' % ' '.join('*' + ext for ext in supportedExtensions) 1289 fileNames, _selectedFilter = QFileDialog.getOpenFileNames(self, 1290 self.tr("Select one or several images to open"), QDir.currentPath(), 1291 self.tr("Supported files") + fileFilter + self.tr("All files (*)")) 1292 1293 cursor = self.currentTab.editBox.textCursor() 1294 1295 imagesMarkup = '\n'.join( 1296 self.currentTab.editBox.getImageMarkup(fileName) 1297 for fileName in fileNames) 1298 cursor.insertText(imagesMarkup) 1299 1300 self.formattingBox.setCurrentIndex(0) 1301 self.currentTab.editBox.setFocus(Qt.FocusReason.OtherFocusReason) 1302 1303 def openHelp(self): 1304 QDesktopServices.openUrl(QUrl('https://github.com/retext-project/retext/wiki')) 1305 1306 def aboutDialog(self): 1307 QMessageBox.about(self, self.aboutWindowTitle, 1308 '<p><b>' + (self.tr('ReText %s (using PyMarkups %s)') % (app_version, markups.__version__)) 1309 +'</b></p>' + self.tr('Simple but powerful editor' 1310 ' for Markdown and reStructuredText') 1311 +'</p><p>'+self.tr('Author: Dmitry Shachnev, 2011').replace('2011', '2011–2021') 1312 +'<br><a href="https://github.com/retext-project/retext">GitHub</a> | ' 1313 +'<a href="https://daringfireball.net/projects/markdown/syntax">' 1314 +self.tr('Markdown syntax') 1315 +'</a> | <a href="https://docutils.sourceforge.io/docs/user/rst/quickref.html">' 1316 +self.tr('reStructuredText syntax')+'</a></p>') 1317 1318 def setDefaultMarkup(self, markupClass): 1319 globalSettings.defaultMarkup = markupClass.name 1320 for tab in self.iterateTabs(): 1321 if not tab.fileName: 1322 tab.updateActiveMarkupClass() 1323