FreeCAD

Форк
0
/
package_list.py 
770 строк · 33.6 Кб
1
# SPDX-License-Identifier: LGPL-2.1-or-later
2
# ***************************************************************************
3
# *                                                                         *
4
# *   Copyright (c) 2022-2023 FreeCAD Project Association                   *
5
# *                                                                         *
6
# *   This file is part of FreeCAD.                                         *
7
# *                                                                         *
8
# *   FreeCAD is free software: you can redistribute it and/or modify it    *
9
# *   under the terms of the GNU Lesser General Public License as           *
10
# *   published by the Free Software Foundation, either version 2.1 of the  *
11
# *   License, or (at your option) any later version.                       *
12
# *                                                                         *
13
# *   FreeCAD is distributed in the hope that it will be useful, but        *
14
# *   WITHOUT ANY WARRANTY; without even the implied warranty of            *
15
# *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      *
16
# *   Lesser General Public License for more details.                       *
17
# *                                                                         *
18
# *   You should have received a copy of the GNU Lesser General Public      *
19
# *   License along with FreeCAD. If not, see                               *
20
# *   <https://www.gnu.org/licenses/>.                                      *
21
# *                                                                         *
22
# ***************************************************************************
23

24
""" Defines the PackageList QWidget for displaying a list of Addons. """
25
import datetime
26
import threading
27

28
import FreeCAD
29

30
from PySide import QtCore, QtGui, QtWidgets
31

32
from Addon import Addon
33

34
from compact_view import Ui_CompactView
35
from expanded_view import Ui_ExpandedView
36

37
import addonmanager_utilities as utils
38
from addonmanager_metadata import get_first_supported_freecad_version, Version
39
from Widgets.addonmanager_widget_view_control_bar import WidgetViewControlBar, SortOptions
40
from Widgets.addonmanager_widget_view_selector import AddonManagerDisplayStyle
41
from Widgets.addonmanager_widget_filter_selector import StatusFilter, Filter
42
from Widgets.addonmanager_widget_progress_bar import WidgetProgressBar
43
from addonmanager_licenses import get_license_manager
44

45
translate = FreeCAD.Qt.translate
46

47

48
# pylint: disable=too-few-public-methods
49

50

51
class PackageList(QtWidgets.QWidget):
52
    """A widget that shows a list of packages and various widgets to control the
53
    display of the list"""
54

55
    itemSelected = QtCore.Signal(Addon)
56

57
    def __init__(self, parent=None):
58
        super().__init__(parent)
59
        self.ui = Ui_PackageList()
60
        self.ui.setupUi(self)
61

62
        self.item_filter = PackageListFilter()
63
        self.ui.listPackages.setModel(self.item_filter)
64
        self.item_delegate = PackageListItemDelegate(self.ui.listPackages)
65
        self.ui.listPackages.setItemDelegate(self.item_delegate)
66

67
        self.ui.listPackages.clicked.connect(self.on_listPackages_clicked)
68
        self.ui.view_bar.filter_changed.connect(self.update_status_filter)
69
        self.ui.view_bar.search_changed.connect(self.item_filter.setFilterRegularExpression)
70
        self.ui.view_bar.sort_changed.connect(self.item_filter.setSortRole)
71
        self.ui.view_bar.sort_changed.connect(self.item_delegate.set_sort)
72
        self.ui.view_bar.sort_order_changed.connect(lambda order: self.item_filter.sort(0, order))
73

74
        # Set up the view the same as the last time:
75
        pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
76
        package_type = pref.GetInt("PackageTypeSelection", 1)
77
        status = pref.GetInt("StatusSelection", 0)
78
        search_string = pref.GetString("SearchString", "")
79
        self.ui.view_bar.filter_selector.set_contents_filter(package_type)
80
        self.ui.view_bar.filter_selector.set_status_filter(status)
81
        if search_string:
82
            self.ui.view_bar.search.filter_line_edit.setText(search_string)
83
        self.item_filter.setPackageFilter(package_type)
84
        self.item_filter.setStatusFilter(status)
85

86
        # Pre-init of other members:
87
        self.item_model = None
88

89
    def setModel(self, model):
90
        """This is a model-view-controller widget: set its model."""
91
        self.item_model = model
92
        self.item_filter.setSourceModel(self.item_model)
93
        self.item_filter.setSortRole(SortOptions.Alphabetical)
94
        self.item_filter.sort(0, QtCore.Qt.AscendingOrder)
95

96
        pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
97
        style = pref.GetInt("ViewStyle", AddonManagerDisplayStyle.EXPANDED)
98
        self.set_view_style(style)
99
        self.ui.view_bar.view_selector.set_current_view(style)
100

101
        self.item_filter.setHidePy2(pref.GetBool("HidePy2", False))
102
        self.item_filter.setHideObsolete(pref.GetBool("HideObsolete", False))
103
        self.item_filter.setHideNonOSIApproved(pref.GetBool("HideNonOSIApproved", False))
104
        self.item_filter.setHideNonFSFLibre(pref.GetBool("HideNonFSFFreeLibre", False))
105
        self.item_filter.setHideNewerFreeCADRequired(
106
            pref.GetBool("HideNewerFreeCADRequired", False)
107
        )
108
        self.item_filter.setHideUnlicensed(pref.GetBool("HideUnlicensed", False))
109

110
    def select_addon(self, addon_name: str):
111
        for index, addon in enumerate(self.item_model.repos):
112
            if addon.name == addon_name:
113
                row_index = self.item_model.createIndex(index, 0)
114
                if self.item_filter.filterAcceptsRow(index):
115
                    self.ui.listPackages.setCurrentIndex(row_index)
116
                else:
117
                    FreeCAD.Console.PrintLog(
118
                        f"Addon {addon_name} is not visible given current "
119
                        "filter: not selecting it."
120
                    )
121
                return
122
        FreeCAD.Console.PrintLog(f"Could not find addon '{addon_name}' to select it")
123

124
    def on_listPackages_clicked(self, index: QtCore.QModelIndex):
125
        """Determine what addon was selected and emit the itemSelected signal with it as
126
        an argument."""
127
        source_selection = self.item_filter.mapToSource(index)
128
        selected_repo = self.item_model.repos[source_selection.row()]
129
        self.itemSelected.emit(selected_repo)
130

131
    def update_status_filter(self, new_filter: Filter) -> None:
132
        """hide/show rows corresponding to the specified filter"""
133

134
        self.item_filter.setStatusFilter(new_filter.status_filter)
135
        self.item_filter.setPackageFilter(new_filter.content_filter)
136
        pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
137
        pref.SetInt("StatusSelection", new_filter.status_filter)
138
        pref.SetInt("PackageTypeSelection", new_filter.content_filter)
139
        self.item_filter.invalidateFilter()
140

141
    def set_view_style(self, style: AddonManagerDisplayStyle) -> None:
142
        """Set the style (compact or expanded) of the list"""
143
        if self.item_model:
144
            self.item_model.layoutAboutToBeChanged.emit()
145
        self.item_delegate.set_view(style)
146
        if style == AddonManagerDisplayStyle.COMPACT or style == AddonManagerDisplayStyle.COMPOSITE:
147
            self.ui.listPackages.setSpacing(2)
148
            self.ui.listPackages.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerItem)
149
            self.ui.listPackages.verticalScrollBar().setSingleStep(-1)
150
        else:
151
            self.ui.listPackages.setSpacing(5)
152
            self.ui.listPackages.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
153
            self.ui.listPackages.verticalScrollBar().setSingleStep(24)
154
        if self.item_model:
155
            self.item_model.layoutChanged.emit()
156

157
        pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
158
        pref.SetInt("ViewStyle", style)
159

160

161
class PackageListItemModel(QtCore.QAbstractListModel):
162
    """The model for use with the PackageList class."""
163

164
    repos = []
165
    write_lock = threading.Lock()
166

167
    DataAccessRole = QtCore.Qt.UserRole
168

169
    def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
170
        """The number of rows"""
171
        if parent.isValid():
172
            return 0
173
        return len(self.repos)
174

175
    def columnCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
176
        """Only one column, always returns 1."""
177
        if parent.isValid():
178
            return 0
179
        return 1
180

181
    def data(self, index: QtCore.QModelIndex, role: int = QtCore.Qt.DisplayRole):
182
        """Get the data for a given index and role."""
183
        if not index.isValid():
184
            return None
185
        row = index.row()
186
        if role == QtCore.Qt.ToolTipRole:
187
            tooltip = ""
188
            if self.repos[row].repo_type == Addon.Kind.PACKAGE:
189
                tooltip = translate("AddonsInstaller", "Click for details about package {}").format(
190
                    self.repos[row].display_name
191
                )
192
            elif self.repos[row].repo_type == Addon.Kind.WORKBENCH:
193
                tooltip = translate(
194
                    "AddonsInstaller", "Click for details about workbench {}"
195
                ).format(self.repos[row].display_name)
196
            elif self.repos[row].repo_type == Addon.Kind.MACRO:
197
                tooltip = translate("AddonsInstaller", "Click for details about macro {}").format(
198
                    self.repos[row].display_name
199
                )
200
            return tooltip
201
        if role == PackageListItemModel.DataAccessRole:
202
            return self.repos[row]
203

204
        # Sorting
205
        if role == SortOptions.Alphabetical:
206
            return self.repos[row].display_name
207
        if role == SortOptions.LastUpdated:
208
            update_date = self.repos[row].update_date
209
            if update_date and hasattr(update_date, "timestamp"):
210
                return update_date.timestamp()
211
            return 0
212
        if role == SortOptions.DateAdded:
213
            if self.repos[row].stats and self.repos[row].stats.date_created:
214
                return self.repos[row].stats.date_created.timestamp()
215
            return 0
216
        if role == SortOptions.Stars:
217
            if self.repos[row].stats and self.repos[row].stats.stars:
218
                return self.repos[row].stats.stars
219
            return 0
220
        if role == SortOptions.Score:
221
            return self.repos[row].score
222

223
    def headerData(self, _unused1, _unused2, _role=QtCore.Qt.DisplayRole):
224
        """No header in this implementation: always returns None."""
225
        return None
226

227
    def append_item(self, repo: Addon) -> None:
228
        """Adds this addon to the end of the model. Thread safe."""
229
        if repo in self.repos:
230
            # Cowardly refuse to insert the same repo a second time
231
            return
232
        with self.write_lock:
233
            self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
234
            self.repos.append(repo)
235
            self.endInsertRows()
236

237
    def clear(self) -> None:
238
        """Clear the model, removing all rows. Thread safe."""
239
        if self.rowCount() > 0:
240
            with self.write_lock:
241
                self.beginRemoveRows(QtCore.QModelIndex(), 0, self.rowCount() - 1)
242
                self.repos = []
243
                self.endRemoveRows()
244

245
    def reload_item(self, repo: Addon) -> None:
246
        """Sets the addon data for the given addon (based on its name)"""
247
        for index, item in enumerate(self.repos):
248
            if item.name == repo.name:
249
                with self.write_lock:
250
                    self.repos[index] = repo
251
                return
252

253

254
class CompactView(QtWidgets.QWidget):
255
    """A single-line view of the package information"""
256

257
    def __init__(self, parent=None):
258
        super().__init__(parent)
259
        self.ui = Ui_CompactView()
260
        self.ui.setupUi(self)
261

262

263
class ExpandedView(QtWidgets.QWidget):
264
    """A multi-line view of the package information"""
265

266
    def __init__(self, parent=None):
267
        super().__init__(parent)
268
        self.ui = Ui_ExpandedView()
269
        self.ui.setupUi(self)
270

271

272
class PackageListItemDelegate(QtWidgets.QStyledItemDelegate):
273
    """Render the repo data as a formatted region"""
274

275
    def __init__(self, parent=None):
276
        super().__init__(parent)
277
        self.displayStyle = AddonManagerDisplayStyle.EXPANDED
278
        self.sort_order = SortOptions.Alphabetical
279
        self.expanded = ExpandedView()
280
        self.compact = CompactView()
281
        self.widget = self.expanded
282

283
    def set_view(self, style: AddonManagerDisplayStyle) -> None:
284
        """Set the view of to style"""
285
        if not self.displayStyle == style:
286
            self.displayStyle = style
287

288
    def set_sort(self, sort: SortOptions) -> None:
289
        """When sorting by various things, we display the thing that's being sorted on."""
290
        if not self.sort_order == sort:
291
            self.sort_order = sort
292

293
    def sizeHint(self, _option, index):
294
        """Attempt to figure out the correct height for the widget based on its
295
        current contents."""
296
        self.update_content(index)
297
        return self.widget.sizeHint()
298

299
    def update_content(self, index):
300
        """Creates the display of the content for a given index."""
301
        repo = index.data(PackageListItemModel.DataAccessRole)
302
        if self.displayStyle == AddonManagerDisplayStyle.EXPANDED:
303
            self.widget = self.expanded
304
            self._setup_expanded_view(repo)
305
        elif self.displayStyle == AddonManagerDisplayStyle.COMPACT:
306
            self.widget = self.compact
307
            self._setup_compact_view(repo)
308
        elif self.displayStyle == AddonManagerDisplayStyle.COMPOSITE:
309
            self.widget = self.compact  # For now re-use the compact list
310
            self._setup_composite_view(repo)
311
        self.widget.adjustSize()
312

313
    def _setup_expanded_view(self, addon: Addon) -> None:
314
        self.widget.ui.labelPackageName.setText(f"<h1>{addon.display_name}</h1>")
315
        self.widget.ui.labelIcon.setPixmap(addon.icon.pixmap(QtCore.QSize(48, 48)))
316
        self.widget.ui.labelStatus.setText(self.get_expanded_update_string(addon))
317
        self.widget.ui.labelIcon.setText("")
318
        self.widget.ui.labelTags.setText("")
319
        if addon.metadata:
320
            self.widget.ui.labelDescription.setText(addon.metadata.description)
321
            self.widget.ui.labelVersion.setText(f"<i>v{addon.metadata.version}</i>")
322
            self._set_package_maintainer_label(addon)
323
        elif addon.macro:
324
            self.widget.ui.labelDescription.setText(addon.macro.comment)
325
            self._set_macro_version_label(addon)
326
            self._set_macro_maintainer_label(addon)
327
        else:
328
            self.widget.ui.labelDescription.setText("")
329
            self.widget.ui.labelMaintainer.setText("")
330
            self.widget.ui.labelVersion.setText("")
331
        if addon.tags:
332
            self.widget.ui.labelTags.setText(
333
                translate("AddonsInstaller", "Tags") + ": " + ", ".join(addon.tags)
334
            )
335
        if self.sort_order == SortOptions.Alphabetical:
336
            self.widget.ui.labelSort.setText("")
337
        else:
338
            self.widget.ui.labelSort.setText(self._get_sort_label_text(addon))
339

340
    def _setup_compact_view(self, addon: Addon) -> None:
341
        self.widget.ui.labelPackageName.setText(f"<b>{addon.display_name}</b>")
342
        self.widget.ui.labelIcon.setPixmap(addon.icon.pixmap(QtCore.QSize(16, 16)))
343
        self.widget.ui.labelStatus.setText(self.get_compact_update_string(addon))
344
        self.widget.ui.labelIcon.setText("")
345
        if addon.metadata:
346
            self.widget.ui.labelVersion.setText(f"<i>v{addon.metadata.version}</i>")
347
        elif addon.macro:
348
            self._set_macro_version_label(addon)
349
        else:
350
            self.widget.ui.labelVersion.setText("")
351
        if self.sort_order == SortOptions.Alphabetical:
352
            description = self._get_compact_description(addon)
353
            self.widget.ui.labelDescription.setText(description)
354
        else:
355
            self.widget.ui.labelDescription.setText(self._get_sort_label_text(addon))
356

357
    def _setup_composite_view(self, addon: Addon) -> None:
358
        self.widget.ui.labelPackageName.setText(f"<b>{addon.display_name}</b>")
359
        self.widget.ui.labelIcon.setPixmap(addon.icon.pixmap(QtCore.QSize(16, 16)))
360
        self.widget.ui.labelStatus.setText(self.get_compact_update_string(addon))
361
        self.widget.ui.labelIcon.setText("")
362
        if addon.metadata:
363
            self.widget.ui.labelVersion.setText(f"<i>v{addon.metadata.version}</i>")
364
        elif addon.macro:
365
            self._set_macro_version_label(addon)
366
        else:
367
            self.widget.ui.labelVersion.setText("")
368
        if self.sort_order != SortOptions.Alphabetical:
369
            self.widget.ui.labelDescription.setText(self._get_sort_label_text(addon))
370
        else:
371
            self.widget.ui.labelDescription.setText("")
372

373
    def _set_package_maintainer_label(self, addon: Addon):
374
        maintainers = addon.metadata.maintainer
375
        maintainers_string = ""
376
        if len(maintainers) == 1:
377
            maintainers_string = (
378
                translate("AddonsInstaller", "Maintainer")
379
                + f": {maintainers[0].name} <{maintainers[0].email}>"
380
            )
381
        elif len(maintainers) > 1:
382
            n = len(maintainers)
383
            maintainers_string = translate("AddonsInstaller", "Maintainers:", "", n)
384
            for maintainer in maintainers:
385
                maintainers_string += f"\n{maintainer.name} <{maintainer.email}>"
386
        self.widget.ui.labelMaintainer.setText(maintainers_string)
387

388
    def _set_macro_maintainer_label(self, repo: Addon):
389
        if repo.macro.author:
390
            caption = translate("AddonsInstaller", "Author")
391
            self.widget.ui.labelMaintainer.setText(caption + ": " + repo.macro.author)
392
        else:
393
            self.widget.ui.labelMaintainer.setText("")
394

395
    def _set_macro_version_label(self, addon: Addon):
396
        version_string = ""
397
        if addon.macro.version:
398
            version_string = addon.macro.version + " "
399
        if addon.macro.on_wiki:
400
            version_string += "(wiki)"
401
        elif addon.macro.on_git:
402
            version_string += "(git)"
403
        else:
404
            version_string += "(unknown source)"
405
        self.widget.ui.labelVersion.setText("<i>" + version_string + "</i>")
406

407
    def _get_sort_label_text(self, addon: Addon) -> str:
408
        if self.sort_order == SortOptions.Alphabetical:
409
            return ""
410
        elif self.sort_order == SortOptions.Stars:
411
            if addon.stats and addon.stats.stars and addon.stats.stars > 0:
412
                return translate("AddonsInstaller", "{} ★ on GitHub").format(addon.stats.stars)
413
            return translate("AddonsInstaller", "No ★, or not on GitHub")
414
        elif self.sort_order == SortOptions.DateAdded:
415
            if addon.stats and addon.stats.date_created:
416
                epoch_seconds = addon.stats.date_created.timestamp()
417
                qdt = QtCore.QDateTime.fromSecsSinceEpoch(int(epoch_seconds)).date()
418
                time_string = QtCore.QLocale().toString(qdt, QtCore.QLocale.ShortFormat)
419
                return translate("AddonsInstaller", "Created ") + time_string
420
            return ""
421
        elif self.sort_order == SortOptions.LastUpdated:
422
            update_date = addon.update_date
423
            if update_date:
424
                epoch_seconds = update_date.timestamp()
425
                qdt = QtCore.QDateTime.fromSecsSinceEpoch(int(epoch_seconds)).date()
426
                time_string = QtCore.QLocale().toString(qdt, QtCore.QLocale.ShortFormat)
427
                return translate("AddonsInstaller", "Updated ") + time_string
428
            return ""
429
        elif self.sort_order == SortOptions.Score:
430
            return translate("AddonsInstaller", "Score: ") + str(addon.score)
431
        return ""
432

433
    def _get_compact_description(self, addon: Addon) -> str:
434
        description = ""
435
        if addon.metadata:
436
            description = addon.metadata.description
437
        elif addon.macro and addon.macro.comment:
438
            description = addon.macro.comment
439
        trimmed_text, _, _ = description.partition(".")
440
        return trimmed_text.replace("\n", " ")
441

442
    @staticmethod
443
    def get_compact_update_string(repo: Addon) -> str:
444
        """Get a single-line string listing details about the installed version and
445
        date"""
446

447
        result = ""
448
        if repo.status() == Addon.Status.UNCHECKED:
449
            result = translate("AddonsInstaller", "Installed")
450
        elif repo.status() == Addon.Status.NO_UPDATE_AVAILABLE:
451
            result = translate("AddonsInstaller", "Up-to-date")
452
        elif repo.status() == Addon.Status.UPDATE_AVAILABLE:
453
            result = translate("AddonsInstaller", "Update available")
454
        elif repo.status() == Addon.Status.PENDING_RESTART:
455
            result = translate("AddonsInstaller", "Pending restart")
456

457
        if repo.is_disabled():
458
            style = "style='color:" + utils.warning_color_string() + "; font-weight:bold;'"
459
            result += f"<span {style}> [" + translate("AddonsInstaller", "DISABLED") + "]</span>"
460

461
        return result
462

463
    @staticmethod
464
    def get_expanded_update_string(repo: Addon) -> str:
465
        """Get a multi-line string listing details about the installed version and
466
        date"""
467

468
        result = ""
469

470
        installed_version_string = ""
471
        if repo.status() != Addon.Status.NOT_INSTALLED:
472
            if repo.installed_version or repo.installed_metadata:
473
                installed_version_string = (
474
                    "<br/>" + translate("AddonsInstaller", "Installed version") + ": "
475
                )
476
                if repo.installed_metadata:
477
                    installed_version_string += str(repo.installed_metadata.version)
478
                elif repo.installed_version:
479
                    installed_version_string += str(repo.installed_version)
480
            else:
481
                installed_version_string = "<br/>" + translate("AddonsInstaller", "Unknown version")
482

483
        installed_date_string = ""
484
        if repo.updated_timestamp:
485
            installed_date_string = "<br/>" + translate("AddonsInstaller", "Installed on") + ": "
486
            installed_date_string += QtCore.QLocale().toString(
487
                QtCore.QDateTime.fromSecsSinceEpoch(int(round(repo.updated_timestamp, 0))),
488
                QtCore.QLocale.ShortFormat,
489
            )
490

491
        available_version_string = ""
492
        if repo.metadata:
493
            available_version_string = (
494
                "<br/>" + translate("AddonsInstaller", "Available version") + ": "
495
            )
496
            available_version_string += str(repo.metadata.version)
497

498
        if repo.status() == Addon.Status.UNCHECKED:
499
            result = translate("AddonsInstaller", "Installed")
500
            result += installed_version_string
501
            result += installed_date_string
502
        elif repo.status() == Addon.Status.NO_UPDATE_AVAILABLE:
503
            result = translate("AddonsInstaller", "Up-to-date")
504
            result += installed_version_string
505
            result += installed_date_string
506
        elif repo.status() == Addon.Status.UPDATE_AVAILABLE:
507
            result = translate("AddonsInstaller", "Update available")
508
            result += installed_version_string
509
            result += installed_date_string
510
            result += available_version_string
511
        elif repo.status() == Addon.Status.PENDING_RESTART:
512
            result = translate("AddonsInstaller", "Pending restart")
513

514
        if repo.is_disabled():
515
            style = "style='color:" + utils.warning_color_string() + "; font-weight:bold;'"
516
            result += (
517
                f"<br/><span {style}>[" + translate("AddonsInstaller", "DISABLED") + "]</span>"
518
            )
519

520
        return result
521

522
    def paint(
523
        self,
524
        painter: QtGui.QPainter,
525
        option: QtWidgets.QStyleOptionViewItem,
526
        _: QtCore.QModelIndex,
527
    ):
528
        """Main paint function: renders this widget into a given rectangle,
529
        successively drawing all of its children."""
530
        painter.save()
531
        self.widget.resize(option.rect.size())
532
        painter.translate(option.rect.topLeft())
533
        self.widget.render(
534
            painter, QtCore.QPoint(), QtGui.QRegion(), QtWidgets.QWidget.DrawChildren
535
        )
536
        painter.restore()
537

538

539
class PackageListFilter(QtCore.QSortFilterProxyModel):
540
    """Handle filtering the item list on various criteria"""
541

542
    def __init__(self):
543
        super().__init__()
544
        self.package_type = 0  # Default to showing everything
545
        self.status = 0  # Default to showing any
546
        self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
547
        self.hide_obsolete = False
548
        self.hide_py2 = False
549
        self.hide_non_OSI_approved = False
550
        self.hide_non_FSF_libre = False
551
        self.hide_unlicensed = False
552
        self.hide_newer_freecad_required = False
553

554
    def setPackageFilter(
555
        self, package_type: int
556
    ) -> None:  # 0=All, 1=Workbenches, 2=Macros, 3=Preference Packs
557
        """Set the package filter to package_type and refreshes."""
558
        self.package_type = package_type
559
        self.invalidateFilter()
560

561
    def setStatusFilter(
562
        self, status: int
563
    ) -> None:  # 0=Any, 1=Installed, 2=Not installed, 3=Update available
564
        """Sets the status filter to status and refreshes."""
565
        self.status = status
566
        self.invalidateFilter()
567

568
    def setHidePy2(self, hide_py2: bool) -> None:
569
        """Sets whether to hide Python 2-only Addons"""
570
        self.hide_py2 = hide_py2
571
        self.invalidateFilter()
572

573
    def setHideObsolete(self, hide_obsolete: bool) -> None:
574
        """Sets whether to hide Addons marked obsolete"""
575
        self.hide_obsolete = hide_obsolete
576
        self.invalidateFilter()
577

578
    def setHideNonOSIApproved(self, hide: bool) -> None:
579
        """Sets whether to hide Addons with non-OSI-approved licenses"""
580
        self.hide_non_OSI_approved = hide
581
        self.invalidateFilter()
582

583
    def setHideNonFSFLibre(self, hide: bool) -> None:
584
        """Sets whether to hide Addons with non-FSF-Libre licenses"""
585
        self.hide_non_FSF_libre = hide
586
        self.invalidateFilter()
587

588
    def setHideUnlicensed(self, hide: bool) -> None:
589
        """Sets whether to hide addons without a specified license"""
590
        self.hide_unlicensed = hide
591
        self.invalidateFilter()
592

593
    def setHideNewerFreeCADRequired(self, hide_nfr: bool) -> None:
594
        """Sets whether to hide packages that have indicated they need a newer version
595
        of FreeCAD than the one currently running."""
596
        self.hide_newer_freecad_required = hide_nfr
597
        self.invalidateFilter()
598

599
    # def lessThan(self, left_in, right_in) -> bool:
600
    #    """Enable sorting of display name (not case-sensitive)."""
601
    #
602
    #    left = self.sourceModel().data(left_in, self.sortRole)
603
    #    right = self.sourceModel().data(right_in, self.sortRole)
604
    #
605
    #    return left.display_name.lower() < right.display_name.lower()
606

607
    def filterAcceptsRow(self, row, _parent=QtCore.QModelIndex()):
608
        """Do the actual filtering (called automatically by Qt when drawing the list)"""
609

610
        index = self.sourceModel().createIndex(row, 0)
611
        data = self.sourceModel().data(index, PackageListItemModel.DataAccessRole)
612
        if self.package_type == 1:
613
            if not data.contains_workbench():
614
                return False
615
        elif self.package_type == 2:
616
            if not data.contains_macro():
617
                return False
618
        elif self.package_type == 3:
619
            if not data.contains_preference_pack():
620
                return False
621

622
        if self.status == StatusFilter.INSTALLED:
623
            if data.status() == Addon.Status.NOT_INSTALLED:
624
                return False
625
        elif self.status == StatusFilter.NOT_INSTALLED:
626
            if data.status() != Addon.Status.NOT_INSTALLED:
627
                return False
628
        elif self.status == StatusFilter.UPDATE_AVAILABLE:
629
            if data.status() != Addon.Status.UPDATE_AVAILABLE:
630
                return False
631

632
        license_manager = get_license_manager()
633
        if data.status() == Addon.Status.NOT_INSTALLED:
634

635
            # If it's not installed, check to see if it's Py2 only
636
            if self.hide_py2 and data.python2:
637
                return False
638

639
            # If it's not installed, check to see if it's marked obsolete
640
            if self.hide_obsolete and data.obsolete:
641
                return False
642

643
            if self.hide_unlicensed:
644
                if not data.license or data.license in ["UNLICENSED", "UNLICENCED"]:
645
                    FreeCAD.Console.PrintLog(f"Hiding {data.name} because it has no license set\n")
646
                    return False
647

648
            # If it is not an OSI-approved license, check to see if we are hiding those
649
            if self.hide_non_OSI_approved or self.hide_non_FSF_libre:
650
                if not data.license:
651
                    return False
652
                licenses_to_check = []
653
                if type(data.license) is str:
654
                    licenses_to_check.append(data.license)
655
                elif type(data.license) is list:
656
                    for license_id in data.license:
657
                        if type(license_id) is str:
658
                            licenses_to_check.append(license_id)
659
                        else:
660
                            licenses_to_check.append(license_id.name)
661
                else:
662
                    licenses_to_check.append(data.license.name)
663

664
                fsf_libre = False
665
                osi_approved = False
666
                for license_id in licenses_to_check:
667
                    if not osi_approved and license_manager.is_osi_approved(license_id):
668
                        osi_approved = True
669
                    if not fsf_libre and license_manager.is_fsf_libre(license_id):
670
                        fsf_libre = True
671
                if self.hide_non_OSI_approved and not osi_approved:
672
                    # FreeCAD.Console.PrintLog(
673
                    #    f"Hiding addon {data.name} because its license, {licenses_to_check}, "
674
                    #    f"is "
675
                    #    f"not OSI approved\n"
676
                    # )
677
                    return False
678
                if self.hide_non_FSF_libre and not fsf_libre:
679
                    # FreeCAD.Console.PrintLog(
680
                    #    f"Hiding addon {data.name} because its license, {licenses_to_check},  is "
681
                    #    f"not FSF Libre\n"
682
                    # )
683
                    return False
684

685
        # If it's not installed, check to see if it's for a newer version of FreeCAD
686
        if (
687
            data.status() == Addon.Status.NOT_INSTALLED
688
            and self.hide_newer_freecad_required
689
            and data.metadata
690
        ):
691
            # Only hide if ALL content items require a newer version, otherwise
692
            # it's possible that this package actually provides versions of itself
693
            # for newer and older versions
694

695
            first_supported_version = get_first_supported_freecad_version(data.metadata)
696
            if first_supported_version is not None:
697
                current_fc_version = Version(from_list=FreeCAD.Version())
698
                if first_supported_version > current_fc_version:
699
                    return False
700

701
        name = data.display_name
702
        desc = data.description
703
        if hasattr(self, "filterRegularExpression"):  # Added in Qt 5.12
704
            re = self.filterRegularExpression()
705
            if re.isValid():
706
                re.setPatternOptions(QtCore.QRegularExpression.CaseInsensitiveOption)
707
                if re.match(name).hasMatch():
708
                    return True
709
                if re.match(desc).hasMatch():
710
                    return True
711
                if data.macro and data.macro.comment and re.match(data.macro.comment).hasMatch():
712
                    return True
713
                for tag in data.tags:
714
                    if re.match(tag).hasMatch():
715
                        return True
716
            return False
717
        # Only get here for Qt < 5.12
718
        re = self.filterRegExp()
719
        if re.isValid():
720
            re.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
721
            if re.indexIn(name) != -1:
722
                return True
723
            if re.indexIn(desc) != -1:
724
                return True
725
            if data.macro and data.macro.comment and re.indexIn(data.macro.comment) != -1:
726
                return True
727
            for tag in data.tags:
728
                if re.indexIn(tag) != -1:
729
                    return True
730
        return False
731

732

733
# pylint: disable=attribute-defined-outside-init, missing-function-docstring
734

735

736
class Ui_PackageList:
737
    """The contents of the PackageList widget"""
738

739
    def setupUi(self, form):
740
        if not form.objectName():
741
            form.setObjectName("PackageList")
742
        self.verticalLayout = QtWidgets.QVBoxLayout(form)
743
        self.verticalLayout.setObjectName("verticalLayout")
744
        self.horizontalLayout_6 = QtWidgets.QHBoxLayout()
745
        self.horizontalLayout_6.setObjectName("horizontalLayout_6")
746

747
        self.view_bar = WidgetViewControlBar(form)
748
        self.view_bar.setObjectName("ViewControlBar")
749
        self.horizontalLayout_6.addWidget(self.view_bar)
750

751
        self.verticalLayout.addLayout(self.horizontalLayout_6)
752

753
        self.listPackages = QtWidgets.QListView(form)
754
        self.listPackages.setObjectName("listPackages")
755
        self.listPackages.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
756
        self.listPackages.setProperty("showDropIndicator", False)
757
        self.listPackages.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
758
        self.listPackages.setResizeMode(QtWidgets.QListView.Adjust)
759
        self.listPackages.setUniformItemSizes(False)
760
        self.listPackages.setAlternatingRowColors(True)
761
        self.listPackages.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
762
        self.listPackages.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
763
        self.listPackages.verticalScrollBar().setSingleStep(24)
764

765
        self.verticalLayout.addWidget(self.listPackages)
766

767
        self.progressBar = WidgetProgressBar()
768
        self.verticalLayout.addWidget(self.progressBar)
769

770
        QtCore.QMetaObject.connectSlotsByName(form)
771

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.