====== 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]].