source: trunk/GSASIIctrls.py @ 2621

Last change on this file since 2621 was 2621, checked in by toby, 6 years ago

cruft for Windows to avoid capturing windows resize events

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