Notebook Key Processing

Written by Roman Stangl

The problem
The OS/2 PM notebook control has a limitation that causes inconsistent usability compared to other PM controls, namely it does not pass on all unprocessed messages to its owner.

As a result the following consequences, which we want to avoid, exist:
 * Breaking this focus chain means, for example, that if the input focus is on a control of a notebook page, and one presses e.g. Alt+F and there exists a notebook tab having the letter F as the shortcut key, the key doesn't get passed on to the notebook, so that it can adjust itself to display the corresponding notebook page as the top page.
 * Pressing the ESC key to cancel would dismiss the dialog that implements the notebook page, but not the window containing the notebook control.
 * Pressing the Enter key does not dismiss the dialog window by selecting the default pushbutton (e.g. Save, Ok,...).
 * Pressing accelerator keys does not trigger pushbuttons on your dialog's window.

The window layout we assume
A typical window containing a notebook is a dialog window (most likely created with the dialog editor) that contains a Notebook Control and Pushbuttons in its client area. The notebook below should demonstrate this.

By the way, the screen capture was taken from my Program Commander/2 (PC/2) program, which is available from [my homepage], and it's Freeware!



This will probably not be new to you, however I would like to summarize this from the point of the Z-Order and owner chain, as this is the basis to understand how to enhance the processing. The controls in the above notebook are created in the following order: The Notebook Control keeps track of all the pages you have inserted and displays the Notebook Page that corresponds to the currently selected Notebook Tab and hides the other pages. The important thing to mention here is, that the Notebook Pages are child windows of the Notebook Control.
 * 1) The Dialog Window is the PM window that implements the whole dialog, which is created by the Dialog editor and processed by the OS/2 PM APIs WinDlgBox or WinLoadDlg, WinProcessDlg. If you for example press ESC, you want this Dialog Window to be dismissed.
 * 2) The client area of the Dialog Window consists of the Notebook control and the Pushbuttons like S ave, C ancel and H elp. The important thing to mention here is, that the Notebook Control and the Pushbuttons are child windows of the Dialog Window.
 * 3) The Notebook Control is a complex OS/2 PM control that consists of the Notebook Tabs, the Notebook Page(s) (and the Status line and Page buttons not shown here).
 * 4) The Notebook Tabs are drawn by the Notebook Control. However you determine the text drawn onto the individual tabs and there is nothing that prevents you from including shortcut keys, for example to specify O ptions for your options notebook page.
 * 5) Finally, the Notebook Control contains an area where the Notebook page(s) are displayed at. Notebook Pages are also dialog windows usually created by the dialog editor. When adding to a notebook, the notebook implicitly performs the WinLoadDlg to load the dialog from a resource.

Instance data structure
In order to forward key events from the notebook control to its owner, we have to modify the way the Notebook Control processes such events, in case the notebook is not interested in that event.

When working with data that is window dependent (instance data), the proper way is to use the window words to save a pointer to your data. The QWL_USER window word is reserved for the user and would be a good starting point.

However, while implementing the advanced processing, it can be useful to store the data in the heap, as it can be accessed by the debugger even when not stepping through a window procedure. In the following code excerpt, the processing supports 3 different dialogs. typedef struct _NOTEBOOKSUBCLASS   NOTEBOOKSUBCLASS; /* Structure to save which notebook was subclassed from which previous window procedure */ struct _NOTEBOOKSUBCLASS { HWND    hwndNotebook;    /* Notebook window handle */ PFNWP  pfnwpNotebook;   /* Notebook control's window procedure before subclassing */ }; /* Desktop dialog notebook subclassed */ #define DD_SUBCLASSEDNOTEBOOK      0 /* Program Installation dialog notebook subclassed */ #define PI_SUBCLASSEDNOTEBOOK      1 /* Control Configuration dialog notebook subclassed */ #define CC_SUBCLASSEDNOTEBOOK      2 /* As only one instance of the Desktop and Program Installation dialogs is allowed, its save to avoid more complicated per dialog instantiation, but use module class storage */ #define NOTEBOOKSUBCLASSMAX        3 NOTEBOOKSUBCLASS   DialogNotebookSubclass[NOTEBOOKSUBCLASSMAX]; In this example our application consists of 3 different Dialog Windows for which we want to enhance the message processing.

Modifying the notebook processing
In order to forward key events from the notebook control to its owner, we have to modify the way the Notebook Control processes such events, in case the notebook is not interested in that event.

In order to change the Notebook Control's default message processing, we have to subclass the control by using the PM API WinSubclassWindow: /* Subclass notebook's window procedure to add handling of      notebook pages, notebook and outer dialog accelerator keys */ DialogNotebookSubclass[DD_SUBCLASSEDNOTEBOOK].hwndNotebook=hwndNB; DialogNotebookSubclass[DD_SUBCLASSEDNOTEBOOK].pfnwpNotebook= WinSubclassWindow(hwndNB, SubclassedNotebookProcedure);

Subclassing means, that we add a new window procedure to the existing window procedure of the Notebook Control to be able to add processing logic before the notebook's window procedure. In your own controls, you would add the processing somewhere in the big switch-Statement of your window procedure, however as the notebook (and all other controls shipped with OS/2) is implemented inside PM, you can only get access to the control's window procedure by using WinSubclassWindow.

Another thing to mention here is, that in our example all procedures are coded only once, that is they run in the context of the window that is currently processing the user input. As a consequence special caution needs to be taken not to share variables between different window contexts.

The subclassed window procedure of the Notebook Control is especially interested in WM_CHAR messages, as the WM_CHAR message reflects the keyboard input we want to change:

Subclassed window procedure. NBKEY1.C

The first thing we do in the subclassed window procedure is look for the control structure element that corresponds to the notebook control in whose context we are currently running in. As I said, the most elegant way would be to use the notebook's window words, but the easier to debug list approach is taken here.

In detail, the following happens for a WM_CHAR message is routed to the notebook:


 * 1) As we have subclassed the notebook, SubClassedNotebookProcedure will be called when the Notebook control has the input focus (e.g. when you click on one of the tabs), because it is now part of the notebook control's window procedure.
 * 2) A lookup is done to find out the instance data.
 * 3) The original notebook window procedure is called to see if the notebook does accept that key, which we know by the result returned. We are interested in the result, as we have to forward keys not accepted by the Notebook Control to the owning Dialog Window.
 * 4) If the key is the shortcut of a Notebook Tab, as for example the O for an O ptions tab, then the notebook will accept that key and switch to the Notebook Page corresponding to that tab. If the key is not a shortcut accepted by the notebook, we will forward it to the Dialog Window (which is the owner of the Notebook Control), as for example the C key may be the accelerator key of a C ancel Pushbutton.

Modifying the notebook pages' processing
In the window procedure of the Notebook Pages we also have to catch the key events, by looking for WM_CHAR messages. As the dialog that implements the notebook page is created by us, we also supply the window procedure (or the dialog procedure to be exact), so we just can easily add the processing as shown here: case WM_CHAR: /*  * Process the navigation keys on this page and forward all * unprocessed keys to the dialog where the notebook is part of. * \*      ProcessPageKey(hwndDlg, WinQueryWindow(hwndNotebook, QW_OWNER), mp1, mp2); break; In order to allow window procedures of different notebook pages to share the same implementation, all WM_CHAR messages are processed by ProcessPageKey.

Note: If you have multiple notebook pages you can write window procedure and share it for all notebook pages, just ensure that the controls of the individual notebook pages contain different identifiers.

ProcessPageKey (NBKEY2.C

This function is somewhat more complex. In order to make debugging easier, printf calls are added to generate debug information. You can either redirect stdout to a file (e.g. by invoking myapp.exe with "myapp > debuginfo"), or you get the very useful IBM EWS-written PMPRINTF package, which replaces the C-library printf function with one that writes into an OS/2 queue, and using the included PM viewer you can display that queue in real-time.

In detail, the following happens for a WM_CHAR message routed to a notebook page's dialog procedure:
 * 1) If the key event is a navigation key, that is the tab and cursor keys, we accept the processing of the Notebook Page's window procedure.
 * 2) If the key event is not a navigation key, we ask the Dialog Window if the key makes sense there, for example the ALT+C key for a C ancel Pushbutton).

Modifying the dialog window's processing
The Dialog Window may process messages either generated while the dialog had the input focus, or messages forwarded up from the owner chain, that is message forwarded by the Notebook Control and by the individual Notebook Pages. In any case, we are interested in the processing of WM_CHAR messages as shown below.

again, as the dialog that implements the dialog window is created by us, we also supply the window procedure (or the dialog procedure to be exact), so we just can easily add the WM_CHAR processing. To share code, again we call a function named DispatchKeyToNotebook (NBKEY3.C). As a notebook page's window procedure calls the dialog's window procedure by using WinSendMsg (as shown in ProcessPageKey - NBKEY2.C) and DispatchKeyToNotebook again calls the notebook page's window procedure, we have to check for recursion.

The reason why we may get into recursion is, that while processing a key in the Dialog Window's dialog procedure, we no longer know where it was generated, that is in the dialog window itself, or sent from one of the owned controls. However, we want our logic to enhance the key processing to work in all cases, so we have to take the recursion into account.

DispatchKeyToNotebook (NBKEY3.C)

In detail, the following happens for a WM_CHAR message routed to the dialog procedure: One thing to note is that Enter (VK_ENTER) is not the same as Newline (VK_NEWLINE). If Enter (that is the "Enter" key on the numeric keypad) is pressed, we want the dialog to be dismissed, but not in the case of Newline, because notebook pages may contain controls that allow you to input the "Enter" key (e.g. multiline entry fields), and your user for sure want to be able to input that key too. On a Laptop, some key combinations may be required to select the Enter key instead of the Newline key (as Laptops usually have no numeric keypad). You may try SHIFT+Enter, which works on IBM ThinkPads, or take a look into your User's Guide.
 * 1) We check if this key is the ESC or Enter key, as they are used to dismiss the dialog, causing the Pushbuttons DID_CANCEL for ESC and DID_OK for Enter to be selected.
 * 1) To prevent recursion, one static variable per Dialog Window is used to track the recursion depth.
 * 2) After having protected us against recursion (and still running in the Dialog window's dialog procedure), we check if the key event is a key press while the ALT key is still pressed (as accelerator keys usually are used in conjunction with the ALT key).
 * 3) If the key is pressed while the ALT key is still pressed, we check if the dialog procedure of the Dialog Window can accept that key, by calling the dialog's window procedure and checking the result returned. This would allow for example the ALT+C key for C ancel to be accepted.
 * 4) If the dialog window procedure does not accept that key, we query the window handle of the top Notebook Page, that is the one displayed in the Notebook Control. If the notebook page could handle it we're done.
 * 5) If that Notebook Page can't accept that key, see if the Notebook page can accept that key as if no ALT key has been pressed. We remove the ALT modifier, as if a notebook control has the input focus one can navigate between the notebook pages by pressing the accelerator key, for example O for O ptions without being required to hold the ALT key meanwhile.
 * 6) If the Notebook Control can accept that key, find out the Notebook page that has been put onto top and activate the control that had the input focus immediately before that notebook page was paged away.

Credits
The code shown here was greatly influenced by some discussions in the OS/2 development fora on the IBMPC conference disk.