This repository was archived by the owner on Sep 16, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathGSASIIctrlGUI.py
10061 lines (9278 loc) · 424 KB
/
GSASIIctrlGUI.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
#GSASIIctrlGUI - Custom GSAS-II GUI controls
########### SVN repository information ###################
# $Date: 2024-03-17 12:50:24 -0500 (Sun, 17 Mar 2024) $
# $Author: toby $
# $Revision: 5767 $
# $URL: https://subversion.xray.aps.anl.gov/pyGSAS/trunk/GSASIIctrlGUI.py $
# $Id: GSASIIctrlGUI.py 5767 2024-03-17 17:50:24Z toby $
########### SVN repository information ###################
'''Documentation for all the routines in module :mod:`GSASIIctrlGUI`
follows.
'''
# Documentation moved to doc/source/GSASIIGUIr.rst
#
from __future__ import division, print_function
import os
import sys
import platform
try:
import wx
import wx.grid as wg
# import wx.wizard as wz
import wx.aui
import wx.lib.scrolledpanel as wxscroll
import wx.html # could postpone this for quicker startup
import wx.lib.mixins.listctrl as listmix
import wx.richtext as wxrt
import wx.lib.filebrowsebutton as wxfilebrowse
import matplotlib as mpl
import matplotlib.figure as mplfig
except ImportError:
print('ImportError for wx/mpl in GSASIIctrlGUI: ignore if docs build')
import time
import glob
import copy
import ast
import random as ran
import webbrowser # could postpone this for quicker startup
import numpy as np
import matplotlib as mpl
try:
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as Canvas
except ImportError:
from matplotlib.backends.backend_wx import FigureCanvas as Canvas
import GSASIIpath
GSASIIpath.SetVersionNumber("$Revision: 5767 $")
import GSASIIdataGUI as G2gd
import GSASIIpwdGUI as G2pdG
import GSASIIspc as G2spc
import GSASIIlog as log
import GSASIIobj as G2obj
import GSASIIfiles as G2fil
import GSASIIElem as G2elem
import GSASIIscriptable as G2sc
import GSASIIpwd as G2pwd
import GSASIIlattice as G2lat
import GSASIImath as G2mth
import GSASIIstrMain as G2stMn
import GSASIIIO as G2IO
import config_example
if sys.version_info[0] >= 3:
unicode = str
basestring = str
# Define a short names for convenience
DULL_YELLOW = (230,230,190)
# Don't depend on wx, for scriptable
try:
WACV = wx.ALIGN_CENTER_VERTICAL
wxaui_NB_TOPSCROLL = wx.aui.AUI_NB_TOP | wx.aui.AUI_NB_SCROLL_BUTTONS
except: # Don't depend on GUI
wxaui_NB_TOPSCROLL = None
try:
wx.NewIdRef
wx.NewId = wx.NewIdRef
except AttributeError:
pass
try: #phoenix
wxValidator = wx.Validator
except AttributeError: #classic - i.e. old
wxValidator = wx.pyValidator
#### Fixed definitions for wx Ids ################################################################################
def Define_wxId(*args):
'''routine to create unique global wx Id symbols in this module.
'''
for arg in args:
if GSASIIpath.GetConfigValue('debug') and not arg.startswith('wxID_'):
print ('DBG_Problem in name'+arg)
if arg in globals():
if GSASIIpath.GetConfigValue('debug'): print ('DBG_'+arg+'already defined')
continue
exec('global '+arg+';'+arg+' = wx.NewId()')
#### Tree Control ################################################################################
class G2TreeCtrl(wx.TreeCtrl):
'''Create a wrapper around the standard TreeCtrl so we can "wrap"
various events.
This logs when a tree item is selected (in :meth:`onSelectionChanged`)
This also wraps lists and dicts pulled out of the tree to track where
they were retrieved from.
'''
#def SelectItem(self,event):
# print 'Select Item'
# import GSASIIobj as G2obj
# G2obj.HowDidIgetHere()
# wx.TreeCtrl.SelectItem(self,event)
def __init__(self,parent=None,*args,**kwargs):
super(self.__class__,self).__init__(parent=parent,*args,**kwargs)
self.G2frame = parent.GetTopLevelParent()
self.root = self.AddRoot('Loaded Data: ')
self.SelectionChanged = None
self.textlist = None
log.LogInfo['Tree'] = self
def _getTreeItemsList(self,item):
'''Get the full tree hierarchy from a reference to a tree item.
Note that this effectively hard-codes phase and histogram names in the
returned list. We may want to make these names relative in the future.
'''
textlist = [self.GetItemText(item)]
parent = self.GetItemParent(item)
while parent:
if parent == self.root: break
textlist.insert(0,self.GetItemText(parent))
parent = self.GetItemParent(parent)
return textlist
def GetItemPyData(self,treeId):
if 'phoenix' in wx.version():
return wx.TreeCtrl.GetItemData(self,treeId)
else:
return wx.TreeCtrl.GetItemPyData(self,treeId)
def SetItemPyData(self,treeId,data):
if 'phoenix' in wx.version():
return wx.TreeCtrl.SetItemData(self,treeId,data)
else:
return wx.TreeCtrl.SetItemPyData(self,treeId,data)
def UpdateSelection(self):
TId = self.GetFocusedItem()
self.SelectItem(self.root)
self.SelectItem(TId)
# def onSelectionChanged(self,event):
# '''Log each press on a tree item here.
# '''
# if not self.G2frame.treePanel:
# return
# if self.SelectionChanged:
# textlist = self._getTreeItemsList(event.GetItem())
# if log.LogInfo['Logging'] and event.GetItem() != self.root:
# textlist[0] = self.GetRelativeHistNum(textlist[0])
# if textlist[0] == "Phases" and len(textlist) > 1:
# textlist[1] = self.GetRelativePhaseNum(textlist[1])
# log.MakeTreeLog(textlist)
# if textlist == self.textlist:
# return #same as last time - don't get it again
# self.textlist = textlist
# self.SelectionChanged(event)
# def Bind(self,eventtype,handler,*args,**kwargs):
# '''Override the Bind() function so that page change events can be trapped
# '''
# if eventtype == wx.EVT_TREE_SEL_CHANGED:
# self.SelectionChanged = handler
# wx.TreeCtrl.Bind(self,eventtype,self.onSelectionChanged)
# return
# wx.TreeCtrl.Bind(self,eventtype,handler,*args,**kwargs)
# commented out, disables Logging
# def GetItemPyData(self,*args,**kwargs):
# '''Override the standard method to wrap the contents
# so that the source can be logged when changed
# '''
# data = super(self.__class__,self).GetItemPyData(*args,**kwargs)
# textlist = self._getTreeItemsList(args[0])
# if type(data) is dict:
# return log.dictLogged(data,textlist)
# if type(data) is list:
# return log.listLogged(data,textlist)
# if type(data) is tuple: #N.B. tuples get converted to lists
# return log.listLogged(list(data),textlist)
# return data
def GetRelativeHistNum(self,histname):
'''Returns list with a histogram type and a relative number for that
histogram, or the original string if not a histogram
'''
histtype = histname.split()[0]
if histtype != histtype.upper(): # histograms (only) have a keyword all in caps
return histname
item, cookie = self.GetFirstChild(self.root)
i = 0
while item:
itemtext = self.GetItemText(item)
if itemtext == histname:
return histtype,i
elif itemtext.split()[0] == histtype:
i += 1
item, cookie = self.GetNextChild(self.root, cookie)
else:
raise Exception("Histogram not found: "+histname)
def ConvertRelativeHistNum(self,histtype,histnum):
'''Converts a histogram type and relative histogram number to a
histogram name in the current project
'''
item, cookie = self.GetFirstChild(self.root)
i = 0
while item:
itemtext = self.GetItemText(item)
if itemtext.split()[0] == histtype:
if i == histnum: return itemtext
i += 1
item, cookie = self.GetNextChild(self.root, cookie)
else:
raise Exception("Histogram #'+str(histnum)+' of type "+histtype+' not found')
def GetRelativePhaseNum(self,phasename):
'''Returns a phase number if the string matches a phase name
or else returns the original string
'''
item, cookie = self.GetFirstChild(self.root)
while item:
itemtext = self.GetItemText(item)
if itemtext == "Phases":
parent = item
item, cookie = self.GetFirstChild(parent)
i = 0
while item:
itemtext = self.GetItemText(item)
if itemtext == phasename:
return i
item, cookie = self.GetNextChild(parent, cookie)
i += 1
else:
return phasename # not a phase name
item, cookie = self.GetNextChild(self.root, cookie)
else:
raise Exception("No phases found ")
def ConvertRelativePhaseNum(self,phasenum):
'''Converts relative phase number to a phase name in
the current project
'''
item, cookie = self.GetFirstChild(self.root)
while item:
itemtext = self.GetItemText(item)
if itemtext == "Phases":
parent = item
item, cookie = self.GetFirstChild(parent)
i = 0
while item:
if i == phasenum:
return self.GetItemText(item)
item, cookie = self.GetNextChild(parent, cookie)
i += 1
else:
raise Exception("Phase "+str(phasenum)+" not found")
item, cookie = self.GetNextChild(self.root, cookie)
else:
raise Exception("No phases found ")
def GetImageLoc(self,TreeId):
'''Get Image data from the Tree. Handles cases where the
image name is specified, as well as where the image file name is
a tuple containing the image file and an image number
'''
size,imagefile = self.GetItemPyData(TreeId)
if type(imagefile) is tuple or type(imagefile) is list:
return size,imagefile[0],imagefile[1]
else:
return size,imagefile,None
def UpdateImageLoc(self,TreeId,imagefile):
'''Saves a new imagefile name in the Tree. Handles cases where the
image name is specified, as well as where the image file name is
a tuple containing the image file and an image number
'''
idata = self.GetItemPyData(TreeId)
if type(idata[1]) is tuple or type(idata[1]) is list:
idata[1] = list(idata[1])
idata[1][0] = [imagefile,idata[1][1]]
else:
idata[1] = imagefile
def SaveExposedItems(self):
'''Traverse the top level tree items and save names of exposed (expanded) tree items.
Done before a refinement.
'''
self.ExposedItems = []
item, cookie = self.GetFirstChild(self.root)
while item:
name = self.GetItemText(item)
if self.IsExpanded(item): self.ExposedItems.append(name)
item, cookie = self.GetNextChild(self.root, cookie)
# print 'exposed:',self.ExposedItems
def RestoreExposedItems(self):
'''Traverse the top level tree items and restore exposed (expanded) tree items
back to their previous state (done after a reload of the tree after a refinement)
'''
item, cookie = self.GetFirstChild(self.root)
while item:
name = self.GetItemText(item)
if name in self.ExposedItems: self.Expand(item)
item, cookie = self.GetNextChild(self.root, cookie)
def ReadOnlyTextCtrl(*args,**kwargs):
'''Create a read-only TextCtrl for display of constants
This is probably not ideal as it mixes visual cues, but it does look nice.
Addresses 4.2 bug where TextCtrl has no default size
'''
kwargs['style'] = wx.TE_READONLY
if wx.__version__.startswith('4.2') and 'size' not in kwargs:
kwargs['size'] = (105, 22)
Txt = wx.TextCtrl(*args,**kwargs)
Txt.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
return Txt
#### TextCtrl that stores input as entered with optional validation ################################################################################
class ValidatedTxtCtrl(wx.TextCtrl):
'''Create a TextCtrl widget that uses a validator to prevent the
entry of inappropriate characters and changes color to highlight
when invalid input is supplied. As valid values are typed,
they are placed into the dict or list where the initial value
came from. The type of the initial value must be int,
float or str or None (see :obj:`key` and :obj:`typeHint`);
this type (or the one in :obj:`typeHint`) is preserved.
Float values can be entered in the TextCtrl as numbers or also
as algebraic expressions using operators + - / \\* () and \\*\\*,
in addition pi, sind(), cosd(), tand(), and sqrt() can be used,
as well as appreviations s, sin, c, cos, t, tan and sq.
:param wx.Panel parent: name of panel or frame that will be
the parent to the TextCtrl. Can be None.
:param dict/list loc: the dict or list with the initial value to be
placed in the TextCtrl.
:param int/str key: the dict key or the list index for the value to be
edited by the TextCtrl. The ``loc[key]`` element must exist, but may
have value None. If None, the type for the element is taken from
:obj:`typeHint` and the value for the control is set initially
blank (and thus invalid.) This is a way to specify a field without a
default value: a user must set a valid value.
If the value is not None, it must have a base
type of int, float, str or unicode; the TextCrtl will be initialized
from this value.
:param list nDig: number of digits, places and optionally the format
([nDig,nPlc,fmt]) after decimal to use for display of float. The format
is either 'f' (default) or 'g'. Alternately, None can be specified which
causes numbers to be displayed with approximately 5 significant figures
for floats. If this is specified, then :obj:`typeHint` = float becomes the
default.
(Default=None).
:param bool notBlank: if True (default) blank values are invalid
for str inputs.
:param number xmin: minimum allowed valid value. If None (default) the
lower limit is unbounded.
NB: test in NumberValidator is val >= xmin not val > xmin
:param number xmax: maximum allowed valid value. If None (default) the
upper limit is unbounded
NB: test in NumberValidator is val <= xmax not val < xmax
:param list exclLim: if True exclude min/max value ([exclMin,exclMax]);
(Default=[False,False])
:param function OKcontrol: specifies a function or method that will be
called when the input is validated. The called function is supplied
with one argument which is False if the TextCtrl contains an invalid
value and True if the value is valid.
Note that this function should check all values
in the dialog when True, since other entries might be invalid.
The default for this is None, which indicates no function should
be called.
:param function OnLeave: specifies a function or method that will be
called when the focus for the control is lost.
The called function is supplied with (at present) three keyword arguments:
* invalid: (*bool*) True if the value for the TextCtrl is invalid
* value: (*int/float/str*) the value contained in the TextCtrl
* tc: (*wx.TextCtrl*) the TextCtrl object
The number of keyword arguments may be increased in the future should needs arise,
so it is best to code these functions with a \\*\\*kwargs argument so they will
continue to run without errors
The default for OnLeave is None, which indicates no function should
be called.
:param type typeHint: the value of typeHint should be int, float or str (or None).
The value for this will override the initial type taken from value
for the dict/list element ``loc[key]`` if not None and thus specifies the
type for input to the TextCtrl.
Defaults as None, which is ignored, unless :obj:`nDig` is specified in which
case the default is float.
:param bool CIFinput: for str input, indicates that only printable
ASCII characters may be entered into the TextCtrl. Forces output
to be ASCII rather than Unicode. For float and int input, allows
use of a single '?' or '.' character as valid input.
:param dict OnLeaveArgs: a dict with keyword args that are passed to
the :attr:`OnLeave` function. Defaults to ``{}``
:param bool ASCIIonly: if set as True will remove unicode characters from
strings
:param (other): other optional keyword parameters for the
wx.TextCtrl widget such as size or style may be specified.
'''
def __init__(self,parent,loc,key,nDig=None,notBlank=True,xmin=None,xmax=None,
OKcontrol=None,OnLeave=None,typeHint=None,CIFinput=False,exclLim=[False,False],
OnLeaveArgs={}, ASCIIonly=False,
min=None, max=None, # patch: remove this eventually
**kw):
# save passed values needed outside __init__
self.result = loc
self.key = key
self.nDig = nDig
self.OKcontrol=OKcontrol
self.OnLeave = OnLeave
self.OnLeaveArgs = OnLeaveArgs
self.CIFinput = CIFinput
self.notBlank = notBlank
self.ASCIIonly = ASCIIonly
# patch: remove this when min & max are no longer used to call this
if min is not None:
xmin=min
if GSASIIpath.GetConfigValue('debug'):
print('Call to ValidatedTxtCtrl using min (change to xmin) here:')
G2obj.HowDidIgetHere(True)
if max is not None:
xmax=max
if GSASIIpath.GetConfigValue('debug'):
print('Call to ValidatedTxtCtrl using max (change to xmax) here:')
G2obj.HowDidIgetHere(True)
# end patch
# initialization
self.invalid = False # indicates if the control has invalid contents
self.evaluated = False # set to True when the validator recognizes an expression
self.timer = None # tracks pending updates for expressions in float textctrls
self.delay = 5000 # delay for timer update (5 sec)
self.type = str
val = loc[key]
if 'style' in kw: # add a "Process Enter" to style
kw['style'] |= wx.TE_PROCESS_ENTER
else:
kw['style'] = wx.TE_PROCESS_ENTER
if 'size' not in kw: # wx 4.2.0 needs a size
kw['size'] = (105,-1)
if typeHint is not None:
self.type = typeHint
elif nDig is not None:
self.type = float
elif 'int' in str(type(val)):
self.type = int
elif 'float' in str(type(val)):
self.type = float
elif isinstance(val,str) or isinstance(val,unicode):
self.type = str
elif val is None:
raise Exception("ValidatedTxtCtrl error: value of "+str(key)+
" element is None and typeHint not defined as int or float")
else:
raise Exception("ValidatedTxtCtrl error: Unknown element ("+str(key)+
") type: "+str(type(val)))
if self.type is int:
wx.TextCtrl.__init__(self,parent,wx.ID_ANY,
validator=NumberValidator(int,result=loc,key=key,xmin=xmin,xmax=xmax,
exclLim=exclLim,OKcontrol=OKcontrol,CIFinput=CIFinput),**kw)
if val is not None:
self._setValue(val)
else: # no default is invalid for a number
self.invalid = True
self._IndicateValidity()
elif self.type is float:
wx.TextCtrl.__init__(self,parent,wx.ID_ANY,
validator=NumberValidator(float,result=loc,key=key,xmin=xmin,xmax=xmax,
exclLim=exclLim,OKcontrol=OKcontrol,CIFinput=CIFinput),**kw)
if val is not None:
self._setValue(val)
else:
self.invalid = True
self._IndicateValidity()
else:
if self.CIFinput:
wx.TextCtrl.__init__(
self,parent,wx.ID_ANY,
validator=ASCIIValidator(result=loc,key=key),
**kw)
else:
wx.TextCtrl.__init__(self,parent,wx.ID_ANY,**kw)
if val is not None:
self.SetValue(val)
if notBlank:
self.Bind(wx.EVT_CHAR,self._onStringKey)
self.ShowStringValidity() # test if valid input
else:
self.invalid = False
self.Bind(wx.EVT_CHAR,self._GetStringValue)
# When the mouse is moved away or the widget loses focus,
# display the last saved value, if an expression
self.Bind(wx.EVT_LEAVE_WINDOW, self._onLeaveWindow)
self.Bind(wx.EVT_KILL_FOCUS, self._onLoseFocus)
self.Bind(wx.EVT_TEXT_ENTER, self._onLoseFocus)
# patch for wx 2.9 on Mac
i,j= wx.__version__.split('.')[0:2]
if int(i)+int(j)/10. > 2.8 and 'wxOSX' in wx.PlatformInfo:
self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
def SetValue(self,val):
if self.result is not None: # note that this bypasses formatting
self.result[self.key] = val
log.LogVarChange(self.result,self.key)
self._setValue(val)
def _setValue(self,val,show=True):
'''Check the validity of an int or float value and convert to a str.
Possibly format it. If show is True, display the formatted value in
the Text widget.
'''
self.invalid = False
if self.type is int:
try:
if int(val) != val:
self.invalid = True
else:
val = int(val)
except:
if self.CIFinput and (val == '?' or val == '.'):
pass
else:
self.invalid = True
if show and not self.invalid: wx.TextCtrl.SetValue(self,str(val))
elif self.type is float:
try:
if type(val) is str: val = val.replace(',','.')
val = float(val) # convert strings, if needed
except:
if self.CIFinput and (val == '?' or val == '.'):
pass
else:
self.invalid = True
if self.nDig and show and not self.invalid:
wx.TextCtrl.SetValue(self,str(G2fil.FormatValue(val,self.nDig)))
self.evaluated = False # expression has been recast as value, reset flag
elif show and not self.invalid:
wx.TextCtrl.SetValue(self,str(G2fil.FormatSigFigs(val)).rstrip('0'))
self.evaluated = False # expression has been recast as value, reset flag
else:
if self.ASCIIonly:
s = ''
for c in val:
if ord(c) < 128:
s += c
else:
s += '!'
if val != s:
val = s
show = True
if show:
try:
wx.TextCtrl.SetValue(self,str(val))
except:
wx.TextCtrl.SetValue(self,val)
self.ShowStringValidity() # test if valid input
return
self._IndicateValidity()
if self.OKcontrol:
self.OKcontrol(not self.invalid)
def OnKeyDown(self,event):
'Special callback for wx 2.9+ on Mac where backspace is not processed by validator'
key = event.GetKeyCode()
if key in [wx.WXK_BACK, wx.WXK_DELETE]:
if self.Validator: wx.CallAfter(self.Validator.TestValid,self)
if key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER:
self._onLoseFocus(None)
if event: event.Skip()
if self.timer:
self.timer.Restart(self.delay)
def _onStringKey(self,event):
if event: event.Skip()
if self.invalid: # check for validity after processing the keystroke
wx.CallAfter(self.ShowStringValidity,True) # was invalid
else:
wx.CallAfter(self.ShowStringValidity,False) # was valid
def _IndicateValidity(self):
'Set the control colors to show invalid input'
if self.invalid:
ins = self.GetInsertionPoint()
self.SetForegroundColour("red")
self.SetBackgroundColour("yellow")
self.SetFocus()
self.Refresh() # this selects text on some Linuxes
self.SetSelection(0,0) # unselect
self.SetInsertionPoint(ins) # put insertion point back
else: # valid input
self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
self.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNTEXT))
self.Refresh()
def _GetNumValue(self):
'Get and where needed convert string from GetValue into int or float'
try:
val = self.GetValue()
if self.type is int:
val = int(val)
elif self.type is float:
val = float(val)
except:
if self.CIFinput and (val == '?' or val == '.'):
pass
else:
self.invalid = True
return val
def ShowStringValidity(self,previousInvalid=True):
'''Check if input is valid. Anytime the input is
invalid, call self.OKcontrol (if defined) because it is fast.
If valid, check for any other invalid entries only when
changing from invalid to valid, since that is slower.
:param bool previousInvalid: True if the TextCtrl contents were
invalid prior to the current change.
'''
val = self.GetValue().strip()
if self.notBlank:
self.invalid = not val
else:
self.invalid = False
self._IndicateValidity()
if self.invalid:
if self.OKcontrol:
self.OKcontrol(False)
elif self.OKcontrol and previousInvalid:
self.OKcontrol(True)
self._SaveStringValue() # always store the result
def _GetStringValue(self,event):
'''Get string input and store.
'''
if event: event.Skip() # process keystroke
wx.CallAfter(self._SaveStringValue)
def _SaveStringValue(self):
val = self.GetValue().strip()
# always store the result
if self.CIFinput and '2' in platform.python_version_tuple()[0]: # Py2/CIF make results ASCII
self.result[self.key] = val.encode('ascii','replace')
else:
self.result[self.key] = val
log.LogVarChange(self.result,self.key)
def _onLeaveWindow(self,event):
'''If the mouse leaves the text box, save the result, if valid,
but (unlike _onLoseFocus) there is a two second delay before
the textbox contents are updated with the value from the formula.
'''
def delayedUpdate():
self.timer = None
try:
self._setValue(self.result[self.key])
except:
pass
if self.type is not str:
if not self.IsModified(): return #ignore mouse crusing
elif self.result[self.key] == self.GetValue(): # .IsModified() seems unreliable for str
return
if self.evaluated and not self.invalid: # deal with computed expressions
if self.timer:
self.timer.Restart(self.delay)
else:
self.timer = wx.CallLater(self.delay,delayedUpdate) # this includes a try in case the widget is deleted
if self.invalid: # don't update an invalid expression
if event: event.Skip()
return
self._setValue(self.result[self.key],show=False) # save value quietly
if self.OnLeave:
self.event = event
self.OnLeave(invalid=self.invalid,value=self.result[self.key],
tc=self,**self.OnLeaveArgs)
if event: event.Skip()
def _onLoseFocus(self,event):
'''Enter has been pressed or focus transferred to another control,
Evaluate and update the current control contents
'''
if event: event.Skip()
if self.type is not str:
if not self.IsModified(): return #ignore mouse crusing
elif self.result[self.key] == self.GetValue(): # .IsModified() seems unreliable for str
return
if self.evaluated: # deal with computed expressions
if self.invalid: # don't substitute for an invalid expression
return
self._setValue(self.result[self.key])
elif self.result is not None: # show formatted result, as Bob wants
self.result[self.key] = self._GetNumValue()
if not self.invalid: # don't update an invalid expression
self._setValue(self.result[self.key])
if self.OnLeave:
self.event = event
try:
self.OnLeave(invalid=self.invalid,value=self.result[self.key],
tc=self,**self.OnLeaveArgs)
except:
pass
################################################################################
class NumberValidator(wxValidator):
'''A validator to be used with a TextCtrl to prevent
entering characters other than digits, signs, and for float
input, a period and exponents.
The value is checked for validity after every keystroke
If an invalid number is entered, the box is highlighted.
If the number is valid, it is saved in result[key]
:param type typ: the base data type. Must be int or float.
:param bool positiveonly: If True, negative integers are not allowed
(default False). This prevents the + or - keys from being pressed.
Used with typ=int; ignored for typ=float.
:param number xmin: Minimum allowed value. If None (default) the
lower limit is unbounded
:param number xmax: Maximum allowed value. If None (default) the
upper limit is unbounded
:param list exclLim: if True exclude xmin/xmax value ([exclMin,exclMax]);
(Default=[False,False])
:param dict/list result: List or dict where value should be placed when valid
:param any key: key to use for result (int for list)
:param function OKcontrol: function or class method to control
an OK button for a window.
Ignored if None (default)
:param bool CIFinput: allows use of a single '?' or '.' character
as valid input.
'''
def __init__(self, typ, positiveonly=False, xmin=None, xmax=None,exclLim=[False,False],
result=None, key=None, OKcontrol=None, CIFinput=False):
'Create the validator'
wxValidator.__init__(self)
# save passed parameters
self.typ = typ
self.positiveonly = positiveonly
self.xmin = xmin
self.xmax = xmax
self.exclLim = exclLim
self.result = result
self.key = key
self.OKcontrol = OKcontrol
self.CIFinput = CIFinput
# set allowed keys by data type
self.Bind(wx.EVT_CHAR, self.OnChar)
if self.typ == int and self.positiveonly:
self.validchars = '0123456789'
elif self.typ == int:
self.validchars = '0123456789+-'
elif self.typ == float:
# allow for above and sind, cosd, sqrt, tand, pi, and abbreviations
# also addition, subtraction, division, multiplication, exponentiation
self.validchars = '0123456789.-+eE/cosindcqrtap()*,'
else:
self.validchars = None
return
if self.CIFinput:
self.validchars += '?.'
def Clone(self):
'Create a copy of the validator, a strange, but required component'
return NumberValidator(typ=self.typ,
positiveonly=self.positiveonly,
xmin=self.xmin, xmax=self.xmax,
result=self.result, key=self.key,
OKcontrol=self.OKcontrol,
CIFinput=self.CIFinput)
def TransferToWindow(self):
'Needed by validator, strange, but required component'
return True # Prevent wxDialog from complaining.
def TransferFromWindow(self):
'Needed by validator, strange, but required component'
return True # Prevent wxDialog from complaining.
def TestValid(self,tc):
'''Check if the value is valid by casting the input string
into the current type.
Set the invalid variable in the TextCtrl object accordingly.
If the value is valid, save it in the dict/list where
the initial value was stored, if appropriate.
:param wx.TextCtrl tc: A reference to the TextCtrl that the validator
is associated with.
'''
tc.invalid = False # assume valid
if self.CIFinput:
val = tc.GetValue().strip()
if val == '?' or val == '.':
self.result[self.key] = val
log.LogVarChange(self.result,self.key)
return
try:
val = self.typ(tc.GetValue())
except (ValueError, SyntaxError):
if self.typ is float: # for float values, see if an expression can be evaluated
val = G2fil.FormulaEval(tc.GetValue().replace(',','.'))
if val is None:
tc.invalid = True
return
else:
tc.evaluated = True
else:
tc.invalid = True
return
if self.xmax != None:
if val >= self.xmax and self.exclLim[1]:
tc.invalid = True
elif val > self.xmax:
tc.invalid = True
if self.xmin != None:
if val <= self.xmin and self.exclLim[0]:
tc.invalid = True
elif val < self.xmin:
tc.invalid = True # invalid
if self.key is not None and self.result is not None and not tc.invalid:
self.result[self.key] = val
log.LogVarChange(self.result,self.key)
def ShowValidity(self,tc):
'''Set the control colors to show invalid input
:param wx.TextCtrl tc: A reference to the TextCtrl that the validator
is associated with.
'''
if tc.invalid:
ins = tc.GetInsertionPoint()
tc.SetForegroundColour("red")
tc.SetBackgroundColour("yellow")
tc.SetFocus()
tc.Refresh() # this selects text on some Linuxes
tc.SetSelection(0,0) # unselect
tc.SetInsertionPoint(ins) # put insertion point back
return False
else: # valid input
tc.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
tc.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNTEXT))
tc.Refresh()
return True
def CheckInput(self,previousInvalid):
'''called to test every change to the TextCtrl for validity and
to change the appearance of the TextCtrl
Anytime the input is invalid, call self.OKcontrol
(if defined) because it is fast.
If valid, check for any other invalid entries only when
changing from invalid to valid, since that is slower.
:param bool previousInvalid: True if the TextCtrl contents were
invalid prior to the current change.
'''
tc = self.GetWindow()
self.TestValid(tc)
self.ShowValidity(tc)
# if invalid
if tc.invalid and self.OKcontrol:
self.OKcontrol(False)
if not tc.invalid and self.OKcontrol and previousInvalid:
self.OKcontrol(True)
def OnChar(self, event):
'''Called each type a key is pressed
ignores keys that are not allowed for int and float types
'''
key = event.GetKeyCode()
tc = self.GetWindow()
if key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER:
if tc.invalid:
self.CheckInput(True)
else:
self.CheckInput(False)
if event: event.Skip()
return
if key < wx.WXK_SPACE or key == wx.WXK_DELETE or key > 255: # control characters get processed
if event: event.Skip()
if tc.invalid:
wx.CallAfter(self.CheckInput,True)
else:
wx.CallAfter(self.CheckInput,False)
return
elif chr(key) in self.validchars: # valid char pressed?
if event: event.Skip()
if tc.invalid:
wx.CallAfter(self.CheckInput,True)
else:
wx.CallAfter(self.CheckInput,False)
return
return # Returning without calling event.Skip, which eats the keystroke
################################################################################
class ASCIIValidator(wxValidator):
'''A validator to be used with a TextCtrl to prevent
entering characters other than ASCII characters.
The value is checked for validity after every keystroke
If an invalid number is entered, the box is highlighted.
If the number is valid, it is saved in result[key]
:param dict/list result: List or dict where value should be placed when valid
:param any key: key to use for result (int for list)
'''
def __init__(self, result=None, key=None):
'Create the validator'
import string
wxValidator.__init__(self)
# save passed parameters
self.result = result
self.key = key
self.validchars = string.ascii_letters + string.digits + string.punctuation + string.whitespace
self.Bind(wx.EVT_CHAR, self.OnChar)
def Clone(self):
'Create a copy of the validator, a strange, but required component'
return ASCIIValidator(result=self.result, key=self.key)
tc = self.GetWindow()
tc.invalid = False # make sure the validity flag is defined in parent
def TransferToWindow(self):
'Needed by validator, strange, but required component'
return True # Prevent wxDialog from complaining.
def TransferFromWindow(self):
'Needed by validator, strange, but required component'
return True # Prevent wxDialog from complaining.
def TestValid(self,tc):
'''Check if the value is valid by casting the input string
into ASCII.
Save it in the dict/list where the initial value was stored
:param wx.TextCtrl tc: A reference to the TextCtrl that the validator
is associated with.
'''
if '2' in platform.python_version_tuple()[0]:
self.result[self.key] = tc.GetValue().encode('ascii','replace')
else:
self.result[self.key] = tc.GetValue()
log.LogVarChange(self.result,self.key)
def OnChar(self, event):
'''Called each type a key is pressed
ignores keys that are not allowed for int and float types
'''
key = event.GetKeyCode()
tc = self.GetWindow()
if key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER:
self.TestValid(tc)
if event: event.Skip()
return
if key < wx.WXK_SPACE or key == wx.WXK_DELETE or key > 255: # control characters get processed
if event: event.Skip()
self.TestValid(tc)
return