bauiv1lib.settings.plugins

Plugin Window UI.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Plugin Window UI."""
  4
  5from __future__ import annotations
  6
  7import logging
  8from enum import Enum
  9from typing import TYPE_CHECKING, assert_never, override
 10
 11import bauiv1 as bui
 12from bauiv1lib import popup
 13
 14if TYPE_CHECKING:
 15    pass
 16
 17
 18class Category(Enum):
 19    """Categories we can display."""
 20
 21    ALL = 'all'
 22    ENABLED = 'enabled'
 23    DISABLED = 'disabled'
 24
 25    @property
 26    def resource(self) -> str:
 27        """Resource name for us."""
 28        return f'{self.value}Text'
 29
 30
 31class PluginWindow(bui.MainWindow):
 32    """Window for configuring plugins."""
 33
 34    def __init__(
 35        self,
 36        transition: str | None = 'in_right',
 37        origin_widget: bui.Widget | None = None,
 38    ):
 39        app = bui.app
 40
 41        self._category = Category.ALL
 42
 43        assert bui.app.classic is not None
 44        uiscale = bui.app.ui_v1.uiscale
 45        self._width = 870.0 if uiscale is bui.UIScale.SMALL else 670.0
 46        x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
 47        self._height = (
 48            370.0
 49            if uiscale is bui.UIScale.SMALL
 50            else 450.0 if uiscale is bui.UIScale.MEDIUM else 520.0
 51        )
 52        top_extra = 10 if uiscale is bui.UIScale.SMALL else 0
 53        super().__init__(
 54            root_widget=bui.containerwidget(
 55                size=(self._width, self._height + top_extra),
 56                toolbar_visibility=(
 57                    'menu_minimal'
 58                    if uiscale is bui.UIScale.SMALL
 59                    else 'menu_full'
 60                ),
 61                scale=(
 62                    1.9
 63                    if uiscale is bui.UIScale.SMALL
 64                    else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
 65                ),
 66                stack_offset=(
 67                    (0, -25) if uiscale is bui.UIScale.SMALL else (0, 0)
 68                ),
 69            ),
 70            transition=transition,
 71            origin_widget=origin_widget,
 72        )
 73
 74        self._scroll_width = self._width - (100 + 2 * x_inset)
 75        self._scroll_height = self._height - 115.0
 76        self._sub_width = self._scroll_width * 0.95
 77        self._sub_height = 724.0
 78
 79        assert app.classic is not None
 80        if uiscale is bui.UIScale.SMALL:
 81            bui.containerwidget(
 82                edit=self._root_widget, on_cancel_call=self.main_window_back
 83            )
 84            self._back_button = None
 85        else:
 86            self._back_button = bui.buttonwidget(
 87                parent=self._root_widget,
 88                position=(53 + x_inset, self._height - 60),
 89                size=(140, 60),
 90                scale=0.8,
 91                autoselect=True,
 92                label=bui.Lstr(resource='backText'),
 93                button_type='back',
 94                on_activate_call=self.main_window_back,
 95            )
 96            bui.containerwidget(
 97                edit=self._root_widget, cancel_button=self._back_button
 98            )
 99
100        self._title_text = bui.textwidget(
101            parent=self._root_widget,
102            position=(self._width * 0.5, self._height - 41),
103            size=(0, 0),
104            text=bui.Lstr(resource='pluginsText'),
105            color=app.ui_v1.title_color,
106            maxwidth=170,
107            h_align='center',
108            v_align='center',
109        )
110
111        if self._back_button is not None:
112            bui.buttonwidget(
113                edit=self._back_button,
114                button_type='backSmall',
115                size=(60, 60),
116                label=bui.charstr(bui.SpecialChar.BACK),
117            )
118
119        settings_button_x = 670 if uiscale is bui.UIScale.SMALL else 570
120
121        self._num_plugins_text = bui.textwidget(
122            parent=self._root_widget,
123            position=(settings_button_x - 130, self._height - 41),
124            size=(0, 0),
125            text='',
126            h_align='center',
127            v_align='center',
128        )
129
130        self._category_button = bui.buttonwidget(
131            parent=self._root_widget,
132            scale=0.7,
133            position=(settings_button_x - 105, self._height - 60),
134            size=(130, 60),
135            label=bui.Lstr(resource='allText'),
136            autoselect=True,
137            on_activate_call=bui.WeakCall(self._show_category_options),
138            color=(0.55, 0.73, 0.25),
139            iconscale=1.2,
140        )
141
142        self._settings_button = bui.buttonwidget(
143            parent=self._root_widget,
144            position=(settings_button_x, self._height - 58),
145            size=(40, 40),
146            label='',
147            on_activate_call=self._open_settings,
148        )
149
150        bui.imagewidget(
151            parent=self._root_widget,
152            position=(settings_button_x + 3, self._height - 57),
153            draw_controller=self._settings_button,
154            size=(35, 35),
155            texture=bui.gettexture('settingsIcon'),
156        )
157
158        bui.widget(
159            edit=self._settings_button,
160            up_widget=self._settings_button,
161            right_widget=self._settings_button,
162        )
163
164        self._scrollwidget = bui.scrollwidget(
165            parent=self._root_widget,
166            position=(50 + x_inset, 50),
167            simple_culling_v=20.0,
168            highlight=False,
169            size=(self._scroll_width, self._scroll_height),
170            selection_loops_to_parent=True,
171            claims_left_right=True,
172        )
173        bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget)
174
175        self._no_plugins_installed_text = bui.textwidget(
176            parent=self._root_widget,
177            position=(self._width * 0.5, self._height * 0.5),
178            size=(0, 0),
179            text='',
180            color=(0.6, 0.6, 0.6),
181            scale=0.8,
182            h_align='center',
183            v_align='center',
184        )
185
186        if bui.app.meta.scanresults is None:
187            bui.screenmessage(
188                'Still scanning plugins; please try again.', color=(1, 0, 0)
189            )
190            bui.getsound('error').play()
191        plugspecs = bui.app.plugins.plugin_specs
192        plugstates: dict[str, dict] = bui.app.config.get('Plugins', {})
193        assert isinstance(plugstates, dict)
194
195        plug_line_height = 50
196        sub_width = self._scroll_width
197        sub_height = len(plugspecs) * plug_line_height
198        self._subcontainer = bui.containerwidget(
199            parent=self._scrollwidget,
200            size=(sub_width, sub_height),
201            background=False,
202        )
203        self._show_plugins()
204        bui.containerwidget(
205            edit=self._root_widget, selected_child=self._scrollwidget
206        )
207        self._restore_state()
208
209    @override
210    def get_main_window_state(self) -> bui.MainWindowState:
211        # Support recreating our window for back/refresh purposes.
212        cls = type(self)
213        return bui.BasicMainWindowState(
214            create_call=lambda transition, origin_widget: cls(
215                transition=transition, origin_widget=origin_widget
216            )
217        )
218
219    @override
220    def on_main_window_close(self) -> None:
221        self._save_state()
222
223    def _check_value_changed(self, plug: bui.PluginSpec, value: bool) -> None:
224        bui.screenmessage(
225            bui.Lstr(resource='settingsWindowAdvanced.mustRestartText'),
226            color=(1.0, 0.5, 0.0),
227        )
228        plugstates: dict[str, dict] = bui.app.config.setdefault('Plugins', {})
229        assert isinstance(plugstates, dict)
230        plugstate = plugstates.setdefault(plug.class_path, {})
231        plugstate['enabled'] = value
232        bui.app.config.commit()
233
234    def _open_settings(self) -> None:
235        # pylint: disable=cyclic-import
236        from bauiv1lib.settings.pluginsettings import PluginSettingsWindow
237
238        # no-op if we don't have control.
239        if not self.main_window_has_control():
240            return
241
242        self.main_window_replace(PluginSettingsWindow(transition='in_right'))
243
244    def _show_category_options(self) -> None:
245        uiscale = bui.app.ui_v1.uiscale
246
247        popup.PopupMenuWindow(
248            position=self._category_button.get_screen_space_center(),
249            scale=(
250                2.3
251                if uiscale is bui.UIScale.SMALL
252                else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23
253            ),
254            choices=[c.value for c in Category],
255            choices_display=[bui.Lstr(resource=c.resource) for c in Category],
256            current_choice=self._category.value,
257            delegate=self,
258        )
259
260    def popup_menu_selected_choice(
261        self, popup_window: popup.PopupMenuWindow, choice: str
262    ) -> None:
263        """Called when a choice is selected in the popup."""
264        del popup_window  # unused
265        self._category = Category(choice)
266        self._clear_scroll_widget()
267        self._show_plugins()
268
269        bui.buttonwidget(
270            edit=self._category_button,
271            label=bui.Lstr(resource=self._category.resource),
272        )
273
274    def popup_menu_closing(self, popup_window: popup.PopupWindow) -> None:
275        """Called when the popup is closing."""
276
277    def _clear_scroll_widget(self) -> None:
278        existing_widgets = self._subcontainer.get_children()
279        if existing_widgets:
280            for i in existing_widgets:
281                i.delete()
282
283    def _show_plugins(self) -> None:
284        # pylint: disable=too-many-locals
285        # pylint: disable=too-many-branches
286        # pylint: disable=too-many-statements
287        plugspecs = bui.app.plugins.plugin_specs
288        plugstates: dict[str, dict] = bui.app.config.setdefault('Plugins', {})
289        assert isinstance(plugstates, dict)
290
291        plug_line_height = 50
292        sub_width = self._scroll_width
293        num_enabled = 0
294        num_disabled = 0
295
296        plugspecs_sorted = sorted(plugspecs.items())
297
298        bui.textwidget(
299            edit=self._no_plugins_installed_text,
300            text='',
301        )
302
303        for _classpath, plugspec in plugspecs_sorted:
304            # counting number of enabled and disabled plugins
305            # plugstate = plugstates.setdefault(plugspec[0], {})
306            if plugspec.enabled:
307                num_enabled += 1
308            else:
309                num_disabled += 1
310
311        if self._category is Category.ALL:
312            sub_height = len(plugspecs) * plug_line_height
313            bui.containerwidget(
314                edit=self._subcontainer, size=(self._scroll_width, sub_height)
315            )
316        elif self._category is Category.ENABLED:
317            sub_height = num_enabled * plug_line_height
318            bui.containerwidget(
319                edit=self._subcontainer, size=(self._scroll_width, sub_height)
320            )
321        elif self._category is Category.DISABLED:
322            sub_height = num_disabled * plug_line_height
323            bui.containerwidget(
324                edit=self._subcontainer, size=(self._scroll_width, sub_height)
325            )
326        else:
327            # Make sure we handle all cases.
328            assert_never(self._category)
329
330        num_shown = 0
331        for classpath, plugspec in plugspecs_sorted:
332            plugin = plugspec.plugin
333            enabled = plugspec.enabled
334
335            if self._category is Category.ALL:
336                show = True
337            elif self._category is Category.ENABLED:
338                show = enabled
339            elif self._category is Category.DISABLED:
340                show = not enabled
341            else:
342                assert_never(self._category)
343
344            if not show:
345                continue
346
347            item_y = sub_height - (num_shown + 1) * plug_line_height
348            check = bui.checkboxwidget(
349                parent=self._subcontainer,
350                text=bui.Lstr(value=classpath),
351                autoselect=True,
352                value=enabled,
353                maxwidth=self._scroll_width
354                - (
355                    200
356                    if plugin is not None and plugin.has_settings_ui()
357                    else 80
358                ),
359                position=(10, item_y),
360                size=(self._scroll_width - 40, 50),
361                on_value_change_call=bui.Call(
362                    self._check_value_changed, plugspec
363                ),
364                textcolor=(
365                    (0.8, 0.3, 0.3)
366                    if (plugspec.attempted_load and plugspec.plugin is None)
367                    else (
368                        (0.6, 0.6, 0.6)
369                        if plugspec.plugin is None
370                        else (0, 1, 0)
371                    )
372                ),
373            )
374            # noinspection PyUnresolvedReferences
375            if plugin is not None and plugin.has_settings_ui():
376                button = bui.buttonwidget(
377                    parent=self._subcontainer,
378                    label=bui.Lstr(resource='mainMenu.settingsText'),
379                    autoselect=True,
380                    size=(100, 40),
381                    position=(sub_width - 130, item_y + 6),
382                )
383                # noinspection PyUnresolvedReferences
384                bui.buttonwidget(
385                    edit=button,
386                    on_activate_call=bui.Call(plugin.show_settings_ui, button),
387                )
388            else:
389                button = None
390
391            # Allow getting back to back button.
392            if num_shown == 0:
393                bui.widget(
394                    edit=check,
395                    up_widget=self._back_button,
396                    left_widget=self._back_button,
397                    right_widget=(
398                        self._settings_button if button is None else button
399                    ),
400                )
401                if button is not None:
402                    bui.widget(edit=button, up_widget=self._back_button)
403
404            # Make sure we scroll all the way to the end when using
405            # keyboard/button nav.
406            bui.widget(edit=check, show_buffer_top=40, show_buffer_bottom=40)
407            num_shown += 1
408
409        bui.textwidget(
410            edit=self._num_plugins_text,
411            text=str(num_shown),
412        )
413
414        if num_shown == 0:
415            bui.textwidget(
416                edit=self._no_plugins_installed_text,
417                text=bui.Lstr(resource='noPluginsInstalledText'),
418            )
419
420    def _save_state(self) -> None:
421        try:
422            sel = self._root_widget.get_selected_child()
423            if sel == self._category_button:
424                sel_name = 'Category'
425            elif sel == self._settings_button:
426                sel_name = 'Settings'
427            elif sel == self._back_button:
428                sel_name = 'Back'
429            elif sel == self._scrollwidget:
430                sel_name = 'Scroll'
431            else:
432                raise ValueError(f'unrecognized selection \'{sel}\'')
433            assert bui.app.classic is not None
434            bui.app.ui_v1.window_states[type(self)] = sel_name
435        except Exception:
436            logging.exception('Error saving state for %s.', self)
437
438    def _restore_state(self) -> None:
439        try:
440            assert bui.app.classic is not None
441            sel_name = bui.app.ui_v1.window_states.get(type(self))
442            sel: bui.Widget | None
443            if sel_name == 'Category':
444                sel = self._category_button
445            elif sel_name == 'Settings':
446                sel = self._settings_button
447            elif sel_name == 'Back':
448                sel = self._back_button
449            else:
450                sel = self._scrollwidget
451            if sel:
452                bui.containerwidget(edit=self._root_widget, selected_child=sel)
453        except Exception:
454            logging.exception('Error restoring state for %s.', self)
class Category(enum.Enum):
19class Category(Enum):
20    """Categories we can display."""
21
22    ALL = 'all'
23    ENABLED = 'enabled'
24    DISABLED = 'disabled'
25
26    @property
27    def resource(self) -> str:
28        """Resource name for us."""
29        return f'{self.value}Text'

Categories we can display.

ALL = <Category.ALL: 'all'>
ENABLED = <Category.ENABLED: 'enabled'>
DISABLED = <Category.DISABLED: 'disabled'>
resource: str
26    @property
27    def resource(self) -> str:
28        """Resource name for us."""
29        return f'{self.value}Text'

Resource name for us.

Inherited Members
enum.Enum
name
value
class PluginWindow(bauiv1._uitypes.MainWindow):
 32class PluginWindow(bui.MainWindow):
 33    """Window for configuring plugins."""
 34
 35    def __init__(
 36        self,
 37        transition: str | None = 'in_right',
 38        origin_widget: bui.Widget | None = None,
 39    ):
 40        app = bui.app
 41
 42        self._category = Category.ALL
 43
 44        assert bui.app.classic is not None
 45        uiscale = bui.app.ui_v1.uiscale
 46        self._width = 870.0 if uiscale is bui.UIScale.SMALL else 670.0
 47        x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
 48        self._height = (
 49            370.0
 50            if uiscale is bui.UIScale.SMALL
 51            else 450.0 if uiscale is bui.UIScale.MEDIUM else 520.0
 52        )
 53        top_extra = 10 if uiscale is bui.UIScale.SMALL else 0
 54        super().__init__(
 55            root_widget=bui.containerwidget(
 56                size=(self._width, self._height + top_extra),
 57                toolbar_visibility=(
 58                    'menu_minimal'
 59                    if uiscale is bui.UIScale.SMALL
 60                    else 'menu_full'
 61                ),
 62                scale=(
 63                    1.9
 64                    if uiscale is bui.UIScale.SMALL
 65                    else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
 66                ),
 67                stack_offset=(
 68                    (0, -25) if uiscale is bui.UIScale.SMALL else (0, 0)
 69                ),
 70            ),
 71            transition=transition,
 72            origin_widget=origin_widget,
 73        )
 74
 75        self._scroll_width = self._width - (100 + 2 * x_inset)
 76        self._scroll_height = self._height - 115.0
 77        self._sub_width = self._scroll_width * 0.95
 78        self._sub_height = 724.0
 79
 80        assert app.classic is not None
 81        if uiscale is bui.UIScale.SMALL:
 82            bui.containerwidget(
 83                edit=self._root_widget, on_cancel_call=self.main_window_back
 84            )
 85            self._back_button = None
 86        else:
 87            self._back_button = bui.buttonwidget(
 88                parent=self._root_widget,
 89                position=(53 + x_inset, self._height - 60),
 90                size=(140, 60),
 91                scale=0.8,
 92                autoselect=True,
 93                label=bui.Lstr(resource='backText'),
 94                button_type='back',
 95                on_activate_call=self.main_window_back,
 96            )
 97            bui.containerwidget(
 98                edit=self._root_widget, cancel_button=self._back_button
 99            )
100
101        self._title_text = bui.textwidget(
102            parent=self._root_widget,
103            position=(self._width * 0.5, self._height - 41),
104            size=(0, 0),
105            text=bui.Lstr(resource='pluginsText'),
106            color=app.ui_v1.title_color,
107            maxwidth=170,
108            h_align='center',
109            v_align='center',
110        )
111
112        if self._back_button is not None:
113            bui.buttonwidget(
114                edit=self._back_button,
115                button_type='backSmall',
116                size=(60, 60),
117                label=bui.charstr(bui.SpecialChar.BACK),
118            )
119
120        settings_button_x = 670 if uiscale is bui.UIScale.SMALL else 570
121
122        self._num_plugins_text = bui.textwidget(
123            parent=self._root_widget,
124            position=(settings_button_x - 130, self._height - 41),
125            size=(0, 0),
126            text='',
127            h_align='center',
128            v_align='center',
129        )
130
131        self._category_button = bui.buttonwidget(
132            parent=self._root_widget,
133            scale=0.7,
134            position=(settings_button_x - 105, self._height - 60),
135            size=(130, 60),
136            label=bui.Lstr(resource='allText'),
137            autoselect=True,
138            on_activate_call=bui.WeakCall(self._show_category_options),
139            color=(0.55, 0.73, 0.25),
140            iconscale=1.2,
141        )
142
143        self._settings_button = bui.buttonwidget(
144            parent=self._root_widget,
145            position=(settings_button_x, self._height - 58),
146            size=(40, 40),
147            label='',
148            on_activate_call=self._open_settings,
149        )
150
151        bui.imagewidget(
152            parent=self._root_widget,
153            position=(settings_button_x + 3, self._height - 57),
154            draw_controller=self._settings_button,
155            size=(35, 35),
156            texture=bui.gettexture('settingsIcon'),
157        )
158
159        bui.widget(
160            edit=self._settings_button,
161            up_widget=self._settings_button,
162            right_widget=self._settings_button,
163        )
164
165        self._scrollwidget = bui.scrollwidget(
166            parent=self._root_widget,
167            position=(50 + x_inset, 50),
168            simple_culling_v=20.0,
169            highlight=False,
170            size=(self._scroll_width, self._scroll_height),
171            selection_loops_to_parent=True,
172            claims_left_right=True,
173        )
174        bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget)
175
176        self._no_plugins_installed_text = bui.textwidget(
177            parent=self._root_widget,
178            position=(self._width * 0.5, self._height * 0.5),
179            size=(0, 0),
180            text='',
181            color=(0.6, 0.6, 0.6),
182            scale=0.8,
183            h_align='center',
184            v_align='center',
185        )
186
187        if bui.app.meta.scanresults is None:
188            bui.screenmessage(
189                'Still scanning plugins; please try again.', color=(1, 0, 0)
190            )
191            bui.getsound('error').play()
192        plugspecs = bui.app.plugins.plugin_specs
193        plugstates: dict[str, dict] = bui.app.config.get('Plugins', {})
194        assert isinstance(plugstates, dict)
195
196        plug_line_height = 50
197        sub_width = self._scroll_width
198        sub_height = len(plugspecs) * plug_line_height
199        self._subcontainer = bui.containerwidget(
200            parent=self._scrollwidget,
201            size=(sub_width, sub_height),
202            background=False,
203        )
204        self._show_plugins()
205        bui.containerwidget(
206            edit=self._root_widget, selected_child=self._scrollwidget
207        )
208        self._restore_state()
209
210    @override
211    def get_main_window_state(self) -> bui.MainWindowState:
212        # Support recreating our window for back/refresh purposes.
213        cls = type(self)
214        return bui.BasicMainWindowState(
215            create_call=lambda transition, origin_widget: cls(
216                transition=transition, origin_widget=origin_widget
217            )
218        )
219
220    @override
221    def on_main_window_close(self) -> None:
222        self._save_state()
223
224    def _check_value_changed(self, plug: bui.PluginSpec, value: bool) -> None:
225        bui.screenmessage(
226            bui.Lstr(resource='settingsWindowAdvanced.mustRestartText'),
227            color=(1.0, 0.5, 0.0),
228        )
229        plugstates: dict[str, dict] = bui.app.config.setdefault('Plugins', {})
230        assert isinstance(plugstates, dict)
231        plugstate = plugstates.setdefault(plug.class_path, {})
232        plugstate['enabled'] = value
233        bui.app.config.commit()
234
235    def _open_settings(self) -> None:
236        # pylint: disable=cyclic-import
237        from bauiv1lib.settings.pluginsettings import PluginSettingsWindow
238
239        # no-op if we don't have control.
240        if not self.main_window_has_control():
241            return
242
243        self.main_window_replace(PluginSettingsWindow(transition='in_right'))
244
245    def _show_category_options(self) -> None:
246        uiscale = bui.app.ui_v1.uiscale
247
248        popup.PopupMenuWindow(
249            position=self._category_button.get_screen_space_center(),
250            scale=(
251                2.3
252                if uiscale is bui.UIScale.SMALL
253                else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23
254            ),
255            choices=[c.value for c in Category],
256            choices_display=[bui.Lstr(resource=c.resource) for c in Category],
257            current_choice=self._category.value,
258            delegate=self,
259        )
260
261    def popup_menu_selected_choice(
262        self, popup_window: popup.PopupMenuWindow, choice: str
263    ) -> None:
264        """Called when a choice is selected in the popup."""
265        del popup_window  # unused
266        self._category = Category(choice)
267        self._clear_scroll_widget()
268        self._show_plugins()
269
270        bui.buttonwidget(
271            edit=self._category_button,
272            label=bui.Lstr(resource=self._category.resource),
273        )
274
275    def popup_menu_closing(self, popup_window: popup.PopupWindow) -> None:
276        """Called when the popup is closing."""
277
278    def _clear_scroll_widget(self) -> None:
279        existing_widgets = self._subcontainer.get_children()
280        if existing_widgets:
281            for i in existing_widgets:
282                i.delete()
283
284    def _show_plugins(self) -> None:
285        # pylint: disable=too-many-locals
286        # pylint: disable=too-many-branches
287        # pylint: disable=too-many-statements
288        plugspecs = bui.app.plugins.plugin_specs
289        plugstates: dict[str, dict] = bui.app.config.setdefault('Plugins', {})
290        assert isinstance(plugstates, dict)
291
292        plug_line_height = 50
293        sub_width = self._scroll_width
294        num_enabled = 0
295        num_disabled = 0
296
297        plugspecs_sorted = sorted(plugspecs.items())
298
299        bui.textwidget(
300            edit=self._no_plugins_installed_text,
301            text='',
302        )
303
304        for _classpath, plugspec in plugspecs_sorted:
305            # counting number of enabled and disabled plugins
306            # plugstate = plugstates.setdefault(plugspec[0], {})
307            if plugspec.enabled:
308                num_enabled += 1
309            else:
310                num_disabled += 1
311
312        if self._category is Category.ALL:
313            sub_height = len(plugspecs) * plug_line_height
314            bui.containerwidget(
315                edit=self._subcontainer, size=(self._scroll_width, sub_height)
316            )
317        elif self._category is Category.ENABLED:
318            sub_height = num_enabled * plug_line_height
319            bui.containerwidget(
320                edit=self._subcontainer, size=(self._scroll_width, sub_height)
321            )
322        elif self._category is Category.DISABLED:
323            sub_height = num_disabled * plug_line_height
324            bui.containerwidget(
325                edit=self._subcontainer, size=(self._scroll_width, sub_height)
326            )
327        else:
328            # Make sure we handle all cases.
329            assert_never(self._category)
330
331        num_shown = 0
332        for classpath, plugspec in plugspecs_sorted:
333            plugin = plugspec.plugin
334            enabled = plugspec.enabled
335
336            if self._category is Category.ALL:
337                show = True
338            elif self._category is Category.ENABLED:
339                show = enabled
340            elif self._category is Category.DISABLED:
341                show = not enabled
342            else:
343                assert_never(self._category)
344
345            if not show:
346                continue
347
348            item_y = sub_height - (num_shown + 1) * plug_line_height
349            check = bui.checkboxwidget(
350                parent=self._subcontainer,
351                text=bui.Lstr(value=classpath),
352                autoselect=True,
353                value=enabled,
354                maxwidth=self._scroll_width
355                - (
356                    200
357                    if plugin is not None and plugin.has_settings_ui()
358                    else 80
359                ),
360                position=(10, item_y),
361                size=(self._scroll_width - 40, 50),
362                on_value_change_call=bui.Call(
363                    self._check_value_changed, plugspec
364                ),
365                textcolor=(
366                    (0.8, 0.3, 0.3)
367                    if (plugspec.attempted_load and plugspec.plugin is None)
368                    else (
369                        (0.6, 0.6, 0.6)
370                        if plugspec.plugin is None
371                        else (0, 1, 0)
372                    )
373                ),
374            )
375            # noinspection PyUnresolvedReferences
376            if plugin is not None and plugin.has_settings_ui():
377                button = bui.buttonwidget(
378                    parent=self._subcontainer,
379                    label=bui.Lstr(resource='mainMenu.settingsText'),
380                    autoselect=True,
381                    size=(100, 40),
382                    position=(sub_width - 130, item_y + 6),
383                )
384                # noinspection PyUnresolvedReferences
385                bui.buttonwidget(
386                    edit=button,
387                    on_activate_call=bui.Call(plugin.show_settings_ui, button),
388                )
389            else:
390                button = None
391
392            # Allow getting back to back button.
393            if num_shown == 0:
394                bui.widget(
395                    edit=check,
396                    up_widget=self._back_button,
397                    left_widget=self._back_button,
398                    right_widget=(
399                        self._settings_button if button is None else button
400                    ),
401                )
402                if button is not None:
403                    bui.widget(edit=button, up_widget=self._back_button)
404
405            # Make sure we scroll all the way to the end when using
406            # keyboard/button nav.
407            bui.widget(edit=check, show_buffer_top=40, show_buffer_bottom=40)
408            num_shown += 1
409
410        bui.textwidget(
411            edit=self._num_plugins_text,
412            text=str(num_shown),
413        )
414
415        if num_shown == 0:
416            bui.textwidget(
417                edit=self._no_plugins_installed_text,
418                text=bui.Lstr(resource='noPluginsInstalledText'),
419            )
420
421    def _save_state(self) -> None:
422        try:
423            sel = self._root_widget.get_selected_child()
424            if sel == self._category_button:
425                sel_name = 'Category'
426            elif sel == self._settings_button:
427                sel_name = 'Settings'
428            elif sel == self._back_button:
429                sel_name = 'Back'
430            elif sel == self._scrollwidget:
431                sel_name = 'Scroll'
432            else:
433                raise ValueError(f'unrecognized selection \'{sel}\'')
434            assert bui.app.classic is not None
435            bui.app.ui_v1.window_states[type(self)] = sel_name
436        except Exception:
437            logging.exception('Error saving state for %s.', self)
438
439    def _restore_state(self) -> None:
440        try:
441            assert bui.app.classic is not None
442            sel_name = bui.app.ui_v1.window_states.get(type(self))
443            sel: bui.Widget | None
444            if sel_name == 'Category':
445                sel = self._category_button
446            elif sel_name == 'Settings':
447                sel = self._settings_button
448            elif sel_name == 'Back':
449                sel = self._back_button
450            else:
451                sel = self._scrollwidget
452            if sel:
453                bui.containerwidget(edit=self._root_widget, selected_child=sel)
454        except Exception:
455            logging.exception('Error restoring state for %s.', self)

Window for configuring plugins.

PluginWindow( transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None)
 35    def __init__(
 36        self,
 37        transition: str | None = 'in_right',
 38        origin_widget: bui.Widget | None = None,
 39    ):
 40        app = bui.app
 41
 42        self._category = Category.ALL
 43
 44        assert bui.app.classic is not None
 45        uiscale = bui.app.ui_v1.uiscale
 46        self._width = 870.0 if uiscale is bui.UIScale.SMALL else 670.0
 47        x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
 48        self._height = (
 49            370.0
 50            if uiscale is bui.UIScale.SMALL
 51            else 450.0 if uiscale is bui.UIScale.MEDIUM else 520.0
 52        )
 53        top_extra = 10 if uiscale is bui.UIScale.SMALL else 0
 54        super().__init__(
 55            root_widget=bui.containerwidget(
 56                size=(self._width, self._height + top_extra),
 57                toolbar_visibility=(
 58                    'menu_minimal'
 59                    if uiscale is bui.UIScale.SMALL
 60                    else 'menu_full'
 61                ),
 62                scale=(
 63                    1.9
 64                    if uiscale is bui.UIScale.SMALL
 65                    else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
 66                ),
 67                stack_offset=(
 68                    (0, -25) if uiscale is bui.UIScale.SMALL else (0, 0)
 69                ),
 70            ),
 71            transition=transition,
 72            origin_widget=origin_widget,
 73        )
 74
 75        self._scroll_width = self._width - (100 + 2 * x_inset)
 76        self._scroll_height = self._height - 115.0
 77        self._sub_width = self._scroll_width * 0.95
 78        self._sub_height = 724.0
 79
 80        assert app.classic is not None
 81        if uiscale is bui.UIScale.SMALL:
 82            bui.containerwidget(
 83                edit=self._root_widget, on_cancel_call=self.main_window_back
 84            )
 85            self._back_button = None
 86        else:
 87            self._back_button = bui.buttonwidget(
 88                parent=self._root_widget,
 89                position=(53 + x_inset, self._height - 60),
 90                size=(140, 60),
 91                scale=0.8,
 92                autoselect=True,
 93                label=bui.Lstr(resource='backText'),
 94                button_type='back',
 95                on_activate_call=self.main_window_back,
 96            )
 97            bui.containerwidget(
 98                edit=self._root_widget, cancel_button=self._back_button
 99            )
100
101        self._title_text = bui.textwidget(
102            parent=self._root_widget,
103            position=(self._width * 0.5, self._height - 41),
104            size=(0, 0),
105            text=bui.Lstr(resource='pluginsText'),
106            color=app.ui_v1.title_color,
107            maxwidth=170,
108            h_align='center',
109            v_align='center',
110        )
111
112        if self._back_button is not None:
113            bui.buttonwidget(
114                edit=self._back_button,
115                button_type='backSmall',
116                size=(60, 60),
117                label=bui.charstr(bui.SpecialChar.BACK),
118            )
119
120        settings_button_x = 670 if uiscale is bui.UIScale.SMALL else 570
121
122        self._num_plugins_text = bui.textwidget(
123            parent=self._root_widget,
124            position=(settings_button_x - 130, self._height - 41),
125            size=(0, 0),
126            text='',
127            h_align='center',
128            v_align='center',
129        )
130
131        self._category_button = bui.buttonwidget(
132            parent=self._root_widget,
133            scale=0.7,
134            position=(settings_button_x - 105, self._height - 60),
135            size=(130, 60),
136            label=bui.Lstr(resource='allText'),
137            autoselect=True,
138            on_activate_call=bui.WeakCall(self._show_category_options),
139            color=(0.55, 0.73, 0.25),
140            iconscale=1.2,
141        )
142
143        self._settings_button = bui.buttonwidget(
144            parent=self._root_widget,
145            position=(settings_button_x, self._height - 58),
146            size=(40, 40),
147            label='',
148            on_activate_call=self._open_settings,
149        )
150
151        bui.imagewidget(
152            parent=self._root_widget,
153            position=(settings_button_x + 3, self._height - 57),
154            draw_controller=self._settings_button,
155            size=(35, 35),
156            texture=bui.gettexture('settingsIcon'),
157        )
158
159        bui.widget(
160            edit=self._settings_button,
161            up_widget=self._settings_button,
162            right_widget=self._settings_button,
163        )
164
165        self._scrollwidget = bui.scrollwidget(
166            parent=self._root_widget,
167            position=(50 + x_inset, 50),
168            simple_culling_v=20.0,
169            highlight=False,
170            size=(self._scroll_width, self._scroll_height),
171            selection_loops_to_parent=True,
172            claims_left_right=True,
173        )
174        bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget)
175
176        self._no_plugins_installed_text = bui.textwidget(
177            parent=self._root_widget,
178            position=(self._width * 0.5, self._height * 0.5),
179            size=(0, 0),
180            text='',
181            color=(0.6, 0.6, 0.6),
182            scale=0.8,
183            h_align='center',
184            v_align='center',
185        )
186
187        if bui.app.meta.scanresults is None:
188            bui.screenmessage(
189                'Still scanning plugins; please try again.', color=(1, 0, 0)
190            )
191            bui.getsound('error').play()
192        plugspecs = bui.app.plugins.plugin_specs
193        plugstates: dict[str, dict] = bui.app.config.get('Plugins', {})
194        assert isinstance(plugstates, dict)
195
196        plug_line_height = 50
197        sub_width = self._scroll_width
198        sub_height = len(plugspecs) * plug_line_height
199        self._subcontainer = bui.containerwidget(
200            parent=self._scrollwidget,
201            size=(sub_width, sub_height),
202            background=False,
203        )
204        self._show_plugins()
205        bui.containerwidget(
206            edit=self._root_widget, selected_child=self._scrollwidget
207        )
208        self._restore_state()

Create a MainWindow given a root widget and transition info.

Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
210    @override
211    def get_main_window_state(self) -> bui.MainWindowState:
212        # Support recreating our window for back/refresh purposes.
213        cls = type(self)
214        return bui.BasicMainWindowState(
215            create_call=lambda transition, origin_widget: cls(
216                transition=transition, origin_widget=origin_widget
217            )
218        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
220    @override
221    def on_main_window_close(self) -> None:
222        self._save_state()

Called before transitioning out a main window.

A good opportunity to save window state/etc.

def popup_menu_selected_choice(self, popup_window: bauiv1lib.popup.PopupMenuWindow, choice: str) -> None:
261    def popup_menu_selected_choice(
262        self, popup_window: popup.PopupMenuWindow, choice: str
263    ) -> None:
264        """Called when a choice is selected in the popup."""
265        del popup_window  # unused
266        self._category = Category(choice)
267        self._clear_scroll_widget()
268        self._show_plugins()
269
270        bui.buttonwidget(
271            edit=self._category_button,
272            label=bui.Lstr(resource=self._category.resource),
273        )

Called when a choice is selected in the popup.

def popup_menu_closing(self, popup_window: bauiv1lib.popup.PopupWindow) -> None:
275    def popup_menu_closing(self, popup_window: popup.PopupWindow) -> None:
276        """Called when the popup is closing."""

Called when the popup is closing.

Inherited Members
bauiv1._uitypes.MainWindow
main_window_back_state
main_window_is_top_level
main_window_close
main_window_has_control
main_window_back
main_window_replace
bauiv1._uitypes.Window
get_root_widget