small-fish-gui 1.9.3__py3-none-any.whl → 1.10.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.
@@ -2,145 +2,16 @@
2
2
  Contains Napari wrappers to visualise and correct spots/clusters.
3
3
  """
4
4
 
5
- import napari.layers
6
- import napari.types
7
5
  import numpy as np
8
6
  import napari
9
7
 
10
- from sklearn.cluster import DBSCAN
11
- from sklearn.neighbors import NearestNeighbors
12
-
13
8
  from magicgui import widgets
14
9
 
15
10
  from bigfish.stack import check_parameter
16
- from bigfish.detection.cluster_detection import _extract_information
17
- from ._napari_widgets import cell_label_eraser, segmentation_reseter, changes_propagater, free_label_picker
11
+ from ._napari_widgets import CellLabelEraser, SegmentationReseter, ChangesPropagater, FreeLabelPicker
12
+ from ._napari_widgets import ClusterIDSetter, ClusterMerger, ClusterUpdater, ClusterCreator
13
+ from ._napari_widgets import initialize_all_cluster_wizards
18
14
  from ..utils import compute_anisotropy_coef
19
- from ..pipeline._colocalisation import spots_multicolocalisation
20
-
21
- #Post detection
22
-
23
- def _update_clusters(
24
- old_spots : np.ndarray,
25
- spot_cluster_id : np.ndarray,
26
- new_spots : np.ndarray,
27
- old_clusters : np.ndarray,
28
- new_clusters : np.ndarray,
29
- cluster_size : int,
30
- min_number_spot : int,
31
- voxel_size : tuple,
32
- null_value = -2,
33
- talks = False,
34
- ) :
35
- """
36
-
37
- new_spots get weight of 1.
38
- spots already in cluster get weight 1
39
- spots not in cluster before but now in cluster radius get weigth = min_number_spot/*number of spot in new cluster radius (>=1)*
40
- spots in radius of deleted cluster get weight = 0 unless they are in radius of a new cluster.
41
-
42
- Parameters
43
- ----------
44
- new_spots : array (spots_number, space_dim + 1,) containing coordinates of each spots after napari correction as well as the id of belonging cluster. -1 if free spot, np.NaN if unknown.
45
- old_clusters : array (spots_number, space_dim + 2,) containing coordinates of each clusters centroid before napari correction, number of spots in cluster and the id of cluster.
46
- new_clusters : array (spots_number, space_dim + 2,) containing coordinates of each clusters centroid after napari correction, number of spots in cluster and the id of cluster. number of spots is NaN if new cluster.
47
- cluster_size : size of cluster in nanometer passed to DBSCAN.
48
-
49
- Returns
50
- -------
51
- corrected_spots : array with updated cluster id.
52
- corrected_clusters : array with updated number of spot.
53
-
54
- """
55
-
56
- spots_weights = np.ones(len(new_spots), dtype=float)
57
-
58
- if talks :
59
- print("\nTALKS IN napari_visualiser._update_clusters")
60
- print('new_spots_shape : ', new_spots.shape)
61
- print('old_clusters : ', old_clusters.shape)
62
- print('new_clusters : ', new_clusters.shape)
63
-
64
- #Finding new and deleted clusters
65
- deleted_cluster = old_clusters[~(np.isin(old_clusters[:,-1], new_clusters[:,-1]))]
66
- added_cluster = new_clusters[new_clusters[:,-1] == null_value]
67
-
68
- if talks :
69
- print('deleted_cluster : ', deleted_cluster.shape)
70
- print('added_cluster : ', added_cluster.shape)
71
-
72
-
73
-
74
- #Removing cluster_id from points clustered in deleted clusters
75
- spots_0_weights = old_spots[np.isin(spot_cluster_id, deleted_cluster[:,-1])]
76
- spots_weights[np.isin(new_spots, spots_0_weights).all(axis=1)] = 0 #Setting weigth to 0 for spots in deleted clusters.
77
-
78
- if talks :
79
- print("deleted cluster ids : ", deleted_cluster[:,-1])
80
- print("spots in deleted cluster : \n", spots_0_weights)
81
-
82
- #Finding spots in range of new clusters
83
- if len(added_cluster) > 0 :
84
- points_neighbors = NearestNeighbors(radius= cluster_size)
85
- points_neighbors.fit(new_spots*voxel_size)
86
- neighbor_query = points_neighbors.radius_neighbors(added_cluster[:,:-2]*voxel_size, return_distance=False)
87
-
88
- for cluster_neighbor in neighbor_query :
89
- neighboring_spot_number = len(cluster_neighbor)
90
- if neighboring_spot_number == 0 : continue # will not add a cluster if there is not even one spot nearby.
91
- weight = min_number_spot / neighboring_spot_number # >1
92
- if weight <= 1 : print("napari._update_clusters warning : weight <= 1; this should not happen some clusters might be missed during post napari computation.")
93
- if any(spots_weights[cluster_neighbor] > weight) : # Not replacing a weight for a smaller weigth to ensure all new clusters will be added.
94
- mask = spots_weights[cluster_neighbor] > weight
95
- cluster_neighbor = np.delete(cluster_neighbor, mask)
96
- if len(cluster_neighbor) > 0 : spots_weights[cluster_neighbor] = weight
97
-
98
- #Initiating new DBSCAN model
99
- dbscan_model = DBSCAN(cluster_size, min_samples=min_number_spot)
100
- dbscan_model.fit(new_spots*voxel_size, sample_weight=spots_weights)
101
-
102
- #Constructing corrected_arrays
103
- spots_labels = dbscan_model.labels_.reshape(len(new_spots), 1)
104
- corrected_spots = np.concatenate([new_spots, spots_labels], axis=1).astype(int)
105
- corrected_cluster = _extract_information(corrected_spots)
106
-
107
- if talks :
108
- print("spots with weigth 0 :", len(spots_weights[spots_weights == 0]))
109
- print("spots with weigth > 1 :", len(spots_weights[spots_weights > 1]))
110
- print("spots with weigth == 1 :", len(spots_weights[spots_weights == 1]))
111
- print("spots with weigth < 1 :", len(spots_weights[np.logical_and(spots_weights < 1,spots_weights > 0)]))
112
-
113
- print('corrected_spots : ', corrected_spots.shape)
114
- print('corrected_cluster : ', corrected_cluster.shape)
115
- print("END TALK\n")
116
-
117
-
118
- return corrected_spots, corrected_cluster
119
-
120
-
121
- def __update_clusters(new_clusters: np.ndarray, spots: np.ndarray, voxel_size, cluster_size, shape) :
122
- """
123
- Outdated. previous behaviour.
124
- """
125
- if len(new_clusters) == 0 : return new_clusters
126
- if len(spots) == 0 : return np.empty(shape=(0,2+len(voxel_size)), dtype=int)
127
-
128
- if len(new_clusters[0]) in [2,3] :
129
- new_clusters = np.concatenate([
130
- new_clusters,
131
- np.zeros(shape=(len(new_clusters),1), dtype=int),
132
- np.arange(len(new_clusters), dtype=int).reshape(len(new_clusters),1)
133
- ],axis=1, dtype=int)
134
-
135
- assert len(new_clusters[0]) == 4 or len(new_clusters[0]) == 5, "Wrong number of coordinates for clusters should not happen."
136
-
137
- # Update spots clusters
138
- new_clusters[:,-2] = spots_multicolocalisation(new_clusters[:,:-2], spots, radius_nm= cluster_size, voxel_size=voxel_size, image_shape=shape)
139
-
140
- # delete too small clusters
141
- new_clusters = np.delete(new_clusters, new_clusters[:,-2] == 0, 0)
142
-
143
- return new_clusters
144
15
 
145
16
  def correct_spots(
146
17
  image,
@@ -184,14 +55,19 @@ def correct_spots(
184
55
  for im, color in zip(other_images, other_colors) :
185
56
  Viewer.add_image(im, scale=scale, blending='additive', visible=False, colormap=color, contrast_limits=[im.min(), im.max()])
186
57
 
187
- Viewer.add_points( # single molecule spots; this layer can be update by user.
58
+ single_layer = Viewer.add_points( # single molecule spots; this layer can be update by user.
188
59
  spots,
189
60
  size = 5,
190
61
  scale=scale,
191
- face_color= 'transparent',
62
+ face_color= 'transparent',
63
+ border_color ='red',
192
64
  opacity= 1,
193
65
  symbol= 'disc',
194
- name= 'single spots'
66
+ name= 'single spots',
67
+ features={
68
+ "cluster_id" : spot_cluster_id if not spot_cluster_id is None else [],
69
+ "end" : [True] * len(spots)
70
+ }
195
71
  )
196
72
 
197
73
  if type(clusters) != type(None) :
@@ -199,7 +75,7 @@ def correct_spots(
199
75
  clusters_coordinates = clusters[:, :dim]
200
76
  else :
201
77
  clusters_coordinates = np.empty(shape=(0,dim), dtype=int)
202
- Viewer.add_points( # cluster; this layer can be update by user.
78
+ cluster_layer = Viewer.add_points( # cluster; this layer can be update by user.
203
79
  clusters_coordinates,
204
80
  size = 10,
205
81
  scale=scale,
@@ -207,45 +83,70 @@ def correct_spots(
207
83
  opacity= 0.7,
208
84
  symbol= 'diamond',
209
85
  name= 'foci',
210
- features= {"spot_number" : clusters[:,dim], "id" : clusters[:,dim+1]},
211
- feature_defaults= {"spot_number" : 0, "id" : -2} # napari features default will not work with np.NaN passing -2 instead.
86
+ features= {
87
+ "spot_number" : clusters[:,dim],
88
+ "cluster_id" : clusters[:,dim+1],
89
+ "end" : [True] * len(clusters_coordinates)
90
+ },
91
+ feature_defaults= {"spot_number" : 0, "cluster_id" : -2, "end" : True} # napari features default will not work with np.NaN passing -2 instead.
212
92
  )
213
93
 
214
94
  if type(cell_label) != type(None) and not np.array_equal(nucleus_label, cell_label) : Viewer.add_labels(cell_label, scale=scale, opacity= 0.2, blending= 'additive')
215
95
  if type(nucleus_label) != type(None) : Viewer.add_labels(nucleus_label, scale=scale, opacity= 0.2, blending= 'additive')
216
-
96
+
97
+ #Adding widget
98
+ if type(clusters) != type(None) :
99
+ initialize_all_cluster_wizards(
100
+ single_layer=single_layer,
101
+ cluster_layer=cluster_layer
102
+ )
103
+
104
+ widget_clusterID = ClusterIDSetter(single_layer=single_layer, cluster_layer=cluster_layer)
105
+ widget_cluster_merge =ClusterMerger(single_layer=single_layer, cluster_layer=cluster_layer)
106
+ widget_cluster_updater = ClusterUpdater(
107
+ single_layer=single_layer,
108
+ cluster_layer=cluster_layer,
109
+ default_cluster_radius= cluster_size,
110
+ default_min_spot= min_spot_number,
111
+ voxel_size=voxel_size
112
+ )
113
+ widget_cluster_creator = ClusterCreator(
114
+ cluster_layer=cluster_layer,
115
+ single_layer=single_layer
116
+ )
117
+
118
+
119
+ buttons_container = widgets.Container(widgets=[widget_clusterID.widget, widget_cluster_creator.widget], labels=False, layout='horizontal')
120
+ updater_container = widgets.Container(widgets=[widget_cluster_updater.widget, widget_cluster_merge.widget], labels=False)
121
+ tools_container = widgets.Container(
122
+ widgets = [updater_container, buttons_container],
123
+ labels=False,
124
+ )
125
+ Viewer.window.add_dock_widget(tools_container, name='SmallFish', area='left')
126
+
217
127
  Viewer.show(block=False)
218
128
  napari.run()
219
129
 
220
- new_spots = np.array(Viewer.layers['single spots'].data, dtype= int)
130
+ new_spots = np.concatenate([
131
+ single_layer.data,
132
+ single_layer.features.loc[:,["cluster_id"]].to_numpy()
133
+ ], axis=1).astype(int)
221
134
 
222
135
  if type(clusters) != type(None) :
223
- new_clusters = np.round(Viewer.layers['foci'].data).astype(int)
224
- if len(new_clusters) == 0 :
225
- new_clusters = np.empty(shape=(0,dim + 2), dtype=int)
226
- new_cluster_id = -1 * np.ones(shape=(len(new_spots), 1), dtype=int)
227
- new_spots = np.concatenate([new_spots, new_cluster_id], axis=1)
228
- else :
229
- new_cluster_id = Viewer.layers['foci'].features.to_numpy()
230
- new_clusters = np.concatenate([new_clusters, new_cluster_id], axis=1)
136
+ new_clusters = np.concatenate([
137
+ cluster_layer.data,
138
+ cluster_layer.features.loc[:,["spot_number","cluster_id"]].to_numpy()
139
+ ],axis=1)
231
140
 
232
-
233
- new_spots, new_clusters = _update_clusters(
234
- old_spots =spots,
235
- spot_cluster_id = spot_cluster_id,
236
- new_spots=new_spots,
237
- old_clusters=clusters,
238
- new_clusters=new_clusters,
239
- cluster_size=cluster_size,
240
- min_number_spot=min_spot_number,
241
- voxel_size=voxel_size,
242
- null_value= -2
243
- )
244
-
141
+ new_cluster_radius = widget_cluster_updater.cluster_radius
142
+ new_min_spot_number = widget_cluster_updater.min_spot
245
143
 
246
- else : new_clusters = None
144
+ else :
145
+ new_clusters = None
146
+ new_cluster_radius = None
147
+ new_min_spot_number = None
247
148
 
248
- return new_spots, new_clusters
149
+ return new_spots, new_clusters, new_cluster_radius, new_min_spot_number
249
150
 
250
151
 
251
152
  # Segmentation
@@ -295,10 +196,10 @@ def show_segmentation(
295
196
  labels_layer_list += [cyto_label_layer]
296
197
 
297
198
  #Adding widget
298
- label_eraser = cell_label_eraser(labels_layer_list)
299
- label_picker = free_label_picker(labels_layer_list)
300
- label_reseter = segmentation_reseter(labels_layer_list)
301
- changes_applier = changes_propagater(labels_layer_list)
199
+ label_eraser = CellLabelEraser(labels_layer_list)
200
+ label_picker = FreeLabelPicker(labels_layer_list)
201
+ label_reseter = SegmentationReseter(labels_layer_list)
202
+ changes_applier = ChangesPropagater(labels_layer_list)
302
203
 
303
204
  buttons_container = widgets.Container(widgets=[label_picker.widget, changes_applier.widget, label_reseter.widget], labels=False, layout='horizontal')
304
205
  tools_container = widgets.Container(
@@ -5,13 +5,13 @@ import numpy as np
5
5
  from typing import Literal, Union, Any
6
6
  from .layout import path_layout, parameters_layout, bool_layout, tuple_layout, combo_elmt, add_header, path_layout, radio_layout
7
7
  from ..interface import open_image, check_format, FormatError
8
- from .help_module import ask_help
8
+
9
9
 
10
10
  def prompt(layout, add_ok_cancel=True, timeout=None, timeout_key='TIMEOUT_KEY', add_scrollbar=True) :
11
11
  """
12
12
  Default event : 'Ok', 'Cancel'
13
13
  """
14
- if add_ok_cancel : layout += [[sg.Button('Ok'), sg.Button('Cancel')]]
14
+ if add_ok_cancel : layout += [[sg.Button('Ok', bind_return_key=True), sg.Button('Cancel')]]
15
15
 
16
16
  if add_scrollbar :
17
17
  size = (400,500)
@@ -20,7 +20,7 @@ def prompt(layout, add_ok_cancel=True, timeout=None, timeout_key='TIMEOUT_KEY',
20
20
  else :
21
21
  size = (None,None)
22
22
 
23
- window = sg.Window('small fish', layout=layout, margins=(10,10), size=size, resizable=True)
23
+ window = sg.Window('small fish', layout=layout, margins=(10,10), size=size, resizable=True, location=None)
24
24
  event, values = window.read(timeout=timeout, timeout_key=timeout_key)
25
25
  if event == None :
26
26
  window.close()
@@ -33,33 +33,7 @@ def prompt(layout, add_ok_cancel=True, timeout=None, timeout_key='TIMEOUT_KEY',
33
33
  window.close()
34
34
  return event, values
35
35
 
36
- def prompt_with_help(layout, help =None, add_scrollbar=True, vertical_scroll_only=True) :
37
- layout += [[]]
38
- layout += [[sg.Button('Ok'), sg.Button('Cancel')]]
39
-
40
- if add_scrollbar :
41
- size = (400,500)
42
- col_elmt = sg.Column(layout, scrollable=True, vertical_scroll_only=vertical_scroll_only, size=size)
43
- layout = [[col_elmt]]
44
- else :
45
- size = (None,None)
46
36
 
47
- window = sg.Window('small fish', layout=layout, size=size, resizable=True)
48
- while True :
49
- event, values = window.read()
50
- if event == None :
51
- window.close()
52
- quit()
53
-
54
- elif event == 'Ok':
55
- window.close()
56
- return event, values
57
- elif event == 'Help' :
58
- ask_help(chapter= help)
59
-
60
- else:
61
- window.close()
62
- return event,{}
63
37
 
64
38
  def input_image_prompt(
65
39
  is_3D_stack_preset=False,
@@ -70,7 +44,7 @@ def input_image_prompt(
70
44
  ) :
71
45
  """
72
46
  Keys :
73
- - 'image path'
47
+ - 'image_path'
74
48
  - 'is_3D_stack'
75
49
  - 'time stack'
76
50
  - 'is_multichannel'
@@ -81,18 +55,18 @@ def input_image_prompt(
81
55
  Returns Values
82
56
 
83
57
  """
84
- layout_image_path = path_layout(['image path'], header= "Image")
58
+ layout_image_path = path_layout(['image_path'], header= "Image")
85
59
  layout_image_path += bool_layout(['3D stack', 'Multichannel stack'],keys= ['is_3D_stack', 'is_multichannel'], preset= [is_3D_stack_preset, multichannel_preset])
86
60
 
87
61
  if type(do_dense_regions_deconvolution_preset) != type(None) and type(do_clustering_preset) != type(None) and type(do_Napari_correction) != type(None):
88
62
  layout_image_path += bool_layout(['Dense regions deconvolution', 'Compute clusters', 'Open results in Napari'], keys = ['do_dense_regions_deconvolution', 'do_cluster_computation', 'show_napari_corrector'], preset= [do_dense_regions_deconvolution_preset, do_clustering_preset, do_Napari_correction], header= "Pipeline settings")
89
63
 
90
- event, values = prompt_with_help(layout_image_path, help= 'general', add_scrollbar=False)
64
+ event, values = prompt(layout_image_path, add_scrollbar=False)
91
65
 
92
66
  if event == 'Cancel' :
93
67
  return None
94
68
 
95
- im_path = values['image path']
69
+ im_path = values['image_path']
96
70
  is_3D_stack = values['is_3D_stack']
97
71
  is_multichannel = values['is_multichannel']
98
72
 
@@ -191,8 +165,8 @@ def detection_parameters_promt(
191
165
 
192
166
  #Clustering
193
167
  if do_clustering :
194
- layout += parameters_layout(['cluster size'], unit="radius(nm)", default_values=[default_dict.setdefault('cluster size',400)])
195
- layout += parameters_layout(['min number of spots'], default_values=[default_dict.setdefault('min number of spots', 5)])
168
+ layout += parameters_layout(['cluster_size'], unit="radius(nm)", default_values=[default_dict.setdefault('cluster_size',400)])
169
+ layout += parameters_layout(['min_number_of_spots'], default_values=[default_dict.setdefault('min_number_of_spots', 5)])
196
170
 
197
171
  if is_multichannel and segmentation_done :
198
172
  default_segmentation = [default_dict.setdefault('nucleus channel signal', default_dict.setdefault('nucleus channel',0))]
@@ -205,9 +179,10 @@ def detection_parameters_promt(
205
179
  header= "Individual spot extraction",
206
180
  preset= default_dict.setdefault('spots_extraction_folder', '')
207
181
  )
182
+ default_filename = default_dict.setdefault("filename","") + "_spot_extraction"
208
183
  layout += parameters_layout(
209
184
  parameters=['spots_filename'],
210
- default_values=[default_dict.setdefault('spots_filename','spots_extraction')],
185
+ default_values=[default_filename],
211
186
  size= 13
212
187
  )
213
188
  layout += bool_layout(
@@ -216,13 +191,12 @@ def detection_parameters_promt(
216
191
  preset= [default_dict.setdefault('do_spots_csv',False), default_dict.setdefault('do_spots_excel',False),default_dict.setdefault('do_spots_feather',False)]
217
192
  )
218
193
 
219
- event, values = prompt_with_help(layout, help='detection')
194
+ event, values = prompt(layout)
220
195
  if event == 'Cancel' : return None
221
196
  if is_3D_stack : values['dim'] = 3
222
197
  else : values['dim'] = 2
223
198
  return values
224
199
 
225
-
226
200
  def ask_replace_file(filename:str) :
227
201
  layout = [
228
202
  [sg.Text("{0} already exists, replace ?")],
@@ -286,10 +260,10 @@ def hub_prompt(fov_results : pd.DataFrame, do_segmentation=False) -> 'Union[Lite
286
260
  [sg.Table(values= list(sumup_df.values), headings= list(sumup_df.columns), row_height=20, num_rows= 5, vertical_scroll_only=False, key= "result_table"), segmentation_object],
287
261
  [sg.Button('Segment cells'), sg.Button('Add detection'), sg.Button('Compute colocalisation'), sg.Button('Batch detection')],
288
262
  [sg.Button('Save results', button_color= 'green'), sg.Button('Save segmentation', button_color= 'green'), sg.Button('Load segmentation', button_color= 'green')],
289
- [sg.Button('Rename acquisition', button_color= 'gray'), sg.Button('Delete acquisitions',button_color= 'gray'), sg.Button('Reset segmentation',button_color= 'gray'), sg.Button('Reset all',button_color= 'gray')],
263
+ [sg.Button('Rename acquisition', button_color= 'gray'), sg.Button('Delete acquisitions',button_color= 'gray'), sg.Button('Reset segmentation',button_color= 'gray'), sg.Button('Reset all',button_color= 'gray'), sg.Button('Open wiki',button_color= 'yellow', key='wiki')],
290
264
  ]
291
265
 
292
- window = sg.Window('small fish', layout= layout, margins= (10,10))
266
+ window = sg.Window('small fish', layout= layout, margins= (10,10), location=None)
293
267
 
294
268
  while True :
295
269
  event, values = window.read()
@@ -301,7 +275,7 @@ def hub_prompt(fov_results : pd.DataFrame, do_segmentation=False) -> 'Union[Lite
301
275
 
302
276
  def coloc_prompt() :
303
277
  layout = parameters_layout(['colocalisation distance'], unit= 'nm', header= 'Colocalisation', default_values= 0)
304
- event, values = prompt_with_help(layout)
278
+ event, values = prompt(layout)
305
279
 
306
280
  if event == 'Ok' :
307
281
  return values['colocalisation distance']
@@ -309,7 +283,7 @@ def coloc_prompt() :
309
283
 
310
284
  def rename_prompt() :
311
285
  layout = parameters_layout(['name'], header= "Rename acquisitions", size=12)
312
- event, values = prompt_with_help(layout)
286
+ event, values = prompt(layout)
313
287
  if event == 'Ok' :
314
288
  return values['name']
315
289
  else : return False
@@ -341,7 +315,6 @@ def ask_cancel_detection() :
341
315
  else :
342
316
  return True
343
317
 
344
-
345
318
  def ask_confirmation(question_displayed : str) :
346
319
  layout =[
347
320
  [sg.Text(question_displayed, font= 'bold 10')],
@@ -355,7 +328,6 @@ def ask_confirmation(question_displayed : str) :
355
328
  else :
356
329
  return True
357
330
 
358
-
359
331
  def prompt_save_segmentation() -> 'dict[Literal["folder","filename","ext"]]':
360
332
  while True :
361
333
  relaunch = False
@@ -406,4 +378,29 @@ def prompt_load_segmentation() -> 'dict[Literal["nucleus","cytoplasm"]]':
406
378
  if not relaunch : break
407
379
 
408
380
 
409
- return values
381
+ return values
382
+
383
+ def prompt_restore_main_menu() -> bool :
384
+ """
385
+ Warn user that software will try to go back to main menu while saving parameters, and propose to save results and quit if stuck.
386
+
387
+ Returns True if user want to save and quit else False, to raise error close window.
388
+ """
389
+
390
+
391
+ layout = [
392
+ [sg.Text("An error was caught while proceeding.\nSoftware can try to save parameters and return to main menu or save results and quit.")],
393
+ [sg.Button("Return to main menu", key='menu'), sg.Button("Save and quit", key='save')]
394
+ ]
395
+
396
+ window = sg.Window('small fish', layout=layout, margins=(10,10), auto_size_text=True, resizable=True)
397
+ event, values = window.read(close=True)
398
+
399
+ if event is None :
400
+ return None
401
+ elif event == "save" :
402
+ return True
403
+ elif event == "menu" :
404
+ return False
405
+ else :
406
+ raise AssertionError("Unforseen answer")