Dieses Tutorial erweitert das Hello World Add-in um eine Dokumentansicht. Das Add-in registriert einen Dokumenttyp mit einer Factory und stellt einen TWvdSDocumentFrame bereit, den der Host als Tab in die Shell einbettet.
Zurück zur Übersicht.
Ein Add-in namens „Asset Editor“, das:
IWvdSDocumentFactory bereitstellt{ "id": "demo.asset-editor", "name": "asset-editor", "displayName": "Asset Editor", "version": "1.0.0", "publisher": "demo", "kind": "native", "main": "bin/AssetEditor.dll", "engineVersion": "^1.0.0", "activationEvents": [ "onCommand:assets.open" ], "contributes": { "commands": [ { "command": "assets.open", "title": "Anlage öffnen", "category": "Assets", "icon": "media/asset-open.svg" }, { "command": "assets.save", "title": "Anlage speichern", "category": "Assets" } ], "menus": { "menuBar/file": [ { "command": "assets.open", "group": "open", "order": 10 } ] }, "toolbar": [ { "toolbarId": "actionbar", "command": "assets.save", "icon": "media/save.svg", "tooltip": "Anlage speichern", "when": "activeDocument == assets.editor && isDirty" } ], "keybindings": [ { "command": "assets.save", "key": "Ctrl+S", "when": "activeDocument == assets.editor" } ] } }
Hier fallen mehrere Dinge zusammen: Der Save-Command ist nur in der ActionBar sichtbar, wenn ein Asset-Editor aktiv ist und ungespeicherte Änderungen vorliegen. Das Tastenkürzel Ctrl+S ist ebenfalls kontextgebunden. Dadurch stört das Add-in nicht, wenn ein anderer Dokumenttyp aktiv ist.
Die Factory erzeugt auf Anfrage einen neuen Frame und teilt dem Host den Dokumenttyp mit.
unit AssetEditorFactory; {$mode objfpc}{$H+} interface uses Classes, WvdS.Document.Host.Api, AssetEditorFrame; type TAssetEditorFactory = class(TInterfacedObject, IWvdSDocumentFactory) private FHost: IHost; public constructor Create(const AHost: IHost); function CreateFrame(AOwner: TComponent): TWvdSDocumentFrame; function GetDocumentType: string; function GetDocumentKind: TDocumentKind; function GetDisplayName: string; function GetIcon: string; end; implementation constructor TAssetEditorFactory.Create(const AHost: IHost); begin inherited Create; FHost := AHost; end; function TAssetEditorFactory.CreateFrame(AOwner: TComponent): TWvdSDocumentFrame; var Frame: TAssetEditorFrame; begin Frame := TAssetEditorFrame.Create(AOwner); Frame.Initialize(FHost); Result := Frame; end; function TAssetEditorFactory.GetDocumentType: string; begin Result := 'assets.editor'; end; function TAssetEditorFactory.GetDocumentKind: TDocumentKind; begin Result := dkMultiInstance; end; function TAssetEditorFactory.GetDisplayName: string; begin Result := 'Asset Editor'; end; function TAssetEditorFactory.GetIcon: string; begin Result := 'media/asset-open.svg'; end; end.
dkMultiInstance erlaubt mehrere Tabs desselben Typs. Wenn nur ein einziger Tab erlaubt sein soll (etwa für eine Übersichtsliste), wäre dkSingleInstance die richtige Wahl.
Der Frame ist ein gewöhnlicher LCL-TFrame, der von TWvdSDocumentFrame erbt. Er enthält die eigentliche Benutzeroberfläche.
unit AssetEditorFrame; {$mode objfpc}{$H+} interface uses Classes, SysUtils, Controls, Forms, StdCtrls, WvdS.Document.Host.Api; type TAssetEditorFrame = class(TWvdSDocumentFrame) private FHost: IHost; FContext: IDocumentContext; FNameEdit: TEdit; FDescMemo: TMemo; procedure HandleChange(Sender: TObject); protected function DoCanClose: Boolean; override; procedure DoSave; override; procedure DoActivate; override; procedure DoDeactivate; override; public constructor Create(AOwner: TComponent); override; procedure Initialize(const AHost: IHost); procedure SetDocumentContext(const AContext: IDocumentContext); end; implementation uses Dialogs; constructor TAssetEditorFrame.Create(AOwner: TComponent); var Lbl: TLabel; begin inherited; Lbl := TLabel.Create(Self); Lbl.Parent := Self; Lbl.Caption := 'Bezeichnung:'; Lbl.Left := 12; Lbl.Top := 12; FNameEdit := TEdit.Create(Self); FNameEdit.Parent := Self; FNameEdit.Left := 12; FNameEdit.Top := 32; FNameEdit.Width := 300; FNameEdit.OnChange := @HandleChange; Lbl := TLabel.Create(Self); Lbl.Parent := Self; Lbl.Caption := 'Beschreibung:'; Lbl.Left := 12; Lbl.Top := 64; FDescMemo := TMemo.Create(Self); FDescMemo.Parent := Self; FDescMemo.Left := 12; FDescMemo.Top := 84; FDescMemo.Width := 300; FDescMemo.Height := 200; FDescMemo.OnChange := @HandleChange; end; procedure TAssetEditorFrame.Initialize(const AHost: IHost); begin FHost := AHost; end; procedure TAssetEditorFrame.SetDocumentContext(const AContext: IDocumentContext); begin FContext := AContext; FContext.SetTitle('Neue Anlage'); end; procedure TAssetEditorFrame.HandleChange(Sender: TObject); begin if Assigned(FContext) then begin FContext.SetDirty(True); FHost.Context.SetContext('isDirty', 'true'); end; end; function TAssetEditorFrame.DoCanClose: Boolean; begin if Assigned(FContext) and FContext.GetDirty then begin case MessageDlg('Änderungen speichern?', mtConfirmation, [mbYes, mbNo, mbCancel], 0) of mrYes: begin DoSave; Result := True; end; mrNo: Result := True; else Result := False; end; end else Result := True; end; procedure TAssetEditorFrame.DoSave; begin // Hier: Daten speichern (Datei, Datenbank, API) FHost.Notifications.ShowInfo('Anlage gespeichert: ' + FNameEdit.Text); if Assigned(FContext) then FContext.SetDirty(False); FHost.Context.SetContext('isDirty', ''); end; procedure TAssetEditorFrame.DoActivate; begin FHost.Context.SetContext('activeDocument', 'assets.editor'); if Assigned(FContext) and FContext.GetDirty then FHost.Context.SetContext('isDirty', 'true') else FHost.Context.SetContext('isDirty', ''); end; procedure TAssetEditorFrame.DoDeactivate; begin FHost.Context.SetContext('activeDocument', ''); FHost.Context.SetContext('isDirty', ''); end; end.
Die vier virtuellen Methoden bilden den Vertrag zwischen Frame und Host:
DoCanClose — Der Host fragt vor dem Schließen. Gibt der Frame False zurück, bleibt der Tab offen.DoSave — Der Host ruft diese Methode bei Ctrl+S auf. Der Frame speichert und setzt Dirty := False.DoActivate — Der Host ruft diese Methode beim Tab-Wechsel auf. Der Frame setzt Context Keys, damit die ActionBar und die Menüs korrekt reagieren.DoDeactivate — Der Host ruft diese Methode auf, wenn ein anderer Tab aktiviert wird. Der Frame räumt seine Context Keys auf.unit AssetEditorPlugin; {$mode objfpc}{$H+} interface uses WvdS.Document.Host.Api; type TAssetEditorPlugin = class(TInterfacedObject, IPlugin) private FHost: IHost; public constructor Create(const AHost: IHost); procedure Activate(const AContext: IExtensionContext); procedure Deactivate; end; implementation uses AssetEditorFactory; type TOpenAssetHandler = class(TInterfacedObject, ICommandHandler) private FHost: IHost; public constructor Create(const AHost: IHost); procedure Execute; end; TSaveAssetHandler = class(TInterfacedObject, ICommandHandler) private FHost: IHost; public constructor Create(const AHost: IHost); procedure Execute; end; { TAssetEditorPlugin } constructor TAssetEditorPlugin.Create(const AHost: IHost); begin inherited Create; FHost := AHost; end; procedure TAssetEditorPlugin.Activate(const AContext: IExtensionContext); begin // Document Factory registrieren AContext.Subscribe( FHost.Documents.RegisterFactory('assets.editor', TAssetEditorFactory.Create(FHost)) ); // Command-Handler registrieren AContext.Subscribe( FHost.Commands.RegisterCommand('assets.open', 'Anlage öffnen', TOpenAssetHandler.Create(FHost)) ); AContext.Subscribe( FHost.Commands.RegisterCommand('assets.save', 'Anlage speichern', TSaveAssetHandler.Create(FHost)) ); end; procedure TAssetEditorPlugin.Deactivate; begin end; { TOpenAssetHandler } constructor TOpenAssetHandler.Create(const AHost: IHost); begin inherited Create; FHost := AHost; end; procedure TOpenAssetHandler.Execute; begin FHost.Documents.OpenDocument('assets.editor'); end; { TSaveAssetHandler } constructor TSaveAssetHandler.Create(const AHost: IHost); begin inherited Create; FHost := AHost; end; procedure TSaveAssetHandler.Execute; begin // Der Host leitet den Save-Command an den aktiven Frame weiter end; end.
In Activate werden drei Dinge registriert: die Document Factory und zwei Command-Handler. Alle drei Registrierungen geben ein IDisposable zurück, das an AContext.Subscribe übergeben wird. Beim Deaktivieren des Add-ins räumt der Host alles automatisch auf.
Der Ablauf beim Öffnen eines Dokuments:
TOpenAssetHandler.Execute auf.FHost.Documents.OpenDocument('assets.editor') auf.TAssetEditorFactory.GetDocumentKind ab → dkMultiInstance.TAssetEditorFactory.CreateFrame(TabContainer) auf.TAssetEditorFrame und gibt ihn zurück.SetDocumentContext auf. Der Frame setzt den Titel.DoActivate auf. Der Frame setzt Context Keys.when: activeDocument == assets.editor).Weiter zum Sidebar-View Tutorial oder zurück zur Übersicht.