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

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
239    @override
240    def on_main_window_close(self) -> None:
241        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:
280    def popup_menu_selected_choice(
281        self, popup_window: popup.PopupMenuWindow, choice: str
282    ) -> None:
283        """Called when a choice is selected in the popup."""
284        del popup_window  # unused
285        self._category = Category(choice)
286        self._clear_scroll_widget()
287        self._show_plugins()
288
289        bui.buttonwidget(
290            edit=self._category_button,
291            label=bui.Lstr(resource=self._category.resource),
292        )

Called when a choice is selected in the popup.

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

Called when the popup is closing.