Von Fall zu Fall

Das Kontextmenü des MacOS 8 erweitern

Wissen | Know-how

Anwender von Windows 95 kennen das kontextsensitive Menü schon länger. Auf dem Macintosh hat es erst mit MacOS 8 Einzug gehalten, läßt sich dafür aber über eine Schnittstelle erweitern.

Als sich die Entwickler des Macintosh zu Beginn der achtziger Jahre für eine Eintastenmaus entschieden, konnten sie nicht ahnen, daß sie eines Tages über eine zweite Taste froh sein würden. Deshalb mußten ihre Nachfolger bei der Implementation ein wenig in die Trickkiste greifen. Ein Klick bei gedrückter Kontrolltaste (Kontextklick) simuliert die fehlende rechte Maustaste, die unter Windows das Menü aufruft. Trotzdem stellt das Kontextmenü eine Arbeitserleichterung dar, denn es faßt alle auf eine Auswahl anwendbaren Befehle übersichtlich in einem Menü zusammen, und es erscheint an der Stelle des Klicks.

Das Kontextmenü steht systemweit zur Verfügung. Allerdings kommen Programme, die es nicht explizit unterstützen, nicht automatisch in dessen Genuß. Im aktuellen MacOS 8 macht lediglich der Finder intensiv davon Gebrauch. Um Programmen, die es noch nicht unterstützen, auf die Sprünge zu helfen, hat Apple deshalb die Systemerweiterung 'Contextual Menu Enabler' entwickelt. Damit können die meisten Programme das Menü nutzen. Den Enabler gibt es nicht einzeln, sondern nur als Bestandteil der 'Internet Address Detectors'. Diese Detektoren untersuchen eine Textauswahl und hängen erkannte URLs an das Kontextmenü an. Wählt man eine URL aus, wird sie von einem geeigneten Internetprogramm (sofern vorhanden) geöffnet.

Ein Entwickler sollte sich jedoch nicht nur auf den 'Contextual Menu Enabler' verlassen. Nur durch direkte Unterstützung des 'Contextual Menu Manager' lassen sich eigene Menüpunkte in das Kontextmenü aufnehmen und alle Faktoren, die eine Auswahl beeinflussen, analysieren. Der Enabler geht bei seiner Unterstützung nämlich recht dümmlich zu Werke: per Kopieren-Befehl befördert er die aktuelle Auswahl in die Zwischenablage und faßt diese als Kontext für das Menü auf. Diese Arbeitsweise hat nicht nur den Schönheitsfehler, daß dabei der ursprüngliche Inhalt der Zwischenablage verlorengeht (das ließe sich von einem schlaueren Enabler umgehen), sondern bestimmte Operationen sind auf diese Art nicht machbar.

Dabei ist es sehr einfach, das Kontextmenü zu implementieren. Ein Programm wird weich mit der 'Kontextmenü Erweiterung' verbunden (Import weak), damit es auch auf Systemen ohne diese Erweiterung läuft. Zuerst meldet sich ein Programm durch Aufruf von InitContextualMenu als Klient beim Contextual Menu Manager an. Liefert dieser Aufruf ein anderes Ergebnis als noErr, ist wahrscheinlich die Erweiterung nicht installiert und deren Aufrufe stehen nicht zur Verfügung. IsShowContextualMenuClick überprüft vor der üblichen Verarbeitung der Events, ob das aktuelle Ereignis ein Kontextklick ist. Für jedes Ereignis mit einem von nullEvent verschiedenen Typ muß man dann mittels IsShowContextualMenu klären, ob das Kontextmenü angezeigt werden soll. Falls ja, muß das Programm herausfinden, auf welchem Objekt (Text, Fenster et cetera) der Klick erfolgt ist und das Menü entsprechend zusammenstellen. Ein Verweis auf das Objekt wird in einen AppleEvent-Deskriptor verpackt, um es an den Contextual Menu Manager weiterreichen zu können. ContextualMenuSelect liefert als Ergebnis, ob und welchen der programmeigenen Menüeinträge der Anwender ausgewählt hat.

Die grundsätzliche Unterstützung des Kontextmenüs ist also nicht aufwendig. Die Schwierigkeit liegt darin herauszufinden, welche Menüeinträge überhaupt sinnvoll sind. Wer dafür keine eigene Lösung stricken möchte, kann unter der CodeWarrior-Entwicklungsumgebung auf PowerPlant-Zusatzklassen zurückgreifen, die als Shareware verfügbar sind [3, 4].

Die Fähigkeiten des Contextual Menu Manager lassen sich über eine Plug-in-Schnittstelle bequem erweitern. Plug-ins gehören in den Ordner 'Contextual Menu Items' im Systemordner und sind erst nach einem Neustart aktiv. Neue Funktionen stehen anschließend systemweit zur Verfügung. Wenn ein Plug-in nicht überall funktioniert, so liegt dies höchstwahrscheinlich daran, daß der Kontext nicht den Bedingungen des Plug-ins genügt. Aus diesem Grund entstand kurz nach Erscheinen von MacOS 8 der Eindruck, daß nur das Kontextmenü im Finder erweiterbar sei, da die meisten Erweiterungen nur dort funktionierten. Das liegt aber daran, daß ein Plug-in, welches Dateireferenzen als Kontext erwartet, eben nichts mit Text- oder Bildreferenzen aus einem DTP-Programm anzufangen weiß.

Ein Plug-in ist ein Objekt, das vier Funktionsaufrufe (Initialize, ExamineContext, HandleSelection, PostMenuCleanup) verstehen muß. Nachdem beim Systemstart das Objekt erzeugt wurde, erhält es durch einen Initialize-Aufruf Gelegenheit, später benötigte Ressourcen im Speicher abzulegen. Der Ressourcenzweig eines Plug-in wird nicht automatisch geöffnet, sondern muß bei Bedarf explizit geöffnet und geschlossen werden.

Wenn der Benutzer einen Kontextklick ausführt, ruft das Plug-in ExamineContext auf. Den Kontext bekommt ein Plug-in in Form von AppleEvent-Deskriptoren (AEDesc) geliefert, die die ausgewählten Objekte beschreiben. üblicherweise entspricht dies genau dem Ergebnis des AppleScript-Konstrukts get selection. Im Finder könnte das eine Liste von Referenzen auf Dateien und Ordner sein. Es gilt herauszufinden, ob dieser Kontext 'paßt'. Mit den Funktionen des AppleEvent Managers versucht man, die erhaltenen Deskriptoren in die benötigte Form umzuwandeln. Gelingt dies nicht, kann das Plug-in nichts mit den Objekten anfangen. An dieser Stelle ist dann zu entscheiden, ob ein Menüeintrag erfolgen soll, wenn das Plug-in nicht mit allen ausgewählten Objekten etwas anzufangen weiß. Im Falle eines Menüeintrags ist ein Deskriptor anzulegen, der den neuen Eintrag beschreibt. Im einfachsten Fall enthält der nur den Text des Eintrags und eine ID-Nummer. Hierarchische Menüeinträge besitzen statt der ID eine Liste von Untermenüs. Die Deskriptoren werden an eine Deskriptorliste, die ExamineContext als Parameter bekommt, angehängt.

Um einem Benutzer nicht den Eindruck eines 'hängenden' Rechners zu geben, erhält ExamineContext mitgeteilt, wieviel Zeit zur Verfügung steht. Ein Plug-in, das mehr benötigt, muß diese anfordern. Zum Glück können einfachere Plug-ins die Zeitbeschränkung meist ignorieren, was die Programmierung erheblich vereinfacht. Will man aber rücksichtsvoll sein, kann das zu einiger Komplexität führen. Die Arbeit muß dann über mehrere Aufrufe von ExamineContext verteilt werden. Rekursive Algorithmen muß man zuerst iterativ umformulieren. Auch darf der Bearbeitungsstand zwischen den Aufrufen nicht verlorengehen. Im einfachsten Fall genügt es, ihn in Variablen des Plug-in-Objekts festzuhalten.

Hat der Benutzer einen Menüpunkt ausgewählt, erhält HandleSelection zusätzlich zum Kontextdeskriptor die ID des ausgewählten Befehls. Deren Bedeutung hängt allerdings ganz vom Plug-in ab. Beim Abarbeiten des Befehls bleibt mehr Zeit als bei der Kontextprüfung, aber trödeln darf das Plug-in auch hier nicht. Während das Plug-in werkelt, bleibt der Mac für den Benutzer blockiert; es ist nicht möglich, in andere Programme zu wechseln. Statt also die Arbeit im Plug-in selbst auszuführen, kann es sinnvoll sein, ein separates Programm per AppleEvent damit zu beauftragen. Das hat aber nur Sinn, wenn das Plug-in nicht auf eine Antwort des Programms warten muß, in einem Aufruf von AESend also kAENoReply verwenden kann.

Sowohl nach getaner Arbeit, aber auch, wenn kein Befehl des Plug-ins ausgewählt wurde, ruft man auf jeden Fall PostMenuCleanup auf. Wenn ExamineContext Speicher alloziert, Dateien geöffnet oder andere Ressourcen in Beschlag genommen hat, besteht hier die Gelegenheit, alles wieder ans System oder das Programm zurückzugeben.

Kontextmenü-Erweiterungen verwenden das System Object Model (SOM). Leider unterliegen SOM-basierte Klassen einigen Einschränkungen. Sie lassen sich nicht einfach um neue Funktionen erweitern. Am besten realisiert man ein Plug-in als gewöhnliche C++-Klasse, die man in eine SOM-basierte Klasse verpackt. Die SOM-Klasse leitet die Aufrufe an die C++-Klasse weiter und sorgt zusätzlich dafür, daß nur Fehlernummern, aber keine Exceptions nach außen dringen.

Die C++-Funktionen zur Speicherverwaltung verwenden in den meisten Implementierungen sogenannte Pools. Sie fordern vom System große Speicherblöcke an und zerlegen sie beim Allozieren von Speicher mit new in passende Unterblöcke. Der Speicherplatz der Pools wird häufig nicht mehr ans System zurückgegeben. In einem gewöhnlichen Anwendungsprogramm ist das sinnvoll, in einem Plug-in oft nicht. Im Metrowerks CodeWarrior läßt sich das umgehen, indem die Allokation im NEWMODE_SIMPLE ausgeführt wird. Dann kommen keine Pools zum Einsatz, sondern new und delete rufen direkt NewPtr und DisposePtr auf.

In einem Plug-in, das sich ins System hineinhängt, ist es besonders wichtig, daß es sich auch unter außergewöhnlichen Umständen gutartig verhält. Dazu gehört, daß Speicher und Ressourcen nicht 'aufgefressen' werden. Ein probates Mittel, um dies zu erreichen, sind 'Wächterobjekte' (smart pointer, handle), die die Lebensdauer des bewachten Objekts oder Speicherblocks auf eine Funktion beschränken (allgemeiner: auf einen lexikalischen Bereich). Prominentes Beispiel in C++ ist die Klasse auto_ptr. In PowerPlant beginnen die Namen solcher Klassen mit 'St', für 'stack-basiert'.

Kein Plug-in ist auf Anhieb fehlerfrei. Zum Glück lassen sie sich am (noch) lebenden System debuggen. Am einfachsten geht das, wenn sich zusätzlich zum Plug-in auch die Symboldatei ('.xSYM') im Ordner 'Contextual Menu Items'befindet. Es genügt dann, die Symboldatei zu öffnen und an geeigneten Stellen Breakpoints zu setzen.

Das Beispiel-Plug-in arbeitet lediglich mit Dateireferenzen. Nur, wenn mindestens eine Datei oder ein Ordner ausgewählt ist, fügt es dem Kontextmenü ein hierarchisches 'öffnen mit'-Menü hinzu. Das Untermenü enthält alle Programme beziehungsweise Aliasse auf Programme, die im Ordner 'Kontextöffner Programme Ÿ' im Ordner 'Contextual Menu Items' liegen. Wählt man eines der Programme im Menü aus, so teilt das Plug-in dem Finder per AppleEvent mit, daß er die ausgewählten Objekte mit diesem Programm öffnen soll.

Die Programmierung eines Plug-in ist nicht allzu aufwendig. Das Beispiel läßt sich mit wenigen Handgriffen umbauen. Befreit man in ExamineContext den BuildMenu-Aufruf von der if-Abfrage, erhält man zum Beispiel einen universellen Programmstarter. Dieser zeigt sein Menü auch dann an, wenn der Kontext nicht paßt. Sinnvollerweise paßt man dann auch gleich BuildMenu an, um mit einem geänderten Menüeintrag auf diesen Sachverhalt hinzuweisen. Weitere Plug-ins könnten sich mit der Verschlüsselung von Dateien mittels PGP oder deren Versand durch das in Internet Config festgelegte EMail-Programm befassen. Oder alle Programme anzeigen, die eine Auswahl öffnen können. Der Phantasie sind keine Grenzen gesetzt. Schicken Sie uns doch einfach Ihre Meisterwerke, und wir veröffentlichen [#beispiele Verweise auf die gelungensten am Ende dieser Seite]. (adb)

[1] Trygve Isaacson's Plug-in Framework

[2] Kontextmenü SDK, http://devworld.apple.com/ngs/lpp/adrpub/docs/MacOS8/Contextual_Menus.sea.hqx

[3] CCMArea von David Catmull, http://www.kagi.com/dathorc/ccmarea.html

[4] CContextualMenu von Dair Grant, http://www.kagi.com/authors/dair/purpleshark/default.html

[#anfang Seitenanfang]


Wenn Sie ein Plug-in erstellt haben, nehmen wir es gerne in diese Liste mit auf oder verweisen auf Ihre Homepage.

In der Regel sind die Dateien komprimiert und BinHex-kodiert, um sie sicherer über das Internet übertragen zu können. Am einfachsten dekomprimiert und dekodiert diese der kostenlose 'StuffIt Expander'.

  • Vom Autor des Artikels stammen auch die ersten beiden Beiträge: CMScript ermöglicht die Anwendung beliebiger AppleScripts auf einen Kontext. In welchen Ordner auf dem Schreibtisch liegende Dateien ursprünglich gehörten läßt sich mit Find Home Folder herausfinden.
Anzeige