Commit 07446e4038c30bcac42070bdc2e0541182088e6d
1 parent
208b8ff6
Exists in
master
and in
6 other branches
ENH: Project toolbar and task_import buttons and operations / restructured dialogs
Showing
4 changed files
with
174 additions
and
124 deletions
Show diff stats
invesalius/constants.py
... | ... | @@ -276,7 +276,7 @@ VTK_WARNING = 0 |
276 | 276 | #---------------------------------------------------------- |
277 | 277 | |
278 | 278 | [ID_FILE_IMPORT, ID_FILE_LOAD_INTERNET, ID_FILE_SAVE, ID_FILE_PHOTO, |
279 | -ID_FILE_PRINT] = [wx.NewId() for number in range(5)] | |
279 | +ID_FILE_PRINT, ID_FILE_OPEN] = [wx.NewId() for number in range(6)] | |
280 | 280 | |
281 | 281 | |
282 | 282 | ... | ... |
invesalius/gui/dialogs.py
... | ... | @@ -16,10 +16,15 @@ |
16 | 16 | # PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais |
17 | 17 | # detalhes. |
18 | 18 | #-------------------------------------------------------------------------- |
19 | +import os | |
20 | +import sys | |
21 | + | |
19 | 22 | import wx |
20 | 23 | from wx.lib import masked |
21 | 24 | import wx.lib.pubsub as ps |
22 | 25 | |
26 | +import project | |
27 | + | |
23 | 28 | class NumberDialog(wx.Dialog): |
24 | 29 | def __init__(self, message, value=0): |
25 | 30 | pre = wx.PreDialog() |
... | ... | @@ -115,4 +120,74 @@ class ProgressDialog(object): |
115 | 120 | |
116 | 121 | def Close(self): |
117 | 122 | self.dlg.Destroy() |
123 | + | |
124 | + | |
125 | + | |
126 | + | |
127 | + | |
128 | + | |
129 | +#--------- | |
130 | +WILDCARD_OPEN = "InVesalius 3 project (*.inv3)|*.inv3|"\ | |
131 | + "All files (*.*)|*.*" | |
132 | + | |
133 | +def ShowOpenProjectDialog(): | |
134 | + # Default system path | |
135 | + if sys.platform == 'win32': | |
136 | + default_path = "" | |
137 | + else: | |
138 | + default_path = os.getcwd() | |
139 | + | |
140 | + dlg = wx.FileDialog(None, message="Open InVesalius 3 project...", | |
141 | + defaultDir=default_path, | |
142 | + defaultFile="", wildcard=WILDCARD_OPEN, | |
143 | + style=wx.OPEN|wx.CHANGE_DIR) | |
144 | + | |
145 | + # In OSX this filter is not working - wxPython 2.8.10 problem | |
146 | + if sys.platform != 'darwin': | |
147 | + dlg.SetFilterIndex(0) | |
148 | + else: | |
149 | + dlg.SetFilterIndex(1) | |
150 | + | |
151 | + # Show the dialog and retrieve the user response. If it is the OK response, | |
152 | + # process the data. | |
153 | + filepath = None | |
154 | + if dlg.ShowModal() == wx.ID_OK: | |
155 | + # This returns a Python list of files that were selected. | |
156 | + filepath = dlg.GetPath() | |
157 | + | |
158 | + # Destroy the dialog. Don't do this until you are done with it! | |
159 | + # BAD things can happen otherwise! | |
160 | + dlg.Destroy() | |
161 | + return filepath | |
162 | + | |
163 | +def ShowImportDirDialog(): | |
164 | + dlg = wx.DirDialog(None, "Choose a DICOM folder:", "", | |
165 | + style=wx.DD_DEFAULT_STYLE | |
166 | + | wx.DD_DIR_MUST_EXIST | |
167 | + | wx.DD_CHANGE_DIR) | |
168 | + | |
169 | + path = None | |
170 | + if dlg.ShowModal() == wx.ID_OK: | |
171 | + path = dlg.GetPath() | |
118 | 172 | |
173 | + # Only destroy a dialog after you're done with it. | |
174 | + dlg.Destroy() | |
175 | + return path | |
176 | + | |
177 | +def ShowSaveAsProjectDialog(default_filename=None): | |
178 | + dlg = wx.FileDialog(None, | |
179 | + "Save project as...", # title | |
180 | + "", # last used directory | |
181 | + default_filename, | |
182 | + "InVesalius project (*.inv3)|*.inv3", | |
183 | + wx.SAVE|wx.OVERWRITE_PROMPT) | |
184 | + #dlg.SetFilterIndex(0) # default is VTI | |
185 | + | |
186 | + filename = None | |
187 | + if dlg.ShowModal() == wx.ID_OK: | |
188 | + filename = dlg.GetPath() | |
189 | + extension = "inv3" | |
190 | + if sys.platform != 'win32': | |
191 | + if filename.split(".")[-1] != extension: | |
192 | + filename = filename + "." + extension | |
193 | + return filename | ... | ... |
invesalius/gui/frame.py
... | ... | @@ -28,6 +28,7 @@ import wx.lib.pubsub as ps |
28 | 28 | import constants as const |
29 | 29 | import default_tasks as tasks |
30 | 30 | import default_viewers as viewers |
31 | +import gui.dialogs as dlg | |
31 | 32 | import import_panel as imp |
32 | 33 | from project import Project |
33 | 34 | |
... | ... | @@ -82,6 +83,15 @@ class Frame(wx.Frame): |
82 | 83 | ps.Publisher().subscribe(self.SetProjectName, 'Set project name') |
83 | 84 | ps.Publisher().subscribe(self.ShowContentPanel, 'Cancel DICOM load') |
84 | 85 | ps.Publisher().subscribe(self.HideImportPanel, 'Hide import panel') |
86 | + ps.Publisher().subscribe(self.BeginBusyCursor, 'Begin busy cursor') | |
87 | + ps.Publisher().subscribe(self.EndBusyCursor, 'End busy cursor') | |
88 | + | |
89 | + | |
90 | + def EndBusyCursor(self, pubsub_evt=None): | |
91 | + wx.EndBusyCursor() | |
92 | + | |
93 | + def BeginBusyCursor(self, pubsub_evt=None): | |
94 | + wx.BeginBusyCursor() | |
85 | 95 | |
86 | 96 | def SetProjectName(self, pubsub_evt): |
87 | 97 | proj_name = pubsub_evt.data |
... | ... | @@ -377,26 +387,15 @@ class ProjectToolBar(wx.ToolBar): |
377 | 387 | |
378 | 388 | def __init_items(self): |
379 | 389 | |
380 | - | |
381 | - BMP_IMPORT = wx.Bitmap(os.path.join(const.ICON_DIR, "file_import.png"), | |
382 | - wx.BITMAP_TYPE_PNG) | |
383 | - BMP_NET = wx.Bitmap(os.path.join(const.ICON_DIR, | |
384 | - "file_from_internet.png"), | |
385 | - wx.BITMAP_TYPE_PNG) | |
386 | - BMP_SAVE = wx.Bitmap(os.path.join(const.ICON_DIR, "file_save.png"), | |
387 | - wx.BITMAP_TYPE_PNG) | |
388 | - BMP_PRINT = wx.Bitmap(os.path.join(const.ICON_DIR, "print.png"), | |
389 | - wx.BITMAP_TYPE_PNG) | |
390 | - BMP_PHOTO = wx.Bitmap(os.path.join(const.ICON_DIR, "tool_photo.png"), | |
391 | - wx.BITMAP_TYPE_PNG) | |
392 | - | |
393 | 390 | if sys.platform == 'darwin': |
394 | - BMP_IMPORT = wx.Bitmap(os.path.join(const.ICON_DIR, | |
395 | - "file_import_original.png"), | |
396 | - wx.BITMAP_TYPE_PNG) | |
397 | 391 | BMP_NET = wx.Bitmap(os.path.join(const.ICON_DIR, |
398 | 392 | "file_from_internet_original.png"), |
399 | 393 | wx.BITMAP_TYPE_PNG) |
394 | + BMP_IMPORT = wx.Bitmap(os.path.join(const.ICON_DIR, | |
395 | + "file_import_original.png"), | |
396 | + wx.BITMAP_TYPE_PNG) | |
397 | + BMP_OPEN = wx.Bitmap(os.path.join(const.ICON_DIR,"file_open_original.png"), | |
398 | + wx.BITMAP_TYPE_PNG) | |
400 | 399 | BMP_SAVE = wx.Bitmap(os.path.join(const.ICON_DIR, |
401 | 400 | "file_save_original.png"), |
402 | 401 | wx.BITMAP_TYPE_PNG) |
... | ... | @@ -407,25 +406,28 @@ class ProjectToolBar(wx.ToolBar): |
407 | 406 | "tool_photo_original.png"), |
408 | 407 | wx.BITMAP_TYPE_PNG) |
409 | 408 | else: |
410 | - BMP_IMPORT = wx.Bitmap(os.path.join(const.ICON_DIR, | |
411 | - "file_import.png"), | |
412 | - wx.BITMAP_TYPE_PNG) | |
413 | - BMP_NET = wx.Bitmap(os.path.join(const.ICON_DIR, | |
414 | - "file_from_internet.png"), | |
409 | + BMP_NET = wx.Bitmap(os.path.join(const.ICON_DIR,"file_from_internet.png"), | |
415 | 410 | wx.BITMAP_TYPE_PNG) |
411 | + BMP_IMPORT = wx.Bitmap(os.path.join(const.ICON_DIR, "file_import.png"), | |
412 | + wx.BITMAP_TYPE_PNG) | |
413 | + BMP_OPEN = wx.Bitmap(os.path.join(const.ICON_DIR,"file_open.png"), | |
414 | + wx.BITMAP_TYPE_PNG) | |
416 | 415 | BMP_SAVE = wx.Bitmap(os.path.join(const.ICON_DIR, "file_save.png"), |
417 | - wx.BITMAP_TYPE_PNG) | |
416 | + wx.BITMAP_TYPE_PNG) | |
418 | 417 | BMP_PRINT = wx.Bitmap(os.path.join(const.ICON_DIR, "print.png"), |
419 | 418 | wx.BITMAP_TYPE_PNG) |
420 | 419 | BMP_PHOTO = wx.Bitmap(os.path.join(const.ICON_DIR, "tool_photo.png"), |
421 | - wx.BITMAP_TYPE_PNG) | |
420 | + wx.BITMAP_TYPE_PNG) | |
422 | 421 | |
423 | - self.AddLabelTool(const.ID_FILE_IMPORT, | |
424 | - "Import medical image...", | |
425 | - BMP_IMPORT) | |
426 | 422 | self.AddLabelTool(const.ID_FILE_LOAD_INTERNET, |
427 | 423 | "Load medical image...", |
428 | 424 | BMP_NET) |
425 | + self.AddLabelTool(const.ID_FILE_IMPORT, | |
426 | + "Import medical image...", | |
427 | + BMP_IMPORT) | |
428 | + self.AddLabelTool(const.ID_FILE_OPEN, | |
429 | + "Open InVesalius 3 project...", | |
430 | + BMP_OPEN) | |
429 | 431 | self.AddLabelTool(const.ID_FILE_SAVE, |
430 | 432 | "Save InVesalius project", |
431 | 433 | BMP_SAVE) |
... | ... | @@ -440,32 +442,36 @@ class ProjectToolBar(wx.ToolBar): |
440 | 442 | |
441 | 443 | def __bind_events(self): |
442 | 444 | self.Bind(wx.EVT_TOOL, self.OnToolSave, id=const.ID_FILE_SAVE) |
445 | + self.Bind(wx.EVT_TOOL, self.OnToolOpen, id=const.ID_FILE_OPEN) | |
446 | + self.Bind(wx.EVT_TOOL, self.OnToolImport, id=const.ID_FILE_IMPORT) | |
443 | 447 | |
444 | - def OnToolSave(self, evt): | |
445 | - filename = None | |
446 | - project_name = (Project().name).replace(' ','_') | |
447 | - | |
448 | + def OnToolImport(self, event): | |
449 | + dirpath = dlg.ShowImportDirDialog() | |
450 | + if dirpath: | |
451 | + ps.Publisher().sendMessage("Load data to import panel", path) | |
452 | + event.Skip() | |
453 | + | |
454 | + def OnToolOpen(self, event): | |
455 | + filepath = dlg.ShowOpenProjectDialog() | |
456 | + if filepath: | |
457 | + ps.Publisher().sendMessage('Open Project', filepath) | |
458 | + event.Skip() | |
459 | + | |
460 | + def OnToolSave(self, event): | |
461 | + filename = (Project().name).replace(' ','_') | |
448 | 462 | if Project().save_as: |
449 | - dlg = wx.FileDialog(None, | |
450 | - "Save InVesalius project as...", # title | |
451 | - "", # last used directory | |
452 | - project_name, # filename | |
453 | - "InVesalius project (*.inv3)|*.inv3", | |
454 | - wx.SAVE|wx.OVERWRITE_PROMPT) | |
455 | - dlg.SetFilterIndex(0) # default is VTI | |
456 | - | |
457 | - if dlg.ShowModal() == wx.ID_OK: | |
458 | - filename = dlg.GetPath() | |
459 | - print "filename", filename | |
460 | - extension = "inv3" | |
461 | - if sys.platform != 'win32': | |
462 | - if filename.split(".")[-1] != extension: | |
463 | - filename = filename + "."+ extension | |
464 | - | |
465 | - Project().save_as = False | |
466 | - | |
463 | + filename = dlg.ShowSaveAsProjectDialog(filename) | |
464 | + if filename: | |
465 | + Project().save_as = False | |
466 | + else: | |
467 | + return | |
467 | 468 | ps.Publisher().sendMessage('Save Project',filename) |
468 | -# ------------------------------------------------------------------ | |
469 | + event.Skip() | |
470 | + | |
471 | + | |
472 | + | |
473 | + | |
474 | + # ------------------------------------------------------------------ | |
469 | 475 | |
470 | 476 | class ObjectToolBar(wx.ToolBar): |
471 | 477 | def __init__(self, parent): |
... | ... | @@ -593,11 +599,14 @@ class SliceToolBar(wx.ToolBar): |
593 | 599 | BMP_SLICE = wx.Bitmap(os.path.join(const.ICON_DIR, |
594 | 600 | "slice_original.png"), |
595 | 601 | wx.BITMAP_TYPE_PNG) |
602 | + | |
603 | + BMP_CROSS = wx.Bitmap(os.path.join(const.ICON_DIR,"cross_original.png"), | |
604 | + wx.BITMAP_TYPE_PNG) | |
596 | 605 | else: |
597 | 606 | BMP_SLICE = wx.Bitmap(os.path.join(const.ICON_DIR, "slice.png"), |
598 | 607 | wx.BITMAP_TYPE_PNG) |
599 | 608 | |
600 | - BMP_CROSS = wx.Bitmap(os.path.join(const.ICON_DIR, "cross.png"), | |
609 | + BMP_CROSS = wx.Bitmap(os.path.join(const.ICON_DIR, "cross.png"), | |
601 | 610 | wx.BITMAP_TYPE_PNG) |
602 | 611 | |
603 | 612 | self.AddLabelTool(ID_SLICE_SCROLL, "Scroll slice", | ... | ... |
invesalius/gui/task_importer.py
... | ... | @@ -25,15 +25,13 @@ import wx.lib.platebtn as pbtn |
25 | 25 | import wx.lib.pubsub as ps |
26 | 26 | |
27 | 27 | import constants as const |
28 | +import gui.dialogs as dlg | |
28 | 29 | |
29 | 30 | BTN_IMPORT_LOCAL = wx.NewId() |
30 | 31 | BTN_IMPORT_PACS = wx.NewId() |
31 | 32 | BTN_OPEN_PROJECT = wx.NewId() |
32 | 33 | |
33 | -WILDCARD_OPEN = "InVesalius 1 project (*.promed)|*.promed|"\ | |
34 | - "InVesalius 2 project (*.inv)|*.inv|"\ | |
35 | - "InVesalius 3 project (*.inv3)|*.inv3|"\ | |
36 | - "All files (*.*)|*.*" | |
34 | + | |
37 | 35 | |
38 | 36 | class TaskPanel(wx.Panel): |
39 | 37 | def __init__(self, parent): |
... | ... | @@ -93,9 +91,9 @@ class InnerTaskPanel(wx.Panel): |
93 | 91 | # Image(s) for buttons |
94 | 92 | BMP_IMPORT = wx.Bitmap("../icons/file_import.png", wx.BITMAP_TYPE_PNG) |
95 | 93 | BMP_NET = wx.Bitmap("../icons/file_from_internet.png", wx.BITMAP_TYPE_PNG) |
96 | - BMP_NULL = wx.Bitmap("../icons/object_invisible.jpg", wx.BITMAP_TYPE_JPEG) | |
94 | + BMP_OPEN_PROJECT = wx.Bitmap("../icons/file_open.png", wx.BITMAP_TYPE_PNG) | |
97 | 95 | |
98 | - bmp_list = [BMP_IMPORT, BMP_NET, BMP_NULL] | |
96 | + bmp_list = [BMP_IMPORT, BMP_NET, BMP_OPEN_PROJECT] | |
99 | 97 | for bmp in bmp_list: |
100 | 98 | bmp.SetWidth(25) |
101 | 99 | bmp.SetHeight(25) |
... | ... | @@ -103,12 +101,12 @@ class InnerTaskPanel(wx.Panel): |
103 | 101 | # Buttons related to hyperlinks |
104 | 102 | button_style = pbtn.PB_STYLE_SQUARE | pbtn.PB_STYLE_DEFAULT |
105 | 103 | |
106 | - button_import_local = pbtn.PlateButton(self, BTN_IMPORT_LOCAL, "", | |
107 | - BMP_IMPORT, style=button_style) | |
108 | 104 | button_import_pacs = pbtn.PlateButton(self, BTN_IMPORT_PACS, "", BMP_NET, |
109 | 105 | style=button_style) |
106 | + button_import_local = pbtn.PlateButton(self, BTN_IMPORT_LOCAL, "", | |
107 | + BMP_IMPORT, style=button_style) | |
110 | 108 | button_open_proj = pbtn.PlateButton(self, BTN_OPEN_PROJECT, "", |
111 | - BMP_NULL, style=button_style) | |
109 | + BMP_OPEN_PROJECT, style=button_style) | |
112 | 110 | |
113 | 111 | # When using PlaneButton, it is necessary to bind events from parent win |
114 | 112 | self.Bind(wx.EVT_BUTTON, self.OnButton) |
... | ... | @@ -119,10 +117,10 @@ class InnerTaskPanel(wx.Panel): |
119 | 117 | |
120 | 118 | fixed_sizer = wx.FlexGridSizer(rows=3, cols=2, hgap=2, vgap=0) |
121 | 119 | fixed_sizer.AddGrowableCol(0, 1) |
122 | - fixed_sizer.AddMany([ (link_import_local, 1, flag_link, 3), | |
123 | - (button_import_local, 0, flag_button), | |
124 | - (link_import_pacs, 1, flag_link, 3), | |
120 | + fixed_sizer.AddMany([ (link_import_pacs, 1, flag_link, 3), | |
125 | 121 | (button_import_pacs, 0, flag_button), |
122 | + (link_import_local, 1, flag_link, 3), | |
123 | + (button_import_local, 0, flag_button), | |
126 | 124 | (link_open_proj, 1, flag_link, 3), |
127 | 125 | (button_open_proj, 0, flag_button) ]) |
128 | 126 | |
... | ... | @@ -137,76 +135,44 @@ class InnerTaskPanel(wx.Panel): |
137 | 135 | |
138 | 136 | # Test load and unload specific projects' links |
139 | 137 | self.TestLoadProjects() |
140 | - #self.UnloadProjects() | |
141 | - ps.Publisher().subscribe(self.OnLinkImport,("Run menu item", | |
142 | - str(const.ID_FILE_IMPORT))) | |
143 | - | |
144 | - def OnLinkImport(self, evt=None): | |
145 | - dlg = wx.DirDialog(self, "Choose a directory:", "", | |
146 | - style=wx.DD_DEFAULT_STYLE | |
147 | - | wx.DD_DIR_MUST_EXIST | |
148 | - | wx.DD_CHANGE_DIR) | |
149 | - | |
150 | - if dlg.ShowModal() == wx.ID_OK: | |
151 | - path = dlg.GetPath() | |
152 | - ps.Publisher().sendMessage("Load data to import panel", path) | |
153 | - | |
154 | - # Only destroy a dialog after you're done with it. | |
155 | - dlg.Destroy() | |
156 | - | |
157 | - try: | |
158 | - if evt: | |
159 | - evt.Skip() | |
160 | - except(AttributeError): | |
161 | - pass | |
162 | - | |
163 | - def OnLinkImportPACS(self, evt=None): | |
138 | + | |
139 | + def OnLinkImport(self, event): | |
140 | + self.LinkImport() | |
141 | + event.Skip() | |
142 | + | |
143 | + def OnLinkImportPACS(self, event): | |
144 | + self.LinkImportPACS() | |
145 | + event.Skip() | |
146 | + | |
147 | + def OnLinkOpenProject(self, event): | |
148 | + self.LinkOpenProject() | |
149 | + event.Skip() | |
150 | + | |
151 | + def LinkImport(self): | |
152 | + dirpath = dlg.ShowImportDirDialog() | |
153 | + if dirpath: | |
154 | + ps.Publisher().sendMessage("Load data to import panel", dirpath) | |
155 | + | |
156 | + def LinkImportPACS(self): | |
164 | 157 | print "TODO: Send Signal - Import DICOM files from PACS" |
165 | - if evt: | |
166 | - evt.Skip() | |
167 | 158 | |
168 | - def OnLinkOpenProject(self, evt=None, proj_name=""): | |
169 | - if proj_name: | |
170 | - print "TODO: Send Signal - Open project "+ proj_name | |
159 | + def LinkOpenProject(self, filename=None): | |
160 | + if filename: | |
161 | + print "TODO: Send Signal - Open inv3 last project" | |
171 | 162 | else: |
172 | - if sys.platform == 'win32': | |
173 | - path = "" | |
174 | - else: | |
175 | - path = os.getcwd() | |
176 | - dlg = wx.FileDialog(self, message="Open project...", | |
177 | - defaultDir=path, | |
178 | - defaultFile="", wildcard=WILDCARD_OPEN, | |
179 | - style=wx.OPEN|wx.CHANGE_DIR) | |
180 | - if sys.platform != 'darwin': | |
181 | - dlg.SetFilterIndex(2) | |
182 | - else: | |
183 | - dlg.SetFilterIndex(3) | |
184 | - | |
185 | - # Show the dialog and retrieve the user response. If it is the OK response, | |
186 | - # process the data. | |
187 | - if dlg.ShowModal() == wx.ID_OK: | |
188 | - # This returns a Python list of files that were selected. | |
189 | - proj_path = dlg.GetPath() | |
190 | - proj_name = dlg.GetFilename() | |
191 | - ps.Publisher().sendMessage('Open Project', proj_path) | |
192 | - print "TODO: Send Signal - Change frame title "+ proj_name | |
193 | - | |
194 | - # Destroy the dialog. Don't do this until you are done with it! | |
195 | - # BAD things can happen otherwise! | |
196 | - dlg.Destroy() | |
197 | - | |
198 | - if evt: | |
199 | - evt.Skip() | |
163 | + filepath = dlg.ShowOpenProjectDialog() | |
164 | + if filepath: | |
165 | + ps.Publisher().sendMessage('Open Project', filepath) | |
200 | 166 | |
201 | 167 | def OnButton(self, evt): |
202 | 168 | id = evt.GetId() |
203 | 169 | |
204 | 170 | if id == BTN_IMPORT_LOCAL: |
205 | - self.OnLinkImport() | |
171 | + self.LinkImport() | |
206 | 172 | elif id == BTN_IMPORT_PACS: |
207 | - self.OnLinkImportPACS() | |
173 | + self.LinkImportPACS() | |
208 | 174 | else: #elif id == BTN_OPEN_PROJECT: |
209 | - self.OnLinkOpenProject() | |
175 | + self.LinkOpenProject() | |
210 | 176 | |
211 | 177 | def TestLoadProjects(self): |
212 | 178 | self.LoadProject("test1.inv3") |
... | ... | @@ -235,7 +201,7 @@ class InnerTaskPanel(wx.Panel): |
235 | 201 | proj_link.AutoBrowse(False) |
236 | 202 | proj_link.UpdateLink() |
237 | 203 | proj_link.Bind(hl.EVT_HYPERLINK_LEFT, |
238 | - lambda e: self.OnLinkOpenProject(e, proj_name)) | |
204 | + lambda e: self.LinkOpenProject(proj_name)) | |
239 | 205 | |
240 | 206 | # Add to existing frame |
241 | 207 | self.sizer.Add(proj_link, 1, wx.GROW | wx.EXPAND | wx.ALL, 2) | ... | ... |