# KeyReader.java

    /*
    ** Module   : KeyReader.java
    ** Abstract : helper methods to manage reading a key/char and accessing the
    **            last key read
    **
    ** Copyright (c) 2005-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- ----------------------------------Description-----------------------------------
    ** 001 GES 20051109   @23284 Created initial version, supporting helper
    **                           methods to manage reading a key/char and
    **                           accessing the last key read.
    ** 002 NVS 20051212   @23679 LogicalTerminal.readKey() is called which was
    **                           previously commented out since LT wasn't
    **                           fully implemented.
    ** 003 GES 20060115   @23922 Better safety code for stream access.
    ** 004 GES 20060123   @24020 Moved to context-local storage.
    ** 005 GES 20060207   @24326 Ensure that lastkey is set to -1 in case
    **                           there is a StopConditionException while
    **                           reading.
    ** 006 GES 20060207   @24346 Added new readkey signatures and otherwise
    **                           handle new unnamed streams interface.
    ** 007 NVS 20070725   @34686 Renamed isInOK() method to isIn() for the
    **                           sake of uniformity.
    ** 008 RK  20080227   @37177 Javadoc has been updated to state the
    **                           different initial values for different
    **                           terminals.
    ** 009 GES 20080320   @37632 Added some LAST-EVENT system handle support
    **                           (for the FUNCTION attribute).
    ** 010 GES 20090624   @42884 isIn() usage to determine if the stream represents
    **                           the terminal is switched to isTerm(). This fixes
    **                           a problem where a terminal was incorrectly
    **                           calculated when there was a closed input stream
    **                           (which should fail with an error instead of
    **                           automatically redirecting to the terminal).
    ** 011 CA  20130112          Added asHandle, getType, isValid and isUnknown
    **                           methods, accessed by the LAST-EVENT handle.
    ** 012 CA  20130221          Added LABEL support. getType was renamed to getResourceType.
    **                           DATA-TYPE, COLUMN-LABEL and FORMAT attributes are not supported (but
    **                           forced to be defined by CommonField interface) so proper error APIs
    **                           are called.
    ** 013 OM  20130304          Refactored isValid and isUnknown of WrappedResource to valid
    **                           and unknown.
    ** 014 CA  20130927          Static proxies need the unknown() API. Resource type is determined
    **                           from LegacyResource constants.
    ** 015 EVL 20131014          Adding annotation for legacy attributes to be ignored for the class.
    ** 016 VIG 20131122          Adding support of LAST-EVENT:LABEL and
    **                           LAST-EVENT:EVENT-TYPE attributes.
    ** 017 GES 20140116          Code formatting and javadoc cleanup.
    ** 018 CA  20141021          Added support for legacy MOUSE events.
    ** 019 CA  20141029          A minor improvement for setting the LAST-EVENT attrs, when the key is
    **                           read via READKEY.
    ** 020 CA  20150502          The resource ID needs to be kept at the resource, so it can be
    **                           accessed after the resource is deleted.
    ** 021 IAS 20150801          WIDGET-LEAVE/WIDGER-ENTER support.
    ** 022 OM  20150926          X/Y attributes support.
    ** 023 CA  20160327          Finished X/Y attribute implementation.
    ** 024 CA  20171218          Fixed LAST-EVENT:WIDGET-ENTER/LEAVE attributes, in cases when
    **                           ROW-ENTRY/LEAVE events are used.
    ** 025 HC  20181212          Implemented support for asynchronous key state read emulation.
    ** 026 CA  20200211          Allow fractional seconds (convert to millis based on the legacy version).
    ** 027 GES 20200627          Renamed Coordinates interface to be more generic.  Added the undocumented
    **                           LAST-EVENT:SET-LASTKEY() method.  Added the LAST-EVENT:COLUMN/ROW attributes
    **                           support.
    **     OM  20201030          Invalid attribute API support for getters/setters.
    **     OM  20201203          Fixed handling of READ/ONLY attributes.
    ** 028 RFB 20210512          Initial value of lastkey changed from 401 to -1. Ref #5363.
    **     VVT 20211206          'state' replaced by 'modifiers' in code related to key modifiers.
    **                           See #5850-26.
    **     VVT 20211215          New methods added: setX(int), setY(int). See #5899.
    **     VVT 20221003          CommonHandle.getResourceType() method renamed to resourceType() to prevent conflicts
    **                           with namesakes in DMO. See #6694.
    ** 029 EVL 20230620          Consider the WINDOW UI mark bit to get legacy lastKey value.
    ** 030 CA  20230828          'getLastKey' must return 'integer', and not Java 'int'.
    ** 031 AB  20231107          Modified getWidgetEnter() to handle correctly the entry event when the frame is
    **                           opened.
    ** 032 SVL 20231124          Fix for ROW-ENTRY event.
    ** 033 VVT 20240206          setLastKey(key, modifiers) now does not set LAST-EVENT:LABEL. See #8068.
    **                           Code cleanup.
    ** 034 SVL 20240530          Disabled usage of keyReader.label.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.util;

    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.ui.*;
    import com.goldencode.p2j.ui.client.event.*;
    import com.goldencode.proxy.*;

    /**
     * Provides Progress compatible helper methods to manage reading a key/char
     * and accessing the last key read.  This class provides a generic interface
     * and uses the {@link Stream} implementation or the
     * <code>LogicalTerminal</code> implementation to back the service based
     * on runtime state.
     * <p>
     * <b>The initial last key value is currently set to 401 which is valid for
     * only a sub-set of terminal types (e.g xterm/vt100). It is known that for
     * vt220/vt320 type terminals this would need to be corrected (they report
     * -1 in Progress).  For other terminal types, the initial value is not known.
     * The solution will require that a terminal specific value be reported
     * depending on runtime data. This solution has not yet been defined. Part of
     * the problem with the solution is that the server has no knowledge of the
     * terminal type nor does it currently have any other terminal type
     * dependencies.</b>
     */
    public class KeyReader
    {
       /** LAST-EVENT:EVENT-TYPE for high-level Progress events */
       private static final String PROGRESS_EVTYPE = "PROGRESS";

       /** LAST-EVENT:EVENT-TYPE for keyboard Progress events */
       private static final String KEY_EVTYPE = "KEYPRESS";

       /** LAST-EVENT:EVENT-TYPE for mouse Progress events */
       private static final String MOUSE_EVTYPE = "MOUSE";

       /** Context-local data area for last key state. */
       private static ContextContainer workArea = new ContextContainer();

       /**
        * Get a the instance for the LAST-EVENT system handle.  Is obtained using
        * a call to the {@link StaticProxy#obtain(Class, Class[])}, using the
        * {@link CommonLastEvent} interface and its methods implemented by these
        * classes: {@link KeyReader}.
        *
        * @return   See above.
        */
       public static handle asHandle()
       {
          WorkArea wa = workArea.obtain();

          Class[] classes = { KeyReader.class };
          CommonLastEvent proxy = StaticProxy.obtain(CommonLastEvent.class, classes);

          if (wa.id == null)
          {
             wa.id = handle.resourceId(proxy);
          }

          return new handle(proxy);
       }

       /**
        * Get this resource's ID.
        *
        * @return   The resource's ID.
        */
       public static Long id()
       {
          WorkArea wa = workArea.obtain();

          return wa.id;
       }

       /**
        * Set this resource's ID.
        * <p>
        * This is a no-op for system handles.
        *
        * @param    id
        *           The resource's ID.
        */
       public static void id(@SuppressWarnings("unused") long id)
       {
          // no-op
       }

       /**
        * Implementation for the {@link WrappedResource#valid()} API.
        *
        * @return   Always true.
        */
       public static boolean valid()
       {
          return true;
       }

       /**
        * Implementation for the {@link WrappedResource#unknown()} API.
        *
        * @return   Always false.
        */
       public static boolean unknown()
       {
          return false;
       }

       /**
        * API needed to implement read-only attribute assignment (a 4GL "feature").
        *
        * @param    attribute
        *           The attribute's name.
        *
        * @see      handle#readOnlyError(handle, String)
        */
       public static void readOnlyError(String attribute)
       {
          handle.readOnlyError(asHandle(), attribute);
       }

       /**
        * API needed to implement read-only attribute assignment (a 4GL "feature").
        *
        * @param    attribute
        *           The attribute's name.
        * @param    expr
        *           The value which is attempted to be assigned to the read-only attribute.
        *
        * @see      handle#readOnlyError(handle, String, Object)
        */
       public static void readOnlyError(String attribute, @SuppressWarnings("unused") Object expr)
       {
          handle.readOnlyError(asHandle(), attribute);
       }

       /**
        * Obtains the last key read from any source (stream or terminal).
        *
        * @return   The last key read.
        */
       public static integer lastKey()
       {
          return getLastKey();
       }

       /**
        * Obtains the last key read from any source (stream or terminal).
        *
        * @return   The last key read.
        */
       public static integer getLastKey()
       {
          return new integer(workArea.obtain().lastkey);
       }

       /**
        * Sets the event type and last key read from any source (stream or terminal). This is an undocumented
        * method for the LAST-EVENT system handle that was found in customer source code.
        *
        * @param    etype
        *           The event-type of the last event.  This may be 0 (for KEYPRESS), 1 (for MOUSE).  If one of
        *           these valid values is passed, then the method will change the event state and return
        *           {@code true}.  With any other value, the method will not change state and will return
        *           {@code false}.
        * @param    kcode
        *           The key-code of the last event.  Any value may be passed but any changes will only be saved
        *           if the {@code etype} is valid (and the method thus returns {@code true}).  Any integer
        *           between -32768 and 32767 (inclusive) will set the key code to that same number. Any other
        *           number will be cast into a 16-bit signed integer before assignment (which will drop the most
        *           significant 48-bits).
        *
        * @return   {@code true} if the {@code etype} is valid and state is changed.  {@code false} if
        *           {@code etype} is NOT valid and state is NOT changed.
        */
       public static logical _setLastKey(long etype, long kcode)
       {
          // TODO: should we be setting the keyFunction, label and lastKeyState?

          if (etype == 0 || etype == 1)
          {
             WorkArea wa = workArea.obtain();

             wa.eventType = (etype == 0) ? KEY_EVTYPE : MOUSE_EVTYPE;
             wa.lastkey   = (short) kcode;

             return new logical(true);
          }

          return new logical(false);
       }

       /**
        * Sets the event type and last key read from any source (stream or terminal). This is an undocumented
        * method for the LAST-EVENT system handle that was found in customer source code.
        *
        * @param    etype
        *           The event-type of the last event.  This may be 0 (for KEYPRESS), 1 (for MOUSE).  If one of
        *           these valid values is passed, then the method will change the event state and return
        *           {@code true}.  With any other value, the method will not change state and will return
        *           {@code false}.  Passing unknown value is equivalent to KEYPRESS.
        * @param    kcode
        *           The key-code of the last event.  Any value may be passed but any changes will only be saved
        *           if the {@code etype} is valid (and the method thus returns {@code true}).  Any integer
        *           between -32768 and 32767 (inclusive) will set the key code to that same number. Any other
        *           number will be cast into a 16-bit signed integer before assignment (which will drop the most
        *           significant 48-bits).
        *
        * @return   {@code true} if the {@code etype} is valid and state is changed.  {@code false} if
        *           {@code etype} is NOT valid and state is NOT changed.
        */
       public static logical _setLastKey(NumberType etype, long kcode)
       {
          return _setLastKey((etype == null || etype.isUnknown()) ? 0L : etype.longValue(), kcode);
       }

       /**
        * Sets the event type and last key read from any source (stream or terminal). This is an undocumented
        * method for the LAST-EVENT system handle that was found in customer source code.
        *
        * @param    etype
        *           The event-type of the last event.  This may be 0 (for KEYPRESS), 1 (for MOUSE).  If one of
        *           these valid values is passed, then the method will change the event state and return
        *           {@code true}.  With any other value, the method will not change state and will return
        *           {@code false}.
        * @param    kcode
        *           The key-code of the last event.  Any value may be passed but any changes will only be saved
        *           if the {@code etype} is valid (and the method thus returns {@code true}).  Any integer
        *           between -32768 and 32767 (inclusive) will set the key code to that same number. Any other
        *           number will be cast into a 16-bit signed integer before assignment (which will drop the most
        *           significant 48-bits).  Passing unknown value is the same as passing 0.
        *
        * @return   {@code true} if the {@code etype} is valid and state is changed.  {@code false} if
        *           {@code etype} is NOT valid and state is NOT changed.
        */
       public static logical _setLastKey(long etype, NumberType kcode)
       {
          return _setLastKey(etype, (kcode == null || kcode.isUnknown()) ? 0L : kcode.longValue());
       }

       /**
        * Sets the event type and last key read from any source (stream or terminal). This is an undocumented
        * method for the LAST-EVENT system handle that was found in customer source code.
        *
        * @param    etype
        *           The event-type of the last event.  This may be 0 (for KEYPRESS), 1 (for MOUSE).  If one of
        *           these valid values is passed, then the method will change the event state and return
        *           {@code true}.  With any other value, the method will not change state and will return
        *           {@code false}.  Passing unknown value is equivalent to KEYPRESS.
        * @param    kcode
        *           The key-code of the last event.  Any value may be passed but any changes will only be saved
        *           if the {@code etype} is valid (and the method thus returns {@code true}).  Any integer
        *           between -32768 and 32767 (inclusive) will set the key code to that same number. Any other
        *           number will be cast into a 16-bit signed integer before assignment (which will drop the most
        *           significant 48-bits).  Passing unknown value is the same as passing 0.
        *
        * @return   {@code true} if the {@code etype} is valid and state is changed.  {@code false} if
        *           {@code etype} is NOT valid and state is NOT changed.
        */
       public static logical _setLastKey(NumberType etype, NumberType kcode)
       {
          return _setLastKey((etype == null || etype.isUnknown()) ? 0L : etype.longValue(),
                            (kcode == null || kcode.isUnknown()) ? 0L : kcode.longValue());
       }

       /**
        * Sets the last key read and main attributes: LABEL, FUNCTION, EVENT-TYPE
        *
        * @param    key
        *           The last key read (LASTKEY).
        */
       public static void setLastKey(int key)
       {
          setLastKey(key, 0);
       }

       /**
        * Sets the last key read and main attributes: LABEL, FUNCTION, EVENT-TYPE
        *
        * @param    key
        *           The last key read (LASTKEY).
        * @param    modifiers
        *           The last extended keys modifiers (MODIFIERS).
        */
       public static void setLastKey(final int key, final int modifiers)
       {
          WorkArea wa = workArea.obtain();
          wa.lastkey = key > 0 ? key & ~Keyboard.WINDOW_UI_EVENT_MASK : key;
          wa.lastKeyModifiers = modifiers;

          wa.eventType = Keyboard.isPGEvent(key)
             ? PROGRESS_EVTYPE
             : Keyboard.isMouseEvent(key)
             ? MOUSE_EVTYPE
             : KEY_EVTYPE;

          wa.keyFunction = Keyboard.keyFunction(key);
       }

       /**
        * Returns the last extended keys state.
        *
        * @return  see above.
        */
       public static int getLastKeyModifiers()
       {
          return workArea.obtain().lastKeyModifiers;
       }

       /**
        * Implementation of the LAST-EVENT system handle's FUNCTION attribute.
        *
        * @return   The key function of the last key event.
        */
       public static character getLastFunction()
       {
          //  The FUNCTION attribute read at this step can originate from two locations:
          //   - value from ClientState, cached in LogicalTerminal or
          //   - value set explicitly through setLastKey() or readKey()
          //
          //  TODO: double check if this is read/written correctly, as it doesn't look right

          int functionKey = LogicalTerminal.getFunctionKey();

          if (functionKey != KeyInput.CHAR_UNDEFINED)
          {
             setKeyFunctionWorker(functionKey);
          }

          return new character(workArea.obtain().keyFunction);
       }

       /**
        * Used internally by P2J to set the {@link WorkArea#keyFunction}
        *
        * @param    func
        *           The FUNCTION to be set.
        */
       public static void setKeyFunctionWorker(int func)
       {
          WorkArea wa = workArea.obtain();
          wa.keyFunction = Keyboard.eventName(func);
          wa.eventType = Keyboard.isPGEvent(func) ? PROGRESS_EVTYPE
                                                  : Keyboard.isMouseEvent(func) ? MOUSE_EVTYPE
                                                                                : KEY_EVTYPE;
       }

       /**
        * Read a character from the unnamed input stream (if open) or from the
        * <code>LogicalTerminal</code>, blocking until input is received.
        * <p>
        * The exact character read will be stored as the lastkey and can be
        * accessed via {@link #lastKey}.  The lastkey will be set to -1 if there
        * is any interruption or error during processing or will be set to -2 at
        * <code>EOF</code>.
        *
        * @return   The character read, -1 if there is any interruption or
        *           error during processing or -2 at <code>EOF</code>.
        *
        * @throws   StopConditionException
        *           If any interruption occurs during I/O processing.
        */
       public static int readKey()
       throws StopConditionException
       {
          return readKey(null, -1);
       }

       /**
        * Read a character from the given stream or from the
        * <code>LogicalTerminal</code>, blocking until input is received.
        * <p>
        * The exact character read will be stored as the lastkey and can be
        * accessed via {@link #lastKey}.  The lastkey will be set to -1 if there
        * is any interruption or error during processing or will be set to -2 at
        * <code>EOF</code>.
        *
        * @param    in
        *           The stream from which to read.  If <code>null</code> or if
        *           it is an instance of {@link StreamWrapper} which has no
        *           backing stream, then the <code>LogicalTerminal</code> will
        *           be read.
        *
        * @return   The character read, -1 if there is any interruption or
        *           error during processing or -2 at <code>EOF</code>.
        *
        * @throws   StopConditionException
        *           If any interruption occurs during I/O processing.
        */
       public static int readKey(Stream in)
       throws StopConditionException
       {
          return readKey(in, -1);
       }

       /**
        * Read a character from the unnamed input stream (if open) or from the
        * <code>LogicalTerminal</code>.
        * <p>
        * The exact character read will be stored as the lastkey and can be
        * accessed via {@link #lastKey}.  The lastkey will be set to -1 if there
        * is any interruption or error during processing or will be set to -2 at
        * <code>EOF</code>.
        *
        * @param    pause
        *           The number of seconds to wait for a character to be available
        *           for reading. Use 0 to get an immediate response (it will
        *           return -1 if there is no character immediately available).
        *           Use -1 to specify an infinite timeout (it will never
        *           timeout).  If set to the <code>unknown value</code>, an
        *           indefinite wait will occur.
        *
        * @return   The character read, -1 if there is any interruption or
        *           error during processing or -2 at <code>EOF</code>.
        *
        * @throws   StopConditionException
        *           If any interruption occurs during I/O processing.
        */
       public static int readKey(NumberType pause)
       throws StopConditionException
       {
          return readKey(null, pause);
       }

       /**
        * Read a character from the unnamed input stream (if open) or from the
        * <code>LogicalTerminal</code>.
        * <p>
        * The exact character read will be stored as the lastkey and can be
        * accessed via {@link #lastKey}.  The lastkey will be set to -1 if there
        * is any interruption or error during processing or will be set to -2 at
        * <code>EOF</code>.
        *
        * @param    pause
        *           The number of seconds to wait for a character to be available
        *           for reading. Use 0 to get an immediate response (it will
        *           return -1 if there is no character immediately available).
        *           Use -1 to specify an infinite timeout (it will never
        *           timeout).
        *
        * @return   The character read, -1 if there is any interruption or
        *           error during processing or -2 at <code>EOF</code>.
        *
        * @throws   StopConditionException
        *           If any interruption occurs during I/O processing.
        */
       public static int readKey(double pause)
       throws StopConditionException
       {
          return readKey(null, pause);
       }

       /**
        * Read a character from the given stream or from the
        * <code>LogicalTerminal</code>.
        * <p>
        * The exact character read will be stored as the lastkey and can be
        * accessed via {@link #lastKey}.  The lastkey will be set to -1 if there
        * is any interruption, the timeout expires without a character being read
        * or there is an error during processing.  The lastkey will be set to -2
        * at <code>EOF</code>.
        *
        * @param    in
        *           The stream from which to read.  If <code>null</code> or if
        *           it is an instance of {@link StreamWrapper} which has no
        *           backing stream, then the <code>LogicalTerminal</code> will
        *           be read.
        * @param    pause
        *           The number of seconds to wait for a character to be available
        *           for reading. Use 0 to get an immediate response (it will
        *           return -1 if there is no character immediately available).
        *           Use -1 to specify an infinite timeout (it will never
        *           timeout).  If set to the <code>unknown value</code>, an
        *           indefinite wait will occur.
        *
        * @return   The character read, -1 if there is any interruption, timeout
        *           or error during processing or -2 at <code>EOF</code>.
        *
        * @throws   StopConditionException
        *           If any interruption occurs during I/O processing.
        */
       public static int readKey(Stream in, NumberType pause)
       throws StopConditionException
       {
          return readKey(in, (pause.isUnknown() ? -1 : pause.doubleValue()));
       }

       /**
        * Read a character from the given stream or from the
        * <code>LogicalTerminal</code>.
        * <p>
        * The exact character read will be stored as the lastkey and can be
        * accessed via {@link #lastKey}.  The lastkey will be set to -1 if there
        * is any interruption, the timeout expires without a character being read
        * or there is an error during processing.  The lastkey will be set to -2
        * at <code>EOF</code>.
        *
        * @param    in
        *           The stream from which to read.  If <code>null</code> or if
        *           it is an instance of {@link StreamWrapper} which has no
        *           backing stream, then the <code>LogicalTerminal</code> will
        *           be read.
        * @param    pause
        *           The number of seconds to wait for a character to be available
        *           for reading. Use 0 to get an immediate response (it will
        *           return -1 if there is no character immediately available).
        *           Use -1 to specify an infinite timeout (it will never
        *           timeout).
        *
        * @return   The character read, -1 if there is any interruption, timeout
        *           or error during processing or -2 at <code>EOF</code>.
        *
        * @throws   StopConditionException
        *           If any interruption occurs during I/O processing.
        */
       public static int readKey(Stream in, double pause)
       throws StopConditionException
       {
          int result = -1;

          // default the key to -1 in case we are interrupted
          WorkArea wa = workArea.obtain();
          wa.lastkey = result;

          boolean terminal = false;

          if (in == null)
          {
             in       = UnnamedStreams.input();
             terminal = (in == null);
          }
          else
          {
             if (in instanceof StreamWrapper)
             {
                terminal = in.isTerm();
             }
          }

          int millis = LogicalTerminal.toMilliseconds(pause);

          if (terminal)
          {
             // read from the terminal
             result = LogicalTerminal.readKey(millis);
          }
          else
          {
             // read from a named or unnamed stream
             result = in.readChar(millis).intValue();
          }

          // save the key that was read
          setLastKey(result);

          return result;
       }

       /**
        * Get the type of its associated handle.
        *
        * @return   Always return the PSEUDO-WIDGET value.
        */
       public static character resourceType()
       {
          return new character(LegacyResource.PSEUDO_WIDGET);
       }

       /**
        * Calls {@link handle#invalidAttribute}, as the FORMAT attribute is not supported.
        *
        * @return   unknown value.
        */
       @LegacyAttribute(name = "FORMAT", ignore = true)
       public static character getFormat()
       {
          handle.invalidAttribute("FORMAT", asHandle(), false);

          return new character();
       }

       /**
        * Calls {@link handle#readOnlyError} as this attribute is not supported.
        *
        * @param    format
        *           Not used.
        */
       @LegacyAttribute(name = "FORMAT", setter = true, ignore = true)
       public static void setFormat(@SuppressWarnings("unused") character format)
       {
          handle.readOnlyError(asHandle(), "FORMAT");
       }

       /**
        * Calls {@link handle#readOnlyError} as this attribute is not supported.
        *
        * @param    format
        *           Not used.
        */
       @LegacyAttribute(name = "FORMAT", setter = true, ignore = true)
       public static void setFormat(@SuppressWarnings("unused") String format)
       {
          handle.readOnlyError(asHandle(), "FORMAT");
       }

       /**
        * Calls {@link handle#invalidAttribute}, as the COLUMN-LABEL attribute is not supported.
        *
        * @return   unknown value.
        */
       @LegacyAttribute(name = "COLUMN-LABEL", ignore = true)
       public static character getColumnLabel()
       {
          handle.invalidAttribute("COLUMN-LABEL", asHandle(), false);

          return new character();
       }

       /**
        * Calls {@link handle#readOnlyError} as this attribute is not supported.
        *
        * @param    columnLabel
        *           Not used.
        */
       @LegacyAttribute(name = "COLUMN-LABEL", setter = true, ignore = true)
       public static void setColumnLabel(@SuppressWarnings("unused") character columnLabel)
       {
          handle.readOnlyError(asHandle(), "COLUMN-LABEL");
       }

       /**
        * Calls {@link handle#readOnlyError} as this attribute is not supported.
        *
        * @param    columnLabel
        *           Not used.
        */
       @LegacyAttribute(name = "COLUMN-LABEL", setter = true, ignore = true)
       public static void setColumnLabel(@SuppressWarnings("unused") String columnLabel)
       {
          handle.readOnlyError(asHandle(), "COLUMN-LABEL");
       }

       /**
        * Calls {@link handle#invalidAttribute}, as the DATA-TYPE attribute is not supported.
        *
        * @return   unknown value.
        */
       @LegacyAttribute(name = "DATA-TYPE", ignore = true)
       public static character getDataType()
       {
          handle.invalidAttribute("DATA-TYPE", asHandle(), false);

          return new character();
       }

       /**
        * Calls {@link handle#readOnlyError} as this attribute is not supported.
        *
        * @param    dataType
        *           Not used.
        */
       @LegacyAttribute(name = "DATA-TYPE", setter = true, ignore = true)
       public static void setDataType(@SuppressWarnings("unused") String dataType)
       {
          handle.readOnlyError(asHandle(), "DATA-TYPE");
       }

       /**
        * Calls {@link handle#readOnlyError} as this attribute is not supported.
        *
        * @param    dataType
        *           Not used.
        */
       @LegacyAttribute(name = "DATA-TYPE", setter = true, ignore = true)
       public static void setDataType(@SuppressWarnings("unused") character dataType)
       {
          handle.readOnlyError(asHandle(), "DATA-TYPE");
       }

       /**
        * Returns the value of the LAST-EVENT:WIDGET-LEAVE attribute
        *
        * @return  character
        *          current value of LAST-EVENT:WIDGET-LEAVE
        */
       public static handle getWidgetLeave()
       {
          EventInfo info = LogicalTerminal.getEventInfo();
          if (info == null)
          {
             return new handle();
          }

          if ("ENTRY".equals(info.name) || "ROW-ENTRY".equals(info.name))
          {
             return LogicalTerminal.getWidgetHandleById(info.otherId);
          }
          else if ("LEAVE".equals(info.name) || "ROW-LEAVE".equals(info.name))
          {
             return LogicalTerminal.getWidgetHandleById(info.widgetId);
          }

          return new handle();
       }

       /**
        * Returns the value of the LAST-EVENT:WIDGET-ENTER attribute
        *
        * @return  character
        *          current value of LAST-EVENT:WIDGET-ENTER
        */
       public static handle getWidgetEnter()
       {
          EventInfo info = LogicalTerminal.getEventInfo();
          if (info == null)
          {
             return new handle();
          }

          if ("ENTRY".equals(info.name))
          {
             if (info.otherId == -1)
             {
                return new handle();
             }
             return LogicalTerminal.getWidgetHandleById(info.widgetId);
          }
          else if ("ROW-ENTRY".equals(info.name))
          {
             return LogicalTerminal.getWidgetHandleById(info.widgetId);
          }
          else if ("LEAVE".equals(info.name) || "ROW-LEAVE".equals(info.name))
          {
             return LogicalTerminal.getWidgetHandleById(info.otherId);
          }

          return new handle();
       }

       /**
        * Returns the value of the LAST-EVENT:EVENT-TYPE attribute
        *
        * @return  character
        *          current value of LAST-EVENT:EVENT-TYPE
        */
       public static character getEventType()
       {
          return new character(workArea.obtain().eventType);
       }

       /**
        * Returns the value of the LABEL attribute
        *
        * @return    Current value of the LABEL attribute.
        */
       public static character getLabel()
       {
           // The case LABEL=ENTRY at start of procedure when a field got a focus. This case stands
           // out from the main line of implementation. The value in this case is coming from
           // the ClientState unlike the main scenario when a label is set via setLabel(),
           // setLabelWorker(), readKey() methods.
           //
           // This code is commented because it interferes the main scenario, so the problem is not
           // solved. TODO: find solution for this case.
           //
           //       int functionKey = LogicalTerminal.getFunctionKey();
           //       if (functionKey != -1)
           //       {
           //          setLabelWorker(functionKey);
           //       }

          final WorkArea keyReader = workArea.obtain();
          //return new character(PROGRESS_EVTYPE.equals(keyReader.eventType) ? keyReader.label
          //         : LogicalTerminal.getEventLabel());
          return new character(LogicalTerminal.getEventLabel());
       }

       /**
        * Set the <code>name</code> attribute of handle.
        *
        * @param label
        *        The LABEL to be set.
        */
       @LegacyAttribute(name = "LABEL", setter = true, ignore = true)
       public static void setLabel(@SuppressWarnings("unused") String label)
       {
          // setLabel(new character(label));
          handle.readOnlyError(asHandle(), "LABEL");
       }

       /**
        * Set the <code>name</code> attribute of handle.
        *
        * @param label
        *        The LABEL to be set.
        */
       @LegacyAttribute(name = "LABEL", setter = true, ignore = true)
       public static void setLabel(@SuppressWarnings("unused") character label)
       {
          handle.readOnlyError(asHandle(), "LABEL");
       }

       /**
        * Used internally by P2J to set the {@link WorkArea#label} in server-side event cases.
        *
        * @param label
        *        The LABEL to be set.
        */
       public static void setLabelWorker(String label)
       {
          WorkArea wa = workArea.obtain();

          wa.label = label;

          // for all server-side events EVENT-TYPE=PROGRESS
          wa.eventType = PROGRESS_EVTYPE;

          // TODO: should we set the function here?
       }

       /**
        * Get the COLUMN attribute.
        *
        * @return  The column value in characters.
        */
       @LegacyAttribute(name = "COLUMN")
       public static decimal getColumn()
       {
          // TODO: we need testcases to confirm behavior
          Double col = LogicalTerminal.getEventColumn();
          return col == null ? new decimal() : new decimal(col);
       }

       /**
        * Get the ROW attribute.
        *
        * @return  The row value in characters.
        */
       @LegacyAttribute(name = "ROW")
       public static decimal getRow()
       {
          // TODO: we need testcases to confirm behavior
          Double row = LogicalTerminal.getEventRow();
          return row == null ? new decimal() : new decimal(row);
       }

       /**
        * Implementation of the LAST-EVENT system handle's X attribute.
        *
        * @return  The pixel location of the mouse cursor relative to the left edge of the display.
        */
       @LegacyAttribute(name = "X")
       public static integer getX()
       {
          Integer x = LogicalTerminal.getEventX();
          return x == null ? new integer() : new integer(x);
       }

       /**
        * Implementation of the LAST-EVENT system handle's Y attribute.
        *
        * @return  The pixel location of the mouse cursor relative to the left edge of the display.
        */
       @LegacyAttribute(name = "Y")
       public static integer getY()
       {
          Integer y = LogicalTerminal.getEventY();
          return y == null ? new integer() : new integer(y);
       }

       /**
        * Set the X attribute.
        *
        * @param   x
        *          The X attribute.
        */
       @LegacyAttribute(name = "X", setter = true)
       public static void setX(@SuppressWarnings("unused") NumberType x)
       {
          readOnlyError("X");
       }

       /**
        * Set the Y attribute.
        *
        * @param   y
        *          The Y attribute.
        */
       @LegacyAttribute(name = "Y", setter = true)
       public static void setY(@SuppressWarnings("unused") NumberType y)
       {
          readOnlyError("Y");
       }

       /**
        * Set the X attribute.
        *
        * @param   x
        *          The X attribute.
        */
       @LegacyAttribute(name = "X", setter = true)
       public static void setX(@SuppressWarnings("unused") int x)
       {
          readOnlyError("X");
       }

       /**
        * Set the Y attribute.
        *
        * @param   y
        *          The Y attribute.
        */
       @LegacyAttribute(name = "Y", setter = true)
       public static void setY(@SuppressWarnings("unused") int y)
       {
          readOnlyError("Y");
       }

       /**
        * Set the COLUMN attribute.
        *
        * @param   column
        *          The column value in characters.
        */
       @LegacyAttribute(name = "COLUMN", setter = true)
       public static void setColumn(@SuppressWarnings("unused") double column)
       {
          readOnlyError("COLUMN");
       }

       /**
        * Set the COLUMN attribute.
        *
        * @param   column
        *          The column value in characters.
        */
       @LegacyAttribute(name = "COLUMN", setter = true)
       public static void setColumn(@SuppressWarnings("unused") NumberType column)
       {
          readOnlyError("COLUMN");
       }

       /**
        * Set the ROW attribute.
        *
        * @param   row
        *          The row value in characters.
        */
       @LegacyAttribute(name = "ROW", setter = true)
       public static void setRow(@SuppressWarnings("unused") double row)
       {
          readOnlyError("ROW");
       }

       /**
        * Set the ROW attribute.
        *
        * @param   row
        *          The row value in characters.
        */
       @LegacyAttribute(name = "ROW", setter = true)
       public static void setRow(@SuppressWarnings("unused") NumberType row)
       {
          readOnlyError("ROW");
       }

       /**
        * Implementation of the ON-FRAME-BORDER system handle's EVENT-TYPE attribute.
        *
        * @return  {@code YES} if the last event was a mouse event that occurred on a frame border.
        */
       @LegacyAttribute(name = "ON-FRAME-BORDER")
       public static logical isOnFrameBorder()
       {
          UnimplementedFeature.missing("LAST-EVENT:ON-FRAME-BORDER attribute not implemented.");
          return new logical();
       }

       /**
        * Stores data relating to the state of the current context's key reading.
        */
       private static class WorkArea
       {
          /** The resource's ID. */
          private Long id = null;

          /** Stores the last key read from any source (stream or terminal). */
          private int lastkey = -1;

          /** Stores the last extended keys state. */
          private int lastKeyModifiers = 0;

          /**
           * LAST-EVENT:LABEL value that can be set explicitly (setLabelWorker) or implicitly
           * ({@link KeyReader#setLastKey(int)}, or in {@link KeyReader#readKey()}). Commonly have
           * a name of pressed key, but in some specific cases can have the same value as
           * {@link #keyFunction} for example values "CHOOSE" and "ENTRY" - at the start of procedure.
           */
          private String label;

          /**
           * LAST-EVENT:EVENT-TYPE attribute. Can have on of following values:
           * <ul>
           *   <li>"PROGRESS",</li>
           *   <li>"KEYPRESS",</li>
           *   <li>"MOUSE".</li>
           * </ul>
           * It has the "PROGRESS" values for different cases:
           * <ul>
           *   <li>CHOOSE event (button press),</li>
           *   <li>ENTRY event,</li>
           *   <li>server events such as CONNECT, PROCEDURE-COMPLETE, READ-RESPONSE,</li>
           *   <li>VALUE-CHANGED event,</li>
           *   <li>WINDOW events at OS level</li>
           * </ul>
           * and "KEYPRESS" in other cases.
           */
          private String eventType;

          /**
           * LAST-EVENT:FUNCTION attribute value that can be set explicitly (setKeyFunctionWorker).
           * The high-level name of action based on EVENT-TYPE. For EVENT-TYPE="KEYPRESS" contains
           * key function (for example "RETURN"), for EVENT-TYPE="PROGRESS" contains some high-level
           * widget event name such as "CHOOSE" or "LEAVE".
           */
          private String keyFunction;
       }

       /**
        * Simple container that stores and returns a context-local instance of
        * the global work area.
        */
       private static class ContextContainer
       extends ContextLocal
       {
          /**
           * Obtains the context-local instance of the contained global work
           * area.
           *
           * @return   The work area associated with this context.
           */
          public WorkArea obtain()
          {
             return (WorkArea) this.get();
          }

          /**
           * Initializes the work area, the first time it is requested within a
           * new context.
           *
           * @return   The newly instantiated work area.
           */
          @Override
          protected synchronized Object initialValue()
          {
             return new WorkArea();
          }
       }
    }