; $Id: cw_field.pro,v 1.28 2004/02/27 23:06:39 douge Exp $ ; ; Copyright (c) 1992-2004, Research Systems, Inc. All rights reserved. ; Unauthorized reproduction prohibited. ;+ ; NAME: ; CW_FIELD ; ; PURPOSE: ; This widget cluster function manages a data entry field widget. ; The field consists of a label and a text widget. CW_FIELD's can ; be string fields, integer fields or floating-point fields. The ; default is an editable string field. ; ; CATEGORY: ; Widget Clusters. ; ; CALLING SEQUENCE: ; Result = CW_FIELD(Parent) ; ; INPUTS: ; Parent: The widget ID of the widget to be the field's parent. ; ; KEYWORD PARAMETERS: ; TITLE: A string containing the text to be used as the label for the ; field. The default is "Input Field:". ; ; VALUE: The initial value in the text widget. This value is ; automatically converted to the type set by the STRING, ; INTEGER, and FLOATING keywords described below. ; ; UVALUE: A user value to assign to the field cluster. This value ; can be of any type. ; ; UNAME: A user supplied string name to be stored in the ; widget's user name field. ; ; FRAME: The width, in pixels, of a frame to be drawn around the ; entire field cluster. The default is no frame. ; ;RETURN_EVENTS: Set this keyword to make cluster return an event when a ; is pressed in a text field. The default is ; not to return events. Note that the value of the text field ; is always returned when the WIDGET_CONTROL, field, GET_VALUE=X ; command is used. ; ; ALL_EVENTS: Like RETURN_EVENTS but return an event whenever the ; contents of a text field have changed. ; ; COLUMN: Set this keyword to center the label above the text field. ; The default is to position the label to the left of the text ; field. ; ; ROW: Set this keyword to position the label to the left of the text ; field. This is the default. ; ; XSIZE: An explicit horizontal size (in characters) for the text input ; area. The default is to let the window manager size the ; widget. Using the XSIZE keyword is not recommended. ; ; YSIZE: An explicit vertical size (in lines) for the text input ; area. The default is 1. ; ; STRING: Set this keyword to have the field accept only string values. ; Numbers entered in the field are converted to their string ; equivalents. This is the default. ; ; FLOATING: Set this keyword to have the field accept only floating-point ; values. Any number or string entered is converted to its ; floating-point equivalent. ; ; INTEGER: Set this keyword to have the field accept only integer values. ; Any number or string entered is converted to its integer ; equivalent (using FIX). For example, if 12.5 is entered in ; this type of field, it is converted to 12. ; ; LONG: Set this keyword to have the field accept only long integer ; values. Any number or string entered is converted to its ; long integer equivalent (using LONG). ; ; FONT: A string containing the name of the X Windows font to use ; for the TITLE of the field. ; ; FIELDFONT: A string containing the name of the X Windows font to use ; for the TEXT part of the field. ; ; NOEDIT: Normally, the value in the text field can be edited. Set this ; keyword to make the field non-editable. ; ; OUTPUTS: ; This function returns the widget ID of the newly-created cluster. ; ; COMMON BLOCKS: ; None. ; ; PROCEDURE: ; Create the widgets, set up the appropriate event handlers, and return ; the widget ID of the newly-created cluster. ; ; EXAMPLE: ; The code below creates a main base with a field cluster attached ; to it. The cluster accepts string input, has the title "Name:", and ; has a frame around it: ; ; base = WIDGET_BASE() ; field = CW_FIELD(base, TITLE="Name:", /FRAME) ; WIDGET_CONTROL, base, /REALIZE ; ; MODIFICATION HISTORY: ; Written by: Keith R. Crosley June 1992 ; KRC, January 1993 -- Added support for LONG ; integers. ; AB, 7 April 1993, Removed state caching. ; JWG, August 1993, Completely rewritten to make ; use of improved TEXT widget functionality ; ACY, 25 March, 1994, fix usage of FRAME keyword ; KDB, May 1994, Initial value =0 would result ; in a null text field. Fixed ; keyword check. ; CT, RSI, March 2001: Pass keywords directly into WIDGET_BASE, ; without assigning default values, since the defaults are ; handled by WIDGET_BASE. Avoids assigning defaults if user passes ; in undefined variables. ; CT, RSI, July 2001: Fix bug in previous mod. If user passes in a ; numeric VALUE but forgets to set the /FLOAT, we still need ; to convert to a string before passing onto WIDGET_TEXT. ; ;- ; ; Check and return the portion of a string that ; is a valid floating point number. ; FUNCTION CW_FIELD_VALIDATE, Value ; Look for invalid mantissa COMPILE_OPT hidden IF Value EQ '' THEN RETURN, '' Chars = [ BYTE(Value), 0b ] Curr = 0 NeedDecimal = 1 ; Valid #s ; [+-][exponent] ; number ::= [0-9]+[.[0-9]*] or .[0-9]+ ; exponent ::= {eEdD}[+-][0-9]+ ; Signed value? IF Chars[Curr] EQ 43b OR Chars[Curr] EQ 45b THEN Curr = Curr + 1 ; Look for digits before the decimal point IF Chars[Curr] GE 48b AND Chars[Curr] LE 57b THEN BEGIN NeedDecimal = 0 ; while(isdigit(*p))++p; WHILE Chars[Curr] GE 48b AND Chars[Curr] LE 57b DO Curr = Curr + 1 ENDIF ; Must have .[0-9]+ IF NeedDecimal THEN BEGIN IF Chars[Curr] NE 46b THEN RETURN,'' ; invalid # Curr = Curr + 1 IF Chars[Curr] LT 48b OR Chars[Curr] GT 57b THEN RETURN,'' ; while(isdigit(*p))++p; WHILE Chars[Curr] GE 48b AND Chars[Curr] LE 57b DO Curr = Curr + 1 ENDIF ELSE BEGIN ; Might have .[0-9]* IF Chars[Curr] EQ 46b THEN BEGIN Curr = Curr + 1 ; while(isdigit(*p))++p; WHILE Chars[Curr] GE 48b AND Chars[Curr] LE 57b DO Curr = Curr + 1 ENDIF ENDELSE ; Exponent? Dummy = WHERE(Chars[Curr] EQ BYTE("dDeE"), Count) IF Count THEN BEGIN ; Save exponent position in case the exponent is invalid ; and only mantissa is valid number. SaveCurr = Curr - 1 Curr = Curr + 1 ; skip 'e' ; Signed exponent? IF Chars[Curr] EQ 43b OR Chars[Curr] EQ 45b THEN Curr = Curr + 1 ; At least one digit after 'e' or exponent is malformed IF Chars[Curr] LT 48b OR Chars[Curr] GT 57b THEN BEGIN Curr = SaveCurr ; Revert -- invalid exponent ENDIF ELSE BEGIN ; find end of exponent digits WHILE Chars[Curr] GE 48b AND Chars[Curr] LE 57b DO Curr = Curr + 1 ENDELSE ENDIF RETURN,STRING(Chars[0:Curr]) ; Chars from 0-Curr are valid END FUNCTION CW_FIELD_VALUE, Value, Type COMPILE_OPT hidden IF Type EQ 0 THEN RETURN, Value NValue = CW_FIELD_VALIDATE(Value[0]) CASE Type OF 1: RETURN, FLOAT(NValue) 2: RETURN, FIX(NValue) 3: RETURN, LONG(NValue) ENDCASE END ; ; Procedure to set the value of a CW_FIELD ; PRO CW_FIELD_SET, Base, Value COMPILE_OPT hidden sValue = Value ; Prevent alteration from reaching back to caller Sz = SIZE(sValue) IF Sz[0] NE 7 THEN sValue = STRTRIM(Value,2) Child = WIDGET_INFO(Base, /CHILD) WIDGET_CONTROL, Child, GET_UVALUE=State, /NO_COPY WIDGET_CONTROL, State.TextId, $ SET_VALUE=STRTRIM(CW_FIELD_VALUE(sValue, State.Type),2) WIDGET_CONTROL, Child, SET_UVALUE=State, /NO_COPY END ; ; Function to get the value of a CW_FIELD ; FUNCTION CW_FIELD_GET, Base COMPILE_OPT hidden Child = WIDGET_INFO(Base, /CHILD) WIDGET_CONTROL, Child, GET_UVALUE=State, /NO_COPY WIDGET_CONTROL, State.TextId, GET_VALUE=Value Ret = CW_FIELD_VALUE(Value, State.Type) WIDGET_CONTROL, Child, SET_UVALUE=State, /NO_COPY RETURN, Ret END ; Ascii assumptions ; ; + - . = 43,45,46 ; 0-9 = 48-57 ; DEde = 68,69,100,101 ; ; ; Examine an input stream of characters. ; Alter the field contents to reflect this. ; If any character inserted in a single operation is invalid, ; ignore the entire operation. ; Consider that field may contain a (known) invalid state ; PRO CW_FIELD_INT, Ch, State, Event, Altered COMPILE_OPT hidden Altered = 0 ; nothing so far Nil = 0 ; field has contents Minus = 0 ; field is not just a '-' Negate = 0 ; new text has no '-'s in it TextId = State.TextId ; Special Cases: ; We don't actually care where in the input string a ; '-' is. If there is an odd number of them, we ; negate the value (see below) ; ; Current String Char Result ; Nil '-' '-' ; - '-' Nil ; '-' - WIDGET_CONTROL, TextId, GET_VALUE=Value Value = Value[0] IF Value EQ '' THEN Nil = 1 ; Value is nil string IF Value EQ '-' THEN Minus = 1 ; Value is an invalid number ; IF Ch EQ 10b THEN BEGIN Altered = 2 RETURN ENDIF IF Ch EQ 45b THEN Negate = 1 $ ELSE IF Ch GE 48b AND Ch LE 57b THEN BEGIN Nil = 0 & Minus = 0 ENDIF ELSE RETURN ; ![0-9] ; Add new character (if any) Selection = WIDGET_INFO(TextId, /TEXT_SELECT) TIP = Selection[0]+1-Negate ; Text Insertion Point IF Negate EQ 0 THEN BEGIN WIDGET_CONTROL, TextId, SET_VALUE=STRING(Ch), /USE_TEXT_SELECT Altered = 1 ENDIF IF Negate THEN BEGIN IF Nil THEN BEGIN WIDGET_CONTROL, TextId, SET_VALUE='-' TIP = 1 ENDIF ELSE IF Minus THEN BEGIN WIDGET_CONTROL, TextId, SET_VALUE='' ENDIF ELSE BEGIN ; We actually have a number to negate WIDGET_CONTROL, TextId, GET_VALUE=Value IValue = LONG(Value) TIP = TIP + (IValue GT 0) - (IValue LT 0) WIDGET_CONTROL, TextId, SET_VALUE=STRTRIM(-IValue,2) ENDELSE Altered = 1 ENDIF ; Set selection point IF Altered THEN WIDGET_CONTROL, TextId, SET_TEXT_SELECT=[TIP,0] END FUNCTION CW_FIELD_EXPONENT, Value, Idx COMPILE_OPT hidden BValue = BYTE(Value) Idx = WHERE(BValue EQ 68b, Count) IF Count EQ 1 THEN RETURN, 1 Idx = WHERE(BValue EQ 69b, Count) IF Count EQ 1 THEN RETURN, 1 Idx = WHERE(BValue EQ 100b, Count) IF Count EQ 1 THEN RETURN, 1 Idx = WHERE(BValue EQ 101b, Count) IF Count EQ 1 THEN RETURN, 1 RETURN, 0 END ; ; Floating point number are even more complicated. ; There are more invalid states available. ; Currently, we are more lax. ; PRO CW_FIELD_FLOAT, Ch, State, Event, Altered COMPILE_OPT hidden TextId = State.TextId WIDGET_CONTROL, TextId, GET_VALUE=Value Value = Value[0] IF Ch EQ 10b THEN BEGIN Value = CW_FIELD_VALIDATE(Value) WIDGET_CONTROL, TextId, SET_VALUE=Value WIDGET_CONTROL, TextId, SET_TEXT_SELECT=[Strlen(Value),0] Altered = 2 RETURN ENDIF ; Unfortunately, we have a lot of invalid states ; possible that aren't a problem. ; We make sure of just a minimum number of ; things: ; One EeDd per field. ; One decimal point, before the exponent ; - sign must be 1st char or follow the exponent. Selection = WIDGET_INFO(TextId, /TEXT_SELECT) IF Ch GE 48b AND Ch LE 57b THEN BEGIN WIDGET_CONTROL, TextId, SET_VALUE=STRING(Ch), /USE_TEXT_SELECT ENDIF ELSE BEGIN CASE Ch OF 46b: BEGIN ; Ignore it if there is one already ; New decimal point must precede exponent Idx = WHERE(BYTE(Value) EQ 46b, Count) IF Count EQ 1 THEN RETURN IF CW_FIELD_EXPONENT( Value, Idx ) THEN BEGIN IF Idx[0] LT Selection[0] THEN RETURN ENDIF WIDGET_CONTROL, TextId, SET_VALUE=STRING(Ch), /USE_TEXT_SELECT END 43b: BEGIN ; + must follow exponent IF CW_FIELD_EXPONENT( Value, Idx ) THEN BEGIN IF Idx[0]+1 EQ Selection[0] THEN BEGIN WIDGET_CONTROL, TextId, SET_VALUE='+', /USE_TEXT_SELECT ENDIF ELSE RETURN ENDIF ELSE RETURN END 45b: BEGIN HaveExp = CW_FIELD_EXPONENT( Value, Idx ) IF (HaveExp AND Idx[0]+1 EQ Selection[0]) OR $ (Selection[0] EQ 0 AND STRMID(Value,0,1) NE "-") THEN BEGIN WIDGET_CONTROL, TextId, SET_VALUE='-', /USE_TEXT_SELECT ENDIF ELSE RETURN END 68b: GOTO, Exponent 69b: GOTO, Exponent 100b: GOTO, Exponent 101b: BEGIN Exponent: ; Replace if one exists. Otherwise allow it anywhere ; AFTER the decimal point IF CW_FIELD_EXPONENT( Value, Idx ) THEN BEGIN Selection = [ Idx, 1 ] WIDGET_CONTROL, TextId, SET_TEXT_SELECT=Selection ENDIF ELSE BEGIN Idx = WHERE(BYTE(Value) EQ 46b, Count) IF Count EQ 1 THEN BEGIN IF Selection[0] LE Idx[0] THEN RETURN ENDIF ENDELSE WIDGET_CONTROL, TextId, SET_VALUE=STRING(Ch), /USE_TEXT_SELECT END ELSE: RETURN ; Bad ENDCASE ENDELSE Altered = 1 WIDGET_CONTROL, TextId, SET_TEXT_SELECT=[Selection[0]+1,0] END FUNCTION CW_FIELD_EVENT, Event COMPILE_OPT hidden StateHolder = WIDGET_INFO(Event.Handler, /CHILD) WIDGET_CONTROL, StateHolder, GET_UVALUE=State, /NO_COPY ; At this point, we need to look at what kind of field ; we have: Altered = 0 ; If the user has types then update field IF State.Type NE 0 THEN BEGIN ; Not a String? IF Event.Type LE 1 THEN BEGIN ; Insert Characters? IF State.Type EQ 1 THEN Procedure='CW_FIELD_FLOAT' $ ELSE Procedure='CW_FIELD_INT' Altered = 0 IF Event.Type EQ 0 THEN BEGIN CALL_PROCEDURE, Procedure, Event.Ch, State, Event, Altered ENDIF ELSE BEGIN Chars = BYTE(Event.Str) FOR I=0,N_ELEMENTS(Chars)-1 DO $ CALL_PROCEDURE, Procedure, Chars[I], State, Event, Altered ENDELSE ENDIF ELSE IF Event.Type EQ 2 THEN BEGIN ; Delete Text IF Event.Length GT 0 THEN BEGIN ; Bug in widget WIDGET_CONTROL, State.TextId, $ SET_TEXT_SELECT=[Event.Offset,Event.Length] WIDGET_CONTROL, State.TextId, SET_VALUE='', /USE_TEXT_SELECT ; See if user wants an update Altered = 1 ENDIF ENDIF ENDIF ELSE BEGIN ; All delete/add char events effect the contents of ; a string. is considered special. IF Event.Type GE 0 AND Event.Type LE 2 THEN Altered = 1 IF Event.Type EQ 0 THEN $ Altered = 1 + (Event.Ch EQ 10b) ENDELSE Ret = 0 ; If the entry has been modified or was hit ; And the user is interested in all event or ; Just AND was the cause of update then ; send it IF State.Update NE 0 AND $ Altered GE State.Update THEN BEGIN WIDGET_CONTROL, State.TextId, GET_VALUE=Value RValue = CW_FIELD_VALUE(Value, State.Type) Ret = { $ ID: Event.Handler, $ TOP: Event.Top, $ HANDLER: 0L, $ VALUE: RValue, $ TYPE: State.Type, $ UPDATE: Altered - 1 $ ; 0=any,1=CR } ENDIF ; Restore our state structure WIDGET_CONTROL, StateHolder, SET_UVALUE=State, /NO_COPY RETURN, Ret END FUNCTION CW_FIELD, Parent, COLUMN=Column, ROW=Row, $ FLOATING=Float, INTEGER=Int, LONG=Long, STRING=String, $ FONT=LabelFont, FRAME=Frame, TITLE=Title, UVALUE=UValue, VALUE=TextValueIn, $ RETURN_EVENTS=ReturnEvents, ALL_EVENTS=AllUpdates, $ FIELDFONT=FieldFont, NOEDIT=NoEdit, TEXT_FRAME=Text_Frame, $ XSIZE=XSize, YSIZE=YSize, UNAME=uname, TAB_MODE=tab_mode ; FLOOR=vmin, CEILING=vmax ; Examine our keyword list and set default values ; for keywords that are not explicitly set. Column = KEYWORD_SET(Column) Row = 1 - Column AllEvents = 1 - KEYWORD_SET(NoEdit) ; Enum Update { None, All, CRonly } Update = 0 IF KEYWORD_SET(AllUpdates) THEN Update = 1 IF KEYWORD_SET(ReturnEvents) THEN Update = 2 IF N_ELEMENTS(Title) EQ 0 THEN Title='Input Field:' TextValue = (N_ELEMENTS(TextValueIn) gt 0) ? TextValueIn : '' ; Convert non-string values to strings. if (SIZE(TextValue, /TNAME) ne 'STRING') then $ TextValue = STRTRIM(TextValue,2) IF N_ELEMENTS(YSize) EQ 0 THEN YSize=1 IF N_ELEMENTS(uname) EQ 0 THEN uname='CW_FIELD_UNAME' Type = 0 ; string is default IF KEYWORD_SET(Float) THEN Type = 1 IF KEYWORD_SET(Int) THEN Type = 2 IF KEYWORD_SET(Long) THEN Type = 3 ; Don't allow multiline non string widgets if (Type ne 0) then $ YSize=1 YSize = YSize > 1 ; Build Widget Base = WIDGET_BASE(Parent, ROW=Row, COLUMN=Column, UVALUE=UValue, $ EVENT_FUNC='CW_FIELD_EVENT', $ PRO_SET_VALUE='CW_FIELD_SET', $ FUNC_GET_VALUE='CW_FIELD_GET', $ FRAME=Frame, UNAME=uname ) if ( N_ELEMENTS(tab_mode) ne 0 ) then $ WIDGET_CONTROL, Base, TAB_MODE = tab_mode Label = WIDGET_LABEL(Base, VALUE=Title, FONT=LabelFont, $ UNAME=uname+'_LABEL') Text = WIDGET_TEXT(Base, VALUE=TextValue, $ XSIZE=XSize, YSIZE=YSize, FONT=FieldFont, $ ALL_EVENTS=AllEvents, $ EDITABLE=(AllEvents AND TYPE EQ 0), $ FRAME=Text_Frame , $ UNAME=uname+'_TEXT') ; NO_ECHO=(AllEvents AND (TYPE NE 0))) ; Save our internal state in the first child widget State = { $ TextId:Text, $ Title:Title, $ Update:Update, $ Type:Type $ } WIDGET_CONTROL, WIDGET_INFO(Base, /CHILD), SET_UVALUE=State, /NO_COPY RETURN, Base END