-
-
Notifications
You must be signed in to change notification settings - Fork 617
/
storeDialog.py
335 lines (284 loc) · 12.8 KB
/
storeDialog.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2022-2023 NV Access Limited
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
from typing import (
cast,
)
import wx
from addonHandler import (
BUNDLE_EXTENSION,
)
from _addonStore.models.channel import Channel, _channelFilters
from _addonStore.models.status import (
EnabledStatus,
_statusFilters,
_StatusFilterKey,
)
from core import callLater
import gui
from gui import (
guiHelper,
addonGui,
)
from gui.message import DisplayableError
from gui.settingsDialogs import SettingsDialog
from logHandler import log
from ..viewModels.store import AddonStoreVM
from .actions import _ActionsContextMenu
from .addonList import AddonVirtualList
from .details import AddonDetails
class AddonStoreDialog(SettingsDialog):
# Translators: The title of the addonStore dialog where the user can find and download add-ons
title = pgettext("addonStore", "Add-on Store")
helpId = "addonStore"
def __init__(self, parent: wx.Window, storeVM: AddonStoreVM):
self._storeVM = storeVM
self._storeVM.onDisplayableError.register(self.handleDisplayableError)
self._actionsContextMenu = _ActionsContextMenu(self._storeVM)
super().__init__(parent, resizeable=True, buttons={wx.CLOSE})
def _enterActivatesOk_ctrlSActivatesApply(self, evt: wx.KeyEvent):
"""Disables parent behaviour which overrides behaviour for enter and ctrl+s"""
evt.Skip()
def handleDisplayableError(self, displayableError: DisplayableError):
displayableError.displayError(gui.mainFrame)
def makeSettings(self, settingsSizer: wx.BoxSizer):
splitViewSizer = wx.BoxSizer(wx.HORIZONTAL)
self.addonListTabs = wx.Notebook(self)
# Use a single tab page for every tab.
# Instead perform dynamic updates to the tab page when the tab is changed.
dynamicTabPage = wx.Panel(self.addonListTabs)
tabPageHelper = guiHelper.BoxSizerHelper(dynamicTabPage, wx.VERTICAL)
splitViewSizer.Add(tabPageHelper.sizer, flag=wx.EXPAND, proportion=1)
for statusFilter in _statusFilters:
self.addonListTabs.AddPage(dynamicTabPage, statusFilter.displayString)
tabPageHelper.addItem(self.addonListTabs, flag=wx.EXPAND)
if any(self._storeVM._installedAddons[channel] for channel in self._storeVM._installedAddons):
# If there's any installed add-ons, use the installed add-ons page by default
self.addonListTabs.SetSelection(0)
else:
availableTabIndex = list(_statusFilters.keys()).index(_StatusFilterKey.AVAILABLE)
self.addonListTabs.SetSelection(availableTabIndex)
self.addonListTabs.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.onListTabPageChange, self.addonListTabs)
self.filterCtrlHelper = guiHelper.BoxSizerHelper(self, wx.VERTICAL)
self._createFilterControls()
tabPageHelper.addItem(self.filterCtrlHelper.sizer, flag=wx.EXPAND)
tabPageHelper.sizer.AddSpacer(5)
settingsSizer.Add(splitViewSizer, flag=wx.EXPAND, proportion=1)
# add a label for the AddonListVM so that it is announced with a name in NVDA
self.listLabel = wx.StaticText(self)
tabPageHelper.addItem(
self.listLabel,
flag=wx.EXPAND
)
self.listLabel.Hide()
self._setListLabels()
self.addonListView = AddonVirtualList(
parent=self,
addonsListVM=self._storeVM.listVM,
actionsContextMenu=self._actionsContextMenu,
)
# Add alt+l accelerator key
_setFocusToAddonListView_eventId = wx.NewIdRef(count=1)
self.Bind(wx.EVT_MENU, lambda e: self.addonListView.SetFocus(), _setFocusToAddonListView_eventId)
self.SetAcceleratorTable(wx.AcceleratorTable([
wx.AcceleratorEntry(wx.ACCEL_ALT, ord("l"), _setFocusToAddonListView_eventId)
]))
tabPageHelper.addItem(self.addonListView, flag=wx.EXPAND, proportion=1)
splitViewSizer.AddSpacer(5)
self.addonDetailsView = AddonDetails(
parent=self,
detailsVM=self._storeVM.detailsVM,
actionsContextMenu=self._actionsContextMenu,
)
splitViewSizer.Add(self.addonDetailsView, flag=wx.EXPAND, proportion=1)
generalActions = guiHelper.ButtonHelper(wx.HORIZONTAL)
# Translators: The label for a button in add-ons Store dialog to install an external add-on.
externalInstallLabelText = pgettext("addonStore", "Install from e&xternal source")
self.externalInstallButton = generalActions.addButton(self, label=externalInstallLabelText)
self.externalInstallButton.Bind(wx.EVT_BUTTON, self.openExternalInstall, self.externalInstallButton)
self.bindHelpEvent("AddonStoreInstalling", self.externalInstallButton)
settingsSizer.Add(generalActions.sizer)
self.onListTabPageChange(None)
def _createFilterControls(self):
filterCtrlsLine0 = guiHelper.BoxSizerHelper(self, wx.HORIZONTAL)
filterCtrlsLine1 = guiHelper.BoxSizerHelper(self, wx.HORIZONTAL)
self.filterCtrlHelper.addItem(filterCtrlsLine0.sizer)
# Add margin left padding
FILTER_MARGIN_PADDING = 15
filterCtrlsLine0.sizer.AddSpacer(FILTER_MARGIN_PADDING)
filterCtrlsLine1.sizer.AddSpacer(FILTER_MARGIN_PADDING)
self.filterCtrlHelper.addItem(filterCtrlsLine1.sizer, flag=wx.EXPAND, proportion=1)
self.channelFilterCtrl = cast(wx.Choice, filterCtrlsLine0.addLabeledControl(
# Translators: The label of a selection field to filter the list of add-ons in the add-on store dialog.
labelText=pgettext("addonStore", "Cha&nnel:"),
wxCtrlClass=wx.Choice,
choices=list(c.displayString for c in _channelFilters),
))
self.channelFilterCtrl.Bind(wx.EVT_CHOICE, self.onChannelFilterChange, self.channelFilterCtrl)
self.bindHelpEvent("AddonStoreFilterChannel", self.channelFilterCtrl)
# Translators: The label of a checkbox to filter the list of add-ons in the add-on store dialog.
incompatibleAddonsLabel = _("Include &incompatible add-ons")
self.includeIncompatibleCtrl = cast(wx.CheckBox, filterCtrlsLine0.addItem(
wx.CheckBox(self, label=incompatibleAddonsLabel)
))
self.includeIncompatibleCtrl.SetValue(0)
self.includeIncompatibleCtrl.Bind(
wx.EVT_CHECKBOX,
self.onIncompatibleFilterChange,
self.includeIncompatibleCtrl
)
self.bindHelpEvent("AddonStoreFilterIncompatible", self.includeIncompatibleCtrl)
self.enabledFilterCtrl = cast(wx.Choice, filterCtrlsLine0.addLabeledControl(
# Translators: The label of a selection field to filter the list of add-ons in the add-on store dialog.
labelText=pgettext("addonStore", "Ena&bled/disabled:"),
wxCtrlClass=wx.Choice,
choices=list(c.displayString for c in EnabledStatus),
))
self.enabledFilterCtrl.Bind(wx.EVT_CHOICE, self.onEnabledFilterChange, self.enabledFilterCtrl)
self.bindHelpEvent("AddonStoreFilterEnabled", self.enabledFilterCtrl)
# Translators: The label of a text field to filter the list of add-ons in the add-on store dialog.
searchFilterLabel = wx.StaticText(self, label=pgettext("addonStore", "&Search:"))
# noinspection PyAttributeOutsideInit
self.searchFilterCtrl = wx.TextCtrl(self)
self.searchFilterCtrl.Bind(wx.EVT_TEXT, self.onFilterTextChange, self.searchFilterCtrl)
self.bindHelpEvent("AddonStoreFilterSearch", self.searchFilterCtrl)
filterCtrlsLine1.addItem(searchFilterLabel)
filterCtrlsLine1.addItem(self.searchFilterCtrl, proportion=1)
# Add end margin right padding
filterCtrlsLine0.sizer.AddSpacer(FILTER_MARGIN_PADDING)
filterCtrlsLine1.sizer.AddSpacer(FILTER_MARGIN_PADDING)
def postInit(self):
self.addonListView.SetFocus()
def _onWindowDestroy(self, evt: wx.WindowDestroyEvent):
requiresRestart = self._requiresRestart
super()._onWindowDestroy(evt)
if requiresRestart:
wx.CallAfter(addonGui.promptUserForRestart)
# Translators: Title for message shown prior to installing add-ons when closing the add-on store dialog.
_installationPromptTitle = pgettext("addonStore", "Add-on installation")
def onClose(self, evt: wx.CommandEvent):
numInProgress = len(self._storeVM._downloader.progress)
if numInProgress:
res = gui.messageBox(
# Translators: Message shown prior to installing add-ons when closing the add-on store dialog
# The placeholder {} will be replaced with the number of add-ons to be installed
pgettext("addonStore", "Download of {} add-ons in progress, cancel downloading?").format(
numInProgress
),
self._installationPromptTitle,
style=wx.YES_NO
)
if res == wx.YES:
log.debug("Cancelling the download.")
self._storeVM.cancelDownloads()
# Continue to installation if any downloads completed
else:
# Let the user return to the add-on store and inspect add-ons being downloaded.
return
if self._storeVM._pendingInstalls:
installingDialog = gui.IndeterminateProgressDialog(
self,
self._installationPromptTitle,
# Translators: Message shown while installing add-ons after closing the add-on store dialog
# The placeholder {} will be replaced with the number of add-ons to be installed
pgettext("addonStore", "Installing {} add-ons, please wait.").format(len(self._storeVM._pendingInstalls))
)
self._storeVM.installPending()
wx.CallAfter(installingDialog.done)
# let the dialog exit.
super().onClose(evt)
@property
def _requiresRestart(self) -> bool:
if self._storeVM._pendingInstalls:
return True
for addonsForChannel in self._storeVM._installedAddons.values():
for addon in addonsForChannel.values():
if addon._addonHandlerModel.requiresRestart:
log.debug(f"Add-on {addon.name} modified, restart required")
return True
return False
@property
def _statusFilterKey(self) -> _StatusFilterKey:
index = self.addonListTabs.GetSelection()
return list(_statusFilters.keys())[index]
@property
def _channelFilterKey(self) -> Channel:
index = self.channelFilterCtrl.GetSelection()
return list(_channelFilters.keys())[index]
@property
def _titleText(self) -> str:
return f"{self.title} - {self._listLabelText}"
@property
def _listLabelText(self) -> str:
return f"{self._channelFilterKey.displayString} {self._statusFilterKey.displayString}"
def _setListLabels(self):
self.listLabel.SetLabelText(self._listLabelText)
self.SetTitle(self._titleText)
def _toggleFilterControls(self):
if self._storeVM._filteredStatusKey in {
_StatusFilterKey.AVAILABLE,
_StatusFilterKey.UPDATE,
}:
self._storeVM._filterChannelKey = Channel.STABLE
self.enabledFilterCtrl.Hide()
self.enabledFilterCtrl.Disable()
self.includeIncompatibleCtrl.Enable()
self.includeIncompatibleCtrl.Show()
else:
self._storeVM._filterChannelKey = Channel.ALL
self.enabledFilterCtrl.Show()
self.enabledFilterCtrl.Enable()
self.includeIncompatibleCtrl.Hide()
self.includeIncompatibleCtrl.Disable()
def onListTabPageChange(self, evt: wx.EVT_CHOICE):
self._storeVM._filterEnabledDisabled = EnabledStatus.ALL
self.enabledFilterCtrl.SetSelection(0)
self._storeVM._filteredStatusKey = self._statusFilterKey
self.addonListView._refreshColumns()
self._toggleFilterControls()
channelFilterIndex = list(_channelFilters.keys()).index(self._storeVM._filterChannelKey)
self.channelFilterCtrl.SetSelection(channelFilterIndex)
self._storeVM.listVM.setSelection(None)
self._setListLabels()
self._storeVM.refresh()
self.Layout()
self.addonListView.SetFocus()
def onChannelFilterChange(self, evt: wx.EVT_CHOICE):
self._storeVM._filterChannelKey = self._channelFilterKey
self._storeVM.listVM.setSelection(None)
self._setListLabels()
self._storeVM.refresh()
def onFilterTextChange(self, evt: wx.EVT_TEXT):
filterText = self.searchFilterCtrl.GetValue()
self.filter(filterText)
def onEnabledFilterChange(self, evt: wx.EVT_CHOICE):
index = self.enabledFilterCtrl.GetCurrentSelection()
self._storeVM._filterEnabledDisabled = list(EnabledStatus)[index]
self._storeVM.refresh()
def onIncompatibleFilterChange(self, evt: wx.EVT_CHECKBOX):
self._storeVM._filterIncludeIncompatible = self.includeIncompatibleCtrl.GetValue()
self._storeVM.refresh()
def filter(self, filterText: str):
self._storeVM.listVM.applyFilter(filterText)
def openExternalInstall(self, evt: wx.EVT_BUTTON):
# Translators: the label for the NVDA add-on package file type in the Choose add-on dialog.
fileTypeLabel = pgettext("addonStore", "NVDA Add-on Package (*.{ext})")
fd = wx.FileDialog(
self,
# Translators: The message displayed in the dialog that
# allows you to choose an add-on package for installation.
message=pgettext("addonStore", "Choose Add-on Package File"),
wildcard=(fileTypeLabel + "|*.{ext}").format(ext=BUNDLE_EXTENSION),
defaultDir="c:",
style=wx.FD_OPEN,
)
if fd.ShowModal() != wx.ID_OK:
return
addonPath = fd.GetPath()
try:
addonGui.installAddon(self, addonPath)
except DisplayableError as displayableError:
callLater(delay=0, callable=self._storeVM.onDisplayableError.notify, displayableError=displayableError)
return
self._storeVM.refresh()