Skip to content

Commit b29435b

Browse files
authored
Merge 5c255bb into a5d9e5b
2 parents a5d9e5b + 5c255bb commit b29435b

4 files changed

Lines changed: 326 additions & 68 deletions

File tree

source/gui/_addonStoreGui/controls/actions.py

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# A part of NonVisual Desktop Access (NVDA)
2-
# Copyright (C) 2022-2023 NV Access Limited
2+
# Copyright (C) 2022-2023 NV Access Limited, Cyrille Bougot
33
# This file is covered by the GNU General Public License.
44
# See the file COPYING for more details.
55

@@ -77,15 +77,15 @@ def _populateContextMenu(self):
7777
elif isMenuItemInContextMenu:
7878
# The action is invalid but the menu item exists and is in the context menu.
7979
# Remove the menu item from the context menu.
80-
self._contextMenu.RemoveItem(menuItem)
80+
self._contextMenu.Remove(menuItem)
8181
del self._actionMenuItemMap[action]
8282

8383
menuItems: List[wx.MenuItem] = list(self._contextMenu.GetMenuItems())
8484
for menuItem in menuItems:
8585
if menuItem not in self._actionMenuItemMap.values():
8686
# The menu item is not in the action menu item map.
8787
# It should be removed from the context menu.
88-
self._contextMenu.RemoveItem(menuItem)
88+
self._contextMenu.Remove(menuItem)
8989

9090

9191
class _MonoActionsContextMenu(_ActionsContextMenuP[AddonActionVM]):
@@ -140,7 +140,85 @@ def _actions(self) -> List[BatchAddonActionVM]:
140140
# Translators: Label for an action that installs the selected add-ons
141141
displayName=pgettext("addonStore", "&Install selected add-ons"),
142142
actionHandler=self._storeVM.getAddons,
143-
validCheck=lambda aVMs: self._storeVM._filteredStatusKey == _StatusFilterKey.AVAILABLE,
143+
validCheck=lambda aVMs: (
144+
self._storeVM._filteredStatusKey == _StatusFilterKey.AVAILABLE
145+
and AddonListValidator(aVMs).canUseInstallAction()
146+
),
147+
actionTarget=self._selectedAddons
148+
),
149+
BatchAddonActionVM(
150+
# Translators: Label for an action that updates the selected add-ons
151+
displayName=pgettext("addonStore", "&Update selected add-ons"),
152+
actionHandler=self._storeVM.getAddons,
153+
validCheck=lambda aVMs: (
154+
self._storeVM._filteredStatusKey in [
155+
_StatusFilterKey.INSTALLED,
156+
_StatusFilterKey.UPDATE,
157+
]
158+
and AddonListValidator(aVMs).canUseUpdateAction()
159+
),
160+
actionTarget=self._selectedAddons
161+
),
162+
BatchAddonActionVM(
163+
# Translators: Label for an action that removes the selected add-ons
164+
displayName=pgettext("addonStore", "&Remove selected add-ons"),
165+
actionHandler=self._storeVM.removeAddons,
166+
validCheck=lambda aVMs: (
167+
self._storeVM._filteredStatusKey in [
168+
_StatusFilterKey.INSTALLED,
169+
_StatusFilterKey.INCOMPATIBLE,
170+
]
171+
and AddonListValidator(aVMs).canUseRemoveAction()
172+
),
173+
actionTarget=self._selectedAddons
174+
),
175+
BatchAddonActionVM(
176+
# Translators: Label for an action that enables the selected add-ons
177+
displayName=pgettext("addonStore", "&Enable selected add-ons"),
178+
actionHandler=self._storeVM.enableAddons,
179+
validCheck=lambda aVMs: AddonListValidator(aVMs).canUseEnableAction(),
180+
actionTarget=self._selectedAddons
181+
),
182+
BatchAddonActionVM(
183+
# Translators: Label for an action that disables the selected add-ons
184+
displayName=pgettext("addonStore", "&Disable selected add-ons"),
185+
actionHandler=self._storeVM.disableAddons,
186+
validCheck=lambda aVMs: AddonListValidator(aVMs).canUseDisableAction(),
144187
actionTarget=self._selectedAddons
145188
),
146189
]
190+
191+
192+
class AddonListValidator:
193+
def __init__(self, addonsList: List[AddonListItemVM]):
194+
self.addonsList = addonsList
195+
196+
def canUseInstallAction(self):
197+
for aVM in self.addonsList:
198+
if aVM.canUseInstallAction() or aVM.canUseInstallOverrideIncompatibilityAction():
199+
return True
200+
return False
201+
202+
def canUseUpdateAction(self):
203+
for aVM in self.addonsList:
204+
if aVM.canUseUpdateAction():
205+
return True
206+
return False
207+
208+
def canUseRemoveAction(self):
209+
for aVM in self.addonsList:
210+
if aVM.canUseRemoveAction():
211+
return True
212+
return False
213+
214+
def canUseEnableAction(self):
215+
for aVM in self.addonsList:
216+
if aVM.canUseEnableOverrideIncompatibilityAction() or aVM.canUseEnableAction():
217+
return True
218+
return False
219+
220+
def canUseDisableAction(self) -> bool:
221+
for aVM in self.addonsList:
222+
if aVM.canUseDisableAction():
223+
return True
224+
return False

source/gui/_addonStoreGui/controls/messageDialogs.py

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# A part of NonVisual Desktop Access (NVDA)
2-
# Copyright (C) 2023 NV Access Limited
2+
# Copyright (C) 2023 NV Access Limited, Cyrille Bougot
33
# This file is covered by the GNU General Public License.
44
# See the file COPYING for more details.
55

@@ -32,6 +32,10 @@
3232

3333

3434
class ErrorAddonInstallDialogWithYesNoButtons(ErrorAddonInstallDialog):
35+
def __init__(self, *args, useRememberChoiceCheckbox=False, **kwargs):
36+
self.useRememberChoiceCheckbox = useRememberChoiceCheckbox
37+
super().__init__(*args, **kwargs)
38+
3539
def _addButtons(self, buttonHelper: ButtonHelper) -> None:
3640
addonInfoButton = buttonHelper.addButton(
3741
self,
@@ -58,6 +62,21 @@ def _addButtons(self, buttonHelper: ButtonHelper) -> None:
5862
noButton.SetDefault()
5963
noButton.Bind(wx.EVT_BUTTON, lambda evt: self.EndModal(wx.NO))
6064

65+
def _addContents(self, contentsSizer: BoxSizerHelper):
66+
if self.useRememberChoiceCheckbox:
67+
# Translators: A checkbox in the dialog to remember the choice made when installing or enabling
68+
# incompatible add-ons, or when removing add-ons.
69+
self.rememberChoiceCheckbox = wx.CheckBox(
70+
self,
71+
label=pgettext("addonStore", "Remember this choice for subsequent add-ons"),
72+
)
73+
contentsSizer.addItem(self.rememberChoiceCheckbox)
74+
75+
def shouldRememberChoice(self):
76+
if self.useRememberChoiceCheckbox:
77+
return self.rememberChoiceCheckbox.IsChecked()
78+
return False
79+
6180

6281
def _shouldProceedWhenInstalledAddonVersionUnknown(
6382
parent: wx.Window,
@@ -92,26 +111,34 @@ def _shouldProceedWhenInstalledAddonVersionUnknown(
92111

93112

94113
def _shouldProceedToRemoveAddonDialog(
95-
addon: "SupportsVersionCheck"
114+
parent,
115+
addon: "SupportsVersionCheck",
116+
useRememberChoiceCheckbox: bool = False,
96117
) -> bool:
97-
return messageBox(
98-
pgettext(
99-
"addonStore",
100-
# Translators: Presented when attempting to remove the selected add-on.
101-
# {addon} is replaced with the add-on name.
102-
"Are you sure you wish to remove the {addon} add-on from NVDA? "
103-
"This cannot be undone."
104-
).format(addon=addon.name),
118+
removeMessage = pgettext(
119+
"addonStore",
120+
# Translators: Presented when attempting to remove the selected add-on.
121+
# {addon} is replaced with the add-on name.
122+
"Are you sure you wish to remove the {addon} add-on from NVDA? "
123+
"This cannot be undone."
124+
).format(addon=addon.name)
125+
dlg = ErrorAddonInstallDialogWithYesNoButtons(
126+
parent=parent,
105127
# Translators: Title for message asking if the user really wishes to remove the selected Add-on.
106-
pgettext("addonStore", "Remove Add-on"),
107-
wx.YES_NO | wx.NO_DEFAULT | wx.ICON_WARNING
108-
) == wx.YES
128+
title=pgettext("addonStore", "Remove Add-on"),
129+
message=removeMessage,
130+
showAddonInfoFunction=lambda: _showAddonInfo(addon),
131+
useRememberChoiceCheckbox=useRememberChoiceCheckbox,
132+
)
133+
res = displayDialogAsModal(dlg)
134+
return (res == wx.YES), dlg.shouldRememberChoice()
109135

110136

111137
def _shouldInstallWhenAddonTooOldDialog(
112138
parent: wx.Window,
113-
addon: _AddonGUIModel
114-
) -> bool:
139+
addon: _AddonGUIModel,
140+
useRememberChoiceCheckbox: bool = False,
141+
) -> tuple[bool, bool]:
115142
incompatibleMessage = pgettext(
116143
"addonStore",
117144
# Translators: The message displayed when installing an add-on package that is incompatible
@@ -128,20 +155,23 @@ def _shouldInstallWhenAddonTooOldDialog(
128155
lastTestedNVDAVersion=addonAPIVersion.formatForGUI(addon.lastTestedNVDAVersion),
129156
NVDAVersion=addonAPIVersion.formatForGUI(addonAPIVersion.CURRENT)
130157
)
131-
res = displayDialogAsModal(ErrorAddonInstallDialogWithYesNoButtons(
158+
dlg = ErrorAddonInstallDialogWithYesNoButtons(
132159
parent=parent,
133160
# Translators: The title of a dialog presented when an error occurs.
134161
title=pgettext("addonStore", "Add-on not compatible"),
135162
message=incompatibleMessage,
136-
showAddonInfoFunction=lambda: _showAddonInfo(addon)
137-
))
138-
return res == wx.YES
163+
showAddonInfoFunction=lambda: _showAddonInfo(addon),
164+
useRememberChoiceCheckbox=useRememberChoiceCheckbox,
165+
)
166+
res = displayDialogAsModal(dlg)
167+
return (res == wx.YES), dlg.shouldRememberChoice()
139168

140169

141170
def _shouldEnableWhenAddonTooOldDialog(
142171
parent: wx.Window,
143-
addon: _AddonGUIModel
144-
) -> bool:
172+
addon: _AddonGUIModel,
173+
useRememberChoiceCheckbox: bool = False,
174+
) -> tuple[bool, bool]:
145175
incompatibleMessage = pgettext(
146176
"addonStore",
147177
# Translators: The message displayed when enabling an add-on package that is incompatible
@@ -158,14 +188,16 @@ def _shouldEnableWhenAddonTooOldDialog(
158188
lastTestedNVDAVersion=addonAPIVersion.formatForGUI(addon.lastTestedNVDAVersion),
159189
NVDAVersion=addonAPIVersion.formatForGUI(addonAPIVersion.CURRENT)
160190
)
161-
res = displayDialogAsModal(ErrorAddonInstallDialogWithYesNoButtons(
191+
dlg = ErrorAddonInstallDialogWithYesNoButtons(
162192
parent=parent,
163193
# Translators: The title of a dialog presented when an error occurs.
164194
title=pgettext("addonStore", "Add-on not compatible"),
165195
message=incompatibleMessage,
166-
showAddonInfoFunction=lambda: _showAddonInfo(addon)
167-
))
168-
return res == wx.YES
196+
showAddonInfoFunction=lambda: _showAddonInfo(addon),
197+
useRememberChoiceCheckbox=useRememberChoiceCheckbox,
198+
)
199+
res = displayDialogAsModal(dlg)
200+
return (res == wx.YES), dlg.shouldRememberChoice()
169201

170202

171203
def _showAddonInfo(addon: _AddonGUIModel) -> None:

source/gui/_addonStoreGui/viewModels/addonList.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,42 @@ def status(self, newStatus: AvailableAddonStatus):
124124
def Id(self) -> str:
125125
return self._model.listItemVMId
126126

127+
def canUseInstallAction(self):
128+
return self.status == AvailableAddonStatus.AVAILABLE
129+
130+
def canUseInstallOverrideIncompatibilityAction(self):
131+
return self.status == AvailableAddonStatus.INCOMPATIBLE and self.model.canOverrideCompatibility
132+
133+
def canUseUpdateAction(self):
134+
return self.status == AvailableAddonStatus.UPDATE
135+
136+
def canUseReplaceAction(self):
137+
return self.status == AvailableAddonStatus.REPLACE_SIDE_LOAD
138+
139+
def canUseRemoveAction(self):
140+
return (
141+
self.model.isInstalled
142+
and self.status != AvailableAddonStatus.PENDING_REMOVE
143+
)
144+
145+
def canUseEnableAction(self):
146+
return self.status == AvailableAddonStatus.DISABLED or self.status == AvailableAddonStatus.PENDING_DISABLE
147+
148+
def canUseEnableOverrideIncompatibilityAction(self):
149+
return self.status in (
150+
AvailableAddonStatus.INCOMPATIBLE_DISABLED,
151+
AvailableAddonStatus.PENDING_INCOMPATIBLE_DISABLED,
152+
) and self.model.canOverrideCompatibility
153+
154+
def canUseDisableAction(self):
155+
return self.model.isInstalled and self.status not in (
156+
AvailableAddonStatus.DISABLED,
157+
AvailableAddonStatus.PENDING_DISABLE,
158+
AvailableAddonStatus.INCOMPATIBLE_DISABLED,
159+
AvailableAddonStatus.PENDING_INCOMPATIBLE_DISABLED,
160+
AvailableAddonStatus.PENDING_REMOVE,
161+
)
162+
127163
def __repr__(self) -> str:
128164
return f"{self.__class__.__name__}: {self.Id}, {self.status}"
129165

0 commit comments

Comments
 (0)