LicensePresenter.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. # Copyright (c) 2022 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import os
  4. from typing import Dict, Optional, List, Any
  5. from PyQt6.QtCore import QObject, pyqtSlot
  6. from UM.Logger import Logger
  7. from UM.PackageManager import PackageManager
  8. from UM.Signal import Signal
  9. from cura.CuraApplication import CuraApplication
  10. from UM.i18n import i18nCatalog
  11. from .LicenseModel import LicenseModel
  12. class LicensePresenter(QObject):
  13. """Presents licenses for a set of packages for the user to accept or reject.
  14. Call present() exactly once to show a licenseDialog for a set of packages
  15. Before presenting another set of licenses, create a new instance using resetCopy().
  16. licenseAnswers emits a list of Dicts containing answers when the user has made a choice for all provided packages.
  17. """
  18. def __init__(self, app: CuraApplication) -> None:
  19. super().__init__()
  20. self._presented = False
  21. """Whether present() has been called and state is expected to be initialized"""
  22. self._dialog: Optional[QObject] = None
  23. self._package_manager: PackageManager = app.getPackageManager()
  24. # Emits List[Dict[str, [Any]] containing for example
  25. # [{ "package_id": "BarbarianPlugin", "package_path" : "/tmp/dg345as", "accepted" : True }]
  26. self.licenseAnswers = Signal()
  27. self._current_package_idx = 0
  28. self._package_models: List[Dict] = []
  29. self._catalog = i18nCatalog("cura")
  30. decline_button_text = self._catalog.i18nc("@button", "Decline and remove from account")
  31. self._license_model: LicenseModel = LicenseModel(decline_button_text=decline_button_text)
  32. self._page_count = 0
  33. self._app = app
  34. self._compatibility_dialog_path = "resources/qml/MultipleLicenseDialog.qml"
  35. def present(self, plugin_path: str, packages: Dict[str, Dict[str, str]]) -> None:
  36. """Show a license dialog for multiple packages where users can read a license and accept or decline them
  37. :param plugin_path: Root directory of the Toolbox plugin
  38. :param packages: Dict[package id, file path]
  39. """
  40. if self._presented:
  41. Logger.error("{clazz} is single-use. Create a new {clazz} instead", clazz=self.__class__.__name__)
  42. return
  43. path = os.path.join(plugin_path, self._compatibility_dialog_path)
  44. self._initState(packages)
  45. if self._page_count == 0:
  46. self.licenseAnswers.emit(self._package_models)
  47. return
  48. if self._dialog is None:
  49. context_properties = {
  50. "licenseModel": self._license_model,
  51. "handler": self
  52. }
  53. self._dialog = self._app.createQmlComponent(path, context_properties)
  54. self._presentCurrentPackage()
  55. self._presented = True
  56. def resetCopy(self) -> "LicensePresenter":
  57. """Clean up and return a new copy with the same settings such as app"""
  58. if self._dialog:
  59. self._dialog.close()
  60. self.licenseAnswers.disconnectAll()
  61. return LicensePresenter(self._app)
  62. @pyqtSlot()
  63. def onLicenseAccepted(self) -> None:
  64. self._package_models[self._current_package_idx]["accepted"] = True
  65. self._checkNextPage()
  66. @pyqtSlot()
  67. def onLicenseDeclined(self) -> None:
  68. self._package_models[self._current_package_idx]["accepted"] = False
  69. self._checkNextPage()
  70. def _initState(self, packages: Dict[str, Dict[str, Any]]) -> None:
  71. implicitly_accepted_count = 0
  72. for package_id, item in packages.items():
  73. item["package_id"] = package_id
  74. try:
  75. item["licence_content"] = self._package_manager.getPackageLicense(item["package_path"])
  76. except EnvironmentError as e:
  77. Logger.error(f"Could not open downloaded package {package_id} to read license file! {type(e)} - {e}")
  78. continue # Skip this package.
  79. if item["licence_content"] is None:
  80. # Implicitly accept when there is no license
  81. item["accepted"] = True
  82. implicitly_accepted_count = implicitly_accepted_count + 1
  83. self._package_models.append(item)
  84. else:
  85. item["accepted"] = None #: None: no answer yet
  86. # When presenting the packages, we want to show packages which have a license first.
  87. # In fact, we don't want to show the others at all because they are implicitly accepted
  88. self._package_models.insert(0, item)
  89. CuraApplication.getInstance().processEvents()
  90. self._page_count = len(self._package_models) - implicitly_accepted_count
  91. self._license_model.setPageCount(self._page_count)
  92. def _presentCurrentPackage(self) -> None:
  93. package_model = self._package_models[self._current_package_idx]
  94. package_info = self._package_manager.getPackageInfo(package_model["package_path"])
  95. self._license_model.setCurrentPageIdx(self._current_package_idx)
  96. self._license_model.setPackageName(package_info["display_name"])
  97. self._license_model.setIconUrl(package_model["icon_url"])
  98. self._license_model.setLicenseText(package_model["licence_content"])
  99. if self._dialog:
  100. self._dialog.open() # Does nothing if already open
  101. def _checkNextPage(self) -> None:
  102. if self._current_package_idx + 1 < self._page_count:
  103. self._current_package_idx += 1
  104. self._presentCurrentPackage()
  105. else:
  106. if self._dialog:
  107. self._dialog.close()
  108. self.licenseAnswers.emit(self._package_models)