Changeset 933 for trunk/GSASIIgrid.py
- Timestamp:
- May 26, 2013 1:35:30 PM (10 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/GSASIIgrid.py
r927 r933 12 12 -------------------------------- 13 13 14 15 14 ''' 16 15 import wx … … 18 17 import wx.wizard as wz 19 18 import wx.aui 19 import wx.lib.scrolledpanel as wxscroll 20 20 import time 21 21 import cPickle … … 39 39 import GSASIIconstrGUI as G2cnstG 40 40 import GSASIIrestrGUI as G2restG 41 import GSASIIpy3 as G2py3 41 42 42 43 # trig functions in degrees … … 137 138 ################################################################################ 138 139 140 class ValidatedTxtCtrl(wx.TextCtrl): 141 '''Create a TextCtrl widget that uses a validator to prevent the 142 entry of inappropriate characters and changes color to highlight 143 when invalid input is supplied. As valid values are typed, 144 they are placed into the dict or list where the initial value 145 came from. The type of the initial value must be int, float or str; 146 this type is preserved. 147 148 Float values can be entered in the TextCtrl as numbers or also 149 as algebraic expressions using operators + - / \* () and \*\*, 150 in addition pi, sind(), cosd(), tand(), and sqrt() can be used, 151 as well as appreviations s, sin, c, cos, t, tan and sq. 152 153 :param wx.Panel parent: name of panel or frame that will be 154 the parent to the TextCtrl 155 156 :param dict/list loc: the dict or list with the initial value to be 157 placed in the TextCtrl 158 159 :param int/str key: the dict key or the list index for the value 160 161 :param bool notBlank: if True (default) blank values are not allowed 162 for str inputs. 163 164 :param number min: Minimum allowed value. If None (default) the 165 lower limit is unbounded 166 167 :param number max: Maximum allowed value. If None (default) the 168 upper limit is unbounded 169 170 :param wx.Size size: an optional size parameter that dictates the 171 size for the TextCtrl. None (the default) indicates that the 172 default size should be used. 173 174 :param function OKcontrol: specifies a function or method that will be 175 called when the input is validated. It is supplied with one argument 176 which is False if the TextCtrl contains an invalid value and True if 177 the value is valid. Note that this function should check all values 178 in the dialog when True, since other entries might be invalid. 179 180 ''' 181 def __init__(self,parent,loc,key,notBlank=True,min=None,max=None, 182 size=None,OKcontrol=None): 183 self.result = loc 184 self.key = key 185 self.OKcontrol=OKcontrol 186 self.invalid = None 187 val = loc[key] 188 if isinstance(val,int): 189 wx.TextCtrl.__init__( 190 self,parent,wx.ID_ANY,str(val), 191 validator=NumberValidator(int,result=loc,key=key,min=min,max=max, 192 OKcontrol=OKcontrol) 193 ) 194 self.invalid = not self.Validate() 195 elif isinstance(val,int) or isinstance(val,float): 196 wx.TextCtrl.__init__( 197 self,parent,wx.ID_ANY,str(val), 198 validator=NumberValidator(float,result=loc,key=key,min=min,max=max, 199 OKcontrol=OKcontrol) 200 ) 201 self.invalid = not self.Validate() 202 elif isinstance(val,str): 203 wx.TextCtrl.__init__(self,parent,wx.ID_ANY,str(val)) 204 if notBlank: 205 self.Bind(wx.EVT_CHAR,self._onStringKey) 206 self.ShowStringValidity() # test if valid input 207 else: 208 self.invalid = False 209 else: 210 raise Exception,"ValidatedTxtCtrl Unknown type "+str(type(val)) 211 if size: self.SetSize(size) 212 213 def _onStringKey(self,event): 214 event.Skip() 215 if self.invalid: # check for validity after processing the keystroke 216 wx.CallAfter(self.ShowStringValidity,True) # was invalid 217 else: 218 wx.CallAfter(self.ShowStringValidity,False) # was valid 219 220 def ShowStringValidity(self,previousInvalid=None): 221 '''Check if input is valid. Anytime the input is 222 invalid, call self.OKcontrol (if defined) because it is fast. 223 If valid, check for any other invalid entries only when 224 changing from invalid to valid, since that is slower. 225 226 :param bool previousInvalid: True if the TextCtrl contents were 227 invalid prior to the current change. 228 229 ''' 230 val = self.GetValue().strip() 231 self.invalid = not val 232 'Set the control colors to show invalid input' 233 if self.invalid: 234 self.SetForegroundColour("red") 235 self.SetBackgroundColour("yellow") 236 self.SetFocus() 237 self.Refresh() 238 if self.OKcontrol: 239 self.OKcontrol(False) 240 else: # valid input 241 self.SetBackgroundColour( 242 wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)) 243 self.SetForegroundColour("black") 244 self.Refresh() 245 self.result[self.key] = val 246 if self.OKcontrol and previousInvalid: 247 self.OKcontrol(True) 248 249 class NumberValidator(wx.PyValidator): 250 '''A validator to be used with a TextCtrl to prevent 251 entering characters other than digits, signs, and for float 252 input, a period and exponents. 253 254 The value is checked for validity after every keystroke 255 If an invalid number is entered, the box is highlighted. 256 If the number is valid, it is saved in result[key] 257 258 :param type typ: data type, int or float 259 260 :param bool positiveonly: used for typ=int. If True, only positive 261 integers are allowed (default False) 262 263 :param number min: Minimum allowed value. If None (default) the 264 lower limit is unbounded 265 266 :param number max: Maximum allowed value. If None (default) the 267 upper limit is unbounded 268 269 :param dict/list result: List or dict where value should be placed when valid 270 271 :param any key: key to use for result (int for list) 272 273 :param function OKcontrol: function or class method to control 274 an OK button for a window. 275 Ignored if None (default) 276 ''' 277 def __init__(self, typ, positiveonly=False, min=None, max=None, 278 result=None, key=None, OKcontrol=None): 279 'Create the validator' 280 wx.PyValidator.__init__(self) 281 self.typ = typ 282 self.positiveonly = positiveonly 283 self.min = min 284 self.max = max 285 self.result = result 286 self.key = key 287 self.OKcontrol = OKcontrol 288 self.evaluated = False 289 # When the mouse is moved away or the widget loses focus 290 # display the last saved value 291 self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave) 292 self.Bind(wx.EVT_KILL_FOCUS, self.OnLeave) 293 if self.typ == int and self.positiveonly: 294 self.validchars = '0123456789' 295 self.Bind(wx.EVT_CHAR, self.OnChar) 296 elif self.typ == int: 297 self.validchars = '0123456789+-' 298 self.Bind(wx.EVT_CHAR, self.OnChar) 299 elif self.typ == float: 300 # allow for above and sind, cosd, sqrt, tand, pi, and abbreviations 301 # also addition, subtraction, division, multiplication, exponentiation 302 self.validchars = '0123456789.-+eE/cosindcqrtap()*' 303 self.Bind(wx.EVT_CHAR, self.OnChar) 304 else: 305 self.validchars = None 306 def Clone(self): 307 'Create a copy of the validator, a strange, but required component' 308 return NumberValidator(typ=self.typ, 309 positiveonly=self.positiveonly, 310 min=self.min, max=self.max, 311 result=self.result, key=self.key, 312 OKcontrol=self.OKcontrol) 313 tc = self.GetWindow() 314 tc.invalid = False # make sure the validity flag is defined in parent 315 def TransferToWindow(self): 316 'Needed by validator, strange, but required component' 317 return True # Prevent wxDialog from complaining. 318 def TransferFromWindow(self): 319 'Needed by validator, strange, but required component' 320 return True # Prevent wxDialog from complaining. 321 def TestValid(self,tc): 322 '''Check if the value is valid by casting the input string 323 into the current type. 324 325 Set the invalid variable in the TextCtrl object accordingly. 326 327 If the value is valid, save it in the dict/list where 328 the initial value was stored, if appropriate. 329 ''' 330 tc.invalid = False # assume invalid 331 try: 332 val = self.typ(tc.GetValue()) 333 except (ValueError, SyntaxError) as e: 334 if self.typ is float: # for float values, see if an expression can be 335 # evaluated 336 val = G2py3.FormulaEval(tc.GetValue()) 337 if val is None: 338 tc.invalid = True 339 return 340 else: 341 self.evaluated = True 342 else: 343 tc.invalid = True 344 return 345 if self.max != None and self.typ == int: 346 if val > self.max: 347 tc.invalid = True 348 if self.max != None and self.typ == int: 349 if val < self.min: 350 tc.invalid = True # invalid 351 if self.key is not None and self.result is not None and not tc.invalid: 352 self.result[self.key] = val 353 354 def ShowValidity(self,tc): 355 'Set the control colors to show invalid input' 356 if tc.invalid: 357 tc.SetForegroundColour("red") 358 tc.SetBackgroundColour("yellow") 359 tc.SetFocus() 360 tc.Refresh() 361 return False 362 else: # valid input 363 tc.SetBackgroundColour( 364 wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)) 365 tc.SetForegroundColour("black") 366 tc.Refresh() 367 return True 368 369 def OnLeave(self, event): 370 '''Show the computed value when an expression is entered to the TextCtrl 371 Make sure that the number fits by truncating decimal places and switching 372 to scientific notation, as needed. 373 Called on loss of focus. 374 ''' 375 tc = self.GetWindow() 376 if tc.invalid: return # don't substitute for an invalid expression 377 if not self.evaluated: return # true when an expression is evaluated 378 self.evaluated = False 379 if self.result is not None: # retrieve the stored result 380 val = self.result[self.key] 381 tc.SetValue(G2py3.FormatValue(val)) 382 383 def CheckInput(self,previousInvalid): 384 '''called to test every change to the TextCtrl for validity and 385 to change the appearance of the TextCtrl 386 387 Anytime the input is invalid, call self.OKcontrol 388 (if defined) because it is fast. 389 If valid, check for any other invalid entries only when 390 changing from invalid to valid, since that is slower. 391 ''' 392 tc = self.GetWindow() 393 self.TestValid(tc) 394 self.ShowValidity(tc) 395 # if invalid and 396 if tc.invalid and self.OKcontrol: 397 self.OKcontrol(False) 398 if not tc.invalid and self.OKcontrol and previousInvalid: 399 self.OKcontrol(True) 400 401 def OnChar(self, event): 402 '''Called each type a key is pressed 403 ignores keys that are not allowed for int and float types 404 ''' 405 key = event.GetKeyCode() 406 if key == wx.WXK_RETURN: 407 tc = self.GetWindow() 408 if tc.invalid: 409 self.CheckInput(True) 410 else: 411 self.CheckInput(False) 412 return 413 if key < wx.WXK_SPACE or key == wx.WXK_DELETE or key > 255: # control characters get processed 414 event.Skip() 415 tc = self.GetWindow() 416 if tc.invalid: 417 wx.CallAfter(self.CheckInput,True) 418 else: 419 wx.CallAfter(self.CheckInput,False) 420 return 421 elif chr(key) in self.validchars: # valid char pressed? 422 event.Skip() 423 tc = self.GetWindow() 424 if tc.invalid: 425 wx.CallAfter(self.CheckInput,True) 426 else: 427 wx.CallAfter(self.CheckInput,False) 428 return 429 if not wx.Validator_IsSilent(): 430 wx.Bell() 431 return # Returning without calling event.Skip, which eats the keystroke 432 433 ################################################################################ 434 class ScrolledMultiEditor(wx.Dialog): 435 '''Define a window for editing a potentially large number of dict- or 436 list-contained values with validation for each item. Edited values are 437 automatically placed in their source location. If invalid entries 438 are provided, the TextCtrl is turned yellow and the OK button is disabled. 439 440 The type for each TextCtrl validation is determined by the 441 initial value of the entry (int, float or string). 442 Float values can be entered in the TextCtrl as numbers or also 443 as algebraic expressions using operators + - / \* () and \*\*, 444 in addition pi, sind(), cosd(), tand(), and sqrt() can be used, 445 as well as appreviations s(), sin(), c(), cos(), t(), tan() and sq(). 446 447 :param wx.Frame parent: name of parent window, or may be None 448 449 :param tuple dictlst: a list of dicts or lists containing values to edit 450 451 :param tuple elemlst: a list of keys for each item in a dictlst. Must have the 452 same length as dictlst. 453 454 :param wx.Frame parent: name of parent window, or may be None 455 456 :param tuple prelbl: a list of labels placed before the TextCtrl for each 457 item (optional) 458 459 :param tuple postlbl: a list of labels placed after the TextCtrl for each 460 item (optional) 461 462 :param str title: a title to place in the frame of the dialog 463 464 :param str header: text to place at the top of the window. May contain 465 new line characters. 466 467 :param wx.Size size: a size parameter that dictates the 468 size for the scrolled region of the dialog. The default is 469 (300,250). 470 471 :returns: the wx.Dialog created here. Use .ShowModal() to 472 473 ''Example for use of ScrolledMultiEditor:'' 474 475 :: 476 477 dlg = <pkg>.ScrolledMultiEditor(frame,dictlst,elemlst,prelbl,postlbl, 478 header=header) 479 if dlg.ShowModal() == wx.ID_OK: 480 for d,k in zip(dictlst,elemlst): 481 print d[k] 482 483 ''Example definitions for dictlst and elemlst:'' 484 485 :: 486 487 dictlst = (dict1,list1,dict1,list1) 488 elemlst = ('a', 1, 2, 3) 489 490 This causes items dict1['a'], list1[1], dict1[2] and list1[3] to be edited. 491 492 Note that these items must have int, float or str values assigned to 493 them. The dialog will force these types to be retained. String values 494 that are blank are marked as invalid. 495 ''' 496 497 def __init__(self,parent,dictlst,elemlst,prelbl=[],postlbl=[], 498 title='Edit items',header='',size=(300,250)): 499 if len(dictlst) != len(elemlst): 500 raise Exception,"ScrolledMultiEditor error: len(dictlst) != len(elemlst) "+str(len(dictlst))+" != "+str(len(elemlst)) 501 wx.Dialog.__init__( # create dialog & sizer 502 self,parent,wx.ID_ANY,title, 503 style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) 504 mainSizer = wx.BoxSizer(wx.VERTICAL) 505 # ad a header if supplied 506 if header: 507 subSizer = wx.BoxSizer(wx.HORIZONTAL) 508 subSizer.Add((-1,-1),1,wx.EXPAND) 509 subSizer.Add(wx.StaticText(self,wx.ID_ANY,header)) 510 subSizer.Add((-1,-1),1,wx.EXPAND) 511 mainSizer.Add(subSizer,0,wx.EXPAND,0) 512 # make OK button now, because we will need it for validation 513 self.OKbtn = wx.Button(self, wx.ID_OK) 514 self.OKbtn.SetDefault() 515 # create scrolled panel and sizer 516 panel = wxscroll.ScrolledPanel( 517 self, wx.ID_ANY, 518 size=size, 519 style = wx.TAB_TRAVERSAL|wx.SUNKEN_BORDER) 520 subSizer = wx.FlexGridSizer(rows=len(dictlst),cols=3,hgap=2,vgap=2) 521 self.ValidatedControlsList = [] # make list of TextCtrls 522 for i,(d,k) in enumerate(zip(dictlst,elemlst)): 523 if i >= len(prelbl): # label before TextCtrl, or put in a blank 524 subSizer.Add((-1,-1)) 525 else: 526 subSizer.Add(wx.StaticText(panel,wx.ID_ANY,str(prelbl[i]))) 527 # create the validated TextCrtl, store it and add it to the sizer 528 ctrl = ValidatedTxtCtrl(panel,d,k,OKcontrol=self.ControlOKButton) 529 self.ValidatedControlsList.append(ctrl) 530 subSizer.Add(ctrl) 531 if i >= len(postlbl): # label after TextCtrl, or put in a blank 532 subSizer.Add((-1,-1)) 533 else: 534 subSizer.Add(wx.StaticText(panel,wx.ID_ANY,str(postlbl[i]))) 535 # finish up ScrolledPanel 536 panel.SetSizer(subSizer) 537 panel.SetAutoLayout(1) 538 panel.SetupScrolling() 539 mainSizer.Add(panel,1, wx.ALL|wx.EXPAND,1) 540 541 # Sizer for OK/Close buttons. N.B. using Close rather than Cancel because 542 # Cancel would imply that the changes should be discarded. In fact 543 # any valid changes are retained, unless one makes a copy of the 544 # input dicts & lists and restores them. 545 btnsizer = wx.BoxSizer(wx.HORIZONTAL) 546 btnsizer.Add(self.OKbtn) 547 btn = wx.Button(self, wx.ID_CLOSE) 548 btn.Bind(wx.EVT_BUTTON,self._onClose) 549 btnsizer.Add(btn) 550 mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5) 551 # size out the window. Set it to be enlarged but not made smaller 552 self.SetSizer(mainSizer) 553 mainSizer.Fit(self) 554 self.SetMinSize(self.GetSize()) 555 556 def _onClose(self,event): 557 'Close the window' 558 self.EndModal(wx.ID_CANCEL) 559 560 def ControlOKButton(self,setvalue): 561 '''Enable or Disable the OK button for the dialog. Note that this is 562 passed into the ValidatedTxtCtrl for use by validators. 563 564 :param bool setvalue: if True, all entries in the dialog are 565 checked for validity. if False then the OK button is disabled. 566 567 ''' 568 if setvalue: # turn button on, do only if all controls show as valid 569 for ctrl in self.ValidatedControlsList: 570 if ctrl.invalid: 571 self.OKbtn.Disable() 572 return 573 else: 574 self.OKbtn.Enable() 575 else: 576 self.OKbtn.Disable() 577 578 ################################################################################ 139 579 class SymOpDialog(wx.Dialog): 140 580 '''Class to select a symmetry operator … … 460 900 parent.Raise() 461 901 self.EndModal(wx.ID_CANCEL) 462 902 903 ################################################################################ 904 class SingleStringDialog(wx.Dialog): 905 '''Dialog to obtain a single string value from user 906 907 :param wx.Frame parent: name of parent frame 908 :param str title: title string for dialog 909 :param str prompt: string to tell use what they are inputting 910 :param str value: default input value, if any 911 ''' 912 def __init__(self,parent,title,prompt,value='',size=(200,-1)): 913 wx.Dialog.__init__(self,parent,wx.ID_ANY,title, 914 pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE) 915 self.value = value 916 self.prompt = prompt 917 self.CenterOnParent() 918 self.panel = wx.Panel(self) 919 mainSizer = wx.BoxSizer(wx.VERTICAL) 920 mainSizer.Add(wx.StaticText(self.panel,-1,self.prompt),0,wx.ALIGN_CENTER) 921 self.valItem = wx.TextCtrl(self.panel,-1,value=str(self.value),size=size) 922 mainSizer.Add(self.valItem,0,wx.ALIGN_CENTER) 923 btnsizer = wx.StdDialogButtonSizer() 924 OKbtn = wx.Button(self.panel, wx.ID_OK) 925 OKbtn.SetDefault() 926 btnsizer.AddButton(OKbtn) 927 btn = wx.Button(self.panel, wx.ID_CANCEL) 928 btnsizer.AddButton(btn) 929 btnsizer.Realize() 930 mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER) 931 self.panel.SetSizer(mainSizer) 932 self.panel.Fit() 933 self.Fit() 934 935 def Show(self): 936 '''Use this method after creating the dialog to post it 937 :returns: True if the user pressed OK; False if the User pressed Cancel 938 ''' 939 if self.ShowModal() == wx.ID_OK: 940 self.value = self.valItem.GetValue() 941 return True 942 else: 943 return False 944 945 def GetValue(self): 946 '''Use this method to get the value entered by the user 947 :returns: string entered by user 948 ''' 949 return self.value 950 951 ################################################################################ 952 def ItemSelector(ChoiceList, ParentFrame=None, 953 title='Select an item', 954 size=None, header='Item Selector', 955 useCancel=True): 956 ''' Provide a wx dialog to select a single item from list of choices 957 958 :param list ChoiceList: a list of choices where one will be selected 959 :param wx.Frame ParentFrame: Name of parent frame (default None) 960 :param str title: heading above list of choices (default 'Select an item') 961 :param wx.Size size: Size for dialog to be created (default None -- size as needed) 962 :param str header: Title to place on window frame (default 'Item Selector') 963 :param bool useCancel: If True (default) both the OK and Cancel buttons are offered 964 965 :returns: the selection index or None 966 ''' 967 if useCancel: 968 dlg = wx.SingleChoiceDialog( 969 ParentFrame,title, header, ChoiceList) 970 else: 971 dlg = wx.SingleChoiceDialog( 972 ParentFrame,title, header,ChoiceList, 973 style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.OK|wx.CENTRE) 974 if size: dlg.SetSize(size) 975 if dlg.ShowModal() == wx.ID_OK: 976 sel = dlg.GetSelection() 977 return sel 978 else: 979 return None 980 dlg.Destroy() 981 982 ################################################################################ 463 983 class GridFractionEditor(wg.PyGridCellEditor): 464 984 '''A grid cell editor class that allows entry of values as fractions as well … … 546 1066 evt.Skip() 547 1067 1068 ################################################################################ 548 1069 class MyHelp(wx.Menu): 549 1070 ''' … … 704 1225 return 705 1226 1227 ################################################################################ 706 1228 class AddHelp(wx.Menu): 707 1229 '''This class a single entry for the help menu (used on the Mac only): … … 728 1250 ShowHelp(self.HelpById,self.frame) 729 1251 1252 ################################################################################ 730 1253 class MyHtmlPanel(wx.Panel): 731 1254 '''Defines a panel to display Help information''' … … 785 1308 self.GetOpenedPageTitle()) 786 1309 1310 ################################################################################ 787 1311 class DataFrame(wx.Frame): 788 1312 '''Create the dataframe window and all the entries in menus. … … 2396 2920 sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10) 2397 2921 2922 if __name__ == '__main__': 2923 # test ScrolledMultiEditor 2924 app = wx.PySimpleApp() 2925 frm = wx.Frame(None) # create a frame 2926 frm.Show(True) 2927 Data1 = { 2928 'Order':0, 2929 'omega':'string', 2930 'chi':2.0, 2931 'phi':'', 2932 } 2933 elemlst = sorted(Data1.keys()) 2934 postlbl = sorted(Data1.keys()) 2935 dictlst = len(elemlst)*[Data1,] 2936 2937 Data2 = list(range(100)) 2938 elemlst += range(2,60) 2939 postlbl += range(2,60) 2940 dictlst += len(range(2,60))*[Data2,] 2941 2942 prelbl = range(len(elemlst)) 2943 postlbl[1] = "a very long label for the 2nd item to force a horiz. scrollbar" 2944 header="""This is a longer\nmultiline and perhaps silly header""" 2945 dlg = ScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl, 2946 header=header) 2947 print Data1 2948 if dlg.ShowModal() == wx.ID_OK: 2949 for d,k in zip(dictlst,elemlst): 2950 print k,d[k] 2951 #app.MainLoop()
Note: See TracChangeset
for help on using the changeset viewer.