import re import shutil from logging import getLogger from typing import Dict, List from PyQt5.QtCore import pyqtSignal, QSettings, QSize, Qt, QMimeData, pyqtSlot, QCoreApplication from PyQt5.QtGui import QDrag, QDropEvent, QDragEnterEvent, QDragMoveEvent, QFont, QMouseEvent from PyQt5.QtWidgets import ( QHBoxLayout, QLabel, QPushButton, QInputDialog, QFrame, QMessageBox, QSizePolicy, QWidget, QScrollArea, ) from rare.shared import RareCore from rare.ui.components.tabs.settings.widgets.wrapper import Ui_WrapperSettings from rare.utils import config_helper from rare.utils.misc import icon logger = getLogger("WrapperSettings") extra_wrapper_regex = { "proton": "\".*proton\" run", # proton "mangohud": "mangohud" # mangohud } class Wrapper: pass class WrapperWidget(QFrame): delete_wrapper = pyqtSignal(str) def __init__(self, text: str, show_text=None, parent=None): super(WrapperWidget, self).__init__(parent=parent) if not show_text: show_text = text.split(" ")[0] self.setFrameShape(QFrame.StyledPanel) self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed) self.text = text self.text_lbl = QLabel(show_text, parent=self) self.text_lbl.setFont(QFont("monospace")) self.image_lbl = QLabel(parent=self) self.image_lbl.setPixmap(icon("mdi.drag-vertical").pixmap(QSize(20, 20))) self.setToolTip(text) self.delete_button = QPushButton(icon("ei.remove"), "", parent=self) if show_text in extra_wrapper_regex.keys(): self.delete_button.setDisabled(True) self.delete_button.setToolTip(self.tr("Disable in settings")) else: self.delete_button.setToolTip(self.tr("Remove")) self.delete_button.clicked.connect(self.delete) layout = QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.image_lbl) layout.addWidget(self.text_lbl) layout.addWidget(self.delete_button) self.setLayout(layout) # lk: set object names for the stylesheet self.setObjectName(type(self).__name__) self.delete_button.setObjectName(f"{self.objectName()}Button") def delete(self): self.delete_wrapper.emit(self.text) def mouseMoveEvent(self, e): if e.buttons() == Qt.LeftButton: drag = QDrag(self) mime = QMimeData() drag.setMimeData(mime) drag.exec_(Qt.MoveAction) def mouseDoubleClickEvent(self, a0: QMouseEvent) -> None: if a0.button() == Qt.LeftButton: a0.accept() pass # self.edit class WrapperSettings(QWidget, Ui_WrapperSettings): def __init__(self): super(WrapperSettings, self).__init__() self.setupUi(self) self.wrappers: Dict[str, WrapperWidget] = {} self.app_name: str self.wrapper_scroll = QScrollArea(self.widget_stack) self.wrapper_scroll.setWidgetResizable(True) self.wrapper_scroll.setSizeAdjustPolicy(QScrollArea.AdjustToContents) self.wrapper_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.wrapper_scroll.setProperty("no_kinetic_scroll", True) self.scroll_content = WrapperContainer( save_cb=self.save, parent=self.wrapper_scroll ) self.wrapper_scroll.setWidget(self.scroll_content) self.widget_stack.insertWidget(0, self.wrapper_scroll) self.core = RareCore.instance().core() self.add_button.clicked.connect(self.add_button_pressed) self.settings = QSettings() self.wrapper_scroll.horizontalScrollBar().rangeChanged.connect(self.adjust_scrollarea) # lk: set object names for the stylesheet self.setObjectName(type(self).__name__) self.no_wrapper_label.setObjectName(f"{self.objectName()}Label") self.wrapper_scroll.setObjectName(f"{self.objectName()}Scroll") self.wrapper_scroll.horizontalScrollBar().setObjectName( f"{self.wrapper_scroll.objectName()}Bar") self.wrapper_scroll.verticalScrollBar().setObjectName( f"{self.wrapper_scroll.objectName()}Bar") @pyqtSlot(int, int) def adjust_scrollarea(self, min: int, max: int): wrapper_widget = self.scroll_content.findChild(WrapperWidget) if not wrapper_widget: return # lk: when the scrollbar is not visible, min and max are 0 if max > min: self.wrapper_scroll.setMaximumHeight( wrapper_widget.sizeHint().height() + self.wrapper_scroll.rect().height() // 2 - self.wrapper_scroll.contentsRect().height() // 2 + self.scroll_content.layout().spacing() + self.wrapper_scroll.horizontalScrollBar().sizeHint().height() ) else: self.wrapper_scroll.setMaximumHeight( wrapper_widget.sizeHint().height() + self.wrapper_scroll.rect().height() - self.wrapper_scroll.contentsRect().height() ) def get_wrapper_string(self): return " ".join(self.get_wrapper_list()) def get_wrapper_list(self): wrappers = list(self.wrappers.values()) wrappers.sort(key=lambda x: self.scroll_content.layout().indexOf(x)) return [w.text for w in wrappers] def add_button_pressed(self): header = self.tr("Add wrapper") wrapper, done = QInputDialog.getText( self, f"{header} - {QCoreApplication.instance().applicationName()}", self.tr("Insert wrapper executable") ) if not done: return self.add_wrapper(wrapper) def add_wrapper(self, text: str, from_load=False): if text == "mangohud" and self.wrappers.get("mangohud"): return show_text = "" for key, extra_wrapper in extra_wrapper_regex.items(): if re.match(extra_wrapper, text): show_text = key if not show_text: show_text = text.split()[0] # validate if not text.strip(): # is empty return if not from_load: if self.wrappers.get(text): QMessageBox.warning( self, self.tr("Warning"), self.tr("Wrapper is already in the list") ) return if show_text != "proton" and not shutil.which(text.split()[0]): if QMessageBox.question( self, self.tr("Warning"), self.tr("Wrapper is not in $PATH. Ignore? "), QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) == QMessageBox.No: return if text == "proton": QMessageBox.warning( self, self.tr("Warning"), self.tr("Do not insert proton manually. Add it through Proton settings") ) return self.widget_stack.setCurrentIndex(0) if widget := self.wrappers.get(show_text, None): widget.deleteLater() widget = WrapperWidget(text, show_text, self.scroll_content) self.scroll_content.layout().addWidget(widget) self.adjust_scrollarea( self.wrapper_scroll.horizontalScrollBar().minimum(), self.wrapper_scroll.horizontalScrollBar().maximum() ) widget.delete_wrapper.connect(self.delete_wrapper) self.wrappers[show_text] = widget if not from_load: self.save() def delete_wrapper(self, text: str): widget = self.wrappers.get(text, None) if widget: self.wrappers.pop(text) widget.deleteLater() if not self.wrappers: self.wrapper_scroll.setMaximumHeight(self.label_page.sizeHint().height()) self.widget_stack.setCurrentIndex(1) self.save() def save(self): # save wrappers twice, to support wrappers with spaces if len(self.wrappers) == 0: config_helper.remove_option(self.app_name, "wrapper") self.settings.remove(f"{self.app_name}/wrapper") else: config_helper.add_option(self.app_name, "wrapper", self.get_wrapper_string()) self.settings.setValue(f"{self.app_name}/wrapper", self.get_wrapper_list()) def load_settings(self, app_name: str): self.app_name = app_name for i in self.wrappers.values(): i.deleteLater() self.wrappers.clear() wrappers = self.settings.value(f"{self.app_name}/wrapper", [], str) if not wrappers and (cfg := self.core.lgd.config.get(self.app_name, "wrapper", fallback="")): logger.info("Loading wrappers from legendary config") # no qt wrapper, but legendary wrapper, to have backward compatibility pattern = re.compile(r'''((?:[^ "']|"[^"]*"|'[^']*')+)''') wrappers = pattern.split(cfg)[1::2] for wrapper in wrappers: self.add_wrapper(wrapper, True) if not self.wrappers: self.wrapper_scroll.setMaximumHeight(self.label_page.sizeHint().height()) self.widget_stack.setCurrentIndex(1) else: self.widget_stack.setCurrentIndex(0) self.save() class WrapperContainer(QWidget): drag_widget: QWidget def __init__(self, save_cb, parent=None): super(WrapperContainer, self).__init__(parent=parent) self.setAcceptDrops(True) self.save = save_cb layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) self.setLayout(layout) # lk: set object names for the stylesheet self.setObjectName(type(self).__name__) def dragEnterEvent(self, e: QDragEnterEvent): widget = e.source() self.drag_widget = widget e.accept() def _get_drop_index(self, x): drag_idx = self.layout().indexOf(self.drag_widget) if drag_idx > 0: prev_widget = self.layout().itemAt(drag_idx - 1).widget() if x < self.drag_widget.x() - prev_widget.width() // 2: return drag_idx - 1 if drag_idx < self.layout().count() - 1: next_widget = self.layout().itemAt(drag_idx + 1).widget() if x > self.drag_widget.x() + self.drag_widget.width() + next_widget.width() // 2: return drag_idx + 1 return drag_idx def dragMoveEvent(self, e: QDragMoveEvent) -> None: i = self._get_drop_index(e.pos().x()) self.layout().insertWidget(i, self.drag_widget) def dropEvent(self, e: QDropEvent): pos = e.pos() widget = e.source() index = self._get_drop_index(pos.x()) self.layout().insertWidget(index, widget) self.drag_widget = None e.accept() self.save()