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 our underlying widget is dead or on its way out.
239        if not self._root_widget or self._root_widget.transitioning_out:
240            return
241
242        self._save_state()
243        bui.containerwidget(edit=self._root_widget, transition='out_left')
244        assert bui.app.classic is not None
245        bui.app.ui_v1.set_main_window(
246            PluginSettingsWindow(transition='in_right'), from_window=self
247        )
248
249    def _show_category_options(self) -> None:
250        uiscale = bui.app.ui_v1.uiscale
251
252        popup.PopupMenuWindow(
253            position=self._category_button.get_screen_space_center(),
254            scale=(
255                2.3
256                if uiscale is bui.UIScale.SMALL
257                else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23
258            ),
259            choices=[c.value for c in Category],
260            choices_display=[bui.Lstr(resource=c.resource) for c in Category],
261            current_choice=self._category.value,
262            delegate=self,
263        )
264
265    def popup_menu_selected_choice(
266        self, popup_window: popup.PopupMenuWindow, choice: str
267    ) -> None:
268        """Called when a choice is selected in the popup."""
269        del popup_window  # unused
270        self._category = Category(choice)
271        self._clear_scroll_widget()
272        self._show_plugins()
273
274        bui.buttonwidget(
275            edit=self._category_button,
276            label=bui.Lstr(resource=self._category.resource),
277        )
278
279    def popup_menu_closing(self, popup_window: popup.PopupWindow) -> None:
280        """Called when the popup is closing."""
281
282    def _clear_scroll_widget(self) -> None:
283        existing_widgets = self._subcontainer.get_children()
284        if existing_widgets:
285            for i in existing_widgets:
286                i.delete()
287
288    def _show_plugins(self) -> None:
289        # pylint: disable=too-many-locals
290        # pylint: disable=too-many-branches
291        # pylint: disable=too-many-statements
292        plugspecs = bui.app.plugins.plugin_specs
293        plugstates: dict[str, dict] = bui.app.config.setdefault('Plugins', {})
294        assert isinstance(plugstates, dict)
295
296        plug_line_height = 50
297        sub_width = self._scroll_width
298        num_enabled = 0
299        num_disabled = 0
300
301        plugspecs_sorted = sorted(plugspecs.items())
302
303        bui.textwidget(
304            edit=self._no_plugins_installed_text,
305            text='',
306        )
307
308        for _classpath, plugspec in plugspecs_sorted:
309            # counting number of enabled and disabled plugins
310            # plugstate = plugstates.setdefault(plugspec[0], {})
311            if plugspec.enabled:
312                num_enabled += 1
313            else:
314                num_disabled += 1
315
316        if self._category is Category.ALL:
317            sub_height = len(plugspecs) * plug_line_height
318            bui.containerwidget(
319                edit=self._subcontainer, size=(self._scroll_width, sub_height)
320            )
321        elif self._category is Category.ENABLED:
322            sub_height = num_enabled * plug_line_height
323            bui.containerwidget(
324                edit=self._subcontainer, size=(self._scroll_width, sub_height)
325            )
326        elif self._category is Category.DISABLED:
327            sub_height = num_disabled * plug_line_height
328            bui.containerwidget(
329                edit=self._subcontainer, size=(self._scroll_width, sub_height)
330            )
331        else:
332            # Make sure we handle all cases.
333            assert_never(self._category)
334
335        num_shown = 0
336        for classpath, plugspec in plugspecs_sorted:
337            plugin = plugspec.plugin
338            enabled = plugspec.enabled
339
340            if self._category is Category.ALL:
341                show = True
342            elif self._category is Category.ENABLED:
343                show = enabled
344            elif self._category is Category.DISABLED:
345                show = not enabled
346            else:
347                assert_never(self._category)
348
349            if not show:
350                continue
351
352            item_y = sub_height - (num_shown + 1) * plug_line_height
353            check = bui.checkboxwidget(
354                parent=self._subcontainer,
355                text=bui.Lstr(value=classpath),
356                autoselect=True,
357                value=enabled,
358                maxwidth=self._scroll_width
359                - (
360                    200
361                    if plugin is not None and plugin.has_settings_ui()
362                    else 80
363                ),
364                position=(10, item_y),
365                size=(self._scroll_width - 40, 50),
366                on_value_change_call=bui.Call(
367                    self._check_value_changed, plugspec
368                ),
369                textcolor=(
370                    (0.8, 0.3, 0.3)
371                    if (plugspec.attempted_load and plugspec.plugin is None)
372                    else (
373                        (0.6, 0.6, 0.6)
374                        if plugspec.plugin is None
375                        else (0, 1, 0)
376                    )
377                ),
378            )
379            # noinspection PyUnresolvedReferences
380            if plugin is not None and plugin.has_settings_ui():
381                button = bui.buttonwidget(
382                    parent=self._subcontainer,
383                    label=bui.Lstr(resource='mainMenu.settingsText'),
384                    autoselect=True,
385                    size=(100, 40),
386                    position=(sub_width - 130, item_y + 6),
387                )
388                # noinspection PyUnresolvedReferences
389                bui.buttonwidget(
390                    edit=button,
391                    on_activate_call=bui.Call(plugin.show_settings_ui, button),
392                )
393            else:
394                button = None
395
396            # Allow getting back to back button.
397            if num_shown == 0:
398                bui.widget(
399                    edit=check,
400                    up_widget=self._back_button,
401                    left_widget=self._back_button,
402                    right_widget=(
403                        self._settings_button if button is None else button
404                    ),
405                )
406                if button is not None:
407                    bui.widget(edit=button, up_widget=self._back_button)
408
409            # Make sure we scroll all the way to the end when using
410            # keyboard/button nav.
411            bui.widget(edit=check, show_buffer_top=40, show_buffer_bottom=40)
412            num_shown += 1
413
414        bui.textwidget(
415            edit=self._num_plugins_text,
416            text=str(num_shown),
417        )
418
419        if num_shown == 0:
420            bui.textwidget(
421                edit=self._no_plugins_installed_text,
422                text=bui.Lstr(resource='noPluginsInstalledText'),
423            )
424
425    def _save_state(self) -> None:
426        try:
427            sel = self._root_widget.get_selected_child()
428            if sel == self._category_button:
429                sel_name = 'Category'
430            elif sel == self._settings_button:
431                sel_name = 'Settings'
432            elif sel == self._back_button:
433                sel_name = 'Back'
434            elif sel == self._scrollwidget:
435                sel_name = 'Scroll'
436            else:
437                raise ValueError(f'unrecognized selection \'{sel}\'')
438            assert bui.app.classic is not None
439            bui.app.ui_v1.window_states[type(self)] = sel_name
440        except Exception:
441            logging.exception('Error saving state for %s.', self)
442
443    def _restore_state(self) -> None:
444        try:
445            assert bui.app.classic is not None
446            sel_name = bui.app.ui_v1.window_states.get(type(self))
447            sel: bui.Widget | None
448            if sel_name == 'Category':
449                sel = self._category_button
450            elif sel_name == 'Settings':
451                sel = self._settings_button
452            elif sel_name == 'Back':
453                sel = self._back_button
454            else:
455                sel = self._scrollwidget
456            if sel:
457                bui.containerwidget(edit=self._root_widget, selected_child=sel)
458        except Exception:
459            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 our underlying widget is dead or on its way out.
240        if not self._root_widget or self._root_widget.transitioning_out:
241            return
242
243        self._save_state()
244        bui.containerwidget(edit=self._root_widget, transition='out_left')
245        assert bui.app.classic is not None
246        bui.app.ui_v1.set_main_window(
247            PluginSettingsWindow(transition='in_right'), from_window=self
248        )
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)

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:
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        )

Called when a choice is selected in the popup.

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

Called when the popup is closing.

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