source: trunk/GSASIIctrlGUI.py @ 3565

Last change on this file since 3565 was 3565, checked in by vondreele, 4 years ago

G2ctrls: change MultiFloatDialog? to accept bool arguments - maybe ints & strings could be added?
now used only for setup of Bilbao runs
G2phaseGUI: Add selection menu item the Phase/General? to select from Bilbao derived magnetic phase list
remove Bilbao call from TransformDialogG2pwdr: make getHKLpeak return np.array instead of list - that's how HKLs are used everywhere
G2pwdrGUI: show Bilbao results as table in Unit Cells List - selection shows peak indexing; Keep allows future use in making new magnetic phase.
Add menu item in Unit Cells List to call Bilbao k-SUBGROUPSMAG routineG2spc: Revise TextGen? & make new routine GetOprNames?

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