rexx-things/modules/windows/oodialog/controls/userStringDTP.rex
2025-03-12 20:50:48 +00:00

647 lines
29 KiB
Rexx
Executable File

/*----------------------------------------------------------------------------*/
/* */
/* Copyright (c) 2011-2014 Rexx Language Association. All rights reserved. */
/* */
/* This program and the accompanying materials are made available under */
/* the terms of the Common Public License v1.0 which accompanies this */
/* distribution. A copy is also available at the following address: */
/* https://www.oorexx.org/license.html */
/* */
/* Redistribution and use in source and binary forms, with or */
/* without modification, are permitted provided that the following */
/* conditions are met: */
/* */
/* Redistributions of source code must retain the above copyright */
/* notice, this list of conditions and the following disclaimer. */
/* Redistributions in binary form must reproduce the above copyright */
/* notice, this list of conditions and the following disclaimer in */
/* the documentation and/or other materials provided with the distribution. */
/* */
/* Neither the name of Rexx Language Association nor the names */
/* of its contributors may be used to endorse or promote products */
/* derived from this software without specific prior written permission. */
/* */
/* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS */
/* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT */
/* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS */
/* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT */
/* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, */
/* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED */
/* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, */
/* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY */
/* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING */
/* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS */
/* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
/* */
/*----------------------------------------------------------------------------*/
/**
* An example of a date and time picker control and how to handle the USERSTRING
* event notification.
*
* This program mimics an application that could be used to change the system
* date and time. It allows the user to enter a new system date and or time
* through a DTP control. Once a new date and / or new time are displayed in
* the DTP control, if the user clicks the ok button the system date and time
* is updated to the date and time displayed in the control.
*
* When a DTP control has the CANPARSE style, it allows the user to directly
* enter text in the display area of the DTP. When the user is done editing,
* a USERSTRING event notification is sent. The programmer connects this event
* and then handles the notification by updating the DTP control based on the
* text the user entered.
*
* This program allows the user to directly enter what are termed 'shortcut'
* strings.
*
* When the program starts, the DTP control is set to the current system date
* and time. Of course as the dialog executes the system time is advancing. If
* the user has changed the DTP to a new date and / or time, but then wants to
* undo those changes, it would be difficult to manually set the DTP control to
* the current system date and time. So that operation is one set of shortcuts.
*
* The other set of shortcuts allows the user to directly go to some time or
* date by just typing it in. For instance, if the user wanted to change the
* date to May 12, 2050, rather than page through the DTP control to get there,
* the user can just type 5/12/2050 directly in the DTP control.
*
* To directly type in the DTP control, the user can either use the F2 key, or
* mouse click on the DTP control's display area when the DTP currently has the
* focus. This is a (little known?) feature of the DTP control itself. It is
* not something that the ooDialog framework provides.
*
* The valid shortcuts are these:
*
* Reset shortcut:
*
* This shortcut resets any changes to the date and or time to none. It also
* resets the date and time picker control to the current systeme date and
* time.
*
* Type 'reset', 'cancel' 'r', or 'c' Case is not significant.
*
* New date and or new time shortcut:
*
* This shortcut allows directly entering a new date, or a new time, or both.
*
* Formats for new date: mm/dd, or mm/dd/yyyy
*
* Formats for new time: hh:mm, or hh:mm:ss
*
* When both date and time are entered, the date must precede the time. The
* date and time must be separated by one or more spaces. It is not required
* to enter a date and a time. Only a new date can be entered or only a new
* time.
*
* Months (mm), days (dd), hours (hh), minutes (mm), or seconds(ss) can
* contain a leading 0, but are not required to. I.e., for June 5th, the new
* date string can be 06/05, 6/05, 06/5, or 6/5. The year (yyyy) portion of
* the new date string is required to be the 4 digit year. I.e. 6/5/11 is not
* valid for June 5th 2011.
*/
sd = locate()
.application~setDefaults('O', sd'userStringDTP.h', .false)
dlg = .SystemTimeDlg~new(sd'userStringDTP.rc', IDD_SYSTEMTIME)
dlg~execute("SHOWTOP")
return 0
::requires "ooDialog.cls"
::class 'SystemTimeDlg' subclass RcDialog
/** initDialog()
*
* initDialog() is automatically invoked by the ooDialog framework when the
* underlying Windows dialog is created. It is used to do initialization that
* can only be done when the underlying dialog and dialog controls exist.
*
* This is a typical dialog initialization.
*/
::method initDialog
expose curDate curTime dtp newDate newTime resetting colorIsSet stInvalid
-- We need to be sure the Rexx stInvalid object exists before we connect the
-- events to onFocus(). Otherwise, the events can fire with stInvalid being
-- .nil.
stInvalid = self~newStatic(IDC_ST_INVALID)~~setText("")
self~connectDateTimePickerEvent(IDC_DTP_SYSTIME, "SETFOCUS", onFocus)
self~connectDateTimePickerEvent(IDC_DTP_SYSTIME, "KILLFOCUS", onFocus)
self~connectDateTimePickerEvent(IDC_DTP_SYSTIME, "DROPDOWN", onFocus)
-- Connect the help key event to our onHelp() method.
self~connectHelp(onHelp)
-- Connect the other DTP control event notifications and initialize the DTP.
self~connectDateTimePickerEvent(IDC_DTP_SYSTIME, "USERSTRING", onUserString)
self~connectDateTimePickerEvent(IDC_DTP_SYSTIME, "DATETIMECHANGE", onDateTime)
dtp = self~newDateTimePicker(IDC_DTP_SYSTIME);
dtp~setFormat("'Today is:' dddd MMMM dd, yyyy 'at' hh':'mm':'ss tt")
-- Save references to the read only edit controls.
curDate = self~newEdit(IDC_EDIT_CUR_DATE)
curTime = self~newEdit(IDC_EDIT_CUR_TIME)
newDate = self~newEdit(IDC_EDIT_NEW_DATE)
newTime = self~newEdit(IDC_EDIT_NEW_TIME)
-- Set the new date and new time fields to 'no change at present.'
self~resetNewDateTime
-- Begin updating the current date and time fields with the current system
-- date and time.
self~start('updateSysTime')
/** resetNewDateTime()
*
* The new date and new time fields display the date and time that the user has
* selected to set the system date and time to. The fields are updated every
* time the user changes the date and time in the DTP control.
*
* However, once the user has changed the DTP time and date, one of the
* shortcuts can be used to reset the DTP to the current system date and time.
* At this point the new date and new time fields are set to blank and the color
* of the fields is set to green, to indicate that no change will be made if the
* user clicks the ok button a this time.
*/
::method resetNewDateTime unguarded private
expose newDate newTime resetting colorIsSet
newDate~setText('')
newTime~setText('')
self~setControlColor(IDC_EDIT_NEW_DATE, 3)
self~setControlColor(IDC_EDIT_NEW_TIME, 3)
newDate~redraw
newTime~redraw
resetting = .false
colorIsSet = .false
/** onDateTime()
*
* This is the event handler for the DATETIMECHANGE event. This method is
* invoked each time there is a change in the DTP control.
*
* There are 2 cases here we need to check for. Either, the user is changing
* the date and / or time to set the system to a new time, or the user has used
* the short cut to set the DTP control to the current system data and time.
*
* When the user is resetting the DTP control to the current system time, we
* need to set new date and new time back to blank - in effect undo any changes
* that may have selected a new data and time.
*
* Otherwise, the user has changed the DTP control to reflect the date and time
* she wants to change the system date and time to. In this case we update the
* new date and time fields to show what the system time will be set to if the
* ok button is clicked.
*/
::method onDateTime unguarded
expose dtp newDate newTime resetting colorIsSet stInvalid
stInvalid~setText('')
if resetting then do
self~resetNewDateTime
end
else do
parse value dtp~getDateTime with yy '-' mm '-' dd 'T' time '.' junk
if \ colorIsSet then do
self~setControlColor(IDC_EDIT_NEW_DATE, 13)
self~setControlColor(IDC_EDIT_NEW_TIME, 13)
colorIsSet = .true
end
newDate~setText(mm'/'dd'/'yy)
newTime~setText(time)
end
return 0
/** onUserString()
*
* This is the event handler for the USERSTRING event. It is invoked when the
* user has elected to type a shortcut in the DTP control and has finished
* editing.
*
* This is the main point of this example, to show how to handle the user string
* event. We examine what the user typed and determine if it is a valid
* 'shortcut' string. When it is valid, we return a .DateTime object with the
* date and time we want the DTP control to update its display to. If it is not
* valid we return the same .DateTime object we received.
*
* When the returned date and time is different from the DTP control's current
* date an it, the control updates its display to the returned date and time.
* When the returned date and time is the same as the DTP controls current date
* and time, the DTP does nothing.
*
* When users type a shortcut string, they will expect the DTP control's display
* to change. However, typos are common. Without a clue as to what they did
* wrong, it is hard for users to know what happened. So, it the user string is
* not valid, we display a clue to the problem by setting the IDC_ST_INVALID
* static control's text to an error message.
*
* Other than those basic principles, it is merely a matter of determining if
* the user typed a valid shortcut, or not.
*/
::method onUserString unguarded
expose resetting stInvalid
use arg dt, userStr, id, hwnd
stInvalid~setText('')
-- Check for the shortcut to set the DTP to the current date and time.
upStr = userStr~upper
if upStr == 'C' | upStr == 'R' | upStr == 'CANCEL' | upStr == 'RESET' then do
resetting = .true
return .DateTime~new
end
-- The rest is just mechanics. There is no attempt to be clever, this is just
-- a rather brute force approach. Check for things that can't possibly be
-- valid and return invalid for them. Continue doing that until we can parse
-- out month, day, year, hour, minutes, seconds. Check that the parsed values
-- are valid numbers, the return a new .DateTime object based on those values.
dateStr = ''
timeStr = ''
if userStr~words == 2 then do
dateStr = userStr~word(1)
timeStr = userStr~word(2)
end
else if userStr~words == 1 then do
if userStr~pos('/') <> 0 then dateStr = userStr
else if userStr~pos(':') <> 0 then timeStr = userStr
end
else do
return self~invalid(userStr, dt)
end
slashes = dateStr~countStr('/')
colons = timeStr~countStr(':')
if slashes == 0, colons == 0 then return self~invalid(userStr, dt)
if slashes > 2 | colons > 2 then return self~invalid(userStr, dt)
parse value dt~isoDate with yy '-' mm '-' dd 'T' hh ':' min ':' ss '.' junk
nYY = -1; nMM = -1; nDD = -1; nHH = -1; nMin = -1; nSS = -1
if slashes == 1 then do
parse var dateStr nMM '/' nDD .
nMM = nMM~strip~strip( , '0')
nDD = nDD~strip~strip( , '0')
if \ nMM~dataType('W') | \ nDD~dataType('W') then return self~invalid(userStr, dt)
if nMM < 1 | nMM > 12 then return self~invalid(userStr, dt)
if nDD < 1 | nDD > 31 then return self~invalid(userStr, dt)
end
else if slashes == 2 then do
parse var dateStr nMM '/' nDD '/' nYY .
nMM = nMM~strip~strip( , '0')
nDD = nDD~strip~strip( , '0')
nYY = nYY~strip~strip( , '0')
if \ nMM~dataType('W') | \ nDD~dataType('W') | \ nYY~dataType('W') then return self~invalid(userStr, dt)
if nMM < 1 | nMM > 12 then return self~invalid(userStr, dt)
if nDD < 1 | nDD > 31 then return self~invalid(userStr, dt)
if nYY < 1601 | nYY > 9999 then return self~invalid(userStr, dt)
end
if colons == 1 then do
parse var timeStr nHH ':' nMin .
nHH = nHH~strip~strip( , '0')
nMin = nMin~strip~strip( , '0')
if \ nHH~dataType('W') | \ nMin~dataType('W') then return self~invalid(userStr, dt)
if nHH < 0 | nHH > 23 then return self~invalid(userStr, dt)
if nMin < 0 | nMin > 59 then return self~invalid(userStr, dt)
end
else if colons == 2 then do
parse var timeStr nHH ':' nMin ':' nSS .
nHH = nHH~strip~strip( , '0')
nMin = nMin~strip~strip( , '0')
nSS = nSS~strip~strip( , '0')
if \ nHH~dataType('W') | \ nMin~dataType('W') | \ nSS~dataType('W') then return self~invalid(userStr, dt)
if nHH < 0 | nHH > 23 then return self~invalid(userStr, dt)
if nMin < 0 | nMin > 59 then return self~invalid(userStr, dt)
if nSS < 0 | nSS > 59 then return self~invalid(userStr, dt)
end
if nYY <> -1 then yy = nYY
if nMM <> -1 then mm = nMM~right(2, 0)
if nDD <> -1 then dd = nDD~right(2, 0)
if nHH <> -1 then hh = nHH~right(2, 0)
if nMin <> -1 then min = nMin~right(2, 0)
if nSS <> -1 then ss = nSS~right(2, 0)
-- Okay, things should be good, but we haven't checked for something like
-- February 30, or June 31. Construct our ISO string and then check it is
-- valid.
isoStr = yy'-'mm'-'dd'T'hh':'min':'ss'.000000'
if \ self~validIso(isoStr) then return self~invalid(userStr, dt)
nDT = .DateTime~fromIsoDate(isoStr)
return nDT
/** updateSysTime()
*
* This method is started and runs asynchronously to the rest of the dialog. It
* updates the current System date and time as the dialog is running.
*
* When the dialog is started the current date and the current time read only
* edit controls are set to the system date and time. Of course, the correct
* system date and time is constantly incrementing. This method updates the
* edit with the correct current time approximately every second. We do this by
* setting an alarm to fire every second that re-invokes this method.
*
* To get the current date and time, we instantiate a new .DateTime object each
* time this method is inovked. This makes things very easy for us, and auto-
* matically takes care of any 'drift' in the time. For instance, if we just
* added a second, to the last time, each time we are invoked, we would slowly
* drift away from the correct time. This is due to the fact that the timer
* can not be 100% accurate. Also there is some time taken up in processing
* each time we are invoked.
*/
::method updateSysTime unguarded
expose curDate curTime updateAlarm
-- If the dialog is ended, quit.
if \ self~isDialogActive then return
parse value .DateTime~new with yy '-' mm '-' dd 'T' time '.' junk
curDate~setText(mm'/'dd'/'yy)
curTime~setText(time)
timerMsg = .Message~new(self, updateSysTime)
updateAlarm = .Alarm~new(1, timerMsg)
/** onFocus
*
* This event handler is connected to several of the DTP control's event
* notifications. Its only purpose is to erase the 'Invalid shortcut string'
* text.
*/
::method onFocus unguarded
expose stInvalid
stInvalid~setText("")
return 0
/** invalid()
*
* Convenience method called from onUserString() when the user typed an invalid
* shortcut. It sets a static control to message showing the invalid shortcut.
* This gives the user a visual clue as to why the time was not updated.
*/
::method invalid unguarded private
expose stInvalid
use strict arg userStr, dt
stInvalid~setText('Invalid shortcut string:' userStr)
return dt
/** validIso()
*
* This convenience method is called from onUserString() when the method gets to
* the point where it thinks it has a valid shortcut to update to a new date and
* time.
*
* But, it hasn't checked for things like June 31 or February 29.
*
* Here, rather than do an elaborate table lookup to check for invalid days, we
* take a simple approach. We trap a syntax condition and then just try to
* instantiate a .DateTime object with the string. If a condition is raised, we
* say - no the string is not valid. If no condition is raised we say - yes the
* string is valid.
*/
::method validIso unguarded private
use strict arg str
signal on syntax
.DateTime~fromIsoDate(str)
return .true
syntax:
return .false
/** cancel()
*
* The event handler for the cancel event. This is invoked when the user closes
* the dialog, either through the cancel button or the escape key.
*
* We intercept the event to cancel the 'current' time updating, and then
* forward the message on to let the superclass handle the ending of the dialog
* prorperly.
*
* This is probably not really needed. If the alarm was not canceled, then the
* next time the alarm went off, updatesysTime() would see that the dialog was
* ended and quit on its own.
*/
::method cancel unguarded
expose updateAlarm
updateAlarm~cancel
return self~cancel:super
/** ok()
*
* The event handler for the OK event. This is invoked when the user clicks the
* ok button.
*
* In a real application, this is where the system date and time would be
* reset, if the user actually had a new date and time selected. Here we just
* put up a message box 'saying' we reset the date and time.
*
* Note that the newDate and newTime will not contain any text, *unless*, the
* user has actually changed the date or time in the DTP control. This makes a
* convenient check to see if we need to do anthing.
*/
::method ok unguarded
expose updateAlarm dtp newDate newTime
updateAlarm~cancel
nDate = newDate~getText
if nDate \== "" then do
nTime = newTime~getText
msg = "Set new system date and time to" nDate nTime
title = "System Date Time Reset"
j = MessageDialog(msg, self~hwnd, title)
end
return self~ok:super
/** help()
*
* This is the event handler for the IDHELP command. Any button or menu item
* with the resource ID of IDHELP will generate the HELP command event. The
* ooDialog framework automatically connects the IDOK, IDCANCEL, and IDHELP
* commands to the ok(), cancel(), and help() methods in the dialog object.
*
* The default help() method does nothing. To do something the programmer over-
* rides the method in her subclass. Here we simply invoke the onHelp() method
* to do the work.
*
* Do not confuse the HELP command event with the help key (F1) event.
*/
::method help
self~onHelp
/** onHelp()
*
* This is the event handler for the help key (F1) event. The event is
* generated when the user presses the F1 key.
*
* The programmer connects this event to a method in the Rexx dialog by using
* the connectHelp() method. Here in this event handler, we show a modal dialog
* with some help text.
*
* Do not confuse the help key (F1) event with the HELP command event.
*/
::method onHelp
.SystemTimeHelp~new(.application~srcDir'userStringDTP.rc', IDD_HELP)~execute("SHOWTOP")
::class 'SystemTimeHelp' subclass RcDialog
::method initDialog
expose newFont e visibleLines
e = self~newEdit(IDC_HELP_TEXT)
-- Create a mono-spaced font for the edit control that displays the help text.
newFont = self~createFontEx('Courier New', 9)
-- Set the font of the edit control to our custom font, set the text of the
-- edit control to our help text.
e~setFont(newFont)
e~setText(getHelpText())
/** leaving()
*
* The leaving method is invoked automatically by the ooDialog framework
* immediately before the dialog is destroyed.
*
* At this point, the underlying Windows dialog still exists. This makes the
* leaving method the proper place to do clean up. Especially if some of the
* clean up, as does deleteFont(), requires the underlying dialog to exist.
*
* Here we delete the mono-spaced font created for the help text display.
*/
::method leaving
expose newFont
self~deleteFont(newFont)
/** routine::getHelpText()
*
* This is convenience routine that returns the help text for the examp program.
*
* The help text is constructed here as a single string.
*
* The text could be contained in a file and read in when needed. To reduce the
* nubmer of files needed for the program, the text was just typed into the
* program file.
*/
::routine getHelpText
txt = -
"" || .endOfLine || -
"" || .endOfLine || -
" Changing the System Date and Time" || .endOfLine || -
" =================================" || .endOfLine || -
"" || .endOfLine || -
" This program allows you to change the system date and or time by" || .endOfLine || -
" setting the displayed date and time in the date and time picker control" || .endOfLine || -
" in the center of the dialog to the desired new date (and / or new" || .endOfLine || -
" time.)" || .endOfLine || -
"" || .endOfLine || -
" User Interface" || .endOfLine || -
" ==============" || .endOfLine || -
"" || .endOfLine || -
" Two fields on the left of the dialog show the current system date and" || .endOfLine || -
" time." || .endOfLine || -
"" || .endOfLine || -
" Two fields on the right of the dialog show the date and time the system" || .endOfLine || -
" date and time will be changed to, if the user clicks the okay button at" || .endOfLine || -
" that point. When there is no change to the system date and time, the" || .endOfLine || -
" fields are empty and colored greenish as a visual clue that nothing" || .endOfLine || -
" will change. When there will be a change to the system date and time" || .endOfLine || -
" the fields are colored reddish." || .endOfLine || -
"" || .endOfLine || -
" The new system date and time are specified by changing the date and /" || .endOfLine || -
" or time in the date and time picker control in the center of the" || .endOfLine || -
" dialog. This is done through the standard date and time picker user" || .endOfLine || -
" interface." || .endOfLine || -
"" || .endOfLine || -
" This program allows directly entering what are termed 'shortcut'" || .endOfLine || -
" strings." || .endOfLine || -
"" || .endOfLine || -
" When the program starts, the DTP control is set to the current system" || .endOfLine || -
" date and time. Of course as the dialog executes the system time is" || .endOfLine || -
" advancing. If the DTP is changed to a new date and / or time, but then" || .endOfLine || -
" those changes need to be undone, it would be difficult to manually set" || .endOfLine || -
" the DTP control to the current system date and time. So that operation" || .endOfLine || -
" is one set of shortcuts." || .endOfLine || -
"" || .endOfLine || -
" The other set of shortcuts allows directly changing the DTP display to" || .endOfLine || -
" some time or date by just typing it in. For instance, to change the" || .endOfLine || -
" date to May 12, 2050, rather than page through the DTP control to get" || .endOfLine || -
" there, 5/12/2050 can be typed directly in the DTP control." || .endOfLine || -
"" || .endOfLine || -
" To directly type in the DTP control, use either the F2 key, or mouse" || .endOfLine || -
" click on the DTP control's display area when the DTP currently has the" || .endOfLine || -
" focus. This is a (little known?) feature of the DTP control itself." || .endOfLine || -
"" || .endOfLine || -
" The valid shortcuts are these:" || .endOfLine || -
"" || .endOfLine || -
" Reset shortcut:" || .endOfLine || -
"" || .endOfLine || -
" This shortcut resets any changes to the date and or time to none. It" || .endOfLine || -
" also resets the date and time picker control to the current system" || .endOfLine || -
" date and time." || .endOfLine || -
"" || .endOfLine || -
" Type 'reset', 'cancel' 'r', or 'c' Case is not significant." || .endOfLine || -
"" || .endOfLine || -
" New date and or new time shortcut:" || .endOfLine || -
"" || .endOfLine || -
" This shortcut allows directly entering a new date, or a new time," || .endOfLine || -
" or both." || .endOfLine || -
"" || .endOfLine || -
" Formats for new date: mm/dd, or mm/dd/yyyy" || .endOfLine || -
"" || .endOfLine || -
" Formats for new time: hh:mm, or hh:mm:ss" || .endOfLine || -
"" || .endOfLine || -
" When both date and time are entered, the date must precede the time." || .endOfLine || -
" The date and time must be separated by one or more spaces. It is not" || .endOfLine || -
" required to enter both a date and a time. Only a new date can be" || .endOfLine || -
" entered, or only a new time." || .endOfLine || -
"" || .endOfLine || -
" Months (mm), days (dd), hours (hh), minutes (mm), or seconds(ss) can" || .endOfLine || -
" contain a leading 0, but are not required to. I.e., for June 5th," || .endOfLine || -
" the new date string can be 06/05, 6/05, 06/5, or 6/5. The year" || .endOfLine || -
" (yyyy) portion of the new date string is required to be the 4 digit" || .endOfLine || -
" year. I.e. 6/5/11 is not valid for June 5th 2011." || .endOfLine || -
"" || .endOfLine
return txt