Thunderbird-Addons: Optionen

Heute schauen wir uns, wieder am Beispiel von Quicker Filer 0.5.1, an, wie man in einem Thunderbird-Addon Einstellungen benutzt, die als Preferences persistiert werden.

In der install.rdf heißt es:

[sourcecode language="xml"]
<em:optionsURL>chrome://quickerfiler/content/options.xul</em:optionsURL>
[/sourcecode]

Damit wird ein Einstellungsdialog definiert. Standardmäßig wird dieser als Dialogfenster geöffnet. Über die Definition des Content Packages in der chrome.manifest (vgl. dieses Posting) ergibt sich also, dass der Dialog in content/options.xul definiert ist.

Gehen wir das also der Reihe nach durch.

[sourcecode language="xml"]
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
[/sourcecode]

Hier wird ein Stylesheet eingebunden, und zwar ein recht generisches.

[sourcecode language="xml"]
<prefwindow id="quickerfilerPrefWindow" title="Quicker Filer – Options" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
buttons="accept,cancel" height="550">
[/sourcecode]

Das Fenster ist ein prefwindow, also speziell für Einstellungsdialoge vorgesehen. Es gibt weiterführende Dokumentation, die aber bis auf ein Beispiel und einen Hinweis, wie man so ein Fenster aufruft, keine wesentlichen Infos enthält. Also gehen wir weiter unsere Datei im Detail durch.

Zwei der Attribute sind selbsterklärend: title definiert den Fenstertitel und height setzt die bevorzugte Höhe des Fensters in Pixeln. buttons definiert, welche Schaltflächen angezeigt werden, hier also OK und Abbrechen. Das dürfte üblicherweise eine gute Wahl sein.

[sourcecode language="xml"]
<script type="application/x-javascript" src="chrome://quickerfiler/content/options.js"/>
[/sourcecode]

Hier wird ein Script eingebunden. Es definiert einigige Funktionen, die dann in Event-Handlern aufgerufen werden – dazu also unten mehr.

[sourcecode language="xml"]
<prefpane id="quickerfilerOptions.general.prefpane" label="General" selected="true">
[/sourcecode]

Eine prefpane ist ein Panel, also wohl eine Seite, mit Einstellungen. Der Inhalt kann entweder aus einer gesonderten .xul-Datei geladen oder – wie hier – inline definiert werden.

label definiert eine Beschriftung. Ob es tatsächlich vorgesehen ist, selected direkt zu setzen, bezweifle ich gerade ein wenig …

[sourcecode language="xml"]
<preferences>
<preference id="extensions.quickerfiler.defaultfolder.text" name="extensions.quickerfiler.defaultfolder.text" type="string"/>
<preference id="extensions.quickerfiler.search.root" name="extensions.quickerfiler.search.root" type="string"/>
<preference id="extensions.quickerfiler.search.root.account" name="extensions.quickerfiler.search.root.account" type="string"/>
<preference id="extensions.quickerfiler.search.root.folder.text" name="extensions.quickerfiler.search.root.folder.text" type="string"/>
<preference id="extensions.quickerfiler.mkdir.enable" name="extensions.quickerfiler.mkdir.enable" type="bool"/>
<preference id="extensions.quickerfiler.debug.enable" name="extensions.quickerfiler.debug.enable" type="bool"/>
<preference id="extensions.quickerfiler.suggestlastfolder.enable" name="extensions.quickerfiler.suggestlastfolder.enable" type="bool"/>
<preference id="extensions.quickerfiler.copysentmessage.enable" name="extensions.quickerfiler.copysentmessage.enable" type="bool"/>
</preferences>
[/sourcecode]

Dieser Block beschreibt die Einstellungen, die in der prefpane geändert werden. Er besteht aus einzelnen preference-Elementen.

Die Attribute sind wieder recht selbsterklärend. name setzt den Namen der zu ändernden Einstellung (hier zweckmäßigerweise identisch zur ID des jeweiligen Elements), type legt fest, von welchem Typ der Wert ist.

[sourcecode language="xml"]
<vbox>
[/sourcecode]

Die vbox dient der Gliederung bzw. Layout-Zwecken.

[sourcecode language="xml"]
<groupbox>
<caption label="Default folder"/>
<description>Default folder for instant copy/move</description>
<textbox id="quickerfiler.defaultfolder.textbox"
preference="extensions.quickerfiler.defaultfolder.text"
type="autocomplete"
autocompletesearch="quickerfiler-autocomplete"
tabScrolling="true"
autoFill="true"
forceComplete="true"
showcommentcolumn="false" />
</groupbox>
[/sourcecode]

Die groupbox dient der Gruppierung von Elementen und wird in der Regel mit Umriss gezeichnet. Die caption wird in diesem Umnriss gezeichnet. Die description ist ein Textblock, der einfach angezeigt wird.

Die textbox kann Auto-Vervollständigung und entspricht der im letzten Artikel schon genauer erklärten. Genaueres also dort, und die Erläuterung, wie Auto-Vervollständigung im Hintergrund funktioniert, schieben wir immer noch nach hinten.

[sourcecode language="xml"]
<groupbox>
<caption label="Search Root"/>
<description>Determines the scope of the folder list</description>
<radiogroup id="quickerfiler.search.root.radiogroup"
preference="extensions.quickerfiler.search.root">
[/sourcecode]

Es folgt eine weitere groupbox mit caption und description. radiogroup definiert eine Gruppe von Radio-Buttons. Es ist die zugehörige preference angegeben. Wohlgemerkt verweist der Wert auf die ID des entsprechenden preference-Elements!

[sourcecode language="xml"]
<radio id="quickerfiler.search.root.allfolders.radio"
label="All folders"
value="msgaccounts:/"/>
[/sourcecode]

Hier wird ein einzelner Radio-Button definiert. Die Dokumentation weiß anscheinend nicht so recht, dass das value-Attribut im Zusammenhang mit Preferences eine feste Bedeutung hat. Vermutlich sollte man das mal explizit dokumentieren. label definiert die Beschriftung des Radio-Buttons.

[sourcecode language="xml"]
<hbox>
<radio id="quickerfiler.search.root.account.radio"
label="This account"
value="– account –"/>

<hbox flex="1" pack="end">
[/sourcecode]

Hier wird es layouttechnisch interessant. Der Radio-Button wird zusammen mit der menulist in eine hbox gesteckt. Die menulist wird nochmal in eine eigene hbox gesteckt, bei der zwei Attribute gesetzt sind. flex="1" sorgt dafür, dass sie sämtlichen freien Platz verbraucht. pack="end" lässt die menulist nach rechts rutschen. Was mir hier nicht klar ist:

  • Warum ist pack="end" noch nötig, wenn flex="1" angegeben ist? Wie kann in dem Fall noch Platz übrig sein?
  • Warum ist die menulist nochmal in eine hbox verpackt? Man könnte diese Attribute auch auf der menulist spezifizieren. Erzielt das ggf. auch den gewünscht Effekt? Wenn nein, warum nicht? Wenn ja, warum sollte man es so wie hier machen?

[sourcecode language="xml"]
<menulist id="quickerfiler.search.root.account.menulist"
preference="extensions.quickerfiler.search.root.account"
sortResource="http://home.netscape.com/NC-rdf#Name"
sortDirection="ascending"
datasources="rdf:msgaccountmanager rdf:mailnewsfolders"
containment="http://home.netscape.com/NC-rdf#child"
ref="msgaccounts:/" flex="1">
[/sourcecode]

menulist definiert eine Dropdown-Auswahlliste. preference legt wieder fest, welche Einstellung gesetzt wird. flex="1" (nochmal!) sorgt dafür, dass die menulist die komplette Breite ausfüllt.

Die restlichen Attribute (sortResource, sortDirection, datasources, containment, ref) beziehen sich darauf, wie man den Inhalt der Liste dynamisch aus einer RDF-Datenquelle zusammenstellen lässt.

Dazu dient auch das darauffolgende template:

[sourcecode language="xml"]
<template xmlns:nc="http://home.netscape.com/NC-rdf#">
<rule nc:ServerType="nntp"/>
<rule nc:IsDeferred="true"/>
<rule nc:IsServer="true">
<menupopup>
<menuitem uri="…"
value="…"
label="rdf:http://home.netscape.com/NC-rdf#Name"/>
</menupopup>
</rule>
</template>
[/sourcecode]

Dafür gibt es eine eigene Anleitung, und jemanden, der das alles für großen Schrott hält.

Insofern verschieben wir das lieber auf später – bis ich rausgefunden habe, ob das überhaupt etwas taugt. Jedenfalls erstellt das Template wohl ein menupopup mit vielen menuitems – aber ich verstehe schon nicht, warum/ob da nur ein menupopup, aber mehrere menuitems rauskommen …

Danach wird es zum Glück erstmal unspannend – daran merkt man, dass etwas hängen bleibt … Es werden einige noch offene Elemente geschlossen, und dann kommt eine weitere Radiobox, verbunden mit der Möglichkeit, einen Ordner auszuwählen . Hier kommt wieder die altbekannte Textbox mit Autovervollständigen zum Einsatz. So weit unproblematisch, aber da ist eine Sache, der ich noch auf den Grund gehen muss: In den Einstellungen wird definiert, was die Wurzel für die Suche nach Ordnern ist – es wäre aber unschön, wenn diese Einstellung auch bei der Festlegung der Wurzel zum Tragen kommt. Ich muss dementsprechend mal ergründen, warum sie es nicht tut – bzw., ob sie es vielleicht doch tut – das wäre dann wohl ein Bug.

Danach geht es auch relativ unspannend weiter:

[sourcecode language="xml"]
<groupbox>
<caption label="Suggest last folder"/>
<description>Search box fill be filled with the last transfer folder used</description>
<checkbox label="Suggest last folder" preference="extensions.quickerfiler.suggestlastfolder.enable"/>
</groupbox>
[/sourcecode]

Hier kommt zum ersten Mal eine checkbox vor. Die hat ein label und ist an eine bestimmte preference gebunden – alles easy. Danach kommen noch weitere checkboxes, die ich wiederum überspringe. Interessant ist in dem Zusammenhang nur die Entdeckung der Einstellung quickerfiler.debug.enable. Unter Linux habe ich nämlich Probleme – vielleicht hilft das ja beim Debuggen.

Jedenfalls ist damit:
[sourcecode language="xml"]
</prefpane>
[/sourcecode]

die erste Seite der Einstellungen fertig. Es folgt die zur Festlegung der Tastenkombinationen.

[sourcecode language="xml"]
<prefpane id="quickerfilerOptions.shortcuts.prefpane"
label="Keyboard Shortcut"
onpaneload="sQuickerFilerOptions.onPaneLoad();">
<preferences>
<preference id="extensions.quickerfiler.shortcuts.copy.key"
name="extensions.quickerfiler.shortcuts.copy.key"
type="string"/>

<!– … –>

<preference id="extensions.quickerfiler.shortcuts.selfolder.modifiers"
name="extensions.quickerfiler.shortcuts.selfolder.modifiers"
type="string"/>
</preferences>
[/sourcecode]

Zu Beginn wird wieder definiert, welche Einstellungen auf dieser Seite geändert werden können. Für diverse Aktionen (copy, move, inscopy, insmove, selfolder) wird jeweils eine Einstellung key und eine Einstellung modifiers. Aus Gründen ist das etwas gekürzt.

onpaneload definiert, dass beim Laden der Seite folgende Funktion ausgeführt wird:

[sourcecode language="javascript"]
onPaneLoad: function onPaneLoad()
{
this.updateTreeView();
},
[/sourcecode]

Die Funktion updateTreeView() füllt anscheinend einen Treeview anhand der Einstellungen mit den bis jetzt definierten Tastenkürzeln. Die genaue Funktionsweise schaue ich mir an, sobald ich die UI-Elemente selbst angeschaut habe.

In einer vbox folgt dann folgende groupbox:

[sourcecode language="xml"]
<groupbox>
<caption label="Available shortcuts"/>

<tree id="quickerfilerOptions.shortcuts.availableShortcutsTree"
onselect="sQuickerFilerOptions.onAvailableShortcutsTreeSelect();"
rows="5" hidecolumnpicker="true" seltype="single">

<treecols>
<treecol id="nameColumn" label="Name" flex="1"/>
<treecol id="shortcutColumn" label="Shortcut" flex="2"/>
</treecols>
[/sourcecode]

Bis hierhin wurde ersteinmal die Struktur des Baums definiert. Das Hauptelement ist ein tree. onselect wird ausgeführt, wenn eine Zeile im Baum ausgewählt wird. Hier werden wohl die Controls für die Auswahl der Tastenkombination für die entsprechende Aktion gesetzt. Genaueres dann unten, wenn wir uns anschauen, wie der Inhalt aufgebaut ist und initialisiert wird. rows gibt an, wieviele Zeilen gleichzeitig angezeigt werden. Das ist hier gerade die Anzahl der insgesamt existierenden Zeilen. seltype="single" bedeutet, dass es nicht möglich ist, mehrere Zeilen auf einmal auszuwählen. hidecolumnpicker="true" schließlich blendet das “Menü” zum Anzeigen und Verstecken einzelner Spalten aus. treecols enthält einzelne treecol-Elemente. Deren label-Attribut legt die Spaltenüberschrift fest.

trees sind im übrigen keine triviale Angelegenheit, weshalb es dazu auch ein Tutorial gibt. Im allgemeinen Fall können sie aus beliebigen Datenquellen mit großen Datenmengen dynamisch befüllt werden – beispielsweise die Nachrichten in einer Newsgroup. Diese Datenquellen heißen tree views. Netterweise gibt es auch einen vordefinierten treeview, bei dem die Daten aus XUL-Elementen kommen. Das nennt sich dann content tree. Was man dabei beachten muss: Dadurch, dass die Daten durch einen tree view geschleift werden, ist man nicht sehr flexibel, was man hier angeben kann. Es können wirklich nur für jede Tabellenzelle ein Text und ein Icon angegeben werden. Wie es das Tutorial ausdrückt:

Having said that the data to be displayed in a tree comes from a view and not from XUL tags, there happens to be a built-in tree view which gets its data from XUL tags.

Der Rest ist dann unspektakulär:

[sourcecode language="xml"]
<treechildren>
<treeitem>
<treerow>
<treecell label="Copy" value="copy"/>
<treecell label="disabled"/>
</treerow>
</treeitem>
<!– vier weitere <treeitem>-Elemente für die anderen Aktionen –>
</treechildren>
</tree>
</groupbox>
[/sourcecode]

Das Hauptelement ist treechildren. Dazu sagt die Doku:

This element is the body of the tree. For content trees, the content will be placed inside this element. This element is also used to define container rows in the tree.

Zum Glück haben wir einen content tree. Wie der Inhalt sonst definiert wird, oder was container rows sein sollen, steht da nämlich leider nicht.

Darin sind dann fünf treeitems, die jeweils eine treerow enthalten. Darin wiederum ist dann jeweils eine treecell pro Spalte. Dabei ist jeweils das Attribut label gesetzt, und in der linken Spalte noch value, offenbar zum Wiederfinden aus Scripts.

Und mit den oben noch zurückgestellten Scripts machen wir jetzt auch weiter. Zuerst die Funktion updateTreeView(), die (unter anderem) beim Laden der Pane ausgeführt wird:

[sourcecode language="javascript"]
updateTreeView: function updateTreeView()
{
var tree = document.getElementById(‘quickerfilerOptions.shortcuts.availableShortcutsTree’);
var rowCount = tree.view.rowCount;

for(var i = 0; i<rowCount; i++)
{
var shortcut = tree.view.getCellValue(i, tree.columns[0]);
var key = document.getElementById(‘extensions.quickerfiler.shortcuts.’+shortcut+’.key’).value;
var modifiers = document.getElementById(‘extensions.quickerfiler.shortcuts.’+shortcut+’.modifiers’).value;

if(key != undefined && key != ”)
{
var text = "";
if(modifiers)
text = modifiers.toUpperCase().split(‘ ‘).sort().join(‘ + ‘);
if(text)
text += ‘ + ‘;
text += key.toUpperCase();

tree.view.setCellText(i, tree.columns[1], text);
}
else
{
tree.view.setCellText(i, tree.columns[1], ‘disabled’);
}
}
}
[/sourcecode]

Diese Funktion holt sich zunächst das
-Element und geht alle Tabellenzeilen durch: Die Variable shortcut wird auf den value der Tabellenzelle in der ersten Spalte gesetzt. Dieses Attribut dient also, wie oben vermutet, nur dem “Wiedererkennen”. key und modifiers werden auf den Wert der entsprechenden Preferences gesetzt (ausgelesen aus den jeweiligen preference-Elementen). Falls diese Werte sinnvoll sind, wird daraus ein String zusammengebastelt, der dann in der zweiten Tabellenspalte angezeigt wird.

Interessant dürften noch die aufgerufenen Methoden (und benutzten Eigenschaften) des tree view (tree.view) sein, die im Interface nsITreeView definiert sind:

Ebenfalls bereits bemerkt hatten wir die Funktion onAvailableShortcutsTreeSelect. Die stellen wir auch weiterhin zurück, da sie Elemente bearbeitet, die wir noch nicht angeschaut haben.

In options.xul kommt nun eine weitere groupbox:

[sourcecode language="xml"]
<groupbox id="quickerfilerOptions.shortcuts.settingsGroupbox" hidden="true">
<caption label="Shortcut Settings"/>
<description>These settings apply to the selected shortcut above</description>

<hbox>
<vbox>
<groupbox>
<caption label="Key"/>

<menulist id="quickerfilerOptions.shortcuts.shortcutKey"
oncommand="sQuickerFilerOptions.onShortcutKeyMenulistCommand();">
<menupopup>
<menuitem value="" label="disabled"/>

<menuitem value="A" label="A"/>
<!– … –>
<menuitem value="Z" label="Z"/>

<menuitem value="/" label="/"/>
<!– … –>
<menuitem value="]" label="]"/>
</menupopup>
</menulist>
</groupbox>
</vbox>

<groupbox flex="1">
<caption label="Modifiers"/>

<checkbox id="quickerfilerOptions.shortcuts.shortcutModifier.accel"
label="Accel"
value="accel"
oncommand="sQuickerFilerOptions.onShortcutModifierCommand();"/>
<!– und dasselbe für alt, control, meta und shift –>
</groupbox>
</hbox>
</groupbox>
[/sourcecode]

Zu beachten ist, dass diese groupbox anfangs versteckt ist (hidden="true")! Sichtbar gemacht wird sie per Script (siehe unten), sobald im Treeview ein Befehl ausgewählt wird. Die eigentlichen Controls sind dann eine Dropdown-Liste (menulist), die alle möglichen Tasten enthält, und checkboxes für die verschiedenen Modifier. Das ist jetzt hinreichend langweilig, dass es der geneigte Leser gerne eigenständig in der Dokumentation nachlesen darf. Für Veränderungen sind Event-Listener definiert, auf die ich später noch eingehe. Damit ist die Beschreibung des Fensters dann auch schon zu Ende.

Jetzt also die Funktion, die ausgeführt wird, wenn man ein Kommando auswählt, um dafür ein Tastenkürzel zu vergeben:

[sourcecode language="javascript"]
onAvailableShortcutsTreeSelect: function onAvailableShortcutsTreeSelect()
{
var tree = document.getElementById(‘quickerfilerOptions.shortcuts.availableShortcutsTree’);
var shortcut = tree.view.getCellValue(tree.currentIndex, tree.columns[0]);

var keyEl = document.getElementById(‘extensions.quickerfiler.shortcuts.’+shortcut+’.key’);
var key = "";
var modifiers = "";
if(keyEl!=null)
{
key = keyEl.value;
if(key==null)
key = "";
else
modifiers = document.getElementById(‘extensions.quickerfiler.shortcuts.’+shortcut+’.modifiers’).value;
}

document.getElementById(‘quickerfilerOptions.shortcuts.settingsGroupbox’).hidden = false;
document.getElementById(‘quickerfilerOptions.shortcuts.shortcutKey’).value = key.toUpperCase();

for(var i in this.mModifiers)
{
document.getElementById(‘quickerfilerOptions.shortcuts.shortcutModifier.’+this.mModifiers[i]).checked = (modifiers.indexOf(this.mModifiers[i])>-1)
}

this.updateTreeView();
}
[/sourcecode]

Zunächst wird die aktuell ausgewählte Zeile der treelist bestimmt (tree.currentIndex). Diese Eigenschaft funktioniert nur bei seltype="single", aber das ist hier ja gesetzt. keyEl wird dann auf das preference-Element gesetzt. Ggf. werden aus diesem und dem für die Modifiers die entsprechenden Werte ausgelesen. Dann wird die groupbox angezeigt (hidden wird auf false gesetzt), der passende Eintrag der menulist wird selektiert, und die Chechboxes der entsprechenden Modifiers ggf. angekreuzt.

Bleiben noch die zwei Event-Listener, die auf Veränderungen an diesen Elementen reagieren:

[sourcecode language="javascript"]
onShortcutKeyMenulistCommand: function onShortcutKeyMenulistCommand()
{
// User selected another shortcut key from the pop up menu,
// set this as new shortcut key for the current selected command:
var tree = document.getElementById(‘quickerfilerOptions.shortcuts.availableShortcutsTree’)
var shortcut = tree.view.getCellValue(tree.currentIndex, tree.columns[0]);

document.getElementById(‘extensions.quickerfiler.shortcuts.’ + shortcut + ‘.key’).value =
document.getElementById(‘quickerfilerOptions.shortcuts.shortcutKey’).value;

this.updateTreeView();
}
[/sourcecode]

Eigentlich recht selbsterklärend: Wenn eine andere Taste ausgewählt wurde, wird die entsprechende preference gesetzt. updateTreeView() setzt dann für alle Zeilen des treeview die Werte neu (was im Grunde Overkill ist – aber immerhin bleiben die Zeilen an sich erhalten – sonst ginge bestimmt auch der Fokus verloren).

[sourcecode language="javascript"]
onShortcutModifierCommand: function onShortcutModifierCommand()
{
var tree = document.getElementById(‘quickerfilerOptions.shortcuts.availableShortcutsTree’);
var shortcut = tree.view.getCellValue(tree.currentIndex, tree.columns[0]);

var modifiers = [];
for(var i in this.mModifiers)
{
if(document.getElementById(‘quickerfilerOptions.shortcuts.shortcutModifier.’+this.mModifiers[i]).checked)
modifiers.push(this.mModifiers[i]);
}

document.getElementById(‘extensions.quickerfiler.shortcuts.’+shortcut+’.modifiers’).value = modifiers.join(‘ ‘);

this.updateTreeView();
}
[/sourcecode]

Auch das ist wieder selbsterklärend: Wenn sich ein Modifier geändert hat, wird die komplette Liste wieder erstellt und in der Preference gespeichert, danach werden die Werte des treelist neu gesetzt.

Damit wären wir auch am Ende dieses Teils unseres kleinen Kurses. Zum Lernen ist dieses Beispiel ja ganz nett – aber um einfach nur eine Tastenkombination zu setzen, ist das doch ziemlich viel Aufwand. Schöner wäre ein eigenes XUL-Control, bei dem man die Tastenkombination einfach eintippen kann. Daher habe ich das in Bug 673669 vorgeschlagen.

Und wieder der obligatorische Hinweis: Code-Schnipsel stammen aus Quicker Filer 0.5.1, sind Copyright (C) 2010 Eivind Rovik und stehen unter GPL (Version 3 oder jede spätere Version).

Hinterlasse eine Antwort

Deine E-Mail-Adresse wird nicht veröffentlicht.

Du kannst folgende HTML-Tags benutzen: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>