Commit inicial: fuentes MBS ERP (Genero 6) + .gitignore + docs/SETUP_PC_MBS.md

This commit is contained in:
2026-08-18 20:59:52 -04:00
commit 454973e269
5978 changed files with 2664094 additions and 0 deletions
+168
View File
@@ -0,0 +1,168 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
# Included in other demos to simplify compilation / link
FUNCTION select_city(prefix)
DEFINE prefix STRING
DEFINE filter STRING
DEFINE r SMALLINT
DEFINE name, state STRING
DEFINE cityarr DYNAMIC ARRAY OF RECORD
city_id INTEGER,
city_name VARCHAR(50),
city_state CHAR(2)
END RECORD
OPEN WINDOW w_city WITH FORM "CityList"
DIALOG ATTRIBUTES(UNBUFFERED, FIELD ORDER FORM)
INPUT BY NAME filter
END INPUT
DISPLAY ARRAY cityarr TO sr.*
END DISPLAY
BEFORE DIALOG
IF LENGTH(prefix) < 3 THEN
LET filter = prefix || "*"
ELSE
LET filter = "*"
END IF
CALL select_city_fill(DIALOG,filter,cityarr)
CALL DIALOG.setActionActive("accept", DIALOG.getCurrentRow("sr")>0)
ON ACTION filter
CALL select_city_fill(DIALOG,filter,cityarr)
CALL DIALOG.setActionActive("accept", DIALOG.getCurrentRow("sr")>0)
ON ACTION accept
LET name = cityarr[DIALOG.getCurrentRow("sr")].city_name
LET state = cityarr[DIALOG.getCurrentRow("sr")].city_state
LET r = 0
EXIT DIALOG
ON ACTION cancel
LET r = -1
EXIT DIALOG
END DIALOG
CLOSE WINDOW w_city
RETURN r, name, state
END FUNCTION
FUNCTION select_city_fill(d,filter, cityarr)
DEFINE d ui.Dialog
DEFINE filter STRING
DEFINE cityarr DYNAMIC ARRAY OF RECORD
city_id INTEGER,
city_name VARCHAR(50),
city_state CHAR(2)
END RECORD
DEFINE i INTEGER
LET i = 0
CALL cityarr.clear()
&define ADD_CITY(name, state) \
LET cityarr[i:=i+1].city_id = i \
LET cityarr[i].city_name = name \
LET cityarr[i].city_state = state
ADD_CITY("Austin", "TX")
ADD_CITY("Boston", "MA")
ADD_CITY("Chicago", "IL")
ADD_CITY("Dallas", "TX")
ADD_CITY("Denver", "CO")
ADD_CITY("Detroit", "MI")
ADD_CITY("Houston", "TX")
ADD_CITY("Las Vegas", "NV")
ADD_CITY("Los Angeles", "CA")
ADD_CITY("Miami", "FL")
ADD_CITY("New York", "NY")
ADD_CITY("Philadelphia", "PA")
ADD_CITY("Phoenix", "AZ")
ADD_CITY("Portland", "ME")
ADD_CITY("San Antonio", "TX")
ADD_CITY("San Diego", "CA")
ADD_CITY("San Francisco", "CA")
ADD_CITY("Washington", "DC")
FOR i=1 TO cityarr.getLength()
IF cityarr[i].city_name NOT MATCHES filter THEN
CALL cityarr.deleteElement(i)
LET i = i - 1
END IF
END FOR
CALL d.setCurrentRow("sr",1)
END FUNCTION
FUNCTION combo_fill_states(cb)
DEFINE cb ui.ComboBox
CALL cb.addItem("AK","Alaska")
CALL cb.addItem("AL","Alabama")
CALL cb.addItem("AR","Arkansas")
CALL cb.addItem("AZ","Arizona")
CALL cb.addItem("CA","California")
CALL cb.addItem("CO","Colorado")
CALL cb.addItem("CT","Connecticut")
CALL cb.addItem("DC","D.C.")
CALL cb.addItem("DE","Delaware")
CALL cb.addItem("FL","Florida")
CALL cb.addItem("GA","Georgia")
CALL cb.addItem("HI","Hawaii")
CALL cb.addItem("IA","Iowa")
CALL cb.addItem("ID","Idaho")
CALL cb.addItem("IL","Illinois")
CALL cb.addItem("IN","Indiana")
CALL cb.addItem("KS","Kansas")
CALL cb.addItem("KY","Kentucky")
CALL cb.addItem("LA","Louisiana")
CALL cb.addItem("MA","Massachusetts")
CALL cb.addItem("MD","Maryland")
CALL cb.addItem("ME","Maine")
CALL cb.addItem("MI","Michigan")
CALL cb.addItem("MN","Minnesota")
CALL cb.addItem("MO","Missouri")
CALL cb.addItem("MS","Mississippi")
CALL cb.addItem("MT","Montana")
CALL cb.addItem("NC","North Carolina")
CALL cb.addItem("ND","North Dakota")
CALL cb.addItem("NE","Nebraska")
CALL cb.addItem("NH","New Hampshire")
CALL cb.addItem("NJ","New Jersey")
CALL cb.addItem("NM","New Mexico")
CALL cb.addItem("NV","Nevada")
CALL cb.addItem("NY","New York")
CALL cb.addItem("OH","Ohio")
CALL cb.addItem("OK","Oklahoma")
CALL cb.addItem("OR","Oregon")
CALL cb.addItem("PA","Pennsylvania")
CALL cb.addItem("PR","Puerto Rico")
CALL cb.addItem("RI","Rhode Island")
CALL cb.addItem("SC","South Carolina")
CALL cb.addItem("SD","South Dakota")
CALL cb.addItem("TN","Tennessee")
CALL cb.addItem("TX","Texas")
CALL cb.addItem("UT","Utah")
CALL cb.addItem("VA","Virginia")
CALL cb.addItem("VT","Vermont")
CALL cb.addItem("WA","Washington")
CALL cb.addItem("WI","Wisconsin")
CALL cb.addItem("WV","West Virginia")
CALL cb.addItem("WY","Wyoming")
END FUNCTION
+43
View File
@@ -0,0 +1,43 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
ACTION DEFAULTS
ACTION filter (ACCELERATOR = CONTROL-ENTER, ACCELERATOR2 = CONTROL-RETURN)
END
LAYOUT ( STYLE = "dialog4" )
GRID
{
Filter: [f ]
<t t1 >
Id City State
[id |name |st ]
[id |name |st ]
[id |name |st ]
[id |name |st ]
[id |name |st ]
[id |name |st ]
< >
}
END
END
ATTRIBUTES
BUTTONEDIT f = FORMONLY.filter TYPE CHAR, ACTION=filter, IMAGE="filter",
COMMENT = "Presse Control-Enter to apply filter";
TABLE t1 : COMMENT = "Double-click to select an item from the list";
EDIT id = FORMONLY.city_id TYPE INTEGER;
EDIT name = FORMONLY.city_name TYPE CHAR;
EDIT st = FORMONLY.city_state TYPE CHAR;
END
INSTRUCTIONS
SCREEN RECORD sr (FORMONLY.city_id, FORMONLY.city_name, FORMONLY.city_state);
END
+180
View File
@@ -0,0 +1,180 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
IMPORT FGL CityList
IMPORT FGL MsgBox
DEFINE sql_cond STRING
TYPE t_custrec RECORD
c_id INTEGER,
c_name VARCHAR(50),
c_city VARCHAR(30),
c_state CHAR(2),
c_zipcode VARCHAR(6),
c_address VARCHAR(100)
END RECORD
DEFINE custarr DYNAMIC ARRAY OF t_custrec
DEFINE changing, temprow SMALLINT
MAIN
DEFINE s, r INTEGER
DEFINE name, state STRING
OPTIONS INPUT WRAP
OPEN FORM f1 FROM "CustListEdit"
DISPLAY FORM f1
CALL cust_loadlist(2)
DIALOG ATTRIBUTES(FIELD ORDER FORM, UNBUFFERED)
CONSTRUCT BY NAME sql_cond ON cust_id, cust_name, cust_city
END CONSTRUCT
INPUT ARRAY custarr FROM sr.* ATTRIBUTE(WITHOUT DEFAULTS, AUTO APPEND = FALSE)
BEFORE ROW
CALL cust_clean(DIALOG)
CALL cust_setup(DIALOG)
ON ROW CHANGE
CALL cust_save(DIALOG)
BEFORE DELETE
CALL cust_save(DIALOG)
BEFORE INSERT
LET temprow = (DIALOG.getCurrentRow("sr") == Dialog.getArrayLength("sr"))
LET r = DIALOG.getCurrentRow("sr")
LET custarr[r].c_id = r
CALL cust_touch_setup(DIALOG)
AFTER INSERT
CALL cust_save(DIALOG)
LET temprow = FALSE
END INPUT
BEFORE DIALOG
CALL cust_clean(DIALOG)
CALL cust_setup(DIALOG)
ON ACTION dialogtouched
CALL cust_touch_setup(DIALOG)
ON ACTION zoom_city_1
CALL select_city(DIALOG.getFieldBuffer("cust_city")) RETURNING s, name, state
IF s==0 THEN
LET r = DIALOG.getCurrentRow("sr")
DISPLAY name TO cust_city
END IF
ON ACTION zoom_city_2
CALL select_city(DIALOG.getFieldBuffer("c_city")) RETURNING s, name, state
IF s==0 THEN
LET r = DIALOG.getCurrentRow("sr")
LET custarr[r].c_city = name
LET custarr[r].c_state = state
CALL DIALOG.setFieldTouched("c_city",TRUE)
CALL cust_touch_setup(DIALOG)
END IF
ON ACTION fetch
DISPLAY sql_cond TO condition
CALL cust_loadlist(length(sql_cond))
ON ACTION save
CALL cust_save(DIALOG)
IF temprow THEN
CALL DIALOG.setFieldTouched("sr.*", TRUE)
LET temprow = FALSE
END IF
ON ACTION close
IF NOT cust_check_close(DIALOG) THEN
CONTINUE DIALOG
END IF
EXIT DIALOG
END DIALOG
END MAIN
FUNCTION cust_clean(d)
DEFINE d ui.Dialog
LET changing = FALSE
END FUNCTION
FUNCTION cust_touch_setup(d)
DEFINE d ui.Dialog
-- ignore changes in the filter fields!
LET changing = (d.getCurrentItem() MATCHES "sr.*")
CALL cust_setup(d)
END FUNCTION
FUNCTION cust_check_changing(d)
DEFINE d ui.Dialog
IF changing THEN
CALL cust_setup(d)
CALL mbox_ok("Customers", "You must first save your changes!","stop")
RETURN FALSE
END IF
RETURN TRUE
END FUNCTION
FUNCTION cust_check_close(d)
DEFINE d ui.Dialog
IF changing THEN
CALL cust_setup(d)
RETURN mbox_yn("Customers","Do you want to close the window without saving changes?","question")
END IF
RETURN TRUE
END FUNCTION
FUNCTION cust_setup(d)
DEFINE d ui.Dialog
CALL d.setActionActive("dialogtouched", NOT changing)
CALL d.setActionActive("fetch", NOT changing)
CALL d.setActionActive("save", changing)
END FUNCTION
FUNCTION cust_save(d)
DEFINE d ui.Dialog
-- UDPATE/INSERT/DELETE to database...
LET changing = FALSE
CALL d.setFieldTouched("sr.*", FALSE)
CALL cust_clean(d)
CALL cust_setup(d)
MESSAGE "Changes saved in database - ", CURRENT YEAR TO SECOND
END FUNCTION
-- Dummy function to fill the list of customers (normally, SELECT from database)
FUNCTION cust_loadlist(m)
DEFINE m,i INTEGER
CALL custarr.clear()
FOR i=1 TO m
CALL custarr.appendElement()
LET custarr[i].c_id = i
LET custarr[i].c_name = "Customer #"||i
IF i MOD 3 == 0 THEN
LET custarr[i].c_city = "San Francisco"
LET custarr[i].c_state = "CA"
LET custarr[i].c_zipcode = "76231"
LET custarr[i].c_address = "24, Blue Lagon Street"
ELSE
LET custarr[i].c_city = "Richmond"
LET custarr[i].c_state = "VA"
LET custarr[i].c_zipcode = "71212"
LET custarr[i].c_address = "45, Yellow Lake"
END IF
END FOR
END FUNCTION
+69
View File
@@ -0,0 +1,69 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
ACTION DEFAULTS
ACTION save (TEXT="Save changes",ACCELERATOR=control-s)
ACTION close (DEFAULTVIEW=Yes)
END
LAYOUT ( TEXT = "Query and editable list" )
GRID
{
<g g1 >
Customer id: [q1 ]
Name: [q2 ]
City: [q3 ]
[bf ]
[sql ]
[ ]
[ ]
< >
<g g2 >
<t t1 >
Id Name City State ZipCode Address
[c1 |c2 |c3 |c4 |c5 |c6 ]
[c1 |c2 |c3 |c4 |c5 |c6 ]
[c1 |c2 |c3 |c4 |c5 |c6 ]
[c1 |c2 |c3 |c4 |c5 |c6 ]
[c1 |c2 |c3 |c4 |c5 |c6 ]
< >
[ins :app :del :sav ]
< >
}
END
END
ATTRIBUTES
GROUP g1: TEXT = "Filter";
EDIT q1 = FORMONLY.cust_id TYPE INTEGER;
EDIT q2 = FORMONLY.cust_name TYPE VARCHAR;
BUTTONEDIT q3 = FORMONLY.cust_city TYPE VARCHAR, ACTION=zoom_city_1, IMAGE="find";
TEXTEDIT sql = FORMONLY.condition, NOENTRY;
BUTTON bf: fetch, TEXT="Fetch";
GROUP g2: TEXT = "Customers";
EDIT c1 = FORMONLY.c_id TYPE INTEGER, SCROLL, DEFAULT = 0;
EDIT c2 = FORMONLY.c_name TYPE VARCHAR, SCROLL, DEFAULT = "<no name>";
BUTTONEDIT c3 = FORMONLY.c_city TYPE CHAR, SCROLL, ACTION=zoom_city_2, IMAGE="find";
COMBOBOX c4 = FORMONLY.c_state TYPE VARCHAR, SCROLL,
QUERYEDITABLE, DEFAULT="CA", INITIALIZER=combo_fill_states;
EDIT c5 = FORMONLY.c_zipcode TYPE VARCHAR, SCROLL;
EDIT c6 = FORMONLY.c_address TYPE VARCHAR, SCROLL;
BUTTON ins: sr.insert;
BUTTON app: sr.append;
BUTTON del: sr.delete;
BUTTON sav: save;
END
INSTRUCTIONS
SCREEN RECORD sr (FORMONLY.c_id THROUGH FORMONLY.c_address);
END
+12
View File
@@ -0,0 +1,12 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
include ../../Makeincl
all:: testfiledlg.42m filedlg.42m filedlg.42f
+53
View File
@@ -0,0 +1,53 @@
File Dialog sample
------------------
This example tries to work like the Windows File Open box as closely as possible
(but omits the Favorites/Desktop etc buttons at the left hand side and the Search combo at the top; this is really platform-dependent).
To use the file dialog, fill a predefined record with the desired title, file patterns, etc., and then call the functions
filedlg_open() or filedlg_save() with the record as the parameter.
This works similar to the windows common dialog C-library (comdlg32.dll).
The advantage in using a record (FILEDLG_RECORD) instead of a large numer of parameters to those functions is that it's possible to use the autocompletion feature.
The DIALOG construct is used in FUNCTION _filedlg_doDlg() to let a DISPLAY ARRAY and an INPUT run in parallel to display the list of files, and to be able to enter the filename and switch between several file types using a combobox ( for example between *.per and *.*).
The accept action works in a special way in this dialog:
It determines first where the focus is, and then does the following:
1.focus is in the DISPLAY ARRAY
a)If a file is highlighted, the dialog terminates and returns the file name
b)If a directory is highlighted, the dialog switches to that directory and
reloads the file array
2. focus is in the INPUT
a)if the typed name is an existing file, the dialog terminates and returns the file name.
b)if the typed name is a directory, the code tries to switch to that directory, and to reload the file array.
c) if the typed name is neither -> failure box in open mode, return file name in save mode
The new DIALOG.getCurrentItem() built-in function is used to determine where the focus is, because INFIELD does not work for DISPLAY ARRAY.
DIALOG.getCurrentItem() returns a string thst is a field name for the usual input fields. and the screen record name for the DISPLAY ARRAY.
As an example, you can choose (as in the Windows original) that a file in the dialog can be deleted with the "Del" key if the focus is in the DISPLAY ARRAY.
This is achieved by a sub dialog action in the DISPLAY ARRAY sub dialog statement:
DIALOG
DISPLAY ARRAY
...
ON ACTION del
--perform code to delete the current highlighted file
--the action is only active if the focus is inside this display array
..
END DISPLAY
END DIALOG
The runtime system ensures that this action is only active when the focus is inside the DISPLAY ARRAY, in contradiction to the global actions below the sub dialog statements.
(If the action was also active in the INPUT sub dialog, the result would be conflicting accelerators; the 'Del' key should erase the text in the edit fields)
$FGLDIR/demo/tools/fglped makes use of this file dialog.
In $FGLDIR/demo/tools/fgltlib/fgltfiledlg.4gl there is a similar file dialog that uses a larger parameter list for the dialog.
Use whichever method is appropriate for you.
+415
View File
@@ -0,0 +1,415 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
#+ File open/save dialog box using MD
IMPORT FGL fgldialog
IMPORT os
PUBLIC TYPE FILEDLG_RECORD RECORD
title STRING,
defaultfilename STRING,
defaultpath STRING,
opt_create_dirs SMALLINT, -- allows the creation of a new subdirectory
--(not yet implemented)
opt_delete_files SMALLINT, -- allows to delete files when running the dialog
types DYNAMIC ARRAY OF RECORD -- list for the file type combobox
description STRING, -- string to display
suffixes STRING -- pipe separated string of all possible suffixes for one entry
-- example "*.per|*.4gl"
END RECORD
END RECORD
DEFINE _filedlg_list DYNAMIC ARRAY OF RECORD
eimage STRING, -- image name
entry STRING, -- Filename or Dirname
esize INT, --file size
emodt STRING, --modification time
etype STRING -- C_DIRECTORY or "*.xxx File"
END RECORD
DEFINE last_opendlg_directory STRING
DEFINE last_savedlg_directory STRING
DEFINE m_typearr DYNAMIC ARRAY OF STRING
DEFINE m_typelen INT
CONSTANT C_DIRECTORY="Directory"
CONSTANT C_OPEN="open"
CONSTANT C_SAVE="save"
#+ Opens a file dialog to open a file.
#+ @returnType String
#+ @return The selected file path, or NULL is canceled.
#+ @param r the record describing the file dialog
#
FUNCTION filedlg_open(r)
DEFINE r FILEDLG_RECORD
DEFINE t, fn STRING
IF r.defaultpath IS NULL THEN
IF last_opendlg_directory IS NULL THEN
LET last_opendlg_directory = "."
END IF
LET r.defaultpath = last_opendlg_directory
END IF
LET t= "Open File"
IF r.title IS NOT NULL THEN
LET t = r.title
END IF
LET fn = _filedlg_doDlg(C_OPEN,t,r.*)
IF fn IS NOT NULL THEN
LET last_opendlg_directory = _file_get_dirname(fn)
END IF
RETURN fn
END FUNCTION
#+ Opens a file dialog to save a file.
#+ @returnType String
#+ @return The selected file path, or NULL is canceled.
#+ @param r The record describing the file dialog
#
FUNCTION filedlg_save(r)
DEFINE r FILEDLG_RECORD
DEFINE t, fn STRING
IF r.defaultpath IS NULL THEN
IF last_savedlg_directory IS NULL THEN
LET last_savedlg_directory = "."
END IF
LET r.defaultpath = last_savedlg_directory
END IF
LET t = "Save File"
IF r.title IS NOT NULL THEN
LET t = r.title
END IF
LET fn = _filedlg_doDlg(C_SAVE,t,r.*)
IF fn IS NOT NULL THEN
LET last_savedlg_directory = _file_get_dirname(fn)
END IF
RETURN fn
END FUNCTION
------------------- internal _filedlg_xxx functions ----------------------------
FUNCTION _filedlg_doDlg(dlgtype,title,r)
DEFINE dlgtype STRING
DEFINE title STRING
DEFINE r FILEDLG_RECORD
DEFINE currpath, path, filename, ftype, dirname, filepath STRING
DEFINE delfilename, errstr STRING
DEFINE doContinue, i INT
DEFINE cb ui.ComboBox
OPEN WINDOW _filedlg WITH FORM "filedlg"
ATTRIBUTES(STYLE='dialog',TEXT=title)
CALL fgl_settitle(title)
LET currpath = r.defaultpath
LET filename = r.defaultfilename
DISPLAY BY NAME filename
IF currpath="." THEN
LET currpath=os.Path.pwd()
END IF
DISPLAY currpath TO currpath
LET cb = ui.ComboBox.forName("formonly.ftype")
IF cb IS NULL THEN
DISPLAY "ERROR:form field \"ftype\" not found in form filedlg"
EXIT PROGRAM
END IF
FOR i=1 TO r.types.getLength()
CALL cb.addItem(r.types[i].suffixes,r.types[i].description)
END FOR
LET ftype=r.types[1].suffixes
DIALOG ATTRIBUTE(UNBUFFERED)
--use a DISPLAY ARRAY for showing the file list
DISPLAY ARRAY _filedlg_list TO sr.*
BEFORE DISPLAY
--the following call is not effective, but should be!!!
CALL DIALOG.setActionActive("del",r.opt_delete_files)
BEFORE ROW
IF _filedlg_list[arr_curr()].etype<>C_DIRECTORY THEN
LET filename=_filedlg_list[arr_curr()].entry
DISPLAY BY NAME filename
END IF
ON ACTION del --ask for deleting the highlighted file
LET delfilename=_filedlg_list[arr_curr()].entry
IF _filedlg_mbox_yn("Confirm delete",sfmt("Really delete '%1'?",delfilename),"question") THEN
CALL _file_delete(os.Path.join(currpath,delfilename))
CALL _filedlg_fetch_filenames(DIALOG,currpath,ftype,NULL)
END IF
END DISPLAY
INPUT BY NAME filename,ftype ATTRIBUTE(WITHOUT DEFAULTS)
--when the type combobox changes we need to redisplay
ON CHANGE ftype
CALL _filedlg_fetch_filenames(DIALOG,currpath,ftype,NULL)
END INPUT
BEFORE DIALOG
CALL _filedlg_fetch_filenames(DIALOG,currpath,ftype,filename)
IF dlgtype = C_SAVE THEN
NEXT FIELD filename
END IF
ON ACTION accept
LET doContinue=FALSE
--we use DIALOG.getCurrentItem() to detect where the focus currently is
--it works similar like INFIELD however also for DISPLAY ARRAY
IF DIALOG.getCurrentItem()="sr" THEN
--we are in the display array
LET filepath = os.Path.join(currpath,_filedlg_list[arr_curr()].entry)
ELSE
--not in display array
LET filepath = os.Path.join(currpath,filename)
END IF
IF os.Path.exists(filepath) AND os.Path.isdirectory(filepath) THEN
--switch the directory and refill the array
LET currpath=_file_normalize_dir(filepath)
CALL _filedlg_fetch_filenames(DIALOG,filepath,ftype,"..")
DISPLAY BY NAME currpath
LET filename=""
LET doContinue=TRUE
END IF
IF NOT doContinue AND dlgtype = C_OPEN THEN
IF NOT os.Path.exists(filepath) THEN
LET errstr=SFMT(%"file '%1' does not exist!",
os.Path.basename(filepath))
CALL _filedlg_mbox_ok("Error", errstr, "stop")
ERROR errstr
LET doContinue=TRUE
END IF
END IF
IF NOT doContinue AND dlgtype = C_SAVE THEN
LET dirname=_file_get_dirname(filepath)
IF NOT os.Path.exists(dirname) THEN
CALL _filedlg_mbox_ok("Error", SFMT(%"directory '%1' does not exist!",filepath), "stop")
LET doContinue=TRUE
END IF
END IF
IF NOT doContinue THEN
EXIT DIALOG
END IF
ON ACTION cancel
LET filepath=NULL
EXIT DIALOG
ON ACTION move_up --move 1 directory level up
LET path = _file_get_dirname(currpath)
CALL _filedlg_fetch_filenames(DIALOG,path,ftype,currpath)
LET currpath=path
DISPLAY BY NAME currpath
END DIALOG
CLOSE WINDOW _filedlg
RETURN filepath
END FUNCTION
FUNCTION _filedlg_fetch_filenames(d,currpath,typelist,currfile)
DEFINE d ui.Dialog
DEFINE currpath STRING
DEFINE typelist STRING
DEFINE currfile STRING
DEFINE i,len,found INT
DEFINE st base.StringTokenizer
LET st = base.StringTokenizer.create(typelist,"|")
CALL m_typearr.clear()
WHILE st.hasMoreTokens()
LET m_typearr[m_typearr.getLength()+1]=st.nextToken()
END WHILE
LET m_typelen=m_typearr.getLength()
CALL _filedlg_getfiles_int(currpath)
LET len=_filedlg_list.getLength()
--jump to the current file
LET currfile=os.Path.basename(currfile)
FOR i=1 TO len
IF currfile=_filedlg_list[i].entry THEN
LET found=1
CALL d.setCurrentRow("sr",i)
EXIT FOR
END IF
END FOR
IF NOT found THEN
CALL d.setCurrentRow("sr",1)
END IF
END FUNCTION
FUNCTION _filedlg_getfiles_int(dirpath)
DEFINE dirpath STRING
DEFINE dh, isdir INTEGER
DEFINE fname, pname, size STRING
CALL _filedlg_list.clear()
LET dh = os.Path.diropen(dirpath)
IF dh == 0 THEN
RETURN
END IF
WHILE TRUE
LET fname = os.Path.dirnext(dh)
IF fname IS NULL THEN
EXIT WHILE
END IF
IF fname == "." THEN
CONTINUE WHILE
END IF
LET pname = os.Path.join(dirpath,fname)
LET isdir=os.Path.isdirectory(pname)
IF isdir THEN
LET size = NULL
ELSE
LET size = os.Path.size(pname)
END IF
CALL _filedlg_appendEntry(isdir,fname,size,os.Path.mtime(pname))
END WHILE
CALL os.Path.dirclose(dh)
END FUNCTION
FUNCTION _filedlg_in_typearr(type)
DEFINE type STRING
DEFINE i INT
FOR i=1 TO m_typelen
IF type=m_typearr[i] THEN
RETURN TRUE
END IF
END FOR
RETURN FALSE
END FUNCTION
FUNCTION _filedlg_checktypeandext(ext)
DEFINE ext STRING
IF _filedlg_in_typearr("*") THEN
RETURN TRUE
END IF
IF ext IS NOT NULL THEN
IF _filedlg_in_typearr("*.*") THEN
RETURN TRUE
END IF
IF _filedlg_in_typearr("*"||ext) THEN
RETURN TRUE
END IF
END IF
RETURN FALSE
END FUNCTION
FUNCTION _filedlg_appendEntry(isdir,name,size,modDate)
DEFINE isdir INT
DEFINE name STRING
DEFINE size INT
DEFINE modDate STRING
DEFINE type,image,ext STRING
DEFINE len INT
IF isdir THEN
LET ext=""
LET type=C_DIRECTORY
LET image="folder"
ELSE
LET ext = _file_extension(name)
LET type = SFMT(%"%1-File",ext)
LET image="file"
END IF
IF NOT isdir AND NOT _filedlg_checktypeandext(ext) THEN
RETURN
END IF
CALL _filedlg_list.appendElement()
LET len=_filedlg_list.getLength()
LET _filedlg_list[len].entry = name
LET _filedlg_list[len].etype = type
LET _filedlg_list[len].eimage = image
LET _filedlg_list[len].esize = size
LET _filedlg_list[len].emodt = modDate
END FUNCTION
FUNCTION _filedlg_mbox_ok(title,message,icon)
DEFINE title, message, icon STRING
CALL fgl_winMessage(title,message,icon)
END FUNCTION
FUNCTION _filedlg_mbox_yn(title,message,icon)
DEFINE title, message, icon STRING
DEFINE r STRING
LET r = fgl_winQuestion(title,message,"yes","yes|no",icon,0)
RETURN ( r == "yes" )
END FUNCTION
-------------------------- _file helpers ---------------------------------------
FUNCTION _file_get_dirname(filename)
DEFINE filename STRING
DEFINE dirname STRING
LET dirname=os.Path.dirname(filename)
IF dirname IS NULL THEN
LET dirname="."
END IF
RETURN dirname
END FUNCTION
FUNCTION _file_extension(filename)
DEFINE filename STRING
DEFINE extension STRING
LET extension=os.Path.extension(filename)
IF extension IS NOT NULL THEN
LET extension=".",extension
END IF
RETURN extension
END FUNCTION
FUNCTION _file_delete(filename)
DEFINE filename STRING
IF NOT os.Path.delete(filename) THEN
CALL _filedlg_mbox_ok("Error",sfmt("Can't delete %1",filename),"stop")
END IF
END FUNCTION
--normalizes a given directory name
--example /home/foo/bar/../spong -> /home/foo/spong
FUNCTION _file_normalize_dir(fname)
DEFINE fname STRING
DEFINE cmd STRING
DEFINE arr DYNAMIC ARRAY OF STRING
IF NOT os.Path.isdirectory(fname) THEN
RETURN fname
END IF
IF fgl_getenv("WINDIR") IS NOT NULL THEN
LET cmd="cd ",fname,"&&cd"
ELSE
LET cmd="cd ",fname,"&&pwd"
END IF
IF NOT _file_get_output(cmd,arr) THEN
RETURN fname
END IF
RETURN arr[1]
END FUNCTION
--runs a program and returns an array with the output lines,
--used by _file_normalize_dir
FUNCTION _file_get_output(program,arr)
DEFINE program,linestr STRING
DEFINE arr DYNAMIC ARRAY OF STRING
DEFINE mystatus,idx INTEGER
DEFINE c base.Channel
LET c = base.channel.create()
CALL c.setDelimiter("")
WHENEVER ERROR CONTINUE
CALL c.openpipe(program,"r")
LET mystatus=status
WHENEVER ERROR STOP
IF mystatus THEN
DISPLAY "error in file_get_output(program,arr)"
RETURN 0
END IF
CALL arr.clear()
WHILE (linestr:=c.readline()) IS NOT NULL
LET idx=idx+1
LET arr[idx]=linestr
END WHILE
CALL c.close()
RETURN 1
END FUNCTION
+53
View File
@@ -0,0 +1,53 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
ACTION DEFAULTS
ACTION del (TEXT="Delete",ACCELERATOR=Delete)
END
LAYOUT(text="",style="dialog3")
GRID
{
[lfd |path |bu ]
<T t1 >
[ei |entry |esize |emodt |etype ]
[ei |entry |esize |emodt |etype ]
[ei |entry |esize |emodt |etype ]
[ei |entry |esize |emodt |etype ]
[ei |entry |esize |emodt |etype ]
[ei |entry |esize |emodt |etype ]
[ei |entry |esize |emodt |etype ]
[ei |entry |esize |emodt |etype ]
[lfn |file | open]
[lft |ftype | canc]
}
END
ATTRIBUTES
LABEL lfd:text="Directory:";
path = FORMONLY.currpath,SCROLL,comment="Displays the current directory";
BUTTON bu : move_up,image="uplevel",comment="Moves one directory up";
IMAGE ei = FORMONLY.eimage;
entry = FORMONLY.entry, TITLE="Name";
esize = FORMONLY.esize, TITLE="Size", JUSTIFY = RIGHT;
emodt = FORMONLY.emodt, TITLE="Time";
etype = FORMONLY.etype, TITLE="Type";
LABEL lfn:text="File:";
LABEL lft:text="Files of type:";
file = FORMONLY.filename,SCROLL,COMMENT="Input the wanted file name";
COMBOBOX ftype = FORMONLY.ftype,SCROLL,sizepolicy=DYNAMIC,NOT NULL;
BUTTON open:accept,text="Open";
BUTTON canc:cancel,text="Cancel";
END
INSTRUCTIONS
SCREEN RECORD sr(eimage THRU etype);
@@ -0,0 +1,49 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
IMPORT FGL filedlg
MAIN
DEFINE cname STRING
DEFINE r1 FILEDLG_RECORD
DEFINE r2 FILEDLG_RECORD
OPTIONS INPUT WRAP
DISPLAY "Try options in the action panel" AT 5,5
MENU "File dialog"
COMMAND "Choose .per"
-- get back an existing .per filename
LET r1.title="Please choose a form"
LET r1.defaultfilename="filedlg.per"
LET r1.types[1].description="Form files (*.per)"
LET r1.types[1].suffixes="*.per"
LET r1.types[2].description="All files (*)"
LET r1.types[2].suffixes="*"
LET r1.opt_delete_files=TRUE
LET cname= filedlg_open(r1.*)
MESSAGE ">>>>chosen file:",cname
COMMAND "New .per"
-- get back a name for a file not existing yet
LET r2.title="Please enter a filename for save"
LET r2.defaultfilename="spong"
LET r2.types[1].description="All files (*.*)"
LET r2.types[1].suffixes="*.*"
LET cname= filedlg_save(r2.*)
MESSAGE ">>>>new file:",cname
COMMAND "Quit"
EXIT MENU
END MENU
END MAIN
+58
View File
@@ -0,0 +1,58 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
IMPORT FGL donations
DEFINE fname,last VARCHAR(50)
DEFINE m_details RECORD
clubmember INT,
otherdetail CHAR(50)
END RECORD
MAIN
OPEN FORM f FROM "Folder"
OPTIONS INPUT WRAP
DISPLAY FORM f
LET fname="Willi"
CALL donations.fill()
DIALOG ATTRIBUTES(UNBUFFERED)
INPUT BY NAME fname,last ATTRIBUTES(WITHOUT DEFAULTS)
AFTER INPUT
--some checking if we changed things
IF FIELD_TOUCHED(fname) OR FIELD_TOUCHED(last) THEN
DISPLAY "Update customer in the database"
CALL DIALOG.setFieldTouched("fname",0)
CALL DIALOG.setFieldTouched("last",0)
END IF
END INPUT
SUBDIALOG donations
INPUT m_details.* FROM details.*
AFTER FIELD otherdetail
--just have a uselesse contraint here to check we come thru
--in case of leaving the folder
IF m_details.otherdetail<>'o' THEN
ERROR "otherdetail must be 'o'"
NEXT FIELD otherdetail
END IF
AFTER INPUT
--some checking if we changed things
IF FIELD_TOUCHED(details.*) THEN
DISPLAY "Update detail in the database"
CALL DIALOG.setFieldTouched("details.*",0)
END IF
END INPUT
ON ACTION accept
DISPLAY "accept"
ACCEPT DIALOG
ON ACTION cancel
EXIT DIALOG
END DIALOG
END MAIN
+51
View File
@@ -0,0 +1,51 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
ACTION DEFAULTS
ACTION cancel(VALIDATE=NO)
END
LAYOUT
VBOX
GROUP(TEXT="Customer (Master Record)")
GRID
{
First Name:[fname ] Last Name:[last ]
}
END
END --GROUP
FOLDER
PAGE(TEXT="Donations",COMMENT="edits the customers donations")
FORM "donations"
END --PAGE
PAGE(TEXT="More Details",COMMENT="edits some more customer details")
GROUP(TEXT="some data")
GRID
{
Member in the organization [clubmember]
Other detail [otherdetail ]
}
END
END
END
END
END
ATTRIBUTES
fname = FORMONLY.fname;
last = FORMONLY.last,NOT NULL;
CHECKBOX clubmember = FORMONLY.clubmember,NOT NULL;
otherdetail = FORMONLY.otherdetail,REQUIRED;
INSTRUCTIONS
SCREEN RECORD cr(FORMONLY.fname,FORMONLY.last);
SCREEN RECORD details(FORMONLY.clubmember THRU FORMONLY.otherdetail);
+61
View File
@@ -0,0 +1,61 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
IMPORT FGL donations
DEFINE fname,last VARCHAR(50)
DEFINE m_details RECORD
clubmember INT,
otherdetail CHAR(50)
END RECORD
MAIN
DEFINE i INT
OPTIONS INPUT WRAP
FOR i=1 TO 2
IF i=1 THEN
OPEN FORM f FROM "Folder_unrolled"
ELSE
OPEN FORM f FROM "Folder"
END IF
DISPLAY FORM f
LET fname="Willi"
CALL donations.fill()
DIALOG ATTRIBUTES(UNBUFFERED)
INPUT BY NAME fname,last ATTRIBUTES(WITHOUT DEFAULTS)
END INPUT
SUBDIALOG donations
INPUT m_details.* FROM details.*
AFTER FIELD otherdetail
--just have a uselesse contraint here to check we come thru
--in case of leaving the folder
IF m_details.otherdetail<>'o' THEN
ERROR "otherdetail must be 'o'"
NEXT FIELD otherdetail
END IF
AFTER INPUT
END INPUT
ON ACTION accept
ACCEPT DIALOG
ON ACTION cancel
EXIT DIALOG
AFTER DIALOG
--do complete database handling here
DISPLAY "AFTER DIALOG:save customer to database"
FOR i=1 TO arr_count()
DISPLAY "eventually save donation row :",i
END FOR
IF FIELD_TOUCHED(details.*) THEN
DISPLAY "Update detail in the database"
END IF
END DIALOG
CLOSE FORM f
END FOR
END MAIN
+125
View File
@@ -0,0 +1,125 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
DEFINE fname,last VARCHAR(50)
DEFINE donations DYNAMIC ARRAY OF RECORD
p_date DATE,
p_amount MONEY,
p_reason VARCHAR(250)
END RECORD
DEFINE m_details RECORD
clubmember INT,
otherdetail CHAR(50)
END RECORD
MAIN
DEFINE state STRING
DEFINE flag INT
OPTIONS INPUT WRAP
OPEN FORM f FROM "Folder_old"
DISPLAY FORM f
CALL fill_arrays()
--manage the 'state' variable to jump into different dialogs depending
--on the state value
LET fname="Willi"
LET state="master" --initially go to the master input
WHILE state="master" OR state="page1" OR state="page2"
LET int_flag=FALSE
CASE state
--master input
WHEN "master"
LET state="invalid"
INPUT BY NAME fname, last WITHOUT DEFAULTS
--no master
ON ACTION page1 LET state="page1" ACCEPT INPUT
ON ACTION page2 LET state="page2" ACCEPT INPUT
ON ACTION accept LET state="ok" ACCEPT INPUT
AFTER INPUT
IF int_flag THEN
LET state="cancel"
EXIT INPUT
END IF
--some checking if we changed things
IF FIELD_TOUCHED(fname) OR FIELD_TOUCHED(last) THEN
DISPLAY "AFTER INPUT master, save to the database"
END IF
END INPUT
--donations input
WHEN "page1"
LET state="invalid"
INPUT ARRAY donations WITHOUT DEFAULTS FROM donations.* ATTRIBUTES(UNBUFFERED)
ON ACTION master LET state="master" ACCEPT INPUT
--no page1
ON ACTION page2 LET state="page2" ACCEPT INPUT
ON ACTION accept LET state="ok" ACCEPT INPUT
AFTER FIELD p_amount
IF donations[arr_curr()].p_amount<2.0 THEN
ERROR "must be at least 2 dollar:-)"
NEXT FIELD CURRENT
END IF
AFTER ROW
DISPLAY "AFTER ROW donations arr_curr:",arr_curr(),",arr_count:",arr_count()
IF int_flag THEN --check int_flag in AFTER ROW in contradiction to
--checking int_flag for INPUT in AFTER INPUT
LET state="cancel"
EXIT INPUT
END IF
IF arr_curr()<=arr_count() THEN
DISPLAY "AFTER ROW donations:check row and save to database"
END IF
AFTER INPUT
DISPLAY "AFTER INPUT donations,state:",state
END INPUT
--more details input
WHEN "page2"
LET state="invalid"
INPUT m_details.* FROM details.* ATTRIBUTES(UNBUFFERED,WITHOUT DEFAULTS=flag)
ON ACTION master LET state="master" ACCEPT INPUT
ON ACTION page1 LET state="page1" ACCEPT INPUT
--no page2
ON ACTION accept LET state="ok" ACCEPT INPUT
AFTER FIELD otherdetail
--just have a uselesse contraint here to check we come thru
--in case of leaving the folder
IF m_details.otherdetail<>'o' THEN
ERROR "otherdetail must be 'o'"
NEXT FIELD otherdetail
END IF
AFTER INPUT
IF int_flag THEN
LET state="cancel"
EXIT INPUT
END IF
DISPLAY "AFTER INPUT details,state:",state
--some checking if we changed things
IF FIELD_TOUCHED(details.*) THEN
DISPLAY "Update detail in the database"
LET flag=1
END IF
END INPUT
END CASE
END WHILE
CASE state
WHEN "ok"
DISPLAY "save"
WHEN "cancel"
DISPLAY "cancel"
WHEN "invalid"
DISPLAY "error occured"
END CASE
END MAIN
FUNCTION fill_arrays()
DEFINE i INT
FOR i=1 TO 5
LET donations[i].p_date = TODAY -10 +i
LET donations[i].p_amount = i+1
END FOR
END FUNCTION
@@ -0,0 +1,65 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
LAYOUT
VBOX
GROUP(TEXT="Customer (Master Record)")
GRID
{
First Name:[fname ] Last Name:[last ] [bmaster]
}
END --GRID
END --GROUP
FOLDER
PAGE(TEXT="Donations",COMMENT="edits the customers donations",ACTION=page1)
TABLE
{
DATE Amount Reason
[p_date][p_amount ][p_reason ]
[p_date][p_amount ][p_reason ]
[p_date][p_amount ][p_reason ]
[p_date][p_amount ][p_reason ]
[p_date][p_amount ][p_reason ]
[p_date][p_amount ][p_reason ]
[p_date][p_amount ][p_reason ]
[p_date][p_amount ][p_reason ]
[p_date][p_amount ][p_reason ]
}
END
END --PAGE
PAGE(TEXT="More Details",COMMENT="edits some more customer details",ACTION=page2)
GROUP(TEXT="some data")
GRID
{
Member in the organization [clubmember]
Other detail [otherdetail ]
}
END --GRID
END --GROUP
END --PAGE
END --FOLDER
END --VBOX
ATTRIBUTES
fname = FORMONLY.fname;
last = FORMONLY.last,NOT NULL;
BUTTON bmaster:master,TEXT="Edit Master";
p_date = FORMONLY.p_date,NOT NULL,DEFAULT=TODAY;
p_amount = FORMONLY.p_amount,NOT NULL;
p_reason = FORMONLY.p_reason,NOT NULL;
CHECKBOX clubmember = FORMONLY.clubmember,NOT NULL,DEFAULT="";
otherdetail = FORMONLY.otherdetail,REQUIRED;
INSTRUCTIONS
SCREEN RECORD cr(FORMONLY.fname,FORMONLY.last);
SCREEN RECORD donations(FORMONLY.p_date THRU FORMONLY.p_reason);
SCREEN RECORD details(FORMONLY.clubmember THRU FORMONLY.otherdetail);
@@ -0,0 +1,44 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
LAYOUT(TEXT="Folders unrolled to a plain form")
VBOX
GROUP(TEXT="Customer (Master Record)")
GRID
{
First Name:[fname ] Last Name:[last ]
}
END
END --GROUP
GROUP (TEXT="Donations")
FORM "donations"
END --GROUP
GROUP(TEXT="some data")
GRID
{
Member in the organization [clubmember]
Other detail [otherdetail ]
}
END --GRID
END --GROUP
END --VBOX
ATTRIBUTES
fname = FORMONLY.fname;
last = FORMONLY.last,NOT NULL;
CHECKBOX clubmember = FORMONLY.clubmember,NOT NULL;
otherdetail = FORMONLY.otherdetail,REQUIRED;
INSTRUCTIONS
SCREEN RECORD cr(FORMONLY.fname,FORMONLY.last);
SCREEN RECORD details(FORMONLY.clubmember THRU FORMONLY.otherdetail);
+21
View File
@@ -0,0 +1,21 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
MODULES = \
Folder.42m \
Folder_old.42m\
Folder2.42m
FORMS = \
donations.42f \
Folder.42f\
Folder_old.42f\
Folder_unrolled.42f
include ../../Makeincl
+57
View File
@@ -0,0 +1,57 @@
Folder example
--------------
This example shows how to use a master record and multiple sub dialogs to input details in several folder pages.
To show how complex it was before, there is a Folder_old.4gl example that compiles with older versions and does basically the same as the Folder.4gl sample.
Each sub dialog contains some useful validation code to prove that we are coming thru when jumping from one page to the other.
Folder_old.4gl:
A big while loop was needed with page actions in each page to leave the current interaction statement, set the state, and jump into the next dialog. The difficulty in leaving the dialogs to move to another page is in using the ACCEPT INPUT statement properly (introduced for this purpose). Otherwise, the AFTER FIELD/AFTER ROW/AFTER INPUT triggers are not called (exiting the current dialog with EXIT INPUT will cause this to happen, and you will need to call your validations again for each page action).
You need to set the correct state before each ACCEPT INPUT, also for the 'accept' action, because if a validation in between fails, state still has the value of the last clicked action. In the cancel case, we need to check int_flag.
If you want to add another sub dialog, you must edit all existing dialog statements to add the new action for activating the new sub dialog.
It's annoying that it is not possible for the user to simply click into the master dialog if the focus is in one of the pages.
You cant't tab simply from the master to the details.
When leaving a particular INPUT, you should check if something has been changed for the insert/update case.
It's obvious that this programming style is only a vehicle to make things happen, and the visual results are also not fully satisfying.
Folder.4gl
All those headaches now disappear when you use the DIALOG statement.
Each page can be reached without the need for an action to raise it:
the master record is always reachable with this mode. The AFTER FIELD/(AFTER ROW)/AFTER INPUT triggers of one sub dialog are called when you move to another sub dialog. In the AFTER INPUT trigger you can place database instructions as in the Folder_old sample.
But, just like in the old sample, a sub dialog can be visited multiple times, so it must be checked in case something has to be inserted/updated.
If we want to reset the internal flags that indicate a field of a sub dialog has been changed, we must call the DIALOG.setTouchedFlag() function (of course, there are also other methods to check whether a field has changed: make a record copy or use ON CHANGE).
To indicate that a validation has gone wrong, the NEXT FIELD statement is used (as usual) to interrupt the DIALOG's internal state machine and stay in the current (invalid) sub dialog.
Folder2.4gl
The Folder2 sample shows that it's also possible to fill in all data in the sub dialogs first, and to do the database activity later in the AFTER DIALOG. This lets the database code run at one central place of the multiple dialog in a transaction. It also avoids checking whether a sub dialog has been entered/left twice.
The 'ok' action then starts the transaction, the 'cancel' action cancels everything (with no need to cleanup things done earlier in the dialog).
The Folder2 sample also shows that there is no difference in how the sub dialogs work if they are not placed inside folders, by running the same code again in another form (with no folder at all).
Source code splitting:
------------------------------
In the Folder_old.4gl it was possible to put the code for the individual (sub) dialogs into different source files (not actually done in the sample )
For example:
WHEN "page1"
LET state="invalid"
INPUT ARRAY donations WITHOUT DEFAULTS FROM donations.* ATTRIBUTES(UNBUFFERED)
...
END INPUT
could be coded by using a function call to perform the INPUT ARRAY
WHEN "page1"
LET state="invalid"
CALL input_donations()
and "input_donations" could reside in an extra source code module. This could be done for all other dialogs used in the WHILE loop. This helps to prevent a huge monolithic source code module, regardless of the fact that the modules have a strong affinity in terms of the folder states.
Now, with multiple dialogs and SUBDIALOGs and embeddable FORM's this is done but in a much cleaner way.
Each SUBDIALOG can be moved into an individual module and the form belonging to that source code module can be in a separate form file which is then included via "FORM" in the master form.
In the example this is done for the "donations" sub DIALOG. It resides in a module called "donations.4gl" and the form for that module is in "donations.per".
Extra sugar: donations.4gl contains a MAIN function which can call the individual dialog standalone for testing (just do fglrun donations)
This shows that there is no relation to the master dialog anymore,
it helps much for splitting the work for individual dialogs into separate testable pieces.
+48
View File
@@ -0,0 +1,48 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
DEFINE donations DYNAMIC ARRAY OF RECORD
p_date DATE,
p_amount MONEY,
p_reason VARCHAR(250)
END RECORD
FUNCTION fill()
DEFINE i INT
FOR i=1 TO 5
LET donations[i].p_date = TODAY -10 +i
LET donations[i].p_amount = i+1
END FOR
END FUNCTION
DIALOG donations()
INPUT ARRAY donations FROM donations.*
AFTER FIELD p_amount
IF donations[arr_curr()].p_amount<2.0 THEN
ERROR "must be at least 2 dollar:-)"
NEXT FIELD CURRENT
END IF
AFTER ROW
IF arr_curr()<=arr_count() THEN
DISPLAY "AFTER ROW donations:check row and save to database"
END IF
END INPUT
END DIALOG
FUNCTION MAIN() -- This MAIN is for testing only.
OPEN FORM f FROM "donations"
DISPLAY FORM f
CALL FILL()
DIALOG
SUBDIALOG donations
ON ACTION ACCEPT
ACCEPT DIALOG
END DIALOG
END FUNCTION
+31
View File
@@ -0,0 +1,31 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
LAYOUT
TABLE
{
DATE Amount Reason
[p_date][p_amount ][p_reason ]
[p_date][p_amount ][p_reason ]
[p_date][p_amount ][p_reason ]
[p_date][p_amount ][p_reason ]
[p_date][p_amount ][p_reason ]
[p_date][p_amount ][p_reason ]
[p_date][p_amount ][p_reason ]
[p_date][p_amount ][p_reason ]
[p_date][p_amount ][p_reason ]
}
END
END
ATTRIBUTES
p_date = FORMONLY.p_date,NOT NULL,DEFAULT=TODAY;
p_amount = FORMONLY.p_amount,NOT NULL;
p_reason = FORMONLY.p_reason,NOT NULL;
INSTRUCTIONS
SCREEN RECORD donations(FORMONLY.*);
+18
View File
@@ -0,0 +1,18 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
MODULES= \
formwizard.42m \
formwizard_old.42m
FORMS= \
formwizard.42f \
formwizard_old.42f
include ../../Makeincl
+56
View File
@@ -0,0 +1,56 @@
Form wizard
-----------
This is an example to mimic the main component of the Microsoft Access form wizard, the dialog to choose which database columns should be handled in the generated form.
It turned out that it is pretty easy to mimic with the help of the multiple dialogs; it behaves exactly like the original dialog (with the same logic behind and all Keyboard shortcuts).
The code to do this is pretty short and readable.
This dialog is used in the $FGLDIR/demo/tools/fglped program to generate a form from a list of chosen columns.
formwizard_old
This example shows what you would have to do (theoretically) to program a similar dialog in 4GL without multiple dialogs available. As you can see in the wizard() function, 3 tradional dialogs are called in a loop and a lot of actions are needed to jump from one dialog to the other. 3 additional buttons are inside the form to leave the current active dialog and jump to a different dialog...pure headache!
Unfortunately the GDC by default renders non-active tables in the same way as active tables, so the end user may think that he could click on a table and edit there; but, bummer, you must use the "Edit Left/Right" button or <Tab> to leave the current active table (Shift-Tab is not even possible).
This example also shows why it is not sufficient to add a trigger like
ON OTHERTABLECLICKED (demanded by a lot of our customers).
Although it makes the "Edit Left/Right" button unnecessary, it still forces you to write such a big state loop of traditional dialogs.
Further Differences
-------------------
If you look at the code in formwizard.4gl and formwizard_old.4gl there are also differences in performing the actions.
FUNCTION right(d) --multiple dialog variant
DEFINE d ui.Dialog
DEFINE idx,lastC INT
--get index of the left array; cannot use arr_curr() here because arr_curr()
--returns the index of the current active row in the current focused table
--which is not necessarily the right hand side array
--the "right" action is always active regardless of the focus
LET idx = d.getCurrentRow("a")
LET lastC=cfields.getLength()+1
LET cfields[lastC].ctable=afields[idx].atable
LET cfields[lastC].ccol=afields[idx].acol
CALL afields.deleteElement(idx)
--set the current line to the last index
CALL d.setCurrentRow("c",lastC)
END FUNCTION
You may also note that you don't need to refresh the arrays on the screen; this is done with the ATTRIBUTES(UNBUFFERED) flag of the entire dialog. Since all arrays are active, you won't need to refresh one of them manually.
Please note that you can do this only for DISPLAY ARRAY; for INPUT ARRAY it is strongly recommended that you don't manipulate the bound arrays with the array methods (deleteElement(), insertElement()) because this conflicts with the built-in triggers of INPUT ARRAY.
Now the "right" function from the formwizard_old example:
--move the current item to the right hand side and delete it on the left hand side
FUNCTION right(d)
DEFINE d ui.Dialog
DEFINE idx,lastC,i INT
LET idx = arr_curr()
LET lastC=cfields.getLength()+1
LET cfields[lastC].ctable=afields[idx].atable
LET cfields[lastC].ccol=afields[idx].acol
CALL afields.deleteElement(idx)
FOR i=1 TO cfields.getLength()
DISPLAY cfields[i].* TO c[i].*
END FOR
END FUNCTION
The "right" action is only active if the focus is inside the left hand side array; that's why it's possible to use arr_curr(). At the end of the function we need to redisplay the right hand side array, because it's not active (we are in a DISPLAY ARRAY of the left hand side).
@@ -0,0 +1,239 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
TYPE schema_rec RECORD
table_name STRING, -- customer
field_name STRING, -- customer_name
field_type SMALLINT, -- 0
field_strtype STRING, -- CHAR(30)
field_length SMALLINT -- 80
END RECORD
DEFINE col_arr DYNAMIC ARRAY OF schema_rec
DEFINE tab_arr DYNAMIC ARRAY OF STRING
DEFINE currtable STRING --current table in combobox
--a(vailable) fields
DEFINE afields DYNAMIC ARRAY OF RECORD
atable STRING,
acol STRING
END RECORD
--c(hosen) fields
DEFINE cfields DYNAMIC ARRAY OF RECORD
ctable STRING,
ccol STRING
END RECORD
-- Current wizard page
DEFINE currpage SMALLINT
DEFINE myd ui.Dialog
CONSTANT TOTAL_PAGES = 5
MAIN
CLOSE WINDOW screen
CALL wizard()
END MAIN
--contains the wizard MD dialog
FUNCTION wizard()
DEFINE i INT
CALL readTables("stores.sch")
OPEN WINDOW formwizard WITH FORM "formwizard"
CALL init_tables()
OPTIONS FIELD ORDER FORM
DIALOG ATTRIBUTES(UNBUFFERED)
INPUT BY NAME currtable ATTRIBUTES(WITHOUT DEFAULTS=TRUE)
ON CHANGE currtable
CALL on_change_currtable()
CALL setup(DIALOG)
END INPUT
DISPLAY ARRAY afields TO a.* END DISPLAY
DISPLAY ARRAY cfields TO c.* END DISPLAY
ON ACTION right CALL right(DIALOG)
ON ACTION allright CALL allright(DIALOG)
ON ACTION left CALL left(DIALOG)
ON ACTION allleft CALL allleft(DIALOG)
ON ACTION prevwiz DISPLAY "prevwiz" EXIT DIALOG
ON ACTION nextwiz DISPLAY "nextwiz" EXIT DIALOG
ON ACTION cancel
EXIT DIALOG
ON ACTION accept
ACCEPT DIALOG
BEFORE DIALOG
LET currpage = 1
CALL setup(DIALOG)
AFTER DIALOG
--we just show all chosen columns in the terminal
FOR i=1 TO cfields.getLength()
DISPLAY sfmt("table:%1,column:%2",cfields[i].ctable,cfields[i].ccol)
END FOR
END DIALOG
LET myd=NULL
CLOSE WINDOW formwizard
END FUNCTION
--fill the combobox with all table names
FUNCTION init_tables()
DEFINE cb ui.ComboBox
DEFINE i INTEGER
LET cb = ui.ComboBox.forName("formonly.currtable")
IF cb IS NULL THEN
CALL myerror("combobox not found")
END IF
FOR i=1 TO tab_arr.getLength()
CALL cb.addItem(tab_arr[i],tab_arr[i])
END FOR
LET currtable=tab_arr[1]
CALL on_change_currtable()
END FUNCTION
--combo value has changed,refill the left array and omit
--values of the right hand side
FUNCTION on_change_currtable()
DEFINE tab,col STRING
DEFINE col_len,used_col_len,idx,i,j,foundUsed INT
IF myd IS NOT NULL THEN
CALL myd.deleteAllRows("a")
ELSE
CALL afields.clear()
END IF
LET idx=0
LET col_len=col_arr.getLength()
FOR i=1 TO col_len
LET tab=col_arr[i].table_name
LET col=col_arr[i].field_name
LET used_col_len=cfields.getLength()
LET foundUsed=FALSE
FOR j=1 TO used_col_len
IF cfields[j].ctable=tab AND cfields[j].ccol=col THEN
LET foundUsed=TRUE
EXIT FOR
END IF
END FOR
IF NOT foundUsed AND tab=currtable THEN
LET idx=idx+1
IF myd IS NOT NULL THEN
CALL myd.appendRow("a")
ELSE
CALL afields.appendElement()
END IF
LET afields[idx].atable=tab
LET afields[idx].acol=col
END IF
END FOR
END FUNCTION
FUNCTION setup(d)
DEFINE d ui.Dialog
DEFINE rl,rr INT
LET myd=d
LET rl = afields.getLength()
LET rr = cfields.getLength()
CALL d.setActionActive("right", rl>0)
CALL d.setActionActive("allright", rl>0)
CALL d.setActionActive("left", rr>0)
CALL d.setActionActive("allleft", rr>0)
CALL d.setActionActive("prevwiz", currpage>1)
CALL d.setActionActive("nextwiz", currpage<TOTAL_PAGES)
END FUNCTION
--move the current item to the right hand side and delete it on the left hand side
FUNCTION right(d)
DEFINE d ui.Dialog
DEFINE idx,lastC INT
LET idx = d.getCurrentRow("a")
IF idx<1 THEN RETURN END IF
LET lastC=cfields.getLength()+1
CALL myd.appendRow("c")
LET cfields[lastC].ctable=afields[idx].atable
LET cfields[lastC].ccol=afields[idx].acol
CALL afields.deleteElement(idx)
CALL d.setCurrentRow("c",lastC)
CALL setup(d)
END FUNCTION
--move all items to the right hand side
FUNCTION allright(d)
DEFINE d ui.Dialog
DEFINE lastC,i INT
LET lastC=cfields.getLength()
FOR i=1 TO afields.getLength()
LET lastC=lastC+1
CALL myd.appendRow("c")
LET cfields[lastC].ctable=afields[i].atable
LET cfields[lastC].ccol=afields[i].acol
END FOR
CALL myd.deleteAllRows("a") --CALL afields.clear()
CALL d.setCurrentRow("c",lastC)
CALL setup(d)
END FUNCTION
--remove the current item from the right hand side and refill the left hand side
FUNCTION left(d)
DEFINE d ui.Dialog
DEFINE idx,i INT
DEFINE col, tab STRING
LET idx =d.getCurrentRow("c")
IF idx<1 THEN RETURN END IF
LET tab=cfields[idx].ctable
LET col=cfields[idx].ccol
CALL cfields.deleteElement(idx)
CALL on_change_currtable()
FOR i=1 TO afields.getLength()
IF afields[i].atable=tab AND afields[i].acol=col THEN
CALL d.setCurrentRow("a",i)
EXIT FOR
END IF
END FOR
CALL setup(d)
END FUNCTION
--remove all items from the right hand side and refill the left hand side
FUNCTION allleft(d)
DEFINE d ui.Dialog
CALL cfields.clear()
CALL on_change_currtable()
CALL setup(d)
END FUNCTION
--read in a schema file
FUNCTION readTables(schemaFile)
DEFINE schemaFile STRING
DEFINE ch base.channel
DEFINE prevTabName STRING
DEFINE sch schema_rec
CALL col_arr.clear()
CALL tab_arr.clear()
LET ch = base.channel.create()
WHENEVER ERROR CONTINUE
CALL ch.openFile(schemaFile, "r")
WHENEVER ERROR STOP
IF STATUS!=0 THEN
CALL myerror( "error: could not open schema file '"||schemaFile||"'")
END IF
CALL ch.setdelimiter("^")
LET prevTabName=NULL
WHILE ch.read(sch)
IF sch.table_name.getIndexOf("sys",1)==1 THEN
CONTINUE WHILE
END IF
LET col_arr[col_arr.getLength()+1].* = sch.*
IF prevTabName IS NULL OR sch.table_name <> prevTabName THEN
LET tab_arr[tab_arr.getLength()+1]=sch.table_name
LET prevTabName=sch.table_name
END IF
END WHILE
CALL ch.close()
IF tab_arr.getLength()==0 THEN
CALL myerror("did not find any coluns in "||schemaFile)
END IF
END FUNCTION
FUNCTION myerror(str)
DEFINE str STRING
DISPLAY "ERROR:",str
EXIT PROGRAM 1
END FUNCTION
@@ -0,0 +1,60 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
LAYOUT(STYLE="dialog3",TEXT="Form assistant")
GRID
{
[img ]
[ ] [quest : ]
[ ]
[ ]
Tables:
[currtable ]
<T t > <T z >
Available fields Chosen fields
[x|afields ] [r ][y|cfields ]
[x|afields ] [rr ][y|cfields ]
[x|afields ] [y|cfields ]
[x|afields ] [l ][y|cfields ]
[x|afields ] [ll ][y|cfields ]
[""]
---------------------------------------------------------------
[ :cancel:" ":back : next :" ":finish ]
}
END
END
ATTRIBUTES
IMAGE img:IMAGE="formwizard.png";
LABEL quest:TEXT="Which fields your form should contain ?";
COMBOBOX currtable=FORMONLY.currtable,COMMENT="You can choose from more than one table.",NOT NULL;
TABLE t:DOUBLECLICK=right;
TABLE z:DOUBLECLICK=left;
x=FORMONLY.atable,SCROLL,HIDDEN;
afields=FORMONLY.acol,SCROLL;
y=FORMONLY.ctable,SCROLL,HIDDEN;
cfields=FORMONLY.ccol;
BUTTON r:right,TEXT=">";
BUTTON rr:allright,TEXT=">>";
BUTTON l:left,TEXT="<";
BUTTON ll:allleft,TEXT="<<";
BUTTON cancel:cancel,TEXT="Cancel";
BUTTON back:prevwiz,TEXT="< &Back";
BUTTON next:nextwiz,TEXT="&Next >";
BUTTON finish:accept,TEXT="&Finish";
END
INSTRUCTIONS
SCREEN RECORD a(atable,acol);
SCREEN RECORD c(ctable,ccol);
END
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,247 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
TYPE schema_rec RECORD
table_name STRING, -- customer
field_name STRING, -- customer_name
field_type SMALLINT, -- 0
field_strtype STRING, -- CHAR(30)
field_length SMALLINT -- 80
END RECORD
DEFINE col_arr DYNAMIC ARRAY OF schema_rec
DEFINE tab_arr DYNAMIC ARRAY OF STRING
DEFINE currtable STRING --current table in combobox
--a(vailable) fields
DEFINE afields DYNAMIC ARRAY OF RECORD
atable STRING,
acol STRING
END RECORD
--c(hosen) fields
DEFINE cfields DYNAMIC ARRAY OF RECORD
ctable STRING,
ccol STRING
END RECORD
MAIN
CLOSE WINDOW screen
CALL wizard()
END MAIN
--contains the wizard dialog build with traditional 4GL dialogs called in a loop
--the common actions must be cloned into each dialog
FUNCTION wizard()
DEFINE i INT
DEFINE state STRING
CALL readTables("stores.sch")
OPEN WINDOW formwizard WITH FORM "formwizard_old"
CALL init_tables()
OPTIONS FIELD ORDER FORM
LET state="combo"
WHILE state<>"exit" AND state<>"accept"
CASE state
WHEN "combo"
LET state="left"
INPUT BY NAME currtable WITHOUT DEFAULTS ATTRIBUTES(UNBUFFERED)
ON CHANGE currtable CALL on_change_currtable()
ON ACTION gocombo LET state="combo" EXIT INPUT
ON ACTION goleft LET state="left" EXIT INPUT
ON ACTION goright LET state="right" EXIT INPUT
ON ACTION cancel LET state="exit" EXIT INPUT
ON ACTION accept LET state="accept" EXIT INPUT
END INPUT
WHEN "left"
LET state="right"
DISPLAY ARRAY afields TO a.* ATTRIBUTES(UNBUFFERED)
ON ACTION right CALL right(DIALOG)
ON KEY(RIGHT) CALL right(DIALOG)
ON ACTION allright CALL allright(DIALOG)
ON KEY(TAB) EXIT DISPLAY
ON ACTION gocombo LET state="combo" EXIT DISPLAY
ON ACTION goleft LET state="left" EXIT DISPLAY
ON ACTION goright LET state="right" EXIT DISPLAY
ON ACTION cancel LET state="exit" EXIT DISPLAY
ON ACTION accept LET state="accept" EXIT DISPLAY
END DISPLAY
WHEN "right"
LET state="combo"
DISPLAY ARRAY cfields TO c.* ATTRIBUTES(UNBUFFERED)
ON ACTION left CALL left(DIALOG)
ON KEY(LEFT) CALL left(DIALOG)
ON ACTION allleft CALL allleft(DIALOG)
ON KEY(TAB) EXIT DISPLAY
ON ACTION gocombo LET state="combo" EXIT DISPLAY
ON ACTION goleft LET state="left" EXIT DISPLAY
ON ACTION goright LET state="right" EXIT DISPLAY
ON ACTION cancel LET state="exit" EXIT DISPLAY
ON ACTION accept LET state="accept" EXIT DISPLAY
ON ACTION prevwiz LET state="exit" EXIT DISPLAY
ON ACTION nextwiz LET state="exit" EXIT DISPLAY
END DISPLAY
END CASE
END WHILE
--we just show all chosen columns in the terminal
IF state=="accept" THEN
FOR i=1 TO cfields.getLength()
DISPLAY sfmt("table:%1,column:%2",cfields[i].ctable,cfields[i].ccol)
END FOR
END IF
CLOSE WINDOW formwizard
END FUNCTION
--fill the combobox with all table names
FUNCTION init_tables()
DEFINE cb ui.ComboBox
DEFINE i INTEGER
LET cb = ui.ComboBox.forName("formonly.currtable")
IF cb IS NULL THEN
CALL myerror("combobox not found")
END IF
FOR i=1 TO tab_arr.getLength()
CALL cb.addItem(tab_arr[i],tab_arr[i])
END FOR
LET currtable=tab_arr[1]
CALL on_change_currtable()
END FUNCTION
--combo value has changed,refill the left array and omit
--values of the right hand side
FUNCTION on_change_currtable()
DEFINE tab,col STRING
DEFINE col_len,used_col_len,idx,i,j,foundUsed,tabSize INT
CALL afields.clear()
LET idx=0
LET col_len=col_arr.getLength()
FOR i=1 TO col_len
LET tab=col_arr[i].table_name
LET col=col_arr[i].field_name
LET used_col_len=cfields.getLength()
LET foundUsed=FALSE
FOR j=1 TO used_col_len
IF cfields[j].ctable=tab AND cfields[j].ccol=col THEN
LET foundUsed=TRUE
EXIT FOR
END IF
END FOR
IF NOT foundUsed AND tab=currtable THEN
LET idx=idx+1
LET afields[idx].atable=tab
LET afields[idx].acol=col
END IF
END FOR
LET tabSize=tableSize("a")
FOR i=1 TO tabSize
DISPLAY "" TO a[i].acol
END FOR
FOR i=1 TO afields.getLength()
DISPLAY afields[i].* TO a[i].*
END FOR
END FUNCTION
--move the current item to the right hand side and delete it on the left hand side
FUNCTION right(d)
DEFINE d ui.Dialog
DEFINE idx,lastC,i INT
LET idx = arr_curr()
LET lastC=cfields.getLength()+1
LET cfields[lastC].ctable=afields[idx].atable
LET cfields[lastC].ccol=afields[idx].acol
CALL afields.deleteElement(idx)
FOR i=1 TO cfields.getLength()
DISPLAY cfields[i].* TO c[i].*
END FOR
END FUNCTION
--move all items to the right hand side
FUNCTION allright(d)
DEFINE d ui.Dialog
DEFINE lastC,i INT
LET lastC=cfields.getLength()
FOR i=1 TO afields.getLength()
LET lastC=lastC+1
LET cfields[lastC].ctable=afields[i].atable
LET cfields[lastC].ccol=afields[i].acol
END FOR
CALL afields.clear()
FOR i=1 TO cfields.getLength()
DISPLAY cfields[i].* TO c[i].*
END FOR
END FUNCTION
--remove the current item from the right hand side and refill the left hand side
FUNCTION left(d)
DEFINE d ui.Dialog
CALL cfields.deleteElement(arr_curr())
CALL on_change_currtable()
END FUNCTION
--remove all items from the right hand side and refill the left hand side
FUNCTION allleft(d)
DEFINE d ui.Dialog
CALL cfields.clear()
CALL on_change_currtable()
END FUNCTION
--read in a schema file
FUNCTION readTables(schemaFile)
DEFINE schemaFile STRING
DEFINE ch base.channel
DEFINE prevTabName STRING
DEFINE sch schema_rec
CALL col_arr.clear()
CALL tab_arr.clear()
LET ch = base.channel.create()
WHENEVER ERROR CONTINUE
CALL ch.openFile(schemaFile, "r")
WHENEVER ERROR STOP
IF STATUS!=0 THEN
CALL myerror( "error: could not open schema file '"||schemaFile||"'")
END IF
CALL ch.setdelimiter("^")
LET prevTabName=NULL
WHILE ch.read(sch)
IF sch.table_name.getIndexOf("sys",1)==1 THEN
CONTINUE WHILE
END IF
LET col_arr[col_arr.getLength()+1].* = sch.*
IF prevTabName IS NULL OR sch.table_name <> prevTabName THEN
LET tab_arr[tab_arr.getLength()+1]=sch.table_name
LET prevTabName=sch.table_name
END IF
END WHILE
CALL ch.close()
IF tab_arr.getLength()==0 THEN
CALL myerror("did not find any coluns in "||schemaFile)
END IF
END FUNCTION
FUNCTION myerror(str)
DEFINE str STRING
DISPLAY "ERROR:",str
EXIT PROGRAM 1
END FUNCTION
FUNCTION tableSize(screenRec)
DEFINE screenRec STRING
DEFINE nl om.NodeList
DEFINE root,tab om.DomNode
DEFINE doc om.DomDocument
DEFINE currWinId,bufferSize INT
LET root=ui.Interface.getRootNode()
LET currWinId=root.getAttribute("currentWindow")
LET doc =ui.Interface.getDocument()
LET root=doc.getElementById(currWinId)
LET nl=root.selectByPath(sfmt("//Table[@tabName=\"%1\"]",screenRec))
IF nl.getLength()>0 THEN
LET tab=nl.item(1)
LET bufferSize=tab.getAttribute("bufferSize")
ELSE
DISPLAY "can't find Table"
END IF
RETURN bufferSize
END FUNCTION
@@ -0,0 +1,54 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
LAYOUT(STYLE="dialog3",TEXT="Form assistant")
GRID
{
[img ]
[ ] [quest : ]
[ ]
[ ]
[gocombo: ]
[currtable ]
<T t > <T z >
Available fields Chosen fields
[x|afields ] [r ][y|cfields ]
[x|afields ] [rr ][y|cfields ]
[x|afields ] [y|cfields ]
[x|afields ] [l ][y|cfields ]
[x|afields ] [ll ][y|cfields ]
[goleft ] [goright ]
[""]
---------------------------------------------------------------
[ :cancel:" ":back : next :" ":finish ]
}
END
ATTRIBUTES
IMAGE img:IMAGE="formwizard.png";
LABEL quest:TEXT="Which fields your form should contain ?";
COMBOBOX currtable=FORMONLY.currtable,COMMENT="You can choose from more than one table.",NOT NULL;
x=FORMONLY.atable,SCROLL,HIDDEN;
afields=FORMONLY.acol,SCROLL;
y=FORMONLY.ctable,SCROLL,HIDDEN;
cfields=FORMONLY.ccol;
BUTTON gocombo:gocombo,TEXT="Tables:";
BUTTON goleft:goleft,TEXT="Edit Left";
BUTTON goright:goright,TEXT="Edit Right";
BUTTON r:right,TEXT=">";
BUTTON rr:allright,TEXT=">>";
BUTTON l:left,TEXT="<";
BUTTON ll:allleft,TEXT="<<";
BUTTON cancel:cancel,TEXT="Cancel";
BUTTON back:prevwiz,TEXT="< &Back";
BUTTON next:nextwiz,TEXT="&Next >";
BUTTON finish:accept,TEXT="&Finish";
INSTRUCTIONS
SCREEN RECORD a(atable,acol);
SCREEN RECORD c(ctable,ccol);
+410
View File
@@ -0,0 +1,410 @@
customer^customer_num^262^4^1^
customer^fname^0^15^2^
customer^lname^0^15^3^
customer^company^0^20^4^
customer^address1^0^20^5^
customer^address2^0^20^6^
customer^city^0^15^7^
customer^state^0^2^8^
customer^zipcode^0^5^9^
customer^phone^0^18^10^
items^item_num^1^2^1^
items^order_num^2^4^2^
items^stock_num^1^2^3^
items^manu_code^0^3^4^
items^quantity^1^2^5^
items^total_price^8^2050^6^
manufact^manu_code^0^3^1^
manufact^manu_name^0^15^2^
orders^order_num^262^4^1^
orders^order_date^7^4^2^
orders^customer_num^2^4^3^
orders^ship_instruct^0^40^4^
orders^backlog^0^1^5^
orders^po_num^0^10^6^
orders^ship_date^7^4^7^
orders^ship_weight^5^2050^8^
orders^ship_charge^8^1538^9^
orders^paid_date^7^4^10^
state^code^0^2^1^
state^sname^0^15^2^
stock^stock_num^1^2^1^
stock^manu_code^0^3^2^
stock^description^0^15^3^
stock^unit_price^8^1538^4^
stock^unit^0^4^5^
stock^unit_descr^0^15^6^
sysaggregates^name^13^128^1^
sysaggregates^owner^0^32^2^
sysaggregates^aggid^6^4^3^
sysaggregates^init_func^13^128^4^
sysaggregates^iter_func^13^128^5^
sysaggregates^combine_func^13^128^6^
sysaggregates^final_func^13^128^7^
sysams^am_name^13^128^1^
sysams^am_owner^0^32^2^
sysams^am_id^2^4^3^
sysams^am_type^0^1^4^
sysams^am_sptype^0^3^5^
sysams^am_defopclass^2^4^6^
sysams^am_keyscan^2^4^7^
sysams^am_unique^2^4^8^
sysams^am_cluster^2^4^9^
sysams^am_rowids^2^4^10^
sysams^am_readwrite^2^4^11^
sysams^am_parallel^2^4^12^
sysams^am_costfactor^4^4^13^
sysams^am_create^2^4^14^
sysams^am_drop^2^4^15^
sysams^am_open^2^4^16^
sysams^am_close^2^4^17^
sysams^am_insert^2^4^18^
sysams^am_delete^2^4^19^
sysams^am_update^2^4^20^
sysams^am_stats^2^4^21^
sysams^am_scancost^2^4^22^
sysams^am_check^2^4^23^
sysams^am_beginscan^2^4^24^
sysams^am_endscan^2^4^25^
sysams^am_rescan^2^4^26^
sysams^am_getnext^2^4^27^
sysams^am_getbyid^2^4^28^
sysams^am_build^2^4^29^
sysams^am_init^2^4^30^
sysattrtypes^extended_id^2^4^1^
sysattrtypes^seqno^1^2^2^
sysattrtypes^levelno^1^2^3^
sysattrtypes^parent_no^1^2^4^
sysattrtypes^fieldname^13^128^5^
sysattrtypes^fieldno^1^2^6^
sysattrtypes^type^1^2^7^
sysattrtypes^length^1^2^8^
sysattrtypes^xtd_type_id^2^4^9^
sysblobs^spacename^13^128^1^
sysblobs^type^0^1^2^
sysblobs^tabid^2^4^3^
sysblobs^colno^1^2^4^
syscasts^owner^0^32^1^
syscasts^argument_type^1^2^2^
syscasts^argument_xid^2^4^3^
syscasts^result_type^1^2^4^
syscasts^result_xid^2^4^5^
syscasts^routine_name^13^128^6^
syscasts^routine_owner^0^32^7^
syscasts^class^0^1^8^
syschecks^constrid^2^4^1^
syschecks^type^0^1^2^
syschecks^seqno^1^2^3^
syschecks^checktext^0^32^4^
syscolatt^tabname^0^18^1^
syscolatt^colname^0^18^2^
syscolatt^seqno^262^4^3^
syscolatt^color^1^2^4^
syscolatt^inverse^0^1^5^
syscolatt^underline^0^1^6^
syscolatt^blink^0^1^7^
syscolatt^left^0^1^8^
syscolatt^def_format^0^64^9^
syscolatt^condition^0^64^10^
syscolattribs^tabid^2^4^1^
syscolattribs^colno^1^2^2^
syscolattribs^extentsize^2^4^3^
syscolattribs^flags^2^4^4^
syscolattribs^flags1^2^4^5^
syscolattribs^sbspace^13^128^6^
syscolauth^grantor^0^32^1^
syscolauth^grantee^0^32^2^
syscolauth^tabid^2^4^3^
syscolauth^colno^1^2^4^
syscolauth^colauth^0^3^5^
syscoldepend^constrid^2^4^1^
syscoldepend^tabid^2^4^2^
syscoldepend^colno^1^2^3^
syscolumns^colname^13^128^1^
syscolumns^tabid^2^4^2^
syscolumns^colno^1^2^3^
syscolumns^coltype^1^2^4^
syscolumns^collength^1^2^5^
syscolumns^colmin^2^4^6^
syscolumns^colmax^2^4^7^
syscolumns^extended_id^2^4^8^
sysconstraints^constrid^6^4^1^
sysconstraints^constrname^13^128^2^
sysconstraints^owner^0^32^3^
sysconstraints^tabid^2^4^4^
sysconstraints^constrtype^0^1^5^
sysconstraints^idxname^13^128^6^
sysconstraints^collation^0^36^7^
sysdefaults^tabid^2^4^1^
sysdefaults^colno^1^2^2^
sysdefaults^type^0^1^3^
sysdefaults^default^0^256^4^
sysdefaults^class^0^1^5^
sysdepend^btabid^2^4^1^
sysdepend^btype^0^1^2^
sysdepend^dtabid^2^4^3^
sysdepend^dtype^0^1^4^
sysdistrib^tabid^2^4^1^
sysdistrib^colno^1^2^2^
sysdistrib^seqno^2^4^3^
sysdistrib^constructed^7^4^4^
sysdistrib^mode^0^1^5^
sysdistrib^resolution^4^4^6^
sysdistrib^confidence^4^4^7^
sysdomains^id^262^4^1^
sysdomains^owner^0^32^2^
sysdomains^name^13^128^3^
sysdomains^type^1^2^4^
syserrors^sqlstate^0^5^1^
syserrors^locale^0^36^2^
syserrors^level^1^2^3^
syserrors^seqno^1^2^4^
syserrors^message^13^255^5^
sysfragauth^grantor^0^32^1^
sysfragauth^grantee^0^32^2^
sysfragauth^tabid^2^4^3^
sysfragauth^fragment^13^128^4^
sysfragauth^fragauth^0^6^5^
sysfragments^fragtype^0^1^1^
sysfragments^tabid^2^4^2^
sysfragments^indexname^13^128^3^
sysfragments^colno^2^4^4^
sysfragments^partn^2^4^5^
sysfragments^strategy^0^1^6^
sysfragments^location^0^1^7^
sysfragments^servername^13^128^8^
sysfragments^evalpos^2^4^9^
sysfragments^exprtext^12^56^10^
sysfragments^exprbin^11^56^11^
sysfragments^exprarr^11^56^12^
sysfragments^flags^2^4^13^
sysfragments^dbspace^13^128^14^
sysfragments^levels^1^2^15^
sysfragments^npused^2^4^16^
sysfragments^nrows^2^4^17^
sysfragments^clust^2^4^18^
sysindexes^idxname^13^128^1^
sysindexes^owner^0^32^2^
sysindexes^tabid^2^4^3^
sysindexes^idxtype^0^1^4^
sysindexes^clustered^0^1^5^
sysindexes^part1^1^2^6^
sysindexes^part2^1^2^7^
sysindexes^part3^1^2^8^
sysindexes^part4^1^2^9^
sysindexes^part5^1^2^10^
sysindexes^part6^1^2^11^
sysindexes^part7^1^2^12^
sysindexes^part8^1^2^13^
sysindexes^part9^1^2^14^
sysindexes^part10^1^2^15^
sysindexes^part11^1^2^16^
sysindexes^part12^1^2^17^
sysindexes^part13^1^2^18^
sysindexes^part14^1^2^19^
sysindexes^part15^1^2^20^
sysindexes^part16^1^2^21^
sysindexes^levels^1^2^22^
sysindexes^leaves^2^4^23^
sysindexes^nunique^2^4^24^
sysindexes^clust^2^4^25^
sysindices^idxname^13^128^1^
sysindices^owner^0^32^2^
sysindices^tabid^2^4^3^
sysindices^idxtype^0^1^4^
sysindices^clustered^0^1^5^
sysindices^levels^1^2^6^
sysindices^leaves^2^4^7^
sysindices^nunique^2^4^8^
sysindices^clust^2^4^9^
sysindices^nrows^3^8^10^
sysinherits^child^2^4^1^
sysinherits^parent^2^4^2^
sysinherits^class^0^1^3^
syslangauth^grantor^0^32^1^
syslangauth^grantee^0^32^2^
syslangauth^langid^2^4^3^
syslangauth^langauth^0^1^4^
syslogmap^tabloc^2^4^1^
syslogmap^tabid^2^4^2^
syslogmap^fragid^2^4^3^
syslogmap^flags^2^4^4^
sysobjstate^objtype^0^1^1^
sysobjstate^owner^0^32^2^
sysobjstate^name^13^128^3^
sysobjstate^tabid^2^4^4^
sysobjstate^state^0^1^5^
sysopclasses^opclassname^13^128^1^
sysopclasses^owner^0^32^2^
sysopclasses^amid^2^4^3^
sysopclasses^opclassid^6^4^4^
sysopclstr^owner^0^32^1^
sysopclstr^clstrname^13^128^2^
sysopclstr^clstrsize^2^4^3^
sysopclstr^tabid^2^4^4^
sysopclstr^blobcol1^1^2^5^
sysopclstr^blobcol2^1^2^6^
sysopclstr^blobcol3^1^2^7^
sysopclstr^blobcol4^1^2^8^
sysopclstr^blobcol5^1^2^9^
sysopclstr^blobcol6^1^2^10^
sysopclstr^blobcol7^1^2^11^
sysopclstr^blobcol8^1^2^12^
sysopclstr^blobcol9^1^2^13^
sysopclstr^blobcol10^1^2^14^
sysopclstr^blobcol11^1^2^15^
sysopclstr^blobcol12^1^2^16^
sysopclstr^blobcol13^1^2^17^
sysopclstr^blobcol14^1^2^18^
sysopclstr^blobcol15^1^2^19^
sysopclstr^blobcol16^1^2^20^
sysopclstr^clstrkey1^1^2^21^
sysopclstr^clstrkey2^1^2^22^
sysopclstr^clstrkey3^1^2^23^
sysopclstr^clstrkey4^1^2^24^
sysopclstr^clstrkey5^1^2^25^
sysopclstr^clstrkey6^1^2^26^
sysopclstr^clstrkey7^1^2^27^
sysopclstr^clstrkey8^1^2^28^
sysopclstr^clstrkey9^1^2^29^
sysopclstr^clstrkey10^1^2^30^
sysopclstr^clstrkey11^1^2^31^
sysopclstr^clstrkey12^1^2^32^
sysopclstr^clstrkey13^1^2^33^
sysopclstr^clstrkey14^1^2^34^
sysopclstr^clstrkey15^1^2^35^
sysopclstr^clstrkey16^1^2^36^
sysprocauth^grantor^0^32^1^
sysprocauth^grantee^0^32^2^
sysprocauth^procid^2^4^3^
sysprocauth^procauth^0^1^4^
sysprocbody^procid^2^4^1^
sysprocbody^datakey^0^1^2^
sysprocbody^seqno^2^4^3^
sysprocbody^data^0^256^4^
sysprocedures^procname^13^128^1^
sysprocedures^owner^0^32^2^
sysprocedures^procid^6^4^3^
sysprocedures^mode^0^1^4^
sysprocedures^retsize^2^4^5^
sysprocedures^symsize^2^4^6^
sysprocedures^datasize^2^4^7^
sysprocedures^codesize^2^4^8^
sysprocedures^numargs^2^4^9^
sysprocedures^isproc^0^1^10^
sysprocedures^specificname^13^128^11^
sysprocedures^externalname^13^255^12^
sysprocedures^paramstyle^0^1^13^
sysprocedures^langid^2^4^14^
sysprocplan^procid^2^4^1^
sysprocplan^planid^2^4^2^
sysprocplan^datakey^0^1^3^
sysprocplan^seqno^2^4^4^
sysprocplan^created^7^4^5^
sysprocplan^datasize^2^4^6^
sysprocplan^data^0^256^7^
sysreferences^constrid^2^4^1^
sysreferences^primary^2^4^2^
sysreferences^ptabid^2^4^3^
sysreferences^updrule^0^1^4^
sysreferences^delrule^0^1^5^
sysreferences^matchtype^0^1^6^
sysreferences^pendant^0^1^7^
sysroleauth^rolename^0^32^1^
sysroleauth^grantee^0^32^2^
sysroleauth^is_grantable^0^1^3^
sysroutinelangs^langid^6^4^1^
sysroutinelangs^langname^0^30^2^
sysroutinelangs^langinitfunc^13^128^3^
sysroutinelangs^langpath^0^255^4^
sysroutinelangs^langclass^0^18^5^
syssequences^seqid^6^4^1^
syssequences^tabid^2^4^2^
syssynonyms^owner^0^32^1^
syssynonyms^synname^13^128^2^
syssynonyms^created^7^4^3^
syssynonyms^tabid^2^4^4^
syssyntable^tabid^2^4^1^
syssyntable^servername^13^128^2^
syssyntable^dbname^13^128^3^
syssyntable^owner^0^32^4^
syssyntable^tabname^13^128^5^
syssyntable^btabid^2^4^6^
systabamdata^tabid^2^4^1^
systabamdata^am_param^0^256^2^
systabamdata^am_space^13^128^3^
systabauth^grantor^0^32^1^
systabauth^grantee^0^32^2^
systabauth^tabid^2^4^3^
systabauth^tabauth^0^9^4^
systables^tabname^13^128^1^
systables^owner^0^32^2^
systables^partnum^2^4^3^
systables^tabid^6^4^4^
systables^rowsize^1^2^5^
systables^ncols^1^2^6^
systables^nindexes^1^2^7^
systables^nrows^2^4^8^
systables^created^7^4^9^
systables^version^2^4^10^
systables^tabtype^0^1^11^
systables^locklevel^0^1^12^
systables^npused^2^4^13^
systables^fextsize^2^4^14^
systables^nextsize^2^4^15^
systables^flags^1^2^16^
systables^site^13^128^17^
systables^dbname^13^128^18^
systables^type_xid^2^4^19^
systables^am_id^2^4^20^
systraceclasses^name^0^18^1^
systraceclasses^classid^6^4^2^
systracemsgs^name^13^128^1^
systracemsgs^msgid^6^4^2^
systracemsgs^locale^0^36^3^
systracemsgs^seqno^1^2^4^
systracemsgs^message^13^255^5^
systrigbody^trigid^2^4^1^
systrigbody^datakey^0^1^2^
systrigbody^seqno^2^4^3^
systrigbody^data^0^256^4^
systriggers^trigid^6^4^1^
systriggers^trigname^13^128^2^
systriggers^owner^0^32^3^
systriggers^tabid^2^4^4^
systriggers^event^0^1^5^
systriggers^old^13^128^6^
systriggers^new^13^128^7^
systriggers^mode^0^1^8^
systriggers^collation^0^36^9^
sysusers^username^0^32^1^
sysusers^usertype^0^1^2^
sysusers^priority^1^2^3^
sysusers^password^0^16^4^
sysviews^tabid^2^4^1^
sysviews^seqno^1^2^2^
sysviews^viewtext^0^64^3^
sysviolations^targettid^2^4^1^
sysviolations^viotid^2^4^2^
sysviolations^diatid^2^4^3^
sysviolations^maxrows^2^4^4^
sysxtddesc^extended_id^2^4^1^
sysxtddesc^seqno^1^2^2^
sysxtddesc^description^0^256^3^
sysxtdtypeauth^grantor^0^32^1^
sysxtdtypeauth^grantee^0^32^2^
sysxtdtypeauth^type^2^4^3^
sysxtdtypeauth^auth^0^2^4^
sysxtdtypes^extended_id^6^4^1^
sysxtdtypes^domain^0^1^2^
sysxtdtypes^mode^0^1^3^
sysxtdtypes^owner^0^32^4^
sysxtdtypes^name^13^128^5^
sysxtdtypes^type^1^2^6^
sysxtdtypes^source^2^4^7^
sysxtdtypes^maxlen^2^4^8^
sysxtdtypes^length^2^4^9^
sysxtdtypes^byvalue^0^1^10^
sysxtdtypes^cannothash^0^1^11^
sysxtdtypes^align^1^2^12^
sysxtdtypes^locator^2^4^13^
@@ -0,0 +1,330 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
TYPE t_custrec RECORD
cust_id INTEGER,
cust_name VARCHAR(50),
cust_ts DATETIME YEAR TO SECOND,
cust_city VARCHAR(30),
cust_state CHAR(2),
cust_zipcode SMALLINT,
cust_address VARCHAR(100)
END RECORD
DEFINE custrec, old_custrec t_custrec
DEFINE custarr DYNAMIC ARRAY OF t_custrec
CONSTANT STATE_NAVIGATE = 0
CONSTANT STATE_MODIFY = 1
CONSTANT STATE_APPEND = 2
DEFINE cust_state SMALLINT
DEFINE last_row INTEGER
CONSTANT ctitle = "Customers"
MAIN
DEFINE r INTEGER
OPTIONS INPUT WRAP
OPEN FORM f1 FROM "CustListDetail"
DISPLAY FORM f1
CALL cust_loadlist(10)
DIALOG ATTRIBUTES(FIELD ORDER FORM, UNBUFFERED)
DISPLAY ARRAY custarr TO sr.*
BEFORE ROW
LET r = DIALOG.getCurrentRow("sr")
IF last_row != r THEN
IF NOT cust_check_changing(DIALOG) THEN
CALL DIALOG.setCurrentRow("sr",last_row)
CONTINUE DIALOG
END IF
CALL cust_getdetail(DIALOG, r)
END IF
AFTER ROW
LET last_row = DIALOG.getCurrentRow("sr")
END DISPLAY
INPUT custrec.* FROM cr.*
AFTER FIELD cust_address
IF NOT cust_validate_address(DIALOG) THEN NEXT FIELD CURRENT END IF
END INPUT
BEFORE DIALOG
LET r = DIALOG.getCurrentRow("sr")
LET last_row = r
CALL cust_getdetail(DIALOG, r)
ON ACTION dialogtouched
CALL cust_touched(DIALOG)
ON ACTION search
CALL cust_search(DIALOG)
ON ACTION undo -- VALIDATE=NO!
CALL cust_undo(DIALOG)
DISPLAY BY NAME custrec.* -- To sync current field value (validate=no)
ON ACTION save
IF NOT cust_save(DIALOG) THEN CONTINUE DIALOG END IF
ON ACTION append
IF NOT cust_validate(DIALOG) THEN CONTINUE DIALOG END IF
CALL cust_append(DIALOG)
LET last_row = DIALOG.getCurrentRow("sr")
NEXT FIELD cust_name
ON ACTION delete -- VALIDATE=NO!
CALL cust_delete(DIALOG)
DISPLAY BY NAME custrec.* -- To sync current field value (validate=no)
ON ACTION close -- VALIDATE=NO!
IF cust_check_changing(DIALOG) THEN EXIT DIALOG END IF
END DIALOG
END MAIN
FUNCTION cust_modifying()
RETURN cust_state != STATE_NAVIGATE
END FUNCTION
FUNCTION cust_validate_address(d)
DEFINE d ui.Dialog
IF d.getFieldTouched("cust_address") AND custrec.cust_address NOT MATCHES "*,*" THEN
ERROR "Address must contain a comma (',')"
RETURN FALSE
END IF
RETURN TRUE
END FUNCTION
FUNCTION cust_validate(d)
DEFINE d ui.Dialog
IF d.validate("cr.*") < 0 OR NOT cust_validate_address(d) THEN
RETURN FALSE
END IF
RETURN TRUE
END FUNCTION
FUNCTION cust_touched(d)
DEFINE d ui.Dialog
DEFINE r INTEGER
LET r = d.getCurrentRow("sr")
LET cust_state = STATE_MODIFY
LET custrec.cust_ts = CURRENT
LET custarr[r].cust_ts = custrec.cust_ts
CALL cust_setup(d)
END FUNCTION
FUNCTION cust_clean(d, r)
DEFINE d ui.Dialog
DEFINE r INTEGER
LET old_custrec.* = custrec.*
IF r>0 THEN
LET custarr[r].* = custrec.*
END IF
LET cust_state = STATE_NAVIGATE
CALL cust_setup(d)
END FUNCTION
FUNCTION cust_check_changing(d)
DEFINE d ui.Dialog
DEFINE r INTEGER
IF cust_modifying() THEN
CALL cust_setup(d)
LET r = mbox_ync(ctitle,"Do you want to save your changes?","question")
CASE r
WHEN 1 IF NOT cust_save(d) THEN LET r = 3 END IF
WHEN 2 CALL cust_undo(d)
END CASE
RETURN (r == 1 OR r == 2)
END IF
RETURN TRUE
END FUNCTION
FUNCTION cust_setup(d)
DEFINE d ui.Dialog
DEFINE modifying, r SMALLINT
LET modifying = cust_modifying()
CALL d.setActionActive("dialogtouched", NOT modifying)
CALL d.setActionActive("search", NOT modifying)
CALL d.setActionActive("undo", modifying)
CALL d.setActionActive("save", modifying)
LET r = (d.getArrayLength("sr")>0)
CALL d.setActionActive("delete", r)
CALL d.setFieldActive("cr.cust_name", r)
CALL d.setFieldActive("cr.cust_address", r)
CALL d.setFieldActive("cr.cust_zipcode", r)
CALL d.setFieldActive("cr.cust_state", r)
CALL d.setFieldActive("cr.cust_city", r)
-- FIXME: Missing CALL d.setFieldActive("cr.*", r)
END FUNCTION
FUNCTION cust_append(d)
DEFINE d ui.Dialog
DEFINE i INTEGER
IF NOT cust_check_changing(d) THEN RETURN END IF
IF custarr.getLength() = 1 AND custarr[1].cust_id IS NULL THEN
LET i = 1
ELSE
CALL d.appendRow("sr")
LET i = d.getArrayLength("sr")
END IF
INITIALIZE custrec.* TO NULL
LET custarr[i].cust_id = ( custrec.cust_id := 100 + i )
LET custarr[i].cust_name = ( custrec.cust_name := "Customer #"||custrec.cust_id )
LET custarr[i].cust_ts = ( custrec.cust_ts := CURRENT )
CALL d.setCurrentRow("sr",i)
LET cust_state = STATE_APPEND
CALL cust_setup(d)
END FUNCTION
FUNCTION cust_delete(d)
DEFINE d ui.Dialog
DEFINE l, r INTEGER
IF cust_modifying() THEN
CALL cust_setup(d)
IF NOT mbox_yn(ctitle,"Delete record and throw away your changes?","question") THEN
RETURN
END IF
ELSE
IF NOT mbox_yn(ctitle,"Delete current record?","question") THEN
RETURN
END IF
END IF
LET l = d.getArrayLength("sr")
IF l == 0 THEN RETURN END IF
LET r = d.getCurrentRow("sr")
CALL d.deleteRow("sr",r)
LET r = d.getCurrentRow("sr")
CALL cust_getdetail(d, r)
END FUNCTION
FUNCTION cust_undo(d)
DEFINE d ui.Dialog
DEFINE r INTEGER
-- Undo changes...
CASE cust_state
WHEN STATE_APPEND
CALL d.deleteRow("sr",last_row)
LET r = d.getCurrentRow("sr")
CALL cust_getdetail(d, r)
WHEN STATE_MODIFY
LET custrec.* = old_custrec.*
LET r = last_row
OTHERWISE
DISPLAY "Invalid state in cust_undo" EXIT PROGRAM 1
END CASE
CALL d.setFieldTouched("cr.*", FALSE)
CALL cust_clean(d, r)
END FUNCTION
FUNCTION cust_save(d)
DEFINE d ui.Dialog
IF NOT cust_validate(d) THEN RETURN FALSE END IF
CASE cust_state
WHEN STATE_APPEND
DISPLAY "Execute SQL INSERT statement..."
IF SQLCA.SQLCODE < 0 THEN RETURN FALSE END IF
WHEN STATE_MODIFY
DISPLAY "Execute SQL UPDATE statement..."
IF SQLCA.SQLCODE < 0 THEN RETURN FALSE END IF
OTHERWISE
DISPLAY "Invalid state in cust_save" EXIT PROGRAM 1
END CASE
CALL d.setFieldTouched("cr.*", FALSE)
CALL cust_clean(d, last_row)
RETURN TRUE
END FUNCTION
FUNCTION cust_search(d)
DEFINE d ui.Dialog
DEFINE cond STRING
IF NOT cust_check_changing(d) THEN RETURN END IF
CONSTRUCT BY NAME cond ON cust_id,
cust_name,
cust_city,
cust_state,
cust_zipcode,
cust_address
ATTRIBUTES(ACCEPT=FALSE,CANCEL=FALSE)
ON ACTION search_run
ACCEPT CONSTRUCT
ON ACTION search_cancel
EXIT CONSTRUCT
END CONSTRUCT
IF int_flag == FALSE THEN
MESSAGE cond
CALL cust_loadlist(length(cond) / 3)
CALL d.setCurrentRow("sr",1)
CALL cust_getdetail(d, 1)
END IF
END FUNCTION
-- Dummy function to fill the list of customers (normally, SELECT from database)
FUNCTION cust_loadlist(m)
DEFINE m,i INTEGER
CALL custarr.clear()
FOR i=1 TO m
CALL custarr.appendElement()
LET custarr[i].cust_id = i
LET custarr[i].cust_name = "Name " || i
LET custarr[i].cust_ts = CURRENT
IF i MOD 3 == 0 THEN
LET custarr[i].cust_city = "San Francisco"
LET custarr[i].cust_state = "CA"
LET custarr[i].cust_zipcode = 94102
LET custarr[i].cust_address = "24, Blue Lagon Street"
ELSE
LET custarr[i].cust_city = "Richmond"
LET custarr[i].cust_state = "VA"
LET custarr[i].cust_zipcode = 23219
LET custarr[i].cust_address = "45, Yellow Lake"
END IF
END FOR
END FUNCTION
-- Dummy function to get customer details (normally, SELECT from database)
FUNCTION cust_getdetail(d, r)
DEFINE d ui.Dialog
DEFINE r INTEGER
IF r <= 0 THEN
INITIALIZE custrec.* TO NULL
INITIALIZE old_custrec.* TO NULL
ELSE
LET custrec.* = custarr[r].*
END IF
CALL cust_clean(d, r)
END FUNCTION
FUNCTION mbox_yn(t, m, i)
DEFINE t,m,i STRING
DEFINE r INTEGER
MENU t ATTRIBUTES(STYLE="dialog",COMMENT=m,IMAGE=i)
COMMAND "Yes" LET r = TRUE EXIT MENU
COMMAND "No" LET r = FALSE EXIT MENU
END MENU
RETURN r
END FUNCTION
FUNCTION mbox_ync(t, m, i)
DEFINE t,m,i STRING
DEFINE r INTEGER
MENU t ATTRIBUTES(STYLE="dialog",COMMENT=m,IMAGE=i)
COMMAND "Yes" LET r = 1 EXIT MENU
COMMAND "No" LET r = 2 EXIT MENU
COMMAND "Cancel" LET r = 3 EXIT MENU
END MENU
RETURN r
END FUNCTION
@@ -0,0 +1,76 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
ACTION DEFAULTS
ACTION search (TEXT="Query",ACCELERATOR=control-q)
ACTION search_run (TEXT="Run query",ACCELERATOR=Enter,ACCELERATOR2=Return)
ACTION search_cancel (TEXT="Cancel query",ACCELERATOR=Escape,VALIDATE=NO)
ACTION undo (TEXT="Undo",ACCELERATOR=control-u,VALIDATE=NO)
ACTION save (TEXT="Save",ACCELERATOR=control-s)
ACTION close (DEFAULTVIEW=YES,VALIDATE=NO)
ACTION delete (VALIDATE=NO)
ACTION modify (TEXT="Modify",DEFAULTVIEW=YES)
END
LAYOUT ( TEXT = "List and Detail" )
GRID
{
<g g1 >
<t t1 >
Id Name Timestamp City State Zip code Address
[c1 |c2 |c3 |c4 |c5 |c6 |c7 ]
[c1 |c2 |c3 |c4 |c5 |c6 |c7 ]
[c1 |c2 |c3 |c4 |c5 |c6 |c7 ]
[c1 |c2 |c3 |c4 |c5 |c6 |c7 ]
[c1 |c2 |c3 |c4 |c5 |c6 |c7 ]
[c1 |c2 |c3 |c4 |c5 |c6 |c7 ]
< >
< >
<g g2 >
[f1 ] [f2 ]
City: [f4 ] TS: [f3 ]
State: [f5 ]
Zipcode: [f6 ]
Address: [f7 ]
[ ]
< >
}
END
END
ATTRIBUTES
TABLE t1:DOUBLECLICK=modify;
GROUP g1: TEXT = "Customer list";
c1 = FORMONLY.c_id TYPE INTEGER;
c2 = FORMONLY.c_name TYPE VARCHAR;
DATEEDIT c3 = FORMONLY.c_ts TYPE DATETIME YEAR TO SECOND, HIDDEN;
c4 = FORMONLY.c_city,SCROLL;
c5 = FORMONLY.c_state, HIDDEN=USER;
c6 = FORMONLY.c_zipcode, HIDDEN=USER;
c7 = FORMONLY.c_address, HIDDEN=USER;
GROUP g2: TEXT = "Details";
f1 = FORMONLY.cust_id TYPE INTEGER, NOENTRY ;
f2 = FORMONLY.cust_name TYPE VARCHAR, NOT NULL, REQUIRED;
LABEL f3 = FORMONLY.cust_ts TYPE DATETIME YEAR TO SECOND;
f4 = FORMONLY.cust_city TYPE CHAR, NOT NULL, REQUIRED;
COMBOBOX f5 = FORMONLY.cust_state TYPE VARCHAR, NOT NULL, REQUIRED,
QUERYEDITABLE, ITEMS=(("AK","Alaska"),("CA","California"),("VA","Virginia"));
f6 = FORMONLY.cust_zipcode TYPE VARCHAR,
INCLUDE=(10000 TO 99999),DEFAULT=11111,
COMMENT = "Must be a number between 10000 and 99999";
f7 = FORMONLY.cust_address TYPE VARCHAR,REQUIRED,SCROLL;
END
INSTRUCTIONS
SCREEN RECORD sr (FORMONLY.c_id THROUGH FORMONLY.c_address);
SCREEN RECORD cr (FORMONLY.cust_id THROUGH FORMONLY.cust_address);
END
@@ -0,0 +1,276 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
TYPE t_custrec RECORD
cust_id INTEGER,
cust_name VARCHAR(50),
cust_ts DATETIME YEAR TO SECOND,
cust_city VARCHAR(30),
cust_state CHAR(2),
cust_zipcode SMALLINT,
cust_address VARCHAR(100)
END RECORD
DEFINE custrec, old_custrec t_custrec
DEFINE custarr DYNAMIC ARRAY OF t_custrec
DEFINE myd ui.Dialog --module dialog variable to avoid passing DIALOG
DEFINE isNew INT -- if > 0 then we are in new state
DEFINE cust_touched INT
MAIN
DEFINE ret INT
DEFINE save_or_cancel STRING
OPTIONS INPUT WRAP
OPEN FORM f1 FROM "CustListDetail2"
DISPLAY FORM f1
CALL cust_loadlist(10)
DIALOG ATTRIBUTES(FIELD ORDER FORM, UNBUFFERED)
DISPLAY ARRAY custarr TO sr.*
BEFORE ROW
CALL cust_getdetail(arr_curr())
ON ACTION append
CALL cust_new()
ON ACTION search
CALL cust_search()
END DISPLAY
INPUT custrec.* FROM cr.* ATTRIBUTES(WITHOUT DEFAULTS)
BEFORE INPUT
CALL set_title()
CALL cust_setup(FALSE)
ON ACTION save
LET save_or_cancel="save"
ACCEPT DIALOG
AFTER FIELD cust_address
IF NOT custrec.cust_address MATCHES "*,*" THEN
ERROR "Address must contain a comma (',')"
LET save_or_cancel=NULL; NEXT FIELD CURRENT
END IF
AFTER INPUT
--do the checking if the record has to be saved
IF cust_touched THEN
LET ret=save_or_cancel IS NOT NULL
IF NOT ret THEN
LET ret = mbox_ync("Customers","Do you want to save your changes?","question")
END IF
CASE ret
WHEN 1 --yes
IF NOT cust_save() THEN
LET save_or_cancel=NULL; NEXT FIELD CURRENT
END IF
CALL DIALOG.setFieldTouched("cr.*",0)
LET custarr[myd.getCurrentRow("sr")].* = custrec.*
WHEN 3 --cancel
LET save_or_cancel=NULL; NEXT FIELD CURRENT
END CASE
END IF
LET isNew=0
CALL set_title()
CALL cust_setup(0)
CASE save_or_cancel
WHEN "save" --stay in the dialog
LET save_or_cancel=NULL; NEXT FIELD CURRENT
WHEN "cancel" --exit the dialog
EXIT DIALOG
END CASE
END INPUT
BEFORE DIALOG
LET myd=DIALOG
CALL set_title()
CALL cust_setup(FALSE)
IF custarr.getLength()=0 THEN
CALL cust_new() --must be in a valid state if array is empty
END IF
ON ACTION dialogtouched
CALL cust_setup(TRUE)
ON ACTION undo
LET custrec.* = old_custrec.*
DISPLAY BY NAME custrec.*
CALL myd.setFieldTouched("cr.*",FALSE)
CALL cust_setup(FALSE)
ON ACTION close
IF NOT cust_touched THEN EXIT DIALOG END IF
--some extra code to ask back
CASE mbox_ync("Customers","Do you want to save your changes?","question")
WHEN 1 --yes
LET save_or_cancel="cancel"
ACCEPT DIALOG
WHEN 2 --no
EXIT DIALOG
END CASE
ON ACTION delete
IF custarr.getLength()>0 THEN
IF mbox_yn("Customers", "Delete current record?", "question") THEN
CALL cust_delete(DIALOG.getCurrentRow("sr"))
IF custarr.getLength()=0 THEN
CALL cust_new()
END IF
CALL cust_setup(FALSE)
END IF
END IF
END DIALOG
LET myd=NULL
END MAIN
FUNCTION cust_setup(touched)
DEFINE touched INT
LET cust_touched=touched
CALL myd.setActionActive("dialogtouched", NOT touched)
CALL myd.setActionActive("undo", touched)
TRY --we might not be in the INPUT context
CALL myd.setActionActive("save", touched)
END TRY
END FUNCTION
FUNCTION cust_new()
LET isNew=1
CALL cust_setup(0)
--set the dialog in virgin state and reload the defaults
INITIALIZE custrec TO NULL
CALL myd.setFieldTouched("cr.*",FALSE)
--jump to1st field of input
CALL myd.nextField("cust_name")
CALL myd.appendRow("sr")
CALL myd.setCurrentRow("sr",custarr.getLength())
CALL set_title()
END FUNCTION
FUNCTION cust_search()
DEFINE cond STRING
LET int_flag=FALSE
CONSTRUCT BY NAME cond ON cust_id,
cust_name,
cust_city,
cust_state,
cust_zipcode,
cust_address
IF int_flag == FALSE THEN
MESSAGE cond
CALL cust_loadlist(length(cond) / 3)
CALL myd.setCurrentRow("sr",1)
END IF
END FUNCTION
FUNCTION cust_delete(currRow)
DEFINE currRow INTEGER
DEFINE len INT
LET len=custarr.getLength()
CALL myd.deleteRow("sr",currRow)
LET isNew=0
IF currRow > len-1 THEN
LET currRow=len-1
CALL myd.setCurrentRow("sr",currRow)
END IF
CALL cust_getdetail(currRow)
END FUNCTION
FUNCTION cust_save()
IF isNew>0 THEN
DISPLAY "Execute SQL INSERT statement..."
ELSE
DISPLAY "Execute SQL UPDATE statement..."
END IF
IF custrec.cust_name="err" THEN --IF status...
--simulate a database error
ERROR "SQL ERROR"
RETURN FALSE
END IF
RETURN TRUE
END FUNCTION
-- Dummy function to fill the list of customers (normally, SELECT from database)
FUNCTION cust_loadlist(m)
DEFINE m,i INTEGER
CALL custarr.clear()
FOR i=1 TO m
CALL custarr.appendElement()
LET custarr[i].cust_id = i
LET custarr[i].cust_name = "Name " || i
LET custarr[i].cust_ts=CURRENT
IF i MOD 3 == 0 THEN
LET custarr[i].cust_city = "San Francisco"
LET custarr[i].cust_state = "CA"
LET custarr[i].cust_zipcode = 23219
LET custarr[i].cust_address = "24, Blue Lagon Street"
ELSE
LET custarr[i].cust_city = "Richmond"
LET custarr[i].cust_state = "CA"
LET custarr[i].cust_zipcode = 12345
LET custarr[i].cust_address = "45, Yellow Lake"
END IF
END FOR
END FUNCTION
-- Dummy function to get customer details (normally, SELECT from database)
FUNCTION cust_getdetail(row)
DEFINE row INTEGER
CALL set_title()
CALL cust_setup(0)
IF row=0 THEN
INITIALIZE old_custrec.* TO NULL
CLEAR cr.*
RETURN
END IF
LET custrec.* = custarr[row].*
LET old_custrec.* = custarr[row].*
DISPLAY BY NAME custrec.*
CALL myd.setFieldTouched("cr.*",0)
END FUNCTION
FUNCTION set_title()
DEFINE row INT
LET row=myd.getCurrentRow("sr")
IF myd.getCurrentItem()="sr" THEN
CALL fgl_settitle(sfmt("Record %1 of %2",row,custarr.getLength()))
ELSE IF isNew>0 THEN
CALL fgl_settitle("Input new customer")
ELSE
CALL fgl_settitle(sfmt("Modify Record %1 of %2",row,custarr.getLength()))
END IF
END IF
END FUNCTION
FUNCTION mbox_yn(t, m, i)
DEFINE t,m,i STRING
DEFINE r INTEGER
MENU t ATTRIBUTES(STYLE="dialog",COMMENT=m,IMAGE=i)
COMMAND "Yes" LET r = TRUE EXIT MENU
COMMAND "No" LET r = FALSE EXIT MENU
END MENU
RETURN r
END FUNCTION
FUNCTION mbox_ync(t, m, i)
DEFINE t,m,i STRING
DEFINE r INTEGER
MENU t ATTRIBUTES(STYLE="dialog",COMMENT=m,IMAGE=i)
COMMAND "Yes" LET r = 1 EXIT MENU
COMMAND "No" LET r = 2 EXIT MENU
COMMAND "Cancel" LET r = 3 EXIT MENU
END MENU
RETURN r
END FUNCTION
@@ -0,0 +1,74 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
ACTION DEFAULTS
ACTION close (VALIDATE=NO,DEFAULTVIEW=yes)
ACTION delete (VALIDATE=NO)
ACTION cancel (VALIDATE=NO)
ACTION append (IMAGE="new")
ACTION undo (TEXT="Undo",VALIDATE=NO)
ACTION save (TEXT="Save")
END
LAYOUT ( TEXT = "List and Detail" )
VBOX
GROUP (TEXT="Customer List")
TABLE (DOUBLECLICK=modify,HEIGHT=6 LINES)
{
Id Name Timestamp City State Zip code Address
[c1 |c2 |c3 |c4 |c5 |c6 |c7 ]
}
END
END
GROUP (TEXT="Details")
GRID
{
Id [f1 ]
Name [f2 ]
City [f4 ] TS: [f3 ]
State [f5 ]
Zipcode[f6 ]
Address[f7 ]
[ :bsearch :bsave : bnew :bdel ]
}
END
END
END
ATTRIBUTES
c1 = FORMONLY.c_id TYPE INTEGER;
c2 = FORMONLY.c_name TYPE VARCHAR;
DATEEDIT c3 = FORMONLY.c_ts TYPE DATETIME YEAR TO SECOND, HIDDEN;
c4 = FORMONLY.c_city,SCROLL;
c5 = FORMONLY.c_state, HIDDEN = USER;
c6 = FORMONLY.c_zipcode, HIDDEN = USER;
c7 = FORMONLY.c_address, HIDDEN = USER;
f1 = FORMONLY.cust_id TYPE INTEGER, NOENTRY ;
f2 = FORMONLY.cust_name TYPE VARCHAR, NOT NULL, REQUIRED,SCROLL;
LABEL f3 = FORMONLY.cust_ts TYPE DATETIME YEAR TO SECOND;
f4 = FORMONLY.cust_city TYPE CHAR, NOT NULL, REQUIRED;
COMBOBOX f5 = FORMONLY.cust_state TYPE VARCHAR, NOT NULL, REQUIRED,
QUERYEDITABLE, ITEMS=(("AK","Alaska"),("CA","California"),("VA","Virginia"));
f6 = FORMONLY.cust_zipcode TYPE VARCHAR,
INCLUDE=(10000 TO 99999),DEFAULT=11111,
COMMENT = "Must be a number between 10000 and 99999";
f7 = FORMONLY.cust_address TYPE VARCHAR,REQUIRED,SCROLL;
BUTTON bsearch:sr.search,TEXT="Search",IMAGE="find";
BUTTON bsave:save,TEXT="Save",IMAGE="disk";
BUTTON bnew:sr.append,TEXT="New",IMAGE="new";
BUTTON bdel:delete,TEXT="Del",IMAGE="delete";
END
INSTRUCTIONS
SCREEN RECORD sr (FORMONLY.c_id THROUGH FORMONLY.c_address);
SCREEN RECORD cr (FORMONLY.cust_id THROUGH FORMONLY.cust_address);
END
@@ -0,0 +1,223 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
TYPE t_custrec RECORD
cust_id INTEGER,
cust_name VARCHAR(50),
cust_ts DATETIME YEAR TO SECOND,
cust_city VARCHAR(30),
cust_state CHAR(2),
cust_zipcode VARCHAR(6),
cust_address VARCHAR(100)
END RECORD
DEFINE custarr,carr,oldarr DYNAMIC ARRAY OF t_custrec
DEFINE myd ui.Dialog --module dialog variable to avoid passing DIALOG
DEFINE cust_touched INT
MAIN
DEFINE i INT
OPTIONS INPUT WRAP
OPEN FORM f1 FROM "CustListDetail3"
DISPLAY FORM f1
CALL cust_search()
FOR i=1 TO custarr.getLength()
LET oldarr[i].*=custarr[i].*
END FOR
DIALOG ATTRIBUTES(UNBUFFERED)
DISPLAY ARRAY custarr TO sr.*
BEFORE ROW
DISPLAY "BEFORE ROW sr:", sr_arr_curr()
IF sr_arr_curr()==0 THEN --input new record
NEXT FIELD cr.cust_name
ELSE --sync the other array
CALL myd.setCurrentRow("cr",sr_arr_curr())
CALL fgl_settitle(sfmt("Record %1 of %2",sr_arr_curr(),custarr.getLength()))
END IF
END DISPLAY
INPUT ARRAY carr FROM cr.* ATTRIBUTES(INSERT ROW=FALSE)
BEFORE ROW --sync the other array
DISPLAY "BEFORE ROW cr:", cr_arr_curr(),cr_arr_count()
CALL DIALOG.setCurrentRow("sr",cr_arr_curr())
CALL fgl_settitle(sfmt("Modify Record %1 of %2",cr_arr_curr(),custarr.getLength()))
AFTER FIELD cust_address
IF NOT carr[cr_arr_curr()].cust_address MATCHES "*,*" THEN
ERROR "Address must contain a comma (',')"
NEXT FIELD CURRENT
END IF
AFTER DELETE --delete the row in the other array
CALL DIALOG.deleteRow("sr",cr_arr_curr())
BEFORE INSERT --append the row in the other array
DISPLAY "BEFORE INSERT cr_arr_curr:",cr_arr_curr(),cr_arr_count()
CALL DIALOG.appendRow("sr")
CALL DIALOG.setCurrentRow("sr",cr_arr_curr())
CALL fgl_settitle("Input new customer")
AFTER ROW
DISPLAY "AFTER ROW cr:",cr_arr_curr(),cr_arr_count()
IF cr_arr_curr()>cr_arr_count() THEN --delete temp row
CALL DIALOG.deleteRow("sr",cr_arr_curr())
ELSE
IF FIELD_TOUCHED(cr.*) THEN --do your row checking here
LET cust_touched=1
LET custarr[cr_arr_curr()].*=carr[cr_arr_curr()].*
END IF
END IF
END INPUT
BEFORE DIALOG
LET cust_touched=0
LET myd=DIALOG
ON ACTION accept
ACCEPT DIALOG
ON ACTION cancel
IF (NOT cust_touched) AND (NOT FIELD_TOUCHED(cr.*)) THEN
EXIT DIALOG
END IF
--some extra code to ask back
IF mbox_yn("Customers", "Cancel without saving?", "question") THEN
EXIT DIALOG
END IF
AFTER DIALOG
DISPLAY "AFTER DIALOG"
--BEGIN WORK
IF (i:=cust_save())<>0 THEN
--ROLLBACK WORK
--FIXME: where is the function? CALL mbox_ok("Error","Could not update/insert customer","stop")
CALL myd.setCurrentRow("cr",i)
NEXT FIELD cr.cust_name
END IF
--COMMIT WORK
END DIALOG
LET myd=NULL
END MAIN
--helpers to prevent writing too much...
FUNCTION cr_arr_curr() RETURN myd.getCurrentRow("cr") END FUNCTION
FUNCTION cr_arr_count() RETURN myd.getArrayLength("cr") END FUNCTION
FUNCTION sr_arr_curr() RETURN myd.getCurrentRow("sr") END FUNCTION
FUNCTION cust_search()
DEFINE cond STRING
LET int_flag=FALSE
CALL fgl_settitle("Input search criteria and press <Enter>")
CONSTRUCT BY NAME cond ON cust_id,
cust_name,
cust_city,
cust_state,
cust_zipcode,
cust_address
IF int_flag == FALSE THEN
MESSAGE cond
CALL cust_loadlist(10)
END IF
END FUNCTION
--compare custarr with the oldarr and make the necessary updates/inserts/deletes
FUNCTION cust_save()
DEFINE i,j,oldlen INT
DEFINE oldarr2 DYNAMIC ARRAY OF t_custrec --make another copy for multiple trials
LET oldlen=oldarr.getLength()
FOR i=1 TO oldlen
LET oldarr2[i].*=oldarr[i].*
END FOR
FOR i=1 TO custarr.getLength()
IF custarr[i].cust_id>0 THEN --existing customer
FOR j=1 TO oldlen
IF custarr[i].cust_id=oldarr2[j].cust_id THEN
IF custarr[i].* <> oldarr2[j].* THEN --the record has been changed
DISPLAY "UPDATE cust_id:",custarr[i].cust_id,",",custarr[i].cust_name
--UPDATE customer where... WHERE cust_id=custarr[i].cust_id
END IF
CALL oldarr2.deleteElement(j)
LET oldlen = oldlen - 1
EXIT FOR
END IF
END FOR
ELSE
DISPLAY "insert customer name:",custarr[i].cust_name
--INSERT INTO customer ...
END IF
IF custarr[i].cust_name="err" THEN --simulate database error
RETURN i
END IF
END FOR
--remove remaining oldarr2 items
FOR i=1 TO oldlen
DISPLAY "DELETE cust_id:",oldarr2[i].cust_id,",",oldarr2[i].cust_name
--DELETE FROM customer WHERE cust_id=oldarr[i].cust_id
END FOR
RETURN 0
END FUNCTION
-- Dummy function to fill the list of customers (normally, SELECT from database)
FUNCTION cust_loadlist(m)
DEFINE m,i INTEGER
CALL custarr.clear()
FOR i=1 TO m
CALL custarr.appendElement()
LET custarr[i].cust_id = i
LET custarr[i].cust_name = "Name " || i
LET custarr[i].cust_ts=CURRENT
IF i MOD 3 == 0 THEN
LET custarr[i].cust_city = "San Francisco"
LET custarr[i].cust_state = "CA"
LET custarr[i].cust_zipcode = "76231"
LET custarr[i].cust_address = "24, Blue Lagon Street"
ELSE
LET custarr[i].cust_city = "Richmond"
LET custarr[i].cust_state = "CA"
LET custarr[i].cust_zipcode = "71212"
LET custarr[i].cust_address = "45, Yellow Lake"
END IF
END FOR
FOR i=1 TO custarr.getLength()
LET carr[i].*=custarr[i].*
END FOR
END FUNCTION
FUNCTION mbox_yn(t, m, i)
DEFINE t,m,i STRING
DEFINE r INTEGER
MENU t ATTRIBUTES(STYLE="dialog",COMMENT=m,IMAGE=i)
COMMAND "Yes" LET r = TRUE EXIT MENU
COMMAND "No" LET r = FALSE EXIT MENU
END MENU
RETURN r
END FUNCTION
FUNCTION mbox_ync(t, m, i)
DEFINE t,m,i STRING
DEFINE r INTEGER
MENU t ATTRIBUTES(STYLE="dialog",COMMENT=m,IMAGE=i)
COMMAND "Yes" LET r = 1 EXIT MENU
COMMAND "No" LET r = 2 EXIT MENU
COMMAND "Cancel" LET r = 3 EXIT MENU
END MENU
RETURN r
END FUNCTION
@@ -0,0 +1,80 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
ACTION DEFAULTS
ACTION close (VALIDATE=NO)
ACTION cancel (VALIDATE=NO)
ACTION append (IMAGE="new")
ACTION undo (TEXT="Undo",VALIDATE=NO)
END
TOOLBAR
ITEM close
END
LAYOUT ( TEXT = "List and Detail" )
GRID
{
<g g1 >
<t t1 >
Id Name Timestamp City State Zip code Address
[c1 |c2 |c3 |c4 |c5 |c6 |c7 ]
[c1 |c2 |c3 |c4 |c5 |c6 |c7 ]
[c1 |c2 |c3 |c4 |c5 |c6 |c7 ]
[c1 |c2 |c3 |c4 |c5 |c6 |c7 ]
[c1 |c2 |c3 |c4 |c5 |c6 |c7 ]
[c1 |c2 |c3 |c4 |c5 |c6 |c7 ]
< >
<g g2 >
Id [f1 ]
Name [f2 ]
City [f4 ] TS: [f3 ]
State [f5 ]
Zipcode[f6 ]
Address[f7 ]
[ :bf :bp :bn :bl : bnew :bdel ]
< >
}
END
END
ATTRIBUTES
GROUP g1: TEXT = "Customer list";
c1 = FORMONLY.c_id TYPE INTEGER;
c2 = FORMONLY.c_name TYPE VARCHAR;
DATEEDIT c3 = FORMONLY.c_ts TYPE DATETIME YEAR TO SECOND, HIDDEN;
c4 = FORMONLY.c_city,SCROLL;
c5 = FORMONLY.c_state;
c6 = FORMONLY.c_zipcode;
c7 = FORMONLY.c_address;
GROUP g2: TEXT = "Details";
f1 = FORMONLY.cust_id TYPE INTEGER, NOENTRY,DEFAULT=0 ;
f2 = FORMONLY.cust_name TYPE VARCHAR, NOT NULL, REQUIRED,SCROLL;
LABEL f3 = FORMONLY.cust_ts TYPE DATETIME YEAR TO SECOND;
f4 = FORMONLY.cust_city TYPE CHAR, NOT NULL, REQUIRED;
COMBOBOX f5 = FORMONLY.cust_state TYPE VARCHAR, NOT NULL, REQUIRED,
QUERYEDITABLE, ITEMS=(("AK","Alaska"),("CA","California"),("VA","Virginia"));
f6 = FORMONLY.cust_zipcode TYPE VARCHAR,
INCLUDE=(10000 TO 99999),DEFAULT=11111,
COMMENT = "Must be a number between 10000 and 99999";
f7 = FORMONLY.cust_address TYPE VARCHAR,REQUIRED,SCROLL;
BUTTON bf:firstrow;
BUTTON bp:prevrow;
BUTTON bn:nextrow;
BUTTON bl:lastrow;
BUTTON bnew:cr.append,TEXT="New",IMAGE="new";
BUTTON bdel:cr.delete,TEXT="Del",IMAGE="delete";
END
INSTRUCTIONS
SCREEN RECORD sr (FORMONLY.c_id THROUGH FORMONLY.c_address);
SCREEN RECORD cr (FORMONLY.cust_id THROUGH FORMONLY.cust_address);
END
@@ -0,0 +1,175 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
TYPE t_custrec RECORD
cust_id INTEGER,
cust_name VARCHAR(50),
cust_ts DATETIME YEAR TO SECOND,
cust_city VARCHAR(30),
cust_state CHAR(2),
cust_zipcode VARCHAR(6),
cust_address VARCHAR(100)
END RECORD
--normally this is a DEFINE custarr DYNAMIC ARRAY OF RECORD LIKE customer.*
DEFINE custarr DYNAMIC ARRAY OF t_custrec
DEFINE myd ui.DIALOG
MAIN
DEFINE currRow INTEGER
OPEN FORM f1 FROM "CustListDetail"
DISPLAY FORM f1
CALL cust_loadlist(10)
DISPLAY ARRAY custarr TO sr.* ATTRIBUTES(UNBUFFERED,ACCEPT=FALSE)
BEFORE DISPLAY
LET myd=DIALOG
BEFORE ROW
CALL cust_getdetail(arr_curr())
ON ACTION search
CALL cust_search()
ON ACTION modify
CALL cust_modify(0,arr_curr()) RETURNING currRow
ON ACTION append
CALL fgl_set_arr_curr(cust_modify(1,arr_curr()))
ON ACTION delete
IF custarr.getLength()>0 THEN
IF mbox_yn("Customers", "Do you want to delete the current customer?", "question") THEN
CALL cust_delete(arr_curr())
END IF
END IF
END DISPLAY
LET myd=NULL
END MAIN
FUNCTION cust_modify(new,i)
DEFINE new INT
DEFINE i INT
DEFINE myrec t_custrec
IF new THEN
CALL fgl_settitle("Input New Customer")
ELSE
CALL fgl_settitle("Modify Customer")
LET myrec.*=custarr[i].*
END IF
LET int_flag=FALSE
INPUT BY NAME myrec.* ATTRIBUTES(UNBUFFERED,WITHOUT DEFAULTS=NOT new)
AFTER FIELD cust_address
IF NOT myrec.cust_address MATCHES "*,*" THEN
ERROR "Address must contain a comma (',')"
NEXT FIELD CURRENT
END IF
END INPUT
IF int_flag=FALSE THEN
IF new THEN
LET i=custarr.getLength()+1
LET myrec.cust_id=100+i
DISPLAY "INSERT INTO database: id:",myrec.cust_id,",name:",myrec.cust_name
ELSE
DISPLAY "UPDATE DATABASE: id:",myrec.cust_id,",name:",myrec.cust_name
END IF
LET custarr[i].* = myrec.*
DISPLAY BY NAME custarr[i].*
END IF
CALL cust_getdetail(i)
RETURN i
END FUNCTION
FUNCTION cust_delete(currRow)
DEFINE currRow INTEGER
DEFINE len INT
LET len=custarr.getLength()
CALL custarr.deleteElement(currRow)
IF currRow > len-1 THEN
LET currRow=len-1
END IF
CALL cust_getdetail(currRow)
END FUNCTION
FUNCTION cust_search()
DEFINE cond STRING
LET int_flag=FALSE
CALL fgl_settitle("Search Customer")
CONSTRUCT BY NAME cond ON cust_id,
cust_name,
cust_city,
cust_state,
cust_zipcode,
cust_address
IF int_flag == FALSE THEN
MESSAGE cond
CALL cust_loadlist(length(cond) / 3)
CALL cust_getdetail(arr_curr())
END IF
END FUNCTION
-- Dummy function to fill the list of customers (normally, SELECT from database)
FUNCTION cust_loadlist(m)
DEFINE m,i INTEGER
CALL custarr.clear()
FOR i=1 TO m
CALL custarr.appendElement()
LET custarr[i].cust_id = i
LET custarr[i].cust_name = "Name " || i
LET custarr[i].cust_ts = CURRENT
IF i MOD 3 == 0 THEN
LET custarr[i].cust_city = "San Francisco"
LET custarr[i].cust_state = "CA"
LET custarr[i].cust_zipcode = 94102
LET custarr[i].cust_address = "24, Blue Lagon Street"
ELSE
LET custarr[i].cust_city = "Richmond"
LET custarr[i].cust_state = "VA"
LET custarr[i].cust_zipcode = 23219
LET custarr[i].cust_address = "45, Yellow Lake"
END IF
END FOR
END FUNCTION
-- Dummy function to get customer details (normally, SELECT from database)
FUNCTION cust_getdetail(row)
DEFINE row INTEGER
CALL fgl_settitle(sfmt("Record %1 of %2",row,custarr.getLength()))
CALL myd.setActionActive("delete",custarr.getLength()>0)
CALL myd.setActionActive("modify",custarr.getLength()>0)
IF row <= 0 THEN
CLEAR cr.*
RETURN
END IF
DISPLAY custarr[row].* TO cr.*
END FUNCTION
FUNCTION mbox_yn(t, m, i)
DEFINE t,m,i STRING
DEFINE r INTEGER
MENU t ATTRIBUTES(STYLE="dialog",COMMENT=m,IMAGE=i)
COMMAND "Yes" LET r = TRUE EXIT MENU
COMMAND "No" LET r = FALSE EXIT MENU
END MENU
RETURN r
END FUNCTION
FUNCTION mbox_ync(t, m, i)
DEFINE t,m,i STRING
DEFINE r INTEGER
MENU t ATTRIBUTES(STYLE="dialog",COMMENT=m,IMAGE=i)
COMMAND "Yes" LET r = 1 EXIT MENU
COMMAND "No" LET r = 2 EXIT MENU
COMMAND "Cancel" LET r = 3 EXIT MENU
END MENU
RETURN r
END FUNCTION
@@ -0,0 +1,19 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
MODULES = \
CustListDetail.42m\
CustListDetail_old.42m\
CustListDetail2.42m\
CustListDetail3.42m
FORMS = CustListDetail.42f\
CustListDetail_old.42m\
CustListDetail2.42f\
CustListDetail3.42f
include ../../Makeincl
@@ -0,0 +1,107 @@
Editable Customer List
-----------------------
This directory contains some trials to solve the following task:
A database table should be displayed and it should be possible to simultaneously edit the highlighted table record in a detail input.
It should be possible to edit/delete records , append new records and limit the displayed amount of data by calling a filter dialog.
This sounds like an easy game for multiple dialogs, but it turned out to be a bit harder than expected.
First lets inspect a sample implementing this task without multiple dialogs.
CustListDetail_old.4gl:
-----------------------
Browses the table in a DISPLAY ARRAY and shows simultaneously the details in another screen record, but to edit the record one has to double-click the highlighted line or to press the "Modify" button. To append a new record in the database, one has to press the "Append" button. The title of the window is set to assist the end user in which mode the program is. The code to do that is pretty short compared to the multiple dialogs samples.
Improved with multiple dialogs?
------------------------------
Now when starting CustListDetail or CustListDetail2 they both show nearly the same behavior:
It's possible to click directly into the record INPUT for the selected line in the DISPLAY ARRAY.
In the moment if we start to edit and change the current record we immediately run into a serious validation problem: some of our actions require the validation of the current record before we can continue with the intended action.
Example:
Change something in the current Record and pressing the 'Append' button:
In this moment you will be asked if you want to save your changes (CustListDetail) and after the confirmation the INPUT is cleared and you can enter new data.
The code to do that isn't that hard but you will have to perform the same steps which would have been done automatically for you if you would have left the dialog with ACCEPT DIALOG:
v1: Verify if the field constraints are met (by calling the new DIALOG.validate() method).
v2: Call the code which is called in the AFTER FIELD trigger of the field you are currently inside.
v3: Call the code which is called in the AFTER INPUT.
Especially v2 is hard to solve if you have a lot of AFTER FIELD clauses, then you will either have to check with
IF INFIELD(a) THEN CALL after_field(a) END IF
or you check anyway all your AFTER FIELD constraints in each action which is type of ACCEPT DIALOG.
In the current sample it is sufficient to call the check code for AFTER FIELD address (done in CustListDetail.4gl in 'cust_validate_address' and in CustListDetail2 directly in AFTER FIELD address)
The validation steps have to be invoked for each action in the multiple dialog which require it: 'Append', 'Query' (we want to have saved correctly before we refill the list with data) and 'Save'.
The 'Save' button is optional, normally we don't need it because we ask in each possible step for confirmation, but it might help to assist end users which miss the 'Ok' button.
If we click into the DISPLAY ARRAY, MD helps us by automatically invoking the AFTER FIELD and AFTER INPUT of the INPUT sub dialog.
CustListDetail.4gl :
--------------------
The 'hard' way:
Solves the validation problems described above by calling a validation procedure ( cust_validate() ) which makes all the necessary checks. If an AFTER FIELD constraint is added later to a particular field, it must be called also in this validation procedure. The code to handle the actions 'append' and 'search' need to call the validation explicitly before performing the indended action code. The code for 'save' is similar: it must als check the validation before.
The sample shows also how to use the new 'dialogtouched' action, however this action is not limited to Multiple Dialogs, it is also possible to use it in traditional 4GL dialogs.
The wish to use 'dialogtouched' is driven to enable/disable the 'Save' action depending on changes in the current record: Because there is no clear Ok/Cancel dialog, it's adds more visibility to the current input state.
Note 'dialogtouched' in a traditional INPUT with Ok/Cancel could be used to enable the 'Ok' button only if something has been changed.
CustListdetail2.4gl
-------------------
The sub dialog action way:
Uses actions dedicated to the sub dialog to surround the need for multiple validation checkings.
The 'append' and the 'search' actions are inside the DISPLAY ARRAY statement.
This means normally that this actions are only shown in the Action Panel if the focus is inside the DISPLAY ARRAY . If the focus is inside the DISPLAY ARRAY we went already thru the validation in the AFTER INPUT and hence have no need to check again: all validations for the INPUT sub dialog have been done already.
However there is the possility to make those actions always visible by fully qualifying them as BUTTON's inside the .per file, the end user does not see that this action is a sub dialog action:
BUTTON bnew:sr.append,TEXT="New",IMAGE="new";
the "sr.append" name means:display the append action for the sub dialog which handles the "sr" screen record (which is the DISPLAY ARRAY). This fully qualified action name instructs the run time to make the action always visible and do the following if one clicks on it:
If the focus is not inside the DISPLAY ARRAY,move the focus to the display array and perform the validation of the current sub dialog (the INPUT in this case).
Then the action code is performed.
This is exactly the case we need: our 'append'/'search' action code should be only performed if the INPUT is valid. By using this technique we achieve that.
If the focus is already inside the DISPLAY, the code for the action is immediately executed:no need to validate the INPUT, ist must have been validated earlier in the AFTER INPUT.
CustListDetail3.4gl
-------------------
The 3rd sample works like a traditional INPUT ARRAY, it has an OK/Cancel button and all the database work is assumed to take place after Ok has been clicked.
It also uses a single row INPUT ARRAY in place of the INPUT of sample1/sample2 showing that one can again save code if he uses the INPUT ARRAY.
Advantage:
-no confirmations inbetween the rows.
-Database stuff in 1 transaction possible
Disadvantages:
-some more logic at the end needed to delete/insert/update rows from the database
-The sample also omit's the 'search' action because a search must be done then before calling this dialog.
Further caveeat
---------------
Both multiple dialogs using INPUT samples have another caveeat:
The DIALOG is not ready yet to toggle sub dialogs between the 2 modes WITHOUT DEFAULTS and without WITHOUT DEFAULTS dynamically.
Typically in the CustListDetail_old sample the INPUT is called with
INPUT ATTRIBUTES(WITHOUT DEFAULTS=NOT new)
and in the new mode (WITHOUT DEFAULTS=FALSE) the DEFAULT values are load from the .per definition and the REQUIRED attribute is taken into account.
In the multiple dialog sample we can only use WITHOUT DEFAULTS=TRUE because we 'stay' in the INPUT if the record is changed and have yet no dynamic method available to put the INPUT into 'virgin' state if we press the 'Append' Button.
(Writing WITHOUT DEFAULTS=FALSE works but of cause only once for the first record displayed in the program and will be immediately overwritten by the first BEFORE ROW code of the DISPLAY ARRAY)
That's why the REQUIRED and DEFAULT attributes are not usable in this case, which raises again problems:
REQUIRED is used to force a user to visit a field in WITHOUT DEFAULTS=FALSE mode to ensure that an AFTER FIELD trigger is called.
This mechanism is not usable with WITHOUT DEFAULTS=TRUE, instead one has to set the NOT NULL constraint together with a NULL value as default to force an end user to visit the field initially.
Note:
This only arises in the situation that we use the multiple dialog for inputting several records in one INPUT , as long as you use the multiple dialog as a typical Ok/Cancel dialog like shown in other samples (../Folder) the WITHOUT DEFAULTS mechanism works like expected.
Conclusion
----------
To make the wanted visual behavior happen you have a lot of possibilities
1. Calling all validations 'manually' for each action needing the validation for the current sub dialog(CustListDetail.4gl). This forces you to make extra functions for *each* AFTER FIELD check in your INPUT!
As already said, this is the 'hard' way and is also not looking intuitive, we cannot recommend it. Use one of the next techniques to make your life easier.
2. using sub dialog actions local to the DISPLAY ARRAY to let the DIALOG doing the validation for you . You need to get a bit experience how to fully qualify the sub dialog local action in the .per to let this action be active in each place of the DIALOG.
(CustListDetail2.4gl)
3. Synchronizing an INPUT ARRAY with a DISPLAY ARRAY to save code lines and get 'append'/'delete' for free
In case 1.) case you will have to write a lot more code than in the original sample ,in case 2. and 3.) you must know about the possibilies in using sub dialog local actions for achieving the necessary validations.
+762
View File
@@ -0,0 +1,762 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
IMPORT os
IMPORT util
IMPORT FGL MsgBox
DEFINE charset STRING
DEFINE boundary STRING
DEFINE rv INTEGER
DEFINE smtplog base.StringBuffer
DEFINE logtext STRING
DEFINE tz STRING
DEFINE ext2mime DYNAMIC ARRAY OF RECORD
ext STRING,
mime STRING,
encoding STRING
END RECORD
&define EXT2MIME(a, b, c) \
LET ext2mime[nmime].ext = a \
LET ext2mime[nmime].mime = b \
LET ext2mime[nmime].encoding = c \
LET nmime = nmime + 1
TYPE cnf_t RECORD
hostname STRING,
smtphost STRING,
smtpport INTEGER,
senderemail STRING,
cnxtimeout INTEGER
END RECORD
DEFINE cnf cnf_t
MAIN
DEFINE subject STRING
DEFINE recipients DYNAMIC ARRAY OF RECORD mode STRING, recipient STRING END RECORD
DEFINE mailtext STRING
DEFINE mailbody DYNAMIC ARRAY OF STRING
DEFINE attachments DYNAMIC ARRAY OF RECORD fname, sname STRING END RECORD
DEFINE rcpt DYNAMIC ARRAY OF STRING
DEFINE nmime INTEGER
DEFINE filename STRING
OPTIONS INPUT WRAP
# TZ checks
IF NUM_ARGS() == 1 THEN
IF ARG_VAL(1) == "TZGMT" THEN
CALL fgl_setenv("TZ", "GMT")
CALL showTZ()
END IF
IF ARG_VAL(1) == "TZLOCAL" THEN
CALL showTZ()
END IF
IF ARG_VAL(1) == "DTSHOW" THEN
DISPLAY CURRENT YEAR TO MINUTE
END IF
EXIT PROGRAM 0
END IF
OPEN FORM maildemo FROM "Mailer"
DISPLAY FORM maildemo
LET charset = getCharset()
# Mime types initialization - should be left configurable by the client
LET nmime = 1
EXT2MIME("bin", "application/octet-stream", "base64")
EXT2MIME("pdf", "application/x-pdf", "base64")
EXT2MIME("gz", "application/x-gzip", "base64")
EXT2MIME("tgz", "application/x-compressed-tar", "base64")
EXT2MIME("gif", "image/gif", "base64")
EXT2MIME("jpg", "image/jpg", "base64")
EXT2MIME("png", "image/png", "base64")
EXT2MIME("txt", "text/plain", "7bit")
EXT2MIME("per", "text/plain", "7bit")
EXT2MIME("4gl", "text/plain", "7bit")
EXT2MIME("42m", "application/octet-stream", "base64")
CALL load_maildemo_cnf()
LET tz = detect_timezone()
LET boundary='--------------' || util.Math.rand(1000) || util.Math.rand(1000) || util.Math.rand(1000) || util.Math.rand(1000)
# Mail composition dialog
DIALOG ATTRIBUTE(UNBUFFERED)
INPUT BY NAME cnf.* ATTRIBUTES(WITHOUT DEFAULTS)
END INPUT
INPUT ARRAY recipients FROM srrecipients.* ATTRIBUTES(WITHOUT DEFAULTS)
BEFORE INSERT
LET recipients[dialog.getCurrentRow("srrecipients")].mode = "To"
AFTER ROW
IF LENGTH(recipients[dialog.getCurrentRow("srrecipients")].recipient) == 0 THEN
CALL recipients.deleteElement(dialog.getCurrentRow("srrecipients"))
END IF
END INPUT
INPUT BY NAME subject, mailtext
END INPUT
DISPLAY ARRAY attachments TO srattachments.*
END DISPLAY
INPUT BY NAME logtext
END INPUT
BEFORE DIALOG
IF LENGTH(cnf.hostname) == 0 OR LENGTH(cnf.smtpHost) == 0 OR LENGTH(cnf.senderEmail) == 0 THEN
NEXT FIELD hostname
END IF
NEXT FIELD recipient
ON ACTION attachfile
CALL ui.Interface.frontCall("standard", "openfile", ["C:\\", "*", "", "Attach file"], [filename])
IF filename IS NOT NULL THEN
LET attachments[attachments.getLength()+1].fname = filename
LET attachments[attachments.getLength()].sname = "attachment" || (attachments.getLength() USING "<<<<<<<")
END IF
ON ACTION delattachment
CALL attachments.deleteElement(dialog.getCurrentRow("srattachments"))
ON ACTION viewattachment
CALL mbox_ok("View attachment", "View : " || attachments[dialog.getCurrentRow("srattachments")].sname, "information")
ON ACTION close
EXIT DIALOG
ON ACTION send
IF INFIELD(recipient) THEN
# We are currently editing the recipient column
IF LENGTH(recipients[dialog.getCurrentRow("srrecipients")].recipient) == 0 THEN
# Empty, remove it
# AFTER FIELD has not been executed, do it here
CALL recipients.deleteElement(dialog.getCurrentRow("srrecipients"))
END IF
END IF
IF recipients.getLength() == 0 THEN
CALL mbox_ok("Error", "The recipient list is empty.", "stop")
CONTINUE DIALOG
END IF
IF LENGTH(subject) == 0 THEN
# Note: click on the X button returns NULL or "", I would expect the default specified, i.e. "No" here.
IF NOT mbox_yn("Warning", "The subject line is empty.\nDo you really want to send this mail ?", "exclamation") THEN
NEXT FIELD subject
END IF
END IF
IF NOT retrieve_attachments(attachments) THEN
CONTINUE DIALOG
END IF
CALL genmail(cnf.senderemail, recipients, subject, mailtext, attachments, mailbody, rcpt)
IF sendmail(rcpt, mailbody) THEN
CALL clear_attachments(attachments)
CALL mbox_ok("Send mail", "Mail successfully sent.", "information")
CALL rcpt.clear()
CALL recipients.clear()
CALL attachments.clear()
CALL mailbody.clear()
LET subject = ""
LET mailtext = ""
NEXT FIELD recipient
END IF
CALL clear_attachments(attachments)
ON ACTION savecnf
CALL save_maildemo_cnf()
ON ACTION chkcnf
CALL check_maildemo_cnf()
END DIALOG
END MAIN
# Retrieve the current charset
FUNCTION getCharset()
DEFINE l STRING
DEFINE c base.Channel
LET c = base.Channel.create()
CALL c.openPipe("fglrun -i mbcs 2>&1", "r")
LET l = c.readLine()
WHILE l IS NOT NULL
IF l MATCHES "Charmap : *" THEN
RETURN l.subString(16,length(l))
EXIT WHILE
END IF
LET l = c.readLine()
END WHILE
END FUNCTION
&define MB(array, idx, line) \
LET array[idx] = line \
LET idx = idx + 1
# Generate the mail content
FUNCTION genmail(senderemail, recipients, subject, mailtext, attachments, mailbody, rcpt)
DEFINE senderemail STRING
DEFINE subject STRING
DEFINE recipients DYNAMIC ARRAY OF RECORD mode STRING, recipient STRING END RECORD
DEFINE mailtext STRING
DEFINE sbmt base.StringBuffer
DEFINE i, found INTEGER
DEFINE rcpt DYNAMIC ARRAY OF STRING
DEFINE attachments DYNAMIC ARRAY OF RECORD fname, sname STRING END RECORD
DEFINE nl INTEGER
DEFINE nrcpt INTEGER
DEFINE nr INTEGER
DEFINE extension STRING
DEFINE mimeid INTEGER
DEFINE mailbody DYNAMIC ARRAY OF STRING
LET nl = 1
LET nr = 1
LET nrcpt = 0
# Add mail date
MB(mailbody, nl, SFMT("Date: %1 %2 %3\r", TODAY USING "ddd, dd mmm yyyy", TIME(CURRENT), tz))
# First add headers
MB(mailbody, nl, SFMT("From: %1\r", senderemail))
# Add user agent
MB(mailbody, nl, "User-Agent: maildemo sample program.\r")
# Add MIME version
MB(mailbody, nl, "MIME-Version: 1.0\r")
# Add To: recipients
LET found = 0
FOR i = 1 TO recipients.getLength()
IF recipients[i].mode == "To" THEN
IF found THEN
LET mailbody[nl] = mailbody[nl].append(SFMT(", %1", recipients[i].recipient))
ELSE
LET mailbody[nl]=SFMT("To: %1", recipients[i].recipient)
LET found = 1
END IF
MB(rcpt, nr, SFMT("RCPT TO: %1", extract_email(recipients[i].recipient)))
END IF
END FOR
IF found == 1 THEN
LET mailbody[nl] = mailbody[nl].append("\r")
LET nl = nl + 1
END IF
# Add CC: recipients
LET found = 0
FOR i = 1 TO recipients.getLength()
IF recipients[i].mode == "CC" THEN
IF found THEN
LET mailbody[nl] = mailbody[nl].append(SFMT(", %1", recipients[i].recipient))
ELSE
LET mailbody[nl]=SFMT("CC: %1", recipients[i].recipient)
LET found = 1
END IF
MB(rcpt, nr, SFMT("RCPT TO: %1", extract_email(recipients[i].recipient)))
END IF
END FOR
IF found == 1 THEN
LET mailbody[nl] = mailbody[nl].append("\r")
LET nl = nl + 1
END IF
# Add CC: recipients
LET found = 0
FOR i = 1 TO recipients.getLength()
IF recipients[i].mode == "BCC" THEN
IF found THEN
LET mailbody[nl] = mailbody[nl].append(SFMT(", %1", recipients[i].recipient))
ELSE
LET mailbody[nl]=SFMT("BCC: %1", recipients[i].recipient)
LET found = 1
END IF
MB(rcpt, nr, SFMT("RCPT TO: %1", extract_email(recipients[i].recipient)))
END IF
END FOR
IF found == 1 THEN
LET mailbody[nl] = mailbody[nl].append("\r")
LET nl = nl + 1
END IF
MB(mailbody, nl, SFMT("Subject: %1\r", subject))
# Now add the message content
# Multipart message not handled
# Should check the body is really 7bit and not line starts with ., ...
# To be conformant with 7bit encoding. See RFC for details.
# \n.\n is the DATA section end mark.
# Should be replaced by \n..\n
LET sbmt = base.StringBuffer.create()
CALL sbmt.append(mailtext)
# Need to do it twice to properly handle following case: \n.\n.\n.\n.\n.\n
CALL sbmt.replace("\n.\n", "\n..\n", 0)
CALL sbmt.replace("\n.\n", "\n..\n", 0)
LET mailtext=sbmt.toString()
IF attachments.getLength() == 0 THEN
MB(mailbody, nl, SFMT("Content-Type: text/plain; charset=%1\r", "ISO-8859-1"))
MB(mailbody, nl, SFMT("Content-Transfer-Encoding: %1\r", "7bit"))
MB(mailbody, nl, "\r")
MB(mailbody, nl, mailtext)
MB(mailbody, nl, "\r")
ELSE
MB(mailbody, nl, "Content-Type: multipart/mixed;\r")
MB(mailbody, nl, ' boundary="' || boundary || '"\r')
MB(mailbody, nl, "\r")
MB(mailbody, nl, "This is a multi-part message in MIME format.\r")
# Add message text
MB(mailbody, nl, "--" || boundary || "\r")
MB(mailbody, nl, SFMT("Content-Type: text/plain; charset=%1; format=flowed\r", "ISO-8859-1"))
MB(mailbody, nl, SFMT("Content-Transfer-Encoding: %1\r", "7bit"))
MB(mailbody, nl, "\r")
MB(mailbody, nl, mailtext)
MB(mailbody, nl, '\r')
FOR i = 1 TO attachments.getLength()
MB(mailbody, nl, "--" || boundary || "\r")
LET extension = os.Path.extension(attachments[i].fname)
LET mimeid = getmime(extension)
IF mimeid == -1 THEN
# Unknown extension.
# Default to application/octet-stream, base64 encoded
LET mimeid = 1
END IF
CASE ext2mime[mimeid].encoding
WHEN "base64"
MB(mailbody, nl, SFMT("Content-Type: %1;\r", ext2mime[mimeid].mime))
MB(mailbody, nl, ' name="'|| attachments[i].fname || '"\r')
MB(mailbody, nl, 'Content-Transfer-Encoding: base64\r')
MB(mailbody, nl, 'Content-Disposition: inline;\r')
MB(mailbody, nl, ' filename="'|| os.Path.basename(attachments[i].fname) || '"\r')
MB(mailbody, nl, '\r')
LET nl = file_to_base64(attachments[i].sname, mailbody, nl)
WHEN "7bit"
MB(mailbody, nl, SFMT("Content-Type: %1;\r", ext2mime[mimeid].mime))
MB(mailbody, nl, ' name="'|| attachments[i].fname || '"\r')
MB(mailbody, nl, 'Content-Transfer-Encoding: 7bit\r')
MB(mailbody, nl, 'Content-Disposition: inline;\r')
MB(mailbody, nl, ' filename="'|| os.Path.basename(attachments[i].fname) || '"\r')
MB(mailbody, nl, '\r')
LET nl = file_to_7bit(attachments[i].sname, mailbody, nl)
OTHERWISE
CALL mbox_ok("Attachment encoding", SFMT("Unsupported encoding %1.", ext2mime[mimeid].encoding), "stop")
EXIT PROGRAM 1
END CASE
END FOR
MB(mailbody, nl, "--" || boundary || "--\r")
END IF
# Add end of message marker
MB(mailbody, nl, ".\r")
END FUNCTION
FUNCTION smtpSend(ch, command)
DEFINE ch base.Channel
DEFINE command STRING
DEFINE smtpStatus INTEGER
DEFINE smtpMessage STRING
CALL clientWrite(ch, command)
CALL readSmtpAnswer(ch) RETURNING smtpStatus, smtpMessage
RETURN smtpStatus, smtpMessage
END FUNCTION
FUNCTION readSmtpAnswer(ch)
DEFINE ch base.Channel
DEFINE line STRING
DEFINE smtpStatus INTEGER
DEFINE smtpMessage STRING
LET smtpMessage = ""
WHILE 1
LET line = serverRead(ch)
IF line IS NULL THEN
RETURN -1, "COULD NOT READ SMTP ANSWER"
END IF
IF line MATCHES "[0-9][0-9][0-9] *" THEN
IF smtpMessage.getLength() != 0 THEN
LET smtpMessage=smtpMessage || "\n"
END IF
LET smtpMessage=smtpMessage.append(line.subString(4, line.getLength()))
LET smtpStatus = line.subString(1,3)
RETURN smtpStatus, smtpMessage
END IF
IF line MATCHES "[0-9][0-9][0-9]-*" THEN
IF smtpMessage.getLength() != 0 THEN
LET smtpMessage=smtpMessage || "\n"
END IF
LET smtpMessage=smtpMessage.append(line.subString(4, line.getLength()))
END IF
END WHILE
END FUNCTION
FUNCTION sendmail(rcpt, mailbody)
DEFINE mailbody DYNAMIC ARRAY OF STRING
DEFINE rcpt DYNAMIC ARRAY OF STRING
DEFINE smtpStatus INTEGER
DEFINE smtpMessage STRING
DEFINE mc base.Channel
DEFINE i INTEGER
LET smtplog = base.StringBuffer.create()
LET mc = base.Channel.create()
TRY
CALL mc.openClientSocket(cnf.smtphost, cnf.smtpport, "ub", cnf.cnxtimeout)
CATCH
CALL mbox_ok("Socket error", "The connection to the SMTP Server failed.\n", "stop")
RETURN FALSE
END TRY
CALL readSmtpAnswer(mc) RETURNING smtpStatus, smtpMessage
CALL smtpSend(mc, "HELO " || cnf.hostname || "\r") RETURNING smtpStatus, smtpMessage
IF smtpStatus >= 400 THEN
CALL smtpError(mc, smtpMessage)
RETURN FALSE
END IF
CALL smtpSend(mc, SFMT("MAIL FROM: %1\r", extract_email(cnf.senderemail))) RETURNING smtpStatus, smtpMessage
IF smtpStatus >= 400 THEN
CALL smtpError(mc, smtpMessage)
RETURN FALSE
END IF
FOR i = 1 TO rcpt.getLength()
CALL smtpSend(mc, rcpt[i]) RETURNING smtpStatus, smtpMessage
IF smtpStatus >= 400 THEN
CALL smtpError(mc, smtpMessage)
RETURN FALSE
END IF
END FOR
CALL smtpSend(mc, "DATA\r") RETURNING smtpStatus, smtpMessage
IF smtpStatus >= 400 THEN
CALL smtpError(mc, smtpMessage)
RETURN FALSE
END IF
FOR i = 1 TO mailbody.getLength()
CALL clientWrite(mc, mailbody[i])
END FOR
CALL readSmtpAnswer(mc) RETURNING smtpStatus, smtpMessage
IF smtpStatus >= 400 THEN
CALL smtpError(mc, smtpMessage)
RETURN FALSE
END IF
CALL smtpSend(mc, "QUIT\r") RETURNING smtpStatus, smtpMessage
IF smtpStatus >= 400 THEN
CALL smtpError(mc, smtpMessage)
RETURN FALSE
END IF
CALL mc.close()
RETURN TRUE
END FUNCTION
FUNCTION clientWrite(mc, line)
DEFINE mc base.Channel
DEFINE line STRING
CALL smtplog.append('<FONT COLOR="#339933"><B>C&gt; </B>')
CALL smtplog.append(line)
CALL smtplog.append("<BR></FONT>")
LET logtext=smtplog.toString()
CALL mc.writeLine(line)
END FUNCTION
FUNCTION serverRead(mc)
DEFINE line STRING
DEFINE mc base.Channel
LET line = mc.readLine()
CALL smtplog.append("<FONT COLOR=red><B>S&gt; </B>")
CALL smtplog.append(line)
CALL smtplog.append("<BR></FONT>")
LET logtext=smtplog.toString()
RETURN line
END FUNCTION
# Returns the mime entry index for the extension `ext'
# -1 if not entry matching the extension is found.
FUNCTION getmime(ext)
DEFINE ext STRING
DEFINE j INTEGER
FOR j = 1 TO ext2mime.getLength()
IF ext2mime[j].ext == ext THEN
RETURN j
END IF
END FOR
RETURN -1
END FUNCTION
# Save configuration
FUNCTION save_maildemo_cnf()
DEFINE pch base.Channel
DEFINE path STRING
LET path = os.Path.join(os.Path.homedir(), "maildemo.cnf")
IF os.Path.exists(path) THEN
IF NOT os.Path.writable(path) THEN
CALL mbox_ok("Write configuration", SFMT("The file %1 can not be written", path), "stop")
RETURN
END IF
END IF
TRY
LET pch=base.channel.create()
CALL pch.openFile(path, "w")
CALL pch.write([cnf.*])
CALL pch.close()
CATCH
CALL mbox_ok("Write configuration", SFMT("Failed to save the configuration in the file %1", path), "stop")
RETURN
END TRY
CALL mbox_ok("Save configuration", SFMT("The configuration was successfully saved in %1.", path), "information")
END FUNCTION
# Load configuration
FUNCTION load_maildemo_cnf()
DEFINE pch base.Channel
DEFINE path STRING
LET path = os.Path.join(os.Path.homedir(), "maildemo.cnf")
IF NOT os.Path.exists(path) THEN
CALL default_maildemo_cnf()
RETURN
END IF
IF NOT os.Path.readable(path) THEN
CALL default_maildemo_cnf()
RETURN
END IF
# Read save parameters
LET pch = base.channel.create()
CALL pch.openFile(path,"r")
LET rv = pch.read([cnf.*])
CALL pch.close()
IF rv THEN
RETURN
END IF
CALL default_maildemo_cnf()
END FUNCTION
FUNCTION default_maildemo_cnf()
LET cnf.smtpPort = 25
LET cnf.smtpHost = ""
LET cnf.senderEmail = ""
LET cnf.hostname = ""
LET cnf.cnxtimeout = 10
END FUNCTION
FUNCTION file_to_base64(fname, mailbody, nl)
DEFINE mailbody DYNAMIC ARRAY OF STRING
DEFINE fname STRING
DEFINE nl INTEGER
DEFINE l BYTE
DEFINE lfn STRING
DEFINE doc om.DomDocument
DEFINE node om.DomNode
DEFINE lnode om.NodeList
LET lfn = fname || ".xml"
LOCATE l IN FILE fname
CALL fgl_report_set_document_handler(om.XmlWriter.createFileWriter(lfn))
START REPORT gen_base64 TO FILE "dummy_report_file"
OUTPUT TO REPORT gen_base64(l)
FINISH REPORT gen_base64
# Load the generated XML file
LET doc = om.DomDocument.createFromXmlFile(lfn)
LET node = doc.getDocumentElement()
LET lnode = node.selectByTagName("Item")
LET node = lnode.item(1)
MB(mailbody, nl, node.getAttribute("value"))
IF NOT os.Path.delete(lfn) THEN
CALL mbox_ok("Warning", SFMT("The temporary file %1 could not be removed.", lfn), "stop")
END IF
RETURN nl
END FUNCTION
# Convert a file to a base64 string
REPORT gen_base64(l)
DEFINE l BYTE
OUTPUT
PAGE LENGTH 1
TOP MARGIN 0
BOTTOM MARGIN 0
LEFT MARGIN 0
FORMAT
ON EVERY ROW
PRINTX name=base64_l l
END REPORT
# Convert a file to 7bit encoding (fake)
FUNCTION file_to_7bit(fname, mailbody, nl)
DEFINE fname STRING
DEFINE mailbody DYNAMIC ARRAY OF STRING
DEFINE ac base.Channel
DEFINE line STRING
DEFINE nl INTEGER
LET ac = base.Channel.create()
CALL ac.openFile(fname, "rb")
WHILE NOT ac.isEof()
LET line = ac.readLine()
MB(mailbody, nl, line)
END WHILE
CALL ac.close()
RETURN nl
END FUNCTION
FUNCTION check_maildemo_cnf()
DEFINE smtpStatus INTEGER
DEFINE smtpMessage STRING
DEFINE mc base.Channel
LET smtplog = base.StringBuffer.create()
LET mc = base.Channel.create()
TRY
CALL mc.openClientSocket(cnf.smtphost, cnf.smtpport, "ub", cnf.cnxtimeout)
CATCH
CALL mbox_ok("Socket error", "The connection to the SMTP Server failed.\n", "stop")
RETURN
END TRY
CALL readSmtpAnswer(mc) RETURNING smtpStatus, smtpMessage
CALL smtpSend(mc, "HELO " || cnf.hostname || "\r") RETURNING smtpStatus, smtpMessage
IF smtpStatus >= 400 THEN
CALL smtpError(mc, smtpMessage)
RETURN
END IF
CALL smtpSend(mc, "QUIT\r") RETURNING smtpStatus, smtpMessage
IF smtpStatus >= 400 THEN
CALL smtpError(mc, smtpMessage)
RETURN
END IF
CALL mc.close()
CALL mbox_ok("SMTP Connection", "The SMTP parameters are correct", "information")
RETURN
END FUNCTION
FUNCTION smtpError(mc, msg)
DEFINE msg STRING
DEFINE mc base.Channel
DEFINE smtpStatus INTEGER
DEFINE smtpMessage STRING
CALL mbox_ok("SMTP Error", msg, "stop")
CALL smtpSend(mc, "QUIT\r") RETURNING smtpStatus, smtpMessage
CALL mc.close()
END FUNCTION
FUNCTION getProgramCmd()
DEFINE cmd STRING
LET cmd = os.Path.join(fgl_getenv("FGLDIR"), "demo")
LET cmd = os.Path.join(cmd, "MultipleDialogs")
LET cmd = os.Path.join(cmd, "Mailer.42m")
IF NOT os.Path.exists(cmd) THEN
LET cmd = os.Path.join(".", "Mailer.42m")
IF NOT os.Path.exists(cmd) THEN
DISPLAY "ERROR: Could not find Mailer.42m program file."
EXIT PROGRAM 1
END IF
END IF
IF fgl_getenv("WINDIR") THEN
LET cmd = "\"" || cmd || "\""
END IF
LET cmd = SFMT("fglrun %1 ", cmd)
RETURN cmd
END FUNCTION
FUNCTION showTZ()
CALL FGL_SETENV("FGLGUI","0")
RUN getProgramCmd() || "DTSHOW"
CALL FGL_SETENV("FGLGUI","1")
END FUNCTION
FUNCTION detect_timezone()
DEFINE ch base.Channel
DEFINE dnow, dgmt DATETIME YEAR TO MINUTE
DEFINE dh INTERVAL MINUTE(5) TO MINUTE
DEFINE sminutes STRING
DEFINE iminutes INTEGER
DEFINE ihours INTEGER
CALL FGL_SETENV("FGLGUI","0")
LET ch = base.Channel.create()
CALL ch.openPipe(getProgramCmd()||" TZLOCAL", "r")
LET rv=ch.read(dnow)
CALL ch.close()
CALL ch.openPipe(getProgramCmd()||" TZGMT", "r")
LET rv=ch.read(dgmt)
CALL ch.close()
LET dh = dnow - dgmt
LET sminutes = dh
LET iminutes = sminutes
IF iminutes MOD 2 == 1 THEN
LET iminutes = iminutes + 1
END IF
LET ihours = iminutes / 60
LET iminutes = iminutes - ihours * 60
CALL FGL_SETENV("FGLGUI","1")
RETURN SFMT("%1%2", ihours USING "+&&", iminutes USING "&&")
END FUNCTION
{
Retrieves attachments from the client.
If many attachments should be retrieved, maybe a progress bar could be displayed
}
FUNCTION retrieve_attachments(attachments)
DEFINE attachments DYNAMIC ARRAY OF RECORD fname, sname STRING END RECORD
DEFINE i, j INTEGER
FOR i = 1 TO attachments.getLength()
DISPLAY "Retrieving attachment : ", attachments[i].fname, " TO ", attachments[i].sname
TRY
CALL fgl_getfile(attachments[i].fname, attachments[i].sname)
CATCH
CALL mbox_ok("Attachment download", SFMT("Could not retrieve attachment %1", attachments[i].fname), "stop")
FOR j = 1 TO i
LET rv = os.Path.delete(attachments[j].sname)
END FOR
RETURN FALSE
END TRY
END FOR
RETURN TRUE
END FUNCTION
# Deletes temporary attachment files
FUNCTION clear_attachments(attachments)
DEFINE attachments DYNAMIC ARRAY OF RECORD fname, sname STRING END RECORD
DEFINE i INTEGER
FOR i = 1 TO attachments.getLength()
LET rv = os.Path.delete(attachments[i].sname)
END FOR
END FUNCTION
FUNCTION extract_email(email)
DEFINE email STRING
DEFINE s, e INTEGER
LET s = email.getIndexOf("<", 1)
LET e = email.getIndexOf(">", 1)
IF s == 0 OR e == 0 OR s >= e THEN
RETURN email
END IF
RETURN email.subString(s + 1, e - 1)
END FUNCTION
+124
View File
@@ -0,0 +1,124 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
ACTION DEFAULTS
ACTION send (TEXT="Send", IMAGE="mail", ACCELERATOR=CONTROL-RETURN)
ACTION attachfile (TEXT="Attach file", IMAGE="plus", ACCELERATOR=CONTROL-J)
ACTION close (IMAGE="quit", VALIDATE = NO, DEFAULTVIEW=YES)
ACTION viewattachment (DEFAULTVIEW=NO)
END
LAYOUT(TEXT="Send EMAIL")
FOLDER
PAGE p1(TEXT="Account settings")
GRID
{
[lhostname ][hostname ]
[lsmtphost ][smtphost ]
[lsmtpport ][smtpport]
[lsenderem ][senderemail ]
[lcnxtout ][cnxtimeout] (-1 = INFINITE TIMEOUT)
[chkcnf ]
[savecnf ]
}
END
END
PAGE p2(TEXT="Mail compose")
VBOX(splitter)
HBOX(splitter)
GROUP(TEXT="Recipients")
GRID
{
<T recipients >[a]
[mode|recipient ][i]
[mode|recipient ][d]
[mode|recipient ]
[mode|recipient ]
}
END
END
GROUP(TEXT="Attachments")
GRID
{
<T attachments >
[sname |fname ]
[ :add:rm]
}
END
END
END
GROUP mc(TEXT="Compose mail")
GRID
{
[s|subject ]
[mailtext ]
[ ]
[ ]
[ ]
[send]
}
END
END
END
END
PAGE p3(TEXT="SMTP Dialog log")
GRID
{
[smtplog ]
[ ]
[ ]
[ ]
}
END
END
END
ATTRIBUTES
# Configuration
# smtphost should be a simple EDIT field, but EDIT does not support STRETCH attribute.
TEXTEDIT hostname=formonly.hostname, STRETCH=X, SCROLLBARS=NONE, WANTNORETURNS;
TEXTEDIT smtphost=formonly.smtphost, STRETCH=X, SCROLLBARS=NONE, WANTNORETURNS;
TABLE recipients: UNHIDABLECOLUMNS, UNMOVABLECOLUMNS;
EDIT smtpport=formonly.smtpport;
EDIT senderemail=formonly.senderemail, SCROLL;
EDIT cnxtimeout=formonly.cnxtimeout;
BUTTON savecnf:savecnf, TEXT="Save", IMAGE="disk";
BUTTON chkcnf:chkcnf, TEXT="Check", IMAGE="refresh";
BUTTON a: srrecipients.append, IMAGE = "listadd", TEXT="";
BUTTON i: srrecipients.insert, IMAGE = "listins", TEXT="";
BUTTON d: srrecipients.delete, IMAGE = "listdel", TEXT="";
LABEL lhostname:TEXT="Hostname";
LABEL lsmtphost:TEXT="Smtp server";
LABEL lsmtpport:TEXT="Smtp port";
LABEL lsenderem:TEXT="Email";
LABEL lcnxtout:TEXT="Connection timeout";
# To
COMBOBOX mode=formonly.mode, ITEMS=("To", "CC", "BCC"), TITLE="To";
EDIT recipient=formonly.recipient, TITLE="Email";
# Subject and mail body
LABEL s: TEXT="Subject:";
EDIT subject=formonly.subject, SCROLL;
TEXTEDIT mailtext=formonly.mailtext, STRETCH=BOTH;
BUTTON send:send, TEXT="Send", IMAGE="mail";
# Attachments
TABLE attachments: DOUBLECLICK=viewattachment;
EDIT fname=formonly.fname, TITLE="Filename";
EDIT sname=formonly.sname, HIDDEN=USER, TITLE="Local Filename";
BUTTON rm: delattachment, TEXT="Remove", IMAGE="eraser";
BUTTON add: attachfile, TEXT="Attach", IMAGE="plus";
# SMTP dialog log
TEXTEDIT smtplog=formonly.logtext, STRETCH=BOTH;
INSTRUCTIONS
SCREEN RECORD srrecipients(formonly.mode, formonly.recipient);
SCREEN RECORD srattachments(formonly.fname, formonly.sname);
+38
View File
@@ -0,0 +1,38 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
FUNCTION mbox_ok(t, m, i)
DEFINE t,m,i STRING
MENU t ATTRIBUTES(STYLE="dialog",COMMENT=m,IMAGE=i)
COMMAND "Ok" EXIT MENU
END MENU
END FUNCTION
FUNCTION mbox_yn(t, m, i)
DEFINE t,m,i STRING
DEFINE r INTEGER
MENU t ATTRIBUTES(STYLE="dialog",COMMENT=m,IMAGE=i)
COMMAND "Yes" LET r = TRUE EXIT MENU
COMMAND "No" LET r = FALSE EXIT MENU
END MENU
RETURN r
END FUNCTION
FUNCTION mbox_ync(t, m, i)
DEFINE t,m,i STRING
DEFINE r INTEGER
MENU t ATTRIBUTES(STYLE="dialog",COMMENT=m,IMAGE=i)
COMMAND "Yes" LET r = 1 EXIT MENU
COMMAND "No" LET r = 2 EXIT MENU
COMMAND "Cancel" LET r = 3 EXIT MENU
END MENU
RETURN r
END FUNCTION
@@ -0,0 +1,169 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
# Included in other demos to simplify compilation / link
IMPORT FGL MsgBox
FUNCTION select_city(prefix)
DEFINE prefix STRING
DEFINE filter STRING
DEFINE r SMALLINT
DEFINE name, state STRING
DEFINE cityarr DYNAMIC ARRAY OF RECORD
city_id INTEGER,
city_name VARCHAR(50),
city_state CHAR(2)
END RECORD
OPEN WINDOW w_city WITH FORM "CityList"
DIALOG ATTRIBUTES(UNBUFFERED, FIELD ORDER FORM)
INPUT BY NAME filter
END INPUT
DISPLAY ARRAY cityarr TO sr.*
END DISPLAY
BEFORE DIALOG
IF LENGTH(prefix) < 3 THEN
LET filter = prefix || "*"
ELSE
LET filter = "*"
END IF
CALL select_city_fill(DIALOG,filter,cityarr)
CALL DIALOG.setActionActive("accept", DIALOG.getCurrentRow("sr")>0)
ON ACTION filter
CALL select_city_fill(DIALOG,filter,cityarr)
CALL DIALOG.setActionActive("accept", DIALOG.getCurrentRow("sr")>0)
ON ACTION accept
LET name = cityarr[DIALOG.getCurrentRow("sr")].city_name
LET state = cityarr[DIALOG.getCurrentRow("sr")].city_state
LET r = 0
EXIT DIALOG
ON ACTION cancel
LET r = -1
EXIT DIALOG
END DIALOG
CLOSE WINDOW w_city
RETURN r, name, state
END FUNCTION
FUNCTION select_city_fill(d,filter, cityarr)
DEFINE d ui.Dialog
DEFINE filter STRING
DEFINE cityarr DYNAMIC ARRAY OF RECORD
city_id INTEGER,
city_name VARCHAR(50),
city_state CHAR(2)
END RECORD
DEFINE i INTEGER
LET i = 0
CALL cityarr.clear()
&define ADD_CITY(name, state) \
LET cityarr[i:=i+1].city_id = i \
LET cityarr[i].city_name = name \
LET cityarr[i].city_state = state
ADD_CITY("Austin", "TX")
ADD_CITY("Boston", "MA")
ADD_CITY("Chicago", "IL")
ADD_CITY("Dallas", "TX")
ADD_CITY("Denver", "CO")
ADD_CITY("Detroit", "MI")
ADD_CITY("Houston", "TX")
ADD_CITY("Las Vegas", "NV")
ADD_CITY("Los Angeles", "CA")
ADD_CITY("Miami", "FL")
ADD_CITY("New York", "NY")
ADD_CITY("Philadelphia", "PA")
ADD_CITY("Phoenix", "AZ")
ADD_CITY("Portland", "ME")
ADD_CITY("San Antonio", "TX")
ADD_CITY("San Diego", "CA")
ADD_CITY("San Francisco", "CA")
ADD_CITY("Washington", "DC")
FOR i=1 TO cityarr.getLength()
IF cityarr[i].city_name NOT MATCHES filter THEN
CALL cityarr.deleteElement(i)
LET i = i - 1
END IF
END FOR
CALL d.setCurrentRow("sr",1)
END FUNCTION
FUNCTION combo_fill_states(cb)
DEFINE cb ui.ComboBox
CALL cb.addItem("AK","Alaska")
CALL cb.addItem("AL","Alabama")
CALL cb.addItem("AR","Arkansas")
CALL cb.addItem("AZ","Arizona")
CALL cb.addItem("CA","California")
CALL cb.addItem("CO","Colorado")
CALL cb.addItem("CT","Connecticut")
CALL cb.addItem("DC","D.C.")
CALL cb.addItem("DE","Delaware")
CALL cb.addItem("FL","Florida")
CALL cb.addItem("GA","Georgia")
CALL cb.addItem("HI","Hawaii")
CALL cb.addItem("IA","Iowa")
CALL cb.addItem("ID","Idaho")
CALL cb.addItem("IL","Illinois")
CALL cb.addItem("IN","Indiana")
CALL cb.addItem("KS","Kansas")
CALL cb.addItem("KY","Kentucky")
CALL cb.addItem("LA","Louisiana")
CALL cb.addItem("MA","Massachusetts")
CALL cb.addItem("MD","Maryland")
CALL cb.addItem("ME","Maine")
CALL cb.addItem("MI","Michigan")
CALL cb.addItem("MN","Minnesota")
CALL cb.addItem("MO","Missouri")
CALL cb.addItem("MS","Mississippi")
CALL cb.addItem("MT","Montana")
CALL cb.addItem("NC","North Carolina")
CALL cb.addItem("ND","North Dakota")
CALL cb.addItem("NE","Nebraska")
CALL cb.addItem("NH","New Hampshire")
CALL cb.addItem("NJ","New Jersey")
CALL cb.addItem("NM","New Mexico")
CALL cb.addItem("NV","Nevada")
CALL cb.addItem("NY","New York")
CALL cb.addItem("OH","Ohio")
CALL cb.addItem("OK","Oklahoma")
CALL cb.addItem("OR","Oregon")
CALL cb.addItem("PA","Pennsylvania")
CALL cb.addItem("PR","Puerto Rico")
CALL cb.addItem("RI","Rhode Island")
CALL cb.addItem("SC","South Carolina")
CALL cb.addItem("SD","South Dakota")
CALL cb.addItem("TN","Tennessee")
CALL cb.addItem("TX","Texas")
CALL cb.addItem("UT","Utah")
CALL cb.addItem("VA","Virginia")
CALL cb.addItem("VT","Vermont")
CALL cb.addItem("WA","Washington")
CALL cb.addItem("WI","Wisconsin")
CALL cb.addItem("WV","West Virginia")
CALL cb.addItem("WY","Wyoming")
END FUNCTION
@@ -0,0 +1,43 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
ACTION DEFAULTS
ACTION filter (ACCELERATOR = CONTROL-ENTER, ACCELERATOR2 = CONTROL-RETURN)
END
LAYOUT ( STYLE = "dialog4" )
GRID
{
Filter: [f ]
<t t1 >
Id City State
[id |name |st ]
[id |name |st ]
[id |name |st ]
[id |name |st ]
[id |name |st ]
[id |name |st ]
< >
}
END
END
ATTRIBUTES
BUTTONEDIT f = FORMONLY.filter TYPE CHAR, ACTION=filter, IMAGE="filter",
COMMENT = "Presse Control-Enter to apply filter";
TABLE t1 : COMMENT = "Double-click to select an item from the list";
EDIT id = FORMONLY.city_id TYPE INTEGER;
EDIT name = FORMONLY.city_name TYPE CHAR;
EDIT st = FORMONLY.city_state TYPE CHAR;
END
INSTRUCTIONS
SCREEN RECORD sr (FORMONLY.city_id, FORMONLY.city_name, FORMONLY.city_state);
END
@@ -0,0 +1,490 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
IMPORT FGL CityList
IMPORT FGL OrderInput
IMPORT FGL MsgBox
GLOBALS "Globals.4gl"
DEFINE cust_curr INTEGER
DEFINE cust, old_cust custrec
DEFINE cust_state SMALLINT
CONSTANT STATE_NAVIGATE = 0
CONSTANT STATE_MODIFY = 1
CONSTANT STATE_APPEND = 2
CONSTANT otitle = "Customer"
MAIN
DEFINE ordlist_lastrow INTEGER
OPTIONS INPUT WRAP
OPEN FORM f_cust FROM "CustOrders"
DISPLAY FORM f_cust
DIALOG ATTRIBUTES(FIELD ORDER FORM, UNBUFFERED)
INPUT cust.* FROM custrec.* ATTRIBUTES(WITHOUT DEFAULTS)
ON CHANGE custrec.cust_name
IF NOT cust_check_name(DIALOG) THEN NEXT FIELD CURRENT END IF
ON CHANGE custrec.cust_address
IF NOT cust_check_address(DIALOG) THEN NEXT FIELD CURRENT END IF
ON CHANGE custrec.cust_state
INITIALIZE cust.cust_city, cust.cust_zipcode, cust.cust_address TO NULL
END INPUT
DISPLAY ARRAY orders TO ordlist.*
BEFORE ROW
MESSAGE SFMT("Order record %1 / %2", DIALOG.getCurrentRow("ordlist"), DIALOG.getArrayLength("ordlist"))
AFTER ROW
LET ordlist_lastrow = arr_curr()
END DISPLAY
BEFORE DIALOG
CALL cust_loadlist(DIALOG,100)
CALL cust_move_to(DIALOG, 1)
ON ACTION zoom_city
CALL cust_zoom_city(DIALOG)
ON ACTION cust_query
CALL cust_query(DIALOG)
ON ACTION cust_first
CALL cust_move_to(DIALOG, 1)
ON ACTION cust_previous
CALL cust_move_to(DIALOG, cust_curr - 1)
ON ACTION cust_next
CALL cust_move_to(DIALOG, cust_curr + 1)
ON ACTION cust_last
CALL cust_move_to(DIALOG, customers.getLength())
ON ACTION dialogtouched
CALL cust_touched(DIALOG)
ON ACTION cust_append
IF NOT cust_append(DIALOG) THEN CONTINUE DIALOG END IF
NEXT FIELD cust_name
ON ACTION cust_delete -- VALIDATE=NO
CALL cust_delete(DIALOG)
DISPLAY BY NAME cust.*
ON ACTION cust_undo -- VALIDATE=NO
CALL cust_undo(DIALOG)
DISPLAY BY NAME cust.*
ON ACTION cust_save
IF NOT cust_save(DIALOG) THEN CONTINUE DIALOG END IF
ON ACTION ord_append
CALL ord_append(DIALOG)
ON ACTION ord_modify
CALL ord_modify(DIALOG)
ON ACTION ord_delete
CALL ord_delete(DIALOG)
ON ACTION close
IF cust_check_changed(DIALOG) THEN EXIT DIALOG END IF
END DIALOG
END MAIN
FUNCTION cust_zoom_city(d)
DEFINE d ui.Dialog
DEFINE s INTEGER
DEFINE name, state STRING
CALL select_city(d.getFieldBuffer("cust_city")) RETURNING s, name, state
IF s==0 THEN
LET cust.cust_city = name
LET cust.cust_state = state
CALL d.setFieldTouched("cust_city",TRUE)
CALL d.setFieldTouched("cust_state",TRUE)
CALL cust_touched(d)
END IF
END FUNCTION
FUNCTION cust_touched(d)
DEFINE d ui.Dialog
LET cust_state = STATE_MODIFY
CALL cust_setup(d)
END FUNCTION
FUNCTION cust_check_changed(d)
DEFINE d ui.Dialog
DEFINE r INTEGER
IF cust_modifying() THEN
CALL cust_setup(d)
LET r = mbox_ync(otitle,"Do you want to save your changes?","question")
IF r == 1 THEN
IF NOT cust_save(d) THEN LET r = 3 END IF
END IF
IF r == 2 THEN
CALL cust_clean(d)
END IF
RETURN (r == 1 OR r == 2)
END IF
RETURN TRUE
END FUNCTION
FUNCTION cust_modifying()
RETURN cust_state != STATE_NAVIGATE
END FUNCTION
FUNCTION cust_undo(d)
DEFINE d ui.Dialog
CASE cust_state
WHEN STATE_MODIFY
IF NOT mbox_yn(otitle,"Undo customer modification?","question") THEN RETURN END IF
LET cust.* = old_cust.*
CALL cust_clean(d)
WHEN STATE_APPEND
IF NOT mbox_yn(otitle,"Cancel customer creation?","question") THEN RETURN END IF
CALL customers.deleteElement(customers.getLength())
CALL cust_clean(d)
CALL cust_move_to(d, customers.getLength())
END CASE
END FUNCTION
FUNCTION cust_verify_address_format(value)
DEFINE value STRING
DEFINE num STRING, p,x INTEGER
IF value IS NULL THEN RETURN TRUE END IF
LET p = value.getIndexOf(",", 1)
IF p == 0 THEN RETURN FALSE END IF
LET num = value.subString(1, p)
LET x = num
IF x <= 0 THEN RETURN FALSE END IF
IF value.getLength() <= p THEN RETURN FALSE END IF
RETURN TRUE
END FUNCTION
FUNCTION cust_check_name(d)
DEFINE d ui.Dialog
IF LENGTH(cust.cust_name) = 0 THEN
CALL mbox_ok(otitle,"You must specify a customer name.","stop")
RETURN FALSE
END IF
IF cust.cust_name MATCHES "*[@#$%^&]*" THEN
CALL mbox_ok(otitle,"The customer name containts invalid characters.","stop")
RETURN FALSE
END IF
RETURN TRUE
END FUNCTION
FUNCTION cust_check_address(d)
DEFINE d ui.Dialog
IF NOT cust_verify_address_format(cust.cust_address) THEN
CALL mbox_ok(otitle,"Address must hold a number followed by a comma and the street name.","stop")
RETURN FALSE
END IF
RETURN TRUE
END FUNCTION
FUNCTION cust_check_city(d)
DEFINE d ui.Dialog
-- Inter-dependent field validation
IF cust.cust_city IS NULL AND cust.cust_zipcode IS NOT NULL THEN
CALL mbox_ok(otitle,"You must specify a city because zipcode is not null.","stop")
RETURN FALSE
END IF
RETURN TRUE
END FUNCTION
FUNCTION cust_check_data(d)
DEFINE d ui.Dialog
IF d.validate("custrec.*") < 0 THEN
RETURN "*"
END IF
IF NOT cust_check_name(d) THEN RETURN "custrec.cust_name" END IF
IF NOT cust_check_city(d) THEN RETURN "custrec.cust_city" END IF
IF NOT cust_check_address(d) THEN RETURN "custrec.cust_address" END IF
RETURN NULL -- data is valid
END FUNCTION
FUNCTION cust_check_fields(d)
DEFINE d ui.Dialog
DEFINE fn STRING
LET fn = cust_check_data(d)
IF fn IS NULL THEN RETURN TRUE END IF
IF fn != "*" THEN CALL d.nextField(fn) END IF
RETURN FALSE
END FUNCTION
FUNCTION cust_save(d)
DEFINE d ui.Dialog
IF NOT cust_check_fields(d) THEN RETURN FALSE END IF
IF cust_state == STATE_APPEND THEN
-- SQL INSERT ...
END IF
IF cust_state == STATE_MODIFY THEN
-- SQL UPDATE ...
END IF
LET customers[cust_curr].* = cust.*
CALL cust_clean(d)
CALL mbox_ok("Customers", "Changes saved to database", "information")
CALL d.setFieldTouched("custrec.*",FALSE)
CALL cust_setup(d)
RETURN TRUE
END FUNCTION
FUNCTION cust_append(d)
DEFINE d ui.Dialog
IF NOT cust_autosave(d) THEN RETURN FALSE END IF
INITIALIZE cust.* TO NULL
CALL ord_loadlist(d,0,0)
LET cust.cust_id = cust_curr + 101044
LET cust_state = STATE_APPEND
CALL customers.appendElement()
LET cust_curr = customers.getLength()
CALL cust_setup(d)
RETURN TRUE
END FUNCTION
FUNCTION cust_delete(d)
DEFINE d ui.Dialog
IF cust_curr<= 0 THEN RETURN END IF
IF cust_modifying() THEN
CALL cust_setup(d)
IF NOT mbox_yn(otitle,"Cancel current changes?","question") THEN RETURN END IF
CALL cust_clean(d)
ELSE
IF NOT mbox_yn(otitle,"Delete current customer?","question") THEN RETURN END IF
END IF
-- SQL DELETE ...
CALL customers.deleteElement(cust_curr)
IF customers.getLength()==0 THEN
CALL mbox_ok(otitle,"No more customers in list, switching to query mode","stop")
CALL cust_query(d)
RETURN
END IF
IF cust_curr>customers.getLength() THEN
LET cust_curr=customers.getLength()
END IF
CALL cust_move_to(d,cust_curr)
CALL cust_setup(d)
END FUNCTION
FUNCTION cust_query(d)
DEFINE d ui.Dialog
DEFINE sql STRING
IF NOT cust_check_changed(d) THEN RETURN END IF
DISPLAY "Enter customer search criterias" TO cust_info
WHILE TRUE
LET int_flag = FALSE
CONSTRUCT BY NAME sql ON cust_id, cust_name, cust_state,
cust_city, cust_zipcode, cust_address, cust_checked
ATTRIBUTES( CANCEL=FALSE, ACCEPT=FALSE )
ON ACTION close
IF mbox_yn(otitle,"Quit application?","question") THEN EXIT PROGRAM END IF
ON ACTION cust_fetch
ACCEPT CONSTRUCT
END CONSTRUCT
IF NOT int_flag THEN
-- Dummy query, should use database query instead.
CALL cust_loadlist(d,LENGTH(sql))
CALL cust_setup(d)
CALL mbox_ok(otitle,SFMT("Found %1 customer records in database",customers.getLength()),"information")
EXIT WHILE
END IF
END WHILE
END FUNCTION
FUNCTION cust_setup(d)
DEFINE d ui.Dialog
DEFINE has_rows, on_first, on_last, cust_ro INTEGER
LET has_rows = (cust_curr > 0 AND customers.getLength()>0)
LET on_first = (cust_curr == 1)
LET on_last = (cust_curr == customers.getLength())
LET cust_ro = (has_rows AND cust_state == STATE_NAVIGATE)
CALL d.setActionActive("dialogtouched", cust_ro)
CALL d.setActionActive("cust_undo", NOT cust_ro)
CALL d.setActionActive("cust_save", NOT cust_ro)
CALL d.setActionActive("cust_append", TRUE) -- always active
CALL d.setActionActive("cust_delete", has_rows) -- can delete creation
CALL d.setActionActive("cust_query", TRUE) -- always active
CALL d.setActionActive("cust_first", NOT on_first)
CALL d.setActionActive("cust_previous", NOT on_first)
CALL d.setActionActive("cust_next", NOT on_last)
CALL d.setActionActive("cust_last", NOT on_last)
CALL d.setActionActive("ord_append", cust_ro)
CALL d.setActionActive("ord_modify", cust_ro)
CALL d.setActionActive("ord_delete", cust_ro)
CASE cust_state
WHEN STATE_APPEND
DISPLAY "Creating a new customer record (save with ctrl-s)" TO cust_info
WHEN STATE_MODIFY
DISPLAY "Modifying current customer record (save with ctrl-s)" TO cust_info
OTHERWISE
DISPLAY SFMT("Customer record %1 / %2",cust_curr,customers.getLength()) TO cust_info
END CASE
END FUNCTION
FUNCTION cust_clean(d)
DEFINE d ui.Dialog
LET old_cust.* = cust.*
LET cust_state = STATE_NAVIGATE
CALL cust_setup(d)
END FUNCTION
FUNCTION cust_autosave(d)
DEFINE d ui.Dialog
IF cust_modifying() THEN
IF NOT cust_save(d) THEN RETURN FALSE END IF
END IF
RETURN TRUE
END FUNCTION
FUNCTION cust_move_to(d,index)
DEFINE d ui.Dialog
DEFINE index INTEGER
IF NOT cust_autosave(d) THEN RETURN END IF
IF index > customers.getLength() THEN
LET index = customers.getLength()
END IF
IF index < 1 THEN
LET index = 1
END IF
LET cust.* = customers[index].*
LET old_cust.* = cust.*
LET cust_curr = index
MESSAGE SFMT("Customer: %1/%2", cust_curr, customers.getLength())
CALL ord_loadlist(d, customers[index].cust_id, (index mod 5) * 3)
CALL d.setCurrentRow("ordlist",1)
CALL cust_setup(d)
END FUNCTION
-- Dummy function to fill the list of customers (normally, SELECT from database)
FUNCTION cust_loadlist(d,m)
DEFINE d ui.Dialog
DEFINE m,i INTEGER
CALL customers.clear()
FOR i=1 TO m
LET customers[i].cust_id = i
LET customers[i].cust_name = "Name " || i
LET customers[i].cust_state = "CA"
LET customers[i].cust_city = "San Fransisco"
LET customers[i].cust_zipcode = 94102
LET customers[i].cust_address = "52, Marlon Street"
LET customers[i].cust_checked = i mod 2
END FOR
IF customers.getLength()>0 THEN
LET cust_curr = 1
LET cust.* = customers[cust_curr].*
CALL ord_loadlist(d, customers[cust_curr].cust_id, (cust_curr mod 5) * 3)
ELSE
LET cust_curr = 0
INITIALIZE cust.* TO NULL
CALL ord_loadlist(d, 0, 0)
END IF
CALL cust_clean(d)
END FUNCTION
FUNCTION ord_loadlist(d,mid,m)
DEFINE d ui.Dialog
DEFINE mid,m,i INTEGER
CALL orders.clear()
FOR i=1 TO m
CALL orders.appendElement()
LET orders[i].ord_id = mid * 100 + i
LET orders[i].ord_crea = CURRENT YEAR TO SECOND
LET orders[i].ord_amount = ( i mod 5 ) * 133.56
END FOR
END FUNCTION
FUNCTION ord_append(d)
DEFINE d ui.Dialog
IF NOT cust_check_changed(d) THEN RETURN END IF
CALL ord_input('A', cust.cust_id, cust.cust_name, 0)
END FUNCTION
FUNCTION ord_modify(d)
DEFINE d ui.Dialog
IF NOT cust_check_changed(d) THEN RETURN END IF
IF d.getCurrentRow("ordlist")==0 THEN RETURN END IF
CALL ord_input('M', cust.cust_id, cust.cust_name, orders[d.getCurrentRow("ordlist")].ord_id)
END FUNCTION
FUNCTION ord_delete(d)
DEFINE d ui.Dialog
IF NOT cust_check_changed(d) THEN RETURN END IF
IF NOT mbox_yn(otitle,"Delete select order from database?","question") THEN
RETURN
END IF
CALL d.deleteRow("ordlist",d.getCurrentRow("ordlist"))
END FUNCTION
FUNCTION combo_fill_states(cb)
DEFINE cb ui.ComboBox
CALL cb.addItem("AK","Alaska")
CALL cb.addItem("AL","Alabama")
CALL cb.addItem("AR","Arkansas")
CALL cb.addItem("AZ","Arizona")
CALL cb.addItem("CA","California")
CALL cb.addItem("CO","Colorado")
CALL cb.addItem("CT","Connecticut")
CALL cb.addItem("DC","D.C.")
CALL cb.addItem("DE","Delaware")
CALL cb.addItem("FL","Florida")
CALL cb.addItem("GA","Georgia")
CALL cb.addItem("HI","Hawaii")
CALL cb.addItem("IA","Iowa")
CALL cb.addItem("ID","Idaho")
CALL cb.addItem("IL","Illinois")
CALL cb.addItem("IN","Indiana")
CALL cb.addItem("KS","Kansas")
CALL cb.addItem("KY","Kentucky")
CALL cb.addItem("LA","Louisiana")
CALL cb.addItem("MA","Massachusetts")
CALL cb.addItem("MD","Maryland")
CALL cb.addItem("ME","Maine")
CALL cb.addItem("MI","Michigan")
CALL cb.addItem("MN","Minnesota")
CALL cb.addItem("MO","Missouri")
CALL cb.addItem("MS","Mississippi")
CALL cb.addItem("MT","Montana")
CALL cb.addItem("NC","North Carolina")
CALL cb.addItem("ND","North Dakota")
CALL cb.addItem("NE","Nebraska")
CALL cb.addItem("NH","New Hampshire")
CALL cb.addItem("NJ","New Jersey")
CALL cb.addItem("NM","New Mexico")
CALL cb.addItem("NV","Nevada")
CALL cb.addItem("NY","New York")
CALL cb.addItem("OH","Ohio")
CALL cb.addItem("OK","Oklahoma")
CALL cb.addItem("OR","Oregon")
CALL cb.addItem("PA","Pennsylvania")
CALL cb.addItem("PR","Puerto Rico")
CALL cb.addItem("RI","Rhode Island")
CALL cb.addItem("SC","South Carolina")
CALL cb.addItem("SD","South Dakota")
CALL cb.addItem("TN","Tennessee")
CALL cb.addItem("TX","Texas")
CALL cb.addItem("UT","Utah")
CALL cb.addItem("VA","Virginia")
CALL cb.addItem("VT","Vermont")
CALL cb.addItem("WA","Washington")
CALL cb.addItem("WI","Wisconsin")
CALL cb.addItem("WV","West Virginia")
CALL cb.addItem("WY","Wyoming")
END FUNCTION
@@ -0,0 +1,133 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
ACTION DEFAULTS
ACTION cust_query ( IMAGE="find", COMMENT = "Query customers", ACCELERATOR = control-q )
ACTION cust_append ( IMAGE="new", COMMENT = "Create new customer", ACCELERATOR = control-n )
ACTION cust_undo ( IMAGE="refresh", COMMENT = "Undo last changes", ACCELERATOR = control-u, VALIDATE = NO )
ACTION cust_save ( IMAGE="disk", COMMENT = "Save changes in database", ACCELERATOR = control-s )
ACTION cust_delete ( IMAGE="eraser", COMMENT = "Delete current customer", ACCELERATOR = control-d, VALIDATE = NO )
ACTION cust_fetch ( IMAGE="services", COMMENT = "Search records in database", ACCELERATOR = enter, ACCELERATOR2 = return )
ACTION cust_first ( IMAGE = "gobegin", COMMENT = "Move to first customer in list")
ACTION cust_previous ( IMAGE = "gorev", COMMENT = "Move to previous customer in list")
ACTION cust_next ( IMAGE = "goforw", COMMENT = "Move to next customer in list")
ACTION cust_last ( IMAGE = "goend", COMMENT = "Move to last customer in list")
ACTION cust_print ( IMAGE="printer", COMMENT = "Print customer")
ACTION ord_append ( IMAGE = "new", COMMENT="Add a new order")
ACTION ord_modify ( IMAGE = "pen", COMMENT="Modify selected order")
ACTION ord_delete ( IMAGE = "eraser", COMMENT="Delete selected order")
END
TOPMENU
GROUP cust ( TEXT="Customers" )
COMMAND cust_query ( TEXT="Query" )
COMMAND cust_append ( TEXT="New" )
COMMAND cust_undo ( TEXT="Undo" )
COMMAND cust_save ( TEXT="Save" )
COMMAND cust_print ( TEXT="Print" )
SEPARATOR
COMMAND cust_fetch ( TEXT="Fetch" )
COMMAND cancel
SEPARATOR
COMMAND cust_first ( TEXT="First" )
COMMAND cust_previous ( TEXT="Previous" )
COMMAND cust_next ( TEXT="Next" )
COMMAND cust_last ( TEXT="Last" )
SEPARATOR
COMMAND close ( TEXT="Close" )
END
GROUP ord ( TEXT="Orders" )
COMMAND ord_append ( TEXT="Append" )
COMMAND ord_modify ( TEXT="Modify" )
COMMAND ord_delete ( TEXT="Delete" )
END
GROUP help ( TEXT="Help" )
COMMAND help
SEPARATOR
COMMAND about ( TEXT="About" )
END
END
LAYOUT ( STYLE="main2", TEXT = "Customers and Orders" )
GRID
{
<g g1 >
[f1 ][f2 ]
State: [f3 ] [f4 ]
City: [f5 ]
Zipcode: [f6 ]
Address: [f7 ]
[ ]
-------------------------------------------------------------------
[tr |cq:cx:cf:cp:cn:cl:ca:cd:cu:cs]
< >
<g g2 >
<t t1 >[oad]
[c1 |c2 |c3 ][omo]
[c1 |c2 |c3 ][ode]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
< >
< >
}
END
END
ATTRIBUTES
GROUP g1: TEXT = "Customer information";
EDIT f1 = FORMONLY.cust_id TYPE INTEGER, NOT NULL, NOENTRY;
EDIT f2 = FORMONLY.cust_name TYPE VARCHAR, NOT NULL, REQUIRED;
COMBOBOX f3 = FORMONLY.cust_state TYPE VARCHAR, NOT NULL, REQUIRED,
QUERYEDITABLE, DEFAULT="CA", INITIALIZER=combo_fill_states;
CHECKBOX f4 = FORMONLY.cust_checked TYPE CHAR, TEXT="Checked";
BUTTONEDIT f5 = FORMONLY.cust_city TYPE CHAR, NOT NULL, REQUIRED,
ACTION=zoom_city, IMAGE="find";
EDIT f6 = FORMONLY.cust_zipcode TYPE VARCHAR, NOT NULL, REQUIRED,
COMMENT="Possible values: 10000 to 99999",
INCLUDE=(10000 to 99999);
TEXTEDIT f7 = FORMONLY.cust_address TYPE CHAR,
COMMENT="Must hold '<number>, <street name>'";
LABEL tr = FORMONLY.cust_info, STYLE="information";
BUTTON cq: cust_query, TABINDEX=0;
BUTTON cx: cust_fetch, TABINDEX=0;
BUTTON cf: cust_first, TABINDEX=0;
BUTTON cp: cust_previous, TABINDEX=0;
BUTTON cn: cust_next, TABINDEX=0;
BUTTON cl: cust_last, TABINDEX=0;
BUTTON ca: cust_append, TABINDEX=0;
BUTTON cd: cust_delete, TABINDEX=0;
BUTTON cu: cust_undo, TABINDEX=0;
BUTTON cs: cust_save, TABINDEX=0;
GROUP g2: TEXT = "Orders";
TABLE t1: DOUBLECLICK=ord_modify;
EDIT c1 = FORMONLY.ord_id TYPE INTEGER, TITLE = "Id";
EDIT c2 = FORMONLY.ord_crea TYPE DATE, TITLE = "Creation";
EDIT c3 = FORMONLY.ord_amount TYPE MONEY(15,2), TITLE = "Amount";
BUTTON oad: ord_append, TABINDEX=0;
BUTTON omo: ord_modify, TABINDEX=0;
BUTTON ode: ord_delete, TABINDEX=0;
END
INSTRUCTIONS
SCREEN RECORD custrec (FORMONLY.cust_id THROUGH FORMONLY.cust_address);
SCREEN RECORD ordlist (FORMONLY.ord_id THROUGH FORMONLY.ord_amount);
END
@@ -0,0 +1,432 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
IMPORT FGL CityList
IMPORT FGL OrderInput
IMPORT FGL MsgBox
GLOBALS "Globals.4gl"
DEFINE cust, old_cust custrec
DEFINE cust_curr INT -- current position in the array (scroll cursor)
DEFINE cust_touched INT -- set if something in the customer record was changed
DEFINE cust_new INT -- > 0 if new customer is currently edited
CONSTANT mytitle = "Customer"
CONSTANT S_MOVE = "move"
CONSTANT S_NEW = "new"
CONSTANT S_CLOSE = "close"
CONSTANT S_QUERY = "query"
CONSTANT S_SAVE = "save"
MAIN
DEFINE state STRING -- requested state in the dialog
DEFINE req_pos INT -- required position in the dialog
DEFINE del INT -- delete flag
OPTIONS INPUT WRAP
OPEN FORM f_cust FROM "CustOrders2"
DISPLAY FORM f_cust
CALL cust_loadlist(9)
LET state=S_MOVE
LET cust_curr=1
WHILE state<>S_CLOSE
DISPLAY "WHILE,state=",state,",cust_curr():",cust_curr,",req_pos:",req_pos
IF cust_len() == 0 OR state == S_NEW THEN
LET cust_curr=cust_len()+1
LET cust_new=cust_curr
ELSE
LET cust_new=0
CALL cust_fetch(req_pos)
END IF
LET state=S_MOVE
LET req_pos=cust_curr
LET cust_touched=0
DIALOG ATTRIBUTES(FIELD ORDER FORM, UNBUFFERED)
INPUT cust.* FROM custrec.* ATTRIBUTES(WITHOUT DEFAULTS=NOT cust_new)
AFTER FIELD cust_name
IF cust.cust_name MATCHES "*[@#$%^&]*" THEN
ERROR "The customer name contains invalid characters."
NEXT FIELD cust_name
END IF
AFTER FIELD cust_address
IF NOT cust_check_address(cust.cust_address) THEN
NEXT FIELD cust_address
END IF
AFTER INPUT
IF cust.cust_city IS NULL AND cust.cust_zipcode IS NOT NULL THEN
CALL mbox_ok(mytitle,"You must specify a city because zipcode is not null.","stop")
NEXT FIELD cust_city
END IF
END INPUT
DISPLAY ARRAY orders TO ordlist.*
BEFORE DISPLAY
IF cust_new THEN
CALL mbox_ok(mytitle,"Please save the new customer before adding orders!","stop")
NEXT FIELD cust_name
END IF
BEFORE ROW
MESSAGE SFMT("Order record %1 / %2", DIALOG.getCurrentRow("ordlist"), DIALOG.getArrayLength("ordlist"))
ON ACTION ord_append
CALL ord_append(DIALOG)
ON ACTION ord_modify
CALL ord_modify(DIALOG)
ON ACTION ord_delete
CALL ord_delete(DIALOG)
END DISPLAY
BEFORE DIALOG
CALL cust_setup(DIALOG)
CALL order_of_cust(cust_curr)
IF cust_new THEN
LET old_cust.* = cust.*
END IF
ON ACTION zoom_city
CALL cust_zoom_city(DIALOG)
ON ACTION cust_first
LET req_pos=1
LET state=S_MOVE ; ACCEPT DIALOG
ON ACTION cust_previous
LET req_pos=cust_curr-1
LET state=S_MOVE ; ACCEPT DIALOG
ON ACTION cust_next
LET req_pos=cust_curr+1
LET state=S_MOVE ; ACCEPT DIALOG
ON ACTION cust_last
LET req_pos=cust_len()
LET state=S_MOVE ; ACCEPT DIALOG
ON ACTION cust_append
LET state=S_NEW ; ACCEPT DIALOG
ON ACTION cust_query --we can perform a safe query also only after
LET state=S_QUERY ; ACCEPT DIALOG --the validations have been done
ON ACTION cust_delete
IF cust_len() > 0 THEN
LET del=0
IF cust_new AND NOT cust_touched THEN
LET del=1
ELSE
IF mbox_yn(mytitle,"Delete current customer?","question") THEN
LET del=1
END IF
END IF
IF del THEN
CALL customers.deleteElement(cust_curr)
-- normally a
-- IF cust_id>0 THEN
-- DELETE from customers WHERE cust_id=cust.cust_id
-- END IF
LET state=S_MOVE
EXIT DIALOG
END IF
END IF
ON ACTION dialogtouched
LET cust_touched=1
CALL cust_setup(DIALOG)
ON ACTION cust_undo
LET cust.* = old_cust.*
DISPLAY BY NAME cust.*
CALL DIALOG.setFieldTouched("custrec.*",FALSE)
LET cust_touched=0
CALL cust_setup(DIALOG)
ON ACTION cust_save
LET state=S_SAVE ; ACCEPT DIALOG
ON ACTION cancel
LET state=S_CLOSE
--close is a bit difficult: if we did not touch something,
--we can go out safely, otherwise we need to duplicate things
--we do normally in AFTER DIALOG
IF cust_touched THEN
--we touched something
CASE mbox_ync(mytitle,"Save your changes?","question")
WHEN 1 --yes
ACCEPT DIALOG --go thru the validation and fall finally out
--in AFTER DIALOG
WHEN 2 --no
EXIT DIALOG
WHEN 3 --cancel
CONTINUE DIALOG
END CASE
ELSE
--nothing changed, just go out
EXIT DIALOG
END IF
AFTER DIALOG
IF state=S_CLOSE OR state=S_SAVE THEN
--we already confirmed the save, jump to the save code
GOTO savedb
ELSE
IF old_cust.* <> cust.* THEN
CASE mbox_ync(mytitle,"Save your changes?","question")
WHEN 1 --yes
GOTO savedb
WHEN 3 --cancel and stay in the dialog
LET state=S_MOVE ; CONTINUE DIALOG
END CASE
END IF
END IF
EXIT DIALOG
LABEL savedb: --use the label here to save a local var and some IF's
IF cust_new THEN
DISPLAY "INSERT new customer into db"
LET customers[cust_new].* = cust.*
ELSE
DISPLAY "UPDATE customer in db"
LET customers[cust_curr].* = cust.*
END IF
END DIALOG
--we can perform the query action only if everything was validated before
IF state == S_QUERY THEN
CALL cust_query()
END IF
END WHILE
END MAIN
FUNCTION cust_len()
--we use the 'customers' array as a db replacement here
--normally you could return the real COUNT of the DB statement cursor
RETURN customers.getLength()
END FUNCTION
FUNCTION cust_fetch(index)
DEFINE index INTEGER
IF index > cust_len() THEN
LET index = cust_len()
END IF
IF index < 1 THEN
LET index = 1
END IF
--fetch customer from the DB, index could be used to FETCH ABSOLUTE
LET cust.* = customers[index].*
LET old_cust.* = cust.*
LET cust_curr = index
MESSAGE SFMT("Fetched Customer: %1/%2", cust_curr, cust_len())
END FUNCTION
FUNCTION cust_setup(d)
DEFINE d ui.Dialog
DEFINE has_rows, on_first, on_last, cust_ro INTEGER
LET has_rows = (cust_curr > 0 AND cust_len() > 0 )
LET on_first = (cust_curr == 1)
LET on_last = (cust_curr == cust_len())
LET cust_ro = (has_rows AND NOT cust_touched)
CALL d.setActionActive("dialogtouched", cust_ro)
CALL d.setActionActive("cust_undo", NOT cust_ro)
CALL d.setActionActive("cust_save", NOT cust_ro)
CALL d.setActionActive("cust_append", TRUE) -- always active
CALL d.setActionActive("cust_delete", has_rows) -- can delete creation
CALL d.setActionActive("cust_query", TRUE) -- always active
CALL d.setActionActive("cust_first", NOT on_first)
CALL d.setActionActive("cust_previous", NOT on_first)
CALL d.setActionActive("cust_next", NOT on_last)
CALL d.setActionActive("cust_last", NOT on_last)
IF cust_new THEN
DISPLAY "New customer (save with ctrl-s)" TO cust_info
ELSE
IF cust_touched THEN
DISPLAY "Modifying customer (save with ctrl-s)" TO cust_info
ELSE
DISPLAY SFMT("Customer record %1 / %2",cust_curr,cust_len()) TO cust_info
END IF
END IF
END FUNCTION
FUNCTION cust_query()
DEFINE sql STRING
DISPLAY "Enter customer search criterias" TO cust_info
LET int_flag = FALSE
CONSTRUCT BY NAME sql ON cust_id, cust_name, cust_state,
cust_city, cust_zipcode, cust_address, cust_checked
IF NOT int_flag THEN
-- Dummy query, should use database query instead.
CALL cust_loadlist(LENGTH(sql))
CALL mbox_ok(mytitle,SFMT("Found %1 customer records in database",cust_len()),"information")
END IF
END FUNCTION
FUNCTION cust_zoom_city(d)
DEFINE d ui.Dialog
DEFINE s INTEGER
DEFINE name, state STRING
CALL select_city(d.getFieldBuffer("cust_city")) RETURNING s, name, state
IF s==0 THEN
LET cust.cust_city = name
LET cust.cust_state = state
CALL d.setFieldTouched("cust_city",TRUE)
CALL d.setFieldTouched("cust_state",TRUE)
LET cust_touched = TRUE
CALL cust_setup(d)
END IF
END FUNCTION
FUNCTION cust_verify_address_format(value)
DEFINE value STRING
DEFINE num STRING, p,x INTEGER
IF value IS NULL THEN RETURN TRUE END IF
LET p = value.getIndexOf(",", 1)
IF p == 0 THEN RETURN FALSE END IF
LET num = value.subString(1, p)
LET x = num
IF x <= 0 THEN RETURN FALSE END IF
IF value.getLength() <= p THEN RETURN FALSE END IF
RETURN TRUE
END FUNCTION
FUNCTION cust_check_address(value)
DEFINE value STRING
IF NOT cust_verify_address_format(value) THEN
CALL mbox_ok(mytitle,"Address must hold a number followed by a comma and the street name.","stop")
RETURN FALSE
END IF
RETURN TRUE
END FUNCTION
FUNCTION order_of_cust(index)
DEFINE index INTEGER --index should be normally the cust_id
--MESSAGE SFMT("Customer: %1/%2", cust_curr, cust_len())
IF index >= 1 AND index <= cust_len() THEN
CALL ord_loadlist(customers[index].cust_id, (index mod 5) * 3)
ELSE
CALL orders.clear()
END IF
END FUNCTION
-- Dummy function to fill the list of customers (normally, SELECT from database)
FUNCTION cust_loadlist(m)
DEFINE m,i INTEGER
CALL customers.clear()
FOR i=1 TO m
LET customers[i].cust_id = i
LET customers[i].cust_name = "Name " || i
LET customers[i].cust_state = "CA"
LET customers[i].cust_city = "San Fransisco"
LET customers[i].cust_zipcode = 94102
LET customers[i].cust_address = "52, Marlon Street"
LET customers[i].cust_checked = i mod 2
END FOR
END FUNCTION
FUNCTION ord_loadlist(mid,m)
DEFINE mid,m,i INTEGER
CALL orders.clear()
FOR i=1 TO m
CALL orders.appendElement()
LET orders[i].ord_id = mid * 100 + i
LET orders[i].ord_crea = CURRENT YEAR TO SECOND
LET orders[i].ord_amount = ( i mod 5 ) * 133.56
END FOR
END FUNCTION
FUNCTION ord_append(d)
DEFINE d ui.Dialog
CALL ord_input('A', cust.cust_id, cust.cust_name, 0)
END FUNCTION
FUNCTION ord_modify(d)
DEFINE d ui.Dialog
IF d.getCurrentRow("ordlist")==0 THEN RETURN END IF
CALL ord_input('M', cust.cust_id, cust.cust_name, orders[d.getCurrentRow("ordlist")].ord_id)
END FUNCTION
FUNCTION ord_delete(d)
DEFINE d ui.Dialog
IF NOT mbox_yn(mytitle,"Delete select order from database?","question") THEN
RETURN
END IF
CALL d.deleteRow("ordlist",d.getCurrentRow("ordlist"))
END FUNCTION
FUNCTION combo_fill_states(cb)
DEFINE cb ui.ComboBox
CALL cb.addItem("AK","Alaska")
CALL cb.addItem("AL","Alabama")
CALL cb.addItem("AR","Arkansas")
CALL cb.addItem("AZ","Arizona")
CALL cb.addItem("CA","California")
CALL cb.addItem("CO","Colorado")
CALL cb.addItem("CT","Connecticut")
CALL cb.addItem("DC","D.C.")
CALL cb.addItem("DE","Delaware")
CALL cb.addItem("FL","Florida")
CALL cb.addItem("GA","Georgia")
CALL cb.addItem("HI","Hawaii")
CALL cb.addItem("IA","Iowa")
CALL cb.addItem("ID","Idaho")
CALL cb.addItem("IL","Illinois")
CALL cb.addItem("IN","Indiana")
CALL cb.addItem("KS","Kansas")
CALL cb.addItem("KY","Kentucky")
CALL cb.addItem("LA","Louisiana")
CALL cb.addItem("MA","Massachusetts")
CALL cb.addItem("MD","Maryland")
CALL cb.addItem("ME","Maine")
CALL cb.addItem("MI","Michigan")
CALL cb.addItem("MN","Minnesota")
CALL cb.addItem("MO","Missouri")
CALL cb.addItem("MS","Mississippi")
CALL cb.addItem("MT","Montana")
CALL cb.addItem("NC","North Carolina")
CALL cb.addItem("ND","North Dakota")
CALL cb.addItem("NE","Nebraska")
CALL cb.addItem("NH","New Hampshire")
CALL cb.addItem("NJ","New Jersey")
CALL cb.addItem("NM","New Mexico")
CALL cb.addItem("NV","Nevada")
CALL cb.addItem("NY","New York")
CALL cb.addItem("OH","Ohio")
CALL cb.addItem("OK","Oklahoma")
CALL cb.addItem("OR","Oregon")
CALL cb.addItem("PA","Pennsylvania")
CALL cb.addItem("PR","Puerto Rico")
CALL cb.addItem("RI","Rhode Island")
CALL cb.addItem("SC","South Carolina")
CALL cb.addItem("SD","South Dakota")
CALL cb.addItem("TN","Tennessee")
CALL cb.addItem("TX","Texas")
CALL cb.addItem("UT","Utah")
CALL cb.addItem("VA","Virginia")
CALL cb.addItem("VT","Vermont")
CALL cb.addItem("WA","Washington")
CALL cb.addItem("WI","Wisconsin")
CALL cb.addItem("WV","West Virginia")
CALL cb.addItem("WY","Wyoming")
END FUNCTION
@@ -0,0 +1,105 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
ACTION DEFAULTS
ACTION close(VALIDATE=NO)
ACTION cancel(VALIDATE=NO)
ACTION cust_query ( IMAGE="find", COMMENT = "Query customers", ACCELERATOR = control-q )
ACTION cust_append ( IMAGE="new", COMMENT = "Create new customer", ACCELERATOR = control-n )
ACTION cust_undo ( IMAGE="refresh", COMMENT = "Undo last changes", ACCELERATOR = control-u, VALIDATE = NO )
ACTION cust_save ( IMAGE="disk", COMMENT = "Save changes in database", ACCELERATOR = control-s )
ACTION cust_delete ( IMAGE="eraser", COMMENT = "Delete current customer", ACCELERATOR = control-d, VALIDATE = NO )
ACTION cust_first ( IMAGE = "gobegin", COMMENT = "Move to first customer in list")
ACTION cust_previous ( IMAGE = "gorev", COMMENT = "Move to previous customer in list")
ACTION cust_next ( IMAGE = "goforw", COMMENT = "Move to next customer in list")
ACTION cust_last ( IMAGE = "goend", COMMENT = "Move to last customer in list")
ACTION cust_print ( IMAGE="printer", COMMENT = "Print customer")
ACTION ord_append ( IMAGE = "new", COMMENT="Add a new order")
ACTION ord_modify ( IMAGE = "pen", COMMENT="Modify selected order")
ACTION ord_delete ( IMAGE = "eraser", COMMENT="Delete selected order")
END
LAYOUT (TEXT = "Customers and Orders" )
GRID
{
<g g1 >
[f1 ][f2 ]
State: [f3 ] [f4 ]
City: [f5 ]
Zipcode: [f6 ]
Address: [f7 ]
[ ]
-------------------------------------------------------------------
[tr : :cq:cf:cp:cn:cl:ca:cd:cu:cs]
< >
<g g2 >
<t t1 >[oad]
[c1 |c2 |c3 ][omo]
[c1 |c2 |c3 ][ode]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
< >
< >
}
END
END
ATTRIBUTES
GROUP g1: TEXT = "Customer information";
EDIT f1 = FORMONLY.cust_id TYPE INTEGER, NOT NULL, NOENTRY;
EDIT f2 = FORMONLY.cust_name TYPE VARCHAR, NOT NULL, REQUIRED;
COMBOBOX f3 = FORMONLY.cust_state TYPE VARCHAR, NOT NULL, REQUIRED,
QUERYEDITABLE, DEFAULT="CA", INITIALIZER=combo_fill_states;
CHECKBOX f4 = FORMONLY.cust_checked TYPE CHAR, TEXT="Checked";
BUTTONEDIT f5 = FORMONLY.cust_city TYPE CHAR, NOT NULL, REQUIRED,
ACTION=zoom_city, IMAGE="find";
EDIT f6 = FORMONLY.cust_zipcode TYPE VARCHAR, NOT NULL, REQUIRED,
COMMENT="Possible values: 10000 to 99999",
INCLUDE=(10000 to 99999);
TEXTEDIT f7 = FORMONLY.cust_address TYPE CHAR,
COMMENT="Must hold '<number>, <street name>'";
LABEL tr = FORMONLY.cust_info, STYLE="information",SIZEPOLICY=FIXED;
BUTTON cq: cust_query, TABINDEX=0;
BUTTON cf: cust_first, TABINDEX=0;
BUTTON cp: cust_previous, TABINDEX=0;
BUTTON cn: cust_next, TABINDEX=0;
BUTTON cl: cust_last, TABINDEX=0;
BUTTON ca: cust_append, TABINDEX=0;
BUTTON cd: cust_delete, TABINDEX=0;
BUTTON cu: cust_undo, TABINDEX=0;
BUTTON cs: cust_save, TABINDEX=0;
GROUP g2: TEXT = "Orders";
TABLE t1: DOUBLECLICK=ord_modify;
EDIT c1 = FORMONLY.ord_id TYPE INTEGER, TITLE = "Id";
EDIT c2 = FORMONLY.ord_crea TYPE DATE, TITLE = "Creation";
EDIT c3 = FORMONLY.ord_amount TYPE MONEY(15,2), TITLE = "Amount";
BUTTON oad: ord_append, TABINDEX=0;
BUTTON omo: ord_modify, TABINDEX=0;
BUTTON ode: ord_delete, TABINDEX=0;
END
INSTRUCTIONS
SCREEN RECORD custrec (FORMONLY.cust_id THROUGH FORMONLY.cust_address);
SCREEN RECORD ordlist (FORMONLY.ord_id THROUGH FORMONLY.ord_amount);
END
@@ -0,0 +1,309 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
IMPORT FGL CityList
IMPORT FGL OrderInput
IMPORT FGL MsgBox
GLOBALS "Globals.4gl"
CONSTANT mytitle = "Customer"
DEFINE myd ui.Dialog
DEFINE cust_new INT
MAIN
DEFINE deleted INT
DEFINE oldcust custrec
OPTIONS INPUT WRAP
OPEN FORM f_cust FROM "CustOrders3"
DISPLAY FORM f_cust
CALL cust_loadlist(9)
DIALOG ATTRIBUTES(FIELD ORDER FORM, UNBUFFERED)
INPUT ARRAY customers FROM custrec.* ATTRIBUTES(INSERT ROW=0)
BEFORE ROW
CALL order_of_cust(arr_curr())
CALL cust_setup(0)
LET oldcust.*=customers[arr_curr()].*
AFTER FIELD cust_name
IF customers[arr_curr()].cust_name MATCHES "*[@#$%^&]*" THEN
ERROR "The customer name contains invalid characters."
NEXT FIELD cust_name
END IF
AFTER FIELD cust_address
IF NOT cust_check_address(customers[arr_curr()].cust_address) THEN
NEXT FIELD cust_address
END IF
BEFORE INSERT
LET cust_new=1
CALL cust_setup(0)
LET oldcust.*=customers[arr_curr()].*
CALL order_of_cust(arr_curr())
AFTER ROW
--be aware of the temp row...
IF arr_curr()<= arr_count() AND NOT deleted THEN
IF customers[arr_curr()].cust_city IS NULL AND customers[arr_curr()].cust_zipcode IS NOT NULL THEN
ERROR "You must specify a city because zipcode is not null."
NEXT FIELD cust_city
END IF
IF FIELD_TOUCHED(custrec.*) THEN
IF cust_new THEN
--INSERT INTO ....
DISPLAY sfmt("INSERT into DB for name:%1",customers[arr_curr()].cust_name)
MESSAGE sfmt("INSERT into DB for name:%1",customers[arr_curr()].cust_name)
ELSE
--UPDATE
DISPLAY sfmt("UPDATE DB for name:%1",customers[arr_curr()].cust_name)
MESSAGE sfmt("UPDATE DB for name:%1",customers[arr_curr()].cust_name)
END IF
IF customers[arr_curr()].cust_name="err" THEN --simulate error
CALL mbox_ok("Error","Can't write record to DB","stop")
NEXT FIELD cust_name
END IF
END IF
END IF
LET deleted=0
LET cust_new=0
BEFORE DELETE
IF NOT cust_new THEN
IF NOT mbox_yn(mytitle,"Delete current customer?","question") THEN
CANCEL DELETE
ELSE
DISPLAY "DELETE from DB"
MESSAGE "DELETED from DB:",customers[arr_curr()].cust_name
END IF
END IF
AFTER DELETE
LET deleted=1
CALL cust_setup(0)
ON ACTION zoom_city
CALL cust_zoom_city()
ON ACTION undo
DISPLAY BY NAME oldcust.*
LET customers[arr_curr()].* = oldcust.*
CALL DIALOG.setFieldTouched("custrec.*",FALSE)
CALL cust_setup(0)
ON ACTION dialogtouched
CALL cust_setup(1)
END INPUT
DISPLAY ARRAY orders TO ordlist.*
BEFORE ROW
MESSAGE SFMT("Order record %1 / %2", ARR_CURR(), ARR_COUNT())
ON ACTION append
CALL ord_append()
ON ACTION ord_modify
CALL ord_modify()
ON ACTION delete
CALL ord_delete()
END DISPLAY
BEFORE DIALOG
LET myd=DIALOG
ON ACTION cancel
IF FIELD_TOUCHED(custrec.*) THEN
CASE mbox_ync(mytitle,"Save your changes?","question")
WHEN 1 --yes
ACCEPT DIALOG --go thru the validation
WHEN 2 --no
EXIT DIALOG
WHEN 3 --cancel
CONTINUE DIALOG
END CASE
END IF
EXIT DIALOG
END DIALOG
LET myd=NULL
END MAIN
FUNCTION cust_setup(touched)
DEFINE touched INT
--because the dialogtouched/undo action is only defined in the INPUT ARRAY
--may run into an error if we try them to switch off in another context
CALL myd.setActionActive("dialogtouched",NOT touched)
CALL myd.setActionActive("undo", touched)
IF cust_new THEN
DISPLAY "Input new customer" TO cust_info
ELSE
DISPLAY SFMT("Customer record %1 / %2",c_arr_curr(),customers.getLength()) TO cust_info
END IF
END FUNCTION
FUNCTION c_arr_curr() RETURN myd.getCurrentRow("custrec") END FUNCTION
FUNCTION o_arr_curr() RETURN myd.getCurrentRow("ordlist") END FUNCTION
FUNCTION cust_zoom_city()
DEFINE s INTEGER
DEFINE name, state STRING
CALL select_city(myd.getFieldBuffer("cust_city")) RETURNING s, name, state
IF s==0 THEN
LET customers[c_arr_curr()].cust_city = name
LET customers[c_arr_curr()].cust_state = state
CALL myd.setFieldTouched("cust_city",TRUE)
CALL myd.setFieldTouched("cust_state",TRUE)
CALL cust_setup(1)
END IF
END FUNCTION
FUNCTION cust_verify_address_format(value)
DEFINE value STRING
DEFINE num STRING, p,x INTEGER
IF value IS NULL THEN RETURN TRUE END IF
LET p = value.getIndexOf(",", 1)
IF p == 0 THEN RETURN FALSE END IF
LET num = value.subString(1, p)
LET x = num
IF x <= 0 THEN RETURN FALSE END IF
IF value.getLength() <= p THEN RETURN FALSE END IF
RETURN TRUE
END FUNCTION
FUNCTION cust_check_address(value)
DEFINE value STRING
IF NOT cust_verify_address_format(value) THEN
CALL mbox_ok(mytitle,"Address must hold a number followed by a comma and the street name.","stop")
RETURN FALSE
END IF
RETURN TRUE
END FUNCTION
FUNCTION order_of_cust(index)
DEFINE index INTEGER
--MESSAGE SFMT("Customer: %1/%2", cust_curr, customers.getLength())
IF cust_new THEN
CALL orders.clear()
ELSE
CALL ord_loadlist(customers[index].cust_id, (index mod 5) * 3)
END IF
END FUNCTION
-- Dummy function to fill the list of customers (normally, SELECT from database)
FUNCTION cust_loadlist(m)
DEFINE m,i INTEGER
CALL customers.clear()
FOR i=1 TO m
LET customers[i].cust_id = i
LET customers[i].cust_name = "Name " || i
LET customers[i].cust_state = "CA"
LET customers[i].cust_city = "San Fransisco"
LET customers[i].cust_zipcode = 94102
LET customers[i].cust_address = "52, Marlon Street"
LET customers[i].cust_checked = i mod 2
END FOR
END FUNCTION
FUNCTION ord_loadlist(mid,m)
DEFINE mid,m,i INTEGER
CALL orders.clear()
FOR i=1 TO m
CALL orders.appendElement()
LET orders[i].ord_id = mid * 100 + i
LET orders[i].ord_crea = CURRENT YEAR TO SECOND
LET orders[i].ord_amount = ( i mod 5 ) * 133.56
END FOR
END FUNCTION
FUNCTION ord_append()
DEFINE r INT
LET r=c_arr_curr()
CALL ord_input('A', customers[r].cust_id, customers[r].cust_name, 0)
END FUNCTION
FUNCTION ord_modify()
DEFINE r INT
LET r=c_arr_curr()
IF o_arr_curr()==0 THEN RETURN END IF
CALL ord_input('M', customers[r].cust_id, customers[r].cust_name, orders[o_arr_curr()].ord_id)
END FUNCTION
FUNCTION ord_delete()
IF NOT mbox_yn(mytitle,"Delete select order from database?","question") THEN
RETURN
END IF
CALL myd.deleteRow("ordlist",o_arr_curr())
END FUNCTION
FUNCTION combo_fill_states(cb)
DEFINE cb ui.ComboBox
CALL cb.addItem("AK","Alaska")
CALL cb.addItem("AL","Alabama")
CALL cb.addItem("AR","Arkansas")
CALL cb.addItem("AZ","Arizona")
CALL cb.addItem("CA","California")
CALL cb.addItem("CO","Colorado")
CALL cb.addItem("CT","Connecticut")
CALL cb.addItem("DC","D.C.")
CALL cb.addItem("DE","Delaware")
CALL cb.addItem("FL","Florida")
CALL cb.addItem("GA","Georgia")
CALL cb.addItem("HI","Hawaii")
CALL cb.addItem("IA","Iowa")
CALL cb.addItem("ID","Idaho")
CALL cb.addItem("IL","Illinois")
CALL cb.addItem("IN","Indiana")
CALL cb.addItem("KS","Kansas")
CALL cb.addItem("KY","Kentucky")
CALL cb.addItem("LA","Louisiana")
CALL cb.addItem("MA","Massachusetts")
CALL cb.addItem("MD","Maryland")
CALL cb.addItem("ME","Maine")
CALL cb.addItem("MI","Michigan")
CALL cb.addItem("MN","Minnesota")
CALL cb.addItem("MO","Missouri")
CALL cb.addItem("MS","Mississippi")
CALL cb.addItem("MT","Montana")
CALL cb.addItem("NC","North Carolina")
CALL cb.addItem("ND","North Dakota")
CALL cb.addItem("NE","Nebraska")
CALL cb.addItem("NH","New Hampshire")
CALL cb.addItem("NJ","New Jersey")
CALL cb.addItem("NM","New Mexico")
CALL cb.addItem("NV","Nevada")
CALL cb.addItem("NY","New York")
CALL cb.addItem("OH","Ohio")
CALL cb.addItem("OK","Oklahoma")
CALL cb.addItem("OR","Oregon")
CALL cb.addItem("PA","Pennsylvania")
CALL cb.addItem("PR","Puerto Rico")
CALL cb.addItem("RI","Rhode Island")
CALL cb.addItem("SC","South Carolina")
CALL cb.addItem("SD","South Dakota")
CALL cb.addItem("TN","Tennessee")
CALL cb.addItem("TX","Texas")
CALL cb.addItem("UT","Utah")
CALL cb.addItem("VA","Virginia")
CALL cb.addItem("VT","Vermont")
CALL cb.addItem("WA","Washington")
CALL cb.addItem("WI","Wisconsin")
CALL cb.addItem("WV","West Virginia")
CALL cb.addItem("WY","Wyoming")
END FUNCTION
@@ -0,0 +1,89 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
ACTION DEFAULTS
ACTION close(VALIDATE=NO)
ACTION cancel(VALIDATE=NO)
ACTION undo(TEXT="Undo",IMAGE="refresh",VALIDATE=NO)
ACTION ord_modify ( TEXT="Edit Order", IMAGE="pen", COMMENT="Modifies selected order")
END
TOOLBAR
ITEM firstrow (COMMENT="Move to the first record")
ITEM prevrow (TEXT="Prev",COMMENT="Move to the previous record")
ITEM nextrow (COMMENT="Move to the next record")
ITEM lastrow (COMMENT="Move to the last record")
SEPARATOR
ITEM append (TEXT="New",IMAGE="new",COMMENT="Input a new record")
ITEM delete (TEXT="Del",IMAGE="delete",COMMENT="Delete the current record")
SEPARATOR
ITEM ord_modify
END
LAYOUT (TEXT = "Customers and Orders" )
GRID
{
<g g1 >
[f1 ][f2 ]
State: [f3 ] [f4 ]
City: [f5 ]
Zipcode: [f6 ]
Address: [f7 ]
[ ]
-------------------------------------------------------------------
[tr : :undo ]
<g g2 >
<t t1 >
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
< >
}
END
END
ATTRIBUTES
GROUP g1: TEXT = "Customer information";
f1 = FORMONLY.cust_id TYPE INTEGER, NOT NULL, NOENTRY,DEFAULT=0;
EDIT f2 = FORMONLY.cust_name TYPE VARCHAR, NOT NULL, REQUIRED;
COMBOBOX f3 = FORMONLY.cust_state TYPE VARCHAR, NOT NULL, REQUIRED,
QUERYEDITABLE, DEFAULT="CA", INITIALIZER=combo_fill_states;
CHECKBOX f4 = FORMONLY.cust_checked TYPE CHAR, TEXT="Checked",HIDDEN;
BUTTONEDIT f5 = FORMONLY.cust_city TYPE CHAR, NOT NULL, REQUIRED,
ACTION=zoom_city, IMAGE="find";
f6 = FORMONLY.cust_zipcode TYPE VARCHAR, NOT NULL, REQUIRED,DEFAULT=11111, COMMENT="Possible values: 10000 to 99999",
INCLUDE=(10000 to 99999);
TEXTEDIT f7 = FORMONLY.cust_address TYPE CHAR,
COMMENT="Must hold '<number>, <street name>'";
LABEL tr = FORMONLY.cust_info, STYLE="information",SIZEPOLICY=FIXED;
BUTTON undo:undo,TEXT="Undo";
GROUP g2: TEXT = "Orders";
TABLE t1: DOUBLECLICK=ord_modify;
EDIT c1 = FORMONLY.ord_id TYPE INTEGER, TITLE = "Id";
EDIT c2 = FORMONLY.ord_crea TYPE DATE, TITLE = "Creation";
EDIT c3 = FORMONLY.ord_amount TYPE MONEY(15,2), TITLE = "Amount";
END
INSTRUCTIONS
SCREEN RECORD custrec (FORMONLY.cust_id THROUGH FORMONLY.cust_address);
SCREEN RECORD ordlist (FORMONLY.ord_id THROUGH FORMONLY.ord_amount);
END
@@ -0,0 +1,38 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
GLOBALS
TYPE custrec RECORD
cust_id INTEGER,
cust_name VARCHAR(50),
cust_state CHAR(3),
cust_checked SMALLINT,
cust_city VARCHAR(50),
cust_zipcode INT,
cust_address VARCHAR(200)
END RECORD
DEFINE customers DYNAMIC ARRAY OF custrec
DEFINE orders DYNAMIC ARRAY OF RECORD
ord_id INTEGER,
ord_crea DATE,
ord_amount MONEY(15,2)
END RECORD
DEFINE ordrec RECORD
ord_id INTEGER,
ord_daddr VARCHAR(50),
ord_crea DATE,
ord_deliv DATE,
ord_valid CHAR(1),
ord_amount MONEY(15,2)
END RECORD
END GLOBALS
@@ -0,0 +1,33 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
LAYOUT
TABLE
{
Id Item Price
[id |name |price ]
[id |name |price ]
[id |name |price ]
[id |name |price ]
[id |name |price ]
[id |name |price ]
}
END
END
ATTRIBUTES
EDIT id = FORMONLY.item_id TYPE INTEGER;
EDIT name = FORMONLY.item_name TYPE VARCHAR;
EDIT price = FORMONLY.item_price TYPE DECIMAL(10,2);
END
INSTRUCTIONS
SCREEN RECORD sr (FORMONLY.*);
END
@@ -0,0 +1,26 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
MODULES =\
MsgBox.42m\
CityList.42m\
CityList.42f\
OrderInput.42m\
CustOrders.42m\
CustOrders2.42m\
CustOrders3.42m
FORMS = \
CustOrders.42f\
CustOrders2.42f\
CustOrders3.42f\
Orders.42f\
ItemList.42f
include ../../Makeincl
@@ -0,0 +1,36 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
FUNCTION mbox_ok(t, m, i)
DEFINE t,m,i STRING
MENU t ATTRIBUTES(STYLE="dialog",COMMENT=m,IMAGE=i)
COMMAND "Ok" EXIT MENU
END MENU
END FUNCTION
FUNCTION mbox_yn(t, m, i)
DEFINE t,m,i STRING
DEFINE r INTEGER
MENU t ATTRIBUTES(STYLE="dialog",COMMENT=m,IMAGE=i)
COMMAND "Yes" LET r = TRUE EXIT MENU
COMMAND "No" LET r = FALSE EXIT MENU
END MENU
RETURN r
END FUNCTION
FUNCTION mbox_ync(t, m, i)
DEFINE t,m,i STRING
DEFINE r INTEGER
MENU t ATTRIBUTES(STYLE="dialog",COMMENT=m,IMAGE=i)
COMMAND "Yes" LET r = 1 EXIT MENU
COMMAND "No" LET r = 2 EXIT MENU
COMMAND "Cancel" LET r = 3 EXIT MENU
END MENU
RETURN r
END FUNCTION
@@ -0,0 +1,186 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
IMPORT FGL MsgBox
GLOBALS "Globals.4gl"
DEFINE items DYNAMIC ARRAY OF RECORD
item_line INTEGER,
item_name VARCHAR(50),
item_quant DECIMAL(8,2),
item_price MONEY(10,2),
item_total MONEY(15,2),
item_id INTEGER
END RECORD
CONSTANT mytitle = "Orders"
FUNCTION ord_input(mode, cust_id, cust_name, ord_id)
DEFINE mode CHAR(1)
DEFINE cust_id INTEGER
DEFINE cust_name VARCHAR(50)
DEFINE ord_id INTEGER
DEFINE i, id, s,r INTEGER
DEFINE name STRING
DEFINE price DECIMAL(10,2)
DEFINE touched,reorder INT
OPEN WINDOW w_ord WITH FORM "Orders"
CALL items.clear()
IF mode == 'M' THEN
-- Simulate database fetch
LET ordrec.ord_id = ord_id
LET ordrec.ord_daddr = "5, Market Street"
LET ordrec.ord_crea = TODAY - ord_id
LET ordrec.ord_deliv = ordrec.ord_crea + 3
LET ordrec.ord_valid = "n"
-- Some item lines...
LET i=0
LET items[i:=i+1].item_line = i
LET items[1].item_id = 2
LET items[i].item_name = "Oranges (1Kg)"
LET items[i].item_quant = 2
LET items[i].item_price = 4.65
LET items[i].item_total = items[i].item_quant * items[i].item_price
LET items[i:=i+1].item_line = i
LET items[1].item_id = 1
LET items[i].item_name = "Apples (1Kg)"
LET items[i].item_quant = 3
LET items[i].item_price = 3.45
LET items[i].item_total = items[i].item_quant * items[i].item_price
LET ordrec.ord_amount = 0
FOR i=1 TO items.getLength()
LET ordrec.ord_amount = ordrec.ord_amount + items[i].item_total
END FOR
END IF
DIALOG ATTRIBUTES(FIELD ORDER FORM, UNBUFFERED)
INPUT BY NAME ordrec.* ATTRIBUTE(WITHOUT DEFAULTS=(mode=='M'))
END INPUT
INPUT ARRAY items FROM itemlist.* ATTRIBUTE(WITHOUT DEFAULTS)
BEFORE INSERT
CALL orditem_reorder(DIALOG)
AFTER INSERT
LET reorder=1
AFTER DELETE
LET reorder=1
AFTER ROW
IF FIELD_TOUCHED(itemlist.*) THEN
LET touched=TRUE
END IF
IF reorder THEN
CALL orditem_reorder(DIALOG)
LET reorder=0
END IF
ON CHANGE item_quant
CALL orditem_total(DIALOG, arr_curr())
ON ACTION zoom_item
LET r=DIALOG.getCurrentRow("itemlist")
CALL select_item(DIALOG.getFieldBuffer("item_name")) RETURNING s, id, name, price
IF s==0 THEN
LET items[r].item_id = id
LET items[r].item_name = name
LET items[r].item_price = price
CALL orditem_total(DIALOG, r)
CALL DIALOG.setFieldTouched("item_name",TRUE)
END IF
END INPUT
BEFORE DIALOG
DISPLAY BY NAME cust_id, cust_name
--init some items in the new record after the defaults have been filled in
IF mode='A' THEN
LET ordrec.ord_deliv = TODAY+1
END IF
ON ACTION accept
ACCEPT DIALOG
ON ACTION cancel
IF touched OR FIELD_TOUCHED(ordrec.*) OR
FIELD_TOUCHED(itemlist.*) THEN
IF NOT mbox_yn(mytitle,"Close window without saving input?","question") THEN
CONTINUE DIALOG
END IF
END IF
EXIT DIALOG
AFTER DIALOG
-- Save data in database.
DISPLAY "save data"
END DIALOG
CLOSE WINDOW w_ord
END FUNCTION
FUNCTION orditem_reorder(d)
DEFINE d ui.Dialog
DEFINE i INTEGER
FOR i=1 TO d.getArrayLength("itemlist")
LET items[i].item_line = i
END FOR
END FUNCTION
FUNCTION orditem_total(d, r)
DEFINE d ui.Dialog
DEFINE r, i INTEGER
LET items[r].item_total = items[r].item_quant * items[r].item_price
LET ordrec.ord_amount = 0
FOR i=1 TO d.getArrayLength("itemlist")
LET ordrec.ord_amount = ordrec.ord_amount + items[i].item_total
END FOR
END FUNCTION
FUNCTION select_item(filter)
DEFINE filter STRING
DEFINE i INTEGER
DEFINE itemarr DYNAMIC ARRAY OF RECORD
item_id INTEGER,
item_name VARCHAR(50),
item_price MONEY(10,2)
END RECORD
OPEN WINDOW w_item WITH FORM "ItemList"
LET i = 0
LET itemarr[i:=i+1].item_id = i LET itemarr[i].item_name = "Apples (1Kg)" LET itemarr[i].item_price = 4.25
LET itemarr[i:=i+1].item_id = i LET itemarr[i].item_name = "Oranges (1Kg)" LET itemarr[i].item_price = 5.35
LET itemarr[i:=i+1].item_id = i LET itemarr[i].item_name = "Lemons (1Kg)" LET itemarr[i].item_price = 4.95
LET itemarr[i:=i+1].item_id = i LET itemarr[i].item_name = "Melons (unit)" LET itemarr[i].item_price = 1.55
LET itemarr[i:=i+1].item_id = i LET itemarr[i].item_name = "Peanuts (1Kg)" LET itemarr[i].item_price = 2.25
DISPLAY ARRAY itemarr TO sr.*
AFTER DISPLAY
IF NOT int_flag THEN
LET i = arr_curr()
ELSE
LET int_flag = FALSE
LET i = -1
END IF
END DISPLAY
CLOSE WINDOW w_item
IF i > 0 THEN
RETURN 0, itemarr[i].item_id, itemarr[i].item_name, itemarr[i].item_price
ELSE
RETURN -1, NULL, NULL, NULL
END IF
END FUNCTION
@@ -0,0 +1,86 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
ACTION DEFAULTS
ACTION insert ( IMAGE = "listins", COMMENT="Insert a new item line")
ACTION append ( IMAGE = "listadd", COMMENT="Append a new item line")
ACTION delete ( IMAGE = "listdel", COMMENT="Delete current item line", VALIDATE=NO)
ACTION validate ( TEXT = "Validate", COMMENT="Validate the order", ACCELERATOR=Control-V)
END
LAYOUT ( STYLE="dialog4", TEXT = "Order" )
GRID
{
<g g1 >
Id: [f1 ] Name: [f2 ]
< >
<g g2 >
Id: [o1 ] Crea: [o3 ]
Delivery address: [o2 ]
Delivery date: [o4 ][o5 ]
< >
<g g3 >
<t t1 >[iad]
[c1 |c2 |c3 |c4 |c5 |c6][imo]
[c1 |c2 |c3 |c4 |c5 |c6][ide]
[c1 |c2 |c3 |c4 |c5 |c6]
[c1 |c2 |c3 |c4 |c5 |c6]
[c1 |c2 |c3 |c4 |c5 |c6]
[c1 |c2 |c3 |c4 |c5 |c6]
[c1 |c2 |c3 |c4 |c5 |c6]
< >
< >
<g g4 >
[ :o6 ]
< >
}
END
END
ATTRIBUTES
GROUP g1: TEXT = "Customer information";
EDIT f1 = FORMONLY.cust_id TYPE INTEGER, NOENTRY;
EDIT f2 = FORMONLY.cust_name TYPE VARCHAR, NOENTRY;
GROUP g2: TEXT = "Order header";
EDIT o1 = FORMONLY.ord_id, NOENTRY;
EDIT o2 = FORMONLY.ord_daddr TYPE VARCHAR, NOT NULL, REQUIRED;
EDIT o3 = FORMONLY.ord_crea TYPE DATE, NOENTRY,DEFAULT=TODAY;
DATEEDIT o4 = FORMONLY.ord_deliv TYPE DATE;
GROUP g3: TEXT = "Order items";
TABLE t1: DOUBLECLICK=none;
LABEL c1 = FORMONLY.item_line TYPE INTEGER, TITLE = "Pos";
BUTTONEDIT c2 = FORMONLY.item_name TYPE VARCHAR, TITLE = "Item name",
SCROLL, NOT NULL, REQUIRED, ACTION=zoom_item, IMAGE="find";
EDIT c3 = FORMONLY.item_quant TYPE DECIMAL(5,2), TITLE = "Quantity",
SCROLL, NOT NULL, REQUIRED, INCLUDE=(0.0001 TO 10000),DEFAULT=1;
LABEL c4 = FORMONLY.item_price TYPE MONEY(10,2), TITLE = "Unit price";
LABEL c5 = FORMONLY.item_total TYPE MONEY(15,2), TITLE = "Total";
LABEL c6 = FORMONLY.item_id TYPE INTEGER, TITLE = "Item id", HIDDEN=USER;
BUTTON iad: itemlist.insert;
BUTTON imo: itemlist.append;
BUTTON ide: itemlist.delete;
GROUP g4: TEXT = "Order amount", GRIDCHILDRENINPARENT;
CHECKBOX o5 = FORMONLY.ord_valid TYPE CHAR, NOT NULL, REQUIRED,
TEXT="Validated", VALUECHECKED="y", VALUEUNCHECKED="n",DEFAULT='n';
EDIT o6 = FORMONLY.ord_amount TYPE MONEY(15,2), STYLE="amount", NOENTRY;
END
INSTRUCTIONS
SCREEN RECORD custrec (FORMONLY.cust_id, FORMONLY.cust_name);
SCREEN RECORD ordrec (FORMONLY.ord_id, FORMONLY.ord_daddr,
FORMONLY.ord_crea, FORMONLY.ord_deliv,
FORMONLY.ord_valid, FORMONLY.ord_amount);
SCREEN RECORD itemlist (FORMONLY.item_line THROUGH FORMONLY.item_id);
END
@@ -0,0 +1,104 @@
Input multiple master records + details
---------------------------------------
This directory contains some examples to perform the following task:
a master and a detail table should be edited simultaneously.
(a customer record acts as the master record and an orders table contains the details)
It should be possible to have forward/backward actions to advance to the next physical record for the master in the database and update the details display/input as well.
This is something pretty new in 4GL, this was not possible before.
Particularly, the INPUT of the master by staying visually 'in the dialog' was only possible before by using an INPUT ARRAY on a single record (which is still a good idea as shown later on).
3 DIALOG samples try to achieve this: CustOrders.4gl, CustOrders2.4gl, and CustOrders3.4gl
CustOrders.4gl and CustOrders2.4gl have the same user interface but show
different methods for vaidation of the record data.
Both use an INPUT for the master data and a DISPLAY ARRAY for the details.
CustOrders3.4gl has a more simple user interface (no confirm for the row and no save button) and shows how an INPUT ARRAY on a single row could be used to achieve basically the same result as in CustOrders.4gl and CustOrders2.4gl (with other limitations however).
OrderInput.4gl shows the INPUT of a single master record + details in an INPUT ARRAY (FUNCTION ord_input) and is common to all CustOrders*.4gl samples.
In the AFTER DIALOG of ord_input there is only a small DISPLAY "save data", which is of course a bigger piece of code to write a complete order to the DB.
A real database demo with a similar DIALOG is shown in $FGLDIR/demo/MultipleDialogs/StoresMD/d4_orders.4gl (FUNCTION order_modify)
A central problem in this demo is validation:
When the current customer record is modified, clicking on the previous/next/append buttons should cause the validation of the current record. That means the built-in constraints for the fields must be run thru, as well as the AFTER FIELD of the current field, and the code that is normally in AFTER INPUT to perform checks that cannot be done in AFTER FIELD (due to multiple field dependencies).
In the first 2 samples code is written to achieve that, in the 3rd the INPUT ARRAY with it's built-in triggers does a lot of the work.
CustOrders.4gl
--------------
Implements all customer record validation by 'staying in the dialog' and performs explicit field validation for the user programmed actions.
Each action that require a validation, like going to the next customer or invoking a search, must validate the current INPUT.
Further, those actions must perform the same checking as the built-in 'accept' action of a traditional INFORMIX INPUT instruction:
v1:verify if the field constraints are met (achieved by calling the new DIALOG.validate() method).
v2:call the code that is called in the AFTER FIELD trigger of the field you are currently in.
v3:call the code that is called in the AFTER INPUT trigger.
This is achieved by calling the cust_check_changed() function, which checks the validity in cust_save() by calling cust_check_fields()(calling cust_check_data(), then performing DIALOG.validate() + checking all user programmed field constraints)
The user programmed field constraints are the same in each sample:
1. The customer's address is checked when entered
2. A city cannot be entered without entering a zip code
Advantages: 1. you never have to leave the DIALOG instruction (in comparison to sample2)
Disadvantages:1. need to perform self made (explicite) validation for each of the standard actions
2. a lot of boilerplate code needed to achieve the standard
first/next/previous/last/append/delete actions (in comparison to sample3)
3. the WITHOUT DEFAULTS flag for the INPUT is not usable (see in ../ListAndEditableDetail/README what this means)
CustOrders2.4gl
---------------
- implents all customer record validations by using implicit validation: calling ACCEPT DIALOG
That means that each action which requires validation calls ACCEPT DIALOG.
This lets the DIALOG run thru all 3 validation stages and causes a falling out of the current DIALOG instruction if the validation was succesful.
That means also that one has to maintain a state variable to reenter the DIALOG in a WHILE loop around the DIALOG (in addition to maintain a variable to distinguish between new record or record modification).
The main advantage using that technology is that all you AFTER FIELD constraints can be left in 1 place, it's not needed to use a function for a simple constraint and not needed to duplicate the call in your private validating function compared to sample 1 (see for example the AFTER FIELD cust_name checking) .
Advantages: 1. you only need to check the field constraints at the usual places:
AFTER FIELD and AFTER INPUT and not in each action
2. the WITHOUT DEFAULTS flag for the INPUT is still working like intended (see in ../ListAndEditableDetail/README what this means)
Disadvantages: 1. need to write a WHILE loop with a state variable
2. a lot of boilerplate code needed to achieve the standard
first/next/previous/last/append/delete actions (in comparison to sample3)
CustOrders3.4gl
---------------
-uses an INPUT ARRAY in contradiction to the previous samples where an INPUT was used for the master record
-shows that the standard next/previous/append/delete actions are already builtin into INPUT ARRAY and usable nearly like intended
-implements an MS Access behavior: going to the next/prevoius/first/last row saves the current record to the database without confirmation. If one wants to throw away the changes in the current record, he must press 'Undo'
-shows how the firstrow/prevrow/nextrow/lastrow/append/delete actions can be put into the toolbar and work depending on the context .
That means: if the focus is in INPUT ARRAY-> toolbar buttons act on the INPUT ARRAY
if the focus is in the DISPLAY ARRAY-> toolbar buttons act on the DISPLAY ARRAY
This is similar to how the toolbar is working in MS Access.
(Please note that in earlier versions the single row INPUT ARRAY had only prevrow/nextrow actions)
A further goodie is that INPUT ARRAY/DISPLAY ARRAY now manage the activation of firstrow/prevrow/nextrow/lastrow automatically: no need to call setActionActive by yourself.
Advantages: 1. *a lot* shorter code than in the 2 previous samples because the
next/previous/append/delete mechanisms are already foreseen in INPUT ARRAY
2. you only need to check the field constraints at the usual places:
AFTER FIELD and AFTER ROW (which comparable to the AFTER INPUT trigger of the single record INPUT) and not in each action
Disadvantages: 1. Pressing 'Tab' at the last customer record field moves to the next customer record instead of jumoing to the DISPLAY ARRAY
2. you need to operate on an array which may be inadequate for a large number of database items
CustOrders3.4gl sounds promising but at the moment it's impossible to implement confirmation for the new record (which is not absolutely necessary like MS Access shows).
It's still the usual INPUT ARRAY to input an array and not much foreseen to be synchronized with the data base. However it's intended to be improved in one of the next releases. Then it very likely includes built-in undo support as well as built-in support for synchronizing a row with the database.
The navigation is probably also not what the end user expects, pressing the 'Down' arrow moves to the next record.
Pressing 'Tab' at the end of the record advances also to the next record (which is fine if the INPUT ARRAY runs in a table but not expected for the single record case).
This could be enhanced by giving the INPUT ARRAY a hint that it should make the navigation different in the single record case.
Conclusion:
----------
The first 2 samples show that it's at least possible to achieve the wanted behavior however with a pretty large overhead for implementing the standard actions.
In both cases you need to be careful about the validations for the standard actions and need to add extra code to achieve this.
Both samples lack easy readability because of all the necessary validation stuff which would be an easy game using an INPUT ARRAY.
Sample 3 shows that you can have much less code using INPUT ARRAY, but you still need to know well the pitfalls of how to synchronize the INPUT ARRAY with the database and you will have to live with the limitations (navigation a bit different from expectation)
If this kind of dialog is not a top priority for you, you better wait for an enhanced INPUT ARRAY which has built-in support to sync with the database and enhanced navigation for the single record case to make programming this a joy.
+113
View File
@@ -0,0 +1,113 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
IMPORT FGL Styles
IMPORT FGL CityList
DEFINE custarr DYNAMIC ARRAY OF RECORD
c_id INTEGER,
c_name VARCHAR(50),
c_ts DATETIME YEAR TO SECOND
END RECORD
DEFINE custcrit RECORD
cust_id STRING,
cust_name STRING,
cust_state STRING,
cust_city STRING,
cust_zipcode STRING
END RECORD
MAIN
DEFINE s INTEGER
DEFINE where_clause, name, state STRING
DEFINE first_rows SMALLINT
DEFINE first_rows_count INTEGER
OPTIONS INPUT WRAP
OPEN FORM f1 FROM "QueryCustomers"
DISPLAY FORM f1
CALL addCommonStyles()
CALL addStyleList1()
DIALOG ATTRIBUTES(FIELD ORDER FORM, UNBUFFERED)
INPUT BY NAME first_rows, first_rows_count
ON CHANGE first_rows
CALL DIALOG.setFieldActive("first_rows_count",first_rows)
END INPUT
CONSTRUCT BY NAME where_clause
ON cust_id,
cust_name,
cust_state,
cust_city,
cust_zipcode
AFTER FIELD cust_id,
cust_name,
cust_state,
cust_city,
cust_zipcode
DISPLAY BY NAME where_clause
END CONSTRUCT
DISPLAY ARRAY custarr TO sr.*
BEFORE ROW
MESSAGE SFMT("Row: %1/%2", DIALOG.getCurrentRow("sr"), DIALOG.getArrayLength("sr"))
END DISPLAY
BEFORE DIALOG
LET first_rows = FALSE
CALL DIALOG.setFieldActive("first_rows_count",FALSE)
ON ACTION zoom_city
CALL select_city(DIALOG.getFieldBuffer("cust_city")) RETURNING s, name, state
IF s==0 THEN
DISPLAY name, state TO cust_city, cust_state
END IF
-- Save/Load criterias: would be nice to have DIALOG.setFieldBuffer
-- to make it generic.
ON ACTION clear_criterias
CLEAR cust_id, cust_name, cust_state, cust_city, cust_zipcode
LET where_clause = NULL
DISPLAY BY NAME where_clause
ON ACTION load_criterias
DISPLAY BY NAME custcrit.*
ON ACTION save_criterias
CALL get_fldbuf( cust_id, cust_name, cust_state, cust_city, cust_zipcode )
RETURNING custcrit.*
ON ACTION fetch
DISPLAY BY NAME where_clause
CALL cust_loadlist(300 / LENGTH(where_clause))
ON ACTION clear_list
CALL cust_loadlist(0)
ON ACTION close
EXIT DIALOG
END DIALOG
END MAIN
-- Dummy function to fill the list of customers (normally, SELECT from database)
FUNCTION cust_loadlist(m)
DEFINE m,i INTEGER
CALL custarr.clear()
FOR i=1 TO m
CALL custarr.appendElement()
LET custarr[i].c_id = i
LET custarr[i].c_name = "Name " || i
LET custarr[i].c_ts = CURRENT
END FOR
END FUNCTION
+83
View File
@@ -0,0 +1,83 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
ACTION DEFAULTS
ACTION clear_criterias ( VALIDATE = NO )
ACTION clear_list ( TEXT = "Clear list" )
ACTION close ( VALIDATE = NO )
END
LAYOUT ( TEXT = "Query customers" )
GRID
{
<g g1 >
[fr ] Count: [frc ]
< >
<g g2 >
Id: [f1 ] Name: [f2 ]
State: [f3 ]
City: [f4 ]
Zipcode: [f5 ]
[cr :ld :sv :sr : ]
SQL Condition:
[sql ]
[ ]
< >
<g g3 >
<t t1 >
Id Name Timestamp
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
[c1 |c2 |c3 ]
< >
< >
[el : :cw ]
}
END
END
ATTRIBUTES
GROUP g1: TEXT = "Search options";
CHECKBOX fr = FORMONLY.first_rows TYPE SMALLINT, NOT NULL, TEXT="Select first N rows";
EDIT frc= FORMONLY.first_rows_count, NOT NULL;
GROUP g2: TEXT = "Search criterias";
EDIT f1 = FORMONLY.cust_id TYPE INTEGER;
EDIT f2 = FORMONLY.cust_name TYPE VARCHAR;
COMBOBOX f3 = FORMONLY.cust_state TYPE VARCHAR,
QUERYEDITABLE, DEFAULT="CA", INITIALIZER=combo_fill_states;
BUTTONEDIT f4 = FORMONLY.cust_city TYPE CHAR, ACTION=zoom_city, IMAGE="find";
EDIT f5 = FORMONLY.cust_zipcode TYPE VARCHAR;
GROUP g3: TEXT = "Customer list";
EDIT c1 = FORMONLY.c_id TYPE INTEGER;
EDIT c2 = FORMONLY.c_name TYPE VARCHAR;
DATEEDIT c3 = FORMONLY.c_ts TYPE DATETIME YEAR TO SECOND;
BUTTON cr : clear_criterias, TEXT="Clear";
BUTTON ld : load_criterias, TEXT="Load";
BUTTON sv : save_criterias, TEXT="Save";
BUTTON sr : fetch, TEXT="Fetch";
TEXTEDIT sql = FORMONLY.where_clause, STRETCH=X;
BUTTON el : clear_list;
BUTTON cw : close;
END
INSTRUCTIONS
SCREEN RECORD sr (FORMONLY.c_id THROUGH FORMONLY.c_ts);
END
+8
View File
@@ -0,0 +1,8 @@
Multiple Dialogs Demos
----------------------
Multiple Dialogs is the main feature of version 2.10.
The DIALOG instruction acts as a collection of singular dialogs which work in parallel.
You will find here some small demos showing how Multiple Dialogs work, mixing singular dialogs.
The Simple Mailer demo is a more complete application using Multiple Dialogs.
+50
View File
@@ -0,0 +1,50 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
FUNCTION addCommonStyles()
DEFINE r, sl, s, a om.DomNode
DEFINE nl om.NodeList
LET r = ui.Interface.getRootNode()
LET nl = r.selectByTagName("StyleList")
LET sl = nl.item(1)
LET s = sl.createChild("Style")
CALL s.setAttribute("name", ":query")
CALL sl.appendChild(s)
LET a = s.createChild("StyleAttribute")
CALL s.appendChild(a)
CALL a.setAttribute("name", "backgroundColor")
CALL a.setAttribute("value", "yellow")
END FUNCTION
FUNCTION addStyleList1()
DEFINE r, sl, s, a om.DomNode
DEFINE nl om.NodeList
LET r = ui.Interface.getRootNode()
LET nl = r.selectByTagName("StyleList")
LET sl = nl.item(1)
LET s = sl.createChild("Style")
CALL s.setAttribute("name", ":odd")
CALL sl.appendChild(s)
LET a = s.createChild("StyleAttribute")
CALL s.appendChild(a)
CALL a.setAttribute("name", "backgroundColor")
CALL a.setAttribute("value", "#FFFFDD")
LET s = sl.createChild("Style")
CALL s.setAttribute("name", ":even")
CALL sl.appendChild(s)
LET a = s.createChild("StyleAttribute")
CALL s.appendChild(a)
CALL a.setAttribute("name", "backgroundColor")
CALL a.setAttribute("value", "#EEFFFF")
END FUNCTION
+92
View File
@@ -0,0 +1,92 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
MAIN
DEFINE a1 DYNAMIC ARRAY OF RECORD
col11 INTEGER,
col12 CHAR(10)
END RECORD
DEFINE a2 DYNAMIC ARRAY OF RECORD
col21 INTEGER,
col22 CHAR(10)
END RECORD
DEFINE field1 INTEGER
DEFINE field2, field3, field4 CHAR(50)
OPTIONS INPUT WRAP
OPEN FORM f FROM "SubBlockActions"
DISPLAY FORM f
LET field1 = 123
LET field2 = "aaaa"
LET field4 = "xxxx"
LET a1[1].col11 = 1
LET a1[2].col11 = 2
LET a1[3].col11 = 3
LET a2[1].col21 = 1
LET a2[2].col21 = 2
DIALOG ATTRIBUTES(UNBUFFERED)
INPUT BY NAME field1,field2,field3 ATTRIBUTES(NAME="input1", WITHOUT DEFAULTS)
ON ACTION foo
ERROR "Action foo of INPUT input1 sub-block"
ON ACTION bar
ERROR "Action bar of INPUT input1 sub-block"
END INPUT
INPUT ARRAY a1 FROM sr1.* ATTRIBUTES (WITHOUT DEFAULTS)
ON ACTION foo
ERROR "Action foo of INPUT ARRAY sr1 sub-block"
END INPUT
INPUT ARRAY a2 FROM sr2.* ATTRIBUTES (WITHOUT DEFAULTS)
ON ACTION foo
ERROR "Action foo of INPUT ARRAY sr2 sub-block"
END INPUT
INPUT BY NAME field4 ATTRIBUTES(NAME="input2", WITHOUT DEFAULTS)
END INPUT
ON ACTION disable_input1_foo
CALL dialog.setActionActive("input1.foo",FALSE)
ON ACTION enable_input1_foo
CALL dialog.setActionActive("input1.foo",TRUE)
ON ACTION disable_input1_bar
CALL dialog.setActionActive("input1.bar",FALSE)
ON ACTION enable_input1_bar
CALL dialog.setActionActive("input1.bar",TRUE)
ON ACTION disable_actions_sr1
CALL list_actions(DIALOG,"sr1", FALSE)
ON ACTION enable_actions_sr1
CALL list_actions(DIALOG,"sr1", TRUE)
ON ACTION disable_actions_sr2
CALL list_actions(DIALOG,"sr2", FALSE)
ON ACTION enable_actions_sr2
CALL list_actions(DIALOG,"sr2", TRUE)
ON ACTION close EXIT DIALOG
END DIALOG
END MAIN
FUNCTION list_actions(d, name, activate)
DEFINE d ui.Dialog, name STRING, activate INTEGER
CALL d.setActionActive(name||".insert",activate)
CALL d.setActionActive(name||".append",activate)
CALL d.setActionActive(name||".delete",activate)
CALL d.setActionActive(name||".foo",activate)
END FUNCTION
+143
View File
@@ -0,0 +1,143 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
ACTION DEFAULTS
ACTION insert ( IMAGE="listins", TEXT="Ins" )
ACTION append ( IMAGE="listadd", TEXT="App" )
ACTION delete ( IMAGE="listdel", TEXT="Del" )
ACTION foo ( IMAGE="smiley", TEXT="Foo" )
ACTION bar ( IMAGE="ssmiley", TEXT="Bar" )
ACTION close ( VALIDATE = NO, DEFAULTVIEW=YES )
end
TOPMENU
GROUP global (TEXT="global")
COMMAND foo (COMMENT="Bound with the name 'foo'")
COMMAND bar (COMMENT="Bound with the name 'bar'")
END
GROUP input1 (TEXT="Input1")
COMMAND input1.foo (TEXT="input1.foo", COMMENT="Bound with the name 'input1.foo'")
COMMAND input1.bar (TEXT="input1.bar", COMMENT="Bound with the name 'input1.bar'")
END
GROUP sr1 (TEXT="List1")
COMMAND sr1.insert (TEXT="sr1.insert", COMMENT="Bound with the name 'sr1.insert'")
COMMAND sr1.append (TEXT="sr1.append", COMMENT="Bound with the name 'sr1.append'")
COMMAND sr1.delete (TEXT="sr1.delete", COMMENT="Bound with the name 'sr1.delete'")
COMMAND sr1.foo (TEXT="sr1.foo", COMMENT="Bound with the name 'sr1.foo'")
END
GROUP sr2 (TEXT="List2")
COMMAND sr2.insert (TEXT="sr2.insert", COMMENT="Bound with the name 'sr2.insert'")
COMMAND sr2.append (TEXT="sr2.append", COMMENT="Bound with the name 'sr2.append'")
COMMAND sr2.delete (TEXT="sr2.delete", COMMENT="Bound with the name 'sr2.delete'")
COMMAND sr2.foo (TEXT="sr2.foo", COMMENT="Bound with the name 'sr2.foo'")
END
GROUP bothlists (TEXT="Both Lists")
COMMAND insert (TEXT="insert", COMMENT="Bound with the name 'insert'")
COMMAND append (TEXT="append", COMMENT="Bound with the name 'append'")
COMMAND delete (TEXT="delete", COMMENT="Bound with the name 'delete'")
SEPARATOR
COMMAND firstrow (COMMENT="Bound with the name 'firstrow'")
COMMAND prevrow (COMMENT="Bound with the name 'prevrow'")
COMMAND nextrow (COMMENT="Bound with the name 'nextrow'")
COMMAND lastrow (COMMENT="Bound with the name 'lastrow'")
END
END
TOOLBAR
ITEM insert (COMMENT="Bound with the name 'insert'")
ITEM append (COMMENT="Bound with the name 'append'")
ITEM delete (COMMENT="Bound with the name 'delete'")
SEPARATOR
ITEM firstrow (COMMENT="Bound with the name 'firstrow'")
ITEM prevrow (COMMENT="Bound with the name 'prevrow'")
ITEM nextrow (COMMENT="Bound with the name 'nextrow'")
ITEM lastrow (COMMENT="Bound with the name 'lastrow'")
SEPARATOR
ITEM foo (COMMENT="Bound with the name 'foo'")
ITEM bar (COMMENT="Bound with the name 'bar'")
END
LAYOUT
VBOX
GRID
{
<g g1 >
This group defines foo, bar actions
[f1 ]
[f2 ]
[f3 ]
[in_foo|in_bar]
< >
<g g2 >
This group defines insert,append,delete,foo
<t >
[c11 |c12 ][t1_ins]
[c11 |c12 ][t1_app]
[c11 |c12 ][t1_del]
[c11 |c12 ][t1_foo]
[c11 |c12 ]
< >
< >
<g g3 >
This group defines insert,append,delete,foo
<t >
[c21 |c22 ][t2_ins]
[c21 |c22 ][t2_app]
[c21 |c22 ][t2_del]
[c21 |c22 ][t2_foo]
[c21 |c22 ]
< >
< >
<g g4 >
This group defines no actions
[f4 ]
< >
}
END
END
END
ATTRIBUTES
GROUP g1: TEXT = "This is input1";
EDIT f1 = formonly.field1;
BUTTONEDIT f2 = formonly.field2, ACTION=foo,
COMMENT="Bound with the name 'foo'";
BUTTONEDIT f3 = formonly.field3, ACTION=input1.bar,
COMMENT="Bound with the name 'input1.bar'";
BUTTON in_bar: input1.bar, COMMENT="Bound with the name 'input1.bar'";
BUTTON in_foo: input1.foo, COMMENT="Bound with the name 'input1.foo'";
GROUP g2: TEXT = "This is list sr1";
EDIT c11 = formonly.col11;
EDIT c12 = formonly.col12;
BUTTON t1_ins: sr1.insert, image="listins";
BUTTON t1_app: sr1.append, image="listadd";
BUTTON t1_del: sr1.delete, image="listdel";
BUTTON t1_foo: sr1.foo, COMMENT="Bound with the name 'sr1.foo'";
GROUP g3: TEXT = "This is list sr2";
EDIT c21 = formonly.col21;
EDIT c22 = formonly.col22;
BUTTON t2_ins: sr2.insert, image="listins";
BUTTON t2_app: sr2.append, image="listadd";
BUTTON t2_del: sr2.delete, image="listdel";
BUTTON t2_foo: sr2.foo, COMMENT="Bound with the name 'sr2.foo'";
GROUP g4: TEXT = "This is input2";
EDIT f4 = formonly.field4;
END
INSTRUCTIONS
SCREEN RECORD sr1 (FORMONLY.col11, FORMONLY.col12);
SCREEN RECORD sr2 (FORMONLY.col21, FORMONLY.col22);
END
+156
View File
@@ -0,0 +1,156 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
DEFINE arr1 DYNAMIC ARRAY OF RECORD name STRING END RECORD
DEFINE arr2 DYNAMIC ARRAY OF RECORD name STRING END RECORD
DEFINE arr3 DYNAMIC ARRAY OF RECORD name STRING END RECORD
DEFINE att1 DYNAMIC ARRAY OF RECORD color STRING END RECORD
DEFINE att2 DYNAMIC ARRAY OF RECORD color STRING END RECORD
DEFINE att3 DYNAMIC ARRAY OF RECORD color STRING END RECORD
MAIN
OPTIONS INPUT WRAP
OPEN FORM f1 FROM "ThreeLists"
DISPLAY FORM f1
CALL fillarr(arr1, att1, "First List ", 5, "blue reverse")
CALL fillarr(arr2, att2, "Second List ", 7, "red reverse")
CALL fillarr(arr3, att3, "Third List ", 4, "green reverse")
DIALOG ATTRIBUTES(FIELD ORDER FORM, UNBUFFERED)
DISPLAY ARRAY arr1 TO sr1.*
BEFORE ROW
CALL setup(DIALOG)
END DISPLAY
DISPLAY ARRAY arr2 TO sr2.*
BEFORE ROW
CALL setup(DIALOG)
END DISPLAY
DISPLAY ARRAY arr3 TO sr3.*
BEFORE ROW
CALL setup(DIALOG)
END DISPLAY
BEFORE DIALOG
CALL DIALOG.setArrayAttributes("sr1", att1)
CALL DIALOG.setArrayAttributes("sr2", att2)
CALL DIALOG.setArrayAttributes("sr3", att3)
ON ACTION move_1_to_2 CALL move_row(DIALOG, "sr1", "sr2", arr1, arr2, att1, att2)
ON ACTION move_2_to_1 CALL move_row(DIALOG, "sr2", "sr1", arr2, arr1, att2, att1)
ON ACTION move_1_to_3 CALL move_row(DIALOG, "sr1", "sr3", arr1, arr3, att1, att3)
ON ACTION move_2_to_3 CALL move_row(DIALOG, "sr2", "sr3", arr2, arr3, att2, att3)
ON ACTION move_3_to_12 CALL move_3_to_12(DIALOG)
ON ACTION clear_current
CALL clear_current(DIALOG)
ON ACTION quit ATTRIBUTES(TEXT="Quit")
EXIT DIALOG
ON ACTION CLOSE
EXIT DIALOG
END DIALOG
END MAIN
FUNCTION setup(d)
DEFINE d ui.Dialog
-- can't user ui.Dialog.getArrayLength() because it's not synchronized for now.
CALL d.setActionActive("move_1_to_2", arr1.getLength()>0)
CALL d.setActionActive("move_2_to_1", arr2.getLength()>0)
CALL d.setActionActive("move_1_to_3", arr1.getLength()>0)
CALL d.setActionActive("move_2_to_3", arr2.getLength()>0)
CALL d.setActionActive("move_3_to_12", arr3.getLength()>0)
END FUNCTION
FUNCTION clear_current(d)
DEFINE d ui.Dialog
CALL d.deleteAllRows( d.getCurrentItem() )
CALL setup(d)
END FUNCTION
FUNCTION move_row(d, sa, sb, arra, arrb, atta, attb)
DEFINE d ui.Dialog
DEFINE sa, sb STRING
DEFINE arra DYNAMIC ARRAY OF RECORD name STRING END RECORD
DEFINE arrb DYNAMIC ARRAY OF RECORD name STRING END RECORD
DEFINE atta DYNAMIC ARRAY OF RECORD color STRING END RECORD
DEFINE attb DYNAMIC ARRAY OF RECORD color STRING END RECORD
DEFINE ra, rb INTEGER
LET ra = d.getCurrentRow(sa)
IF ra == 0 THEN RETURN END IF
LET rb = d.getCurrentRow(sb)
IF rb == 0 THEN
CALL d.appendRow(sb)
LET rb = 1
ELSE
CALL d.insertRow(sb, rb)
END IF
CALL d.setCurrentRow(sb, rb)
LET arrb[rb].name = arra[ra].name
LET attb[rb].color = atta[ra].color
CALL d.deleteRow(sa, ra)
CALL setup(d)
END FUNCTION
FUNCTION move_3_to_12(d)
DEFINE d ui.Dialog
DEFINE r1, r2, r3 INTEGER
LET r3 = d.getCurrentRow("sr3")
IF r3 == 0 THEN RETURN END IF
LET r1 = d.getCurrentRow("sr1")
LET r2 = d.getCurrentRow("sr2")
IF r1 == 0 THEN
CALL d.appendRow("sr1")
LET r1 = 1
ELSE
CALL d.insertRow("sr1", r1)
END IF
LET arr1[r1].name = arr3[r3].name
LET att1[r1].color = att3[r3].color
CALL d.setCurrentRow("sr1", r1)
IF r2 == 0 THEN
CALL d.appendRow("sr2")
LET r2 = 1
ELSE
CALL d.insertRow("sr2", r2)
END IF
LET arr2[r2].name = arr3[r3].name
LET att2[r2].color = att3[r3].color
CALL d.setCurrentRow("sr2", r2)
CALL d.deleteRow("sr3", r3)
CALL setup(d)
END FUNCTION
FUNCTION fillarr(a, c, p, m, d)
DEFINE a DYNAMIC ARRAY OF RECORD name STRING END RECORD
DEFINE c DYNAMIC ARRAY OF RECORD color STRING END RECORD
DEFINE p, d STRING
DEFINE m, i INTEGER
CALL a.clear()
CALL c.clear()
FOR i = 1 TO m
LET a[i].name = p || "-" || i
LET c[i].color = d
END FOR
END FUNCTION
+54
View File
@@ -0,0 +1,54 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
LAYOUT ( TEXT = "3 Lists" )
GRID
{
<t t1 ><g g><t t2 >
List 1 [b12] List 2
[c1 ] [c2 ]
[c1 ] [c2 ]
[c1 ] [c2 ]
[c1 ] [c2 ]
[c1 ][b21][c2 ]
< >< >< >
<g g2 >
[bx1: :bxc:bx3: :bx2]
< >
<t t3 >
List 3
[c3 ]
[c3 ]
[c3 ]
[c3 ]
[c3 ]
[c3 ]
}
END
END
ATTRIBUTES
EDIT c1 = FORMONLY.col1 TYPE VARCHAR;
EDIT c2 = FORMONLY.col2 TYPE VARCHAR;
EDIT c3 = FORMONLY.col3 TYPE VARCHAR;
GROUP g: TEXT="", GRIDCHILDRENINPARENT;
BUTTON b12 : move_1_to_2, IMAGE="arright";
BUTTON b21 : move_2_to_1, IMAGE="arrleft";
GROUP g2: TEXT="", GRIDCHILDRENINPARENT;
BUTTON bx1 : move_1_to_3, IMAGE="arrdown";
BUTTON bx2 : move_2_to_3, IMAGE="arrdown";
BUTTON bx3 : move_3_to_12, IMAGE="arrup";
BUTTON bxc : clear_current, IMAGE="eraser";
END
INSTRUCTIONS
SCREEN RECORD sr1 (FORMONLY.col1);
SCREEN RECORD sr2 (FORMONLY.col2);
SCREEN RECORD sr3 (FORMONLY.col3);
END
+71
View File
@@ -0,0 +1,71 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
DEFINE arr1 DYNAMIC ARRAY OF VARCHAR(50)
DEFINE arr2 DYNAMIC ARRAY OF VARCHAR(50)
DEFINE nbrows1, nbrows2 INTEGER
MAIN
OPTIONS INPUT WRAP
OPEN FORM f1 FROM "TwoLists"
DISPLAY FORM f1
LET nbrows1 = 5
CALL fillarr(arr1, "First List", nbrows1)
LET nbrows2 = 7
CALL fillarr(arr2, "Second List", nbrows2)
DIALOG ATTRIBUTES(FIELD ORDER FORM, UNBUFFERED)
DISPLAY ARRAY arr1 TO sr1.*
END DISPLAY
DISPLAY ARRAY arr2 TO sr2.*
END DISPLAY
INPUT BY NAME nbrows1, nbrows2 ATTRIBUTES(WITHOUT DEFAULTS)
END INPUT
COMMAND "Clear List 1"
CALL arr1.clear()
COMMAND "Clear List 2"
CALL arr2.clear()
COMMAND "Fill List 1"
CALL fillarr(arr1, "Command Fill 1", nbrows1)
COMMAND "Fill List 2"
CALL fillarr(arr2, "Command Fill 2", nbrows2)
ON KEY(CONTROL-F)
CASE DIALOG.getCurrentItem()
WHEN "sr1" CALL fillarr(arr1, "Key Fill 1", nbrows1)
WHEN "sr2" CALL fillarr(arr2, "Key Fill 2", nbrows2)
END CASE
COMMAND "Quit"
EXIT DIALOG
END DIALOG
END MAIN
FUNCTION fillarr(a, p, m)
DEFINE a DYNAMIC ARRAY OF VARCHAR(50)
DEFINE p STRING
DEFINE m, i INTEGER
CALL a.CLEAR()
FOR i = 1 TO m
CALL a.appendElement()
LET a[i] = p || " : " || i
END FOR
END FUNCTION
+43
View File
@@ -0,0 +1,43 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
SCREEN -- Can be executed in TUI mode
{
This demo can be executed in TUI mode (FGLGUI=0)
Tab to the fields, change the row count and select
the fill option or tab to a list and hit CONTROL-F
----------------------------------------------------
List 1 List 2
[c1 ][c2 ]
[c1 ][c2 ]
[c1 ][c2 ]
[c1 ][c2 ]
[c1 ][c2 ]
[c1 ][c2 ]
[c1 ][c2 ]
[c1 ][c2 ]
[c1 ][c2 ]
[c1 ][c2 ]
----------------------------------------------------
Rows in list1: [f1 ]
Rows in list2: [f2 ]
}
END
ATTRIBUTES
EDIT c1 = FORMONLY.col1 TYPE VARCHAR;
EDIT c2 = FORMONLY.col2 TYPE VARCHAR;
EDIT f1 = FORMONLY.nbrows1 TYPE INTEGER;
EDIT f2 = FORMONLY.nbrows2 TYPE INTEGER;
END
INSTRUCTIONS
SCREEN RECORD sr1 (FORMONLY.col1);
SCREEN RECORD sr2 (FORMONLY.col2);
END
+37
View File
@@ -0,0 +1,37 @@
# Property of Four Js*
# (c) Copyright Four Js 1995, 2019. All Rights Reserved.
# * Trademark of Four Js Development Tools Europe Ltd
# in the United States and elsewhere
#
# Four Js and its suppliers do not warrant or guarantee that these
# samples are accurate and suitable for your purposes. Their inclusion is
# purely for information purposes only.
FORMS=\
CityList.42f\
TwoLists.42f\
ThreeLists.42f\
QueryCustomers.42f\
CustListEdit.42f\
Mailer.42f\
SubBlockActions.42f
MODULES=\
MsgBox.42m\
CityList.42m\
Styles.42m\
TwoLists.42m\
ThreeLists.42m\
QueryCustomers.42m\
CustListEdit.42m\
Mailer.42m\
SubBlockActions.42m
SUBDIRS=\
FormWizard\
FileDlg\
Folder\
ListAndEditableDetail\
MultiMasterDetail\
include ../Makeincl