diff --git a/src/Mod/AddonManager/AddonManagerOptions.py b/src/Mod/AddonManager/AddonManagerOptions.py
index 88fde58910..81b5ab6fb6 100644
--- a/src/Mod/AddonManager/AddonManagerOptions.py
+++ b/src/Mod/AddonManager/AddonManagerOptions.py
@@ -28,29 +28,65 @@ import FreeCAD
import FreeCADGui
from PySide2 import QtCore
+from PySide2.QtGui import QIcon
from PySide2.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. """
+ """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."""
@@ -79,7 +115,7 @@ class AddonManagerOptions:
text = widget.text()
pref.SetString(str(pref_entry, "utf-8"), text)
elif widget.metaObject().className() == "Gui::PrefFileChooser":
- filename = str(widget.property("fileName"), "utf-8")
+ filename = str(widget.property("fileName"))
filename = pref.SetString(str(pref_entry, "utf-8"), filename)
# Recurse over children
@@ -92,6 +128,7 @@ class AddonManagerOptions:
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."""
@@ -126,3 +163,160 @@ class AddonManagerOptions:
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)
diff --git a/src/Mod/AddonManager/AddonManagerOptions.ui b/src/Mod/AddonManager/AddonManagerOptions.ui
index 8964e6de7e..fbf98dccd7 100644
--- a/src/Mod/AddonManager/AddonManagerOptions.ui
+++ b/src/Mod/AddonManager/AddonManagerOptions.ui
@@ -140,44 +140,64 @@ installed addons will be checked for available updates
-
+
+
+ 75
+ true
+
+
- Custom repositories (one per line):
+ Custom repositories
-
-
-
-
- 0
- 0
-
+
+
+ true
-
-
- 0
- 24
-
+
+ QAbstractItemView::SingleSelection
-
-
- 0
- 48
-
+
+ QAbstractItemView::SelectRows
-
- You can use this window to specify additional addon repositories
-to be scanned for available addons. To include a specific branch, add it to the end
-of the line after a space (e.g. https://github.com/FreeCAD/FreeCAD master).
-
-
- CustomRepositories
-
-
- Addons
+
+ false
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ ...
+
+
+
+ -
+
+
+ ...
+
+
+
+
+
-
@@ -399,11 +419,6 @@ of the line after a space (e.g. https://github.com/FreeCAD/FreeCAD master).QComboBox
-
- Gui::PrefTextEdit
- QTextEdit
-
-
Gui::PrefRadioButton
QRadioButton
diff --git a/src/Mod/AddonManager/AddonManagerOptions_AddCustomRepository.ui b/src/Mod/AddonManager/AddonManagerOptions_AddCustomRepository.ui
new file mode 100644
index 0000000000..17d9190b49
--- /dev/null
+++ b/src/Mod/AddonManager/AddonManagerOptions_AddCustomRepository.ui
@@ -0,0 +1,84 @@
+
+
+ AddCustomRepositoryDialog
+
+
+
+ 0
+ 0
+ 400
+ 95
+
+
+
+ Custom repository
+
+
+
-
+
+
+ Repository URL
+
+
+
+ -
+
+
+ -
+
+
+ Branch
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+ -
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ AddCustomRepositoryDialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ AddCustomRepositoryDialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/src/Mod/AddonManager/CMakeLists.txt b/src/Mod/AddonManager/CMakeLists.txt
index ee5f014aba..472b6ff544 100644
--- a/src/Mod/AddonManager/CMakeLists.txt
+++ b/src/Mod/AddonManager/CMakeLists.txt
@@ -22,6 +22,7 @@ SET(AddonManager_SRCS
addonmanager_workers_startup.py
addonmanager_workers_utility.py
AddonManagerOptions.ui
+ AddonManagerOptions_AddCustomRepository.ui
AddonManagerOptions.py
ALLOWED_PYTHON_PACKAGES.txt
change_branch.py