source: sasview/src/sas/sasgui/perspectives/calculator/data_editor.py @ 8b24bb8

Last change on this file since 8b24bb8 was 5251ec6, checked in by Paul Kienzle <pkienzle@…>, 6 years ago

improved support for py37 in sasgui

  • Property mode set to 100644
File size: 22.0 KB
Line 
1
2import wx
3import sys
4import os
5from copy import deepcopy
6
7from sas.sascalc.dataloader.loader import Loader
8from sas.sascalc.dataloader.data_info import Data2D
9
10from sas.sasgui.guiframe.events import StatusEvent
11
12from .detector_editor import DetectorDialog
13from .collimation_editor import CollimationDialog
14from .console import ConsoleDialog
15
16
17
18_QMIN_DEFAULT = 0.001
19_QMAX_DEFAULT = 0.13
20_NPTS_DEFAULT = 50
21#Control panel width
22if sys.platform.count("darwin") == 0:
23    PANEL_WIDTH = 500
24    PANEL_HEIGTH = 350
25    FONT_VARIANT = 0
26    _BOX_WIDTH = 51
27    ON_MAC = False
28else:
29    _BOX_WIDTH = 76
30    PANEL_WIDTH = 550
31    PANEL_HEIGTH = 400
32    FONT_VARIANT = 1
33    ON_MAC = True
34
35def load_error(error=None):
36    """
37        Pop up an error message.
38
39        @param error: details error message to be displayed
40    """
41    message = "You had to try this, didn't you?\n\n"
42    message += "The data file you selected could not be loaded.\n"
43    message += "Make sure the content of your file is properly formatted.\n\n"
44
45    if error is not None:
46        message += "When contacting the SasView team,"
47        message += " mention the following:\n%s" % str(error)
48
49    dial = wx.MessageDialog(None, message,
50                            'Error Loading File', wx.OK | wx.ICON_EXCLAMATION)
51    dial.ShowModal()
52
53
54class DataEditorPanel(wx.ScrolledWindow):
55    """
56    :param data: when not empty the class can
57        same information into a dat object
58        and post event containing the changed data object to some other frame
59    """
60    def __init__(self, parent, data=[], *args, **kwds):
61        kwds['name'] = "Data Editor"
62        kwds["size"] = (PANEL_WIDTH, PANEL_HEIGTH)
63        wx.ScrolledWindow.__init__(self, parent, *args, **kwds)
64        self.parent = parent
65        self._data = data
66        self._reset_data = deepcopy(data)
67        self.reader = None
68        self._notes = ""
69        self._description = "Edit Data"
70        self._default_save_location = os.getcwd()
71        self._do_layout()
72        self.reset_panel()
73        self.bt_apply.Disable()
74        if data:
75            self.complete_loading(data=data)
76            self.bt_apply.Enable()
77
78    def _define_structure(self):
79        """
80        define initial sizer
81        """
82        self.main_sizer = wx.BoxSizer(wx.VERTICAL)
83        name_box = wx.StaticBox(self, -1, "Load Data")
84        self.name_sizer = wx.StaticBoxSizer(name_box, wx.HORIZONTAL)
85
86        self.title_sizer = wx.BoxSizer(wx.HORIZONTAL)
87        self.run_sizer = wx.BoxSizer(wx.HORIZONTAL)
88        self.instrument_sizer = wx.BoxSizer(wx.HORIZONTAL)
89
90        edit_box = wx.StaticBox(self, -1, "Edit ")
91        self.edit_sizer = wx.StaticBoxSizer(edit_box, wx.HORIZONTAL)
92
93        self.button_sizer = wx.BoxSizer(wx.HORIZONTAL)
94
95    def _layout_name(self):
96        """
97        Do the layout for data name related widgets
98        """
99        #data name [string]
100        data_name_txt = wx.StaticText(self, -1, 'Data : ')
101        self.data_cbox = wx.ComboBox(self, -1, style=wx.CB_READONLY)
102        wx.EVT_COMBOBOX(self.data_cbox, -1, self.on_select_data)
103        hint_data = "Loaded data."
104        self.data_cbox.SetToolTipString(hint_data)
105        id = wx.NewId()
106        self.browse_button = wx.Button(self, id, "Browse")
107        hint_on_browse = "Click on this button to import data in this panel."
108        self.browse_button.SetToolTipString(hint_on_browse)
109        self.Bind(wx.EVT_BUTTON, self.on_click_browse, id=id)
110        self.name_sizer.AddMany([(data_name_txt, 0, wx.LEFT, 15),
111                                       (self.data_cbox, 0, wx.LEFT, 10),
112                                       (self.browse_button, 0, wx.LEFT, 10)])
113
114    def _layout_title(self):
115        """
116        Do the layout for data title related widgets
117        """
118        #title name [string]
119        data_title_txt = wx.StaticText(self, -1, 'Title : ')
120        self.data_title_tcl = wx.TextCtrl(self, -1, size=(PANEL_WIDTH * 3 / 5, -1))
121        self.data_title_tcl.Bind(wx.EVT_TEXT_ENTER, self.on_change_title)
122        hint_title = "Data's title."
123        self.data_title_tcl.SetToolTipString(hint_title)
124        self.title_sizer.AddMany([(data_title_txt, 0, wx.LEFT, 15),
125                                       (self.data_title_tcl, 0, wx.LEFT, 10)])
126
127    def _layout_run(self):
128        """
129        Do the layout for data run related widgets
130        """
131        data_run_txt = wx.StaticText(self, -1, 'Run : ')
132        data_run_txt.SetToolTipString('')
133        self.data_run_tcl = wx.TextCtrl(self, -1, size=(PANEL_WIDTH * 3 / 5, -1),
134                                         style=wx.TE_MULTILINE)
135        hint_run = "Data's run."
136        self.data_run_tcl.SetToolTipString(hint_run)
137        self.run_sizer.AddMany([(data_run_txt, 0, wx.LEFT, 15),
138                                       (self.data_run_tcl, 0, wx.LEFT, 10)])
139
140    def _layout_instrument(self):
141        """
142        Do the layout for instrument related widgets
143        """
144        instrument_txt = wx.StaticText(self, -1, 'Instrument : ')
145        hint_instrument_txt = ''
146        instrument_txt.SetToolTipString(hint_instrument_txt)
147        self.instrument_tcl = wx.TextCtrl(self, -1, size=(_BOX_WIDTH * 5, 20))
148        hint_instrument = "Instrument."
149        self.instrument_tcl.SetToolTipString(hint_instrument)
150        self.instrument_sizer.AddMany([(instrument_txt, 0, wx.LEFT, 15),
151                                (self.instrument_tcl, 0, wx.LEFT, 10)])
152
153    def _layout_editor(self):
154        """
155        Do the layout for sample related widgets
156        """
157        self.detector_rb = wx.RadioButton(self, -1, "Detector",
158                                           style=wx.RB_GROUP)
159        self.sample_rb = wx.RadioButton(self, -1, "Sample")
160        self.source_rb = wx.RadioButton(self, -1, "Source")
161        self.collimation_rb = wx.RadioButton(self, -1, "Collimation")
162
163        self.bt_edit = wx.Button(self, -1, "Edit")
164        self.bt_edit.SetToolTipString("Edit data.")
165        self.bt_edit.Bind(wx.EVT_BUTTON, self.on_edit)
166        self.edit_sizer.AddMany([(self.detector_rb, 0, wx.ALL, 10),
167                        (self.sample_rb, 0, wx.RIGHT | wx.BOTTOM | wx.TOP, 10),
168                        (self.source_rb, 0, wx.RIGHT | wx.BOTTOM | wx.TOP, 10),
169                    (self.collimation_rb, 0, wx.RIGHT | wx.BOTTOM | wx.TOP, 10),
170                    (self.bt_edit, 0,
171                                  wx.RIGHT | wx.BOTTOM | wx.TOP, 10)])
172        self.reset_radiobox()
173
174
175    def _layout_source(self):
176        """
177            Do the layout for source related widgets
178        """
179        source_txt = wx.StaticText(self, -1, 'Source ')
180        hint_source_txt = ''
181        source_txt.SetToolTipString(hint_source_txt)
182        self.bt_edit_source = wx.Button(self, -1, "Edit")
183        self.bt_edit_source.SetToolTipString("Edit data's sample.")
184        self.bt_edit_source.Bind(wx.EVT_BUTTON, self.edit_source)
185        #self.source_sizer.AddMany([(source_txt, 0, wx.ALL, 10),
186        #                        (self.bt_edit_source, 0,
187        #                          wx.RIGHT|wx.BOTTOM|wx.TOP, 10)])
188
189    def _layout_summary(self):
190        """
191            Layout widgets related to data's summary
192        """
193        self.data_summary = wx.TextCtrl(self, -1,
194                                         style=wx.TE_MULTILINE | wx.HSCROLL,
195                                        size=(-1, 200))
196        summary = 'No data info available...'
197        self.data_summary.SetValue(summary)
198        #self.summary_sizer.Add(self.data_summary, 1, wx.EXPAND|wx.ALL, 10) 
199
200    def _layout_button(self):
201        """
202            Do the layout for the button widgets
203        """
204        self.bt_summary = wx.Button(self, -1, "View", size=(_BOX_WIDTH, -1))
205        self.bt_summary.SetToolTipString("View final changes on data.")
206        self.bt_summary.Bind(wx.EVT_BUTTON, self.on_click_view)
207
208        self.bt_save = wx.Button(self, -1, "Save As", size=(_BOX_WIDTH, -1))
209        self.bt_save.SetToolTipString("Save changes in a file.")
210        self.bt_save.Bind(wx.EVT_BUTTON, self.on_click_save)
211
212        self.bt_apply = wx.Button(self, -1, "Apply", size=(_BOX_WIDTH, -1))
213        self.bt_apply.SetToolTipString("Save changes into the imported data.")
214        self.bt_apply.Bind(wx.EVT_BUTTON, self.on_click_apply)
215
216        self.bt_reset = wx.Button(self, -1, 'Reset', size=(_BOX_WIDTH, -1))
217        self.bt_reset.Bind(wx.EVT_BUTTON, self.on_click_reset)
218        self.bt_reset.SetToolTipString("Reset data to its initial state.")
219
220        self.bt_close = wx.Button(self, -1, 'Close', size=(_BOX_WIDTH, -1))
221        self.bt_close.Bind(wx.EVT_BUTTON, self.on_close)
222        self.bt_close.SetToolTipString("Close this panel.")
223
224        self.button_sizer.AddMany([(self.bt_save, 0, wx.LEFT, 120),
225                                   (self.bt_apply, 0, wx.LEFT, 10),
226                                   (self.bt_reset, 0, wx.LEFT | wx.RIGHT, 10),
227                                   (self.bt_summary, 0, wx.RIGHT, 10),
228                                   (self.bt_close, 0, wx.RIGHT, 10)])
229
230    def _do_layout(self):
231        """
232        Draw the current panel
233        """
234        self._define_structure()
235        self._layout_name()
236        self._layout_title()
237        self._layout_run()
238        self._layout_editor()
239        self._layout_button()
240        self.main_sizer.AddMany([(self.name_sizer, 0, wx.EXPAND | wx.ALL, 10),
241                                (self.title_sizer, 0,
242                                         wx.EXPAND | wx.TOP | wx.BOTTOM, 5),
243                                (self.run_sizer, 0,
244                                         wx.EXPAND | wx.TOP | wx.BOTTOM, 5),
245                                (self.instrument_sizer, 0,
246                                         wx.EXPAND | wx.TOP | wx.BOTTOM, 5),
247                                (self.edit_sizer, 0,
248                                        wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 10),
249                                (self.button_sizer, 0,
250                                          wx.EXPAND | wx.TOP | wx.BOTTOM, 5)])
251        self.SetSizer(self.main_sizer)
252        self.SetScrollbars(20, 20, 25, 65)
253        self.SetAutoLayout(True)
254
255    def fill_data_combox(self):
256        """
257        fill the current combobox with the available data
258        """
259        if not self._data:
260            return
261        self.data_cbox.Clear()
262        for data in self._data:
263            name = data.title
264            if data.run:
265                name = data.run[0]
266            if name.lstrip().rstrip() == "":
267                name = data.filename
268            pos = self.data_cbox.Append(str(name))
269            self.data_cbox.SetClientData(pos, data)
270            self.data_cbox.SetSelection(pos)
271            self.data_cbox.SetStringSelection(str(name))
272
273    def reset_panel(self):
274        """
275        """
276        self.enable_data_cbox()
277        self.data_title_tcl.SetValue("")
278        self.data_run_tcl.SetValue("")
279
280    def on_select_data(self, event=None):
281        """
282        """
283        data, _, _ = self.get_current_data()
284        self.reset_panel()
285        if data is None:
286            return
287        self.data_title_tcl.SetValue(str(data.title))
288        text = ""
289        if data.run:
290            for item in data.run:
291                text += item + "\n"
292        self.data_run_tcl.SetValue(str(text))
293
294    def get_current_data(self):
295        """
296        """
297        position = self.data_cbox.GetSelection()
298        if position == wx.NOT_FOUND:
299            return None, None, None
300        data_name = self.data_cbox.GetStringSelection()
301        data = self.data_cbox.GetClientData(position)
302        return data, data_name, position
303
304    def enable_data_cbox(self):
305        """
306        """
307        if self._data:
308            self.data_cbox.Enable()
309            self.bt_summary.Enable()
310            self.bt_reset.Enable()
311            self.bt_save.Enable()
312            self.bt_edit.Enable()
313        else:
314            self.data_cbox.Disable()
315            self.bt_summary.Disable()
316            self.bt_reset.Disable()
317            self.bt_save.Disable()
318            self.bt_edit.Disable()
319
320    def reset_radiobox(self):
321        """
322        """
323        self.detector_rb.SetValue(True)
324        self.source_rb.SetValue(False)
325        self.sample_rb.SetValue(False)
326        self.collimation_rb.SetValue(False)
327
328    def set_sample(self, sample, notes=None):
329        """
330        set sample for data
331        """
332        data, _, _ = self.get_current_data()
333        if data is None:
334            return
335        data.sample = sample
336        if notes is not None:
337            data.process.append(notes)
338
339    def set_source(self, source, notes=None):
340        """
341        set source for data
342        """
343        data, data_name, position = self.get_current_data()
344        if data is None:
345            return
346        data.source = source
347        if notes is not None:
348            data.process.append(notes)
349
350    def set_detector(self, detector, notes=None):
351        """
352        set detector for data
353        """
354        data, data_name, position = self.get_current_data()
355        if data is None:
356            return
357        data.detector = detector
358        if notes is not None:
359            data.process.append(notes)
360
361    def set_collimation(self, collimation, notes=None):
362        """
363        set collimation for data
364        """
365        data, data_name, position = self.get_current_data()
366        if data is None:
367            return
368        data.collimation = collimation
369        if notes is not None:
370            data.process.append(notes)
371
372    def edit_collimation(self):
373        """
374        Edit the selected collimation
375        """
376        data, data_name, position = self.get_current_data()
377        if data is None:
378            return
379        dlg = CollimationDialog(collimation=data.collimation)
380        dlg.set_manager(self)
381        dlg.ShowModal()
382
383    def edit_detector(self):
384        """
385        Edit the selected detector
386        """
387        data, data_name, position = self.get_current_data()
388        if data is None:
389            return
390        dlg = DetectorDialog(detector=data.detector)
391        dlg.set_manager(self)
392        dlg.ShowModal()
393
394    def edit_sample(self):
395        """
396        Open the dialog to edit the sample of the current data
397        """
398        data, _, _ = self.get_current_data()
399        if data is None:
400            return
401        from .sample_editor import SampleDialog
402        dlg = SampleDialog(parent=self, sample=data.sample)
403        dlg.set_manager(self)
404        dlg.ShowModal()
405
406    def edit_source(self):
407        """
408        Open the dialog to edit the saource of the current data
409        """
410        data, data_name, position = self.get_current_data()
411        if data is None:
412            return
413        from .source_editor import SourceDialog
414        dlg = SourceDialog(parent=self, source=data.source)
415        dlg.set_manager(self)
416        dlg.ShowModal()
417
418    def choose_data_file(self, location=None):
419        """
420        Open a file dialog to allow loading a file
421        """
422        path = None
423        if location is None:
424            location = os.getcwd()
425
426        l = Loader()
427        cards = l.get_wildcards()
428        wlist = '|'.join(cards)
429
430        dlg = wx.FileDialog(self, "Choose a file", location, "", wlist, wx.FD_OPEN)
431        if dlg.ShowModal() == wx.ID_OK:
432            path = dlg.GetPath()
433            mypath = os.path.basename(path)
434        dlg.Destroy()
435        return path
436
437
438    def complete_loading(self, data=None, filename=''):
439        """
440        Complete the loading and compute the slit size
441        """
442        self.done = True
443        self._data = []
444        if data is None:
445            msg = "Couldn't load data"
446            wx.PostEvent(self.parent.parent, StatusEvent(status=msg,
447                                             info="warning", type='stop'))
448            return
449        if not  data.__class__.__name__ == "list":
450            self._data.append(data)
451            self._reset_data.append(deepcopy(data))
452        else:
453            self._data = deepcopy(data)
454            self._reset_data = deepcopy(data)
455        self.set_values()
456        if self.parent.parent is None:
457            return
458        msg = "Load Complete"
459        wx.PostEvent(self.parent.parent, StatusEvent(status=msg,
460                                                info="info", type='stop'))
461
462    def set_values(self):
463        """
464        take the aperture values of the current data and display them
465        through the panel
466        """
467        if self._data:
468            self.fill_data_combox()
469            self.on_select_data(event=None)
470
471    def get_data(self):
472        """
473        return the current data
474        """
475        return self._data
476
477    def get_notes(self):
478        """
479        return notes
480        """
481        return self._notes
482
483    def on_change_run(self, event=None):
484        """
485        Change run
486        """
487        run = []
488        data, _, _ = self.get_current_data()
489        for i in range(self.data_run_tcl.GetNumberOfLines()):
490            text = self.data_run_tcl.GetLineText(i).lstrip().rstrip()
491            if text != "":
492                run.append(text)
493        if data.run != run:
494            self._notes += "Change data 's "
495            self._notes += "run from %s to %s \n" % (data.run, str(run))
496            data.run = run
497        if event is not None:
498            event.Skip()
499
500    def on_change_title(self, event=None):
501        """
502        Change title
503        """
504        data, _, _ = self.get_current_data()
505        #Change data's name
506        title = self.data_title_tcl.GetValue().lstrip().rstrip()
507
508        if data.title != title:
509            self._notes += "Change data 's "
510            self._notes += "title from %s to %s \n" % (data.title, str(title))
511            data.title = title
512        if event is not None:
513            event.Skip()
514
515    def on_click_browse(self, event):
516        """
517        Open a file dialog to allow the user to select a given file.
518        Display the loaded data if available.
519        """
520        path = self.choose_data_file(location=self._default_save_location)
521        if path is None:
522            return
523        if self.parent.parent is not None:
524            wx.PostEvent(self.parent.parent, StatusEvent(status="Loading...",
525                                        info="info", type="progress"))
526
527        self.done = False
528        self._default_save_location = path
529        try:
530            #Load data
531            from .load_thread import DataReader
532            ## If a thread is already started, stop it
533            if self.reader is not None and self.reader.isrunning():
534                self.reader.stop()
535            self.reader = DataReader(path=path,
536                                    completefn=self.complete_loading,
537                                    updatefn=None)
538            self.reader.queue()
539        except Exception as exc:
540            msg = "Data Editor: %s" % exc
541            load_error(msg)
542            return
543        event.Skip()
544
545    def on_edit(self, event):
546        """
547        """
548        if self.detector_rb.GetValue():
549            self.edit_detector()
550        if self.sample_rb.GetValue():
551            self.edit_sample()
552        if self.source_rb.GetValue():
553            self.edit_source()
554        if self.collimation_rb.GetValue():
555            self.edit_collimation()
556        event.Skip()
557
558    def on_click_apply(self, event):
559        """
560        changes are saved in data object imported to edit
561        """
562        data, _, _ = self.get_current_data()
563        if data is None:
564            return
565        self.on_change_run(event=None)
566        self.on_change_title(event=None)
567        #must post event here
568        event.Skip()
569
570    def on_click_save(self, event):
571        """
572        Save change into a file
573        """
574        if not self._data:
575            return
576        self.on_change_run(event=None)
577        self.on_change_title(event=None)
578        path = None
579        wildcard = "CanSAS 1D files(*.xml)|*.xml"
580        dlg = wx.FileDialog(self, "Choose a file",
581                            self._default_save_location, "", wildcard , wx.SAVE)
582
583        for data in self._data:
584            if issubclass(data.__class__, Data2D):
585                msg = "No conventional writing format for \n\n"
586                msg += "Data2D at this time.\n"
587                dlg = wx.MessageDialog(None, msg, 'Error Loading File',
588                                             wx.OK | wx.ICON_EXCLAMATION)
589                dlg.ShowModal()
590            else:
591                if dlg.ShowModal() == wx.ID_OK:
592                    path = dlg.GetPath()
593                    mypath = os.path.basename(path)
594                    loader = Loader()
595                    format = ".xml"
596                    if os.path.splitext(mypath)[1].lower() == format:
597                        loader.save(path, data, format)
598                    try:
599                        self._default_save_location = os.path.dirname(path)
600                    except:
601                        pass
602                    dlg.Destroy()
603        event.Skip()
604
605    def on_click_view(self, event):
606        """
607        Display data info
608        """
609        data, data_name, position = self.get_current_data()
610        if data is None:
611            return
612        self.on_change_run(event=None)
613        self.on_change_title(event=None)
614        dlg = ConsoleDialog(data=data)
615        dlg.ShowModal()
616        event.Skip()
617
618    def on_click_reset(self, event):
619        """
620        """
621        data, data_name, position = self.get_current_data()
622        if data is None:
623            return
624        self._data[position] = deepcopy(self._reset_data[position])
625        self.set_values()
626        event.Skip()
627
628    def on_close(self, event):
629        """
630        leave data as it is and close
631        """
632        self.parent.Close()
633        event.Skip()
634
635class DataEditorWindow(wx.Frame):
636    def __init__(self, parent, manager, data=None, *args, **kwds):
637        kwds["size"] = (PANEL_WIDTH, PANEL_HEIGTH)
638        wx.Frame.__init__(self, parent, *args, **kwds)
639        self.parent = parent
640        self.manager = manager
641        self.panel = DataEditorPanel(parent=self, data=data)
642        self.Show()
643
644    def get_data(self):
645        """
646            return the current data
647        """
648        return self.panel.get_data()
649
650if __name__ == "__main__":
651
652    app = wx.App()
653    window = DataEditorWindow(parent=None, data=[], title="Data Editor")
654    app.MainLoop()
Note: See TracBrowser for help on using the repository browser.