source: trunk/GSASIIctrlGUI.py @ 3219

Last change on this file since 3219 was 3212, checked in by vondreele, 7 years ago

fix problem of missing rd.MPhase in phase imports
work on display of gray phases & other magnetic symmetry info.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Date Author Revision URL Id
File size: 215.2 KB
Line 
1# -*- coding: utf-8 -*-
2#GSASIIctrlGUI - Custom GSAS-II GUI controls
3########### SVN repository information ###################
4# $Date: 2018-01-04 16:29:00 +0000 (Thu, 04 Jan 2018) $
5# $Author: vondreele $
6# $Revision: 3212 $
7# $URL: trunk/GSASIIctrlGUI.py $
8# $Id: GSASIIctrlGUI.py 3212 2018-01-04 16:29:00Z vondreele $
9########### SVN repository information ###################
10'''
11*GSASIIctrlGUI: Custom GUI controls*
12---------------------------------------------
13
14A library of GUI controls for reuse throughout GSAS-II, as indexed below
15
16================================  =================================================================
17Class or function name             Description
18================================  =================================================================
19:class:`ValidatedTxtCtrl`          A text control with a built-in call back routine to set dict
20                                   or list elements. Optionally validates input as float, int or
21                                   for strings non-blank. Value is set when focus changes
22:class:`EnumSelector`              A combo box with a built-in call back routine that
23                                   automatically sets a dict or list entry.
24:class:`G2ChoiceButton`            A customized wx.Choice that automatically initializes to
25                                   the initial value and saves the choice directly into a dict
26                                   or list value. Optionally calls function when a
27                                   choice is selected
28:class:`G2CheckBox`                A customized wx.CheckBox that automatically initializes to
29                                   the initial value and saves the choice directly into a dict
30                                   or list value. Optionally calls function when a
31                                   choice is selected
32:func:`CallScrolledMultiEditor`    Routine for editing many dict- or list-contained items.
33                                   using the :class:`ScrolledMultiEditor` dialog
34:class:`ScrolledMultiEditor`       wx.Dialog for editing many dict- or list-contained items.
35                                   with validation. Results are placed in dict or list.
36:class:`G2MultiChoiceDialog`       Dialog similar to wx.MultiChoiceDialog, but provides
37                                   a filter to select choices and buttons to make selection
38                                   of multiple items more simple.
39:class:`G2SingleChoiceDialog`      Dialog similar to wx.SingleChoiceDialog, but provides
40                                   a filter to help search through choices.
41:class:`FlagSetDialog`             Dialog that provides a table of items along with a
42                                   checkbox for each.
43:class:`SingleFloatDialog`         Dialog to obtain a single float value from user, with
44                                   optional range validation.
45:class:`MultiFloatDialog`          Dialog to obtain multiple float values from user,
46                                   with optional range validation.
47:class:`SingleStringDialog`        Dialog to obtain a single string value from user,
48                                   with optional an optional default value.
49:class:`MultiStringDialog`         Dialog to obtain multiple string values from user,
50                                   with a description for each value and optional
51                                   defaults.
52:class:`SingleIntDialog`           Dialog to obtain a single integer value from user,
53                                   with optional range validation.
54:class:`MultiIntegerDialog`        Dialog to obtain multiple integer values from user,
55                                   with a description for each value and optional
56                                   defaults.
57:class:`G2ColumnIDDialog`          A dialog for matching column data to desired items; some
58                                   columns may be ignored.
59:class:`G2HistoDataDialog`         A dialog for global edits to histogram data globally
60:class:`OrderBox`                  Creates a wx.Panel with scrollbars where items can be
61                                   ordered into columns.
62:class:`HelpButton`                Creates a button labeled with a "?" that when pressed
63                                   displays help text in a modal message window.
64:func:`G2MessageBox`               Displays text typically used for errors or warnings.
65:func:`HorizontalLine`             Places a line in a Frame or Dialog to separate sections.
66:func:`SelectEdit1Var`             Select a variable from a list, then edit it and select
67                                   histograms to copy it to.
68:func:`ItemSelector`               Select a single item or multiple items from list of choices.
69                                   Creates and then destroys a wx.Dialog and returns the
70                                   selections(s).
71:func:`GetItemOrder`               Creates a dialog for ordering items into columns
72:func:`GetImportFile`              Gets one ore more file from the appropriate import
73                                   directory, which can be overridden. Arguments follow those
74                                   of :func:`wx.FileDialog`
75:func:`Define_wxId`                Create a unique wx.Id symbol that is global to this
76                                   module (:mod:`GSASIIctrlGUI`). Such symbols are needed
77                                   when the menu item is defined in a different location
78                                   from the wx.Bind that links the menu item to a function.
79                                   This function allows menu Ids to be
80                                   defined where they are first used rather than be placed
81                                   yet a third location somewhere in this module.
82================================  =================================================================
83
84Other miscellaneous routines that may be of use:
85
86================================  =================================================================
87Function name                      Description
88================================  =================================================================
89:func:`StripIndents`               Regularizes the intentation from a string with multiple
90                                   newline characters by removing spaces at the beginning
91                                   of each line.
92:func:`StripUnicode`               Removes unicode characters from strings
93:func:`GetImportPath`              Determines the default location to use for importing files.
94                                   Tries sequentially :attr:`G2frame.TutorialImportDir`,
95                                   config var ``Import_directory`` and
96                                   :attr:`G2frame.LastImportDir`.
97:func:`GetExportPath`              Determines the default location to use for writing files.
98                                   Tries sequentially :attr:`G2frame.LastExportDir` and
99                                   :attr:`G2frame.LastGPXdir`
100================================  =================================================================
101
102Documentation for all the routines in module :mod:`GSASIIctrlGUI`.
103
104'''
105from __future__ import division, print_function
106import os
107import sys
108try:
109    import wx
110    import wx.grid as wg
111    # import wx.wizard as wz
112    import wx.aui
113    import wx.lib.scrolledpanel as wxscroll
114    import wx.html        # could postpone this for quicker startup
115    import matplotlib as mpl
116except ImportError:
117    raise
118    # Dumm 'wx' so this file can be imported
119    vals = ('TreeCtrl TextCtrl PyValidator Button ComboBox Choice CheckBox'.split() +
120            'Dialog ID_ANY OPEN'.split())
121    class Placeholder(object):
122        def __init__(self, vals):
123            for val in vals:
124                setattr(self, val, object)
125        def __getattr__(self, value):
126            if value[0].isupper():
127                return object
128            return Placeholder([])
129    wx = Placeholder(vals)
130    wxscroll = Placeholder(['ScrolledPanel'])
131    if 'phoenix' in wx.version():
132        wg = Placeholder('Grid GridTableBase GridCellEditor'.split())
133    else:
134        wg = Placeholder('Grid PyGridTableBase PyGridCellEditor'.split())
135import time
136import copy
137import webbrowser     # could postpone this for quicker startup
138
139import GSASIIpath
140GSASIIpath.SetVersionNumber("$Revision: 3212 $")
141import GSASIIdataGUI as G2gd
142import GSASIIpwdGUI as G2pdG
143import GSASIIpy3 as G2py3
144import GSASIIlog as log
145import GSASIIobj as G2obj
146
147# Define a short names for convenience
148WHITE = (255,255,255)
149DULL_YELLOW = (230,230,190)
150# Don't depend on wx, for scriptable
151try:
152    VERY_LIGHT_GREY = wx.Colour(235,235,235)
153    WACV = wx.ALIGN_CENTER_VERTICAL
154except:
155    # Don't depend on GUI
156    pass
157
158################################################################################
159#### Fixed definitions for wx Ids
160################################################################################
161def Define_wxId(*args):
162    '''routine to create unique global wx Id symbols in this module.
163    '''
164    for arg in args:
165        if GSASIIpath.GetConfigValue('debug') and not arg.startswith('wxID_'):
166            print ('Problem in name'+arg)
167        if arg in globals():
168            if GSASIIpath.GetConfigValue('debug'): print (arg+'already defined')
169            continue
170        exec('global '+arg+';'+arg+' = wx.NewId()')
171
172################################################################################
173#### Tree Control
174################################################################################
175class G2TreeCtrl(wx.TreeCtrl):
176    '''Create a wrapper around the standard TreeCtrl so we can "wrap"
177    various events.
178   
179    This logs when a tree item is selected (in :meth:`onSelectionChanged`)
180
181    This also wraps lists and dicts pulled out of the tree to track where
182    they were retrieved from.
183    '''
184    #def SelectItem(self,event):
185    #    print 'Select Item'
186    #    import GSASIIobj as G2obj
187    #    G2obj.HowDidIgetHere()
188    #    wx.TreeCtrl.SelectItem(self,event)
189       
190    def __init__(self,parent=None,*args,**kwargs):
191        super(self.__class__,self).__init__(parent=parent,*args,**kwargs)
192        self.G2frame = parent.GetTopLevelParent()
193        self.root = self.AddRoot('Loaded Data: ')
194        self.SelectionChanged = None
195        self.textlist = None
196        log.LogInfo['Tree'] = self
197
198    def _getTreeItemsList(self,item):
199        '''Get the full tree hierarchy from a reference to a tree item.
200        Note that this effectively hard-codes phase and histogram names in the
201        returned list. We may want to make these names relative in the future.
202        '''
203        textlist = [self.GetItemText(item)]
204        parent = self.GetItemParent(item)
205        while parent:
206            if parent == self.root: break
207            textlist.insert(0,self.GetItemText(parent))
208            parent = self.GetItemParent(parent)
209        return textlist
210   
211    def GetItemPyData(self,id):
212        if 'phoenix' in wx.version():
213            return wx.TreeCtrl.GetItemData(self,id)
214        else:
215            return wx.TreeCtrl.GetItemPyData(self,id)
216
217    def SetItemPyData(self,id,data):
218        if 'phoenix' in wx.version():
219            return wx.TreeCtrl.SetItemData(self,id,data)
220        else:
221            return wx.TreeCtrl.SetItemPyData(self,id,data)
222
223    def onSelectionChanged(self,event):
224        '''Log each press on a tree item here.
225        '''
226        if not self.G2frame.treePanel:
227            return
228        if self.SelectionChanged:
229            textlist = self._getTreeItemsList(event.GetItem())
230            if log.LogInfo['Logging'] and event.GetItem() != self.root:
231                textlist[0] = self.GetRelativeHistNum(textlist[0])
232                if textlist[0] == "Phases" and len(textlist) > 1:
233                    textlist[1] = self.GetRelativePhaseNum(textlist[1])
234                log.MakeTreeLog(textlist)
235            if textlist == self.textlist:
236                return      #same as last time - don't get it again
237            self.textlist = textlist
238            self.SelectionChanged(event)
239
240    def Bind(self,eventtype,handler,*args,**kwargs):
241        '''Override the Bind() function so that page change events can be trapped
242        '''
243        if eventtype == wx.EVT_TREE_SEL_CHANGED:
244            self.SelectionChanged = handler
245            wx.TreeCtrl.Bind(self,eventtype,self.onSelectionChanged)
246            return
247        wx.TreeCtrl.Bind(self,eventtype,handler,*args,**kwargs)
248
249    # commented out, disables Logging
250    # def GetItemPyData(self,*args,**kwargs):
251    #    '''Override the standard method to wrap the contents
252    #    so that the source can be logged when changed
253    #    '''
254    #    data = super(self.__class__,self).GetItemPyData(*args,**kwargs)
255    #    textlist = self._getTreeItemsList(args[0])
256    #    if type(data) is dict:
257    #        return log.dictLogged(data,textlist)
258    #    if type(data) is list:
259    #        return log.listLogged(data,textlist)
260    #    if type(data) is tuple: #N.B. tuples get converted to lists
261    #        return log.listLogged(list(data),textlist)
262    #    return data
263
264    def GetRelativeHistNum(self,histname):
265        '''Returns list with a histogram type and a relative number for that
266        histogram, or the original string if not a histogram
267        '''
268        histtype = histname.split()[0]
269        if histtype != histtype.upper(): # histograms (only) have a keyword all in caps
270            return histname
271        item, cookie = self.GetFirstChild(self.root)
272        i = 0
273        while item:
274            itemtext = self.GetItemText(item)
275            if itemtext == histname:
276                return histtype,i
277            elif itemtext.split()[0] == histtype:
278                i += 1
279            item, cookie = self.GetNextChild(self.root, cookie)
280        else:
281            raise Exception("Histogram not found: "+histname)
282
283    def ConvertRelativeHistNum(self,histtype,histnum):
284        '''Converts a histogram type and relative histogram number to a
285        histogram name in the current project
286        '''
287        item, cookie = self.GetFirstChild(self.root)
288        i = 0
289        while item:
290            itemtext = self.GetItemText(item)
291            if itemtext.split()[0] == histtype:
292                if i == histnum: return itemtext
293                i += 1
294            item, cookie = self.GetNextChild(self.root, cookie)
295        else:
296            raise Exception("Histogram #'+str(histnum)+' of type "+histtype+' not found')
297       
298    def GetRelativePhaseNum(self,phasename):
299        '''Returns a phase number if the string matches a phase name
300        or else returns the original string
301        '''
302        item, cookie = self.GetFirstChild(self.root)
303        while item:
304            itemtext = self.GetItemText(item)
305            if itemtext == "Phases":
306                parent = item
307                item, cookie = self.GetFirstChild(parent)
308                i = 0
309                while item:
310                    itemtext = self.GetItemText(item)
311                    if itemtext == phasename:
312                        return i
313                    item, cookie = self.GetNextChild(parent, cookie)
314                    i += 1
315                else:
316                    return phasename # not a phase name
317            item, cookie = self.GetNextChild(self.root, cookie)
318        else:
319            raise Exception("No phases found ")
320
321    def ConvertRelativePhaseNum(self,phasenum):
322        '''Converts relative phase number to a phase name in
323        the current project
324        '''
325        item, cookie = self.GetFirstChild(self.root)
326        while item:
327            itemtext = self.GetItemText(item)
328            if itemtext == "Phases":
329                parent = item
330                item, cookie = self.GetFirstChild(parent)
331                i = 0
332                while item:
333                    if i == phasenum:
334                        return self.GetItemText(item)
335                    item, cookie = self.GetNextChild(parent, cookie)
336                    i += 1
337                else:
338                    raise Exception("Phase "+str(phasenum)+" not found")
339            item, cookie = self.GetNextChild(self.root, cookie)
340        else:
341            raise Exception("No phases found ")
342
343    def GetImageLoc(self,TreeId):
344        '''Get Image data from the Tree. Handles cases where the
345        image name is specified, as well as where the image file name is
346        a tuple containing the image file and an image number
347        '''
348       
349        size,imagefile = self.GetItemPyData(TreeId)
350        if type(imagefile) is tuple or type(imagefile) is list:
351            return size,imagefile[0],imagefile[1]
352        else:
353            return size,imagefile,None
354
355    def UpdateImageLoc(self,TreeId,imagefile):
356        '''Saves a new imagefile name in the Tree. Handles cases where the
357        image name is specified, as well as where the image file name is
358        a tuple containing the image file and an image number
359        '''
360       
361        idata = self.GetItemPyData(TreeId)
362        if type(idata[1]) is tuple or type(idata[1]) is list:
363            idata[1] = list(idata[1])
364            idata[1][0] = [imagefile,idata[1][1]]
365        else:
366            idata[1]  = imagefile
367       
368    def SaveExposedItems(self):
369        '''Traverse the top level tree items and save names of exposed (expanded) tree items.
370        Done before a refinement.
371        '''
372        self.ExposedItems = []
373        item, cookie = self.GetFirstChild(self.root)
374        while item:
375            name = self.GetItemText(item)
376            if self.IsExpanded(item): self.ExposedItems.append(name)
377            item, cookie = self.GetNextChild(self.root, cookie)
378#        print 'exposed:',self.ExposedItems
379
380    def RestoreExposedItems(self):
381        '''Traverse the top level tree items and restore exposed (expanded) tree items
382        back to their previous state (done after a reload of the tree after a refinement)
383        '''
384        item, cookie = self.GetFirstChild(self.root)
385        while item:
386            name = self.GetItemText(item)
387            if name in self.ExposedItems: self.Expand(item)
388            item, cookie = self.GetNextChild(self.root, cookie)
389
390################################################################################
391#### TextCtrl that stores input as entered with optional validation
392################################################################################
393class ValidatedTxtCtrl(wx.TextCtrl):
394    '''Create a TextCtrl widget that uses a validator to prevent the
395    entry of inappropriate characters and changes color to highlight
396    when invalid input is supplied. As valid values are typed,
397    they are placed into the dict or list where the initial value
398    came from. The type of the initial value must be int,
399    float or str or None (see :obj:`key` and :obj:`typeHint`);
400    this type (or the one in :obj:`typeHint`) is preserved.
401
402    Float values can be entered in the TextCtrl as numbers or also
403    as algebraic expressions using operators + - / \* () and \*\*,
404    in addition pi, sind(), cosd(), tand(), and sqrt() can be used,
405    as well as appreviations s, sin, c, cos, t, tan and sq.
406
407    :param wx.Panel parent: name of panel or frame that will be
408      the parent to the TextCtrl. Can be None.
409
410    :param dict/list loc: the dict or list with the initial value to be
411      placed in the TextCtrl.
412
413    :param int/str key: the dict key or the list index for the value to be
414      edited by the TextCtrl. The ``loc[key]`` element must exist, but may
415      have value None. If None, the type for the element is taken from
416      :obj:`typeHint` and the value for the control is set initially
417      blank (and thus invalid.) This is a way to specify a field without a
418      default value: a user must set a valid value.
419       
420      If the value is not None, it must have a base
421      type of int, float, str or unicode; the TextCrtl will be initialized
422      from this value.
423     
424    :param list nDig: number of digits, places and optionally the format
425       ([nDig,nPlc,fmt]) after decimal to use for display of float. The format
426       is either 'f' (default) or 'g'. Alternately, None can be specified which
427       causes numbers to be displayed with approximately 5 significant figures
428       (Default=None).
429
430    :param bool notBlank: if True (default) blank values are invalid
431      for str inputs.
432     
433    :param number min: minimum allowed valid value. If None (default) the
434      lower limit is unbounded.
435      NB: test in NumberValidator is val >= min not val > min
436
437    :param number max: maximum allowed valid value. If None (default) the
438      upper limit is unbounded
439      NB: test in NumberValidator is val <= max not val < max
440     
441    :param list exclLim: if True exclude min/max value ([exclMin,exclMax]);
442      (Default=[False,False])
443
444    :param function OKcontrol: specifies a function or method that will be
445      called when the input is validated. The called function is supplied
446      with one argument which is False if the TextCtrl contains an invalid
447      value and True if the value is valid.
448      Note that this function should check all values
449      in the dialog when True, since other entries might be invalid.
450      The default for this is None, which indicates no function should
451      be called.
452
453    :param function OnLeave: specifies a function or method that will be
454      called when the focus for the control is lost.
455      The called function is supplied with (at present) three keyword arguments:
456
457      * invalid: (*bool*) True if the value for the TextCtrl is invalid
458      * value:   (*int/float/str*)  the value contained in the TextCtrl
459      * tc:      (*wx.TextCtrl*)  the TextCtrl object
460
461      The number of keyword arguments may be increased in the future should needs arise,
462      so it is best to code these functions with a \*\*kwargs argument so they will
463      continue to run without errors
464
465      The default for OnLeave is None, which indicates no function should
466      be called.
467
468    :param type typeHint: the value of typeHint is overrides the initial value
469      for the dict/list element ``loc[key]``, if set to
470      int or float, which specifies the type for input to the TextCtrl.
471      Defaults as None, which is ignored.
472
473    :param bool CIFinput: for str input, indicates that only printable
474      ASCII characters may be entered into the TextCtrl. Forces output
475      to be ASCII rather than Unicode. For float and int input, allows
476      use of a single '?' or '.' character as valid input.
477
478    :param dict OnLeaveArgs: a dict with keyword args that are passed to
479      the :attr:`OnLeave` function. Defaults to ``{}``
480
481    :param bool ASCIIonly: if set as True will remove unicode characters from
482      strings
483
484    :param (other): other optional keyword parameters for the
485      wx.TextCtrl widget such as size or style may be specified.
486    '''
487    def __init__(self,parent,loc,key,nDig=None,notBlank=True,min=None,max=None,
488        OKcontrol=None,OnLeave=None,typeHint=None,CIFinput=False,exclLim=[False,False],
489        OnLeaveArgs={}, ASCIIonly=False, **kw):
490        # save passed values needed outside __init__
491        self.result = loc
492        self.key = key
493        self.nDig = nDig
494        self.OKcontrol=OKcontrol
495        self.OnLeave = OnLeave
496        self.OnLeaveArgs = OnLeaveArgs
497        self.CIFinput = CIFinput
498        self.notBlank = notBlank
499        self.ASCIIonly = ASCIIonly
500        self.type = str
501        # initialization
502        self.invalid = False   # indicates if the control has invalid contents
503        self.evaluated = False # set to True when the validator recognizes an expression
504        val = loc[key]
505        if 'style' in kw: # add a "Process Enter" to style
506            kw['style'] += kw['style'] | wx.TE_PROCESS_ENTER
507        else:
508            kw['style'] = wx.TE_PROCESS_ENTER
509        if 'int' in str(type(val)) or typeHint is int:
510            self.type = int
511            wx.TextCtrl.__init__(self,parent,wx.ID_ANY,
512                validator=NumberValidator(int,result=loc,key=key,min=min,max=max,
513                    exclLim=exclLim,OKcontrol=OKcontrol,CIFinput=CIFinput),**kw)
514            if val is not None:
515                self._setValue(val)
516            else: # no default is invalid for a number
517                self.invalid = True
518                self._IndicateValidity()
519
520        elif 'float' in str(type(val)) or typeHint is float:
521            self.type = float
522            wx.TextCtrl.__init__(self,parent,wx.ID_ANY,
523                validator=NumberValidator(float,result=loc,key=key,min=min,max=max,
524                    exclLim=exclLim,OKcontrol=OKcontrol,CIFinput=CIFinput),**kw)
525            if val is not None:
526                self._setValue(val)
527            else:
528                self.invalid = True
529                self._IndicateValidity()
530
531        elif isinstance(val,str) or isinstance(val,unicode) or typeHint is str:
532            if self.CIFinput:
533                wx.TextCtrl.__init__(
534                    self,parent,wx.ID_ANY,
535                    validator=ASCIIValidator(result=loc,key=key),
536                    **kw)
537            else:
538                wx.TextCtrl.__init__(self,parent,wx.ID_ANY,**kw)
539            if val is not None:
540                self.SetValue(val)
541            if notBlank:
542                self.Bind(wx.EVT_CHAR,self._onStringKey)
543                self.ShowStringValidity() # test if valid input
544            else:
545                self.invalid = False
546                self.Bind(wx.EVT_CHAR,self._GetStringValue)
547        elif val is None:
548            raise Exception("ValidatedTxtCtrl error: value of "+str(key)+
549                             " element is None and typeHint not defined as int or float")
550        else:
551            raise Exception("ValidatedTxtCtrl error: Unknown element ("+str(key)+
552                             ") type: "+str(type(val)))
553        # When the mouse is moved away or the widget loses focus,
554        # display the last saved value, if an expression
555        self.Bind(wx.EVT_LEAVE_WINDOW, self._onLeaveWindow)
556        self.Bind(wx.EVT_TEXT_ENTER, self._onLoseFocus)
557        self.Bind(wx.EVT_KILL_FOCUS, self._onLoseFocus)
558        # patch for wx 2.9 on Mac
559        i,j= wx.__version__.split('.')[0:2]
560        if int(i)+int(j)/10. > 2.8 and 'wxOSX' in wx.PlatformInfo:
561            self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
562
563    def SetValue(self,val):
564        if self.result is not None: # note that this bypasses formatting
565            self.result[self.key] = val
566            log.LogVarChange(self.result,self.key)
567        self._setValue(val)
568
569    def _setValue(self,val,show=True):
570        '''Check the validity of an int or float value and convert to a str.
571        Possibly format it. If show is True, display the formatted value in
572        the Text widget.
573        '''
574        self.invalid = False
575        if self.type is int:
576            try:
577                if int(val) != val:
578                    self.invalid = True
579                else:
580                    val = int(val)
581            except:
582                if self.CIFinput and (val == '?' or val == '.'):
583                    pass
584                else:
585                    self.invalid = True
586            if show: wx.TextCtrl.SetValue(self,str(val))
587        elif self.type is float:
588            try:
589                val = float(val) # convert strings, if needed
590            except:
591                if self.CIFinput and (val == '?' or val == '.'):
592                    pass
593                else:
594                    self.invalid = True
595            if self.nDig and show:
596                wx.TextCtrl.SetValue(self,str(G2py3.FormatValue(val,self.nDig)))
597            elif show:
598                wx.TextCtrl.SetValue(self,str(G2py3.FormatSigFigs(val)).rstrip('0'))
599        else:
600            if self.ASCIIonly:
601                s = ''
602                for c in val:
603                    if ord(c) < 128: s += c
604                if val != s:
605                    val = s
606                    show = True
607            if show:
608                try:
609                    wx.TextCtrl.SetValue(self,str(val))
610                except:
611                    wx.TextCtrl.SetValue(self,val)
612            self.ShowStringValidity() # test if valid input
613            return
614       
615        self._IndicateValidity()
616        if self.OKcontrol:
617            self.OKcontrol(not self.invalid)
618
619    def OnKeyDown(self,event):
620        'Special callback for wx 2.9+ on Mac where backspace is not processed by validator'
621        key = event.GetKeyCode()
622        if key in [wx.WXK_BACK, wx.WXK_DELETE]:
623            if self.Validator: wx.CallAfter(self.Validator.TestValid,self)
624        if key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER:
625            self._onLoseFocus(None)
626        if event: event.Skip()
627                   
628    def _onStringKey(self,event):
629        if event: event.Skip()
630        if self.invalid: # check for validity after processing the keystroke
631            wx.CallAfter(self.ShowStringValidity,True) # was invalid
632        else:
633            wx.CallAfter(self.ShowStringValidity,False) # was valid
634
635    def _IndicateValidity(self):
636        'Set the control colors to show invalid input'
637        if self.invalid:
638            ins = self.GetInsertionPoint()
639            self.SetForegroundColour("red")
640            self.SetBackgroundColour("yellow")
641            self.SetFocus()
642            self.Refresh() # this selects text on some Linuxes
643            self.SetSelection(0,0)   # unselect
644            self.SetInsertionPoint(ins) # put insertion point back
645        else: # valid input
646            self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
647            self.SetForegroundColour("black")
648            self.Refresh()
649
650    def ShowStringValidity(self,previousInvalid=True):
651        '''Check if input is valid. Anytime the input is
652        invalid, call self.OKcontrol (if defined) because it is fast.
653        If valid, check for any other invalid entries only when
654        changing from invalid to valid, since that is slower.
655       
656        :param bool previousInvalid: True if the TextCtrl contents were
657          invalid prior to the current change.
658         
659        '''
660        val = self.GetValue().strip()
661        if self.notBlank:
662            self.invalid = not val
663        else:
664            self.invalid = False
665        self._IndicateValidity()
666        if self.invalid:
667            if self.OKcontrol:
668                self.OKcontrol(False)
669        elif self.OKcontrol and previousInvalid:
670            self.OKcontrol(True)
671        # always store the result
672        if self.CIFinput: # for CIF make results ASCII
673            self.result[self.key] = val.encode('ascii','replace') 
674        else:
675            self.result[self.key] = val
676        log.LogVarChange(self.result,self.key)
677
678    def _GetStringValue(self,event):
679        '''Get string input and store.
680        '''
681        if event: event.Skip() # process keystroke
682        wx.CallAfter(self._SaveStringValue)
683       
684    def _SaveStringValue(self):
685        val = self.GetValue().strip()
686        # always store the result
687        if self.CIFinput: # for CIF make results ASCII
688            self.result[self.key] = val.encode('ascii','replace') 
689        else:
690            self.result[self.key] = val
691        log.LogVarChange(self.result,self.key)
692
693    def _onLeaveWindow(self,event):
694        '''If the mouse leaves the text box, save the result, if valid,
695        but (unlike _onLoseFocus) don't update the textbox contents.
696        '''
697        if not self.IsModified():   #ignore mouse crusing
698            return
699        if self.evaluated and not self.invalid: # deal with computed expressions
700            self.evaluated = False # expression has been recast as value, reset flag
701        if self.invalid: # don't update an invalid expression
702            if event: event.Skip()
703            return
704        self._setValue(self.result[self.key],show=False) # save value quietly
705        if self.OnLeave:
706            self.event = event
707            self.OnLeave(invalid=self.invalid,value=self.result[self.key],
708                tc=self,**self.OnLeaveArgs)
709        if event: event.Skip()
710           
711    def _onLoseFocus(self,event):
712        '''Enter has been pressed or focus transferred to another control,
713        Evaluate and update the current control contents
714        '''
715        if event: event.Skip()
716        if not self.IsModified():   #ignore mouse crusing
717            return
718        if self.evaluated: # deal with computed expressions
719            if self.invalid: # don't substitute for an invalid expression
720                return 
721            self.evaluated = False # expression has been recast as value, reset flag
722            self._setValue(self.result[self.key])
723        elif self.result is not None: # show formatted result, as Bob wants
724            if not self.invalid: # don't update an invalid expression
725                self._setValue(self.result[self.key])
726        if self.OnLeave:
727            self.event = event
728            self.OnLeave(invalid=self.invalid,value=self.result[self.key],
729                tc=self,**self.OnLeaveArgs)
730
731################################################################################
732class NumberValidator(wx.PyValidator):
733    '''A validator to be used with a TextCtrl to prevent
734    entering characters other than digits, signs, and for float
735    input, a period and exponents.
736   
737    The value is checked for validity after every keystroke
738      If an invalid number is entered, the box is highlighted.
739      If the number is valid, it is saved in result[key]
740
741    :param type typ: the base data type. Must be int or float.
742
743    :param bool positiveonly: If True, negative integers are not allowed
744      (default False). This prevents the + or - keys from being pressed.
745      Used with typ=int; ignored for typ=float.
746
747    :param number min: Minimum allowed value. If None (default) the
748      lower limit is unbounded
749
750    :param number max: Maximum allowed value. If None (default) the
751      upper limit is unbounded
752     
753    :param list exclLim: if True exclude min/max value ([exclMin,exclMax]);
754     (Default=[False,False])
755
756    :param dict/list result: List or dict where value should be placed when valid
757
758    :param any key: key to use for result (int for list)
759
760    :param function OKcontrol: function or class method to control
761      an OK button for a window.
762      Ignored if None (default)
763
764    :param bool CIFinput: allows use of a single '?' or '.' character
765      as valid input.
766     
767    '''
768    def __init__(self, typ, positiveonly=False, min=None, max=None,exclLim=[False,False],
769        result=None, key=None, OKcontrol=None, CIFinput=False):
770        'Create the validator'
771        if 'phoenix' in wx.version():
772            wx.Validator.__init__(self)
773        else:
774            wx.PyValidator.__init__(self)
775        # save passed parameters
776        self.typ = typ
777        self.positiveonly = positiveonly
778        self.min = min
779        self.max = max
780        self.exclLim = exclLim
781        self.result = result
782        self.key = key
783        self.OKcontrol = OKcontrol
784        self.CIFinput = CIFinput
785        # set allowed keys by data type
786        self.Bind(wx.EVT_CHAR, self.OnChar)
787        if self.typ == int and self.positiveonly:
788            self.validchars = '0123456789'
789        elif self.typ == int:
790            self.validchars = '0123456789+-'
791        elif self.typ == float:
792            # allow for above and sind, cosd, sqrt, tand, pi, and abbreviations
793            # also addition, subtraction, division, multiplication, exponentiation
794            self.validchars = '0123456789.-+eE/cosindcqrtap()*'
795        else:
796            self.validchars = None
797            return
798        if self.CIFinput:
799            self.validchars += '?.'
800    def Clone(self):
801        'Create a copy of the validator, a strange, but required component'
802        return NumberValidator(typ=self.typ, 
803                               positiveonly=self.positiveonly,
804                               min=self.min, max=self.max,
805                               result=self.result, key=self.key,
806                               OKcontrol=self.OKcontrol,
807                               CIFinput=self.CIFinput)
808    def TransferToWindow(self):
809        'Needed by validator, strange, but required component'
810        return True # Prevent wxDialog from complaining.
811    def TransferFromWindow(self):
812        'Needed by validator, strange, but required component'
813        return True # Prevent wxDialog from complaining.
814    def TestValid(self,tc):
815        '''Check if the value is valid by casting the input string
816        into the current type.
817
818        Set the invalid variable in the TextCtrl object accordingly.
819
820        If the value is valid, save it in the dict/list where
821        the initial value was stored, if appropriate.
822
823        :param wx.TextCtrl tc: A reference to the TextCtrl that the validator
824          is associated with.
825        '''
826        tc.invalid = False # assume valid
827        if self.CIFinput:
828            val = tc.GetValue().strip()
829            if val == '?' or val == '.':
830                self.result[self.key] = val
831                log.LogVarChange(self.result,self.key)
832                return
833        try:
834            val = self.typ(tc.GetValue())
835        except (ValueError, SyntaxError):
836            if self.typ is float: # for float values, see if an expression can be evaluated
837                val = G2py3.FormulaEval(tc.GetValue())
838                if val is None:
839                    tc.invalid = True
840                    return
841                else:
842                    tc.evaluated = True
843            else: 
844                tc.invalid = True
845                return
846        if self.max != None:
847            if val >= self.max and self.exclLim[1]:
848                tc.invalid = True
849            elif val > self.max:
850                tc.invalid = True
851        if self.min != None:
852            if val <= self.min and self.exclLim[0]:
853                tc.invalid = True
854            elif val < self.min:
855                tc.invalid = True  # invalid
856        if self.key is not None and self.result is not None and not tc.invalid:
857            self.result[self.key] = val
858            log.LogVarChange(self.result,self.key)
859
860    def ShowValidity(self,tc):
861        '''Set the control colors to show invalid input
862
863        :param wx.TextCtrl tc: A reference to the TextCtrl that the validator
864          is associated with.
865
866        '''
867        if tc.invalid:
868            ins = tc.GetInsertionPoint()
869            tc.SetForegroundColour("red")
870            tc.SetBackgroundColour("yellow")
871            tc.SetFocus()
872            tc.Refresh() # this selects text on some Linuxes
873            tc.SetSelection(0,0)   # unselect
874            tc.SetInsertionPoint(ins) # put insertion point back
875            return False
876        else: # valid input
877            tc.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
878            tc.SetForegroundColour("black")
879            tc.Refresh()
880            return True
881
882    def CheckInput(self,previousInvalid):
883        '''called to test every change to the TextCtrl for validity and
884        to change the appearance of the TextCtrl
885
886        Anytime the input is invalid, call self.OKcontrol
887        (if defined) because it is fast.
888        If valid, check for any other invalid entries only when
889        changing from invalid to valid, since that is slower.
890
891        :param bool previousInvalid: True if the TextCtrl contents were
892          invalid prior to the current change.
893        '''
894        tc = self.GetWindow()
895        self.TestValid(tc)
896        self.ShowValidity(tc)
897        # if invalid
898        if tc.invalid and self.OKcontrol:
899            self.OKcontrol(False)
900        if not tc.invalid and self.OKcontrol and previousInvalid:
901            self.OKcontrol(True)
902
903    def OnChar(self, event):
904        '''Called each type a key is pressed
905        ignores keys that are not allowed for int and float types
906        '''
907        key = event.GetKeyCode()
908        tc = self.GetWindow()
909        if key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER:
910            if tc.invalid:
911                self.CheckInput(True) 
912            else:
913                self.CheckInput(False) 
914            if event: event.Skip()
915            return
916        if key < wx.WXK_SPACE or key == wx.WXK_DELETE or key > 255: # control characters get processed
917            if event: event.Skip()
918            if tc.invalid:
919                wx.CallAfter(self.CheckInput,True) 
920            else:
921                wx.CallAfter(self.CheckInput,False) 
922            return
923        elif chr(key) in self.validchars: # valid char pressed?
924            if event: event.Skip()
925            if tc.invalid:
926                wx.CallAfter(self.CheckInput,True) 
927            else:
928                wx.CallAfter(self.CheckInput,False) 
929            return
930        if not wx.Validator_IsSilent(): wx.Bell()
931        return  # Returning without calling event.Skip, which eats the keystroke
932
933################################################################################
934class ASCIIValidator(wx.PyValidator):
935    '''A validator to be used with a TextCtrl to prevent
936    entering characters other than ASCII characters.
937   
938    The value is checked for validity after every keystroke
939      If an invalid number is entered, the box is highlighted.
940      If the number is valid, it is saved in result[key]
941
942    :param dict/list result: List or dict where value should be placed when valid
943
944    :param any key: key to use for result (int for list)
945
946    '''
947    def __init__(self, result=None, key=None):
948        'Create the validator'
949        import string
950        wx.PyValidator.__init__(self)
951        # save passed parameters
952        self.result = result
953        self.key = key
954        self.validchars = string.ascii_letters + string.digits + string.punctuation + string.whitespace
955        self.Bind(wx.EVT_CHAR, self.OnChar)
956    def Clone(self):
957        'Create a copy of the validator, a strange, but required component'
958        return ASCIIValidator(result=self.result, key=self.key)
959        tc = self.GetWindow()
960        tc.invalid = False # make sure the validity flag is defined in parent
961    def TransferToWindow(self):
962        'Needed by validator, strange, but required component'
963        return True # Prevent wxDialog from complaining.
964    def TransferFromWindow(self):
965        'Needed by validator, strange, but required component'
966        return True # Prevent wxDialog from complaining.
967    def TestValid(self,tc):
968        '''Check if the value is valid by casting the input string
969        into ASCII.
970
971        Save it in the dict/list where the initial value was stored
972
973        :param wx.TextCtrl tc: A reference to the TextCtrl that the validator
974          is associated with.
975        '''
976        self.result[self.key] = tc.GetValue().encode('ascii','replace')
977        log.LogVarChange(self.result,self.key)
978
979    def OnChar(self, event):
980        '''Called each type a key is pressed
981        ignores keys that are not allowed for int and float types
982        '''
983        key = event.GetKeyCode()
984        tc = self.GetWindow()
985        if key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER:
986            self.TestValid(tc)
987            if event: event.Skip()
988            return
989        if key < wx.WXK_SPACE or key == wx.WXK_DELETE or key > 255: # control characters get processed
990            if event: event.Skip()
991            self.TestValid(tc)
992            return
993        elif chr(key) in self.validchars: # valid char pressed?
994            if event: event.Skip()
995            self.TestValid(tc)
996            return
997        if not wx.Validator_IsSilent():
998            wx.Bell()
999        return  # Returning without calling event.Skip, which eats the keystroke
1000
1001################################################################################
1002def HorizontalLine(sizer,parent):
1003    '''Draws a horizontal line as wide as the window.
1004    This shows up on the Mac as a very thin line, no matter what I do
1005    '''
1006    line = wx.StaticLine(parent, size=(-1,3), style=wx.LI_HORIZONTAL)
1007    sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 5)
1008
1009################################################################################
1010class G2LoggedButton(wx.Button):
1011    '''A version of wx.Button that creates logging events. Bindings are saved
1012    in the object, and are looked up rather than directly set with a bind.
1013    An index to these buttons is saved as log.ButtonBindingLookup
1014    :param wx.Panel parent: parent widget
1015    :param int id: Id for button
1016    :param str label: label for button
1017    :param str locationcode: a label used internally to uniquely indentify the button
1018    :param function handler: a routine to call when the button is pressed
1019    '''
1020    def __init__(self,parent,id=wx.ID_ANY,label='',locationcode='',
1021                 handler=None,*args,**kwargs):
1022        super(self.__class__,self).__init__(parent,id,label,*args,**kwargs)
1023        self.label = label
1024        self.handler = handler
1025        self.locationcode = locationcode
1026        key = locationcode + '+' + label # hash code to find button
1027        self.Bind(wx.EVT_BUTTON,self.onPress)
1028        log.ButtonBindingLookup[key] = self
1029    def onPress(self,event):
1030        'create log event and call handler'
1031        log.MakeButtonLog(self.locationcode,self.label)
1032        self.handler(event)
1033       
1034################################################################################
1035class EnumSelector(wx.ComboBox):
1036    '''A customized :class:`wxpython.ComboBox` that selects items from a list
1037    of choices, but sets a dict (list) entry to the corresponding
1038    entry from the input list of values.
1039
1040    :param wx.Panel parent: the parent to the :class:`~wxpython.ComboBox` (usually a
1041      frame or panel)
1042    :param dict dct: a dict (or list) to contain the value set
1043      for the :class:`~wxpython.ComboBox`.
1044    :param item: the dict key (or list index) where ``dct[item]`` will
1045      be set to the value selected in the :class:`~wxpython.ComboBox`. Also, dct[item]
1046      contains the starting value shown in the widget. If the value
1047      does not match an entry in :data:`values`, the first value
1048      in :data:`choices` is used as the default, but ``dct[item]`` is
1049      not changed.   
1050    :param list choices: a list of choices to be displayed to the
1051      user such as
1052      ::
1053     
1054      ["default","option 1","option 2",]
1055
1056      Note that these options will correspond to the entries in
1057      :data:`values` (if specified) item by item.
1058    :param list values: a list of values that correspond to
1059      the options in :data:`choices`, such as
1060      ::
1061     
1062      [0,1,2]
1063     
1064      The default for :data:`values` is to use the same list as
1065      specified for :data:`choices`.
1066    :param (other): additional keyword arguments accepted by
1067      :class:`~wxpython.ComboBox` can be specified.
1068    '''
1069    def __init__(self,parent,dct,item,choices,values=None,**kw):
1070        if values is None:
1071            values = choices
1072        if dct[item] in values:
1073            i = values.index(dct[item])
1074        else:
1075            i = 0
1076        startval = choices[i]
1077        wx.ComboBox.__init__(self,parent,wx.ID_ANY,startval,
1078                             choices = choices,
1079                             style=wx.CB_DROPDOWN|wx.CB_READONLY,
1080                             **kw)
1081        self.choices = choices
1082        self.values = values
1083        self.dct = dct
1084        self.item = item
1085        self.Bind(wx.EVT_COMBOBOX, self.onSelection)
1086    def onSelection(self,event):
1087        # respond to a selection by setting the enum value in the CIF dictionary
1088        if self.GetValue() in self.choices: # should always be true!
1089            self.dct[self.item] = self.values[self.choices.index(self.GetValue())]
1090        else:
1091            self.dct[self.item] = self.values[0] # unknown
1092
1093################################################################################
1094class G2ChoiceButton(wx.Choice):
1095    '''A customized version of a wx.Choice that automatically initializes
1096    the control to match a supplied value and saves the choice directly
1097    into an array or list. Optionally a function can be called each time a
1098    choice is selected. The widget can be used with an array item that is set to
1099    to the choice by number (``indLoc[indKey]``) or by string value
1100    (``strLoc[strKey]``) or both. The initial value is taken from ``indLoc[indKey]``
1101    if not None or ``strLoc[strKey]`` if not None.
1102
1103    :param wx.Panel parent: name of panel or frame that will be
1104      the parent to the widget. Can be None.
1105    :param list choiceList: a list or tuple of choices to offer the user.
1106    :param dict/list indLoc: a dict or list with the initial value to be
1107      placed in the Choice button. If this is None, this is ignored.
1108    :param int/str indKey: the dict key or the list index for the value to be
1109      edited by the Choice button. If indLoc is not None then this
1110      must be specified and the ``indLoc[indKey]`` will be set. If the value
1111      for ``indLoc[indKey]`` is not None, it should be an integer in
1112      range(len(choiceList)). The Choice button will be initialized to the
1113      choice corresponding to the value in this element if not None.
1114    :param dict/list strLoc: a dict or list with the string value corresponding to
1115      indLoc/indKey. Default (None) means that this is not used.
1116    :param int/str strKey: the dict key or the list index for the string value
1117      The ``strLoc[strKey]`` element must exist or strLoc must be None (default).
1118    :param function onChoice: name of a function to call when the choice is made.
1119    '''
1120    def __init__(self,parent,choiceList,indLoc=None,indKey=None,strLoc=None,strKey=None,
1121                 onChoice=None,**kwargs):
1122        wx.Choice.__init__(self,parent,choices=choiceList,id=wx.ID_ANY,**kwargs)
1123        self.choiceList = choiceList
1124        self.indLoc = indLoc
1125        self.indKey = indKey
1126        self.strLoc = strLoc
1127        self.strKey = strKey
1128        self.onChoice = None
1129        self.SetSelection(wx.NOT_FOUND)
1130        if self.indLoc is not None and self.indLoc.get(self.indKey) is not None:
1131            self.SetSelection(self.indLoc[self.indKey])
1132            if self.strLoc is not None:
1133                self.strLoc[self.strKey] = self.GetStringSelection()
1134                log.LogVarChange(self.strLoc,self.strKey)
1135        elif self.strLoc is not None and self.strLoc.get(self.strKey) is not None:
1136            try:
1137                self.SetSelection(choiceList.index(self.strLoc[self.strKey]))
1138                if self.indLoc is not None:
1139                    self.indLoc[self.indKey] = self.GetSelection()
1140                    log.LogVarChange(self.indLoc,self.indKey)
1141            except ValueError:
1142                pass
1143        self.Bind(wx.EVT_CHOICE, self._OnChoice)
1144        #if self.strLoc is not None: # make sure strLoc gets initialized
1145        #    self._OnChoice(None) # note that onChoice will not be called
1146        self.onChoice = onChoice
1147    def _OnChoice(self,event):
1148        if self.indLoc is not None:
1149            self.indLoc[self.indKey] = self.GetSelection()
1150            log.LogVarChange(self.indLoc,self.indKey)
1151        if self.strLoc is not None:
1152            self.strLoc[self.strKey] = self.GetStringSelection()
1153            log.LogVarChange(self.strLoc,self.strKey)
1154        if self.onChoice:
1155            self.onChoice()
1156
1157##############################################################
1158# Custom checkbox that saves values into dict/list as used
1159class G2CheckBox(wx.CheckBox):
1160    '''A customized version of a CheckBox that automatically initializes
1161    the control to a supplied list or dict entry and updates that
1162    entry as the widget is used.
1163
1164    :param wx.Panel parent: name of panel or frame that will be
1165      the parent to the widget. Can be None.
1166    :param str label: text to put on check button
1167    :param dict/list loc: the dict or list with the initial value to be
1168      placed in the CheckBox.
1169    :param int/str key: the dict key or the list index for the value to be
1170      edited by the CheckBox. The ``loc[key]`` element must exist.
1171      The CheckBox will be initialized from this value.
1172      If the value is anything other that True (or 1), it will be taken as
1173      False.
1174    :param function OnChange: specifies a function or method that will be
1175      called when the CheckBox is changed (Default is None).
1176      The called function is supplied with one argument, the calling event.
1177    '''
1178    def __init__(self,parent,label,loc,key,OnChange=None):
1179        wx.CheckBox.__init__(self,parent,id=wx.ID_ANY,label=label)
1180        self.loc = loc
1181        self.key = key
1182        self.OnChange = OnChange
1183        self.SetValue(self.loc[self.key]==True)
1184        self.Bind(wx.EVT_CHECKBOX, self._OnCheckBox)
1185    def _OnCheckBox(self,event):
1186        self.loc[self.key] = self.GetValue()
1187        log.LogVarChange(self.loc,self.key)
1188        if self.OnChange: self.OnChange(event)
1189                   
1190################################################################################
1191#### Commonly used dialogs
1192################################################################################
1193def CallScrolledMultiEditor(parent,dictlst,elemlst,prelbl=[],postlbl=[],
1194                 title='Edit items',header='',size=(300,250),
1195                             CopyButton=False, ASCIIonly=False, **kw):
1196    '''Shell routine to call a ScrolledMultiEditor dialog. See
1197    :class:`ScrolledMultiEditor` for parameter definitions.
1198
1199    :returns: True if the OK button is pressed; False if the window is closed
1200      with the system menu or the Cancel button.
1201
1202    '''
1203    dlg = ScrolledMultiEditor(parent,dictlst,elemlst,prelbl,postlbl,
1204                              title,header,size,
1205                              CopyButton, ASCIIonly, **kw)
1206    if dlg.ShowModal() == wx.ID_OK:
1207        dlg.Destroy()
1208        return True
1209    else:
1210        dlg.Destroy()
1211        return False
1212
1213################################################################################
1214class ScrolledMultiEditor(wx.Dialog):
1215    '''Define a window for editing a potentially large number of dict- or
1216    list-contained values with validation for each item. Edited values are
1217    automatically placed in their source location. If invalid entries
1218    are provided, the TextCtrl is turned yellow and the OK button is disabled.
1219
1220    The type for each TextCtrl validation is determined by the
1221    initial value of the entry (int, float or string).
1222    Float values can be entered in the TextCtrl as numbers or also
1223    as algebraic expressions using operators + - / \* () and \*\*,
1224    in addition pi, sind(), cosd(), tand(), and sqrt() can be used,
1225    as well as appreviations s(), sin(), c(), cos(), t(), tan() and sq().
1226
1227    :param wx.Frame parent: name of parent window, or may be None
1228
1229    :param tuple dictlst: a list of dicts or lists containing values to edit
1230
1231    :param tuple elemlst: a list of keys for each item in a dictlst. Must have the
1232      same length as dictlst.
1233
1234    :param wx.Frame parent: name of parent window, or may be None
1235   
1236    :param tuple prelbl: a list of labels placed before the TextCtrl for each
1237      item (optional)
1238   
1239    :param tuple postlbl: a list of labels placed after the TextCtrl for each
1240      item (optional)
1241
1242    :param str title: a title to place in the frame of the dialog
1243
1244    :param str header: text to place at the top of the window. May contain
1245      new line characters.
1246
1247    :param wx.Size size: a size parameter that dictates the
1248      size for the scrolled region of the dialog. The default is
1249      (300,250).
1250
1251    :param bool CopyButton: if True adds a small button that copies the
1252      value for the current row to all fields below (default is False)
1253     
1254    :param bool ASCIIonly: if set as True will remove unicode characters from
1255      strings
1256     
1257    :param list minvals: optional list of minimum values for validation
1258      of float or int values. Ignored if value is None.
1259    :param list maxvals: optional list of maximum values for validation
1260      of float or int values. Ignored if value is None.
1261    :param list sizevals: optional list of wx.Size values for each input
1262      widget. Ignored if value is None.
1263     
1264    :param tuple checkdictlst: an optional list of dicts or lists containing bool
1265      values (similar to dictlst).
1266    :param tuple checkelemlst: an optional list of dicts or lists containing bool
1267      key values (similar to elemlst). Must be used with checkdictlst.
1268    :param string checklabel: a string to use for each checkbutton
1269     
1270    :returns: the wx.Dialog created here. Use method .ShowModal() to display it.
1271   
1272    *Example for use of ScrolledMultiEditor:*
1273
1274    ::
1275
1276        dlg = <pkg>.ScrolledMultiEditor(frame,dictlst,elemlst,prelbl,postlbl,
1277                                        header=header)
1278        if dlg.ShowModal() == wx.ID_OK:
1279             for d,k in zip(dictlst,elemlst):
1280                 print d[k]
1281
1282    *Example definitions for dictlst and elemlst:*
1283
1284    ::
1285     
1286          dictlst = (dict1,list1,dict1,list1)
1287          elemlst = ('a', 1, 2, 3)
1288
1289      This causes items dict1['a'], list1[1], dict1[2] and list1[3] to be edited.
1290   
1291    Note that these items must have int, float or str values assigned to
1292    them. The dialog will force these types to be retained. String values
1293    that are blank are marked as invalid.
1294    '''
1295   
1296    def __init__(self,parent,dictlst,elemlst,prelbl=[],postlbl=[],
1297                 title='Edit items',header='',size=(300,250),
1298                 CopyButton=False, ASCIIonly=False,
1299                 minvals=[],maxvals=[],sizevals=[],
1300                 checkdictlst=[], checkelemlst=[], checklabel=""):
1301        if len(dictlst) != len(elemlst):
1302            raise Exception("ScrolledMultiEditor error: len(dictlst) != len(elemlst) "+str(len(dictlst))+" != "+str(len(elemlst)))
1303        if len(checkdictlst) != len(checkelemlst):
1304            raise Exception("ScrolledMultiEditor error: len(checkdictlst) != len(checkelemlst) "+str(len(checkdictlst))+" != "+str(len(checkelemlst)))
1305        wx.Dialog.__init__( # create dialog & sizer
1306            self,parent,wx.ID_ANY,title,
1307            style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
1308        mainSizer = wx.BoxSizer(wx.VERTICAL)
1309        self.orig = []
1310        self.dictlst = dictlst
1311        self.elemlst = elemlst
1312        self.checkdictlst = checkdictlst
1313        self.checkelemlst = checkelemlst
1314        self.StartCheckValues = [checkdictlst[i][checkelemlst[i]] for i in range(len(checkdictlst))]
1315        self.ButtonIndex = {}
1316        for d,i in zip(dictlst,elemlst):
1317            self.orig.append(d[i])
1318        # add a header if supplied
1319        if header:
1320            subSizer = wx.BoxSizer(wx.HORIZONTAL)
1321            subSizer.Add((-1,-1),1,wx.EXPAND)
1322            subSizer.Add(wx.StaticText(self,wx.ID_ANY,header))
1323            subSizer.Add((-1,-1),1,wx.EXPAND)
1324            mainSizer.Add(subSizer,0,wx.EXPAND,0)
1325        # make OK button now, because we will need it for validation
1326        self.OKbtn = wx.Button(self, wx.ID_OK)
1327        self.OKbtn.SetDefault()
1328        # create scrolled panel and sizer
1329        panel = wxscroll.ScrolledPanel(self, wx.ID_ANY,size=size,
1330            style = wx.TAB_TRAVERSAL|wx.SUNKEN_BORDER)
1331        cols = 4
1332        if CopyButton: cols += 1
1333        subSizer = wx.FlexGridSizer(cols=cols,hgap=2,vgap=2)
1334        self.ValidatedControlsList = [] # make list of TextCtrls
1335        self.CheckControlsList = [] # make list of CheckBoxes
1336        for i,(d,k) in enumerate(zip(dictlst,elemlst)):
1337            if i >= len(prelbl): # label before TextCtrl, or put in a blank
1338                subSizer.Add((-1,-1)) 
1339            else:
1340                subSizer.Add(wx.StaticText(panel,wx.ID_ANY,str(prelbl[i])))
1341            kargs = {}
1342            if i < len(minvals):
1343                if minvals[i] is not None: kargs['min']=minvals[i]
1344            if i < len(maxvals):
1345                if maxvals[i] is not None: kargs['max']=maxvals[i]
1346            if i < len(sizevals):
1347                if sizevals[i]: kargs['size']=sizevals[i]
1348            if CopyButton:
1349                import wx.lib.colourselect as wscs  # is there a way to test?
1350                but = wscs.ColourSelect(label='v', # would like to use u'\u2193' or u'\u25BC' but not in WinXP
1351                    parent=panel,colour=(255,255,200),size=wx.Size(30,23),
1352                    style=wx.RAISED_BORDER)
1353                but.Bind(wx.EVT_BUTTON, self._OnCopyButton)
1354                but.SetToolTipString('Press to copy adjacent value to all rows below')
1355                self.ButtonIndex[but] = i
1356                subSizer.Add(but)
1357            # create the validated TextCrtl, store it and add it to the sizer
1358            ctrl = ValidatedTxtCtrl(panel,d,k,OKcontrol=self.ControlOKButton,ASCIIonly=ASCIIonly,
1359                                    **kargs)
1360            self.ValidatedControlsList.append(ctrl)
1361            subSizer.Add(ctrl)
1362            if i < len(postlbl): # label after TextCtrl, or put in a blank
1363                subSizer.Add(wx.StaticText(panel,wx.ID_ANY,str(postlbl[i])))
1364            else:
1365                subSizer.Add((-1,-1))
1366            if i < len(checkdictlst):
1367                ch = G2CheckBox(panel,checklabel,checkdictlst[i],checkelemlst[i])
1368                self.CheckControlsList.append(ch)
1369                subSizer.Add(ch)                   
1370            else:
1371                subSizer.Add((-1,-1))
1372        # finish up ScrolledPanel
1373        panel.SetSizer(subSizer)
1374        panel.SetAutoLayout(1)
1375        panel.SetupScrolling()
1376        # patch for wx 2.9 on Mac
1377        i,j= wx.__version__.split('.')[0:2]
1378        if int(i)+int(j)/10. > 2.8 and 'wxOSX' in wx.PlatformInfo:
1379            panel.SetMinSize((subSizer.GetSize()[0]+30,panel.GetSize()[1]))       
1380        mainSizer.Add(panel,1, wx.ALL|wx.EXPAND,1)
1381
1382        # Sizer for OK/Close buttons. N.B. on Close changes are discarded
1383        # by restoring the initial values
1384        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
1385        btnsizer.Add(self.OKbtn)
1386        btn = wx.Button(self, wx.ID_CLOSE,"Cancel") 
1387        btn.Bind(wx.EVT_BUTTON,self._onClose)
1388        btnsizer.Add(btn)
1389        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
1390        # size out the window. Set it to be enlarged but not made smaller
1391        self.SetSizer(mainSizer)
1392        mainSizer.Fit(self)
1393        self.SetMinSize(self.GetSize())
1394
1395    def _OnCopyButton(self,event):
1396        'Implements the copy down functionality'
1397        but = event.GetEventObject()
1398        n = self.ButtonIndex.get(but)
1399        if n is None: return
1400        for i,(d,k,ctrl) in enumerate(zip(self.dictlst,self.elemlst,self.ValidatedControlsList)):
1401            if i < n: continue
1402            if i == n:
1403                val = d[k]
1404                continue
1405            d[k] = val
1406            ctrl.SetValue(val)
1407        for i in range(len(self.checkdictlst)):
1408            if i < n: continue
1409            self.checkdictlst[i][self.checkelemlst[i]] = self.checkdictlst[n][self.checkelemlst[n]]
1410            self.CheckControlsList[i].SetValue(self.checkdictlst[i][self.checkelemlst[i]])
1411    def _onClose(self,event):
1412        'Used on Cancel: Restore original values & close the window'
1413        for d,i,v in zip(self.dictlst,self.elemlst,self.orig):
1414            d[i] = v
1415        for i in range(len(self.checkdictlst)):
1416            self.checkdictlst[i][self.checkelemlst[i]] = self.StartCheckValues[i]
1417        self.EndModal(wx.ID_CANCEL)
1418       
1419    def ControlOKButton(self,setvalue):
1420        '''Enable or Disable the OK button for the dialog. Note that this is
1421        passed into the ValidatedTxtCtrl for use by validators.
1422
1423        :param bool setvalue: if True, all entries in the dialog are
1424          checked for validity. if False then the OK button is disabled.
1425
1426        '''
1427        if setvalue: # turn button on, do only if all controls show as valid
1428            for ctrl in self.ValidatedControlsList:
1429                if ctrl.invalid:
1430                    self.OKbtn.Disable()
1431                    return
1432            else:
1433                self.OKbtn.Enable()
1434        else:
1435            self.OKbtn.Disable()
1436
1437###############################################  Multichoice Dialog with set all, toggle & filter options
1438class G2MultiChoiceDialog(wx.Dialog):
1439    '''A dialog similar to MultiChoiceDialog except that buttons are
1440    added to set all choices and to toggle all choices.
1441
1442    :param wx.Frame ParentFrame: reference to parent frame
1443    :param str title: heading above list of choices
1444    :param str header: Title to place on window frame
1445    :param list ChoiceList: a list of choices where one more will be selected
1446    :param bool toggle: If True (default) the toggle and select all buttons
1447      are displayed
1448    :param bool monoFont: If False (default), use a variable-spaced font;
1449      if True use a equally-spaced font.
1450    :param bool filterBox: If True (default) an input widget is placed on
1451      the window and only entries matching the entered text are shown.
1452    :param dict extraOpts: a dict containing a entries of form label_i and value_i with extra
1453      options to present to the user, where value_i is the default value.
1454      Options are listed ordered by the value_i values.
1455    :param kw: optional keyword parameters for the wx.Dialog may
1456      be included such as size [which defaults to `(320,310)`] and
1457      style (which defaults to `wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL`);
1458      note that `wx.OK` and `wx.CANCEL` style items control
1459      the presence of the eponymous buttons in the dialog.
1460    :returns: the name of the created dialog 
1461    '''
1462    def __init__(self,parent, title, header, ChoiceList, toggle=True,
1463                 monoFont=False, filterBox=True, extraOpts={}, **kw):
1464        # process keyword parameters, notably style
1465        options = {'size':(320,310), # default Frame keywords
1466                   'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
1467                   }
1468        options.update(kw)
1469        self.ChoiceList = ['%4d) %s'%(i,item) for i,item in enumerate(ChoiceList)] # numbered list of choices (list of str values)
1470        self.Selections = len(self.ChoiceList) * [False,] # selection status for each choice (list of bools)
1471        self.filterlist = range(len(self.ChoiceList)) # list of the choice numbers that have been filtered (list of int indices)
1472        self.Stride = 1
1473        if options['style'] & wx.OK:
1474            useOK = True
1475            options['style'] ^= wx.OK
1476        else:
1477            useOK = False
1478        if options['style'] & wx.CANCEL:
1479            useCANCEL = True
1480            options['style'] ^= wx.CANCEL
1481        else:
1482            useCANCEL = False       
1483        # create the dialog frame
1484        wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
1485        # fill the dialog
1486        Sizer = wx.BoxSizer(wx.VERTICAL)
1487        topSizer = wx.BoxSizer(wx.HORIZONTAL)
1488        topSizer.Add(wx.StaticText(self,wx.ID_ANY,title,size=(-1,35)),
1489            1,wx.ALL|wx.EXPAND|WACV,1)
1490        if filterBox:
1491            self.timer = wx.Timer()
1492            self.timer.Bind(wx.EVT_TIMER,self.Filter)
1493            topSizer.Add(wx.StaticText(self,wx.ID_ANY,'Name \nFilter: '),0,wx.ALL|WACV,1)
1494            self.filterBox = wx.TextCtrl(self, wx.ID_ANY, size=(80,-1),style=wx.TE_PROCESS_ENTER)
1495            self.filterBox.Bind(wx.EVT_TEXT,self.onChar)
1496            self.filterBox.Bind(wx.EVT_TEXT_ENTER,self.Filter)
1497            topSizer.Add(self.filterBox,0,wx.ALL|WACV,0)
1498        Sizer.Add(topSizer,0,wx.ALL|wx.EXPAND,8)
1499        self.settingRange = False
1500        self.rangeFirst = None
1501        self.clb = wx.CheckListBox(self, wx.ID_ANY, (30,30), wx.DefaultSize, self.ChoiceList)
1502        self.clb.Bind(wx.EVT_CHECKLISTBOX,self.OnCheck)
1503        if monoFont:
1504            font1 = wx.Font(self.clb.GetFont().GetPointSize(),
1505                            wx.MODERN, wx.NORMAL, wx.NORMAL, False)
1506            self.clb.SetFont(font1)
1507        Sizer.Add(self.clb,1,wx.LEFT|wx.RIGHT|wx.EXPAND,10)
1508        Sizer.Add((-1,10))
1509        # set/toggle buttons
1510        if toggle:
1511            tSizer = wx.FlexGridSizer(cols=2,hgap=5,vgap=5)
1512            tSizer.Add(wx.StaticText(self,label=' Apply stride:'),0,WACV)
1513            numbs = [str(i+1) for i in range(9)]+[str(2*i+10) for i in range(6)]
1514            self.stride = wx.ComboBox(self,value='1',choices=numbs,style=wx.CB_READONLY|wx.CB_DROPDOWN)
1515            self.stride.Bind(wx.EVT_COMBOBOX,self.OnStride)
1516            tSizer.Add(self.stride,0,WACV)
1517            setBut = wx.Button(self,wx.ID_ANY,'Set All')
1518            setBut.Bind(wx.EVT_BUTTON,self._SetAll)
1519            tSizer.Add(setBut)
1520            togBut = wx.Button(self,wx.ID_ANY,'Toggle All')
1521            togBut.Bind(wx.EVT_BUTTON,self._ToggleAll)
1522            tSizer.Add(togBut)
1523            self.rangeBut = wx.ToggleButton(self,wx.ID_ANY,'Set Range')
1524            self.rangeBut.Bind(wx.EVT_TOGGLEBUTTON,self.SetRange)
1525            tSizer.Add(self.rangeBut)
1526            self.rangeCapt = wx.StaticText(self,wx.ID_ANY,'')
1527            tSizer.Add(self.rangeCapt)
1528            Sizer.Add(tSizer,0,wx.LEFT,12)
1529        # Extra widgets
1530        Sizer.Add((-1,5),0,wx.LEFT,0)
1531        bSizer = wx.BoxSizer(wx.VERTICAL)
1532        for lbl in sorted(extraOpts.keys()):
1533            if not lbl.startswith('label'): continue
1534            key = lbl.replace('label','value')
1535            if key not in extraOpts: continue
1536            eSizer = wx.BoxSizer(wx.HORIZONTAL)
1537            if type(extraOpts[key]) is bool:
1538                eSizer.Add(G2CheckBox(self,extraOpts[lbl],extraOpts,key))
1539            else:
1540                eSizer.Add(wx.StaticText(self,wx.ID_ANY,extraOpts[lbl]))
1541                eSizer.Add(ValidatedTxtCtrl(self,extraOpts,key))
1542            bSizer.Add(eSizer,0,wx.LEFT,0)
1543        Sizer.Add(bSizer,0,wx.CENTER,0)
1544        Sizer.Add((-1,5),0,wx.LEFT,0)
1545        # OK/Cancel buttons
1546        btnsizer = wx.StdDialogButtonSizer()
1547        if useOK:
1548            self.OKbtn = wx.Button(self, wx.ID_OK)
1549            self.OKbtn.SetDefault()
1550            btnsizer.AddButton(self.OKbtn)
1551            self.OKbtn.Bind(wx.EVT_BUTTON,self.onOk)
1552        if useCANCEL:
1553            btn = wx.Button(self, wx.ID_CANCEL)
1554            btn.Bind(wx.EVT_BUTTON,self.onCancel)
1555            btnsizer.AddButton(btn)
1556        btnsizer.Realize()
1557        Sizer.Add((-1,5))
1558        Sizer.Add(btnsizer,0,wx.ALIGN_RIGHT,50)
1559        Sizer.Add((-1,20))
1560        # OK done, let's get outa here
1561        self.SetSizer(Sizer)
1562        Sizer.Fit(self)
1563        self.CenterOnParent()
1564       
1565    def onOk(self,event):
1566        parent = self.GetParent()
1567        parent.Raise()
1568        self.EndModal(wx.ID_OK)             
1569       
1570    def onCancel(self,event):
1571        parent = self.GetParent()
1572        parent.Raise()
1573        self.EndModal(wx.ID_CANCEL)
1574       
1575    def OnStride(self,event):
1576        self.Stride = int(self.stride.GetValue())
1577
1578    def SetRange(self,event):
1579        '''Respond to a press of the Set Range button. Set the range flag and
1580        the caption next to the button
1581        '''
1582        self.settingRange = self.rangeBut.GetValue()
1583        if self.settingRange:
1584            self.rangeCapt.SetLabel('Select range start')
1585        else:
1586            self.rangeCapt.SetLabel('')           
1587        self.rangeFirst = None
1588       
1589    def GetSelections(self):
1590        'Returns a list of the indices for the selected choices'
1591        # update self.Selections with settings for displayed items
1592        for i in range(len(self.filterlist)):
1593            self.Selections[self.filterlist[i]] = self.clb.IsChecked(i)
1594        # return all selections, shown or hidden
1595        return [i for i in range(len(self.Selections)) if self.Selections[i]]
1596       
1597    def SetSelections(self,selList):
1598        '''Sets the selection indices in selList as selected. Resets any previous
1599        selections for compatibility with wx.MultiChoiceDialog. Note that
1600        the state for only the filtered items is shown.
1601
1602        :param list selList: indices of items to be selected. These indices
1603          are referenced to the order in self.ChoiceList
1604        '''
1605        self.Selections = len(self.ChoiceList) * [False,] # reset selections
1606        for sel in selList:
1607            self.Selections[sel] = True
1608        self._ShowSelections()
1609
1610    def _ShowSelections(self):
1611        'Show the selection state for displayed items'
1612        self.clb.SetChecked(
1613            [i for i in range(len(self.filterlist)) if self.Selections[self.filterlist[i]]]
1614            ) # Note anything previously checked will be cleared.
1615           
1616    def _SetAll(self,event):
1617        'Set all viewed choices on'
1618        self.clb.SetChecked(range(0,len(self.filterlist),self.Stride))
1619        self.stride.SetValue('1')
1620        self.Stride = 1
1621       
1622    def _ToggleAll(self,event):
1623        'flip the state of all viewed choices'
1624        for i in range(len(self.filterlist)):
1625            self.clb.Check(i,not self.clb.IsChecked(i))
1626           
1627    def onChar(self,event):
1628        'Respond to keyboard events in the Filter box'
1629        self.OKbtn.Enable(False)
1630        if self.timer.IsRunning():
1631            self.timer.Stop()
1632        self.timer.Start(1000,oneShot=True)
1633        if event: event.Skip()
1634       
1635    def OnCheck(self,event):
1636        '''for CheckListBox events; if Set Range is in use, this sets/clears all
1637        entries in range between start and end according to the value in start.
1638        Repeated clicks on the start change the checkbox state, but do not trigger
1639        the range copy.
1640        The caption next to the button is updated on the first button press.
1641        '''
1642        if self.settingRange:
1643            id = event.GetInt()
1644            if self.rangeFirst is None:
1645                name = self.clb.GetString(id)
1646                self.rangeCapt.SetLabel(name+' to...')
1647                self.rangeFirst = id
1648            elif self.rangeFirst == id:
1649                pass
1650            else:
1651                for i in range(min(self.rangeFirst,id), max(self.rangeFirst,id)+1,self.Stride):
1652                    self.clb.Check(i,self.clb.IsChecked(self.rangeFirst))
1653                self.rangeBut.SetValue(False)
1654                self.rangeCapt.SetLabel('')
1655            return
1656       
1657    def Filter(self,event):
1658        '''Read text from filter control and select entries that match. Called by
1659        Timer after a delay with no input or if Enter is pressed.
1660        '''
1661        if self.timer.IsRunning():
1662            self.timer.Stop()
1663        self.GetSelections() # record current selections
1664        txt = self.filterBox.GetValue()
1665        self.clb.Clear()
1666       
1667        self.Update()
1668        self.filterlist = []
1669        if txt:
1670            txt = txt.lower()
1671            ChoiceList = []
1672            for i,item in enumerate(self.ChoiceList):
1673                if item.lower().find(txt) != -1:
1674                    ChoiceList.append(item)
1675                    self.filterlist.append(i)
1676        else:
1677            self.filterlist = range(len(self.ChoiceList))
1678            ChoiceList = self.ChoiceList
1679        self.clb.AppendItems(ChoiceList)
1680        self._ShowSelections()
1681        self.OKbtn.Enable(True)
1682
1683def SelectEdit1Var(G2frame,array,labelLst,elemKeysLst,dspLst,refFlgElem):
1684    '''Select a variable from a list, then edit it and select histograms
1685    to copy it to.
1686
1687    :param wx.Frame G2frame: main GSAS-II frame
1688    :param dict array: the array (dict or list) where values to be edited are kept
1689    :param list labelLst: labels for each data item
1690    :param list elemKeysLst: a list of lists of keys needed to be applied (see below)
1691      to obtain the value of each parameter
1692    :param list dspLst: list list of digits to be displayed (10,4) is 10 digits
1693      with 4 decimal places. Can be None.
1694    :param list refFlgElem: a list of lists of keys needed to be applied (see below)
1695      to obtain the refine flag for each parameter or None if the parameter
1696      does not have refine flag.
1697
1698    Example::
1699      array = data
1700      labelLst = ['v1','v2']
1701      elemKeysLst = [['v1'], ['v2',0]]
1702      refFlgElem = [None, ['v2',1]]
1703
1704     * The value for v1 will be in data['v1'] and this cannot be refined while,
1705     * The value for v2 will be in data['v2'][0] and its refinement flag is data['v2'][1]
1706    '''
1707    def unkey(dct,keylist):
1708        '''dive into a nested set of dicts/lists applying keys in keylist
1709        consecutively
1710        '''
1711        d = dct
1712        for k in keylist:
1713            d = d[k]
1714        return d
1715
1716    def OnChoice(event):
1717        'Respond when a parameter is selected in the Choice box'
1718        if 'phoenix' in wx.version():
1719            valSizer.Clear(True)
1720        else:
1721            valSizer.DeleteWindows()
1722        lbl = event.GetString()
1723        copyopts['currentsel'] = lbl
1724        i = labelLst.index(lbl)
1725        OKbtn.Enable(True)
1726        ch.SetLabel(lbl)
1727        args = {}
1728        if dspLst[i]:
1729            args = {'nDig':dspLst[i]}
1730        Val = ValidatedTxtCtrl(
1731            dlg,
1732            unkey(array,elemKeysLst[i][:-1]),
1733            elemKeysLst[i][-1],
1734            **args)
1735        copyopts['startvalue'] = unkey(array,elemKeysLst[i])
1736        #unkey(array,elemKeysLst[i][:-1])[elemKeysLst[i][-1]] =
1737        valSizer.Add(Val,0,wx.LEFT,5)
1738        dlg.SendSizeEvent()
1739       
1740    # SelectEdit1Var execution begins here
1741    saveArray = copy.deepcopy(array) # keep original values
1742    TreeItemType = G2frame.GPXtree.GetItemText(G2frame.PickId)
1743    copyopts = {'InTable':False,"startvalue":None,'currentsel':None}       
1744    hst = G2frame.GPXtree.GetItemText(G2frame.PatternId)
1745    histList = G2pdG.GetHistsLikeSelected(G2frame)
1746    if not histList:
1747        G2frame.ErrorDialog('No match','No histograms match '+hst,G2frame)
1748        return
1749    dlg = wx.Dialog(G2frame,wx.ID_ANY,'Set a parameter value',
1750        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
1751    mainSizer = wx.BoxSizer(wx.VERTICAL)
1752    mainSizer.Add((5,5))
1753    subSizer = wx.BoxSizer(wx.HORIZONTAL)
1754    subSizer.Add((-1,-1),1,wx.EXPAND)
1755    subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Select a parameter and set a new value'))
1756    subSizer.Add((-1,-1),1,wx.EXPAND)
1757    mainSizer.Add(subSizer,0,wx.EXPAND,0)
1758    mainSizer.Add((0,10))
1759
1760    subSizer = wx.FlexGridSizer(0,2,5,0)
1761    subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Parameter: '))
1762    ch = wx.Choice(dlg, wx.ID_ANY, choices = sorted(labelLst))
1763    ch.SetSelection(-1)
1764    ch.Bind(wx.EVT_CHOICE, OnChoice)
1765    subSizer.Add(ch)
1766    subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Value: '))
1767    valSizer = wx.BoxSizer(wx.HORIZONTAL)
1768    subSizer.Add(valSizer)
1769    mainSizer.Add(subSizer)
1770
1771    mainSizer.Add((-1,20))
1772    subSizer = wx.BoxSizer(wx.HORIZONTAL)
1773    subSizer.Add(G2CheckBox(dlg, 'Edit in table ', copyopts, 'InTable'))
1774    mainSizer.Add(subSizer)
1775
1776    btnsizer = wx.StdDialogButtonSizer()
1777    OKbtn = wx.Button(dlg, wx.ID_OK,'Continue')
1778    OKbtn.Enable(False)
1779    OKbtn.SetDefault()
1780    OKbtn.Bind(wx.EVT_BUTTON,lambda event: dlg.EndModal(wx.ID_OK))
1781    btnsizer.AddButton(OKbtn)
1782    btn = wx.Button(dlg, wx.ID_CANCEL)
1783    btnsizer.AddButton(btn)
1784    btnsizer.Realize()
1785    mainSizer.Add((-1,5),1,wx.EXPAND,1)
1786    mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER,0)
1787    mainSizer.Add((-1,10))
1788
1789    dlg.SetSizer(mainSizer)
1790    dlg.CenterOnParent()
1791    if dlg.ShowModal() != wx.ID_OK:
1792        array.update(saveArray)
1793        dlg.Destroy()
1794        return
1795    dlg.Destroy()
1796
1797    copyList = []
1798    lbl = copyopts['currentsel']
1799    dlg = G2MultiChoiceDialog(G2frame,'Copy parameter '+lbl+' from\n'+hst,
1800        'Copy parameters', histList)
1801    dlg.CenterOnParent()
1802    try:
1803        if dlg.ShowModal() == wx.ID_OK:
1804            for i in dlg.GetSelections(): 
1805                copyList.append(histList[i])
1806        else:
1807            # reset the parameter since cancel was pressed
1808            array.update(saveArray)
1809            return
1810    finally:
1811        dlg.Destroy()
1812
1813    prelbl = [hst]
1814    i = labelLst.index(lbl)
1815    keyLst = elemKeysLst[i]
1816    refkeys = refFlgElem[i]
1817    dictlst = [unkey(array,keyLst[:-1])]
1818    if refkeys is not None:
1819        refdictlst = [unkey(array,refkeys[:-1])]
1820    else:
1821        refdictlst = None
1822    Id = G2gd.GetGPXtreeItemId(G2frame,G2frame.root,hst)
1823    hstData = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,Id,'Instrument Parameters'))[0]
1824    for h in copyList:
1825        Id = G2gd.GetGPXtreeItemId(G2frame,G2frame.root,h)
1826        instData = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,Id,'Instrument Parameters'))[0]
1827        if len(hstData) != len(instData) or hstData['Type'][0] != instData['Type'][0]:  #don't mix data types or lam & lam1/lam2 parms!
1828            print (h+' not copied - instrument parameters not commensurate')
1829            continue
1830        hData = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,Id,TreeItemType))
1831        if TreeItemType == 'Instrument Parameters':
1832            hData = hData[0]
1833        #copy the value if it is changed or we will not edit in a table
1834        valNow = unkey(array,keyLst)
1835        if copyopts['startvalue'] != valNow or not copyopts['InTable']:
1836            unkey(hData,keyLst[:-1])[keyLst[-1]] = valNow
1837        prelbl += [h]
1838        dictlst += [unkey(hData,keyLst[:-1])]
1839        if refdictlst is not None:
1840            refdictlst += [unkey(hData,refkeys[:-1])]
1841    if refdictlst is None:
1842        args = {}
1843    else:
1844        args = {'checkdictlst':refdictlst,
1845                'checkelemlst':len(dictlst)*[refkeys[-1]],
1846                'checklabel':'Refine?'}
1847    if copyopts['InTable']:
1848        dlg = ScrolledMultiEditor(
1849            G2frame,dictlst,
1850            len(dictlst)*[keyLst[-1]],prelbl,
1851            header='Editing parameter '+lbl,
1852            CopyButton=True,**args)
1853        dlg.CenterOnParent()
1854        if dlg.ShowModal() != wx.ID_OK:
1855            array.update(saveArray)
1856        dlg.Destroy()
1857
1858################################################################        Single choice Dialog with filter options
1859class G2SingleChoiceDialog(wx.Dialog):
1860    '''A dialog similar to wx.SingleChoiceDialog except that a filter can be
1861    added.
1862
1863    :param wx.Frame ParentFrame: reference to parent frame
1864    :param str title: heading above list of choices
1865    :param str header: Title to place on window frame
1866    :param list ChoiceList: a list of choices where one will be selected
1867    :param bool monoFont: If False (default), use a variable-spaced font;
1868      if True use a equally-spaced font.
1869    :param bool filterBox: If True (default) an input widget is placed on
1870      the window and only entries matching the entered text are shown.
1871    :param kw: optional keyword parameters for the wx.Dialog may
1872      be included such as size [which defaults to `(320,310)`] and
1873      style (which defaults to ``wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.CENTRE | wx.OK | wx.CANCEL``);
1874      note that ``wx.OK`` and ``wx.CANCEL`` controls
1875      the presence of the eponymous buttons in the dialog.
1876    :returns: the name of the created dialog
1877    '''
1878    def __init__(self,parent, title, header, ChoiceList, 
1879                 monoFont=False, filterBox=True, **kw):
1880        # process keyword parameters, notably style
1881        options = {'size':(320,310), # default Frame keywords
1882                   'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
1883                   }
1884        options.update(kw)
1885        self.ChoiceList = ChoiceList
1886        self.filterlist = range(len(self.ChoiceList))
1887        if options['style'] & wx.OK:
1888            useOK = True
1889            options['style'] ^= wx.OK
1890        else:
1891            useOK = False
1892        if options['style'] & wx.CANCEL:
1893            useCANCEL = True
1894            options['style'] ^= wx.CANCEL
1895        else:
1896            useCANCEL = False       
1897        # create the dialog frame
1898        wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
1899        # fill the dialog
1900        Sizer = wx.BoxSizer(wx.VERTICAL)
1901        topSizer = wx.BoxSizer(wx.HORIZONTAL)
1902        h = max(35,17*int(len(title)/26.+1)) # adjust height of title box with guessed # of lines
1903        topSizer.Add(
1904            wx.StaticText(self,wx.ID_ANY,title,size=(-1,h)),
1905            1,wx.ALL|wx.EXPAND|WACV,1)
1906        if filterBox:
1907            self.timer = wx.Timer()
1908            self.timer.Bind(wx.EVT_TIMER,self.Filter)
1909            topSizer.Add(wx.StaticText(self,wx.ID_ANY,'Filter: '),0,wx.ALL,1)
1910            self.filterBox = wx.TextCtrl(self, wx.ID_ANY, size=(80,-1),
1911                                         style=wx.TE_PROCESS_ENTER)
1912            self.filterBox.Bind(wx.EVT_CHAR,self.onChar)
1913            self.filterBox.Bind(wx.EVT_TEXT_ENTER,self.Filter)
1914            topSizer.Add(self.filterBox,0,wx.ALL,0)
1915        Sizer.Add(topSizer,0,wx.ALL|wx.EXPAND,8)
1916        self.clb = wx.ListBox(self, wx.ID_ANY, (30,30), wx.DefaultSize, ChoiceList)
1917        self.clb.Bind(wx.EVT_LEFT_DCLICK,self.onDoubleClick)
1918        if monoFont:
1919            font1 = wx.Font(self.clb.GetFont().GetPointSize(),
1920                            wx.MODERN, wx.NORMAL, wx.NORMAL, False)
1921            self.clb.SetFont(font1)
1922        Sizer.Add(self.clb,1,wx.LEFT|wx.RIGHT|wx.EXPAND,10)
1923        Sizer.Add((-1,10))
1924        # OK/Cancel buttons
1925        btnsizer = wx.StdDialogButtonSizer()
1926        if useOK:
1927            self.OKbtn = wx.Button(self, wx.ID_OK)
1928            self.OKbtn.SetDefault()
1929            btnsizer.AddButton(self.OKbtn)
1930        if useCANCEL:
1931            btn = wx.Button(self, wx.ID_CANCEL)
1932            btnsizer.AddButton(btn)
1933        btnsizer.Realize()
1934        Sizer.Add((-1,5))
1935        Sizer.Add(btnsizer,0,wx.ALIGN_RIGHT,50)
1936        Sizer.Add((-1,20))
1937        # OK done, let's get outa here
1938        self.SetSizer(Sizer)
1939    def GetSelection(self):
1940        'Returns the index of the selected choice'
1941        i = self.clb.GetSelection()
1942        if i < 0 or i >= len(self.filterlist):
1943            return wx.NOT_FOUND
1944        return self.filterlist[i]
1945    def onChar(self,event):
1946        self.OKbtn.Enable(False)
1947        if self.timer.IsRunning():
1948            self.timer.Stop()
1949        self.timer.Start(1000,oneShot=True)
1950        if event: event.Skip()
1951    def Filter(self,event):
1952        if self.timer.IsRunning():
1953            self.timer.Stop()
1954        txt = self.filterBox.GetValue()
1955        self.clb.Clear()
1956        self.Update()
1957        self.filterlist = []
1958        if txt:
1959            txt = txt.lower()
1960            ChoiceList = []
1961            for i,item in enumerate(self.ChoiceList):
1962                if item.lower().find(txt) != -1:
1963                    ChoiceList.append(item)
1964                    self.filterlist.append(i)
1965        else:
1966            self.filterlist = range(len(self.ChoiceList))
1967            ChoiceList = self.ChoiceList
1968        self.clb.AppendItems(ChoiceList)
1969        self.OKbtn.Enable(True)
1970    def onDoubleClick(self,event):
1971        self.EndModal(wx.ID_OK)
1972       
1973################################################################################
1974class FlagSetDialog(wx.Dialog):
1975    ''' Creates popup with table of variables to be checked for e.g. refinement flags
1976    '''
1977    def __init__(self,parent,title,colnames,rownames,flags):
1978        wx.Dialog.__init__(self,parent,-1,title,
1979            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
1980        self.panel = None
1981        self.colnames = colnames
1982        self.rownames = rownames
1983        self.flags = flags
1984        self.newflags = copy.copy(flags)
1985        self.Draw()
1986       
1987    def Draw(self):
1988        Indx = {}
1989       
1990        def OnSelection(event):
1991            Obj = event.GetEventObject()
1992            [name,ia] = Indx[Obj.GetId()]
1993            self.newflags[name][ia] = Obj.GetValue()
1994           
1995        if self.panel:
1996            self.panel.DestroyChildren()  #safe: wx.Panel
1997            self.panel.Destroy()
1998        self.panel = wx.Panel(self)
1999        mainSizer = wx.BoxSizer(wx.VERTICAL)
2000        flagSizer = wx.FlexGridSizer(0,len(self.colnames),5,5)
2001        for item in self.colnames:
2002            flagSizer.Add(wx.StaticText(self.panel,label=item),0,WACV)
2003        for ia,atm in enumerate(self.rownames):
2004            flagSizer.Add(wx.StaticText(self.panel,label=atm),0,WACV)
2005            for name in self.colnames[1:]:
2006                if self.flags[name][ia]:
2007                    self.newflags[name][ia] = False     #default is off
2008                    flg = wx.CheckBox(self.panel,-1,label='')
2009                    flg.Bind(wx.EVT_CHECKBOX,OnSelection)
2010                    Indx[flg.GetId()] = [name,ia]
2011                    flagSizer.Add(flg,0,WACV)
2012                else:
2013                    flagSizer.Add(wx.StaticText(self.panel,label='na'),0,WACV)
2014           
2015        mainSizer.Add(flagSizer,0)
2016        OkBtn = wx.Button(self.panel,-1,"Ok")
2017        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
2018        CancelBtn = wx.Button(self.panel,-1,'Cancel')
2019        CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
2020        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
2021        btnSizer.Add((20,20),1)
2022        btnSizer.Add(OkBtn)
2023        btnSizer.Add(CancelBtn)
2024        btnSizer.Add((20,20),1)
2025        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
2026        self.panel.SetSizer(mainSizer)
2027        self.panel.Fit()
2028        self.Fit()
2029       
2030    def GetSelection(self):
2031        return self.newflags
2032
2033    def OnOk(self,event):
2034        parent = self.GetParent()
2035        parent.Raise()
2036        self.EndModal(wx.ID_OK)             
2037       
2038    def OnCancel(self,event):
2039        parent = self.GetParent()
2040        parent.Raise()
2041        self.EndModal(wx.ID_CANCEL)
2042
2043###################################################################,#############
2044def G2MessageBox(parent,msg,title='Error'):
2045    '''Simple code to display a error or warning message
2046    '''
2047    dlg = wx.MessageDialog(parent,StripIndents(msg), title, wx.OK)
2048    dlg.ShowModal()
2049    dlg.Destroy()
2050   
2051################################################################################
2052class PickTwoDialog(wx.Dialog):
2053    '''This does not seem to be in use
2054    '''
2055    def __init__(self,parent,title,prompt,names,choices):
2056        wx.Dialog.__init__(self,parent,-1,title, 
2057            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
2058        self.panel = None
2059        self.prompt = prompt
2060        self.choices = choices
2061        self.names = names
2062        self.Draw()
2063
2064    def Draw(self):
2065        Indx = {}
2066       
2067        def OnSelection(event):
2068            Obj = event.GetEventObject()
2069            id = Indx[Obj.GetId()]
2070            self.choices[id] = Obj.GetValue().encode()  #to avoid Unicode versions
2071            self.Draw()
2072           
2073        if self.panel:
2074            self.panel.DestroyChildren()  #safe: wx.Panel
2075            self.panel.Destroy()
2076        self.panel = wx.Panel(self)
2077        mainSizer = wx.BoxSizer(wx.VERTICAL)
2078        mainSizer.Add(wx.StaticText(self.panel,-1,self.prompt),0,wx.ALIGN_CENTER)
2079        for isel,name in enumerate(self.choices):
2080            lineSizer = wx.BoxSizer(wx.HORIZONTAL)
2081            lineSizer.Add(wx.StaticText(self.panel,-1,'Reference atom '+str(isel+1)),0,wx.ALIGN_CENTER)
2082            nameList = self.names[:]
2083            if isel:
2084                if self.choices[0] in nameList:
2085                    nameList.remove(self.choices[0])
2086            choice = wx.ComboBox(self.panel,-1,value=name,choices=nameList,
2087                style=wx.CB_READONLY|wx.CB_DROPDOWN)
2088            Indx[choice.GetId()] = isel
2089            choice.Bind(wx.EVT_COMBOBOX, OnSelection)
2090            lineSizer.Add(choice,0,WACV)
2091            mainSizer.Add(lineSizer)
2092        OkBtn = wx.Button(self.panel,-1,"Ok")
2093        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
2094        CancelBtn = wx.Button(self.panel,-1,'Cancel')
2095        CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
2096        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
2097        btnSizer.Add((20,20),1)
2098        btnSizer.Add(OkBtn)
2099        btnSizer.Add(CancelBtn)
2100        btnSizer.Add((20,20),1)
2101        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
2102        self.panel.SetSizer(mainSizer)
2103        self.panel.Fit()
2104        self.Fit()
2105       
2106    def GetSelection(self):
2107        return self.choices
2108
2109    def OnOk(self,event):
2110        parent = self.GetParent()
2111        parent.Raise()
2112        self.EndModal(wx.ID_OK)             
2113       
2114    def OnCancel(self,event):
2115        parent = self.GetParent()
2116        parent.Raise()
2117        self.EndModal(wx.ID_CANCEL)
2118
2119################################################################################
2120class SingleFloatDialog(wx.Dialog):
2121    '''Dialog to obtain a single float value from user
2122
2123    :param wx.Frame parent: name of parent frame
2124    :param str title: title string for dialog
2125    :param str prompt: string to tell user what they are inputing
2126    :param str value: default input value, if any
2127    :param list limits: upper and lower value used to set bounds for entry, use [None,None]
2128      for no bounds checking, [None,val] for only upper bounds, etc. Default is [0,1].
2129      Values outside of limits will be ignored.
2130    :param str format: string to format numbers. Defaults to '%.5g'. Use '%d' to have
2131      integer input (but dlg.GetValue will still return a float).
2132   
2133    Typical usage::
2134
2135            limits = (0,1)
2136            dlg = G2G.SingleFloatDialog(G2frame,'New value','Enter new value for...',default,limits)
2137            if dlg.ShowModal() == wx.ID_OK:
2138                parm = dlg.GetValue()
2139            dlg.Destroy()   
2140
2141    '''
2142    # TODO: better to generalize this for int & float, use validated text control, OK as default.
2143    # then make SingleFloatDialog & SingleIntDialog as wrappers. Would be good to remove the %-style
2144    # format, too.
2145    def __init__(self,parent,title,prompt,value,limits=[0.,1.],format='%.5g'):
2146        wx.Dialog.__init__(self,parent,-1,title, 
2147            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
2148        self.panel = None
2149        self.limits = limits
2150        self.value = value
2151        self.prompt = prompt
2152        self.format = format
2153        self.Draw()
2154       
2155    def Draw(self):
2156       
2157        def OnValItem(event):
2158            if event: event.Skip()
2159            try:
2160                val = float(valItem.GetValue())
2161                if self.limits[0] is not None and val < self.limits[0]:
2162                    raise ValueError
2163                if self.limits[1] is not None and val > self.limits[1]:
2164                    raise ValueError
2165            except ValueError:
2166                val = self.value
2167            self.value = val
2168            valItem.SetValue(self.format%(self.value))
2169           
2170        if self.panel: self.panel.Destroy()
2171        self.panel = wx.Panel(self)
2172        mainSizer = wx.BoxSizer(wx.VERTICAL)
2173        mainSizer.Add(wx.StaticText(self.panel,-1,self.prompt),0,wx.ALIGN_CENTER)
2174        valItem = wx.TextCtrl(self.panel,-1,value=self.format%(self.value),style=wx.TE_PROCESS_ENTER)
2175        mainSizer.Add(valItem,0,wx.ALIGN_CENTER)
2176        valItem.Bind(wx.EVT_TEXT_ENTER,OnValItem)
2177        valItem.Bind(wx.EVT_KILL_FOCUS,OnValItem)
2178        OkBtn = wx.Button(self.panel,-1,"Ok")
2179        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
2180        CancelBtn = wx.Button(self.panel,-1,'Cancel')
2181        CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
2182        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
2183        btnSizer.Add((20,20),1)
2184        btnSizer.Add(OkBtn)
2185        btnSizer.Add(CancelBtn)
2186        btnSizer.Add((20,20),1)
2187        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
2188        self.panel.SetSizer(mainSizer)
2189        self.panel.Fit()
2190        self.Fit()
2191
2192    def GetValue(self):
2193        return self.value
2194       
2195    def OnOk(self,event):
2196        parent = self.GetParent()
2197        parent.Raise()
2198        self.EndModal(wx.ID_OK)             
2199       
2200    def OnCancel(self,event):
2201        parent = self.GetParent()
2202        parent.Raise()
2203        self.EndModal(wx.ID_CANCEL)
2204
2205class SingleIntDialog(SingleFloatDialog):
2206    '''Dialog to obtain a single int value from user
2207
2208    :param wx.Frame parent: name of parent frame
2209    :param str title: title string for dialog
2210    :param str prompt: string to tell user what they are inputing
2211    :param str value: default input value, if any
2212    :param list limits: upper and lower value used to set bounds for entries. Default
2213      is [None,None] -- for no bounds checking; use [None,val] for only upper bounds, etc.
2214      Default is [0,1]. Values outside of limits will be ignored.
2215   
2216    Typical usage::
2217
2218            limits = (0,None)  # allows zero or positive values only
2219            dlg = G2G.SingleIntDialog(G2frame,'New value','Enter new value for...',default,limits)
2220            if dlg.ShowModal() == wx.ID_OK:
2221                parm = dlg.GetValue()
2222            dlg.Destroy()   
2223
2224    '''
2225    def __init__(self,parent,title,prompt,value,limits=[None,None]):
2226        SingleFloatDialog.__init__(self,parent,title,prompt,value,limits=limits,format='%d')
2227    def GetValue(self):
2228        return int(self.value)
2229
2230################################################################################
2231class MultiFloatDialog(wx.Dialog):
2232    'Dialog to obtain a multi float value from user'
2233    def __init__(self,parent,title,prompts,values,limits=[[0.,1.],],formats=['%.5g',]):
2234        wx.Dialog.__init__(self,parent,-1,title, 
2235            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
2236        self.panel = None
2237        self.limits = limits
2238        self.values = values
2239        self.prompts = prompts
2240        self.formats = formats
2241        self.Draw()
2242       
2243    def Draw(self):
2244       
2245        def OnValItem(event):
2246            if event: event.Skip()
2247            Obj = event.GetEventObject()
2248            id,limits,format = Indx[Obj]
2249            try:
2250                val = float(Obj.GetValue())
2251                if val < limits[0] or val > limits[1]:
2252                    raise ValueError
2253            except ValueError:
2254                val = self.values[id]
2255            self.values[id] = val
2256            Obj.SetValue(format%(val))
2257           
2258        Indx = {}
2259        if self.panel: self.panel.Destroy()
2260        self.panel = wx.Panel(self)
2261        mainSizer = wx.BoxSizer(wx.VERTICAL)
2262        lineSizer = wx.FlexGridSizer(0,2,5,5)
2263        for id,[prompt,value,limits,format] in enumerate(zip(self.prompts,self.values,self.limits,self.formats)):
2264            lineSizer.Add(wx.StaticText(self.panel,label=prompt),0,wx.ALIGN_CENTER)
2265            valItem = wx.TextCtrl(self.panel,value=format%(value),style=wx.TE_PROCESS_ENTER)
2266            Indx[valItem] = [id,limits,format]
2267            lineSizer.Add(valItem,0,wx.ALIGN_CENTER)
2268            valItem.Bind(wx.EVT_TEXT_ENTER,OnValItem)
2269            valItem.Bind(wx.EVT_KILL_FOCUS,OnValItem)
2270        mainSizer.Add(lineSizer)
2271        OkBtn = wx.Button(self.panel,-1,"Ok")
2272        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
2273        CancelBtn = wx.Button(self.panel,-1,'Cancel')
2274        CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
2275        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
2276        btnSizer.Add((20,20),1)
2277        btnSizer.Add(OkBtn)
2278        btnSizer.Add(CancelBtn)
2279        btnSizer.Add((20,20),1)
2280        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
2281        self.panel.SetSizer(mainSizer)
2282        self.panel.Fit()
2283        self.Fit()
2284
2285    def GetValues(self):
2286        return self.values
2287       
2288    def OnOk(self,event):
2289        parent = self.GetParent()
2290        parent.Raise()
2291        self.EndModal(wx.ID_OK)             
2292       
2293    def OnCancel(self,event):
2294        parent = self.GetParent()
2295        parent.Raise()
2296        self.EndModal(wx.ID_CANCEL)
2297
2298################################################################################
2299class SingleStringDialog(wx.Dialog):
2300    '''Dialog to obtain a single string value from user
2301   
2302    :param wx.Frame parent: name of parent frame
2303    :param str title: title string for dialog
2304    :param str prompt: string to tell use what they are inputting
2305    :param str value: default input value, if any
2306    '''
2307    def __init__(self,parent,title,prompt,value='',size=(200,-1),help=''):
2308        wx.Dialog.__init__(self,parent,wx.ID_ANY,title,pos=wx.DefaultPosition,
2309            style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
2310        self.value = value
2311        self.prompt = prompt
2312        self.CenterOnParent()
2313        self.panel = wx.Panel(self)
2314        mainSizer = wx.BoxSizer(wx.VERTICAL)
2315        mainSizer.Add(wx.StaticText(self.panel,-1,self.prompt),0,wx.ALIGN_CENTER)
2316        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
2317        self.valItem = wx.TextCtrl(self.panel,-1,value=self.value,size=size)
2318        sizer1.Add(self.valItem,0,wx.ALIGN_CENTER)
2319        if help:
2320            sizer1.Add(HelpButton(self.panel,help),0,wx.ALIGN_RIGHT|wx.ALL)
2321        mainSizer.Add(sizer1,0,wx.ALIGN_CENTER)
2322        btnsizer = wx.StdDialogButtonSizer()
2323        OKbtn = wx.Button(self.panel, wx.ID_OK)
2324        OKbtn.SetDefault()
2325        btnsizer.AddButton(OKbtn)
2326        btn = wx.Button(self.panel, wx.ID_CANCEL)
2327        btnsizer.AddButton(btn)
2328        btnsizer.Realize()
2329        mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER)
2330        self.panel.SetSizer(mainSizer)
2331        self.panel.Fit()
2332        self.Fit()
2333
2334    def Show(self):
2335        '''Use this method after creating the dialog to post it
2336        :returns: True if the user pressed OK; False if the User pressed Cancel
2337        '''
2338        if self.ShowModal() == wx.ID_OK:
2339            self.value = self.valItem.GetValue()
2340            return True
2341        else:
2342            return False
2343
2344    def GetValue(self):
2345        '''Use this method to get the value entered by the user
2346        :returns: string entered by user
2347        '''
2348        return self.value
2349
2350################################################################################
2351class MultiStringDialog(wx.Dialog):
2352    '''Dialog to obtain a multi string values from user
2353   
2354    :param wx.Frame parent: name of parent frame
2355    :param str title: title string for dialog
2356    :param str prompts: strings to tell use what they are inputting
2357    :param str values: default input values, if any
2358    :param int size: length of the input box in pixels
2359    '''
2360    def __init__(self,parent,title,prompts,values=[],size=-1):
2361       
2362        wx.Dialog.__init__(self,parent,wx.ID_ANY,title, 
2363                           pos=wx.DefaultPosition,
2364                           style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
2365        self.values = values
2366        self.prompts = prompts
2367        self.CenterOnParent()
2368        mainSizer = wx.BoxSizer(wx.VERTICAL)
2369        promptSizer = wx.FlexGridSizer(0,2,5,5)
2370        self.Indx = {}
2371        for prompt,value in zip(prompts,values):
2372            promptSizer.Add(wx.StaticText(self,-1,prompt),0,WACV)
2373            valItem = wx.TextCtrl(self,-1,value=value,style=wx.TE_PROCESS_ENTER,size=(size,-1))
2374            self.Indx[valItem.GetId()] = prompt
2375            valItem.Bind(wx.EVT_TEXT,self.newValue)
2376            promptSizer.Add(valItem,1,WACV|wx.EXPAND,1)
2377        mainSizer.Add(promptSizer,1,wx.ALL|wx.EXPAND,1)
2378        btnsizer = wx.StdDialogButtonSizer()
2379        OKbtn = wx.Button(self, wx.ID_OK)
2380        OKbtn.SetDefault()
2381        btnsizer.AddButton(OKbtn)
2382        btn = wx.Button(self, wx.ID_CANCEL)
2383        btnsizer.AddButton(btn)
2384        btnsizer.Realize()
2385        mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER)
2386        self.SetSizer(mainSizer)
2387        self.Fit()
2388       
2389    def newValue(self,event):
2390        Obj = event.GetEventObject()
2391        item = self.Indx[Obj.GetId()]
2392        id = self.prompts.index(item)
2393        self.values[id] = Obj.GetValue()
2394
2395    def Show(self):
2396        '''Use this method after creating the dialog to post it
2397        :returns: True if the user pressed OK; False if the User pressed Cancel
2398        '''
2399        if self.ShowModal() == wx.ID_OK:
2400            return True
2401        else:
2402            return False
2403
2404    def GetValues(self):
2405        '''Use this method to get the value entered by the user
2406        :returns: string entered by user
2407        '''
2408        return self.values
2409
2410################################################################################
2411class G2ColumnIDDialog(wx.Dialog):
2412    '''A dialog for matching column data to desired items; some columns may be ignored.
2413   
2414    :param wx.Frame ParentFrame: reference to parent frame
2415    :param str title: heading above list of choices
2416    :param str header: Title to place on window frame
2417    :param list ChoiceList: a list of possible choices for the columns
2418    :param list ColumnData: lists of column data to be matched with ChoiceList
2419    :param bool monoFont: If False (default), use a variable-spaced font;
2420      if True use a equally-spaced font.
2421    :param kw: optional keyword parameters for the wx.Dialog may
2422      be included such as size [which defaults to `(320,310)`] and
2423      style (which defaults to ``wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.CENTRE | wx.OK | wx.CANCEL``);
2424      note that ``wx.OK`` and ``wx.CANCEL`` controls
2425      the presence of the eponymous buttons in the dialog.
2426    :returns: the name of the created dialog
2427   
2428    '''
2429
2430    def __init__(self,parent, title, header,Comments,ChoiceList, ColumnData,
2431                 monoFont=False, **kw):
2432
2433        def OnOk(sevent):
2434            OK = True
2435            selCols = []
2436            for col in self.sel:
2437                item = col.GetValue()
2438                if item != ' ' and item in selCols:
2439                    OK = False
2440                    break
2441                else:
2442                    selCols.append(item)
2443            parent = self.GetParent()
2444            if not OK:
2445                parent.ErrorDialog('Duplicate',item+' selected more than once')
2446                return
2447            parent.Raise()
2448            self.EndModal(wx.ID_OK)
2449           
2450        def OnModify(event):
2451            if event: event.Skip()
2452            Obj = event.GetEventObject()
2453            icol,colData = Indx[Obj.GetId()]
2454            modify = Obj.GetValue()
2455            if not modify:
2456                return
2457            #print 'Modify column',icol,' by', modify
2458            for i,item in enumerate(self.ColumnData[icol]):
2459                self.ColumnData[icol][i] = str(eval(item+modify))
2460            colData.SetValue('\n'.join(self.ColumnData[icol]))
2461            Obj.SetValue('')
2462           
2463        # process keyword parameters, notably style
2464        options = {'size':(600,310), # default Frame keywords
2465                   'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
2466                   }
2467        options.update(kw)
2468        self.Comments = ''.join(Comments)
2469        self.ChoiceList = ChoiceList
2470        self.ColumnData = ColumnData
2471        nCol = len(ColumnData)
2472        if options['style'] & wx.OK:
2473            useOK = True
2474            options['style'] ^= wx.OK
2475        else:
2476            useOK = False
2477        if options['style'] & wx.CANCEL:
2478            useCANCEL = True
2479            options['style'] ^= wx.CANCEL
2480        else:
2481            useCANCEL = False       
2482        # create the dialog frame
2483        wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
2484        panel = wxscroll.ScrolledPanel(self)
2485        # fill the dialog
2486        Sizer = wx.BoxSizer(wx.VERTICAL)
2487        Sizer.Add((-1,5))
2488        Sizer.Add(wx.StaticText(panel,label=title),0,WACV)
2489        if self.Comments:
2490            Sizer.Add(wx.StaticText(panel,label=' Header lines:'),0,WACV)
2491            Sizer.Add(wx.TextCtrl(panel,value=self.Comments,size=(200,-1),
2492                style=wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_DONTWRAP),0,wx.ALL|wx.EXPAND|WACV,8)
2493        columnsSizer = wx.FlexGridSizer(0,nCol,5,10)
2494        self.sel = []
2495        self.mod = []
2496        Indx = {}
2497        for icol,col in enumerate(self.ColumnData):
2498            colSizer = wx.BoxSizer(wx.VERTICAL)
2499            colSizer.Add(wx.StaticText(panel,label=' Column #%d Select:'%(icol)),0,WACV)
2500            self.sel.append(wx.ComboBox(panel,value=' ',choices=self.ChoiceList,style=wx.CB_READONLY|wx.CB_DROPDOWN))
2501            colSizer.Add(self.sel[-1])
2502            colData = wx.TextCtrl(panel,value='\n'.join(self.ColumnData[icol]),size=(120,-1),
2503                style=wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_DONTWRAP)
2504            colSizer.Add(colData,0,WACV)
2505            colSizer.Add(wx.StaticText(panel,label=' Modify by:'),0,WACV)
2506            mod = wx.TextCtrl(panel,size=(120,-1),value='',style=wx.TE_PROCESS_ENTER)
2507            mod.Bind(wx.EVT_TEXT_ENTER,OnModify)
2508            mod.Bind(wx.EVT_KILL_FOCUS,OnModify)
2509            Indx[mod.GetId()] = [icol,colData]
2510            colSizer.Add(mod,0,WACV)
2511            columnsSizer.Add(colSizer)
2512        Sizer.Add(columnsSizer)
2513        Sizer.Add(wx.StaticText(panel,label=' For modify by, enter arithmetic string eg. "-12345.67". "+","-","*","/","**" all allowed'),0,WACV) 
2514        Sizer.Add((-1,10))
2515        # OK/Cancel buttons
2516        btnsizer = wx.StdDialogButtonSizer()
2517        if useOK:
2518            self.OKbtn = wx.Button(panel, wx.ID_OK)
2519            self.OKbtn.SetDefault()
2520            btnsizer.AddButton(self.OKbtn)
2521            self.OKbtn.Bind(wx.EVT_BUTTON, OnOk)
2522        if useCANCEL:
2523            btn = wx.Button(panel, wx.ID_CANCEL)
2524            btnsizer.AddButton(btn)
2525        btnsizer.Realize()
2526        Sizer.Add((-1,5))
2527        Sizer.Add(btnsizer,0,wx.ALIGN_LEFT,20)
2528        Sizer.Add((-1,5))
2529        # OK done, let's get outa here
2530        panel.SetSizer(Sizer)
2531        panel.SetAutoLayout(1)
2532        panel.SetupScrolling()
2533        Size = [450,375]
2534        panel.SetSize(Size)
2535        Size[0] += 25; Size[1]+= 25
2536        self.SetSize(Size)
2537       
2538    def GetSelection(self):
2539        'Returns the selected sample parm for each column'
2540        selCols = []
2541        for item in self.sel:
2542            selCols.append(item.GetValue())
2543        return selCols,self.ColumnData
2544   
2545################################################################################
2546class G2HistoDataDialog(wx.Dialog):
2547    '''A dialog for editing histogram data globally.
2548   
2549    :param wx.Frame ParentFrame: reference to parent frame
2550    :param str title: heading above list of choices
2551    :param str header: Title to place on window frame
2552    :param list ParmList: a list of names for the columns
2553    :param list ParmFmt: a list of formatting strings for the columns
2554    :param list: HistoList: a list of histogram names
2555    :param list ParmData: a list of lists of data matched to ParmList; one for each item in HistoList
2556    :param bool monoFont: If False (default), use a variable-spaced font;
2557      if True use a equally-spaced font.
2558    :param kw: optional keyword parameters for the wx.Dialog may
2559      be included such as size [which defaults to `(320,310)`] and
2560      style (which defaults to
2561      ``wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.CENTRE | wx.OK | wx.CANCEL``);
2562      note that ``wx.OK`` and ``wx.CANCEL`` controls the presence of the eponymous buttons in the dialog.
2563    :returns: the modified ParmData
2564   
2565    '''
2566
2567    def __init__(self,parent, title, header,ParmList,ParmFmt,HistoList,ParmData,
2568                 monoFont=False, **kw):
2569
2570        def OnOk(sevent):
2571            parent.Raise()
2572            self.EndModal(wx.ID_OK)
2573           
2574        def OnModify(event):
2575            Obj = event.GetEventObject()
2576            irow,it = Indx[Obj.GetId()]
2577            try:
2578                val = float(Obj.GetValue())
2579            except ValueError:
2580                val = self.ParmData[irow][it]
2581            self.ParmData[irow][it] = val
2582            Obj.SetValue(self.ParmFmt[it]%val)
2583                       
2584        # process keyword parameters, notably style
2585        options = {'size':(600,310), # default Frame keywords
2586                   'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
2587                   }
2588        options.update(kw)
2589        self.ParmList = ParmList
2590        self.ParmFmt = ParmFmt
2591        self.HistoList = HistoList
2592        self.ParmData = ParmData
2593        nCol = len(ParmList)
2594        if options['style'] & wx.OK:
2595            useOK = True
2596            options['style'] ^= wx.OK
2597        else:
2598            useOK = False
2599        if options['style'] & wx.CANCEL:
2600            useCANCEL = True
2601            options['style'] ^= wx.CANCEL
2602        else:
2603            useCANCEL = False       
2604        # create the dialog frame
2605        wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
2606        panel = wxscroll.ScrolledPanel(self)
2607        # fill the dialog
2608        Sizer = wx.BoxSizer(wx.VERTICAL)
2609        Sizer.Add((-1,5))
2610        Sizer.Add(wx.StaticText(panel,label=title),0,WACV)
2611        dataSizer = wx.FlexGridSizer(0,nCol+1,0,0)
2612        self.sel = []
2613        self.mod = []
2614        Indx = {}
2615        for item in ['Histogram',]+self.ParmList:
2616            dataSizer.Add(wx.StaticText(panel,-1,label=' %10s '%(item)),0,WACV)
2617        for irow,name in enumerate(self.HistoList):
2618            dataSizer.Add(wx.StaticText(panel,label=name),0,WACV|wx.LEFT|wx.RIGHT,10)
2619            for it,item in enumerate(self.ParmData[irow]):
2620                dat = wx.TextCtrl(panel,-1,value=self.ParmFmt[it]%(item),style=wx.TE_PROCESS_ENTER)
2621                dataSizer.Add(dat,0,WACV)
2622                dat.Bind(wx.EVT_TEXT_ENTER,OnModify)
2623                dat.Bind(wx.EVT_KILL_FOCUS,OnModify)
2624                Indx[dat.GetId()] = [irow,it]
2625        Sizer.Add(dataSizer)
2626        Sizer.Add((-1,10))
2627        # OK/Cancel buttons
2628        btnsizer = wx.StdDialogButtonSizer()
2629        if useOK:
2630            self.OKbtn = wx.Button(panel, wx.ID_OK)
2631            self.OKbtn.SetDefault()
2632            btnsizer.AddButton(self.OKbtn)
2633            self.OKbtn.Bind(wx.EVT_BUTTON, OnOk)
2634        if useCANCEL:
2635            btn = wx.Button(panel, wx.ID_CANCEL)
2636            btnsizer.AddButton(btn)
2637        btnsizer.Realize()
2638        Sizer.Add((-1,5))
2639        Sizer.Add(btnsizer,0,wx.ALIGN_LEFT,20)
2640        Sizer.Add((-1,5))
2641        # OK done, let's get outa here
2642        panel.SetSizer(Sizer)
2643        panel.SetAutoLayout(1)
2644        panel.SetupScrolling()
2645        Size = [450,375]
2646        panel.SetSize(Size)
2647        Size[0] += 25; Size[1]+= 25
2648        self.SetSize(Size)
2649       
2650    def GetData(self):
2651        'Returns the modified ParmData'
2652        return self.ParmData
2653   
2654################################################################################
2655def ItemSelector(ChoiceList, ParentFrame=None,
2656                 title='Select an item',
2657                 size=None, header='Item Selector',
2658                 useCancel=True,multiple=False):
2659    ''' Provide a wx dialog to select a single item or multiple items from list of choices
2660
2661    :param list ChoiceList: a list of choices where one will be selected
2662    :param wx.Frame ParentFrame: Name of parent frame (default None)
2663    :param str title: heading above list of choices (default 'Select an item')
2664    :param wx.Size size: Size for dialog to be created (default None -- size as needed)
2665    :param str header: Title to place on window frame (default 'Item Selector')
2666    :param bool useCancel: If True (default) both the OK and Cancel buttons are offered
2667    :param bool multiple: If True then multiple items can be selected (default False)
2668   
2669    :returns: the selection index or None or a selection list if multiple is true
2670
2671    Called by GSASIIdataGUI.OnReOrgSelSeq() Which is not fully implemented.
2672    '''
2673    if multiple:
2674        if useCancel:
2675            dlg = G2MultiChoiceDialog(
2676                ParentFrame,title, header, ChoiceList)
2677        else:
2678            dlg = G2MultiChoiceDialog(
2679                ParentFrame,title, header, ChoiceList,
2680                style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.OK|wx.CENTRE)
2681    else:
2682        if useCancel:
2683            dlg = wx.SingleChoiceDialog(
2684                ParentFrame,title, header, ChoiceList)
2685        else:
2686            dlg = wx.SingleChoiceDialog(
2687                ParentFrame,title, header,ChoiceList,
2688                style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.OK|wx.CENTRE)
2689    if size: dlg.SetSize(size)
2690    if dlg.ShowModal() == wx.ID_OK:
2691        if multiple:
2692            dlg.Destroy()
2693            return dlg.GetSelections()
2694        else:
2695            dlg.Destroy()
2696            return dlg.GetSelection()
2697    else:
2698        dlg.Destroy()
2699        return None
2700    dlg.Destroy()
2701
2702######################################################### Column-order selection dialog
2703def GetItemOrder(parent,keylist,vallookup,posdict):
2704    '''Creates a dialog where items can be ordered into columns
2705   
2706    :param list keylist: is a list of keys for column assignments
2707    :param dict vallookup: is a dict keyed by names in keylist where each item is a dict.
2708       Each inner dict contains variable names as keys and their associated values
2709    :param dict posdict: is a dict keyed by names in keylist where each item is a dict.
2710       Each inner dict contains column numbers as keys and their associated
2711       variable name as a value. This is used for both input and output.
2712       
2713    '''
2714    dlg = wx.Dialog(parent,style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
2715    sizer = wx.BoxSizer(wx.VERTICAL)
2716    spanel = OrderBox(dlg,keylist,vallookup,posdict)
2717    spanel.Fit()
2718    sizer.Add(spanel,1,wx.EXPAND)
2719    btnsizer = wx.StdDialogButtonSizer()
2720    btn = wx.Button(dlg, wx.ID_OK)
2721    btn.SetDefault()
2722    btnsizer.AddButton(btn)
2723    #btn = wx.Button(dlg, wx.ID_CANCEL)
2724    #btnsizer.AddButton(btn)
2725    btnsizer.Realize()
2726    sizer.Add(btnsizer, 0, wx.ALIGN_CENTER_VERTICAL|wx.EXPAND|wx.ALL, 5)
2727    dlg.SetSizer(sizer)
2728    sizer.Fit(dlg)
2729    dlg.ShowModal()
2730
2731################################################################################
2732class MultiIntegerDialog(wx.Dialog):
2733    '''Input a series of integers based on prompts
2734    '''
2735    def __init__(self,parent,title,prompts,values):
2736        wx.Dialog.__init__(self,parent,-1,title, 
2737            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
2738        self.panel = wx.Panel(self)         #just a dummy - gets destroyed in Draw!
2739        self.values = values
2740        self.prompts = prompts
2741        self.Draw()
2742       
2743    def Draw(self):
2744       
2745        def OnValItem(event):
2746            event.Skip()
2747            Obj = event.GetEventObject()
2748            ind = Indx[Obj.GetId()]
2749            try:
2750                val = int(Obj.GetValue())
2751                if val <= 0:
2752                    raise ValueError
2753            except ValueError:
2754                val = self.values[ind]
2755            self.values[ind] = val
2756            Obj.SetValue('%d'%(val))
2757           
2758        self.panel.Destroy()
2759        self.panel = wx.Panel(self)
2760        mainSizer = wx.BoxSizer(wx.VERTICAL)
2761        Indx = {}
2762        for ival,[prompt,value] in enumerate(zip(self.prompts,self.values)):
2763            mainSizer.Add(wx.StaticText(self.panel,-1,prompt),0,wx.ALIGN_CENTER)
2764            valItem = wx.TextCtrl(self.panel,-1,value='%d'%(value),style=wx.TE_PROCESS_ENTER)
2765            mainSizer.Add(valItem,0,wx.ALIGN_CENTER)
2766            Indx[valItem.GetId()] = ival
2767            valItem.Bind(wx.EVT_TEXT_ENTER,OnValItem)
2768            valItem.Bind(wx.EVT_KILL_FOCUS,OnValItem)
2769        OkBtn = wx.Button(self.panel,-1,"Ok")
2770        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
2771        CancelBtn = wx.Button(self.panel,-1,'Cancel')
2772        CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
2773        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
2774        btnSizer.Add((20,20),1)
2775        btnSizer.Add(OkBtn)
2776        btnSizer.Add(CancelBtn)
2777        btnSizer.Add((20,20),1)
2778        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
2779        self.panel.SetSizer(mainSizer)
2780        self.panel.Fit()
2781        self.Fit()
2782
2783    def GetValues(self):
2784        return self.values
2785       
2786    def OnOk(self,event):
2787        parent = self.GetParent()
2788        parent.Raise()
2789        self.EndModal(wx.ID_OK)             
2790       
2791    def OnCancel(self,event):
2792        parent = self.GetParent()
2793        parent.Raise()
2794        self.EndModal(wx.ID_CANCEL)
2795
2796################################################################################
2797class OrderBox(wxscroll.ScrolledPanel):
2798    '''Creates a panel with scrollbars where items can be ordered into columns
2799   
2800    :param list keylist: is a list of keys for column assignments
2801    :param dict vallookup: is a dict keyed by names in keylist where each item is a dict.
2802      Each inner dict contains variable names as keys and their associated values
2803    :param dict posdict: is a dict keyed by names in keylist where each item is a dict.
2804      Each inner dict contains column numbers as keys and their associated
2805      variable name as a value. This is used for both input and output.
2806     
2807    '''
2808    def __init__(self,parent,keylist,vallookup,posdict,*arg,**kw):
2809        self.keylist = keylist
2810        self.vallookup = vallookup
2811        self.posdict = posdict
2812        self.maxcol = 0
2813        for nam in keylist:
2814            posdict = self.posdict[nam]
2815            if posdict.keys():
2816                self.maxcol = max(self.maxcol, max(posdict))
2817        wxscroll.ScrolledPanel.__init__(self,parent,wx.ID_ANY,*arg,**kw)
2818        self.GBsizer = wx.GridBagSizer(4,4)
2819        self.SetBackgroundColour(WHITE)
2820        self.SetSizer(self.GBsizer)
2821        colList = [str(i) for i in range(self.maxcol+2)]
2822        for i in range(self.maxcol+1):
2823            wid = wx.StaticText(self,wx.ID_ANY,str(i),style=wx.ALIGN_CENTER)
2824            wid.SetBackgroundColour(DULL_YELLOW)
2825            wid.SetMinSize((50,-1))
2826            self.GBsizer.Add(wid,(0,i),flag=wx.ALIGN_CENTER|wx.EXPAND)
2827        self.chceDict = {}
2828        for row,nam in enumerate(self.keylist):
2829            posdict = self.posdict[nam]
2830            for col in posdict:
2831                lbl = posdict[col]
2832                pnl = wx.Panel(self,wx.ID_ANY)
2833                pnl.SetBackgroundColour(VERY_LIGHT_GREY)
2834                insize = wx.BoxSizer(wx.VERTICAL)
2835                wid = wx.Choice(pnl,wx.ID_ANY,choices=colList)
2836                insize.Add(wid,0,wx.EXPAND|wx.BOTTOM,3)
2837                wid.SetSelection(col)
2838                self.chceDict[wid] = (row,col)
2839                wid.Bind(wx.EVT_CHOICE,self.OnChoice)
2840                wid = wx.StaticText(pnl,wx.ID_ANY,lbl)
2841                insize.Add(wid,0,flag=wx.EXPAND)
2842                try:
2843                    val = G2py3.FormatSigFigs(self.vallookup[nam][lbl],maxdigits=8)
2844                except KeyError:
2845                    val = '?'
2846                wid = wx.StaticText(pnl,wx.ID_ANY,'('+val+')')
2847                insize.Add(wid,0,flag=wx.EXPAND)
2848                pnl.SetSizer(insize)
2849                self.GBsizer.Add(pnl,(row+1,col),flag=wx.EXPAND)
2850        self.SetAutoLayout(1)
2851        self.SetupScrolling()
2852        self.SetMinSize((
2853            min(700,self.GBsizer.GetSize()[0]),
2854            self.GBsizer.GetSize()[1]+20))
2855    def OnChoice(self,event):
2856        '''Called when a column is assigned to a variable
2857        '''
2858        row,col = self.chceDict[event.EventObject] # which variable was this?
2859        newcol = event.Selection # where will it be moved?
2860        if newcol == col:
2861            return # no change: nothing to do!
2862        prevmaxcol = self.maxcol # save current table size
2863        key = self.keylist[row] # get the key for the current row
2864        lbl = self.posdict[key][col] # selected variable name
2865        lbl1 = self.posdict[key].get(col+1,'') # next variable name, if any
2866        # if a posXXX variable is selected, and the next variable is posXXX, move them together
2867        repeat = 1
2868        if lbl[:3] == 'pos' and lbl1[:3] == 'int' and lbl[3:] == lbl1[3:]:
2869            repeat = 2
2870        for i in range(repeat): # process the posXXX and then the intXXX (or a single variable)
2871            col += i
2872            newcol += i
2873            if newcol in self.posdict[key]:
2874                # find first non-blank after newcol
2875                for mtcol in range(newcol+1,self.maxcol+2):
2876                    if mtcol not in self.posdict[key]: break
2877                l1 = range(mtcol,newcol,-1)+[newcol]
2878                l = range(mtcol-1,newcol-1,-1)+[col]
2879            else:
2880                l1 = [newcol]
2881                l = [col]
2882            # move all of the items, starting from the last column
2883            for newcol,col in zip(l1,l):
2884                #print 'moving',col,'to',newcol
2885                self.posdict[key][newcol] = self.posdict[key][col]
2886                del self.posdict[key][col]
2887                self.maxcol = max(self.maxcol,newcol)
2888                obj = self.GBsizer.FindItemAtPosition((row+1,col))
2889                self.GBsizer.SetItemPosition(obj.GetWindow(),(row+1,newcol))
2890                for wid in obj.GetWindow().Children:
2891                    if wid in self.chceDict:
2892                        self.chceDict[wid] = (row,newcol)
2893                        wid.SetSelection(self.chceDict[wid][1])
2894        # has the table gotten larger? If so we need new column heading(s)
2895        if prevmaxcol != self.maxcol:
2896            for i in range(prevmaxcol+1,self.maxcol+1):
2897                wid = wx.StaticText(self,wx.ID_ANY,str(i),style=wx.ALIGN_CENTER)
2898                wid.SetBackgroundColour(DULL_YELLOW)
2899                wid.SetMinSize((50,-1))
2900                self.GBsizer.Add(wid,(0,i),flag=wx.ALIGN_CENTER|wx.EXPAND)
2901            colList = [str(i) for i in range(self.maxcol+2)]
2902            for wid in self.chceDict:
2903                wid.SetItems(colList)
2904                wid.SetSelection(self.chceDict[wid][1])
2905        self.GBsizer.Layout()
2906        self.FitInside()
2907       
2908################################################################################
2909def GetImportFile(G2frame, message, defaultDir="", defaultFile="", style=wx.FD_OPEN,
2910                  *args, **kwargs):
2911    '''Uses a customized dialog that gets files from the appropriate import directory.
2912    Arguments are used the same as in :func:`wx.FileDialog`. Selection of
2913    multiple files is allowed if argument style includes wx.FD_MULTIPLE.
2914
2915    The default initial directory (unless overridden with argument defaultDir)
2916    is found in G2frame.TutorialImportDir, config setting Import_directory or
2917    G2frame.LastImportDir, see :func:`GetImportPath`.
2918
2919    The path of the first file entered is used to set G2frame.LastImportDir
2920    and optionally config setting Import_directory.
2921
2922    :returns: a list of files or an empty list
2923    '''
2924    dlg = wx.FileDialog(G2frame, message, defaultDir, defaultFile, *args,
2925                        style=style, **kwargs)
2926    pth = GetImportPath(G2frame)
2927    if not defaultDir and pth: dlg.SetDirectory(pth)
2928    try:
2929        if dlg.ShowModal() == wx.ID_OK:
2930            if style & wx.FD_MULTIPLE:
2931                filelist = dlg.GetPaths()
2932                if len(filelist) == 0: return []
2933            else:
2934                filelist = [dlg.GetPath(),]
2935            # not sure if we want to do this (why use wx.CHANGE_DIR?)
2936            if style & wx.FD_CHANGE_DIR: # to get Mac/Linux to change directory like windows!
2937                os.chdir(dlg.GetDirectory())
2938        else: # cancel was pressed
2939            return []
2940    finally:
2941        dlg.Destroy()
2942    # save the path of the first file and reset the TutorialImportDir variable
2943    pth = os.path.split(os.path.abspath(filelist[0]))[0]
2944    if GSASIIpath.GetConfigValue('Save_paths'): SaveImportDirectory(pth)
2945    G2frame.LastImportDir = pth
2946    G2frame.TutorialImportDir = None
2947    return filelist
2948
2949def GetImportPath(G2frame):
2950    '''Determines the default location to use for importing files. Tries sequentially
2951    G2frame.TutorialImportDir, config var Import_directory and G2frame.LastImportDir.
2952   
2953    :returns: a string containing the path to be used when reading files or None
2954      if none of the above are specified.
2955    '''
2956    if G2frame.TutorialImportDir:
2957        if os.path.exists(G2frame.TutorialImportDir):
2958            return G2frame.TutorialImportDir
2959        elif GSASIIpath.GetConfigValue('debug'):
2960            print('Tutorial location (TutorialImportDir) not found: '+G2frame.TutorialImportDir)
2961    pth = GSASIIpath.GetConfigValue('Import_directory')
2962    if pth:
2963        pth = os.path.expanduser(pth)
2964        if os.path.exists(pth):
2965            return pth
2966        elif GSASIIpath.GetConfigValue('debug'):
2967            print('Ignoring Config Import_directory value: '+
2968                      GSASIIpath.GetConfigValue('Import_directory'))
2969    if G2frame.LastImportDir:
2970        if os.path.exists(G2frame.LastImportDir):
2971            return G2frame.LastImportDir
2972        elif GSASIIpath.GetConfigValue('debug'):
2973            print('Warning: G2frame.LastImportDir not found = '+G2frame.LastImportDir)
2974    return None
2975
2976def GetExportPath(G2frame):
2977    '''Determines the default location to use for writing files. Tries sequentially
2978    G2frame.LastExportDir and G2frame.LastGPXdir.
2979   
2980    :returns: a string containing the path to be used when writing files or '.'
2981      if none of the above are specified.
2982    '''
2983    if G2frame.LastExportDir:
2984        return G2frame.LastExportDir
2985    elif G2frame.LastGPXdir:
2986        return G2frame.LastGPXdir
2987    else:
2988        return '.'
2989
2990################################################################################
2991class SGMessageBox(wx.Dialog):
2992    ''' Special version of MessageBox that displays space group & super space group text
2993    in two blocks
2994    '''
2995    def __init__(self,parent,title,text,table,):
2996        wx.Dialog.__init__(self,parent,wx.ID_ANY,title,pos=wx.DefaultPosition,
2997            style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
2998        self.text = text
2999        self.table = table
3000        self.panel = wx.Panel(self)
3001        mainSizer = wx.BoxSizer(wx.VERTICAL)
3002        mainSizer.Add((0,10))
3003        for line in text:
3004            mainSizer.Add(wx.StaticText(self.panel,label='     %s     '%(line)),0,WACV)
3005        ncol = self.table[0].count(',')+1
3006        tableSizer = wx.FlexGridSizer(0,2*ncol+3,0,0)
3007        j = 0
3008        for item in self.table:
3009            if 'for' in item:
3010                mainSizer.Add(tableSizer,0,wx.ALIGN_LEFT)
3011                mainSizer.Add(wx.StaticText(self.panel,label=item),0,WACV)
3012                tableSizer = wx.FlexGridSizer(0,2*ncol+3,0,0)
3013                continue
3014            num,flds = item.split(')')
3015            tableSizer.Add(wx.StaticText(self.panel,label='     %s  '%(num+')')),0,WACV|wx.ALIGN_LEFT)           
3016            flds = flds.replace(' ','').split(',')
3017            for i,fld in enumerate(flds):
3018                if i < ncol-1:
3019                    tableSizer.Add(wx.StaticText(self.panel,label='%s, '%(fld)),0,WACV|wx.ALIGN_RIGHT)
3020                else:
3021                    tableSizer.Add(wx.StaticText(self.panel,label='%s'%(fld)),0,WACV|wx.ALIGN_RIGHT)
3022            if not j%2:
3023                tableSizer.Add((20,0))
3024            j += 1
3025        mainSizer.Add(tableSizer,0,wx.ALIGN_LEFT)
3026        btnsizer = wx.StdDialogButtonSizer()
3027        OKbtn = wx.Button(self.panel, wx.ID_OK)
3028        OKbtn.Bind(wx.EVT_BUTTON, self.OnOk)
3029        OKbtn.SetDefault()
3030        btnsizer.AddButton(OKbtn)
3031        btnsizer.Realize()
3032        mainSizer.Add((0,10))
3033        mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER)
3034        self.panel.SetSizer(mainSizer)
3035        self.panel.Fit()
3036        self.Fit()
3037        size = self.GetSize()
3038        self.SetSize([size[0]+20,size[1]])
3039
3040    def Show(self):
3041        '''Use this method after creating the dialog to post it
3042        '''
3043        self.ShowModal()
3044        return
3045
3046    def OnOk(self,event):
3047        parent = self.GetParent()
3048        parent.Raise()
3049        self.EndModal(wx.ID_OK)
3050
3051################################################################################
3052class DisAglDialog(wx.Dialog):
3053    '''Distance/Angle Controls input dialog. After
3054    :meth:`ShowModal` returns, the results are found in
3055    dict :attr:`self.data`, which is accessed using :meth:`GetData`.
3056
3057    :param wx.Frame parent: reference to parent frame (or None)
3058    :param dict data: a dict containing the current
3059      search ranges or an empty dict, which causes default values
3060      to be used.
3061      Will be used to set element `DisAglCtls` in
3062      :ref:`Phase Tree Item <Phase_table>`
3063    :param dict default:  A dict containing the default
3064      search ranges for each element.
3065    :param bool Reset: if True (default), show Reset button
3066    :param bool Angle: if True (default), show angle radii
3067    '''
3068    def __init__(self,parent,data,default,Reset=True,Angle=True):
3069        text = 'Distance Angle Controls'
3070        if not Angle:
3071            text = 'Distance Controls'
3072        wx.Dialog.__init__(self,parent,wx.ID_ANY,text, 
3073            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
3074        self.default = default
3075        self.Reset = Reset
3076        self.Angle = Angle
3077        self.panel = None
3078        self._default(data,self.default)
3079        self.Draw(self.data)
3080               
3081    def _default(self,data,default):
3082        '''Set starting values for the search values, either from
3083        the input array or from defaults, if input is null
3084        '''
3085        if data:
3086            self.data = copy.deepcopy(data) # don't mess with originals
3087        else:
3088            self.data = {}
3089            self.data['Name'] = default['Name']
3090            self.data['Factors'] = [0.85,0.85]
3091            self.data['AtomTypes'] = default['AtomTypes']
3092            self.data['BondRadii'] = default['BondRadii'][:]
3093            self.data['AngleRadii'] = default['AngleRadii'][:]
3094
3095    def Draw(self,data):
3096        '''Creates the contents of the dialog. Normally called
3097        by :meth:`__init__`.
3098        '''
3099        if self.panel: self.panel.Destroy()
3100        self.panel = wx.Panel(self)
3101        mainSizer = wx.BoxSizer(wx.VERTICAL)
3102        mainSizer.Add(wx.StaticText(self.panel,-1,'Controls for phase '+data['Name']),
3103            0,WACV|wx.LEFT,10)
3104        mainSizer.Add((10,10),1)
3105       
3106        ncol = 3
3107        if not self.Angle:
3108            ncol=2
3109        radiiSizer = wx.FlexGridSizer(0,ncol,5,5)
3110        radiiSizer.Add(wx.StaticText(self.panel,-1,' Type'),0,WACV)
3111        radiiSizer.Add(wx.StaticText(self.panel,-1,'Bond radii'),0,WACV)
3112        if self.Angle:
3113            radiiSizer.Add(wx.StaticText(self.panel,-1,'Angle radii'),0,WACV)
3114        self.objList = {}
3115        for id,item in enumerate(self.data['AtomTypes']):
3116            radiiSizer.Add(wx.StaticText(self.panel,-1,' '+item),0,WACV)
3117            bRadii = ValidatedTxtCtrl(self.panel,data['BondRadii'],id,nDig=(10,3))
3118            radiiSizer.Add(bRadii,0,WACV)
3119            if self.Angle:
3120                aRadii = ValidatedTxtCtrl(self.panel,data['AngleRadii'],id,nDig=(10,3))
3121                radiiSizer.Add(aRadii,0,WACV)
3122        mainSizer.Add(radiiSizer,0,wx.EXPAND)
3123        if self.Angle:
3124            factorSizer = wx.FlexGridSizer(0,2,5,5)
3125            Names = ['Bond','Angle']
3126            for i,name in enumerate(Names):
3127                factorSizer.Add(wx.StaticText(self.panel,-1,name+' search factor'),0,WACV)
3128                bondFact = ValidatedTxtCtrl(self.panel,data['Factors'],i,nDig=(10,3))
3129                factorSizer.Add(bondFact)
3130            mainSizer.Add(factorSizer,0,wx.EXPAND)
3131       
3132        OkBtn = wx.Button(self.panel,-1,"Ok")
3133        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
3134        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
3135        btnSizer.Add((20,20),1)
3136        btnSizer.Add(OkBtn)
3137        if self.Reset:
3138            ResetBtn = wx.Button(self.panel,-1,'Reset')
3139            ResetBtn.Bind(wx.EVT_BUTTON, self.OnReset)
3140            btnSizer.Add(ResetBtn)
3141        btnSizer.Add((20,20),1)
3142        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
3143        self.panel.SetSizer(mainSizer)
3144        self.panel.Fit()
3145        self.Fit()
3146   
3147    def GetData(self):
3148        'Returns the values from the dialog'
3149        return self.data
3150       
3151    def OnOk(self,event):
3152        'Called when the OK button is pressed'
3153        parent = self.GetParent()
3154        parent.Raise()
3155        self.EndModal(wx.ID_OK)             
3156       
3157    def OnReset(self,event):
3158        'Called when the Reset button is pressed'
3159        data = {}
3160        self._default(data,self.default)
3161        wx.CallAfter(self.Draw,self.data)
3162               
3163################################################################################
3164class ShowLSParms(wx.Dialog):
3165    '''Create frame to show least-squares parameters
3166    '''
3167    def __init__(self,parent,title,parmDict,varyList,fullVaryList,
3168                 size=(375,430)):
3169       
3170        wx.Dialog.__init__(self,parent,wx.ID_ANY,title,size=size,
3171                           style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
3172        self.panel = wxscroll.ScrolledPanel(self)
3173        self.parmChoice = 'Phase'
3174        self.parmDict = parmDict
3175        self.varyList = varyList
3176        self.fullVaryList = fullVaryList
3177
3178        self.parmNames = list(parmDict.keys())
3179        self.parmNames.sort()
3180        splitNames = [item.split(':') for item in self.parmNames if len(item) > 3 and not isinstance(self.parmDict[item],basestring)]
3181        self.globNames = [':'.join(item) for item in splitNames if not item[0] and not item[1]]
3182        self.globVars = list(set([' ',]+[item[2] for item in splitNames if not item[0] and not item[1]]))
3183        self.globVars.sort()
3184        self.hisNames = [':'.join(item) for item in splitNames if not item[0] and item[1]]
3185        self.hisNums = list(set([int(item.split(':')[1]) for item in self.hisNames]))
3186        self.hisNums.sort()
3187        self.hisNums = [' ',]+[str(item) for item in self.hisNums]
3188        self.hisVars = list(set([' ',]+[item[2] for item in splitNames if not item[0]]))
3189        self.hisVars.sort()
3190        self.phasNames = [':'.join(item) for item in splitNames if not item[1] and 'is' not in item[2]]
3191        self.phasNums = [' ',]+list(set([item.split(':')[0] for item in self.phasNames]))
3192        if '' in self.phasNums: self.phasNums.remove('')
3193        self.phasVars = list(set([' ',]+[item[2] for item in splitNames if not item[1] and 'is' not in item[2]]))
3194        self.phasVars.sort()
3195        self.phasNums.sort()
3196        self.hapNames = [':'.join(item) for item in splitNames if item[0] and item[1]]
3197        self.hapVars = list(set([' ',]+[item[2] for item in splitNames if item[0] and item[1]]))
3198        self.hapVars.sort()
3199        self.hisNum = ' '
3200        self.phasNum = ' '
3201        self.varName = ' '
3202        self.listSel = 'Refined'
3203        self.DrawPanel()
3204       
3205           
3206    def DrawPanel(self):
3207           
3208        def _OnParmSel(event):
3209            self.parmChoice = parmSel.GetStringSelection()
3210            self.varName = ' '
3211            wx.CallLater(100,self.DrawPanel)
3212           
3213        def OnPhasSel(event):
3214            event.Skip()
3215            self.phasNum = phasSel.GetValue()
3216            self.varName = ' '
3217            wx.CallLater(100,self.DrawPanel)
3218
3219        def OnHistSel(event):
3220            event.Skip()
3221            self.hisNum = histSel.GetValue()
3222            self.varName = ' '
3223            wx.CallLater(100,self.DrawPanel)
3224           
3225        def OnVarSel(event):
3226            self.varName = varSel.GetValue()
3227            self.phasNum = ' '
3228            self.hisNum = ' '
3229            wx.CallLater(100,self.DrawPanel)
3230           
3231        def OnListSel(event):
3232            self.listSel = listSel.GetStringSelection()
3233            wx.CallLater(100,self.DrawPanel)
3234
3235        if self.panel.GetSizer(): self.panel.GetSizer().Clear(True)
3236        mainSizer = wx.BoxSizer(wx.VERTICAL)
3237        num = len(self.varyList)
3238        mainSizer.Add(wx.StaticText(self.panel,label=' Number of refined variables: '+str(num)),0)
3239        if len(self.varyList) != len(self.fullVaryList):
3240            num = len(self.fullVaryList) - len(self.varyList)
3241            mainSizer.Add(wx.StaticText(self.panel,label=' + '+str(num)+' parameters are varied via constraints'))
3242        choiceDict = {'Global':self.globNames,'Phase':self.phasNames,'Phase/Histo':self.hapNames,'Histogram':self.hisNames}
3243        choice = ['Phase','Phase/Histo','Histogram']
3244        if len(self.globNames):
3245            choice += ['Global',]
3246        parmSizer = wx.FlexGridSizer(0,3,5,5)
3247        parmSel = wx.RadioBox(self.panel,wx.ID_ANY,'Parameter type:',choices=choice,
3248            majorDimension=1,style=wx.RA_SPECIFY_COLS)
3249        parmSel.Bind(wx.EVT_RADIOBOX,_OnParmSel)
3250        parmSel.SetStringSelection(self.parmChoice)
3251        parmSizer.Add(parmSel,0)
3252        numSizer = wx.BoxSizer(wx.VERTICAL)
3253        numSizer.Add((5,25),0)
3254        if self.parmChoice in ['Phase','Phase/Histo'] and len(self.phasNums) > 1:
3255            numSizer.Add(wx.StaticText(self.panel,label='Phase'),0)
3256            phasSel = wx.ComboBox(self.panel,choices=self.phasNums,value=self.phasNum,
3257                style=wx.CB_READONLY|wx.CB_DROPDOWN)
3258            phasSel.Bind(wx.EVT_COMBOBOX,OnPhasSel)
3259            numSizer.Add(phasSel,0)
3260        if self.parmChoice in ['Histogram','Phase/Histo'] and len(self.hisNums) > 1:
3261            numSizer.Add(wx.StaticText(self.panel,label='Histogram'),0)
3262            histSel = wx.ComboBox(self.panel,choices=self.hisNums,value=self.hisNum,
3263                style=wx.CB_READONLY|wx.CB_DROPDOWN)
3264            histSel.Bind(wx.EVT_COMBOBOX,OnHistSel)
3265#            histSel = wx.TextCtrl(self.panel,size=(50,25),value='0',style=wx.TE_PROCESS_ENTER)
3266#            histSel.Bind(wx.EVT_TEXT_ENTER,OnHistSel)
3267#            histSel.Bind(wx.EVT_KILL_FOCUS,OnHistSel)
3268            numSizer.Add(histSel,0)
3269        parmSizer.Add(numSizer)
3270        varSizer = wx.BoxSizer(wx.VERTICAL)
3271        if self.parmChoice in ['Phase',]:
3272            varSel = wx.ComboBox(self.panel,choices=self.phasVars,value=self.varName,
3273                style=wx.CB_READONLY|wx.CB_DROPDOWN)
3274            varSel.Bind(wx.EVT_COMBOBOX,OnVarSel)
3275        elif self.parmChoice in ['Histogram',]:
3276            varSel = wx.ComboBox(self.panel,choices=self.hisVars,value=self.varName,
3277                style=wx.CB_READONLY|wx.CB_DROPDOWN)
3278            varSel.Bind(wx.EVT_COMBOBOX,OnVarSel)
3279        elif self.parmChoice in ['Phase/Histo',]:
3280            varSel = wx.ComboBox(self.panel,choices=self.hapVars,value=self.varName,
3281                style=wx.CB_READONLY|wx.CB_DROPDOWN)
3282            varSel.Bind(wx.EVT_COMBOBOX,OnVarSel)
3283        if self.parmChoice != 'Global': 
3284            varSizer.Add(wx.StaticText(self.panel,label='Parameter'))
3285            varSizer.Add(varSel,0)
3286        parmSizer.Add(varSizer,0)
3287        mainSizer.Add(parmSizer,0)
3288        listChoice = ['All','Refined']
3289        listSel = wx.RadioBox(self.panel,wx.ID_ANY,'Parameter type:',choices=listChoice,
3290            majorDimension=0,style=wx.RA_SPECIFY_COLS)
3291        listSel.SetStringSelection(self.listSel)
3292        listSel.Bind(wx.EVT_RADIOBOX,OnListSel)
3293        mainSizer.Add(listSel,0)
3294        subSizer = wx.FlexGridSizer(cols=4,hgap=2,vgap=2)
3295        subSizer.Add((-1,-1))
3296        subSizer.Add(wx.StaticText(self.panel,wx.ID_ANY,'Parameter name  '))
3297        subSizer.Add(wx.StaticText(self.panel,wx.ID_ANY,'refine?'))
3298        subSizer.Add(wx.StaticText(self.panel,wx.ID_ANY,'value'),0,wx.ALIGN_RIGHT)
3299        explainRefine = False
3300        for name in choiceDict[self.parmChoice]:
3301            # skip entries without numerical values
3302            if isinstance(self.parmDict[name],basestring): continue
3303            if 'Refined' in self.listSel and (name not in self.fullVaryList
3304                                              ) and (name not in self.varyList):
3305                continue
3306            if 'Phase' in self.parmChoice:
3307                if self.phasNum != ' ' and name.split(':')[0] != self.phasNum: continue
3308            if 'Histo' in self.parmChoice:
3309                if self.hisNum != ' ' and name.split(':')[1] != self.hisNum: continue
3310            if (self.varName != ' ') and (self.varName not in name): continue
3311            try:
3312                value = G2py3.FormatSigFigs(self.parmDict[name])
3313            except TypeError:
3314                value = str(self.parmDict[name])+' -?' # unexpected
3315                #continue
3316            v = G2obj.getVarDescr(name)
3317            if v is None or v[-1] is None:
3318                subSizer.Add((-1,-1))
3319            else:               
3320                ch = HelpButton(self.panel,G2obj.fmtVarDescr(name))
3321                subSizer.Add(ch,0,wx.LEFT|wx.RIGHT|WACV|wx.ALIGN_CENTER,1)
3322            subSizer.Add(wx.StaticText(self.panel,wx.ID_ANY,str(name)))
3323            if name in self.varyList:
3324                subSizer.Add(wx.StaticText(self.panel,label='R'))   #TODO? maybe a checkbox for one stop refinemnt flag setting?
3325            elif name in self.fullVaryList:
3326                subSizer.Add(wx.StaticText(self.panel,label='C'))
3327                explainRefine = True
3328            else:
3329                subSizer.Add((-1,-1))
3330            subSizer.Add(wx.StaticText(self.panel,label=value),0,wx.ALIGN_RIGHT)
3331
3332        mainSizer.Add(subSizer,0)
3333        if explainRefine:
3334            mainSizer.Add(
3335                wx.StaticText(self.panel,label='"R" indicates a refined variable\n'+
3336                    '"C" indicates generated from a constraint'),0, wx.ALL,0)
3337        # make OK button
3338        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
3339        btn = wx.Button(self.panel, wx.ID_CLOSE,"Close") 
3340        btn.Bind(wx.EVT_BUTTON,self._onClose)
3341        btnsizer.Add(btn)
3342        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
3343        # Allow window to be enlarged but not made smaller
3344        self.panel.SetSizer(mainSizer)
3345        self.panel.SetAutoLayout(1)
3346        self.panel.SetupScrolling()
3347        self.panel.SetMinSize(self.GetSize())
3348
3349    def _onClose(self,event):
3350        self.EndModal(wx.ID_CANCEL)
3351
3352################################################################################
3353#####  Customized Grid Support
3354################################################################################           
3355class GSGrid(wg.Grid):
3356    '''Basic wx.Grid implementation
3357    '''
3358    def __init__(self, parent, name=''):
3359        wg.Grid.__init__(self,parent,-1,name=name)
3360        if hasattr(parent.TopLevelParent,'currentGrids'):
3361            parent.TopLevelParent.currentGrids.append(self)      # save a reference to the grid in the Frame
3362        self.SetScrollRate(0,0)         #GSAS-II grids have no scroll bars by default
3363           
3364    def Clear(self):
3365        wg.Grid.ClearGrid(self)
3366       
3367    def SetCellReadOnly(self,r,c,readonly=True):
3368        self.SetReadOnly(r,c,isReadOnly=readonly)
3369       
3370    def SetCellStyle(self,r,c,color="white",readonly=True):
3371        self.SetCellBackgroundColour(r,c,color)
3372        self.SetReadOnly(r,c,isReadOnly=readonly)
3373       
3374    def GetSelection(self):
3375        #this is to satisfy structure drawing stuff in G2plt when focus changes
3376        return None
3377
3378    def InstallGridToolTip(self, rowcolhintcallback,
3379                           colLblCallback=None,rowLblCallback=None):
3380        '''code to display a tooltip for each item on a grid
3381        from http://wiki.wxpython.org/wxGrid%20ToolTips (buggy!), expanded to
3382        column and row labels using hints from
3383        https://groups.google.com/forum/#!topic/wxPython-users/bm8OARRVDCs
3384
3385        :param function rowcolhintcallback: a routine that returns a text
3386          string depending on the selected row and column, to be used in
3387          explaining grid entries.
3388        :param function colLblCallback: a routine that returns a text
3389          string depending on the selected column, to be used in
3390          explaining grid columns (if None, the default), column labels
3391          do not get a tooltip.
3392        :param function rowLblCallback: a routine that returns a text
3393          string depending on the selected row, to be used in
3394          explaining grid rows (if None, the default), row labels
3395          do not get a tooltip.
3396        '''
3397        prev_rowcol = [None,None,None]
3398        def OnMouseMotion(event):
3399            # event.GetRow() and event.GetCol() would be nice to have here,
3400            # but as this is a mouse event, not a grid event, they are not
3401            # available and we need to compute them by hand.
3402            x, y = self.CalcUnscrolledPosition(event.GetPosition())
3403            row = self.YToRow(y)
3404            col = self.XToCol(x)
3405            hinttext = ''
3406            win = event.GetEventObject()
3407            if [row,col,win] == prev_rowcol: # no change from last position
3408                if event: event.Skip()
3409                return
3410            if win == self.GetGridWindow() and row >= 0 and col >= 0:
3411                hinttext = rowcolhintcallback(row, col)
3412            elif win == self.GetGridColLabelWindow() and col >= 0:
3413                if colLblCallback: hinttext = colLblCallback(col)
3414            elif win == self.GetGridRowLabelWindow() and row >= 0:
3415                if rowLblCallback: hinttext = rowLblCallback(row)
3416            else: # this should be the upper left corner, which is empty
3417                if event: event.Skip()
3418                return
3419            if hinttext is None: hinttext = ''
3420            win.SetToolTipString(hinttext)
3421            prev_rowcol[:] = [row,col,win]
3422            if event: event.Skip()
3423
3424        wx.EVT_MOTION(self.GetGridWindow(), OnMouseMotion)
3425        if colLblCallback: wx.EVT_MOTION(self.GetGridColLabelWindow(), OnMouseMotion)
3426        if rowLblCallback: wx.EVT_MOTION(self.GetGridRowLabelWindow(), OnMouseMotion)
3427                                                   
3428################################################################################           
3429class Table(wg.PyGridTableBase):
3430    '''Basic data table for use with GSgrid
3431    '''
3432    def __init__(self, data=[], rowLabels=None, colLabels=None, types = None):
3433        if 'phoenix' in wx.version():
3434            wg.GridTableBase.__init__(self)
3435        else:
3436            wg.PyGridTableBase.__init__(self)
3437        self.colLabels = colLabels
3438        self.rowLabels = rowLabels
3439        self.dataTypes = types
3440        self.data = data
3441       
3442    def AppendRows(self, numRows=1):
3443        self.data.append([])
3444        return True
3445       
3446    def CanGetValueAs(self, row, col, typeName):
3447        if self.dataTypes:
3448            colType = self.dataTypes[col].split(':')[0]
3449            if typeName == colType:
3450                return True
3451            else:
3452                return False
3453        else:
3454            return False
3455
3456    def CanSetValueAs(self, row, col, typeName):
3457        return self.CanGetValueAs(row, col, typeName)
3458
3459    def DeleteRow(self,pos):
3460        data = self.GetData()
3461        self.SetData([])
3462        new = []
3463        for irow,row in enumerate(data):
3464            if irow != pos:
3465                new.append(row)
3466        self.SetData(new)
3467       
3468    def GetColLabelValue(self, col):
3469        if self.colLabels:
3470            return self.colLabels[col]
3471           
3472    def GetData(self):
3473        data = []
3474        for row in range(self.GetNumberRows()):
3475            data.append(self.GetRowValues(row))
3476        return data
3477       
3478    def GetNumberCols(self):
3479        try:
3480            return len(self.colLabels)
3481        except TypeError:
3482            return None
3483       
3484    def GetNumberRows(self):
3485        return len(self.data)
3486       
3487    def GetRowLabelValue(self, row):
3488        if self.rowLabels:
3489            return self.rowLabels[row]
3490       
3491    def GetColValues(self, col):
3492        data = []
3493        for row in range(self.GetNumberRows()):
3494            data.append(self.GetValue(row, col))
3495        return data
3496       
3497    def GetRowValues(self, row):
3498        data = []
3499        for col in range(self.GetNumberCols()):
3500            data.append(self.GetValue(row, col))
3501        return data
3502       
3503    def GetTypeName(self, row, col):
3504        try:
3505            if self.data[row][col] is None: return None
3506            return self.dataTypes[col]
3507        except (TypeError,IndexError):
3508            return None
3509
3510    def GetValue(self, row, col):
3511        try:
3512            if self.data[row][col] is None: return ""
3513            return self.data[row][col]
3514        except IndexError:
3515            return None
3516           
3517    def InsertRows(self, pos, rows):
3518        for row in range(rows):
3519            self.data.insert(pos,[])
3520            pos += 1
3521       
3522    def IsEmptyCell(self,row,col):
3523        try:
3524            return not self.data[row][col]
3525        except IndexError:
3526            return True
3527       
3528    def OnKeyPress(self, event):
3529        dellist = self.GetSelectedRows()
3530        if event.GetKeyCode() == wx.WXK_DELETE and dellist:
3531            grid = self.GetView()
3532            for i in dellist: grid.DeleteRow(i)
3533               
3534    def SetColLabelValue(self, col, label):
3535        numcols = self.GetNumberCols()
3536        if col > numcols-1:
3537            self.colLabels.append(label)
3538        else:
3539            self.colLabels[col]=label
3540       
3541    def SetData(self,data):
3542        for row in range(len(data)):
3543            self.SetRowValues(row,data[row])
3544               
3545    def SetRowLabelValue(self, row, label):
3546        self.rowLabels[row]=label
3547           
3548    def SetRowValues(self,row,data):
3549        self.data[row] = data
3550           
3551    def SetValue(self, row, col, value):
3552        def innerSetValue(row, col, value):
3553            try:
3554                self.data[row][col] = value
3555            except TypeError:
3556                return
3557            except IndexError: # has this been tested?
3558                #print row,col,value
3559                # add a new row
3560                if row > self.GetNumberRows():
3561                    self.data.append([''] * self.GetNumberCols())
3562                elif col > self.GetNumberCols():
3563                    for row in range(self.GetNumberRows()): # bug fixed here
3564                        self.data[row].append('')
3565                #print self.data
3566                self.data[row][col] = value
3567        innerSetValue(row, col, value)
3568
3569################################################################################
3570class GridFractionEditor(wg.PyGridCellEditor):
3571    '''A grid cell editor class that allows entry of values as fractions as well
3572    as sine and cosine values [as s() and c()]
3573    '''
3574    def __init__(self,grid):
3575        if 'phoenix' in wx.version():
3576            wg.GridCellEditor.__init__(self)
3577        else:
3578            wg.PyGridCellEditor.__init__(self)
3579
3580    def Create(self, parent, id, evtHandler):
3581        self._tc = wx.TextCtrl(parent, id, "")
3582        self._tc.SetInsertionPoint(0)
3583        self.SetControl(self._tc)
3584
3585        if evtHandler:
3586            self._tc.PushEventHandler(evtHandler)
3587
3588        self._tc.Bind(wx.EVT_CHAR, self.OnChar)
3589
3590    def SetSize(self, rect):
3591        self._tc.SetDimensions(rect.x, rect.y, rect.width+2, rect.height+2,
3592                               wx.SIZE_ALLOW_MINUS_ONE)
3593
3594    def BeginEdit(self, row, col, grid):
3595        self.startValue = grid.GetTable().GetValue(row, col)
3596        self._tc.SetValue(str(self.startValue))
3597        self._tc.SetInsertionPointEnd()
3598        self._tc.SetFocus()
3599        self._tc.SetSelection(0, self._tc.GetLastPosition())
3600
3601    def EndEdit(self, row, col, grid, oldVal=None):
3602        changed = False
3603
3604        self.nextval = self.startValue
3605        val = self._tc.GetValue().lower().strip()
3606        if val != self.startValue:
3607            changed = True
3608            neg = False
3609            if val.startswith('-'):
3610                neg = True
3611                val = val[1:]
3612            # allow old GSAS s20 and c20 etc for sind(20) and cosd(20)
3613            if val.startswith('s') and '(' not in val:
3614                val = 'sind('+val.strip('s')+')'
3615            elif val.startswith('c') and '(' not in val:
3616                val = 'cosd('+val.strip('c')+')'
3617            if neg:
3618                val = '-' + val
3619            val = G2py3.FormulaEval(val)
3620            if val is not None:
3621                self.nextval = val
3622            else:
3623                return None
3624            if oldVal is None: # this arg appears in 2.9+; before, we should go ahead & change the table
3625                grid.GetTable().SetValue(row, col, val) # update the table
3626            # otherwise self.ApplyEdit gets called
3627
3628        self.startValue = ''
3629        self._tc.SetValue('')
3630        return changed
3631   
3632    def ApplyEdit(self, row, col, grid):
3633        """ Called only in wx >= 2.9
3634        Save the value of the control into the grid if EndEdit() returns as True
3635        """
3636        grid.GetTable().SetValue(row, col, self.nextval) # update the table
3637
3638    def Reset(self):
3639        self._tc.SetValue(self.startValue)
3640        self._tc.SetInsertionPointEnd()
3641
3642    def Clone(self,grid):
3643        return GridFractionEditor(grid)
3644
3645    def StartingKey(self, evt):
3646        self.OnChar(evt)
3647        if evt.GetSkipped():
3648            self._tc.EmulateKeyPress(evt)
3649
3650    def OnChar(self, evt):
3651        key = evt.GetKeyCode()
3652        if key < 32 or key >= 127:
3653            evt.Skip()
3654        elif chr(key).lower() in '.+-*/0123456789cosind()':
3655            evt.Skip()
3656        else:
3657            evt.StopPropagation()
3658           
3659################################################################################
3660#####  Customized Notebook
3661################################################################################           
3662class GSNoteBook(wx.aui.AuiNotebook):
3663    '''Notebook used in various locations; implemented with wx.aui extension
3664    '''
3665    def __init__(self, parent, name='',size = None,style=wx.aui.AUI_NB_TOP |
3666        wx.aui.AUI_NB_SCROLL_BUTTONS):
3667        wx.aui.AuiNotebook.__init__(self, parent, style=style)
3668        if size: self.SetSize(size)
3669        self.parent = parent
3670        self.PageChangeHandler = None
3671       
3672    def PageChangeEvent(self,event):
3673        pass
3674#        G2frame = self.parent.G2frame
3675#        page = event.GetSelection()
3676#        if self.PageChangeHandler:
3677#            if log.LogInfo['Logging']:
3678#                log.MakeTabLog(
3679#                    G2frame.dataWindow.GetTitle(),
3680#                    G2frame.dataDisplay.GetPageText(page)
3681#                    )
3682#            self.PageChangeHandler(event)
3683           
3684#    def Bind(self,eventtype,handler,*args,**kwargs):
3685#        '''Override the Bind() function so that page change events can be trapped
3686#        '''
3687#        if eventtype == wx.aui.EVT_AUINOTEBOOK_PAGE_CHANGED:
3688#            self.PageChangeHandler = handler
3689#            wx.aui.AuiNotebook.Bind(self,eventtype,self.PageChangeEvent)
3690#            return
3691#        wx.aui.AuiNotebook.Bind(self,eventtype,handler,*args,**kwargs)
3692                                                     
3693    def Clear(self):       
3694        GSNoteBook.DeleteAllPages(self)
3695       
3696    def FindPage(self,name):
3697        numPage = self.GetPageCount()
3698        for page in range(numPage):
3699            if self.GetPageText(page) == name:
3700                return page
3701        return None
3702
3703    def ChangeSelection(self,page):
3704        # in wx.Notebook ChangeSelection is like SetSelection, but it
3705        # does not invoke the event related to pressing the tab button
3706        # I don't see a way to do that in aui.
3707        oldPage = self.GetSelection()
3708        self.SetSelection(page)
3709        return oldPage
3710
3711    # def __getattribute__(self,name):
3712    #     '''This method provides a way to print out a message every time
3713    #     that a method in a class is called -- to see what all the calls
3714    #     might be, or where they might be coming from.
3715    #     Cute trick for debugging!
3716    #     '''
3717    #     attr = object.__getattribute__(self, name)
3718    #     if hasattr(attr, '__call__'):
3719    #         def newfunc(*args, **kwargs):
3720    #             print('GSauiNoteBook calling %s' %attr.__name__)
3721    #             result = attr(*args, **kwargs)
3722    #             return result
3723    #         return newfunc
3724    #     else:
3725    #         return attr
3726           
3727################################################################################
3728#### Help support routines
3729################################################################################
3730class MyHelp(wx.Menu):
3731    '''
3732    A class that creates the contents of a help menu.
3733    The menu will start with two entries:
3734
3735    * 'Help on <helpType>': where helpType is a reference to an HTML page to
3736      be opened
3737    * About: opens an About dialog using OnHelpAbout. N.B. on the Mac this
3738      gets moved to the App menu to be consistent with Apple style.
3739
3740    NOTE: for this to work properly with respect to system menus, the title
3741    for the menu must be &Help, or it will not be processed properly:
3742
3743    ::
3744
3745       menu.Append(menu=MyHelp(self,...),title="&Help")
3746
3747    '''
3748    def __init__(self,frame,includeTree=False,morehelpitems=[]):
3749        wx.Menu.__init__(self,'')
3750        self.HelpById = {}
3751        self.frame = frame
3752        self.Append(wx.ID_ABOUT,'&About GSAS-II','')
3753        frame.Bind(wx.EVT_MENU, self.OnHelpAbout, id=wx.ID_ABOUT)
3754        if GSASIIpath.whichsvn():
3755            helpobj = self.Append(wx.ID_ANY,'&Check for updates','')
3756            frame.Bind(wx.EVT_MENU, self.OnCheckUpdates, helpobj)
3757            helpobj = self.Append(wx.ID_ANY,'&Regress to an old GSAS-II version','')
3758            frame.Bind(wx.EVT_MENU, self.OnSelectVersion, helpobj)
3759            # if GSASIIpath.svnTestBranch():
3760            #     msg = "&Switch back to standard GSAS-II version"
3761            # else:
3762            #     msg = "&Switch to test (2frame) GSAS-II version"
3763            # helpobj = self.Append(
3764            #     help='', id=wx.ID_ANY, kind=wx.ITEM_NORMAL,text=msg)
3765            # frame.Bind(wx.EVT_MENU, self.OnSelectBranch, helpobj)
3766        # provide special help topic names for extra items in help menu
3767        for lbl,indx in morehelpitems:
3768            helpobj = self.Append(wx.ID_ANY,lbl,'')
3769            frame.Bind(wx.EVT_MENU, self.OnHelpById, helpobj)
3770            self.HelpById[helpobj.GetId()] = indx
3771        # add help lookup(s) in gsasii.html
3772        self.AppendSeparator()
3773        if includeTree:
3774            helpobj = self.Append(wx.ID_ANY,'Help on Data tree','')
3775            frame.Bind(wx.EVT_MENU, self.OnHelpById, id=helpobj.GetId())
3776            self.HelpById[helpobj.GetId()] = 'Data tree'
3777        helpobj = self.Append(wx.ID_ANY,'Help on current data tree item','')
3778        frame.Bind(wx.EVT_MENU, self.OnHelpById, id=helpobj.GetId())
3779       
3780    def OnHelpById(self,event):
3781        '''Called when Help on... is pressed in a menu. Brings up a web page
3782        for documentation. Uses the helpKey value from the dataWindow window
3783        unless a special help key value has been defined for this menu id in
3784        self.HelpById
3785
3786        Note that self should now (2frame) be child of the main window (G2frame)
3787        '''
3788        if hasattr(self.frame,'dataWindow'):  # Debug code: check this is called from menu in G2frame
3789            # should always be true in 2 Frame version
3790            dW = self.frame.dataWindow
3791        else:
3792            print('help error: not called from standard menu?')
3793            print (self)
3794            return           
3795        try:
3796            helpKey = dW.helpKey # look up help from helpKey in data window
3797            #if GSASIIpath.GetConfigValue('debug'): print 'dataWindow help: key=',helpKey
3798        except AttributeError:
3799            helpKey = ''
3800            if GSASIIpath.GetConfigValue('debug'): print('No helpKey for current dataWindow!')
3801        helpType = self.HelpById.get(event.GetId(),helpKey) # see if there is a special help topic
3802        #if GSASIIpath.GetConfigValue('debug'): print 'helpKey=',helpKey,'  helpType=',helpType
3803        if helpType == 'Tutorials':
3804            dlg = OpenTutorial(self.frame)
3805            dlg.ShowModal()
3806            dlg.Destroy()
3807            return
3808        else:
3809            ShowHelp(helpType,self.frame)
3810
3811    def OnHelpAbout(self, event):
3812        "Display an 'About GSAS-II' box"
3813        import GSASII
3814        try:
3815            import wx.adv as wxadv  # AboutBox moved here in Phoenix
3816        except:
3817            wxadv = wx
3818        info = wxadv.AboutDialogInfo()
3819        info.Name = 'GSAS-II'
3820        ver = GSASIIpath.svnGetRev()
3821        if not ver:
3822            ver = GSASIIpath.GetVersionNumber()
3823        info.SetVersion(ver)
3824        #info.Developers = ['Robert B. Von Dreele','Brian H. Toby']
3825        info.Copyright = ('(c) ' + time.strftime('%Y') +
3826''' Argonne National Laboratory
3827This product includes software developed
3828by the UChicago Argonne, LLC, as
3829Operator of Argonne National Laboratory.''')
3830        info.Description = '''General Structure Analysis System-II (GSAS-II)
3831Robert B. Von Dreele and Brian H. Toby
3832
3833Please cite as:
3834  B.H. Toby & R.B. Von Dreele, J. Appl. Cryst. 46, 544-549 (2013)
3835For small angle use cite:
3836  R.B. Von Dreele, J. Appl. Cryst. 47, 1748-9 (2014)
3837For DIFFaX use cite:
3838  M.M.J. Treacy, J.M. Newsam & M.W. Deem,
3839  Proc. Roy. Soc. Lond. A 433, 499-520 (1991)
3840'''
3841        info.WebSite = ("https://subversion.xray.aps.anl.gov/trac/pyGSAS","GSAS-II home page")
3842        wxadv.AboutBox(info)
3843
3844    def OnCheckUpdates(self,event):
3845        '''Check if the GSAS-II repository has an update for the current source files
3846        and perform that update if requested.
3847        '''           
3848        if not GSASIIpath.whichsvn():
3849            dlg = wx.MessageDialog(self.frame,
3850                                   'No Subversion','Cannot update GSAS-II because subversion (svn) was not found.',
3851                                   wx.OK)
3852            dlg.ShowModal()
3853            dlg.Destroy()
3854            return
3855        wx.BeginBusyCursor()
3856        local = GSASIIpath.svnGetRev()
3857        if local is None: 
3858            wx.EndBusyCursor()
3859            dlg = wx.MessageDialog(self.frame,
3860                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
3861                                   'Subversion error',
3862                                   wx.OK)
3863            dlg.ShowModal()
3864            dlg.Destroy()
3865            return
3866        print ('Installed GSAS-II version: '+local)
3867        repos = GSASIIpath.svnGetRev(local=False)
3868        wx.EndBusyCursor()
3869        # has the current branch disappeared? If so, switch to the trunk -- not fully tested
3870        if (repos is None and "not found" in GSASIIpath.svnLastError.lower()
3871            and "path" in GSASIIpath.svnLastError.lower()):
3872            print('Repository is gone, will switch to trunk')
3873            GSASIIpath.svnSwitch2branch()
3874            return
3875        elif repos is None: 
3876            dlg = wx.MessageDialog(self.frame,
3877                                   'Unable to access the GSAS-II server. Is this computer on the internet?',
3878                                   'Server unavailable',
3879                                   wx.OK)
3880            dlg.ShowModal()
3881            dlg.Destroy()
3882            return
3883        print ('GSAS-II version on server: '+repos)
3884        if local == repos:
3885            dlg = wx.MessageDialog(self.frame,
3886                                   'GSAS-II is up-to-date. Version '+local+' is already loaded.',
3887                                   'GSAS-II Up-to-date',
3888                                   wx.OK)
3889            dlg.ShowModal()
3890            dlg.Destroy()
3891            return
3892        mods = GSASIIpath.svnFindLocalChanges()
3893        if mods:
3894            dlg = wx.MessageDialog(self.frame,
3895                                   'You have version '+local+
3896                                   ' of GSAS-II installed, but the current version is '+repos+
3897                                   '. However, '+str(len(mods))+
3898                                   ' file(s) on your local computer have been modified.'
3899                                   ' Updating will attempt to merge your local changes with '
3900                                   'the latest GSAS-II version, but if '
3901                                   'conflicts arise, local changes will be '
3902                                   'discarded. It is also possible that the '
3903                                   'local changes my prevent GSAS-II from running. '
3904                                   'Press OK to start an update if this is acceptable:',
3905                                   'Local GSAS-II Mods',
3906                                   wx.OK|wx.CANCEL)
3907            if dlg.ShowModal() != wx.ID_OK:
3908                dlg.Destroy()
3909                return
3910            else:
3911                dlg.Destroy()
3912        else:
3913            dlg = wx.MessageDialog(self.frame,
3914                                   'You have version '+local+
3915                                   ' of GSAS-II installed, but the current version is '+repos+
3916                                   '. Press OK to start an update:',
3917                                   'GSAS-II Updates',
3918                                   wx.OK|wx.CANCEL)
3919            if dlg.ShowModal() != wx.ID_OK:
3920                dlg.Destroy()
3921                return
3922            dlg.Destroy()
3923        print ('start updates')
3924        dlg = wx.MessageDialog(self.frame,
3925                               'Your project will now be saved, GSAS-II will exit and an update '
3926                               'will be performed and GSAS-II will restart. Press Cancel to '
3927                               'abort the update',
3928                               'Start update?',
3929                               wx.OK|wx.CANCEL)
3930        if dlg.ShowModal() != wx.ID_OK:
3931            dlg.Destroy()
3932            return
3933        dlg.Destroy()
3934        self.frame.OnFileSave(event)
3935        GPX = self.frame.GSASprojectfile
3936        GSASIIpath.svnUpdateProcess(projectfile=GPX)
3937        return
3938
3939    def OnSelectVersion(self,event):
3940        '''Allow the user to select a specific version of GSAS-II
3941        '''
3942        if not GSASIIpath.whichsvn():
3943            dlg = wx.MessageDialog(self,'No Subversion','Cannot update GSAS-II because subversion (svn) '+
3944                                   'was not found.'
3945                                   ,wx.OK)
3946            dlg.ShowModal()
3947            return
3948        local = GSASIIpath.svnGetRev()
3949        if local is None: 
3950            dlg = wx.MessageDialog(self.frame,
3951                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
3952                                   'Subversion error',
3953                                   wx.OK)
3954            dlg.ShowModal()
3955            dlg.Destroy()
3956            return
3957        mods = GSASIIpath.svnFindLocalChanges()
3958        if mods:
3959            dlg = wx.MessageDialog(self.frame,
3960                                   'You have version '+local+
3961                                   ' of GSAS-II installed'
3962                                   '. However, '+str(len(mods))+
3963                                   ' file(s) on your local computer have been modified.'
3964                                   ' Downdating will attempt to merge your local changes with '
3965                                   'the selected GSAS-II version. '
3966                                   'Downdating is not encouraged because '
3967                                   'if merging is not possible, your local changes will be '
3968                                   'discarded. It is also possible that the '
3969                                   'local changes my prevent GSAS-II from running. '
3970                                   'Press OK to continue anyway.',
3971                                   'Local GSAS-II Mods',
3972                                   wx.OK|wx.CANCEL)
3973            if dlg.ShowModal() != wx.ID_OK:
3974                dlg.Destroy()
3975                return
3976            dlg.Destroy()
3977        if GSASIIpath.svnGetRev(local=False) is None:
3978            dlg = wx.MessageDialog(self.frame,
3979                                   'Error obtaining current GSAS-II version. Is internet access working correctly?',
3980                                   'Subversion error',
3981                                   wx.OK)
3982            dlg.ShowModal()
3983            dlg.Destroy()
3984            return
3985        dlg = downdate(parent=self.frame)
3986        if dlg.ShowModal() == wx.ID_OK:
3987            ver = dlg.getVersion()
3988        else:
3989            dlg.Destroy()
3990            return
3991        dlg.Destroy()
3992        print('start regress to '+str(ver))
3993        self.frame.OnFileSave(event)
3994        GPX = self.frame.GSASprojectfile
3995        GSASIIpath.svnUpdateProcess(projectfile=GPX,version=str(ver))
3996        return
3997
3998    # def OnSelectBranch(self,event):
3999    #     '''Allow the user to select branch of GSAS-II or return to trunk
4000    #     N.B. Name of branch to use is hard-coded here. Must contain a slash
4001    #     '''
4002    #     testbranch = '/branch/2frame'
4003    #     if not GSASIIpath.svnTestBranch():
4004    #         dlg = wx.MessageDialog(self.frame,
4005    #                                'Switching to test GSAS-II version',
4006    #                                'Confirm Switch',
4007    #                                wx.OK|wx.CANCEL)
4008    #         if dlg.ShowModal() != wx.ID_OK: return
4009    #         branch = testbranch
4010    #     else:
4011    #         dlg = wx.MessageDialog(self.frame,
4012    #                                'Switching back to standard GSAS-II version',
4013    #                                'Confirm Switch',
4014    #                                wx.OK|wx.CANCEL)
4015    #         if dlg.ShowModal() != wx.ID_OK: return
4016    #         branch = 'trunk'
4017    #     print('start switch')
4018    #     self.frame.OnFileSave(event)
4019    #     GPX = self.frame.GSASprojectfile
4020    #     GSASIIpath.svnUpdateProcess(projectfile=GPX,branch=branch)
4021
4022################################################################################
4023class HelpButton(wx.Button):
4024    '''Create a help button that displays help information.
4025    The text is displayed in a modal message window.
4026
4027    TODO: it might be nice if it were non-modal: e.g. it stays around until
4028    the parent is deleted or the user closes it, but this did not work for
4029    me.
4030
4031    :param parent: the panel which will be the parent of the button
4032    :param str msg: the help text to be displayed
4033    '''
4034    def __init__(self,parent,msg):
4035        if sys.platform == "darwin": 
4036            wx.Button.__init__(self,parent,wx.ID_HELP)
4037        else:
4038            wx.Button.__init__(self,parent,wx.ID_ANY,'?',style=wx.BU_EXACTFIT)
4039        self.Bind(wx.EVT_BUTTON,self._onPress)
4040        self.msg=StripIndents(msg)
4041        self.parent = parent
4042    def _onClose(self,event):
4043        self.dlg.EndModal(wx.ID_CANCEL)
4044    def _onPress(self,event):
4045        'Respond to a button press by displaying the requested text'
4046        #dlg = wx.MessageDialog(self.parent,self.msg,'Help info',wx.OK)
4047        self.dlg = wx.Dialog(self.parent,wx.ID_ANY,'Help information', 
4048                        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
4049        #self.dlg.SetBackgroundColour(wx.WHITE)
4050        mainSizer = wx.BoxSizer(wx.VERTICAL)
4051        txt = wx.StaticText(self.dlg,wx.ID_ANY,self.msg)
4052        mainSizer.Add(txt,1,wx.ALL|wx.EXPAND,10)
4053        txt.SetBackgroundColour(wx.WHITE)
4054
4055        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
4056        btn = wx.Button(self.dlg, wx.ID_CLOSE) 
4057        btn.Bind(wx.EVT_BUTTON,self._onClose)
4058        btnsizer.Add(btn)
4059        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
4060        self.dlg.SetSizer(mainSizer)
4061        mainSizer.Fit(self.dlg)
4062        self.dlg.CenterOnParent()
4063        self.dlg.ShowModal()
4064        self.dlg.Destroy()
4065################################################################################
4066class MyHtmlPanel(wx.Panel):
4067    '''Defines a panel to display HTML help information, as an alternative to
4068    displaying help information in a web browser.
4069    '''
4070    def __init__(self, frame, id):
4071        self.frame = frame
4072        wx.Panel.__init__(self, frame, id)
4073        sizer = wx.BoxSizer(wx.VERTICAL)
4074        back = wx.Button(self, -1, "Back")
4075        back.Bind(wx.EVT_BUTTON, self.OnBack)
4076        self.htmlwin = G2HtmlWindow(self, id, size=(750,450))
4077        sizer.Add(self.htmlwin, 1,wx.EXPAND)
4078        sizer.Add(back, 0, wx.ALIGN_LEFT, 0)
4079        self.SetSizer(sizer)
4080        sizer.Fit(frame)       
4081        self.Bind(wx.EVT_SIZE,self.OnHelpSize)
4082    def OnHelpSize(self,event):         #does the job but weirdly!!
4083        anchor = self.htmlwin.GetOpenedAnchor()
4084        if anchor:           
4085            self.htmlwin.ScrollToAnchor(anchor)
4086            wx.CallAfter(self.htmlwin.ScrollToAnchor,anchor)
4087            if event: event.Skip()
4088    def OnBack(self, event):
4089        self.htmlwin.HistoryBack()
4090    def LoadFile(self,file):
4091        pos = file.rfind('#')
4092        if pos != -1:
4093            helpfile = file[:pos]
4094            helpanchor = file[pos+1:]
4095        else:
4096            helpfile = file
4097            helpanchor = None
4098        self.htmlwin.LoadPage(helpfile)
4099        if helpanchor is not None:
4100            self.htmlwin.ScrollToAnchor(helpanchor)
4101            xs,ys = self.htmlwin.GetViewStart()
4102            self.htmlwin.Scroll(xs,ys-1)
4103################################################################################
4104class G2HtmlWindow(wx.html.HtmlWindow):
4105    '''Displays help information in a primitive HTML browser type window
4106    '''
4107    def __init__(self, parent, *args, **kwargs):
4108        self.parent = parent
4109        wx.html.HtmlWindow.__init__(self, parent, *args, **kwargs)
4110    def LoadPage(self, *args, **kwargs):
4111        wx.html.HtmlWindow.LoadPage(self, *args, **kwargs)
4112        self.TitlePage()
4113    def OnLinkClicked(self, *args, **kwargs):
4114        wx.html.HtmlWindow.OnLinkClicked(self, *args, **kwargs)
4115        xs,ys = self.GetViewStart()
4116        self.Scroll(xs,ys-1)
4117        self.TitlePage()
4118    def HistoryBack(self, *args, **kwargs):
4119        wx.html.HtmlWindow.HistoryBack(self, *args, **kwargs)
4120        self.TitlePage()
4121    def TitlePage(self):
4122        self.parent.frame.SetTitle(self.GetOpenedPage() + ' -- ' + 
4123            self.GetOpenedPageTitle())
4124
4125################################################################################
4126def StripIndents(msg):
4127    'Strip indentation from multiline strings'
4128    msg1 = msg.replace('\n ','\n')
4129    while msg != msg1:
4130        msg = msg1
4131        msg1 = msg.replace('\n ','\n')
4132    return msg.replace('\n\t','\n')
4133
4134def StripUnicode(string,subs='.'):
4135    '''Strip non-ASCII characters from strings
4136   
4137    :param str string: string to strip Unicode characters from
4138    :param str subs: character(s) to place into string in place of each
4139      Unicode character. Defaults to '.'
4140
4141    :returns: a new string with only ASCII characters
4142    '''
4143    s = ''
4144    for c in string:
4145        if ord(c) < 128:
4146            s += c
4147        else:
4148            s += subs
4149    return s.encode('ascii','replace')
4150       
4151################################################################################
4152# configuration routines (for editing config.py)
4153def SaveGPXdirectory(path):
4154    if GSASIIpath.GetConfigValue('Starting_directory') == path: return
4155    vars = GetConfigValsDocs()
4156    try:
4157        vars['Starting_directory'][1] = path
4158        if GSASIIpath.GetConfigValue('debug'): print('Saving GPX path: '+path)
4159        SaveConfigVars(vars)
4160    except KeyError:
4161        pass
4162
4163def SaveImportDirectory(path):
4164    if GSASIIpath.GetConfigValue('Import_directory') == path: return
4165    vars = GetConfigValsDocs()
4166    try:
4167        vars['Import_directory'][1] = path
4168        if GSASIIpath.GetConfigValue('debug'): print('Saving Import path: '+path)
4169        SaveConfigVars(vars)
4170    except KeyError:
4171        pass
4172
4173def GetConfigValsDocs():
4174    '''Reads the module referenced in fname (often <module>.__file__) and
4175    return a dict with names of global variables as keys.
4176    For each global variable, the value contains four items:
4177
4178    :returns: a dict where keys are names defined in module config_example.py
4179      where the value is a list of four items, as follows:
4180
4181         * item 0: the default value
4182         * item 1: the current value
4183         * item 2: the initial value (starts same as item 1)
4184         * item 3: the "docstring" that follows variable definition
4185
4186    '''
4187    import config_example
4188    import ast
4189    fname = os.path.splitext(config_example.__file__)[0]+'.py' # convert .pyc to .py
4190    with open(fname, 'r') as f:
4191        fstr = f.read()
4192    fstr = fstr.replace('\r\n', '\n').replace('\r', '\n')
4193    if not fstr.endswith('\n'):
4194        fstr += '\n'
4195    tree = ast.parse(fstr)
4196    d = {}
4197    key = None
4198    for node in ast.walk(tree):
4199        if isinstance(node,ast.Assign):
4200            key = node.targets[0].id
4201            d[key] = [config_example.__dict__.get(key),
4202                      GSASIIpath.configDict.get(key),
4203                      GSASIIpath.configDict.get(key),'']
4204        elif isinstance(node,ast.Expr) and key:
4205            d[key][3] = node.value.s.strip()
4206        else:
4207            key = None
4208    return d
4209
4210def SaveConfigVars(vars,parent=None):
4211    '''Write the current config variable values to config.py
4212
4213    :params dict vars: a dictionary of variable settings and meanings as
4214      created in :func:`GetConfigValsDocs`.
4215    :param parent: wx.Frame object or None (default) for parent
4216      of error message if no file can be written.
4217    :returns: True if unable to write the file, None otherwise
4218    '''
4219    # try to write to where an old config file is located
4220    try:
4221        import config
4222        savefile = config.__file__
4223    except ImportError: # no config.py file yet
4224        savefile = os.path.join(GSASIIpath.path2GSAS2,'config.py')
4225    # try to open file for write
4226    try:
4227        savefile = os.path.splitext(savefile)[0]+'.py' # convert .pyc to .py
4228        fp = open(savefile,'w')
4229    except IOError:  # can't write there, write in local mods directory
4230        # create a local mods directory, if needed
4231        g2local = os.path.expanduser('~/.G2local/')
4232        if not os.path.exists(g2local):
4233            try:
4234                print(u'Creating directory '+g2local)
4235                os.mkdir(g2local)
4236            except:
4237                if parent:
4238                    G2MessageBox(parent,u'Error trying to create directory '+g2local,
4239                        'Unable to save')
4240                else:
4241                    print(u'Error trying to create directory '+g2local)
4242                return True
4243            sys.path.insert(0,os.path.expanduser('~/.G2local/'))
4244        savefile = os.path.join(os.path.expanduser('~/.G2local/'),'config.py')
4245        try:
4246            fp = open(savefile,'w')
4247        except IOError:
4248            if parent:
4249                G2MessageBox(parent,'Error trying to write configuration to '+savefile,
4250                    'Unable to save')
4251            else:
4252                print('Error trying to write configuration to '+savefile)
4253            return True
4254    import datetime
4255    fp.write("'''\n")
4256    fp.write("*config.py: Configuration options*\n----------------------------------\n")
4257    fp.write("This file created in SelectConfigSetting on {:%d %b %Y %H:%M}\n".
4258             format(datetime.datetime.now()))
4259    fp.write("'''\n\n")
4260    fp.write("import os.path\n")
4261    fp.write("import GSASIIpath\n\n")
4262    for var in sorted(vars.keys(),key=lambda s: s.lower()):
4263        if vars[var][1] is None: continue
4264        if vars[var][1] == '': continue
4265        if vars[var][0] == vars[var][1]: continue
4266        try:
4267            float(vars[var][1]) # test for number
4268            fp.write(var + ' = ' + str(vars[var][1])+'\n')
4269        except:
4270            try:
4271                eval(vars[var][1]) # test for an expression
4272                fp.write(var + ' = ' + str(vars[var][1])+'\n')
4273            except: # must be a string
4274                varstr = vars[var][1]
4275                if '\\' in varstr:
4276                    fp.write(var + ' = os.path.normpath("' + varstr.replace('\\','/') +'")\n')
4277                else:
4278                    fp.write(var + ' = "' + str(varstr)+'"\n')
4279        if vars[var][3]:
4280            fp.write("'''" + str(vars[var][3]) + "\n'''\n\n")
4281    fp.close()
4282    print('wrote file '+savefile)
4283
4284class SelectConfigSetting(wx.Dialog):
4285    '''Dialog to select configuration variables and set associated values.
4286    '''
4287    def __init__(self,parent=None):
4288        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4289        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Set Config Variable', style=style)
4290        self.sizer = wx.BoxSizer(wx.VERTICAL)
4291        self.vars = GetConfigValsDocs()
4292       
4293        label = wx.StaticText(
4294            self,  wx.ID_ANY,
4295            'Select a GSAS-II configuration variable to change'
4296            )
4297        self.sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4298        self.choice = {}
4299        btn = G2ChoiceButton(self, sorted(self.vars.keys(),key=lambda s: s.lower()),
4300            strLoc=self.choice,strKey=0,onChoice=self.OnSelection)
4301        btn.SetLabel("")
4302        self.sizer.Add(btn)
4303
4304        self.varsizer = wx.BoxSizer(wx.VERTICAL)
4305        self.sizer.Add(self.varsizer,1,wx.ALL|wx.EXPAND,1)
4306       
4307        self.doclbl = wx.StaticBox(self, wx.ID_ANY, "")
4308        self.doclblsizr = wx.StaticBoxSizer(self.doclbl)
4309        self.docinfo = wx.StaticText(self,  wx.ID_ANY, "")
4310        self.doclblsizr.Add(self.docinfo, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4311        self.sizer.Add(self.doclblsizr, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
4312        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
4313        self.saveBtn = wx.Button(self,-1,"Save current settings")
4314        btnsizer.Add(self.saveBtn, 0, wx.ALL, 2) 
4315        self.saveBtn.Bind(wx.EVT_BUTTON, self.OnSave)
4316        self.saveBtn.Enable(False)
4317        self.applyBtn = wx.Button(self,-1,"Use current (no save)")
4318        btnsizer.Add(self.applyBtn, 0, wx.ALL, 2) 
4319        self.applyBtn.Bind(wx.EVT_BUTTON, self.OnApplyChanges)
4320        self.applyBtn.Enable(False)
4321       
4322        btn = wx.Button(self,wx.ID_CANCEL)
4323        btnsizer.Add(btn, 0, wx.ALL, 2) 
4324        self.sizer.Add(btnsizer, 0, wx.ALIGN_CENTRE|wx.ALL, 5) 
4325               
4326        self.SetSizer(self.sizer)
4327        self.sizer.Fit(self)
4328        self.CenterOnParent()
4329       
4330    def OnChange(self,event=None):
4331        ''' Check if anything been changed. Turn the save button on/off.
4332        '''
4333        for var in self.vars:
4334            if self.vars[var][0] is None and self.vars[var][1] is not None:
4335                # make blank strings into None, if that is the default
4336                if self.vars[var][1].strip() == '': self.vars[var][1] = None
4337            if self.vars[var][1] != self.vars[var][2]:
4338                #print 'changed',var,self.vars[var][:3]
4339                self.saveBtn.Enable(True)
4340                self.applyBtn.Enable(True)
4341                break
4342        else:
4343            self.saveBtn.Enable(False)
4344            self.applyBtn.Enable(False)
4345        try:
4346            self.resetBtn.Enable(True)
4347        except:
4348            pass
4349       
4350    def OnApplyChanges(self,event=None):
4351        'Set config variables to match the current settings'
4352        GSASIIpath.SetConfigValue(self.vars)
4353        self.EndModal(wx.ID_OK)
4354        import GSASIImpsubs as G2mp
4355        G2mp.ResetMP()
4356       
4357    def OnSave(self,event):
4358        '''Write the config variables to config.py and then set them
4359        as the current settings
4360        '''
4361        if not SaveConfigVars(self.vars,parent=self):
4362            self.OnApplyChanges() # force a reload of the config settings
4363        else:
4364            self.EndModal(wx.ID_OK)
4365
4366    def OnBoolSelect(self,event):
4367        'Respond to a change in a True/False variable'
4368        rb = event.GetEventObject()
4369        var = self.choice[0]
4370        self.vars[var][1] = (rb.GetSelection() == 0)
4371        self.OnChange()
4372        wx.CallAfter(self.OnSelection)
4373       
4374    def onSelDir(self,event):
4375        'Select a directory from a menu'
4376        dlg = wx.DirDialog(self, "Choose a directory:",style=wx.DD_DEFAULT_STYLE)
4377        if dlg.ShowModal() == wx.ID_OK:
4378            var = self.choice[0]
4379            self.vars[var][1] = dlg.GetPath()
4380            self.strEd.SetValue(self.vars[var][1])
4381            self.OnChange()
4382        dlg.Destroy()
4383       
4384    def OnSelection(self):
4385        'show a selected variable'
4386        def OnNewColorBar(event):
4387            self.vars['Contour_color'][1] = self.colSel.GetValue()
4388            self.OnChange(event)
4389
4390        if 'phoenix' in wx.version():
4391            self.varsizer.Clear(True)
4392        else:
4393            self.varsizer.DeleteWindows()
4394        var = self.choice[0]
4395        showdef = True
4396        if var not in self.vars:
4397            raise Exception("How did this happen?")
4398        if type(self.vars[var][0]) is int:
4399            ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=int,OKcontrol=self.OnChange)
4400            self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4401        elif type(self.vars[var][0]) is float:
4402            ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=float,OKcontrol=self.OnChange)
4403            self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4404        elif type(self.vars[var][0]) is bool:
4405            showdef = False
4406            lbl = "value for "+var
4407            ch = []
4408            for i,v in enumerate((True,False)):
4409                s = str(v)
4410                if v == self.vars[var][0]:
4411                    defopt = i
4412                    s += ' (default)'
4413                ch += [s]
4414            rb = wx.RadioBox(self, wx.ID_ANY, lbl, wx.DefaultPosition, wx.DefaultSize,
4415                ch, 1, wx.RA_SPECIFY_COLS)
4416            # set initial value
4417            if self.vars[var][1] is None:
4418                rb.SetSelection(defopt)
4419            elif self.vars[var][1]:
4420                rb.SetSelection(0)
4421            else:
4422                rb.SetSelection(1)
4423            rb.Bind(wx.EVT_RADIOBOX,self.OnBoolSelect)
4424            self.varsizer.Add(rb, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4425        else:
4426            if var.endswith('_directory') or var.endswith('_location'):
4427                btn = wx.Button(self,wx.ID_ANY,'Select from dialog...')
4428                sz = (400,-1)
4429            else:
4430                btn = None
4431                sz = (250,-1)
4432            if var == 'Contour_color':
4433                if self.vars[var][1] is None:
4434                    self.vars[var][1] = 'Paired'
4435                colorList = sorted([m for m in mpl.cm.datad.keys() ],key=lambda s: s.lower())   #if not m.endswith("_r")
4436                self.colSel = wx.ComboBox(self,value=self.vars[var][1],choices=colorList,
4437                    style=wx.CB_READONLY|wx.CB_DROPDOWN)
4438                self.colSel.Bind(wx.EVT_COMBOBOX, OnNewColorBar)
4439                self.varsizer.Add(self.colSel, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4440            else:
4441                self.strEd = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=str,
4442                    OKcontrol=self.OnChange,size=sz)
4443                if self.vars[var][1] is not None:
4444                    self.strEd.SetValue(self.vars[var][1])
4445                self.varsizer.Add(self.strEd, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4446            if btn:
4447                btn.Bind(wx.EVT_BUTTON,self.onSelDir)
4448                self.varsizer.Add(btn, 0, wx.ALIGN_CENTRE|wx.ALL, 5) 
4449        # button for reset to default value
4450        lbl = "Reset to Default"
4451        if showdef: # spell out default when needed
4452            lbl += ' (='+str(self.vars[var][0])+')'
4453            #label = wx.StaticText(self,  wx.ID_ANY, 'Default value = '+str(self.vars[var][0]))
4454            #self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4455        self.resetBtn = wx.Button(self,-1,lbl)
4456        self.resetBtn.Bind(wx.EVT_BUTTON, self.OnClear)
4457        if self.vars[var][1] is not None and self.vars[var][1] != '': # show current value, if one
4458            #label = wx.StaticText(self,  wx.ID_ANY, 'Current value = '+str(self.vars[var][1]))
4459            #self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4460            self.resetBtn.Enable(True)
4461        else:
4462            self.resetBtn.Enable(False)
4463        self.varsizer.Add(self.resetBtn, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4464        # show meaning, if defined
4465        self.doclbl.SetLabel("Description of "+str(var)) 
4466        if self.vars[var][3]:
4467            self.docinfo.SetLabel(self.vars[var][3])
4468        else:
4469            self.docinfo.SetLabel("(not documented)")
4470        self.sizer.Fit(self)
4471        self.CenterOnParent()
4472        wx.CallAfter(self.SendSizeEvent)
4473
4474    def OnClear(self, event):
4475        var = self.choice[0]
4476        self.vars[var][1] = self.vars[var][0]
4477        self.OnChange()
4478        wx.CallAfter(self.OnSelection)
4479       
4480################################################################################
4481class downdate(wx.Dialog):
4482    '''Dialog to allow a user to select a version of GSAS-II to install
4483    '''
4484    def __init__(self,parent=None):
4485        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4486        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Select Version', style=style)
4487        pnl = wx.Panel(self)
4488        sizer = wx.BoxSizer(wx.VERTICAL)
4489        insver = GSASIIpath.svnGetRev(local=True)
4490        curver = int(GSASIIpath.svnGetRev(local=False))
4491        label = wx.StaticText(
4492            pnl,  wx.ID_ANY,
4493            'Select a specific GSAS-II version to install'
4494            )
4495        sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4496        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4497        sizer1.Add(
4498            wx.StaticText(pnl,  wx.ID_ANY,
4499                          'Currently installed version: '+str(insver)),
4500            0, wx.ALIGN_CENTRE|wx.ALL, 5)
4501        sizer.Add(sizer1)
4502        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4503        sizer1.Add(
4504            wx.StaticText(pnl,  wx.ID_ANY,
4505                          'Select GSAS-II version to install: '),
4506            0, wx.ALIGN_CENTRE|wx.ALL, 5)
4507        self.spin = wx.SpinCtrl(pnl, wx.ID_ANY,size=(150,-1))
4508        self.spin.SetRange(1, curver)
4509        self.spin.SetValue(curver)
4510        self.Bind(wx.EVT_SPINCTRL, self._onSpin, self.spin)
4511        self.Bind(wx.EVT_KILL_FOCUS, self._onSpin, self.spin)
4512        sizer1.Add(self.spin)
4513        sizer.Add(sizer1)
4514
4515        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
4516        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
4517
4518        self.text = wx.StaticText(pnl,  wx.ID_ANY, "")
4519        sizer.Add(self.text, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
4520
4521        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
4522        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
4523        sizer.Add(
4524            wx.StaticText(
4525                pnl,  wx.ID_ANY,
4526                'If "Install" is pressed, your project will be saved;\n'
4527                'GSAS-II will exit; The specified version will be loaded\n'
4528                'and GSAS-II will restart. Press "Cancel" to abort.'),
4529            0, wx.EXPAND|wx.ALL, 10)
4530        btnsizer = wx.StdDialogButtonSizer()
4531        btn = wx.Button(pnl, wx.ID_OK, "Install")
4532        btn.SetDefault()
4533        btnsizer.AddButton(btn)
4534        btn = wx.Button(pnl, wx.ID_CANCEL)
4535        btnsizer.AddButton(btn)
4536        btnsizer.Realize()
4537        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
4538        pnl.SetSizer(sizer)
4539        sizer.Fit(self)
4540        self.topsizer=sizer
4541        self.CenterOnParent()
4542        self._onSpin(None)
4543
4544    def _onSpin(self,event):
4545        'Called to load info about the selected version in the dialog'
4546        if event: event.Skip()
4547        ver = self.spin.GetValue()
4548        d = GSASIIpath.svnGetLog(version=ver)
4549        date = d.get('date','?').split('T')[0]
4550        s = '(Version '+str(ver)+' created '+date
4551        s += ' by '+d.get('author','?')+')'
4552        msg = d.get('msg')
4553        if msg: s += '\n\nComment: '+msg
4554        self.text.SetLabel(s)
4555        self.topsizer.Fit(self)
4556
4557    def getVersion(self):
4558        'Get the version number in the dialog'
4559        return self.spin.GetValue()
4560
4561################################################################################
4562#### Display Help information
4563################################################################################
4564# define some globals
4565htmlPanel = None
4566htmlFrame = None
4567htmlFirstUse = True
4568#helpLocDict = {}  # to be implemented if we ever split gsasii.html over multiple files
4569path2GSAS2 = os.path.dirname(os.path.realpath(__file__)) # save location of this file
4570def ShowHelp(helpType,frame):
4571    '''Called to bring up a web page for documentation.'''
4572    global htmlFirstUse,htmlPanel,htmlFrame
4573    # no defined link to use, create a default based on key
4574    helplink = 'gsasII.html'
4575    if helpType:
4576        helplink += '#'+helpType.replace(')','').replace('(','_').replace(' ','_')
4577    # determine if a web browser or the internal viewer should be used for help info
4578    if GSASIIpath.GetConfigValue('Help_mode'):
4579        helpMode = GSASIIpath.GetConfigValue('Help_mode')
4580    else:
4581        helpMode = 'browser'
4582    if helpMode == 'internal':
4583        helplink = os.path.join(path2GSAS2,'help',helplink)
4584        try:
4585            htmlPanel.LoadFile(helplink)
4586            htmlFrame.Raise()
4587        except:
4588            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
4589            htmlFrame.Show(True)
4590            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
4591            htmlPanel = MyHtmlPanel(htmlFrame,-1)
4592            htmlPanel.LoadFile(helplink)
4593    else:
4594        if sys.platform == "darwin": # for Mac, force use of safari to preserve anchors on file URLs
4595            wb = webbrowser.MacOSXOSAScript('safari')
4596        else:
4597            wb = webbrowser
4598        helplink = os.path.join(path2GSAS2,'help',helplink)
4599        pfx = "file://"
4600        if sys.platform.lower().startswith('win'):
4601            pfx = ''
4602        #if GSASIIpath.GetConfigValue('debug'): print 'Help link=',pfx+helplink
4603        if htmlFirstUse:
4604            wb.open_new(pfx+helplink)
4605            htmlFirstUse = False
4606        else:
4607            wb.open(pfx+helplink, new=0, autoraise=True)
4608
4609def ShowWebPage(URL,frame):
4610    '''Called to show a tutorial web page.
4611    '''
4612    global htmlFirstUse,htmlPanel,htmlFrame
4613    # determine if a web browser or the internal viewer should be used for help info
4614    if GSASIIpath.GetConfigValue('Help_mode'):
4615        helpMode = GSASIIpath.GetConfigValue('Help_mode')
4616    else:
4617        helpMode = 'browser'
4618    if helpMode == 'internal':
4619        try:
4620            htmlPanel.LoadFile(URL)
4621            htmlFrame.Raise()
4622        except:
4623            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
4624            htmlFrame.Show(True)
4625            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
4626            htmlPanel = MyHtmlPanel(htmlFrame,-1)
4627            htmlPanel.LoadFile(URL)
4628    else:
4629        if URL.startswith('http'): 
4630            pfx = ''
4631        elif sys.platform.lower().startswith('win'):
4632            pfx = ''
4633        else:
4634            pfx = "file://"
4635        if htmlFirstUse:
4636            webbrowser.open_new(pfx+URL)
4637            htmlFirstUse = False
4638        else:
4639            webbrowser.open(pfx+URL, new=0, autoraise=True)
4640
4641################################################################################
4642#### Tutorials support
4643################################################################################
4644G2BaseURL = "https://subversion.xray.aps.anl.gov/pyGSAS"
4645tutorialIndex = (
4646    # tutorial dir,      web page file name,      title for page
4647    ['Getting started'],
4648    ['StartingGSASII', 'Starting GSAS.htm', 'Starting GSAS-II'],
4649
4650    ['Rietveld fitting'],
4651       
4652    ['LabData', 'Laboratory X.htm', 'Fitting laboratory X-ray powder data for fluoroapatite'],
4653    ['CWNeutron', 'Neutron CW Powder Data.htm', 'CW Neutron Powder fit for Yttrium-Iron Garnet'],
4654    ['CWCombined', 'Combined refinement.htm', 'Combined X-ray/CW-neutron refinement of PbSO4'],
4655    ['TOF-CW Joint Refinement', 'TOF combined XN Rietveld refinement in GSAS.htm', 'Combined X-ray/TOF-neutron Rietveld refinement'],
4656    ['BkgFit', 'FitBkgTut.htm',  'Fitting the Starting Background using Fixed Points'],
4657   
4658    ['Parametric Rietveld fitting'],
4659    ['SeqRefine', 'SequentialTutorial.htm', 'Sequential refinement of multiple datasets'],
4660    ['SeqParametric', 'ParametricFitting.htm', '     Parametric Fitting and Pseudo Variables for Sequential Fits'],
4661
4662    ['Structure solution'],
4663    ['FitPeaks', 'Fit Peaks.htm', 'Fitting individual peaks & autoindexing'],
4664    ['CFjadarite', 'Charge Flipping in GSAS.htm', '     Charge Flipping structure solution for jadarite'],
4665    ['CFsucrose', 'Charge Flipping - sucrose.htm','     Charge Flipping structure solution for sucrose'],
4666    ['CFXraySingleCrystal', 'CFSingleCrystal.htm', 'Charge Flipping structure solution with Xray single crystal data'],       
4667    ['TOF Charge Flipping', 'Charge Flipping with TOF single crystal data in GSASII.htm', 'Charge flipping with neutron TOF single crystal data'],
4668    ['MCsimanneal', 'MCSA in GSAS.htm', 'Monte-Carlo simulated annealing structure determination'],
4669
4670    ['Stacking Fault Modeling'],
4671    ['StackingFaults-I', 'Stacking Faults-I.htm', 'Stacking fault simulations for diamond'],
4672    ['StackingFaults-II', 'Stacking Faults II.htm', 'Stacking fault simulations for Keokuk kaolinite'],
4673    ['StackingFaults-III', 'Stacking Faults-III.htm', 'Stacking fault simulations for Georgia kaolinite'],
4674
4675    ['Image Calibration/Integration'],
4676    ['2DCalibration', 'Calibration of an area detector in GSAS.htm', 'Calibration of an area detector'],
4677    ['2DIntegration', 'Integration of area detector data in GSAS.htm', '     Integration of area detector data'],
4678    ['TOF Calibration', 'Calibration of a TOF powder diffractometer.htm', 'Calibration of a Neutron TOF diffractometer'],
4679
4680    ['Small-Angle Scattering'],       
4681    ['2DStrain', 'Strain fitting of 2D data in GSAS-II.htm', 'Strain fitting of 2D data'],
4682    ['2DTexture', 'Texture analysis of 2D data in GSAS-II.htm', 'Texture analysis of 2D data'],
4683             
4684    ['SAsize', 'Small Angle Size Distribution.htm', 'Small angle x-ray data size distribution (alumina powder)'],
4685    ['SAfit', 'Fitting Small Angle Scattering Data.htm', '     Fitting small angle x-ray data (alumina powder)'],
4686    ['SAimages', 'Small Angle Image Processing.htm', 'Image Processing of small angle x-ray data'],
4687    ['SAseqref', 'Sequential Refinement of Small Angle Scattering Data.htm', 'Sequential refinement with small angle scattering data'],
4688
4689    ['Other'],   
4690    ['MerohedralTwins', 'Merohedral twin refinement in GSAS.htm', 'Merohedral twin refinements'],
4691    ['TOF Single Crystal Refinement', 'TOF single crystal refinement in GSAS.htm', 'Single crystal refinement from TOF data'],
4692    ['PythonScript','Scripting.htm','Scripting a GSAS-II Refinement from Python'],
4693    ['PythonScript','CommandLine.htm','     Running a GSAS-II Refinement from the Command Line'],
4694   
4695    #['ExampleDir', 'ExamplePage.html', 'Example Tutorial Title'],
4696    )
4697'''A catalog of GSAS-II tutorials with headings. This is the master list of GSAS-II tutorials and must be updated when tutorials are
4698added. Each item has either one or three items.
4699Titles are single item in a list or tuple. Tutorials have three items: (a) the name of the directory,
4700(b) the name of the web page and (c) a title for the tutorial.
4701Tutorials that depend on a previous tutorial being completed should have the title for
4702the tutorial indented by five spaces.
4703
4704Note that :data:`tutorialCatalog` is generated from this tuple.
4705Also see :mod:`makeTutorial` which is used to read this and create a web page.
4706'''
4707
4708#A catalog of GSAS-II tutorials generated from the table in :data:`tutorialIndex`
4709tutorialCatalog = [l for l in tutorialIndex if len(l) == 3]
4710
4711class OpenTutorial(wx.Dialog):
4712    '''Open a tutorial web page, optionally copying the web page, screen images and
4713    data file(s) to the local disk.
4714    '''
4715   
4716    def __init__(self,parent):
4717        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4718        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Open Tutorial', style=style)
4719        self.G2frame = self.frame = parent
4720        pnl = wx.Panel(self)
4721        sizer = wx.BoxSizer(wx.VERTICAL)
4722        sizer1 = wx.BoxSizer(wx.HORIZONTAL)       
4723        label = wx.StaticText(
4724            pnl,  wx.ID_ANY,
4725            'Select the tutorial to be run and the mode of access'
4726            )
4727        msg = '''GSAS-II tutorials and their sample data files
4728        require a fair amount of storage space; few users will
4729        use all of them. This dialog allows users to load selected
4730        tutorials (along with their sample data) to their computer;
4731        optionally all tutorials can be downloaded.
4732
4733        Downloaded tutorials can be viewed and run without internet
4734        access. Tutorials can also be viewed without download, but
4735        users will need to download the sample data files manually.
4736
4737        The location used to download tutorials is set using the
4738        "Set download location" which is saved as the "Tutorial_location"
4739        configuration option see File/Preference or the
4740        config_example.py file. System managers can select to have
4741        tutorial files installed at a shared location.
4742        '''
4743        self.SetTutorialPath()
4744        hlp = HelpButton(pnl,msg)
4745        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
4746        sizer1.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 0)
4747        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
4748        sizer1.Add(hlp,0,wx.ALIGN_RIGHT|wx.ALL)
4749        sizer.Add(sizer1,0,wx.EXPAND|wx.ALL,0)
4750        sizer.Add((10,10))
4751        sizer0 = wx.BoxSizer(wx.HORIZONTAL)       
4752        sizer1a = wx.BoxSizer(wx.VERTICAL)
4753        sizer1b = wx.BoxSizer(wx.VERTICAL)
4754        btn = wx.Button(pnl, wx.ID_ANY, "Download a tutorial and view")
4755        btn.Bind(wx.EVT_BUTTON, self.SelectAndDownload)
4756        sizer1a.Add(btn,0,WACV)
4757        btn = wx.Button(pnl, wx.ID_ANY, "Select from downloaded tutorials")
4758        btn.Bind(wx.EVT_BUTTON, self.onSelectDownloaded)
4759        sizer1a.Add(btn,0,WACV)
4760        btn = wx.Button(pnl, wx.ID_ANY, "Browse tutorial on web")
4761        btn.Bind(wx.EVT_BUTTON, self.onWebBrowse)
4762        sizer1a.Add(btn,0,WACV)
4763        btn = wx.Button(pnl, wx.ID_ANY, "Update downloaded tutorials")
4764        btn.Bind(wx.EVT_BUTTON, self.UpdateDownloaded)
4765        sizer1b.Add(btn,0,WACV)
4766        btn = wx.Button(pnl, wx.ID_ANY, "Download all tutorials")
4767        btn.Bind(wx.EVT_BUTTON, self.DownloadAll)
4768        sizer1b.Add(btn,0,WACV)
4769        sizer0.Add(sizer1a,0,wx.EXPAND|wx.ALL,0)
4770        sizer0.Add(sizer1b,0,wx.EXPAND|wx.ALL,0)
4771        sizer.Add(sizer0,5,wx.EXPAND|wx.ALL,5)
4772       
4773        sizer.Add((10,10))
4774        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4775        btn = wx.Button(pnl, wx.ID_ANY, "Set download location")
4776        btn.Bind(wx.EVT_BUTTON, self.SelectDownloadLoc)
4777        sizer1.Add(btn,0,WACV)
4778        self.dataLoc = wx.StaticText(pnl, wx.ID_ANY,self.tutorialPath)
4779        sizer1.Add(self.dataLoc,0,WACV)
4780        sizer.Add(sizer1)
4781       
4782        btnsizer = wx.StdDialogButtonSizer()
4783        btn = wx.Button(pnl, wx.ID_CANCEL,"Done")
4784        btnsizer.AddButton(btn)
4785        btnsizer.Realize()
4786        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
4787        pnl.SetSizer(sizer)
4788        sizer.Fit(self)
4789        self.topsizer=sizer
4790        self.CenterOnParent()
4791
4792    def SetTutorialPath(self):
4793        '''Get the tutorial location if set; if not pick a default
4794        directory in a logical place
4795        '''
4796        if GSASIIpath.GetConfigValue('Tutorial_location'):
4797            self.tutorialPath = os.path.abspath(GSASIIpath.GetConfigValue('Tutorial_location'))
4798        elif (sys.platform.lower().startswith('win') and
4799              os.path.exists(os.path.abspath(os.path.expanduser('~/My Documents')))):
4800            self.tutorialPath = os.path.abspath(os.path.expanduser('~/My Documents/G2tutorials'))
4801        elif (sys.platform.lower().startswith('win') and
4802              os.path.exists(os.path.abspath(os.path.expanduser('~/Documents')))):
4803            self.tutorialPath = os.path.abspath(os.path.expanduser('~/Documents/G2tutorials'))
4804        else:
4805            self.tutorialPath = os.path.abspath(os.path.expanduser('~/G2tutorials'))
4806
4807    def SelectAndDownload(self,event):
4808        '''Make a list of all tutorials on web and allow user to choose one to
4809        download and then view
4810        '''
4811        indices = [j for j,i in enumerate(tutorialCatalog)
4812            if not os.path.exists(os.path.join(self.tutorialPath,i[0],i[1]))]
4813        if not indices:
4814            G2MessageBox(self,'All tutorials are downloaded','None to download')
4815            return
4816        choices = [tutorialCatalog[i][2] for i in indices]
4817        selected = self.ChooseTutorial(choices)
4818        if selected is None: return
4819        j = indices[selected]
4820        fullpath = os.path.join(self.tutorialPath,tutorialCatalog[j][0],tutorialCatalog[j][1])
4821        fulldir = os.path.join(self.tutorialPath,tutorialCatalog[j][0])
4822        URL = G2BaseURL+'/Tutorials/'+tutorialCatalog[j][0]+'/'
4823        if GSASIIpath.svnInstallDir(URL,fulldir):
4824            ShowWebPage(fullpath,self.frame)
4825        else:
4826            G2MessageBox(self,'Error downloading tutorial','Download error')
4827        self.EndModal(wx.ID_OK)
4828        self.G2frame.TutorialImportDir = os.path.join(self.tutorialPath,tutorialCatalog[j][0],'data')
4829
4830    def onSelectDownloaded(self,event):
4831        '''Select a previously downloaded tutorial
4832        '''
4833        indices = [j for j,i in enumerate(tutorialCatalog)
4834            if os.path.exists(os.path.join(self.tutorialPath,i[0],i[1]))]
4835        if not indices:
4836            G2MessageBox(self,
4837                         'There are no downloaded tutorials in '+self.tutorialPath,
4838                         'None downloaded')
4839            return
4840        choices = [tutorialCatalog[i][2] for i in indices]
4841        selected = self.ChooseTutorial(choices)
4842        if selected is None: return
4843        j = indices[selected]
4844        fullpath = os.path.join(self.tutorialPath,tutorialCatalog[j][0],tutorialCatalog[j][1])
4845        self.EndModal(wx.ID_OK)
4846        ShowWebPage(fullpath,self.frame)
4847        self.G2frame.TutorialImportDir = os.path.join(self.tutorialPath,tutorialCatalog[j][0],'data')
4848       
4849    def onWebBrowse(self,event):
4850        '''Make a list of all tutorials on web and allow user to view one.
4851        '''
4852        choices = [i[2] for i in tutorialCatalog]
4853        selected = self.ChooseTutorial(choices)
4854        if selected is None: return       
4855        tutdir = tutorialCatalog[selected][0]
4856        tutfil = tutorialCatalog[selected][1]
4857        # open web page remotely, don't worry about data
4858        URL = G2BaseURL+'/Tutorials/'+tutdir+'/'+tutfil
4859        self.EndModal(wx.ID_OK)
4860        ShowWebPage(URL,self.frame)
4861       
4862    def ChooseTutorial(self,choices):
4863        'choose a tutorial from a list'
4864        def onDoubleClick(event):
4865            'double-click closes the dialog'
4866            dlg.EndModal(wx.ID_OK)
4867        dlg = wx.Dialog(self,wx.ID_ANY,
4868                        'Select a tutorial to view. NB: indented ones require prerequisite',
4869                        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
4870        pnl = wx.Panel(dlg)
4871        sizer = wx.BoxSizer(wx.VERTICAL)
4872        listbox = wx.ListBox(pnl, wx.ID_ANY, choices=choices,
4873                             size=(450, 100),
4874                             style=wx.LB_SINGLE)
4875        sizer.Add(listbox,1,WACV|wx.EXPAND|wx.ALL,1)
4876        listbox.Bind(wx.EVT_LISTBOX_DCLICK, onDoubleClick)
4877        sizer.Add((10,10))
4878        btnsizer = wx.StdDialogButtonSizer()
4879        btn = wx.Button(pnl, wx.ID_CANCEL)
4880        btnsizer.AddButton(btn)
4881        OKbtn = wx.Button(pnl, wx.ID_OK)
4882        OKbtn.SetDefault()
4883        btnsizer.AddButton(OKbtn)
4884        btnsizer.Realize()
4885        sizer.Add((-1,5))
4886        sizer.Add(btnsizer,0,wx.ALIGN_RIGHT,50)
4887       
4888        pnl.SetSizer(sizer)
4889        sizer.Fit(dlg)
4890        self.CenterOnParent()
4891        if dlg.ShowModal() != wx.ID_OK:
4892            dlg.Destroy()
4893            return
4894        selected = listbox.GetSelection()
4895        dlg.Destroy()
4896        wx.Yield() # close window right away so user sees something happen
4897        if selected < 0: return
4898        return selected
4899
4900    def UpdateDownloaded(self,event):
4901        'Find the downloaded tutorials and run an svn update on them'
4902        updated = 0
4903        for i in tutorialCatalog:
4904            if not os.path.exists(os.path.join(self.tutorialPath,i[0],i[1])): continue
4905            print('Updating '+i[0])
4906            GSASIIpath.svnUpdateDir(os.path.join(self.tutorialPath,i[0]))
4907            updated += 0
4908        if not updated:
4909            G2MessageBox(self,'Warning, you have no downloaded tutorials','None Downloaded')
4910        self.EndModal(wx.ID_OK)
4911       
4912    def DownloadAll(self,event):
4913        'Download or update all tutorials'
4914        fail = ''
4915        for i in tutorialCatalog:
4916            if os.path.exists(os.path.join(self.tutorialPath,i[0],i[1])):
4917                print('Updating '+i[0])
4918                GSASIIpath.svnUpdateDir(os.path.join(self.tutorialPath,i[0]))
4919            else:
4920                fulldir = os.path.join(self.tutorialPath,i[0])
4921                URL = G2BaseURL+'/Tutorials/'+i[0]+'/'
4922                if not GSASIIpath.svnInstallDir(URL,fulldir):
4923                    if fail: fail += ', '
4924                    fail += i[0]
4925        if fail: 
4926            G2MessageBox(self,'Error downloading tutorial(s)\n\t'+fail,'Download error')
4927        self.EndModal(wx.ID_OK)
4928                   
4929    def SelectDownloadLoc(self,event):
4930        '''Select a download location,
4931        Cancel resets to the default
4932        '''
4933        dlg = wx.DirDialog(self, "Choose a directory for tutorial downloads:",
4934                           defaultPath=self.tutorialPath)#,style=wx.DD_DEFAULT_STYLE)
4935                           #)
4936        try:
4937            if dlg.ShowModal() != wx.ID_OK:
4938                return
4939            pth = dlg.GetPath()
4940        finally:
4941            dlg.Destroy()
4942
4943        if not os.path.exists(pth):
4944            try:
4945                os.makedirs(pth)    #failing for no obvious reason
4946            except OSError:
4947                msg = 'The selected directory is not valid.\n\t'
4948                msg += pth
4949                msg += '\n\nAn attempt to create the directory failed'
4950                G2MessageBox(self.frame,msg)
4951                return
4952        if os.path.exists(os.path.join(pth,"help")) and os.path.exists(os.path.join(pth,"Exercises")):
4953            print("Note that you may have old tutorial files in the following directories")
4954            print('\t'+os.path.join(pth,"help"))
4955            print('\t'+os.path.join(pth,"Exercises"))
4956            print('Subdirectories in the above can be deleted to save space\n\n')
4957        self.tutorialPath = pth
4958        self.dataLoc.SetLabel(self.tutorialPath)
4959        if GSASIIpath.GetConfigValue('Tutorial_location') == pth: return
4960        vars = GetConfigValsDocs()
4961        try:
4962            vars['Tutorial_location'][1] = pth
4963            if GSASIIpath.GetConfigValue('debug'): print('Saving Tutorial_location: '+pth)
4964            GSASIIpath.SetConfigValue(vars)
4965            SaveConfigVars(vars)
4966        except KeyError:
4967            pass
4968           
4969if __name__ == '__main__':
4970    app = wx.PySimpleApp()
4971    GSASIIpath.InvokeDebugOpts()
4972    frm = wx.Frame(None) # create a frame
4973    frm.Show(True)
4974   
4975    #======================================================================
4976    # test Grid with GridFractionEditor
4977    #======================================================================
4978    # tbl = [[1.,2.,3.],[1.1,2.1,3.1]]
4979    # colTypes = 3*[wg.GRID_VALUE_FLOAT+':10,5',]
4980    # Gtbl = Table(tbl,types=colTypes,rowLabels=['a','b'],colLabels=['1','2','3'])
4981    # Grid = GSGrid(frm)
4982    # Grid.SetTable(Gtbl,True)
4983    # for i in (0,1,2):
4984    #     attr = wx.grid.GridCellAttr()
4985    #     attr.IncRef()
4986    #     attr.SetEditor(GridFractionEditor(Grid))
4987    #     Grid.SetColAttr(i, attr)
4988    # frm.SetSize((400,200))
4989    # app.MainLoop()
4990    # sys.exit()
4991    #======================================================================
4992    # test Tutorial access
4993    #======================================================================
4994    # dlg = OpenTutorial(frm)
4995    # if dlg.ShowModal() == wx.ID_OK:
4996    #     print "OK"
4997    # else:
4998    #     print "Cancel"
4999    # dlg.Destroy()
5000    # sys.exit()
5001    #======================================================================
5002    # test ScrolledMultiEditor
5003    #======================================================================
5004    # Data1 = {
5005    #      'Order':1,
5006    #      'omega':'string',
5007    #      'chi':2.0,
5008    #      'phi':'',
5009    #      }
5010    # elemlst = sorted(Data1.keys())
5011    # prelbl = sorted(Data1.keys())
5012    # dictlst = len(elemlst)*[Data1,]
5013    #Data2 = [True,False,False,True]
5014    #Checkdictlst = len(Data2)*[Data2,]
5015    #Checkelemlst = range(len(Checkdictlst))
5016    # print 'before',Data1,'\n',Data2
5017    # dlg = ScrolledMultiEditor(
5018    #     frm,dictlst,elemlst,prelbl,
5019    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
5020    #     checklabel="Refine?",
5021    #     header="test")
5022    # if dlg.ShowModal() == wx.ID_OK:
5023    #     print "OK"
5024    # else:
5025    #     print "Cancel"
5026    # print 'after',Data1,'\n',Data2
5027    # dlg.Destroy()
5028    Data3 = {
5029         'Order':1.0,
5030         'omega':1.1,
5031         'chi':2.0,
5032         'phi':2.3,
5033         'Order1':1.0,
5034         'omega1':1.1,
5035         'chi1':2.0,
5036         'phi1':2.3,
5037         'Order2':1.0,
5038         'omega2':1.1,
5039         'chi2':2.0,
5040         'phi2':2.3,
5041         }
5042    elemlst = sorted(Data3.keys())
5043    dictlst = len(elemlst)*[Data3,]
5044    prelbl = elemlst[:]
5045    prelbl[0]="this is a much longer label to stretch things out"
5046    Data2 = len(elemlst)*[False,]
5047    Data2[1] = Data2[3] = True
5048    Checkdictlst = len(elemlst)*[Data2,]
5049    Checkelemlst = range(len(Checkdictlst))
5050    #print 'before',Data3,'\n',Data2
5051    #print dictlst,"\n",elemlst
5052    #print Checkdictlst,"\n",Checkelemlst
5053    # dlg = ScrolledMultiEditor(
5054    #     frm,dictlst,elemlst,prelbl,
5055    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
5056    #     checklabel="Refine?",
5057    #     header="test",CopyButton=True)
5058    # if dlg.ShowModal() == wx.ID_OK:
5059    #     print "OK"
5060    # else:
5061    #     print "Cancel"
5062    #print 'after',Data3,'\n',Data2
5063
5064    # Data2 = list(range(100))
5065    # elemlst += range(2,6)
5066    # postlbl += range(2,6)
5067    # dictlst += len(range(2,6))*[Data2,]
5068
5069    # prelbl = range(len(elemlst))
5070    # postlbl[1] = "a very long label for the 2nd item to force a horiz. scrollbar"
5071    # header="""This is a longer\nmultiline and perhaps silly header"""
5072    # dlg = ScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
5073    #                           header=header,CopyButton=True)
5074    # print Data1
5075    # if dlg.ShowModal() == wx.ID_OK:
5076    #     for d,k in zip(dictlst,elemlst):
5077    #         print k,d[k]
5078    # dlg.Destroy()
5079    # if CallScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
5080    #                            header=header):
5081    #     for d,k in zip(dictlst,elemlst):
5082    #         print k,d[k]
5083
5084    #======================================================================
5085    # test G2MultiChoiceDialog
5086    #======================================================================
5087    choices = []
5088    for i in range(21):
5089        choices.append("option_"+str(i))
5090    od = {
5091        'label_1':'This is a bool','value_1':True,
5092        'label_2':'This is a int','value_2':-1,
5093        'label_3':'This is a float','value_3':1.0,
5094        'label_4':'This is a string','value_4':'test',}
5095    dlg = G2MultiChoiceDialog(frm, 'Sequential refinement',
5096                              'Select dataset to include',
5097                              choices,extraOpts=od)
5098    sel = range(2,11,2)
5099    dlg.SetSelections(sel)
5100    dlg.SetSelections((1,5))
5101    if dlg.ShowModal() == wx.ID_OK:
5102        for sel in dlg.GetSelections():
5103            print (sel,choices[sel])
5104    print (od)
5105    od = {}
5106    dlg = G2MultiChoiceDialog(frm, 'Sequential refinement',
5107                              'Select dataset to include',
5108                              choices,extraOpts=od)
5109    sel = range(2,11,2)
5110    dlg.SetSelections(sel)
5111    dlg.SetSelections((1,5))
5112    if dlg.ShowModal() == wx.ID_OK: pass
5113    #======================================================================
5114    # test wx.MultiChoiceDialog
5115    #======================================================================
5116    # dlg = wx.MultiChoiceDialog(frm, 'Sequential refinement',
5117    #                           'Select dataset to include',
5118    #                           choices)
5119    # sel = range(2,11,2)
5120    # dlg.SetSelections(sel)
5121    # dlg.SetSelections((1,5))
5122    # if dlg.ShowModal() == wx.ID_OK:
5123    #     for sel in dlg.GetSelections():
5124    #         print sel,choices[sel]
5125
5126    # pnl = wx.Panel(frm)
5127    # siz = wx.BoxSizer(wx.VERTICAL)
5128
5129    # td = {'Goni':200.,'a':1.,'calc':1./3.,'string':'s'}
5130    # for key in sorted(td):
5131    #     txt = ValidatedTxtCtrl(pnl,td,key)
5132    #     siz.Add(txt)
5133    # pnl.SetSizer(siz)
5134    # siz.Fit(frm)
5135    # app.MainLoop()
5136    # print td
Note: See TracBrowser for help on using the repository browser.