Gambas2 menu editor: Action

Een van die nuttige, toevallige ontdekkingen: wat de Action betekent in menu: mailinglist

items .. which can throw “actions”

De Action helpt in het beheer van complexe toepassingen met veel menu’s en knoppen, die overlappen in functie, en waarvan de toestand (beschikbaarheid) moet aangepast worden naargelang omstandigheden (itt een altijd bereikbaar menu als Help, About,..).

Met dat Action veld kan je een “actie” definiëren waarop die verschillende “controls” van toepassing zijn, zoals de actie “bewaar huidig document”, actie bv genaamd “ActSaveFile” (zou bv voor menu “File, Save” en shortcut ctrl-s en een toolbutton “tlbtSave” kunnen zijn).

Je kan volgens de beschrijving die met één procedure na het bewaren inactief maken bv.

action["ActSaveFile"].Enabled = FALSE

PUBLIC SUB inactivateSave(bNewState AS Boolean)
  Action["ActSaveFile"].Enabled = bNewState
  mniOther.Enabled = NOT bNewState  ' some other alternating function
END

Bovendien kan je op basis daarvan ook code laten uitvoeren; acties worden gecheckt, je moet alleen de procedure schrijven om er iets mee te doen; de parameter is de naam van de actie en return waarde is een boolean:


PUBLIC SUB Action_Activate(sAction AS String) AS Boolean
  DEBUG sAction
  SELECT sAction
    CASE "ActSaveFile"
      saveCurrentFile()
    CASE
      ' ...
  END SELECT
  RETURN TRUE ' might be located in CASEs as result there
END

Meer uitleg over Actions op de (niet-vertaalde) documentatiepagina: http://www.gambasdoc.org/help/comp/gb.qt/action?nl

Startvenster vanuit MMain

Een applicatie die

  1. – start met MMain
  2. – in FMain heel wat initialisatie en schermopbouw-tijd nodig heeft (database-connecties)
  3. – daardoor slechts na enkele seconden teken van leven geeft (nl. FMain zichtbaar maken op scherm)

Enkele pogingen om alvast een ander venstertje op het scherm te zetten, dat aangeeft dat het programma opgestart is, bv vanuit MMain al, liepen fout op het terug afsluiten van dat startvenster.

Hoe het wel lukte: Vanuit FMain wordt eerst FStartup aangeroepen, al vanuit de
  PUBLIC SUB _new()
code, dan worden de initialisaties gedaan, en wordt de code doorlopen van
  PUBLIC SUB Form_Open()
Deze eindigt, aan het einde van alle initialisaties en opbouw van schermelementen, met
  FStartup.Close()
 END

om het startup venster te sluiten.

Bij andere pogingen, met bv het startscherm geopend vanuit MMain, kreeg ik fouten als:
– het opstartscherm gaat niet dicht als FMain open is.
– het opstartscherm dat uit MMain opgestart was, wordt gesloten in FMain, maar dan gaat FMain mee dicht (?!)
– het opstartscherm wordt wel gesloten, maar na het beëindigen van FMain blijft er “iets” van Gambas aktief; zichtbaar in de ps -AH uitvoer op de commandolijn: gbr blijft draaien.

Het lijkt me dus best om de “hiërarchie” van de schermen zuiver te houden…

Een datum achteruit; bv een week terug

Bij weergave van gegevens van een week geleden tot nu moet je de begindatum van die periode bepalen.
Hoe kan ik de datum van een week terug vinden?

De huidige datum in Gambas krijg je met:

date()
of
date(now())

(Deze commando’s kan je uitproberen in het Console venster onderaan in Gambas IDE, met vraagteken plus commando, zoals “ ? date()“.)

De gewone now() geeft ook de tijd erbij. Maar het interessante aan now() is dat je van daaruit kan teruggaan naar de dag van nu min een week:

date(now()-7)

In de Console moet het zo:

? Date(Now()-"7")

Als je dus from.. until datums nodig hebt, kan je hiermee je “from” datum berekenen.

De aparte waarden voor jaar, maand en dag indien je die nodig hebt, bv voor het opstellen van een SQL vraag:

year(date(now()-7) & "-" & month(date(now())-7) & "-" & day(date(now())

Of als je de hele voorbije week nodig hebt, kan je een array vullen met die dagen:


 PUBLIC SUB fillLastWeek(arrLastWeek AS Date[], iBack AS Integer, OPTIONAL bTodayIncl AS Boolean)
  
  DIM dStart AS Date
  DIM dRun AS Date
  DIM dEnd AS Date
  DIM i AS Integer
  DIM iShiftToday AS Integer = 0
  
  DEBUG arrLastWeek.Type
  
  IF bTodayIncl
    iShiftToday = 1
  ENDIF 

  IF iBack = 0
    iBack = 7 
  ENDIF 
  
  dStart = Date(Now() - (iBack - iShiftToday))
  
  dRun = dStart
  
  FOR i = (1) TO (iBack)
    arrLastWeek.Add(dRun)
    dRun = Date(dStart + i)
    'DEBUG "i=" & i & " iBack=" & iBack & " iShiftToday=" & iShiftToday & " dRun = " & dRun
  NEXT 
  
END

  

Wordt aangeroepen:

fillLastWeek(arrLastWeek, 7)

of

fillLastWeek(arrLastWeek, 7, TRUE)

Nog een vraag: wat was de eerste dag van deze week?

? date(now()-weekday(Now())+"1")

Zonder de +1 kom je op zondag uit; je kan die 1 ook aftrekken van je dagnummer natuurlijk:

? date(now()-(weekday(Now()-"1")))

Zeker nuttig is ook DateAdd, waar je bij een datum aangeeft welke eenheid er hoeveel bij moet geteld worden:

DateAdd(date1, gb.Day, 7)

Klik en sleep van object van een groep

Concept van klikken en slepen toegepast op objecten in een groep
(vervolg op eenvoudig klik en sleep voorbeeld)

Als je een aantal voorwerpen maakt tijdens de uitvoering van je programma (at runtime), bv knoppen, kan je die daarbij toewijzen aan een “group”, zodat je daarna de leden van die groep, als een soort child-objecten kan aanspreken (zie Group van een control definiëren).

Die “group” kan je ook gebruiken voor het klik en sleep gedrag.
De Gambas-omgeving helpt door een “laatst gebruikte” object/control waarde bij te houden: LAST
Via die weg kan je een “sleutel” doorgeven door die (op voorhand) op te nemen in het extra “Tag” veld van een object.

bv:

PUBLIC grpEtiketten_MouseDrag()
  ...
  IF Mouse.Left
    DEBUG LAST.Tag
  ENDIF
  ...
END

en

PUBLIC grpEtiketten_Drop()
  ...
  ... = Drag.Data
  DEBUG "dropped on" & LAST.Tag
  ...
END

Software bedrijfsurenteller: Runcounter

Handig bij het testen van een applicatie is te weten of ze opgestart is. Handig bij het onderhouden van een applicatie is te weten hoe dikwijls ze gebruikt werd, in dit geval gestart werd.

Je kan die waarde gemakkelijk bijhouden in het standaard verborgen instellingenbestand met “Settings”. De component “Settings” moet aktief zijn (Project, Properties, Components).

Je steekt in je hoofdmodule MMain of scherm FMain een stukje code dat de teller checkt, en verhoogt met 1.

Settings["General/LastLaunch"] = Now()
IF Settings["General/Runcounter"]
  TRY Settings["General/Runcounter"] = Settings["General/Runcounter"]  + 1
ELSE
  Settings["General/Runcounter"] = 1
ENDIF

In een van je About of Info schermen kan je die counter dan weergeven.

TextLabel2.ToolTip = "Runcounter " & Settings["General/Runcounter"]
TextLabel2.ToolTip &= "; " & Settings["General/LastLaunch", ""] 
TextLabel2.ToolTip &= " - " & Settings["General/LastShutdown", ""]

Bij het debuggen is het ook handig te kijken of het programma normaal opgestart / normaal gesloten is, vandaar de LastLaunch en LastShutdown ‘timestamps’, de shutdown in de code voor het sluiten van het hoofdvenster:

PUBLIC SUB doClose()
  
  Settings["General/LastShutdown"] = Now()
  ME.Close
  
END

Wachtwoorden gebruiken

Er zijn 3 veel voorkomende soorten situaties ivm wachtwoorden:

  1. checken of iemand toegang krijgt adhv zijn wachtwoord.
  2. toegang geven tot gegevens die met een wachtwoord versleuteld zijn.
  3. toegang verkrijgen tot een dienst door een wachtwoord op te sturen bij het inloggen.

1. Check inkomende toegang
De eerste situatie is het gemakkelijkst op te lossen: de gebruiker heeft een toepassing draaien, waarmee hij wil gebruik maken van een dienst.

Je zou die toepassing het wachtwoord kunnen laten controleren, en daarop toegang geven. Maar dan moet je het wachtwoord van die gebruiker sturen. Dat is dus slecht: je stuurt een wachtwoord onnodig over het netwerk, en je hebt blindelings vertrouwen in de “client” applicatie die dat vraagt. Die client-applicatie kan vals zijn (en zo een wachtwoord opvragen van een gebruiker). Bewaak nooit een poort door als ze buiten kloppen, te roepen “Is het wachtwoord Schild en Vriend?”, en te openen als ze “ja” antwoorden.

Het nuttige van dit voorbeeld is dat je het moet omdraaien:

De client stuurt de server of de basisapplicatie het wachtwoord, en daar beslis je dan of het juist is of niet, en of er toegang gegeven wordt.
De waarschijnlijk meest voorkomende encryptie-manier is MD5. Het gebruik ervan betekent dat je bij het maken van het wachtwoord, dat omrekent naar een MD5-waarde. Die MD5-waarde sla je op.
Als een gebruiker inlogt, geeft hij het wachtwoord, dat alleen maar gebruikt wordt om een MD5 waarde van te berekenen; als die overeenkomt met de opgeslagen waarde is het ok. Voordeel is dat op het moment van het geven van het wachtwoord, dat niet opgeslagen hoeft te worden.

Voorbeeld met MD5 met prefix (salt):

Zet de Project, Properties, Components gb.crypt aan

DIM s AS String
s = Crypt.MD5(InputBox("Set administration password: ", "New installation - set password"), "SpecialP")

De prefix “SpecialP” moet je ook zo mogelijk voor bv meer gebruikers verschillend maken. Je moet die wel bij de hand hebben om een vergelijkings-md5 te maken!

Maar op het netwerk sniffen wordt zo niet vermeden. Dus ideaal zou zijn om het doorsturen van het wachtwoord tussen client en server ook te versleutelen, zodat iemand die het netwerk afluistert, daar ook geen toegang toe krijgt.

2. ….

3…..

Component in Gambas2

Basis-uitleg:

  • hoe componenten in Gambas werken
  • hoe ze zelf te maken
  • hoe ze te installeren.

De originele documentatie over componenten vermeldt uitdrukkelijk dat alle “onderstaande tekst” voor Gambas3 is, dus ik doorloop het even in en voor Gambas2:

Componenten

De componenten die je in de Gambas IDE ziet (project, properties), zijn mee geïnstalleerd met de Gambas IDE. (Kijk in je Linux pakket manager om bij te installeren).

In bv OpenSUSE vind je ze na installatie in: /usr/lib64/gambas2/
Bv de gb.chart.component en gb.chart.gambas

De .component is een tekstbestand, en bevat voor gb.chart:

[Component]
Key=gb.chart
State=2
Authors=Fabien Bodard
Needs=Form
Requires=gb.form

en voor gb.settings:

[Component]
Key=gb.settings
Authors=Benoît Minisini

In de IDE zijn de componenten uit te kiezen bij de projecteigenschappen, en tonen bovenstaande zich als:

gb.chart
Chart drawing (Beta version)
Authors: Fabien Bodard
Requires: Graphical form management, gb.form.

en

gb.settings
Application settings managament
Authors: Benoit Minisini

Bij het gebruik van Settings in je broncode worden volgende methodes en eigenschappen zichtbaar in de editor:

De broncode, van bv Settings, vind je na het afhalen en uitpakken van de broncode:
gambas2-2.23.1/comp/src/gb.settings

Een uittreksel (… is overgeslagen code):

' Gambas class file
'
EXPORT
CREATE STATIC
'
CLASS Window
'
STATIC PROPERTY READ Path AS String
'
PRIVATE $sPath AS String
PRIVATE $sTitle AS String
PRIVATE $cSlot AS Collection
PRIVATE $bModify AS Boolean
'
PRIVATE SUB Load()
'  
  DIM hFile AS File
  DIM iLine AS Integer
  DIM sLine AS String
  DIM sSlot AS String
  DIM iPos AS Integer
'
  $cSlot = NEW Collection
  IF NOT Exist($sPath) THEN RETURN
' ...
PUBLIC SUB _new(OPTIONAL Path AS String, OPTIONAL Title AS String)
'
  DIM sDir AS String
  DIM sPath AS String
  DIM sElt AS String
'
  IF Left(Path) <> "/" THEN 
    IF NOT Path THEN
' ...
PUBLIC SUB _free()
'
  TRY ME.Save
'
END
'
PUBLIC SUB Save()
'
  DIM aKey AS NEW String[]
  DIM cSlot AS Collection
  DIM sKey AS String
  DIM hFile AS File
  DIM vVal AS Variant
'
  IF NOT $bModify THEN RETURN
' ....
PRIVATE FUNCTION GetSlot(sKey AS String) AS String
' ...
PUBLIC FUNCTION _get(Key AS String, OPTIONAL {Default} AS Variant) AS Variant
' ...
PUBLIC SUB _put(Value AS Variant, Key AS String)
' ...
PUBLIC SUB Clear(ParentKey AS String)
' ...
PUBLIC SUB Read(hObject AS Object, OPTIONAL sKey AS String, OPTIONAL vDefault AS Variant)
  '
  DIM sVal AS String
  DIM aVal AS String[]
  '
  IF Object.Is(hObject, "Window") THEN
    sKey = Object.Type(hObject) &/ sKey
    LoadWindow(hObject, sKey)
  ELSE
    IF NOT sKey THEN TRY sKey = hObject.Name
    sKey = GetTopLevel(hObject) &/ sKey
    'DEBUG sKey
    TRY hObject.Settings = ME[sKey, vDefault]
  ENDIF
  '
END
'
PUBLIC SUB Write(hObject AS Object, OPTIONAL sKey AS String)
  '
  IF Object.Is(hObject, "Window") THEN
    sKey = Object.Type(hObject) &/ sKey
    SaveWindow(hObject, sKey)
  ELSE 'IF Object.Is(hObject, "SidePanel") THEN
    IF NOT sKey THEN TRY sKey = hObject.Name
    sKey = GetTopLevel(hObject) &/ sKey
    'DEBUG sKey
    TRY ME[sKey] = hObject.Settings
  ENDIF
  '
END
'
STATIC PRIVATE FUNCTION Path_Read() AS String
  '
  DIM sPath AS String = Application.Env["XDG_CONFIG_HOME"]
  IF NOT sPath THEN sPath = System.User.Home &/ ".config"
  RETURN sPath &/ "gambas"
  '
END
'
STATIC PUBLIC FUNCTION Array(...) AS String[]
  '  
  DIM aVal AS NEW String[]
  DIM iInd AS Integer
  DIM sVal AS String
  DIM vVal AS Variant
  '
  FOR iInd = 0 TO Param.Max
    vVal = Param[iInd]
    IF IsBoolean(vVal) THEN 
      sVal = IIf(vVal, "1", "0")
    ELSE
      sVal = CStr(vVal)
    ENDIF
    aVal.Add(sVal)
  NEXT
  '
  RETURN aVal
  '
END
' 
PUBLIC SUB Reload()
  '
  Load
  '
END

Conclusies:

Classe begint met:
EXPORT
CREATE STATIC

De bruikbare, dwz van buitenaf zichtbare elementen zijn vet weergegeven in de broncode hierboven.

Het path is een eigenschap die van buitenaf (enkel) leesbaar moet zijn.

Dat wordt gerealiseerd met een interne variabele die in de (automatisch bestaande) functie _Read() wordt gebruikt om de externe alleen-lezen variabele af te schermen. Als gevolg komt die eigenlijk twee keer voor, waarbij je onderscheid moet kunnen maken tussen de namen, wat hier gedaan wordt door de private variabele als $sPath te definieren, en de externe gewoon als Path:

 
STATIC PROPERTY READ Path AS String

PRIVATE $sPath AS String
'
STATIC PRIVATE FUNCTION Path_Read() AS String
  '
  DIM sPath AS String = Application.Env["XDG_CONFIG_HOME"]
  IF NOT sPath THEN sPath = System.User.Home &/ ".config"
  RETURN sPath &/ "gambas"
  '
END

Allerlei procedures (groene bol) zonder of met (één of meer optionele) parameters:

PUBLIC SUB Save()
PUBLIC SUB Reload()
PUBLIC SUB Clear(ParentKey AS String)
PUBLIC SUB Write(hObject AS Object, OPTIONAL sKey AS String)
PUBLIC SUB Read(hObject AS Object, OPTIONAL sKey AS String, OPTIONAL vDefault AS Variant)

De array ziet er zo uit (groene kubus):

STATIC PUBLIC FUNCTION Array(...) AS String[]

Zelf Component maken

Zelf maak je in de IDE een nieuw project, of wijzigt* een bestaand via menu:

Project, Properties, Options

kruis aan:

Options, Component: Yes. (Advancement: Beta)

Dat voegt onmiddellijk de tabbladen “Provides” en “Requieres” toe:

Schermafbeelding project venster gambas

Component in Gambas2

* Wijzigen van een bestaand, niet als component gestart project naar “component” leverde me een crash op bij een klik in het tabblad “Provides” als daar nog niets aanwezig is.

Bij een nieuw project krijg je dan
Classes:

  • CContainer
  • CControl

En onder Data de Controls:

  • ccontainer.png
  • ccontrol.png

Class CContainer bevat:

' Gambas class file

EXPORT

INHERITS UserContainer

PUBLIC SUB _new()

END

En Class CControl bevat:

' Gambas class file

EXPORT

INHERITS UserControl

PUBLIC SUB _new()

  DIM hLabel AS Label
  
  hLabel = NEW Label(ME) AS "MyLabel"
  hLabel.Text = "Gambas!"
  hLabel.Font = Font["+4,Bold,Italic"]
  hLabel.Border = Border.Plain
  
END

PUBLIC SUB MyLabel_Enter()

  LAST.Text = "Gambas Almost Means BASIC!"

END

PUBLIC SUB MyLabel_Leave()

  LAST.Text = "Gambas!"

END

De IDE maakte deze bestanden aan, samen met 2 grafische bestanden, en ik veronderstel dat de code een soort voorbeeldcode is die je zelf kan aanpassen of weggooien als je ze niet nodig hebt.

Bij een bestaand project met classen die je algemeen wil hergebruiken: Begin van je project en pas aan zoals hierboven uitgelegd. Ter vergelijking kan je de broncode van Gambas2 gebruiken als voorbeeld.

In de broncode van je class zelf moet je volgens de documentatie met sleutelwoord “EXPORT” aangeven dat ze geëxporteerd moet worden.

Je kan er een gewoon source archive van maken (tar.gz) om te verspreiden naar de gebruikers.

Je kan als gebruiker het project “compileren”; met “make executable”. Daar krijg je een extra optie:

“Install in the user component directory”

Daardoor wordt de component bruikbaar vanuit je Gambas2 IDE, menu Project, Properties, Components.
Hij komt onderaan in de lijst van de componenten, onder de extra titel “User Components”. Geniaal eenvoudig!

De bestanden .gambas en .component worden geïnstalleerd(*) in
~/.local/lib/gambas2
waardoor ze bruikbaar worden in al de gebruiker’s gambas2 projecten.

* Opgelet!
Ze worden daar niet naartoe gekopieerd, maar er wordt slechts een link, een verwijzing gemaakt naar de gecompileerde versie (i.e. de locatie waar je ze gecompileerd hebt). Gooi het originele project daar weg, en je hebt een probleem!

Nota: virtuele class
In geval van een hierarchie van klassen, waarbij de hoogste een virtuele klasse is, die nooit rechtstreeks gebruikt wordt, moet je ze benoemen met een begin-underscore “_”, zodat ze onzichtbaar wordt. De verwijzing INHERITS hoofdklasse in de subklassen ook aanpassen naar INHERITS _hoofdklasse natuurlijk.

Toetsencombinaties met control: Ctrl-s

Om te bewaren wordt in programma’s standaard toetsencombinatie Ctrl-s gebruikt, dat kan je ook in je Gambas programma’s.

Voorbeeldsituatie:

  • een scherm FMain met een paar tekstvelden “TextLabel1”, “TextLabel2″…
  • de toetsen worden opgevangen in het aktieve scherm “FMain”.

Dat kan in Gambas2 zo:

PUBLIC SUB _new()

END

PUBLIC SUB Form_Open()
  
END

PUBLIC SUB Form_KeyPress()

  IF Key.Control AND (Key.Code = Key["A"] OR Key.Code = Key["a"])
    TextLabel1.Text = "CTRL-"
    TextLabel2.Text = "Aa"
  ENDIF 

  IF Key.Control AND Key.Code = Key["z"]
    TextLabel1.Text = "CTRL-"
    TextLabel2.Text = "z"
  ENDIF 

END

Group van een control definiëren vanuit code

In de IDE kan je de group invullen bij de eigenschappen van een control, rechts in de lijst van eigenschappen.
Bovenaan heb je telkens

(Class)
(Name)
(Group)

Die group kan je gebruiken om een aktie op te vangen, bv een klik of een drop event.

Als je een object of control vanuit code gemaakt hebt, kan je niet eevoudigweg die eigenschap invullen zoals de andere eigenschappen.

FOR i = iEerste TO iLaatste
  arrayControls[i] = NEW ToggleButton(frameKnoppen)
  WITH arrayControls[i]
    .Text = Str$(i)
'    .Group="groupKnoppen"
    .Drop = TRUE
  END WITH
NEXT

Geen enkel van die “tussen haakjes” eigenschappen kan je vanuit code wijzigen; ze worden bepaald bij declaratie/initiatie.

De manier om die group toe te kennen is de declaratie/initiatie uit te breiden met de groupnaam als string in AS “groupname”

FOR i = iEerste TO iLaatste
  arrayControls[i] = NEW ToggleButton(frameKnoppen) AS "groupKnoppen"
  WITH arrayControls[i]
    .Text = Str$(i)
'    .Group="groupKnoppen"
    .Drop = TRUE
  END WITH
NEXT

Dan kan de group verder gebruikt worden om events op te vangen:

PUBLIC SUB grpKnoppen_Drop()
  LAST.Text = Drag.Data
  LAST.Value = TRUE
END

Snel data ingeven met een tekstgebied (TextArea)

Gambas2 TextArea Input, leeg (screenshot)

Tekstgebied voor invoer

Als je op een scherm (Form) data laat ingeven moet je altijd een compromis zoeken tussen

  1. efficiëntie van de input
  2. controle van de input

Door punt “controle” zou je geneigd kunnen zijn meer aparte tekstvelden te gaan gebruiken, waarvan je telkens de invoer controleert. Waarbij de gebruiker, om zijn hele invoer te kunnen doen, dan wel telkens van veld moet veranderen (indien: implementeer TAB mogelijkheid!).

Een tekstgebied (TextArea), dat je eventueel “in de hoogte” vormgeeft, laat toe snel opeenvolgende woorden (zinnen) in te geven, hier vooral een reeks onder elkaar dus.

Ik laat de gebruiker achtereenvolgens de invoer doen, en op het moment dat hij het tekstgebied verlaat, doe ik de controle en geef feedback. Daarvoor worden de regels van het tekstgebied gesplitst in “zinnen”, die elk als één invoer beschouwd worden.

Gambas2 TextArea Input End (screenshot)

Na drie lijnen invoer, een enter, en nog een…

PUBLIC SUB doImportClassValues()

  DIM sLine AS String
  DIM hArrStr AS NEW String[]
  hArrStr = Split(txaeClassNames.Text, gb.NewLine) 
  
  setState(cAdd)
  
  FOR EACH sLine IN hArrStr
    doCheckSave(sLine)
  NEXT 
  

Om het toetsenbord niet te moeten verlaten bij het einde van de invoer, tel ik twee achtereenvolgende “volgende regel” (enter/return) als teken om te stoppen: gebruiker heeft een lege regel ingevoerd, en daarna nog één. Dat is het teken voor “klaar”, de verwerking van de invoer wordt gestart.

Om gemakkelijk toegang te geven tot de invoer in het tekstgebied gebruik ik bv een dubbelklik op het tekstgebied.

De teller voor het aantal enters is vooraf hoger gedefinieerd als
PRIVATE $iLastEnter AS integer

De toets voor enter/return wordt zo getest en geteld:

PUBLIC SUB txaeClassNames_KeyPress()
'
  IF Key.Code = Key.Enter OR Key.Code = Key.Return
    INC $iLastEnter 
    IF $iLastEnter = 2
      doCheckSaveValues(txaeClassNames.Text)
      $iLastEnter = 0
    ENDIF 
  ELSE 
    $iLastEnter = 0
  ENDIF 
'
END