Skip to content

Commit e001c81

Browse files
committed
Addon Manager: Switch custom repo prefs to a table
1 parent 600264f commit e001c81

File tree

4 files changed

+328
-34
lines changed

4 files changed

+328
-34
lines changed

AddonManagerOptions.py

Lines changed: 196 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,29 +28,65 @@
2828
import FreeCADGui
2929

3030
from PySide2 import QtCore
31+
from PySide2.QtGui import QIcon
3132
from PySide2.QtWidgets import (
3233
QWidget,
3334
QCheckBox,
3435
QComboBox,
36+
QDialog,
37+
QHeaderView,
3538
QRadioButton,
3639
QLineEdit,
3740
QTextEdit,
3841
)
3942

43+
translate = FreeCAD.Qt.translate
44+
45+
#pylint: disable=too-few-public-methods
4046

4147
class AddonManagerOptions:
42-
""" A class containing a form element that is inserted as a FreeCAD preference page. """
48+
"""A class containing a form element that is inserted as a FreeCAD preference page."""
4349

4450
def __init__(self, _=None):
4551
self.form = FreeCADGui.PySideUic.loadUi(
4652
os.path.join(os.path.dirname(__file__), "AddonManagerOptions.ui")
4753
)
54+
self.table_model = CustomRepoDataModel()
55+
self.form.customRepositoriesTableView.setModel(self.table_model)
56+
57+
self.form.addCustomRepositoryButton.setIcon(
58+
QIcon.fromTheme("add", QIcon(":/icons/list-add.svg"))
59+
)
60+
self.form.removeCustomRepositoryButton.setIcon(
61+
QIcon.fromTheme("remove", QIcon(":/icons/list-remove.svg"))
62+
)
63+
64+
self.form.customRepositoriesTableView.horizontalHeader().setStretchLastSection(
65+
False
66+
)
67+
self.form.customRepositoriesTableView.horizontalHeader().setSectionResizeMode(
68+
0, QHeaderView.Stretch
69+
)
70+
self.form.customRepositoriesTableView.horizontalHeader().setSectionResizeMode(
71+
1, QHeaderView.ResizeToContents
72+
)
73+
74+
self.form.addCustomRepositoryButton.clicked.connect(
75+
self._add_custom_repo_clicked
76+
)
77+
self.form.removeCustomRepositoryButton.clicked.connect(
78+
self._remove_custom_repo_clicked
79+
)
80+
self.form.customRepositoriesTableView.doubleClicked.connect(
81+
self._row_double_clicked
82+
)
4883

4984
def saveSettings(self):
5085
"""Required function: called by the preferences dialog when Apply or Save is clicked,
5186
saves out the preference data by reading it from the widgets."""
5287
for widget in self.form.children():
5388
self.recursive_widget_saver(widget)
89+
self.table_model.save_model()
5490

5591
def recursive_widget_saver(self, widget):
5692
"""Writes out the data for this widget and all of its children, recursively."""
@@ -79,7 +115,7 @@ def recursive_widget_saver(self, widget):
79115
text = widget.text()
80116
pref.SetString(str(pref_entry, "utf-8"), text)
81117
elif widget.metaObject().className() == "Gui::PrefFileChooser":
82-
filename = str(widget.property("fileName"), "utf-8")
118+
filename = str(widget.property("fileName"))
83119
filename = pref.SetString(str(pref_entry, "utf-8"), filename)
84120

85121
# Recurse over children
@@ -92,6 +128,7 @@ def loadSettings(self):
92128
loads the preference data and assigns it to the widgets."""
93129
for widget in self.form.children():
94130
self.recursive_widget_loader(widget)
131+
self.table_model.load_model()
95132

96133
def recursive_widget_loader(self, widget):
97134
"""Loads the data for this widget and all of its children, recursively."""
@@ -126,3 +163,160 @@ def recursive_widget_loader(self, widget):
126163
if isinstance(widget, QtCore.QObject):
127164
for child in widget.children():
128165
self.recursive_widget_loader(child)
166+
167+
def _add_custom_repo_clicked(self):
168+
"""Callback: show the Add custom repo dialog"""
169+
dlg = CustomRepositoryDialog()
170+
url, branch = dlg.exec()
171+
if url and branch:
172+
self.table_model.appendData(url, branch)
173+
174+
def _remove_custom_repo_clicked(self):
175+
"""Callback: when the remove button is clicked, get the current selection and remove it."""
176+
item = self.form.customRepositoriesTableView.currentIndex()
177+
if not item.isValid():
178+
return
179+
row = item.row()
180+
self.table_model.removeRows(row, 1, QtCore.QModelIndex())
181+
182+
def _row_double_clicked(self, item):
183+
"""Edit the row that was double-clicked"""
184+
row = item.row()
185+
dlg = CustomRepositoryDialog()
186+
url_index = self.table_model.createIndex(row, 0)
187+
branch_index = self.table_model.createIndex(row, 1)
188+
dlg.dialog.urlLineEdit.setText(self.table_model.data(url_index))
189+
dlg.dialog.branchLineEdit.setText(self.table_model.data(branch_index))
190+
url, branch = dlg.exec()
191+
if url and branch:
192+
self.table_model.setData(url_index, url)
193+
self.table_model.setData(branch_index, branch)
194+
195+
196+
class CustomRepoDataModel(QtCore.QAbstractTableModel):
197+
"""The model for the custom repositories: wraps the underlying preference data and uses that
198+
as its main data store."""
199+
200+
def __init__(self):
201+
super().__init__()
202+
pref_access_string = "User parameter:BaseApp/Preferences/Addons"
203+
self.pref = FreeCAD.ParamGet(pref_access_string)
204+
self.load_model()
205+
206+
def load_model(self):
207+
"""Load the data from the preferences entry"""
208+
pref_entry: str = self.pref.GetString("CustomRepositories", "")
209+
210+
# The entry is saved as a space- and newline-delimited text block: break it into its
211+
# constituent parts
212+
lines = pref_entry.split("\n")
213+
self.model = []
214+
for line in lines:
215+
if not line:
216+
continue
217+
split_data = line.split()
218+
if len(split_data) > 1:
219+
branch = split_data[1]
220+
else:
221+
branch = "master"
222+
url = split_data[0]
223+
self.model.append([url, branch])
224+
225+
def save_model(self):
226+
"""Save the data into a preferences entry"""
227+
entry = ""
228+
for row in self.model:
229+
entry += f"{row[0]} {row[1]}\n"
230+
self.pref.SetString("CustomRepositories", entry)
231+
232+
def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
233+
"""The number of rows"""
234+
if parent.isValid():
235+
return 0
236+
return len(self.model)
237+
238+
def columnCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
239+
"""The number of columns (which is always 2)"""
240+
if parent.isValid():
241+
return 0
242+
return 2
243+
244+
def data(self, index, role=QtCore.Qt.DisplayRole):
245+
"""The data at an index."""
246+
if role != QtCore.Qt.DisplayRole:
247+
return None
248+
row = index.row()
249+
column = index.column()
250+
if row > len(self.model):
251+
return None
252+
if column > 1:
253+
return None
254+
return self.model[row][column]
255+
256+
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
257+
"""Get the row and column header data."""
258+
if role != QtCore.Qt.DisplayRole:
259+
return None
260+
if orientation == QtCore.Qt.Vertical:
261+
return section + 1
262+
if section == 0:
263+
return translate(
264+
"AddonsInstaller",
265+
"Repository URL",
266+
"Preferences header for custom repositories",
267+
)
268+
if section == 1:
269+
return translate(
270+
"AddonsInstaller",
271+
"Branch name",
272+
"Preferences header for custom repositories",
273+
)
274+
return None
275+
276+
def removeRows(self, row, count, parent):
277+
"""Remove rows"""
278+
self.beginRemoveRows(parent, row, row + count - 1)
279+
for _ in range(count):
280+
self.model.pop(row)
281+
self.endRemoveRows()
282+
283+
def insertRows(self, row, count, parent):
284+
"""Insert blank rows"""
285+
self.beginInsertRows(parent, row, row + count - 1)
286+
for _ in range(count):
287+
self.model.insert(["", ""])
288+
self.endInsertRows()
289+
290+
def appendData(self, url, branch):
291+
"""Append this url and branch to the end of the list"""
292+
row = self.rowCount()
293+
self.beginInsertRows(QtCore.QModelIndex(), row, row)
294+
self.model.append([url, branch])
295+
self.endInsertRows()
296+
297+
def setData(self, index, value, role=QtCore.Qt.EditRole):
298+
"""Set the data at this index"""
299+
if role != QtCore.Qt.EditRole:
300+
return
301+
self.model[index.row()][index.column()] = value
302+
self.dataChanged.emit(index, index)
303+
304+
305+
class CustomRepositoryDialog:
306+
"""A dialog for setting up a custom repository, with branch information"""
307+
308+
def __init__(self):
309+
self.dialog = FreeCADGui.PySideUic.loadUi(
310+
os.path.join(
311+
os.path.dirname(__file__), "AddonManagerOptions_AddCustomRepository.ui"
312+
)
313+
)
314+
315+
def exec(self):
316+
"""Run the dialog modally, and return either None or a tuple or (url,branch)"""
317+
result = self.dialog.exec()
318+
if result == QDialog.Accepted:
319+
url = self.dialog.urlLineEdit.text()
320+
branch = self.dialog.branchLineEdit.text()
321+
return (url, branch)
322+
return (None, None)

AddonManagerOptions.ui

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -140,44 +140,64 @@ installed addons will be checked for available updates
140140
</item>
141141
<item>
142142
<widget class="QLabel" name="label">
143+
<property name="font">
144+
<font>
145+
<weight>75</weight>
146+
<bold>true</bold>
147+
</font>
148+
</property>
143149
<property name="text">
144-
<string>Custom repositories (one per line):</string>
150+
<string>Custom repositories</string>
145151
</property>
146152
</widget>
147153
</item>
148154
<item>
149-
<widget class="Gui::PrefTextEdit" name="guipreftexteditcustomrepositories">
150-
<property name="sizePolicy">
151-
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
152-
<horstretch>0</horstretch>
153-
<verstretch>0</verstretch>
154-
</sizepolicy>
155-
</property>
156-
<property name="minimumSize">
157-
<size>
158-
<width>0</width>
159-
<height>24</height>
160-
</size>
161-
</property>
162-
<property name="baseSize">
163-
<size>
164-
<width>0</width>
165-
<height>48</height>
166-
</size>
155+
<widget class="QTableView" name="customRepositoriesTableView">
156+
<property name="alternatingRowColors">
157+
<bool>true</bool>
167158
</property>
168-
<property name="toolTip">
169-
<string>You can use this window to specify additional addon repositories
170-
to be scanned for available addons. To include a specific branch, add it to the end
171-
of the line after a space (e.g. https://github.com/FreeCAD/FreeCAD master).</string>
159+
<property name="selectionMode">
160+
<enum>QAbstractItemView::SingleSelection</enum>
172161
</property>
173-
<property name="prefEntry" stdset="0">
174-
<cstring>CustomRepositories</cstring>
162+
<property name="selectionBehavior">
163+
<enum>QAbstractItemView::SelectRows</enum>
175164
</property>
176-
<property name="prefPath" stdset="0">
177-
<cstring>Addons</cstring>
165+
<property name="sortingEnabled">
166+
<bool>false</bool>
178167
</property>
179168
</widget>
180169
</item>
170+
<item>
171+
<layout class="QHBoxLayout" name="horizontalLayout_2">
172+
<item>
173+
<spacer name="horizontalSpacer">
174+
<property name="orientation">
175+
<enum>Qt::Horizontal</enum>
176+
</property>
177+
<property name="sizeHint" stdset="0">
178+
<size>
179+
<width>40</width>
180+
<height>20</height>
181+
</size>
182+
</property>
183+
</spacer>
184+
</item>
185+
<item>
186+
<widget class="QToolButton" name="addCustomRepositoryButton">
187+
<property name="text">
188+
<string>...</string>
189+
</property>
190+
</widget>
191+
</item>
192+
<item>
193+
<widget class="QToolButton" name="removeCustomRepositoryButton">
194+
<property name="text">
195+
<string>...</string>
196+
</property>
197+
</widget>
198+
</item>
199+
</layout>
200+
</item>
181201
<item>
182202
<widget class="QGroupBox" name="groupBox">
183203
<property name="title">
@@ -399,11 +419,6 @@ of the line after a space (e.g. https://github.com/FreeCAD/FreeCAD master).</str
399419
<extends>QComboBox</extends>
400420
<header>Gui/PrefWidgets.h</header>
401421
</customwidget>
402-
<customwidget>
403-
<class>Gui::PrefTextEdit</class>
404-
<extends>QTextEdit</extends>
405-
<header>Gui/PrefWidgets.h</header>
406-
</customwidget>
407422
<customwidget>
408423
<class>Gui::PrefRadioButton</class>
409424
<extends>QRadioButton</extends>

0 commit comments

Comments
 (0)