Skip to content

Commit 68bf922

Browse files
authored
Merge e16be4f into 89c815b
2 parents 89c815b + e16be4f commit 68bf922

9 files changed

Lines changed: 307 additions & 84 deletions

File tree

source/gui/_addonStoreGui/controls/actions.py

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,38 @@
33
# This file is covered by the GNU General Public License.
44
# See the file COPYING for more details.
55

6+
from abc import ABC, abstractmethod
67
import functools
78
from typing import (
89
Dict,
10+
Generic,
11+
Iterable,
912
List,
13+
TypeVar,
1014
)
1115

1216
import wx
1317

18+
from _addonStore.models.status import _StatusFilterKey
1419
from logHandler import log
20+
import ui
1521

16-
from ..viewModels.action import AddonActionVM
22+
from ..viewModels.action import AddonActionVM, BatchAddonActionVM
23+
from ..viewModels.addonList import AddonListItemVM
1724
from ..viewModels.store import AddonStoreVM
1825

1926

20-
class _ActionsContextMenu:
21-
def __init__(self, storeVM: AddonStoreVM):
22-
self._storeVM = storeVM
23-
self._actionMenuItemMap: Dict[AddonActionVM, wx.MenuItem] = {}
24-
self._contextMenu = wx.Menu()
27+
AddonActionT = TypeVar("AddonActionT", AddonActionVM, BatchAddonActionVM)
28+
29+
30+
class _ActionsContextMenuP(Generic[AddonActionT], ABC):
31+
_actions: List[AddonActionT]
32+
_actionMenuItemMap: Dict[AddonActionT, wx.MenuItem]
33+
_contextMenu: wx.Menu
34+
35+
@abstractmethod
36+
def _menuItemClicked(self, evt: wx.ContextMenuEvent, actionVM: AddonActionT):
37+
...
2538

2639
def popupContextMenuFromPosition(
2740
self,
@@ -31,14 +44,9 @@ def popupContextMenuFromPosition(
3144
self._populateContextMenu()
3245
targetWindow.PopupMenu(self._contextMenu, pos=position)
3346

34-
def _menuItemClicked(self, evt: wx.ContextMenuEvent, actionVM: AddonActionVM):
35-
selectedAddon = actionVM.listItemVM
36-
log.debug(f"action selected: actionVM: {actionVM}, selectedAddon: {selectedAddon}")
37-
actionVM.actionHandler(selectedAddon)
38-
3947
def _populateContextMenu(self):
4048
prevActionIndex = -1
41-
for action in self._storeVM.actionVMList:
49+
for action in self._actions:
4250
menuItem = self._actionMenuItemMap.get(action)
4351
menuItems: List[wx.MenuItem] = list(self._contextMenu.GetMenuItems())
4452
isMenuItemInContextMenu = menuItem is not None and menuItem in menuItems
@@ -71,3 +79,68 @@ def _populateContextMenu(self):
7179
# Remove the menu item from the context menu.
7280
self._contextMenu.RemoveItem(menuItem)
7381
del self._actionMenuItemMap[action]
82+
83+
menuItems: List[wx.MenuItem] = list(self._contextMenu.GetMenuItems())
84+
for menuItem in menuItems:
85+
if menuItem not in self._actionMenuItemMap.values():
86+
# The menu item is not in the action menu item map.
87+
# It should be removed from the context menu.
88+
self._contextMenu.RemoveItem(menuItem)
89+
90+
91+
class _MonoActionsContextMenu(_ActionsContextMenuP[AddonActionVM]):
92+
"""Context menu for actions for a single add-on"""
93+
def __init__(self, storeVM: AddonStoreVM):
94+
self._storeVM = storeVM
95+
self._actionMenuItemMap = {}
96+
self._contextMenu = wx.Menu()
97+
98+
def _menuItemClicked(self, evt: wx.ContextMenuEvent, actionVM: AddonActionVM):
99+
selectedAddon = actionVM.actionTarget
100+
log.debug(f"action selected: actionVM: {actionVM.displayName}, selectedAddon: {selectedAddon}")
101+
actionVM.actionHandler(selectedAddon)
102+
103+
@property
104+
def _actions(self) -> List[AddonActionVM]:
105+
return self._storeVM.actionVMList
106+
107+
108+
class _BatchActionsContextMenu(_ActionsContextMenuP[BatchAddonActionVM]):
109+
"""Context menu for actions for a group of add-ons"""
110+
def __init__(self, storeVM: AddonStoreVM):
111+
self._storeVM = storeVM
112+
self._actionMenuItemMap = {}
113+
self._contextMenu = wx.Menu()
114+
self._selectedAddons: Iterable[AddonListItemVM] = tuple()
115+
116+
def _updateSelectedAddons(self, selectedAddons: Iterable[AddonListItemVM]):
117+
# Reset the action menu as self._actions depends on the selected add-ons
118+
self._actionMenuItemMap = {}
119+
self._selectedAddons = selectedAddons
120+
121+
def popupContextMenuFromPosition(
122+
self,
123+
targetWindow: wx.Window,
124+
position: wx.Position = wx.DefaultPosition
125+
):
126+
super().popupContextMenuFromPosition(targetWindow, position)
127+
if self._contextMenu.GetMenuItemCount() == 0:
128+
# Translators: a message displayed when activating the context menu on multiple selected add-ons,
129+
# but no actions are available for the add-ons.
130+
ui.message(pgettext("addonStore", "No actions available for the selected add-ons"))
131+
132+
def _menuItemClicked(self, evt: wx.ContextMenuEvent, actionVM: BatchAddonActionVM):
133+
log.debug(f"Performing batch action for actionVM: {actionVM.displayName}")
134+
actionVM.actionHandler(self._selectedAddons)
135+
136+
@property
137+
def _actions(self) -> List[BatchAddonActionVM]:
138+
return [
139+
BatchAddonActionVM(
140+
# Translators: Label for an action that installs the selected add-ons
141+
displayName=pgettext("addonStore", "&Install selected add-ons"),
142+
actionHandler=self._storeVM.getAddons,
143+
validCheck=lambda aVMs: self._storeVM._filteredStatusKey == _StatusFilterKey.AVAILABLE,
144+
actionTarget=self._selectedAddons
145+
),
146+
]

source/gui/_addonStoreGui/controls/addonList.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# See the file COPYING for more details.
55

66
from typing import (
7+
List,
78
Optional,
89
)
910

@@ -16,8 +17,12 @@
1617
from gui.dpiScalingHelper import DpiScalingHelperMixinWithoutInit
1718
from logHandler import log
1819

19-
from .actions import _ActionsContextMenu
20-
from ..viewModels.addonList import AddonListVM
20+
from .actions import (
21+
_ActionsContextMenuP,
22+
_BatchActionsContextMenu,
23+
_MonoActionsContextMenu,
24+
)
25+
from ..viewModels.addonList import AddonListItemVM, AddonListVM
2126

2227

2328
class AddonVirtualList(
@@ -28,21 +33,21 @@ def __init__(
2833
self,
2934
parent: wx.Window,
3035
addonsListVM: AddonListVM,
31-
actionsContextMenu: _ActionsContextMenu,
36+
actionsContextMenu: _MonoActionsContextMenu,
3237
):
3338
super().__init__(
3439
parent,
3540
style=(
3641
wx.LC_REPORT # Single or multicolumn report view, with optional header.
3742
| wx.LC_VIRTUAL # The application provides items text on demand. May only be used with LC_REPORT.
38-
| wx.LC_SINGLE_SEL # Single selection (default is multiple).
3943
| wx.LC_HRULES # Draws light horizontal rules between rows in report mode.
4044
| wx.LC_VRULES # Draws light vertical rules between columns in report mode.
4145
),
4246
autoSizeColumn=1,
4347
)
4448
self._addonsListVM = addonsListVM
4549
self._actionsContextMenu = actionsContextMenu
50+
self._batchActionsContextMenu = _BatchActionsContextMenu(self._addonsListVM._storeVM)
4651

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

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

83+
def _updateBatchContextMenuSelection(self):
84+
numSelected = self.GetSelectedItemCount()
85+
prevSelectedIndex = self.GetFirstSelected()
86+
selectedAddons: List[AddonListItemVM] = []
87+
for _ in range(numSelected):
88+
addon = self._addonsListVM.getAddonAtIndex(prevSelectedIndex)
89+
selectedAddons.append(addon)
90+
prevSelectedIndex = self.GetNextSelected(prevSelectedIndex)
91+
92+
self._batchActionsContextMenu._updateSelectedAddons(selectedAddons)
93+
94+
@property
95+
def _contextMenu(self) -> _ActionsContextMenuP:
96+
numSelected = self.GetSelectedItemCount()
97+
if numSelected > 1:
98+
self._updateBatchContextMenuSelection()
99+
return self._batchActionsContextMenu
100+
return self._actionsContextMenu
101+
78102
def _popupContextMenuFromList(self, evt: wx.ContextMenuEvent):
79103
listSelectionPosition = self._getListSelectionPosition()
80104
if listSelectionPosition is None:
@@ -83,11 +107,11 @@ def _popupContextMenuFromList(self, evt: wx.ContextMenuEvent):
83107
if eventPosition == wx.DefaultPosition:
84108
# keyboard triggered context menu (due to "applications" key)
85109
# don't have position set. It must be fetched from the selected item.
86-
self._actionsContextMenu.popupContextMenuFromPosition(self, listSelectionPosition)
110+
self._contextMenu.popupContextMenuFromPosition(self, listSelectionPosition)
87111
else:
88112
# Mouse (right click) triggered context menu.
89113
# In this case the menu is positioned better with GetPopupMenuSelectionFromUser.
90-
self._actionsContextMenu.popupContextMenuFromPosition(self)
114+
self._contextMenu.popupContextMenuFromPosition(self)
91115

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

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

106134
def OnItemDeselected(self, evt: wx.ListEvent):
107-
log.debug(f"item deselected")
135+
log.debug("item deselected")
108136
self._addonsListVM.setSelection(None)
109137

110138
def OnGetItemText(self, itemIndex: int, colIndex: int) -> str:

source/gui/_addonStoreGui/controls/details.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
# This file is covered by the GNU General Public License.
44
# See the file COPYING for more details.
55

6+
from typing import TYPE_CHECKING
7+
68
import wx
79

810
from _addonStore.models.addon import (
@@ -15,11 +17,14 @@
1517

1618
from ..viewModels.addonList import AddonDetailsVM, AddonListField
1719

18-
from .actions import _ActionsContextMenu
20+
from .actions import _MonoActionsContextMenu
1921

2022
_fontFaceName = "Segoe UI"
2123
_fontFaceName_semiBold = "Segoe UI Semibold"
2224

25+
if TYPE_CHECKING:
26+
from .storeDialog import AddonStoreDialog
27+
2328

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

41+
# Translators: Header (usually the add-on name) when multiple add-ons are selected.
42+
# In the add-on store dialog.
43+
_multiAddonSelectedLabelText: str = pgettext("addonStore", "{num} add-ons selected.")
44+
3645
# Translators: Label for the text control containing a description of the selected add-on.
3746
# In the add-on store dialog.
3847
_descriptionLabelText: str = pgettext("addonStore", "Description:")
@@ -45,11 +54,13 @@ class AddonDetails(
4554
# In the add-on store dialog.
4655
_actionsLabelText: str = pgettext("addonStore", "A&ctions")
4756

57+
Parent: "AddonStoreDialog"
58+
4859
def __init__(
4960
self,
50-
parent: wx.Window,
61+
parent: "AddonStoreDialog",
5162
detailsVM: AddonDetailsVM,
52-
actionsContextMenu: _ActionsContextMenu,
63+
actionsContextMenu: _MonoActionsContextMenu,
5364
):
5465
self._detailsVM: AddonDetailsVM = detailsVM
5566
self._actionsContextMenu = actionsContextMenu
@@ -194,12 +205,16 @@ def _updatedListItem(self, addonDetailsVM: AddonDetailsVM):
194205

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

198210
with guiHelper.autoThaw(self):
199211
# AppendText is used to build up the details so that formatting can be set as text is added, via
200212
# SetDefaultStyle, however, this means the text control must start empty.
201213
self.otherDetailsTextCtrl.SetValue("")
202-
if not details:
214+
if numSelectedAddons > 1:
215+
self.contentsPanel.Hide()
216+
self.updateAddonName(AddonDetails._multiAddonSelectedLabelText.format(num=numSelectedAddons))
217+
elif not details:
203218
self.contentsPanel.Hide()
204219
if self._detailsVM._listVM._isLoading:
205220
self.updateAddonName(AddonDetails._loadingAddonsLabelText)

source/gui/_addonStoreGui/controls/storeDialog.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from logHandler import log
3434

3535
from ..viewModels.store import AddonStoreVM
36-
from .actions import _ActionsContextMenu
36+
from .actions import _MonoActionsContextMenu
3737
from .addonList import AddonVirtualList
3838
from .details import AddonDetails
3939
from .messageDialogs import _SafetyWarningDialog
@@ -50,7 +50,7 @@ class AddonStoreDialog(SettingsDialog):
5050
def __init__(self, parent: wx.Window, storeVM: AddonStoreVM):
5151
self._storeVM = storeVM
5252
self._storeVM.onDisplayableError.register(self.handleDisplayableError)
53-
self._actionsContextMenu = _ActionsContextMenu(self._storeVM)
53+
self._actionsContextMenu = _MonoActionsContextMenu(self._storeVM)
5454
super().__init__(parent, resizeable=True, buttons={wx.CLOSE})
5555
if config.conf["addonStore"]["showWarning"]:
5656
displayDialogAsModal(_SafetyWarningDialog(parent))

0 commit comments

Comments
 (0)