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