Source code for stellarphot.settings.views

from ipyautoui import AutoUi
from ipywidgets import Layout

from .models import SourceLocationSettings, _extract_short_description

__all__ = ["ui_generator"]


[docs] def ui_generator(model, max_field_width=None, file_chooser_max_width=None): """ Generate a user interface with ipyautoui with a few default settings. Parameters ---------- model : `pydantic.BaseModel` subclass The model to generate the user interface for. max_field_width : str, optional The width of the fields in the user interface. Default is `None`, which will use the default width of the fields. The value is passed on to the `layout.width` attribute of the fields, which can be any valid CSS width. These typically include units, e.g. "100px" or "10em". file_chooser_max_width : str, optional The width of the file chooser fields in the user interface. Default is `None`, which will use the default width of the fields. This is separate from `max_field_width` because the FileChooser widget uses a different layout that is less cluttered than the fields. """ ui = AutoUi(model) # Oof, there is a bug in the file choose from ipyautoui. We patch up the # SourceLocationSettings widget here. if model == SourceLocationSettings: # Force a call to the handler to make sure the initial value is correct name = model.model_fields["source_list_file"].default value = ui.value.copy() value["source_list_file"] = name ui.value = value # validation is really messy looking right now, so suppress display of # the validation errors. A green check or red x will still be displayed. ui.show_validation = False # By default nullable are not shown at all but it seems much easier for # the user to understand if they are shown but disabled. ui.show_null = True # In the same spirit, the button to show/hide nullables should be hidden # too. ui.bn_shownull.layout.display = "none" # Set the description to the first sentence of the docstring. The default is # to use the entire docstring, which is often too long. ui.description = _extract_short_description(model.__doc__) # Always show nested models ui.open_nested = True # Validation is checked every time a value is changed, and the contents of each # field are written to the widget if validation passes. In at least one case, # the "Observatory" model, this is not helpful because the format of lat/lon is # not necessarily what the user entered. Furthermore, you can't edit the value # in the widget because it is overwritten every time validation passes. So, for # now, we will disable this feature by turning continuous update off for most # fields. for widget in ui.di_widgets.values(): if hasattr(widget, "continuous_update"): widget.continuous_update = False # In some cases, the entry fields are too wide to fit in the space available, which # makes the widget look bad. We can set the width of the fields to a fixed value # to make them fit better. for widget in ui.di_widgets.values(): if widget.__class__.__name__ == "FileChooser": if file_chooser_max_width is not None: # In a surprising twist, all FileChooser widgets seem to use the same # Layout object under the hood. So, if we change the width of one, we # change the width of all of them. This is not what we want, so for # those we create a fresh Layout object. The default min_width and width # are copied over so that they remain consistent with the other fields. widget.layout = Layout( max_width=file_chooser_max_width, min_width=widget.layout.min_width, width=widget.layout.width, ) continue if max_field_width is not None: widget.layout.max_width = max_field_width # The save and revert buttons should be enabled only when the user has made a # change AND the value in the widget is a valid pydantic model. # We begin by disabling the buttons. ui.savebuttonbar.bn_save.disabled = True ui.savebuttonbar.bn_revert.disabled = True # Now we add observers to enable/disable the buttons based on the validity of # the value and whether there are unsaved changes. # The save button should be enabled only when the user has made a change AND # the value in the widget is a valid pydantic model. ui.is_valid.observe( _handle_save_revert_button_state(ui, ui.savebuttonbar.bn_save), "value" ) ui.savebuttonbar.observe( _handle_save_revert_button_state(ui, ui.savebuttonbar.bn_save), "unsaved_changes", ) # The revert button should be enabled only when there are unsaved changes. ui.savebuttonbar.observe( _handle_save_revert_button_state( ui, ui.savebuttonbar.bn_revert, must_be_valid=False ), "unsaved_changes", ) return ui
def _handle_save_revert_button_state(widget, button, must_be_valid=True): """ Return a callback that will enable/disable the save and revert buttons based on the validity of the value and whether there are unsaved changes. Parameters ---------- widget : `ipyautoui.AutoUi` The user interface widget. button : `ipywidgets.Button` The button to enable/disable based on the widget state. must_be_valid : bool, optional If `True`, the button will only be enabled if the value in the widget is a valid pydantic model. If `False`, the button will be enabled regardless of the validity of the value. Default is `True`. """ def handler(_): """ A handler must take an argument but we don't use it here. """ valid_flag = widget.is_valid.value if must_be_valid else True needs_to_save = valid_flag and widget.savebuttonbar.unsaved_changes button.disabled = not needs_to_save return handler