# -*- coding: utf-8 -*-
#GSASIIctrlGUI - Custom GSAS-II GUI controls
'''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.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 numpy as np
from . import GSASIIpath
from . import GSASIIdataGUI as G2gd
from . import GSASIIpwdGUI as G2pdG
from . import GSASIIspc as G2spc
from . import GSASIIobj as G2obj
from . import GSASIIfiles as G2fil
from . import GSASIIElem as G2elem
from . import GSASIIpwd as G2pwd
from . import GSASIIlattice as G2lat
from . import GSASIImath as G2mth
#from . import GSASIIstrMain as G2stMn
from . import GSASIImiscGUI as G2IO
from .tutorialIndex import tutorialIndex
# 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 ################################################################################
[docs]
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 (f'DBG_Problem in ID name: {arg} needs to start w/wxID_')
if arg in globals():
if GSASIIpath.GetConfigValue('debug'):
print (f'DBG_Warning: {arg} already defined')
continue
exec(f'global {arg}; {arg} = wx.NewId()')
#### Tree Control ################################################################################
[docs]
class G2TreeCtrl(wx.TreeCtrl):
'''Create a wrapper around the standard TreeCtrl so we can "wrap"
various events.
'''
def __init__(self,parent=None,*args,**kwargs):
super(self.__class__,self).__init__(parent=parent,*args,**kwargs)
fontIncr = GSASIIpath.GetConfigValue('FontSize_incr')
self.font = None
self.G2frame = parent.GetTopLevelParent()
self.root = self.AddRoot('Loaded Data: ')
if fontIncr is not None and fontIncr != 0:
self.font = wx.Font(self.GetFont())
self.font.SetPointSize(self.font.PointSize+fontIncr)
self.SetItemFont(self.root, self.font)
self.SelectionChanged = None
self.textlist = None
[docs]
def AppendItem(self, parent, text, image=-1, selImage=-1, data=None):
'override the standard method so font size can be set'
item = super(self.__class__,self).AppendItem(parent, text, image, selImage, data)
if self.font: self.SetItemFont(item, self.font)
return item
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
[docs]
def GetItemPyData(self,treeId):
return wx.TreeCtrl.GetItemData(self,treeId)
[docs]
def SetItemPyData(self,treeId,data):
return wx.TreeCtrl.SetItemData(self,treeId,data)
def UpdateSelection(self):
TId = self.GetFocusedItem()
self.SelectItem(self.root)
self.SelectItem(TId)
[docs]
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)
[docs]
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')
[docs]
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 ")
[docs]
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 ")
[docs]
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
[docs]
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
[docs]
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
[docs]
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)
[docs]
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 ################################################################################
[docs]
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.
Values are processed and saved when Enter is pressed, when the
mouse is moved to another control (leave window or focus is lost)
or after a change and a delay of two seconds.
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,
**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
self.changed = False
# 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 = 2000 # delay for timer update (2 sec)
self.type = str
self.defaultBackgroundColor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)
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):
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()
wx.CallAfter(self._TestValidity) # test/show validity
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.ChangeValue(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)
# Mac is "special" because backspace etc. does not trigger validator
if sys.platform == "darwin":
self.Bind(wx.EVT_KEY_DOWN,self.OnKeyDown)
# 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)
[docs]
def SetValue(self,val,warn=True):
'''Place a value into the text widget and save it into the
associated array element. Note that, unlike the stock wx.TextCtrl,
val is expected to be in the form expected by the widget
(float/int/str) rather than only str's.
For float val values, the value is formatted when placed in the
TextCtrl, but the supplied value is what is actually saved.
This routine triggers a wx.EVT_TEXT event
'''
# GSAS-II callback routines should call ChangeValue not SetValue
# for debugging flag calls. Set warn to False for calls that are not in callbacks
# and thus are OK
if GSASIIpath.GetConfigValue('debug') and warn:
print('ValidatedTxtCtrl.SetValue() used in callback. Better as ChangeValue()?')
G2obj.HowDidIgetHere(True)
if self.result is not None:
if self.result[self.key] != val:
self.changed = True
self.result[self.key] = val
self._setValue(val)
# Direct calls to SetValue should trigger an event
wx.TextCtrl.SetValue(self,wx.TextCtrl.GetValue(self))
[docs]
def ChangeValue(self,val):
'''Place a value into the text widget and save it into the
associated array element. Note that, unlike the stock wx.TextCtrl,
val is expected to be in the form expected by the widget
(float/int/str) rather than only str's.
For float val values, the value is formatted when placed in the
TextCtrl, but the supplied value is what is actually saved.
This routine does not trigger a wx.EVT_TEXT event. This is what
should be used inside event callbacks, not :meth:`SetValue`.
'''
if self.result is not None and self.result[self.key] != val:
self.changed = True
self.result[self.key] = val
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.ChangeValue(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.ChangeValue(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.ChangeValue(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.ChangeValue(self,str(val))
except:
wx.TextCtrl.ChangeValue(self,val)
self.ShowStringValidity() # test if valid input
return
self._IndicateValidity()
if self.OKcontrol:
self.OKcontrol(not self.invalid)
[docs]
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]:
wx.CallAfter(self._TestValidity)
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 _TestValidity(self):
'Check validity and change colors accordingly'
try:
if self.Validator:
self.Validator.TestValid(self)
self._IndicateValidity()
except RuntimeError: #bandaid to avoid C++ error; deleted self.Validator?
pass
[docs]
def SaveBackgroundColor(self,color):
'Use this color when valid rather than default'
self.defaultBackgroundColor = color
self._IndicateValidity()
def _IndicateValidity(self):
'Set the control colors to show invalid input'
if self.invalid:
ins = self.GetInsertionPoint()
self.SetForegroundColour("red")
self.SetBackgroundColour("yellow")
#if not sys.platform.startswith("linux"):
# self.SetFocus() # was needed -- now seems to cause problems
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(self.defaultBackgroundColor)
self.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNTEXT))
self.Refresh()
#if not sys.platform.startswith("linux"):
# self.SetFocus() # seems needed, at least on MacOS to get color change
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
[docs]
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._SaveStringValue() # always store the result
self._IndicateValidity()
if self.invalid:
if self.OKcontrol:
self.OKcontrol(False)
elif self.OKcontrol and previousInvalid:
self.OKcontrol(True)
def _GetStringValue(self,event):
'''Get string input and store.
'''
if event: event.Skip() # process keystroke
wx.CallAfter(self._SaveStringValue)
def _SaveStringValue(self):
try:
val = self.GetValue().strip()
except RuntimeError: # ignore if control has been deleted
return
# always store the result
if self.result[self.key] != val:
self.changed = True
self.result[self.key] = val
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
# ignore mouse crusing
if self.result[self.key] == self.GetValue() and not self.changed: # .IsModified() seems unreliable
return
if self.type is not str:
if not self.IsModified() and not self.changed:
return #ignore mouse crusing
wx.CallAfter(self._TestValidity) # entry changed, test/show validity
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)
self.changed = False
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()
# ignore mouse crusing
if self.result[self.key] == self.GetValue(): # .IsModified() seems unreliable
return
if self.type is not str:
if not self.IsModified(): return #ignore mouse crusing
wx.CallAfter(self._TestValidity) # entry changed, test/show validity
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
if not self.invalid: # don't update an invalid expression
self.result[self.key] = self._GetNumValue()
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
################################################################################
[docs]
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 += '?.'
[docs]
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)
[docs]
def TransferToWindow(self):
'Needed by validator, strange, but required component'
return True # Prevent wxDialog from complaining.
[docs]
def TransferFromWindow(self):
'Needed by validator, strange, but required component'
return True # Prevent wxDialog from complaining.
[docs]
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
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:
# ignore difference if due to rounding
if val != float(G2fil.FormatValue(self.xmax,tc.nDig)):
tc.invalid = True
if self.xmin != None:
if val <= self.xmin and self.exclLim[0]:
tc.invalid = True
elif val < self.xmin:
# ignore difference if due to rounding
if val != float(G2fil.FormatValue(self.xmin,tc.nDig)):
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
[docs]
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(tc.defaultBackgroundColor)
tc.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNTEXT))
tc.Refresh()
return True
[docs]
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
################################################################################
[docs]
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)
[docs]
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
[docs]
def TransferToWindow(self):
'Needed by validator, strange, but required component'
return True # Prevent wxDialog from complaining.
[docs]
def TransferFromWindow(self):
'Needed by validator, strange, but required component'
return True # Prevent wxDialog from complaining.
[docs]
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.
'''
self.result[self.key] = tc.GetValue()
[docs]
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
elif chr(key) in self.validchars: # valid char pressed?
if event: event.Skip()
self.TestValid(tc)
return
return # Returning without calling event.Skip, which eats the keystroke
[docs]
class G2Slider(wx.Slider):
'''Wrapper around wx.Slider widget that implements scaling
Also casts floats as integers to avoid py3.10+ errors
'''
global ci
ci = lambda x: int(x + 0.5) # closest integer
def __init__(self, parent, id=wx.ID_ANY, value=0, minValue=0, maxValue=100, *arg, **kwarg):
wx.Slider.__init__(self, parent, id, ci(value), ci(minValue), ci(maxValue), *arg, **kwarg)
self.iscale = 1
def SetScaling(self,iscale):
self.iscale = iscale
def SetScaledRange(self,xmin,xmax):
self.SetRange(ci(xmin*self.iscale),ci(xmax*self.iscale))
[docs]
def SetLineSize(self,value):
wx.Slider.SetLineSize(self,ci(self.iscale*value))
def SetScaledValue(self,value):
wx.Slider.SetValue(self, ci(self.iscale*value))
def GetScaledValue(self):
return self.GetValue()/float(self.iscale)
[docs]
def SetValue(self,value):
wx.Slider.SetValue(self, ci(value))
[docs]
def SetMax(self,xmax):
wx.Slider.SetMax(self,ci(xmax*self.iscale))
[docs]
def SetMin(self,xmin):
wx.Slider.SetMin(self,ci(xmin*self.iscale))
################################################################################
[docs]
def HorizontalLine(sizer,parent):
'''Draws a horizontal line as wide as the window.
'''
if sys.platform == "darwin":
#sizer.Add((-1,2))
line = wx.Panel(parent, size=(-1, 2))
#line.SetBackgroundColour('red')
line.SetBackgroundColour((128,128,128))
sizer.Add(line, 0, wx.EXPAND|wx.ALL, 0)
sizer.Add((-1,5))
else:
line = wx.StaticLine(parent, size=(-1,3), style=wx.LI_HORIZONTAL)
sizer.Add(line, 0, wx.EXPAND|wx.ALL, 5)
################################################################################
################################################################################
[docs]
class EnumSelector(wx.ComboBox):
'''A customized :class:`wxpython.ComboBox` that selects items from a list
of choices, but sets a dict (list) entry to the corresponding
entry from the input list of values.
:param wx.Panel parent: the parent to the :class:`~wxpython.ComboBox` (usually a
frame or panel)
:param dct: a dict or list to contain the value set
for the :class:`~wxpython.ComboBox`.
:param item: the dict key (or list index) where ``dct[item]`` will
be set to the value selected in the :class:`~wxpython.ComboBox`. Also, dct[item]
contains the starting value shown in the widget. If the value
does not match an entry in :data:`values`, the first value
in :data:`choices` is used as the default, but ``dct[item]`` is
not changed.
:param list choices: a list of choices to be displayed to the
user such as
::
["default","option 1","option 2",]
Note that these options will correspond to the entries in
:data:`values` (if specified) item by item.
:param list values: a list of values that correspond to
the options in :data:`choices`, such as
::
[0,1,2]
The default for :data:`values` is to use the same list as
specified for :data:`choices`.
:param function OnChange: an optional routine that will be called
when the
:class:`~wxpython.ComboBox` can be specified.
:param (other): additional keyword arguments accepted by
:class:`~wxpython.ComboBox` can be specified.
'''
def __init__(self,parent,dct,item,choices,values=None,OnChange=None,**kw):
if values is None:
values = choices
if dct[item] in values:
i = values.index(dct[item])
else:
i = 0
startval = choices[i]
wx.ComboBox.__init__(self,parent,wx.ID_ANY,startval,
choices = choices,
style=wx.CB_DROPDOWN|wx.CB_READONLY,
**kw)
self.choices = choices
self.values = values
self.dct = dct
self.item = item
self.OnChange = OnChange
self.Bind(wx.EVT_COMBOBOX, self.onSelection)
def onSelection(self,event):
# respond to a selection by setting the enum value in the CIF dictionary
if self.GetValue() in self.choices: # should always be true!
self.dct[self.item] = self.values[self.choices.index(self.GetValue())]
else:
self.dct[self.item] = self.values[0] # unknown
if self.OnChange: self.OnChange(event)
################################################################################
################################################################################
#### Custom checkbox that saves values into dict/list as used ##############################################################
[docs]
class G2CheckBox(wx.CheckBox):
'''A customized version of a CheckBox that automatically initializes
the control to a supplied list or dict entry and updates that
entry as the widget is used.
:param wx.Panel parent: name of panel or frame that will be
the parent to the widget. Can be None.
:param str label: text to put on check button
:param dict/list loc: the dict or list with the initial value to be
placed in the CheckBox.
:param int/str key: the dict key or the list index for the value to be
edited by the CheckBox. The ``loc[key]`` element must exist.
The CheckBox will be initialized from this value.
If the value is anything other that True (or 1), it will be taken as
False.
:param function OnChange: specifies a function or method that will be
called when the CheckBox is changed (Default is None).
The called function is supplied with one argument, the calling event.
'''
def __init__(self,parent,label,loc,key,OnChange=None):
wx.CheckBox.__init__(self,parent,id=wx.ID_ANY,label=label)
self.loc = loc
self.key = key
self.OnChange = OnChange
self.SetValue(self.loc[self.key]==True)
self.Bind(wx.EVT_CHECKBOX, self._OnCheckBox)
def _OnCheckBox(self,event):
self.loc[self.key] = self.GetValue()
if self.OnChange: self.OnChange(event)
[docs]
def G2CheckBoxFrontLbl(parent,label,loc,key,OnChange=None):
'''A customized version of a CheckBox that automatically initializes
the control to a supplied list or dict entry and updates that
entry as the widget is used. Same as :class:`G2CheckBox` except the
label is placed before the CheckBox and returns a sizer rather than the
G2CheckBox.
If the CheckBox is needed, reference Sizer.myCheckBox.
'''
Sizer = wx.BoxSizer(wx.HORIZONTAL)
Sizer.Add(wx.StaticText(parent,label=label),0,WACV)
checkBox = G2CheckBox(parent,'',loc,key,OnChange)
Sizer.Add(checkBox,0,WACV)
Sizer.myCheckBox = checkBox
return Sizer
#### Commonly used dialogs ################################################################################
################################################################################
############################################### Multichoice Dialog with set all, toggle & filter options
[docs]
class G2MultiChoiceDialog(wx.Dialog):
'''A dialog similar to wx.MultiChoiceDialog except that buttons are
added to set all choices and to toggle all choices and a filter is
available to select from available entries. Note that if multiple
entries are placed in the filter box separated by spaces, all
of the strings must be present for an item to be shown.
:param wx.Frame ParentFrame: reference to parent frame
:param str title: heading above list of choices
:param str header: Title to place on window frame
:param list ChoiceList: a list of choices where one more will be selected
:param bool toggle: If True (default) the toggle and select all buttons
are displayed
:param bool monoFont: If False (default), use a variable-spaced font;
if True use a equally-spaced font.
:param bool filterBox: If True (default) an input widget is placed on
the window and only entries matching the entered text are shown.
:param dict extraOpts: a dict containing a entries of form label_i and value_i with extra
options to present to the user, where value_i is the default value.
Options are listed ordered by the value_i values.
:param list selected: list of indicies for items that should be
:param kw: optional keyword parameters for the wx.Dialog may
be included such as size [which defaults to `(320,310)`] and
style (which defaults to `wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL`);
note that `wx.OK` and `wx.CANCEL` style items control
the presence of the eponymous buttons in the dialog.
:returns: the name of the created dialog
'''
def __init__(self,parent, title, header, ChoiceList, toggle=True,
monoFont=False, filterBox=True, extraOpts={}, selected=[],
**kw):
# process keyword parameters, notably style
options = {'size':(320,310), # default Frame keywords
'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
}
options.update(kw)
self.ChoiceList = ['%4d) %s'%(i,item) for i,item in enumerate(ChoiceList)] # numbered list of choices (list of str values)
self.Selections = len(self.ChoiceList) * [False,] # selection status for each choice (list of bools)
for i in selected:
self.Selections[i] = True
self.filterlist = range(len(self.ChoiceList)) # list of the choice numbers that have been filtered (list of int indices)
self.Stride = 1
if options['style'] & wx.OK:
useOK = True
options['style'] ^= wx.OK
else:
useOK = False
if options['style'] & wx.CANCEL:
useCANCEL = True
options['style'] ^= wx.CANCEL
else:
useCANCEL = False
# create the dialog frame
wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
# fill the dialog
Sizer = wx.BoxSizer(wx.VERTICAL)
topSizer = wx.BoxSizer(wx.HORIZONTAL)
txt = wx.StaticText(self,wx.ID_ANY,title)
#txt.SetMinSize((-1,35))
topSizer.Add(txt,1,wx.ALL|wx.EXPAND,1)
if filterBox:
self.timer = wx.Timer()
self.timer.Bind(wx.EVT_TIMER,self.Filter)
topSizer.Add((10,-1))
topSizer.Add(wx.StaticText(self,wx.ID_ANY,'Name \nFilter: '),0,wx.ALL|WACV,1)
self.filterBox = wx.TextCtrl(self, wx.ID_ANY, size=(80,-1),style=wx.TE_PROCESS_ENTER)
self.filterBox.Bind(wx.EVT_TEXT,self.onChar)
self.filterBox.Bind(wx.EVT_TEXT_ENTER,self.Filter)
topSizer.Add(self.filterBox,0,wx.ALL|WACV,0)
Sizer.Add(topSizer,0,wx.ALL|wx.EXPAND,8)
self.settingRange = False
self.rangeFirst = None
self.clb = wx.CheckListBox(self, wx.ID_ANY, (30,30), wx.DefaultSize, self.ChoiceList)
self._ShowSelections()
self.clb.Bind(wx.EVT_CHECKLISTBOX,self.OnCheck)
if monoFont:
font1 = wx.Font(self.clb.GetFont().GetPointSize(),
wx.MODERN, wx.NORMAL, wx.NORMAL, False)
self.clb.SetFont(font1)
Sizer.Add(self.clb,1,wx.LEFT|wx.RIGHT|wx.EXPAND,10)
Sizer.Add((-1,10))
# set/toggle buttons
if toggle:
tSizer = wx.FlexGridSizer(cols=2,hgap=5,vgap=5)
tSizer.Add(wx.StaticText(self,label=' Apply stride:'),0,WACV)
numbs = [str(i+1) for i in range(9)]+[str(2*i+10) for i in range(6)]
self.stride = wx.ComboBox(self,value='1',choices=numbs,style=wx.CB_READONLY|wx.CB_DROPDOWN)
self.stride.Bind(wx.EVT_COMBOBOX,self.OnStride)
tSizer.Add(self.stride,0,WACV)
setBut = wx.Button(self,wx.ID_ANY,'Set All')
setBut.Bind(wx.EVT_BUTTON,self._SetAll)
tSizer.Add(setBut)
togBut = wx.Button(self,wx.ID_ANY,'Toggle All')
togBut.Bind(wx.EVT_BUTTON,self._ToggleAll)
tSizer.Add(togBut)
self.rangeBut = wx.ToggleButton(self,wx.ID_ANY,'Set Range')
self.rangeBut.Bind(wx.EVT_TOGGLEBUTTON,self.SetRange)
tSizer.Add(self.rangeBut)
self.rangeCapt = wx.StaticText(self,wx.ID_ANY,'')
tSizer.Add(self.rangeCapt)
Sizer.Add(tSizer,0,wx.LEFT,12)
# Extra widgets
Sizer.Add((-1,5),0,wx.LEFT,0)
bSizer = wx.BoxSizer(wx.VERTICAL)
for lbl in sorted(extraOpts.keys()):
if not lbl.startswith('label'): continue
key = lbl.replace('label','value')
if key not in extraOpts: continue
eSizer = wx.BoxSizer(wx.HORIZONTAL)
if type(extraOpts[key]) is bool:
eSizer.Add(G2CheckBox(self,extraOpts[lbl],extraOpts,key))
else:
eSizer.Add(wx.StaticText(self,wx.ID_ANY,extraOpts[lbl]))
eSizer.Add(ValidatedTxtCtrl(self,extraOpts,key))
bSizer.Add(eSizer,0,wx.LEFT,0)
Sizer.Add(bSizer,0,wx.CENTER,0)
Sizer.Add((-1,5),0,wx.LEFT,0)
# OK/Cancel buttons
btnsizer = wx.StdDialogButtonSizer()
if useOK:
self.OKbtn = wx.Button(self, wx.ID_OK)
self.OKbtn.SetDefault()
btnsizer.AddButton(self.OKbtn)
self.OKbtn.Bind(wx.EVT_BUTTON,self.onOk)
if useCANCEL:
btn = wx.Button(self, wx.ID_CANCEL)
btn.Bind(wx.EVT_BUTTON,self.onCancel)
btnsizer.AddButton(btn)
btnsizer.Realize()
Sizer.Add((-1,5))
Sizer.Add(btnsizer,0,wx.ALIGN_RIGHT,50)
Sizer.Add((-1,20))
# OK done, let's get outa here
Sizer.Layout()
self.SetSizer(Sizer)
Sizer.Fit(self)
self.CenterOnParent()
def onOk(self,event):
parent = self.GetParent()
if parent is not None: parent.Raise()
self.EndModal(wx.ID_OK)
def onCancel(self,event):
parent = self.GetParent()
if parent is not None: parent.Raise()
self.EndModal(wx.ID_CANCEL)
def OnStride(self,event):
self.Stride = int(self.stride.GetValue())
[docs]
def SetRange(self,event):
'''Respond to a press of the Set Range button. Set the range flag and
the caption next to the button
'''
self.settingRange = self.rangeBut.GetValue()
if self.settingRange:
self.rangeCapt.SetLabel('Select range start')
else:
self.rangeCapt.SetLabel('')
self.rangeFirst = None
[docs]
def GetSelections(self):
'Returns a list of the indices for the selected choices'
# update self.Selections with settings for displayed items
for i in range(len(self.filterlist)):
self.Selections[self.filterlist[i]] = self.clb.IsChecked(i)
# return all selections, shown or hidden
return [i for i in range(len(self.Selections)) if self.Selections[i]]
[docs]
def SetSelections(self,selList):
'''Sets the selection indices in selList as selected. Resets any previous
selections for compatibility with wx.MultiChoiceDialog. Note that
the state for only the filtered items is shown.
:param list selList: indices of items to be selected. These indices
are referenced to the order in self.ChoiceList
'''
self.Selections = len(self.ChoiceList) * [False,] # reset selections
for sel in selList:
self.Selections[sel] = True
self._ShowSelections()
def _ShowSelections(self):
'Show the selection state for displayed items'
if 'phoenix' in wx.version():
self.clb.SetCheckedItems(
[i for i in range(len(self.filterlist)) if self.Selections[self.filterlist[i]]]
) # Note anything previously checked will be cleared.
else:
self.clb.SetChecked(
[i for i in range(len(self.filterlist)) if self.Selections[self.filterlist[i]]]
) # Note anything previously checked will be cleared.
def _SetAll(self,event):
'Set all viewed choices on'
if 'phoenix' in wx.version():
self.clb.SetCheckedItems(range(0,len(self.filterlist),self.Stride))
else:
self.clb.SetChecked(range(0,len(self.filterlist),self.Stride))
self.stride.SetValue('1')
self.Stride = 1
def _ToggleAll(self,event):
'flip the state of all viewed choices'
for i in range(len(self.filterlist)):
self.clb.Check(i,not self.clb.IsChecked(i))
[docs]
def onChar(self,event):
'Respond to keyboard events in the Filter box'
self.OKbtn.Enable(False)
if self.timer.IsRunning():
self.timer.Stop()
self.timer.Start(1000,oneShot=True)
if event: event.Skip()
[docs]
def OnCheck(self,event):
'''for CheckListBox events; if Set Range is in use, this sets/clears all
entries in range between start and end according to the value in start.
Repeated clicks on the start change the checkbox state, but do not trigger
the range copy.
The caption next to the button is updated on the first button press.
'''
if self.settingRange:
id = event.GetInt()
if self.rangeFirst is None:
name = self.clb.GetString(id)
self.rangeCapt.SetLabel(name+' to...')
self.rangeFirst = id
elif self.rangeFirst == id:
pass
else:
for i in range(min(self.rangeFirst,id), max(self.rangeFirst,id)+1,self.Stride):
self.clb.Check(i,self.clb.IsChecked(self.rangeFirst))
self.rangeBut.SetValue(False)
self.rangeCapt.SetLabel('')
return
[docs]
def Filter(self,event):
'''Read text from filter control and select entries that match. Called by
Timer after a delay with no input or if Enter is pressed.
'''
if self.timer.IsRunning():
self.timer.Stop()
self.GetSelections() # record current selections
txt = self.filterBox.GetValue()
txt = txt.lower()
self.clb.Clear()
self.Update()
self.filterlist = []
if txt:
ChoiceList = []
for i,item in enumerate(self.ChoiceList):
for t in txt.split():
if item.lower().find(t) == -1:
break
else:
ChoiceList.append(item)
self.filterlist.append(i)
else:
self.filterlist = range(len(self.ChoiceList))
ChoiceList = self.ChoiceList
self.clb.AppendItems(ChoiceList)
self._ShowSelections()
self.OKbtn.Enable(True)
############################################### Multichoice in a sizer with set all, toggle & filter options
[docs]
class G2MultiChoiceWindow(wx.BoxSizer):
'''Creates a sizer similar to G2MultiChoiceDialog except that
buttons are added to set all choices and to toggle all choices. This
is placed in a sizer, so that it can be used in a frame or panel.
:param parent: reference to parent frame/panel
:param str title: heading above list of choices
:param list ChoiceList: a list of choices where one more will be selected
:param list SelectList: a list of selected choices
:param bool toggle: If True (default) the toggle and select all buttons
are displayed
:param bool monoFont: If False (default), use a variable-spaced font;
if True use a equally-spaced font.
:param bool filterBox: If True (default) an input widget is placed on
the window and only entries matching the entered text are shown.
:param function OnChange: a reference to a callable object, that
is called each time any a choice is changed. Default is None which
will not be called.
:param list OnChangeArgs: a list of arguments to be supplied to function
OnChange. The default is a null list.
:returns: the name of the created sizer
'''
def __init__(self, parent, title, ChoiceList, SelectList, toggle=True,
monoFont=False, filterBox=True,
OnChange=None, OnChangeArgs=[], helpText=None):
self.SelectList = SelectList
self.ChoiceList = ['%4d) %s'%(i,item) for i,item in enumerate(ChoiceList)] # numbered list of choices (list of str values)
self.frm = parent
self.Selections = len(self.ChoiceList) * [False,] # selection status for each choice (list of bools)
self.filterlist = range(len(self.ChoiceList)) # list of the choice numbers that have been filtered (list of int indices)
self.Stride = 1
self.OnChange = OnChange
self.OnChangeArgs = OnChangeArgs
# fill frame
wx.BoxSizer.__init__(self,wx.VERTICAL)
# fill the sizer
Sizer = self
topSizer = wx.BoxSizer(wx.HORIZONTAL)
topSizer.Add(wx.StaticText(self.frm,wx.ID_ANY,title,size=(-1,35)),0,WACV)
if helpText:
topSizer.Add(HelpButton(self.frm,helpText,wrap=400),0,wx.ALL,5)
topSizer.Add((1,-1),1,wx.ALL|wx.EXPAND,1)
if filterBox:
self.timer = wx.Timer()
self.timer.Bind(wx.EVT_TIMER,self.Filter)
topSizer.Add(wx.StaticText(self.frm,wx.ID_ANY,'Name \nFilter: '),0,wx.ALL|WACV,1)
self.filterBox = wx.TextCtrl(self.frm, wx.ID_ANY, size=(80,-1),style=wx.TE_PROCESS_ENTER)
self.filterBox.Bind(wx.EVT_TEXT,self.onChar)
self.filterBox.Bind(wx.EVT_TEXT_ENTER,self.Filter)
topSizer.Add(self.filterBox,0,wx.ALL|WACV,0)
Sizer.Add(topSizer,0,wx.ALL|wx.EXPAND,8)
self.settingRange = False
self.rangeFirst = None
self.clb = wx.CheckListBox(self.frm, wx.ID_ANY, (30,30), wx.DefaultSize, self.ChoiceList)
self.clb.Bind(wx.EVT_CHECKLISTBOX,self.OnCheck)
if monoFont:
font1 = wx.Font(self.clb.GetFont().GetPointSize(),
wx.MODERN, wx.NORMAL, wx.NORMAL, False)
self.clb.SetFont(font1)
Sizer.Add(self.clb,1,wx.LEFT|wx.RIGHT|wx.EXPAND,10)
Sizer.Add((-1,10))
# set/toggle buttons
if toggle:
tSizer = wx.BoxSizer(wx.HORIZONTAL)
tSizer.Add(wx.StaticText(self.frm,label=' Apply stride:'),0,WACV)
numbs = [str(i+1) for i in range(9)]+[str(2*i+10) for i in range(6)]
self.stride = wx.ComboBox(self.frm,value='1',choices=numbs,style=wx.CB_READONLY|wx.CB_DROPDOWN)
self.stride.Bind(wx.EVT_COMBOBOX,self.OnStride)
tSizer.Add(self.stride,0,WACV)
Sizer.Add(tSizer,0,wx.LEFT,12)
tSizer = wx.BoxSizer(wx.HORIZONTAL)
setBut = wx.Button(self.frm,wx.ID_ANY,'Set All')
setBut.Bind(wx.EVT_BUTTON,self._SetAll)
tSizer.Add(setBut)
togBut = wx.Button(self.frm,wx.ID_ANY,'Toggle All')
togBut.Bind(wx.EVT_BUTTON,self._ToggleAll)
tSizer.Add(togBut)
self.rangeBut = wx.ToggleButton(self.frm,wx.ID_ANY,'Set Range')
self.rangeBut.Bind(wx.EVT_TOGGLEBUTTON,self.SetRange)
tSizer.Add(self.rangeBut)
Sizer.Add(tSizer,0,wx.LEFT,12)
tSizer = wx.BoxSizer(wx.HORIZONTAL)
self.rangeCapt = wx.StaticText(self.frm,wx.ID_ANY,'')
tSizer.Add(self.rangeCapt,1,wx.EXPAND,1)
Sizer.Add(tSizer,0,wx.LEFT,12)
self.SetSelections(self.SelectList)
Sizer.Layout()
def OnStride(self,event):
self.Stride = int(self.stride.GetValue())
[docs]
def SetRange(self,event):
'''Respond to a press of the Set Range button. Set the range flag and
the caption next to the button
'''
self.settingRange = self.rangeBut.GetValue()
if self.settingRange:
self.rangeCapt.SetLabel('Select range start')
else:
self.rangeCapt.SetLabel('')
self.rangeFirst = None
[docs]
def GetSelections(self):
'Returns a list of the indices for the selected choices'
# update self.Selections with settings for displayed items
for i in range(len(self.filterlist)):
self.Selections[self.filterlist[i]] = self.clb.IsChecked(i)
# return all selections, shown or hidden
return [i for i in range(len(self.Selections)) if self.Selections[i]]
[docs]
def SetSelections(self,selList):
'''Sets the selection indices in selList as selected. Resets any previous
selections for compatibility with wx.MultiChoiceDialog. Note that
the state for only the filtered items is shown.
:param list selList: indices of items to be selected. These indices
are referenced to the order in self.ChoiceList
'''
self.Selections = len(self.ChoiceList) * [False,] # reset selections
for sel in selList:
self.Selections[sel] = True
self._ShowSelections()
def _ShowSelections(self):
'Show the selection state for displayed items'
if 'phoenix' in wx.version():
self.clb.SetCheckedItems(
[i for i in range(len(self.filterlist)) if self.Selections[self.filterlist[i]]]
) # Note anything previously checked will be cleared.
else:
self.clb.SetChecked(
[i for i in range(len(self.filterlist)) if self.Selections[self.filterlist[i]]]
) # Note anything previously checked will be cleared.
if self.OnChange:
self.OnChange(self.GetSelections(),*self.OnChangeArgs)
try:
self.SelectList.clear()
except: # patch: clear not in EPD
for i in reversed((range(len(self.SelectList)))):
del self.SelectList[i]
for i,val in enumerate(self.Selections):
if val: self.SelectList.append(i)
def _SetAll(self,event):
'Set all viewed choices on'
if 'phoenix' in wx.version():
self.clb.SetCheckedItems(range(0,len(self.filterlist),self.Stride))
else:
self.clb.SetChecked(range(0,len(self.filterlist),self.Stride))
self.stride.SetValue('1')
self.Stride = 1
self.GetSelections() # record current selections
self._ShowSelections()
def _ToggleAll(self,event):
'flip the state of all viewed choices'
for i in range(len(self.filterlist)):
self.clb.Check(i,not self.clb.IsChecked(i))
self.GetSelections() # record current selections
self._ShowSelections()
[docs]
def onChar(self,event):
'Respond to keyboard events in the Filter box'
if self.timer.IsRunning():
self.timer.Stop()
self.timer.Start(1000,oneShot=True)
if event: event.Skip()
[docs]
def OnCheck(self,event):
'''for CheckListBox events; if Set Range is in use, this sets/clears all
entries in range between start and end according to the value in start.
Repeated clicks on the start change the checkbox state, but do not trigger
the range copy.
The caption next to the button is updated on the first button press.
'''
if self.settingRange:
id = event.GetInt()
if self.rangeFirst is None:
name = self.clb.GetString(id)
self.rangeCapt.SetLabel(name+' to...')
self.rangeFirst = id
elif self.rangeFirst == id:
pass
else:
for i in range(min(self.rangeFirst,id), max(self.rangeFirst,id)+1,self.Stride):
self.clb.Check(i,self.clb.IsChecked(self.rangeFirst))
self.rangeBut.SetValue(False)
self.rangeCapt.SetLabel('')
self.settingRange = False
self.rangeFirst = None
self.GetSelections() # record current selections
self._ShowSelections()
[docs]
def Filter(self,event):
'''Read text from filter control and select entries that match. Called by
Timer after a delay with no input or if Enter is pressed.
'''
if self.timer.IsRunning():
self.timer.Stop()
self.GetSelections() # record current selections
txt = self.filterBox.GetValue()
self.clb.Clear()
self.filterlist = []
if txt:
txt = txt.lower()
ChoiceList = []
for i,item in enumerate(self.ChoiceList):
if item.lower().find(txt) != -1:
ChoiceList.append(item)
self.filterlist.append(i)
else:
self.filterlist = range(len(self.ChoiceList))
ChoiceList = self.ChoiceList
self.clb.AppendItems(ChoiceList)
self._ShowSelections()
[docs]
def SelectSearchVars(G2frame,labelLst,keyDict):
'''Get a sample parameter and a comment label from the user
so we can search for that in the comments.
:returns: Selection, Key, where Selection is the parameter name to
be set (from labelLst) and Key will be a value tag from keyDict or
an equation object (from :func:`GSASIIexprGUI.ExpressionDialog`).
Both will be None if command is cancelled.
'''
def OnChoice(event):
'Respond when a parameter is selected in the Choice box'
key = event.GetEventObject().key
result[key] = event.GetString()
if result.get('Selection') and result.get('Key'):
dlg.EndModal(wx.ID_OK)
def OnFilter(event):
'Set contents of comments entries based on filter'
event.Skip()
wx.CallAfter(DoFiltering) # launch after key press is processed
def DoFiltering():
'perform the filtering'
s = dlg.filterBox.GetValue()
if s:
l = [i for i in keyDict.keys() if s in i]
dlg.lenInfo.SetLabel(f' ({len(l)} filtered comments entries)')
else:
l = list(keyDict.keys())
dlg.lenInfo.SetLabel(f' ({len(l)} comments entries)')
dlg.CommentsCh.SetItems(l)
dlg.CommentsCh.SetSelection(-1) # unselect
def getEquation(event):
'Allow the user to enter or edit an equation'
from . import GSASIIexprGUI as G2exG
result['Key'] = None
expDlg = G2exG.ExpressionDialog(dlg,keyDict,result.get('expression'),
"Enter Equation to set Sample Parameter",
"Sample Parameter Equation", False,
VarLabel=result.get('Selection','TBD'),
wildCard=False)
obj = expDlg.Show(True)
expDlg.Destroy()
if obj:
calcobj = G2obj.ExpressionCalcObj(obj)
result['Key'] = calcobj
result['expression'] = obj
if result.get('Selection'):
dlg.EndModal(wx.ID_OK)
wx.CallAfter(Paint)
def Paint():
'''Draw (or redraw after an equation is entered/edited) the
window contents
'''
mainsizer = dlg.GetSizer()
mainsizer.Clear(True)
mainSizer.Add((5,5))
subSizer = wx.BoxSizer(wx.HORIZONTAL)
subSizer.Add((-1,-1),1,wx.EXPAND)
subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,
'Select a parameter to set from comments'))
subSizer.Add((-1,-1),1,wx.EXPAND)
mainSizer.Add(subSizer,0,wx.EXPAND,0)
mainSizer.Add((0,10))
subSizer = wx.FlexGridSizer(0,2,5,0)
subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Parameter: '),
0,wx.BOTTOM|wx.ALIGN_RIGHT|WACV,10)
ch = wx.Choice(dlg, wx.ID_ANY, choices = sorted(labelLst))
ch.key = 'Selection'
ch.SetSelection(-1)
ch.Bind(wx.EVT_CHOICE, OnChoice)
subSizer.Add(ch,0,WACV)
subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,' Comments entries: '),
0,wx.TOP|WACV,10)
dlg.CommentsCh = wx.Choice(dlg, wx.ID_ANY, choices = list(keyDict.keys()))
dlg.CommentsCh.key = 'Key'
dlg.CommentsCh.SetSelection(-1)
dlg.CommentsCh.Bind(wx.EVT_CHOICE, OnChoice)
subSizer.Add(dlg.CommentsCh,0,WACV)
subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Filter: '),
0,wx.ALL|wx.ALIGN_RIGHT|WACV)
dlg.filterBox = wx.TextCtrl(dlg, wx.ID_ANY, size=(80,-1),style=wx.TE_PROCESS_ENTER)
dlg.filterBox.Bind(wx.EVT_KEY_UP,OnFilter)
dlg.filterBox.Bind(wx.EVT_TEXT_ENTER,OnFilter)
miniSizer = wx.BoxSizer(wx.HORIZONTAL)
miniSizer.Add(dlg.filterBox,0,wx.ALL|WACV)
dlg.lenInfo = wx.StaticText(dlg,wx.ID_ANY,' ()')
miniSizer.Add(dlg.lenInfo,0,wx.ALL|WACV)
subSizer.Add(miniSizer,0,wx.ALL|wx.ALIGN_LEFT|WACV)
subSizer.Add((-1,10))
subSizer.Add((-1,10))
if result.get('expression'):
eq = f"{result.get('Selection','TBD')} = {result['expression'].expression}"
subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Equation: '),
0,wx.ALL|wx.ALIGN_RIGHT|WACV)
subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,eq),
0,wx.ALL|wx.ALIGN_LEFT|WACV)
subSizer.Add((-1,10))
btn = wx.Button(dlg, wx.ID_ANY,'Edit equation')
else:
subSizer.Add((-1,10))
btn = wx.Button(dlg, wx.ID_ANY,'Enter equation')
subSizer.Add(btn,0,wx.ALL|wx.ALIGN_CENTER|WACV)
btn.Bind(wx.EVT_BUTTON,getEquation)
mainSizer.Add(subSizer)
mainSizer.Add((-1,20))
btnsizer = wx.StdDialogButtonSizer()
btn = wx.Button(dlg, wx.ID_CANCEL)
btnsizer.AddButton(btn)
btnsizer.Realize()
mainSizer.Add((-1,5),1,wx.EXPAND,1)
mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER,0)
mainSizer.Add((-1,10))
DoFiltering()
dlg.CommentsCh.Enable(not bool(result.get('expression')))
dlg.filterBox.Enable(not bool(result.get('expression')))
mainSizer.Layout()
mainSizer.Fit(dlg)
dlg.CenterOnParent()
result = {}
dlg = wx.Dialog(G2frame,wx.ID_ANY,'Select a parameter to set',
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
mainSizer = wx.BoxSizer(wx.VERTICAL)
dlg.SetSizer(mainSizer)
Paint()
if dlg.ShowModal() == wx.ID_OK:
dlg.Destroy()
return result.get('Selection'),result.get('Key')
dlg.Destroy()
return None,None
[docs]
def SelectEdit1Var(G2frame,array,labelLst,elemKeysLst,dspLst,refFlgElem):
'''Select a variable from a list, then edit it and select histograms
to copy it to.
:param wx.Frame G2frame: main GSAS-II frame
:param dict array: the array (dict or list) where values to be edited are kept
:param list labelLst: labels for each data item
:param list elemKeysLst: a list of lists of keys needed to be applied (see below)
to obtain the value of each parameter
:param list dspLst: list list of digits to be displayed (10,4) is 10 digits
with 4 decimal places. Can be None.
:param list refFlgElem: a list of lists of keys needed to be applied (see below)
to obtain the refine flag for each parameter or None if the parameter
does not have refine flag.
Example::
array = data
labelLst = ['v1','v2']
elemKeysLst = [['v1'], ['v2',0]]
refFlgElem = [None, ['v2',1]]
* The value for v1 will be in data['v1'] and this cannot be refined while,
* The value for v2 will be in data['v2'][0] and its refinement flag is data['v2'][1]
'''
def unkey(dct,keylist):
'''dive into a nested set of dicts/lists applying keys in keylist
consecutively
'''
d = dct
for k in keylist:
d = d[k]
return d
def OnChoice(event):
'Respond when a parameter is selected in the Choice box'
if 'phoenix' in wx.version():
valSizer.Clear(True)
else:
valSizer.DeleteWindows()
lbl = event.GetString()
copyopts['currentsel'] = lbl
i = labelLst.index(lbl)
OKbtn.Enable(True)
ch.SetLabel(lbl)
args = {}
if dspLst[i]:
args = {'nDig':dspLst[i]}
Val = ValidatedTxtCtrl(
dlg,
unkey(array,elemKeysLst[i][:-1]),
elemKeysLst[i][-1],
**args)
copyopts['startvalue'] = unkey(array,elemKeysLst[i])
#unkey(array,elemKeysLst[i][:-1])[elemKeysLst[i][-1]] =
valSizer.Add(Val,0,wx.LEFT,5)
dlg.SendSizeEvent()
# SelectEdit1Var execution begins here
saveArray = copy.deepcopy(array) # keep original values
TreeItemType = G2frame.GPXtree.GetItemText(G2frame.PickId)
copyopts = {'InTable':False,"startvalue":None,'currentsel':None}
hst = G2frame.GPXtree.GetItemText(G2frame.PatternId)
histList = G2pdG.GetHistsLikeSelected(G2frame)
dlg = wx.Dialog(G2frame,wx.ID_ANY,'Set a parameter value',
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
mainSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add((5,5))
subSizer = wx.BoxSizer(wx.HORIZONTAL)
subSizer.Add((-1,-1),1,wx.EXPAND)
subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Select a parameter and set a new value'))
subSizer.Add((-1,-1),1,wx.EXPAND)
mainSizer.Add(subSizer,0,wx.EXPAND,0)
mainSizer.Add((0,10))
subSizer = wx.FlexGridSizer(0,2,5,0)
subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Parameter: '))
ch = wx.Choice(dlg, wx.ID_ANY, choices = sorted(labelLst))
ch.SetSelection(-1)
ch.Bind(wx.EVT_CHOICE, OnChoice)
subSizer.Add(ch)
subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Value: '))
valSizer = wx.BoxSizer(wx.HORIZONTAL)
subSizer.Add(valSizer)
mainSizer.Add(subSizer)
mainSizer.Add((-1,20))
if histList:
subSizer = wx.BoxSizer(wx.HORIZONTAL)
subSizer.Add(G2CheckBox(dlg, 'Edit in table ', copyopts, 'InTable'))
mainSizer.Add(subSizer)
btnsizer = wx.StdDialogButtonSizer()
OKbtn = wx.Button(dlg, wx.ID_OK,'Continue')
OKbtn.Enable(False)
OKbtn.SetDefault()
OKbtn.Bind(wx.EVT_BUTTON,lambda event: dlg.EndModal(wx.ID_OK))
btnsizer.AddButton(OKbtn)
btn = wx.Button(dlg, wx.ID_CANCEL)
btnsizer.AddButton(btn)
btnsizer.Realize()
mainSizer.Add((-1,5),1,wx.EXPAND,1)
mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER,0)
mainSizer.Add((-1,10))
dlg.SetSizer(mainSizer)
dlg.CenterOnParent()
if dlg.ShowModal() != wx.ID_OK:
array.update(saveArray)
dlg.Destroy()
return
dlg.Destroy()
copyList = []
lbl = copyopts['currentsel']
ttl = 'Select histograms'
if copyopts['InTable']:
msg = 'Select histograms to include in table'
else:
msg = f'Select hists to copy parameter {lbl} to.\nFine w/no selections. Cancel aborts change.'
if histList:
dlg = G2MultiChoiceDialog(G2frame,msg,ttl, histList)
dlg.CenterOnParent()
try:
if dlg.ShowModal() == wx.ID_OK:
for i in dlg.GetSelections():
copyList.append(histList[i])
else:
# reset the parameter since cancel was pressed
array.update(saveArray)
return
finally:
dlg.Destroy()
prelbl = [hst]
i = labelLst.index(lbl)
keyLst = elemKeysLst[i]
refkeys = refFlgElem[i]
dictlst = [unkey(array,keyLst[:-1])]
if refkeys is not None:
refdictlst = [unkey(array,refkeys[:-1])]
else:
refdictlst = None
Id = G2gd.GetGPXtreeItemId(G2frame,G2frame.root,hst)
hstData = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,Id,'Instrument Parameters'))[0]
for h in copyList:
Id = G2gd.GetGPXtreeItemId(G2frame,G2frame.root,h)
instData = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,Id,'Instrument Parameters'))[0]
if len(hstData) != len(instData) or hstData['Type'][0] != instData['Type'][0]: #don't mix data types or lam & lam1/lam2 parms!
print (h+' not copied - instrument parameters not commensurate')
continue
hData = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,Id,TreeItemType))
if TreeItemType == 'Instrument Parameters':
hData = hData[0]
#copy the value if it is changed or we will not edit in a table
valNow = unkey(array,keyLst)
if copyopts['startvalue'] != valNow or not copyopts['InTable']:
unkey(hData,keyLst[:-1])[keyLst[-1]] = valNow
prelbl += [h]
dictlst += [unkey(hData,keyLst[:-1])]
if refdictlst is not None:
refdictlst += [unkey(hData,refkeys[:-1])]
if refdictlst is None:
args = {}
else:
args = {'checkdictlst':refdictlst,
'checkelemlst':len(dictlst)*[refkeys[-1]],
'checklabel':'Refine?'}
if copyopts['InTable']:
dlg = ScrolledMultiEditor(
G2frame,dictlst,
len(dictlst)*[keyLst[-1]],prelbl,
header='Editing parameter '+lbl,
CopyButton=True,**args)
dlg.CenterOnParent()
if dlg.ShowModal() != wx.ID_OK:
array.update(saveArray)
dlg.Destroy()
##### Single choice Dialog with filter options ###############################################################
[docs]
class G2SingleChoiceDialog(wx.Dialog):
'''A dialog similar to wx.SingleChoiceDialog except that a filter can be
added.
:param wx.Frame ParentFrame: reference to parent frame
:param str title: heading above list of choices
:param str header: Title to place on window frame
:param list ChoiceList: a list of choices where one will be selected
:param bool monoFont: If False (default), use a variable-spaced font;
if True use a equally-spaced font.
:param bool filterBox: If True (default) an input widget is placed on
the window and only entries matching the entered text are shown.
:param kw: optional keyword parameters for the wx.Dialog may
be included such as size [which defaults to `(320,310)`] and
style (which defaults to ``wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.CENTRE | wx.OK | wx.CANCEL``);
note that ``wx.OK`` and ``wx.CANCEL`` controls
the presence of the eponymous buttons in the dialog.
:returns: the name of the created dialog
Example::
dlg = G2SingleChoiceDialog(G2frame,'Select option from list',
'Select option',optList)
dlg.CenterOnParent()
try:
if dlg.ShowModal() == wx.ID_OK:
sel = optList[dlg.GetSelection()]
else:
return
finally:
dlg.Destroy()
'''
def __init__(self,parent, title, header, ChoiceList,
monoFont=False, filterBox=True, **kw):
# process keyword parameters, notably style
options = {'size':(320,310), # default Frame keywords
'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
}
options.update(kw)
self.ChoiceList = ChoiceList
self.filterlist = range(len(self.ChoiceList))
if options['style'] & wx.OK:
useOK = True
options['style'] ^= wx.OK
else:
useOK = False
if options['style'] & wx.CANCEL:
useCANCEL = True
options['style'] ^= wx.CANCEL
else:
useCANCEL = False
# create the dialog frame
wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
# fill the dialog
Sizer = wx.BoxSizer(wx.VERTICAL)
topSizer = wx.BoxSizer(wx.HORIZONTAL)
h = max(35,17*int(len(title)/26.+1)) # adjust height of title box with guessed # of lines
topSizer.Add(wx.StaticText(self,wx.ID_ANY,title,size=(-1,h)),
1,wx.ALL|wx.EXPAND,1)
if filterBox:
self.timer = wx.Timer()
self.timer.Bind(wx.EVT_TIMER,self.Filter)
topSizer.Add(wx.StaticText(self,wx.ID_ANY,'Filter: '),0,wx.ALL,1)
self.filterBox = wx.TextCtrl(self, wx.ID_ANY, size=(80,-1),style=wx.TE_PROCESS_ENTER)
self.filterBox.Bind(wx.EVT_CHAR,self.onChar)
self.filterBox.Bind(wx.EVT_TEXT_ENTER,self.Filter)
topSizer.Add(self.filterBox,0,wx.ALL,0)
Sizer.Add(topSizer,0,wx.ALL|wx.EXPAND,8)
self.clb = wx.ListBox(self, wx.ID_ANY, (30,30), wx.DefaultSize, ChoiceList)
self.clb.Bind(wx.EVT_LEFT_DCLICK,self.onDoubleClick)
if monoFont:
font1 = wx.Font(self.clb.GetFont().GetPointSize(),wx.MODERN, wx.NORMAL, wx.NORMAL, False)
self.clb.SetFont(font1)
Sizer.Add(self.clb,1,wx.LEFT|wx.RIGHT|wx.EXPAND,10)
Sizer.Add((-1,10))
# OK/Cancel buttons
btnsizer = wx.StdDialogButtonSizer()
if useOK:
self.OKbtn = wx.Button(self, wx.ID_OK)
self.OKbtn.SetDefault()
btnsizer.AddButton(self.OKbtn)
if useCANCEL:
btn = wx.Button(self, wx.ID_CANCEL)
btnsizer.AddButton(btn)
btnsizer.Realize()
Sizer.Add((-1,5))
Sizer.Add(btnsizer,0,wx.ALIGN_RIGHT,50)
Sizer.Add((-1,20))
# OK done, let's get outa here
self.SetSizer(Sizer)
[docs]
def GetSelection(self):
'Returns the index of the selected choice'
i = self.clb.GetSelection()
if i < 0 or i >= len(self.filterlist):
return wx.NOT_FOUND
return self.filterlist[i]
def onChar(self,event):
self.OKbtn.Enable(False)
if self.timer.IsRunning():
self.timer.Stop()
self.timer.Start(1000,oneShot=True)
if event: event.Skip()
def Filter(self,event):
if self.timer.IsRunning():
self.timer.Stop()
txt = self.filterBox.GetValue()
self.clb.Clear()
self.Update()
self.filterlist = []
if txt:
txt = txt.lower()
ChoiceList = []
for i,item in enumerate(self.ChoiceList):
if item.lower().find(txt) != -1:
ChoiceList.append(item)
self.filterlist.append(i)
else:
self.filterlist = range(len(self.ChoiceList))
ChoiceList = self.ChoiceList
self.clb.AppendItems(ChoiceList)
self.OKbtn.Enable(True)
def onDoubleClick(self,event):
self.EndModal(wx.ID_OK)
################################################################################
[docs]
class FlagSetDialog(wx.Dialog):
''' Creates popup with table of variables to be checked for e.g. refinement flags
'''
def __init__(self,parent,title,colnames,rownames,flags):
wx.Dialog.__init__(self,parent,-1,title,
pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
self.panel = None
self.colnames = colnames
self.rownames = rownames
self.flags = flags
self.newflags = copy.copy(flags)
self.Draw()
def Draw(self):
Indx = {}
def OnSelection(event):
Obj = event.GetEventObject()
[name,ia] = Indx[Obj.GetId()]
self.newflags[name][ia] = Obj.GetValue()
if self.panel:
self.panel.DestroyChildren() #safe: wx.Panel
self.panel.Destroy()
self.panel = wx.Panel(self)
mainSizer = wx.BoxSizer(wx.VERTICAL)
flagSizer = wx.FlexGridSizer(0,len(self.colnames),5,5)
for item in self.colnames:
flagSizer.Add(wx.StaticText(self.panel,label=item),0,WACV)
for ia,atm in enumerate(self.rownames):
flagSizer.Add(wx.StaticText(self.panel,label=atm),0,WACV)
for name in self.colnames[1:]:
if self.flags[name][ia]:
self.newflags[name][ia] = False #default is off
flg = wx.CheckBox(self.panel,-1,label='')
flg.Bind(wx.EVT_CHECKBOX,OnSelection)
Indx[flg.GetId()] = [name,ia]
flagSizer.Add(flg,0,WACV)
else:
flagSizer.Add(wx.StaticText(self.panel,label='na'),0,WACV)
mainSizer.Add(flagSizer,0)
OkBtn = wx.Button(self.panel,-1,"Ok")
OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
CancelBtn = wx.Button(self.panel,-1,'Cancel')
CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
btnSizer.Add((20,20),1)
btnSizer.Add(OkBtn)
btnSizer.Add(CancelBtn)
btnSizer.Add((20,20),1)
mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
self.panel.SetSizer(mainSizer)
self.panel.Fit()
self.Fit()
def GetSelection(self):
return self.newflags
def OnOk(self,event):
parent = self.GetParent()
if parent is not None: parent.Raise()
self.EndModal(wx.ID_OK)
def OnCancel(self,event):
parent = self.GetParent()
if parent is not None: parent.Raise()
self.EndModal(wx.ID_CANCEL)
################################################################################
[docs]
def G2MessageBox(parent,msg,title='Error'):
'''Simple code to display a error or warning message
TODO: replace wx.MessageDialog with one derived from wx.Dialog because
on most platforms wx.MessageDialog is a native widget and CentreOnParent
will not function.
'''
dlg = wx.MessageDialog(parent,StripIndents(msg), title, wx.OK|wx.CENTRE)
dlg.CentreOnParent()
dlg.ShowModal()
dlg.Destroy()
################################################################################
[docs]
def findValsInNotebook(data,target):
'Pull a string of values from saved values in the GSAS-II notebook'
c = 0
vals = []
pos = []
for l in data:
if '[REF]' in l: c += 1
if '[VALS]' in l:
v = {i.split(' : ')[0].strip() : i.split(' : ')[1]
for i in l[6:].split(',')}
if target not in v:
continue
try:
vals.append(float(v[target]))
pos.append(c)
except:
pass
if '[CEL]' in l:
ph = l.split('Phase')[1].split()[0]
hst = l.split('Hist')[1].split(':')[0].strip()
vars = [i.split('=')[0] for i in l.split(':')[1].split()]
vars = [f'{v}[p{ph}_h{hst}]' for v in vars]
if target not in vars: continue
values = [i.split('=')[1].split('(')[0] for i in l.split(':')[1].split()]
try:
vals.append(float(dict(zip(vars,values))[target]))
pos.append(c)
except:
pass
return pos,vals
[docs]
def G2AfterFit(parent,msg,title='Error',vartbl=[],txtwidth=300):
'''Shows the results from a refinement
:param wx.Frame parent: pointer to parent of window, usually G2frame
:param str msg: text from refinement results
:param str title: text to label window
:param list vartbl: a list of lists. The contents of each inner list
item will be [var-name, val-before, val-after, sigma, meaning]
:param int txtwidth: width (in pixesl) to display msg. Defaults to 300
'''
def OnRowSelected(event):
'''plot the parameter results if it has been recorded
this is called when one clicks on a row in the parameter table
'''
row = event.GetIndex()
var = results.list.GetItemText(row)
val = valDict.get(var)
G2frame = wx.App.GetMainTopWindow()
G2frame.G2plotNB.Delete('fit results')
if val is None: return
nId = G2gd.GetGPXtreeItemId(G2frame,G2frame.root, 'Notebook')
if not nId: return
Notebook = G2frame.GPXtree.GetItemPyData(nId)
pos,vals = findValsInNotebook(Notebook,var)
if len(pos) < 2: return
pos.append(pos[-1]+1)
try:
vals.append(float(val))
except:
return
XY = [np.array(pos),np.array(vals)]
from . import GSASIIplot as G2plt
G2plt.PlotXY(G2frame,[XY,],Title='fit results',newPlot=True,
labelX='seq',labelY=var,lines=True)
# create table to show from input
displayTable = []
valDict = {}
for (var,before,after,sig,what) in vartbl:
valDict[var] = after # save lookup table of values
try:
d = f'{(after-before)/sig:.3g}'
b = G2mth.ValEsd(before,-abs(sig)/10,True)
a = G2mth.ValEsd(after,-abs(sig)/10,True)
except:
d = '0'
b = f'{before:.6g}'
a = f'{after:.6g}'
displayTable.append((var,b,a,d,what))
labels = ('var','before','after','del/sig','parameter description')
just = (0 , 1 , 1 , 1 , 0) # 0 left, 1 right
dlg = wx.Dialog(parent.GetTopLevelParent(), wx.ID_ANY, title,
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
mainSizer = wx.BoxSizer(wx.VERTICAL)
txtSizer = wx.BoxSizer(wx.HORIZONTAL)
txt = wx.StaticText(dlg,wx.ID_ANY,msg)
txt.Wrap(txtwidth-20)
#txt.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
txtSizer.Add(txt,0,wx.RIGHT|wx.LEFT,5)
if displayTable:
results = SortableLstCtrl(dlg)
results.PopulateHeader(labels, just)
for i,l in enumerate(displayTable): results.PopulateLine(i,l)
# set widths to automatic
results.SetColWidth(0)
results.SetColWidth(1,sortType='float')
results.SetColWidth(2,sortType='float')
results.SetColWidth(3,sortType='abs')
results.SetColWidth(4)
results.SetInitialSortColumn(3,False)
results.SetClientSize((450,250))
results.SetMinSize((450,250))
results.Bind(wx.EVT_LIST_ITEM_SELECTED, OnRowSelected) # plot
else:
results = wx.BoxSizer(wx.VERTICAL)
results.Add((-1,-1),1,wx.EXPAND)
results.Add(wx.StaticText(dlg,wx.ID_ANY,
' (no parameter changes\nto display) ',
size=(350,-1),style=wx.ALIGN_CENTER))
results.Add((-1,-1),1,wx.EXPAND)
txtSizer.Add(results,1,wx.EXPAND,0)
mainSizer.Add(txtSizer,1,wx.EXPAND)
mainSizer.Add((-1,5))
txt = wx.StaticText(dlg,wx.ID_ANY,'Load new result?')
mainSizer.Add(txt,0,wx.CENTER)
btnsizer = wx.BoxSizer(wx.HORIZONTAL)
btn = wx.Button(dlg, wx.ID_CANCEL)
btn.Bind(wx.EVT_BUTTON,lambda event: dlg.EndModal(wx.ID_CANCEL))
btnsizer.Add(btn)
btn = wx.Button(dlg, wx.ID_OK)
btn.SetDefault()
btn.Bind(wx.EVT_BUTTON,lambda event: dlg.EndModal(wx.ID_OK))
btnsizer.Add(btn)
mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
dlg.SetSizer(mainSizer)
mainSizer.Fit(dlg)
dlg.Layout()
dlg.CentreOnParent()
ans = dlg.ShowModal()
dlg.Destroy()
return ans
[docs]
def ShowScrolledColText(parent,txt,width=600,height=400,header='Warning info',col1len=999):
'''Simple code to display tabular information in a scrolled wx.Dialog
window.
Lines ending with a colon (:) are centered across all columns
and have a grey background.
Lines beginning and ending with '**' are also are centered
across all columns and are given a yellow background
All other lines have columns split by tab (\\t) characters.
:param wx.Frame parent: parent window
:param str txt: text to be displayed
:param int width: lateral of window in pixels (defaults to 600)
:param int height: vertical dimension of window in pixels (defaults to 400)
:param str header: title to be placed on window
'''
dlg = wx.Dialog(parent.GetTopLevelParent(),wx.ID_ANY,header, style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
spanel = wxscroll.ScrolledPanel(dlg, wx.ID_ANY, size=(width-20, height))
spanel.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
mainSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add(spanel,1,wx.ALL|wx.EXPAND,1)
cols = 1
for i,line in enumerate(txt.split('\n')):
cols = max(cols,line.count('\t')+1)
txtSizer = wx.GridBagSizer(0,9)
for i,line in enumerate(txt.split('\n')):
if line.strip().endswith(':'):
st = wx.StaticText(spanel,wx.ID_ANY,line)
txtSizer.Add(st,pos=(i,0),span=(0,cols),flag=wx.EXPAND)
st.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
continue
elif line.strip().startswith('**') and line.strip().endswith('**'):
st = wx.StaticText(spanel,wx.ID_ANY,line,style=wx.ALIGN_CENTER)
st.SetBackgroundColour(DULL_YELLOW)
txtSizer.Add(st,pos=(i,0),span=(0,cols),flag=wx.EXPAND)
continue
items = line.split('\t')
for col in range(cols):
if col < len(items):
item = items[col].strip()
else:
item = ''
t = item[:]
s = ''
#if len(t) > col1len: GSASIIpath.IPyBreak()
while col == 0 and len(t) > col1len:
b = -1
for sym in (') ',' * ',' + ',' - ',' && '):
b = max(b, t.rfind(sym,0,col1len))
if b > 20:
s += t[:b+1]
t = '\n\t' + t[b+1:]
continue
break
s += t
st = wx.StaticText(spanel,wx.ID_ANY,s)
if col == 0: st.Wrap(650) # last resort...
st.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
txtSizer.Add(st,pos=(i,col),flag=wx.EXPAND)
#txtSizer.AddGrowableRow(i)
txtSizer.AddGrowableCol(0) #to fill screen
spanel.SetSizer(txtSizer)
btnsizer = wx.BoxSizer(wx.HORIZONTAL)
btn = wx.Button(dlg, wx.ID_CLOSE)
btn.Bind(wx.EVT_BUTTON,lambda event: dlg.EndModal(wx.ID_CANCEL))
btnsizer.Add(btn)
mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
dlg.SetSizer(mainSizer)
mainSizer.Fit(dlg)
spanel.SetAutoLayout(1)
spanel.SetupScrolling()
#dlg.SetMaxSize((-1,400))
dlg.CenterOnParent()
dlg.ShowModal()
dlg.Destroy()
################################################################################
[docs]
class PickTwoDialog(wx.Dialog):
'''This does not seem to be in use
'''
def __init__(self,parent,title,prompt,names,choices):
wx.Dialog.__init__(self,parent,-1,title,
pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
self.panel = None
self.prompt = prompt
self.choices = choices
self.names = names
self.Draw()
def Draw(self):
Indx = {}
def OnSelection(event):
Obj = event.GetEventObject()
id = Indx[Obj.GetId()]
self.choices[id] = Obj.GetValue().encode() #to avoid Unicode versions
self.Draw()
if self.panel:
self.panel.DestroyChildren() #safe: wx.Panel
self.panel.Destroy()
self.panel = wx.Panel(self)
mainSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add(wx.StaticText(self.panel,-1,self.prompt),0,wx.ALIGN_CENTER)
for isel,name in enumerate(self.choices):
lineSizer = wx.BoxSizer(wx.HORIZONTAL)
lineSizer.Add(wx.StaticText(self.panel,-1,'Reference atom '+str(isel+1)),0,wx.ALIGN_CENTER)
nameList = self.names[:]
if isel:
if self.choices[0] in nameList:
nameList.remove(self.choices[0])
choice = wx.ComboBox(self.panel,-1,value=name,choices=nameList,
style=wx.CB_READONLY|wx.CB_DROPDOWN)
Indx[choice.GetId()] = isel
choice.Bind(wx.EVT_COMBOBOX, OnSelection)
lineSizer.Add(choice,0,WACV)
mainSizer.Add(lineSizer)
OkBtn = wx.Button(self.panel,-1,"Ok")
OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
CancelBtn = wx.Button(self.panel,-1,'Cancel')
CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
btnSizer.Add((20,20),1)
btnSizer.Add(OkBtn)
btnSizer.Add(CancelBtn)
btnSizer.Add((20,20),1)
mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
self.panel.SetSizer(mainSizer)
self.panel.Fit()
self.Fit()
def GetSelection(self):
return self.choices
def OnOk(self,event):
parent = self.GetParent()
if parent is not None: parent.Raise()
self.EndModal(wx.ID_OK)
def OnCancel(self,event):
parent = self.GetParent()
if parent is not None: parent.Raise()
self.EndModal(wx.ID_CANCEL)
################################################################################
[docs]
class SingleFloatDialog(wx.Dialog):
'''Dialog to obtain a single float value from user
:param wx.Frame parent: name of parent frame
:param str title: title string for dialog
:param str prompt: string to tell user what they are inputing
:param str value: default input value, if any
:param list limits: upper and lower value used to set bounds for entry, use [None,None]
for no bounds checking, [None,val] for only upper bounds, etc. Default is [0,1].
Values outside of limits will be ignored.
:param str format: string to format numbers. Defaults to '%.5g'. Use '%d' to have
integer input (but dlg.GetValue will still return a float).
Typical usage::
limits = (0,1)
dlg = G2G.SingleFloatDialog(G2frame,'New value','Enter new value for...',default,limits)
if dlg.ShowModal() == wx.ID_OK:
parm = dlg.GetValue()
dlg.Destroy()
'''
def __init__(self,parent,title,prompt,value,limits=[0.,1.],fmt='%.5g'):
wx.Dialog.__init__(self,parent,-1,title,
pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
self.CenterOnParent()
mainSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add(wx.StaticText(self,-1,prompt),0,wx.ALIGN_CENTER)
#valItem = wx.TextCtrl(self,-1,value=self.format%(self.value),style=wx.TE_PROCESS_ENTER)
self.buffer = {0:float(fmt%(value))}
a,b = fmt[1:].split('.')
f = b[-1]
try:
d = int(b[:-1])
except:
d = 5
try:
w = int(a)
except:
w = 5+d
self.OKbtn = wx.Button(self,wx.ID_OK)
CancelBtn = wx.Button(self,wx.ID_CANCEL)
valItem = ValidatedTxtCtrl(self,self.buffer,0,nDig=(w,d,f),
xmin=limits[0],xmax=limits[1],
OKcontrol=self.ControlOKButton)
mainSizer.Add(valItem,0,wx.ALIGN_CENTER)
self.OKbtn.Bind(wx.EVT_BUTTON, self.OnOk)
CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
btnSizer.Add((20,20),1)
btnSizer.Add(self.OKbtn)
btnSizer.Add(CancelBtn)
btnSizer.Add((20,20),1)
mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
self.SetSizer(mainSizer)
self.Fit()
def GetValue(self):
return self.buffer[0]
def OnOk(self,event):
parent = self.GetParent()
if parent is not None: parent.Raise()
self.EndModal(wx.ID_OK)
def OnCancel(self,event):
parent = self.GetParent()
if parent is not None: parent.Raise()
self.EndModal(wx.ID_CANCEL)
[docs]
class SingleIntDialog(SingleFloatDialog):
'''Dialog to obtain a single int value from user
:param wx.Frame parent: name of parent frame
:param str title: title string for dialog
:param str prompt: string to tell user what they are inputing
:param str value: default input value, if any
:param list limits: upper and lower value used to set bounds for entries. Default
is [None,None] -- for no bounds checking; use [None,val] for only upper bounds, etc.
Default is [0,1]. Values outside of limits will be ignored.
Typical usage::
limits = (0,None) # allows zero or positive values only
dlg = G2G.SingleIntDialog(G2frame,'New value','Enter new value for...',default,limits)
if dlg.ShowModal() == wx.ID_OK:
parm = dlg.GetValue()
dlg.Destroy()
'''
def __init__(self,parent,title,prompt,value,limits=[None,None]):
SingleFloatDialog.__init__(self,parent,title,prompt,value,limits=limits,format='%d')
def GetValue(self):
return int(self.value)
################################################################################
[docs]
class MultiDataDialog(wx.Dialog):
'''Dialog to obtain multiple values from user. Use ``dlg.GetValues()`` to
get the values set in the window.
:param wx.Frame parent: parent frame for dialog to be created
:param str title: title to place on top of window
:param list prompts: a string to describe each item. Each entry
in this list will designate a row in the generated window.
:param list values: a list of initial values for each item. Use a
nested list when multiple entries are placed on a single row of
the window (see discussion of formats, below). Number of items
in the outer list should match the length of ``prompts``. The
total number of items should match ``formats``.
:param list limits: A nested list with an upper and lower value
for each item or for a choice/edit control a list of allowed
values. Use a nested list when multiple entries are placed on
a single row of the window (see discussion of formats, below).
Number of items in the outer list should match the length of
``prompts``. The total number of items should match ``formats``.
:param list testfxns: A nested list of string test functions.
The total number of items should match ``formats`` or should be
left as the default (None).
:param list formats: A list of values for each entry in the
window. Several different types of values are possible:
* An "old-style" format string (e.g. ``%5d`` or ``%.3f``)
which will be used to display each item's value
* Or a keyword that specifies how the values are used.
Allowed keywords are:
* ``choice``: for a pull-down list;
* ``bool``: for a yes/no checkbox;
* ``str``: for a text entry
* ``edit``: for a pull-down list that allows one to enter an arbitrary value.
* Alternately, a value can be a list of items, in which case multiple
entries are placed on a single row of the window. When this is done,
any value in the list other than ``choice`` or ``edit`` is used as
text to be placed between the ComboBoxes.
The number of items in the outer list should match the length
of ``prompts``.
:param str header: a string to be placed at the top of the
window. Ignored if None (the default.)
Example 1::
dlg = G2G.MultiDataDialog(G2frame,title='ISOCIF search',
prompts=['lattice constants tolerance',
'coordinate tolerance',
'occupancy tolerance'],
values=[0.001,0.01,0.1],
limits=3*[[0.,2.]],formats=3*['%.5g'],
header=isoCite)
dlg.ShowModal()
latTol,coordTol,occTol = dlg.GetValues()
dlg.Destroy()
Example 2::
nm = [' ','0','1','-1','2','-2','3','-3','4','5','6','7','8','9']
dm = ['1','2','3','4','5','6']
kfmt = ['choice','/','choice',', ','choice','/','choice',', ','choice','/','choice',' ']
dlg = MultiDataDialog(G2frame,title='options',
prompts=[' k-vector 1 (x,y,z)',
' k-vector 2 (x,y,z)'],
values=[3*['0','','2',''],3*[' ','','2','']],
limits=[3*[nm[1:],'',dm,''],3*[nm,'',dm,'']],
formats=[kfmt,kfmt])
if dlg.ShowModal() == wx.ID_OK: print(dlg.GetValues())
'''
def __init__(self,parent,title,prompts,values,limits=[[0.,1.],],
testfxns=None,formats=['%.5g',],header=None):
wx.Dialog.__init__(self,parent,-1,title,
pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
self.panel = None
self.limits = limits
self.values = values
self.prompts = prompts
self.formats = formats
if testfxns is None:
self.testfxns = []
for i in formats:
if type(i) is list:
self.testfxns.append(len(i)*[None])
else:
self.testfxns.append(None)
else:
self.testfxns = testfxns
self.header = header
self.Draw()
def Draw(self):
def OnEditItem(event):
if event: event.Skip()
Obj = event.GetEventObject()
fmt = Indx[Obj][-1]
if type(fmt) is list:
tid,idl,limits = Indx[Obj][:3]
else:
tid,idl = Indx[Obj],0
val = Obj.GetValue()
try:
eval(val)
self.values[tid][idl] = val
return
except:
pass
try: # deal with mixed fractions (example: 1 3/4)
val = val.replace(' ','+')
val = str(eval(val))
self.values[tid][idl] = val
return
except:
pass
def OnValItem(event):
if event: event.Skip()
Obj = event.GetEventObject()
fmt = Indx[Obj][-1]
if type(fmt) is list:
tid,idl,limits = Indx[Obj][:3]
if 'testfxn' in fmt:
testfxn = Indx[Obj][3][tid]
val = Obj.GetValue().strip()
if testfxn(val):
self.values[tid][idl] = val
Obj.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
Obj.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNTEXT))
else:
Obj.SetBackgroundColour(wx.YELLOW)
Obj.SetForegroundColour("red")
Obj.SetValue('%s'%(val))
self.values[tid][idl] = Obj.GetValue()
elif 'bool' in fmt:
self.values[Indx[Obj][0]] = Obj.GetValue()
elif 'str' in fmt:
tid,limits = Indx[Obj][:2]
try:
val = Obj.GetValue()
if val not in limits:
raise ValueError
except ValueError:
val = self.values[tid]
self.values[tid] = val
Obj.SetValue('%s'%(val))
elif 'testfxn' in fmt:
tid,x,testfxn = Indx[Obj][:3]
val = Obj.GetValue()
if testfxn(val):
self.values[tid] = val
Obj.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
Obj.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNTEXT))
else:
Obj.SetBackgroundColour(wx.YELLOW)
Obj.SetForegroundColour("red")
Obj.SetValue('%s'%(val))
elif 'choice' in fmt:
self.values[Indx[Obj][0]] = Obj.GetValue()
Indx = {}
if self.panel: self.panel.Destroy()
self.panel = wx.Panel(self)
mainSizer = wx.BoxSizer(wx.VERTICAL)
if self.header:
txt = wx.StaticText(self.panel,wx.ID_ANY,self.header)
txt.Wrap(400)
mainSizer.Add(txt)
mainSizer.Add((-1,5))
HorizontalLine(mainSizer,self.panel)
mainSizer.Add((-1,5))
lineSizer = wx.FlexGridSizer(0,2,5,5)
for tid,[prompt,value,limits,testfxn,fmt] in enumerate(zip(self.prompts,self.values,self.limits,self.testfxns,self.formats)):
lineSizer.Add(wx.StaticText(self.panel,label=prompt),0,wx.ALIGN_CENTER)
if type(fmt) is list:
valItem = wx.BoxSizer(wx.HORIZONTAL)
for idl,item in enumerate(fmt):
if value[idl] in limits[idl]:
initVal = value[idl]
else:
initVal = limits[idl][0]
if 'edit' in item:
style = wx.CB_DROPDOWN
listItem = wx.ComboBox(self.panel,value=initVal,
choices=limits[idl],style=style)
Indx[listItem] = [tid,idl,limits,testfxn,fmt]
listItem.Bind(wx.EVT_TEXT,OnEditItem)
listItem.Bind(wx.EVT_COMBOBOX,OnValItem)
elif 'testfxn' in item:
listItem = wx.TextCtrl(self.panel,value='%s'%(value),style=wx.TE_PROCESS_ENTER)
Indx[listItem] = [tid,idl,limits,testfxn,fmt]
listItem.Bind(wx.EVT_TEXT_ENTER,OnValItem)
listItem.Bind(wx.EVT_KILL_FOCUS,OnValItem)
listItem.SetValue('%s'%value[idl])
elif 'choice' in item:
style = wx.CB_READONLY|wx.CB_DROPDOWN
listItem = wx.ComboBox(self.panel,value=initVal,
choices=limits[idl],style=style)
Indx[listItem] = [tid,idl,limits,testfxn,fmt]
listItem.Bind(wx.EVT_COMBOBOX,OnValItem)
else:
listItem = wx.StaticText(self.panel,wx.ID_ANY,item)
valItem.Add(listItem,0,WACV)
elif 'bool' in fmt:
valItem = wx.CheckBox(self.panel,label='')
valItem.Bind(wx.EVT_CHECKBOX,OnValItem)
valItem.SetValue(value)
elif 'str' in fmt:
valItem = wx.TextCtrl(self.panel,value='%s'%(value),style=wx.TE_PROCESS_ENTER)
valItem.Bind(wx.EVT_TEXT_ENTER,OnValItem)
valItem.Bind(wx.EVT_KILL_FOCUS,OnValItem)
valItem.SetValue('%s'%value)
elif 'edit' in fmt:
valItem = wx.ComboBox(self.panel,value=limits[0],choices=limits,style=wx.CB_DROPDOWN)
valItem.Bind(wx.EVT_COMBOBOX,OnValItem)
valItem.Bind(wx.EVT_TEXT,OnEditItem)
elif 'choice' in fmt:
valItem = wx.ComboBox(self.panel,value=limits[0],choices=limits,style=wx.CB_READONLY|wx.CB_DROPDOWN)
valItem.Bind(wx.EVT_COMBOBOX,OnValItem)
elif 'testfxn' in fmt:
valItem = wx.TextCtrl(self.panel,value='%s'%(value),style=wx.TE_PROCESS_ENTER)
valItem.Bind(wx.EVT_TEXT_ENTER,OnValItem)
valItem.Bind(wx.EVT_KILL_FOCUS,OnValItem)
else:
if '%' in fmt:
if 'd' in fmt:
nDig = None
else:
sfmt = fmt[1:].split('.')
if not sfmt[0]: sfmt[0] = '10'
nDig = (int(sfmt[0]),int(sfmt[1][:-1]),sfmt[1][-1])
valItem = ValidatedTxtCtrl(self.panel,self.values,tid,nDig=nDig,xmin=limits[0],xmax=limits[1])
# valItem = wx.TextCtrl(self.panel,value=fmt%(value),style=wx.TE_PROCESS_ENTER)
# valItem.Bind(wx.EVT_TEXT_ENTER,OnValItem)
# valItem.Bind(wx.EVT_KILL_FOCUS,OnValItem)
if type(fmt) is not list:
Indx[valItem] = [tid,limits,fmt]
lineSizer.Add(valItem,0,wx.ALIGN_CENTER)
mainSizer.Add(lineSizer)
OkBtn = wx.Button(self.panel,-1,"Ok")
OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
CancelBtn = wx.Button(self.panel,-1,'Cancel')
CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
btnSizer.Add((20,20),1)
btnSizer.Add(OkBtn)
OkBtn.SetDefault()
btnSizer.Add(CancelBtn)
btnSizer.Add((20,20),1)
mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
self.panel.SetSizer(mainSizer)
self.panel.Fit()
self.Fit()
self.CenterOnParent()
def GetValues(self):
return self.values
def OnOk(self,event):
parent = self.GetParent()
if parent is not None: parent.Raise()
self.EndModal(wx.ID_OK)
def OnCancel(self,event):
parent = self.GetParent()
if parent is not None: parent.Raise()
self.EndModal(wx.ID_CANCEL)
################################################################################
[docs]
class SingleStringDialog(wx.Dialog):
'''Dialog to obtain a single string value from user
:param wx.Frame parent: name of parent frame
:param str title: title string for dialog
:param str prompt: string to tell use what they are inputting
:param str value: default input value, if any
:param tuple size: specifies default size and width for the text
entry section of the dialog [default (200,-1)]. If the vertical
size (the second number) is greater than 20 (~ a single line) then
the textbox will allow inclusion of new-line characters. In single-line
mode, return causes the dialog to close.
:param str help: if supplied, a help button is added to the dialog that
can be used to display the supplied help text/URL for setting this
variable. (Default is '', which is ignored.)
:param list choices: a set of strings that provide optional values that
can be selected from; these can be edited if desired.
'''
def __init__(self,parent,title,prompt,value='',size=(200,-1),help='',
choices=None):
wx.Dialog.__init__(self,parent,wx.ID_ANY,title,pos=wx.DefaultPosition,
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
self.value = value
self.prompt = prompt
self.CenterOnParent()
self.panel = wx.Panel(self)
mainSizer = wx.BoxSizer(wx.VERTICAL)
sizer1 = wx.BoxSizer(wx.HORIZONTAL)
sizer1.Add(wx.StaticText(self.panel,-1,self.prompt),0,wx.ALIGN_CENTER)
sizer1.Add((-1,-1),1,wx.EXPAND)
if help:
sizer1.Add(HelpButton(self.panel,help),0,wx.ALL)
mainSizer.Add(sizer1,0,wx.EXPAND)
sizer1 = wx.BoxSizer(wx.HORIZONTAL)
if choices:
self.valItem = wx.ComboBox(self.panel, wx.ID_ANY, value,
size=size,choices=[value]+choices,
style=wx.CB_DROPDOWN|wx.TE_PROCESS_ENTER)
elif size[1] > 20:
self.valItem = wx.TextCtrl(self.panel,-1,value=self.value,size=size,
style=wx.TE_MULTILINE|wx.TE_PROCESS_ENTER)
else:
self.valItem = wx.TextCtrl(self.panel,-1,value=self.value,size=size)
#sizer1.Add(self.valItem,0,wx.ALIGN_CENTER)
#mainSizer.Add(sizer1,0,wx.EXPAND)
mainSizer.Add(self.valItem,1,wx.EXPAND)
btnsizer = wx.StdDialogButtonSizer()
OKbtn = wx.Button(self.panel, wx.ID_OK)
OKbtn.SetDefault()
btnsizer.AddButton(OKbtn)
btn = wx.Button(self.panel, wx.ID_CANCEL)
btnsizer.AddButton(btn)
btnsizer.Realize()
mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER)
self.panel.SetSizer(mainSizer)
self.panel.Fit()
self.Fit()
[docs]
def Show(self):
'''Use this method after creating the dialog to post it
:returns: True if the user pressed OK; False if the User pressed Cancel
'''
if self.ShowModal() == wx.ID_OK:
self.value = self.valItem.GetValue()
return True
else:
return False
[docs]
def GetValue(self):
'''Use this method to get the value entered by the user
:returns: string entered by user
'''
return self.value
################################################################################
[docs]
class MultiStringDialog(wx.Dialog):
'''Dialog to obtain a multi string values from user
:param wx.Frame parent: name of parent frame
:param str title: title string for dialog
:param list prompts: list of strings to tell user what they are inputting
:param list values: list of str default input values, if any
:param int size: length of the input box in pixels
:param bool addRows: if True, users can add rows to the table
(default is False)
:param str hlp: if supplied, a help button is added to the dialog that
can be used to display the supplied help text in this variable.
:param str lbl: label placed at top of dialog
:returns: a wx.Dialog instance
'''
def __init__(self,parent,title,prompts,values=[],size=-1,
addRows=False,hlp=None, lbl=None):
wx.Dialog.__init__(self,parent,wx.ID_ANY,title,
pos=wx.DefaultPosition,
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
self.values = list(values)
self.prompts = list(prompts)
self.addRows = addRows
self.size = size
self.hlp = hlp
self.lbl = lbl
self.CenterOnParent()
self.Paint()
def Paint(self):
if self.GetSizer():
self.GetSizer().Clear(True)
mainSizer = wx.BoxSizer(wx.VERTICAL)
if self.hlp:
btnsizer = wx.BoxSizer(wx.HORIZONTAL)
hlp = HelpButton(self, self.hlp, wrap=450)
btnsizer.Add((-1,-1),1, wx.EXPAND, 1)
#btnsizer.Add(hlp,0,wx.ALIGN_RIGHT|wx.ALL)
btnsizer.Add(hlp,0)
mainSizer.Add(btnsizer,0,wx.EXPAND)
if self.lbl:
mainSizer.Add(wx.StaticText(self,wx.ID_ANY,self.lbl))
mainSizer.Add((-1,15))
promptSizer = wx.FlexGridSizer(0,2,5,5)
promptSizer.AddGrowableCol(1,1)
self.Indx = {}
for prompt,value in zip(self.prompts,self.values):
promptSizer.Add(wx.StaticText(self,-1,prompt))
valItem = wx.TextCtrl(self,-1,value=value,style=wx.TE_PROCESS_ENTER,size=(self.size,-1))
self.Indx[valItem.GetId()] = prompt
valItem.Bind(wx.EVT_TEXT,self.newValue)
promptSizer.Add(valItem,1,wx.EXPAND,1)
mainSizer.Add(promptSizer,1,wx.ALL|wx.EXPAND,1)
btnsizer = wx.BoxSizer(wx.HORIZONTAL)
OKbtn = wx.Button(self, wx.ID_OK)
OKbtn.SetDefault()
btnsizer.Add((1,1),1,wx.EXPAND,1)
btnsizer.Add(OKbtn)
btn = wx.Button(self, wx.ID_CANCEL)
btnsizer.Add(btn)
btnsizer.Add((1,1),1,wx.EXPAND,1)
if self.addRows:
btn = wx.Button(self, wx.ID_ANY,'+',style=wx.BU_EXACTFIT)
btn.Bind(wx.EVT_BUTTON,self.onExpand)
btnsizer.Add(btn)
mainSizer.Add(btnsizer,0,wx.EXPAND)
self.SetSizer(mainSizer)
self.Fit()
def onExpand(self,event):
self.values.append('')
self.prompts.append('item '+str(len(self.values)))
self.Paint()
def newValue(self,event):
Obj = event.GetEventObject()
item = self.Indx[Obj.GetId()]
id = self.prompts.index(item)
self.values[id] = Obj.GetValue()
[docs]
def Show(self):
'''Use this method after creating the dialog to post it
:returns: True if the user pressed OK; False if the User pressed Cancel
'''
if self.ShowModal() == wx.ID_OK:
return True
else:
return False
[docs]
def GetValues(self):
'''Use this method to get the value(s) entered by the user
:returns: a list of strings entered by user
'''
return self.values
################################################################################
[docs]
class G2ColumnIDDialog(wx.Dialog):
'''A dialog for matching column data to desired items; some columns may be ignored.
:param wx.Frame ParentFrame: reference to parent frame
:param str title: heading above list of choices
:param str header: Title to place on window frame
:param list ChoiceList: a list of possible choices for the columns
:param list ColumnData: lists of column data to be matched with ChoiceList
:param bool monoFont: If False (default), use a variable-spaced font;
if True use a equally-spaced font.
:param kw: optional keyword parameters for the wx.Dialog may
be included such as size [which defaults to `(320,310)`] and
style (which defaults to ``wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.CENTRE | wx.OK | wx.CANCEL``);
note that ``wx.OK`` and ``wx.CANCEL`` controls
the presence of the eponymous buttons in the dialog.
:returns: the name of the created dialog
'''
def __init__(self,parent, title, header,Comments,ChoiceList, ColumnData,
monoFont=False, **kw):
def OnOk(sevent):
OK = True
selCols = []
for col in self.sel:
item = col.GetValue()
if item != ' ' and item in selCols:
OK = False
break
else:
selCols.append(item)
parent = self.GetParent()
if not OK:
parent.ErrorDialog('Duplicate',item+' selected more than once')
return
if parent is not None: parent.Raise()
self.EndModal(wx.ID_OK)
def OnModify(event):
if event: event.Skip()
Obj = event.GetEventObject()
icol,colData = Indx[Obj.GetId()]
modify = Obj.GetValue()
if not modify:
return
#print 'Modify column',icol,' by', modify
for i,item in enumerate(self.ColumnData[icol]):
self.ColumnData[icol][i] = str(eval(item+modify))
colData.SetValue('\n'.join(self.ColumnData[icol]))
Obj.SetValue('')
# process keyword parameters, notably style
options = {'size':(600,310), # default Frame keywords
'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
}
options.update(kw)
self.Comments = ''.join(Comments)
self.ChoiceList = ChoiceList
self.ColumnData = ColumnData
if options['style'] & wx.OK:
useOK = True
options['style'] ^= wx.OK
else:
useOK = False
if options['style'] & wx.CANCEL:
useCANCEL = True
options['style'] ^= wx.CANCEL
else:
useCANCEL = False
# create the dialog frame
wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
panel = wxscroll.ScrolledPanel(self)
# fill the dialog
Sizer = wx.BoxSizer(wx.VERTICAL)
Sizer.Add((-1,5))
if self.Comments:
if title[-1] == ':':
title = title[:-1] + ' using header line(s):'
else:
title += ' using header line(s):'
Sizer.Add(wx.StaticText(panel,label=title),0)
Sizer.Add((5,5))
if self.Comments[-1] != '\n': self.Comments += '\n'
txt = wx.StaticText(panel,label=self.Comments)
txt.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
font1 = wx.Font(txt.GetFont().GetPointSize(),wx.MODERN, wx.NORMAL, wx.NORMAL, False)
txt.SetFont(font1)
Sizer.Add(txt,0,wx.ALL|wx.EXPAND,0)
txtSize = txt.GetSize()[1]
else:
Sizer.Add(wx.StaticText(panel,label=title),0)
txtSize = 0
columnsSizer = wx.BoxSizer(wx.HORIZONTAL)
self.sel = []
self.mod = []
Indx = {}
for icol,col in enumerate(self.ColumnData):
colSizer = wx.BoxSizer(wx.VERTICAL)
colSizer.Add(wx.StaticText(panel,label=' Column #%d Select:'%(icol)))
self.sel.append(wx.ComboBox(panel,value=' ',choices=self.ChoiceList,style=wx.CB_READONLY|wx.CB_DROPDOWN))
colSizer.Add(self.sel[-1])
colData = wx.TextCtrl(panel,value='\n'.join(self.ColumnData[icol]),size=(120,-1),
style=wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_DONTWRAP)
colSizer.Add(colData,1,wx.ALL|wx.EXPAND,1)
colSizer.Add(wx.StaticText(panel,label=' Modify by:'))
mod = wx.TextCtrl(panel,size=(120,-1),value='',style=wx.TE_PROCESS_ENTER)
mod.Bind(wx.EVT_TEXT_ENTER,OnModify)
mod.Bind(wx.EVT_KILL_FOCUS,OnModify)
Indx[mod.GetId()] = [icol,colData]
colSizer.Add(mod)
columnsSizer.Add(colSizer,0,wx.ALL|wx.EXPAND,10)
Sizer.Add(columnsSizer,1,wx.ALL|wx.EXPAND,1)
Sizer.Add(wx.StaticText(panel,label=' For modify by, enter arithmetic string eg. "-12345.67". "+", "-", "*", "/", "**" all allowed'),0)
Sizer.Add((-1,10))
# OK/Cancel buttons
btnsizer = wx.StdDialogButtonSizer()
if useOK:
self.OKbtn = wx.Button(panel, wx.ID_OK)
self.OKbtn.SetDefault()
btnsizer.AddButton(self.OKbtn)
self.OKbtn.Bind(wx.EVT_BUTTON, OnOk)
if useCANCEL:
btn = wx.Button(panel, wx.ID_CANCEL)
btnsizer.AddButton(btn)
btnsizer.Realize()
Sizer.Add((-1,5))
Sizer.Add(btnsizer,0,wx.ALIGN_LEFT,20)
Sizer.Add((-1,5))
# OK done, let's get outa here
panel.SetSizer(Sizer)
panel.SetAutoLayout(1)
panel.SetupScrolling()
Size = [450,375]
panel.SetSize(Size)
Size[0] += 25; Size[1]+= 25+txtSize
self.SetSize(Size)
[docs]
def GetSelection(self):
'Returns the selected sample parm for each column'
selCols = []
for item in self.sel:
selCols.append(item.GetValue())
return selCols,self.ColumnData
################################################################################
[docs]
class G2HistoDataDialog(wx.Dialog):
'''A dialog for editing histogram data globally.
:param wx.Frame ParentFrame: reference to parent frame
:param str title: heading above list of choices
:param str header: Title to place on window frame
:param list ParmList: a list of names for the columns
:param list ParmFmt: a list of formatting strings for the columns
:param list: HistoList: a list of histogram names
:param list ParmData: a list of lists of data matched to ParmList; one for each item in HistoList
:param bool monoFont: If False (default), use a variable-spaced font;
if True use a equally-spaced font.
:param kw: optional keyword parameters for the wx.Dialog may
be included such as size [which defaults to `(320,310)`] and
style (which defaults to
``wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.CENTRE | wx.OK | wx.CANCEL``);
note that ``wx.OK`` and ``wx.CANCEL`` controls the presence of the eponymous buttons in the dialog.
:returns: the modified ParmData
'''
def __init__(self,parent, title, header,ParmList,ParmFmt,HistoList,ParmData,
monoFont=False, **kw):
def OnOk(sevent):
if parent is not None: parent.Raise()
self.EndModal(wx.ID_OK)
def OnModify(event):
Obj = event.GetEventObject()
irow,it = Indx[Obj.GetId()]
try:
val = float(Obj.GetValue())
except ValueError:
val = self.ParmData[irow][it]
self.ParmData[irow][it] = val
Obj.SetValue(self.ParmFmt[it]%val)
# process keyword parameters, notably style
options = {'size':(600,310), # default Frame keywords
'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
}
options.update(kw)
self.ParmList = ParmList
self.ParmFmt = ParmFmt
self.HistoList = HistoList
self.ParmData = ParmData
nCol = len(ParmList)
if options['style'] & wx.OK:
useOK = True
options['style'] ^= wx.OK
else:
useOK = False
if options['style'] & wx.CANCEL:
useCANCEL = True
options['style'] ^= wx.CANCEL
else:
useCANCEL = False
# create the dialog frame
wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
panel = wxscroll.ScrolledPanel(self)
# fill the dialog
Sizer = wx.BoxSizer(wx.VERTICAL)
Sizer.Add((-1,5))
Sizer.Add(wx.StaticText(panel,label=title),0)
dataSizer = wx.FlexGridSizer(0,nCol+1,0,0)
self.sel = []
self.mod = []
Indx = {}
for item in ['Histogram',]+self.ParmList:
dataSizer.Add(wx.StaticText(panel,-1,label=' %10s '%(item)),0,WACV)
for irow,name in enumerate(self.HistoList):
dataSizer.Add(wx.StaticText(panel,label=name),0,WACV|wx.LEFT|wx.RIGHT,10)
for it,item in enumerate(self.ParmData[irow]):
dat = wx.TextCtrl(panel,-1,value=self.ParmFmt[it]%(item),style=wx.TE_PROCESS_ENTER)
dataSizer.Add(dat,0,WACV)
dat.Bind(wx.EVT_TEXT_ENTER,OnModify)
dat.Bind(wx.EVT_KILL_FOCUS,OnModify)
Indx[dat.GetId()] = [irow,it]
Sizer.Add(dataSizer)
Sizer.Add((-1,10))
# OK/Cancel buttons
btnsizer = wx.StdDialogButtonSizer()
if useOK:
self.OKbtn = wx.Button(panel, wx.ID_OK)
self.OKbtn.SetDefault()
btnsizer.AddButton(self.OKbtn)
self.OKbtn.Bind(wx.EVT_BUTTON, OnOk)
if useCANCEL:
btn = wx.Button(panel, wx.ID_CANCEL)
btnsizer.AddButton(btn)
btnsizer.Realize()
Sizer.Add((-1,5))
Sizer.Add(btnsizer,0,wx.ALIGN_LEFT,20)
Sizer.Add((-1,5))
# OK done, let's get outa here
panel.SetSizer(Sizer)
panel.SetAutoLayout(1)
panel.SetupScrolling()
Size = [450,375]
panel.SetSize(Size)
Size[0] += 25; Size[1]+= 25
self.SetSize(Size)
[docs]
def GetData(self):
'Returns the modified ParmData'
return self.ParmData
################################################################################
[docs]
def ItemSelector(ChoiceList, ParentFrame=None,
title='Select an item',
size=None, header='Item Selector',
useCancel=True,multiple=False):
''' Provide a wx dialog to select a single item or multiple items from list of choices
:param list ChoiceList: a list of choices where one will be selected
:param wx.Frame ParentFrame: Name of parent frame (default None)
:param str title: heading above list of choices (default 'Select an item')
:param wx.Size size: Size for dialog to be created (default None -- size as needed)
:param str header: Title to place on window frame (default 'Item Selector')
:param bool useCancel: If True (default) both the OK and Cancel buttons are offered
:param bool multiple: If True then multiple items can be selected (default False)
:returns: the selection index or None or a selection list if multiple is true
Called by GSASIIdataGUI.OnReOrgSelSeq() Which is not fully implemented.
Example::
choices = ('NXazint1d 1D file','NXazint1d 2D file')
sel = G2G.ItemSelector(choices, ParentFrame=ParentFrame,
header='Select file section',
title='Select the section of the file to read')
if sel is None: return False
'''
if multiple:
if useCancel:
dlg = G2MultiChoiceDialog(
ParentFrame,title, header, ChoiceList)
else:
dlg = G2MultiChoiceDialog(
ParentFrame,title, header, ChoiceList,
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.OK|wx.CENTRE)
else:
if useCancel:
dlg = wx.SingleChoiceDialog(
ParentFrame,title, header, ChoiceList)
else:
dlg = wx.SingleChoiceDialog(
ParentFrame,title, header,ChoiceList,
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.OK|wx.CENTRE)
if size: dlg.SetSize(size)
if dlg.ShowModal() == wx.ID_OK:
if multiple:
dlg.Destroy()
return dlg.GetSelections()
else:
dlg.Destroy()
return dlg.GetSelection()
else:
dlg.Destroy()
return None
dlg.Destroy()
########################################################
# Column-order selection dialog
[docs]
def GetItemOrder(parent,keylist,vallookup,posdict):
'''Creates a dialog where items can be ordered into columns
:param list keylist: is a list of keys for column assignments
:param dict vallookup: is a dict keyed by names in keylist where each item is a dict.
Each inner dict contains variable names as keys and their associated values
:param dict posdict: is a dict keyed by names in keylist where each item is a dict.
Each inner dict contains column numbers as keys and their associated
variable name as a value. This is used for both input and output.
'''
dlg = wx.Dialog(parent,style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
sizer = wx.BoxSizer(wx.VERTICAL)
spanel = OrderBox(dlg,keylist,vallookup,posdict)
spanel.Fit()
sizer.Add(spanel,1,wx.EXPAND)
btnsizer = wx.StdDialogButtonSizer()
btn = wx.Button(dlg, wx.ID_OK)
btn.SetDefault()
btnsizer.AddButton(btn)
#btn = wx.Button(dlg, wx.ID_CANCEL)
#btnsizer.AddButton(btn)
btnsizer.Realize()
sizer.Add(btnsizer, 0, wx.EXPAND|wx.ALL, 5)
dlg.SetSizer(sizer)
sizer.Fit(dlg)
dlg.ShowModal()
################################################################################
[docs]
class MultiIntegerDialog(wx.Dialog):
'''Input a series of integers based on prompts
'''
def __init__(self,parent,title,prompts,values):
wx.Dialog.__init__(self,parent,-1,title,
pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
self.panel = wx.Panel(self) #just a dummy - gets destroyed in Draw!
self.values = values
self.prompts = prompts
self.Draw()
def Draw(self):
def OnValItem(event):
event.Skip()
Obj = event.GetEventObject()
ind = Indx[Obj.GetId()]
try:
val = int(Obj.GetValue())
if val <= 0:
raise ValueError
except ValueError:
val = self.values[ind]
self.values[ind] = val
Obj.SetValue('%d'%(val))
self.panel.Destroy()
self.panel = wx.Panel(self)
mainSizer = wx.BoxSizer(wx.VERTICAL)
Indx = {}
for ival,[prompt,value] in enumerate(zip(self.prompts,self.values)):
mainSizer.Add(wx.StaticText(self.panel,-1,prompt),0,wx.ALIGN_CENTER)
valItem = wx.TextCtrl(self.panel,-1,value='%d'%(value),style=wx.TE_PROCESS_ENTER)
mainSizer.Add(valItem,0,wx.ALIGN_CENTER)
Indx[valItem.GetId()] = ival
valItem.Bind(wx.EVT_TEXT_ENTER,OnValItem)
valItem.Bind(wx.EVT_KILL_FOCUS,OnValItem)
OkBtn = wx.Button(self.panel,-1,"Ok")
OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
CancelBtn = wx.Button(self.panel,-1,'Cancel')
CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
btnSizer.Add((20,20),1)
btnSizer.Add(OkBtn)
btnSizer.Add(CancelBtn)
btnSizer.Add((20,20),1)
mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
self.panel.SetSizer(mainSizer)
self.panel.Fit()
self.Fit()
def GetValues(self):
return self.values
def OnOk(self,event):
parent = self.GetParent()
if parent is not None: parent.Raise()
self.EndModal(wx.ID_OK)
def OnCancel(self,event):
parent = self.GetParent()
if parent is not None: parent.Raise()
self.EndModal(wx.ID_CANCEL)
################################################################################
[docs]
class MultiColumnSelection(wx.Dialog):
'''Defines a Dialog widget that can be used to select an item from a multicolumn list.
The first column should be short, but remaining columns are word-wrapped if the
length of the information extends beyond the column.
When created, the dialog will be shown and <dlg>.Selection will be set to the index
of the selected row, or -1. Be sure to use <dlg>.Destroy() to remove the window
after reading the selection. If the dialog cannot be shown because a very old
version of wxPython is in use, <dlg>.Selection will be None.
If checkLbl is provided with a value, then a set of check buttons starts the table
and <dlg>.Selections has the checked rows.
:param wx.Frame parent: the parent frame (or None)
:param str title: A title for the dialog window
:param list colLabels: labels for each column
:param list choices: a nested list with a value for each row in the table. Within each value
should be a list of values for each column. There must be at least one value, but it is
OK to have more or fewer values than there are column labels (colLabels). Extra are ignored
and unspecified columns are left blank.
:param list colWidths: a list of int values specifying the column width for each
column in the table (pixels). There must be a value for every column label (colLabels).
:param str checkLbl: A label for a row of checkboxes added at the beginning of the table.
This option seems to be broken.
:param int height: an optional height (pixels) for the table (defaults to 400)
:param bool centerCols: if True, items in each column are centered. Default is False
Example use::
lbls = ('col 1','col 2','col 3')
choices=(['test1','explanation of test 1'],
['b', 'a really really long line that will be word-wrapped'],
['test3','more explanation text','optional 3rd column text'])
colWidths=[200,400,100]
dlg = MultiColumnSelection(frm,'select tutorial',lbls,choices,colWidths)
value = choices[dlg.Selection][0]
dlg.Destroy()
'''
def __init__(self, parent, title, colLabels, choices, colWidths, checkLbl="",
height=400, centerCols=False, *args, **kw):
if len(colLabels) != len(colWidths):
raise ValueError('Length of colLabels) != colWidths')
sizex = 20 # extra room for borders, etc.
for i in colWidths: sizex += i
wx.Dialog.__init__(self, parent, wx.ID_ANY, title, *args,
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER,
size=(sizex,height), **kw)
self.Selections = len(choices)*[False]
try:
from wx.lib.wordwrap import wordwrap
import wx.lib.agw.ultimatelistctrl as ULC
except ImportError:
self.Selection = None
return
self.Selection = -1
mainSizer = wx.BoxSizer(wx.VERTICAL)
self.list = ULC.UltimateListCtrl(self, agwStyle=ULC.ULC_REPORT|ULC.ULC_HAS_VARIABLE_ROW_HEIGHT
|ULC.ULC_HRULES|ULC.ULC_HRULES|ULC.ULC_SINGLE_SEL)
if centerCols:
colPosition = ULC.ULC_FORMAT_CENTER
else:
colPosition = ULC.ULC_FORMAT_LEFT
if checkLbl:
self.list.InsertColumn(0, checkLbl, width=8*len(checkLbl), format=colPosition)
inc = 1
else:
inc = 0
for i,(lbl,wid) in enumerate(zip(colLabels, colWidths)):
self.list.InsertColumn(i+inc, lbl, width=wid, format=colPosition)
for i,item in enumerate(choices):
if item[0].startswith(' '):
item[0] = '--- '+item[0].strip()
if checkLbl:
def OnCheck(event,row=i):
self.Selections[row] = event.EventObject.GetValue()
c = wx.CheckBox(self.list)
c.Bind(wx.EVT_CHECKBOX,OnCheck)
self.list.InsertStringItem(i, "")
citem = self.list.GetItem(i,0)
citem.SetWindow(c)
self.list.SetItem(citem)
self.list.SetStringItem(i, 1, item[0])
else:
self.list.InsertStringItem(i, item[0])
for j,item in enumerate(item[1:len(colLabels)]):
item = wordwrap(StripIndents(item,True), colWidths[j+1], wx.ClientDC(self))
item += "\n==========================================="
self.list.SetStringItem(i,1+j+inc, item)
# make buttons
mainSizer.Add(self.list, 1, wx.EXPAND|wx.ALL, 1)
btnsizer = wx.BoxSizer(wx.HORIZONTAL)
OKbtn = wx.Button(self, wx.ID_OK)
OKbtn.SetDefault()
btnsizer.Add(OKbtn)
if not checkLbl:
btn = wx.Button(self, wx.ID_CLOSE,"Cancel")
btnsizer.Add(btn)
mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
# bindings for close of window, double-click,...
self.Bind(wx.EVT_CLOSE, self._onClose)
if not checkLbl:
OKbtn.Bind(wx.EVT_BUTTON,self._onSelect)
self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self._onSelect)
btn.Bind(wx.EVT_BUTTON,self._onClose)
self.SetSizer(mainSizer)
self.ShowModal()
def _onClose(self,event):
event.Skip()
self.EndModal(wx.ID_CANCEL)
def _onSelect(self,event):
if self.list.GetNextSelected(-1) == -1: return
self.Selection = self.list.GetNextSelected(-1)
self.EndModal(wx.ID_OK)
[docs]
def MultiColMultiSelDlg(parent, title, header, colInfo, choices):
'''Provides a dialog widget that can be used to select multiple items
from a multicolumn list.
:param wx.Frame parent: the parent frame (or None)
:param str title: A title for the dialog window
:param str header: A instruction string for the dialog window
:param list colInfo: contains three items for each column: a label for the column,
a width for the column (in pixels), and True if the column should be right justified.
:param list choices: a nested list with values for each row in the table. Within each row
should be a list of values for each column. There must be at least one value, but it is
OK to have more or fewer values than there are column labels (colInfo). Extra are ignored
and unspecified columns are left blank.
:returns: a list of bool values for each entry in choices, True if selected, or
None is the dialog is cancelled.
Example use::
choices = [('xmltodict', 'Bruker .brml Importer'),
('zarr', 'MIDAS Zarr importer'),
('h5py', 'HDF5 image importer'),
('hdf5', 'HDF5 image importer')]
colInfo = [('package', 50, False),
('needed by', 200, True)]
res = G2G.MultiColMultiSelDlg(parent, 'window title', 'Instructions', colInfo, choices)
'''
dlg = wx.Dialog(parent,wx.ID_ANY,title,
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
mainSizer = wx.BoxSizer(wx.VERTICAL)
txt = wx.StaticText(dlg,wx.ID_ANY,header)
txt.Wrap(300)
mainSizer.Add(txt)
lst = wx.ListCtrl(dlg, wx.ID_ANY, style=wx.LC_REPORT)
lst.EnableCheckBoxes()
lst.InsertColumn(0, 'Sel')
lst.SetColumnWidth(0, 30)
cols = len(colInfo)
for i,(lbl,wid,rgt) in enumerate(colInfo):
if rgt:
lst.InsertColumn(i+1, lbl, wx.LIST_FORMAT_RIGHT)
else:
lst.InsertColumn(i+1, lbl)
if type(wid) is int:
lst.SetColumnWidth(i+1, wid)
else:
lst.SetColumnWidth(i+1, wx.LIST_AUTOSIZE)
for line in choices:
index = lst.InsertItem(lst.GetItemCount(),'')
for i,lbl in enumerate(line[:cols]):
lst.SetItem(index, i+1, lbl)
mainSizer.Add(lst,1,wx.EXPAND,1)
btnsizer = wx.StdDialogButtonSizer()
btn = wx.Button(dlg, wx.ID_OK, 'Install Selected')
btn.SetDefault()
btn.Bind(wx.EVT_BUTTON, lambda x: dlg.EndModal(wx.ID_OK))
btnsizer.AddButton(btn)
btn = wx.Button(dlg, wx.ID_CANCEL)
btn.Bind(wx.EVT_BUTTON, lambda x: dlg.EndModal(wx.ID_CANCEL))
btnsizer.AddButton(btn)
btnsizer.Realize()
mainSizer.Add(btnsizer, 0, wx.EXPAND|wx.ALL, 5)
dlg.SetSizer(mainSizer)
dlg.CenterOnParent()
try:
if dlg.ShowModal() == wx.ID_OK:
return [lst.IsItemChecked(i) for i,c in enumerate(choices)]
return
finally:
dlg.Destroy()
################################################################################
[docs]
class OrderBox(wxscroll.ScrolledPanel):
'''Creates a panel with scrollbars where items can be ordered into columns
:param list keylist: is a list of keys for column assignments
:param dict vallookup: is a dict keyed by names in keylist where each item is a dict.
Each inner dict contains variable names as keys and their associated values
:param dict posdict: is a dict keyed by names in keylist where each item is a dict.
Each inner dict contains column numbers as keys and their associated
variable name as a value. This is used for both input and output.
'''
def __init__(self,parent,keylist,vallookup,posdict,*arg,**kw):
self.keylist = keylist
self.vallookup = vallookup
self.posdict = posdict
self.maxcol = 0
for nam in keylist:
posdict = self.posdict[nam]
if posdict.keys():
self.maxcol = max(self.maxcol, max(posdict))
wxscroll.ScrolledPanel.__init__(self,parent,wx.ID_ANY,*arg,**kw)
self.GBsizer = wx.GridBagSizer(4,4)
self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
self.SetSizer(self.GBsizer)
colList = [str(i) for i in range(self.maxcol+2)]
for i in range(self.maxcol+1):
wid = wx.StaticText(self,wx.ID_ANY,str(i),style=wx.ALIGN_CENTER)
wid.SetBackgroundColour(DULL_YELLOW)
wid.SetMinSize((50,-1))
self.GBsizer.Add(wid,(0,i),flag=wx.EXPAND)
self.chceDict = {}
for row,nam in enumerate(self.keylist):
posdict = self.posdict[nam]
for col in posdict:
lbl = posdict[col]
pnl = wx.Panel(self,wx.ID_ANY)
pnl.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
insize = wx.BoxSizer(wx.VERTICAL)
wid = wx.Choice(pnl,wx.ID_ANY,choices=colList)
insize.Add(wid,0,wx.EXPAND|wx.BOTTOM,3)
wid.SetSelection(col)
self.chceDict[wid] = (row,col)
wid.Bind(wx.EVT_CHOICE,self.OnChoice)
wid = wx.StaticText(pnl,wx.ID_ANY,lbl)
insize.Add(wid,0,flag=wx.EXPAND)
try:
val = G2fil.FormatSigFigs(self.vallookup[nam][lbl],maxdigits=8)
except KeyError:
val = '?'
wid = wx.StaticText(pnl,wx.ID_ANY,'('+val+')')
insize.Add(wid,0,flag=wx.EXPAND)
pnl.SetSizer(insize)
self.GBsizer.Add(pnl,(row+1,col),flag=wx.EXPAND)
self.SetAutoLayout(1)
self.SetupScrolling()
self.SetMinSize((
min(700,self.GBsizer.GetSize()[0]),
self.GBsizer.GetSize()[1]+20))
[docs]
def OnChoice(self,event):
'''Called when a column is assigned to a variable
'''
row,col = self.chceDict[event.EventObject] # which variable was this?
newcol = event.Selection # where will it be moved?
if newcol == col:
return # no change: nothing to do!
prevmaxcol = self.maxcol # save current table size
key = self.keylist[row] # get the key for the current row
lbl = self.posdict[key][col] # selected variable name
lbl1 = self.posdict[key].get(col+1,'') # next variable name, if any
# if a posXXX variable is selected, and the next variable is posXXX, move them together
repeat = 1
if lbl[:3] == 'pos' and lbl1[:3] == 'int' and lbl[3:] == lbl1[3:]:
repeat = 2
for i in range(repeat): # process the posXXX and then the intXXX (or a single variable)
col += i
newcol += i
if newcol in self.posdict[key]:
# find first non-blank after newcol
for mtcol in range(newcol+1,self.maxcol+2):
if mtcol not in self.posdict[key]: break
l1 = range(mtcol,newcol,-1)+[newcol]
l = range(mtcol-1,newcol-1,-1)+[col]
else:
l1 = [newcol]
l = [col]
# move all of the items, starting from the last column
for newcol,col in zip(l1,l):
#print 'moving',col,'to',newcol
self.posdict[key][newcol] = self.posdict[key][col]
del self.posdict[key][col]
self.maxcol = max(self.maxcol,newcol)
obj = self.GBsizer.FindItemAtPosition((row+1,col))
self.GBsizer.SetItemPosition(obj.GetWindow(),(row+1,newcol))
for wid in obj.GetWindow().Children:
if wid in self.chceDict:
self.chceDict[wid] = (row,newcol)
wid.SetSelection(self.chceDict[wid][1])
# has the table gotten larger? If so we need new column heading(s)
if prevmaxcol != self.maxcol:
for i in range(prevmaxcol+1,self.maxcol+1):
wid = wx.StaticText(self,wx.ID_ANY,str(i),style=wx.ALIGN_CENTER)
wid.SetBackgroundColour(DULL_YELLOW)
wid.SetMinSize((50,-1))
self.GBsizer.Add(wid,(0,i),flag=wx.EXPAND)
colList = [str(i) for i in range(self.maxcol+2)]
for wid in self.chceDict:
wid.SetItems(colList)
wid.SetSelection(self.chceDict[wid][1])
self.GBsizer.Layout()
self.FitInside()
################################################################################
[docs]
def GetImportFile(G2frame, message, defaultDir="", defaultFile="",
style=wx.FD_OPEN, parent=None,*args, **kwargs):
'''Uses a standard or GSASII-customized dialog that gets files
from the appropriate import directory.
Arguments are used the same as in :func:`wx.FileDialog`. Selection of
multiple files is allowed if argument style includes wx.FD_MULTIPLE.
The default initial directory (unless overridden with argument defaultDir)
is found in G2frame.TutorialImportDir, config setting Import_directory or
G2frame.LastImportDir, see :func:`GetImportPath`.
The path of the first file entered is used to set G2frame.LastImportDir
and optionally config setting Import_directory.
:returns: a list of files or an empty list
'''
if not parent: parent = G2frame
pth = GetImportPath(G2frame)
#if GSASIIpath.GetConfigValue('debug'):
# print('debug: GetImportFile from '+defaultDir)
# print('debug: GetImportFile pth '+pth)
browseOpt = GSASIIpath.GetConfigValue('G2FileBrowser')
# unspecified value defaults to True on Linux, False for Windows/Linux
if sys.platform.startswith("linux") and browseOpt is None:
browseOpt = True
if browseOpt:
filelist = G2FileBrowser(parent, message, defaultDir, style=style,
**kwargs)
if len(filelist) == 0: return []
if style & wx.FD_CHANGE_DIR: # to get Mac/Linux to change directory like windows!
os.chdir(os.path.dirname(filelist[0]))
else:
dlg = wx.FileDialog(parent, message, defaultDir, defaultFile, *args, style=style, **kwargs)
# dlg.CenterOnParent()
if not defaultDir and pth: dlg.SetDirectory(pth)
try:
if dlg.ShowModal() == wx.ID_OK:
if style & wx.FD_MULTIPLE:
filelist = dlg.GetPaths()
if len(filelist) == 0: return []
else:
filelist = [dlg.GetPath(),]
# not sure if we want to do this (why use wx.CHANGE_DIR?)
if style & wx.FD_CHANGE_DIR: # to get Mac/Linux to change directory like windows!
os.chdir(dlg.GetDirectory())
else: # cancel was pressed
return []
finally:
dlg.Destroy()
# save the path of the first file and reset the TutorialImportDir variable
pth = os.path.split(os.path.abspath(filelist[0]))[0]
if GSASIIpath.GetConfigValue('Save_paths'): SaveImportDirectory(pth)
G2frame.LastImportDir = pth
G2frame.TutorialImportDir = None
return filelist
[docs]
def G2FileBrowser(parent, message, defaultDir, *args,
style=wx.FD_OPEN,wildcard="any file (*.*)|*.*",
**kwargs):
'''Create a file browser for selecting one or more files from
a directory. The user may select the directory where the files
and must select an extension from a specified list. Optionally,
a filter may be supplied to ignore files not matching a glob
pattern. When a large number of files is found, the file list
is broken into ranges.
Arguments used are the same as in :func:`wx.FileDialog`. Selection of
multiple files is allowed if argument style includes wx.FD_MULTIPLE.
Note that other positional or keyword parameters, other than those below
are allowed, but are ignored.
:param parent: parent window to dialog to be created
:param message: message to place at top of dialog
:param defaultDir: starting directory
:param style: selection style. Bit settings other than wx.FD_MULTIPLE
are ignored. When wx.FD_MULTIPLE is specified, multiple files can
be selected.
:param wildcard: defines the extensions allowed for selecting files.
of form "type A (*.A;*.B;*.c)|*.A;*.B;*.c|any file (*.*)|*.*". See
:func:`wx.FileDialog` for more information on this.
:returns: a list of selected files. Will contain only one file unless
style=wx.FD_MULTIPLE has been used.
'''
maxFiles = 300 # when more than this number of files is found in a
# directory, after applying the extension and filter
# the list is broken into ranges
import fnmatch # import these now as not commonly used in GSAS-II
import wx.lib.filebrowsebutton as filebrowse
def OnDirSelect(event,**kwargs):
'''Called when a directory or an extension is selected.
Loads dlg.filelist with the files in that directory and
with the selected extension.
'''
try: # exit if listbox not yet created
listbox
except:
return
extSel = typeSel.GetValue()
typeTxt.SetLabel(f"({extDict.get(extSel,'?')})")
dlg.filelist = []
dirVal = fb.GetValue()
if os.path.exists(dirVal):
dlg.filelist = glob.glob(os.path.join(dirVal,extSel))
if filterTxt[0] and filterTxt[0].strip() != '*':
dlg.filelist = [i for i in dlg.filelist if
fnmatch.fnmatch(os.path.split(i)[1], filterTxt[0])]
dlg.filelist.sort()
if len(dlg.filelist) == 0:
dlg.choices=['No matching files present']
listbox.Enable(False)
else:
last = int(1.1*maxFiles)
dlg.choices=dlg.filelist[:last]
listbox.Enable(True)
onSelection(None)
SetupNumSelect()
listbox.SetItems([os.path.split(f)[1] for f in dlg.choices])
def onSelection(event):
'Called when a file is selected. Enables the Read button'
selections = []
if event is not None:
selections = listbox.GetSelections()
if len(selections) > 0:
OKbtn.SetDefault()
OKbtn.Enable(True)
else:
CNbtn.SetDefault()
OKbtn.Enable(False)
def SetupNumSelect():
'defines the contents of the file range selector'
numfiles = len(dlg.filelist)
if numfiles < maxFiles:
choices = ['all files']
numSel.Enable(False)
else:
ranges = []
for i in range(numfiles//maxFiles+1):
first = i*maxFiles+1
if first > numfiles: break
last = min(numfiles,int((i+1.1)*maxFiles))
ranges.append((first,last))
if last >= numfiles: break
choices = []
for first,last in ranges:
choices.append(f"files {first}-{last}")
numSel.Enable(True)
numSel.SetItems(choices)
numSel.SetSelection(0)
def OnNumSelect(event):
'responds to a selection of a file range'
i = numSel.GetSelection()
first = i*maxFiles+1
last = int((i+1.1)*maxFiles)
dlg.choices=dlg.filelist[first-1:last]
listbox.SetItems([os.path.split(f)[1] for f in dlg.choices])
# parse the wildcard list
extDict = {}
for i,item in enumerate(wildcard.split('|')):
if i % 2 == 0:
nam = item.split('(')[0].strip()
else:
for ext in item.split(';'):
extDict[ext] = nam
# single or multiple file selection?
if style & wx.FD_MULTIPLE:
multiple = True
lbl = 'Select one or more files'
else:
multiple = False
lbl = 'Select a file'
# make the GUI
dlg = wx.Dialog(parent,wx.ID_ANY,lbl,
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
dlg.choices=[]
dlg.filelist = []
mainSizer = wx.BoxSizer(wx.VERTICAL)
# title at top
mainSizer.Add((0,5))
subSizer = wx.BoxSizer(wx.HORIZONTAL)
subSizer.Add((-1,-1),1,wx.EXPAND)
subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,message))
subSizer.Add((-1,-1),1,wx.EXPAND)
mainSizer.Add(subSizer,0,wx.EXPAND,0)
mainSizer.Add((0,10))
# directory selector
fb = filebrowse.DirBrowseButton(dlg, wx.ID_ANY,
labelText='Directory:',
changeCallback=OnDirSelect)
mainSizer.Add(fb,0,wx.EXPAND|wx.BOTTOM,5)
# extension selector
subSizer = wx.BoxSizer(wx.HORIZONTAL)
subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,' Extension: '))
choices = list(extDict.keys())
typeSel = wx.ComboBox(dlg,choices=choices,value=choices[0],
style=wx.CB_READONLY|wx.CB_DROPDOWN)
typeSel.Bind(wx.EVT_COMBOBOX, OnDirSelect)
if len(choices) == 1:
typeSel.Enable(False)
subSizer.Add(typeSel)
subSizer.Add((5,-1))
typeTxt = wx.StaticText(dlg,wx.ID_ANY,'')
subSizer.Add(typeTxt)
# filter
mainSizer.Add(subSizer)
subSizer = wx.BoxSizer(wx.HORIZONTAL)
subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,' Filter: '))
filterTxt = ['*']
txt = ValidatedTxtCtrl(dlg,filterTxt,0,notBlank=False,
OKcontrol=OnDirSelect,
OnLeave=OnDirSelect,OnLeaveArgs=[])
txt.SetMinSize((100,-1))
txt.Bind(wx.EVT_LEAVE_WINDOW, OnDirSelect)
txt.Bind(wx.EVT_KILL_FOCUS, OnDirSelect)
txt.Bind(wx.EVT_TEXT_ENTER, OnDirSelect)
subSizer.Add(txt,1,wx.EXPAND|wx.RIGHT,2)
mainSizer.Add(subSizer,0,wx.EXPAND,0)
# listbox
if multiple:
listbox = wx.ListBox(dlg, wx.ID_ANY, style=wx.LB_MULTIPLE|wx.LB_ALWAYS_SB)
else:
listbox = wx.ListBox(dlg, wx.ID_ANY, style=wx.LB_SINGLE|wx.LB_ALWAYS_SB)
listbox.Bind(wx.EVT_LISTBOX,onSelection)
listbox.SetMinSize((350,200))
mainSizer.Add(listbox,1,wx.EXPAND,1)
# file subset selection (disabled when not needed)
subSizer = wx.BoxSizer(wx.HORIZONTAL)
subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,' range of files: '))
choices = ['all files']
numSel = wx.ComboBox(dlg,choices=choices,value=choices[0],
style=wx.CB_READONLY|wx.CB_DROPDOWN)
numSel.SetMinSize((150,-1))
numSel.Bind(wx.EVT_COMBOBOX, OnNumSelect)
if len(choices) == 1:
numSel.Enable(False)
subSizer.Add(numSel)
mainSizer.Add(subSizer,0,wx.TOP,5)
# cancel/read buttons
btnsizer = wx.StdDialogButtonSizer()
CNbtn = wx.Button(dlg, wx.ID_CANCEL)
btnsizer.AddButton(CNbtn)
OKbtn = wx.Button(dlg, wx.ID_OK,'Read')
OKbtn.SetDefault()
btnsizer.AddButton(OKbtn)
btnsizer.Realize()
mainSizer.Add(btnsizer,0,wx.ALIGN_RIGHT,50)
# finish up
dlg.SetSizer(mainSizer)
mainSizer.Layout()
mainSizer.Fit(dlg)
dlg.CenterOnParent()
fb.SetValue(os.path.abspath(os.path.expanduser(defaultDir)))
# wait for button press
selections = []
if dlg.ShowModal() == wx.ID_OK:
selections = [dlg.choices[i] for i in listbox.GetSelections()]
dlg.Destroy()
return selections
[docs]
def GetImportPath(G2frame):
'''Determines the default location to use for importing files. Tries sequentially
G2frame.TutorialImportDir, config var Import_directory, G2frame.LastImportDir
and G2frame.LastGPXdir
:returns: a string containing the path to be used when reading files or '.'
if none of the above are specified.
'''
if G2frame.TutorialImportDir:
if os.path.exists(G2frame.TutorialImportDir):
return G2frame.TutorialImportDir
elif GSASIIpath.GetConfigValue('debug'):
print('DBG_Tutorial location (TutorialImportDir) not found: '+G2frame.TutorialImportDir)
pth = GSASIIpath.GetConfigValue('Import_directory')
if pth:
pth = os.path.expanduser(pth)
if os.path.exists(pth):
return pth
elif GSASIIpath.GetConfigValue('debug'):
print('Ignoring Config Import_directory value: '+GSASIIpath.GetConfigValue('Import_directory'))
if G2frame.LastImportDir:
if os.path.exists(G2frame.LastImportDir):
return G2frame.LastImportDir
elif GSASIIpath.GetConfigValue('debug'):
print('DBG_Warning: G2frame.LastImportDir not found = '+G2frame.LastImportDir)
elif G2frame.LastGPXdir:
return G2frame.LastGPXdir
print('Import path not found - set to current directory') #now shouldn't happen
return '.'
[docs]
def GetExportPath(G2frame):
'''Determines the default location to use for writing files. Tries sequentially
G2frame.LastExportDir and G2frame.LastGPXdir.
:returns: a string containing the path to be used when writing files or '.'
if none of the above are specified.
'''
if G2frame.LastExportDir:
return G2frame.LastExportDir
elif G2frame.LastGPXdir:
return G2frame.LastGPXdir
print('Export path not found - set to current directory') #now shouldn't happen
return '.'
################################################################################
[docs]
class SGMessageBox(wx.Dialog):
''' Special version of MessageBox that displays space group & super space group text
in two blocks
'''
def __init__(self,parent,title,text,table,spins=[],):
wx.Dialog.__init__(self,parent,wx.ID_ANY,title,pos=wx.DefaultPosition,
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
self.text = text
self.table = table
self.panel = wx.Panel(self)
self.spins = spins
self.useAlt = False
mainSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add((0,10))
for line in text:
mainSizer.Add(wx.StaticText(self.panel,label=' %s '%(line)))
ncol = self.table[0].count(',')+1
tableSizer = wx.FlexGridSizer(0,2*ncol+3,0,0)
j = 0
for item in self.table:
if 'for' in item:
mainSizer.Add(tableSizer,0,wx.ALIGN_LEFT)
mainSizer.Add(wx.StaticText(self.panel,label=item),0)
tableSizer = wx.FlexGridSizer(0,2*ncol+3,0,0)
continue
num,flds = item.split(')')
tableSizer.Add(wx.StaticText(self.panel,label=' %s '%(num+')')),0,WACV|wx.ALIGN_LEFT)
flds = flds.replace(' ','').split(',')
for i,fld in enumerate(flds):
if i < ncol-1:
text = wx.StaticText(self.panel,label='%s, '%(fld))
else:
text = wx.StaticText(self.panel,label='%s'%(fld))
if len(self.spins) and self.spins[j] < 0:
text.SetForegroundColour('Red')
tableSizer.Add(text,0,WACV|wx.ALIGN_RIGHT)
if not j%2:
tableSizer.Add((20,0))
j += 1
def OnPrintOps(event):
print(' Symmetry operations for %s:'%self.text[0].split(':')[1])
for iop,opText in enumerate(G2spc.TextOps(self.text,self.table,reverse=True)):
if self.useAlt:
opText = opText.replace('x','x1').replace('y','x2').replace('z','x3').replace('t','x4')
if len(self.spins):
if self.spins[iop] > 0:
print('%s,+%d'%(opText.replace(' ',''),self.spins[iop]))
else:
print('%s,%d'%(opText.replace(' ',''),self.spins[iop]))
else:
print(opText.replace(' ',''))
def OnAlt(event):
self.useAlt = altBtn.GetValue()
mainSizer.Add(tableSizer,0,wx.ALIGN_LEFT)
btnsizer = wx.BoxSizer(wx.HORIZONTAL)
OKbtn = wx.Button(self.panel, wx.ID_OK)
OKbtn.Bind(wx.EVT_BUTTON, self.OnOk)
btnsizer.Add(OKbtn)
printBtn = wx.Button(self.panel,label='Print Ops')
printBtn.Bind(wx.EVT_BUTTON, OnPrintOps)
btnsizer.Add(printBtn)
altBtn = wx.CheckBox(self.panel,label=' Use alt. symbols?')
altBtn.Bind(wx.EVT_CHECKBOX,OnAlt)
btnsizer.Add(altBtn,0,WACV)
mainSizer.Add((0,10))
mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER)
self.panel.SetSizer(mainSizer)
self.panel.Fit()
self.Fit()
size = self.GetSize()
self.SetSize([size[0]+20,size[1]])
self.CenterOnParent()
[docs]
def Show(self):
'''Use this method after creating the dialog to post it
'''
self.ShowModal()
return
def OnOk(self,event):
parent = self.GetParent()
if parent is not None: parent.Raise()
self.EndModal(wx.ID_OK)
################################################################################
[docs]
class SGMagSpinBox(wx.Dialog):
''' Special version of MessageBox that displays magnetic spin text
'''
def __init__(self,parent,title,text,table,Cents,names,spins,ifGray):
wx.Dialog.__init__(self,parent,wx.ID_ANY,title,pos=wx.DefaultPosition,
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER,size=wx.Size(420,350))
self.text = text
self.table = table
self.names = names
Nnames = len(self.names)
self.spins = spins
self.ifGray = ifGray
self.PrintTable = [' Magnetic symmetry operations for %s:'%self.text[0].split(':')[1],]
self.panel = wxscroll.ScrolledPanel(self)
mainSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add((0,10))
cents = [0,]
if len(Cents) > 1:
cents = self.text[-1].split(';')
for line in self.text:
mainSizer.Add(wx.StaticText(self.panel,label=' %s '%(line)),0)
if 'equivalent' in line:
break
ncol = self.table[0].count(',')+2
nG = 1
j = 0
for ng in range(nG):
if ng:
mainSizer.Add(wx.StaticText(self.panel,label=" for (0,0,0)+1'"),0)
j = 0
for ic,cent in enumerate(cents):
Cent = np.zeros(3)
if cent:
cent = cent.strip(' (').strip(')+\n')
Cent = np.array(eval(cent)[:3])
# Cent = np.array(Cents[ic])
if ic:
if cent: cent = cent.strip(' (').strip(')+\n')
label = ' for (%s)+'%(cent)
if ng: #test for gray operators
label += "1'"
mainSizer.Add(wx.StaticText(self.panel,label=label),0)
tableSizer = wx.FlexGridSizer(0,2*ncol+3,0,0)
for item in self.table:
if ')' not in item:
continue
flds = item.split(')')[1]
tableSizer.Add(wx.StaticText(self.panel,label=' (%2d) '%(j+1)),0,WACV)
flds = flds.replace(' ','').split(',')
for i,fld in enumerate(flds):
if i < ncol-1:
text = wx.StaticText(self.panel,label='%s, '%(fld))
else:
text = wx.StaticText(self.panel,label='%s '%(fld))
tableSizer.Add(text,0,WACV)
text = wx.StaticText(self.panel,label=' (%s) '%(self.names[j%Nnames]))
try:
if self.spins[j] < 0:
text.SetForegroundColour('Red')
item += ',-1'
else:
item += ',+1'
except IndexError:
print(self.spins,j,self.names[j%Nnames])
item += ',+1'
M,T,S = G2spc.MagText2MTS(item.split(')')[1].replace(' ',''),CIF=False)
T = (T+Cent)%1.
item = G2spc.MT2text([M,T],reverse=True)
if S > 0:
item += ',+1'
else:
item += ',-1'
self.PrintTable.append(item.replace(' ','').lower())
tableSizer.Add(text,0,WACV)
if not j%2:
tableSizer.Add((20,0))
j += 1
mainSizer.Add(tableSizer,0)
def OnPrintOps(event):
for item in self.PrintTable:
print(item)
btnsizer = wx.BoxSizer(wx.HORIZONTAL)
OKbtn = wx.Button(self.panel, wx.ID_OK)
btnsizer.Add(OKbtn)
printBtn = wx.Button(self.panel,label='Print Ops')
printBtn.Bind(wx.EVT_BUTTON, OnPrintOps)
btnsizer.Add(printBtn)
OKbtn.SetFocus()
mainSizer.Add((0,10))
mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER)
self.panel.SetSizer(mainSizer)
self.panel.SetAutoLayout(True)
self.panel.SetScrollRate(10,10)
self.panel.SendSizeEvent()
[docs]
def Show(self):
'''Use this method after creating the dialog to post it
'''
self.ShowModal()
return
################################################################################
[docs]
class DisAglDialog(wx.Dialog):
'''Distance/Angle Controls input dialog. After
:meth:`ShowModal` returns, the results are found in
dict :attr:`self.data`, which is accessed using :meth:`GetData`.
:param wx.Frame parent: reference to parent frame (or None)
:param dict data: a dict containing the current
search ranges or an empty dict, which causes default values
to be used.
Will be used to set element `DisAglCtls` in
:ref:`Phase Tree Item <Phase_table>`
:param dict default: A dict containing the default
search ranges for each element.
:param bool Reset: if True (default), show Reset button
:param bool Angle: if True (default), show angle radii
'''
def __init__(self,parent,data,default,Reset=True,Angle=True):
text = 'Distance Angle Controls'
if not Angle:
text = 'Distance Controls'
wx.Dialog.__init__(self,parent,wx.ID_ANY,text,
pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
self.default = default
self.Reset = Reset
self.Angle = Angle
self.panel = None
self._default(data,self.default)
self.Draw(self.data)
self.CenterOnParent()
def _default(self,data,default):
'''Set starting values for the search values, either from
the input array or from defaults, if input is null
'''
if data:
self.data = copy.deepcopy(data) # don't mess with originals
else:
self.data = {}
self.data['Name'] = default['Name']
self.data['Factors'] = [0.85,0.85]
self.data['AtomTypes'] = default['AtomTypes']
self.data['BondRadii'] = default['BondRadii'][:]
self.data['AngleRadii'] = default['AngleRadii'][:]
[docs]
def Draw(self,data):
'''Creates the contents of the dialog. Normally called
by :meth:`__init__`.
'''
if self.panel: self.panel.Destroy()
self.panel = wx.Panel(self)
mainSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add(wx.StaticText(self.panel,-1,'Controls for phase '+data['Name']),0,wx.LEFT,10)
mainSizer.Add((10,10),1)
ncol = 3
if not self.Angle:
ncol=2
radiiSizer = wx.FlexGridSizer(0,ncol,5,5)
radiiSizer.Add(wx.StaticText(self.panel,-1,' Type'),0,WACV)
radiiSizer.Add(wx.StaticText(self.panel,-1,'Bond radii'),0,WACV)
if self.Angle:
radiiSizer.Add(wx.StaticText(self.panel,-1,'Angle radii'),0,WACV)
self.objList = {}
for id,item in enumerate(self.data['AtomTypes']):
radiiSizer.Add(wx.StaticText(self.panel,-1,' '+item),0,WACV)
bRadii = ValidatedTxtCtrl(self.panel,data['BondRadii'],id,nDig=(10,3))
radiiSizer.Add(bRadii,0,WACV)
if self.Angle:
aRadii = ValidatedTxtCtrl(self.panel,data['AngleRadii'],id,nDig=(10,3))
radiiSizer.Add(aRadii,0,WACV)
mainSizer.Add(radiiSizer,0,wx.EXPAND)
Names = ['Bond']
factorSizer = wx.FlexGridSizer(0,2,5,5)
if self.Angle:
Names = ['Bond','Angle']
for i,name in enumerate(Names):
factorSizer.Add(wx.StaticText(self.panel,-1,name+' search factor'),0,WACV)
bondFact = ValidatedTxtCtrl(self.panel,data['Factors'],i,nDig=(10,3))
factorSizer.Add(bondFact)
mainSizer.Add(factorSizer,0,wx.EXPAND)
OkBtn = wx.Button(self.panel,-1,"Ok")
OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
btnSizer.Add((20,20),1)
btnSizer.Add(OkBtn)
if self.Reset:
ResetBtn = wx.Button(self.panel,-1,'Reset')
ResetBtn.Bind(wx.EVT_BUTTON, self.OnReset)
btnSizer.Add(ResetBtn)
btnSizer.Add((20,20),1)
mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
self.panel.SetSizer(mainSizer)
self.panel.Fit()
self.Fit()
[docs]
def GetData(self):
'Returns the values from the dialog'
return self.data
[docs]
def OnOk(self,event):
'Called when the OK button is pressed'
parent = self.GetParent()
if parent is not None: parent.Raise()
self.EndModal(wx.ID_OK)
[docs]
def OnReset(self,event):
'Called when the Reset button is pressed'
data = {}
self._default(data,self.default)
wx.CallAfter(self.Draw,self.data)
################################################################################
[docs]
class ShowLSParms(wx.Dialog):
'''Create frame to show least-squares parameters
'''
def __init__(self,G2frame,title,parmDict,varyList,fullVaryList,
Controls, size=(650,430)):
wx.Dialog.__init__(self,G2frame,wx.ID_ANY,title,size=size,
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
self.parmChoice = 'Phase'
self.G2frame = G2frame
self.parmDict = parmDict
self.varyList = varyList
self.fullVaryList = fullVaryList
self.Controls = Controls
self.choiceDict = {}
parmFrozen = Controls.get('parmFrozen',{})
if G2frame.testSeqRefineMode():
frozenList = set()
for h in parmFrozen:
if h == 'FrozenList': continue
frozenList = frozenList.union(parmFrozen[h])
self.frozenList = list(frozenList)
elif 'FrozenList' in parmFrozen:
self.frozenList = copy.copy(parmFrozen['FrozenList'])
else:
self.frozenList = []
# make lists of variables of different types along with lists of parameter names, histogram #s, phase #s,...
self.parmNames = sorted(list(parmDict.keys()))
splitNames = [item.split(':') for item in self.parmNames if len(item) > 3 and not isinstance(self.parmDict[item],str)]
globNames = [':'.join(item) for item in splitNames if not item[0] and not item[1]]
if len(globNames):
self.choiceDict['Global'] = G2obj.SortVariables(globNames)
self.globVars = sorted(list(set([' ',]+[item[2] for item in splitNames if not item[0] and not item[1]])))
hisNames = [':'.join(item) for item in splitNames if not item[0] and item[1]]
self.choiceDict['Histogram'] = G2obj.SortVariables(hisNames)
self.hisNums = sorted(list(set([int(item.split(':')[1]) for item in hisNames])))
self.hisNums = ['*',]+[str(item) for item in self.hisNums]
self.hisVars = sorted(list(set([' ',]+[item[2] for item in splitNames if not item[0]])))
phasNames = [':'.join(item) for item in splitNames if not item[1] and not item[2].startswith('is')]
self.choiceDict['Phase'] = G2obj.SortVariables(phasNames)
self.phasNums = sorted(['*',]+list(set([item.split(':')[0] for item in phasNames])))
if '' in self.phasNums: self.phasNums.remove('')
self.phasVars = sorted(list(set([' ',]+[item[2] for item in splitNames if not item[1] and not item[2].startswith('is')])))
hapNames = [':'.join(item) for item in splitNames if item[0] and item[1]]
self.choiceDict['Phase/Histo'] = G2obj.SortVariables(hapNames)
self.hapVars = sorted(list(set([' ',]+[item[2] for item in splitNames if item[0] and item[1]])))
self.hisNum = '*'
self.phasNum = '*'
self.varName = ' '
self.listSel = 'Refined'
self.DrawPanel()
#if GSASIIpath.GetConfigValue('debug'):
# print('repaintScrollTbl',time.time()-start)
[docs]
def DrawPanel(self):
'''Draws the contents of the entire dialog. Called initially & when radio buttons are pressed
'''
def _OnParmSel(event):
'New parameter type, reset var name choice as list changes'
self.parmChoice = parmSel.GetStringSelection()
if varSel:
varSel.SetSelection(0)
self.varName = ' '
wx.CallLater(100,self.DrawPanel)
def OnPhasSel(event):
'phase has been selected'
event.Skip()
self.phasNum = phasSel.GetValue()
if varSel:
try:
varSel.SetSelection(varSel.GetItems().index(self.varName))
except:
varSel.SetSelection(0)
self.varName = ' '
wx.CallAfter(self.repaintScrollTbl)
def OnHistSel(event):
'histogram has been selected'
event.Skip()
self.hisNum = histSel.GetValue()
if varSel:
try:
varSel.SetSelection(varSel.GetItems().index(self.varName))
except:
varSel.SetSelection(0)
self.varName = ' '
wx.CallAfter(self.repaintScrollTbl)
def OnVarSel(event):
'parameter name has been selected'
event.Skip()
self.varName = varSel.GetValue()
if phasSel:
try:
phasSel.SetSelection(phasSel.GetItems().index(self.phasNum))
except:
phasSel.SetSelection(0)
self.phasNum = '*'
if histSel:
try:
histSel.SetSelection(histSel.GetItems().index(self.hisNum))
except:
histSel.SetSelection(0)
self.hisNum = '*'
wx.CallAfter(self.repaintScrollTbl)
def OnListSel(event):
self.listSel = listSel.GetStringSelection()
wx.CallLater(100,self.DrawPanel)
def OnVarSpin(event):
'''Respond when any of the SpinButton widgets are pressed'''
event.Skip()
Spinner = event.GetEventObject()
move = Spinner.GetValue()
Spinner.SetValue(0)
varSel,binding = self.SpinDict[Spinner.GetId()]
i = varSel.GetSelection() - move
if i < 0:
i = varSel.GetCount()-1
elif i >= varSel.GetCount():
i = 0
varSel.SetSelection(i)
wx.CallLater(100,binding,event)
def AddSpinner(varSizer,label,SelCtrl,binding):
'''Add a label and a SpinButton to a Combo widget (SelCtrl)
Saves a pointer to the combo widget and the callback used by that widget
'''
SelCtrl.Bind(wx.EVT_COMBOBOX,binding)
varSizer.Add(wx.StaticText(self,label=label))
varSelSizer = wx.BoxSizer(wx.HORIZONTAL)
varSelSizer.Add(SelCtrl,0)
varSpin = wx.SpinButton(self,style=wx.SP_VERTICAL)
varSpin.SetValue(0)
varSpin.SetRange(-1,1)
varSpin.Bind(wx.EVT_SPIN, OnVarSpin)
self.SpinDict[varSpin.GetId()] = SelCtrl,binding
varSelSizer.Add(varSpin,0)
varSizer.Add(varSelSizer,0)
if self.GetSizer(): self.GetSizer().Clear(True)
self.SpinDict = {}
mainSizer = wx.BoxSizer(wx.VERTICAL)
num = len(self.varyList)
mainSizer.Add(wx.StaticText(self,label='View Parameters in Project'),0,wx.ALIGN_CENTER)
parmSizer = wx.BoxSizer(wx.HORIZONTAL)
parmSizer.Add(wx.StaticText(self,label=' Number of refined variables: {}'.format(num)),0,wx.ALIGN_LEFT)
if len(self.varyList) != len(self.fullVaryList):
num = len(self.fullVaryList) - len(self.varyList)
parmSizer.Add(wx.StaticText(self,label=
' + {} varied via constraints'.format(
len(self.fullVaryList) - len(self.varyList))
))
parmFrozen = self.Controls.get('parmFrozen',{})
fcount = 0
if self.G2frame.testSeqRefineMode():
for h in parmFrozen:
if h == 'FrozenList': continue
fcount += len(parmFrozen[h])
elif 'FrozenList' in parmFrozen:
fcount = len(parmFrozen['FrozenList'])
if fcount:
parmSizer.Add(wx.StaticText(self,label=
' - {} frozen variables'.format(fcount)))
mainSizer.Add(parmSizer)
choice = ['Phase','Phase/Histo','Histogram']
if 'Global' in self.choiceDict:
choice += ['Global',]
parmSizer = wx.BoxSizer(wx.HORIZONTAL)
parmSel = wx.RadioBox(self,wx.ID_ANY,'Parameter type:',choices=choice,
majorDimension=1,style=wx.RA_SPECIFY_COLS)
parmSel.Bind(wx.EVT_RADIOBOX,_OnParmSel)
parmSel.SetStringSelection(self.parmChoice)
parmSizer.Add(parmSel,0)
selectionsSizer = wx.BoxSizer(wx.VERTICAL)
varSizer = wx.BoxSizer(wx.VERTICAL)
varSel = None
if self.parmChoice != 'Global':
if self.parmChoice in ['Phase',]:
varSel = wx.ComboBox(self,choices=self.phasVars,value=self.varName,
style=wx.CB_READONLY|wx.CB_DROPDOWN)
elif self.parmChoice in ['Histogram',]:
varSel = wx.ComboBox(self,choices=self.hisVars,value=self.varName,
style=wx.CB_READONLY|wx.CB_DROPDOWN)
elif self.parmChoice in ['Phase/Histo',]:
varSel = wx.ComboBox(self,choices=self.hapVars,value=self.varName,
style=wx.CB_READONLY|wx.CB_DROPDOWN)
AddSpinner(varSizer,'Parameter',varSel,OnVarSel)
selectionsSizer.Add(varSizer,0)
varSizer = wx.BoxSizer(wx.HORIZONTAL)
phasSel = None
if self.parmChoice in ['Phase','Phase/Histo'] and len(self.phasNums) > 1:
numSizer = wx.BoxSizer(wx.VERTICAL)
phasSel = wx.ComboBox(self,choices=self.phasNums,value=self.phasNum,
style=wx.CB_READONLY|wx.CB_DROPDOWN,size=(50,-1))
AddSpinner(numSizer,'Phase',phasSel,OnPhasSel)
varSizer.Add(numSizer)
histSel = None
if self.parmChoice in ['Histogram','Phase/Histo'] and len(self.hisNums) > 1:
numSizer = wx.BoxSizer(wx.VERTICAL)
histSel = wx.ComboBox(self,choices=self.hisNums,value=self.hisNum,
style=wx.CB_READONLY|wx.CB_DROPDOWN,size=(50,-1))
AddSpinner(numSizer,'Histogram',histSel,OnHistSel)
varSizer.Add(numSizer)
selectionsSizer.Add(varSizer,0)
parmSizer.Add(selectionsSizer,0)
refChoices = ['All','Refined']
txt = ('"R" indicates a refined variable\n'+
'"C" value generated from a constraint')
if fcount:
refChoices += ['Frozen']
txt += '\n"F" indicates a variable that is Frozen due to exceeding min/max'
listSel = wx.RadioBox(self,wx.ID_ANY,'Refinement Status:',
choices=refChoices,
majorDimension=0,style=wx.RA_SPECIFY_COLS)
listSel.SetStringSelection(self.listSel)
listSel.Bind(wx.EVT_RADIOBOX,OnListSel)
parmSizer.Add(listSel,0,wx.CENTER|wx.ALL,15)
mainSizer.Add(parmSizer,0)
self.countSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add(self.countSizer)
self.headSizer = wx.BoxSizer(wx.HORIZONTAL) # non-scrolling header
mainSizer.Add(self.headSizer,0)
self.varBox = VirtualVarBox(self)
mainSizer.Add(self.varBox,1,wx.ALL|wx.EXPAND,1)
mainSizer.Add(
wx.StaticText(self,label=txt),0, wx.ALL,0)
btnsizer = wx.BoxSizer(wx.HORIZONTAL) # make Close button
btn = wx.Button(self, wx.ID_CLOSE,"Close")
btn.Bind(wx.EVT_BUTTON,self._onClose)
btnsizer.Add(btn)
mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
self.SetSizer(mainSizer)
wx.CallAfter(self.repaintScrollTbl)
def _onClose(self,event):
self.EndModal(wx.ID_CANCEL)
[docs]
class VirtualVarBox(wx.ListCtrl):
'''This is used to construct the parameter list display for viewing
least squares parameters
'''
def __init__(self, parent):
self.parmWin = parent
#patch (added Oct 2020) convert variable names for parm limits to G2VarObj
G2obj.patchControls(self.parmWin.Controls)
# end patch
wx.ListCtrl.__init__(
self, parent, -1,
style=wx.LC_REPORT|wx.LC_VIRTUAL|wx.LC_HRULES|wx.LC_VRULES
)
for i,(lbl,wid) in enumerate(zip(
('#', "Parameter", "Ref", 'Log', "Value", "Min", "Max", "Explanation"),
(40 , 125 , 30 , 30 , 75 , 75 , 75 , 700),)):
self.InsertColumn(i, lbl)
self.SetColumnWidth(i, wid)
self.SetItemCount(0)
try:
self.attr1 = wx.ItemAttr()
except:
self.attr1 = wx.ListItemAttr() # deprecated in wx4.1
self.attr1.SetBackgroundColour((255,255,150))
self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnRowSelected)
self.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnRowRightClick)
def SetContents(self,parent):
self.varList = []
for name in parent.choiceDict[parent.parmChoice]:
if isinstance(parent.parmDict[name],str): continue
if 'Refined' in parent.listSel and (name not in parent.fullVaryList
) and (name not in parent.varyList):
continue
if 'Frozen' in parent.listSel and not (
name in self.parmWin.fullVaryList and
name in self.parmWin.frozenList):
continue
if 'Phase' in parent.parmChoice:
if parent.phasNum != '*' and name.split(':')[0] != parent.phasNum: continue
if 'Histo' in parent.parmChoice:
if parent.hisNum != '*' and name.split(':')[1] != parent.hisNum: continue
if (parent.varName != ' ') and (parent.varName not in name): continue
self.varList.append(name)
#oldlen = self.GetItemCount()
self.SetItemCount(len(self.varList))
[docs]
def OnRowRightClick(self, event, row=None):
'''This adds or removes a variable from the list of logged
paremeters.
When a row is right-clicked this seems to be called twice, once
event.GetIndex() of -1, then OnRowSelected is called and then this
is called again, but where event.GetIndex() is the row that was
selected.
'''
def RightClickClear():
self.RightClick = False
row = event.GetIndex()
self.RightClick = True # inhibit use of OnRowSelected
if row < 0: return
var = self.varList[row]
self.parmWin.Controls['LoggedVars'] = self.parmWin.Controls.get('LoggedVars',[])
if var in self.parmWin.Controls['LoggedVars']:
self.parmWin.Controls['LoggedVars'].remove(var)
else:
self.parmWin.Controls['LoggedVars'].append(var)
#if GSASIIpath.GetConfigValue('debug'): print(self.parmWin.Controls['LoggedVars'])
self.parmWin.SendSizeEvent() # redraws the window
wx.CallAfter(RightClickClear)
[docs]
def OnRowSelected(self, event, row=None):
'Creates an edit window when a parameter is selected'
def ResetFrozen(event):
'''release a frozen parameter (from all histograms in the case of a
sequential fit).
'''
if name in self.parmWin.frozenList:
del self.parmWin.frozenList[self.parmWin.frozenList.index(name)]
parmFrozen = self.parmWin.Controls.get('parmFrozen',{})
if self.parmWin.G2frame.testSeqRefineMode():
for h in parmFrozen:
if h == 'FrozenList': continue
if name in parmFrozen[h]:
del parmFrozen[h][parmFrozen[h].index(name)]
elif 'FrozenList' in parmFrozen:
if name in parmFrozen['FrozenList']:
del parmFrozen['FrozenList'][parmFrozen['FrozenList'].index(name)]
dlg.EndModal(wx.ID_CANCEL)
self.parmWin.SendSizeEvent()
def delM(event):
'Get event info & prepare to delete Max or Min limit'
if hasattr(event.EventObject,'max'):
d = self.parmWin.Controls['parmMaxDict']
else:
d = self.parmWin.Controls['parmMinDict']
# close sub-dialog then delete item and redraw
dlg.EndModal(wx.ID_OK)
wx.CallAfter(delMafter,d,name)
def delMafter(d,name):
'Delete Max or Min limit once dialog is deleted'
key,val = G2obj.prmLookup(name,d) # is this a wild-card?
if val is not None:
del d[key]
self.OnRowSelected(None, row)
def AddM(event):
'Get event info & add a Max or Min limit'
if hasattr(event.EventObject,'max'):
d = self.parmWin.Controls['parmMaxDict']
else:
d = self.parmWin.Controls['parmMinDict']
# close sub-dialog then delete item and redraw
dlg.EndModal(wx.ID_OK)
wx.CallAfter(AddMafter,d,name)
def AddMafter(d,name):
'Add a Max or Min limit & redraw'
try:
d[G2obj.G2VarObj(name)] = float(value)
except:
pass
self.OnRowSelected(None, row)
def SetWild(event):
'Get event info & prepare to set/clear item as wildcard'
if hasattr(event.EventObject,'max'):
d = self.parmWin.Controls['parmMaxDict']
else:
d = self.parmWin.Controls['parmMinDict']
ns = name.split(':')
if hasattr(event.EventObject,'hist'):
ns[1] = '*'
else:
ns[3] = '*'
wname = ':'.join(ns)
# close sub-dialog then delete item and redraw
dlg.EndModal(wx.ID_OK)
wx.CallAfter(SetWildAfter,d,name,wname,event.EventObject.GetValue())
def SetWildAfter(d,name,wname,mode):
'Set/clear item as wildcard & delete old name(s), redraw'
n,val = G2obj.prmLookup(name,d) # is this a wild-card?
if val is None:
print('Error: Limit for parameter {} not found. Should not happen'.format(name))
return
if mode: # make wildcard
for n in list(d.keys()): # delete names matching wildcard
if str(n) == wname: continue
if n == wname: # respects wildcards
del d[n]
d[G2obj.G2VarObj(wname)] = val
else:
del d[n]
d[G2obj.G2VarObj(name)] = val
self.OnRowSelected(None, row)
# start of OnRowSelected
if getattr(self,'RightClick',False): # ignore calls generated by right-click
self.RightClick = False
return
self.RightClick = False
if event is not None:
row = event.Index
elif row is None:
print('Error: row and event should not both be None!')
return
name = self.varList[row]
dlg = wx.Dialog(self.parmWin,wx.ID_ANY,'Parameter {} info'.format(name),
size=(600,-1),
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
mainSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add((5,5))
subSizer = wx.BoxSizer(wx.HORIZONTAL)
subSizer.Add((-1,-1),1,wx.EXPAND)
try:
value = G2fil.FormatSigFigs(self.parmWin.parmDict[name])
except ValueError:
value = str(self.parmWin.parmDict[name])
except TypeError:
value = str(self.parmWin.parmDict[name])+' -?' # unexpected
subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,
'Parameter "{}" information and settings. Value={}'
.format(name,value)))
subSizer.Add((-1,-1),1,wx.EXPAND)
mainSizer.Add(subSizer,0,wx.EXPAND,0)
mainSizer.Add((0,10))
v = G2obj.getVarDescr(name)
if v is not None and v[-1] is not None:
txt = G2obj.fmtVarDescr(name)
if txt:
#txt = txt.replace('Ph=','Phase: ')
#txt = txt.replace('Pwd=','Histogram: ')
txtwid = wx.StaticText(dlg,wx.ID_ANY,'Parameter meaning is "'+txt+'"')
txtwid.Wrap(580)
mainSizer.Add(txtwid)
mainSizer.Add((0,10))
freezebtn = None
if name in self.parmWin.fullVaryList and name in self.parmWin.frozenList:
msg = "Parameter {} exceeded limits and has been frozen".format(name)
freezebtn = wx.Button(dlg, wx.ID_ANY,'Unfreeze')
freezebtn.Bind(wx.EVT_BUTTON, ResetFrozen)
elif name in self.parmWin.varyList:
msg = "Parameter {} is refined".format(name)
elif name in self.parmWin.fullVaryList:
msg = "Parameter {} is refined via a constraint".format(name)
else:
msg = ""
if msg:
subSizer = wx.BoxSizer(wx.HORIZONTAL)
subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,msg),0,wx.CENTER)
if freezebtn:
subSizer.Add(freezebtn,0,wx.ALL|wx.CENTER,5)
mainSizer.Add(subSizer,0)
# draw min value widgets
mainSizer.Add((-1,10),0)
if name not in self.parmWin.varyList and name in self.parmWin.fullVaryList:
mainSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Limits not allowed on constrained variables'),0)
for key in 'parmMinDict','parmMaxDict':
d = self.parmWin.Controls[key]
n,v = G2obj.prmLookup(name,d)
if v is not None and str(n) == name:
try: # strange hard to reproduce problem with this not working
del d[n]
except:
if GSASIIpath.GetConfigValue('debug'):
print('debug: failed to delete ',name,'in',key)
else:
n,val = G2obj.prmLookup(name,self.parmWin.Controls['parmMinDict']) # is this a wild-card?
if val is None:
addMbtn = wx.Button(dlg, wx.ID_ANY,'Add Lower limit')
addMbtn.Bind(wx.EVT_BUTTON, AddM)
mainSizer.Add(addMbtn,0)
else:
subSizer = wx.BoxSizer(wx.HORIZONTAL)
subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Minimum limit'),0,wx.CENTER)
subSizer.Add(ValidatedTxtCtrl(dlg,self.parmWin.Controls['parmMinDict'],n,nDig=(10,2,'g')),0,WACV)
delMbtn = wx.Button(dlg, wx.ID_ANY,'Delete',style=wx.BU_EXACTFIT)
subSizer.Add((5,-1))
subSizer.Add(delMbtn,0,WACV)
delMbtn.Bind(wx.EVT_BUTTON, delM)
if name.split(':')[1]: # is this using a histogram?
subSizer.Add((5,-1))
wild = wx.CheckBox(dlg,wx.ID_ANY,label='Match all histograms ')
wild.SetValue(str(n).split(':')[1] == '*')
wild.Bind(wx.EVT_CHECKBOX,SetWild)
wild.hist = True
subSizer.Add(wild,0,WACV)
elif len(name.split(':')) > 3:
subSizer.Add((5,-1))
wild = wx.CheckBox(dlg,wx.ID_ANY,label='Match all atoms ')
wild.SetValue(str(n).split(':')[3] == '*')
wild.Bind(wx.EVT_CHECKBOX,SetWild)
subSizer.Add(wild,0,WACV)
mainSizer.Add(subSizer,0)
# draw max value widgets
mainSizer.Add((-1,10),0)
n,val = G2obj.prmLookup(name,self.parmWin.Controls['parmMaxDict']) # is this a wild-card?
if val is None:
addMbtn = wx.Button(dlg, wx.ID_ANY,'Add Upper limit')
addMbtn.Bind(wx.EVT_BUTTON, AddM)
addMbtn.max = True
mainSizer.Add(addMbtn,0)
else:
subSizer = wx.BoxSizer(wx.HORIZONTAL)
subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Maximum limit'),0,wx.CENTER)
subSizer.Add(ValidatedTxtCtrl(dlg,self.parmWin.Controls['parmMaxDict'],n,nDig=(10,2,'g')),0,WACV)
delMbtn = wx.Button(dlg, wx.ID_ANY,'Delete',style=wx.BU_EXACTFIT)
subSizer.Add((5,-1))
subSizer.Add(delMbtn,0,WACV)
delMbtn.Bind(wx.EVT_BUTTON, delM)
delMbtn.max = True
if name.split(':')[1]: # is this using a histogram?
subSizer.Add((5,-1))
wild = wx.CheckBox(dlg,wx.ID_ANY,label='Match all histograms ')
wild.SetValue(str(n).split(':')[1] == '*')
wild.Bind(wx.EVT_CHECKBOX,SetWild)
wild.max = True
wild.hist = True
subSizer.Add(wild,0,WACV)
elif len(name.split(':')) > 3:
subSizer.Add((5,-1))
wild = wx.CheckBox(dlg,wx.ID_ANY,label='Match all atoms ')
wild.SetValue(str(n).split(':')[3] == '*')
wild.Bind(wx.EVT_CHECKBOX,SetWild)
wild.max = True
subSizer.Add(wild,0,WACV)
mainSizer.Add(subSizer,0)
self.parmWin.Controls['LoggedVars'] = self.parmWin.Controls.get('LoggedVars',[])
def LogLblButton(button):
if name in self.parmWin.Controls['LoggedVars']:
lbl = 'Remove from Logging'
else:
lbl = 'Log var'
button.SetLabel(lbl)
def LogAddRemove(event):
if name in self.parmWin.Controls['LoggedVars']:
self.parmWin.Controls['LoggedVars'].remove(name)
else:
self.parmWin.Controls['LoggedVars'].append(name)
LogLblButton(event.GetEventObject())
dlg.Layout()
self.parmWin.SendSizeEvent()
mainSizer.Add((-1,10))
addbtn = wx.Button(dlg, wx.ID_ANY,'')
LogLblButton(addbtn)
addbtn.Bind(wx.EVT_BUTTON, LogAddRemove)
mainSizer.Add(addbtn,0)
btnsizer = wx.StdDialogButtonSizer()
OKbtn = wx.Button(dlg, wx.ID_OK)
OKbtn.SetDefault()
OKbtn.Bind(wx.EVT_BUTTON,lambda event: dlg.EndModal(wx.ID_OK))
btnsizer.AddButton(OKbtn)
btnsizer.Realize()
mainSizer.Add((-1,5),1,wx.EXPAND,1)
mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER,0)
mainSizer.Add((-1,10))
dlg.SetSizer(mainSizer)
dlg.CenterOnParent()
if dlg.ShowModal() != wx.ID_OK: # if not OK, destroy & reopen
dlg.Destroy()
wx.CallAfter(self.OnRowSelected, None, row)
return
dlg.Destroy()
self.parmWin.SendSizeEvent()
#-----------------------------------------------------------------
# Callbacks to display info in table
[docs]
def OnGetItemText(self, item, col):
name = self.varList[item]
atmParNam = None
if name.split(':')[2].startswith('dA'):
atmParNam = name.replace(':dA',':A')
if col == 0:
return str(item)
elif col == 1:
if atmParNam: return atmParNam
return name
elif col == 2:
if name in self.parmWin.fullVaryList and name in self.parmWin.frozenList:
return "F"
elif name in self.parmWin.varyList:
return "R"
elif name in self.parmWin.fullVaryList:
return "C"
return ""
elif col == 3:
logged = self.parmWin.Controls.get('LoggedVars',[])
if GSASIIpath.GetConfigValue('LogAllVars') and not logged:
if name in self.parmWin.varyList:
if self.parmWin.varyList.index(name) < 50:
return 'A'
else:
return ' '
return ' '
elif name in logged:
return 'Y'
return 'N'
elif col == 4:
if atmParNam: name = atmParNam
try:
value = G2fil.FormatSigFigs(self.parmWin.parmDict[name])
except ValueError:
value = str(self.parmWin.parmDict[name]) #array!
except TypeError:
value = str(self.parmWin.parmDict[name])+' -?' # unexpected
return value
elif col == 5 or col == 6: # min/max value
if col == 5: # min
d = self.parmWin.Controls['parmMinDict']
else:
d = self.parmWin.Controls['parmMaxDict']
n,val = G2obj.prmLookup(name,d)
if val is None: return ""
try:
return G2fil.FormatSigFigs(val,8)
except TypeError:
return "?"
elif col == 7:
v = G2obj.getVarDescr(name)
if v is not None and v[-1] is not None:
txt = G2obj.fmtVarDescr(name)
if txt: return txt
return ""
else:
return "?"
[docs]
def OnGetItemAttr(self, item):
name = self.varList[item]
if name in self.parmWin.varyList and name in self.parmWin.frozenList:
return self.attr1
else:
return None
##### Customized Grid Support ################################################################################
[docs]
class GSGrid(wg.Grid):
'''Basic wx.Grid implementation
'''
def __init__(self, parent, name=''):
wg.Grid.__init__(self,parent,-1,name=name)
if hasattr(parent.TopLevelParent,'currentGrids'):
parent.TopLevelParent.currentGrids.append(self) # save a reference to the grid in the Frame
self.SetScrollRate(0,0) #GSAS-II grids have no scroll bars by default
def Clear(self):
wg.Grid.ClearGrid(self)
def SetCellReadOnly(self,r,c,readonly=True):
self.SetReadOnly(r,c,isReadOnly=readonly)
def SetCellStyle(self,r,c,color="white",readonly=True):
self.SetCellBackgroundColour(r,c,color)
self.SetReadOnly(r,c,isReadOnly=readonly)
[docs]
def SetTable(self, table, *args, **kwargs):
'''Overrides the standard SetTable method with one that uses
GridFractionEditor for all numeric columns (unless useFracEdit
is false)
'''
setFracEdit = kwargs.get('useFracEdit',True)
if 'useFracEdit' in kwargs: del kwargs['useFracEdit']
wg.Grid.SetTable(self, table, *args, **kwargs)
if setFracEdit:
for i,t in enumerate(table.dataTypes):
if not t.startswith(wg.GRID_VALUE_FLOAT): continue
attr = wx.grid.GridCellAttr()
attr.IncRef()
attr.SetEditor(GridFractionEditor(self))
self.SetColAttr(i, attr)
def GetSelection(self):
#this is to satisfy structure drawing stuff in G2plt when focus changes
return None
[docs]
def completeEdits(self):
'complete any outstanding edits'
if self.IsCellEditControlEnabled(): # complete any grid edits in progress
self.SaveEditControlValue()
self.HideCellEditControl()
self.DisableCellEditControl()
################################################################################
[docs]
class Table(wg.GridTableBase):
'''Basic data table for use with GSgrid
'''
def __init__(self, data=[], rowLabels=None, colLabels=None, types = None):
wg.GridTableBase.__init__(self)
self.colLabels = colLabels
self.rowLabels = rowLabels
self.dataTypes = types
self.data = data
[docs]
def AppendRows(self, numRows=1):
self.data.append([])
return True
[docs]
def CanGetValueAs(self, row, col, typeName):
if self.dataTypes:
colType = self.dataTypes[col].split(':')[0]
if typeName == colType:
return True
else:
return False
else:
return False
[docs]
def CanSetValueAs(self, row, col, typeName):
return self.CanGetValueAs(row, col, typeName)
def DeleteRow(self,pos):
data = self.GetData()
self.SetData([])
new = []
for irow,row in enumerate(data):
if irow != pos:
new.append(row)
self.SetData(new)
[docs]
def GetColLabelValue(self, col):
if self.colLabels:
return self.colLabels[col]
def GetData(self):
data = []
for row in range(self.GetNumberRows()):
data.append(self.GetRowValues(row))
return data
[docs]
def GetNumberCols(self):
try:
return len(self.colLabels)
except TypeError:
return None
[docs]
def GetNumberRows(self):
return len(self.data)
[docs]
def GetRowLabelValue(self, row):
if self.rowLabels:
return self.rowLabels[row]
def GetColValues(self, col):
data = []
for row in range(self.GetNumberRows()):
data.append(self.GetValue(row, col))
return data
def GetRowValues(self, row):
data = []
for col in range(self.GetNumberCols()):
data.append(self.GetValue(row, col))
return data
[docs]
def GetTypeName(self, row, col):
try:
if self.data[row][col] is None:
return wg.GRID_VALUE_STRING
return self.dataTypes[col]
except (TypeError,IndexError):
return wg.GRID_VALUE_STRING
[docs]
def GetValue(self, row, col):
try:
if self.data[row][col] is None: return ""
return self.data[row][col]
except IndexError:
return None
[docs]
def InsertRows(self, pos, rows):
for row in range(rows):
self.data.insert(pos,[])
pos += 1
[docs]
def IsEmptyCell(self,row,col):
try:
return not self.data[row][col]
except IndexError:
return True
def OnKeyPress(self, event):
dellist = self.GetSelectedRows()
if event.GetKeyCode() == wx.WXK_DELETE and dellist:
grid = self.GetView()
for i in dellist: grid.DeleteRow(i)
[docs]
def SetColLabelValue(self, col, label):
numcols = self.GetNumberCols()
if col > numcols-1:
self.colLabels.append(label)
else:
self.colLabels[col]=label
def SetData(self,data):
for row in range(len(data)):
self.SetRowValues(row,data[row])
[docs]
def SetRowLabelValue(self, row, label):
self.rowLabels[row]=label
def SetRowValues(self,row,data):
self.data[row] = data
[docs]
def SetValue(self, row, col, value):
def innerSetValue(row, col, value):
try:
self.data[row][col] = value
except TypeError:
return
except IndexError: # has this been tested?
#print row,col,value
if self.GetNumberRows() == 0: return
# add a new row
if row > self.GetNumberRows():
self.data.append([''] * self.GetNumberCols())
elif col > self.GetNumberCols():
for row in range(self.GetNumberRows()): # bug fixed here
self.data[row].append('')
#print self.data
self.data[row][col] = value
innerSetValue(row, col, value)
################################################################################
[docs]
class GridFractionEditor(wg.PyGridCellEditor):
'''A grid cell editor class that allows entry of values as fractions as well
as sine and cosine values [as s() and c(), sin() or sind(), etc]. Any valid
Python expression will be evaluated.
The current value can be incremented, multiplied or divided by prefixing
an expression by +, * or / respectively.
'''
def __init__(self,grid):
if 'phoenix' in wx.version():
wg.GridCellEditor.__init__(self)
else:
wg.PyGridCellEditor.__init__(self)
[docs]
def Create(self, parent, id, evtHandler):
self._tc = wx.TextCtrl(parent, id, "")
self._tc.SetInsertionPoint(0)
self.SetControl(self._tc)
if evtHandler:
self._tc.PushEventHandler(evtHandler)
self._tc.Bind(wx.EVT_CHAR, self.OnChar)
[docs]
def SetSize(self, rect):
if 'phoenix' in wx.version():
self._tc.SetSize(rect.x, rect.y, rect.width+2, rect.height+2,
wx.SIZE_ALLOW_MINUS_ONE)
else:
self._tc.SetDimensions(rect.x, rect.y, rect.width+2, rect.height+2, wx.SIZE_ALLOW_MINUS_ONE)
[docs]
def BeginEdit(self, row, col, grid):
self.startValue = grid.GetTable().GetValue(row, col)
self._tc.SetValue(str(self.startValue))
self._tc.SetInsertionPointEnd()
self._tc.SetFocus()
self._tc.SetSelection(0, self._tc.GetLastPosition())
[docs]
def EndEdit(self, row, col, grid, oldVal=None):
changed = False
self.nextval = self.startValue
val = self._tc.GetValue().lower().strip()
val = val.replace(',','.') # allow , for decimal
if val != str(self.startValue):
changed = True
neg = False
mult = False
divide = False
add = False
if val.startswith('*'):
mult = True
val = val[1:]
elif val.startswith('/'):
divide = True
val = val[1:]
elif val.startswith('+'):
add = True
val = val[1:]
if val.startswith('-'):
neg = True
val = val[1:]
# allow old GSAS s20 and c20 etc for sind(20) and cosd(20)
if val.startswith('s') and '(' not in val:
val = 'sind('+val.strip('s')+')'
elif val.startswith('c') and '(' not in val:
val = 'cosd('+val.strip('c')+')'
if neg:
val = '-' + val
val = G2fil.FormulaEval(val)
if val is not None:
if mult:
self.nextval *= val
elif divide:
if val != 0: self.nextval /= val
elif add:
self.nextval += val
else:
self.nextval = val
else:
return None
if oldVal is None: # this arg appears in 2.9+; before, we should go ahead & change the table
grid.GetTable().SetValue(row, col, val) # update the table
# otherwise self.ApplyEdit gets called
self.startValue = ''
self._tc.SetValue('')
return changed
[docs]
def ApplyEdit(self, row, col, grid):
""" Called only in wx >= 2.9
Save the value of the control into the grid if EndEdit() returns as True
"""
grid.GetTable().SetValue(row, col, self.nextval) # update the table
[docs]
def Reset(self):
self._tc.SetValue(str(self.startValue))
self._tc.SetInsertionPointEnd()
[docs]
def Clone(self,grid):
return GridFractionEditor(grid)
[docs]
def StartingKey(self, evt):
self.OnChar(evt)
if evt.GetSkipped():
self._tc.EmulateKeyPress(evt)
def OnChar(self, evt):
key = evt.GetKeyCode()
if key < 32 or key >= 127: # outside printable ascii range; needed for backspace etc.
evt.Skip()
elif chr(key).lower() in '.+-*/0123456789cosind(),':
evt.Skip()
else:
evt.StopPropagation()
##### Get an output file or directory ################################################################################
[docs]
def askSaveFile(G2frame,defnam,extension,longFormatName,parent=None):
'''Ask the user to supply a file name
:param wx.Frame G2frame: The main GSAS-II window
:param str defnam: a default file name
:param str extension: the default file extension beginning with a '.'
:param str longFormatName: a description of the type of file
:param wx.Frame parent: the parent window for the dialog. Defaults
to G2frame.
:returns: a file name (str) or None if Cancel is pressed
'''
if not parent: parent = G2frame
pth = GetExportPath(G2frame)
#if GSASIIpath.GetConfigValue('debug'): print('debug: askSaveFile to '+pth)
dlg = wx.FileDialog(
parent, 'Input name for file to write', pth, defnam,
longFormatName+' (*'+extension+')|*'+extension,
wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT)
dlg.CenterOnParent()
try:
if dlg.ShowModal() == wx.ID_OK:
filename = dlg.GetPath()
G2frame.LastExportDir = os.path.split(filename)[0]
filename = os.path.splitext(filename)[0]+extension # make sure extension is correct
else:
filename = None
finally:
dlg.Destroy()
return filename
[docs]
def askSaveDirectory(G2frame):
'''Ask the user to supply a directory name. Path name is used as the
starting point for the next export path search.
:returns: a directory name (str) or None if Cancel is pressed
'''
pth = GetExportPath(G2frame)
dlg = wx.DirDialog(G2frame,'Input directory where file(s) will be written',pth,wx.DD_DEFAULT_STYLE)
dlg.CenterOnParent()
try:
if dlg.ShowModal() == wx.ID_OK:
filename = dlg.GetPath()
G2frame.LastExportDir = filename
else:
filename = None
finally:
dlg.Destroy()
return filename
##### Customized Notebook ################################################################################
[docs]
class GSNoteBook(wx.aui.AuiNotebook):
'''Notebook used in various locations; implemented with wx.aui extension
'''
def __init__(self, parent, name='',size = None,style=wxaui_NB_TOPSCROLL):
wx.aui.AuiNotebook.__init__(self, parent, style=style)
if size: self.SetSize(size)
self.parent = parent
self.PageChangeHandler = None
def PageChangeEvent(self,event):
pass
def Clear(self):
GSNoteBook.DeleteAllPages(self)
[docs]
def FindPage(self,name):
numPage = self.GetPageCount()
for page in range(numPage):
if self.GetPageText(page) == name:
return page
return None
[docs]
def ChangeSelection(self,page):
# in wx.Notebook ChangeSelection is like SetSelection, but it
# does not invoke the event related to pressing the tab button
# I don't see a way to do that in aui.
oldPage = self.GetSelection()
self.SetSelection(page)
return oldPage
# def __getattribute__(self,name):
# '''This method provides a way to print out a message every time
# that a method in a class is called -- to see what all the calls
# might be, or where they might be coming from.
# Cute trick for debugging!
# '''
# attr = object.__getattribute__(self, name)
# if hasattr(attr, '__call__'):
# def newfunc(*args, **kwargs):
# print('GSauiNoteBook calling %s' %attr.__name__)
# result = attr(*args, **kwargs)
# return result
# return newfunc
# else:
# return attr
#### Help support routines ################################################################################
[docs]
class MyHelp(wx.Menu):
'''
A class that creates the contents of a help menu.
The menu will start with two entries:
* 'Help on <helpType>': where helpType is a reference to an HTML page to
be opened
* About: opens an About dialog using OnHelpAbout. N.B. on the Mac this
gets moved to the App menu to be consistent with Apple style.
NOTE: for this to work properly with respect to system menus, the title
for the menu must be &Help, or it will not be processed properly:
::
menu.Append(menu=MyHelp(self,...),title="&Help")
'''
def __init__(self,frame,includeTree=False,morehelpitems=[]):
wx.Menu.__init__(self,'')
self.HelpById = {}
self.frame = frame
self.Append(wx.ID_ABOUT,'&About GSAS-II',
'Shows version and citation info')
frame.Bind(wx.EVT_MENU, self.OnHelpAbout, id=wx.ID_ABOUT)
if GSASIIpath.HowIsG2Installed().startswith('git'):
helpobj = self.Append(wx.ID_ANY,'&Check for updates\tCtrl+U',
'Updates to latest GSAS-II version')
if os.access(GSASIIpath.path2GSAS2, os.W_OK):
frame.Bind(wx.EVT_MENU, self.OnCheckUpdates, helpobj)
else:
helpobj.Enable(False)
helpobj = self.Append(wx.ID_ANY,'&Regress to old GSAS-II version',
'Installs previous GSAS-II version')
if os.access(GSASIIpath.path2GSAS2, os.W_OK):
frame.Bind(wx.EVT_MENU, self.OnSelectVersion, helpobj)
else:
helpobj.Enable(False)
if (GSASIIpath.HowIsG2Installed().startswith('git')
and GSASIIpath.GetConfigValue('debug')):
helpobj = self.Append(wx.ID_ANY,'Switch to/from branch',
'Switch to/from a GSAS-II development branch')
frame.Bind(wx.EVT_MENU, gitSelectBranch, helpobj)
# test if conda present?
helpobj = self.Append(wx.ID_ANY,'Add packages for more functionality',
'Install optional Python packages to provide more GSAS-II capabilities')
helpobj.Enable(bool(G2fil.condaRequestList))
frame.Bind(wx.EVT_MENU, SelectPkgInstall, id=helpobj.GetId())
# provide special help topic names for extra items in help menu
for lbl,indx in morehelpitems:
helpobj = self.Append(wx.ID_ANY,lbl,'')
frame.Bind(wx.EVT_MENU, self.OnHelpById, helpobj)
self.HelpById[helpobj.GetId()] = indx
# add help lookup(s) in GSAS-II Help
self.AppendSeparator()
if includeTree:
helpobj = self.Append(wx.ID_ANY,'Help on GSAS-II',
'Access web pages with information on GSAS-II')
frame.Bind(wx.EVT_MENU, self.OnHelpById, id=helpobj.GetId())
self.HelpById[helpobj.GetId()] = 'HelpIntro'
helpobj = self.Append(wx.ID_ANY,'Help on current data tree item\tF1',
'Access web page on selected item in tree')
frame.Bind(wx.EVT_MENU, self.OnHelpById, id=helpobj.GetId())
helpobj = self.Append(wx.ID_ANY,'Citation information',
'Show papers that GSAS-II users may wish to cite')
frame.Bind(wx.EVT_MENU, ShowCitations, id=helpobj.GetId())
[docs]
def OnHelpById(self,event):
'''Called when Help on... is pressed in a menu. Brings up a web page
for documentation. Uses the G2frame.dataWindow.helpKey value to select
what help is shown, unless a special help key value has been defined
for the calling menu id (via a lookup in self.HelpById[], which is used
for Tutorials & Overall help).
Note that G2frame.dataWindow.helpKey SelectDataTreeItem and reflects
the data tree item that has been selected.
Note that self here should be a child of the main window (G2frame)
where self.frame is G2frame
'''
try:
helpKey = self.frame.dataWindow.helpKey # look up help from helpKey in data window
#if GSASIIpath.GetConfigValue('debug'): print 'DBG_dataWindow help: key=',helpKey
except AttributeError:
helpKey = ''
if GSASIIpath.GetConfigValue('debug'): print('DBG_No helpKey for current dataWindow!')
helpType = self.HelpById.get(event.GetId(),helpKey) # see if there is a special help topic
#if GSASIIpath.GetConfigValue('debug'): print 'DBG_helpKey=',helpKey,' helpType=',helpType
if helpType == 'Tutorials':
dlg = OpenTutorial(self.frame)
dlg.ShowModal()
dlg.Destroy()
return
else:
ShowHelp(helpType,self.frame)
[docs]
def OnHelpAbout(self, event):
"Display an 'About GSAS-II' box"
try:
import wx.adv as wxadv # AboutBox moved here in Phoenix
except:
wxadv = wx
info = wxadv.AboutDialogInfo()
info.Name = 'GSAS-II'
info.SetVersion(GSASIIpath.getG2VersionInfo())
info.Developers = ['Robert B. Von Dreele','Brian H. Toby']
info.WebSite = ("https://gsasii.github.io","GSAS-II home page")
msg = '''Argonne National Laboratory
This product includes software developed
by the UChicago Argonne, LLC, as Operator
of Argonne National Laboratory.'''
info.Copyright = f'(c) {time.strftime("%Y")} {msg}'
msg = '''General Structure Analysis System-II (GSAS-II). Please cite as:
B.H. Toby & R.B. Von Dreele, J. Appl. Cryst.
46, 544-549 (2013)
Also see Help/"Citation information" for other works used in GSAS-II. Citations encourage scientists to make their software available.'''
# msg += '\n'
# for key in CitationDict:
# msg += f"\n * For {key} use cite:\n"
# msg += GetCite(key,wrap=50,indent=3)
info.Description = msg
wxadv.AboutBox(info)
[docs]
def OnCheckUpdates(self,event):
'''Check if the GSAS-II repository has an update for the current source files
and perform that update if requested.
'''
if GSASIIpath.HowIsG2Installed().startswith('git'):
gitCheckUpdates(self.frame)
else:
dlg = wx.MessageDialog(self.frame,
'Cannot update GSAS-II because it was not installed with a version control system or the VCS system could not be accessed.',
'No VCS',wx.OK)
dlg.ShowModal()
dlg.Destroy()
return
[docs]
def OnSelectVersion(self,event):
'''Allow the user to select a specific version of GSAS-II
'''
if GSASIIpath.HowIsG2Installed().startswith('git'):
gitSelectVersion(self.frame)
else:
dlg = wx.MessageDialog(self.frame,
'Cannot update GSAS-II because it was not installed with a version control system or the VCS system could not be accessed.',
'No VCS',wx.OK)
dlg.ShowModal()
dlg.Destroy()
return
[docs]
def ShowCitations(event):
'''Show all work that GSAS-II users may wish to cite
'''
parent = wx.GetApp().GetMainTopWindow()
def copy2clip(event):
'copy citation info to clipboard'
if wx.TheClipboard.Open():
wx.TheClipboard.SetData(wx.TextDataObject(msg))
wx.TheClipboard.Close()
else:
G2frame.ErrorDialog('Clipboard locked','Sorry, unable to access the clipboard, try again later. You might need to restart GSAS-II or reboot')
return
G2MessageBox(parent,
'Citation information placed in clipboard. ',
'Citations copied')
event.GetEventObject().GetParent().EndModal(wx.ID_OK)
msg = '''You are using GSAS-II. Please cite it as:
B.H. Toby & R.B. Von Dreele, J. Appl. Cryst. 46, 544-549 (2013).
Depending on what sections of the code you are using, you may wish to
cite some of the following works as well:'''
for key in CitationDict:
msg += f"\n\n * For {key} use cite:\n"
msg += GetCite(key,wrap=95,indent=6)
msg += '\n\nNote that your citations are one of the strongest ways you can say thank you to the\nscientists who make their software available to you.'
ShowScrolledInfo(parent,msg,header='Please Cite',
buttonlist=[
('Close', lambda event: event.GetEventObject().GetParent().EndModal(wx.ID_OK)),
('Copy to clipboard', copy2clip),
])
################################################################################
################################################################################
updateNoticeDict = {4919:True} # example: {1234:True, 5000:False}
'''A dict with versions that should be noted. The value associated with the
tag is if all older projects should show the warning, or only the first
to be opened.
'''
[docs]
def updateNotifier(G2frame,fileVersion):
'''Posts an update notice when a a specially tagged GSAS-II version
is seen for the first time. Versions to be tagged are set in global
updateNoticeDict; version info is found in file versioninfo.txt.
:param wx.Frame G2frame: GSAS-II main window
:param int fileVersion: version of GSAS-II used to create the current
.gpx file
'''
def tblLine(dlg,pixels=3):
'place line in table'
txtbox = wx.StaticText(dlg,wx.ID_ANY,'',size=(-1,pixels))
txtbox.SetBackgroundColour(wx.Colour(0,0,0))
tblSizer.Add(txtbox,0,wx.EXPAND)
txtbox = wx.StaticText(dlg,wx.ID_ANY,'',size=(-1,pixels))
txtbox.SetBackgroundColour(wx.Colour(0,0,0))
tblSizer.Add(txtbox,0,wx.EXPAND)
size = (700,500)
rev = GSASIIpath.GetVersionNumber()
try:
int(rev)
except:
pass
lastNotice = max(GSASIIpath.GetConfigValue('lastUpdateNotice',0),fileVersion)
show = None # first version number to show
allProjects = False # if True notice is shown for all projects, otherwise only once
for key in updateNoticeDict:
if updateNoticeDict[key]:
if key >= fileVersion:
if show is None:
show = fileVersion
else:
show = min(show,fileVersion)
allProjects = True
else:
if key >= lastNotice:
if show is None:
show = lastNotice
else:
show = min(show,lastNotice)
if show is None: return
filnam = os.path.join(GSASIIpath.path2GSAS2,'inputs','versioninfo.txt')
if not os.path.exists(filnam): # patch 3/2024 for svn dir organization
filnam = os.path.join(GSASIIpath.path2GSAS2,'versioninfo.txt')
if not os.path.exists(filnam):
print('Warning: file versioninfo.txt not found')
return
fp = open(filnam, 'r')
vers = None
noticeDict = {}
for line in fp:
if line.strip().startswith('#'): continue
if vers is not None:
if len(line.strip()) == 0:
vers = None
continue
else:
noticeDict[vers] += ' '
noticeDict[vers] += line.strip().replace('%%','\n')
elif ':' in line:
vers,tag = line.strip().split(':',1)
try:
vers = int(vers)
except:
continue
noticeDict[vers] = tag.strip().replace('%%','\n')
fp.close()
for key in list(noticeDict.keys()):
if key <= show: del noticeDict[key]
if len(noticeDict) == 0: return
dlg = wx.Dialog(G2frame,wx.ID_ANY,'Update notices',
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
sizer = wx.BoxSizer(wx.VERTICAL)
txtbox = wx.StaticText(dlg,wx.ID_ANY,
'Please read the notices below about major GSAS-II updates since'
' this project was last saved')
sizer.Add(txtbox,0)
txtbox.Wrap(size[0]-10)
sizer.Add((10,10))
panel = wxscroll.ScrolledPanel(dlg, wx.ID_ANY, size=(size[0]-20, size[1]))
sizer.Add(panel,1,wx.EXPAND,1)
tblSizer = wx.FlexGridSizer(0,2,5,10)
tblLine(panel)
txtbox = wx.StaticText(panel,wx.ID_ANY,'Version')
tblSizer.Add(txtbox,0,wx.ALIGN_CENTER|wx.ALIGN_CENTER_VERTICAL)
txtbox = wx.StaticText(panel,wx.ID_ANY,'Notice')
tblSizer.Add(txtbox,0,wx.ALIGN_CENTER_VERTICAL|wx.EXPAND)
tblLine(panel)
sizer.Add((10,10))
btnsizer = wx.StdDialogButtonSizer()
if allProjects: # will be shown again
OKbtn = wx.Button(dlg, wx.ID_OK)
else:
OKbtn = wx.Button(dlg, wx.ID_OK,label='Record as seen')
btn = wx.Button(dlg, wx.ID_CANCEL,label='Show again')
btnsizer.AddButton(btn)
OKbtn.SetDefault()
btnsizer.AddButton(OKbtn)
btnsizer.Realize()
sizer.Add((-1,5))
sizer.Add(btnsizer,0,wx.ALIGN_CENTER,50)
for i,key in enumerate(sorted(noticeDict,reverse=True)):
if i != 0: tblLine(panel,1)
txtbox = wx.StaticText(panel,wx.ID_ANY,str(key))
txtbox.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
tblSizer.Add(txtbox,0,wx.ALIGN_CENTER|wx.ALIGN_CENTER_VERTICAL)
txtbox = wx.StaticText(panel,wx.ID_ANY,noticeDict[key])
txtbox.Wrap(size[0]-110)
txtbox.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
tblSizer.Add(txtbox)
tblLine(panel)
panel.SetSizer(tblSizer)
panel.SetAutoLayout(1)
panel.SetupScrolling()
dlg.SetSizer(sizer)
sizer.Fit(dlg)
dlg.CenterOnParent()
if dlg.ShowModal() == wx.ID_OK:
if not allProjects:
vars = GetConfigValsDocs()
vars['lastUpdateNotice'][1] = rev
GSASIIpath.SetConfigValue(vars)
SaveConfigVars(vars)
dlg.Destroy()
return
dlg.Destroy()
################################################################################
[docs]
def viewWebPage(parent,URL='',size=(750,450),newFrame=False,HTML=''):
'''Creates a child wx.Frame with an OS-managed web browser. The window
is modeless, so it can be left open without affecting GSAS-II operations,
but will be closed when GSAS-II is ended if a ``parent`` window is
specified.
The web browser is filled with a supplied URL or HTML text.
Reuses the previous window unless ``newFrame`` is set to True.
:param wx.Frame parent: name of main GSAS-II window (G2frame), if None
a toplevel window is created (probably not a good idea).
:param str URL: web page to be viewed. This is ignored if ``HTML``
(below) is specified, but argument ``URL`` is not optional.
:param wx.Size size: initial size of Frame to be created. Defaults
to (750,450).
:param bool newFrame: When True, a new frame is opened even if the
previously-used frame exists. Default is False.
:param str HTML: HTML text of a web page to be displayed. If this
is specified, the contents of the URL argument is ignored.
:returns: the wx.Frame object used to display the web page
'''
def copyURL(event):
'''Copies URL name to paste buffer
'''
txt = event.GetEventObject().GetValue()
if wx.TheClipboard.Open():
wx.TheClipboard.SetData(wx.TextDataObject(txt))
wx.TheClipboard.Close()
import wx.html2 # don't import until needed, as this is only used here
global lastWebFrame
if not newFrame:
try:
if not lastWebFrame.IsBeingDeleted():
if HTML:
lastWebFrame.wv.SetPage(HTML,'')
else:
lastWebFrame.wv.LoadURL(URL)
return dlg
except:
pass
dlg = wx.Frame(parent,size=size)
lastWebFrame = dlg
dlg.Show()
sizer=wx.BoxSizer(wx.VERTICAL)
dlg.wv = wx.html2.WebView.New(dlg)
# place HTML title into window title
dlg.wv.Bind(wx.EVT_UPDATE_UI, lambda event:
dlg.SetTitle(dlg.wv.GetCurrentTitle()))
#lastWebView = dlg.wv
sizer.Add(dlg.wv,1,wx.EXPAND)
# row of buttons & URL label on bottom of window
bsizer=wx.BoxSizer(wx.HORIZONTAL)
dlg.back = wx.Button(dlg, -1, "Back")
dlg.back.Bind(wx.EVT_BUTTON, lambda event:dlg.wv.GoBack())
# 1st history entry is a blank page. Disable Back button to prevent loading that
dlg.back.Bind(wx.EVT_UPDATE_UI, lambda event: event.Enable(
len(dlg.wv.GetBackwardHistory()) > 1))
dlg.back.Enable(False)
bsizer.Add(dlg.back,0,wx.ALIGN_CENTER_VERTICAL)
# read-only TextCtrl to show the URL
urltxt = wx.TextCtrl(dlg,wx.ID_ANY,URL,style=wx.TE_READONLY|wx.TE_CENTER|wx.TE_PROCESS_ENTER)
urltxt.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
urltxt.Bind(wx.EVT_UPDATE_UI, lambda event: event.SetText(dlg.wv.GetCurrentURL()))
# Can't get copy to work on URL, so Enter copies contents to paste buffer
urltxt.Bind(wx.EVT_TEXT_ENTER, copyURL)
bsizer.Add(urltxt,1,wx.EXPAND)
close = wx.Button(dlg, -1, "Close")
close.Bind(wx.EVT_BUTTON, lambda event:dlg.Destroy())
bsizer.Add(close,0,wx.ALIGN_CENTER_VERTICAL)
sizer.Add(bsizer,0,wx.EXPAND|wx.TOP|wx.BOTTOM,3)
# load URL/HTML contents
if HTML:
dlg.wv.SetPage(HTML,'')
else:
dlg.wv.LoadURL(URL)
dlg.SetSizer(sizer)
sizer.Layout()
dlg.SendSizeEvent()
return dlg
################################################################################
[docs]
def StripIndents(msg,singleLine=False):
'''Strip unintended indentation from multiline strings.
When singleLine is True, all newline are removed, but inserting "%%"
into the string will cause a blank line to be inserted at that point
and %t% will generate a new line and tab (to indent a line)
:param str msg: a string containing one or more lines of text.
spaces or tabs following a newline are removed.
:param bool singleLine: removes all newlines from the msg so that
the text may be wrapped.
:returns: the string but reformatted
'''
msg1 = msg.replace('\n ','\n')
while msg != msg1:
msg = msg1
msg1 = msg.replace('\n ','\n')
msg1 = msg1.replace(' ',' ')
msg1 = msg.replace('\n\t','\n')
while msg != msg1:
msg = msg1
msg1 = msg.replace('\n\t','\n')
if singleLine:
msg = msg.replace('\n',' ')
msg1 = msg.replace(' ',' ')
while msg != msg1:
msg = msg1
msg1 = msg1.replace(' ',' ')
msg = msg.replace('%%','\n\n')
msg = msg.replace('%t%','\n\t')
return msg
[docs]
def StripUnicode(string,subs='.'):
'''Strip non-ASCII characters from strings
:param str string: string to strip Unicode characters from
:param str subs: character(s) to place into string in place of each
Unicode character. Defaults to '.'
:returns: a new string with only ASCII characters
'''
s = ''
for c in string:
if ord(c) < 128:
s += c
else:
s += subs
return s.encode('ascii','replace')
[docs]
def getTextSize(txt):
'Get the size of the text string txt in points, returns (x,y)'
dc = wx.ScreenDC()
return tuple(dc.GetTextExtent(txt))
# wx classes for reading various types of data files ######################################################################
[docs]
def BlockSelector(ChoiceList, ParentFrame=None,title='Select a block',
size=None, header='Block Selector',useCancel=True):
''' Provide a wx dialog to select a single block where one must
be selected. Used for selecting for banks for instrument
parameters if the file contains more than one set.
'''
if useCancel:
dlg = wx.SingleChoiceDialog(
ParentFrame,title, header,ChoiceList)
else:
dlg = wx.SingleChoiceDialog(
ParentFrame,title, header,ChoiceList,
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.OK|wx.CENTRE)
if size: dlg.SetMinSize(size)
dlg.CenterOnParent()
dlg.SendSizeEvent()
if dlg.ShowModal() == wx.ID_OK:
sel = dlg.GetSelection()
return sel
else:
return None
dlg.Destroy()
[docs]
def MultipleBlockSelector(ChoiceList, ParentFrame=None,
title='Select a block',size=None, header='Block Selector'):
'''Provide a wx dialog to select a block of data if the
file contains more than one set of data and one must be
selected. Used in :mod:`G2pwd_CIF` only.
:returns: a list of the selected blocks
'''
dlg = wx.MultiChoiceDialog(ParentFrame,title, header,ChoiceList+['Select all'],
wx.CHOICEDLG_STYLE)
if size: dlg.SetMinSize(size)
dlg.CenterOnScreen()
dlg.SendSizeEvent()
if dlg.ShowModal() == wx.ID_OK:
sel = dlg.GetSelections()
else:
return []
dlg.Destroy()
selected = []
if len(ChoiceList) in sel:
return range(len(ChoiceList))
else:
return sel
return selected
[docs]
class MultipleChoicesDialog(wx.Dialog):
'''A dialog that offers a series of choices, each with a
title and a wx.Choice widget. Intended to be used Modally.
typical input:
* choicelist=[ ('a','b','c'), ('test1','test2'),('no choice',)]
* headinglist = [ 'select a, b or c', 'select 1 of 2', 'No option here']
selections are placed in self.chosen when OK is pressed
Also see GSASIIctrlGUI
'''
def __init__(self,choicelist,headinglist,
head='Select options',
title='Please select from options below',
parent=None):
self.chosen = []
wx.Dialog.__init__(
self,parent,wx.ID_ANY,head,
pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
panel = wx.Panel(self)
mainSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add((10,10),1)
topLabl = wx.StaticText(panel,wx.ID_ANY,title)
mainSizer.Add(topLabl,0,wx.CENTER,10)
self.ChItems = []
for choice,lbl in zip(choicelist,headinglist):
mainSizer.Add((10,10),1)
self.chosen.append(0)
topLabl = wx.StaticText(panel,wx.ID_ANY,' '+lbl)
mainSizer.Add(topLabl,0,wx.ALIGN_LEFT,10)
self.ChItems.append(wx.Choice(panel, wx.ID_ANY, (100, 50), choices = choice))
mainSizer.Add(self.ChItems[-1],0,wx.ALIGN_CENTER,10)
OkBtn = wx.Button(panel,-1,"Ok")
OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
cancelBtn = wx.Button(panel,-1,"Cancel")
cancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
btnSizer.Add((20,20),1)
btnSizer.Add(OkBtn)
btnSizer.Add((20,20),1)
btnSizer.Add(cancelBtn)
btnSizer.Add((20,20),1)
mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
panel.SetSizer(mainSizer)
panel.Fit()
self.Fit()
def OnOk(self,event):
parent = self.GetParent()
if parent is not None: parent.Raise()
# save the results from the choice widgets
self.chosen = []
for w in self.ChItems:
self.chosen.append(w.GetSelection())
self.EndModal(wx.ID_OK)
def OnCancel(self,event):
parent = self.GetParent()
if parent is not None: parent.Raise()
self.chosen = []
self.EndModal(wx.ID_CANCEL)
[docs]
def MultipleChoicesSelector(choicelist, headinglist, ParentFrame=None, **kwargs):
'''A modal dialog that offers a series of choices, each with a title and a wx.Choice
widget. Used in :mod:`G2pwd_CIF` only.
Typical input:
* choicelist=[ ('a','b','c'), ('test1','test2'),('no choice',)]
* headinglist = [ 'select a, b or c', 'select 1 of 2', 'No option here']
optional keyword parameters are: head (window title) and title
returns a list of selected indicies for each choice (or None)
'''
result = None
dlg = MultipleChoicesDialog(choicelist,headinglist,
parent=ParentFrame, **kwargs)
dlg.CenterOnParent()
if dlg.ShowModal() == wx.ID_OK:
result = dlg.chosen
dlg.Destroy()
return result
[docs]
def PhaseSelector(ChoiceList, ParentFrame=None,
title='Select a phase', size=None,header='Phase Selector'):
''' Provide a wx dialog to select a phase, used in importers if a file
contains more than one phase
'''
return BlockSelector(ChoiceList,ParentFrame,title,
size,header)
[docs]
def showUniqueCell(frame,cellSizer,row,cell,SGData=None,
editAllowed=False,OnCellChange=None):
'''function to put cell values into a GridBagSizer.
First column (#0) is reserved for labels etc.
if editAllowed is True, values are placed in a wx.TextCtrl and if needed
two rows are used in the table.
'''
cellGUIlist = [
[['m3','m3m'],[" Unit cell: a = "],["{:.5f}"],[0]],
[['3R','3mR'],[" a = ",u" \u03B1 = "],["{:.5f}","{:.3f}"],[0,3]],
[['3','3m1','31m','6/m','6/mmm','4/m','4/mmm'],
[" a = "," c = ",u" \u03B3 = "],
["{:.5f}","{:.5f}","{:.3f}"],[0,2,-5]],
[['mmm'],[" a = "," b = "," c = "],["{:.5f}","{:.5f}","{:.5f}"],
[0,1,2]],
[['2/m'+'a'],[" a = "," b = "," c = ",u" \u03B1 = "],
["{:.5f}","{:.5f}","{:.5f}","{:.3f}"],[0,1,2,3]],
[['2/m'+'b'],[" a = "," b = "," c = ",u" \u03B2 = "],
["{:.5f}","{:.5f}","{:.5f}","{:.3f}"],[0,1,2,4]],
[['2/m'+'c'],[" a = "," b = "," c = ",u" \u03B3 = "],
["{:.5f}","{:.5f}","{:.5f}","{:.3f}"],[0,1,2,5]],
[['-1'],[" a = "," b = "," c = ",u" \u03B1 = ",u" \u03B2 = ",u" \u03B3 = "],
["{:.5f}","{:.5f}","{:.5f}","{:.3f}","{:.3f}","{:.3f}"],[0,1,2,3,4,5]]
]
cellList = []
if SGData is None:
laue = '-1'
else:
laue = SGData['SGLaue']
if laue == '2/m': laue += SGData['SGUniq']
for cellGUI in cellGUIlist:
if laue in cellGUI[0]:
useGUI = cellGUI
break
for txt,fmt,indx in zip(*useGUI[1:]):
col = 1+2*abs(indx)
cellrow = row
if editAllowed and abs(indx) > 2:
cellrow = row + 1
col = 1+2*(abs(indx)-3)
cellSizer.Add(wx.StaticText(frame,label=txt),(cellrow,col))
if editAllowed and indx >= 0:
Fmt = (10,5)
if '.3' in fmt: Fmt = (10,3)
cellVal = ValidatedTxtCtrl(frame,cell,indx,
xmin=0.1,xmax=500.,nDig=Fmt,OnLeave=OnCellChange)
cellSizer.Add(cellVal,(cellrow,col+1))
cellList.append(cellVal.GetId())
else:
cellSizer.Add(wx.StaticText(frame,label=fmt.format(cell[abs(indx)])),(cellrow,col+1))
#volume
volCol = 13
if editAllowed:
volCol = 8
cellSizer.Add(wx.StaticText(frame,label=' Vol = '),(row,volCol))
if editAllowed:
volVal = wx.TextCtrl(frame,value=('{:.2f}'.format(cell[6])),style=wx.TE_READONLY)
volVal.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
cellSizer.Add(volVal,(row,volCol+1))
else:
cellSizer.Add(wx.StaticText(frame,label='{:.2f}'.format(cell[6])),(row,volCol+1))
return cellrow,cellList
################################################################################
# configuration settings routines
def SaveGPXdirectory(path,write=True):
if GSASIIpath.GetConfigValue('Starting_directory') == path: return
vars = GetConfigValsDocs()
try:
vars['Starting_directory'][1] = path
if GSASIIpath.GetConfigValue('debug'): print('DBG_Saving GPX path: '+path)
if write: SaveConfigVars(vars)
except KeyError:
pass
def SaveImportDirectory(path):
if GSASIIpath.GetConfigValue('Import_directory') == path: return
vars = GetConfigValsDocs()
try:
vars['Import_directory'][1] = path
if GSASIIpath.GetConfigValue('debug'): print('DBG_Saving Import path: '+path)
SaveConfigVars(vars)
except KeyError:
pass
[docs]
def GetConfigValsDocs():
'''Reads the module referenced in fname (often <module>.__file__) and
return a dict with names of global variables as keys.
For each global variable, the value contains four items:
:returns: a dict where keys are names defined in module config_example.py
where the value is a list of four items, as follows:
* item 0: the default value
* item 1: the current value
* item 2: the initial value (starts same as item 1)
* item 3: the "docstring" that follows variable definition
'''
from . import config_example
import ast
fname = os.path.splitext(config_example.__file__)[0]+'.py' # convert .pyc to .py
with open(fname, 'r',encoding='utf-8') as f:
fstr = f.read()
fstr = fstr.replace('\r\n', '\n').replace('\r', '\n')
if not fstr.endswith('\n'):
fstr += '\n'
tree = ast.parse(fstr)
d = {}
key = None
for node in ast.walk(tree):
if isinstance(node,ast.Assign):
key = node.targets[0].id
default = config_example.__dict__.get(key)
start = GSASIIpath.configDict.get(key,default)
d[key] = [default, start, start, '']
elif isinstance(node,ast.Expr) and key:
d[key][3] = node.value.s.strip()
else:
key = None
return d
[docs]
def SaveConfigVars(vars,parent=None):
'''Write the current config variable values to ~/.GSASII/config.ini
:params dict vars: a dictionary of variable settings and meanings as
created in :func:`GetConfigValsDocs`. Most of the information gathered
in GetConfigValsDocs is no longer used here.
:param parent: wx.Frame object or None. No longer used.
:returns: True if unable to write the file, None otherwise
'''
configDict = {}
for var in sorted(vars.keys(),key=lambda s: s.lower()):
if vars[var][1] is None: continue
if vars[var][1] == '': continue
if vars[var][0] == vars[var][1]: continue
configDict[var] = vars[var][1]
return GSASIIpath.WriteConfig(configDict)
[docs]
class SelectConfigSetting(wx.Dialog):
'''Dialog to select configuration variables and set associated values.
'''
def __init__(self,parent):
style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Set Config Variable', style=style)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.vars = GetConfigValsDocs()
self.G2frame = parent
self.restart = False
self.reload = False
label = wx.StaticText(
self, wx.ID_ANY,
'Select a GSAS-II configuration variable to change'
)
self.sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
self.choice = {}
choices = sorted([k for k in self.vars if not k.startswith('enum_')],
key=lambda s: s.lower())
btn = G2ChoiceButton(self, choices,
strLoc=self.choice,strKey=0,onChoice=self.OnSelection)
btn.SetLabel("")
self.sizer.Add(btn)
self.varsizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.varsizer,1,wx.ALL|wx.EXPAND,1)
self.doclbl = wx.StaticBox(self, wx.ID_ANY, "")
self.doclblsizr = wx.StaticBoxSizer(self.doclbl)
self.docinfo = wx.StaticText(self, wx.ID_ANY, "")
self.doclblsizr.Add(self.docinfo, 0, wx.ALIGN_LEFT|wx.ALL, 5)
self.sizer.Add(self.doclblsizr, 0, wx.EXPAND|wx.ALL, 5)
btnsizer = wx.BoxSizer(wx.HORIZONTAL)
self.saveBtn = wx.Button(self,-1,"Save current settings")
btnsizer.Add(self.saveBtn, 0, wx.ALL, 2)
self.saveBtn.Bind(wx.EVT_BUTTON, self.OnSave)
self.saveBtn.Enable(False)
btn = wx.Button(self,wx.ID_CANCEL)
btnsizer.Add(btn, 0, wx.ALL, 2)
self.sizer.Add(btnsizer, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
self.SetSizer(self.sizer)
self.sizer.Fit(self)
self.CenterOnParent()
[docs]
def OnChange(self,event=None):
''' Check if anything been changed. Turn the save button on/off.
'''
for var in self.vars:
if self.vars[var][0] is None and self.vars[var][1] is not None:
# make blank strings into None, if that is the default
if self.vars[var][1].strip() == '': self.vars[var][1] = None
if self.vars[var][1] != self.vars[var][2]:
#print 'changed',var,self.vars[var][:3]
self.saveBtn.Enable(True)
if 'restart' in self.vars[var][3].lower():
self.restart = True
elif 'reload' in self.vars[var][3].lower():
self.reload = True
break
else:
self.saveBtn.Enable(False)
try:
self.resetBtn.Enable(True)
except:
pass
self.ShowColor()
def ShowColor(self):
if self.colorChip:
var = self.choice[0]
if self.vars[var][1]:
color = self.vars[var][1]
else:
color = self.vars[var][0]
self.colorText.SetLabel(color)
# set the color if valid
try:
self.colorChip.SetBackgroundColour(wx.Colour('#'+color))
except:
self.colorChip.SetLabel('Invalid color')
self.colorChip.SetBackgroundColour('yellow')
self.colorChip.SetForegroundColour('black')
[docs]
def OnSave(self,event):
'''Write the config variables to ~/.GSASII/config.ini
as the current settings
'''
SaveConfigVars(self.vars,parent=self)
GSASIIpath.SetConfigValue(self.vars)
self.EndModal(wx.ID_OK)
[docs]
def OnBoolSelect(self,event):
'Respond to a change in a True/False variable'
rb = event.GetEventObject()
var = self.choice[0]
self.vars[var][1] = (rb.GetSelection() == 0)
self.OnChange()
wx.CallAfter(self.OnSelection)
[docs]
def onSelDir(self,event):
'Select a directory from a menu'
dlg = wx.DirDialog(self, "Choose a directory:",style=wx.DD_DEFAULT_STYLE)
if dlg.ShowModal() == wx.ID_OK:
var = self.choice[0]
self.vars[var][1] = dlg.GetPath()
self.strEd.ChangeValue(self.vars[var][1])
self.OnChange()
dlg.Destroy()
[docs]
def onSelColor(self,event):
'Select a color from a menu'
dlg = wx.ColourDialog(self)
dlg.GetColourData().SetChooseFull(True) # Show the full color dialog
if dlg.ShowModal() == wx.ID_OK:
c = dlg.GetColourData().GetColour()
#self.colorChip.SetBackgroundColour(c)
# convert to mpl format w/o '#' prefix
mplcolor = ''.join([f"{i:x}" if i > 15 else f"0{i:x}" for i in c.Get()])
var = self.choice[0]
self.vars[var][1] = mplcolor
#self.strEd.SetValue(self.vars[var][1])
self.OnChange()
dlg.Destroy()
[docs]
def onSelExec(self,event):
'Select an executable file from a menu'
var = self.choice[0]
is_exe = lambda fpath: os.path.isfile(fpath) and os.access(fpath, os.X_OK)
defD = defF = ''
if self.vars[var][1] is not None and os.path.exists(self.vars[var][1]):
defD,defF=os.path.split(self.vars[var][1])
repeat = True
while repeat:
repeat = False
if sys.platform == "win32":
dlg = wx.FileDialog(self, "Choose a .exe file:",
defaultDir=defD,defaultFile=defF,
style=wx.FD_DEFAULT_STYLE|wx.FD_FILE_MUST_EXIST,
wildcard="Executable files|*.exe")
else:
dlg = wx.FileDialog(self, "Choose an executable image:",
defaultDir=defD,defaultFile=defF,
style=wx.FD_DEFAULT_STYLE|wx.FD_FILE_MUST_EXIST)
if dlg.ShowModal() == wx.ID_OK:
val = dlg.GetPath()
if os.path.exists(val) and is_exe(val):
self.vars[var][1] = val
self.strEd.ChangeValue(self.vars[var][1])
self.OnChange()
else:
dlg.Destroy()
G2MessageBox(self,'File not found or not executable',
'Invalid file')
repeat = True
continue
dlg.Destroy()
[docs]
def OnSelection(self):
'show a selected variable and allow it to be changed'
def OnNewColorBar(event):
self.vars['Contour_color'][1] = self.colSel.GetValue()
self.OnChange(event)
if 'phoenix' in wx.version():
self.varsizer.Clear(True)
else:
self.varsizer.DeleteWindows()
var = self.choice[0]
showdef = True
self.colorText = None
self.colorChip = None
if var not in self.vars:
raise Exception("How did this happen?")
if 'enum_'+var in self.vars:
choices = self.vars['enum_'+var][0]
self.colSel = EnumSelector(self,self.vars[var],1,choices,
OnChange=self.OnChange)
self.varsizer.Add(self.colSel, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
elif type(self.vars[var][0]) is int:
ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=int,OKcontrol=self.OnChange)
self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
elif type(self.vars[var][0]) is float:
ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=float,OKcontrol=self.OnChange)
self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
elif type(self.vars[var][0]) is bool:
showdef = False
lbl = "value for "+var
ch = []
for i,v in enumerate((True,False)):
s = str(v)
if v == self.vars[var][0]:
defopt = i
s += ' (default)'
ch += [s]
rb = wx.RadioBox(self, wx.ID_ANY, lbl, wx.DefaultPosition, wx.DefaultSize,
ch, 1, wx.RA_SPECIFY_COLS)
# set initial value
if self.vars[var][1] is None:
rb.SetSelection(defopt)
elif self.vars[var][1]:
rb.SetSelection(0)
else:
rb.SetSelection(1)
rb.Bind(wx.EVT_RADIOBOX,self.OnBoolSelect)
self.varsizer.Add(rb, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
else:
if var.endswith('_directory') or var.endswith('_location'):
btn = wx.Button(self,wx.ID_ANY,'Select from file dialog...')
btn.Bind(wx.EVT_BUTTON,self.onSelDir)
sz = (400,-1)
elif var.endswith('_exec'):
btn = wx.Button(self,wx.ID_ANY,'Select from file dialog...')
btn.Bind(wx.EVT_BUTTON,self.onSelExec)
sz = (400,-1)
elif var.endswith('_color') and var != 'Contour_color':
self.colorText = wx.StaticText(self,wx.ID_ANY,size=(80,20))
self.colorChip = wx.StaticText(self,wx.ID_ANY,size=(80,30))
btn = wx.Button(self,wx.ID_ANY,'Select from color selector...')
btn.Bind(wx.EVT_BUTTON,self.onSelColor)
sz = (400,-1)
else:
btn = None
sz = (250,-1)
if var == 'Contour_color':
if self.vars[var][1] is None:
self.vars[var][1] = 'Paired'
colorList = sorted([m for m in mpl.cm.datad.keys()]+['GSPaired','GSPaired_r',],key=lambda s: s.lower()) #if not m.endswith("_r")
self.colSel = wx.ComboBox(self,value=self.vars[var][1],choices=colorList,
style=wx.CB_READONLY|wx.CB_DROPDOWN)
self.colSel.Bind(wx.EVT_COMBOBOX, OnNewColorBar)
self.varsizer.Add(self.colSel, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
elif var == 'Image_calibrant':
from . import ImageCalibrants as calFile
calList = sorted([m for m in calFile.Calibrants.keys()],
key=lambda s: s.lower())
self.colSel = EnumSelector(self,self.vars[var],1,calList,
OnChange=self.OnChange)
self.varsizer.Add(self.colSel, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
elif self.colorChip:
hSizer = wx.BoxSizer(wx.HORIZONTAL)
hSizer.Add(self.colorText, 0, wx.ALL, 5)
hSizer.Add(self.colorChip, 0, wx.ALL, 5)
self.varsizer.Add(hSizer, 0, wx.ALL|wx.ALIGN_CENTRE, 5)
self.varsizer.Add(btn, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
else:
self.strEd = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=str,
OKcontrol=self.OnChange,size=sz,notBlank=False)
if self.vars[var][1] is not None:
self.strEd.ChangeValue(self.vars[var][1])
if btn:
self.varsizer.Add(self.strEd, 0, wx.ALL|wx.EXPAND, 5)
self.varsizer.Add(btn, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
else:
self.varsizer.Add(self.strEd, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
# button for reset to default value
lbl = "Reset to Default"
if showdef: # spell out default when needed
lbl += ' (='+str(self.vars[var][0])+')'
#label = wx.StaticText(self, wx.ID_ANY, 'Default value = '+str(self.vars[var][0]))
#self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
self.resetBtn = wx.Button(self,-1,lbl)
self.resetBtn.Bind(wx.EVT_BUTTON, self.OnClear)
if self.vars[var][1] is not None and self.vars[var][1] != '': # show current value, if one
#label = wx.StaticText(self, wx.ID_ANY, 'Current value = '+str(self.vars[var][1]))
#self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
self.resetBtn.Enable(True)
else:
self.resetBtn.Enable(False)
self.varsizer.Add(self.resetBtn, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
# show meaning, if defined
self.doclbl.SetLabel("Description of "+str(var))
vartxt = self.vars[var][3]
vartxt = StripIndents(vartxt.replace(' & ',' && '),True)
if vartxt:
self.docinfo.SetLabel(vartxt)
else:
self.docinfo.SetLabel("(not documented)")
self.docinfo.Wrap(500)
self.ShowColor()
self.sizer.Fit(self)
self.CenterOnParent()
wx.CallAfter(self.SendSizeEvent)
def OnClear(self, event):
var = self.choice[0]
self.vars[var][1] = self.vars[var][0]
self.OnChange()
wx.CallAfter(self.OnSelection)
################################################################################
[docs]
class RefinementProgress(wx.ProgressDialog):
'''Defines a wrapper to place around wx.ProgressDialog to be used for
showing refinement progress. At some point a better progress window should be
created that keeps useful info on the screen such as some starting and
current fit metrics, but for now all this adds is window defaults
and a wx.Yield call during progress update calls.
'''
def __init__(self, title='Residual', message='All data Rw =',
maximum=101, parent=None, trialMode=False, seqLen=0,
style=None):
if style is None:
style = wx.PD_ELAPSED_TIME|wx.PD_AUTO_HIDE|wx.PD_CAN_ABORT
super(self.__class__,self).__init__(title, message, int(maximum), parent, style)
Size = self.GetSize()
if 50 < Size[0] < 500: # sanity check on size, since this fails w/Win & wx3.0
self.SetSize((int(Size[0]*1.2),Size[1])) # increase size a bit along x
self.CenterOnParent()
self.Show()
[docs]
def Update(self,value, newmsg=""):
wx.GetApp().Yield()
#print('wx Yield called')
#print('Update:',value,newmsg)
return super(self.__class__,self).Update(int(value), newmsg)
################################################################################
fmtRw = lambda value: '{:.2f}'.format(float(value))
[docs]
class G2RefinementProgress(wx.Dialog):
'''Defines an replacement for wx.ProgressDialog to be used for
showing refinement progress.
:param str title: string to place on border of window (default is
'Refinement progress').
:param str message: initial string to place on top line of window.
:param int maximum: maximum value for progress gauge bar on bottom
of window.
:param wx.Frame parent: parent window for creation of this dialog
:param bool trialMode: Set to True for Levenberg-Marquardt fitting
where Rw may be computed several times for a single cycle.
Call :meth:`AdvanceCycle` when trialMode is True to indicate that a cycle
has been completed. Default is False.
:param int seqLen: Number of histograms in sequential fit. A value of
zero (default) means that the fit is not a sequential fit.
:param int seqShow: Number of histograms to shown in a sequential fit (default 3)
:param int style: optional parameters that determine how the dialog is
is displayed.
'''
def __init__(self, title='Refinement progress', message='All data Rw =',
maximum=101, parent=None, trialMode=False,seqLen=0, seqShow=3,style=None):
#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
self.trialRw = trialMode # used for Levenberg-Marquardt fitting
self.SeqLen = seqLen
self.seqShow = seqShow
if self.SeqLen:
self.maxCycle = self.SeqLen
self.SeqCount = -1
self.rows = 4
if self.trialRw: self.rows = 5
if style is None: style = wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER
super(self.__class__,self).__init__(parent, wx.ID_ANY, title,style=style, size=(-1,-1))
self.Bind(wx.EVT_CLOSE, self._onClose)
mainSizer = wx.BoxSizer(wx.VERTICAL)
self.startTime = time.time()
self.abortStatus = False
self.SetSizer(mainSizer)
mainSizer.Add((-1,3))
self.msgLine1 = wx.StaticText(self,wx.ID_ANY,message)
mainSizer.Add(self.msgLine1)
mainSizer.Add((-1,3))
self.msgLine2 = wx.StaticText(self,wx.ID_ANY,'')
mainSizer.Add(self.msgLine2)
hSizer = wx.BoxSizer(wx.HORIZONTAL)
vSizer = wx.BoxSizer(wx.VERTICAL)
lblSizer = self._makeLabeledTable() # make the Table for Rfactors in the RwPanel
vSizer.Add((-1,-1),1,wx.EXPAND,1)
vSizer.Add(lblSizer,0,wx.EXPAND)
vSizer.Add((-1,-1),1,wx.EXPAND,1)
hSizer.Add(vSizer,1,wx.EXPAND,1)
pltPanel = wx.Panel(self,size=(-1,-1))
self.figure = mplfig.Figure(dpi=100,figsize=(3,2))
self.figure.subplots_adjust(right=0.99,top=0.99)
Canvas(pltPanel, wx.ID_ANY, self.figure) # no need to save, get this from self.figure.canvas
self.plotaxis = self.figure.add_subplot()
hSizer.Add(pltPanel,0,wx.EXPAND,0)
mainSizer.Add(hSizer,1,wx.EXPAND,1)
mainSizer.Add((-1,5))
hSizer = wx.BoxSizer(wx.HORIZONTAL)
vSizer = wx.BoxSizer(wx.VERTICAL)
self.gaugemaximum = int(maximum)
self.gauge = wx.Gauge(self,wx.ID_ANY,range=int(self.gaugemaximum))
self.gauge.SetValue(0)
vSizer.Add(self.gauge,1,wx.EXPAND,1)
vSizer.Add((-1,3))
tSizer = wx.BoxSizer(wx.HORIZONTAL)
tSizer.Add((-1,-1),1,wx.EXPAND,1)
tSizer.Add(wx.StaticText(self,wx.ID_ANY,'Elapsed time: '))
self.elapsed = wx.StaticText(self,wx.ID_ANY,'0:00:00.0')
tSizer.Add(self.elapsed)
tSizer.Add((-1,-1),1,wx.EXPAND,1)
vSizer.Add(tSizer)
hSizer.Add(vSizer,1,wx.EXPAND,1)
hSizer.Add((10,-1))
btn = wx.Button(self, wx.ID_CLOSE,"Abort refinement")
btn.Bind(wx.EVT_BUTTON,self._onClose)
hSizer.Add(btn,0,wx.ALIGN_CENTER_VERTICAL)
mainSizer.Add(hSizer,0,wx.EXPAND,5)
self.Labels.label[2].SetLabel('Start')
self.cycleLbl = self.Labels.label[3]
self.cycleLbl.SetLabel('Cycle ?')
if self.trialRw:
self.Labels.label[4].SetLabel('trial parms')
self.Show()
self.Layout()
self.SetSizer(mainSizer)
mainSizer.Fit(self)
self.CenterOnParent()
self.SendSizeEvent()
self.tblCols = {}
self.tblLbls = {}
self.fitVals = {}
self.trialVals = {} # used when self.trialRw is True
self.trialcount = 0
self.cols = 0
self.maxCycle = 10
self.curHist = None # number of current histogram (may be <0 for overall)
self.seqHist = None # number of current sequential histogram (never <0)
self.prevSeqHist = [] # previous sequential histograms that are still shown
self.plotted = []
self.histOff = {}
def _onClose(self,event):
'''Respond to abort button or close of window
'''
self.abortStatus = True
self.Show(False)
[docs]
def Destroy(self):
'''Destroy the window, but allow events to clear before doing so
'''
wx.CallAfter(wx.Dialog.Destroy,self)
def _makeLabeledTable(self):
'''Create two grid sizers, one with row labels and one scrolled.
Use _xferLabeledTable to make the row heights match.
'''
lblSizer = wx.BoxSizer(wx.HORIZONTAL)
self.Labels = wx.GridBagSizer(2,2)
lblSizer.Add(self.Labels)
self.RwPanel = wx.lib.scrolledpanel.ScrolledPanel(self, wx.ID_ANY, size=(180, 130),
style = wx.SUNKEN_BORDER)
lblSizer.Add(self.RwPanel,1,wx.EXPAND,1)
self.Labels.label = {}
for i in range(self.rows):
self.Labels.label[i] = wx.StaticText(self,wx.ID_ANY,'',style=wx.ALIGN_CENTER_VERTICAL)
self.Labels.Add(self.Labels.label[i],(i,0))
mainsizer = wx.BoxSizer(wx.VERTICAL)
tblsizer = wx.BoxSizer(wx.HORIZONTAL)
self.gridSiz = wx.GridBagSizer(2,2)
tblsizer.Add(self.gridSiz)
mainsizer.Add(tblsizer)
self.RwPanel.SetSizer(mainsizer)
self.RwPanel.SetAutoLayout(1)
self.RwPanel.SetupScrolling()
return lblSizer
def _xferLabeledTable(self):
'''Matches the row sizes of the labels to the row heights in the table
'''
for i,h in enumerate(self.gridSiz.GetRowHeights()):
self.Labels.label[i].SetMinSize((-1,h))
self.Labels.Layout()
[docs]
def SetMaxCycle(self,value):
'''Set the maximum number of cycles or histograms (sequential fit).
Used to scale settings so the gauge bar completes close to 100%.
Ignored for sequential refinements.
'''
if self.SeqLen: return
self.maxCycle = value
def _AddTableColumn(self,label='',col=None):
'add a column to the Rfactor table'
if col is None:
self.cols += 1
col = self.cols
lbls = []
for i in range(self.rows-1):
lbls.append(wx.StaticText(self.RwPanel,wx.ID_ANY,'',style=wx.ALIGN_CENTER))
self.gridSiz.Add(lbls[-1],(1+i,col))
return col,lbls
[docs]
def SetHistogram(self,nextHist,histLbl):
'''Set this before beginning processing of each histogram
'''
if nextHist is None:
self.curHist = None
self.msgLine1.SetLabel(histLbl)
return
if self.SeqLen:
self._SetSeqHistogram(nextHist,histLbl)
return
col = None
lbl = 'Hist {}'.format(nextHist)
if nextHist == -1: # overall fit goes in the 1st column, if shown
col = 0
lbl = 'Overall'
elif nextHist == -2: # Restraint Chi2 contribution goes in the last col
lbl = 'Restraints'
if nextHist not in self.tblCols:
self.tblCols[nextHist],lbls = self._AddTableColumn(lbl,col)
self.tblLbls[nextHist] = lbls
self.tblLbls[nextHist][0].SetLabel(lbl)
self.fitVals[nextHist] = []
if nextHist >= 0:
self.msgLine1.SetLabel('Fitting '+histLbl)
self.curHist = nextHist
def _SetSeqHistogram(self,nextHist,histLbl):
'''Set this before beginning processing of each histogram in a sequential fit.
Advances the completion gauge.
'''
if nextHist == -1: # overall fit is not shown
self.curHist = nextHist
return
elif nextHist == -2: # Restraint Chi2 contribution goes in the 1st col
if nextHist not in self.tblCols:
self.tblCols[-2],lbls = self._AddTableColumn('Restraints',0)
self.tblLbls[-2] = lbls
self.tblLbls[-2][0].SetLabel('Restraints')
self.fitVals[-2] = []
elif self.seqHist != nextHist and nextHist >= 0:
if nextHist not in self.tblCols:
lbl = 'Hist {}'.format(nextHist)
self.tblCols[nextHist],lbls = self._AddTableColumn(lbl,None)
self.tblLbls[nextHist] = lbls
self.tblLbls[nextHist][0].SetLabel(lbl)
if len(self.prevSeqHist) < self.seqShow:
self.prevSeqHist.append(nextHist)
else:
del self.fitVals[self.prevSeqHist[0]]
del self.prevSeqHist[0]
self.prevSeqHist.append(nextHist)
self.fitVals[nextHist] = []
if -2 in self.fitVals: self.fitVals[-2] = []
if nextHist >= 0:
self.msgLine1.SetLabel('Fitting '+histLbl)
self.curHist = nextHist
if nextHist >= 0 and self.seqHist != nextHist:
self.seqHist = nextHist
self.SeqCount += 1
self.gauge.SetValue(int(min(self.gaugemaximum,100.*self.SeqCount/self.SeqLen)))
def _plotBar(self,h):
'plot a vertical bar for a histogram'
sym,lbl = self._getPlotSetting(h)
l = len(self.fitVals[h])
wid = 10.
if h < 0:
if h == -2 and -1 in self.fitVals:
self.histOff[h] = h/wid
else:
self.histOff[h] = -1/wid
else:
self.histOff[h] = len([i for i in self.plotted if i >= 0])/wid
self.plotaxis.bar(np.array(range(l))+self.histOff[h],self.fitVals[h],
width=1/wid,label=lbl,color=sym)
self.plotted.append(h)
def _getPlotSetting(self,h):
'determines how a plot is drawn'
if h == -1:
sym = "m" # magenta
lbl = 'o'
elif h == -2:
sym = "c" # cyan
lbl = 'r'
else:
symbols = ['b','r','g']
sym = symbols[h%3]
lbl = str(h)
return sym,lbl
def _SetCycleRw(self,value):
'''Used to process an Rwp value from the :meth:`Update` method.
The value will be associated with the current histogram (as set
in :meth:`SetHistogram`). If this is the 1st supplied value for
that histogram, the value is set and displayed as as the starting
Rwp. If :data`:self.trialRw` is False, the values are saved to a
list specific to the current histogram, and are displayed and
plotted. When :data`:self.trialRw` is True, the Rwp values are
considered trial values and are only saved and plotted when
:meth:`AdvanceCycle` is called.
'''
if self.curHist in self.fitVals:
cycle = len(self.fitVals[self.curHist])
else:
return
if self.maxCycle and not self.SeqLen:
self.gauge.SetValue(int(min(self.gaugemaximum,100.*cycle/self.maxCycle)))
self.cycleLbl.SetLabel('Cycle {:}'.format(cycle))
if cycle == 0:
self.tblLbls[self.curHist][1].SetLabel('{:8.3g}'.format(value))
self.RwPanel.SetupScrolling()
self._xferLabeledTable()
if not self.trialRw: # show & plot current status here
self.fitVals[self.curHist].append(value)
self.tblLbls[self.curHist][2].SetLabel('{:8.3g}'.format(value))
self.plotaxis.clear()
self.plotted = []
if self.SeqLen:
for h in self.fitVals: # loop over all histograms but not all get plotted
if h == -1: continue
if h in self.prevSeqHist:
self._plotBar(h)
else:
for h in self.fitVals: # loop over all histograms but not all get plotted
if h < 0 or h == self.curHist or len(self.fitVals) < 6 or (
self.curHist < 0 and h==0): # plot overall & restraints, or p to 5 histograms
self._plotBar(h)
if self.plotted: self.plotaxis.legend(loc=3)
self.figure.canvas.draw()
else: # save as trial value
self.trialVals[self.curHist] = value
self.tblLbls[self.curHist][3].SetLabel('{:8.3g}'.format(value))
if self.curHist in self.plotted:
sym,lbl = self._getPlotSetting(self.curHist)
c = len(self.fitVals[self.curHist]) - 1 + self.histOff[self.curHist]
self.plotaxis.plot(c,value,'o'+sym)
self.figure.canvas.draw()
if (self.curHist ==-1 and -2 not in self.fitVals) or self.curHist == -2:
self.trialcount += 1
self.Layout() # in case sizes change
self.RwPanel.ScrollChildIntoView(self.tblLbls[self.curHist][1])
[docs]
def AdvanceCycle(self,cycle=None):
'''Call this directly with Levenberg-Marquardt fitting after a
cycle completes.
Plots the results.
'''
self.plotaxis.clear()
self.trialcount = 0
self.plotted = []
for h,value in self.trialVals.items():
if h not in self.fitVals or h not in self.tblLbls: continue
self.fitVals[h].append(value)
self.tblLbls[h][2].SetLabel('{:8.3g}'.format(value))
self.tblLbls[h][3].SetLabel('')
if self.SeqLen or h < 3 or len(self.trialVals) < 6: # plot overall & restraints, or p to 5 histograms
self._plotBar(h)
if self.plotted: self.plotaxis.legend(loc=3)
self.figure.canvas.draw()
[docs]
def Update(self, value=None, newmsg=""):
'''designed to work with calls intended for wx.ProgressDialog.Update
the value is assumed to be the current wR value for the histogram
selected with SetHistogram and newmsg goes into the 2nd status line.
'''
if self.curHist is not None and value != 101.:
self._SetCycleRw(value)
if newmsg:
self.msgLine2.SetLabel(newmsg)
m,s = divmod(time.time()-self.startTime,60)
h,m = divmod(m,60)
self.elapsed.SetLabel('{:0d}:{:02d}:{:04.1f}'.format(int(h), int(m), s))
wx.GetApp().Yield()
return (not self.abortStatus, True)
################################################################################
[docs]
class gitVersionSelector(wx.Dialog):
'''Dialog to allow a user to select a version of GSAS-II to install
from a git repository
'''
def __init__(self,parent=None):
#import git
self.g2repo = GSASIIpath.openGitRepo(path2GSAS2)
self.githistory = GSASIIpath.gitHistory('hash',self.g2repo)
# patch Feb 2024: don't allow access to versions that are too old
# since they are hard-coded to use svn
import datetime
tz = self.g2repo.commit(self.githistory[0]).committed_datetime.tzinfo
cutoff = datetime.datetime(2024,2,20,tzinfo=tz) # 20-feb-2024
self.githistory = [h for h in self.githistory if
self.g2repo.commit(h).committed_datetime > cutoff]
# end patch
self.initial_commit = self.g2repo.commit('HEAD')
self.initial_commit_info = self.docCommit(self.initial_commit)
if parent is None:
parent = wx.GetApp().GetMainTopWindow()
self.parent = parent
wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Select GSAS-II Version',
style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
sizer = wx.BoxSizer(wx.VERTICAL)
remoteupdates,localupdates = GSASIIpath.gitCountRegressions(self.g2repo)
self.verselection = remoteupdates
sizer.Add((-1,10))
#label = wx.StaticText(
# self, wx.ID_ANY,
# 'Select a specific GSAS-II version to install'
# )
msg = 'This allows you to revert back to a previous GSAS-II version '
msg += 'to compare results between versions and temporarily bypass '
msg += 'bugs. If there is something that works better in an older '
msg += 'GSAS-II version, be sure to test the latest version and '
msg += 'if the problem remains, report it so that it can be fixed.'
label = wx.StaticText(self, wx.ID_ANY, msg)
label.Wrap(400)
sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
sizer.Add((-1,20))
sizer.Add(wx.StaticText(self, wx.ID_ANY,
' Currently installed version:'))
sizer1 = wx.BoxSizer(wx.HORIZONTAL)
sizer1.Add((50,-1))
initpnl = wxscroll.ScrolledPanel(self, wx.ID_ANY,size=(450,90),
style = wx.SUNKEN_BORDER)
ssizer = wx.BoxSizer(wx.HORIZONTAL)
txt = wx.StaticText(initpnl, wx.ID_ANY, self.initial_commit_info)
txt.Wrap(435)
ssizer.Add(txt)
initpnl.SetSizer(ssizer)
initpnl.SetAutoLayout(1)
initpnl.SetupScrolling()
sizer1.Add(initpnl)
sizer.Add(sizer1)
sizer.Add((-1,20))
sizer1 = wx.BoxSizer(wx.HORIZONTAL)
sizer1.Add(
wx.StaticText(self, wx.ID_ANY,
'Select how many versions to regress: '),
0, wx.ALIGN_CENTRE|wx.ALL, 5)
self.spin = wx.SpinCtrl(self, wx.ID_ANY,size=(150,-1))
self.spin.SetRange(-len(self.githistory)+1, 0)
self.spin.SetValue(-self.verselection)
self.Bind(wx.EVT_SPINCTRL, self._onSpin, self.spin)
self.Bind(wx.EVT_KILL_FOCUS, self._onSpin, self.spin)
sizer1.Add(self.spin)
sizer.Add(sizer1)
sizer.Add((-1,20))
sizer.Add(wx.StaticText(self, wx.ID_ANY,
' Selected version to install:'))
sizer1 = wx.BoxSizer(wx.HORIZONTAL)
sizer1.Add((50,-1))
self.spanel = wxscroll.ScrolledPanel(self, wx.ID_ANY,size=(450,90),
style = wx.SUNKEN_BORDER)
ssizer = wx.BoxSizer(wx.HORIZONTAL)
self.text = wx.StaticText(self.spanel, wx.ID_ANY, "")
ssizer.Add(self.text)
self.spanel.SetSizer(ssizer)
self.spanel.SetAutoLayout(1)
self.spanel.SetupScrolling()
sizer1.Add(self.spanel)
sizer.Add(sizer1)
sizer.Add((-1,20))
sizer.Add(
wx.StaticText(
self, wx.ID_ANY,
'Press "Continue" after selecting a version to continue;\n'
'Press "Cancel" to stop regression.'),
0, wx.EXPAND|wx.ALL, 10)
sizer.Add((-1,5))
btnsizer = wx.StdDialogButtonSizer()
btn = wx.Button(self, wx.ID_OK, "Continue")
btn.SetDefault()
btnsizer.AddButton(btn)
btn = wx.Button(self, wx.ID_CANCEL)
btnsizer.AddButton(btn)
btnsizer.Realize()
sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
self.SetSizer(sizer)
sizer.Fit(self)
siz = self.GetSize()
siz[0] = max(siz[0],500)
self.SetSize(siz)
self.CenterOnParent()
self._onSpin(None)
def _onSpin(self,event):
'Called to load info about the selected version in the dialog'
if event: event.Skip()
try:
commit_info = self.docCommit(self.githistory[-self.spin.GetValue()])
self.text.SetLabel(commit_info)
self.text.Wrap(435)
except IndexError:
return
self.spanel.SetAutoLayout(1)
self.spanel.SetupScrolling()
[docs]
def getVersion(self):
'''Gets the selected version that should be installed
:return: returns one of three values:
* 0: if the newest version is selected, so that the
installation should be updated rather than regressed
* None: if the currently installed version is selected,
so that nothing need be done
* A hexsha string: the regressed version that should be
selected.
'''
if self.spin.GetValue() == 0:
return 0
commit = self.githistory[-self.spin.GetValue()]
if self.g2repo.commit(commit) == self.initial_commit:
return None
return commit
[docs]
def docCommit(self,commit):
'''Provides a string with information about a specific git commit.
:returns: a multi-line string
'''
import datetime
fmtdate = lambda c:"{:%d-%b-%Y %H:%M}".format(c.committed_datetime)
commit = self.g2repo.commit(commit) # converts a hash, if supplied
# do not allow regression before May 1, 2025 so we don't go back to
# master branch reorg. Switch was actually ~22 Apr 2025, but leave a
# bit of room
if datetime.datetime(2025,5,1, tzinfo=commit.committed_datetime.tzinfo
) > commit.committed_datetime:
msg = 'Unable to regress automatically to versions prior to last source reorg. If you do need to use an older GSAS-II version please ask for help on mailing list or GitHub Issues.'
G2MessageBox(self.parent,msg,'Too old')
self.spin.SetValue(self.spin.GetValue()+1)
raise IndexError
msg = f'git {commit.hexsha[:10]} from {fmtdate(commit)}'
tags = self.g2repo.git.tag('--points-at',commit).split('\n')
if tags != ['']:
msg += f"\ntags: {', '.join(tags)}"
msg += '\ncomment: ' + commit.message
return msg
################################################################################
[docs]
class SortableLstCtrl(wx.Panel):
'''Creates a read-only table with sortable columns. Sorting is done by
clicking on a column label. A triangle facing up or down is added to
indicate the column is sorted.
To use, the header is labeled using
:meth:`PopulateHeader`, then :meth:`PopulateLine` is called for every
row in table and finally :meth:`SetColWidth` is called to set the column
widths.
:param wx.Frame parent: parent object for control
Example::
data = [
(':0:sig-0', '30.7906', '30.7907', 'Pwd=SNAP066555: TOF profile term'),
(':0:sig-1', '208.419', '208.419', 'Pwd=SNAP066555: TOF profile term'),
(':0:Scale', '582006', '582006', 'Pwd=SNAP066555: Scale factor'),
(':1:Scale', '592440', '592440', 'Pwd=SNAP06 (1): Scale factor'),
]
sortPanel = G2G.SortableLstCtrl(G2frame)
sortPanel.PopulateHeader([f'label{i}' for i in range(4)],4*[0])
for i,l in enumerate(data): sortPanel.PopulateLine(i,l)
for i in range(4): sortPanel.SetColWidth(i) # set width to automatic
sortPanel.SetColWidth(1,sortType='float') # sort 1st column by numeric value not str
'''
def __init__(self, parent):
wx.Panel.__init__(self, parent, wx.ID_ANY)#, style=wx.WANTS_CHARS)
sizer = wx.BoxSizer(wx.VERTICAL)
self.list = G2LstCtrl(self, wx.ID_ANY, style=wx.LC_REPORT| wx.BORDER_SUNKEN)
sizer.Add(self.list, 1, wx.EXPAND)
self.SetSizer(sizer)
self.SetAutoLayout(True)
#self.SortListItems(0, True)
[docs]
def PopulateLine(self, key, data):
'''Enters each row into the table
:param int key: a unique int value for each line, probably should
be sequential
:param list data: a list of strings for each column in that row
'''
index = self.list.InsertItem(self.list.GetItemCount(), data[0])
for i,d in enumerate(data[1:]):
self.list.SetItem(index, i+1, d)
self.list.SetItemData(index, key)
self.list.itemDataMap[key] = data
[docs]
def SetColWidth(self,col,width=None,auto=True,minwidth=0,maxwidth=None,
sortType='str'):
'''Sets the column width.
:param int width: the column width in pixels
:param bool auto: if True (default) and width is None (default) the
width is set by the maximum width entry in the column
:param int minwidth: used when auto is True, sets a minimum
column width
:param int maxwidth: used when auto is True, sets a maximum
column width. Do not use with minwidth
'''
if width:
self.list.SetColumnWidth(col, width)
elif auto:
self.list.SetColumnWidth(col, wx.LIST_AUTOSIZE)
if minwidth and self.list.GetColumnWidth(col) < minwidth:
self.list.SetColumnWidth(col, minwidth)
elif maxwidth and self.list.GetColumnWidth(col) > maxwidth:
self.list.SetColumnWidth(col, maxwidth)
else:
print('Error in SetColWidth: use either auto or width')
if sortType == 'float':
self.list.FloatCols.append(col)
elif sortType == 'abs':
self.list.AbsFloatCols.append(col)
elif sortType != 'str':
print(f'SortableLstCtrl.SetColWidth warning: unexpected sortType value ({sortType})')
[docs]
def SetInitialSortColumn(self, col, ascending=True):
'''Sets the initial column to be used for sorting when the table is first displayed.
This method should be called after all PopulateLine calls are complete.
The up or down arrow indicator will be displayed on the specified column.
:param int col: the column index (0-based) to sort by initially
:param bool ascending: if True (default), sort in ascending order; if False, descending
'''
# Use SortListItems to both sort the items and display the sort indicator arrow
self.list.SortListItems(col, ascending)
try:
class G2LstCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin, listmix.ColumnSorterMixin):
'''Creates a custom ListCtrl with support for images in column labels
'''
def __init__(self, parent, ID=wx.ID_ANY, pos=wx.DefaultPosition,
size=wx.DefaultSize, style=0):
wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
listmix.ListCtrlAutoWidthMixin.__init__(self)
from wx.lib.embeddedimage import PyEmbeddedImage
# from demo/images.py
SmallUpArrow = PyEmbeddedImage(
b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADxJ"
b"REFUOI1jZGRiZqAEMFGke2gY8P/f3/9kGwDTjM8QnAaga8JlCG3CAJdt2MQxDCAUaOjyjKMp"
b"cRAYAABS2CPsss3BWQAAAABJRU5ErkJggg==")
SmallDnArrow = PyEmbeddedImage(
b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAEhJ"
b"REFUOI1jZGRiZqAEMFGke9QABgYGBgYWdIH///7+J6SJkYmZEacLkCUJacZqAD5DsInTLhDR"
b"bcPlKrwugGnCFy6Mo3mBAQChDgRlP4RC7wAAAABJRU5ErkJggg==")
self.il = wx.ImageList(16, 16)
self.UpArrow = self.il.Add(SmallUpArrow.GetBitmap())
self.DownArrow = self.il.Add(SmallDnArrow.GetBitmap())
self.parent=parent
self.SetImageList(self.il, wx.IMAGE_LIST_SMALL)
self.FloatCols = []
self.AbsFloatCols = []
def GetListCtrl(self): # needed for sorting
return self
[docs]
def GetSortImages(self):
return (self.DownArrow, self.UpArrow)
[docs]
def GetColumnSorter(self):
"""Custom sorter that handles absolute numerical values for columns 1, 2,
and 3 (2nd, 3rd, 4th columns)"""
def compare_func(key1, key2):
col = self.GetSortState()[0]
ascending = self.GetSortState()[1]
# Get data for both rows
data1 = self.itemDataMap[key1][col]
data2 = self.itemDataMap[key2][col]
# For columns designated as self.AbsFloatCols, sort by absolute numerical value
if col in self.AbsFloatCols:
try:
val1 = abs(float(data1))
val2 = abs(float(data2))
result = (val1 > val2) - (val1 < val2)
except (ValueError, TypeError):
# Fall back to string comparison if conversion fails
result = (data1 > data2) - (data1 < data2)
elif col in self.FloatCols:
try:
val1 = float(data1)
val2 = float(data2)
result = (val1 > val2) - (val1 < val2)
except (ValueError, TypeError):
# Fall back to string comparison if conversion fails
result = (data1 > data2) - (data1 < data2)
else:
# For other columns, use string comparison
result = (data1 > data2) - (data1 < data2)
return result if ascending else -result
return compare_func
except TypeError:
# avoid "duplicate base class _MockObject" error in class G2LstCtrl():
# where listmix.ListCtrlAutoWidthMixin, listmix.ColumnSorterMixin are same
# in docs build
[docs]
class G2LstCtrl(wx.ListCtrl):
'''Creates a custom ListCtrl with support for images in column labels
'''
pass
print('docs build kludge for G2LstCtrl')
#### Display Help information ################################################################################
# define some globals
htmlPanel = None
htmlFrame = None
htmlFirstUse = True
helpLocDict = {}
'This is an index to the HTML anchors defined in the GSAS-II help files'
path2GSAS2 = os.path.dirname(os.path.realpath(__file__)) # save location of this file
[docs]
def ShowHelp(helpType,frame,helpMode=None):
'''Called to bring up a web page for documentation.'''
global htmlFirstUse,htmlPanel,htmlFrame
if not helpLocDict: # load the anchor index
indx = os.path.abspath(os.path.join(path2GSAS2,'help','anchorIndex.txt'))
print('reading file',indx)
with open(indx,'r') as indexfile:
for line in indexfile.readlines():
fil,anchors = line.split(':')
for a in anchors.split(','):
#if a in helpLocDict: print(a,'repeated!')
helpLocDict[a.strip()] = fil
# no defined link to use, create a default based on key
helplink = 'index.html'
if helpType:
anchor = helpType.replace(')','').replace('(','_').replace(' ','_')
if anchor not in helpLocDict:
print(f'Help lookup problem, anchor {anchor} not found'
'\nPlease report this to toby@anl.gov along with a'
'\nscreen image showing where you tried to get help')
else:
helplink = f'{helpLocDict[anchor]}#{anchor}'
# determine if a web browser or the internal viewer should be used for help info
if helpMode:
pass
elif GSASIIpath.GetConfigValue('Help_mode'):
helpMode = GSASIIpath.GetConfigValue('Help_mode')
else:
helpMode = 'browser'
if helpMode == 'internal':
helplink = 'file://' + os.path.abspath(os.path.join(path2GSAS2,'help',helplink))
viewWebPage(frame,helplink)
else:
import webbrowser # postpone this until now for quicker startup
wb = webbrowser
if sys.platform == "darwin": # on Mac, use a OSXscript so that file anchors work
# Get the default browser, this will fail in py2.7 and might fail, so
# use safari as a backup
appleScript = '''
use framework "AppKit"
use AppleScript version "2.4"
use scripting additions
property NSWorkspace : a reference to current application's NSWorkspace
property NSURL : a reference to current application's NSURL
set wurl to NSURL's URLWithString:"https://www.apple.com"
set thisBrowser to (NSWorkspace's sharedWorkspace)'s ¬
URLForApplicationToOpenURL:wurl
set appname to (thisBrowser's absoluteString)'s lastPathComponent()'s ¬
stringByDeletingPathExtension() as text
return appname as text
'''
import subprocess
try:
browser = subprocess.check_output(["osascript","-e",appleScript], encoding='UTF-8').strip()
wb = webbrowser.MacOSXOSAScript(browser)
except:
wb = webbrowser.MacOSXOSAScript('safari')
# open the link
helplink = os.path.join(path2GSAS2,'help',helplink)
pfx = "file://"
if sys.platform.lower().startswith('win'):
# really don't understand what urlunsplit is doing, but this seems
# to prevent windows from encoding the # for the anchor
# (suggested by Google's AI!)
from urllib.parse import urlunsplit
f = helplink.split('#')[0]
a = ''
if '#' in helplink:
a = helplink.split('#')[1]
helplink = urlunsplit(['file','',f,'',a])
pfx = ''
#if GSASIIpath.GetConfigValue('debug'): print 'DBG_Help link=',pfx+helplink
if htmlFirstUse:
wb.open_new(pfx+helplink)
htmlFirstUse = False
else:
wb.open(pfx+helplink, new=0, autoraise=True)
[docs]
def ShowWebPage(URL,frame,browser=False,internal=False):
'''Called to show a tutorial web page.
:param str URL: web page URL
:param wx.Frame frame: parent window (or None)
:param bool browser: If True, forces the page to be opened in a web
browser, regardless of the ``Help_mode`` config setting.
'''
global htmlFirstUse,htmlPanel,htmlFrame
# determine if a web browser or the internal viewer should be used for help info
if GSASIIpath.GetConfigValue('Help_mode') and not browser and not internal:
helpMode = GSASIIpath.GetConfigValue('Help_mode')
elif internal:
helpMode = 'internal'
else:
# elif browser:
helpMode = 'browser'
if helpMode == 'internal':
viewWebPage(frame,URL)
else:
import webbrowser # postpone this until now for quicker startup
if URL.startswith('http'):
pfx = ''
elif sys.platform.lower().startswith('win'):
pfx = ''
else:
pfx = "file://"
if htmlFirstUse:
webbrowser.open_new(pfx+URL)
htmlFirstUse = False
else:
webbrowser.open(pfx+URL, new=0, autoraise=True)
#### Tutorials support ################################################################################
G2TutURL = "https://advancedphotonsource.github.io/GSAS-II-tutorials/"
tutorialCatalog = [l for l in tutorialIndex if len(l) >= 3]
# A catalog of GSAS-II tutorials generated from the table in :data:`tutorialIndex`
def OpenTutorial(parent):
# how_install = GSASIIpath.HowIsG2Installed()
# if how_install.startswith('git'):
# return OpenGitTutorial(parent)
# else:
return OpenGitTutorial(parent)
[docs]
class OpenGitTutorial(wx.Dialog):
'''Open a tutorial web page from the git repository,
optionally copying the tutorial's exercise data file(s) to
the local disk.
'''
def __init__(self,parent):
style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Open Tutorial', style=style)
self.G2frame = self.frame = parent
pnl = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer1 = wx.BoxSizer(wx.HORIZONTAL)
msg = ('To open a tutorial, select below the mode to be used. '+
'With either choice you can select a tutorial and view it '+
'in a web browser, but the 2nd option offers the option '+
'to automatically download the data files needed to run '+
'that tutorial.')
label = wx.StaticText(pnl, wx.ID_ANY, msg)
label.Wrap(450)
msg = '''The data files needed to run the GSAS-II tutorials
require a fair amount of storage space; few users will
use all of them. This dialog allows you to open a
tutorial in a web browser and select if you want the data
needed to run that exercise to be downloaded to your computer.
The location used to download tutorials is set using the
"Set download location" which is saved as the "Tutorial_location"
configuration option see File/Preference or the
config_example.py file.
'''
self.SetTutorialPath()
hlp = HelpButton(pnl,msg)
sizer1.Add((-1,-1),1, wx.EXPAND, 0)
sizer1.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
sizer1.Add((-1,-1),1, wx.EXPAND, 0)
sizer1.Add(hlp)
sizer.Add(sizer1,0,wx.EXPAND|wx.ALL,0)
sizer.Add((10,10))
sizer0 = wx.BoxSizer(wx.HORIZONTAL)
sizer1 = wx.BoxSizer(wx.VERTICAL)
btn = wx.Button(pnl, wx.ID_ANY, "Select a tutorial to view")
btn.Bind(wx.EVT_BUTTON, self.onWebBrowse)
sizer1.Add(btn,0)
btn = wx.Button(pnl, wx.ID_ANY, "Select a tutorial to view and download its data files")
btn.Bind(wx.EVT_BUTTON, self.SelectAndDownload)
sizer1.Add(btn,0,wx.TOP,5)
sizer0.Add(sizer1,0,wx.EXPAND|wx.ALL,0)
sizer.Add(sizer0,5,wx.EXPAND|wx.ALL,5)
sizer.Add((10,10))
sizer1 = wx.BoxSizer(wx.HORIZONTAL)
btn = wx.Button(pnl, wx.ID_ANY, "Set download location")
btn.Bind(wx.EVT_BUTTON, self.SelectDownloadLoc)
sizer1.Add(btn,0,WACV|wx.RIGHT|wx.LEFT,5)
self.dataLoc = wx.StaticText(pnl, wx.ID_ANY,self.tutorialPath)
sizer1.Add(self.dataLoc,0,WACV)
sizer.Add(sizer1)
btnsizer = wx.StdDialogButtonSizer()
btn = wx.Button(pnl, wx.ID_CANCEL,"Close")
btnsizer.AddButton(btn)
btnsizer.Realize()
sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
pnl.SetSizer(sizer)
sizer.Fit(self)
self.topsizer=sizer
self.CenterOnParent()
[docs]
def SetTutorialPath(self):
'''Get the tutorial location if set; if not pick a default
directory in a logical place
'''
# has the user set a location and is it valid?
if GSASIIpath.GetConfigValue('Tutorial_location'):
tutorialPath = os.path.abspath(GSASIIpath.GetConfigValue('Tutorial_location'))
if os.path.exists(tutorialPath):
self.tutorialPath = tutorialPath
return
try:
os.makedirs(tutorialPath)
if os.path.exists(tutorialPath):
self.tutorialPath = tutorialPath
return
except:
print('Unable to use Tutorial_location config setting',
tutorialPath)
# try a system-specific location
if (sys.platform.lower().startswith('win')):
for p in ('Documents','My Documents',''):
if os.path.exists(os.path.abspath(os.path.expanduser(
os.path.join('~',p)))):
tutorialPath = os.path.abspath(os.path.expanduser(
os.path.join('~',p,'G2tutorials')))
if os.path.exists(tutorialPath):
self.tutorialPath = tutorialPath
return
try:
os.makedirs(tutorialPath)
if os.path.exists(tutorialPath):
self.tutorialPath = tutorialPath
return
except:
pass
else:
tutorialPath = os.path.abspath(os.path.expanduser(
os.path.join('~','G2tutorials')))
if os.path.exists(tutorialPath):
self.tutorialPath = tutorialPath
return
try:
os.makedirs(tutorialPath)
if os.path.exists(tutorialPath):
self.tutorialPath = tutorialPath
return
except:
pass
# no success so far, use current working directory
tutorialPath = os.path.abspath(os.path.join(os.getcwd(),'G2tutorials'))
if os.path.exists(tutorialPath):
self.tutorialPath = tutorialPath
return
try:
os.makedirs(tutorialPath)
if os.path.exists(tutorialPath):
self.tutorialPath = tutorialPath
return
except:
pass
# nothing worked, set self.tutorialPath with os.getcwd() and hope for the best
print('Warning: Unable to set a TutorialPath, using',os.getcwd())
tutorialPath = os.getcwd()
[docs]
def SelectAndDownload(self,event):
'''Shows a list of all tutorials so user can select one to view.
The data files associated with that directory are then downloaded.
'''
tutdir = self.onWebBrowse(event)
if tutdir is None: return
GSASIIpath.downloadDirContents([tutdir,'data'],self.tutorialPath)
[docs]
def onWebBrowse(self,event):
'''Shows a list of all tutorials so user can select one to view.
:returns: the name of the directory where the tutorial is located,
which is used if called from :meth:`SelectAndDownload`.
'''
choices2 = [i[2:4] for i in tutorialCatalog]
selected = self.ChooseTutorial2(choices2)
if selected is None: return
tutdir = tutorialCatalog[selected][0]
tutfil = tutorialCatalog[selected][1]
# open web page remotely, don't worry about data
#URL = G2BaseURL+'/Tutorials/'+tutdir+'/'+tutfil
URL = G2TutURL + tutdir + '/' + tutfil
wx.CallAfter(self.EndModal,wx.ID_OK)
ShowWebPage(URL,self.frame)
return tutdir
[docs]
def ChooseTutorial2(self,choices):
'''Select tutorials from a two-column table, when possible
'''
lbls = ('tutorial name (indent indicates previous is required)','description')
colWidths=[400,400]
dlg = MultiColumnSelection(self,'select tutorial',lbls,choices,colWidths)
selection = dlg.Selection
dlg.Destroy()
if selection is not None:
if selection == -1: return
return selection
[docs]
def SelectDownloadLoc(self,event):
'''Select a download location,
Cancel resets to the default
'''
dlg = wx.DirDialog(self, "Choose a directory for tutorial downloads:",
defaultPath=self.tutorialPath)#,style=wx.DD_DEFAULT_STYLE)
try:
if dlg.ShowModal() != wx.ID_OK:
return
pth = dlg.GetPath()
finally:
dlg.Destroy()
if not os.path.exists(pth):
try:
os.makedirs(pth) #failing for no obvious reason
except OSError:
msg = 'The selected directory is not valid.\n\t'
msg += pth
msg += '\n\nAn attempt to create the directory failed'
G2MessageBox(self.frame,msg)
return
if os.path.exists(os.path.join(pth,"help")) and os.path.exists(os.path.join(pth,"Exercises")):
print("Note that you may have old tutorial files in the following directories")
print('\t'+os.path.join(pth,"help"))
print('\t'+os.path.join(pth,"Exercises"))
print('Subdirectories in the above can be deleted to save space\n\n')
self.tutorialPath = pth
self.dataLoc.SetLabel(self.tutorialPath)
if GSASIIpath.GetConfigValue('Tutorial_location') == pth: return
vars = GetConfigValsDocs()
try:
vars['Tutorial_location'][1] = pth
if GSASIIpath.GetConfigValue('debug'): print('DBG_Saving Tutorial_location: '+pth)
GSASIIpath.SetConfigValue(vars)
SaveConfigVars(vars)
except KeyError:
pass
### Autoload PWDR files ################################################################################
AutoLoadWindow = None
def AutoLoadFiles(G2frame,FileTyp='pwd'):
from . import GSASIIscriptable as G2sc
def OnBrowse(event):
'''Responds when the Browse button is pressed to load a file.
The routine determines which button was pressed and gets the
appropriate file type and loads it into the appropriate place
in the dict.
'''
if btn3 == event.GetEventObject():
d = wx.DirDialog(dlg,
'Select directory for input files',
Settings['indir'],wx.DD_DEFAULT_STYLE)
d.CenterOnParent()
try:
if d.ShowModal() == wx.ID_OK:
Settings['indir'] = d.GetPath()
fInp3.SetValue(Settings['indir'])
finally:
d.Destroy()
elif btn4 == event.GetEventObject():
extList = 'GSAS iparm file (*.prm,*.inst,*.ins,.instprm)|*.prm;*.inst;*.ins;*.instprm'
d = wx.FileDialog(dlg,
'Choose instrument parameter file',
'', '',extList, wx.FD_OPEN)
if os.path.exists(Settings['instfile']):
dr,f = os.path.split(Settings['instfile'])
d.SetDirectory(dr)
d.SetFilename(f)
try:
if d.ShowModal() == wx.ID_OK:
Settings['instfile'] = d.GetPath()
fInp4.SetValue(Settings['instfile'])
# change the "read from" directory if defaulted
if Settings['indir'] == os.getcwd():
Settings['indir'] = os.path.dirname(d.GetPath())
fInp3.SetValue(Settings['indir'])
finally:
d.Destroy()
TestInput()
def OnFileOfFiles(event):
'''Read from a list of files and add those files in the order
specified in that file.
'''
# get a list of existing histograms
if FileTyp == 'pwd':
treePrfx = 'PWDR '
else:
treePrfx = 'PDF '
ReadList = []
if G2frame.GPXtree.GetCount():
item, cookie = G2frame.GPXtree.GetFirstChild(G2frame.root)
while item:
name = G2frame.GPXtree.GetItemText(item)
if name.startswith(treePrfx) and name not in ReadList:
ReadList.append(name)
item, cookie = G2frame.GPXtree.GetNextChild(G2frame.root, cookie)
Settings['ReadList'] = ReadList
extList = 'text file (*.txt,*.csv)|*.txt;*.csv'
d = wx.FileDialog(dlg,
'Choose a text file with file names',
'', '',extList, wx.FD_OPEN)
filelist = []
try:
if d.ShowModal() == wx.ID_OK:
if not os.path.exists(Settings['instfile']): return
if not os.path.exists(d.GetPath()): return
with open(d.GetPath(),'r') as fp:
for line in fp: # split lines at comma/tab, strip quotes, etc
if line.startswith('#'): continue
line = line.split(',')[0]
line = line.split('\t')[0]
f = line.replace('"','').replace("'",'').strip()
if not os.path.exists(f) and not os.path.abspath(f):
f = os.path.join(Settings['indir'],f)
if not os.path.exists(f):
print(f'Skipping file {f}, not found')
else:
filelist.append(f)
G2frame.CheckNotebook()
RunTimerPWDR(None,filelist)
wx.CallAfter(dlg.Destroy)
finally:
d.Destroy()
def onSetFmtSelection():
extSel.Clear()
extSel.AppendItems(fileReaders[Settings['fmt']].extensionlist)
Settings['extStr'] = fileReaders[Settings['fmt']].extensionlist[0]
extSel.SetSelection(0)
onSetExtSelection()
def onSetExtSelection():
Settings['filter'] = os.path.splitext(Settings['filter'])[0] + Settings['extStr']
flterInp.SetValue(Settings['filter'])
TestInput()
def OnQuit(event):
Settings['timer'].Stop()
wx.CallAfter(dlg.Destroy)
def TestInput(*args,**kwargs):
valid = True
if not os.path.exists(Settings['indir']):
valid = False
if FileTyp == 'pwd' and not os.path.exists(Settings['instfile']):
valid = False
btnstart.Enable(valid)
FofFbtn.Enable(valid)
def OnStart(event):
if btnstart.GetLabel() == 'Pause':
Settings['timer'].Stop()
btnstart.SetLabel('Continue')
return
else:
btnstart.SetLabel('Pause')
if Settings['timer'].IsRunning(): return
PollTime = 1 # sec
G2frame.CheckNotebook()
Settings['timer'].Start(int(1000*PollTime),oneShot=False)
# get a list of existing histograms
if FileTyp == 'pwd':
treePrfx = 'PWDR '
else:
treePrfx = 'PDF '
ReadList = []
if G2frame.GPXtree.GetCount():
item, cookie = G2frame.GPXtree.GetFirstChild(G2frame.root)
while item:
name = G2frame.GPXtree.GetItemText(item)
if name.startswith(treePrfx) and name not in ReadList:
ReadList.append(name)
item, cookie = G2frame.GPXtree.GetNextChild(G2frame.root, cookie)
Settings['ReadList'] = ReadList
def RunTimerPWDR(event,filelist=None):
if filelist is None:
if GSASIIpath.GetConfigValue('debug'):
import datetime
print ("DBG_Timer tick at {:%d %b %Y %H:%M:%S}\n".format(datetime.datetime.now()))
filelist = glob.glob(os.path.join(Settings['indir'],Settings['filter']))
if not filelist: return
#if GSASIIpath.GetConfigValue('debug'): print(filelist)
Id = None
for f in filelist:
if f in Settings['filesread']: continue
Settings['filesread'].append(f)
rd = fileReaders[Settings['fmt']]
rd.ReInitialize()
if not rd.ContentsValidator(f):
Settings['timer'].Stop()
btnstart.SetLabel('Continue')
G2MessageBox(dlg,'Error in reading file {}: {}'.format(
f, rd.errors))
return
#if len(rd.selections) > 1:
# G2fil.G2Print('Warning: Skipping file {}: multibank not yet implemented'.format(f))
# continue
block = 0
rdbuffer = {}
repeat = True
while repeat:
repeat = False
try:
flag = rd.Reader(f,buffer=rdbuffer, blocknum=block)
except:
flag = False
if flag:
rd.readfilename = f
if rd.warnings:
G2fil.G2Print("Read warning by", rd.formatName,
"reader:",
rd.warnings)
elif not block:
G2fil.G2Print("{} read by Reader {}"
.format(f,rd.formatName))
else:
G2fil.G2Print("{} block # {} read by Reader {}"
.format(f,block,rd.formatName))
block += 1
repeat = rd.repeat
else:
G2fil.G2Print("Warning: {} Reader failed to read {}"
.format(rd.formatName,f))
Iparm1, Iparm2 = G2sc.load_iprms(Settings['instfile'],rd)
if 'phoenix' in wx.version():
HistName = 'PWDR '+rd.idstring
else:
HistName = 'PWDR '+G2obj.StripUnicode(rd.idstring,'_')
# make new histogram names unique
HistName = G2obj.MakeUniqueLabel(HistName,Settings['ReadList'])
Settings['ReadList'].append(HistName)
# put into tree
Id = G2frame.GPXtree.AppendItem(parent=G2frame.root,text=HistName)
if 'T' in Iparm1['Type'][0]:
if not rd.clockWd and rd.GSAS:
rd.powderdata[0] *= 100. #put back the CW centideg correction
cw = np.diff(rd.powderdata[0])
rd.powderdata[0] = rd.powderdata[0][:-1]+cw/2.
if rd.GSAS: #NB: old GSAS wanted intensities*CW even if normalized!
npts = min(len(rd.powderdata[0]),len(rd.powderdata[1]),len(cw))
rd.powderdata[1] = rd.powderdata[1][:npts]/cw[:npts]
rd.powderdata[2] = rd.powderdata[2][:npts]*cw[:npts]**2 #1/var=w at this point
else: #NB: from topas/fullprof type files
rd.powderdata[1] = rd.powderdata[1][:-1]
rd.powderdata[2] = rd.powderdata[2][:-1]
if 'Itype' in Iparm2:
Ibeg = np.searchsorted(rd.powderdata[0],Iparm2['Tminmax'][0])
Ifin = np.searchsorted(rd.powderdata[0],Iparm2['Tminmax'][1])
rd.powderdata[0] = rd.powderdata[0][Ibeg:Ifin]
YI,WYI = G2pwd.calcIncident(Iparm2,rd.powderdata[0])
rd.powderdata[1] = rd.powderdata[1][Ibeg:Ifin]/YI
var = 1./rd.powderdata[2][Ibeg:Ifin]
var += WYI*rd.powderdata[1]**2
var /= YI**2
rd.powderdata[2] = 1./var
rd.powderdata[3] = np.zeros_like(rd.powderdata[0])
rd.powderdata[4] = np.zeros_like(rd.powderdata[0])
rd.powderdata[5] = np.zeros_like(rd.powderdata[0])
Ymin = np.min(rd.powderdata[1])
Ymax = np.max(rd.powderdata[1])
valuesdict = {
'wtFactor':1.0,
'Dummy':False,
'ranId':ran.randint(0,sys.maxsize),
'Offset':[0.0,0.0],'delOffset':0.02*Ymax,'refOffset':-.1*Ymax,'refDelt':0.1*Ymax,
'Yminmax':[Ymin,Ymax]
}
# apply user-supplied corrections to powder data
if 'CorrectionCode' in Iparm1:
print('Warning: CorrectionCode from instprm file not applied')
rd.Sample['ranId'] = valuesdict['ranId'] # this should be removed someday
G2frame.GPXtree.SetItemPyData(Id,[valuesdict,rd.powderdata])
G2frame.GPXtree.SetItemPyData(
G2frame.GPXtree.AppendItem(Id,text='Comments'),
rd.comments)
Tmin = min(rd.powderdata[0])
Tmax = max(rd.powderdata[0])
Tmin1 = Tmin
if 'NT' in Iparm1['Type'][0] and G2lat.Pos2dsp(Iparm1,Tmin) < 0.4:
Tmin1 = G2lat.Dsp2pos(Iparm1,0.4)
G2frame.GPXtree.SetItemPyData(
G2frame.GPXtree.AppendItem(Id,text='Limits'),
rd.pwdparms.get('Limits',[(Tmin,Tmax),[Tmin1,Tmax]])
)
G2frame.PatternId = G2gd.GetGPXtreeItemId(G2frame,Id,'Limits')
G2frame.GPXtree.SetItemPyData(
G2frame.GPXtree.AppendItem(Id,text='Background'),
rd.pwdparms.get('Background',
[['chebyschev-1',True,3,1.0,0.0,0.0],{'nDebye':0,'debyeTerms':[],'nPeaks':0,'peaksList':[],
'background PWDR':['',1.0,False]}]))
G2frame.GPXtree.SetItemPyData(
G2frame.GPXtree.AppendItem(Id,text='Instrument Parameters'),
[Iparm1,Iparm2])
G2frame.GPXtree.SetItemPyData(
G2frame.GPXtree.AppendItem(Id,text='Sample Parameters'),
rd.Sample)
G2frame.GPXtree.SetItemPyData(
G2frame.GPXtree.AppendItem(Id,text='Peak List')
,{'peaks':[],'sigDict':{}})
G2frame.GPXtree.SetItemPyData(
G2frame.GPXtree.AppendItem(Id,text='Index Peak List'),
[[],[]])
G2frame.GPXtree.SetItemPyData(
G2frame.GPXtree.AppendItem(Id,text='Unit Cells List'),
[])
G2frame.GPXtree.SetItemPyData(
G2frame.GPXtree.AppendItem(Id,text='Reflection Lists'),
{})
# if any Control values have been set, move them into tree
Controls = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,G2frame.root, 'Controls'))
Controls.update(rd.Controls)
# Tree entries complete
# select and show last PWDR file to be read
if Id:
G2frame.EnablePlot = True
G2frame.GPXtree.Expand(Id)
G2frame.GPXtree.SelectItem(Id)
dlg.Raise()
def RunTimerGR(event):
if GSASIIpath.GetConfigValue('debug'):
import datetime
print ("DBG_Timer tick at {:%d %b %Y %H:%M:%S}\n".format(datetime.datetime.now()))
filelist = glob.glob(os.path.join(Settings['indir'],Settings['filter']))
if not filelist: return
#if GSASIIpath.GetConfigValue('debug'): print(filelist)
Id = None
for f in filelist:
if f in Settings['filesread']: continue
Settings['filesread'].append(f)
rd = fileReaders[Settings['fmt']]
rd.ReInitialize()
if not rd.ContentsValidator(f):
Settings['timer'].Stop()
btnstart.SetLabel('Continue')
G2MessageBox(dlg,'Error in reading file {}: {}'.format(
f, rd.errors))
return
#if len(rd.selections) > 1:
# G2fil.G2Print('Warning: Skipping file {}: multibank not yet implemented'.format(f))
# continue
block = 0
rdbuffer = {}
repeat = True
while repeat:
repeat = False
try:
flag = rd.Reader(f,buffer=rdbuffer, blocknum=block)
except:
flag = False
if flag:
rd.readfilename = f
if rd.warnings:
G2fil.G2Print("Read warning by", rd.formatName,
"reader:",
rd.warnings)
elif not block:
G2fil.G2Print("{} read by Reader {}"
.format(f,rd.formatName))
else:
G2fil.G2Print("{} block # {} read by Reader {}"
.format(f,block,rd.formatName))
block += 1
repeat = rd.repeat
else:
G2fil.G2Print("Warning: {} Reader failed to read {}"
.format(rd.formatName,f))
if 'phoenix' in wx.version():
HistName = 'PDF '+rd.idstring
else:
HistName = 'PDF '+G2obj.StripUnicode(rd.idstring,'_')
HistName = G2obj.MakeUniqueLabel(HistName,Settings['ReadList'])
Settings['ReadList'].append(HistName)
# put into tree
Id = G2frame.GPXtree.AppendItem(parent=G2frame.root,text=HistName)
Ymin = np.min(rd.pdfdata[1])
Ymax = np.max(rd.pdfdata[1])
valuesdict = {
'wtFactor':1.0,'Dummy':False,'ranId':ran.randint(0,sys.maxsize),
'Offset':[0.0,0.0],'delOffset':0.02*Ymax,
'Yminmax':[Ymin,Ymax],
}
G2frame.GPXtree.SetItemPyData(
G2frame.GPXtree.AppendItem(Id,text='PDF Controls'),
{'G(R)':[valuesdict,rd.pdfdata,HistName],
'diffGRname':'','diffMult':1.0,'Rmax':Ymax,})
G2frame.GPXtree.SetItemPyData(G2frame.GPXtree.AppendItem(Id,text='PDF Peaks'),
{'Limits':[1.,5.],'Background':[2,[0.,-0.2*np.pi],False],'Peaks':[]})
# select and show last PWDR file to be read
if Id:
G2frame.EnablePlot = True
G2frame.GPXtree.Expand(Id)
G2frame.GPXtree.SelectItem(Id)
global AutoLoadWindow
Settings = {}
if AutoLoadWindow: # make sure only one window is open at a time
try:
AutoLoadWindow.Destroy()
except:
pass
AutoLoadWindow = None
if FileTyp == 'pwd':
fileReaders = [i for i in G2fil.LoadImportRoutines("pwd", "Powder_Data")
if i.scriptable]
fmtchoices = [p.longFormatName for p in fileReaders]
Settings['fmt'] = [i for i,v in enumerate(fmtchoices) if 'fxye' in v][0]
else:
fileReaders = [i for i in G2frame.ImportPDFReaderlist]
# if i.scriptable]
fmtchoices = [p.longFormatName for p in fileReaders]
Settings['fmt'] = 0
Settings['ext'] = 0
Settings['extStr'] = ''
Settings['filter'] = '*.*'
Settings['indir'] = os.getcwd()
Settings['instfile'] = ''
Settings['timer'] = wx.Timer()
if FileTyp == 'pwd':
Settings['timer'].Bind(wx.EVT_TIMER,RunTimerPWDR)
else:
Settings['timer'].Bind(wx.EVT_TIMER,RunTimerGR)
Settings['filesread'] = []
dlg = wx.Frame(G2frame,title='Automatic Data Loading',
style=wx.DEFAULT_FRAME_STYLE ^ wx.CLOSE_BOX)
mnpnl = wx.Panel(dlg)
mnsizer = wx.BoxSizer(wx.VERTICAL)
sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(wx.StaticText(mnpnl, wx.ID_ANY,'Select format:'))
fmtSel = G2ChoiceButton(mnpnl,fmtchoices,Settings,'fmt',onChoice=onSetFmtSelection)
sizer.Add(fmtSel)
sizer.Add((-1,-1),1,wx.EXPAND,1)
msg = '''This window serves two purposes. It can be used to read files
as they are added to a directory or it can be used to read files from an
externally-created file list. For either, set the file format and an
instrument parameter file must be specified.
%%
* For automatic reading, the files must be found in the directory specified by
"Read from:" and the selected extension. The "File filter:" can be used to
limit the files to those matching a wildcard, (for example, if
"202408*pow*.*" is used as a filter, then files must begin with "202408"
and must also contain the string "pow".)
%%
* For reading from a list of files, press the "Read from file with a list
of files" button. The input file must contain a list of files, one per line.
Lines beginning in '#' are ignored. If more than one column is used
(separated by commas or tabs), the file name should be the first column.
File names can be in quotes, but this is not required. The extension
is ignored, as is the "File filter". The "Read from" directory will be used
if the file name does not contain a full path and the file is not in the
current working directory.
'''
sizer.Add(HelpButton(mnpnl,msg,wrap=400),0,wx.RIGHT,5)
mnsizer.Add(sizer,0,wx.EXPAND)
sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(wx.StaticText(mnpnl, wx.ID_ANY,'Select extension:'))
extSel = G2ChoiceButton(mnpnl,[],Settings,'ext',Settings,'extStr',onChoice=onSetExtSelection)
sizer.Add(extSel,0)
sizer.Add((-1,-1),1,wx.EXPAND,1)
sizer.Add(wx.StaticText(mnpnl, wx.ID_ANY,' File filter: '))
flterInp = ValidatedTxtCtrl(mnpnl,Settings,'filter')
sizer.Add(flterInp)
mnsizer.Add(sizer,0,wx.EXPAND,0)
sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(wx.StaticText(mnpnl, wx.ID_ANY,'Read from: '),0,wx.ALIGN_CENTER_VERTICAL)
fInp3 = ValidatedTxtCtrl(mnpnl,Settings,'indir',size=(300,-1),OnLeave=TestInput)
sizer.Add(fInp3,1,wx.EXPAND)
btn3 = wx.Button(mnpnl, wx.ID_ANY, "Browse")
btn3.Bind(wx.EVT_BUTTON, OnBrowse)
sizer.Add(btn3,0,wx.ALIGN_CENTER_VERTICAL)
mnsizer.Add(sizer,0,wx.EXPAND)
if FileTyp == 'pwd':
sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(wx.StaticText(mnpnl, wx.ID_ANY,'Instrument parameter file from: '),0,wx.ALIGN_CENTER_VERTICAL)
fInp4 = ValidatedTxtCtrl(mnpnl,Settings,'instfile',size=(300,-1),OnLeave=TestInput)
sizer.Add(fInp4,1,wx.EXPAND)
btn4 = wx.Button(mnpnl, wx.ID_ANY, "Browse")
btn4.Bind(wx.EVT_BUTTON, OnBrowse)
sizer.Add(btn4,0,wx.ALIGN_CENTER_VERTICAL)
mnsizer.Add(sizer,0,wx.EXPAND)
# read a list of files
FofFbtn = wx.Button(mnpnl, wx.ID_ANY, 'Read from file with a list of files')
FofFbtn.Bind(wx.EVT_BUTTON, OnFileOfFiles)
mnsizer.Add(FofFbtn)
# buttons on bottom
sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add((-1,-1),1,wx.EXPAND)
btnstart = wx.Button(mnpnl, wx.ID_ANY, "Start")
btnstart.Bind(wx.EVT_BUTTON, OnStart)
sizer.Add(btnstart)
sizer.Add((20,-1),0,wx.EXPAND)
btnclose = wx.Button(mnpnl, wx.ID_ANY, "Close")
onSetFmtSelection()
btnclose.Bind(wx.EVT_BUTTON, OnQuit)
sizer.Add(btnclose)
sizer.Add((-1,-1),1,wx.EXPAND)
mnsizer.Add(sizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP,5)
mnpnl.SetSizer(mnsizer)
mnsizer.Fit(dlg)
dlg.CenterOnParent()
dlg.Show()
AutoLoadWindow = dlg # save window reference
# Deal with Origin 1/2 ambiguities ################################################################################
def ChooseOrigin(G2frame,rd):
G2elem.SetupGeneral(rd.Phase,G2frame.dirname)
# make copy of Phase but shift atoms Origin 1->2
O2Phase = copy.deepcopy(rd.Phase)
# make copy of atoms, shift to alternate origin
T = G2spc.spg2origins[rd.Phase['General']['SGData']['SpGrp']]
O2atoms = O2Phase['Atoms']
cx,ct,cs,cia = rd.Phase['General']['AtomPtrs']
SGData = rd.Phase['General']['SGData']
for atom in O2atoms:
for i in [0,1,2]:
atom[cx+i] += T[i]
atom[cs:cs+2] = G2spc.SytSym(atom[cx:cx+3],SGData)[0:2] # update symmetry & mult
#get density & distances
DisAglData = {}
DisAglData['SGData'] = rd.Phase['General']['SGData']
DisAglData['Cell'] = rd.Phase['General']['Cell'][1:] #+ volume
DisAglCtls = {'Factors': [0.85, 0],
'BondRadii': [], 'AngleRadii': [], 'AtomTypes': []}
for atom in rd.Phase['Atoms']:
DisAglCtls['BondRadii'].append(1.5)
DisAglCtls['AngleRadii'].append(0)
DisAglCtls['AtomTypes'].append(atom[ct])
txt = ''
for i,phObj in enumerate([rd.Phase,O2Phase]):
if i:
txt += "\n\nWith origin 1->2 shift applied\n"
else:
txt += "\nWith current coordinates and original origin\n"
cellContents = {}
G2elem.SetupGeneral(phObj,phObj['General']['Mydir'])
for atom in phObj['Atoms']:
if atom[ct] in cellContents:
cellContents[atom[ct]] += atom[cs+1]
else:
cellContents[atom[ct]] = atom[cs+1]
txt += ' Unit cell Contents: '
for i,k in enumerate(cellContents):
if i: txt += ', '
txt += '{}*{}'.format(cellContents[k],k)
den,_ = G2mth.getDensity(phObj['General'])
txt += "\n Density {:.2f} g/cc\n".format(den)
DisAglData['OrigAtoms'] = DisAglData['TargAtoms'] = [
[i,]+atom[ct-1:ct+1]+atom[cx:cx+3] for
i,atom in enumerate(phObj['Atoms'])]
# lbl,dis,angle = G2stMn.RetDistAngle(DisAglCtls,DisAglData)
# # get unique distances
# minDis = {}
# for i in dis:
# for j,o,s,d,e in dis[i]:
# key = '-'.join(sorted([lbl[i],lbl[j]]))
# if key not in minDis:
# minDis[key] = d
# elif d < minDis[key]:
# minDis[key] = d
# thirdShortest = sorted([minDis[k] for k in minDis])[:3][-1]
# shortTxt = ''
# for k in minDis:
# if minDis[k] <= thirdShortest:
# if shortTxt: shortTxt += ', '
# shortTxt += "{}: {:.2f}".format(k,minDis[k])
# txt += " Shortest distances are "+shortTxt
# do we know if there is a center of symmetry at origin?
centro = None
if 'xyz' in rd.SymOps:
centro = False
if '-x,-y,-z' in [i.replace(' ','').lower() for i in rd.SymOps['xyz']]:
centro = True
msg = 'Be careful here. This space group has two origin settings. GSAS-II requires the origin to be placed at a center of symmetry (Origin 2). You must choose the correct option below or all subsequent results will be *wrong*. For more info, press the help button (bottom right).\n'
if centro:
msg += '\nThere is an -x,-y,-z symmetry op in the file input, so this is likely already in Origin 2.\n'
elif centro is None:
msg += '\nNo symmetry operations are provided in the input file; you must review this yourself. You are recommended to review a plot of the structure to make sure the symmetry is correct.\n'
else:
msg += '\nSymmetry operations in the input file do not contain -x,-y,-z, indicating an origin shift is likely needed.\n'
msg += '\nNote that the stoichometry computations below, made from the coordinates, may help indicate the correct origin choice:'
width = 600
dlg = wx.Dialog(G2frame,wx.ID_ANY,'Warning: Shift origin?',
pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE,
size=(width,-1))
dlg.CenterOnParent()
mainSizer = wx.BoxSizer(wx.VERTICAL)
txtbox = wx.StaticText(dlg,wx.ID_ANY,msg)
txtbox.Wrap(width-10)
mainSizer.Add(txtbox,0)
mainSizer.Add((5,5))
txtbox = wx.StaticText(dlg,wx.ID_ANY,txt)
mainSizer.Add(txtbox,0,wx.ALIGN_CENTER,1)
mainSizer.Add((10,10))
O1Btn = wx.Button(dlg,wx.ID_ANY,"Apply origin shift")
O1Btn.Bind(wx.EVT_BUTTON, lambda x: dlg.EndModal(wx.ID_OK))
O2Btn = wx.Button(dlg,wx.ID_ANY,"Keep current coordinates")
O2Btn.Bind(wx.EVT_BUTTON, lambda x: dlg.EndModal(wx.ID_YES))
if centro:
O2Btn.SetDefault()
elif centro is not None:
O1Btn.SetDefault()
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
btnSizer.Add((20,20),1)
btnSizer.Add(O1Btn)
btnSizer.Add((10,10),0)
btnSizer.Add(O2Btn)
btnSizer.Add((20,20),1)
btnSizer.Add(HelpButton(dlg,helpIndex='Origin1'),0,wx.RIGHT,5)
mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
dlg.SetSizer(mainSizer)
dlg.Fit()
ans = dlg.ShowModal()
if ans == wx.ID_OK:
dlg.Destroy()
return O2Phase
elif ans == wx.ID_YES:
dlg.Destroy()
return rd.Phase
else:
dlg.Destroy()
return None
[docs]
def makeContourSliders(G2frame,Ymax,PlotPatterns,newPlot,plottype):
'''Create a non-modal dialog for sliders to set contour plot
intensity thresholds.
'''
def updatePlot():
'updates plot after a change in values'
wx.CallAfter(PlotPatterns,G2frame,newPlot=newPlot,plotType=plottype)
def OnSlider(event):
'respond when min or max slider is moved'
obj = event.GetEventObject()
val = obj.GetValue()/100.
if obj.mode == 'max':
G2frame.Cmax = val
else:
G2frame.Cmin = val
obj.txt.ChangeValue(int(Ymax*val))
updatePlot()
def OnNewVal(*args,**kwargs):
'respond when a value is placed in the min or max text box'
obj = kwargs['tc']
if obj.mode == 'max':
val = Range[1]
G2frame.Cmax = val/Ymax
else:
val = Range[0]
G2frame.Cmin = val/Ymax
obj.slider.SetValue(int((100*val/Ymax) + 0.5))
updatePlot()
# makeContourSliders starts here
Range = [Ymax*G2frame.Cmin,Ymax*G2frame.Cmax]
dlg = wx.Dialog(G2frame.plotFrame,style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
vbox = wx.BoxSizer(wx.VERTICAL)
vbox.Add((-1,5))
hbox = wx.BoxSizer(wx.HORIZONTAL)
hbox.Add((-1,-1),1,wx.EXPAND,1)
hbox.Add(wx.StaticText(
dlg,wx.ID_ANY,'Set Contour Intensity Limits'),1,wx.ALL)
hbox.Add((-1,-1),1,wx.EXPAND,1)
vbox.Add(hbox)
vbox.Add((-1,10))
dlg.slideSizer = wx.FlexGridSizer(2,3,5,5)
dlg.slideSizer.AddGrowableCol(2)
dlg.slideSizer.Add(wx.StaticText(parent=dlg,label=' Min intensity'),0,WACV)
minSel = wx.Slider(parent=dlg,style=wx.SL_HORIZONTAL,
value=int(100*G2frame.Cmin+0.5))
minSel.Bind(wx.EVT_SLIDER, OnSlider)
minVal = ValidatedTxtCtrl(dlg,Range,0,xmin=0,
xmax=Ymax-1, OnLeave=OnNewVal)
minVal.slider = minSel
minVal.mode = 'min'
minSel.txt = minVal
minSel.mode = 'min'
dlg.slideSizer.Add(minVal,0,WACV)
dlg.slideSizer.Add(minSel,0,wx.EXPAND|wx.ALL,3)
dlg.slideSizer.Add(wx.StaticText(parent=dlg,label=' Max intensity'),0,WACV)
maxSel = wx.Slider(parent=dlg,style=wx.SL_HORIZONTAL,
value=int(100*G2frame.Cmax+0.5))
maxSel.Bind(wx.EVT_SLIDER, OnSlider)
maxVal = ValidatedTxtCtrl(dlg,Range,1,xmin=1,
xmax=Ymax, OnLeave=OnNewVal)
maxVal.slider = maxSel
maxVal.mode = 'max'
maxSel.txt = maxVal
maxSel.mode = 'max'
dlg.slideSizer.Add(maxVal,0,WACV)
dlg.slideSizer.Add(maxSel,0,wx.EXPAND|wx.ALL,3)
vbox.Add(dlg.slideSizer,0,wx.EXPAND,0)
vbox.Add((-1,15))
hbox = wx.BoxSizer(wx.HORIZONTAL)
hbox.Add((-1,-1),1,wx.EXPAND,1)
btn = wx.Button(dlg, wx.ID_CLOSE)
hbox.Add(btn,0,wx.ALL,0)
btn.Bind(wx.EVT_BUTTON,lambda x: dlg.Destroy())
hbox.Add((-1,-1),1,wx.EXPAND,1)
vbox.Add(hbox,0,wx.EXPAND,0)
vbox.Add((-1,5))
dlg.SetSizer(vbox)
vbox.Fit(dlg)
wx.CallLater(100,minVal.SetValue,Range[0])
dlg.Show()
################################################################################
# GPX browser routines
[docs]
def skimGPX(fl):
'''pull out fit information from a .gpx file quickly
:returns: dict with status info
'''
if fl is None: return {}
result = {'other':[]}
if not os.path.exists(fl):
return {'error':'File does not exist!'}
cnt = 0
hist = 0
fp = open(fl,'rb')
result['last saved'] = time.ctime(os.stat(fl).st_mtime)
try:
while True:
cnt += 1
note = None
try:
data = G2IO.pickleLoad(fp)
except EOFError:
#print(cnt,'entries read')
break
if cnt > 50: # don't spend too long on this file, if big
result['PWDR'] += 3*[' .']
break
datum = data[0]
if datum[0] == 'Notebook':
result[datum[0]] = datum[1][-1]
elif 'Controls' in datum[0]:
# datum[0]['Seq Data']
if 'LastSavedUsing' in datum[1]:
result['last saved'] += ' (v' + datum[1]['LastSavedUsing'] +')'
elif datum[0] == 'Covariance':
d = datum[1].get('Rvals')
if d:
result[datum[0]] = 'Overall: Rwp={:.2f}, GOF={:.1f}'.format(
d.get('Rwp','?'),d.get('GOF','?'))
if d.get('converged',False): result[datum[0]] += ' **Converged**'
elif datum[0].startswith('PWDR '):
if 'Residuals' not in datum[1][0]: continue
if 'PWDR' not in result: result['PWDR'] = []
result['PWDR'].append(
"hist #{}: wR={:.2f} ({:})".format(
hist,datum[1][0]['Residuals'].get('wR','?'),datum[0]))
hist += 1
elif datum[0].startswith('HKLF '):
note = 'Single crystal histogram(s)'
elif datum[0].startswith('REFD '):
note = 'Reflectivity histogram(s)'
elif datum[0].startswith('SASD '):
note = 'Small angle histogram(s)'
elif datum[0].startswith('PDF '):
note = 'PDF histogram(s)'
elif datum[0].startswith('IMG '):
note = 'Image(s)'
elif datum[0] == 'Sequential results':
note = 'Sequential results'
elif datum[0] in ('Constraints','Restraints','Rigid bodies'):
pass
else:
# print(datum[0])
# breakpoint()
pass
if note:
if note not in result['other']:
result['other'].append(note)
except Exception as msg:
result['error'] = 'read error: '+str(msg)
finally:
fp.close()
return result
[docs]
class gpxFileSelector(wx.Dialog):
'''Create a file selection widget for locating .gpx files as a modal
dialog. Displays status information on selected files. After creating
this use dlg.ShowModal() to wait for selection of a file.
If dlg.ShowModal() returns wx.ID_OK, use dlg.Selection (multiple=False)
to obtain the selected file or dlg.Selections (multiple=True) to
obtain a list of multiple files.
:param wx.Frame parent: name of panel or frame that will be
the parent to the dialog. Can be None.
:param path startdir: Specifies the initial directory that is
opened when the window is initially opened. Default is '.'
:param bool multiple: if True, checkboxes are used to allow
selection of multiple files. Default is False
'''
def __init__(self,parent,startdir='.',multiple=False,*args,**kwargs):
self.timer = None
self.delay = 1500 # time to wait before applying filter (1.5 sec)
self.Selection = None
self.Selections = []
self.startDir = startdir
if startdir == '.':
self.startDir = os.getcwd()
self.multiple = multiple
wx.Dialog.__init__(self, parent=parent,
style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
self.CenterOnParent()
topSizer = wx.BoxSizer(wx.VERTICAL)
self.dirBtn = wxfilebrowse.DirBrowseButton(self,wx.ID_ANY, size=(650, -1),
changeCallback = self.DirSelected,
startDirectory = self.startDir
)
topSizer.Add(self.dirBtn,0,wx.EXPAND,1)
subSiz = wx.BoxSizer(wx.HORIZONTAL)
self.opt = {'useBak':False, 'sort':0, 'filter':'*'}
chk = G2CheckBoxFrontLbl(self,' Include .bakXX?',self.opt,'useBak',
OnChange=self.DirSelected)
subSiz.Add(chk,0,wx.ALIGN_CENTER_VERTICAL,0)
subSiz.Add((10,-1),1,wx.EXPAND,1)
subSiz.Add(wx.StaticText(self,wx.ID_ANY,' Sort by: '),0,wx.ALIGN_CENTER_VERTICAL,1)
choices = ['age','name (alpha+case)','name (alpha)']
for w in G2RadioButtons(self,self.opt,'sort',choices,
OnChange=self.DirSelected):
subSiz.Add(w,0,wx.ALIGN_CENTER_VERTICAL,0)
subSiz.Add((10,-1),1,wx.EXPAND,1)
subSiz.Add(wx.StaticText(self,wx.ID_ANY,'Name \nFilter: '),0,wx.ALIGN_CENTER_VERTICAL,1)
self.filterBox = ValidatedTxtCtrl(self, self.opt, 'filter',
size=(80,-1), style=wx.TE_PROCESS_ENTER,
OnLeave=self.DirSelected, notBlank=False)
self.filterBox.Bind(wx.EVT_TEXT,self._startUpdateTimer)
self.filterBox.Bind(wx.EVT_TEXT_ENTER,self.DirSelected)
subSiz.Add(self.filterBox)
subSiz.Add((2,-1))
topSizer.Add(subSiz,0,wx.EXPAND,0)
mainPanel = wx.SplitterWindow(self, wx.ID_ANY, style=wx.SP_LIVE_UPDATE|wx.SP_3D)
mainPanel.SetMinimumPaneSize(100)
if self.multiple:
self.fileBox = wx.CheckListBox(mainPanel,wx.ID_ANY, size=(200, 200),
style=wx.LB_SINGLE)
self.fileBox.Bind(wx.EVT_CHECKLISTBOX,self.FileSelected)
else:
self.fileBox = wx.ListBox(mainPanel,wx.ID_ANY, size=(200, 200),
style=wx.LB_SINGLE)
self.fileBox.Bind(wx.EVT_LISTBOX,self.FileSelected)
self.rtc = wxrt.RichTextCtrl(mainPanel, style=wx.VSCROLL|wx.HSCROLL|
wx.NO_BORDER|wx.richtext.RE_READONLY)
mainPanel.SplitVertically(self.fileBox, self.rtc, 200)
topSizer.Add(mainPanel,1,wx.EXPAND)
subSiz = wx.BoxSizer(wx.HORIZONTAL)
subSiz.Add((-1,-1),1,wx.EXPAND,1)
self.OKbtn = wx.Button(self, wx.ID_OK, label='Open')
self.OKbtn.Enable(False) # A file must be selected 1st
btn = wx.Button(self, wx.ID_CANCEL)
subSiz.Add(self.OKbtn)
subSiz.Add((5,-1))
subSiz.Add(btn)
subSiz.Add((-1,-1),1,wx.EXPAND,1)
topSizer.Add((-1,5))
topSizer.Add(subSiz,0,wx.EXPAND)
topSizer.Add((-1,5))
self.SetSizer(topSizer)
topSizer.Layout()
topSizer.Fit(self)
self.dirBtn.SetValue(self.startDir)
def _startUpdateTimer(self,event):
if self.timer:
self.timer.Restart(self.delay)
else:
self.timer = wx.CallLater(self.delay,self.DirSelected)
[docs]
def DirSelected(self,event=None,*args,**kwargs):
'''Respond to a directory being selected. List files found in fileBox and
clear any selections. Also clear any reference to a timer.
'''
import re
try:
if self.timer: self.timer.Stop()
except:
pass
self.timer = None
self.fileBox.Clear()
self.rtc.Clear()
self.Selection = None
self.Selections = []
self.OKbtn.Enable(False)
glb = self.opt['filter'].strip()
if not glb:
glb = '*'
elif not '*' in glb:
glb = '*' + glb + '*'
fullglob = os.path.join(self.dirBtn.GetValue(),glb+'.gpx')
self.fl = glob.glob(fullglob)
if self.opt['useBak']:
self.sl = [(os.path.split(i)[1],os.stat(i).st_mtime,i) for i in self.fl]
else:
self.sl = [(os.path.split(i)[1],os.stat(i).st_mtime,i) for i in self.fl
if not re.match(r'.*\.bak\d+\.gpx.*',i)]
if self.opt['sort'] == 0:
self.sl.sort(key=lambda x: x[1],reverse=True)
elif self.opt['sort'] == 1:
self.sl.sort(key=lambda x: x[0])
else:
self.sl.sort(key=lambda x: x[0].lower())
items = [i[0]+' ('+self._fmtTimeStampDelta(i[1])+')' for i in self.sl]
if items:
self.fileBox.InsertItems(items,0)
[docs]
def FileSelected(self,event):
'''Respond to a file being selected (or checked in multiple mode)
'''
if self.multiple: # disable Open when nothing is selected
self.Selections = []
OK = False
for i in self.fileBox.GetCheckedItems():
self.Selections.append(self.sl[i][2])
OK = True
self.OKbtn.Enable(OK)
else:
self.OKbtn.Enable(True)
self.Selection = self.sl[self.fileBox.GetSelection()][2]
result = skimGPX(self.Selection)
self.displayGPXrtc(result,self.Selection)
[docs]
def displayGPXrtc(self,result,fwp):
'''Show info about selected file in a RichText display'''
self.rtc.Clear()
if fwp is None: return
self.rtc.Freeze()
self.rtc.BeginSuppressUndo()
self.rtc.BeginAlignment(wx.TEXT_ALIGNMENT_CENTER)
self.rtc.BeginFontSize(14)
self.rtc.BeginBold()
self.rtc.WriteText(os.path.split(fwp)[1])
self.rtc.EndBold()
self.rtc.Newline()
self.rtc.EndFontSize()
self.rtc.EndAlignment()
self.rtc.WriteText('last saved on ')
self.rtc.WriteText(result['last saved'])
self.rtc.Newline()
if 'Covariance' in result:
self.rtc.BeginLeftIndent(0,40)
self.rtc.WriteText(result['Covariance'])
self.rtc.Newline()
self.rtc.EndLeftIndent()
if 'Notebook' in result and len(result.get('Notebook','').strip()):
self.rtc.BeginLeftIndent(0,40)
self.rtc.BeginItalic()
self.rtc.WriteText('Last notebook entry: ')
self.rtc.EndItalic()
self.rtc.WriteText(result['Notebook'])
self.rtc.Newline()
self.rtc.EndLeftIndent()
if len(result.get('other',[])) > 0:
self.rtc.BeginParagraphSpacing(0,0)
self.rtc.BeginLeftIndent(0)
self.rtc.BeginBold()
self.rtc.WriteText('Data types in project:')
self.rtc.EndBold()
self.rtc.EndLeftIndent()
self.rtc.Newline()
self.rtc.BeginLeftIndent(40)
for line in result['other']:
self.rtc.WriteText(line+'\n')
self.rtc.EndLeftIndent()
self.rtc.EndParagraphSpacing()
if 'PWDR' in result:
self.rtc.BeginParagraphSpacing(0,0)
self.rtc.BeginLeftIndent(0)
self.rtc.BeginBold()
self.rtc.WriteText('Powder histograms:')
self.rtc.EndBold()
self.rtc.EndLeftIndent()
self.rtc.Newline()
self.rtc.BeginLeftIndent(40)
for line in result['PWDR']:
self.rtc.WriteText(line+'\n')
self.rtc.EndLeftIndent()
self.rtc.EndParagraphSpacing()
if 'error' in result:
self.rtc.Newline()
self.rtc.BeginBold()
self.rtc.WriteText('Error encountered: ')
self.rtc.EndBold()
self.rtc.WriteText(result['error'])
self.rtc.EndSuppressUndo()
self.rtc.Thaw()
def _fmtTimeStampDelta(self,tm):
'Show file age relative to now'
delta = time.time() - tm
if delta > 60*60*24*365:
return "{:.2f} years".format(delta/(60*60*24*365))
elif delta > 60*60*24*7:
return "{:.1f} weeks".format(delta/(60*60*24*7))
elif delta > 60*60*24:
return "{:.1f} days".format(delta/(60*60*24))
elif delta > 60*60:
return "{:.1f} hours".format(delta/(60*60))
else:
return "{:.1f} minutes".format(delta/60)
CitationDict = {}
[docs]
def SaveCite(prog,text):
'''Save citation information as it is referenced so that all of it can be
displayed in the About GSAS-II window
'''
global CitationDict
CitationDict[prog] = text
[docs]
def GetCite(key,wrap=None,indent=None):
'''Return citation information, optionally with text wrapping.
'''
if GSASIIpath.GetConfigValue('debug') and key not in CitationDict:
print(f'Warning: GetCite citation ref {key!r} not defined.')
txt = CitationDict.get(key,'')
leftmargin = ''
if indent:
leftmargin=indent*' '
if wrap:
import textwrap
txt = '\n'.join(textwrap.wrap(txt,wrap,
replace_whitespace=False,
break_long_words=False,
initial_indent=leftmargin, subsequent_indent=leftmargin))
return txt
#########################
# Catalog citation refs #
#########################
# OK to call SaveCite anywhere in GSAS-II, but putting calls here allows control
# over their sequence. Calls anywhere else will show up after these.
SaveCite('small angle scattering',
'R.B. Von Dreele, J. Appl. Cryst. 47, 1748-9 (2014)')
SaveCite('DIFFaX',
'M.M.J. Treacy, J.M. Newsam & M.W. Deem, Proc. Roy. Soc. Lond. A 433, 499-520 (1991) doi: https://doi.org/10.1098/rspa.1991.0062')
SaveCite('NIST*LATTICE',
'''V. L. Karen and A. D. Mighell, NIST Technical Note 1290 (1991), https://nvlpubs.nist.gov/nistpubs/Legacy/TN/nbstechnicalnote1290.pdf; V. L. Karen & A. D. Mighell, U.S. Patent 5,235,523, https://patents.google.com/patent/US5235523A/en?oq=5235523''')
SaveCite('Parameter Impact',
'Toby, B. H. (2024). "A simple solution to the Rietveld refinement recipe problem." J. Appl. Cryst. 57(1): 175-180.')
SaveCite('Fundamental parameter fitting',
'''MH Mendenhall, K Mullen && JP Cline (2015), J. Res. of NIST, 120, p223. DOI: 10.6028/jres.120.014;
For Incident Beam Mono model, also cite: MH Mendenhall, D Black && JP Cline (2019), J. Appl. Cryst., 52, p1087. DOI: 10.1107/S1600576719010951
''')
SaveCite('RMCProfile',
'"RMCProfile: Reverse Monte Carlo for polycrystalline materials", M.G. Tucker, D.A. Keen, M.T. Dove, A.L. Goodwin and Q. Hui, Jour. Phys.: Cond. Matter 2007, 19, 335218. doi: https://doi.org/10.1088/0953-8984/19/33/335218')
SaveCite('PDFfit2',
'"PDFfit2 and PDFgui: computer programs for studying nanostructures in crystals", C.L. Farrow, P.Juhas, J.W. Liu, D. Bryndin, E.S. Bozin, J. Bloch, Th. Proffen and S.J.L. Billinge, J. Phys, Condens. Matter 19, 335219 (2007)., https://doi.org/10.1088/0953-8984/19/33/335219')
SaveCite('ISOTROPY, ISODISTORT, ISOCIF...',
'H. T. Stokes, D. M. Hatch, and B. J. Campbell, ISOTROPY Software Suite, iso.byu.edu.; B. J. Campbell, H. T. Stokes, D. E. Tanner, and D. M. Hatch, "ISODISPLACE: An Internet Tool for Exploring Structural Distortions." J. Appl. Cryst. 39, 607-614 (2006).')
SaveCite('ISODISPLACE',
'D. E. Tanner, and D. M. Hatch, "ISODISPLACE: An Internet Tool for Exploring Structural Distortions." J. Appl. Cryst. 39, 607-614 (2006).')
SaveCite('fullrmc',
'''"Atomic Stochastic Modeling & Optimization with fullrmc", B. Aoun, J. Appl. Cryst. 2022, 55(6) 1664-1676, DOI: 10.1107/S1600576722008536.
"Fullrmc, a Rigid Body Reverse Monte Carlo Modeling Package Enabled with Machine Learning and Artificial Intelligence", B. Aoun, Jour. Comp. Chem. 2016, 37, 1102-1111. DOI: 10.1002/jcc.24304''')
SaveCite('Bilbao: PSEUDO',
'''C. Capillas, E.S. Tasci, G. de la Flor, D. Orobengoa, J.M. Perez-Mato and M.I. Aroyo. "A new computer tool at the Bilbao Crystallographic Server to detect and characterize pseudosymmetry". Z. Krist. (2011), 226(2), 186-196 DOI:10.1524/zkri.2011.1321.''')
SaveCite('Bilbao: k-SUBGROUPSMAG',
'Symmetry-Based Computational Tools for Magnetic Crystallography, J.M. Perez-Mato, S.V. Gallego, E.S. Tasci, L. Elcoro, G. de la Flor, and M.I. Aroyo, Annu. Rev. Mater. Res. 2015. 45,217-48. doi: 10.1146/annurev-matsci-070214-021008')
SaveCite('Bilbao: PSEUDOLATTICE',
'Bilbao Crystallographic Server I: Databases and crystallographic computing programs, M. I. Aroyo, J. M. Perez-Mato, C. Capillas, E. Kroumova, S. Ivantchev, G. Madariaga, A. Kirov & H. Wondratschek, Z. Krist. 221, 1, 15-27 (2006). doi: https://doi.org/doi:10.1524/zkri.2006.221.1.15''')
SaveCite('Bilbao+GSAS-II magnetism',
'Determining magnetic structures in GSAS-II using the Bilbao Crystallographic Server tool k-SUBGROUPSMAG, R.B. Von Dreele & L. Elcoro, Acta Cryst. 2024, B80. doi: https://doi.org/10.1107/S2052520624008436')
SaveCite('SHAPES',
'A New Algorithm for the Reconstruction of Protein Molecular Envelopes from X-ray Solution Scattering Data, J. Badger, Jour. of Appl. Chrystallogr. 2019, 52, 937-944. doi: https://doi.org/10.1107/S1600576719009774')
SaveCite('Scikit-Learn',
'"Scikit-learn: Machine Learning in Python", Pedregosa, F., Varoquaux, G., Gramfort, A., Michel, V., Thirion, B., Grisel, O., Blondel, M., Prettenhofer, P., Weiss, R., Dubourg, V., Vanderplas, J., Passos, A., Cournapeau, D., Brucher, M., Perrot, M. and Duchesnay, E., Journal of Machine Learning Research (2011) 12, 2825-2830.')
SaveCite('Dysnomia',
'Dysnomia, a computer program for maximum-entropy method (MEM) analysis and its performance in the MEM-based pattern fitting, K. Moma, T. Ikeda, A.A. Belik & F. Izumi, Powder Diffr. 2013, 28, 184-193. doi: https://doi.org/10.1017/S088571561300002X')
def NISTlatUse(msgonly=False):
msg = f'Performing cell symmetry search using NIST*LATTICE.\n\nPlease cite:\n{GetCite("NIST*LATTICE",wrap=70,indent=5)}'
print(msg)
if msgonly: return msg
wx.MessageBox(msg,caption='Using NIST*LATTICE',style=wx.ICON_INFORMATION)
[docs]
def Load2Cells(G2frame,phase):
'''Accept two unit cells and use NIST*LATTICE to search for a relationship
that relates them.
The first unit cell is initialized as the currently selected phase and
the second unit cell is set to the first different phase from the tree.
The user can initialize the cell parameters to select a different phase
for either cell or can type in the values themselves.
:param wx.Frame G2frame: The main GSAS-II window
:param dict phase: the currently selected frame
'''
def setRatioMax(*arg,**kwarg):
'''Set the value for the max volume used in the search according
to the type of search selected.
'''
if nistInput[2] == 'I':
volRatW.Validator.xmax = 40
volMaxLbl.SetLabel(' (ratio, 1 to 40)')
else:
volRatW.Validator.xmax = 10
volMaxLbl.SetLabel(' (ratio, 1 to 10)')
volRatW.SetValue(min(volRatW.Validator.xmax,nistInput[3]))
def computeNISTlatCompare(event):
'run NIST*LATTICE after the compute button is pressed'
from . import nistlat
out = nistlat.CompareCell(cellLen[0], cellCntr[0],
cellLen[1], cellCntr[1],
tolerance=3*[nistInput[0]]+3*[nistInput[1]],
mode=nistInput[2], vrange=nistInput[3])
if len(out):
msg = str(len(out))+' Transformations were found. See console for matrices.'
G2MessageBox(G2frame,msg,'Transforms found')
print(len(out),'transform matrices found')
for i in out:
print(' ',i[5][0],'/',i[5][1],'/',i[5][2])
else:
G2MessageBox(G2frame,
'No transforms were found within supplied limits',
'No transforms found')
def setCellFromPhase(event):
'''respond to "set from phase" button. A phase is selected and
the unit cell info is loaded from that phase into the appropriate
cell widgets.
'''
cell = event.GetEventObject().cellNum
widgets = event.GetEventObject().widgets
phaseList = list(Phases.keys())
if len(Phases) == 0:
print('No phases in project')
return
elif len(Phases) == 1:
p = phaseList[0]
else:
dlg = G2SingleChoiceDialog(G2frame,'Select a phase from list',
'Select phase',phaseList)
dlg.CenterOnParent()
try:
if dlg.ShowModal() == wx.ID_OK:
p = phaseList[dlg.GetSelection()]
else:
return
finally:
dlg.Destroy()
ph2 = Phases[p]
cellLen[cell] = ph2['General']['Cell'][1:7]
cellCntr[cell] = ph2['General']['SGData']['SpGrp'].strip()[0]
for val,wid in zip(cellLen[cell],widgets[:6]):
wid.SetValue(val)
widgets[6].SetValue(cellCntr[cell])
dlg.Raise() # needed to bring modal dialog to front, at least on Mac
# Load2Cells starts here
msg = NISTlatUse(True)
nistInput=[0.2,1.,'I',8]
cellLen = [None,None]
cellCntr = [None,None]
cellLen[0] = phase['General']['Cell'][1:7]
cellCntr[0] = phase['General']['SGData']['SpGrp'].strip()[0]
Histograms,Phases = G2frame.GetUsedHistogramsAndPhasesfromTree()
cellLen[1] = 3*[1.]+3*[90.]
cellCntr[1] = 'P'
for p in Phases:
ph2 = Phases[p]
if ph2['ranId'] != phase['ranId']:
cellLen[1] = ph2['General']['Cell'][1:7]
cellCntr[1] = ph2['General']['SGData']['SpGrp'].strip()[0]
break
dlg = wx.Dialog(G2frame,style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
dlg.CenterOnParent()
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(wx.StaticText(dlg,label='NIST*LATTICE: Relate Two Unit Cells'),
0,wx.ALIGN_CENTER_HORIZONTAL,0)
sizer.Add((-1,15))
sizer.Add(wx.StaticText(dlg,label=msg))
sizer.Add((-1,15))
tableSizer = wx.FlexGridSizer(0,9,0,0)
tableSizer.Add((-1,-1))
for l in G2lat.cellUlbl:
tableSizer.Add(wx.StaticText(dlg,label=l),0,WACV|wx.ALIGN_CENTER)
tableSizer.Add(wx.StaticText(dlg,label='Centering'),0,WACV|wx.ALIGN_LEFT)
tableSizer.Add((-1,-1))
for cell in range(2):
tableSizer.Add(wx.StaticText(dlg,label='Cell '+str(cell+1)),0,wx.ALIGN_CENTER|wx.RIGHT,5)
wlist = []
for i in range(6):
l = 3
if i < 3: l = 4
w = ValidatedTxtCtrl(dlg,cellLen[cell],i,nDig=(7,l),size=(60,-1))
wlist.append(w)
tableSizer.Add(w,0,wx.LEFT|wx.RIGHT,3)
w = EnumSelector(dlg,cellCntr,cell,['P','A','B','C','F','I','R'])
tableSizer.Add(w)
wlist.append(w)
btn = wx.Button(dlg, wx.ID_ANY, 'Load from phase')
btn.cellNum = cell
btn.widgets = wlist
btn.Bind(wx.EVT_BUTTON, setCellFromPhase)
tableSizer.Add(btn)
sizer.Add(tableSizer,0,wx.LEFT|wx.RIGHT,20)
tableSizer = wx.FlexGridSizer(0,3,0,0)
tableSizer.Add(wx.StaticText(dlg,label='Cell length tolerance: '),
0,WACV|wx.ALIGN_LEFT)
w = ValidatedTxtCtrl(dlg,nistInput,0,nDig=(6,2))
tableSizer.Add(w)
tableSizer.Add(wx.StaticText(dlg,label=' (A) '))
tableSizer.Add(wx.StaticText(dlg,label='Cell angle tolerance: '),
0,WACV|wx.ALIGN_LEFT)
w = ValidatedTxtCtrl(dlg,nistInput,1,nDig=(6,1))
tableSizer.Add(w)
tableSizer.Add(wx.StaticText(dlg,label=' (degrees) '))
tableSizer.Add(wx.StaticText(dlg,label='Cell volume range: '),
0,WACV|wx.ALIGN_LEFT)
volRatW = ValidatedTxtCtrl(dlg,nistInput,3,xmin=1,xmax=40)
tableSizer.Add(volRatW)
volMaxLbl = wx.StaticText(dlg,label=' (ratio, 1 to 40)')
tableSizer.Add(volMaxLbl)
sizer.Add(tableSizer,0,wx.EXPAND)
tableSizer = wx.FlexGridSizer(0,2,0,0)
tableSizer.Add(wx.StaticText(dlg,label='Search mode: '),0,WACV|wx.ALIGN_LEFT)
tableSizer.Add(EnumSelector(dlg,nistInput,2,['Integral matrices', 'Fractional matrices'],
['I','F'],OnChange=setRatioMax))
sizer.Add(tableSizer,0,wx.EXPAND)
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
btn = wx.Button(dlg, wx.ID_ANY,'Compute')
btn.Bind(wx.EVT_BUTTON, computeNISTlatCompare)
btnSizer.Add((-1,-1),1,wx.EXPAND)
btnSizer.Add(btn)
btnSizer.Add((-1,-1),1,wx.EXPAND)
sizer.Add(btnSizer,0,wx.EXPAND|wx.CENTER,0)
btnsizer = wx.StdDialogButtonSizer()
btn = wx.Button(dlg, wx.ID_CLOSE)
btn.SetDefault()
btn.Bind(wx.EVT_BUTTON, lambda x: dlg.EndModal(wx.ID_OK))
btnsizer.AddButton(btn)
btnsizer.Realize()
sizer.Add(btnsizer, 0, wx.EXPAND|wx.ALL, 5)
dlg.SetSizer(sizer)
sizer.Fit(dlg)
if dlg.ShowModal() == wx.ID_OK:
dlg.Destroy()
else:
dlg.Destroy()
return
[docs]
class ScrolledStaticText(wx.StaticText):
'''Fits a long string into a small space by scrolling it. Inspired by
ActiveText.py from J Healey <rolfofsaxony@gmx.com>
https://discuss.wxpython.org/t/activetext-rather-than-statictext/36370
Use examples::
frm = wx.Frame(None) # create a frame
ms = wx.BoxSizer(wx.VERTICAL)
text = 'this is a long string that will be scrolled'
ms.Add(G2G.ScrolledStaticText(frm,label=text))
txt = G2G.ScrolledStaticText(frm,label=text, lbllen=20)
smallfont = wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT)
smallfont.SetPointSize(10)
txt.SetFont(smallfont)
ms.Add(txt)
ms.Add(G2G.ScrolledStaticText(frm,label=text,dots=False,delay=250,lbllen=20))
frm.SetSizer(ms)
:param w.Frame parent: Frame or Panel where widget will be placed
:param str label: string to be displayed
:param int delay: time between updates in ms (default is 100)
:param int lbllen: number of characters to show (default is 15)
:param bool dots: If True (default) ellipsis (...) are placed
at the beginning and end of the string when any characters
in the string are not shown. The displayed string length
will thus be lbllen+6 most of the time
:param (other): other optional keyword parameters for the
wx.StaticText widget such as size or style may be specified.
'''
def __init__(self, parent, label='', delay=100, lbllen=15,
dots=True, **kwargs):
wx.StaticText.__init__(self, parent, wx.ID_ANY, '', **kwargs)
self.fullmsg = label
self.lbllen = lbllen
self.msgpos = 0
self.dots = dots
self.onTimer(None)
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.onTimer)
self.timer.Start(delay, wx.TIMER_CONTINUOUS)
def onTimer(self,event):
if self.dots and self.msgpos > 0:
txt = '...'
else:
txt = ''
txt += self.fullmsg[self.msgpos:self.msgpos+self.lbllen+1]
if self.dots and self.msgpos+self.lbllen < len(self.fullmsg):
txt += '...'
try:
self.SetLabel(txt)
except:
self.timer.Stop()
self.msgpos += 1
if self.msgpos >= len(self.fullmsg): self.msgpos = 0
#===========================================================================
[docs]
def askQuestion(parent,question,title):
'''Simple code to ask a Y/N question and get answer'''
ans = True
try:
dlg = wx.MessageDialog(parent,question,title,wx.YES_NO | wx.ICON_QUESTION)
ans = (dlg.ShowModal() == wx.ID_YES)
finally:
dlg.Destroy()
return ans
[docs]
def haveGUI():
'''Test if there is a GUI that can be accessed
:returns: True if a GUI is available
'''
try:
return wx.App.IsMainLoopRunning()
except:
return False
#===========================================================================
def gitFetch(G2frame):
wx.BeginBusyCursor()
pdlg = wx.ProgressDialog('Updating','Performing git update',11,
style = wx.PD_ELAPSED_TIME|wx.PD_CAN_ABORT,
parent=G2frame)
try:
pdlg.CenterOnParent()
if hasattr(G2frame,'UpdateTask'): # check if git update has completed
count = 0
while G2frame.UpdateTask.poll() is None:
count += 1
if count > 10:
G2MessageBox(G2frame,
'Background git update has not completed, try again later',
title='Warning')
return
time.sleep(1)
ok,_ = pdlg.Update(count)
wx.GetApp().Yield()
if not ok:
return
if GSASIIpath.GetConfigValue('debug'): print('background update complete')
# try update one more time just to make sure
GSASIIpath.gitGetUpdate('immediate')
except Exception as msg:
raise Exception(msg)
finally:
pdlg.Destroy()
wx.EndBusyCursor()
[docs]
def gitCheckUpdates(G2frame):
'''Used to update to the latest GSAS-II version, but checks for a variety
of repository conditions that could make this process more complex. If
there are uncommitted local changes, these changes must be cached or
deleted first. If there are local changes that have been committed or a new
branch has been created, the user (how obstensibly must know use of git)
will probably need to do this manually. If GSAS-II has previously been
regressed (using :func:`gitSelectVersion`), then this is noted as well.
When all is done, function :func:`GSASIIpath.gitStartUpdate` is called to
actually perform the update.
'''
try:
gitFetch(G2frame) # download latest updates from server
except:
G2MessageBox(G2frame,
'Unable to perform update: if you have an internet connection, do you have write access to installation?',
title='git error')
return
status = GSASIIpath.gitTestGSASII()
if status < 0:
G2MessageBox(G2frame,
'Problem with git access to GSAS-II. Seek help or reinstall',
title='git error')
return
localChanges = bool(status & 5) # If True need to select between
# --git-stash or --git-reset
# False: neither should be used
if status&8: # not on local branch
if localChanges:
G2MessageBox(G2frame,
'You have made a local branch and have uncommited changes. Save, stash or restore then before an update can be done.',
title='update not possible')
return
else:
msg = ('You have made a local branch and have switched to that.'+
' Do you want to update anyway? Doing so will return'+
' you to the main branch. This may be better handled'+
' manually.\n\nPress "Yes" to continue with update\n'+
'Press "Cancel" to stop the update.')
dlg = wx.MessageDialog(G2frame, msg, 'Confirm update?',
wx.OK|wx.CANCEL|wx.CANCEL_DEFAULT|wx.CENTRE|wx.ICON_QUESTION)
ans = dlg.ShowModal()
dlg.Destroy()
if ans == wx.ID_CANCEL: return
regressmsg = ''
if status&2: # detached head
remoteupdates,localupdates = GSASIIpath.gitCountRegressions()
if localupdates:
msg = ("Your copy of GSAS-II has been regressed. You are "+
f"{remoteupdates} versions behind the current. "+
f"WARNING: you have committed {localupdates} "+
"changes locally. Your local commits will be difficult "+
"to locate if you update. SUGGESTION: if your changes "+
"are meaningful, create a new git branch and return "+
"to the main branch.\n\n"+
'Press "Yes" to continue, stranding your local changes\n'+
'Press "Cancel" to stop the update.')
dlg = wx.MessageDialog(G2frame, msg, 'Confirm update?',
wx.YES|wx.CANCEL|wx.CANCEL_DEFAULT|wx.CENTRE|wx.ICON_QUESTION)
ans = dlg.ShowModal()
dlg.Destroy()
if ans == wx.ID_CANCEL: return
regressmsg = f"Your copy of GSAS-II has been regressed. You are {remoteupdates} versions behind the current.\n\n"
else:
# on head, standard update, is update needed or blocked by local changes
rc,lc,_ = GSASIIpath.gitCheckForUpdates(False)
if len(rc) == 0:
G2MessageBox(G2frame,
'Your copy of GSAS-II is up to date. No update is needed.',
title='no updates')
return
if len(lc) != 0:
msg = ('You have made local changes and committed them '+
'into the main branch. GUI-based updates cannot '+
'be made. You should perform a git merge manually')
G2MessageBox(G2frame,msg,title='Do manual update')
return
cmds = ['--git-update']
if localChanges and GSASIIpath.GetConfigValue('debug'):
msg = ('You have locally-made changes to the GSAS-II source '+
'code files. Do you want to stash them before updating?\n\n'+
'If you select "Yes" the update will be applied after '+
'using git stash. You will need to use "git stash pop" '+
'to get these changes back.'+
'\n\nIf you select "No" no update will '+
'be performed.')
dlg = wx.MessageDialog(G2frame, msg, 'Stash Local Changes?',
wx.YES_NO|wx.NO_DEFAULT|wx.CENTRE|wx.ICON_QUESTION)
ans = dlg.ShowModal()
dlg.Destroy()
if ans != wx.ID_YES:
return
g2repo = GSASIIpath.openGitRepo(path2GSAS2)
import datetime as dt
now = dt.datetime.strftime(dt.datetime.now(),"%Y-%m-%dT%H:%M")
g2repo.git.stash(f'-m "preserving local changes prior to update from GUI @ {now}"')
elif localChanges:
if gitAskLocalChanges(G2frame,cmds): return
if gitAskSave(G2frame,regressmsg,cmds): return
# launch changes and restart
GSASIIpath.gitStartUpdate(cmds)
def gitAskLocalChanges(G2frame,cmds):
msg = ('You have locally-made changes to the GSAS-II source '+
'code files. Do you want to discard these changes?\n\n'+
'If you select "Yes" the changes will be overwritten '+
'before the update.\nIf you select "No" the changes will '+
'be archived (git stash).\nSelect "Cancel" '+
' to discontinue the update process.')
dlg = wx.MessageDialog(G2frame, msg, 'Save/Discard Local Changes?',
wx.YES_NO|wx.CANCEL|wx.NO_DEFAULT|wx.CENTRE|wx.ICON_QUESTION)
ans = dlg.ShowModal()
dlg.Destroy()
if ans == wx.ID_CANCEL:
return True
elif ans == wx.ID_YES:
cmds += ['--git-reset']
else:
cmds += ['--git-stash']
return False
def gitAskSave(G2frame,regressmsg,cmds):
# before doing update, need to save project
msg = ('Before continuing, do you want to save your project? '+
'Select "Yes" to save, "No" to skip the save, or "Cancel"'+
' to discontinue the update process.\n\n'+
'If "Yes", GSAS-II will reopen the project after the update. '+
'The update will now begin unless Cancel is pressed.')
dlg = wx.MessageDialog(G2frame, regressmsg+msg,
'Save Project and Start Update?',
wx.YES_NO|wx.CANCEL|wx.YES_DEFAULT|wx.CENTRE|wx.ICON_QUESTION)
ans = dlg.ShowModal()
dlg.Destroy()
if ans == wx.ID_CANCEL:
return True
elif ans == wx.ID_YES:
ans = G2frame.OnFileSave(None)
if ans:
cmds += [G2frame.GSASprojectfile]
return False
[docs]
def gitSelectVersion(G2frame):
'''Used to regress to a previous GSAS-II version, checking first
for a variety of repository conditions that could make this process
more complex. If there are uncommitted local changes, these changes
must be cached or deleted before a different version can be installed.
If there are local changes that have been committed or a new
branch has been created, the user (how obstensibly must know use of git)
will probably need to do this manually. If GSAS-II has previously been
regressed (using :func:`gitSelectVersion`), then this is noted as well.
When all is done, function :func:`GSASIIpath.gitStartUpdate` is called to
actually perform the update.
'''
# get updates from server
gitFetch(G2frame) # download latest updates from server
status = GSASIIpath.gitTestGSASII()
if status < 0:
G2MessageBox(G2frame,
'Problem with git access to GSAS-II. Seek help or reinstall',
title='git error')
return
localChanges = bool(status & 5) # If True need to select between
# --git-stash or --git-reset
# False: neither should be used
if status&8: # not on local branch
if localChanges:
msg = ('You have switched to a local branch and have '+
'uncommited changes. Save, stash or restore and switch '+
'back to the main branch. Regression is not possible '+
'from the GUI when on any other branch.')
else:
msg = ('You have made and switched to a local branch. '+
'You must manually switch back to the main branch.'+
' Regression is not possible '+
'from the GUI when on any other branch.')
G2MessageBox(G2frame,msg,title='update not possible')
return
regressmsg = ''
if status&2: # detached head
remoteupdates,localupdates = GSASIIpath.gitCountRegressions()
if localupdates:
msg = ("Your copy of GSAS-II has been regressed. You are "+
f"{remoteupdates} versions behind the current. "+
f"WARNING: you have committed {localupdates} "+
"changes locally. Your local commits will be difficult "+
"to locate if you update. SUGGESTION: if your changes "+
"are meaningful, create a new git branch and return "+
"to the main branch.\n\n"+
'Press "Yes" to continue, stranding your local changes\n'+
'Press "Cancel" to stop the update.')
dlg = wx.MessageDialog(G2frame, msg, 'Confirm update?',
wx.YES|wx.CANCEL|wx.CANCEL_DEFAULT|wx.CENTRE|wx.ICON_QUESTION)
ans = dlg.ShowModal()
dlg.Destroy()
if ans == wx.ID_CANCEL: return
regressmsg = f"Your copy of GSAS-II has been regressed. You are {remoteupdates} versions behind the current.\n\n"
else:
# on head, standard update, is update blocked by local changes
rc,lc,_ = GSASIIpath.gitCheckForUpdates(False)
if len(lc) != 0:
msg = ('You have made local changes and committed them '+
'into the main branch. GUI-based regression cannot '+
'be done. You should perform a "git checkout" to the '+
'desired version manually')
G2MessageBox(G2frame,msg,title='Do manual update')
return
# browse and select a version here
dlg = gitVersionSelector()
ans = dlg.ShowModal()
if ans == wx.ID_CANCEL: return
githash = dlg.getVersion()
if githash is None:
print('Nothing to be done')
return
if githash == 0:
print('select newest GSAS-II version')
cmds = ['--git-update']
else:
cmds = [f'--git-regress={githash}']
if localChanges:
if gitAskLocalChanges(G2frame,cmds): return
if gitAskSave(G2frame,regressmsg,cmds): return
# launch changes and restart
GSASIIpath.gitStartUpdate(cmds)
[docs]
def gitSelectBranch(event):
'''Pull in latest GSAS-II branches on origin server; Allow user to
select a branch; checkout that branch and restart GSAS-II.
Expected to be used by developers and by expert users only.
'''
G2frame = wx.App.GetMainTopWindow()
gitInst = GSASIIpath.HowIsG2Installed()
if not gitInst.startswith('github-rev'):
G2MessageBox(G2frame,
'Unable to switch branches unless GSAS-II has been installed from GitHub; installed as: '+gitInst,
'Not a git install')
return
if not os.path.exists(GSASIIpath.path2GSAS2):
print(f'Warning: Directory {GSASIIpath.path2GSAS2} not found')
return
if os.path.exists(os.path.join(GSASIIpath.path2GSAS2,'..','.git')):
path2repo = os.path.join(path2GSAS2,'..') # expected location
elif os.path.exists(os.path.join(GSASIIpath.path2GSAS2,'.git')):
path2repo = GSASIIpath.path2GSAS2
else:
print(f'Warning: Repository {path2GSAS2} not found')
return
try:
g2repo = GSASIIpath.openGitRepo(path2repo)
except Exception as msg:
print(f'Warning: Failed to open repository. Error: {msg}')
return
if g2repo.is_dirty() or g2repo.index.diff("HEAD"): # changed or staged files
G2MessageBox(G2frame,
'You have local changes. They must be reset, committed or stashed before switching branches',
'Local changes')
return
if g2repo.head.is_detached:
G2MessageBox(G2frame,
'You have a old previous version loaded; you must be on a branch head to switching branches',
'Detached head')
return
# make sure that branches are accessible & get updates
print('getting updates...',end='')
g2repo.git.remote('set-branches','origin','*')
print('..',end='')
g2repo.git.fetch()
print('.done')
branchlist = [i.strip() for i in g2repo.git.branch('-r').split('\n') if '->' not in i]
choices = [i for i in [os.path.split(i)[1] for i in branchlist] if i != g2repo.active_branch.name]
if len(choices) == 0:
G2MessageBox(G2frame,
'No branches were found to select. Unexpected!',
'No branches')
return
if len(choices) == 1:
b = choices[0]
else:
dlg = G2SingleChoiceDialog(G2frame,'Select branch to use','Select Branch',
choices)
dlg.CenterOnParent()
try:
if dlg.ShowModal() == wx.ID_OK:
b = choices[dlg.GetSelection()]
else:
return
finally:
dlg.Destroy()
msg = f'''Confirm switching from git branch {g2repo.active_branch.name!r} to {b!r}.
If confirmed here, GSAS-II will restart.
Do you want to save your project before restarting?
Select "Yes" to save, "No" to skip the save, or "Cancel"
to discontinue the restart process.
If "Yes", GSAS-II will reopen the project after the update.
The switch will be made unless Cancel is pressed.'''
dlg = wx.MessageDialog(G2frame, msg, 'Confirm branch switch?',
wx.YES_NO|wx.CANCEL|wx.YES_DEFAULT|wx.CENTRE|wx.ICON_QUESTION)
ans = dlg.ShowModal()
dlg.Destroy()
if ans == wx.ID_CANCEL:
return
elif ans == wx.ID_YES:
ans = G2frame.OnFileSave(None)
if not ans: return
project = os.path.abspath(G2frame.GSASprojectfile)
print(f"Restarting GSAS-II with project file {project!r}")
else:
print("Restarting GSAS-II without a project file ")
project = None
# I hope that it is possible to do a checkout on Windows
# (source files are not locked). If this is not the case
# then another approach will be needed, where a .bat file is used
# or GSASIIpath is used, as is the case for updates
a = g2repo.git.checkout(b)
if 'Your branch is behind' in a:
print('updating local copy of branch')
print(g2repo.git.pull())
G2fil.openInNewTerm(project)
print ('exiting GSAS-II')
sys.exit()
[docs]
def gitSwitchMaster2Main():
'''This is "patch" code to switch from the master branch
to the main branch. At some point this will be made part of the
update process in the master branch. This routine is not needed in
the main branch.
Switching is a bit complicated as additional Python packages are needed
and becuase the GSASII.py file gets renamed to G2.py so "shortcuts" need
to be re-created to reference that.
'''
G2frame = wx.App.GetMainTopWindow()
gitInst = GSASIIpath.HowIsG2Installed()
if not gitInst.startswith('github-rev'):
G2MessageBox(G2frame,
'Unable to update to new branch because GSAS-II was not installed from GitHub; installed as: '+gitInst,
'Not a git install')
return
if not os.path.exists(GSASIIpath.path2GSAS2):
msg = f'Warning: Directory {GSASIIpath.path2GSAS2} not found; not installed properly'
print(msg)
G2MessageBox(G2frame,msg)
return
if os.path.exists(os.path.join(GSASIIpath.path2GSAS2,'..','.git')):
path2repo = os.path.join(path2GSAS2,'..') # expected location
elif os.path.exists(os.path.join(GSASIIpath.path2GSAS2,'.git')):
path2repo = GSASIIpath.path2GSAS2
else:
msg = f'Warning: Repository {path2GSAS2} not found; Update not possible if not installed with git.'
print(msg)
G2MessageBox(G2frame,msg)
return
try:
g2repo = GSASIIpath.openGitRepo(path2repo)
except Exception as msg:
msg = f'Warning: Failed to open repository. Error: {msg}'
print(msg)
G2MessageBox(G2frame,msg)
return
if g2repo.is_dirty() or g2repo.index.diff("HEAD"): # changed or staged files
msg = 'You have local changes. They must be reset, committed or stashed before an update is possible'
print(msg)
G2MessageBox(G2frame,msg,'Local changes')
return
# Should not get here with detached head (regressed version) need to be on latest (last) version
# go get into this routine
if g2repo.head.is_detached:
G2MessageBox(G2frame,
'You have a old previous version loaded; you must be on a branch head to updateswitching branches',
'Detached head')
return
# this also should not happen
if g2repo.active_branch.name != "master":
G2MessageBox(G2frame,
f'You are on the {g2repo.active_branch.name} branch. This can only be run from master.',
'Not on master')
return
# make sure that branches are accessible & get updates
print('getting updates...',end='')
g2repo.git.remote('set-branches','origin','*')
print('..',end='')
g2repo.git.fetch()
print('.done')
branchlist = [i.strip() for i in g2repo.git.branch('-r').split('\n') if '->' not in i]
choices = [i for i in [os.path.split(i)[1] for i in branchlist] if i != g2repo.active_branch.name]
b = "main"
if b not in choices:
G2MessageBox(G2frame,
f'You are on the {g2repo.active_branch.name!r} branch, but branch {b!r} was not found.',
f'Unexpected: No {b} branch')
return
if not GSASIIpath.condaTest():
msg = '''In April 2025, GSAS-II switched to a new branch ("main")
that has significant internal reorganization requested by several users. All
future updates will be on this branch. It appears you have installed Python
manually, so an automatic update is not possible.
You can install the recommended Python packages and manually use git
commands to switch from the "master" branch to the "main" branch, but
it may be easier to simply reinstall GSAS-II.
See web page GSASII.github.io for information on how to install GSAS-II.
'''
ShowScrolledInfo(G2frame,msg,header='Please Note',
height=250)
return
# all checks passed, check with user and then get started
msg = f'''In April 2025, GSAS-II GSAS-II switched to a new branch ("main")
that has significant internal reorganization requested by several users. All
future updates will be on this branch. If you continue here, GSAS-II will
make the changes needed to move your installation to the new branch:
1) Additional Python packages needed by GSAS-II will be installed.
2) Git will be used to install the latest GSAS-II files
3) Shortcuts to the latest GSAS-II version will be installed; shortcuts
previously installed will fail if not replaced.
If the update fails, please reinstall GSAS-II from https://bit.ly/G2download
(https://github.com/AdvancedPhotonSource/GSAS-II-buildtools/releases/latest)
See web page GSASII.github.io for information on how to install.
Confirm switching from git branch {g2repo.active_branch.name!r} to {b!r} by
selecting "Save" or "Skip" below ("Cancel" will quit the update).
If confirmed here, GSAS-II will restart after the update.
Do you want to save your project before restarting?
Select "Save" to save, "Skip" to skip the save, or "Cancel"
to discontinue the update process.
The update will be made unless Cancel is pressed.'''
ans = ShowScrolledInfo(G2frame,msg,header='Please Note',
height=400,
buttonlist=[
('Save project and update',
lambda event: event.GetEventObject().GetParent().EndModal(wx.ID_OK)),
('Skip save and update',
lambda event: event.GetEventObject().GetParent().EndModal(wx.ID_NO)),
('Cancel',
lambda event: event.GetEventObject().GetParent().EndModal(wx.ID_CANCEL))
])
if ans == wx.ID_CANCEL:
return
elif ans == wx.ID_YES:
ans = G2frame.OnFileSave(None)
if not ans: return
project = os.path.abspath(G2frame.GSASprojectfile)
print(f"Restarting GSAS-II with project file {project!r}")
else:
print("Restarting GSAS-II without a project file ")
project = None
# 1) install the needed Python packages
#=======================================
# listing of GSAS-II packages here; TODO: should be unified into a single location
#ReqPackages = ['git','numpy','matplotlib','wx','OpenGL','scipy'] # assume present
NewReqPackages = ['CifFile', 'conda'] # make sure installed
# packages that are optional but should be present for all of GSAS-II to function:
RunOptPackages = ['PIL','requests','h5py','imageio','zarr','xmltodict','pybaselines','seekpath']
# where the import name is not the package name, translate with:
pkgnames = {'wx':'wxpython', 'OpenGL':'pyopengl','CifFile':'PyCifRW','PIL':'pillow',
'git':'gitpython'}
install = ['PyCifRW','pybaselines'] # these need to be reinstalled as they are no longer "vendored"
for pkglist in NewReqPackages,RunOptPackages: # Packages
for pkg in pkglist:
try:
exec('import '+pkg)
except:
install.append(pkgnames.get(pkg,pkg))
#install = [] # TODO: skip on testing
if install:
print('Installing packages: ',' ,'.join(install))
dlg = wx.ProgressDialog('Installing Python packages',
f'Please wait while conda downloads and installs {len(install)} package(s). This can take a while.',10,
style=wx.PD_AUTO_HIDE)
dlg.Update(1)
GSASIIpath.condaInstall(install)
dlg.Destroy()
# 2) switch to the main branch
#=======================================
print('Starting checkout of new branch')
a = g2repo.git.checkout("main")
if 'Your branch is behind' in a:
print('updating local copy of branch')
print(g2repo.git.pull())
# post-install stuff
print(f'Byte-compiling all .py files in {GSASIIpath.path2GSAS2!r}... ',end='')
import compileall
compileall.compile_dir(GSASIIpath.path2GSAS2,quiet=True)
print('done')
# now run the replaced system-specific installers to create shortcuts
# pointing to G2.py
print('\nStart system-specific install')
for k,s in {'win':"makeBat.py", 'darwin':"makeMacApp.py",
'linux':"makeLinux.py"}.items():
if sys.platform.startswith(k):
script = os.path.join(GSASIIpath.path2GSAS2,'install',s)
if not os.path.exists(script):
print(f'Platform-specific script {script!r} not found')
script = ''
break
else:
print(f'Unknown platform {sys.platform}')
# on a Mac, make an applescript
if script and sys.platform.startswith('darwin'):
print(f'running {script}')
import subprocess
subprocess.run([sys.executable,script],cwd=GSASIIpath.path2GSAS2)
# On windows make a batch file with hard-coded paths to Python and GSAS-II
elif script and sys.platform.startswith('win'):
script = os.path.normpath(os.path.join(GSASIIpath.path2GSAS2,'install',s))
print(f'running {script!r}')
import subprocess
subprocess.run([sys.executable,script],cwd=GSASIIpath.path2GSAS2)
# On linux, make a desktop icon with hard-coded paths to Python and GSAS-II
elif script:
sys.argv = [script]
print(f'running {sys.argv[0]}')
with open(sys.argv[0]) as source_file:
exec(source_file.read())
print('system-specific install done, restarting\n\n')
g2script = os.path.join(GSASIIpath.path2GSAS2,'G2.py')
if os.path.exists(g2script):
G2fil.openInNewTerm(project,g2script)
print ('exiting this session after update completed')
sys.exit()
else:
print(f'Unexpected error: file {g2script!r} not found')
#===========================================================================
# Importer GUI stuff
[docs]
def ImportMsg(parent,msgs):
'''Show a message with the warnings from importers that
could not be installed (due to uninstalled Python packages). Then
offer the chance to install GSAS-II packages using :func:`SelectPkgInstall`
'''
text = ('Warnings message(s) from load of importers:\n\n * '+
'\n\n * '.join(msgs)+
'\n\n\tTo use any of these importers, press the "Install packages" button\n\tbelow. It is fine to ignore these warnings if you will not need to read\n\tthat file type.')
ShowScrolledInfo(parent,text,
header='Importer load problems',
width=650,
buttonlist=[('Install packages',SelectPkgInstall), wx.ID_CLOSE]
)
[docs]
def patch_condarc():
'''Comment out any references to "file:" locations in the .condarc
file. These should not be there and cause problems.
'''
rc = os.path.normpath(os.path.join(GSASIIpath.path2GSAS2,'..','..','.condarc'))
if os.path.exists(rc):
txt = open(rc,'r').read()
else:
return
if '\n - /' in txt:
print(f'Patching file {rc}')
with open(rc,'w') as fp:
fp.write(txt.replace('\n - /','\n# - /'))
[docs]
def SelectPkgInstall(event):
'''Offer the user a chance to install Python packages needed by one or
more importers. There might be times where something like this will be
useful for other GSAS-II actions.
'''
dlg = event.GetEventObject().GetParent()
if hasattr(dlg,'EndModal'): # present when called from a dialog
dlg.EndModal(wx.ID_OK)
G2frame = wx.App.GetMainTopWindow()
choices = {}
keylist = []
for key in G2fil.condaRequestList:
for item in G2fil.condaRequestList[key]:
if item in choices:
choices[item] += f', {key}'
else:
choices[item] = key
keylist.append(item)
msg = 'Select package(s) to install'
if GSASIIpath.condaTest():
msg += ' using conda'
else:
msg += ' using pip'
sel = MultiColMultiSelDlg(G2frame, 'Install packages?', msg,
[('package',120,0),('needed by',300,0)],
[i for i in choices.items()])
if sel is None: return
if not any(sel): return
pkgs = [keylist[i] for i,f in enumerate(sel) if f]
wx.BeginBusyCursor()
pdlg = wx.ProgressDialog('Updating','Installing Python packages; this can take a while',
parent=G2frame)
if GSASIIpath.condaTest():
patch_condarc()
if not GSASIIpath.condaTest(True):
GSASIIpath.addCondaPkg()
err = GSASIIpath.condaInstall(pkgs)
if err:
print(f'Error from conda: {err}')
wx.EndBusyCursor()
pdlg.Destroy()
return
else:
err = GSASIIpath.pipInstall(pkgs)
if err:
print(f'Error from pip: {err}')
wx.EndBusyCursor()
pdlg.Destroy()
return
wx.EndBusyCursor()
pdlg.Destroy()
msg = '''You must restart GSAS-II to access the importer(s)
requiring the package(s) just installed.
Select "Yes" to save and restart, "No" to restart without
the save, or "Cancel" to discontinue the restart process
and continue use of GSAS-II without the importer(s).
If "Yes", GSAS-II will reopen the project after the update.
'''
dlg = wx.MessageDialog(G2frame, msg, 'Save and restart?',
wx.YES_NO|wx.CANCEL|wx.YES_DEFAULT|wx.CENTRE|wx.ICON_QUESTION)
ans = dlg.ShowModal()
dlg.Destroy()
if ans == wx.ID_CANCEL:
return
elif ans == wx.ID_YES:
ans = G2frame.OnFileSave(None)
if not ans: return
project = os.path.abspath(G2frame.GSASprojectfile)
print(f"Restarting GSAS-II with project file {project!r}")
else:
print("Restarting GSAS-II without a project file ")
project = None
G2fil.openInNewTerm(project)
print ('exiting GSAS-II')
sys.exit()
[docs]
def StringSearchTemplate(parent,title,prompt,start,help=None):
'''Dialog to obtain a single string value from user
:param wx.Frame parent: name of parent frame
:param str title: title string for dialog
:param str prompt: string to tell use what they are inputting
:param str start: default input value, if any
:param str help: if supplied, a help button is added to the dialog that
can be used to display the supplied help text/URL for setting this
variable. (Default is '', which is ignored.)
'''
def on_invalid():
G2MessageBox(parent,
'The pattern must retain some non-blank characters from the original string. Resetting so you can start again.','Try again')
valItem.SetValue(start)
return
def on_char_typed(event):
keycode = event.GetKeyCode()
if keycode == 32 or keycode == 63: # ' ' or '?' - replace with '?'
#has a range been selected?
sel = valItem.GetSelection()
if sel[0] == sel[1]:
insertion_point = valItem.GetInsertionPoint()
sel = (insertion_point,insertion_point+1)
for i in range(*sel):
valItem.Replace(i, i + 1, '?')
# Move the insertion point forward one character
valItem.SetInsertionPoint(i + 1)
# make sure some characters remain
if len(valItem.GetValue().replace('?','').strip()) == 0:
wx.CallAfter(on_invalid)
event.Skip(False)
elif keycode >= wx.WXK_SPACE: # anything else printable, ignore
event.Skip(False)
else: # arrows etc are processed naturally
event.Skip(True)
dlg = wx.Dialog(parent,wx.ID_ANY,title,pos=wx.DefaultPosition,
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
dlg.CenterOnParent()
mainSizer = wx.BoxSizer(wx.VERTICAL)
sizer1 = wx.BoxSizer(wx.HORIZONTAL)
txt = wx.StaticText(dlg,wx.ID_ANY,prompt)
sizer1.Add((10,-1),1,wx.EXPAND)
txt.Wrap(500)
sizer1.Add(txt,0,wx.ALIGN_CENTER)
sizer1.Add((10,-1),1,wx.EXPAND)
if help:
sizer1.Add(HelpButton(dlg,help),0,wx.ALL)
mainSizer.Add(sizer1,0,wx.EXPAND)
sizer1 = wx.BoxSizer(wx.HORIZONTAL)
valItem = wx.TextCtrl(dlg,wx.ID_ANY,value=start,style=wx.TE_PROCESS_ENTER)
valItem.Bind(wx.EVT_CHAR, on_char_typed)
valItem.Bind(wx.EVT_TEXT_ENTER, lambda event: event.Skip(False))
wx.CallAfter(valItem.SetSelection,0,0) # clear the initial selection
mainSizer.Add(valItem,1,wx.EXPAND)
btnsizer = wx.StdDialogButtonSizer()
OKbtn = wx.Button(dlg, wx.ID_OK)
OKbtn.SetDefault()
btnsizer.AddButton(OKbtn)
btn = wx.Button(dlg, wx.ID_CANCEL)
btnsizer.AddButton(btn)
btnsizer.Realize()
mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER)
dlg.SetSizer(mainSizer)
mainSizer.Fit(dlg)
ans = dlg.ShowModal()
if ans != wx.ID_OK:
dlg.Destroy()
return
else:
val = valItem.GetValue()
dlg.Destroy()
return val
[docs]
def HistogramNameTemplate(exporter,stripChars):
'''Dialog to obtain a string value for grouping histograms
:param obj exporter: reference to exporter object
:param str stripChars: Characters that are not allowed in file names
'''
def on_char_typed(event):
keycode = event.GetKeyCode()
#has a range been selected?
sel = valItem.GetSelection()
if sel[0] == sel[1]:
insertion_point = valItem.GetInsertionPoint()
sel = (insertion_point,insertion_point+1)
if keycode == 32 or keycode == 46: # ' ' or '.' - replace with '|'
for i in range(*sel):
if i >= len(valItem.GetValue()): continue
if valItem.GetValue()[i] != box:
valItem.Replace(i, i + 1, '|')
# Move the insertion point forward one character
valItem.SetInsertionPoint(i + 1)
event.Skip(False)
# elif keycode == wx.WXK_SPACE:
# insertion_point = valItem.GetInsertionPoint()
# valItem.SetInsertionPoint(insertion_point + 1)
elif keycode == 33: # ! put original value back
for i in range(*sel):
if i > len(valItem.GetValue()): continue
if valItem.GetValue()[i] != box:
valItem.Replace(i, i + 1, common[i])
# Move the insertion point forward one character
valItem.SetInsertionPoint(i + 1)
event.Skip(False)
elif keycode >= wx.WXK_SPACE and keycode <= 255: # anything else printable, substitute
for i in range(*sel):
if i >= len(valItem.GetValue()): continue
if valItem.GetValue()[i] != box:
valItem.Replace(i, i + 1, chr(keycode))
valItem.SetInsertionPoint(i + 1)
event.Skip(False)
else: # arrows etc are processed naturally
event.Skip(True)
setExampleName()
def ConvertHistname2File(histname):
out = ''
for h,m,t in zip(histname,mask,valItem.GetValue()):
if not m:
out += h
elif t != '|':
out += t
return out.translate(stripDict)
def setExampleName():
dlg.firstname.SetLabel(
ConvertHistname2File(first[5:])+extension
)
parent = exporter.G2frame
hists = exporter.histnam
extension = exporter.extension
stripDict = str.maketrans({c: "_" for c in stripChars}) # used to strip characters
n = min(len(s) for s in hists) # find shortest name
mask = [1==len( # find characters that are the same in every histogram
{s[i+5] for s in [s.translate(stripDict) for s in hists]}
) for i in range(n-5)]
# process first histogram name showing a box for letters that change
box = '\u25A1'
first = hists[0]
commonWbox = [first[i+5] if l else box for i,l in enumerate(mask)]
common = ''.join(commonWbox)
dlg = wx.Dialog(parent,wx.ID_ANY,'Histogram name template',
pos=wx.DefaultPosition,
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
dlg.CenterOnParent()
start = common
mainSizer = wx.BoxSizer(wx.VERTICAL)
sizer1 = wx.BoxSizer(wx.HORIZONTAL)
sizer1.Add(wx.StaticText(dlg,wx.ID_ANY,'Define a template to be used to generate file names'))
sizer1.Add((10,-1),1,wx.EXPAND)
help = '''This dialog is used to determine the file names that will
be used when exporting a series of PWDR files.
The file names will include all the characters that
differ between the histogram names but here you can select
which of the characters that are the same for all the
histogram names will be included. Optionally you can change
some or all the text that is in common between the histograms'''
sizer1.Add(HelpButton(dlg,help),0,wx.ALL)
mainSizer.Add(sizer1,0,wx.EXPAND)
mainSizer.Add((-1,10))
prompt = ('The following characters are the same in all histograms, '
'except where \u25A1 is present. '
'File names will be generated from this as a template. '
'Press "." or space to delete characters from the template'
'(which are shown as "|"). '
'Use "!" to revert changes. '
'Type any other text to place characters into the template.\n\n'
f'Note that characters "{stripChars}" will be converted to "_".'
)
txt = wx.StaticText(dlg,wx.ID_ANY,prompt)
txt.Wrap(500)
mainSizer.Add(txt,0,wx.ALIGN_LEFT)
mainSizer.Add((-1,10))
valItem = wx.TextCtrl(dlg,wx.ID_ANY,value=start,style=wx.TE_PROCESS_ENTER)
valItem.Bind(wx.EVT_CHAR, on_char_typed)
valItem.Bind(wx.EVT_TEXT_ENTER, lambda event: event.Skip(False))
valItem.Bind(wx.EVT_CHAR_HOOK, lambda event: event.Skip(
not event.GetKeyCode() in [wx.WXK_BACK,wx.WXK_DELETE]))
wx.CallAfter(valItem.SetSelection,0,0) # clear the initial selection
mainSizer.Add(valItem,1,wx.EXPAND,1)
mainSizer.Add((-1,10))
mainSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Name of first file will be:'))
mainSizer.Add((-1,5))
dlg.firstname = wx.StaticText(dlg,wx.ID_ANY,'?')
mainSizer.Add(dlg.firstname,0,wx.ALIGN_LEFT|wx.LEFT,10)
mainSizer.Add((-1,-1),1,wx.EXPAND)
setExampleName()
btnsizer = wx.StdDialogButtonSizer()
OKbtn = wx.Button(dlg, wx.ID_OK)
OKbtn.SetDefault()
btnsizer.AddButton(OKbtn)
btn = wx.Button(dlg, wx.ID_CANCEL)
btnsizer.AddButton(btn)
btnsizer.Realize()
mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER)
dlg.SetSizer(mainSizer)
mainSizer.Fit(dlg)
ans = dlg.ShowModal()
if ans != wx.ID_OK:
dlg.Destroy()
exporter.fileNames = None
else:
val = valItem.GetValue()
dlg.Destroy()
exporter.fileNames = [ConvertHistname2File(i[5:]) for i in hists]
if __name__ == '__main__':
app = wx.App()
GSASIIpath.InvokeDebugOpts()
frm = wx.Frame(None) # create a frame
ms = wx.BoxSizer(wx.VERTICAL)
#siz = G2SliderWidget(pnl,valArr,'k','test slider w/entry',.2,1.2,100)
#ms.Add(siz)
#siz = G2SliderWidget(pnl,valArr,'k','test slider w/entry',20,50,.1)
#ms.Add(siz)
text = 'this is a long string that will be scrolled'
#ms.Add(ScrolledStaticText(frm,label=text))
#txt = ScrolledStaticText(frm,label=text, lbllen=20)
#smallfont = wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT)
#smallfont.SetPointSize(10)
#txt.SetFont(smallfont)
#ms.Add(txt)
#ms.Add(ScrolledStaticText(frm,label=text,dots=False,delay=250, lbllen=20))
frm.SetSizer(ms)
frm.Show(True)
G2frame = frm
#ShowWebPage('http://wxpython.org',G2frame,internal=True)
#ShowHelp('hist/phase',G2frame,'internal')
testAtoms = ['']
nm = [' ','0','1','-1','2','-2','3','-3','4','5','6','7','8','9']
dm = ['1','2','3','4','5','6']
kfmt = ['choice','/','choice',', ','choice','/','choice',', ','choice','/','choice',' ']
def strTest(text):
if '.' in text: # no decimals
return False
elif text.strip() in [' ','0','1','-1','3/2']: # specials
return True
elif '/' in text: #process fraction
nums = text.split('/')
return (0 < int(nums[1]) < 10) and (0 < abs(int(nums[0])) < int(nums[1]))
return False
msg = 'test of MultiDataDialog'
kvec = [['0','0','0'],[' ',' ',' '],[' ',' ',' ',' ']]
dlg = MultiDataDialog(G2frame,title='k-SUBGROUPSMAG options',
prompts=[' k-vector 1',' k-vector 2',' k-vector 3',
' Use whole star',' Filter by','preserve axes',
'test for mag. atoms','all have moment','max unique'],
values=kvec+[False,'',True,'',False,100],
limits=[['0','0','0'],['0','0','0'],['0','0','0'],
[True,False],['',' Landau transition',' Only maximal subgroups',],
[True,False],testAtoms,[True,False],[1,100]],
testfxns = [[strTest,strTest,strTest],
[strTest,strTest,strTest],
[strTest,strTest,strTest],
None,None,None,None,None,None],
formats=[['testfxn','testfxn','testfxn'],
['testfxn','testfxn','testfxn'],
['testfxn','testfxn','testfxn'],
'bool','choice','bool','choice','bool','%d',],
header=msg)
if dlg.ShowModal() == wx.ID_OK: print(dlg.GetValues())
# if True:
# title='title here'
# header = 'this is where a header goes. this is where a header to explain what to do goes this is where a header to explain what to do goes'
# choices = [('xmltodict', 'Bruker .brml Importer'),
# ('zarr', 'MIDAS Zarr importer'),
# ('h5py', 'HDF5 image importer'),
# ('hdf5', 'HDF5 image importer'),
# ('test',),('val','used','ignored'),[]]
# colInfo = [('package',100, False),
# ('needed by',200, False)]
# parent = wx.App.GetMainTopWindow()
# print(MultiColMultiSelDlg(parent, title, header, colInfo, choices))
sys.exit()
app.MainLoop()
# choices = [wx.ID_YES,wx.ID_NO]
# warnmsg = '\nsome info\non a few lines\nand one more'
# ans = ShowScrolledInfo(header='Constraint Warning',
# txt='Warning noted after last constraint edit:\n'+warnmsg+
# '\n\nKeep this change?',
# buttonlist=choices,parent=frm,height=250)
# print(ans, choices)
import sys; sys.exit()
#======================================================================
# test Grid with GridFractionEditor
#======================================================================
# tbl = [[1.,2.,3.],[1.1,2.1,3.1]]
# colTypes = 3*[wg.GRID_VALUE_FLOAT+':10,5',]
# Gtbl = Table(tbl,types=colTypes,rowLabels=['a','b'],colLabels=['1','2','3'])
# Grid = GSGrid(frm)
# Grid.SetTable(Gtbl,True)
# for i in (0,1,2):
# attr = wx.grid.GridCellAttr()
# attr.IncRef()
# attr.SetEditor(GridFractionEditor(Grid))
# Grid.SetColAttr(i, attr)
# frm.SetSize((400,200))
# app.MainLoop()
# sys.exit()
#======================================================================
# test Tutorial access
#======================================================================
# dlg = OpenTutorial(frm)
# if dlg.ShowModal() == wx.ID_OK:
# print("OK")
# else:
# print("Cancel")
# dlg.Destroy()
# sys.exit()
#======================================================================
# test ScrolledMultiEditor
#======================================================================
# Data1 = {
# 'Order':1,
# 'omega':'string',
# 'chi':2.0,
# 'phi':'',
# }
# elemlst = sorted(Data1.keys())
# prelbl = sorted(Data1.keys())
# dictlst = len(elemlst)*[Data1,]
#Data2 = [True,False,False,True]
#Checkdictlst = len(Data2)*[Data2,]
#Checkelemlst = range(len(Checkdictlst))
# print 'before',Data1,'\n',Data2
# dlg = ScrolledMultiEditor(
# frm,dictlst,elemlst,prelbl,
# checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
# checklabel="Refine?",
# header="test")
# if dlg.ShowModal() == wx.ID_OK:
# print "OK"
# else:
# print "Cancel"
# print 'after',Data1,'\n',Data2
# dlg.Destroy()
# Data3 = {
# 'Order':1.0,
# 'omega':1.1,
# 'chi':2.0,
# 'phi':2.3,
# 'Order1':1.0,
# 'omega1':1.1,
# 'chi1':2.0,
# 'phi1':2.3,
# 'Order2':1.0,
# 'omega2':1.1,
# 'chi2':2.0,
# 'phi2':2.3,
# }
# elemlst = sorted(Data3.keys())
# dictlst = len(elemlst)*[Data3,]
# prelbl = elemlst[:]
# prelbl[0]="this is a much longer label to stretch things out"
# Data2 = len(elemlst)*[False,]
# Data2[1] = Data2[3] = True
# Checkdictlst = len(elemlst)*[Data2,]
# Checkelemlst = range(len(Checkdictlst))
#print 'before',Data3,'\n',Data2
#print dictlst,"\n",elemlst
#print Checkdictlst,"\n",Checkelemlst
# dlg = ScrolledMultiEditor(
# frm,dictlst,elemlst,prelbl,
# checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
# checklabel="Refine?",
# header="test",CopyButton=True)
# if dlg.ShowModal() == wx.ID_OK:
# print "OK"
# else:
# print "Cancel"
#print 'after',Data3,'\n',Data2
# Data2 = list(range(100))
# elemlst += range(2,6)
# postlbl += range(2,6)
# dictlst += len(range(2,6))*[Data2,]
# prelbl = range(len(elemlst))
# postlbl[1] = "a very long label for the 2nd item to force a horiz. scrollbar"
# header="""This is a longer\nmultiline and perhaps silly header"""
# dlg = ScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
# header=header,CopyButton=True)
# print Data1
# if dlg.ShowModal() == wx.ID_OK:
# for d,k in zip(dictlst,elemlst):
# print k,d[k]
# dlg.Destroy()
# if CallScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
# header=header):
# for d,k in zip(dictlst,elemlst):
# print k,d[k]
#======================================================================
# test G2MultiChoiceDialog
#======================================================================
# choices = []
# for i in range(21):
# choices.append("option_"+str(i))
# od = {
# 'label_1':'This is a bool','value_1':True,
# 'label_2':'This is a int','value_2':-1,
# 'label_3':'This is a float','value_3':1.0,
# 'label_4':'This is a string','value_4':'test',}
# dlg = G2MultiChoiceDialog(frm, 'Sequential refinement',
# 'Select dataset to include',
# choices,extraOpts=od)
# sel = range(2,11,2)
# dlg.SetSelections(sel)
# dlg.SetSelections((1,5))
# if dlg.ShowModal() == wx.ID_OK:
# for sel in dlg.GetSelections():
# print (sel,choices[sel])
# print (od)
# od = {}
# dlg = G2MultiChoiceDialog(frm, 'Sequential refinement',
# 'Select dataset to include',
# choices,extraOpts=od)
# sel = range(2,11,2)
# dlg.SetSelections(sel)
# dlg.SetSelections((1,5))
# if dlg.ShowModal() == wx.ID_OK: pass
#======================================================================
# test wx.MultiChoiceDialog
#======================================================================
# dlg = wx.MultiChoiceDialog(frm, 'Sequential refinement',
# 'Select dataset to include',
# choices)
# sel = range(2,11,2)
# dlg.SetSelections(sel)
# dlg.SetSelections((1,5))
# if dlg.ShowModal() == wx.ID_OK:
# for sel in dlg.GetSelections():
# print sel,choices[sel]
# pnl = wx.Panel(frm)
# siz = wx.BoxSizer(wx.VERTICAL)
# td = {'Goni':200.,'a':1.,'int':1,'calc':1./3.,'string':'s'}
# for key in sorted(td):
# txt = ValidatedTxtCtrl(pnl,td,key,typeHint=float)
# siz.Add(txt)
# pnl.SetSizer(siz)
# siz.Fit(frm)
# app.MainLoop()
# print(td)
choicelist=[ ('a','b','c'), ('test1','test2'),('no choice',)]
headinglist = [ 'select a, b or c', 'select 1 of 2', 'No option here']
dlg = MultipleChoicesDialog(choicelist,headinglist,parent=frm)
if dlg.ShowModal() == wx.ID_OK:
print(dlg.chosen)
print(MultipleChoicesSelector(choicelist,headinglist,frm))
pnl = wx.Panel(frm)
valArr = {'k':1.0}
ms = wx.BoxSizer(wx.VERTICAL)
#siz = G2SliderWidget(pnl,valArr,'k','test slider w/entry',.2,1.2,100)
#ms.Add(siz)
siz = G2SliderWidget(pnl,valArr,'k','test slider w/entry',2,5,1)
ms.Add(siz)
#siz = G2SliderWidget(pnl,valArr,'k','test slider w/entry',20,50,.1)
#ms.Add(siz)
pnl.SetSizer(ms)
ms.Fit(frm)
app.MainLoop()
print(valArr)