Skip to content

Commit

Permalink
Merge 35efcb9 into 56472d0
Browse files Browse the repository at this point in the history
  • Loading branch information
seanbudd committed Sep 8, 2023
2 parents 56472d0 + 35efcb9 commit 603f9a5
Show file tree
Hide file tree
Showing 12 changed files with 322 additions and 90 deletions.
3 changes: 3 additions & 0 deletions source/_addonStore/models/status.py
Expand Up @@ -156,6 +156,9 @@ def getStatus(model: "_AddonGUIModel") -> AvailableAddonStatus:
from .version import MajorMinorPatch
addonHandlerModel = model._addonHandlerModel

if model.name in (d.model.name for d in addonDataManager._downloadsPendingCompletion):
return AvailableAddonStatus.DOWNLOADING

if model.name in (d.model.name for d, _ in addonDataManager._downloadsPendingInstall):
return AvailableAddonStatus.DOWNLOAD_SUCCESS

Expand Down
5 changes: 3 additions & 2 deletions source/_addonStore/network.py
Expand Up @@ -109,7 +109,7 @@ def download(
f.add_done_callback(self._done)

def _done(self, downloadAddonFuture: Future[Optional[os.PathLike]]):
isCancelled = downloadAddonFuture not in self._pending
isCancelled = downloadAddonFuture.cancelled() or downloadAddonFuture not in self._pending
addonId = "CANCELLED" if isCancelled else self._pending[downloadAddonFuture][0].model.addonId
log.debug(f"Done called for {addonId}")
if isCancelled:
Expand Down Expand Up @@ -144,7 +144,8 @@ def _done(self, downloadAddonFuture: Future[Optional[os.PathLike]]):

def cancelAll(self):
log.debug("Cancelling all")
for f in self._pending.keys():
futuresCopy = self._pending.copy()
for f in futuresCopy.keys():
f.cancel()
assert self._executor
self._executor.shutdown(wait=False)
Expand Down
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

0 comments on commit 603f9a5

Please sign in to comment.