Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add-on store: Handle bulk install #15350

Merged
merged 30 commits into from Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3ae2042
Add-on store: Handle bulk install
seanbudd Aug 31, 2023
0f70756
Fix up download complete
seanbudd Aug 31, 2023
6cfd6e2
update docs
seanbudd Aug 31, 2023
1f2a703
add message when no context actions available
seanbudd Aug 31, 2023
6f5acc5
make code generic
seanbudd Aug 31, 2023
cb9d00f
make code generic
seanbudd Aug 31, 2023
871fc41
fixp typo
seanbudd Aug 31, 2023
ffda1ec
fixp typo
seanbudd Aug 31, 2023
d4a69b2
move para
seanbudd Aug 31, 2023
f176a63
simplify fix/type error
seanbudd Aug 31, 2023
0364589
use batch not bulk, more popular terminology
seanbudd Aug 31, 2023
d583b69
add manual test plan
seanbudd Aug 31, 2023
e1a5f88
simplify fix/type error
seanbudd Aug 31, 2023
424a3af
Fix background downloading of add-ons
seanbudd Sep 6, 2023
e0f4beb
Allow bulk install
seanbudd Sep 6, 2023
ac4e962
Merge branch 'allowBulkInstall' into allowBulkInstall2
seanbudd Sep 6, 2023
7991508
Fix background downloading of add-ons
seanbudd Sep 6, 2023
1cae28b
Allow bulk install
seanbudd Sep 6, 2023
e1c1099
Merge commit 'ac4e962b1' into allowBulkInstall
seanbudd Sep 6, 2023
6c1bb31
address review comments
seanbudd Sep 6, 2023
7cff663
fix lint
seanbudd Sep 6, 2023
b4cdc5c
unbind space
seanbudd Sep 6, 2023
28b1e21
Merge remote-tracking branch 'origin/beta' into allowBulkInstall
seanbudd Sep 7, 2023
675f00a
Merge remote-tracking branch 'origin/beta' into allowBulkInstall
seanbudd Sep 8, 2023
35efcb9
minor fixes
seanbudd Sep 8, 2023
075514e
fix up lint: too complex
seanbudd Sep 11, 2023
e16be4f
Merge remote-tracking branch 'origin/master' into allowBulkInstall
seanbudd Sep 12, 2023
47474fd
fix lint
seanbudd Sep 14, 2023
5a6d1bc
Merge remote-tracking branch 'origin/master' into allowBulkInstall
seanbudd Sep 28, 2023
ddbfa0c
update changes
seanbudd Sep 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
97 changes: 85 additions & 12 deletions source/gui/_addonStoreGui/controls/actions.py
Expand Up @@ -3,25 +3,38 @@
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

from abc import ABC, abstractmethod
import functools
from typing import (
Dict,
Generic,
Iterable,
List,
TypeVar,
)

import wx

from _addonStore.models.status import _StatusFilterKey
from logHandler import log
import ui

from ..viewModels.action import AddonActionVM
from ..viewModels.action import AddonActionVM, BatchAddonActionVM
from ..viewModels.addonList import AddonListItemVM
from ..viewModels.store import AddonStoreVM


class _ActionsContextMenu:
def __init__(self, storeVM: AddonStoreVM):
self._storeVM = storeVM
self._actionMenuItemMap: Dict[AddonActionVM, wx.MenuItem] = {}
self._contextMenu = wx.Menu()
AddonActionT = TypeVar("AddonActionT", AddonActionVM, BatchAddonActionVM)


class _ActionsContextMenuP(Generic[AddonActionT], ABC):
_actions: List[AddonActionT]
_actionMenuItemMap: Dict[AddonActionT, wx.MenuItem]
_contextMenu: wx.Menu

@abstractmethod
def _menuItemClicked(self, evt: wx.ContextMenuEvent, actionVM: AddonActionT):
...

def popupContextMenuFromPosition(
self,
Expand All @@ -31,14 +44,9 @@ def popupContextMenuFromPosition(
self._populateContextMenu()
targetWindow.PopupMenu(self._contextMenu, pos=position)

def _menuItemClicked(self, evt: wx.ContextMenuEvent, actionVM: AddonActionVM):
selectedAddon = actionVM.listItemVM
log.debug(f"action selected: actionVM: {actionVM}, selectedAddon: {selectedAddon}")
actionVM.actionHandler(selectedAddon)

def _populateContextMenu(self):
prevActionIndex = -1
for action in self._storeVM.actionVMList:
for action in self._actions:
menuItem = self._actionMenuItemMap.get(action)
menuItems: List[wx.MenuItem] = list(self._contextMenu.GetMenuItems())
isMenuItemInContextMenu = menuItem is not None and menuItem in menuItems
Expand Down Expand Up @@ -71,3 +79,68 @@ def _populateContextMenu(self):
# Remove the menu item from the context menu.
self._contextMenu.RemoveItem(menuItem)
del self._actionMenuItemMap[action]

menuItems: List[wx.MenuItem] = list(self._contextMenu.GetMenuItems())
for menuItem in menuItems:
if menuItem not in self._actionMenuItemMap.values():
# The menu item is not in the action menu item map.
# It should be removed from the context menu.
self._contextMenu.RemoveItem(menuItem)


class _MonoActionsContextMenu(_ActionsContextMenuP[AddonActionVM]):
"""Context menu for actions for a single add-on"""
def __init__(self, storeVM: AddonStoreVM):
self._storeVM = storeVM
self._actionMenuItemMap = {}
self._contextMenu = wx.Menu()

def _menuItemClicked(self, evt: wx.ContextMenuEvent, actionVM: AddonActionVM):
selectedAddon = actionVM.actionTarget
log.debug(f"action selected: actionVM: {actionVM.displayName}, selectedAddon: {selectedAddon}")
actionVM.actionHandler(selectedAddon)

@property
def _actions(self) -> List[AddonActionVM]:
return self._storeVM.actionVMList


class _BatchActionsContextMenu(_ActionsContextMenuP[BatchAddonActionVM]):
"""Context menu for actions for a group of add-ons"""
def __init__(self, storeVM: AddonStoreVM):
self._storeVM = storeVM
self._actionMenuItemMap = {}
self._contextMenu = wx.Menu()
self._selectedAddons: Iterable[AddonListItemVM] = tuple()

def _updateSelectedAddons(self, selectedAddons: Iterable[AddonListItemVM]):
# Reset the action menu as self._actions depends on the selected add-ons
self._actionMenuItemMap = {}
self._selectedAddons = selectedAddons

def popupContextMenuFromPosition(
self,
targetWindow: wx.Window,
position: wx.Position = wx.DefaultPosition
):
super().popupContextMenuFromPosition(targetWindow, position)
if self._contextMenu.GetMenuItemCount() == 0:
# Translators: a message displayed when activating the context menu on multiple selected add-ons,
# but no actions are available for the add-ons.
ui.message(pgettext("addonStore", "No actions available for the selected add-ons"))

def _menuItemClicked(self, evt: wx.ContextMenuEvent, actionVM: BatchAddonActionVM):
log.debug(f"Performing batch action for actionVM: {actionVM.displayName}")
actionVM.actionHandler(self._selectedAddons)

@property
def _actions(self) -> List[BatchAddonActionVM]:
return [
BatchAddonActionVM(
# Translators: Label for an action that installs the selected add-ons
displayName=pgettext("addonStore", "&Install selected add-ons"),
actionHandler=self._storeVM.getAddons,
validCheck=lambda aVMs: self._storeVM._filteredStatusKey == _StatusFilterKey.AVAILABLE,
actionTarget=self._selectedAddons
),
]
44 changes: 36 additions & 8 deletions source/gui/_addonStoreGui/controls/addonList.py
Expand Up @@ -4,6 +4,7 @@
# See the file COPYING for more details.

from typing import (
List,
Optional,
)

Expand All @@ -16,8 +17,12 @@
from gui.dpiScalingHelper import DpiScalingHelperMixinWithoutInit
from logHandler import log

from .actions import _ActionsContextMenu
from ..viewModels.addonList import AddonListVM
from .actions import (
_ActionsContextMenuP,
_BatchActionsContextMenu,
_MonoActionsContextMenu,
)
from ..viewModels.addonList import AddonListItemVM, AddonListVM


class AddonVirtualList(
Expand All @@ -28,21 +33,21 @@ def __init__(
self,
parent: wx.Window,
addonsListVM: AddonListVM,
actionsContextMenu: _ActionsContextMenu,
actionsContextMenu: _MonoActionsContextMenu,
):
super().__init__(
parent,
style=(
wx.LC_REPORT # Single or multicolumn report view, with optional header.
| wx.LC_VIRTUAL # The application provides items text on demand. May only be used with LC_REPORT.
| wx.LC_SINGLE_SEL # Single selection (default is multiple).
| wx.LC_HRULES # Draws light horizontal rules between rows in report mode.
| wx.LC_VRULES # Draws light vertical rules between columns in report mode.
),
autoSizeColumn=1,
)
self._addonsListVM = addonsListVM
self._actionsContextMenu = actionsContextMenu
self._batchActionsContextMenu = _BatchActionsContextMenu(self._addonsListVM._storeVM)

self.SetMinSize(self.scaleSize((500, 500)))

Expand Down Expand Up @@ -75,6 +80,25 @@ def _getListSelectionPosition(self) -> Optional[wx.Position]:
itemRect: wx.Rect = self.GetItemRect(firstSelectedIndex)
return itemRect.GetBottomLeft()

def _updateBatchContextMenuSelection(self):
numSelected = self.GetSelectedItemCount()
prevSelectedIndex = self.GetFirstSelected()
selectedAddons: List[AddonListItemVM] = []
for _ in range(numSelected):
addon = self._addonsListVM.getAddonAtIndex(prevSelectedIndex)
selectedAddons.append(addon)
prevSelectedIndex = self.GetNextSelected(prevSelectedIndex)

self._batchActionsContextMenu._updateSelectedAddons(selectedAddons)

@property
def _contextMenu(self) -> _ActionsContextMenuP:
numSelected = self.GetSelectedItemCount()
if numSelected > 1:
self._updateBatchContextMenuSelection()
return self._batchActionsContextMenu
return self._actionsContextMenu

def _popupContextMenuFromList(self, evt: wx.ContextMenuEvent):
listSelectionPosition = self._getListSelectionPosition()
if listSelectionPosition is None:
Expand All @@ -83,11 +107,11 @@ def _popupContextMenuFromList(self, evt: wx.ContextMenuEvent):
if eventPosition == wx.DefaultPosition:
# keyboard triggered context menu (due to "applications" key)
# don't have position set. It must be fetched from the selected item.
self._actionsContextMenu.popupContextMenuFromPosition(self, listSelectionPosition)
self._contextMenu.popupContextMenuFromPosition(self, listSelectionPosition)
else:
# Mouse (right click) triggered context menu.
# In this case the menu is positioned better with GetPopupMenuSelectionFromUser.
self._actionsContextMenu.popupContextMenuFromPosition(self)
self._contextMenu.popupContextMenuFromPosition(self)

def _itemDataUpdated(self, index: int):
log.debug(f"index: {index}")
Expand All @@ -99,12 +123,16 @@ def OnItemSelected(self, evt: wx.ListEvent):
self._addonsListVM.setSelection(index=newIndex)

def OnItemActivated(self, evt: wx.ListEvent):
if evt.GetKeyCode() == wx.WXK_SPACE:
# Space key is used to toggle add-on selection.
# Don't trigger the context menu.
return
position = self._getListSelectionPosition()
self._actionsContextMenu.popupContextMenuFromPosition(self, position)
self._contextMenu.popupContextMenuFromPosition(self, position)
log.debug(f"item activated: {evt.GetIndex()}")

def OnItemDeselected(self, evt: wx.ListEvent):
log.debug(f"item deselected")
log.debug("item deselected")
self._addonsListVM.setSelection(None)

def OnGetItemText(self, itemIndex: int, colIndex: int) -> str:
Expand Down
23 changes: 19 additions & 4 deletions source/gui/_addonStoreGui/controls/details.py
Expand Up @@ -3,6 +3,8 @@
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

from typing import TYPE_CHECKING

import wx

from _addonStore.models.addon import (
Expand All @@ -15,11 +17,14 @@

from ..viewModels.addonList import AddonDetailsVM, AddonListField

from .actions import _ActionsContextMenu
from .actions import _MonoActionsContextMenu

_fontFaceName = "Segoe UI"
_fontFaceName_semiBold = "Segoe UI Semibold"

if TYPE_CHECKING:
from .storeDialog import AddonStoreDialog


class AddonDetails(
wx.Panel,
Expand All @@ -33,6 +38,10 @@ class AddonDetails(
# Translators: Header (usually the add-on name) when no add-on is selected. In the add-on store dialog.
_noAddonSelectedLabelText: str = pgettext("addonStore", "No add-on selected.")

# Translators: Header (usually the add-on name) when multiple add-ons are selected.
# In the add-on store dialog.
_multiAddonSelectedLabelText: str = pgettext("addonStore", "{num} add-ons selected.")

# Translators: Label for the text control containing a description of the selected add-on.
# In the add-on store dialog.
_descriptionLabelText: str = pgettext("addonStore", "Description:")
Expand All @@ -45,11 +54,13 @@ class AddonDetails(
# In the add-on store dialog.
_actionsLabelText: str = pgettext("addonStore", "A&ctions")

Parent: "AddonStoreDialog"

def __init__(
self,
parent: wx.Window,
parent: "AddonStoreDialog",
detailsVM: AddonDetailsVM,
actionsContextMenu: _ActionsContextMenu,
actionsContextMenu: _MonoActionsContextMenu,
):
self._detailsVM: AddonDetailsVM = detailsVM
self._actionsContextMenu = actionsContextMenu
Expand Down Expand Up @@ -194,12 +205,16 @@ def _updatedListItem(self, addonDetailsVM: AddonDetailsVM):

def _refresh(self):
details = None if self._detailsVM.listItem is None else self._detailsVM.listItem.model
numSelectedAddons = self.Parent.addonListView.GetSelectedItemCount()

with guiHelper.autoThaw(self):
# AppendText is used to build up the details so that formatting can be set as text is added, via
# SetDefaultStyle, however, this means the text control must start empty.
self.otherDetailsTextCtrl.SetValue("")
if not details:
if numSelectedAddons > 1:
self.contentsPanel.Hide()
self.updateAddonName(AddonDetails._multiAddonSelectedLabelText.format(num=numSelectedAddons))
elif not details:
self.contentsPanel.Hide()
if self._detailsVM._listVM._isLoading:
self.updateAddonName(AddonDetails._loadingAddonsLabelText)
Expand Down
4 changes: 2 additions & 2 deletions source/gui/_addonStoreGui/controls/storeDialog.py
Expand Up @@ -33,7 +33,7 @@
from logHandler import log

from ..viewModels.store import AddonStoreVM
from .actions import _ActionsContextMenu
from .actions import _MonoActionsContextMenu
from .addonList import AddonVirtualList
from .details import AddonDetails
from .messageDialogs import _SafetyWarningDialog
Expand All @@ -50,7 +50,7 @@ class AddonStoreDialog(SettingsDialog):
def __init__(self, parent: wx.Window, storeVM: AddonStoreVM):
self._storeVM = storeVM
self._storeVM.onDisplayableError.register(self.handleDisplayableError)
self._actionsContextMenu = _ActionsContextMenu(self._storeVM)
self._actionsContextMenu = _MonoActionsContextMenu(self._storeVM)
super().__init__(parent, resizeable=True, buttons={wx.CLOSE})
if config.conf["addonStore"]["showWarning"]:
displayDialogAsModal(_SafetyWarningDialog(parent))
Expand Down