1# This file is part of ReText 2# Copyright: 2013-2021 Dmitry Shachnev 3# 4# This program is free software; you can redistribute it and/or modify 5# it under the terms of the GNU General Public License as published by 6# the Free Software Foundation; either version 2 of the License, or 7# (at your option) any later version. 8# 9# This program is distributed in the hope that it will be useful, 10# but WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12# GNU General Public License for more details. 13# 14# You should have received a copy of the GNU General Public License 15# along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17import sys 18from ReText import globalSettings, getBundledIcon, getSettingsFilePath 19from ReText.icontheme import get_icon_theme 20from markups.common import CONFIGURATION_DIR 21from os.path import join 22 23from PyQt5.QtCore import pyqtSignal, QFile, QFileInfo, QUrl, Qt 24from PyQt5.QtGui import QDesktopServices, QIcon 25from PyQt5.QtWidgets import QCheckBox, QDialog, QDialogButtonBox, \ 26 QFileDialog, QGridLayout, QLabel, QLineEdit, QPushButton, QSpinBox, \ 27 QComboBox, QTabWidget, QVBoxLayout, QWidget 28 29MKD_EXTS_FILE = join(CONFIGURATION_DIR, 'markdown-extensions.txt') 30 31class FileDialogButton(QPushButton): 32 def __init__(self, parent, fileName): 33 QPushButton.__init__(self, parent) 34 self.fileName = fileName 35 self.defaultText = self.tr('(none)') 36 self.updateButtonText() 37 self.clicked.connect(self.processClick) 38 39 def processClick(self): 40 pass 41 42 def updateButtonText(self): 43 if self.fileName: 44 self.setText(QFileInfo(self.fileName).fileName()) 45 else: 46 self.setText(self.defaultText) 47 48class FileSelectButton(FileDialogButton): 49 def processClick(self): 50 startDir = (QFileInfo(self.fileName).absolutePath() 51 if self.fileName else '') 52 self.fileName = QFileDialog.getOpenFileName( 53 self, self.tr('Select file to open'), startDir)[0] 54 self.updateButtonText() 55 56class DirectorySelectButton(FileDialogButton): 57 def processClick(self): 58 startDir = (QFileInfo(self.fileName).absolutePath() 59 if self.fileName else '') 60 self.fileName = QFileDialog.getExistingDirectory( 61 self, self.tr('Select directory to open'), startDir) 62 self.updateButtonText() 63 64class ClickableLabel(QLabel): 65 clicked = pyqtSignal() 66 67 def mousePressEvent(self, event): 68 self.clicked.emit() 69 super().mousePressEvent(event) 70 71 72def setIconThemeFromSettings(): 73 QIcon.setThemeName(globalSettings.iconTheme) 74 if QIcon.themeName() in ('hicolor', ''): 75 if not QFile.exists(getBundledIcon('document-new')): 76 QIcon.setThemeName(get_icon_theme()) 77 if QIcon.themeName() == 'Yaru' and not QIcon.hasThemeIcon('document-new'): 78 # Old Yaru does not have non-symbolic action icons, so all 79 # document-* icons fall back to mimetypes/document.png. 80 # See https://github.com/ubuntu/yaru/issues/1294 81 QIcon.setThemeName('Humanity') 82 83 84class ConfigDialog(QDialog): 85 def __init__(self, parent): 86 QDialog.__init__(self, parent) 87 self.parent = parent 88 self.initConfigOptions() 89 self.layout = QVBoxLayout(self) 90 path = getSettingsFilePath() 91 pathLabel = QLabel(self.tr('Using configuration file at:') + 92 ' <a href="%(path)s">%(path)s</a>' % {'path': path}, self) 93 pathLabel.linkActivated.connect(self.openLink) 94 self.layout.addWidget(pathLabel) 95 self.tabWidget = QTabWidget(self) 96 self.layout.addWidget(self.tabWidget) 97 buttonBox = QDialogButtonBox(self) 98 buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Ok | 99 QDialogButtonBox.StandardButton.Apply | QDialogButtonBox.StandardButton.Cancel) 100 buttonBox.accepted.connect(self.acceptSettings) 101 buttonBox.button(QDialogButtonBox.StandardButton.Apply).clicked.connect(self.saveSettings) 102 buttonBox.rejected.connect(self.close) 103 self.initWidgets() 104 self.configurators['rightMargin'].valueChanged.connect(self.handleRightMarginSet) 105 self.configurators['rightMarginWrap'].stateChanged.connect(self.handleRightMarginWrapSet) 106 self.layout.addWidget(buttonBox) 107 108 def initConfigOptions(self): 109 self.tabs = ( 110 (self.tr('Behavior'), ( 111 (self.tr('Automatically save documents'), 'autoSave'), 112 (self.tr('Automatically open last documents on startup'), 'openLastFilesOnStartup'), 113 (self.tr('Number of recent documents'), 'recentDocumentsCount'), 114 (self.tr('Restore window geometry'), 'saveWindowGeometry'), 115 (self.tr('Default preview state'), 'defaultPreviewState'), 116 (self.tr('Open external links in ReText window'), 'handleWebLinks'), 117 (self.tr('Markdown syntax extensions (comma-separated)'), 'markdownExtensions'), 118 (None, 'markdownExtensions'), 119 (self.tr('Enable synchronized scrolling for Markdown'), 'syncScroll'), 120 # (self.tr('Default Markdown file extension'), 'markdownDefaultFileExtension'), 121 # (self.tr('Default reStructuredText file extension'), 'restDefaultFileExtension'), 122 )), 123 (self.tr('Editor'), ( 124 (self.tr('Highlight current line'), 'highlightCurrentLine'), 125 (self.tr('Show line numbers'), 'lineNumbersEnabled'), 126 (self.tr('Line numbers are relative to current line'), 'relativeLineNumbers'), 127 (self.tr('Tab key inserts spaces'), 'tabInsertsSpaces'), 128 (self.tr('Tabulation width'), 'tabWidth'), 129 (self.tr('Draw vertical line at column'), 'rightMargin'), 130 (self.tr('Enable soft wrap'), 'rightMarginWrap'), 131 (self.tr('Show document stats'), 'documentStatsEnabled'), 132 (self.tr('Ordered list mode'), 'orderedListMode'), 133 )), 134 (self.tr('Interface'), ( 135 (self.tr('Hide toolbar'), 'hideToolBar'), 136 (self.tr('Icon theme name'), 'iconTheme'), 137 (self.tr('Stylesheet file'), 'styleSheet', True), 138 (self.tr('Hide tabs bar when there is only one tab'), 'tabBarAutoHide'), 139 (self.tr('Show full path in window title'), 'windowTitleFullPath'), 140 (self.tr('Show directory tree'), 'showDirectoryTree', False), 141 (self.tr('Working directory'), 'directoryPath', True), 142 )) 143 ) 144 145 def initWidgets(self): 146 self.configurators = {} 147 for tabTitle, options in self.tabs: 148 page = self.getPageWidget(options) 149 self.tabWidget.addTab(page, tabTitle) 150 151 def getPageWidget(self, options): 152 page = QWidget(self) 153 layout = QGridLayout(page) 154 for index, option in enumerate(options): 155 displayname, name = option[:2] 156 fileselector = option[2] if len(option) > 2 else False 157 if name is None: 158 header = QLabel('<h3>%s</h3>' % displayname, self) 159 layout.addWidget(header, index, 0, 1, 2, Qt.AlignmentFlag.AlignHCenter) 160 continue 161 if displayname: 162 label = ClickableLabel(displayname + ':', self) 163 if name == 'markdownExtensions': 164 if displayname: 165 url = QUrl('https://github.com/retext-project/retext/wiki/Markdown-extensions') 166 helpButton = QPushButton(self.tr('Help'), self) 167 helpButton.clicked.connect(lambda: QDesktopServices.openUrl(url)) 168 layout.addWidget(label, index, 0) 169 layout.addWidget(helpButton, index, 1) 170 continue 171 try: 172 extsFile = open(MKD_EXTS_FILE) 173 value = extsFile.read().rstrip().replace(extsFile.newlines, ', ') 174 extsFile.close() 175 except Exception: 176 value = '' 177 self.configurators[name] = QLineEdit(self) 178 self.configurators[name].setText(value) 179 layout.addWidget(self.configurators[name], index, 0, 1, 2) 180 continue 181 value = getattr(globalSettings, name) 182 if name == 'defaultPreviewState': 183 self.configurators[name] = QComboBox(self) 184 self.configurators[name].addItem(self.tr('Editor'), 'editor') 185 self.configurators[name].addItem(self.tr('Live preview'), 'live-preview') 186 self.configurators[name].addItem(self.tr('Normal preview'), 'normal-preview') 187 comboBoxIndex = self.configurators[name].findData(value) 188 self.configurators[name].setCurrentIndex(comboBoxIndex) 189 elif name == 'highlightCurrentLine': 190 self.configurators[name] = QComboBox(self) 191 self.configurators[name].addItem(self.tr('Disabled'), 'disabled') 192 self.configurators[name].addItem(self.tr('Cursor Line'), 'cursor-line') 193 self.configurators[name].addItem(self.tr('Wrapped Line'), 'wrapped-line') 194 comboBoxIndex = self.configurators[name].findData(value) 195 self.configurators[name].setCurrentIndex(comboBoxIndex) 196 elif name == 'orderedListMode': 197 self.configurators[name] = QComboBox(self) 198 self.configurators[name].addItem(self.tr('Increment'), 'increment') 199 self.configurators[name].addItem(self.tr('Repeat'), 'repeat') 200 comboBoxIndex = self.configurators[name].findData(value) 201 self.configurators[name].setCurrentIndex(comboBoxIndex) 202 elif name == 'directoryPath': 203 self.configurators[name] = DirectorySelectButton(self, value) 204 elif isinstance(value, bool): 205 self.configurators[name] = QCheckBox(self) 206 self.configurators[name].setChecked(value) 207 label.clicked.connect(self.configurators[name].nextCheckState) 208 elif isinstance(value, int): 209 self.configurators[name] = QSpinBox(self) 210 if name == 'tabWidth': 211 self.configurators[name].setRange(1, 10) 212 elif name == 'recentDocumentsCount': 213 self.configurators[name].setRange(5, 20) 214 else: 215 self.configurators[name].setMaximum(200) 216 self.configurators[name].setValue(value) 217 elif isinstance(value, str) and fileselector: 218 self.configurators[name] = FileSelectButton(self, value) 219 elif isinstance(value, str): 220 self.configurators[name] = QLineEdit(self) 221 self.configurators[name].setText(value) 222 layout.addWidget(label, index, 0) 223 layout.addWidget(self.configurators[name], index, 1, Qt.AlignmentFlag.AlignRight) 224 return page 225 226 def handleRightMarginSet(self, value): 227 if value < 10: 228 self.configurators['rightMarginWrap'].setChecked(False) 229 230 def handleRightMarginWrapSet(self, state): 231 if state == Qt.CheckState.Checked and self.configurators['rightMargin'].value() < 10: 232 self.configurators['rightMargin'].setValue(80) 233 234 def saveSettings(self): 235 for name, configurator in self.configurators.items(): 236 if name == 'markdownExtensions': 237 continue 238 if isinstance(configurator, QCheckBox): 239 value = configurator.isChecked() 240 elif isinstance(configurator, QSpinBox): 241 value = configurator.value() 242 elif isinstance(configurator, QLineEdit): 243 value = configurator.text() 244 elif isinstance(configurator, QComboBox): 245 value = configurator.currentData() 246 elif isinstance(configurator, FileDialogButton): 247 value = configurator.fileName 248 setattr(globalSettings, name, value) 249 self.applySettings() 250 251 def applySettings(self): 252 setIconThemeFromSettings() 253 try: 254 extsFile = open(MKD_EXTS_FILE, 'w') 255 for ext in self.configurators['markdownExtensions'].text().split(','): 256 if ext.strip(): 257 extsFile.write(ext.strip() + '\n') 258 extsFile.close() 259 except Exception as e: 260 print(e, file=sys.stderr) 261 for tab in self.parent.iterateTabs(): 262 tab.editBox.updateFont() 263 tab.editBox.setWrapModeAndWidth() 264 tab.editBox.viewport().update() 265 self.parent.updateStyleSheet() 266 self.parent.tabWidget.setTabBarAutoHide(globalSettings.tabBarAutoHide) 267 self.parent.toolBar.setVisible(not globalSettings.hideToolBar) 268 self.parent.editBar.setVisible(not globalSettings.hideToolBar) 269 self.parent.initDirectoryTree(globalSettings.showDirectoryTree, globalSettings.directoryPath) 270 271 def acceptSettings(self): 272 self.saveSettings() 273 self.close() 274 275 def openLink(self, link): 276 QDesktopServices.openUrl(QUrl.fromLocalFile(link)) 277