RWStyledTextView

RWStyledTextView

This page is also available in english!

In meinem ersten Tutorial zur Programmierung von RapidWeaver Plugins geht es um die Verwendung des RWStyledTextView, der mit RapidWeaver 4 eingeführt wurde. Er verfügt im vergleich zum RWTextView über ein Toolbar am unteren Rand, in dem die meisten Textformatierungen direkt aufgerufen werden können. Außerdem enthält er bereits die Schaltflächen zum Hinzufügen und Löschen von Links. Den meisten Benutzernd dürfte er von den Formatierter Text oder Blog Seiten bekannt sein.
Leider zeigt das von Realmac Software gelieferte HelloWrold Beispiel nicht, wie man diesen View in eigenen Plugins verwendet. Und auch die "comprehensive comments to some of the header files" war mir dabei nicht sonderlich dienlich. Vielmehr ist es mir nur mit Unterstützung aus dem RapidWeaver Forum gelungen, die Rätsel des RWStyledTextView zu lüften. Deshlab geht an dieser Stelle ein spezieller dank an Isaiah von Yourhead, John von Loghound und RapidAlbum Smackie, für die Unterstützung. Ich hoffe, dass das Tutorial einigen von euch den Einstieg etwas erleichtert und freue mich auf weitere nützliche Plugins.
Das Tutorial ist dabei nicht der Weisheit letzter Schluss. Genau bin ich mir bei einem bestimmten Punkt immer noch sehr unklar. Und selbst André Pang von Realmac Software konnte mir diesen Punkt bis jetzt noch nicht ganz klar machen. Leider (für mich) hat André mittlerweile Realmac Software verlassen um bei Pixar zu arbeiten. Deshalb seid Ihr alle dazu aufgerufen Verbesserungsvorschläge und weitere Informationen beizusteuern.

Aktueller Hinweis!
Seit dem Update auf RapidWeaver 4.2.2 bekomme ich einen Fehler beim kompilieren. Offensichtlich gibt es einen Fehler in der RWLink.h Headerdatei. Zum kompilieren meiner Plugin verwende ich immer noch die Version 4.2 bei der soweit alles funktioniert.

Vorbereitung des Projekts

Als Ausgangsbasis für das Tutorial dient das HelloWorld Beispiel aus dem Plugin-SDK von RapidWeaver. Doch für den Sinnvollen Einsatz des Projektes müssen noch einige Kleinigkeiten geändert werden. Zumindest an der Version, die zum Zeitpunkt der Erstellung dieses Tutorials erhältlich war (RapidWeaver 4.2).
Zunächst einmal einige Einstellung die generell zu machen sind, bzw. gemacht werden sollten. Damit wir später das Plugin direkt aus Xcode heraus testen und debuggen können müssen wir eine ausfürbare Datei zum Projekt hinzufügen. In unserem Fall handelt es sich dabei natürlich um RapidWeaver. Hierzu wählen wir in der linken Spalte unter Groups & Files den Eintrag Executables mit einem Rechtsklick aus und wählen aus dem Kontextmenü Add/New Custom Executable... aus.

Im Dialogfenster New Custom Executable geben wir dann als Executable Name RapidWeaver ein und wählen das Programm mit Hilfe des Choose Knopfes hinter Executable Path aus. Das Dialogfenster können wir dann mit Finish schließen und bekommen als Ergebnis den Info Dialog unserer ausführbaren Datei zu Kontrolle angezeigt. Das Info-Fenster könnt ihr dann einfach schließen.

Der zweite Punkt ist gleichzeitig der erste wichtige Tip, den ich mal von Smackie (RapidAlbum) bekommen habe. Es geht darum den Compiler sehr pingelich zu machen. Er soll uns alle Warnungen ausgeben und Warnungen wie Fehler bahandeln, sprich das compilieren abbrechen. Ihr tut euch keinen gefallen damit Warnungen zu ignorieren/akzeptieren. Früher oder später holen sie euch ein und führen zu Fehlern, die ihr nicht mehr nachvollziehen könnt. Deshalb werden wir den Compiler im Projekt scharf stellen. Dazu selektieren wir unser Plugin unter Groups & Files und klicken dann auf den Info Knopf im Toolbar. Im Dialogfenster Project "HelloWorldPlugin" Info wählen wir den Bereich Build und geben im Suchfeld Warning ein. In der Liste mit Einstellungen suchen wir dann im Abschnitt GCC 4.0 - Warnings den Eintrag Treat Warnings as Erros und aktivieren diesen. Anschließend suchen wir noch den Eintrag Other Warning Flags und schreiben dort den Parameter -Wall hinein. Damit haben wir den Compiler scharf geschaltet und er wird unser Programm erst dann komplett zusammenbauen, wenn sich der Quelltext ohne Warnungen übersetzen lässt.

Doch kommen wir jetzt endlich zum eigentlichen Plugin. Gegenüber der ersten Version des Plugin-SDK für die Version 4 wurde zwar die Einbindung der RapidWeaver Frameworks im Sourcecode auf die neue Version umgestellt, nicht aber die Einbindung der Franeworks ins Projekt. So findet man in der Header Datei des Plugins zwar den folgenden Code

#import <RMKit/RMKit.h>
#import <RWKit/RWKit.h>

im Projekt wird aber immer noch der von RapidWeaver 3 stammende RWPluginUtilities Framework eingebunden. Zwar sorgt ein Dummy-Framework im RapidWeaver Programmpaket dafür, dass diese Einbindung funktioniert und die neuen Frameworks eingebunden werden. Das ist wenig hilfreich. Wollen wir uns die zum Beispiel die Header Dateien des Frameworks ansehen, um in den Genuß der "comprehensive" Dokumentation zu kommen, finden wir nur gähnende Leere. Auch bei der Verwendung der Komponenten im Interface Builder müssten wir mit Einschränkungen leben. Also wollen wir das gleich ändern. Dazu suchen wir im Programme Ordner das RapidWeaver Programmpaket und lassen uns über den entsprechenden Menüpunkt im Kontextmenü den Paketinhalte des Programms anzeigen. Dort navigieren wir zum Ordner Contents/Frameworks in dem wir die beiden neuen Framworks RMKit und RWKit finden. Die beiden Frameworks ziehen wir per Drag'n'Drop in den Ordner Frameworks des geöffneten HelloWorldPlugin Projekts.

Jetzt können wir den überflüssig gewordenen RWPluginUtilities Framework aus dem Projekt entferenen und haben eine gute Grundlagen für unsere weitere Entwicklung. Später müssen wir noch an einer anderen Stelle eine Einstellung ändern, doch dazu bei passender Gelegenheit mehr.
In den neuen Frameworks werden wir auch endlich fündig. Hier können wir jetzt einblick in die Headerdatein der einzelnen Komponenten nehmen.

Um unser Plugin auf seine zukünftigen Aufgaben vorzubereiten ändern wir die Vererbung. Unser Plugin wird nicht länger von der Klasse RWAbstractPlugin sondern von der Klasse RWTextViewPluginClient abgeleitet. Außerdem soll unser Plugin das Interface RWStyledTextViewDelegate implementieren. So vorbereiten können wir auch ein neues Outlet für den RWStyledTextView hinzufügen. Die geändert Headerdatei unseres Plugins sieht dann wie folgt aus:

@interface HelloWorldPlugin : RWTextViewPluginClient <RWStyledTextViewDelegate>
{
    IBOutlet NSView *pluginView; //Interface View
    IBOutlet NSView *optionsView; //Options View
    IBOutlet NSTextField *outputTextField; // Text Field
    IBOutlet RWStyledTextView *contentTextView;
}

 - (IBAction)showOptions:(id)sender;

@end

Interface Builder

Jetzt sind wir bereit dafür, den Interface Builder zum ersten mal zu starten. Auch hier gilt es zunächst einige Vorbereitungen zu Treffen. Wir wollen dem Interface Builder einige Informationen über die neuen Komponenten bereitstellen, damit dieser besser mit ihnen umgehen kann. Hierzu ziehen wir die Headerdatei RWStyledTextView.h aus dem RWKit.framework auf das Document Window des Projekts.

Genauso verfahren wir mit den Headerdateien RWAbstractPlugin.h und RWTextViewPluginClient.h. Jetzt können wird den RWStyledTextView in den PluginView einfügen. Wir ziehen aus der Library einen CustomView auf den PluginView. Wie dieser View platziert und in der Größe angepasst wird setze ich dabei mal als bekannt voraus. Um aus dem CustomView nun einen RWStyledTextView zu machen wechseln wir zum Identity Inspector und als Klasse RWStyledTextView aus. Ein entsprechender Eintrag sollte dank unsers Imports der Headerdatei bereits in der Liste vorhanden sein.

Der nächste Schritt wäre nun, die Verbindung zum Outlet in unserem Plugin herzustellen. Wir öffnen also den Connections Inspector für unseren File's Owner und müssen feststellen, dass unser Outlet nicht angezeigt wird. Im Identitiy Inspector des File's Owner finden wir die Ursache dafür. Hier ist im HelloWorld Beispiel fälschlicher Weise noch die Klasse TemplatePlugin angegeben. Der Klassenname unseres Plugins lautet aber HelloWorldPlugin. Also tragen wir diesen Namen als Klasse ein. Wechseln wir nun zurück zum Connections Inspector wird auch endlich unser Outlet angezeigt, und wir können eine Verbindung mit dem RWStyledTextView herstellen.

Damit ist unsere Arbeit im Interface Builder auch schon abgeschlossen und wir müssen uns noch kurz um die ersten Codezeilen in der Datei HelloWorldPlugin.m kümmern, bevor wir einen ersten Test wagen können.

Plugin Initialisierung

In der Headerdatei haben wir angegeben, das unser Plugin das Interface RWStyledTextViewDelegate implementiert. Das müssen wir dann jetzt auch tun. Glücklicher Weise enthält das Interface nur eine Methode, die wir einfach wie folgt am ende des Quellcodes einfügen:

 - (NSObject<RWPluginProtocol>*)pluginForStyledTextView:(RWStyledTextView*)styledTextView
{
    return self;
}

Jetzt müssen wir nur noch in der Methode init dafür sorgen, dass sich unser Plugin als Delegate für den RWStyledTextView registriert. Hierzu fügen wir wie im nachfolgenden Code angegeben die Zeile 8 ein:

- (id)init
{
    self = [super init];
    if(self == nil) return nil;
	
    [NSBundle loadNibNamed:@"HelloWorld" owner:self];
 
    [contentTextView setDelegate:self];
 
    return self;
}

Jetzt ist der große Moment gekommen, an dem wir unser Plugin zum ersten mal testen können. Wenn alles richtig gemacht wurde und mein Tutorial bis hierher gut genug war, sollte das Plugin im Bearbeiten-Modus bereits funktionieren. Man kann Text eingeben, mit Formaten versehen, Bilder und Links einfügen.

Nachdem wir uns nun eine halbe Ewigkeit über unseren Erfolg gefreut und mit dem Plugin herumgespielt haben, geht es jetzt ans Eingemachte. Wir müssen dafür sorgen, dass unser Plugin auch den entsprechenden Code für die Webseite liefert.

Den Inhalt des Plugins exportieren

Wir müssen uns also um die Methode contentHTML kümmern. In dieser Methode müssen wir als den NSAttributedString aus unserem RSStyledTextView parsen und die Attribute, die dem Text zugeordnet worden sind in entsprechenden HTML-Code übersetzen. Außerdem müssen wir die eingebetteten Bilder erkennen und extrahieren, damit wir sie wiederum in entsprechenden HTML-Code referenzieren können. Wir müssen RapidWeaver natürlich auch noch dazu bringen die Bilder zu exportieren. Ach ja, die Links im Text dürfen wir ja auch nicht vergesse!
Na, Angst bekommen angesichts dieser imensen Aufgabe?
Glücklicherweise müssen wir das ganze nicht selbst machen. Hier geben uns die Jungs von Realmac Software ein sehr mächtiges Werzeug an die Hand. Ein Instanz der Klasse RHTML wird die Aufgabe für uns erledigen.
Leider liegt in dieser Klasse auch das große Problem, das die Fertigstellung dieses Tutorials so lange verzögert hat. Bis heute konnte mir noch niemand ganau die einzelnen Parameter richtig erklären und vor allem den richtigen Umgang mit den Parametern. Und so müsst Ihr also die nachfolgende Lösung zunächst einmal als Ergebnis von Ausprobieren und Spicken aus dem Quellcode anderer Plugin-Projekte betrachten. Das ganze funktioniert, ich bin mir aber nicht sicher ob es der richtige Weg ist. Ich hoffe, dass diejehnigen unter euch, die es besser wissen, mir einige Tips und Tricks zukommen lassen.
Doch damit genug der Vorrede. Hier kommt der Code.

- (id)contentHTML:(NSDictionary*)params
{
  RMHTML* contentExporter = [self exporterWithParams:params];
  NSMutableString *contentExported = [NSMutableString string];
  NSString *path = [NSString stringWithFormat:@"%@/files", [params valueForKey:@"path"]];
  
  [contentExported setString:[contentExporter exportAttributedString:[contentTextView attributedString]
                                                              toPath:path
                                                        imagesFolder:path
                                                         imagePrefix:@""
                                                        HTMLTemplate:[NSMutableString stringWithString:@"%content%"]
                                                          contentTag:@"content"
                                                            fromPage:[[self document] performSelector:@selector(pageFromUniqueID:) withObject:[self uniqueID]]]];
  
  return contentExported;  
}

Als erstes erzeugen wir uns ein Objekt vom typ RHTML, das später die Umwandlung unsers formatierten Textes in HTML vornehmen soll. Außerdem Legen erzeugen wir noch ein Objekt vom Typ NSMutableString, das den umgewandelten HTML Code aufnehmen wird.
Der contentHTML Methode die wir gerade implementieren, wird beim Aufruf ein NSDictionary mit Paremetern übergeben. In diesem Dictionary werden einige recht nützliche Informationen über die aktuelle RapidWeaver Umgebung geliefert. Wir griefen uns hier den Parameter path heraus, der Ziel für die exportierte Seite dienen soll. RapidWeaver liefert hier einen Entsprechenden Wert, der je nach Ziel (Vorschau oder Export) angepasst ist. Jetzt folgt endlich der Aufruf der Funktion exportAttributedString unseres RHTML Objektes, die die ganze Arbeit erledigt. Und genau hier liegt immer noch mein größtes Problem. Leider weiß ich nicht genau wie die einzelnen Paramter aussehen sollen. In den Header-Dateien ist die Methode nicht dokumentiert und auch die Namen der Parameter helfen nicht wirklich weiter. Der Aufruf, wie er oben gezeigt ist, ist das ergebnis von Spicken und Tips aus der Community. Die Grundlage dafür habe ich glaube ich aus dem Quellcode des PageTOC Plugins. Natürlich lassen die Namen der Parameter einigermaßen erahnen worum es geht, aber halt eben nicht genau. Sollte also jemand unter euch näheres wissen, ist er herzlich eingeladen sein Wissen zu teilen.

Finale

Bevor wir uns nun endgültig feiern können fehlt nur noch eins. Unser RWStyledTextView muss seine Daten natürlich noch in speichern und wieder laden können. Damit es das tut, müssen wir die Methoden initWithCoder und encodeWithCoder anpassen. In den Methoden schauen wir uns einfach die Zeilen an mit denen bereits das outputTextField gespeichert wird und erzeugen die Zeilen entsprechend für unseren contentTextView.
Ein Änderung habe ich jedoch noch zusätzlich vorgenommen. Statt z.B. encodeObject: habe ich encodeObject: forKey: verwendet. Das ist der erste Schritt um das Laden und Speichern etwas robuster zu machen.

//Load Data
- (id)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [self init])
	{
		[outputTextField setStringValue:[aDecoder decodeObjectForKey:@"headline"]];
        [contentTextView setAttributedString:[aDecoder decodeObjectForKey:@"content"]];
    }
    return self;
}
 
//Save Data
- (void)encodeWithCoder:(NSCoder *)aCoder 
{
    [aCoder encodeObject:[outputTextField stringValue] forKey:@"headline"];
    [aCoder encodeObject:[contentTextView attributedString] forKey:@"content"];
}

Wenn wiederum alles richtig war, solltet ihr jetzt ein Funktionsfähiges Beispiel für den Einsatz eines RWStyledTextView haben. Nachfolgend findet ihr noch meinen Projektordner als Download.

Extended HelloWorldPlugin Source


Werbung