nova-trame 0.23.1__py3-none-any.whl → 0.24.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ """Internal utilities for nova-trame."""
2
+
3
+ from typing import Any, Tuple, Union
4
+
5
+ from trame_server.core import State
6
+
7
+ from nova.mvvm._internal.utils import rgetdictvalue, rsetdictvalue
8
+
9
+
10
+ # Reads a state parameter from Trame. For internal use only, if you're using this in your application you're violating
11
+ # the MVVM framework. :)
12
+ def get_state_param(state: State, value: Union[Any, Tuple]) -> Any:
13
+ if isinstance(value, tuple):
14
+ return rgetdictvalue(state, value[0])
15
+
16
+ return value
17
+
18
+
19
+ # Writes a state parameter to Trame. For internal use only, if you're using this in your application you're violating
20
+ # the MVVM framework. :)
21
+ def set_state_param(state: State, value: Union[Any, Tuple], new_value: Any = None) -> Any:
22
+ with state:
23
+ if isinstance(value, tuple):
24
+ if new_value is not None:
25
+ rsetdictvalue(state, value[0], new_value)
26
+ elif len(value) > 1:
27
+ rsetdictvalue(state, value[0], value[1])
28
+ state.dirty(value[0].split(".")[0])
29
+
30
+ return get_state_param(state, value)
@@ -12,20 +12,23 @@ class DataSelectorState(BaseModel, validate_assignment=True):
12
12
  """Selection state for identifying datafiles."""
13
13
 
14
14
  directory: str = Field(default="")
15
- subdirectory: str = Field(default="")
16
15
  extensions: List[str] = Field(default=[])
17
- prefix: str = Field(default="")
16
+ subdirectory: str = Field(default="")
18
17
 
19
18
 
20
19
  class DataSelectorModel:
21
20
  """Manages file system interactions for the DataSelector widget."""
22
21
 
23
- def __init__(self, state: DataSelectorState, directory: str, extensions: List[str], prefix: str) -> None:
22
+ def __init__(self, state: DataSelectorState) -> None:
24
23
  self.state: DataSelectorState = state
25
24
 
26
- self.state.directory = directory
27
- self.state.extensions = extensions
28
- self.state.prefix = prefix
25
+ def set_binding_parameters(self, **kwargs: Any) -> None:
26
+ if "directory" in kwargs:
27
+ self.state.directory = kwargs["directory"]
28
+ if "extensions" in kwargs:
29
+ self.state.extensions = kwargs["extensions"]
30
+ if "subdirectory" in kwargs:
31
+ self.state.subdirectory = kwargs["subdirectory"]
29
32
 
30
33
  def sort_directories(self, directories: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
31
34
  # Sort the current level of dictionaries
@@ -89,10 +92,7 @@ class DataSelectorModel:
89
92
  def get_datafiles_from_path(self, base_path: Path) -> List[str]:
90
93
  datafiles = []
91
94
  try:
92
- if self.state.prefix:
93
- datafile_path = base_path / self.state.prefix
94
- else:
95
- datafile_path = base_path / self.state.subdirectory
95
+ datafile_path = base_path / self.state.subdirectory
96
96
 
97
97
  for entry in os.scandir(datafile_path):
98
98
  if entry.is_file():
@@ -107,23 +107,21 @@ class NeutronDataSelectorState(DataSelectorState):
107
107
  class NeutronDataSelectorModel(DataSelectorModel):
108
108
  """Manages file system interactions for the DataSelector widget."""
109
109
 
110
- def __init__(
111
- self,
112
- state: NeutronDataSelectorState,
113
- facility: str,
114
- instrument: str,
115
- extensions: List[str],
116
- prefix: str,
117
- allow_custom_directories: bool,
118
- ) -> None:
119
- super().__init__(state, "", extensions, prefix)
110
+ def __init__(self, state: NeutronDataSelectorState) -> None:
111
+ super().__init__(state)
120
112
  self.state: NeutronDataSelectorState = state
121
113
 
122
- self.state.facility = facility
123
- self.state.instrument = instrument
124
- self.state.extensions = extensions
125
- self.state.prefix = prefix
126
- self.state.allow_custom_directories = allow_custom_directories
114
+ def set_binding_parameters(self, **kwargs: Any) -> None:
115
+ super().set_binding_parameters(**kwargs)
116
+
117
+ if "facility" in kwargs:
118
+ self.state.facility = kwargs["facility"]
119
+ if "instrument" in kwargs:
120
+ self.state.instrument = kwargs["instrument"]
121
+ if "experiment" in kwargs:
122
+ self.state.experiment = kwargs["experiment"]
123
+ if "allow_custom_directories" in kwargs:
124
+ self.state.allow_custom_directories = kwargs["allow_custom_directories"]
127
125
 
128
126
  def get_facilities(self) -> List[str]:
129
127
  return natsorted(self.state.get_facilities())
@@ -183,11 +181,3 @@ class NeutronDataSelectorModel(DataSelectorModel):
183
181
  return []
184
182
 
185
183
  return self.get_datafiles_from_path(base_path)
186
-
187
- def set_state(self, facility: Optional[str], instrument: Optional[str], experiment: Optional[str]) -> None:
188
- if facility is not None:
189
- self.state.facility = facility
190
- if instrument is not None:
191
- self.state.instrument = instrument
192
- if experiment is not None:
193
- self.state.experiment = experiment
@@ -1,13 +1,17 @@
1
1
  """View Implementation for DataSelector."""
2
2
 
3
3
  from asyncio import ensure_future, sleep
4
- from typing import Any, List, Optional, cast
4
+ from typing import Any, List, Tuple, Union, cast
5
+ from warnings import warn
5
6
 
6
7
  from trame.app import get_server
7
8
  from trame.widgets import client, datagrid, html
8
9
  from trame.widgets import vuetify3 as vuetify
10
+ from trame_server.core import State
9
11
 
12
+ from nova.mvvm._internal.utils import rgetdictvalue
10
13
  from nova.mvvm.trame_binding import TrameBinding
14
+ from nova.trame._internal.utils import get_state_param, set_state_param
11
15
  from nova.trame.model.data_selector import DataSelectorModel, DataSelectorState
12
16
  from nova.trame.view.layouts import GridLayout, HBoxLayout, VBoxLayout
13
17
  from nova.trame.view_model.data_selector import DataSelectorViewModel
@@ -22,33 +26,36 @@ class DataSelector(datagrid.VGrid):
22
26
 
23
27
  def __init__(
24
28
  self,
25
- v_model: str,
26
- directory: str,
27
- extensions: Optional[List[str]] = None,
28
- prefix: str = "",
29
- refresh_rate: int = 30,
30
- select_strategy: str = "all",
29
+ v_model: Union[str, Tuple],
30
+ directory: Union[str, Tuple],
31
+ extensions: Union[List[str], Tuple, None] = None,
32
+ prefix: Union[str, Tuple] = "",
33
+ subdirectory: Union[str, Tuple] = "",
34
+ refresh_rate: Union[int, Tuple] = 30,
35
+ select_strategy: Union[str, Tuple] = "all",
31
36
  **kwargs: Any,
32
37
  ) -> None:
33
38
  """Constructor for DataSelector.
34
39
 
35
40
  Parameters
36
41
  ----------
37
- v_model : str
42
+ v_model : Union[str, Tuple]
38
43
  The name of the state variable to bind to this widget. The state variable will contain a list of the files
39
44
  selected by the user.
40
- directory : str
45
+ directory : Union[str, Tuple]
41
46
  The top-level folder to expose to users. Only contents of this directory and its children will be exposed to
42
47
  users.
43
- extensions : List[str], optional
48
+ extensions : Union[List[str], Tuple], optional
44
49
  A list of file extensions to restrict selection to. If unset, then all files will be shown.
45
- prefix : str, optional
46
- A subdirectory within the selected top-level folder to show files. If not specified, the user will be shown
47
- a folder browser and will be able to see all files in the selected top-level folder.
48
- refresh_rate : int, optional
50
+ prefix : Union[str, Tuple], optional
51
+ Deprecated. Please refer to the `subdirectory` parameter.
52
+ subdirectory : Union[str, Tuple], optional
53
+ A subdirectory within the selected top-level folder to show files. If not specified as a string, the user
54
+ will be shown a folder browser and will be able to see all files in the selected top-level folder.
55
+ refresh_rate : Union[int, Tuple], optional
49
56
  The number of seconds between attempts to automatically refresh the file list. Set to zero to disable this
50
57
  feature. Defaults to 30 seconds.
51
- select_strategy : str, optional
58
+ select_strategy : Union[str, Tuple], optional
52
59
  The selection strategy to pass to the `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`__.
53
60
  If unset, the `all` strategy will be used.
54
61
  **kwargs
@@ -73,11 +80,28 @@ class DataSelector(datagrid.VGrid):
73
80
  else:
74
81
  self._label = None
75
82
 
83
+ if prefix:
84
+ warn(
85
+ "The prefix parameter has been deprecated. Please switch to using the subdirectory parameter.",
86
+ category=DeprecationWarning,
87
+ stacklevel=1,
88
+ )
89
+
90
+ if not subdirectory:
91
+ subdirectory = prefix
92
+
76
93
  self._v_model = v_model
77
- self._v_model_name_in_state = v_model.split(".")[0]
94
+ if isinstance(v_model, str):
95
+ self._v_model_name_in_state = v_model.split(".")[0]
96
+ else:
97
+ self._v_model_name_in_state = v_model[0].split(".")[0]
98
+
78
99
  self._directory = directory
100
+ self._last_directory = get_state_param(self.state, self._directory)
79
101
  self._extensions = extensions if extensions is not None else []
80
- self._prefix = prefix
102
+ self._last_extensions = get_state_param(self.state, self._extensions)
103
+ self._subdirectory = subdirectory
104
+ self._last_subdirectory = get_state_param(self.state, self._subdirectory)
81
105
  self._refresh_rate = refresh_rate
82
106
  self._select_strategy = select_strategy
83
107
 
@@ -92,13 +116,18 @@ class DataSelector(datagrid.VGrid):
92
116
  ).exec
93
117
  self._reset_state = client.JSEval(exec=f"{self._v_model} = []; {self._flush_state}").exec
94
118
 
95
- self.create_model()
96
- self.create_viewmodel()
119
+ self._create_model()
120
+ self._create_viewmodel()
121
+ self._setup_bindings()
97
122
 
98
123
  self.create_ui(**kwargs)
99
124
 
100
125
  ensure_future(self._refresh_loop())
101
126
 
127
+ @property
128
+ def state(self) -> State:
129
+ return get_server(None, client_type="vue3").state
130
+
102
131
  def create_ui(self, *args: Any, **kwargs: Any) -> None:
103
132
  with VBoxLayout(classes="nova-data-selector", height="100%") as self._layout:
104
133
  with HBoxLayout(valign="center"):
@@ -110,7 +139,7 @@ class DataSelector(datagrid.VGrid):
110
139
  vuetify.VTooltip("Refresh Contents", activator="parent")
111
140
 
112
141
  with GridLayout(columns=2, classes="flex-1-0 h-0", valign="start"):
113
- if not self._prefix:
142
+ if isinstance(self._subdirectory, tuple) or not self._subdirectory:
114
143
  with html.Div(classes="d-flex flex-column h-100 overflow-hidden"):
115
144
  vuetify.VListSubheader("Available Directories", classes="flex-0-1 justify-center px-0")
116
145
  vuetify.VTreeview(
@@ -122,7 +151,7 @@ class DataSelector(datagrid.VGrid):
122
151
  item_value="path",
123
152
  items=(self._directories_name,),
124
153
  click_open=(self._vm.expand_directory, "[$event.path]"),
125
- update_activated=(self._vm.set_subdirectory, "$event"),
154
+ update_activated=(self.set_subdirectory, "$event"),
126
155
  )
127
156
  vuetify.VListItem("No directories found", classes="flex-0-1 text-center", v_else=True)
128
157
 
@@ -139,6 +168,7 @@ class DataSelector(datagrid.VGrid):
139
168
  " prop: 'title',"
140
169
  "}]",
141
170
  ),
171
+ column_span=1 if isinstance(self._subdirectory, tuple) or not self._subdirectory else 2,
142
172
  frame_size=10,
143
173
  hide_attribution=True,
144
174
  id=self._revogrid_id,
@@ -183,11 +213,11 @@ class DataSelector(datagrid.VGrid):
183
213
  f"(+{{{{ {self._v_model}.length - 2 }}}} others)", v_if="index === 2", classes="text-caption"
184
214
  )
185
215
 
186
- def create_model(self) -> None:
216
+ def _create_model(self) -> None:
187
217
  state = DataSelectorState()
188
- self._model = DataSelectorModel(state, self._directory, self._extensions, self._prefix)
218
+ self._model = DataSelectorModel(state)
189
219
 
190
- def create_viewmodel(self) -> None:
220
+ def _create_viewmodel(self) -> None:
191
221
  server = get_server(None, client_type="vue3")
192
222
  binding = TrameBinding(server.state)
193
223
 
@@ -197,8 +227,6 @@ class DataSelector(datagrid.VGrid):
197
227
  self._vm.datafiles_bind.connect(self._datafiles_name)
198
228
  self._vm.reset_bind.connect(self.reset)
199
229
 
200
- self._vm.update_view()
201
-
202
230
  def refresh_contents(self) -> None:
203
231
  self._vm.update_view(refresh_directories=True)
204
232
 
@@ -206,16 +234,86 @@ class DataSelector(datagrid.VGrid):
206
234
  self._reset_state()
207
235
  self._reset_rv_grid()
208
236
 
237
+ def set_subdirectory(self, subdirectory_path: str = "") -> None:
238
+ set_state_param(self.state, self._subdirectory, subdirectory_path)
239
+ self._vm.set_subdirectory(subdirectory_path)
240
+
209
241
  def set_state(self, *args: Any, **kwargs: Any) -> None:
210
242
  raise TypeError(
211
243
  "The old DataSelector component has been renamed to NeutronDataSelector. Please import it from "
212
244
  "`nova.trame.view.components.ornl`."
213
245
  )
214
246
 
247
+ # This method sets up Trame state change listeners for each binding parameter that can be changed directly by this
248
+ # component. This allows us to communicate the changes to the developer's bindings without requiring our own. We
249
+ # don't want bindings in the internal implementation as our callbacks could compete with the developer's.
250
+ def _setup_bindings(self) -> None:
251
+ # If the bindings were given initial values, write these to the state.
252
+ set_state_param(self.state, self._directory)
253
+ set_state_param(self.state, self._extensions)
254
+ set_state_param(self.state, self._subdirectory)
255
+ self._vm.set_binding_parameters(
256
+ directory=get_state_param(self.state, self._directory),
257
+ extensions=get_state_param(self.state, self._extensions),
258
+ subdirectory=get_state_param(self.state, self._subdirectory),
259
+ )
260
+
261
+ # The component used by this parameter will attempt to set the initial value itself, which will trigger the
262
+ # below change listeners causing unpredictable behavior.
263
+ if isinstance(self._subdirectory, tuple):
264
+ self._subdirectory = (self._subdirectory[0],)
265
+
266
+ # Now we set up the change listeners for all bound parameters. These are responsible for updating the component
267
+ # when other portions of the application manipulate these parameters.
268
+ if isinstance(self._directory, tuple):
269
+
270
+ @self.state.change(self._directory[0].split(".")[0])
271
+ def on_directory_change(**kwargs: Any) -> None:
272
+ directory = rgetdictvalue(kwargs, self._directory[0])
273
+ if directory != self._last_directory:
274
+ self._last_directory = directory
275
+ self._vm.set_binding_parameters(
276
+ directory=set_state_param(self.state, self._directory, directory),
277
+ )
278
+
279
+ if isinstance(self._extensions, tuple):
280
+
281
+ @self.state.change(self._extensions[0].split(".")[0])
282
+ def on_extensions_change(**kwargs: Any) -> None:
283
+ extensions = rgetdictvalue(kwargs, self._extensions[0])
284
+ if extensions != self._last_extensions:
285
+ self._last_extensions = extensions
286
+ self._vm.set_binding_parameters(
287
+ extensions=set_state_param(self.state, self._extensions, extensions),
288
+ )
289
+
290
+ if isinstance(self._subdirectory, tuple):
291
+
292
+ @self.state.change(self._subdirectory[0].split(".")[0])
293
+ def on_subdirectory_change(**kwargs: Any) -> None:
294
+ subdirectory = rgetdictvalue(kwargs, self._subdirectory[0])
295
+ if subdirectory != self._last_subdirectory:
296
+ self._last_subdirectory = subdirectory
297
+ self._vm.set_binding_parameters(
298
+ subdirectory=set_state_param(self.state, self._subdirectory, subdirectory),
299
+ )
300
+
215
301
  async def _refresh_loop(self) -> None:
216
- if self._refresh_rate > 0:
302
+ refresh_rate: int = set_state_param(self.state, self._refresh_rate)
303
+ skip = False
304
+
305
+ if refresh_rate > 0:
217
306
  while True:
218
- await sleep(self._refresh_rate)
307
+ await sleep(refresh_rate)
308
+ if skip:
309
+ continue
219
310
 
220
311
  self.refresh_contents()
221
312
  self.state.dirty(self._datafiles_name)
313
+
314
+ try:
315
+ refresh_rate = int(get_state_param(self.state, self._refresh_rate))
316
+ skip = False
317
+ except TypeError:
318
+ refresh_rate = 1
319
+ skip = True
@@ -1,11 +1,12 @@
1
1
  """View Implementation for DataSelector."""
2
2
 
3
- from typing import Any, List, Optional
3
+ from typing import Any, List, Tuple, Union
4
4
  from warnings import warn
5
5
 
6
6
  from trame.app import get_server
7
7
  from trame.widgets import vuetify3 as vuetify
8
8
 
9
+ from nova.mvvm._internal.utils import rgetdictvalue
9
10
  from nova.mvvm.trame_binding import TrameBinding
10
11
  from nova.trame.model.ornl.neutron_data_selector import (
11
12
  CUSTOM_DIRECTORIES_LABEL,
@@ -15,7 +16,7 @@ from nova.trame.model.ornl.neutron_data_selector import (
15
16
  from nova.trame.view.layouts import GridLayout
16
17
  from nova.trame.view_model.ornl.neutron_data_selector import NeutronDataSelectorViewModel
17
18
 
18
- from ..data_selector import DataSelector
19
+ from ..data_selector import DataSelector, get_state_param, set_state_param
19
20
  from ..input_field import InputField
20
21
 
21
22
  vuetify.enable_lab()
@@ -26,39 +27,42 @@ class NeutronDataSelector(DataSelector):
26
27
 
27
28
  def __init__(
28
29
  self,
29
- v_model: str,
30
- allow_custom_directories: bool = False,
31
- facility: str = "",
32
- instrument: str = "",
33
- extensions: Optional[List[str]] = None,
34
- prefix: str = "",
35
- refresh_rate: int = 30,
36
- select_strategy: str = "all",
30
+ v_model: Union[str, Tuple],
31
+ allow_custom_directories: Union[bool, Tuple] = False,
32
+ facility: Union[str, Tuple] = "",
33
+ instrument: Union[str, Tuple] = "",
34
+ experiment: Union[str, Tuple] = "",
35
+ extensions: Union[List[str], Tuple, None] = None,
36
+ subdirectory: Union[str, Tuple] = "",
37
+ refresh_rate: Union[int, Tuple] = 30,
38
+ select_strategy: Union[str, Tuple] = "all",
37
39
  **kwargs: Any,
38
40
  ) -> None:
39
41
  """Constructor for DataSelector.
40
42
 
41
43
  Parameters
42
44
  ----------
43
- v_model : str
45
+ v_model : Union[str, Tuple]
44
46
  The name of the state variable to bind to this widget. The state variable will contain a list of the files
45
47
  selected by the user.
46
- allow_custom_directories : bool, optional
48
+ allow_custom_directories : Union[bool, Tuple], optional
47
49
  Whether or not to allow users to provide their own directories to search for datafiles in. Ignored if the
48
50
  facility parameter is set.
49
- facility : str, optional
51
+ facility : Union[str, Tuple], optional
50
52
  The facility to restrict data selection to. Options: HFIR, SNS
51
- instrument : str, optional
53
+ instrument : Union[str, Tuple], optional
52
54
  The instrument to restrict data selection to. Please use the instrument acronym (e.g. CG-2).
53
- extensions : List[str], optional
55
+ experiment : Union[str, Tuple], optional
56
+ The experiment to restrict data selection to.
57
+ extensions : Union[List[str], Tuple], optional
54
58
  A list of file extensions to restrict selection to. If unset, then all files will be shown.
55
- prefix : str, optional
56
- A subdirectory within the user's chosen experiment to show files. If not specified, the user will be shown a
57
- folder browser and will be able to see all files in the experiment that they have access to.
58
- refresh_rate : int, optional
59
+ subdirectory : Union[str, Tuple], optional
60
+ A subdirectory within the user's chosen experiment to show files. If not specified as a string, the user
61
+ will be shown a folder browser and will be able to see all files in the experiment that they have access to.
62
+ refresh_rate : Union[str, Tuple], optional
59
63
  The number of seconds between attempts to automatically refresh the file list. Set to zero to disable this
60
64
  feature. Defaults to 30 seconds.
61
- select_strategy : str, optional
65
+ select_strategy : Union[str, Tuple], optional
62
66
  The selection strategy to pass to the `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`__.
63
67
  If unset, the `all` strategy will be used.
64
68
  **kwargs
@@ -69,53 +73,76 @@ class NeutronDataSelector(DataSelector):
69
73
  -------
70
74
  None
71
75
  """
72
- if facility and allow_custom_directories:
73
- warn("allow_custom_directories will be ignored since the facility parameter is set.", stacklevel=1)
76
+ if isinstance(facility, str) and allow_custom_directories:
77
+ warn("allow_custom_directories will be ignored since the facility parameter is fixed.", stacklevel=1)
74
78
 
75
79
  self._facility = facility
76
80
  self._instrument = instrument
81
+ self._experiment = experiment
77
82
  self._allow_custom_directories = allow_custom_directories
83
+ self._last_allow_custom_directories = self._allow_custom_directories
78
84
 
85
+ self._state_name = f"nova__dataselector_{self._next_id}_state"
79
86
  self._facilities_name = f"nova__neutrondataselector_{self._next_id}_facilities"
87
+ self._selected_facility_name = (
88
+ self._facility[0] if isinstance(self._facility, tuple) else f"{self._state_name}.facility"
89
+ )
80
90
  self._instruments_name = f"nova__neutrondataselector_{self._next_id}_instruments"
91
+ self._selected_instrument_name = (
92
+ self._instrument[0] if isinstance(self._instrument, tuple) else f"{self._state_name}.instrument"
93
+ )
81
94
  self._experiments_name = f"nova__neutrondataselector_{self._next_id}_experiments"
95
+ self._selected_experiment_name = (
96
+ self._experiment[0] if isinstance(self._experiment, tuple) else f"{self._state_name}.experiment"
97
+ )
82
98
 
83
- super().__init__(v_model, "", extensions, prefix, refresh_rate, select_strategy, **kwargs)
99
+ super().__init__(
100
+ v_model,
101
+ "",
102
+ extensions=extensions,
103
+ subdirectory=subdirectory,
104
+ refresh_rate=refresh_rate,
105
+ select_strategy=select_strategy,
106
+ **kwargs,
107
+ )
84
108
 
85
109
  def create_ui(self, **kwargs: Any) -> None:
86
110
  super().create_ui(**kwargs)
87
111
  with self._layout.filter:
88
112
  with GridLayout(columns=3):
89
113
  columns = 3
90
- if self._facility == "":
114
+ if isinstance(self._facility, tuple) or not self._facility:
91
115
  columns -= 1
92
116
  InputField(
93
- v_model=f"{self._state_name}.facility", items=(self._facilities_name,), type="autocomplete"
117
+ v_model=self._selected_facility_name,
118
+ items=(self._facilities_name,),
119
+ type="autocomplete",
120
+ update_modelValue=(self.update_facility, "[$event]"),
94
121
  )
95
- if self._instrument == "":
122
+ if isinstance(self._instrument, tuple) or not self._instrument:
96
123
  columns -= 1
97
124
  InputField(
98
- v_if=f"{self._state_name}.facility !== '{CUSTOM_DIRECTORIES_LABEL}'",
99
- v_model=f"{self._state_name}.instrument",
125
+ v_if=f"{self._selected_facility_name} !== '{CUSTOM_DIRECTORIES_LABEL}'",
126
+ v_model=self._selected_instrument_name,
100
127
  items=(self._instruments_name,),
101
128
  type="autocomplete",
129
+ update_modelValue=(self.update_instrument, "[$event]"),
102
130
  )
103
131
  InputField(
104
- v_if=f"{self._state_name}.facility !== '{CUSTOM_DIRECTORIES_LABEL}'",
105
- v_model=f"{self._state_name}.experiment",
132
+ v_if=f"{self._selected_facility_name} !== '{CUSTOM_DIRECTORIES_LABEL}'",
133
+ v_model=self._selected_experiment_name,
106
134
  column_span=columns,
107
135
  items=(self._experiments_name,),
108
136
  type="autocomplete",
137
+ update_modelValue=(self.update_experiment, "[$event]"),
109
138
  )
110
139
  InputField(v_else=True, v_model=f"{self._state_name}.custom_directory", column_span=2)
111
140
 
112
- def create_model(self) -> None:
141
+ def _create_model(self) -> None:
113
142
  state = NeutronDataSelectorState()
114
- self._model: NeutronDataSelectorModel = NeutronDataSelectorModel(
115
- state, self._facility, self._instrument, self._extensions, self._prefix, self._allow_custom_directories
116
- )
143
+ self._model: NeutronDataSelectorModel = NeutronDataSelectorModel(state)
117
144
 
118
- def create_viewmodel(self) -> None:
145
+ def _create_viewmodel(self) -> None:
119
146
  server = get_server(None, client_type="vue3")
120
147
  binding = TrameBinding(server.state)
121
148
 
@@ -130,26 +157,94 @@ class NeutronDataSelector(DataSelector):
130
157
 
131
158
  self._vm.update_view()
132
159
 
133
- def set_state(
134
- self, facility: Optional[str] = None, instrument: Optional[str] = None, experiment: Optional[str] = None
135
- ) -> None:
136
- """Programmatically set the facility, instrument, and/or experiment to restrict data selection to.
160
+ # This method sets up Trame state change listeners for each binding parameter that can be changed directly by this
161
+ # component. This allows us to communicate the changes to the developer's bindings without requiring our own. We
162
+ # don't want bindings in the internal implementation as our callbacks could compete with the developer's.
163
+ def _setup_bindings(self) -> None:
164
+ # If the bindings were given initial values, write these to the state.
165
+ set_state_param(self.state, self._facility)
166
+ set_state_param(self.state, self._instrument)
167
+ set_state_param(self.state, self._experiment)
168
+ set_state_param(self.state, self._allow_custom_directories)
169
+ self._last_facility = get_state_param(self.state, self._facility)
170
+ self._last_instrument = get_state_param(self.state, self._instrument)
171
+ self._last_experiment = get_state_param(self.state, self._experiment)
172
+ self._vm.set_binding_parameters(
173
+ facility=get_state_param(self.state, self._facility),
174
+ instrument=get_state_param(self.state, self._instrument),
175
+ experiment=get_state_param(self.state, self._experiment),
176
+ allow_custom_directories=get_state_param(self.state, self._allow_custom_directories),
177
+ )
137
178
 
138
- If a parameter is None, then it will not be updated.
179
+ # Now we set up the change listeners for all bound parameters. These are responsible for updating the component
180
+ # when other portions of the application manipulate these parameters.
181
+ if isinstance(self._facility, tuple):
182
+
183
+ @self.state.change(self._facility[0].split(".")[0])
184
+ def on_facility_change(**kwargs: Any) -> None:
185
+ facility = rgetdictvalue(kwargs, self._facility[0])
186
+ if facility != self._last_facility:
187
+ self._last_facility = facility
188
+ self._vm.set_binding_parameters(
189
+ facility=set_state_param(self.state, (self._selected_facility_name,), facility)
190
+ )
191
+ self._vm.reset()
139
192
 
140
- Parameters
141
- ----------
142
- facility : str, optional
143
- The facility to restrict data selection to. Options: HFIR, SNS
144
- instrument : str, optional
145
- The instrument to restrict data selection to. Must be at the selected facility.
146
- experiment : str, optional
147
- The experiment to restrict data selection to. Must begin with "IPTS-". It is your responsibility to validate
148
- that the provided experiment exists within the instrument directory. If it doesn't then no datafiles will be
149
- shown to the user.
193
+ if isinstance(self._instrument, tuple):
150
194
 
151
- Returns
152
- -------
153
- None
154
- """
155
- self._vm.set_state(facility, instrument, experiment)
195
+ @self.state.change(self._instrument[0].split(".")[0])
196
+ def on_instrument_change(**kwargs: Any) -> None:
197
+ instrument = rgetdictvalue(kwargs, self._instrument[0])
198
+ if instrument != self._last_instrument:
199
+ self._last_instrument = instrument
200
+ self._vm.set_binding_parameters(
201
+ instrument=set_state_param(self.state, (self._selected_instrument_name,), instrument)
202
+ )
203
+ self._vm.reset()
204
+
205
+ if isinstance(self._experiment, tuple):
206
+
207
+ @self.state.change(self._experiment[0].split(".")[0])
208
+ def on_experiment_change(**kwargs: Any) -> None:
209
+ experiment = rgetdictvalue(kwargs, self._experiment[0])
210
+ if experiment != self._last_experiment:
211
+ self._last_experiment = experiment
212
+ self._vm.set_binding_parameters(
213
+ experiment=set_state_param(self.state, (self._selected_experiment_name,), experiment)
214
+ )
215
+ self._vm.reset()
216
+
217
+ if isinstance(self._allow_custom_directories, tuple):
218
+
219
+ @self.state.change(self._allow_custom_directories[0].split(".")[0])
220
+ def on_allow_custom_directories_change(**kwargs: Any) -> None:
221
+ allow_custom_directories = rgetdictvalue(kwargs, self._allow_custom_directories[0]) # type: ignore
222
+ if allow_custom_directories != self._last_allow_custom_directories:
223
+ self._last_allow_custom_directories = allow_custom_directories
224
+ self._vm.set_binding_parameters(
225
+ allow_custom_directories=set_state_param(
226
+ self.state, self._allow_custom_directories, allow_custom_directories
227
+ )
228
+ )
229
+
230
+ # These update methods notify the rest of the application when the component changes bound parameters.
231
+ def update_facility(self, facility: str) -> None:
232
+ self._vm.set_binding_parameters(
233
+ facility=set_state_param(self.state, (self._selected_facility_name,), facility),
234
+ instrument=set_state_param(self.state, (self._selected_instrument_name,), ""), # Reset the instrument
235
+ experiment=set_state_param(self.state, (self._selected_experiment_name,), ""), # Reset the experiment
236
+ )
237
+ self._vm.reset()
238
+
239
+ def update_instrument(self, instrument: str) -> None:
240
+ self._vm.set_binding_parameters(
241
+ instrument=set_state_param(self.state, (self._selected_instrument_name,), instrument),
242
+ experiment=set_state_param(self.state, (self._selected_experiment_name,), ""), # Reset the experiment
243
+ )
244
+ self._vm.reset()
245
+
246
+ def update_experiment(self, experiment: str) -> None:
247
+ self._vm.set_binding_parameters(
248
+ experiment=set_state_param(self.state, (self._selected_experiment_name,), experiment),
249
+ )
250
+ self._vm.reset()
@@ -57,6 +57,10 @@ class DataSelectorViewModel:
57
57
  def on_state_updated(self, results: Dict[str, Any]) -> None:
58
58
  pass
59
59
 
60
+ def set_binding_parameters(self, **kwargs: Any) -> None:
61
+ self.model.set_binding_parameters(**kwargs)
62
+ self.update_view(refresh_directories=True)
63
+
60
64
  def set_subdirectory(self, subdirectory_path: str = "") -> None:
61
65
  self.model.set_subdirectory(subdirectory_path)
62
66
  self.update_view()
@@ -1,6 +1,6 @@
1
1
  """View model implementation for the DataSelector widget."""
2
2
 
3
- from typing import Any, Dict, Optional
3
+ from typing import Any, Dict
4
4
 
5
5
  from nova.mvvm.interface import BindingInterface
6
6
  from nova.trame.model.ornl.neutron_data_selector import NeutronDataSelectorModel
@@ -18,10 +18,6 @@ class NeutronDataSelectorViewModel(DataSelectorViewModel):
18
18
  self.instruments_bind = binding.new_bind()
19
19
  self.experiments_bind = binding.new_bind()
20
20
 
21
- def set_state(self, facility: Optional[str], instrument: Optional[str], experiment: Optional[str]) -> None:
22
- self.model.set_state(facility, instrument, experiment)
23
- self.update_view()
24
-
25
21
  def reset(self) -> None:
26
22
  self.model.set_subdirectory("")
27
23
  self.directories = self.model.get_directories()
@@ -31,17 +27,9 @@ class NeutronDataSelectorViewModel(DataSelectorViewModel):
31
27
  def on_state_updated(self, results: Dict[str, Any]) -> None:
32
28
  for update in results.get("updated", []):
33
29
  match update:
34
- case "facility":
35
- self.model.set_state(facility=None, instrument="", experiment="")
36
- self.reset()
37
- case "instrument":
38
- self.model.set_state(facility=None, instrument=None, experiment="")
39
- self.reset()
40
- case "experiment":
41
- self.reset()
42
30
  case "custom_directory":
43
31
  self.reset()
44
- self.update_view()
32
+ self.update_view()
45
33
 
46
34
  def update_view(self, refresh_directories: bool = False) -> None:
47
35
  self.facilities_bind.update_in_view(self.model.get_facilities())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nova-trame
3
- Version: 0.23.1
3
+ Version: 0.24.0
4
4
  Summary: A Python Package for injecting curated themes and custom components into Trame applications
5
5
  License: MIT
6
6
  Keywords: NDIP,Python,Trame,Vuetify
@@ -1,15 +1,16 @@
1
1
  nova/__init__.py,sha256=ED6jHcYiuYpr_0vjGz0zx2lrrmJT9sDJCzIljoDfmlM,65
2
2
  nova/trame/__init__.py,sha256=gFrAg1qva5PIqR5TjvPzAxLx103IKipJLqp3XXvrQL8,59
3
- nova/trame/model/data_selector.py,sha256=8eaIVGak6XOAQMxdUotOsiXisW14sSRd1V0oViglJVQ,4448
4
- nova/trame/model/ornl/neutron_data_selector.py,sha256=nfjXwT93JcPjJwyDynoP_stDVAfjUs7neeVZTk_04gc,6424
3
+ nova/trame/_internal/utils.py,sha256=Yi6zdHfeIHE5dQXmxZ9x0Yyuwkjn2geFoXAYtZ_PO2s,1060
4
+ nova/trame/model/data_selector.py,sha256=rDmWDtHVGgi5e2fBEYvChM2vVKNu798m67Sq8MUN2UI,4463
5
+ nova/trame/model/ornl/neutron_data_selector.py,sha256=Eu-CnX4Gr_T-Kz_l3SsMhyGf61hlHhRCYpcn7em7TCk,6168
5
6
  nova/trame/model/remote_file_input.py,sha256=9KAf31ZHzpsh_aXUrNcF81Q5jvUZDWCzW1QATKls-Jk,3675
6
7
  nova/trame/view/components/__init__.py,sha256=60BeS69aOrFnkptjuD17rfPE1f4Z35iBH56TRmW5MW8,451
7
- nova/trame/view/components/data_selector.py,sha256=d5UDF7ENBNifmLyIecx8VyINRY4vSxlQHyJ2a5G7IOY,9606
8
+ nova/trame/view/components/data_selector.py,sha256=EJ8fYCrkhRgDExAcewiUb7jTs7Qzo-GKVYWx7oFlrT0,14592
8
9
  nova/trame/view/components/execution_buttons.py,sha256=fIkrWKI3jFZqk3GHhtmYh3nK2c-HOXpD3D3zd_TUpi0,4049
9
10
  nova/trame/view/components/file_upload.py,sha256=7VcpfA6zmiqMDLkwVPlb35Tf0IUTBN1xsHpoUFnSr1w,3111
10
11
  nova/trame/view/components/input_field.py,sha256=q6WQ_N-BOlimUL9zgazDlsDfK28FrrKjH4he8e_HzRA,16088
11
12
  nova/trame/view/components/ornl/__init__.py,sha256=HnxzzSsxw0vQSDCVFfWsAxx1n3HnU37LMuQkfiewmSU,90
12
- nova/trame/view/components/ornl/neutron_data_selector.py,sha256=8B0BVXzVtMApnbMGD6ms6G4gdv5l-dIWM6YlHcBgnoc,6878
13
+ nova/trame/view/components/ornl/neutron_data_selector.py,sha256=YTYuLD5eDn3AsKguT8ksPx2Yme2RxheuXQGlhuTLOOc,12527
13
14
  nova/trame/view/components/progress_bar.py,sha256=fCfPw4MPAvORaeFOXugreok4GLpDVZGMkqvnv-AhMxg,2967
14
15
  nova/trame/view/components/remote_file_input.py,sha256=ByrBFj8svyWezcardCWrS_4Ag3fgTYNg_11lDW1FIA8,9669
15
16
  nova/trame/view/components/tool_outputs.py,sha256=-6pDURd2l_FK_8EWa9BI3KhU_KJXJ6uyJ_rW4nQVc08,2337
@@ -31,14 +32,14 @@ nova/trame/view/theme/assets/vuetify_config.json,sha256=a0FSgpLYWGFlRGSMhMq61MyD
31
32
  nova/trame/view/theme/exit_button.py,sha256=Kqv1GVJZGrSsj6_JFjGU3vm3iNuMolLC2T1x2IsdmV0,3094
32
33
  nova/trame/view/theme/theme.py,sha256=8JqSrEbhxK1SccXE1_jUdel9Wtc2QNObVEwtbVWG_QY,13146
33
34
  nova/trame/view/utilities/local_storage.py,sha256=vD8f2VZIpxhIKjZwEaD7siiPCTZO4cw9AfhwdawwYLY,3218
34
- nova/trame/view_model/data_selector.py,sha256=yDyYceZAUIxv4hhJGAPdIo54zHmrwqkxNZh7F2sRlw0,2810
35
+ nova/trame/view_model/data_selector.py,sha256=LeVbrBatzwffiOywI8M7F9ldZxlpi92rFRishHkFhmo,2975
35
36
  nova/trame/view_model/execution_buttons.py,sha256=MfKSp95D92EqpD48C15cBo6dLO0Yld4FeRZMJNxJf7Y,3551
36
- nova/trame/view_model/ornl/neutron_data_selector.py,sha256=WTuz9QO4WzbOftTH0uP9Qxn7nYdsnIoixu1hWHKCVNU,2128
37
+ nova/trame/view_model/ornl/neutron_data_selector.py,sha256=l1l_e0CFsVZ0h-9MPSjXTA2w1rgH3KoGAS67UyL8DvY,1551
37
38
  nova/trame/view_model/progress_bar.py,sha256=6AUKHF3hfzbdsHqNEnmHRgDcBKY5TT8ywDx9S6ovnsc,2854
38
39
  nova/trame/view_model/remote_file_input.py,sha256=ojEOJ8ZPkajpbAaZi9VLj7g-uBjhb8BMrTdMmwf_J6A,3367
39
40
  nova/trame/view_model/tool_outputs.py,sha256=ev6LY7fJ0H2xAJn9f5ww28c8Kpom2SYc2FbvFcoN4zg,829
40
- nova_trame-0.23.1.dist-info/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
41
- nova_trame-0.23.1.dist-info/METADATA,sha256=VVMLgB8JhBIzt_FbCAEUIjPh3Zu8gEqQy091t3Fpd8E,1688
42
- nova_trame-0.23.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
43
- nova_trame-0.23.1.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
44
- nova_trame-0.23.1.dist-info/RECORD,,
41
+ nova_trame-0.24.0.dist-info/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
42
+ nova_trame-0.24.0.dist-info/METADATA,sha256=pSBLrSiLCUFV9pg6yj-m9ZlQjUYT7Ms2uef6QYr2gKI,1688
43
+ nova_trame-0.24.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
44
+ nova_trame-0.24.0.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
45
+ nova_trame-0.24.0.dist-info/RECORD,,