Skip to content

Commit 33addd5

Browse files
authored
Merge 12e7dad into d0614df
2 parents d0614df + 12e7dad commit 33addd5

3 files changed

Lines changed: 152 additions & 11 deletions

File tree

source/gui/_addonStoreGui/controls/actions.py

Lines changed: 46 additions & 5 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

@@ -15,7 +15,7 @@
1515

1616
import wx
1717

18-
from _addonStore.models.status import _StatusFilterKey
18+
from _addonStore.models.status import _StatusFilterKey, AvailableAddonStatus
1919
from logHandler import log
2020
import ui
2121

@@ -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,48 @@ 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+
#zzz validCheck=lambda aVMs: self._storeVM._filteredStatusKey == _StatusFilterKey.AVAILABLE,
144+
validCheck=lambda aVMs: any([
145+
(
146+
aVM.status == AvailableAddonStatus.AVAILABLE
147+
or (aVM.status == AvailableAddonStatus.INCOMPATIBLE and aVM.model.canOverrideCompatibility)
148+
)
149+
for aVM in aVMs
150+
]),
151+
actionTarget=self._selectedAddons
152+
),
153+
BatchAddonActionVM(
154+
# Translators: Label for an action that updates the selected add-ons
155+
displayName=pgettext("addonStore", "&Update selected add-ons"),
156+
actionHandler=self._storeVM.getAddons,
157+
validCheck=lambda aVMs: self._storeVM._filteredStatusKey in [
158+
_StatusFilterKey.INSTALLED,
159+
_StatusFilterKey.UPDATE,
160+
],
161+
actionTarget=self._selectedAddons
162+
),
163+
BatchAddonActionVM(
164+
# Translators: Label for an action that removes the selected add-ons
165+
displayName=pgettext("addonStore", "&Remove selected add-ons"),
166+
actionHandler=self._storeVM.removeAddons,
167+
validCheck=lambda aVMs: self._storeVM._filteredStatusKey in [
168+
_StatusFilterKey.INSTALLED,
169+
_StatusFilterKey.INCOMPATIBLE,
170+
],
171+
actionTarget=self._selectedAddons
172+
),
173+
BatchAddonActionVM(
174+
# Translators: Label for an action that enables the selected add-ons
175+
displayName=pgettext("addonStore", "&Enable selected add-ons"),
176+
actionHandler=self._storeVM.enableAddons,
177+
validCheck=lambda aVMs: True,
178+
actionTarget=self._selectedAddons
179+
),
180+
BatchAddonActionVM(
181+
# Translators: Label for an action that disables the selected add-ons
182+
displayName=pgettext("addonStore", "&Disable selected add-ons"),
183+
actionHandler=self._storeVM.disableAddons,
184+
validCheck=lambda aVMs: True,
144185
actionTarget=self._selectedAddons
145186
),
146187
]

source/gui/_addonStoreGui/controls/messageDialogs.py

Lines changed: 37 additions & 1 deletion
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

@@ -108,6 +108,20 @@ def _shouldProceedToRemoveAddonDialog(
108108
) == wx.YES
109109

110110

111+
def _shouldProceedToRemoveMultipleAddonDialog(nAddons) -> bool:
112+
return messageBox(
113+
pgettext(
114+
"addonStore",
115+
# Translators: Presented when attempting to remove multiple selected add-ons.
116+
"Are you sure you wish to remove the {nAddons} selected add-ons from NVDA? "
117+
"This cannot be undone.",
118+
).format(nAddons=nAddons),
119+
# Translators: Title for message asking if the user really wishes to remove the selected Add-on.
120+
pgettext("addonStore", "Remove Add-ons"),
121+
wx.YES_NO | wx.NO_DEFAULT | wx.ICON_WARNING
122+
) == wx.YES
123+
124+
111125
def _shouldInstallWhenAddonTooOldDialog(
112126
parent: wx.Window,
113127
addon: _AddonGUIModel
@@ -168,6 +182,28 @@ def _shouldEnableWhenAddonTooOldDialog(
168182
return res == wx.YES
169183

170184

185+
def _shouldEnableWhenMultipleAddonsTooOldDialog(
186+
nAddons: int,
187+
) -> bool:
188+
incompatibleMessage = pgettext(
189+
"addonStore",
190+
# Translators: The message displayed when enabling one or more incompatible add-on packages
191+
# because the add-ons are too old for the running version of NVDA.
192+
"Warning: {nAddons} add-ons are incompatible. "
193+
"Check for an updated version of these add-on if possible. "
194+
"Enabling may cause unstable behavior in NVDA.\n"
195+
"Proceed with enabling anyway? "
196+
).format(
197+
nAddons=nAddons,
198+
)
199+
return messageBox(
200+
incompatibleMessage,
201+
# Translators: Title for message asking if the user really wishes to enable the selected Add-ons.
202+
pgettext("addonStore", "Add-ons not compatible"),
203+
wx.YES_NO | wx.NO_DEFAULT | wx.ICON_WARNING,
204+
) == wx.YES
205+
206+
171207
def _showAddonInfo(addon: _AddonGUIModel) -> None:
172208
message = [
173209
pgettext(

source/gui/_addonStoreGui/viewModels/store.py

Lines changed: 69 additions & 5 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

@@ -44,7 +44,9 @@
4444

4545
from ..controls.messageDialogs import (
4646
_shouldEnableWhenAddonTooOldDialog,
47+
_shouldEnableWhenMultipleAddonsTooOldDialog,
4748
_shouldProceedToRemoveAddonDialog,
49+
_shouldProceedToRemoveMultipleAddonDialog,
4850
_shouldInstallWhenAddonTooOldDialog,
4951
_shouldProceedWhenInstalledAddonVersionUnknown,
5052
)
@@ -249,16 +251,26 @@ def helpAddon(self, listItemVM: AddonListItemVM) -> None:
249251
assert path is not None
250252
startfile(path)
251253

252-
def removeAddon(self, listItemVM: AddonListItemVM[_AddonGUIModel]) -> None:
254+
def removeAddon(self, listItemVM: AddonListItemVM[_AddonGUIModel], askConfirmation: bool = True) -> None:
253255
assert addonDataManager
254256
assert listItemVM.model
255-
if _shouldProceedToRemoveAddonDialog(listItemVM.model):
257+
if not askConfirmation or _shouldProceedToRemoveAddonDialog(listItemVM.model):
256258
addonDataManager._deleteCacheInstalledAddon(listItemVM.model.name)
257259
assert listItemVM.model._addonHandlerModel is not None
258260
listItemVM.model._addonHandlerModel.requestRemove()
259261
self.refresh()
260262
listItemVM.status = getStatus(listItemVM.model)
261263

264+
def removeAddons(self, listItemVMs: Iterable[AddonListItemVM[_AddonStoreModel]]) -> None:
265+
if not _shouldProceedToRemoveMultipleAddonDialog(nAddons=len(listItemVMs)):
266+
log.debug("Aborting batch remove add-ons.")
267+
return
268+
for aVM in listItemVMs:
269+
if aVM.status == AvailableAddonStatus.PENDING_REMOVE:
270+
log.debug(f"Skipping {aVM.Id} as it is already pending remove")
271+
else:
272+
self.removeAddon(aVM, askConfirmation=False)
273+
262274
def installOverrideIncompatibilityForAddon(self, listItemVM: AddonListItemVM) -> None:
263275
from gui import mainFrame
264276
if _shouldInstallWhenAddonTooOldDialog(mainFrame, listItemVM.model):
@@ -298,18 +310,70 @@ def _handleEnableDisable(self, listItemVM: AddonListItemVM[_AddonManifestModel],
298310
listItemVM.status = getStatus(listItemVM.model)
299311
self.refresh()
300312

301-
def enableOverrideIncompatibilityForAddon(self, listItemVM: AddonListItemVM[_AddonManifestModel]) -> None:
313+
def enableOverrideIncompatibilityForAddon(
314+
self,
315+
listItemVM: AddonListItemVM[_AddonManifestModel],
316+
askConfirmation: bool = True,
317+
) -> None:
302318
from ... import mainFrame
303-
if _shouldEnableWhenAddonTooOldDialog(mainFrame, listItemVM.model):
319+
if not askConfirmation or _shouldEnableWhenAddonTooOldDialog(mainFrame, listItemVM.model):
304320
listItemVM.model.enableCompatibilityOverride()
305321
self._handleEnableDisable(listItemVM, True)
306322

307323
def enableAddon(self, listItemVM: AddonListItemVM) -> None:
308324
self._handleEnableDisable(listItemVM, True)
309325

326+
def enableAddons(self, listItemVMs: Iterable[AddonListItemVM[_AddonStoreModel]]) -> None:
327+
disabledIncompatibleAddons = [
328+
aVM for aVM in listItemVMs
329+
if aVM.status in (
330+
AvailableAddonStatus.INCOMPATIBLE_DISABLED,
331+
AvailableAddonStatus.PENDING_INCOMPATIBLE_DISABLED,
332+
)
333+
and not aVM.model.isCompatible
334+
and aVM.model.canOverrideCompatibility
335+
]
336+
if (
337+
len(disabledIncompatibleAddons) > 0
338+
and not _shouldEnableWhenMultipleAddonsTooOldDialog(len(disabledIncompatibleAddons))
339+
):
340+
log.debug("Override incompatibility has been declined. Aborting batch enable add-ons.")
341+
return
342+
for aVM in listItemVMs:
343+
if aVM.status in (
344+
AvailableAddonStatus.INCOMPATIBLE_DISABLED,
345+
AvailableAddonStatus.PENDING_INCOMPATIBLE_DISABLED,
346+
) and not aVM.model.isCompatible and aVM.model.canOverrideCompatibility:
347+
self.enableOverrideIncompatibilityForAddon(aVM, askConfirmation=False)
348+
elif aVM.status in (
349+
AvailableAddonStatus.DISABLED,
350+
AvailableAddonStatus.PENDING_DISABLE,
351+
):
352+
self.enableAddon(aVM)
353+
else:
354+
log.debug(
355+
f"Skipping {aVM.Id} as it is not disabled, pending disable, incompatible disabled "
356+
"or pending incompatible disabled"
357+
)
358+
359+
360+
310361
def disableAddon(self, listItemVM: AddonListItemVM) -> None:
311362
self._handleEnableDisable(listItemVM, False)
312363

364+
def disableAddons(self, listItemVMs: Iterable[AddonListItemVM[_AddonStoreModel]]) -> None:
365+
for aVM in listItemVMs:
366+
if aVM.status in [
367+
AvailableAddonStatus.DISABLED,
368+
AvailableAddonStatus.PENDING_DISABLE,
369+
AvailableAddonStatus.INCOMPATIBLE_DISABLED,
370+
AvailableAddonStatus.PENDING_INCOMPATIBLE_DISABLED,
371+
AvailableAddonStatus.PENDING_REMOVE,
372+
]:
373+
log.debug(f"Skipping {aVM.Id} as it is already disabled, pending disabled or pending remove")
374+
else:
375+
self.disableAddon(aVM)
376+
313377
def replaceAddon(self, listItemVM: AddonListItemVM) -> None:
314378
from ... import mainFrame
315379
if _shouldProceedWhenInstalledAddonVersionUnknown(mainFrame, listItemVM.model):

0 commit comments

Comments
 (0)