====== Tutorial: Document-View Add-in ====== Dieses Tutorial erweitert das [[hello-world|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 [[..:start|Übersicht]]. ===== Was gebaut wird ===== Ein Add-in namens "Asset Editor", das: * einen Command "Anlage öffnen" registriert * eine ''IWvdSDocumentFactory'' bereitstellt * bei Aufruf des Commands einen Dokument-Tab öffnet * Dirty-State, Titel und Speichern unterstützt ===== Manifest ===== { "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. ===== Document Factory ===== 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. ===== Document Frame ===== 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. ===== Plugin-Klasse ===== 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. ===== Zusammenspiel ===== Der Ablauf beim Öffnen eines Dokuments: - Benutzer wählt "Anlage öffnen" im Menü. - Host ruft ''TOpenAssetHandler.Execute'' auf. - Handler ruft ''FHost.Documents.OpenDocument('assets.editor')'' auf. - Host fragt ''TAssetEditorFactory.GetDocumentKind'' ab → ''dkMultiInstance''. - Host ruft ''TAssetEditorFactory.CreateFrame(TabContainer)'' auf. - Factory erzeugt ''TAssetEditorFrame'' und gibt ihn zurück. - Host bettet den Frame in einen neuen Tab ein. - Host ruft ''SetDocumentContext'' auf. Der Frame setzt den Titel. - Host ruft ''DoActivate'' auf. Der Frame setzt Context Keys. - Die ActionBar zeigt den Save-Button (wegen ''when'': ''activeDocument == assets.editor''). Weiter zum [[sidebar-view|Sidebar-View Tutorial]] oder zurück zur [[..:start|Übersicht]].