# -*- coding: utf-8 -*-
# Licensed under EUPL v1.2
# © 2020 bicobus <bicobus@keemail.me>
"""Constructors for the setting window.
Some of the design and code were influenced by `Spyder Ide <https://www.spyder-ide.org/>`_. Spyder
IDE is released under MIT.
The setting window has two major elements:
1. A side bar on the left listing the different pages
2. A content area on the right with the selected page widgets
+------------+-----------------+
| Names | Sections |
+============+=================+
| Page name | - Page widget |
+------------+ - Page widget |
| Page name | - Page widget |
+------------+ - Page widget |
| Page name | - Page widget |
+------------+-----------------+
| Yes / No|
+------------------------------+
"""
from PyQt5.QtCore import QSize, Qt, pyqtSignal # , pyqtSlot
from PyQt5.QtWidgets import (
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
QFileDialog,
QGridLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QListView,
QListWidget,
QListWidgetItem,
QMessageBox,
QPushButton,
QScrollArea,
QSplitter,
QStackedWidget,
QVBoxLayout,
QWidget,
)
from qmm.common import settings
from qmm.settings.validators import IsDirValidator, make_html_list
# Horizontal:
# | 0 | 1 | 2
# +---+---+---
# 0 | L | W | W+
# 1 | \ | H | \
#
# Vertical:
# | 0 | 1 | 2
# +---+---+---
# 0 | L | \ |
# 1 | W | H |
# 2 | W+| \ |
#
[docs]def make_verbose_layout(parent, align, helper, *widgets):
"""Generate a GridLayout, insert extra helper widget on the second row.
The helper widget is supposed to be a `QLabel`.
Tries to respect `align`.
"""
layout = QGridLayout(parent)
for idx, widget in enumerate(widgets):
if align == Qt.Vertical:
layout.addWidget(widget, idx, 0) # row, col
else:
layout.addWidget(widget, 0, idx)
layout.addWidget(helper, 1, 1)
layout.setContentsMargins(0, 0, 0, 0)
return layout
[docs]def make_layout(parent, align, *widgets):
"""Generate a box layout depending of `align`."""
layout = QVBoxLayout(parent) if align == Qt.Vertical else QHBoxLayout(parent)
for widget in widgets:
layout.addWidget(widget)
layout.setContentsMargins(0, 0, 0, 0)
return layout
[docs]class Page(QWidget):
NAME = None
ICON = None
show_this_page = pyqtSignal()
def __init__(self, parent, verbose_mode=False):
super().__init__(parent)
self.main = parent
self._verbose = bool(verbose_mode)
self.pages = {}
self.validators = {}
self.lineedits = {}
self.comboboxes = {}
self.init_page()
[docs] def init_page(self):
self.setup_ui()
self.load_configuration()
[docs] def setup_ui(self):
raise NotImplementedError()
[docs] def get_name(self):
return self.NAME
[docs] def get_icon(self):
return self.ICON
[docs] def load_configuration(self):
for widget, confkey in self.lineedits.items():
try:
text = settings[confkey]
except KeyError:
text = ""
widget.setText(text)
for combobox, confkey in self.comboboxes.items():
try:
data = settings[confkey]
except KeyError:
data = None
idx = combobox.findData(data)
if idx != -1:
combobox.setCurrentIndex(idx)
[docs] def save(self):
changed_elements = []
restart_required = False
for lineedit, confkey in self.lineedits.items():
data = lineedit.text()
if data != settings[confkey]:
settings[confkey] = data
changed_elements.append(lineedit)
if lineedit.restart_needed:
restart_required = True
for combobox, confkey in self.comboboxes.items():
data = combobox.itemData(combobox.currentIndex())
if data != settings[confkey]:
settings[confkey] = data
changed_elements.append(combobox)
if combobox.restart_required:
restart_required = True
if restart_required:
self.prompt_restart_required(changed_elements)
[docs] def validate(self):
# NOTE: validation might only work for text input, either QLineEdit or
# QPlainTextEdit
for widget, validators in self.validators.items():
msgs = []
if not isinstance(validators, list):
validators = [validators]
for validator in validators:
try:
validator(widget.text())
except ValueError as e:
msgs.append(e.args[0])
if msgs:
QMessageBox.critical(
self,
self.get_name(),
"{}:<br><b>{}</b>".format(widget.text(), make_html_list(msgs)),
)
return False
return True
[docs] def c_lineedit(self, text, confkey, restart=False, **qtparams):
wordwrap = qtparams.setdefault("wordwrap", False)
alignment = qtparams.setdefault("alignment", Qt.Vertical)
tip = qtparams.setdefault("tip", None)
placeholder = qtparams.setdefault("placeholder", None)
label = QLabel(text)
label.setWordWrap(wordwrap)
edit = QLineEdit()
if self._verbose:
htedit = QLabel(tip)
htedit.setWordWrap(True)
layout = make_verbose_layout(self, alignment, htedit, label, edit)
else:
layout = make_layout(self, alignment, label, edit)
if tip:
edit.setToolTip(tip)
if placeholder:
edit.setPlaceholderText(placeholder)
self.lineedits[edit] = confkey
widget = QWidget(self)
widget.label = label
widget.textbox = edit
widget.setLayout(layout)
edit.label_text = text
edit.restart_needed = restart
return widget
[docs] def c_browsedir(self, text, confkey, tip=None, restart=False, placeholder=None):
widget = self.c_lineedit(
text, confkey, tip=tip, alignment=Qt.Horizontal, placeholder=placeholder,
restart=restart
)
edit = None
for edit in self.lineedits:
if widget.isAncestorOf(edit):
break
if not edit:
return None
self.validators[edit] = [IsDirValidator]
button = QPushButton(self)
button.setText(_("Browse..."))
button.setToolTip(_("Select a directory"))
button.clicked.connect(lambda: self.select_directory(edit))
layout = make_layout(self, Qt.Horizontal, widget, button)
browsedir = QWidget(self)
browsedir.setLayout(layout)
return browsedir
[docs] def select_directory(self, lineedit):
path = lineedit.text()
value = QFileDialog.getExistingDirectory(
parent=self,
caption=_("Select a directory"), # caption := window title
directory=path,
)
lineedit.setText(value)
[docs] def c_combobox(self, text, choices, confkey, restart=False, tip=None):
label = QLabel(text)
combobox = QComboBox()
combobox.restart_required = restart
combobox.label_text = text
if tip:
combobox.setToolTip(tip)
for name, key in choices:
if name and key:
combobox.addItem(name, key)
i = 0
for idx, item in enumerate(choices):
name, key = item
if not name and not key:
combobox.insertSeparator(idx + i)
i += 1
self.comboboxes[combobox] = confkey
layout = make_layout(self, Qt.Horizontal, label, combobox)
layout.addStretch(1)
widget = QWidget(self)
widget.label = label
widget.combobox = combobox
widget.setLayout(layout)
return widget
[docs] def prompt_restart_required(self, changed_elements):
"""Prompt user to restart software."""
if len(changed_elements) == 1:
msg_start = _(
"QModManager needs to be restarted in order to apply the following setting:"
)
else:
msg_start = _(
"QModManager needs to be restarted in order to apply the following settings:"
)
msg_elements = make_html_list([x.label_text for x in changed_elements])
title = _("Restart needed")
msg = "{0}{1}".format(msg_start, msg_elements)
QMessageBox.information(self, title, msg, QMessageBox.Ok)
[docs]class PreferencesDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.main = parent
self.pages_widget = QStackedWidget()
self.pages_widget.setMinimumSize(600, 0)
self.contents_widget = QListWidget()
bbox = QDialogButtonBox(
QDialogButtonBox.Ok | QDialogButtonBox.Apply | QDialogButtonBox.Cancel
)
self.apply_btn = bbox.button(QDialogButtonBox.Apply)
self.ok_btn = bbox.button(QDialogButtonBox.Ok)
qhelp = QCheckBox(_("Enable Descriptive Help"), self)
self.qhelp_ckb = qhelp
try:
qhelp.setChecked(settings["ck_descriptive_text"])
except KeyError:
qhelp.setChecked(False)
self.setAttribute(Qt.WA_DeleteOnClose)
self.setWindowTitle(_("Settings"))
self.contents_widget.setMovement(QListView.Static)
self.contents_widget.setSpacing(1)
self.contents_widget.setCurrentRow(0)
self.contents_widget.setMinimumWidth(220)
self.contents_widget.setMinimumHeight(400)
hsplitter = QSplitter()
hsplitter.addWidget(self.contents_widget)
hsplitter.addWidget(self.pages_widget)
hsplitter.setStretchFactor(0, 1)
hsplitter.setStretchFactor(1, 2)
btnlayout = QHBoxLayout()
btnlayout.addWidget(qhelp)
btnlayout.addStretch(1)
btnlayout.addWidget(bbox)
vlayout = QVBoxLayout()
vlayout.addWidget(hsplitter)
vlayout.addLayout(btnlayout)
self.setLayout(vlayout)
bbox.accepted.connect(self.accept)
bbox.rejected.connect(self.reject)
bbox.clicked.connect(self.button_clicked)
qhelp.stateChanged.connect(self.checkbox_toggled)
self.contents_widget.currentRowChanged.connect(self.pages_widget.setCurrentIndex)
[docs] def accept(self):
"""Go through all the pages and save everything."""
for idx in range(0, self.pages_widget.count()):
page = self.get_page(idx)
if not page.validate():
return
page.save()
super().accept()
[docs] def checkbox_toggled(self, checkbox):
"""Enable descriptive help."""
if checkbox == Qt.Checked:
QMessageBox.information(
self,
_("Descriptive Text"),
_(
"Enabling this option will tell the software to use a different build process "
"for each element and embed a descriptive information alongside them.\n"
"The setting window will be required to be closed then reopened after enabling "
"or disabling this option."
),
QMessageBox.Ok,
)
settings["ck_descriptive_text"] = True
return
settings["ck_descriptive_text"] = False
[docs] def add_page(self, widget):
widget.show_this_page.connect(
lambda row=self.contents_widget.count(): self.contents_widget.setCurrentRow(row)
)
scrollarea = QScrollArea(self)
scrollarea.setWidgetResizable(True)
scrollarea.setWidget(widget)
self.pages_widget.addWidget(scrollarea)
page_row = QListWidgetItem(self.contents_widget)
page_row.setText(widget.get_name())
page_row.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
page_row.setSizeHint(QSize(0, 25))
[docs] def get_page(self, index=None):
if not index:
widget = self.pages_widget.currentWidget()
else:
widget = self.pages_widget.widget(index)
if widget:
return widget.widget()
return None