# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * * # * Copyright (c) 2022 FreeCAD Project Association * # * * # * This file is part of FreeCAD. * # * * # * FreeCAD is free software: you can redistribute it and/or modify it * # * under the terms of the GNU Lesser General Public License as * # * published by the Free Software Foundation, either version 2.1 of the * # * License, or (at your option) any later version. * # * * # * FreeCAD is distributed in the hope that it will be useful, but * # * WITHOUT ANY WARRANTY; without even the implied warranty of * # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * # * Lesser General Public License for more details. * # * * # * You should have received a copy of the GNU Lesser General Public * # * License along with FreeCAD. If not, see * # * . * # * * # *************************************************************************** """ Contains a the Addon Manager's preferences dialog management class """ import os import FreeCAD import FreeCADGui from PySide import QtCore from PySide.QtGui import QIcon from PySide.QtWidgets import ( QWidget, QCheckBox, QComboBox, QDialog, QHeaderView, QRadioButton, QLineEdit, QTextEdit, ) translate = FreeCAD.Qt.translate # pylint: disable=too-few-public-methods class AddonManagerOptions: """A class containing a form element that is inserted as a FreeCAD preference page.""" def __init__(self, _=None): self.form = FreeCADGui.PySideUic.loadUi( os.path.join(os.path.dirname(__file__), "AddonManagerOptions.ui") ) self.table_model = CustomRepoDataModel() self.form.customRepositoriesTableView.setModel(self.table_model) self.form.addCustomRepositoryButton.setIcon( QIcon.fromTheme("add", QIcon(":/icons/list-add.svg")) ) self.form.removeCustomRepositoryButton.setIcon( QIcon.fromTheme("remove", QIcon(":/icons/list-remove.svg")) ) self.form.customRepositoriesTableView.horizontalHeader().setStretchLastSection(False) self.form.customRepositoriesTableView.horizontalHeader().setSectionResizeMode( 0, QHeaderView.Stretch ) self.form.customRepositoriesTableView.horizontalHeader().setSectionResizeMode( 1, QHeaderView.ResizeToContents ) self.form.addCustomRepositoryButton.clicked.connect(self._add_custom_repo_clicked) self.form.removeCustomRepositoryButton.clicked.connect(self._remove_custom_repo_clicked) self.form.customRepositoriesTableView.doubleClicked.connect(self._row_double_clicked) def saveSettings(self): """Required function: called by the preferences dialog when Apply or Save is clicked, saves out the preference data by reading it from the widgets.""" for widget in self.form.children(): self.recursive_widget_saver(widget) self.table_model.save_model() def recursive_widget_saver(self, widget): """Writes out the data for this widget and all of its children, recursively.""" if isinstance(widget, QWidget): # See if it's one of ours: pref_path = widget.property("prefPath") pref_entry = widget.property("prefEntry") if pref_path and pref_entry: pref_path = pref_path.data() pref_entry = pref_entry.data() pref_access_string = f"User parameter:BaseApp/Preferences/{str(pref_path,'utf-8')}" pref = FreeCAD.ParamGet(pref_access_string) if isinstance(widget, QCheckBox): checked = widget.isChecked() pref.SetBool(str(pref_entry, "utf-8"), checked) elif isinstance(widget, QRadioButton): checked = widget.isChecked() pref.SetBool(str(pref_entry, "utf-8"), checked) elif isinstance(widget, QComboBox): new_index = widget.currentIndex() pref.SetInt(str(pref_entry, "utf-8"), new_index) elif isinstance(widget, QTextEdit): text = widget.toPlainText() pref.SetString(str(pref_entry, "utf-8"), text) elif isinstance(widget, QLineEdit): text = widget.text() pref.SetString(str(pref_entry, "utf-8"), text) elif widget.metaObject().className() == "Gui::PrefFileChooser": filename = str(widget.property("fileName")) filename = pref.SetString(str(pref_entry, "utf-8"), filename) # Recurse over children if isinstance(widget, QtCore.QObject): for child in widget.children(): self.recursive_widget_saver(child) def loadSettings(self): """Required function: called by the preferences dialog when it is launched, loads the preference data and assigns it to the widgets.""" for widget in self.form.children(): self.recursive_widget_loader(widget) self.table_model.load_model() def recursive_widget_loader(self, widget): """Loads the data for this widget and all of its children, recursively.""" if isinstance(widget, QWidget): # See if it's one of ours: pref_path = widget.property("prefPath") pref_entry = widget.property("prefEntry") if pref_path and pref_entry: pref_path = pref_path.data() pref_entry = pref_entry.data() pref_access_string = f"User parameter:BaseApp/Preferences/{str(pref_path,'utf-8')}" pref = FreeCAD.ParamGet(pref_access_string) if isinstance(widget, QCheckBox): widget.setChecked(pref.GetBool(str(pref_entry, "utf-8"))) elif isinstance(widget, QRadioButton): if pref.GetBool(str(pref_entry, "utf-8")): widget.setChecked(True) elif isinstance(widget, QComboBox): new_index = pref.GetInt(str(pref_entry, "utf-8")) widget.setCurrentIndex(new_index) elif isinstance(widget, QTextEdit): text = pref.GetString(str(pref_entry, "utf-8")) widget.setText(text) elif isinstance(widget, QLineEdit): text = pref.GetString(str(pref_entry, "utf-8")) widget.setText(text) elif widget.metaObject().className() == "Gui::PrefFileChooser": filename = pref.GetString(str(pref_entry, "utf-8")) widget.setProperty("fileName", filename) # Recurse over children if isinstance(widget, QtCore.QObject): for child in widget.children(): self.recursive_widget_loader(child) def _add_custom_repo_clicked(self): """Callback: show the Add custom repo dialog""" dlg = CustomRepositoryDialog() url, branch = dlg.exec() if url and branch: self.table_model.appendData(url, branch) def _remove_custom_repo_clicked(self): """Callback: when the remove button is clicked, get the current selection and remove it.""" item = self.form.customRepositoriesTableView.currentIndex() if not item.isValid(): return row = item.row() self.table_model.removeRows(row, 1, QtCore.QModelIndex()) def _row_double_clicked(self, item): """Edit the row that was double-clicked""" row = item.row() dlg = CustomRepositoryDialog() url_index = self.table_model.createIndex(row, 0) branch_index = self.table_model.createIndex(row, 1) dlg.dialog.urlLineEdit.setText(self.table_model.data(url_index)) dlg.dialog.branchLineEdit.setText(self.table_model.data(branch_index)) url, branch = dlg.exec() if url and branch: self.table_model.setData(url_index, url) self.table_model.setData(branch_index, branch) class CustomRepoDataModel(QtCore.QAbstractTableModel): """The model for the custom repositories: wraps the underlying preference data and uses that as its main data store.""" def __init__(self): super().__init__() pref_access_string = "User parameter:BaseApp/Preferences/Addons" self.pref = FreeCAD.ParamGet(pref_access_string) self.load_model() def load_model(self): """Load the data from the preferences entry""" pref_entry: str = self.pref.GetString("CustomRepositories", "") # The entry is saved as a space- and newline-delimited text block: break it into its # constituent parts lines = pref_entry.split("\n") self.model = [] for line in lines: if not line: continue split_data = line.split() if len(split_data) > 1: branch = split_data[1] else: branch = "master" url = split_data[0] self.model.append([url, branch]) def save_model(self): """Save the data into a preferences entry""" entry = "" for row in self.model: entry += f"{row[0]} {row[1]}\n" self.pref.SetString("CustomRepositories", entry) def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int: """The number of rows""" if parent.isValid(): return 0 return len(self.model) def columnCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int: """The number of columns (which is always 2)""" if parent.isValid(): return 0 return 2 def data(self, index, role=QtCore.Qt.DisplayRole): """The data at an index.""" if role != QtCore.Qt.DisplayRole: return None row = index.row() column = index.column() if row > len(self.model): return None if column > 1: return None return self.model[row][column] def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole): """Get the row and column header data.""" if role != QtCore.Qt.DisplayRole: return None if orientation == QtCore.Qt.Vertical: return section + 1 if section == 0: return translate( "AddonsInstaller", "Repository URL", "Preferences header for custom repositories", ) if section == 1: return translate( "AddonsInstaller", "Branch name", "Preferences header for custom repositories", ) return None def removeRows(self, row, count, parent): """Remove rows""" self.beginRemoveRows(parent, row, row + count - 1) for _ in range(count): self.model.pop(row) self.endRemoveRows() def insertRows(self, row, count, parent): """Insert blank rows""" self.beginInsertRows(parent, row, row + count - 1) for _ in range(count): self.model.insert(["", ""]) self.endInsertRows() def appendData(self, url, branch): """Append this url and branch to the end of the list""" row = self.rowCount() self.beginInsertRows(QtCore.QModelIndex(), row, row) self.model.append([url, branch]) self.endInsertRows() def setData(self, index, value, role=QtCore.Qt.EditRole): """Set the data at this index""" if role != QtCore.Qt.EditRole: return self.model[index.row()][index.column()] = value self.dataChanged.emit(index, index) class CustomRepositoryDialog: """A dialog for setting up a custom repository, with branch information""" def __init__(self): self.dialog = FreeCADGui.PySideUic.loadUi( os.path.join(os.path.dirname(__file__), "AddonManagerOptions_AddCustomRepository.ui") ) def exec(self): """Run the dialog modally, and return either None or a tuple or (url,branch)""" result = self.dialog.exec() if result == QDialog.Accepted: url = self.dialog.urlLineEdit.text() branch = self.dialog.branchLineEdit.text() return (url, branch) return (None, None)