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

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

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
226    @override
227    def on_main_window_close(self) -> None:
228        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:
267    def popup_menu_selected_choice(
268        self, popup_window: popup.PopupMenuWindow, choice: str
269    ) -> None:
270        """Called when a choice is selected in the popup."""
271        del popup_window  # unused
272        self._category = Category(choice)
273        self._clear_scroll_widget()
274        self._show_plugins()
275
276        bui.buttonwidget(
277            edit=self._category_button,
278            label=bui.Lstr(resource=self._category.resource),
279        )

Called when a choice is selected in the popup.

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

Called when the popup is closing.