source: sasview/src/sas/sasgui/perspectives/calculator/model_editor.py @ a5e1b6ca

ticket-1249
Last change on this file since a5e1b6ca was 82d88d5, checked in by Paul Kienzle <pkienzle@…>, 6 years ago

Merge branch 'master' into py37-sasgui

  • Property mode set to 100644
File size: 41.2 KB
Line 
1'''
2This module provides three model editor classes: the composite model editor,
3the easy editor which provides a simple interface with tooltip help to enter
4the parameters of the model and their default value and a panel to input a
5function of y (usually the intensity).  It also provides a drop down of
6standard available math functions.  Finally a full python editor panel for
7complete customization is provided.
8
9:TODO the writing of the file and name checking (and maybe some other
10functions?) should be moved to a computational module which could be called
11from a python script.  Basically one just needs to pass the name,
12description text and function text (or in the case of the composite editor
13the names of the first and second model and the operator to be used).
14'''
15
16################################################################################
17#This software was developed by the University of Tennessee as part of the
18#Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
19#project funded by the US National Science Foundation.
20#
21#See the license text in license.txt
22#
23#copyright 2009, University of Tennessee
24################################################################################
25from __future__ import print_function
26
27import wx
28import sys
29import os
30import math
31import re
32import logging
33import datetime
34
35from wx.py.editwindow import EditWindow
36
37from sas.sasgui.guiframe.documentation_window import DocumentationWindow
38
39from .pyconsole import show_model_output, check_model
40
41logger = logging.getLogger(__name__)
42
43if sys.platform.count("win32") > 0:
44    FONT_VARIANT = 0
45    PNL_WIDTH = 500
46    PNL_HEIGHT = 320
47else:
48    FONT_VARIANT = 1
49    PNL_WIDTH = 500
50    PNL_HEIGHT = 350
51M_NAME = 'Model'
52EDITOR_WIDTH = 800
53EDITOR_HEIGTH = 735
54PANEL_WIDTH = 500
55_BOX_WIDTH = 55
56
57def _delete_file(path):
58    """
59    Delete file in the path
60    """
61    try:
62        os.remove(path)
63    except:
64        raise
65
66
67class TextDialog(wx.Dialog):
68    """
69    Dialog for easy custom composite models.  Provides a wx.Dialog panel
70    to choose two existing models (including pre-existing Plugin Models which
71    may themselves be composite models) as well as an operation on those models
72    (add or multiply) the resulting model will add a scale parameter for summed
73    models and a background parameter for a multiplied model.
74
75    The user also gives a brief help for the model in a description box and
76    must provide a unique name which is verified as unique before the new
77    model is saved.
78
79    This Dialog pops up for the user when they press 'Sum|Multi(p1,p2)' under
80    'Plugin Model Operations' under 'Fitting' menu.  This is currently called as
81    a Modal Dialog.
82
83    :TODO the built in compiler currently balks at when it tries to import
84    a model whose name contains spaces or symbols (such as + ... underscore
85    should be fine).  Have fixed so the editor cannot save such a file name
86    but if a file is dropped in the plugin directory from outside this class
87    will create a file that cannot be compiled.  Should add the check to
88    the write method or to the on_modelx method.
89
90    - PDB:April 5, 2015
91    """
92    def __init__(self, parent=None, base=None, id=None, title='',
93                 model_list=[], plugin_dir=None):
94        """
95        This class is run when instatiated.  The __init__ initializes and
96        calls the internal methods necessary.  On exiting the wx.Dialog
97        window should be destroyed.
98        """
99        wx.Dialog.__init__(self, parent=parent, id=id,
100                           title=title, size=(PNL_WIDTH, PNL_HEIGHT))
101        self.parent = base
102        #Font
103        self.SetWindowVariant(variant=FONT_VARIANT)
104        # default
105        self.overwrite_name = False
106        self.plugin_dir = plugin_dir
107        self.model_list = model_list
108        self.model1_string = "sphere"
109        self.model2_string = "cylinder"
110        self.name = 'Sum' + M_NAME
111        self._notes = ''
112        self._operator = '+'
113        self._operator_choice = None
114        self.explanation = ''
115        self.explanationctr = None
116        self.type = None
117        self.name_sizer = None
118        self.name_tcl = None
119        self.desc_sizer = None
120        self.desc_tcl = None
121        self._selection_box = None
122        self.model1 = None
123        self.model2 = None
124        self.static_line_1 = None
125        self.ok_button = None
126        self.close_button = None
127        self._msg_box = None
128        self.msg_sizer = None
129        self.fname = None
130        self.cm_list = None
131        self.is_p1_custom = False
132        self.is_p2_custom = False
133        self._build_sizer()
134        self.model1_name = str(self.model1.GetValue())
135        self.model2_name = str(self.model2.GetValue())
136        self.good_name = True
137        self.fill_operator_combox()
138
139    def _layout_name(self):
140        """
141        Do the layout for file/function name related widgets
142        """
143        #container for new model name input
144        self.name_sizer = wx.BoxSizer(wx.HORIZONTAL)
145
146        #set up label and input box with tool tip and event handling
147        name_txt = wx.StaticText(self, -1, 'Function Name : ')
148        self.name_tcl = wx.TextCtrl(self, -1, value='MySumFunction')
149        self.name_tcl.Bind(wx.EVT_TEXT_ENTER, self.on_change_name)
150        hint_name = "Unique Sum/Multiply Model Function Name."
151        self.name_tcl.SetToolTipString(hint_name)
152
153        self.name_sizer.AddMany([(name_txt, 0, wx.LEFT | wx.TOP, 10),
154                                 (self.name_tcl, -1,
155                                  wx.EXPAND | wx.RIGHT | wx.TOP | wx.BOTTOM,
156                                  10)])
157
158
159    def _layout_description(self):
160        """
161        Do the layout for description related widgets
162        """
163        #container for new model description input
164        self.desc_sizer = wx.BoxSizer(wx.HORIZONTAL)
165
166        #set up description label and input box with tool tip and event handling
167        desc_txt = wx.StaticText(self, -1, 'Description (optional) : ')
168        self.desc_tcl = wx.TextCtrl(self, -1)
169        hint_desc = "Write a short description of this model function."
170        self.desc_tcl.SetToolTipString(hint_desc)
171
172        self.desc_sizer.AddMany([(desc_txt, 0, wx.LEFT | wx.TOP, 10),
173                                 (self.desc_tcl, -1,
174                                  wx.EXPAND | wx.RIGHT | wx.TOP | wx.BOTTOM,
175                                  10)])
176
177
178    def _layout_model_selection(self):
179        """
180        Do the layout for model selection related widgets
181        """
182        box_width = 195 # combobox width
183
184        #First set up main sizer for the selection
185        selection_box_title = wx.StaticBox(self, -1, 'Select',
186                                           size=(PNL_WIDTH - 30, 70))
187        self._selection_box = wx.StaticBoxSizer(selection_box_title,
188                                                wx.VERTICAL)
189
190        #Next create the help labels for the model selection
191        select_help_box = wx.BoxSizer(wx.HORIZONTAL)
192        model_string = " Model%s (p%s):"
193        select_help_box.Add(wx.StaticText(self, -1, model_string % (1, 1)),
194                            0, 0)
195        select_help_box.Add((box_width - 25, 10), 0, 0)
196        select_help_box.Add(wx.StaticText(self, -1, model_string % (2, 2)),
197                            0, 0)
198        self._selection_box.Add(select_help_box, 0, 0)
199
200        #Next create the actual selection box with 3 combo boxes
201        selection_box_choose = wx.BoxSizer(wx.HORIZONTAL)
202
203        self.model1 = wx.ComboBox(self, -1, style=wx.CB_READONLY)
204        wx.EVT_COMBOBOX(self.model1, -1, self.on_model1)
205        self.model1.SetMinSize((box_width * 5 / 6, -1))
206        self.model1.SetToolTipString("model1")
207
208        self._operator_choice = wx.ComboBox(self, -1, size=(50, -1),
209                                            style=wx.CB_READONLY)
210        wx.EVT_COMBOBOX(self._operator_choice, -1, self.on_select_operator)
211        operation_tip = "Add: +, Multiply: * "
212        self._operator_choice.SetToolTipString(operation_tip)
213
214        self.model2 = wx.ComboBox(self, -1, style=wx.CB_READONLY)
215        wx.EVT_COMBOBOX(self.model2, -1, self.on_model2)
216        self.model2.SetMinSize((box_width * 5 / 6, -1))
217        self.model2.SetToolTipString("model2")
218        self._set_model_list()
219
220        selection_box_choose.Add(self.model1, 0, 0)
221        selection_box_choose.Add((15, 10))
222        selection_box_choose.Add(self._operator_choice, 0, 0)
223        selection_box_choose.Add((15, 10))
224        selection_box_choose.Add(self.model2, 0, 0)
225        # add some space between labels and selection
226        self._selection_box.Add((20, 5), 0, 0)
227        self._selection_box.Add(selection_box_choose, 0, 0)
228
229    def _build_sizer(self):
230        """
231        Build GUI with calls to _layout_name, _layout Description
232        and _layout_model_selection which each build a their portion of the
233        GUI.
234        """
235        mainsizer = wx.BoxSizer(wx.VERTICAL) # create main sizer for dialog
236
237        # build fromm top by calling _layout_name and _layout_description
238        # and adding to main sizer
239        self._layout_name()
240        mainsizer.Add(self.name_sizer, 0, wx.EXPAND)
241        self._layout_description()
242        mainsizer.Add(self.desc_sizer, 0, wx.EXPAND)
243
244        # Add an explanation of dialog (short help)
245        self.explanationctr = wx.StaticText(self, -1, self.explanation)
246        self.fill_explanation_helpstring(self._operator)
247        mainsizer.Add(self.explanationctr, 0, wx.LEFT | wx.EXPAND, 15)
248
249        # Add the selection box stuff with border and labels built
250        # by _layout_model_selection
251        self._layout_model_selection()
252        mainsizer.Add(self._selection_box, 0, wx.LEFT, 15)
253
254        # Add a space and horizontal line before the notification
255        #messages and the buttons at the bottom
256        mainsizer.Add((10, 10))
257        self.static_line_1 = wx.StaticLine(self, -1)
258        mainsizer.Add(self.static_line_1, 0, wx.EXPAND, 10)
259
260        # Add action status notification line (null at startup)
261        self._msg_box = wx.StaticText(self, -1, self._notes)
262        self.msg_sizer = wx.BoxSizer(wx.HORIZONTAL)
263        self.msg_sizer.Add(self._msg_box, 0, wx.LEFT, 0)
264        mainsizer.Add(self.msg_sizer, 0,
265                      wx.LEFT | wx.RIGHT | wx.ADJUST_MINSIZE | wx.BOTTOM, 10)
266
267        # Finally add the buttons (apply and close) on the bottom
268        # Eventually need to add help here
269        self.ok_button = wx.Button(self, wx.ID_OK, 'Apply')
270        _app_tip = "Save the new Model."
271        self.ok_button.SetToolTipString(_app_tip)
272        self.ok_button.Bind(wx.EVT_BUTTON, self.check_name)
273        self.help_button = wx.Button(self, -1, 'HELP')
274        _app_tip = "Help on composite model creation."
275        self.help_button.SetToolTipString(_app_tip)
276        self.help_button.Bind(wx.EVT_BUTTON, self.on_help)
277        self.close_button = wx.Button(self, wx.ID_CANCEL, 'Close')
278        sizer_button = wx.BoxSizer(wx.HORIZONTAL)
279        sizer_button.AddMany([((20, 20), 1, 0),
280                              (self.ok_button, 0, 0),
281                              (self.help_button, 0, 0),
282                              (self.close_button, 0, wx.LEFT | wx.RIGHT, 10)])
283        mainsizer.Add(sizer_button, 0, wx.EXPAND | wx.BOTTOM | wx.TOP, 10)
284
285        self.SetSizer(mainsizer)
286        self.Centre()
287
288    def on_change_name(self, event=None):
289        """
290        Change name
291        """
292        if event is not None:
293            event.Skip()
294        self.name_tcl.SetBackgroundColour('white')
295        self.Refresh()
296
297    def check_name(self, event=None):
298        """
299        Check that proposed new model name is a valid Python module name
300        and that it does not already exist. If not show error message and
301        pink background in text box else call on_apply
302
303        :TODO this should be separated out from the GUI code.  For that we
304        need to pass it the name (or if we want to keep the default name
305        option also need to pass the self._operator attribute) We just need
306        the function to return an error code that the name is good or if
307        not why (not a valid name, name exists already).  The rest of the
308        error handling should be done in this module. so on_apply would then
309        start by checking the name and then either raise errors or do the
310        deed.
311        """
312        #Get the function/file name
313        mname = M_NAME
314        self.on_change_name(None)
315        title = self.name_tcl.GetValue().lstrip().rstrip()
316        if title == '':
317            text = self._operator
318            if text.count('+') > 0:
319                mname = 'Sum'
320            else:
321                mname = 'Multi'
322            mname += M_NAME
323            title = mname
324        self.name = title
325        t_fname = title + '.py'
326
327        #First check if the name is a valid Python name
328        if re.match('^[A-Za-z0-9_]*$', title):
329            self.good_name = True
330        else:
331            self.good_name = False
332            msg = ("%s is not a valid Python name. Only alphanumeric \n" \
333                   "and underscore allowed" % self.name)
334
335        #Now check if the name already exists
336        if not self.overwrite_name and self.good_name:
337            #Create list of existing model names for comparison
338            list_fnames = os.listdir(self.plugin_dir)
339            # fake existing regular model name list
340            m_list = [model + ".py" for model in self.model_list]
341            list_fnames.append(m_list)
342            if t_fname in list_fnames and title != mname:
343                self.good_name = False
344                msg = "Name exists already."
345
346        if not self.good_name:
347            self.name_tcl.SetBackgroundColour('pink')
348            info = 'Error'
349            wx.MessageBox(msg, info)
350            self._notes = msg
351            color = 'red'
352            self._msg_box.SetLabel(msg)
353            self._msg_box.SetForegroundColour(color)
354            return self.good_name
355        self.fname = os.path.join(self.plugin_dir, t_fname)
356        s_title = title
357        if len(title) > 20:
358            s_title = title[0:19] + '...'
359        self._notes = "Model function (%s) has been set! \n" % str(s_title)
360        self.good_name = True
361        self.on_apply(self.fname)
362        return self.good_name
363
364    def on_apply(self, path):
365        """
366        This method is a misnomer - it is not bound to the apply button
367        event.  Instead the apply button event goes to check_name which
368        then calls this method if the name of the new file is acceptable.
369
370        :TODO this should be bound to the apply button.  The first line
371        should call the check_name method which itself should be in another
372        module separated from the the GUI modules.
373        """
374        self.name_tcl.SetBackgroundColour('white')
375        try:
376            label = self.get_textnames()
377            fname = path
378            name1 = label[0]
379            name2 = label[1]
380            self.write_string(fname, name1, name2)
381            success = show_model_output(self, fname)
382            if success:
383                self.parent.update_custom_combo()
384            msg = self._notes
385            info = 'Info'
386            color = 'blue'
387        except:
388            msg = "Easy Sum/Multipy Plugin: Error occurred..."
389            info = 'Error'
390            color = 'red'
391        self._msg_box.SetLabel(msg)
392        self._msg_box.SetForegroundColour(color)
393        self._set_model_list()
394        if self.parent.parent is not None:
395            from sas.sasgui.guiframe.events import StatusEvent
396            wx.PostEvent(self.parent.parent, StatusEvent(status=msg,
397                                                         info=info))
398
399    def on_help(self, event):
400        """
401        Bring up the Composite Model Editor Documentation whenever
402        the HELP button is clicked.
403
404        Calls DocumentationWindow with the path of the location within the
405        documentation tree (after /doc/ ....".  Note that when using old
406        versions of Wx (before 2.9) and thus not the release version of
407        installers, the help comes up at the top level of the file as
408        webbrowser does not pass anything past the # to the browser when it is
409        running "file:///...."
410
411    :param evt: Triggers on clicking the help button
412    """
413
414        _TreeLocation = "user/sasgui/perspectives/fitting/fitting_help.html"
415        _PageAnchor = "#sum-multi-p1-p2"
416        _doc_viewer = DocumentationWindow(self, -1, _TreeLocation, _PageAnchor,
417                                          "Composite Model Editor Help")
418
419    def _set_model_list(self):
420        """
421        Set the list of models
422        """
423        # list of model names
424        # get regular models
425        main_list = self.model_list
426        # get custom models
427        self.update_cm_list()
428        # add custom models to model list
429        for name in self.cm_list:
430            if name not in main_list:
431                main_list.append(name)
432
433        if len(main_list) > 1:
434            main_list.sort()
435        self.model1.Clear()
436        self.model2.Clear()
437        for idx in range(len(main_list)):
438            self.model1.Append(str(main_list[idx]), idx)
439            self.model2.Append(str(main_list[idx]), idx)
440        self.model1.SetStringSelection(self.model1_string)
441        self.model2.SetStringSelection(self.model2_string)
442
443    def update_cm_list(self):
444        """
445        Update custom model list
446        """
447        cm_list = []
448        al_list = os.listdir(self.plugin_dir)
449        for c_name in al_list:
450            if c_name.split('.')[-1] == 'py' and \
451                    c_name.split('.')[0] != '__init__':
452                name = str(c_name.split('.')[0])
453                cm_list.append(name)
454        self.cm_list = cm_list
455
456    def on_model1(self, event):
457        """
458        Set model1
459        """
460        event.Skip()
461        self.update_cm_list()
462        self.model1_name = str(self.model1.GetValue())
463        self.model1_string = self.model1_name
464        if self.model1_name in self.cm_list:
465            self.is_p1_custom = True
466        else:
467            self.is_p1_custom = False
468
469    def on_model2(self, event):
470        """
471        Set model2
472        """
473        event.Skip()
474        self.update_cm_list()
475        self.model2_name = str(self.model2.GetValue())
476        self.model2_string = self.model2_name
477        if self.model2_name in self.cm_list:
478            self.is_p2_custom = True
479        else:
480            self.is_p2_custom = False
481
482    def on_select_operator(self, event=None):
483        """
484        On Select an Operator
485        """
486        # For Mac
487        if event is not None:
488            event.Skip()
489        item = event.GetEventObject()
490        text = item.GetValue()
491        self.fill_explanation_helpstring(text)
492
493    def fill_explanation_helpstring(self, operator):
494        """
495        Choose the equation to use depending on whether we now have
496        a sum or multiply model then create the appropriate string
497       
498        for the sum model the result will be:
499        scale_factor * (scale1 * model_1 + scale2 * model_2) + background
500        while for the multiply model it will just be:
501        scale_factor * (model_1* model_2) + background
502        """
503        name = ''
504        if operator == '*':
505            name = 'Multi'
506            factor_1 = ''
507            factor_2 = ''
508        else:
509            name = 'Sum'
510            factor_1 = 'scale_1 * '
511            factor_2 = 'scale_2 * '
512
513        self._operator = operator
514        self.explanation = ("  Plugin_model = scale_factor * ({}model_1 {} "
515            "{}model_2) + background").format(factor_1,operator,factor_2)
516        self.explanationctr.SetLabel(self.explanation)
517        self.name = name + M_NAME
518
519
520    def fill_operator_combox(self):
521        """
522        fill the current combobox with the operator
523        """
524        operator_list = ['+', '*']
525        for oper in operator_list:
526            pos = self._operator_choice.Append(str(oper))
527            self._operator_choice.SetClientData(pos, str(oper))
528        self._operator_choice.SetSelection(0)
529
530    def get_textnames(self):
531        """
532        Returns model name string as list
533        """
534        return [self.model1_name, self.model2_name]
535
536    def write_string(self, fname, model1_name, model2_name):
537        """
538        Write and Save file
539        """
540        self.fname = fname
541        description = self.desc_tcl.GetValue().lstrip().rstrip()
542        desc_line = ''
543        if description.strip() != '':
544            # Sasmodels generates a description for us. If the user provides
545            # their own description, add a line to overwrite the sasmodels one
546            desc_line = "\nmodel_info.description = '{}'".format(description)
547        name = os.path.splitext(os.path.basename(self.fname))[0]
548        output = SUM_TEMPLATE.format(name=name, model1=model1_name,
549            model2=model2_name, operator=self._operator, desc_line=desc_line)
550        with open(self.fname, 'w') as out_f:
551            out_f.write(output)
552
553    def compile_file(self, path):
554        """
555        Compile the file in the path
556        """
557        path = self.fname
558        show_model_output(self, path)
559
560    def delete_file(self, path):
561        """
562        Delete file in the path
563        """
564        _delete_file(path)
565
566
567class EditorPanel(wx.ScrolledWindow):
568    """
569    Simple Plugin Model function editor
570    """
571    def __init__(self, parent, base, path, title, *args, **kwds):
572        kwds['name'] = title
573#        kwds["size"] = (EDITOR_WIDTH, EDITOR_HEIGTH)
574        kwds["style"] = wx.FULL_REPAINT_ON_RESIZE
575        wx.ScrolledWindow.__init__(self, parent, *args, **kwds)
576        self.SetScrollbars(1,1,1,1)
577        self.parent = parent
578        self.base = base
579        self.path = path
580        self.font = wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT)
581        self.font.SetPointSize(10)
582        self.reader = None
583        self.name = 'untitled'
584        self.overwrite_name = False
585        self.is_2d = False
586        self.fname = None
587        self.main_sizer = None
588        self.name_sizer = None
589        self.name_hsizer = None
590        self.name_tcl = None
591        self.overwrite_cb = None
592        self.desc_sizer = None
593        self.desc_tcl = None
594        self.param_sizer = None
595        self.param_tcl = None
596        self.function_sizer = None
597        self.func_horizon_sizer = None
598        self.button_sizer = None
599        self.param_strings = ''
600        self.function_strings = ''
601        self._notes = ""
602        self._msg_box = None
603        self.msg_sizer = None
604        self.warning = ""
605        #This does not seem to be used anywhere so commenting out for now
606        #    -- PDB 2/26/17
607        #self._description = "New Plugin Model"
608        self.function_tcl = None
609        self.math_combo = None
610        self.bt_apply = None
611        self.bt_close = None
612        #self._default_save_location = os.getcwd()
613        self._do_layout()
614
615
616
617    def _define_structure(self):
618        """
619        define initial sizer
620        """
621        #w, h = self.parent.GetSize()
622        self.main_sizer = wx.BoxSizer(wx.VERTICAL)
623        self.name_sizer = wx.BoxSizer(wx.VERTICAL)
624        self.name_hsizer = wx.BoxSizer(wx.HORIZONTAL)
625        self.desc_sizer = wx.BoxSizer(wx.VERTICAL)
626        self.param_sizer = wx.BoxSizer(wx.VERTICAL)
627        self.function_sizer = wx.BoxSizer(wx.VERTICAL)
628        self.func_horizon_sizer = wx.BoxSizer(wx.HORIZONTAL)
629        self.button_sizer = wx.BoxSizer(wx.HORIZONTAL)
630        self.msg_sizer = wx.BoxSizer(wx.HORIZONTAL)
631
632    def _layout_name(self):
633        """
634        Do the layout for file/function name related widgets
635        """
636        #title name [string]
637        name_txt = wx.StaticText(self, -1, 'Function Name : ')
638        self.overwrite_cb = wx.CheckBox(self, -1, "Overwrite existing plugin model of this name?", (10, 10))
639        self.overwrite_cb.SetValue(False)
640        self.overwrite_cb.SetToolTipString("Overwrite it if already exists?")
641        wx.EVT_CHECKBOX(self, self.overwrite_cb.GetId(), self.on_over_cb)
642        self.name_tcl = wx.TextCtrl(self, -1, size=(PANEL_WIDTH * 3 / 5, -1))
643        self.name_tcl.Bind(wx.EVT_TEXT_ENTER, self.on_change_name)
644        self.name_tcl.SetValue('')
645        self.name_tcl.SetFont(self.font)
646        hint_name = "Unique Model Function Name."
647        self.name_tcl.SetToolTipString(hint_name)
648        self.name_hsizer.AddMany([(self.name_tcl, 0, wx.LEFT | wx.TOP, 0),
649                                  (self.overwrite_cb, 0, wx.LEFT, 20)])
650        self.name_sizer.AddMany([(name_txt, 0, wx.LEFT | wx.TOP, 10),
651                                 (self.name_hsizer, 0,
652                                  wx.LEFT | wx.TOP | wx.BOTTOM, 10)])
653
654
655    def _layout_description(self):
656        """
657        Do the layout for description related widgets
658        """
659        #title name [string]
660        desc_txt = wx.StaticText(self, -1, 'Description (optional) : ')
661        self.desc_tcl = wx.TextCtrl(self, -1, size=(PANEL_WIDTH * 3 / 5, -1))
662        self.desc_tcl.SetValue('')
663        hint_desc = "Write a short description of the model function."
664        self.desc_tcl.SetToolTipString(hint_desc)
665        self.desc_sizer.AddMany([(desc_txt, 0, wx.LEFT | wx.TOP, 10),
666                                 (self.desc_tcl, 0,
667                                  wx.LEFT | wx.TOP | wx.BOTTOM, 10)])
668    def _layout_param(self):
669        """
670        Do the layout for parameter related widgets
671        """
672        param_txt = wx.StaticText(self, -1, 'Fit Parameters: ')
673
674        param_tip = "#Set the parameters and their initial values.\n"
675        param_tip += "#Example:\n"
676        param_tip += "A = 1\nB = 1"
677        #param_txt.SetToolTipString(param_tip)
678        newid = wx.NewId()
679        self.param_tcl = EditWindow(self, newid, wx.DefaultPosition,
680                                    wx.DefaultSize,
681                                    wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
682        self.param_tcl.setDisplayLineNumbers(True)
683        self.param_tcl.SetToolTipString(param_tip)
684
685        self.param_sizer.AddMany([(param_txt, 0, wx.LEFT, 10),
686                                  (self.param_tcl, 1, wx.EXPAND | wx.ALL, 10)])
687
688
689    def _layout_function(self):
690        """
691        Do the layout for function related widgets
692        """
693        function_txt = wx.StaticText(self, -1, 'Function(x) : ')
694        hint_function = "#Example:\n"
695        hint_function += "if x <= 0:\n"
696        hint_function += "    y = A + B\n"
697        hint_function += "else:\n"
698        hint_function += "    y = A + B * cos(2 * pi * x)\n"
699        hint_function += "return y\n"
700        math_txt = wx.StaticText(self, -1, '*Useful math functions: ')
701        math_combo = self._fill_math_combo()
702
703        newid = wx.NewId()
704        self.function_tcl = EditWindow(self, newid, wx.DefaultPosition,
705                                       wx.DefaultSize,
706                                       wx.CLIP_CHILDREN | wx.SUNKEN_BORDER)
707        self.function_tcl.setDisplayLineNumbers(True)
708        self.function_tcl.SetToolTipString(hint_function)
709
710        self.func_horizon_sizer.Add(function_txt)
711        self.func_horizon_sizer.Add(math_txt, 0, wx.LEFT, 250)
712        self.func_horizon_sizer.Add(math_combo, 0, wx.LEFT, 10)
713
714        self.function_sizer.Add(self.func_horizon_sizer, 0, wx.LEFT, 10)
715        self.function_sizer.Add(self.function_tcl, 1, wx.EXPAND | wx.ALL, 10)
716
717    def _layout_msg(self):
718        """
719        Layout msg
720        """
721        self._msg_box = wx.StaticText(self, -1, self._notes,
722                                      size=(PANEL_WIDTH, -1))
723        self.msg_sizer.Add(self._msg_box, 0, wx.LEFT, 10)
724
725    def _layout_button(self):
726        """
727        Do the layout for the button widgets
728        """
729        self.bt_apply = wx.Button(self, -1, "Apply", size=(_BOX_WIDTH, -1))
730        self.bt_apply.SetToolTipString("Save changes into the imported data.")
731        self.bt_apply.Bind(wx.EVT_BUTTON, self.on_click_apply)
732
733        self.bt_help = wx.Button(self, -1, "HELP", size=(_BOX_WIDTH, -1))
734        self.bt_help.SetToolTipString("Get Help For Model Editor")
735        self.bt_help.Bind(wx.EVT_BUTTON, self.on_help)
736
737        self.bt_close = wx.Button(self, -1, 'Close', size=(_BOX_WIDTH, -1))
738        self.bt_close.Bind(wx.EVT_BUTTON, self.on_close)
739        self.bt_close.SetToolTipString("Close this panel.")
740
741        self.button_sizer.AddMany([(self.bt_apply, 0,0),
742                                   (self.bt_help, 0, wx.LEFT | wx.BOTTOM,15),
743                                   (self.bt_close, 0, wx.LEFT | wx.RIGHT, 15)])
744
745    def _do_layout(self):
746        """
747        Draw the current panel
748        """
749        self._define_structure()
750        self._layout_name()
751        self._layout_description()
752        self._layout_param()
753        self._layout_function()
754        self._layout_msg()
755        self._layout_button()
756        self.main_sizer.AddMany([(self.name_sizer, 0, wx.EXPAND | wx.ALL, 5),
757                                 (wx.StaticLine(self), 0,
758                                  wx.ALL | wx.EXPAND, 5),
759                                 (self.desc_sizer, 0, wx.EXPAND | wx.ALL, 5),
760                                 (wx.StaticLine(self), 0,
761                                  wx.ALL | wx.EXPAND, 5),
762                                 (self.param_sizer, 1, wx.EXPAND | wx.ALL, 5),
763                                 (wx.StaticLine(self), 0,
764                                  wx.ALL | wx.EXPAND, 5),
765                                 (self.function_sizer, 2,
766                                  wx.EXPAND | wx.ALL, 5),
767                                 (wx.StaticLine(self), 0,
768                                  wx.ALL | wx.EXPAND, 5),
769                                 (self.msg_sizer, 0, wx.EXPAND | wx.ALL, 5),
770                                 (self.button_sizer, 0, wx.ALIGN_RIGHT)])
771        self.SetSizer(self.main_sizer)
772        self.SetAutoLayout(True)
773
774    def _fill_math_combo(self):
775        """
776        Fill up the math combo box
777        """
778        self.math_combo = wx.ComboBox(self, -1, size=(100, -1),
779                                      style=wx.CB_READONLY)
780        for item in dir(math):
781            if item.count("_") < 1:
782                try:
783                    exec("float(math.%s)" % item)
784                    self.math_combo.Append(str(item))
785                except Exception:
786                    self.math_combo.Append(str(item) + "()")
787        self.math_combo.Bind(wx.EVT_COMBOBOX, self._on_math_select)
788        self.math_combo.SetSelection(0)
789        return self.math_combo
790
791    def _on_math_select(self, event):
792        """
793        On math selection on ComboBox
794        """
795        event.Skip()
796        label = self.math_combo.GetValue()
797        self.function_tcl.SetFocus()
798        # Put the text at the cursor position
799        pos = self.function_tcl.GetCurrentPos()
800        self.function_tcl.InsertText(pos, label)
801        # Put the cursor at appropriate position
802        length = len(label)
803        print(length)
804        if label[length-1] == ')':
805            length -= 1
806        f_pos = pos + length
807        self.function_tcl.GotoPos(f_pos)
808
809    def get_notes(self):
810        """
811        return notes
812        """
813        return self._notes
814
815    def on_change_name(self, event=None):
816        """
817        Change name
818        """
819        if event is not None:
820            event.Skip()
821        self.name_tcl.SetBackgroundColour('white')
822        self.Refresh()
823
824    def check_name(self):
825        """
826        Check name if exist already
827        """
828        self._notes = ''
829        self.on_change_name(None)
830        plugin_dir = self.path
831        list_fnames = os.listdir(plugin_dir)
832        # function/file name
833        title = self.name_tcl.GetValue().lstrip().rstrip()
834        self.name = title
835        t_fname = title + '.py'
836        if not self.overwrite_name:
837            if t_fname in list_fnames:
838                self.name_tcl.SetBackgroundColour('pink')
839                return False
840        self.fname = os.path.join(plugin_dir, t_fname)
841        s_title = title
842        if len(title) > 20:
843            s_title = title[0:19] + '...'
844        self._notes += "Model function name is set "
845        self._notes += "to %s. \n" % str(s_title)
846        return True
847
848    def on_over_cb(self, event):
849        """
850        Set overwrite name flag on cb event
851        """
852        if event is not None:
853            event.Skip()
854        cb_value = event.GetEventObject()
855        self.overwrite_name = cb_value.GetValue()
856
857    def on_click_apply(self, event):
858        """
859        Changes are saved in data object imported to edit.
860
861        checks firs for valid name, then if it already exists then checks
862        that a function was entered and finally that if entered it contains at
863        least a return statement.  If all passes writes file then tries to
864        compile.  If compile fails or import module fails or run method fails
865        tries to remove any .py and pyc files that may have been created and
866        sets error message.
867
868        :todo this code still could do with a careful going over to clean
869        up and simplify. the non GUI methods such as this one should be removed
870        to computational code of SasView. Most of those computational methods
871        would be the same for both the simple editors.
872        """
873        #must post event here
874        event.Skip()
875        name = self.name_tcl.GetValue().lstrip().rstrip()
876        info = 'Info'
877        msg = ''
878        result, check_err = '', ''
879        # Sort out the errors if occur
880        # First check for valid python name then if the name already exists
881        if not name or not bool(re.match('^[A-Za-z0-9_]*$', name)):
882            msg = '"%s" '%name
883            msg += "is not a valid model name. Name must not be empty and \n"
884            msg += "may include only alpha numeric or underline characters \n"
885            msg += "and no spaces"
886        elif self.check_name():
887            description = self.desc_tcl.GetValue()
888            param_str = self.param_tcl.GetText()
889            func_str = self.function_tcl.GetText()
890            # No input for the model function
891            if func_str.lstrip().rstrip():
892                if func_str.count('return') > 0:
893                    self.write_file(self.fname, name, description, param_str,
894                                    func_str)
895                    try:
896                        result, msg = check_model(self.fname), None
897                    except Exception:
898                        import traceback
899                        result, msg = None, "error building model"
900                        check_err = "\n"+traceback.format_exc(limit=2)
901                else:
902                    msg = "Error: The func(x) must 'return' a value at least.\n"
903                    msg += "For example: \n\nreturn 2*x"
904            else:
905                msg = 'Error: Function is not defined.'
906        else:
907            msg = "Name exists already."
908
909        #
910        if self.base is not None and not msg:
911            self.base.update_custom_combo()
912
913        # Prepare the messagebox
914        if msg:
915            info = 'Error'
916            color = 'red'
917            self.overwrite_cb.SetValue(True)
918            self.overwrite_name = True
919        else:
920            self._notes = result
921            msg = "Successful! Please look for %s in Plugin Models."%name
922            msg += "  " + self._notes
923            info = 'Info'
924            color = 'blue'
925        self._msg_box.SetLabel(msg)
926        self._msg_box.SetForegroundColour(color)
927        # Send msg to the top window
928        if self.base is not None:
929            from sas.sasgui.guiframe.events import StatusEvent
930            wx.PostEvent(self.base.parent,
931                         StatusEvent(status=msg+check_err, info=info))
932        self.warning = msg
933
934    def write_file(self, fname, name, desc_str, param_str, func_str):
935        """
936        Write content in file
937
938        :param fname: full file path
939        :param desc_str: content of the description strings
940        :param param_str: content of params; Strings
941        :param func_str: content of func; Strings
942        """
943        out_f = open(fname, 'w')
944
945        out_f.write(CUSTOM_TEMPLATE % {
946            'name': name,
947            'title': 'User model for ' + name,
948            'description': desc_str,
949            'date': datetime.datetime.now().strftime('%YYYY-%mm-%dd'),
950        })
951
952        # Write out parameters
953        param_names = []    # to store parameter names
954        out_f.write('parameters = [ \n')
955        out_f.write('#   ["name", "units", default, [lower, upper], "type", "description"],\n')
956        for pname, pvalue, desc in self.get_param_helper(param_str):
957            param_names.append(pname)
958            out_f.write("    ['%s', '', %s, [-inf, inf], '', '%s'],\n"
959                        % (pname, pvalue, desc))
960        out_f.write('    ]\n')
961
962        # Write out function definition
963        out_f.write('\n')
964        out_f.write('def Iq(%s):\n' % ', '.join(['x'] + param_names))
965        out_f.write('    """Absolute scattering"""\n')
966        if "scipy." in func_str:
967            out_f.write('    import scipy\n')
968        if "numpy." in func_str:
969            out_f.write('    import numpy\n')
970        if "np." in func_str:
971            out_f.write('    import numpy as np\n')
972        for func_line in func_str.split('\n'):
973            out_f.write('%s%s\n' % ('    ', func_line))
974        out_f.write('## uncomment the following if Iq works for vector x\n')
975        out_f.write('#Iq.vectorized = True\n')
976
977        # Create place holder for Iqxy
978        out_f.write('\n')
979        out_f.write('#def Iqxy(%s):\n' % ', '.join(["x", "y"] + param_names))
980        out_f.write('#    """Absolute scattering of oriented particles."""\n')
981        out_f.write('#    ...\n')
982        out_f.write('#    return oriented_form(x, y, args)\n')
983        out_f.write('## uncomment the following if Iqxy works for vector x, y\n')
984        out_f.write('#Iqxy.vectorized = True\n')
985
986        out_f.close()
987
988    def get_param_helper(self, param_str):
989        """
990        yield a sequence of name, value pairs for the parameters in param_str
991
992        Parameters can be defined by one per line by name=value, or multiple
993        on the same line by separating the pairs by semicolon or comma.  The
994        value is optional and defaults to "1.0".
995        """
996        for line in param_str.replace(';', ',').split('\n'):
997            for item in line.split(','):
998                defn, desc = item.split('#', 1) if '#' in item else (item, '')
999                name, value = defn.split('=', 1) if '=' in defn else (defn, '1.0')
1000                if name:
1001                    yield [v.strip() for v in (name, value, desc)]
1002
1003    def set_function_helper(self, line):
1004        """
1005        Get string in line to define the local params
1006
1007        :param line: one line of string got from the param_str
1008        """
1009        params_str = ''
1010        spaces = '        '#8spaces
1011        items = line.split(";")
1012        for item in items:
1013            name = item.split("=")[0].lstrip().rstrip()
1014            params_str += spaces + "%s = self.params['%s']\n" % (name, name)
1015        return params_str
1016
1017    def get_warning(self):
1018        """
1019        Get the warning msg
1020        """
1021        return self.warning
1022
1023    def on_help(self, event):
1024        """
1025        Bring up the New Plugin Model Editor Documentation whenever
1026        the HELP button is clicked.
1027
1028        Calls DocumentationWindow with the path of the location within the
1029        documentation tree (after /doc/ ....".  Note that when using old
1030        versions of Wx (before 2.9) and thus not the release version of
1031        installers, the help comes up at the top level of the file as
1032        webbrowser does not pass anything past the # to the browser when it is
1033        running "file:///...."
1034
1035        :param evt: Triggers on clicking the help button
1036        """
1037
1038        _TreeLocation = "user/sasgui/perspectives/fitting/fitting_help.html"
1039        _PageAnchor = "#new-plugin-model"
1040        _doc_viewer = DocumentationWindow(self, -1, _TreeLocation, _PageAnchor,
1041                                          "Plugin Model Editor Help")
1042
1043    def on_close(self, event):
1044        """
1045        leave data as it is and close
1046        """
1047        self.parent.Show(False)#Close()
1048        event.Skip()
1049
1050class EditorWindow(wx.Frame):
1051    """
1052    Editor Window
1053    """
1054    def __init__(self, parent, base, path, title,
1055                 size=(EDITOR_WIDTH, EDITOR_HEIGTH), *args, **kwds):
1056        """
1057        Init
1058        """
1059        kwds["title"] = title
1060        kwds["size"] = size
1061        wx.Frame.__init__(self, parent=None, *args, **kwds)
1062        self.parent = parent
1063        self.panel = EditorPanel(parent=self, base=parent,
1064                                 path=path, title=title)
1065        self.Show(True)
1066        wx.EVT_CLOSE(self, self.on_close)
1067
1068    def on_close(self, event):
1069        """
1070        On close event
1071        """
1072        self.Show(False)
1073        #if self.parent is not None:
1074        #    self.parent.new_model_frame = None
1075        #self.Destroy()
1076
1077## Templates for plugin models
1078
1079CUSTOM_TEMPLATE = '''\
1080r"""
1081Definition
1082----------
1083
1084Calculates %(name)s.
1085
1086%(description)s
1087
1088References
1089----------
1090
1091Authorship and Verification
1092---------------------------
1093
1094* **Author:** --- **Date:** %(date)s
1095* **Last Modified by:** --- **Date:** %(date)s
1096* **Last Reviewed by:** --- **Date:** %(date)s
1097"""
1098
1099from math import *
1100from numpy import inf
1101
1102name = "%(name)s"
1103title = "%(title)s"
1104description = """%(description)s"""
1105
1106'''
1107
1108SUM_TEMPLATE = """
1109from sasmodels.core import load_model_info
1110from sasmodels.sasview_model import make_model_from_info
1111
1112model_info = load_model_info('{model1}{operator}{model2}')
1113model_info.name = '{name}'{desc_line}
1114Model = make_model_from_info(model_info)
1115"""
1116if __name__ == "__main__":
1117    main_app = wx.App()
1118    main_frame = TextDialog(id=1, model_list=["SphereModel", "CylinderModel"],
1119                            plugin_dir='../fitting/plugin_models')
1120    main_frame.ShowModal()
1121    main_app.MainLoop()
Note: See TracBrowser for help on using the repository browser.