nova-trame 0.23.0__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
@@ -47,6 +50,10 @@ class DataSelectorModel:
47
50
 
48
51
  if len(path_parts) > 1:
49
52
  dirs.clear()
53
+ elif path_parts != ["."]:
54
+ # Subdirectories are fully queried upon being opened, so we only need to query one item to determine
55
+ # if the target directory has any children.
56
+ dirs[:] = dirs[:1]
50
57
 
51
58
  # Only create a new entry for top-level directories
52
59
  if len(path_parts) == 1 and path_parts[0] != ".": # This indicates a top-level directory
@@ -85,10 +92,7 @@ class DataSelectorModel:
85
92
  def get_datafiles_from_path(self, base_path: Path) -> List[str]:
86
93
  datafiles = []
87
94
  try:
88
- if self.state.prefix:
89
- datafile_path = base_path / self.state.prefix
90
- else:
91
- datafile_path = base_path / self.state.subdirectory
95
+ datafile_path = base_path / self.state.subdirectory
92
96
 
93
97
  for entry in os.scandir(datafile_path):
94
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,14 +1,19 @@
1
1
  """View Implementation for DataSelector."""
2
2
 
3
- from typing import Any, List, Optional, cast
3
+ from asyncio import ensure_future, sleep
4
+ from typing import Any, List, Tuple, Union, cast
5
+ from warnings import warn
4
6
 
5
7
  from trame.app import get_server
6
8
  from trame.widgets import client, datagrid, html
7
9
  from trame.widgets import vuetify3 as vuetify
10
+ from trame_server.core import State
8
11
 
12
+ from nova.mvvm._internal.utils import rgetdictvalue
9
13
  from nova.mvvm.trame_binding import TrameBinding
14
+ from nova.trame._internal.utils import get_state_param, set_state_param
10
15
  from nova.trame.model.data_selector import DataSelectorModel, DataSelectorState
11
- from nova.trame.view.layouts import GridLayout, VBoxLayout
16
+ from nova.trame.view.layouts import GridLayout, HBoxLayout, VBoxLayout
12
17
  from nova.trame.view_model.data_selector import DataSelectorViewModel
13
18
 
14
19
  from .input_field import InputField
@@ -21,29 +26,36 @@ class DataSelector(datagrid.VGrid):
21
26
 
22
27
  def __init__(
23
28
  self,
24
- v_model: str,
25
- directory: str,
26
- extensions: Optional[List[str]] = None,
27
- prefix: str = "",
28
- 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",
29
36
  **kwargs: Any,
30
37
  ) -> None:
31
38
  """Constructor for DataSelector.
32
39
 
33
40
  Parameters
34
41
  ----------
35
- v_model : str
42
+ v_model : Union[str, Tuple]
36
43
  The name of the state variable to bind to this widget. The state variable will contain a list of the files
37
44
  selected by the user.
38
- directory : str
45
+ directory : Union[str, Tuple]
39
46
  The top-level folder to expose to users. Only contents of this directory and its children will be exposed to
40
47
  users.
41
- extensions : List[str], optional
48
+ extensions : Union[List[str], Tuple], optional
42
49
  A list of file extensions to restrict selection to. If unset, then all files will be shown.
43
- prefix : str, optional
44
- A subdirectory within the selected top-level folder to show files. If not specified, the user will be shown
45
- a folder browser and will be able to see all files in the selected top-level folder.
46
- select_strategy : str, 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
56
+ The number of seconds between attempts to automatically refresh the file list. Set to zero to disable this
57
+ feature. Defaults to 30 seconds.
58
+ select_strategy : Union[str, Tuple], optional
47
59
  The selection strategy to pass to the `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`__.
48
60
  If unset, the `all` strategy will be used.
49
61
  **kwargs
@@ -68,11 +80,29 @@ class DataSelector(datagrid.VGrid):
68
80
  else:
69
81
  self._label = None
70
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
+
71
93
  self._v_model = v_model
72
- 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
+
73
99
  self._directory = directory
100
+ self._last_directory = get_state_param(self.state, self._directory)
74
101
  self._extensions = extensions if extensions is not None else []
75
- 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)
105
+ self._refresh_rate = refresh_rate
76
106
  self._select_strategy = select_strategy
77
107
 
78
108
  self._revogrid_id = f"nova__dataselector_{self._next_id}_rv"
@@ -86,17 +116,30 @@ class DataSelector(datagrid.VGrid):
86
116
  ).exec
87
117
  self._reset_state = client.JSEval(exec=f"{self._v_model} = []; {self._flush_state}").exec
88
118
 
89
- self.create_model()
90
- self.create_viewmodel()
119
+ self._create_model()
120
+ self._create_viewmodel()
121
+ self._setup_bindings()
91
122
 
92
123
  self.create_ui(**kwargs)
93
124
 
125
+ ensure_future(self._refresh_loop())
126
+
127
+ @property
128
+ def state(self) -> State:
129
+ return get_server(None, client_type="vue3").state
130
+
94
131
  def create_ui(self, *args: Any, **kwargs: Any) -> None:
95
132
  with VBoxLayout(classes="nova-data-selector", height="100%") as self._layout:
96
- self._layout.filter = html.Div()
133
+ with HBoxLayout(valign="center"):
134
+ self._layout.filter = html.Div(classes="flex-1-1")
135
+ with vuetify.VBtn(
136
+ classes="mx-1", density="compact", icon=True, variant="text", click=self.refresh_contents
137
+ ):
138
+ vuetify.VIcon("mdi-refresh")
139
+ vuetify.VTooltip("Refresh Contents", activator="parent")
97
140
 
98
141
  with GridLayout(columns=2, classes="flex-1-0 h-0", valign="start"):
99
- if not self._prefix:
142
+ if isinstance(self._subdirectory, tuple) or not self._subdirectory:
100
143
  with html.Div(classes="d-flex flex-column h-100 overflow-hidden"):
101
144
  vuetify.VListSubheader("Available Directories", classes="flex-0-1 justify-center px-0")
102
145
  vuetify.VTreeview(
@@ -108,7 +151,7 @@ class DataSelector(datagrid.VGrid):
108
151
  item_value="path",
109
152
  items=(self._directories_name,),
110
153
  click_open=(self._vm.expand_directory, "[$event.path]"),
111
- update_activated=(self._vm.set_subdirectory, "$event"),
154
+ update_activated=(self.set_subdirectory, "$event"),
112
155
  )
113
156
  vuetify.VListItem("No directories found", classes="flex-0-1 text-center", v_else=True)
114
157
 
@@ -125,6 +168,7 @@ class DataSelector(datagrid.VGrid):
125
168
  " prop: 'title',"
126
169
  "}]",
127
170
  ),
171
+ column_span=1 if isinstance(self._subdirectory, tuple) or not self._subdirectory else 2,
128
172
  frame_size=10,
129
173
  hide_attribution=True,
130
174
  id=self._revogrid_id,
@@ -169,11 +213,11 @@ class DataSelector(datagrid.VGrid):
169
213
  f"(+{{{{ {self._v_model}.length - 2 }}}} others)", v_if="index === 2", classes="text-caption"
170
214
  )
171
215
 
172
- def create_model(self) -> None:
216
+ def _create_model(self) -> None:
173
217
  state = DataSelectorState()
174
- self._model = DataSelectorModel(state, self._directory, self._extensions, self._prefix)
218
+ self._model = DataSelectorModel(state)
175
219
 
176
- def create_viewmodel(self) -> None:
220
+ def _create_viewmodel(self) -> None:
177
221
  server = get_server(None, client_type="vue3")
178
222
  binding = TrameBinding(server.state)
179
223
 
@@ -183,14 +227,93 @@ class DataSelector(datagrid.VGrid):
183
227
  self._vm.datafiles_bind.connect(self._datafiles_name)
184
228
  self._vm.reset_bind.connect(self.reset)
185
229
 
186
- self._vm.update_view()
230
+ def refresh_contents(self) -> None:
231
+ self._vm.update_view(refresh_directories=True)
187
232
 
188
233
  def reset(self, _: Any = None) -> None:
189
234
  self._reset_state()
190
235
  self._reset_rv_grid()
191
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
+
192
241
  def set_state(self, *args: Any, **kwargs: Any) -> None:
193
242
  raise TypeError(
194
243
  "The old DataSelector component has been renamed to NeutronDataSelector. Please import it from "
195
244
  "`nova.trame.view.components.ornl`."
196
245
  )
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
+
301
+ async def _refresh_loop(self) -> None:
302
+ refresh_rate: int = set_state_param(self.state, self._refresh_rate)
303
+ skip = False
304
+
305
+ if refresh_rate > 0:
306
+ while True:
307
+ await sleep(refresh_rate)
308
+ if skip:
309
+ continue
310
+
311
+ self.refresh_contents()
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,35 +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
- 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",
36
39
  **kwargs: Any,
37
40
  ) -> None:
38
41
  """Constructor for DataSelector.
39
42
 
40
43
  Parameters
41
44
  ----------
42
- v_model : str
45
+ v_model : Union[str, Tuple]
43
46
  The name of the state variable to bind to this widget. The state variable will contain a list of the files
44
47
  selected by the user.
45
- allow_custom_directories : bool, optional
48
+ allow_custom_directories : Union[bool, Tuple], optional
46
49
  Whether or not to allow users to provide their own directories to search for datafiles in. Ignored if the
47
50
  facility parameter is set.
48
- facility : str, optional
51
+ facility : Union[str, Tuple], optional
49
52
  The facility to restrict data selection to. Options: HFIR, SNS
50
- instrument : str, optional
53
+ instrument : Union[str, Tuple], optional
51
54
  The instrument to restrict data selection to. Please use the instrument acronym (e.g. CG-2).
52
- 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
53
58
  A list of file extensions to restrict selection to. If unset, then all files will be shown.
54
- prefix : str, optional
55
- A subdirectory within the user's chosen experiment to show files. If not specified, the user will be shown a
56
- folder browser and will be able to see all files in the experiment that they have access to.
57
- select_strategy : str, 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
63
+ The number of seconds between attempts to automatically refresh the file list. Set to zero to disable this
64
+ feature. Defaults to 30 seconds.
65
+ select_strategy : Union[str, Tuple], optional
58
66
  The selection strategy to pass to the `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`__.
59
67
  If unset, the `all` strategy will be used.
60
68
  **kwargs
@@ -65,53 +73,76 @@ class NeutronDataSelector(DataSelector):
65
73
  -------
66
74
  None
67
75
  """
68
- if facility and allow_custom_directories:
69
- 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)
70
78
 
71
79
  self._facility = facility
72
80
  self._instrument = instrument
81
+ self._experiment = experiment
73
82
  self._allow_custom_directories = allow_custom_directories
83
+ self._last_allow_custom_directories = self._allow_custom_directories
74
84
 
85
+ self._state_name = f"nova__dataselector_{self._next_id}_state"
75
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
+ )
76
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
+ )
77
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
+ )
78
98
 
79
- super().__init__(v_model, "", extensions, prefix, 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
+ )
80
108
 
81
109
  def create_ui(self, **kwargs: Any) -> None:
82
110
  super().create_ui(**kwargs)
83
111
  with self._layout.filter:
84
112
  with GridLayout(columns=3):
85
113
  columns = 3
86
- if self._facility == "":
114
+ if isinstance(self._facility, tuple) or not self._facility:
87
115
  columns -= 1
88
116
  InputField(
89
- 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]"),
90
121
  )
91
- if self._instrument == "":
122
+ if isinstance(self._instrument, tuple) or not self._instrument:
92
123
  columns -= 1
93
124
  InputField(
94
- v_if=f"{self._state_name}.facility !== '{CUSTOM_DIRECTORIES_LABEL}'",
95
- 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,
96
127
  items=(self._instruments_name,),
97
128
  type="autocomplete",
129
+ update_modelValue=(self.update_instrument, "[$event]"),
98
130
  )
99
131
  InputField(
100
- v_if=f"{self._state_name}.facility !== '{CUSTOM_DIRECTORIES_LABEL}'",
101
- 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,
102
134
  column_span=columns,
103
135
  items=(self._experiments_name,),
104
136
  type="autocomplete",
137
+ update_modelValue=(self.update_experiment, "[$event]"),
105
138
  )
106
139
  InputField(v_else=True, v_model=f"{self._state_name}.custom_directory", column_span=2)
107
140
 
108
- def create_model(self) -> None:
141
+ def _create_model(self) -> None:
109
142
  state = NeutronDataSelectorState()
110
- self._model: NeutronDataSelectorModel = NeutronDataSelectorModel(
111
- state, self._facility, self._instrument, self._extensions, self._prefix, self._allow_custom_directories
112
- )
143
+ self._model: NeutronDataSelectorModel = NeutronDataSelectorModel(state)
113
144
 
114
- def create_viewmodel(self) -> None:
145
+ def _create_viewmodel(self) -> None:
115
146
  server = get_server(None, client_type="vue3")
116
147
  binding = TrameBinding(server.state)
117
148
 
@@ -126,26 +157,94 @@ class NeutronDataSelector(DataSelector):
126
157
 
127
158
  self._vm.update_view()
128
159
 
129
- def set_state(
130
- self, facility: Optional[str] = None, instrument: Optional[str] = None, experiment: Optional[str] = None
131
- ) -> None:
132
- """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
+ )
133
178
 
134
- 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()
135
192
 
136
- Parameters
137
- ----------
138
- facility : str, optional
139
- The facility to restrict data selection to. Options: HFIR, SNS
140
- instrument : str, optional
141
- The instrument to restrict data selection to. Must be at the selected facility.
142
- experiment : str, optional
143
- The experiment to restrict data selection to. Must begin with "IPTS-". It is your responsibility to validate
144
- that the provided experiment exists within the instrument directory. If it doesn't then no datafiles will be
145
- shown to the user.
193
+ if isinstance(self._instrument, tuple):
146
194
 
147
- Returns
148
- -------
149
- None
150
- """
151
- 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()
@@ -16,7 +16,7 @@ class DataSelectorViewModel:
16
16
 
17
17
  self.datafiles: List[Dict[str, Any]] = []
18
18
  self.directories: List[Dict[str, Any]] = []
19
- self.expanded: List[str] = []
19
+ self.expanded: Dict[str, List[str]] = {}
20
20
 
21
21
  self.state_bind = binding.new_bind(self.model.state, callback_after_update=self.on_state_updated)
22
22
  self.directories_bind = binding.new_bind()
@@ -44,20 +44,32 @@ class DataSelectorViewModel:
44
44
  current_level["children"] = new_directories
45
45
 
46
46
  # Mark this directory as expanded and display the new content
47
- self.expanded.append(paths[-1])
47
+ self.expanded[paths[-1]] = paths
48
48
  self.directories_bind.update_in_view(self.directories)
49
49
 
50
+ def reexpand_directories(self) -> None:
51
+ paths_to_expand = self.expanded.values()
52
+ self.expanded = {}
53
+
54
+ for paths in paths_to_expand:
55
+ self.expand_directory(paths)
56
+
50
57
  def on_state_updated(self, results: Dict[str, Any]) -> None:
51
58
  pass
52
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
+
53
64
  def set_subdirectory(self, subdirectory_path: str = "") -> None:
54
65
  self.model.set_subdirectory(subdirectory_path)
55
66
  self.update_view()
56
67
 
57
- def update_view(self) -> None:
68
+ def update_view(self, refresh_directories: bool = False) -> None:
58
69
  self.state_bind.update_in_view(self.model.state)
59
- if not self.directories:
70
+ if not self.directories or refresh_directories:
60
71
  self.directories = self.model.get_directories()
72
+ self.reexpand_directories()
61
73
  self.directories_bind.update_in_view(self.directories)
62
74
 
63
75
  self.datafiles = [
@@ -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,34 +18,22 @@ 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()
28
- self.expanded = []
24
+ self.expanded = {}
29
25
  self.reset_bind.update_in_view(None)
30
26
 
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
- def update_view(self) -> None:
34
+ def update_view(self, refresh_directories: bool = False) -> None:
47
35
  self.facilities_bind.update_in_view(self.model.get_facilities())
48
36
  self.instruments_bind.update_in_view(self.model.get_instruments())
49
37
  self.experiments_bind.update_in_view(self.model.get_experiments())
50
38
 
51
- super().update_view()
39
+ super().update_view(refresh_directories)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nova-trame
3
- Version: 0.23.0
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=jXu2tRmk0sI6fSbBs-bSggEiRw99M5d8NwpvPOk9nDs,4182
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=uahrLdTRKrec-g8L-xIYW0OG_dCnFuKJxq69ROf2c6I,8532
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=RZAowBxRaxlVsHTsukHTX0reveWG1K6ib8zb1kKL9Ow,6631
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=8XBCc12CcQfctOyz5zAOZTIXddKfpzIp8A_yZI-MpFc,2499
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=CtVva_MXWGRG_KclE4ln8XJP_dfy6-5unE3pjEStOpE,2074
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.0.dist-info/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
41
- nova_trame-0.23.0.dist-info/METADATA,sha256=vKtoLVHkJVqpA4yqwZZmjWWoYdPk1avkhUc5B8KuF80,1688
42
- nova_trame-0.23.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
43
- nova_trame-0.23.0.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
44
- nova_trame-0.23.0.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,,