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