Tastenunterstützung in Dropdown-Menüs

Ein Grund für die hohe Benutzerfreundlichkeit von GEM-Programmen sind sicher die Dropdown-Menüs. Doch können sie nach einiger Zeit genauso benutzerunfreundlich werden, falls sie nicht konsequent durch Tastenkombinationen unterstützt werden. An diesem Punkt mangelt es leider bei vielen Programmen. Ein Grund dafür ist sicher, daß das AES solche Tastenkombinationen nicht direkt unterstützt. Ein weiterer Grund dürfte sein, daß das am weitesten verbreitete Programm, das Desktop, dies ebenfalls nicht tut. In diesem Beitrag wird eine Methode beschrieben, die mindestens den ersten Grund entfernt.

In der GEM-Dokumentation von Digital Research wir dem Programmierer empfohlen: Wenn der Benutzer eine Tastenkombination drückt, welche einem Menüeintrag zugeordnet ist, so soll das Programm zuerst den entsprechenden Titel mit der Funktion menu_tnormal invertieren. Danach kann die entsprechende Aktion durchgeführt werden, und schließlich muß durch einen erneuten Aufruf von menu_tnormal der Menütitel wieder normal dargestellt werden.

Jeder Programmierer, der obiges Rezept in seinen Programmen berücksichtigt, weiß, wie mühsam es unter Umständen sein kann, die Menüabfrage zweispurig fahren zu müssen. Einerseits wird ja auf die Meldung einer richtigen Menübetätigung gewartet, andererseits müssen die Tasteneingaben analysiert werden. Umstrukturierungen im Menü werden damit ebenfalls unnötig aufwendig.

Ein besseres Konzept

Nun ist es in den meisten Fällen für das Programm unwichtig zu wissen, ob ein Menüeintrag angeklickt oder ob die entsprechende Tastenkombination gedrückt worden ist. Für den Programmierer wäre es also angenehm, wenn er die Zuordnung der Menüeinträge zu den Tastenkombinationen bereits im Resource Construction Set vornehmen könnte, und wenn das AES danach für das Klicken auf einen Menüeintrag oder das Betätigen der entsprechenden Tastensequenz dem Programm dieselbe Meldung schicken würde.

Damit würde die oben erwähnte Zweispurigkeit verhindert. Es könnten jederzeit, ohne Programmänderung, die Tastenkombinationen anderen Menüeinträgen zugewiesen werden. Auch wäre die Neuzuweisung der Tastenkombinationen, welche bei der Übersetzung der Resource in eine andere Sprache eventuell notwendig ist, kein Problem mehr.

Meldungen unter GEM

Meldungen können nicht nur vom System an Programme geschickt werden, auch Programme selbst haben die Möglichkeit, Meldungen an andere Applikationen zu schicken. Dazu dient der AES-Befehl appl_write. So kann ein Programm beispielsweise mit einem Accessory “sprechen”. Ein schönes Beispiel dafür ist der IDC-Standard [1]. Ebenfalls ist es für ein Programm nicht verboten, Meldungen an sich selbst zu verschicken.

Damit wird folgendes Vorgehen ermöglicht: Das Programm wartet im Event-Loop auf eine Meldung (MU_MESSAG) und auf einen Tastendruck (MU_KEYBD) und, falls nötig, auf weitere Ereignisse. Im Falle eines Tastendrucks wird dieser nicht auf die Zugehörigkeit zu einem Menüeintrag untersucht, sondern er wird einer Prozedur übergeben. Diese durchsucht die letzten Buchstaben aller Menüeinträge. Wird dabei zum Beispiel die Zeichenkette ^C gefunden, was heißt, daß die entsprechende Funktion auch mit der Tastenkombination [Control] + ’C’ ausgelöst werden kann, und ist diese Kombination auch gedrückt worden, wird eine entsprechende Meldung an die eigene Applikation geschickt. Danach wird die Subroutine beendet, und das Hauptprogramm schließt seinen Loop und wartet auf ein neues Ereignis. Dieses wird die zuvor abgeschickte Meldung sein, worauf das Programm glaubt, der entsprechende Menüeintrag sei mit der Maus angeklickt worden, und die richtige Funktion ausführt.

Das heißt, die Zuweisung der Tastenkombinationen zu den Menüeinträgen geschieht in den Menüeinträgen selbst, indem man dort die entsprechende Tastenkombination vermerkt.

Der Menübaum

Bevor wir den Menübaum nach Einträgen durchsuchen können, brauchen wir etwas Kenntnis über dessen Aufbau. Ein Menübaum unterscheidet sich prinzipiell nicht von anderen Objektbäumen. Die Objekte sind dieselben und werden durch die 24 Bytes lange Struktur (in pascal Record) OBJECT dargestellt. Für uns wichtig sind die drei Zeiger ob_next, ob_head und ob_tail, welche auf das nächste Objekt auf derselben Stufe, auf das erste und das letzte Objekt des jeweiligen Unterbaumes zeigen. Das letzte Objekt auf einer Stufe zeigt auf die “Mutter” zurück. Ein Zeiger ins “Nichts” wird durch -1 repräsentiert. Die Zeiger beinhalten nicht direkt eine Adresse, sondern besagen, welche Nummer das bezeichnende Objekt in der Objektliste hat.

Weiter werden uns ob_type und ob_spec die Art des Objektes und ein Zeiger (Adresse) auf den String in den Menüeinträgen interessieren. Eine genaue Beschreibung der Objekte und der Objektbäume befindet sich in den meisten GEM-Büchern. Die Struktur eines Menübaumes ist in Abb. 1 dargestellt.

Eine mögliche Realisierung

Im folgenden wird eine mögliche Realisierung der obigen Idee beschrieben. Die Routine heißt menu_search und ist in der Programmiersprache C geschrieben. Sie kann zum Hauptprogramm gelinkt und von dort aufgerufen werden. Ein einfaches Beispielprogramm erläutert die Anwendung der Routine.

Folgende Tastenkombinationen werden unterstützt (X steht für ein beliebiges einzelnes Zeichen):

Accessory-Einträge sind von der Suche ausgeschlossen (der Eintrag “Über Programm...” wird aber berücksichtigt). Kommt dieselbe Tastenkombination in mehreren Menüeinträgen vor, wird immer der erste gefundene Eintrag genommen.

Wird bei der Suche durch den Menübaum auf einen Eintrag gestoßen, dessen Vermerk mit der übergebenen Tastenkombination (kstate und key) übereinstimmt, werden folgende Schritte unternommen:

  1. Der entsprechende Menütitel wird invers dargestellt.
  2. Es wird eine Meldung an die eigene Applikation abgeschickt. Der Message-Buffer sieht dabei folgendermaßen aus:

msg_buff[0] = 10 (MN_SELECTED)
msg_buff[1] = ap_id
msg_buff[2] = 0 (Länge der Meldung kleiner als 16 Bytes)
msg_buff[3] = Index des Menütitels
msg_buff[4] = Index des Menüeintrages

Abb.1: Struktur eines Menübaumes

Damit wird exakt die Situation simuliert, wie sie das Hauptprogramm antrifft, wenn derselbe Menüeintrag mit der Maus angeklickt worden wäre.

Ist ein Menüeintrag disabled (grau dargestellt), kann er mit der Maus nicht selektiert werden. Die Routine verschickt in einem solchen Fall ebenfalls keine Meldung an das Hauptprogramm. Ist ein Menütitel disabled, kann das entsprechende Menü gar nicht herunterklappen. In diesem Fall ist das ganze Menü von der Suche ausgeschlossen.

Folgende Parameter müssen beim Aufruf der Funktion übergeben werden:

int ap_id: Application Identification. Diese ist notwendig für die Suchroutine, um zu wissen, an wen die Meldungen verschickt werden müssen. Die ap_id wird von der Funktion appl_init zurückgegeben. Sie kann aber auch später noch aus dem AES-Global-Array gelesen werden.

long m_tree: Adresse des Menübaumes. Sie kann mit rsrc_gaddr nach dem rsrc_load-Aufruf erfragt werden und muß unter anderem auch der Funktion menu_bar übergeben werden.

int kstate: Status der Sondertasten ([Alternate], [Control], [Shift]). Dieser Wert wird von der Funktion evnt_multi zurückgegeben.

int key: Scan-und ASCII-Code der gedrückten Taste. Dieser Wert wird ebenfalls von der Funktion evnt_multi zurückgegeben.

Der Aufruf von C aus sieht also wie folgt aus:

menu_search(ap_id,m_tree,kstate,key);

Die Funktion kann aber auch vom ST Pascal Plus aus aufgerufen werden. Sie muß dann folgendermaßen importiert werden:

procedure menu_searc(ap_i: integer; m_tree: Menu_Ptr; kstate, key: integer); c;

und kann danach wie gewohnt aufgerufen werden.

Auch von Assembler kann die Funktion aufgerufen werden:

move.w key,-(sp) 
move.w kstate,-(sp) 
move.l m_tree,-(sp) 
move.w ap_id,-(sp) 
jsr    menu_search
lea    10(sp),sp

Die Beschreibung der Routine

Die Routine besteht aus zwei Teilen: der Hauptroutine menu_search, welche den gesamten Menübaum absucht, und der Hilfsroutine test_entry, welche die Menüeinträge auf zugewiesene Tastenkombinationen untersucht.

Ein erwähnenswertes Problem der Hauptroutine ist, festzustellen, welches Zeichen vom Benutzer gedrückt worden ist. Diese Information ist in den übergebenen Variablen kstate und key enthalten. Wenn zum Beispiel [Control] zusammen mit einer Taste gedrückt wird, erhält man nicht mehr den ASCII-Code des entsprechenden Zeichens. Der dann erhaltene ASCII-Code ist nicht einmal eindeutig. So liefern zum Beispiel die Kombinationen [Control]+’3’ und [Control]+’S’ denselben ASCII-Code!

Man ist also gezwungen, auf den Scancode zu achten. Dieser ist jedoch an die physikalische Position der Tasten gebunden. Gewisse Zeichen sind bei verschiedensprachigen Tastaturen an verschiedenen Orten angebracht, liefern also verschiedene Scancodes. Typisch ist das Beispiel von Programmen, welche in ihren Menüeinträgen die Kombination “^-” versprechen. Auf der deutschen Tastatur muß man aber [Shift][Control]’?’ drücken.

Ein möglicher Ausweg besteht darin, die Tastaturtabelle des XBIOS zu verwenden. Sie ist bei jedem Betriebssystem an die jeweilige Tastatur angepaßt. Es sind insgesamt drei Tabellen für “Unshift”, “Shift” und für “Caps Lock”. Ein Zeiger auf ein Array mit den Zeigern auf die drei Tabellen kann mit der XBIOS-Funktion keytbl erfragt werden. Diese Tabellen werden mit dem Scancode als Index gelesen und enthalten den ASCII-Code der entsprechenden Taste.

Die Hilfsroutine test_entry durchsucht das Ende des übergebenen Strings nach möglichen Vermerken von Tastaturkombinationen. Diese Routine muß also abgeändert werden, falls andere als die hier implementierten Tastenkombinationen oder andere Notationsformen gewünscht sind.

Literatur:

[1] Obrero A./Waldvogel M.: “Ein Standard für Scannersoftware”, ST-Magazin 11/89

/****************************************************/ 
/*                                                  */
/* menu_search                                      */
/*                                                  */
/* Aufruf: menu_search(ap_id,m_tree,kstate,key)     */
/*                                                  */
/* int ap_id: ID der aufrufenden Applikation        */
/* OBJECT *m_tree: Zeiger auf den Menubaum          */
/* int kstate: Keyboard state (liefert event_multi) */
/* int key: ASCII & Scancode (liefert evnt_multi)   */
/*                                                  */
/* Kompiliert mit Megamax Laser C, Version 1.2      */
/*                                                  */
/*                                                  */
/*  von Urs Mueller, Mai 1989                       */
/*  (c) MAXON Computer GmbH                         */
/****************************************************/

/* durchsucht alle Menueintraege des uebergebenen Menubaumes nach den */
/* letzten Buchstaben. Wird bei einem Eintrag eine logische ueberein- */
/* Stimmung mit den Werten von kstate und key gefunden, so wird eine  */
/* Meldung MN_SELECTED an die aufrufende Applikation geschickt.       */
/* Dies geschieht natuerlich nur dann, wenn der Menueintrag und der   */
/* entsprechende Menutitel nicht "disabled" sind.                     */

/* Moegliche Werte fuer die letzten Buchstaben in einem Menueintrag   */
/* sind:                                                              */
/* "F1" bis "F10": Funktionstasten                                    */
/* "F11" bis "F20": [Shift] Funktionstasten                           */
/* [Shift]F1 bis [Shift]F10:                                          */
/* ( [Shift] = ASCII(1) )                                             */
/* "A" und Buchstabe: [Control] Buchstabe                             */
/* Fullbox-Symbol und Buchstabe [Alternate] Buchstabe                 */
/* Hochkomma-Buchstabe-Hochkomma : Buchstabe ohne [Ctrl] oder [Alt]   */
/* Gesucht wird im Menueintrag von Rechts (wobei Spaces uebersprungen */
/* werden) nach einer der obigen Kombinationen.                       */ 
/* Bei den Buchstaben wird zwischen klein- und Grossbuchstaben nicht  */
/* unterschieden                                                      */




/*******************/
/* Include-Dateien */
/*******************/

#include "GEMDEFS.H" /* common object definitions and structures */ 
#include "OBDEFS.H"  /* common GEM definitions */
#include "OSBIND.H"  /* gemdos, bios, xbios */



/**************/
/* Konstanten */
/**************/

#define TRUE 1 
#define FALSE 0

#define F1  0x3b    /* Scancode von F1 */
#define F10 0x44    /* Scancode von F10 */
#define F11 0x54    /* Scancode von [shift] F1 */
#define F20 0x5d    /* Scancode von [shift] F10 */

#define M_NORMAL 1  /* Menutitel normal dargestellt */
#define M_REVERSE 0 /* Menutitel Weiss auf Schwarz */




/***************************/
/* Menueintrag untersuchen */ 
/***************************/

static int test_entry(str, chr, scan, state) 
char *str, chr; 
int scan, state;

{
char    *pchar, vchr; 
int     ret, zahl;

ret = FALSE;
pchar = str;

while (*pchar) pchar++; /* pchar an den Schluss des Strings */ 
while (*--pchar == ' ');/* von rechts erstes Zeichen suchen */
vchr = *pchar;
if (vchr >= 'a' && vchr <= 'z') vchr = vchr - 'a' + 'A';

if (vchr == chr)
    {
        pchar—-;
        if (*pchar=='A' && state==K_CTRL || *pchar==7 && state==K_ALT) 
            ret = TRUE;
    }
else if (*pchar == '\'' && (state & (K_CTRL | K_ALT)) == 0)
    {
        pchar--; 
        vchr = *pchar;
        if (vchr >= 'a' && vchr <= 'z') vchr = vchr - 'a' + 'A';
        if (vchr == chr && *(pchar-1) == '\'') ret = TRUE;
    }
else if (*pchar >= '0' && *pchar <= '9')
    {
        zahl = *pchar - '0';
        pchar--;
        if (*pchar >= '0' && *pchar <= '9')
            {
                zahl += (*pchar - '0') * 10;
                pchar--;
            }
        if (*pchar == 'F')
            {
                if (*(pchar-1) == 1) /* [Shift]-Zeichen */
                    zahl += 10; 
                if ((zahl >= 1) && (zahl <= 10))
                    if (zahl == scan - F1 + 1) ret = TRUE; 
                if ((zahl >= 11) && (zahl <= 20))
                    if (zahl == scan - F11 + 11) ret = TRUE;
            }
    }
    return(ret);

} /* end of test_entry */





/*****************/
/* Hauptprogramm */
/*****************/

void menu_search(ap_id, m_tree, kstate, key) 
int     ap_id;
OBJECT *m_tree; 
int     kstate;
int     key;


{
int     msg_buff[8];    /* message buffer */
int     do_quit;
keytbl  *pkeytbl;
char    *kbd_unshift;   /* TastTab. normal */
char    *kbd_shift;     /* TastTab. shift */
char    *kbd_caps;      /* TastTab. caps lock */
char    chr;
int     state, scan, desk;
int     mother_title, child_title, mother_entry, child_entry;


do_quit = FALSE; 
desk = TRUE;

pkeytbl = Keytbl(-1L, -1L, -1L);

kbd_unshift = (*pkeytbl).unshift;   /* Zeiger auf Tabelle 'normal' */ 
kbd_shift   = (*pkeytbl).shift;     /* Zeiger auf Tabelle 'shift' */
kbd_caps    = (*pkeytbl).capslock;  /* Zeiger auf Tabelle 'caps lock' */

wind_update(BEG_UPDATE);            /* warten bis Drop-Downs.. */
wind_update(END_UPDATE);            /* geschlossen */

scan = key/256 & Oxff;
if ((kstate & (K_LSHIFT | K_RSHIFT)) == 0)
    chr = *(kbd_unshift + (scan));
else
    chr = *(kbd_shift + (scan));
if (chr >= 'a' && chr <= 'z') chr += 'A' - 'a'; /* upcase */

if ((kstate&K_ALT) != 0 && (kstate&K_CTRL) == 0)
    state = K_ALT;  /* [Alt] gedrueckt */
else if ((kstate&K_CTRL) != 0 && (kstate&K_ALT) == 0)
    state = K_CTRL; /* [Shift] gedrueckt */
else if ((kstate&K_ALT) == 0 && (kstate&K_CTRL) == 0)
    state = 0;  /* nichts gedrueckt */
else do_quit = TRUE; /* [Alt] und [Shift] */

mother_title = (m_tree + m_tree->ob_head)->ob_head;
child_title = (m_tree + mother_title)->ob_head;
mother_entry = (m_tree + m_tree->ob_tail)->ob_head;
child_entry = (m_tree + mother_entry)->ob_head;

while (!do_quit)
    {               /* title loop */
    if (((m_tree + child_title)->ob_state & DISABLED) == 0)
        while(!do_quit && child_entry != mother__entry && child_entry != -1)
            {
                if ((((m_tree + child_entry)->ob_state & DISABLED) == 0) &&
                   ((m_tree + child_entry)->ob_type == G_STRING ||
                    (m_tree + child_entry)->ob_type == G_BUTTON))
                    do_quit = test_entry((char*)(m__tree + child_entry)->ob_spec, chr, scan, state);
                if (do_quit)
                {
                    msg_buff[0] = MN_SELECTED;
                    msg_buff[1] = ap_id;
                    msg_buff[2] = 0; /* Laenge Nachricht */
                    msg_buff[3] = child_title; /* Titel */
                    msg_buff[4] = child_entry; /* Eintrag */
                    menu_tnormal (m_tree, child_title, M_REVERSE);
                    appl_write(ap_id, 16, msg_buff); /* Message senden */
                }
                child_entry = (m_tree + child_entry)->ob_next;
                if (desk)
                    {           /* Accessories nicht testen */
                        child_entry = mother_entry;
                        desk = FALSE;
                    }
            }
    child_title = (m_tree + child_title)->ob_next;
    mother_entry = (m_tree + mother_entry)->ob_next;
    child_entry = (m_tree + mother_entry)->ob_head;
    if (child__title == mother_title) do_quit = TRUE;
} /* while (!do_quit) */

} /* end of menu_search */
#define MENU 0      /* TREE */
#define M_INFO 7    /* OBJECT in TREE #0 */
#define M_CTRL_R 16 /* OBJECT in TREE #0 */
#define M_ALT_E 17  /* OBJECT in TREE #0 */
#define M_QUEST 18  /* OBJECT in TREE #0 */
#define M_F1 19     /* OBJECT in TREE #0 */
#define M_F13 20    /* OBJECT in TREE #0 */
#define M_F15 21    /* OBJECT in TREE #0 */
#define M_QUIT 23   /* OBJECT in TREE #0 */

/***********************************************/
/* Beispiel zur Nutzung der Routine            */
/* menu_search                                 */
/*                                             */
/* von Urs Müller, Mai 1989                    */
/* (c) MAXON Computer GmbH                     */
/***********************************************/





/*******************/
/* Include-Dateien */
/*******************/

#include "M_DEMO.H"  /* Includefile für Resource */
#include "GEMDEFS.H" /* common object definitions and structures */ 
#include "OBDEFS.H" /* common GEM definitions */



/**************/
/* Konstanten */
/**************/

#define RSCNAME "M_DEMO.RSC" /* Name der Resource-Datei */

#define TRUE 1 
#define FALSE 0

/* Konstanten für menu_tnormal (AES 33) */

#define M_NORMAL 1 /* Menutitel normal dargestellt */ 
#define M_REVERSE 0 /* Menutitel Weiss auf Schwarz */




/*************/
/* Variablen */
/*************/

OBJECT *menu_tree; /* &Menu */
int ap_id; /* Application Identification */
int dummy;




/**************/
/* Funktionen */
/**************/

static void do_info()
{
dummy = form_alert(1, "[0][ |Menueintrag Info ][ OK ]");
}

static void do_ctrl_r()
{
dummy = form_alert(1, "[0][ |Funktion 1 ][ OK ]");
}

static void do_alt_e()
{
dummy = form_alert(1, "[0][ |Funktion 2 ][ OK ]");
}

static void do_quest()
{
dummy = form_alert(1, "[0][ |Funktion 3 ][ OK ]");
}

static void do_f1()
{
dummy = form_alert(1, "[0][ |Funktion 4 ][ OK ]");
}

static void do_f13()
{
dummy = form_alert(1, "[0][ |Funktion 5 ][ OK ]");
}

static void do_f15()
{
dummy = form_alert(1, "[0][ |Funktion 6 ][ OK ]");
}




/**************/
/* event_loop */
/**************/

static void event_loop()

{
int     msg[8];      /* Message-Buffer */
int     event;       /* Event-Typ */
int     key, kstate; /* Taste und Status der Kontrolltasten */ 
int     do_quit;     /* Flag */




do_quit = FALSE; 

do
    {
    event = evnt_multi(MU_KEYBD | MU_MESAG,
                       0, 0, 0, 0, 0, 0, 0, 0,
                       0, 0, 0, 0, 0, msg,
                       0, 0, &dummy, &dummy, &dummy, 
                       &kstate,
                       &key,
                       &dummy);
    switch(event)
        {
        case MU_KEYBD: menu_search(ap_id, menu_tree, kstate, key);
                       break;

        case MU_MESAG: if (msg[0] == MN_SELECTED)
                       {
                            switch(msg[4])
                            {
                            case M_INFO: do_info();
                                         break;
                            case M_CTRL_R:do_ctrl_r();
                                         break;
                            case M_ALT_E: do_alt_e();
                                         break;
                            case M_QUEST: do_quest();
                                         break; 
                            case M_F1:   do_f1();
                                         break;
                            case M_F13:  do_f13();
                                         break;
                            case M_F15:  do_f15();
                                         break;
                            case M_QUIT: do_quit = TRUE; break; 
                            } /* switch(msg[4]) */
                            if (!do_quit)
                                menu_tnormal(menu_tree,msg[3], M_NORMAL);
                        }
                        break;
        } /* switch(event) */
    }
while (!do_quit);
} /* end of event_loop */




/*****************/
/* Haupt-Routine */
/*****************/


void main()

{
    char *rsc_name;

    rsc_name = RSCNAME; /* Name des Resourcefiles */

    ap_id = appl_init(); 
    if (ap_id >= 0)
    {
        graf_mouse(ARROW, 0L); /* Mauszeiger = Pfeil */ 
        if (rsrc_load(rsc_name) == 0)
            dummy = form_alert(1, "[3][ |Resourcefile not found! ][ OK ]");
        else
        {                      /* Resource wurde geladen */
            rsrc_gaddr(R_TREE, MENU, &menu_tree); /* Menu */
            menu_bar(menu_tree, 1); /* Menu zeichnen */
            event_loop();
            menu_bar(menu_tree, 0); /* Menu entfernen */
            rsrc_free();
        }
        appl_exit();
    }
}

Urs Müller
Aus: ST-Computer 11 / 1989, Seite 80

Links

Copyright-Bestimmungen: siehe Über diese Seite